feat: async + SSE message flow with interactive permission/question UI
Replace the synchronous /edit (wait for full markdown response) with an
async + SSE flow per demo.html so the user sees text stream in real-time
and can interact with permission/question events the agent raises.
Backend
-------
- EditHandler now calls OpenCode /session/:id/prompt_async and returns
immediately with {ok, sessionId, notebookPath}; the LLM reply is no
longer embedded in this response.
- New GlobalEventHandler proxies OpenCode /global/event as
text/event-stream. Server forwards ALL events; the client filters.
A too-eager server-side ?session= filter was silently dropping events
the client would have accepted, so it was removed.
- New PermissionReplyHandler + QuestionReplyHandler forward user
replies (once/always/reject and freeform answer) back to OpenCode
Serve at /session/:sid/permissions/:permId and
/session/:sid/question/:qId/reply.
- OpenCodeClient gains send_message_async(), stream_global_events()
(async generator over the SSE feed via tornado streaming_callback),
reply_permission() and reply_question(). Legacy send_message_sync
removed.
Frontend
--------
- subscribeOpenCodeEvents() opens a fetch+reader SSE client (XSRF
token injected from serverSettings); AbortController-backed close()
is idempotent.
- OpenCodeInlinePrompt.applyEvent() routes events into the streaming
UI: text delta -> assistant message (re-rendered as markdown on
every delta so the user sees formatted <pre><code> blocks in
real-time, not raw fence source); reasoning/tool/permission/question
get collapsible details blocks. session.idle resets stream pointers
and fires onStreamEnd.
- Permission/question blocks render real interactive UI: three
buttons (once/always/reject) for permission, a text input + submit
for question. Click handlers post through the new API routes and
show success/failure status in place.
- Centralized idle detection (4 shapes: top-level + payload-nested
x {session.idle, session.status/idle}) inside the prompt; cell
action reacts via onStreamEnd callback rather than re-parsing
event types.
- System / workspace / pty / lsp / mcp / installation events AND
session-level control events (agent.switched, model.switched,
file.edited) are not rendered in the frontend.
Tests
-----
- 52 backend pytest (was 43)
- 58 frontend jest (was 46)
design.md section 3 updated for the new async /edit response shape
and the new GET /events SSE endpoint.
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
+32
-11
@@ -1,12 +1,13 @@
|
||||
/**
|
||||
* Shared types for the opencode-bridge frontend.
|
||||
*
|
||||
* These mirror the Python server's `CellContext` / `OpenCodeRequest` / `OpenCodeResponse`
|
||||
* shapes in `opencode_bridge/routes.py`. Keep them in sync.
|
||||
* Mirrors the Python server's response shapes in `opencode_bridge/routes.py`
|
||||
* and the OpenCode Serve SSE event shape documented in
|
||||
* `/Users/taochen/temp/demo.html` (`handleGlobalEvent` / `processSessionEvent`).
|
||||
*
|
||||
* v3-final: the JupyterLab plugin settings have been removed entirely.
|
||||
* All connection config lives in startup environment variables (see
|
||||
* `opencode_bridge/config.py`); the inline model picker is fully dynamic.
|
||||
* v4: POST /edit is async (returns immediately with sessionId). The LLM
|
||||
* reply is consumed via the GET /events SSE stream. Each SSE event is
|
||||
* a JSON object with a `type` string; consumers route on `type`.
|
||||
*/
|
||||
|
||||
export interface ErrorOutput {
|
||||
@@ -33,21 +34,41 @@ export interface OpenCodeRequest {
|
||||
modelId?: string;
|
||||
}
|
||||
|
||||
export interface OpenCodeSuccess {
|
||||
/**
|
||||
* Response from POST /opencode-bridge/edit (async).
|
||||
* The actual assistant reply arrives via the /events SSE stream and is
|
||||
* NOT embedded in this response.
|
||||
*/
|
||||
export interface OpenCodeEditSuccess {
|
||||
ok: true;
|
||||
/** The AI's reply as markdown (code in ```fences```, optional explanation).
|
||||
* Render with marked. The cell source is NOT replaced. */
|
||||
markdown: string;
|
||||
sessionId: string;
|
||||
notebookPath: string;
|
||||
}
|
||||
|
||||
export interface OpenCodeFailure {
|
||||
export interface OpenCodeEditFailure {
|
||||
ok: false;
|
||||
error: string;
|
||||
}
|
||||
|
||||
export type OpenCodeResponse = OpenCodeSuccess | OpenCodeFailure;
|
||||
export type OpenCodeEditResponse = OpenCodeEditSuccess | OpenCodeEditFailure;
|
||||
|
||||
/**
|
||||
* One SSE event from GET /opencode-bridge/events (proxied from OpenCode
|
||||
* Serve's /global/event). The shape is loosely typed because OpenCode's
|
||||
* payload envelope is not strictly documented; we follow demo.html's
|
||||
* defensive property lookup pattern (try top-level then nested under
|
||||
* `payload`).
|
||||
*/
|
||||
export interface OpenCodeEvent {
|
||||
type: string;
|
||||
payload?: {
|
||||
type?: string;
|
||||
properties?: { [k: string]: any };
|
||||
syncEvent?: { aggregateID?: string };
|
||||
};
|
||||
properties?: { [k: string]: any };
|
||||
syncEvent?: { aggregateID?: string };
|
||||
}
|
||||
|
||||
/** Response from GET /opencode-bridge/session-messages?notebook=... .
|
||||
* Projected from OpenCode's {info, parts}[] by the server into a
|
||||
|
||||
Reference in New Issue
Block a user