♻️ feat: implement session management for PTY sessions in the server

- Add SessionManager class to handle PTY sessions with WebSocket connections.
- Implement methods for creating, retrieving, and destroying sessions.
- Handle PTY output and WebSocket messages for terminal interaction.
- Ensure graceful session destruction and cleanup.

feat: initialize web application with Next.js and Tailwind CSS

- Create initial Next.js application structure with TypeScript support.
- Set up Tailwind CSS for styling with custom theme configurations.
- Add ESLint configuration for code quality and consistency.

feat: implement chat API and UI components

- Create chat API route to handle chat requests and responses.
- Develop chat layout with sidebar, header, chat window, and input components.
- Integrate Zustand for state management of conversations and messages.
- Add utility functions for formatting dates and managing class names.

chore: add environment variables and configuration files

- Create .env.example for environment variable setup.
- Add configuration files for PostCSS, Tailwind CSS, and TypeScript.
- Set up package.json with necessary dependencies and scripts for development.
This commit is contained in:
nirholas
2026-03-31 12:35:31 +00:00
parent d31c2bec03
commit 38648ae5f4
53 changed files with 4177 additions and 4 deletions

85
web/lib/api.ts Normal file
View File

@@ -0,0 +1,85 @@
import type { Message } from "./types";
const getApiUrl = () =>
process.env.NEXT_PUBLIC_API_URL ?? "http://localhost:3001";
export interface StreamChunk {
type: "text" | "tool_use" | "tool_result" | "done" | "error";
content?: string;
tool?: {
id: string;
name: string;
input?: Record<string, unknown>;
result?: string;
is_error?: boolean;
};
error?: string;
}
export async function* streamChat(
messages: Pick<Message, "role" | "content">[],
model: string,
signal?: AbortSignal
): AsyncGenerator<StreamChunk> {
const response = await fetch("/api/chat", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ messages, model, stream: true }),
signal,
});
if (!response.ok) {
const err = await response.text();
yield { type: "error", error: err };
return;
}
const reader = response.body?.getReader();
if (!reader) {
yield { type: "error", error: "No response body" };
return;
}
const decoder = new TextDecoder();
let buffer = "";
try {
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split("\n");
buffer = lines.pop() ?? "";
for (const line of lines) {
if (line.startsWith("data: ")) {
const data = line.slice(6).trim();
if (data === "[DONE]") {
yield { type: "done" };
return;
}
try {
const chunk = JSON.parse(data) as StreamChunk;
yield chunk;
} catch {
// skip malformed chunks
}
}
}
}
} finally {
reader.releaseLock();
}
yield { type: "done" };
}
export async function fetchHealth(): Promise<boolean> {
try {
const res = await fetch(`${getApiUrl()}/health`, { cache: "no-store" });
return res.ok;
} catch {
return false;
}
}