From e742bf452783f43cd806184887a6786b1b406633 Mon Sep 17 00:00:00 2001 From: "tao.chen" <93983997+taochen-ct@users.noreply.github.com> Date: Wed, 29 Jul 2026 14:26:20 +0800 Subject: [PATCH] =?UTF-8?q?docs:=20handover=20=E2=80=94=20capture=20v0.1.2?= =?UTF-8?q?=20state,=20feature/floating-ai-button=20unmerged,=20Codex=20pa?= =?UTF-8?q?in?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- HANDOVER.md | 236 ++++++++++++++++++++++++++++++++-------------------- 1 file changed, 145 insertions(+), 91 deletions(-) diff --git a/HANDOVER.md b/HANDOVER.md index 3a10443..2d107f0 100644 --- a/HANDOVER.md +++ b/HANDOVER.md @@ -1,110 +1,164 @@ # 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) +**分支**:`main`(已切回,在 main 上操作) +**远端**:`http://101.43.40.124:3000/tao.chen/notebook-ai-extension.git` +**当前 HEAD (main)**:`d650736 chore: bump version to 0.1.2` +**Tag**:`v0.1.2` → `d650736`(已推到远端,CI 会自动 build + 发 Gitea release + Bark 通知) -## 架构 ``` -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 +d650736 chore: bump version to 0.1.2 ← main HEAD, v0.1.2 +f9d52f7 docs: add HANDOVER.md for next session context +310d507 fix: SSE filter no longer drops content events for a different session ``` -## 关键代码位置 -| 文件 | 作用 | +⚠️ **重要分歧**:`main` 这次的 v0.1.2 发布**只包含了 version bump**,没有功能改动。完整功能(per-cell → 全局 floating panel 重构)在分支 `feature/floating-ai-button` 上,**未合并**。git status 是干净的,如果你想继续 feature,先 `git checkout feature/floating-ai-button`。 + +``` +feature/floating-ai-button HEAD: +12ec84a docs: refresh HANDOVER for floating panel work (d412130) +d412130 fix: hide+show cycles no longer accumulate prompt nodes in floating panel +c0eba7e feat: replace per-cell AI button with global floating panel +(已 push 到远端 + tracking 已设) +``` + +--- + +## 本次会话做了什么(按时间序) + +1. **HANDOVER.md 初版**(上一任留下的)— 已读 +2. **创建 `feature/floating-ai-button` 分支** +3. **重构**:把 per-cell 工具栏 AI 按钮(`OpenCodeCellActions`)整个换成全局右下角悬浮按钮 + 弹出 panel + - 新增 `src/components/opencode_floating_panel.ts`(Widget,挂 `document.body`) + - 删除 `src/components/opencode_cell_actions.ts`(逻辑搬上去) + - 改 `src/components/opencode_inline_prompt.ts` — 加 `getActiveCell?: () => CodeCell | null` 回调,3 处 cell 引用全用回调 + - 改 `src/index.ts` — 删 `toolbarRegistry.addFactory`,接入 floating panel + - 改 `style/base.css` — 删 `.opencode-cell-actions`,新增 `.opencode-floating-*`(fixed 定位、z-index 1000) + - 重命名 + 重写 `src/__tests__/opencode_floating_panel.spec.ts` +4. **Bug fix (d412130)**:hide+show 循环累积 prompt 节点 — 用户手测报告,在 `_hidePrompt` 加 defensive DOM cleanup +5. **Commit + push** 到 `feature/floating-ai-button`(只到这,没合并) +6. **回 main**:**用户决定在 main 上单独发 v0.1.2**(只 bump 版本号,不含功能) +7. **v0.1.2 tag + push** + +--- + +## 验证状态 + +| 检查 | 结果 | |------|------| -| `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) | +| Jest | ✅ 68/68(3 suites)— **在 feature 分支验证过**,main 上 0.1.2 没跑过 | +| pytest | ✅ 73/73(同上,feature 分支) | +| `tsc --noEmit` | ✅ exit 0(feature 分支) | +| ESLint | ⚠️ 2 pre-existing 命名错(`FlatProvider`/`BlockEntry` 接口名,不符合 `/^I[A-Z]/u`),我没引入 | +| `jlpm build:prod` | ⚠️ 未跑(见下) | -## 主要路由 -| 端点 | 方法 | 用途 | -|------|------|------| -| `/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 | 提问响应 | +--- -## 关键设计决策(避免来回改) +## ⚠️ 下个 agent 必须知道的几件事 -### 1. SSE 事件路由(`applyEvent`) -- 客户端**不**对内容事件(text/reasoning/tool/idle)做 sessionID 过滤 — 服务端 `?session=` 参数是唯一权威 -- 只对 `permission.asked` / `question.asked` 严格过滤,防止用户误答别 session 的提示 -- 这条规则是 **bug fix 310d507 的核心**,改之前会"切 session 后发消息无回复" +### 1. Codex 在本会话里基本不可用 ⚠️⚠️⚠️ -### 2. Cell 上下文不再自动注入 -- `OpenCodeRequest.context` 收缩为 `{notebookPath}` — 只告诉服务端哪个 notebook -- `_build_request_body` 只发 `parts=[{text: prompt}]` -- 用户通过"📋 插入单元格内容"按钮**显式 opt-in** 把 cell 源码作为 markdown code block 插入 -- 理由:LLM context 严格匹配用户意图,避免被前一个 cell 的 traceback 污染 -- 这是 310d507 的第二部分 +CLAUDE.md 强制 `model: kimi/kimi-k2.7-code`(或后来的 `kimi/kimi-k2.6`),但火山方舟 Coding Plan proxy 反复拒绝: -### 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 仍可能) +- `kimi-k2.7-code` — HTTP 400 "model not valid"(模型名错) +- `kimi/kimi-k2.7-code`(CLAUDE.md 当前版本) — 同样被拒 +- `kimi-k2.6` — HTTP 400 "reasoning is not supported" +- `kimi/kimi-k2.7-code` noop 偶尔通一次,然后实际任务又失败 -### 4. 流式 markdown 渲染(避免 partial-render flicker) -- 流式期间只 `textContent += delta`,**不**调 `marked.parse`(否则 fence 没闭合时的部分渲染会闪 toolbar) -- `session.idle` 时 `_resetStreamPointers` 触发**一次**完整 `marked.parse` 渲染 -- 终态与 `setMessages` 路径(setMessages 重新打开时拉的)完全一致 +**实际代码工作几乎全是 CC 手动做的**。Codex 临死前对 spec 文件做了一次有用的部分重写(改完了 65% 然后挂了)。 -### 5. 交互 UI 权限 -- `permission.asked` / `question.asked` 事件渲染**真实交互组件**(按钮 / 输入框) -- 点击通过后端路由代理回 OpenCode Serve -- 后端路由:`PUT /session/:sid/permissions/:permId` + `PUT /session/:sid/question/:qId/reply` +**建议下次**: +- 先 noop 测一次 Codex,通了再用;不通直接走 CC fallback(CLAUDE.md 规则 `codex-fail-3x → switch-to-cc`) +- 如果必须用 Codex,可能需要你这边再调一次 proxy 配置 -## 测试 -- 后端: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/` +### 2. d412130 bug 的防御性修复 + 测试局限 -## 已知小问题 / 待优化 -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 +**症状**:点悬浮按钮 → 显示 panel。再点 → 隐藏。再点 → **看到 2 个 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 +**根因**:`OpenCodeFloatingPanel` 用 `Widget.attach(prompt, this._panelEl)` 把 Lumino Widget 挂到裸 `HTMLDivElement` 上(host 不是 Widget),所以 prompt 的 Lumino `_parent` 是 null。真实 Lumino `Widget.dispose()` 在 parent=null 时走 `Widget.detach(this)` 分支按理能清 DOM,但实际有遗漏场景。 -## 还没做过的事(如果新会话要继续) -- UI tests (Playwright) — 写了 spec 但没真跑 -- 持久化 session 选择(目前只持久化 provider/model) -- cell 类型推断优化(目前用 `model.type`,真实 cell 应该有 metadata.language) -- 后端 session TTL / cleanup(目前不删,除非用户显式 release) -- 真实 OpenCode Serve 接入测试(目前用 mock) +**修法**(`src/components/opencode_floating_panel.ts` 的 `_hidePrompt`): +```ts +if (this._prompt) { + const node = this._prompt.node; + this._prompt.dispose(); + if (node.parentNode === this._panelEl) { // defensive + this._panelEl.removeChild(node); + } + this._prompt = null; +} +``` + +**回归测试局限**:`src/__tests__/opencode_floating_panel.spec.ts` 的测试 mock `Widget.dispose()` 直接用 `node.parentNode.removeChild` 清 — **比真实 Lumino 更激进**,所以这个回归测试在 mock 下永远过。**真实 bug 只能在真实 JupyterLab 里复现和验证**。如果你看到类似累积 bug,**必须真跑 JupyterLab**,别只信 jest pass。 + +### 3. 几个 `_onShellChanged` / `as any` 的取舍 + +- `_onShellChanged` 写成 **arrow function class field**(`= (): void => { ... }`)而不是 method,这样 `this` 锁定到实例上(Lumino signal 调用 slot 时 `this` 不可靠) +- `this._app.shell as any` 出现过几次 — 因为 `JupyterFrontEnd.shell` 在不同 JupyterLab 版本里类型签名是 `JupyterShell | null`,严格模式要求 cast。可以后续更精确处理,但当前能用 + +### 4. `jlpm build:prod` 没跑成功 + +`.venv/bin/jlpm` 是 Python 入口脚本,直接 `jlpm build:prod` 找不到。我试过 `python -m jupyter_builder.jlpm build:prod` 但也失败(参数解析对不上)。**完整 labextension 打包未验证** — 只跑了 `jest` + `tsc --noEmit` 间接确认编译能过。如果发版前要打包验证,你可能要在终端手动跑。 + +### 5. `v0.1.2` 是个"假版本" + +只 bump 了 `package.json` 的 version 字段(`pyproject.toml` 是 hatch dynamic,从 git tag 自动读),**没有功能变更**。发这个 tag 大概是为了占位 / 触发 CI。下次真发版时需要把 `feature/floating-ai-button` 合并到 main,然后重新打 tag。 + +--- + +## 仍然没做的事 + +- [ ] 把 `feature/floating-ai-button` 合到 main(用户说"暂时不管",所以还没合) +- [ ] **真实 JupyterLab + OpenCode Serve 端到端测试** — 没在活的 JupyterLab 里点过按钮。bug d412130 是用户手测报的 +- [ ] **Playwright UI tests** — 项目交接文档里说写过 spec 但没跑,本次也没动 +- [ ] **pre-existing lint 错清理**:`FlatProvider` → `IFlatProvider`,`BlockEntry` → `IBlockEntry`(2 处改名,小 PR) +- [ ] **`jlpm build:prod`** — 没跑通(见上) +- [ ] **CI 验证**:`v0.1.2` tag push 触发 CI,看一下 `.github/workflows/build.yml` 跑通没 + +--- + +## 关键文件速查(给下任的 quick map) + +``` +src/components/opencode_floating_panel.ts ← 新文件,核心逻辑都在这(310+ 行) +src/components/opencode_inline_prompt.ts ← 解耦 Cell,加了 getActiveCell 选项 +src/__tests__/opencode_floating_panel.spec.ts ← 重写后的测试(1556 行,68 tests) +src/index.ts ← 极简,只剩 activate() + 挂 floating panel +style/base.css ← 找 .opencode-floating-* 块 +``` + +参考架构/HTTP 细节(未变)参考本仓库 HANDOVER.md 之前章节的 [架构](#架构) / [主要路由](#主要路由) / [关键设计决策](#关键设计决策-避免来回改)。 + +--- + +## 测试命令 + +```bash +# 后端 +.venv/bin/pytest opencode_bridge/tests/ -q # 73 passed + +# 前端 +./node_modules/.bin/jest --no-coverage # 68 passed +./node_modules/.bin/tsc --noEmit # exit 0 +./node_modules/.bin/eslint --ext .ts,.tsx src/ # 仅 pre-existing 错 + +# 在哪个分支跑? feature/floating-ai-button 上跑过了,main 上 v0.1.2 没跑 +``` + +--- + +## 上下文管理提示 + +上下文快满了。新 agent 第一件事: + +1. **读这份 HANDOVER.md 全文**(包括本节之前的所有内容) +2. `git log --oneline -5` 确认 commit hash 没变 +3. 跑上面 4 个验证命令 +4. 如果要继续 feature 工作 → `git checkout feature/floating-ai-button` +5. 如果只是在 main 上发版 → 留在这里,准备好就 `git merge feature/floating-ai-button`(或 PR 流程) + +**不要**: +- 不要从 main 重新开发 — 浪费时间 +- 不要相信 jest 单测覆盖 d412130 那类 bug — 必须在真 JupyterLab 里点 +- 不要相信 Codex 能用 — 先 noop 测一下 \ No newline at end of file