# diff 与工具调用可视化 · 看见 agent 在干什么

> agent 改一个文件、跑一条命令，你在 scrollback 里看到的那块彩色 diff、那个带计时的工具卡片，是怎么从 ACP 流式事件画出来的。这一章看 grok 的 diff 引擎、工具块类型、渐进式语法高亮，以及流式工具调用如何变成滚动区里的块。

- 作者：David（道雾轩）
- 专栏：Grok Build 源码解读（https://daiw.org/manual/grok-build.md）
- 最后更新：2026-09-15
- 原文：https://daiw.org/manual/grok-build/diff-visualization
- 转载与引用：请注明出处并附原文链接（https://daiw.org/about/copyright）

# diff 与工具调用可视化 · 看见 agent 在干什么

一个编码 agent 最重要的可视化，就是「它到底改了什么」。这一章看 grok 怎么把[工具执行](https://daiw.org/manual/grok-build/tool-registry)画成你屏幕上那块彩色 diff 和工具卡片。

## diff 引擎：拆成独立 crate

算 diff 的纯逻辑已从 pager 里拆出来，成了独立的小 crate `xai-grok-pager-diff`(原先的 `diff.rs` 基本原样搬过去)，构建 diff 不再牵动整个 pager。它基于 `similar::TextDiff`，把改动算成 `DiffHunk`(一串 `DiffLine`，各带旧/新行号 + `ChangeTag`)。`extract_edit_hunks` 从一个 ACP 工具调用里抽 hunk 有**三种策略**：① 从结构化的 `SearchReplace` 输出；② 从 ACP 的 `Diff.meta`；③ 从 `old_text`/`new_text` 直接对比(**执行前的预览**)。同一文件的多次编辑可由 `stitch_overlapping_hunks` 缝成连续 diff——行号对不上时宁可保留分开的 hunk，也不画出错的内容。

## 工具块：一个和类型对齐的 sum type

scrollback 里每个工具调用是一个 `ToolCallBlock`——`Execute`/`Read`/`Edit`/`Search`/`Skill`/… 共 13 个变体，大多包一个专属块结构(包括专门展示 `send_subagent_message` 的 `SentMessage`)。流式路径上，`tool_call_to_block` 按 ACP 的 `tool_call.kind` 分派到对应的块；另有 `from_name` 按工具名映射(`bash`→Execute、`read_file`→Read、`apply_patch`→Edit……)，供通用构造器用。

一个体贴的细节是 **`VerbGroupKind` 的时态感知**：连续的同类工具行会折叠成一行标题，比如「Read 3 files」「Searched 4 patterns」——组里还有在跑的就用进行时(`Reading`、`Searching`)，**全部完成后**用过去式(`Read`、`Searched`)，名词还按数量变复数。小事，但让界面读起来像人话。

## 渐进式语法高亮

编辑块的高亮(`edit.rs`)有个漂亮的渐进设计 `EditHighlightPhase`:

1. **`HunkOnly`**：第一次绘制，**只按 hunk 用 syntect 高亮**(快，立即出画面)；
2. **`Pending` → `FileScoped`**：后台 worker 算出**全文件作用域**的高亮再回填——这样跨越多行的语法结构(一个字符串、一段注释)也能正确着色(删除行仍按 hunk 高亮；主题切换后对不上，就退回 hunk-only)。

超过 `EDIT_HL_MAX_BYTES`(2 MiB)/ 5 万行就停在 hunk-only，不硬扛。hunk 之间用「… N unchanged lines」占位；默认只显示新文件一列行号，也可以打开 GitHub 式的旧+新双列行号。

<Callout type="info">
  「先出个够用的、再后台升级成更好的」这个模式——先 hunk 高亮秒出、再全文件高亮回填——在交互式软件里很常见：**先反馈，后精修**。它让「大文件 diff 卡一下」变成「立刻有、随后更准」。
</Callout>

## 流式：ACP 事件 → 滚动区块

最动态的部分是 `acp/tracker.rs` 的 `AcpUpdateTracker`——把 ACP 的 `SessionUpdate` 事件**转成 scrollback 变更**。工具调用一到：若还在跑，就压一个**运行中**的块并记进 `pending_tools`(到达时已完成就直接定稿);`ToolCallUpdate` 把状态和输出**增量**灌进那个块(命令输出做增量 UTF-8 解码)；`Completed`/`Failed` 时 `tool_call_to_block` 定稿成完整块，编辑块随后排上全文件高亮任务。编辑块还能**合并**(`try_coalesce_edit`)：同一文件相邻、都已成功完成的多次编辑折叠成一块，并把全文件高亮任务重指到存活的那块——不过这是个灰度开关，默认关闭，要在 `[ui]` 里打开 `collapsed_edit_blocks`。

```mermaid
flowchart LR
  ACP["ACP SessionUpdate"] --> TR[AcpUpdateTracker]
  TR -->|ToolCall 运行中| RB[压入运行中块 + pending_tools]
  TR -->|ToolCallUpdate| INC[增量灌输出到块]
  TR -->|Completed/Failed| FIN[tool_call_to_block 定稿 + 抽 diff]
  FIN --> SB[Scrollback]
```

## 和《从 LLM 到 Coding Agent》对照

教学最小实现里，工具结果就是 `print(result)`。grok 的增量是把「agent 干活」做成**实时、可读、可折叠**的可视化：三策略抽 diff、时态感知的动词、渐进式高亮、流式块增量、编辑合并。这是「看得见 agent 在想什么、改什么」——交互式编码 agent 的体验核心。下一章看这一切怎么适配五花八门的终端。

---

上一篇：[输入、slash 与补全](https://daiw.org/manual/grok-build/input-and-completions) · 下一篇：[主题与终端适配](https://daiw.org/manual/grok-build/theming-terminal)
