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
This commit is contained in:
Jared Miller 2026-01-28 11:41:50 -05:00
parent b4ac7beead
commit 65b8acf5f8
Signed by: shmup
GPG key ID: 22B5C6D66A38B06C
2 changed files with 58 additions and 14 deletions

View file

@ -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<SessionData>({
// SSE endpoint for dashboard
if (url.pathname === "/events") {
let ctrl: ReadableStreamDefaultController<string>;
const stream = new ReadableStream<string>({
start(controller) {
ctrl = controller;
sseClients.add(controller);
// Send initial headers
controller.enqueue(": connected\n\n");
},
cancel() {
sseClients.delete(
this as unknown as ReadableStreamDefaultController<string>,
);
sseClients.delete(ctrl);
},
});
@ -87,6 +90,41 @@ const server = Bun.serve<SessionData>({
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<SessionData>({
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<SessionData>({
// 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<SessionData>({
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<SessionData>({
}
} catch (error) {
console.error("WebSocket message error:", error);
ws.send(
JSON.stringify({ type: "error", message: "Invalid message format" }),
);
ws.close(1008, "Invalid message format");
}
},

View file

@ -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 }