Turn changed-files card
本轮改动文件卡片
After a turn, users want to see which files the model changed and by how much. The Web turn tail listed only the paths of successful `write`, `edit`, and `str_replace_editor` calls, without line counts, and missed every file a shell command changed; the per-call diff cards in the message flow answered the question one call at a time.
English
Problem
After a turn, users want to see which files the model changed and by how much. The Web turn tail listed only the paths of successful write, edit, and str_replace_editor calls, without line counts, and missed every file a shell command changed; the per-call diff cards in the message flow answered the question one call at a time.
Decision
The Host workspace-changes plugin summarizes each top-level turn's changed files, announces the summary with a workspace/changes Session event that carries only the turn number, and serves the summary through its workspaceChanges service until the Session is disposed; the deliverables plugin reads the served summary and renders it as the changed-files card in place of the mutation-call row. The event is log-only and never model-visible.
The recorder snapshots the working tree with git at turn start and turn end: add --all into a private index seeded from the repository's index, then write-tree. Both write into a temporary object directory owned by the Session while the repository's object store is attached as a read-only alternate, so the user's repository gains no objects; keeping every captured byte outside the workspace follows the workspace change journal POC (#2973) and costs nothing measurable because the stat cache lives in the index. The two tree ids are diffed with diff-tree -r -M --numstat, so the summary contains exactly the turn's changes — the user's earlier uncommitted work, staged or not, is part of the baseline — and commits the model makes mid-turn cannot hide changes. The repository's own index, objects, work tree, and refs are never modified. On a 10k-file repository one snapshot costs about 60 ms and the diff about 10 ms; the baseline runs concurrently with the first model request, and tool execution waits for it.
Git is the default executable on PATH; no environment plugin is consulted. Outside any repository, or without git, no snapshot is taken and the summary lists the file-tool edits alone, with the working directory as the workspace, so the card still appears but misses shell edits. Nested repositories and submodules are gitlinks and are not descended into.
Changes outside snapshot coverage come from whole-file captures: before a write, edit, or mutating str_replace_editor call runs, the recorder copies the named file into the Session's temporary directory, once per path per turn, and copies it again at turn end; ignored files, files outside the work tree, and every file-tool edit without a snapshot are listed from a line comparison of the two copies (comparison decision). Files under the temporary directories are omitted unless they lie inside the working directory; a file left in /tmp needs present to reach the user. Shell edits outside coverage are a known limitation.
The list sorts by a display path in code-unit order: the path relative to the working directory, ../ for repository files above it, ~ under the home directory, otherwise absolute; parent and absolute paths therefore lead without a separate group. The card shows the total count with summed added and deleted lines in its header, three rows before a fold, and a collapse control at the bottom once expanded. Each row opens the turn's review tab in the right Sidebar on that file and the header opens it on the first file (review decision); with a Host desktop the review tab offers the default-application open.
The recorder appends inside the turn on agent/turn-stopping and again after turn/end only when tool results settled after the last record, so aborted, failed, and steered turns are covered. The Client keeps the latest announcement per turn and reads its summary once through the authenticated summary route.
The log deliberately carries nothing but the turn number. Summaries, snapshot trees, and captured copies live only as long as the Session in the Host process: the summaries in the recorder, the objects and copies in a temporary directory removed on disposal. A conversation reopened after a Host restart has no card for its earlier turns. The product decision is that a card whose content the Host can no longer open should not appear at all, so the card's lifetime equals the content's lifetime rather than the log's. Content is not kept across restarts (comparison decision).
Alternatives considered
Extending the mutation-call row with hunk counts kept a second, weaker implementation beside the per-call diff cards and still missed shell edits; git sees every write regardless of the tool.
Diffing against HEAD at turn end needs one command but attributes the user's uncommitted work to the turn.
A git tag or stash create per turn leaves refs in the user's repository or omits untracked files; a tree written through a private index does neither.
A pure-JavaScript git or a bundled binary for hosts without git adds megabytes and a platform matrix for users who mostly run without the card; until git exists the card lists file-tool edits only.
Summing the hunks the file tools persist for uncovered files was the first implementation; it counted a repeatedly edited line more than once and could not show a whole-file comparison, so the recorder now copies the whole file at first touch instead (comparison decision).
Recording the file list and counts in the event was the first implementation: the card would then render from the log forever, while the content it opens would not survive. It was replaced by the announcement-only event so that the card and its content share one lifetime.
A snapshot object store under the Harness home with a byte bound was the first implementation's placement: one store per repository, shared by every Session, discarded when it outgrew the bound. It survived Host restarts that the summaries no longer do, needed two configuration fields, and made a Session's snapshot fail when another Session discarded the store; a per-Session temporary directory removes all three.
An environment-provider seam for locating git was raised by the team but not settled; the plugin uses PATH and keeps its lookup in one place.
A shadow repository for working directories outside any repository — a git directory under the Harness home with the work tree pointing at the working directory — would add shell edits to those users' card without adding a .git, but a shadow repository has no .gitignore, and a configured exclude list cannot reliably keep build outputs, caches, and dependency trees out of every project layout. It is deferred until that exclude policy is settled; the recorder already treats the repository as an input, so adding the tier changes only where the snapshot goes.
Consequences
Every turn with tool results costs two snapshots and one diff on the Host, and writes blob and tree objects for the changed files into the Session's temporary directory, which disposal removes. Edits the user makes during a turn are attributed to it. Every file-tool edit also copies its whole file into that directory once per turn.
The Web bundle alone mounts the recorder, so headless, SDK, and ACP logs are unchanged; recorded Web scenarios gain the event and the card whenever a turn changes a file; one dedicated scenario seeds a git repository so the card also carries a shell edit, while the others list their file-tool writes alone. The card replaces the Chinese and English "Files changed" row; prose file mentions still resolve against mutation-call paths and deliveries.
Focused tests cover repositories with real git, directories outside any repository, coverage classification of captured paths, an interrupted turn overlapped by the next, ordering, caps, disposal, the macOS stub, the changed-file route, the card's fold and gesture states, and a Loader composition. The keyless Web scenario replays the recorder end to end, including a created file the repository ignores.
中文
Problem
一轮结束后,用户想看到模型改了哪些文件、改了多少。Web 的轮尾只列出成功的 write、edit 与 str_replace_editor 调用的路径,没有行数,也漏掉所有 shell 命令改动的文件;消息流中逐调用的 diff 卡片只能一次回答一个调用。
Decision
Host 侧的 workspace-changes 插件汇总每个顶层轮次改动的文件,用只带轮号的 workspace/changes Session 事件宣告,并通过 workspaceChanges 服务提供摘要直到 Session 释放;产出物插件读取提供的摘要并渲染为改动文件卡片,取代修改调用行。该事件仅写日志,模型永远看不到。
记录器在轮次开始和结束时用 git 对工作树做快照:以仓库 index 为种子在私有 index 上执行 add --all,再执行 write-tree。两者都写入 Session 自己拥有的临时对象目录,仓库自己的对象库以只读 alternate 挂接,因此用户仓库不会多出任何对象;把捕获的每个字节都放在工作区外学自工作区变更日志 POC(#2973),而且因为 stat 缓存在 index 里,它对耗时没有可测量的影响。两个 tree id 用 diff-tree -r -M --numstat 比较,因此摘要恰好只包含本轮的改动——用户此前未提交的工作,无论是否暂存,都属于基线——模型在轮中做的提交也藏不住改动。仓库自己的 index、对象、工作树和 ref 从不被修改。在一万个文件的仓库上,一次快照约 60 ms,比较约 10 ms;基线与首次模型请求并行,工具执行会等待它。
git 使用 PATH 上的默认可执行文件,不询问任何环境插件。不在任何仓库内或没有 git 时,不做快照,摘要只列文件工具的编辑,并以工作目录作为工作区,因此卡片仍会出现,但缺少 shell 的改动。嵌套仓库和 submodule 是 gitlink,不会深入。
快照覆盖之外的改动来自整文件捕获:在 write、edit 或有修改作用的 str_replace_editor 调用运行之前,记录器把所指文件复制到 Session 的临时目录,每轮每个路径一次,轮次结束时再复制一次;被忽略的文件、工作树之外的文件,以及没有快照时的每一次文件工具编辑,都由两份副本的逐行对比列出(对比决定)。临时目录下的文件被省略,除非它们位于工作目录内;留在 /tmp 里的文件需要 present 才能到达用户。覆盖之外的 shell 编辑是已知限制。
列表按展示路径的码元顺序排序:相对工作目录的路径,仓库内位于其上的文件为 ../,家目录下为 ~,其余为绝对路径;上级路径与绝对路径因此自然排在最前,不需要单独分组。卡片标题显示总数与增删行数合计,折叠前显示三行,展开后底部有收起控件。每一行在右侧 Sidebar 打开本轮的 review tab 并选中该文件,标题则在第一个文件上打开它(review 决定);Host 有桌面时,review tab 提供用默认应用打开。
记录器在 agent/turn-stopping 时于轮内追加事件,并且仅当最后一次记录之后仍有工具结果结束时在 turn/end 后再次追加,因此中止、失败和被转向的轮次都被覆盖。Client 保留每轮最新的宣告,并通过经过认证的摘要路由读取一次摘要。
日志有意只携带轮号。摘要、快照树与捕获的副本只在 Host 进程内随 Session 存活:摘要在记录器里,对象和副本在释放时删除的临时目录里。Host 重启后重新打开的对话,先前轮次没有卡片。产品上的决定是,Host 已经打不开内容的卡片根本不该出现,所以卡片的寿命等于内容的寿命,而不是日志的寿命。内容不跨重启保留(对比决定)。
Alternatives considered
给修改调用行补上 hunk 计数会在逐调用 diff 卡片旁保留第二个更弱的实现,且仍然漏掉 shell 编辑;git 不管工具是什么都能看到每次写入。
轮末直接与 HEAD 比较只需一条命令,但会把用户未提交的工作算到本轮。
**每轮打一个 git tag 或 stash create**会在用户仓库里留下 ref,或者遗漏未跟踪文件;通过私有 index 写出的树两者都不会发生。
为没有 git 的主机引入纯 JavaScript 的 git 或捆绑二进制会为大多数不使用卡片的用户增加数 MB 体积和一套平台矩阵;在 git 出现之前卡片只列文件工具的编辑。
累加文件工具持久化的 hunk 是未覆盖文件的第一版实现;它会把反复编辑的行计算多次,也给不出整文件对比,因此记录器改为在首次触碰时复制整个文件(对比决定)。
把文件列表和行数记进事件是第一版实现:卡片从此可以永远从日志渲染,而它打开的内容却活不了那么久。它被只做宣告的事件取代,让卡片和内容共用一个寿命。
Harness home 下带字节上限的快照对象库是第一版实现的放置方式:每个仓库一个、所有 Session 共用、超过上限就丢弃。它能活过 Host 重启,而摘要已经不再需要这一点;它还需要两个配置字段,并且一个 Session 丢弃对象库会让另一个 Session 的快照失败。每个 Session 一个临时目录把这三点一起去掉。
用于定位 git 的环境提供者接缝由团队提出但尚未定案;插件使用 PATH 并把查找收敛在一处。
为不在任何仓库内的工作目录建影子仓库,即把 git 目录放在 Harness home 下、工作树指向工作目录,可以让这些用户在不多出 .git 的情况下也看到 shell 改动,但影子仓库没有 .gitignore,一份配置的排除列表无法在所有项目布局里可靠地挡住构建产物、缓存和依赖目录。它暂缓到排除策略定下来为止;记录器已经把仓库当作输入,加这一档只改变快照写到哪里。
Consequences
每个有工具结果的轮次在 Host 上花费两次快照和一次比较,并把改动文件的 blob 与 tree 对象写入 Session 的临时目录,释放时一并删除。用户在轮次进行中的编辑会被算到该轮。每次文件工具编辑还会把整个文件复制进该目录,每轮一次。
只有 Web bundle 挂载记录器,因此 headless、SDK 与 ACP 日志不变;录制的 Web 场景只要某一轮改动了文件就会新增该事件与卡片;一个专门的场景种入 git 仓库,让卡片也带上 shell 的改动,其余场景只列它们的文件工具写入。卡片取代了中英文的“本轮文件改动”行;正文文件提及仍按修改调用路径与交付解析。
聚焦测试用真实 git 覆盖仓库、不在任何仓库内的目录、对已捕获路径的覆盖范围分类、被下一轮压上的中断轮次、排序、上限、释放、macOS 桩程序、改动文件路由、卡片的折叠与手势状态,以及一次 Loader 组合。无密钥的 Web 场景端到端回放记录器,其中包括一个被仓库忽略的新建文件。