openpencil/apps/web/server/utils/sse-stream.ts
Kayshen-X a7d73ebb62 feat(ai): pencil-style agentic design tool-loop, multi-chat tabs, #27 panel restyle
Built-in design generation now runs as an agentic MCP tool-loop (reusing the
agent-rs BuiltInProvider), gated behind OPENPENCIL_DESIGN_AGENT_LOOP / the
Settings experimental toggle; the orchestrator stays the default.

- design-agent system prompt + in-process design toolset (parity-locked with
  the MCP surface) + flag-gated Intent::Design routing
- spawn_agents execution as sequential sub-loops + live creation-mode badges
  (per-agent glow + 'N/M designing...' header)
- new MCP tools: get_guidelines, ToolSearch, get_screenshot, get_editor_state,
  export_nodes, spawn_agents; style-guide local audit
- #27 AI panel restyle: rounded tool cards + green check-rings, gray user
  bubbles, model-pill bottom toolbar, header, empty-state pills, the
  PARALLEL AGENTS (agent_team_size) 1x-6x chip dropdown
- multi-chat tabs: ChatSessions model (Deref-to-active) + tab row UI
  (switch / close / + / Cmd+T) with each run bound to its tab

Large checkpoint commit spanning the working tree (Rust shell crates).
2026-07-02 21:21:06 +08:00

60 lines
1.5 KiB
TypeScript

export type SSEEvent =
| { type: 'text'; content: string }
| { type: 'thinking'; content: string }
| { type: 'error'; content: string }
| { type: 'done' };
export function createSSEResponse(
producer: (emit: (event: SSEEvent) => void, signal: AbortSignal) => Promise<void>,
): Response {
const encoder = new TextEncoder();
const abortController = new AbortController();
const stream = new ReadableStream({
async start(controller) {
const enqueue = (raw: string) => {
try {
controller.enqueue(encoder.encode(raw));
} catch {
/* closed */
}
};
const emit = (event: SSEEvent) => {
enqueue(`data: ${JSON.stringify(event)}\n\n`);
};
const pingTimer = setInterval(
() => enqueue(`data: ${JSON.stringify({ type: 'ping', content: '' })}\n\n`),
5000,
);
try {
await producer(emit, abortController.signal);
emit({ type: 'done' });
} catch (error) {
const msg = error instanceof Error ? error.message : 'Unknown error';
emit({ type: 'error', content: msg });
} finally {
clearInterval(pingTimer);
try {
controller.close();
} catch {
/* already closed */
}
}
},
cancel() {
abortController.abort();
},
});
return new Response(stream, {
headers: {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
Connection: 'keep-alive',
},
});
}