docs: add HANDOVER.md for next session context
The current session's context window is nearly full. This document
captures the project state, the key design decisions that future
maintainers should NOT re-derive, and the known minor issues that
were intentionally left for a follow-up session.
Contents:
- Project overview + current state (tag v0.1.1, branch main @ 310d507)
- Architecture diagram
- Critical code locations
- All 11 route endpoints in one table
- 5 key design decisions (SSE filter, no cell auto-inject, multi-session
model, no partial-render markdown, real interactive permission/question
UI) - written so the next session doesn't accidentally undo them
- How to run the tests
- Known minor issues / TODO list
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
+110
@@ -0,0 +1,110 @@
|
|||||||
|
# opencode_bridge 交接文档
|
||||||
|
|
||||||
|
## 项目概览
|
||||||
|
JupyterLab extension,把本地 OpenCode Serve 进程的能力桥接到 notebook 单元。
|
||||||
|
**v0.1.1 (tag 在远端,当前 main 已经是 310d507)**。
|
||||||
|
|
||||||
|
## 仓库
|
||||||
|
`/Users/taochen/llm/notebook-extension/opencode-bridge`
|
||||||
|
- 主分支:`main`(已切回)
|
||||||
|
- 远端:`http://101.43.40.124:3000/tao.chen/notebook-ai-extension.git`
|
||||||
|
- Tag:`v0.1.1` (310d507)
|
||||||
|
|
||||||
|
## 架构
|
||||||
|
```
|
||||||
|
Frontend (JupyterLab TS) Server ext (Python tornado) OpenCode Serve
|
||||||
|
───────────────────────────────────── ───────────────────────────────── ─────────────
|
||||||
|
opencode_cell_actions.ts ─POST /opencode-bridge/edit──> EditHandler
|
||||||
|
per-cell AI 按钮 + inline panel _build_request_body(prompt) ──> /session/:id/prompt_async
|
||||||
|
(只发 prompt,不再自动注入 cell) ──> /session/:id/abort
|
||||||
|
opencode_inline_prompt.ts /session (list all)
|
||||||
|
1. SessionSelector 下拉(全 OpenCode sessions)
|
||||||
|
2. Provider + Model 选择(本地存储)
|
||||||
|
3. History 区域(流式 markdown 渲染)
|
||||||
|
4. "📋 插入单元格内容" 按钮(显式 opt-in)
|
||||||
|
5. 输入框 + send/cancel
|
||||||
|
|
||||||
|
SSE 订阅 GET /events?session= GlobalEventHandler ──proxy──> /global/event
|
||||||
|
→ 5 类事件路由:text/reasoning/tool/permission.asked/question.asked + session.idle
|
||||||
|
```
|
||||||
|
|
||||||
|
## 关键代码位置
|
||||||
|
| 文件 | 作用 |
|
||||||
|
|------|------|
|
||||||
|
| `src/components/opencode_cell_actions.ts` | cell 工具栏按钮 + 流编排(open/edit/SSE/permission/question/session 管理) |
|
||||||
|
| `src/components/opencode_inline_prompt.ts` | 内嵌 prompt 面板(最复杂,~1300 行) |
|
||||||
|
| `src/api/opencode_client.ts` | 8 个 session 管理 API + 4 个原有 API(edit/messages/permissions/questions/events) |
|
||||||
|
| `src/components/model_selection.ts` | (provider, model) localStorage 持久化 |
|
||||||
|
| `opencode_bridge/opencode_client.py` | HTTP 客户端,带 `send_message_async` + `stream_global_events`(SSE) + `reply_permission/question` + `list_all_sessions` |
|
||||||
|
| `opencode_bridge/routes.py` | 11 个路由 |
|
||||||
|
| `opencode_bridge/session_manager.py` | **1 notebook N sessions** 模型(`_active` + `_bound`) |
|
||||||
|
| `opencode_bridge/config.py` | OpencodeConfig Configurable(env var → `OpencodeConfig` Traitlets) |
|
||||||
|
|
||||||
|
## 主要路由
|
||||||
|
| 端点 | 方法 | 用途 |
|
||||||
|
|------|------|------|
|
||||||
|
| `/opencode-bridge/edit` | POST | 异步发 prompt_async(只发 prompt + context.notebookPath) |
|
||||||
|
| `/opencode-bridge/events?session=X` | GET | SSE 代理 OpenCode `/global/event` |
|
||||||
|
| `/opencode-bridge/session-messages?notebook=X` | GET | 当前 session 的消息历史 |
|
||||||
|
| `/opencode-bridge/sessions/all` | GET | OpenCode 上**所有** session(全局) |
|
||||||
|
| `/opencode-bridge/sessions/notebook?notebook=X` | GET/POST/PUT/DELETE | 绑定 + 列出 + 创建 + 删除 |
|
||||||
|
| `/opencode-bridge/sessions/active?notebook=X` | GET/PUT/DELETE | active session 操作 |
|
||||||
|
| `/opencode-bridge/permissions/:permId?session=X` | POST | 权限响应(once/always/reject) |
|
||||||
|
| `/opencode-bridge/questions/:qId/reply?session=X` | POST | 提问响应 |
|
||||||
|
|
||||||
|
## 关键设计决策(避免来回改)
|
||||||
|
|
||||||
|
### 1. SSE 事件路由(`applyEvent`)
|
||||||
|
- 客户端**不**对内容事件(text/reasoning/tool/idle)做 sessionID 过滤 — 服务端 `?session=` 参数是唯一权威
|
||||||
|
- 只对 `permission.asked` / `question.asked` 严格过滤,防止用户误答别 session 的提示
|
||||||
|
- 这条规则是 **bug fix 310d507 的核心**,改之前会"切 session 后发消息无回复"
|
||||||
|
|
||||||
|
### 2. Cell 上下文不再自动注入
|
||||||
|
- `OpenCodeRequest.context` 收缩为 `{notebookPath}` — 只告诉服务端哪个 notebook
|
||||||
|
- `_build_request_body` 只发 `parts=[{text: prompt}]`
|
||||||
|
- 用户通过"📋 插入单元格内容"按钮**显式 opt-in** 把 cell 源码作为 markdown code block 插入
|
||||||
|
- 理由:LLM context 严格匹配用户意图,避免被前一个 cell 的 traceback 污染
|
||||||
|
- 这是 310d507 的第二部分
|
||||||
|
|
||||||
|
### 3. 1 notebook N sessions
|
||||||
|
- `SessionManager._active` (dict[notebook] → active sessionId) + `_bound` (dict[notebook] → [sessionId, ...])
|
||||||
|
- 新方法:`create_new / bind_existing / set_active / unbind / unbind_active / delete_session / list_sessions_for_notebook / get_active`
|
||||||
|
- `release(notebook)` **删所有** bound sessions(不只是 active)
|
||||||
|
- `delete_session(notebook, sid)`:先调 OpenCode delete,成功后再 unbind(失败时 retry 仍可能)
|
||||||
|
|
||||||
|
### 4. 流式 markdown 渲染(避免 partial-render flicker)
|
||||||
|
- 流式期间只 `textContent += delta`,**不**调 `marked.parse`(否则 fence 没闭合时的部分渲染会闪 toolbar)
|
||||||
|
- `session.idle` 时 `_resetStreamPointers` 触发**一次**完整 `marked.parse` 渲染
|
||||||
|
- 终态与 `setMessages` 路径(setMessages 重新打开时拉的)完全一致
|
||||||
|
|
||||||
|
### 5. 交互 UI 权限
|
||||||
|
- `permission.asked` / `question.asked` 事件渲染**真实交互组件**(按钮 / 输入框)
|
||||||
|
- 点击通过后端路由代理回 OpenCode Serve
|
||||||
|
- 后端路由:`PUT /session/:sid/permissions/:permId` + `PUT /session/:sid/question/:qId/reply`
|
||||||
|
|
||||||
|
## 测试
|
||||||
|
- 后端:73 pytest(主要看 `test_routes.py` + `test_session_manager.py`)
|
||||||
|
- 前端:72 jest(`opencode_cell_actions.spec.ts` 是核心,900+ 行覆盖了 SessionSelector / 流式渲染 / permission/question 交互)
|
||||||
|
- 命令: `source .venv/bin/activate && jlpm test && python -m pytest opencode_bridge/tests/`
|
||||||
|
|
||||||
|
## 已知小问题 / 待优化
|
||||||
|
1. **多 session 切换**时,前端用 PUT `/sessions/notebook` 一个调用搞定(bind + set active 都做),不再用 `/sessions/active` — 后者保留供"已 bound sessions 之间切换"用
|
||||||
|
2. `cell_context.ts` 已删,如果新增"只想要 cellId"的功能,需要重建
|
||||||
|
3. `package.json` version 是 `0.1.1`,跟远端 tag 一致(之前是用户手工 bump 上去的)
|
||||||
|
4. `UNIFIED_SYSTEM_PROMPT` 还在后端 routes.py 里,描述 LLM 的角色("你是一个代码助手...")。如果改 prompt,会同时影响所有 prompt
|
||||||
|
|
||||||
|
## 修改后的交互模式
|
||||||
|
1. 用户在 cell 工具栏点 🪄 按钮
|
||||||
|
2. 内嵌 prompt 出现:SessionSelector(列出 OpenCode 所有 session) + Provider/Model + history + "📋 插入单元格内容" + 输入框 + send/cancel
|
||||||
|
3. 用户输入 prompt,**可选**按"📋 插入单元格内容"把 cell 源码作为 code block 追加
|
||||||
|
4. send → 后端 `prompt_async` 立即返回 `{ok, sessionId}` + 订阅 SSE
|
||||||
|
5. 流式事件实时渲染,`session.idle` 触发 markdown 渲染(终态正确)
|
||||||
|
6. permission/question 事件渲染交互 UI,点击代理回 OpenCode
|
||||||
|
7. 切换 session → 关闭旧 SSE + bind 新 session(后端 PUT `/sessions/notebook`)+ reload history
|
||||||
|
|
||||||
|
## 还没做过的事(如果新会话要继续)
|
||||||
|
- UI tests (Playwright) — 写了 spec 但没真跑
|
||||||
|
- 持久化 session 选择(目前只持久化 provider/model)
|
||||||
|
- cell 类型推断优化(目前用 `model.type`,真实 cell 应该有 metadata.language)
|
||||||
|
- 后端 session TTL / cleanup(目前不删,除非用户显式 release)
|
||||||
|
- 真实 OpenCode Serve 接入测试(目前用 mock)
|
||||||
Reference in New Issue
Block a user