# DSH Notebook v2:本轮需求统一整理 + 实施计划 > 日期:2026-11-04 > 插件:`dsh-notebook-v2`(源码在仓库顶层,junction 装到 `~/.dsh/profiles/desktop/node_modules/`) > 本文是「先统一整理、再定计划、再动手」的产物:**需求清单 → 根因证据 → 方案 → 分阶段计划 → 待确认决策点**。 --- ## 一、需求统一整理 用户先后提了 5 组问题(原话已归并去重)。我按「可验收的行为」重写,并标注归入哪条主线: | # | 用户原话(意译) | 我的理解(验收口径) | 现状 | |---|---|---|---| | **R1** | jupyter 插件「添加到对话」这部分有问题;建议用具体逻辑:cell 的序列 + 第几行到第几行;并且对话框里可以渲染成**文件的那种形式** | ① 点「💬 添加到对话」必须真的进到主对话输入框(不是剪贴板);② 引用要带**结构定位**:`notebook 路径#Cell[序号]:L起-L止`;③ 在对话里呈现成**文件卡片/芯片**形态,而不是糊一坨文本 | ❌ 完全不可用(退化成剪贴板) | | **R2** | jupyter 常见的快捷键这部分,你调研下,我们的插件也都应该具备了 | 对照 JupyterLab / Notebook 常用键位清单,把插件缺的补齐(命令态/编辑态分开) | ⚠️ 部分(无命令态概念) | | **R3** | AI 补全这部分:注释生成代码,还有内置的 AI 改写,应该是**两个设置**来分别设置 | 「AI 自动补全(ghost,注释→代码)」与「内置 AI 改写(内联 AiBar)」各有独立设置:开关 + provider/model/思考强度 | ❌ 共用一份 `aiPref`;补全开关还不持久化 | | **R4** | 我新开 session 时让它生成 notebook,它采用了 py 脚本而不是 tools,是不是我们的 tools 都没了 | 模型在「新建/编辑 notebook」场景应优先用 `nb_*` 工具,而不是写 `nbformat` 脚本 | ⚠️ 工具都在,但**没被引导** | | **R5** | 截图中 md 的 cell 按 Shift+Enter 为什么没运行渲染?必须点「完成」才渲染;而且 Shift+Enter 后下一格自动成了编辑状态 | ① Markdown 格 Shift+Enter = 渲染并跳到下一格;② Shift+Enter 后下一格应为**选中(命令态)**,与 Jupyter 一致 | ❌ 两个都是真 Bug | **归并后只有三条技术主线**: 1. **对话投递线(R1)**:通道 + 结构化格式 + 文件卡片形态。 2. **交互对齐线(R2 + R5)**:Jupyter 语义(命令态/编辑态、运行三连、渲染提交)。 3. **AI 设置线(R3)**:补全与改写拆成两套设置。 4. **可发现性线(R4)**:让模型知道该用 `nb_*` 工具。 --- ## 二、逐条根因与证据(都已核实,不是推测) ### R1 · 添加到对话:为什么点了只进剪贴板 **现象**:截图里顶部橙色提示 `已复制到剪贴板(当前挂载点未提供对话桥接)`。 **代码**:`lib/client.js:4679-4706` 的 `cite()`: ```js const ia = props && props.inputActions if (ia && typeof ia.appendDraft === "function") { ia.appendDraft(body); return true } // 兜底 → navigator.clipboard.writeText(body) + 上面那句 notice ``` 即:`props.inputActions` 取不到(或没有 `appendDraft`)→ 走剪贴板。 **证据链(四步都做了实测)**: | # | 事实 | 来源 | |---|---|---| | A | `sidebar.right.pane.tab` 槽位 catalog 的 `standardProps` **确实列了** `inputActions: InputActions` | `cordis_inspect_query(client/Slots, root=sidebar.right.pane.tab)` | | B | 但 `@deepseek-ai/dsh-client-ui-sidebar-right` 的客户端 bundle 里 **0 处** `inputActions` 字样(全 asar 扫描 13903 个文本文件,命中只在 conversation / voice-input / client-runner 的类型目录字符串里) | `asar-scan` 全量扫描 | | C | 真正能用的参照实现 `dsh-better-sidebar` **完全不碰** `props.inputActions`,它走 **client 服务**:`ctx.get("conversation").input.for(actx)` → 读 `draft` → `input.setDraft(next)` | `dsh-better-sidebar/lib/client-editor.js:49451-49473` | | D | 宿主 `InputActions` 确实提供 `appendDraft(text)`、`insertReference(ref, span)`(带 `span.draftRev === rev` 的 CAS)、`captureInsertion()` | asar `dsh-client-ui-conversation/lib/client.js` ~14704 / ~15347;voice-input 用 `captureInsertion()` + `insertText()` | **结论**:A 是「类型目录」,不等于「owner 运行时真给」。侧栏 tab 没有 composer,所以没有 `inputActions`。 **正道是 C**(会话作用域的 `conversation.input`),要文件芯片再用 D 的 `insertReference(ref, captureInsertion())`。 **当前投递格式**(`client.js:5199`): ```js cite(`\`\`\`${lang} title="${title}#${label}"\n${selPop.text}\n\`\`\``) // label = Cell [3]:L4-L8 ``` → 是**纯文本围栏**,所以用户看到的是「一坨文本」,不是文件卡片。 --- ### R3 · AI 补全 / AI 改写 共用一份设置 - `aiPref = { provider, model, reasoningEffort }`(`client.js:4569`,localStorage 键 `nb2.aiPrefs.v1`)。 - **补全**用它:`client.js:5080-5082` 请求 `/nb2/ai/complete` 时逐字段带上。 - **内联改写**也用它:`runAi` → `/nb2/ai/stream`。 - 补全开关 `aiComplete` 是 `useState(false)`(`client.js:4557`)——**不持久化**,每次挂载都回到关。 - 宿主两个端点已各自接受 `provider/model/reasoningEffort`,**协议不用改**,只需前端拆两份 + 后端各取各的。 --- ### R4 · 「tools 都没了」不成立 - `lib/index.js:1753`:`tools.map((tool) => ctx.tools.register(tool))`,`lib/tools.js` 里 **28 个** `nb_*` 工具(`nb_new` / `nb_add_cell` / `nb_edit_cell` / `nb_run_cell` …)。 - 实测 `cordis_inspect_query(host/Tool, listTools)`:**28 个 nb_* 全部在可调用清单里**(本会话可见)。 - 插件**没有注册任何 `systemPrompt` section**(grep `systemPrompt|promptSection` 零命中)。 - 所以真问题是**可发现性 + 行为**:模型没有被告知「.ipynb 要走 nb_* 工具」,于是选了它更熟的路(写 `nbformat` 脚本)。截图里它自己也说 "Notebook is valid (16 markdown cells, 0 code cells, nbformat OK)" —— 它是**故意用脚本**造 markdown-only 的 notebook。 **待补证据**:那个 session 的原始记录(`~/.dsh/sessions/.../session.v4.jsonl.zstd` 是多帧 zstd,单帧解压只得到 312 字节)→ 需要在实施时用能读多帧的方式确认它当时看到的工具清单。**但这不阻塞**:工具在,缺的是引导。 --- ### R5 · Markdown 格 Shift+Enter 不渲染 + 下一格自动进编辑态 **两处独立的实现缺陷,同一个根:`mdEditing` 状态与焦点语义。** 1. **Markdown 编辑态复用同一个 CM6 编辑器**(`client.js:3904-3950`): `isDoc && !mdEditing` 才渲染 `MarkdownView`,否则落到 `h(CellEditor, editorProps)`。 2. **Shift-Enter 只跑格、不复位 `mdEditing`**:CM6 keymap(`client.js:2597`)`Shift-Enter → onRunBelow()` → `runBelow()`(`client.js:4287`)只做 `runCell` + `focusCell(下一格)`。 而「完成」按钮(`client.js:3847-3851`)是 `setMdEditing(false)` **再** `onRunBelow()`。 → 所以 Shift-Enter 后该格仍是 `mdEditing === true`,**继续显示源码** = 用户看到的「没渲染」。 3. **下一格自动进编辑态**:`runBelow` → `focusCell(target.id)`(`client.js:4295`)→ `focusReq` → CellView 聚焦 CM6。 Jupyter 语义是 **`run-cell-and-select-next`**:跑完**选中**下一格(命令态),要再按 `Enter` 才进编辑。 代码注释说这是为早先一条反馈故意加的(「跑完不能接着打下一格」)—— 现在这条与 Jupyter 习惯冲突。 --- ## 三、方案 ### R1 · 对话投递 v2(结构化 + 文件卡片) **通道**(照 `dsh-better-sidebar` 的做法,不用 `props.inputActions`): ```js const actx = ctx.sessions.scope(sessionId) const conversation = ctx.get("conversation") const input = conversation.input.for(actx) // 纯文本路径 input.setDraft(splice(currentDraft, text, caret)) // 文件芯片路径(首选,形态更接近用户要的「文件那种形式」) input.insertReference(ref, input.captureInsertion()) ``` **定位格式**(纯函数、可单测): | 场景 | 定位串 | |---|---| | 代码格选中若干行 | `#Cell[3]:L4-L8` | | 整格 | `#Cell[3]` | | 输出 / 报错 | `#Cell[3].output` / `.error` | | 行号未知(DOM 反查失败) | 只给 `#Cell[3]`,**绝不瞎猜行号** | - cell 序号 = **1-based 显示序号**(与 UI 左侧 `[ ]` 一致),不是内部 id。 - 行号 = cell 内 **1-based** 行号(与 CM6 gutter 一致)。 - 抽出 `formatNotebookRef({ path, cellIndex, startLine, endLine, kind })` 纯函数 → 进 `test/client-logic.mjs`。 **形态**:优先 `insertReference(ref, span)` 造文件芯片;若 `ref` 形状不支持 notebook 定位(待研究报告确认),退化为 「**芯片(文件)** + 正文结构化围栏」:芯片给「文件身份」,围栏给「cell/行范围 + 代码」。 **验收**: 1. 点「💬 添加到对话」→ 主对话输入框出现内容/芯片,**不再出现剪贴板提示**; 2. 内容带 `#Cell[n]:Lx-Ly`; 3. 非右栏挂载(拿不到会话作用域)时,才允许退回剪贴板,且提示文案要说清「无法桥接,已复制」。 --- ### R2 · 快捷键对齐(等研究报告定稿,先定框架) 引入**命令态 / 编辑态**两个状态(当前完全没有),这是 Jupyter 键位的语义基础: - 命令态(选中格、蓝色边、按键不输入文本):`↑/↓` 选格、`Enter` 进编辑、`A` 上插、`B` 下插、`DD` 删、`Z` 撤销删、`Y/M/R` 切类型、`Shift-M` 合并、`Ctrl-Shift-Minus` 拆分、`Ctrl-S` 保存、`L` 行号、`O` 折叠输出。 - 编辑态:`Shift-Enter`(跑并选中下一格)、`Ctrl-Enter`(原地跑)、`Alt-Enter`(跑并下方插入)、`Ctrl-]` / `Ctrl-[` 缩进、`Tab` 补全/缩进、`Ctrl-/` 注释、`Ctrl-D` 删行、`Esc` 回命令态。 - 与宿主冲突的键(`Ctrl-S`、`Ctrl-/`、`Esc`)先做「谁先拿到事件」的实测再定。 (详细清单与冲突矩阵见研究报告,报告落 `docs/research/2026-11-04-jupyter-shortcuts.md`。) --- ### R3 · AI 双设置 **数据模型**(新键 `nb2.aiPrefs.v2`,自动迁移 v1): ```js { completion: { enabled: false, provider, model, reasoningEffort }, // ghost / 注释生成代码 inline: { provider, model, reasoningEffort } // AiBar 改写/解释/修错 } ``` - 迁移:v1 的 `{provider,model,reasoningEffort}` → `inline`;`completion` 继承同模型但 `enabled: false`。 - 补全开关进 `completion.enabled` → **持久化**(修掉「每次重开都关」)。 - UI:工具栏「AI 补全」按钮保留(快捷开关),旁边加齿轮 → 面板两段式设置:`补全` / `改写`,各自 provider + model + 强度 + 「测试」。 - 后端:`/nb2/ai/complete` 取 `completion.*`,`/nb2/ai/stream` 取 `inline.*`;两个端点协议不变。 **验收**:改「补全」的模型不影响「改写」的模型;刷新页面后两者都记住;关掉补全不影响 Mod-K 改写。 --- ### R4 · 让模型用 nb_* 工具 1. **host 半注册 `systemPrompt.section`**(现在完全没有): 「涉及 `.ipynb` 的创建/编辑/运行,一律用 `nb_*` 工具;**不要**写 `nbformat` 脚本改 ipynb。用户说『新建 notebook / 写个 notebook』→ `nb_new` + `nb_add_cell`。」 (注意踩坑:section 的 `text()` 拿不到 session/cwd,只能写静态文本。) 2. **工具描述补「何时用」**:`nb_new` 明确「用户要新建 notebook 时先调它」;`nb_add_cell` 明确「先写 markdown 方案再写代码时,用 `cellType: "markdown"` 连续调用」。 3. **降低脚本动机**:`nb_add_cell` 支持一次传多格(`cells: [...]`),避免「16 个 markdown 格 → 写脚本更快」的直觉。 **验收**:新 session 说「在这个目录新建一个 notebook,先写方案文字」→ 轨迹里出现 `nb_new` / `nb_add_cell`,**没有** `nbformat` 脚本。 --- ### R5 · Markdown 提交 + 命令态(改法已明确) 1. **`mdEditing` 复位**:CellView 里把 `editorProps.onRunBelow` 包一层: ```js onRunBelow: () => { if (isDoc) setMdEditing(false); onRunBelow() } ``` 同理 `Ctrl-Enter`(原地跑)也要渲染。 2. **`runBelow` 增加焦点模式**: ```js runBelow(cellId, { mode: "select" | "focus" }) // 默认 "select" ``` `select` → 只 `setSelected(next)`(命令态,不聚焦编辑器);`focus` → 现在的 `focusCell`。 3. **设置项**(跟着 R3 的设置面板一起):`Shift-Enter 后进入下一格编辑态`(默认**关**,即 Jupyter 语义)。 **验收**:Markdown 格 Shift+Enter → 立刻渲染成排版视图 + 下一格被选中(无光标);再按 `Enter` 才进编辑。 --- ## 四、分阶段计划 | 阶段 | 内容 | 文件 | 验收 | |---|---|---|---| | **P0-1(R5)** | Markdown Shift+Enter 渲染 + `runBelow` 的 select/focus 双模式 | `lib/client.js`、`test/client-logic.mjs` | md 格 Shift+Enter 立即渲染;下一格为命令态;「完成」按钮行为不变 | | **P0-2(R1 通道)** | `cite()` 改走 `ctx.get("conversation").input.for(actx)`;抽出 `formatNotebookRef` 纯函数;结构化定位串 | `lib/client.js`、`test/client-logic.mjs` | 点按钮内容进输入框;带 `#Cell[n]:Lx-Ly` | | **P0-3(R3 拆分)** | `aiPrefs.v2` + 迁移 + 补全开关持久化 + 两段式设置 UI | `lib/client.js`、`lib/index.js`(AI 端点按 settings 取值)、`test/ai.mjs` | 两套设置互不影响且持久化 | | **P1-1(R2 键位)** | 命令态/编辑态状态机 + 运行三连 + A/B/DD/Y/M/R/↑↓/Enter/Esc | `lib/client.js`、`test/client-logic.mjs` | 报告里的 P0 键位全绿 | | **P1-2(R1 芯片)** | `insertReference(ref, captureInsertion())` 文件芯片;失败退化围栏 | `lib/client.js` | 对话里出现文件卡片形态 | | **P2-1(R4)** | systemPrompt section + 工具描述 + `nb_add_cell` 批量 | `lib/index.js`、`lib/tools.js`、`test/tools` 相关 | 新 session 走 nb_* 工具 | | **P2-2** | 设置项持久化统一(`nb2.settings.v1`)、README/踩坑同步 | 全量 | 文档与实现一致 | **每阶段收尾统一跑**:`node test/all.mjs --fast`(现有 16 个测试文件);新增断言进对应文件,不新建平行测试壳。 --- ## 五、待确认的决策点(影响行为,先问再动) 1. **R5 默认语义**:Shift-Enter 后下一格 = **命令态选中**(Jupyter 默认,推荐)/ 保持现在的**自动进编辑态** / 做成设置项默认关(推荐方案:设置项 + 默认命令态)。 2. **R3 拆分粒度**:只拆「开关 + 模型」两套(推荐、最小可用)/ 连「触发延迟、最大续写行数、是否只在注释行触发」也各自可配(更重)。 3. **R1 芯片形态**:若 `ref` 不支持 notebook 定位 → 「文件芯片 + 结构化围栏正文」(推荐)/ 只发结构化围栏纯文本(最简单、与 better-sidebar 一致)。 4. **R4 是否允许改模型行为**:加 `systemPrompt` 段会**改变所有会话**里模型对 `.ipynb` 的处理方式(推荐加,但需明确接受)。 --- ## 六、风险与回滚 - **R1 通道换血**:`ctx.get("conversation")` 若在非右栏挂载下也拿不到 → 必须保留剪贴板兜底 + 明确文案;`setDraft` 是「整串替换」,**必须基于当前 draft 做 splice**,不能直接覆盖(否则吞掉用户已输入内容)。 - **R3 迁移**:v1 → v2 迁移只做一次;迁移失败一律回落默认值,**不允许抛到 UI**。 - **R2 键位**:`Ctrl-S` / `Ctrl-/` / `Esc` 与宿主争抢 → 每加一个键都要有「宿主是否先消费」的实测记录。 - **R5 焦点语义**:命令态没有光标时,`Enter` / 方向键必须由面板级分发器(`decideCellKey`)接住,否则「选中了但按什么都没反应」——这正是之前踩过的坑(焦点掉到 body)。 - 回滚:所有改动集中在 `lib/client.js` + `lib/index.js`;`git` 未跟踪该包,**改动前先 `cp` 一份 `lib/client.js.bak-`**。 --- ## 七、并行调研产物(本轮同时进行) | 报告 | 内容 | 用途 | |---|---|---| | `docs/research/2026-11-04-chat-reference-api.md` | `InputActions` / `insertReference` / `ReferenceChipNode` 的精确契约,`ref` 形状,为什么侧栏 tab 拿不到 | R1 的芯片实现依据 | | `docs/research/2026-11-04-jupyter-shortcuts.md` | 现状键位穷举 + JupyterLab/Notebook 权威对照 + 缺口优先级 + 冲突风险 | R2 的实施清单 | --- ## 八、实施进度(滚动更新) ### 已完成(测试全绿:`node test/all.mjs --fast`) | 需求 | 做了什么 | 关键文件 | |---|---|---| | **R5** | ① doc 格的 `onRun` / `onRunBelow` 先 `setMdEditing(false)` 再跑 → Shift-Enter 立刻渲染;② 新增 `selectCellCommand()`(选中 + 焦点交给**容器**,不进编辑);③ `runBelow(cellId, {mode})` 默认 `select`,最后一格 `addCell(..., {focus:false})` | `lib/client.js` | | **R1** | ① 新增模块级 `getChatBridge(sid)`:`ctx.sessions.scope` + `ctx.get("conversation").input.for(actx)`;② 芯片通道 `actx.emit("slash/input-insert-reference", {reference, span:{draftRev,start,end}})`,`appearance:"file"`;③ 纯文本 `setDraft` 降级;④ 剪贴板最后兜底(文案改成「无法桥接」);⑤ 新增纯函数 `formatNotebookRef()` 产出 `nb.ipynb#Cell[3]:L4-L8` | `lib/client.js`、`test/client-logic.mjs` | | **R3** | ① `aiPrefs.v2 = { completion:{enabled,provider,model,reasoningEffort}, inline:{...} }` + v1→v2 迁移;② 补全开关持久化;③ **改写请求终于带上 provider/model/effort**(早先一个都不传,设置对改写无效);④ 设置栏改两段式(共用 `AiTargetPicker`) | `lib/client.js`、`test/client-logic.mjs` | | **R2(补)** | 多选:`Shift-↑/↓` 扩选、`Mod-A` 全选、`DD` 删全部选中;`Shift-M` 合并(宿主原子 `cell/merge` + `Notebook.mergeWithNext`) | `lib/client.js`、`lib/index.js`、`lib/notebook.js` | | **AI P0(补)** | P0-4 触发放宽(光标后只剩 `)]},;` 也算行尾)、P0-6 失败可见(`AI ⚠` 5s 自动消失,区分超时/取消与真失败) | `lib/client.js` | | **架构** | 快捷键重构为「命令注册表 + 键位表」(见第十节):`COMMANDS` / `KEYBINDINGS` / `commandsRef.current`,右键菜单改由命令 id 驱动 | `lib/client.js`、测试 | ### R1 的**更正**(原诊断是错的,以调研报告为准) - ❌ 旧结论:「侧栏 tab 拿不到 `props.inputActions`」→ 部分正确但**不是根因**。 - ✅ 实测根因:`props.inputActions` **有值**,但它公开的 `actions` 只有 8 个方法 (`captureInsertion / insertText / setDraft / persistDraft / addAttachments / removeAttachment / pruneAttachments / submit`)——**没有 `appendDraft`,也没有 `insertReference`**(这两个只在内部 `DraftEditorRuntime` 上)。所以 `typeof ia.appendDraft === "function"` **恒为假**,必然退化成剪贴板。 - ✅ 另修正一处错记:`dsh-better-sidebar` **并不用** `inputActions`(全仓库 grep 0 命中), 它走的是 `ctx.get("conversation").input.for(actx)` + 事件。 - 版本基线:DSH **0.2.1-rc.5**。 ### 待做 R2(命令模式 + 键位)、R6(右键菜单)、R7(大纲停靠)、R4(systemPrompt + 工具描述)。 真机验证项:R1 芯片是否出现 / R5 md 渲染 / R3 两套设置(客户端改动需**刷新页面**生效)。 --- ## 九、AI 自动补全修改计划(据开源调研,2026-11-04) > 调研报告:`docs/research/2026-11-04-ai-completion-survey.md`(395 行,3 张表 + NES 专段) ### 一句话结论 开源实现的差距**不在「会不会调模型」**,而在四件工程事: ① 输入即取消 + 前缀缓存(延迟/成本)② 局部接受 + 多候选(交互) ③ 结构化上下文(FIM 拼接 / 跨 cell / kernel 变量)④ 上下文过滤器(**什么时候不发请求**)。 我们此前 ①②④ 基本空缺,③ 只做了「前 3 格源码」。 ### 已实施(本轮,测试全绿) | # | 能力 | 做法 | 参考 | |---|---|---|---| | P0-1 | **在途请求即时取消** | `schedule()` 入口 abort 上一个 `inflight`;此前只在编辑器销毁时 abort → 每敲一个字符白烧一次 1–2s 的 token | Continue `CompletionProvider` | | P0-2 | **前缀缓存(LRU 64)** | 键 = `光标位置 + 文档全文`;命中直接 dispatch 幽灵文本,0 网络延迟;退格/改回来立即可见 | Continue `AutocompleteLruCache` | | P0-3 | **按词接受** | `acceptWord` 原为**死代码**(0 处调用),现绑 `Ctrl-→` + `Alt-f`;吃掉下一个词,其余继续留在幽灵态 | VS Code `acceptNextWord` / Cursor `Cmd-→` | | P0-5 | **内核变量进补全** | `/nb2/ai/complete` 透传 `vars`(≤16 条,名字+类型+一句细节);`ai.js` ghost 分支拼 `Live kernel variables` 块。数据链路早已存在(`/nb2/vars`),只差传参 | jupyter-ai-lite 跨 cell 上下文 | ### 待做(按优先级) | # | 能力 | 现状 | 做法 | 量级 | |---|---|---|---|---| | P0-4 | **触发放宽** | 只允许「行尾」触发 | 行中触发条件改为「右侧只有空白/闭合符」——Copilot 正是这么做的,能显著提升触发率 | S | | P0-6 | **失败可见** | `catch { return null }`,超时/限流与「没啥可补」无法区分 | 失败时在格工具条显示极轻量 `AI ⚠`,5s 自动消失(JupyterLab 的 error 指示同款) | S | | P1-1 | **多行 ghost 的真实行盒** | 单 inline widget + `whiteSpace:pre`,**推测**下方内容不被推开、长建议溢出(未真机验证) | 抄 JupyterLab `ghost.ts`:行高 spacer + `maxLines` 截断 + `⇓` 提示 | M | | P1-2 | **增量收缩** | 打字时幽灵直接消失 | JupyterLab 的做法:新文本与建议前缀匹配时**收缩**而不是消失(连续打字手感关键) | M | | P1-3 | **多候选轮换** | 单建议 | 一次请求拿 2–3 条,`Alt-]` 轮换;代价是 token ×N,需设置开关 | M | | P2-1 | **按行接受** | 无 | `acceptLine`:吃到行尾;与 `acceptWord` 同一套机制 | S | | P2-2 | **补全延迟可配** | 硬编码 550ms | 进 `aiPrefs.completion` 设置(用户已要求「两个设置分开」,顺手加字段) | S | ### 明确**不做**(有理由,不是遗漏) | 项 | 为什么不做 | |---|---| | **Next Edit Suggestion(NES)** | Copilot 官方承认「PR diff 训不出来、比普通 LLM 还差」;Zed 靠自建推理 + 投机解码才做到 p50<200ms。我们本地 HTTP + chat 模型会落在 1–3s,**体验负收益** | | **FIM 特殊 token**(`` 等) | 我们的模型没被 FIM 训练过。Continue 也只对 FIM 模型用它,对 GPT/Claude 改走 hole-filler 模板 —— 硬拼只会退化 | | **仓库级 RAG 索引** | Tabby 那套要 tree-sitter + BM25 + 索引服务;notebook 收益远低于 IDE,且我们已有「主对话引用 cell」通道 | ### 验收口径 - P0-1:打字时 Network 里**只有一个**在途 `/nb2/ai/complete`(新键入把旧的 abort 掉)。 - P0-2:输入 `df.` → 等建议 → 退格 → 再打回 `df.` → 建议**立即**出现(0 请求)。 - P0-3:有建议时按 `Ctrl-→` 只吃一个词,剩余仍是灰色幽灵文本。 - P0-5:先跑一格 `df = pd.DataFrame(...)`,再在下一格写 `df.`,建议里出现真实列名。 --- ## 十、快捷键架构:命令注册表 + 键位表(2026-11-04,用户提议后重构) ### 用户的提议(原话意译) > 「快捷键这部分,是不是要先把各个功能的 API 都预留出来,然后再写个脚本把快捷键和 API 联通起来,是不是更优雅一点?」 **是对的**,而且这正是 Jupyter / VS Code 的标准做法(command registry + keybinding contribution)。 重构前的实际问题是:同一条动作被**四处**各写一遍闭包 —— 面板快捷键、工具栏按钮、右键菜单、CM6 keymap。改一处忘三处是必然结果, `acceptWord` 写好了却从来没接线(死代码)就是这么产生的。 ### 重构后的三层 | 层 | 位置 | 职责 | 可测 | |---|---|---|---| | **`COMMANDS`** | 模块级纯数据 | 命令 id → `{ label, hint }`(动作的**元数据**,也是 UI 文案的唯一来源) | ✅ 纯数据 | | **`KEYBINDINGS`** | 模块级纯数据 | 键位 → 命令 id(`context: "command" \| "global"`、`twoStep`)。**只有这张表知道快捷键** | ✅ 纯数据 | | **`commandsRef.current`** | 面板内,每帧重建 | 命令 id → 真正执行的闭包(要 state,只能在组件里建) | 间接(源码级断言) | 分发链路:`keydown` → `decideCommandKey(state)`(纯函数,键位表 → 命令 id) → `commandsRef.current[command]`(执行)。找不到执行体时**明喊**「未实现的命令」, 不再静默变成「按了没反应」。 ### 为什么用 `commandsRef` 而不是 `useMemo(deps)` 执行表引用了 `moveCellTo` / `mergeBelow` / `citeCellRef` 等**在后面才声明**的符号。 把它们写进依赖数组会在首次渲染就 TDZ 崩 —— 本仓库已经踩过一次 (`Cannot access 'citeCell' before initialization`,冒烟测试抓到的)。 ref 每帧重赋值既避开 TDZ,又保证闭包永远是最新 state。 ### 消费者已统一 - **面板快捷键**:键位表驱动(`decideCommandKey`)。 - **右键菜单**:菜单项只写命令 id,`label`/`hint` 从 `COMMANDS` 取、执行从 `commandsRef` 取 → 菜单上显示的快捷键**永远等于**真实绑定。 - **工具栏按钮**:仍是各自闭包(低频、且带视觉状态),**待迁移**; 迁移时只需把 onClick 换成 `commandsRef.current[id]`。 - **CM6 keymap**(编辑器内部:Tab / Ctrl-→ / Alt-\ / Mod-K):仍由 `buildExtensions` 持有, 因为需要 view 级 API(dispatch、选区坐标)。它们的 hint 已登记进 `COMMANDS`, 并有测试断言「这 4 条是唯一允许没有面板执行体的命令」。 ### 护栏(都是自动化的) 1. 每条键位指向的命令必须在 `COMMANDS` 里有元数据; 2. 同一 context 下不允许重复键位(否则先匹配者赢,另一个静默失效); 3. 除 4 条编辑器内部命令外,**每个命令都必须有执行体**(防死代码); 4. 源码级断言:不许再出现 `cmd.action` 分支(防止退回旧写法); 5. Jupyter P0 键位覆盖清单(A/B/D/Z/Y/M/R/↑/↓/Enter/Esc/Mod-S/Shift-M)逐条断言。 ### 怎么加东西(下次照做) - **加一个快捷键**:`KEYBINDINGS` 加一行 → 跑测试(缺命令立刻红)。 - **加一个动作**:`COMMANDS` 一条 + `commandsRef.current` 一条 → 菜单/按钮直接引用 id。