Files
notebook-ai-extension/src/types.ts
T
tao.chenandClaude 4f8bbaf09f 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>
2026-07-27 17:00:16 +08:00

104 lines
2.7 KiB
TypeScript

/**
* Shared types for the opencode-bridge frontend.
*
* 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`).
*
* 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 {
ename: string;
evalue: string;
traceback: string[];
}
export interface CellContext {
notebookPath: string;
cellId: string;
language: string;
cellIndex: number;
totalCells: number;
source: string;
previousCode: string | null;
error: ErrorOutput | null;
}
export interface OpenCodeRequest {
prompt: string;
context: CellContext;
providerId?: string;
modelId?: string;
}
/**
* 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;
sessionId: string;
notebookPath: string;
}
export interface OpenCodeEditFailure {
ok: false;
error: string;
}
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
* frontend-friendly {role, content}[]. */
export interface OpenCodeMessage {
role: "user" | "assistant";
content: string;
}
export interface OpenCodeMessagesResponse {
messages: OpenCodeMessage[];
}
/** Response from GET /opencode-bridge/providers (proxies OpenCode /config/providers).
* Each Provider.models is a Record keyed by modelID (NOT an array). */
export interface OpenCodeModel {
id: string;
name?: string;
[k: string]: unknown;
}
export interface OpenCodeProvider {
id: string;
name?: string;
source?: string;
models: { [modelID: string]: OpenCodeModel };
}
export interface OpenCodeProvidersResponse {
providers: OpenCodeProvider[];
default?: { [providerID: string]: string };
}