@cardinal4/opencode-fold-diffs 0.3.4 → 0.3.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +20 -41
- package/package.json +1 -1
- package/tui.js +15 -9
package/README.md
CHANGED
|
@@ -1,24 +1,11 @@
|
|
|
1
1
|
# opencode-fold-diffs
|
|
2
2
|
|
|
3
|
-
> [!
|
|
4
|
-
> **This
|
|
5
|
-
>
|
|
6
|
-
> The original plugin is **[Tanner Bruhn](https://github.com/tannerbruhn)**'s [`tannerbruhn/opencode-fold-diffs`](https://github.com/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`](https://github.com/cardin/opencode-fold-diffs), published as [`@cardinal4/opencode-fold-diffs`](https://www.npmjs.com/package/@cardinal4/opencode-fold-diffs) — ports it to **OpenCode V2** (the `@opencode/cli` 2.x line, `opencode v2.0.x`). Install this fork if you are on V2; install upstream if you are on V1.
|
|
7
|
-
>
|
|
8
|
-
> **All credit for the original plugin goes to [Tanner Bruhn](https://github.com/tannerbruhn).** 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.
|
|
3
|
+
> [!NOTE]
|
|
4
|
+
> **Unofficial OpenCode V2 port of [Tanner Bruhn](https://github.com/tannerbruhn)'s [`opencode-fold-diffs`](https://github.com/tannerbruhn/opencode-fold-diffs) (npm: `opencode-fold-diffs`) — the OpenCode V1 plugin.** This fork — [`cardin/opencode-fold-diffs`](https://github.com/cardin/opencode-fold-diffs), published as [`@cardinal4/opencode-fold-diffs`](https://www.npmjs.com/package/@cardinal4/opencode-fold-diffs) — ports it to **OpenCode V2** (`@opencode/cli` 2.x, `opencode v2.0.x`). Install this fork on V2, upstream on V1. Tanner has not reviewed or endorsed it; report bugs here, not upstream.
|
|
9
5
|
|
|
10
|
-
**Every `write` and `
|
|
6
|
+
**Every `write`, `edit` and `apply_patch` 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.
|
|
11
7
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
## What V2 already folds
|
|
15
|
-
|
|
16
|
-
V2 tightened the transcript on its own, and this plugin deliberately leaves those parts alone:
|
|
17
|
-
|
|
18
|
-
- `read` / `glob` / `grep` calls fold into a one-line exploration group (`Explored — 3 reads, 2 searches`), click to expand.
|
|
19
|
-
- A bash **command** is trimmed to two lines and its **output** to ten, both click to expand.
|
|
20
|
-
|
|
21
|
-
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](https://github.com/anomalyco/opencode/issues/9089) (minimal diff display), [#14511](https://github.com/anomalyco/opencode/issues/14511) (a toggle keybind, like Claude Code's `ctrl+o`), [#19074](https://github.com/anomalyco/opencode/issues/19074) (collapse tool output) — and all three were closed without a setting. This is that setting, from outside.
|
|
8
|
+
V2 already collapses the tools it considers noisy: `read` / `glob` / `grep` fold into a one-line exploration group (`Explored: 3 reads, 2 searches`), and a bash **command** trims to two lines with its **output** to ten, all click-to-expand. The three tools that actually fill the scrollback — `write`, `edit`, `apply_patch` — are the ones it leaves alone. Upstream has been asked three times — [#9089](https://github.com/anomalyco/opencode/issues/9089) (minimal diff display), [#14511](https://github.com/anomalyco/opencode/issues/14511) (a toggle keybind, like Claude Code's `ctrl+o`), [#19074](https://github.com/anomalyco/opencode/issues/19074) (collapse tool output) — and all three were closed without a setting. This is that setting, from outside.
|
|
22
9
|
|
|
23
10
|
## What you see
|
|
24
11
|
|
|
@@ -55,7 +42,7 @@ Then list it in your CLI config so the terminal loads the TUI entrypoint. Add it
|
|
|
55
42
|
}
|
|
56
43
|
```
|
|
57
44
|
|
|
58
|
-
This
|
|
45
|
+
This plugin's behavior is TUI-only (its server entry is a no-op that exists so OpenCode can discover it by id), so configure it in `cli.json`. TUI plugins also stay active when the terminal connects to a remote server.
|
|
59
46
|
|
|
60
47
|
With options:
|
|
61
48
|
|
|
@@ -89,7 +76,7 @@ Clone this repository and point the plugin entry at the checkout. The package sh
|
|
|
89
76
|
## Options
|
|
90
77
|
|
|
91
78
|
```json
|
|
92
|
-
"plugins": [
|
|
79
|
+
"plugins": [{ "package": "@cardinal4/opencode-fold-diffs", "options": { "lines": 3, "min_lines": 10, "key": "ctrl+f" } }]
|
|
93
80
|
```
|
|
94
81
|
|
|
95
82
|
| Option | Default | Meaning |
|
|
@@ -116,35 +103,21 @@ Clone this repository and point the plugin entry at the checkout. The package sh
|
|
|
116
103
|
|
|
117
104
|
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:
|
|
118
105
|
|
|
119
|
-
1. **Find the transcript.** The
|
|
106
|
+
1. **Find the transcript.** The scrollbox with `stickyScroll && stickyStart === "bottom"`. The sidebar, dialogs, autocomplete and diff viewer all have scrollboxes; none of them are sticky. The one other sticky scrollbox in V2 is the shell output viewer, which is a dialog and is skipped by its `shell-output-scroll` id.
|
|
120
107
|
2. **Find the blocks.** A V2 `BlockTool` renders 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.
|
|
121
108
|
3. **Fold.** Set `visible = false` on the children carrying the bulk. That sets Yoga `display: none`, so the body leaves layout instead of leaving a hole. A zero `max-height` alone is not enough — the box collapses to zero rows but OpenTUI still paints the diff. A positive `lines` value keeps the first body visible and uses `maxHeight` for it instead. The block's own chrome (`gap`, `paddingTop`, `paddingBottom` of 1) is tightened to zero so a folded block occupies one row.
|
|
122
109
|
4. **Toggle.** Assign `onMouseUp` on the block. The solid adapter sets that as a plain property, so a plugin can set it the same way — it replaces `BlockTool`'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 with `stopPropagation()` instead.
|
|
123
110
|
5. **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.
|
|
124
111
|
|
|
125
|
-
Renderable classes are matched by duck-typing (`typeof node.diff === "string"
|
|
112
|
+
Renderable classes are matched by duck-typing (`typeof node.diff === "string"` for a diff, `typeof node.content === "string"` for a written file) rather than `instanceof`, because the opentui classes are minified in the shipped binary and their names are not stable. `filetype` is deliberately not required: it was dropped in 2.0.16 and restored in 2.0.27, and keying off it silently stopped write blocks from folding.
|
|
126
113
|
|
|
127
114
|
Blocks are re-scanned on V2's `session.message.content.updated`, `session.tool.success` and `session.tool.failed`, plus `session.step.ended` and `session.execution.succeeded` (`session.shell.ended` joins them when `bash` folding is on). A 2 s sweep catches a session opened from history, whose parts land before any event this plugin sees.
|
|
128
115
|
|
|
129
|
-
##
|
|
130
|
-
|
|
131
|
-
V1 plugin implementations do not run in V2. The V2 port made these changes:
|
|
132
|
-
|
|
133
|
-
- Entrypoint is a `{ id, setup(context) }` definition (`Plugin.define()` is an identity helper, so the shape is the same). `setup` returns the cleanup function instead of `api.lifecycle.onDispose`.
|
|
134
|
-
- `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`.
|
|
135
|
-
- `context.keymap.layer` is called directly from `setup` on 2.0.23+; earlier V2 builds read Solid context there, so the plugin still registers from a component mounted in the `app` slot for them.
|
|
136
|
-
- Event names are V2's: `message.updated` / `message.part.updated` are legacy schemas in V2 and are never emitted. The plugin listens to `session.message.content.updated`, `session.tool.success` / `session.tool.failed`, `session.step.ended` and `session.execution.succeeded` (plus `session.shell.ended` when bash folding is on). These names exist from 2.0.10 on, so older V2 installs get the same fix without upgrading.
|
|
137
|
-
- 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.
|
|
138
|
-
- `bash` defaults to `false`, because V2 now trims long commands to two lines itself.
|
|
139
|
-
- Config moves from `tui.json` to `cli.json` (or `opencode.json(c)`).
|
|
116
|
+
## Status
|
|
140
117
|
|
|
141
|
-
|
|
118
|
+
Written against **opencode v2.0.24** and re-checked against **v2.0.27**; the `session.*` events it listens to exist from v2.0.10 on, so an older V2 install gets the fix without upgrading. 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`.
|
|
142
119
|
|
|
143
|
-
|
|
144
|
-
2. Confirm it loaded: `/plugins` should list `opencode-fold-diffs` by id, and `Ctrl+P` → **Fold / unfold file diffs** should be in the palette.
|
|
145
|
-
3. 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.
|
|
146
|
-
4. 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**.
|
|
147
|
-
5. If nothing folds, run `Ctrl+P` → **Fold diffs: diagnose**. The toast reports what the plugin can see:
|
|
120
|
+
To confirm it loaded, restart (`opencode service restart`) and check `/plugins` lists `opencode-fold-diffs` and `Ctrl+P` → **Fold / unfold file diffs** is in the palette. If nothing folds, run **Fold diffs: diagnose** — the toast reports what the plugin can see:
|
|
148
121
|
|
|
149
122
|
| Result | Meaning |
|
|
150
123
|
|---|---|
|
|
@@ -153,11 +126,17 @@ V1 plugin implementations do not run in V2. The V2 port made these changes:
|
|
|
153
126
|
| `transcript: yes · blocks: N · folded: N` | Detection and folding ran; if the blocks still look expanded, the fold did not take on the renderables. |
|
|
154
127
|
| `stats: off` | Folding works but the header could not be restated (Solid owns the label node). |
|
|
155
128
|
|
|
156
|
-
|
|
129
|
+
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.
|
|
157
130
|
|
|
158
|
-
|
|
131
|
+
## V2 port notes
|
|
159
132
|
|
|
160
|
-
|
|
133
|
+
The port follows V2's new plugin API and render tree, so the V1 code could not be reused:
|
|
134
|
+
|
|
135
|
+
- Entrypoint is a `{ id, setup(context) }` definition; `setup` returns the cleanup function instead of `api.lifecycle.onDispose`.
|
|
136
|
+
- `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` (called straight from `setup` on 2.0.23+, from an `app`-slot component before that).
|
|
137
|
+
- Event names are V2's `session.*` events; `message.updated` / `message.part.updated` are legacy schemas that never fire.
|
|
138
|
+
- Tool headers are a label node plus a path node, so detection and the stats suffix target the label.
|
|
139
|
+
- `bash` defaults to `false` because V2 trims long commands itself, and config moves from `tui.json` to `cli.json`.
|
|
161
140
|
|
|
162
141
|
## License
|
|
163
142
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cardinal4/opencode-fold-diffs",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.5",
|
|
4
4
|
"description": "Unofficial OpenCode V2 fork of opencode-fold-diffs: write, edit and apply_patch blocks render folded to their header line, click to expand.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
package/tui.js
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
// expand src/app.ts" -- and opens on click, or with the optional `key`
|
|
14
14
|
// binding for every block at once.
|
|
15
15
|
//
|
|
16
|
-
// Options (cli.json ->
|
|
16
|
+
// Options (cli.json -> { "package": "@cardinal4/opencode-fold-diffs", "options": { ... } }):
|
|
17
17
|
// lines lines of the body left visible when folded (default 0, title only)
|
|
18
18
|
// min_lines leave blocks with fewer content lines alone (default 6)
|
|
19
19
|
// stats append "40 lines · click to expand" to the header (default true)
|
|
@@ -51,8 +51,9 @@ const DEFAULTS = {
|
|
|
51
51
|
|
|
52
52
|
// Header labels of the file-writing tools, as V2 renders them. Unlike V1, V2
|
|
53
53
|
// splits a block header into a label text node ("# Wrote", "← Edit", ...) and a
|
|
54
|
-
// separate path node, so these match the label alone. The
|
|
55
|
-
//
|
|
54
|
+
// separate path node, so these match the label alone. The stats suffix a folded
|
|
55
|
+
// header carries is stripped by baseLabel() before matching, so re-adopting a
|
|
56
|
+
// block on a later scan is idempotent.
|
|
56
57
|
const LABELS = ["# Wrote", "← Edit", "← Patched", "# Created", "# Deleted", "# Moved"];
|
|
57
58
|
|
|
58
59
|
function isLabel(text) {
|
|
@@ -91,9 +92,9 @@ function plain(node) {
|
|
|
91
92
|
// @opentui/core and are minified, so their names are not stable. A diff
|
|
92
93
|
// renderable is the only thing in the tree carrying a `diff` string. A code
|
|
93
94
|
// renderable is the only thing whose `content` is a string -- text renderables
|
|
94
|
-
// carry a styled-text `content` object and expose `plainText` instead.
|
|
95
|
-
//
|
|
96
|
-
//
|
|
95
|
+
// carry a styled-text `content` object and expose `plainText` instead. Never key
|
|
96
|
+
// off `filetype`: OpenCode stopped setting it in 2.0.16 and restored it in
|
|
97
|
+
// 2.0.27, and requiring it silently stopped every write block from folding.
|
|
97
98
|
function isDiff(node) {
|
|
98
99
|
return typeof node?.diff === "string";
|
|
99
100
|
}
|
|
@@ -139,15 +140,20 @@ function bulk(node, found = []) {
|
|
|
139
140
|
return found;
|
|
140
141
|
}
|
|
141
142
|
|
|
142
|
-
// The transcript is the
|
|
143
|
-
//
|
|
143
|
+
// The session transcript is the scrollbox that sticks to the bottom. Since V2
|
|
144
|
+
// the shell output viewer is also a sticky scrollbox, but it is a dialog and
|
|
145
|
+
// must never be mistaken for the transcript, so it is excluded by id. Staying
|
|
146
|
+
// inside the transcript is what keeps the permission dialog's diff preview
|
|
144
147
|
// untouched -- you should always see in full what you are about to approve.
|
|
148
|
+
const SHELL_VIEWER_ID = "shell-output-scroll";
|
|
149
|
+
|
|
145
150
|
function isTranscript(node) {
|
|
146
151
|
return (
|
|
147
152
|
typeof node?.scrollTo === "function" &&
|
|
148
153
|
typeof node?.scrollHeight === "number" &&
|
|
149
154
|
node.stickyScroll === true &&
|
|
150
|
-
node.stickyStart === "bottom"
|
|
155
|
+
node.stickyStart === "bottom" &&
|
|
156
|
+
node.id !== SHELL_VIEWER_ID
|
|
151
157
|
);
|
|
152
158
|
}
|
|
153
159
|
|