diff
将 diff 面板作为插件使用:/diff 会把会话尚未提交的更改按文件和 hunk 显示在 transcript 旁,Claude 编辑和运行命令时实时刷新;首次编辑会在终端足够宽时打开面板,文件的询问按钮会在下一条提示中带上它的 hunks。
关于这个 mod
diff
把 diff 面板作为插件使用:/diff 会在 transcript 旁打开会话尚未提交的更改,每个变更文件一行,下面列出该文件的所有 hunks;也可以再次关闭。每次切换都会在 transcript 中留下 Diff panel shown 或 Diff panel hidden。标题、文件列表和切换按钮保持不动;滚轮在 hunks 上移动时每次移动三行,在超过八行的文件列表上移动时每次移动一个文件(插件自己处理面板的 ui.scroll);点击一行会把该文件的 hunks 移到顶部。文件列表也会响应内置列表的按键(ctrl+up/ctrl+down、opt+up/opt+down),ctrl+x b 会像内置快捷键一样向前移动比较基线:两者都通过声明引擎自身操作的 Buttons 完成。面板会在 Claude 编辑并运行 shell 命令时刷新;打开期间还会轮询仓库的 HEAD,因此在其他地方完成的提交或 checkout 也会显示出来。
主循环第一次成功编辑会话时会自行打开面板,就像内置面板在第一次 checkpoint 时打开一样:布局会根据每个绘图的 viewport 所说明的全屏布局,把它停靠在 transcript 旁;终端足够宽(用户从未选择过时为 144 列,之前保持打开时为 110 列;用户主动关闭过则尊重该选择),并且文件 checkpointing 已开启。子代理的编辑不会打开面板;如果界面没有说明,面板也不会自行打开。停靠的面板会先 fetch 再打开,就像内置面板会预热数据一样,因此不会停在 Loading diff…;如果引擎留下了一个正在等待但尚未绘制的打开请求,会撤回该请求,后续 resize 也不会把它放回去,下一次编辑会重新询问。恢复或继续的会话如果 transcript 已经包含这样的编辑,在宽度确定后也会按相同条件打开面板,就像内置面板会根据恢复的历史打开一样。
在全屏布局下,宽度小于 110 列的终端会显示内置面板要求加宽的那一行,不会打开任何内容。没有该布局(CLAUDE_CODE_NO_FLICKER=0,/diff 会从命令的 presentation 中读取这一点)时,面板会在任意宽度内嵌打开,获得焦点,并按照内容高度(打开请求的 rows)显示,形状仿照内置对话框:标题、数量、围绕选中项一次显示五个文件行(❯ 表示焦点环开始的位置;插件遵循焦点环通过 ui.focus 的移动,并像内置界面一样重新居中这些行)、按键提示;Enter 只显示该文件的 hunks,Escape 退回列表后关闭,并留下 Diff dialog dismissed;面板打开时会暂存 toast。文件的询问按钮会让该文件进入待处理状态:它的 hunks 会作为上下文搭上下一条提示,而且只搭一次。
面板将工作树与 HEAD 比较,默认以会话开始时的状态切分(引擎在 $.session.usage() 中提供该起点,因此恢复的会话保留最初的起点,而 /clear 会重新开始);也可以直接与 HEAD 比较,或与默认分支的 merge-base 比较。标题下方的基线行会在基线不是会话基线时标出它,选择会按仓库保存在插件的 store 中。选择器可以显示某个更早回合的编辑,而不是工作树;这些编辑从会话消息中读取。会话开始前已经发生变化的文件(依据时间戳,在面板第一次读取仓库时已处于 dirty 状态的路径中筛选),以及噪声文件(lockfile、生成文件和测试文件)会分开列出,并折叠到用户主动查看为止;重命名会按照 git 的打印方式列出。在 git 仓库之外,/diff 会说明这一点,不做其他处理。
内置面板会运行 git 的时机,这里也相同:会话开始时不运行;当 /diff 或第一次有空间打开面板的编辑首次需要仓库时,在会话启动目录中运行一次 git rev-parse(“不是仓库”的结果也会保留,直到 /clear 或 /resume 忘掉它);工作树只会在面板打开后、一次已落地的编辑或运行过 shell 命令之后,通过一次 fetch 读取。内置面板没有对应物的唯一读取,是面板第一次 fetch 时的 git status;它用来代替内置界面按移动文件的时间标记的变更时间。
hooks/register.ts 是模块;hooks/ 下的全部内容都是它的组成部分。
接入的事件
| 事件 | 钩子做什么 |
| --- | --- |
| session.start | 绑定引擎一次并注册 /diff(如果另一个 /diff 已在列表中,会让插件保持闲置);不询问仓库,等 /diff 或第一次编辑到来时再固定仓库;在它的分派之外读取 transcript,对已经编辑过回合的恢复会话,像第一次编辑一样打开面板。 |
| ui.render of PromptHint | 读取终端宽度以及布局是否会停靠面板;这两项决定第一次编辑是否打开面板。 |
| ui.render of Pane | 绘制面板:停靠时显示标题、基线行、源选择器、文件列表和切换按钮,并覆盖 hunks 的窗口;内嵌时绘制对话框。 |
| command.run of diff | 在仓库尚未固定时固定仓库,打开或关闭面板(获得焦点;没有全屏布局时按 Escape 关闭),说明发生了哪一种,并记住选择。 |
| ui.close of the pane | 从对话框的详细视图退回,而不是关闭面板;否则记住用户通过 /diff 关闭面板的选择。 |
| ui.scroll of the pane | 停靠时让 hunks 在固定的标题和列表下方移动(滚轮每次三行,页面按键每次一页);滚轮位于列表上时移动列表,并保持引擎窗口不动。 |
| ui.focus in the pane | 在对话框列表中选中焦点环落到的文件,以它为中心重新排列五行,并让焦点环落在该行现在的位置。 |
| command.run of clear, resume | 关闭面板并忘掉会话状态,也一并忘掉已固定的仓库。 |
| tool.call of Edit, Write, NotebookEdit | 在编辑已落地后(没有被拒绝,也没有失败)刷新打开的面板;主循环的第一次此类编辑会在终端有空间且 checkpointing 已开启时打开面板,并在此时固定仓库。 |
| tool.call of Bash, PowerShell | 在命令未被拒绝之后刷新打开的面板;失败和中断的命令也包括在内。 |
| prompt.submit | 将已待处理文件的 hunks 加入提示上下文,然后取消待处理状态。 |
它对 $ 的调用
clock.after、clock.every、clock.now、command.register、env.get
(CLAUDE_CODE_DISABLE_FILE_CHECKPOINTING)、fs.list、fs.read、fs.stat、
process.run(只读的 git)、session.id、session.messages、
settings.read、store.get、store.set、telemetry.log、telemetry.mark、
ui.close、ui.invalidate、ui.log、ui.open、ui.resolve、ui.status。
$.telemetry 是 telemetry 插件的名词;如果它不存在,相关行会被丢弃,其他内容不会改变。
试用
claude --plugin-dir /path/to/diff
然后在有修改文件的 git 仓库中执行 /diff。
安装
请先查看作者 README 确认 marketplace 和插件名称;命令可能随仓库结构改变。
claude plugin marketplace add anthropics/claude-code claude plugin install diff
原文 / README
diff
The diff pane as a plugin: /diff opens the session's uncommitted changes
beside the transcript, one row per changed file and every file's hunks
beneath, and closes it again; each toggle leaves Diff panel shown or
Diff panel hidden in the transcript. The header, the file list and its
toggles stay put while the wheel moves the hunks under them three rows a
tick, or the list a file a tick while the wheel is over a list longer than
its eight rows (the plugin answers the pane's ui.scroll itself); a row's
click puts that file's hunks at the top; the list also scrolls under the
built-in's list keys (ctrl+up/ctrl+down, opt+up/opt+down), and
ctrl+x b moves the comparison base on, as the built-in's chord does: both
through Buttons that declare the engine's own actions. The pane refreshes
as Claude edits and runs shell commands, and while it is open it polls
the repository's HEAD so a commit or checkout made elsewhere shows too.
The main loop's first successful edit of a session opens the pane by
itself, as the built-in panel opens on its first checkpoint: where the
layout docks it beside the transcript (the fullscreen layout, which each
drawing's viewport says), the terminal is wide enough (144 columns when
the person never chose, 110 when they kept it open before; a person who
closed it is left alone) and file checkpointing is on; a subagent's edit
opens nothing, and where the surface does not say, nothing opens by
itself. A docked pane fetches before it opens, as the built-in panel
primes its data, so it never lands on Loading diff…; an open the engine
leaves waiting undrawn is withdrawn, so no later resize seats it, and the
next edit asks again. A session resumed or continued whose transcript
already holds such an edit opens the pane on the same terms as soon as the
width is known, as the built-in opens on the history it restores.
Under the fullscreen layout a terminal under 110 columns gets the
built-in's line asking for a wider one and nothing opens. Without that
layout (CLAUDE_CODE_NO_FLICKER=0, which /diff learns from the command's
presentation) the pane opens inline at any width, focused and as tall
as its content (the open's rows), in the built-in dialog's shape: the
title, the count, five file rows at a time round the selected one (❯,
where the focus ring starts; the plugin follows the ring's walk through
ui.focus and re-centres the rows as the built-in does), the key hints;
Enter shows that file's hunks alone,
Escape backs out to the list and then closes, leaving Diff dialog dismissed; toasts are held while it is up. A file's ask button arms that
file: its hunks ride the next prompt as context, once.
The pane compares the working tree against HEAD, split at the session's
start (the default; the start the engine gives in $.session.usage(), so a
resumed session keeps its first and /clear begins anew), against HEAD
plainly, or against the merge-base with
the default branch; the base line under the header names a base other than
the session's, and the choice is kept per repository in the plugin's store.
A picker shows one earlier turn's edits instead of the working tree, read
from the session's messages. Files that changed before the session started
(by their timestamp, among the paths already dirty when the pane first
read the repository), and noise (lockfiles, generated and test files), are
listed apart and folded until asked for; a rename lists as git prints it.
Outside a git repository /diff says so and does nothing else.
Git runs when the built-in panel's would: nothing at the session's start;
one git rev-parse, in the directory the session started in, when /diff
or the first edit a pane has room to open on first needs the repository
(an answer of no repository is kept too, until /clear or /resume
forgets it); and the working tree is read only by a fetch for a pane that
is open, after an edit that landed or a shell command that ran. The one
read the built-in has no counterpart for is a git status at a pane's
first fetch, which stands in for the change time the built-in dates a
moved file by.
hooks/register.ts is the module; everything under hooks/ is its parts.
What it hooks
| event | what the hook does |
| --- | --- |
| session.start | Binds the engine once and registers /diff (a session where another /diff is listed leaves the plugin idle); asks nothing of the repository, which /diff or the first edit pins when it comes; off its dispatch, reads the transcript, and for a resumed session whose turns edited opens the pane as the first edit would. |
| ui.render of PromptHint | Reads the terminal's width and whether its layout docks a pane, which decide whether the first edit opens the pane. |
| ui.render of Pane | Draws the pane: docked, the header, base line, source picker, file list and toggles over the window of hunks; inline, the dialog. |
| command.run of diff | Pins the repository when none is, opens or closes the pane (focused and closing on Escape without the fullscreen layout), says which, and remembers the choice. |
| ui.close of the pane | Backs out of the dialog's detail view instead of closing; else remembers the person's close as /diff's. |
| ui.scroll of the pane | Docked, moves the hunks under the pinned header and list (three rows a wheel tick, a page a page key), or the list when the wheel is over it, and keeps the engine's window still. |
| ui.focus in the pane | In the dialog's list, selects the file the ring lands on, re-centres the five rows on it, and lands the ring where that row now sits. |
| command.run of clear, resume | Closes the pane and forgets the session's state, the pinned repository with it. |
| tool.call of Edit, Write, NotebookEdit | After an edit that landed (not refused, not failed), refreshes an open pane; the main loop's first such edit opens it, pinning the repository then if the terminal has the room and checkpointing is on. |
| tool.call of Bash, PowerShell | After a command that was not refused, failed and interrupted ones too, refreshes an open pane. |
| prompt.submit | Adds the armed file's hunks to the prompt's context and disarms. |
What it calls on $
clock.after, clock.every, clock.now, command.register, env.get
(CLAUDE_CODE_DISABLE_FILE_CHECKPOINTING), fs.list, fs.read, fs.stat,
process.run (git, read-only), session.id, session.messages,
settings.read, store.get, store.set, telemetry.log, telemetry.mark,
ui.close, ui.invalidate, ui.log, ui.open, ui.resolve, ui.status.
$.telemetry is the telemetry plugin's noun; where it is absent the rows
are dropped and nothing else changes.
Try it
claude --plugin-dir /path/to/diff
then /diff inside a git repository with a modified file.
