✨ feat(vscode): 代码阅读加「问 AI」,块下面问 Claude Code,问答存进 .spx 挂在块下

This commit is contained in:
cruldra committed 2026-09-26 21:12:47 +08:00
1 parent badc722640
commit fb53bb727c
34 files changed
+2726 -43

No files matched your search

+67 -6
View File
@@ -28,6 +28,7 @@
| 高亮名字的各处出现 | 和普通编辑器一样单击名字就高亮它在这个文件里的各处出现,由语言扩展按语义认(同名的别的东西不算),颜色用 VS Code 的单词高亮色;看不见的(折叠、筛掉的)不自动展开,筛选栏上写「名字 · N 处」 |
| 侧栏文件树 | 只看和打开:展开目录、打开文件、刷新、全部折叠,外加给目录 / 文件记注释;不做新建 / 重命名 / 删除 |
| 文件注释 | 读代码时给目录 / 文件随手记一句:存在各工作区文件夹的 `.spx/code-notes.json`,跟着仓库走;在 VS Code 里改名、挪位置、删除时注释跟着变 |
| 问 AI | 块右上角「问 AI」,在块下面问 Claude Code(`claude -p`,只读,用插件默认的 profile)这块代码,回答逐字出来按 Markdown 显示、能追问(接着同一个会话)、能停;问答存进各工作区文件夹的 `.spx/code-qa.json`,灰字挂在块下面免得重复问,理解了就删。问答按块的位置记号挂,记号只由和语言无关的块数据(种类、facts 里的名字、源码哈希)算出,文件别处改了照样认得;认不回来的放在预览顶上。见「问 AI」 |
| 译文用词 | 结构照抄原语言,只翻关键字、内置类型、运算符;用专业术语(`export` → 公共,`catch` → 捕获,`===` → 全等) |
| 函数、类、导入、常量、变量块 | 预览不显示一行译文,显示树形属性表;属性值里的类型、表达式照常直译 |
| 字典 / 对象字面量 | 常量、变量的值是 Python 字典 / TypeScript 对象字面量时不挤成一长行:值写「字典 N 项」/「对象 N 项」,各项展开成「键 \| 值」两列的子行,值又是字典 / 对象时一层层往下展开;参数默认值照旧一行 |
@@ -85,7 +86,7 @@
- 每一句是一块:导入、函数、类、枚举、类型、字段、常量、变量、条件、否则、循环、尝试、捕获、最终、返回、语句、注释。
- 嵌套的块放在父块下面、往右缩进;每一层左边一条竖线(颜色 `--vscode-editorIndentGuide-background`)。
- 块的组成(从左到右):折叠箭头(只有有子块的块才显示,其余占位对齐)、种类标签(字取翻译插件给 `kind` 值起的名字,见「筛选」)、内容、右上角一排图标按钮。
- 右上角的图标按钮(原型组件 `Reader / BlockTools`,普通块、属性表块、库规则的卡片共用):只有图标、不带字,名字写在悬停提示(`title`)和 `aria-label` 里;整排是 `role="group"`(`aria-label="块操作"`),以后的按钮(问 AI)加在这一排里。眼下只有「编辑 / 预览」切换:预览时铅笔「编辑」,编辑时眼睛「预览」、带按下的底色(`--vscode-toolbar-activeBackground`)。按钮宽 24px、高 20px(和种类标签同高),图标 14px、`descriptionForeground` 色;悬停 `--vscode-toolbar-hoverBackground`,键盘聚焦描 1px `--vscode-focusBorder`;Tab 能走到,回车 / 空格切换。
- 右上角的图标按钮(原型组件 `Reader / BlockTools`,普通块、属性表块、库规则的卡片共用):只有图标、不带字,名字写在悬停提示(`title`)和 `aria-label` 里;整排是 `role="group"`(`aria-label="块操作"`)。先是「问 AI」(sparkles 图标,点开 / 收起块下面的问答区,展开时带按下的底色、`aria-expanded`;文件不在工作区文件夹里时没有,见「问 AI」),再是「编辑 / 预览」切换:预览时铅笔「编辑」,编辑时眼睛「预览」、带按下的底色(`--vscode-toolbar-activeBackground`)。按钮宽 24px、高 20px(和种类标签同高),图标 14px、`descriptionForeground` 色;悬停 `--vscode-toolbar-hoverBackground`,键盘聚焦描 1px `--vscode-focusBorder`;Tab 能走到,回车 / 空格切换。
- 种类标签和底色按种类区分(颜色见下表);注释块虚线框。
- 内容两种状态,每块单独切:
- 预览:函数、类、枚举、导入、常量、变量块是属性表(见下节),注释块是 Markdown 说明(见「注释的 Markdown 排版」),其余块是一行译文;按词着色,等宽字体(中文部分用系统中文字体回退)。一行译文的块带说明(Python 字段的属性 docstring)时,说明按 Markdown 排在译文下面。
@@ -205,6 +206,40 @@
- 清掉(连同筛选栏上那句):点空白处或不是名字的字、按 Esc(输入框、下拉列表自己用掉的 Esc 不算)、文档换了版本、换了翻译。
- 不抢别的点击:⌘ / Ctrl(以及 Alt、Shift)+ 点击、双击的第二下、拖着选字都不动高亮(双击的第一下照样算一次单击,和编辑器一样);点在按钮、链接、输入框、下拉列表、筛选栏、属性名上照旧,高亮不动——折叠箭头、「编辑」、Markdown 里的链接都照旧;名字本身在按钮里(带下一层的子行的键)时照旧折叠,同时高亮。
### 问 AI
原型画板「问 AI」、组件 `Reader / AskPanel`(`State=history | asking`)。读代码时对一块有疑问,直接在块下面问 Claude Code,答完存起来,下次打开还在,理解了就删。
- 入口:块右上角那排图标按钮里的「问 AI」(每种块都有:普通块、属性表块、库规则的卡片)。点一下在块下面展开问答区,再点(或在输入框里按 Esc)收起。文件不在任何工作区文件夹里时问答没地方存,不给这个按钮。
- 问答区紧跟在块下面、子块前面,和块同宽,左边一条 2px 的链接色竖线(`--vscode-textLink-foreground` 50%)。从上到下:
- 灰字的历史:这块以前问过的,每条一行问题(`descriptionForeground`,一行放不下截断)、右边时间(「刚刚」「5 分钟前」「3 小时前」「昨天」「3 天前」,一个月以前写日期,悬停看完整时间)和垃圾桶「已理解,删除」;下面是回答的前两行(去掉 Markdown 记号的纯文字,更淡)。点问题或这两行展开整段回答,照样灰字,按 Markdown 排(同注释的 Markdown 渲染),再点收起。
- 当前这一轮(展开以来问的,和历史之间一条细分隔线):问题照常颜色、加粗,回答照常颜色、按 Markdown 排,逐字长出来,末尾一个闪的光标;下面一行灰字写正在做的事(「读取 exceptions.py」「搜索 get_user」,还没开口时「正在想…」)。出错时这一问下面一个红框写原因(找不到 claude、退出码非零带它的原话、没登录时提示在终端里运行 claude 登录、超时);叫停的写「已停止,这次没存。」
- 输入框:没问过时提示「问这块代码… Enter 发送,Esc 收起」,问过了「继续问… Enter 发送,Esc 收起」;Enter 发送,Shift+Enter 换行,输入法选字时的 Enter 不算;Esc 收起(不清掉名字高亮);展开时光标就在输入框里。右边是发送按钮(没字时不能点),正在答时换成「停止」。
- 没展开、但问过的块:块下面只露一行灰字「问过 N 次」,读的时候一眼知道这里问过;点开看灰字历史(能展开、能删),没有输入框。
- 收起就结束这一轮:这一轮答完的都回到灰字历史里;还没答完的接着答,答完照样存。同一块同时只答一个问题(正在答时不能再发),不同块可以同时问,全插件同时最多 3 个,再多就在这一问下面说「同时最多问 3 个问题,等前面的答完再问」。
- 追问:这一轮里已经答成过,再问就接着那次的会话(`--resume <会话 id>`,会话 id 取 stream-json 的 init 消息),只发问题和「还是第几行那块」;会话找不到了(过期、被清理)就按第一次那样重新问。收起再展开是新的一轮,重新开会话、带上代码上下文。
- 删除:点垃圾桶马上删,不用确认;一块下面删光了这块就不挂了,一份文件删光了文件里就没有这份文件。
- 找不到原来位置的问答:记号(见下)对不上这份文件里任何一块的问答,放在预览顶上(筛选栏下面、代码块上面)一个虚线框里,标题「代码改动后找不到原来位置的问答 · N 条」(点标题收起 / 展开,默认展开),每组写「原来在:函数 get_user › 条件」(种类用翻译插件起的名字),下面同灰字历史,能看、能删。解析失败时一块都没有,不显示这一栏。
- 预览重新加载(标签页切走再切回来)后,扩展端还在答的问题接上(展开它那块、接着长),答完照样显示。同一份文件开在几处,问答、正在答的都一样。
怎么问(扩展端 `qa/`):
- 用 Claude Code 命令行:和插件别处的 `claude -p` 一样找 claude(`resolveClaudeBin`)、清环境变量(`ANTHROPIC_*`、嵌套会话标记)、带默认 profile(`getDefaultProfilePath`,读不到就不带 `--settings`),在这份文件所在的工作区文件夹里跑,它能自己读项目里别的文件。
- 只读,靠三层:`--tools Read,Grep,Glob`(可用的内置工具只剩读、搜,编辑、写文件、跑命令的工具根本没有),`--allowedTools Read,Grep,Glob`(这几个不用再批),`--permission-mode dontAsk`(别的一律拒、不等人批,盖过 profile 里的 bypassPermissions),外加 `--strict-mcp-config` 且不给 `--mcp-config`(不加载任何 MCP 服务,免得 MCP 工具能改东西)。不带 `--dangerously-skip-permissions`。
- 输出 `--output-format stream-json --verbose --include-partial-messages`:init 消息给会话 id;逐字的 `text_delta` 拼成回答;工具调用换成「读取 X」「搜索 Y」;新的一条回复(读完文件后开口)一开口就换掉前面那句(「我先看看 X」);答完以 result 行的最终回答为准,`is_error` 当失败。超时 5 分钟;「停止」给进程发 SIGTERM,这次不存。
- 第一次问的话:要求用中文、大白话、简洁,只读,提到文件写相对项目根目录的路径(带行号);带上文件的相对路径和语言、外面套着的各层块的开头(每层最多 6 行,带行号,例如类和它里面的函数)、这块代码(从块的第一行到它最后一个子孙的最后一行,带行号,超过 120 行只贴开头并说剩下的在哪几行)、问题。追问只发问题和「还是 路径 第 a–b 行那块代码,代码可能刚改过,需要时重新读文件」。这些都只用视图数据的行号、记号和树形,不认识任何语言。
块的位置记号(`BlockView.anchor`,`qa/anchor.ts`):
- 扩展端生成视图数据时按源码先后逐块给:有名字的块(`defines` 的来源:函数名、类名、枚举名、单个变量名,库规则卡片取规则给的定义范围)是外层各个有名字的块加自己的「种类 名字」,例如 `class ReferralRelation > function display`;没名字的块(条件、语句、返回、注释、导入、多变量声明……)是外层最近的有名字的块加「种类 @源码哈希」,哈希是这块编辑范围的源码去掉所有空白后的 SHA-1 前 8 位,例如 `function display > if @3fa1c2d0`。同一层里记号相同的(同名的两个函数、一模一样的两行语句)按源码先后在后面补 `[1]`、`[2]`。
- 所以:上面插几行、改缩进空白、改别的函数的名字,记号都不变;有名字的块改了主体、签名还认得;没名字的块自己的源码改了,记号就变了,旧问答成了「找不到原来位置的」。种类用视图数据的种类(库规则的卡片是规则的种类 id)。
- webview 只拿记号比相等,不拆开看;「原来在哪」由扩展端把记号拆回各层(`where: { kind, name? }[]`)发过去。
存法:
- 每个工作区文件夹一份 `<工作区文件夹>/.spx/code-qa.json`,和 `.spx/code-notes.json` 同一套写法(`pathMap.ts`、`folderStore.ts` 两者共用):`{ "相对路径": [{ "anchor": 记号, "entries": [{ "id", "question", "answer", "createdAt", "sessionId" }] }] }`,回答是 Markdown 原文,`createdAt` 是 ISO 时间;文件缺失或解析失败当作没有,认不出的块、问答跳过;写时按需建 `.spx`,两格缩进、文件按路径排、块按记号排、问答按时间排、末尾换行;存第一条前不生成文件,删光后留 `{}`。
- 在 VS Code 里改名、挪位置、删除文件 / 目录时问答跟着走(同文件注释);外部改了 `code-qa.json`(git pull、手改)文件监听重读,开着的预览跟着变。
## 通用契约
放在 `vscode/src/codeReader/contract.ts`,语法树类型用 `@vscode/tree-sitter-wasm` 自带的 web-tree-sitter 类型。
@@ -878,6 +913,7 @@ Python 内置类型(仅注解里):str 字符串、int 整数、float 浮
```ts
interface BlockViewBase {
id: string // 在树里的位置,例如 '6.1.0'
anchor: string // 块的位置记号(见「问 AI」):问答挂在它上面,webview 只比相等
kind: string // BlockKind(库规则认成的现成块也是,如 enum),或库规则卡片的种类 id(sqlalchemy.table)
startLine: number // 编辑范围的起止行,0 起:一般是 header,库规则认领的块是原块整条语句(盖住它留下的子块)
endLine: number
@@ -902,6 +938,9 @@ type BlockView = BlockViewBase & ({ translation: PreviewText, doc?: Markdown } |
- `{ type: 'definitionProbed', id, found }`:回答第 `id` 次 `probeDefinition`。
- `{ type: 'occurrencesFound', id, occurrences: { start, end, kind: 'text' | 'read' | 'write' }[] }`:回答第 `id` 次 `findOccurrences`,`[start, end)` 是源码下标,`kind` 同 `DocumentHighlightKind`;语言扩展没答就是空数组。
- `{ type: 'filter', excluded: Record<属性 id, 值 id[]> }`:整个工作区共用的块筛选。webview 发 `ready` 后先于第一份文档发一次(第一份文档按筛选画),之后任何一个预览改了都发给所有开着的预览(包括改的那个)。
- `{ type: 'qa', available, threads: { anchor, where: { kind, name? }[], entries: { id, question, answer, createdAt }[] }[], running: { askId, anchor, question, text, activity? }[] }`:这份文件的问答(见「问 AI」)。`ready` 后发一次,存的问答、正在答的问题变了再发给开着这份文件的所有预览;`available` 为假时文件不在工作区文件夹里,不给「问 AI」;`running` 让重新加载的页面接上还在答的问题。
- `{ type: 'answer', askId, text, activity? }`:`askId` 那个问题的回答长了(`text` 是到现在为止的全文)或换了在做的事。webview 攒着、最多每 80ms 画一次。
- `{ type: 'answerEnd', askId, status: 'done' | 'failed' | 'stopped', text?, error?, entryId?, sessionId? }`:答完了。`done` 带最终回答、存下的那条的 id(历史里不再重复显示它)和会话 id(追问接着它),`failed` 带给人看的原因,`stopped` 是被叫停、没存。
- webview → 扩展:
- `{ type: 'probeDefinition', id, version, offset }`:按住修饰键指到带 `offset` 的名字上时问有没有定义,`id` 每问一次加一;`version` 不是当前文档版本就不答。
- `{ type: 'goToDefinition', version, offset }`:修饰键 + 点击,按「跳到定义」一节跳。这两条都只带下标:扩展端按发出去的文档认出规则标了引用的下标交给规则查(见「规则自己查定义」),其余问语言扩展。
@@ -910,6 +949,9 @@ type BlockView = BlockViewBase & ({ translation: PreviewText, doc?: Markdown } |
- `{ type: 'edit', startLine, endLine, text }`:用 `WorkspaceEdit` 把这几行整行替换成 `text`(每行补回原来第一行的缩进)。
- `{ type: 'selectTranslator', id }`:按 `languageId` 记到 `globalState`,重发文档。
- `{ type: 'setFilter', excluded }`:改块筛选(整份替换);扩展端逐项核对后记进 `workspaceState`,再发 `filter` 给所有预览。webview 不在本地先改,只按扩展端发来的显示。
- `{ type: 'ask', askId, anchor, question, resume? }`:问 `anchor` 那块;`askId` 由 webview 起,`resume` 是追问时这一轮最近答成的会话 id。扩展端按最近一次发出去的块树和源码找这块、拼问题;找不到(代码刚改过)或问不了(这块正在答、同时答的太多)时直接回 `answerEnd` failed。
- `{ type: 'stopAsk', askId }`:叫停,结束 claude 进程,这次不存。
- `{ type: 'deleteQa', anchor, entryId }`:删掉一次问答。
- 块的编辑状态和用户的折叠选择在 webview 里按块 id 记(`vscode.setState`):`{ editing: string[], folds: Record<id, boolean>, scroll? }`。`folds` 只记用户点过的(id → 折叠着没有),属性行和带下一层的子行也记在里面:属性行 id 是 `<块 id>/p<行序号>`(例如 `6.1.0/p3`),子行 id 是上一层(行或子行)的 id 加 `/<子行序号>`,一层层往后接(例如 `6.1.0/p3/1` 是第 3 行的第 1 个子行,`6.1.0/p3/1/0` 是它下面的第 0 个);块 id 只有数字和点,只有行那一节带 `p`、其余各节是纯数字,所以块、行、各层子行的 id 互不相撞。子行 id 也是跳到定义时在页面上找要闪的那一项用的。没记的按默认:函数、类、枚举块和库规则的卡片折叠,其余块、属性行和子行展开。眼下折叠着哪些由「默认 + folds」算出来,界面、跳到定义的展开都只看这一份。跳到定义展开祖先块、属性行时也记进 `folds`(记成展开)。读回来的状态逐项核对,旧版本存的、形状不对的项当没存过。文档刷新后保留;正在编辑且有焦点的块,刷新时不覆盖输入框内容。
## 颜色
@@ -960,11 +1002,21 @@ vscode/src/codeReader/
explorer/fileTree.ts 侧栏文件树(FileTreeDecorator:给行添信息的接口)
explorer/copyText.ts 复制菜单的命令和复制出什么(纯函数)
explorer/copyCommands.ts 复制命令:写剪贴板、状态栏提示
notes/noteMap.ts 注释的键、保存 / 改名 / 删除时怎么改(纯函数)
notes/store.ts .spx/code-notes.json 读写
pathMap.ts 按文件 / 目录记东西的表(文件注释、问答共用):路径的键、改名 / 挪位置 / 删除时怎么改、.spx 里 JSON 的读写(不碰 VS Code)
folderStore.ts FolderStore:每个工作区文件夹一份表,内存里的一份、文件监听、跟着改名 / 删除走、读写排队
notes/noteMap.ts 保存一条注释(纯函数)
notes/store.ts .spx/code-notes.json 叫什么、每条怎么认
notes/noteItem.ts 有注释的行怎么显示:描述、悬停提示、contextValue(纯函数)
notes/fileNotes.ts FileNotes:内存里的注释、文件监听、跟着改名 / 删除走、给树画行
notes/fileNotes.ts FileNotes:注释的 FolderStore、给树画行
notes/commands.ts 「添加注释 / 编辑注释」输入框
qa/anchor.ts 块的位置记号(BlockAnchors)、拆回各层(不碰 VS Code)
qa/qaMap.ts .spx/code-qa.json 的形状、每条怎么认、记一次 / 删一次(纯函数)
qa/prompt.ts 问 AI 的话:找到那块、外层块的开头、带行号的代码、追问
qa/answerStream.ts stream-json 的每行 → 会话 id、逐字的回答、在做的事、最终回答 / 失败
qa/claudeAsk.ts 命令行参数(只读)、问一次(追问找不到会话时重新问)、失败给人看的话
qa/asks.ts QaAsks:正在答的问题,一块一个、最多 3 个、能叫停(不碰 VS Code)
qa/codeQa.ts CodeQa:问答的 FolderStore、开问、答完存、删,发给预览的问答
(cc/spawnClaude.ts 的 runClaudeLines:跑 claude、stdout 逐行交出、超时、叫停,问 AI 用它)
editor/previewEditor.ts CustomTextEditorProvider
editor/messages.ts 消息类型
editor/blockFilter.ts 块筛选:workspaceState 读写、通知所有预览
@@ -973,7 +1025,7 @@ vscode/src/codeReader/
editor/definitionAction.ts 查到的定义 → 怎么跳(纯函数)
editor/occurrences.ts 查名字的各处出现(vscode.executeDocumentHighlights)
index.ts registerCodeReader(context): CodeReaderApi
vscode/webview-ui/code-reader.html + src/codeReader/ 预览界面(React,Vite 第二个入口;块右上角的图标按钮在 BlockTools.tsx,属性表在 PropertyGrid.tsx,子行排成表格还是列表在 subRows.ts,着色的词和框在 SegmentText.tsx,Markdown 渲染在 markdown.ts / TextPreview.tsx,筛选栏在 FilterBar.tsx,筛选规则在 filter.ts,编辑时盖住哪些块、点「编辑」退出哪些块在 editing.ts,高亮各处出现的规则在 occurrences.ts、接到页面上在 useOccurrences.ts)
vscode/webview-ui/code-reader.html + src/codeReader/ 预览界面(React,Vite 第二个入口;块右上角的图标按钮在 BlockTools.tsx,属性表在 PropertyGrid.tsx,子行排成表格还是列表在 subRows.ts,着色的词和框在 SegmentText.tsx,Markdown 渲染在 markdown.ts / TextPreview.tsx,筛选栏在 FilterBar.tsx,筛选规则在 filter.ts,编辑时盖住哪些块、点「编辑」退出哪些块在 editing.ts,高亮各处出现的规则在 occurrences.ts、接到页面上在 useOccurrences.ts,问 AI 的问答区在 AskThread.tsx、状态和规则在 qa.ts、收发在 useQa.ts)
```
## 测试
@@ -1004,7 +1056,16 @@ vscode/webview-ui/code-reader.html + src/codeReader/ 预览界面(React,Vit
- 文件注释:键相对工作区文件夹、`/` 分隔,根和外面的路径没有键,手写的 `./`、结尾 `/` 规范掉;保存去空白、清空即删;文件改名、目录挪位置里面各层一起走、名字只是前缀相同的不动、跨工作区文件夹先取出再放进去;删目录连带里面的;读写:文件缺失 / 解析失败 / 不是对象返回空表、跳过不合法条目、建 `.spx`、键排序、删光留 `{}`;有注释 / 没注释的行的描述、悬停提示(Markdown 转义)、contextValue,和 `package.json` 里菜单的 when 子句对得上。
- 侧栏行上的菜单(`explorer/rowMenus.test.ts`,读 `package.json`):行尾按钮先「复制」子菜单再注释,有没有注释换「添加 / 编辑注释」,工作区根都没有;「复制」子菜单带 `$(copy)`,注释图标 `$(note)` / `$(edit)`;子菜单和右键菜单里文件四项、目录三项(有没有注释一样)、菜单上的字;复制命令都登记了、和代码里的命令表一致、命令面板里藏起来。
- 复制出什么(`explorer/copyText.test.ts`):文件、目录的名字 / 相对路径 / 绝对路径;不带后缀只去最后一个扩展名(`a.test.ts`、`archive.tar.gz`),`.env` 原样、`.env.local` → `.env`、没有扩展名的原样;相对路径总用 `/`,绝对路径照本机写法(Windows 反斜杠)。
- 块右上角的图标按钮(webview `BlockTools.test.tsx`,渲染成 HTML):一组一个按钮、没有字、`title` 和 `aria-label` 是「编辑」/「预览」、有图标、编辑时带按下标记。
- 块右上角的图标按钮(webview `BlockTools.test.tsx`,渲染成 HTML):一组一个按钮、没有字、`title` 和 `aria-label` 是「编辑」/「预览」、有图标、编辑时带按下标记;给了「问 AI」时它排在前面、只有图标、问答区展开时带按下标记和 `aria-expanded`。
- 问 AI:
- 位置记号(`qa/anchor.test.ts`,真实的 Python 解析):有名字 / 没名字的块的记号、同一层重复的编号、两个函数里一样的条件互不影响;上面插几行、改空白记号不变;改别的函数的名字只动它和它里面的块;没名字的块自己的源码改了记号变、有名字的块改签名不变;拆回各层。
- 存法(`qa/qaMap.test.ts`):记一次、删一次、删光不挂 / 不记;文件缺失、解析失败、不是对象当没有;认不出的块和问答跳过、没会话 id 的照认;手改的顺序、重复的块读进来整理好;写出来排好序、两格缩进、读得回来、删光留 `{}`;改名、挪目录、删除跟着走。
- 问的话(`qa/prompt.test.ts`):完整问题逐字(要求、文件、外层块的开头、带行号的代码、问题),顶层块没有外层、函数连主体一起贴,很长的块只贴开头、外层块每层最多几行,找不到这块没有,追问的话。
- 命令行(`qa/claudeAsk.test.ts`,假的 claude):只读的参数、没有跳过权限、追问带 `--resume`;问一次的过程和结果、在工作区文件夹里跑、带超时和叫停;追问接着会话、会话找不到时按完整问题重问、别的失败不重试;失败给人看的话。
- stream-json(`qa/answerStream.test.ts`,照本机 claude 2.1 的输出截的):会话 id、逐字的回答、读文件、新回复换掉前面那句、没开逐字片段时一样、一条回复里两段空一行、result 没带回答时用眼下的、失败的原因、坏行和子代理的消息不算。
- 正在答的问题(`qa/asks.test.ts`):进展、答完、叫停(结束进程、报 stopped、这块能再问)、出错、同一块只答一个、最多几个、全部叫停。
- 跑 claude(`cc/spawnClaude.test.ts`,假的 claude 脚本):逐行交出、最后没换行的那段、参数和 cwd、退出码非零带原话、叫停马上结束进程、超时。
- 页面(`qa.test.ts`、`AskThread.test.tsx`):问一次、答完记下存成的那条、追问接着哪个会话、收起结束这一轮、重新加载接上还在答的、停止和失败、不认识的消息不理;找不到原来位置的问答、原来在哪;问的时间、回答的前两行;问答区没问过不画、没展开只露「问过 N 次」、展开的历史 / 当前这一轮 / 输入框、出错和停止、找不到原来位置的那一栏。
## 收尾