From 65b8acf5f8da91a45b3f03832de8644d5b7e46bc Mon Sep 17 00:00:00 2001 From: Jared Miller Date: Wed, 28 Jan 2026 11:41:50 -0500 Subject: [PATCH] Fix SSE cleanup and add prompt creation endpoint - Fix SSE cancel handler to properly capture controller in closure - Remove error JSON messages before WebSocket close (close reason is sufficient) - Add POST /api/sessions/:sessionId/prompts endpoint for prompt creation - Add SSE client cleanup on broadcast errors - Add createPrompt and getSession imports - Add prompt message type to ClientMessage for CLI prompt reporting - Add prompt message handler in WebSocket to create and broadcast prompts --- src/server.ts | 69 +++++++++++++++++++++++++++++++++++++++++---------- src/types.ts | 3 ++- 2 files changed, 58 insertions(+), 14 deletions(-) diff --git a/src/server.ts b/src/server.ts index f5d087f..17621a0 100644 --- a/src/server.ts +++ b/src/server.ts @@ -3,11 +3,13 @@ import type { ServerWebSocket } from "bun"; import { appendOutput, + createPrompt, createSession, endSession, getActiveSessions, getDeviceBySecret, getPrompt, + getSession, initDb, respondToPrompt, updateLastSeen, @@ -30,8 +32,9 @@ function broadcastSSE(event: SSEEvent): void { try { controller.enqueue(eventStr); } catch (error) { - // Client disconnected, will be cleaned up + // Client disconnected, clean up console.debug("SSE client write error:", error); + sseClients.delete(controller); } } } @@ -59,16 +62,16 @@ const server = Bun.serve({ // SSE endpoint for dashboard if (url.pathname === "/events") { + let ctrl: ReadableStreamDefaultController; const stream = new ReadableStream({ start(controller) { + ctrl = controller; sseClients.add(controller); // Send initial headers controller.enqueue(": connected\n\n"); }, cancel() { - sseClients.delete( - this as unknown as ReadableStreamDefaultController, - ); + sseClients.delete(ctrl); }, }); @@ -87,6 +90,41 @@ const server = Bun.serve({ return Response.json(sessions); } + // Create prompt for a session + if ( + url.pathname.match(/^\/api\/sessions\/\d+\/prompts$/) && + req.method === "POST" + ) { + const parts = url.pathname.split("/"); + const sessionId = Number.parseInt(parts[3] || "", 10); + + if (Number.isNaN(sessionId)) { + return new Response("Invalid session ID", { status: 400 }); + } + + const session = getSession(sessionId); + if (!session) { + return new Response("Session not found", { status: 404 }); + } + + const body = (await req.json()) as { prompt_text?: unknown }; + if (!body.prompt_text || typeof body.prompt_text !== "string") { + return new Response("Missing or invalid prompt_text", { status: 400 }); + } + + const prompt = createPrompt(sessionId, body.prompt_text); + + // Broadcast to dashboards + broadcastSSE({ + type: "prompt", + prompt_id: prompt.id, + session_id: sessionId, + prompt_text: prompt.prompt_text, + }); + + return Response.json(prompt); + } + if (url.pathname.startsWith("/api/prompts/")) { const parts = url.pathname.split("/"); const promptId = Number.parseInt(parts[3] || "", 10); @@ -180,9 +218,6 @@ const server = Bun.serve({ if (msg.type === "auth") { const device = getDeviceBySecret(msg.secret); if (!device) { - ws.send( - JSON.stringify({ type: "error", message: "Invalid secret" }), - ); ws.close(1008, "Invalid secret"); return; } @@ -219,9 +254,7 @@ const server = Bun.serve({ // All other messages require authentication if (!ws.data.sessionId) { - ws.send( - JSON.stringify({ type: "error", message: "Not authenticated" }), - ); + ws.close(1008, "Not authenticated"); return; } @@ -236,6 +269,18 @@ const server = Bun.serve({ return; } + // Handle prompt message + if (msg.type === "prompt") { + const prompt = createPrompt(msg.session_id, msg.prompt_text); + broadcastSSE({ + type: "prompt", + prompt_id: prompt.id, + session_id: msg.session_id, + prompt_text: msg.prompt_text, + }); + return; + } + // Handle resize message if (msg.type === "resize") { // Store resize info if needed (not yet implemented) @@ -265,9 +310,7 @@ const server = Bun.serve({ } } catch (error) { console.error("WebSocket message error:", error); - ws.send( - JSON.stringify({ type: "error", message: "Invalid message format" }), - ); + ws.close(1008, "Invalid message format"); } }, diff --git a/src/types.ts b/src/types.ts index 1231083..e35be2a 100644 --- a/src/types.ts +++ b/src/types.ts @@ -41,7 +41,8 @@ export type ClientMessage = | { type: "auth"; secret: string; cwd?: string; command?: string } | { type: "output"; data: string } | { type: "resize"; cols: number; rows: number } - | { type: "exit"; code: number }; + | { type: "exit"; code: number } + | { type: "prompt"; session_id: number; prompt_text: string }; export type ServerMessage = | { type: "input"; data: string }