Commit Graph
9 Commits
Author SHA1 Message Date
tao.chen 310d50759a fix: SSE filter no longer drops content events for a different session
Bug: after switching to a new session via the SessionSelector, the
next /edit would not get a reply. Root cause: applyEvent dropped
ANY event whose sessionID didn't match the prompt's _sessionId,
including text deltas / reasoning / tool / idle. OpenCode's sessionID
extraction from event payloads is not perfectly consistent across
event types, so the prompt could end up with a stale _sessionId
that doesn't match the events coming in, and EVERY event got
filtered out -> no reply.

Fix: the cross-session filter is now strict ONLY for
permission.asked / question.asked (so the user can't accidentally
reply to another session's prompt). Content events always pass
through. The server's /events handler is the single source of
truth for session filtering (it has the URL ?session= param); the
client's filter would only ever mask the user's interaction with
their own active session.

Also: drop the cell-context auto-injection. The OpenCodeRequest
now carries only {notebookPath}. The user explicitly attaches
whatever they want via the new '📋 插入单元格内容' button
(inserts the cell source as a markdown code block into the input).
This makes the LLM context match user intent and stops the
OpenCode prompt from being polluted with stale previousCode /
traceback snapshots.

Backend
-------
- _build_request_body: now takes only the prompt, returns
  parts=[{text: prompt}]. No more <previous_cell>/<traceback>/<cell>
  tag wrapping.

Frontend
-------
- types.ts: CellContext collapsed to {notebookPath}. ErrorOutput /
  cellId / source / previousCode / error / cellIndex / totalCells
  / language all removed.
- opencode_cell_actions.ts: extractCellContextFromCell() replaced
  with extractNotebookPathFromCell(). _context field renamed to
  _notebookPath. NotebookPanel / extractCellContext / context/
  cell_context imports all removed.
- src/context/cell_context.ts + src/__tests__/cell_context.spec.ts:
  deleted.

New feature: '📋 插入单元格内容' button
----------------------------------------
The button sits at the start of the actions row (visually
left-aligned via margin-right:auto) and appends the current cell's
source as a markdown code fence to the textarea. Caret is moved
to the end so the user can keep typing. The fence info string is
the cell's model type (falls back to a plain fence if the type
isn't a clean language identifier). No-op for empty cells.

Tests
-----
- 72 jest (was 78: -8 cell-context tests + 1 SSE filter test +
  3 insert-cell tests + rewrites). 73 pytest (unchanged). Build
  green.
2026-07-28 18:02:21 +08:00
tao.chenandClaude d2bd4f1e48 feat: multi-session management (browse all + bind N sessions per notebook)
User can now see EVERY session on the OpenCode Serve side (not just
the per-notebook mapping) and bind multiple sessions to a single
notebook. One of the bound sessions is the "active" one (the one
the next /edit uses); the others are historical. The user can
switch, create, or delete via the new session selector UI in the
inline prompt.

Backend
-------
- SessionManager rewritten for 1-notebook-N-sessions: keeps both an
   map and a  map. New methods: create_new,
  bind_existing, set_active, unbind, unbind_active, delete_session,
  list_sessions_for_notebook. Existing get_or_create / peek /
  invalidate / list_sessions / release semantics preserved
  (release now deletes ALL bound sessions, not just the active).
- New route GET /opencode-bridge/sessions/all — list ALL OpenCode
  sessions via /session.
- New route GET /opencode-bridge/sessions/notebook?notebook=... —
  bound sessions for one notebook, enriched with title/createdAt
  from the global list.
- POST .../sessions/notebook — create a new session, bind, set
  active.
- PUT  .../sessions/notebook — bind an existing sessionId, set
  active (no OpenCode round-trip).
- DELETE .../sessions/notebook — delete the session on OpenCode
  AND remove its binding.
- New route GET/PUT/DELETE /opencode-bridge/sessions/active —
  read / switch / unbind the active session for one notebook.
- OpenCodeClient.list_all_sessions() — proxy OpenCode /session.

Frontend
-------
- SessionSelector dropdown in the inline prompt: lists ALL OpenCode
  sessions with the active one selected, plus a "+ 新建会话"
  sentinel entry. "+ 新建" button + "🔄" refresh button.
  Switching calls onSwitchSession; selecting "+ 新建会话" or
  clicking the new button calls onCreateSession. After both,
  the prompt's history is reloaded for the new active session.
- New cell-action callbacks: onListSessions, onCreateSession,
  onSwitchSession, onReloadHistory. Each closes the SSE first
  so events for the OLD session don't bleed in during the switch.
- 8 new API client functions in api/opencode_client.ts.
- New types in types.ts: OpenCodeSessionMeta,
  OpenCodeNotebookSession, OpenCodeAllSessionsResponse,
  OpenCodeNotebookSessionsResponse, OpenCodeSessionOpResponse.

Tests
-----
- 73 backend pytest (was 64): +6 new SessionManager multi-session
  unit tests + 7 new route tests (all-sessions, notebook GET/POST/
  PUT/DELETE, active GET/PUT/DELETE).
- 76 frontend jest (was 72): +4 new prompt tests (session selector
  rendered when callbacks wired, initialize() populates the list,
  + 新建 creates+binds, dropdown change switches and reloads).
- package.json version bumped 0.1.0 -> 0.1.1 to match the v0.1.1 tag
  that was already published.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-28 16:25:42 +08:00
b7089519fd 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:38:53 +08:00
tao.chenandClaude Fable 5 ce59502e97 feat: inline prompt history area — scrollable session messages
The inline prompt now shows the current session's full message
history (user + assistant), scrollable, replacing the single-output
display from the previous commit. The user can scroll back through
prior exchanges in the same notebook's OpenCode session.

Server (opencode_bridge/):
  - New route: GET /opencode-bridge/session-messages?notebook=<path>
    - Resolves notebook to session via SessionManager.peek (no create).
    - If no session: {"messages": []} (no OpenCode call).
    - Else: GET /session/{id}/message on OpenCode Serve, project the
      raw {info, parts}[] into a frontend-friendly {role, content}[].
  - OpenCodeClient.list_session_messages(sid).
  - SessionManager.peek(notebook_path) -> Optional[str] (read without
    creating, to avoid spawning a session just to report emptiness).
  - Wired the new route in setup_route_handlers.

Client (src/):
  - types.ts: OpenCodeMessage { role, content } + OpenCodeMessagesResponse.
  - api/opencode_client.ts: callOpenCodeSessionMessages(notebook, serverSettings).
  - components/opencode_inline_prompt.ts:
    - Replaces the single .opencode-inline-output area with a
      scrollable .opencode-inline-history (max-height 320px, overflow-y
      auto, auto-scrolls to bottom on update).
    - setMessages(messages): renders user messages as plain text,
      assistant messages via marked.parse. No more setOutput/hideOutput.
  - components/opencode_cell_actions.ts:
    - _showPrompt now fires a _refreshHistory(notebookPath) which
      fetches and calls prompt.setMessages.
    - _handleResponse on success also calls _refreshHistory (the new
      assistant message appears as the last item in the history).
    - Cell source is still NOT replaced.
  - api/opencode_client module is mocked in the cell_actions test to
    avoid jsdom network calls.

style/base.css:
  - .opencode-inline-output* rules replaced with .opencode-inline-history
    (max-height 320px, overflow-y auto, border, padding) and
    .opencode-msg / .opencode-msg-user / .opencode-msg-assistant.
  - pre/code/p/h1-3 content styling scoped under .opencode-inline-history.

Tests:
  - pytest 37/37: FakeOpenCodeClient.list_session_messages + 3 new
    session_messages route tests (no session, projects messages, 400).
  - jest 29/29: setMessages renders user/assistant + history area tests.
  - FakeSessionManager.peek (returns None when session_id is falsy).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-23 18:45:03 +08:00
tao.chenandClaude Fable 5 1d7f5da9d4 feat: inline markdown output box — render AI reply with marked, do not replace cell
Flow (v3-final corrected):
  1. Model call succeeds.
  2. Server passes the AI reply through unchanged as 'markdown' (the
     system prompt allows ```language fences + a brief explanation, so
     the response is real markdown that marked can render into code
     blocks, headings, etc.).
  3. Client OpenCodeCellActions._handleResponse calls prompt.setOutput(
     resp.markdown); the inline prompt widget renders it with
     marked.parse and shows it in a new output area (hidden until a
     response arrives). The cell source is NOT replaced.
  4. User can close the output or cancel the whole prompt.

Server:
  - New unified system prompt: '你是代码助手 ... 按指令修改代码,可附简
    短说明' (allows ```fences``` + explanation; no more 'no markdown
    fences' restriction).
  - EditHandler returns {ok, markdown, sessionId, notebookPath} (raw
    text, fences intact). _strip_code_fence kept as a helper for any
    future apply-to-cell path; no longer called.
  - finalSource field dropped (the cell-apply path is gone).

Client:
  - OpenCodeSuccess: markdown: string (finalSource removed).
  - OpenCodeInlinePrompt: new output area, setOutput(md) renders via
    marked.parse, hideOutput() closes it.
  - OpenCodeCellActions._handleResponse: setOutput(markdown) instead of
    sharedModel.setSource + auto-hide. The prompt stays open so the
    user can read the output.
  - Uses marked@17 (already in node_modules via JupyterLab; no new dep).
  - CSS: output area styling (border, max-height 320px scroll, code/pre
    styling, close button).

Tests: pytest 34/34, jest 29/29. marked is mocked in jest.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-23 18:29:58 +08:00
tao.chenandClaude Fable 5 e7579d3779 fix: Provider.models is a Record keyed by modelID, not an array
Per the OpenCode server docs (GET /config/providers), each Provider's
'models' is { [modelID: string]: Model } (a Record), so model IDs can be
referenced as strings (matching the 'default' map of providerID ->
modelID). Our code treated it as Model[], which:
  1. crashed with 'p.models is not iterable' on real responses, and
  2. after the previous Array.isArray guard, skipped EVERY provider
     (a Record is never an array), so the inline picker showed nothing.

Update types.ts (OpenCodeProvider.models -> Record, add OpenCodeModel,
add optional 'default' map to the response) and rewrite
flattenProviders to iterate Object.keys(p.models). Update test fixture
to the Record shape.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-23 18:01:16 +08:00
tao.chen f9b93906c6 refactor: move all server config to startup env vars; drop plugin settings 2026-07-23 13:21:31 +08:00
tao.chen d03f0fa434 refactor: drop mode from frontend types; react to settings changes 2026-07-23 12:47:44 +08:00
tao.chen c919c95842 Initial commit: opencode_bridge JupyterLab extension (Slices 1-3.5)
A JupyterLab extension that bridges the cell UI to a local OpenCode Serve
process. The extension is a dual package: a Python server extension
exposed under /opencode-bridge/*, plus a TypeScript frontend that
registers per-cell toolbars.

Backend (Python, tornado)
- Slice 1: config + auth + OpenCode HTTP client (tornado.httpclient,
  no aiohttp). 4 settings in schema/plugin.json (url, user, password,
  request timeout).
- Slice 2: handlers for /hello, /health, /providers, /edit.
- Slice 2.1 (correction): SessionManager with 1 notebook = 1 session
  mapping, async-safe via per-path locks, 404 recovery via invalidate().
  Two new endpoints: GET /sessions, DELETE /session?notebook=<path>.
- 32 pytest tests pass.

Frontend (TypeScript, JupyterLab 4.6)
- src/types.ts: CellContext, OpenCodeRequest/Response, OpenCodeSettings.
- src/context/cell_context.ts: extract CellContext from a CodeCell +
  its parent NotebookPanel, structured error collection.
- src/api/opencode_client.ts: callOpenCodeEdit, callOpenCodeProviders.
- src/components/opencode_cell_footer.ts: OpenCodeCellFooter Widget
  implementing ICellFooter with 3 buttons (optimize / fix / edit),
  resolved via this.parent instanceof CodeCell. NOT cellToolbar
  (does not exist in JL 4.6) and NOT Widget.findParent (removed in
  @lumino/widgets 2.x).
- src/components/opencode_cell_factory.ts: Cell.ContentFactory
  subclass returning the OpenCodeCellFooter.
- src/components/opencode_installer.ts: installOpenCodeEverywhere
  patches every notebook (existing + new) to use the custom factory.
- src/index.ts: registers the factory, loads settings, fetches
  /providers on activation and logs the list to the console.
- 23 jest tests pass (mocked JupyterLab boundary, pnpm path safe).

Settings
- 6 fields: 3 auth (url/user/password) + 1 timeout + 2 model selection
  (provider/model). Provider list is fetched at startup from
  /opencode-bridge/providers and printed to the browser console so
  users can copy values into Settings Editor.

Docs
- design.md: 6 sections covering architecture, UI flow, API contract,
  TS skeletons, session management (v0.2.1 correction), and
  provider/model selection (v0.2.2 addition).
- CLAUDE.md: agent guidance for working in this repo.
- TODO.md: remaining work for Slices 4-7 + v0.4+ backlog.

CI
- Gitea release workflow at .github/workflows/build.yml.
- Bark notification helper (non-fatal on failure).

Generated artefacts ignored: opencode_bridge/labextension/, _version.py,
*.tsbuildinfo, junit.xml, test.ipynb scratch notebook.
2026-07-22 19:07:58 +08:00