Fold Diffs
Unofficial OpenCode V2 fork of opencode-fold-diffs: write, edit and apply_patch blocks render folded to their header line, click to expand.
0
501
近 7 天 125
36.5
生态多维模型
8 天前
2026-09-27
快速安装与配置
opencode.json写入当前项目的 opencode.json,只对这个仓库生效。
opencode.json
{
"$schema": "https://opencode.ai/config.json",
"plugin": ["@cardinal4/opencode-fold-diffs@0.3.2"]
}写入 ~/.config/opencode/opencode.json,对所有项目生效。
~/.config/opencode/opencode.json
{
"$schema": "https://opencode.ai/config.json",
"plugin": ["@cardinal4/opencode-fold-diffs@0.3.2"]
}若你要在本地改造这个插件,先装到项目里再从本地路径引用。
shell
pnpm add -D @cardinal4/opencode-fold-diffsOpenCode 启动时会通过内嵌运行时自动加载 npm 依赖并缓存至本地目录,无需手动在全局环境执行安装。
[!IMPORTANT] This is a fork, and it exists for one reason: OpenCode V2 support.
The original plugin is Tanner Bruhn's
tannerbruhn/opencode-fold-diffs(npm:opencode-fold-diffs) — the OpenCode V1 plugin, whose V1 implementation does not run in V2. This fork —cardin/opencode-fold-diffs, published as@cardinal4/opencode-fold-diffs— ports it to OpenCode V2 (the@opencode/cli2.x line,opencode v2.0.x). Install this fork if you are on V2; install upstream if you are on V1.All credit for the original plugin goes to Tanner Bruhn. This fork is unofficial and independent: Tanner has not reviewed it, does not maintain it, and does not endorse it. Report bugs in this fork here, not upstream.
Every write and edit dumps the whole file or the whole diff into your transcript, and stays there. This plugin folds those blocks down to their header line — # Wrote 40 lines · click to expand src/app.ts — and opens them again on click, or with a fold/unfold-all key if you configure one.
The V2 port is not a re-host: it follows V2's new plugin API and render tree, so the V1 code cannot be reused. See Migrating from V1 for what changed.
What V2 already folds
V2 tightened the transcript on its own, and this plugin deliberately leaves those parts alone:
read/glob/grepcalls fold into a one-line exploration group (Explored — 3 reads, 2 searches), click to expand.- A bash command is trimmed to two lines and its output to ten, both click to expand.
The tools V2 still renders in full are exactly the ones that fill the scrollback: write, edit, apply_patch. Upstream has been asked three times — #9089 (minimal diff display), #14511 (a toggle keybind, like Claude Code's ctrl+o), #19074 (collapse tool output) — and all three were closed without a setting. This is that setting, from outside.
What you see
Before:
← Edit src/session/index.ts
│ 1 import { createMemo } from "solid-js"
│ 2 - const [expanded, setExpanded] = createSignal(false)
│ 3 + const [expanded, setExpanded] = createSignal(props.open)
│ … 40 more lines
After:
← Edit +12 −3 · click to expand src/session/index.ts
Click the row to open it. If you set the key option, that binding folds or unfolds every block in the session and sets what newly arriving blocks do — same as a verbose toggle. By default there is no binding, so OpenCode's own shortcuts (including ctrl+o) are left alone.
Install
opencode plugin add @cardinal4/opencode-fold-diffs
Then list it in your CLI config so the terminal loads the TUI entrypoint. Add it to ~/.config/opencode/cli.json (or $XDG_CONFIG_HOME/opencode/cli.json):
{
"$schema": "https://opencode.ai/v2/cli.json",
"plugins": ["@cardinal4/opencode-fold-diffs"]
}
This package is TUI-only (it has no server entrypoint), so configure it in cli.json. CLI-only plugins also stay active when the terminal connects to a remote server.
With options:
{
"plugins": [
{
"package": "@cardinal4/opencode-fold-diffs",
"options": {
"lines": 3,
"min_lines": 10,
"key": "ctrl+f"
}
}
]
}
Restart OpenCode afterwards; plugins load at startup.
From a local checkout
Clone this repository and point the plugin entry at the checkout. The package ships a server entry (index.js) and a TUI entry (tui.js); OpenCode discovers a plugin directory by its server entry and loads the tui entry beside it, so the plugin shows up by id instead of as an anonymous entry.
{
"plugins": ["/home/me/projects_l/opencode-fold-diffs"]
}
Options
"plugins": [["@cardinal4/opencode-fold-diffs", { "lines": 3, "min_lines": 10, "key": "ctrl+f" }]]
| Option | Default | Meaning |
|---|---|---|
lines |
0 |
Lines of the body left showing when folded. 0 is the header only, and also tightens the block's padding so it occupies one row. Any positive number leaves a peek at the top. |
min_lines |
6 |
Blocks with fewer changed lines than this are left alone — a two-line edit is already its own summary. |
stats |
true |
Append +12 −3 · click to expand to the header. |
folded |
true |
Whether blocks start folded. false gives you only the toggle. |
key |
"" |
Optional binding for fold/unfold-all. Empty by default so no OpenCode shortcut is overridden; set e.g. "ctrl+shift+d" to opt in. |
bash |
false |
Fold long bash commands too. Off by default on V2 because the host already trims them to two lines. |
bash_lines |
1 |
Rows of the command left showing when folded. 1 keeps the line that says what the thing was. |
dump |
"" |
Where Fold diffs: diagnose writes its tree dump. "" means /tmp/opencode-fold-diffs-tree.txt; false disables the dump. |
ctrl+obelongs to OpenCode's Open recent sessions and projects (open.menu), so this plugin does not bind it. Choose an unused key for thekeyoption.
What it does not touch
- Permission dialogs. The diff you approve renders in full, always. The plugin only ever walks inside the transcript scrollbox, and the permission preview is not in it.
- Diagnostics and errors. Only the children carrying a diff or a file body get folded, so an edit that introduced a type error still says so with the block closed.
- Bash output, and the click that expands it. Only the command is ever folded, and only when
bashis enabled; the output keeps the host's ten-line collapse. The block's own click handler belongs to the host, so the plugin attaches to the command text instead and callsstopPropagation(): clicking the command folds the command, clicking anywhere else in the block does exactly what it did before. - Todos, questions, and the generic fallback. They keep the host's own collapse behaviour.
How it works, for anyone extending it
The V2 TUI plugin API (@opencode/plugin/tui) has no slot for message parts, so a plugin cannot render a tool block itself. It has to reach the renderables the host already made:
- Find the transcript. The only scrollbox in the tree with
stickyScroll && stickyStart === "bottom". The sidebar, dialogs, autocomplete and diff viewer all have scrollboxes; none of them are sticky. - Find the blocks. A V2
BlockToolrenders its header first as a row box whose first two children are the label text (# Wrote,← Edit,← Patched,# Created,# Deleted) and the path value. File blocks are matched on that label; a bash block carries no header, so it is found by shape — a child whose first grandchild is the$-prefixed command. - Fold. Set
visible = falseon the children carrying the bulk. That sets Yogadisplay: none, so the body leaves layout instead of leaving a hole. A zeromax-heightalone is not enough — the box collapses to zero rows but OpenTUI still paints the diff. A positivelinesvalue keeps the first body visible and usesmaxHeightfor it instead. The block's own chrome (gap,paddingTop,paddingBottomof 1) is tightened to zero so a folded block occupies one row. - Toggle. Assign
onMouseUpon the block. The solid adapter sets that as a plain property, so a plugin can set it the same way — it replacesBlockTool's own handler, which is why the copy-on-select guard is reimplemented here. For a bash block the host's handler is the output toggle and must be preserved, so the handler goes on the command text withstopPropagation()instead. - Restate the header. The stats suffix is appended to the label text node, not the path. If solid will not let go of that node the plugin stops trying and folds without the suffix.
Renderable classes are matched by duck-typing (typeof node.diff === "string", content + filetype for code) rather than instanceof, because the opentui classes are minified in the shipped binary and their names are not stable.
Blocks are re-scanned on message.part.updated and message.updated, plus a 2 s sweep that catches a session opened from history, whose parts land before any event this plugin sees.
Migrating from V1
V1 plugin implementations do not run in V2. The V2 port made these changes:
- Entrypoint is a
{ id, setup(context) }definition (Plugin.define()is an identity helper, so the shape is the same).setupreturns the cleanup function instead ofapi.lifecycle.onDispose. api.renderer→context.renderer;api.route.current.name→context.ui.router.current().type;api.event.on→context.data.on;api.ui.toast→context.ui.toast.show;api.keymap.registerLayer→context.keymap.layer.- Tool headers are no longer one string. V1 rendered
"← Edit src/app.ts"; V2 renders a label node plus a path node, so detection and the stats suffix target the label. bashdefaults tofalse, because V2 now trims long commands to two lines itself.- Config moves from
tui.jsontocli.json(oropencode.json(c)).
Testing
- Restart so the plugin loads:
opencode service restart, then relaunch the TUI. - Confirm it loaded:
/pluginsshould listopencode-fold-diffsby id, andCtrl+P→ Fold / unfold file diffs should be in the palette. - Ask the agent for a small edit. The block should render as a single header row,
← Edit +2 −1 · click to expand path. Click it to open, click again to close. - If you configured a
key, press it to fold or unfold every block in the session. Otherwise use the palette command Fold / unfold file diffs. - If nothing folds, run
Ctrl+P→ Fold diffs: diagnose. The toast reports what the plugin can see:
| Result | Meaning |
|---|---|
transcript: not found |
No session view is open, so there is nothing to fold. |
transcript: yes · blocks: 0 |
The transcript was found but no file blocks matched — the host render tree differs from what this plugin expects. |
transcript: yes · blocks: N · folded: N |
Detection and folding ran; if the blocks still look expanded, the fold did not take on the renderables. |
stats: off |
Folding works but the header could not be restated (Solid owns the label node). |
Status
Written against opencode v2.0.10 / v2.0.11. The tree-walking, block matching, fold/unfold and toggle logic run green against a mock renderer tree shaped like V2's (node --test), and the fold was checked against a real transcript: visible = false (Yoga display: none) is what actually hides a body, not maxHeight.
Edits with fewer than min_lines changed lines (default 6) are left expanded on purpose — a one-line change is already its own summary. Set min_lines: 0 to fold every file block.
License
MIT
同类生态推荐
Fold Diffs
opencode-fold-diffs
OpenCode TUI plugin: write, edit, apply_patch and long bash commands render folded to their title line, click or ctrl+o to open them.
Snap To Bottom
opencode-snap-to-bottom
Terminal-style snap-to-bottom for the opencode transcript
Review
@opencode-cockpit/review
A pull request in the terminal: read the diff, hold your notes, send one review