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:
parent
b4ac7beead
commit
65b8acf5f8
2 changed files with 58 additions and 14 deletions
|
|
@ -3,11 +3,13 @@
|
||||||
import type { ServerWebSocket } from "bun";
|
import type { ServerWebSocket } from "bun";
|
||||||
import {
|
import {
|
||||||
appendOutput,
|
appendOutput,
|
||||||
|
createPrompt,
|
||||||
createSession,
|
createSession,
|
||||||
endSession,
|
endSession,
|
||||||
getActiveSessions,
|
getActiveSessions,
|
||||||
getDeviceBySecret,
|
getDeviceBySecret,
|
||||||
getPrompt,
|
getPrompt,
|
||||||
|
getSession,
|
||||||
initDb,
|
initDb,
|
||||||
respondToPrompt,
|
respondToPrompt,
|
||||||
updateLastSeen,
|
updateLastSeen,
|
||||||
|
|
@ -30,8 +32,9 @@ function broadcastSSE(event: SSEEvent): void {
|
||||||
try {
|
try {
|
||||||
controller.enqueue(eventStr);
|
controller.enqueue(eventStr);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Client disconnected, will be cleaned up
|
// Client disconnected, clean up
|
||||||
console.debug("SSE client write error:", error);
|
console.debug("SSE client write error:", error);
|
||||||
|
sseClients.delete(controller);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -59,16 +62,16 @@ const server = Bun.serve<SessionData>({
|
||||||
|
|
||||||
// SSE endpoint for dashboard
|
// SSE endpoint for dashboard
|
||||||
if (url.pathname === "/events") {
|
if (url.pathname === "/events") {
|
||||||
|
let ctrl: ReadableStreamDefaultController<string>;
|
||||||
const stream = new ReadableStream<string>({
|
const stream = new ReadableStream<string>({
|
||||||
start(controller) {
|
start(controller) {
|
||||||
|
ctrl = controller;
|
||||||
sseClients.add(controller);
|
sseClients.add(controller);
|
||||||
// Send initial headers
|
// Send initial headers
|
||||||
controller.enqueue(": connected\n\n");
|
controller.enqueue(": connected\n\n");
|
||||||
},
|
},
|
||||||
cancel() {
|
cancel() {
|
||||||
sseClients.delete(
|
sseClients.delete(ctrl);
|
||||||
this as unknown as ReadableStreamDefaultController<string>,
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -87,6 +90,41 @@ const server = Bun.serve<SessionData>({
|
||||||
return Response.json(sessions);
|
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/")) {
|
if (url.pathname.startsWith("/api/prompts/")) {
|
||||||
const parts = url.pathname.split("/");
|
const parts = url.pathname.split("/");
|
||||||
const promptId = Number.parseInt(parts[3] || "", 10);
|
const promptId = Number.parseInt(parts[3] || "", 10);
|
||||||
|
|
@ -180,9 +218,6 @@ const server = Bun.serve<SessionData>({
|
||||||
if (msg.type === "auth") {
|
if (msg.type === "auth") {
|
||||||
const device = getDeviceBySecret(msg.secret);
|
const device = getDeviceBySecret(msg.secret);
|
||||||
if (!device) {
|
if (!device) {
|
||||||
ws.send(
|
|
||||||
JSON.stringify({ type: "error", message: "Invalid secret" }),
|
|
||||||
);
|
|
||||||
ws.close(1008, "Invalid secret");
|
ws.close(1008, "Invalid secret");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -219,9 +254,7 @@ const server = Bun.serve<SessionData>({
|
||||||
|
|
||||||
// All other messages require authentication
|
// All other messages require authentication
|
||||||
if (!ws.data.sessionId) {
|
if (!ws.data.sessionId) {
|
||||||
ws.send(
|
ws.close(1008, "Not authenticated");
|
||||||
JSON.stringify({ type: "error", message: "Not authenticated" }),
|
|
||||||
);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -236,6 +269,18 @@ const server = Bun.serve<SessionData>({
|
||||||
return;
|
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
|
// Handle resize message
|
||||||
if (msg.type === "resize") {
|
if (msg.type === "resize") {
|
||||||
// Store resize info if needed (not yet implemented)
|
// Store resize info if needed (not yet implemented)
|
||||||
|
|
@ -265,9 +310,7 @@ const server = Bun.serve<SessionData>({
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("WebSocket message error:", error);
|
console.error("WebSocket message error:", error);
|
||||||
ws.send(
|
ws.close(1008, "Invalid message format");
|
||||||
JSON.stringify({ type: "error", message: "Invalid message format" }),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -41,7 +41,8 @@ export type ClientMessage =
|
||||||
| { type: "auth"; secret: string; cwd?: string; command?: string }
|
| { type: "auth"; secret: string; cwd?: string; command?: string }
|
||||||
| { type: "output"; data: string }
|
| { type: "output"; data: string }
|
||||||
| { type: "resize"; cols: number; rows: number }
|
| { 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 =
|
export type ServerMessage =
|
||||||
| { type: "input"; data: string }
|
| { type: "input"; data: string }
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue