@cardinal4/opencode-fold-diffs 0.3.4 → 0.3.6

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.
Files changed (3) hide show
  1. package/README.md +20 -41
  2. package/package.json +1 -1
  3. package/tui.js +15 -9
package/README.md CHANGED
@@ -1,24 +1,11 @@
1
1
  # opencode-fold-diffs
2
2
 
3
- > [!IMPORTANT]
4
- > **This is a fork, and it exists for one reason: OpenCode V2 support.**
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 `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.
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
- 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](#migrating-from-v1) for what changed.
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 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.
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": [["@cardinal4/opencode-fold-diffs", { "lines": 3, "min_lines": 10, "key": "ctrl+f" }]]
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 only scrollbox in the tree with `stickyScroll && stickyStart === "bottom"`. The sidebar, dialogs, autocomplete and diff viewer all have scrollboxes; none of them are sticky.
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"`, `content` + `filetype` for code) rather than `instanceof`, because the opentui classes are minified in the shipped binary and their names are not stable.
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
- ## Migrating from V1
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
- ## Testing
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
- 1. Restart so the plugin loads: `opencode service restart`, then relaunch the TUI.
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
- ## Status
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
- Written against **opencode v2.0.24**, and compatible across the V2 line: the `session.*` events this version 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`.
131
+ ## V2 port notes
159
132
 
160
- 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.
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.4",
3
+ "version": "0.3.6",
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 -> ["opencode-fold-diffs", { ... }] or opencode.json(c)):
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 trailing alternation
55
- // keeps a header that already carries the stats suffix matching on later scans.
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. OpenCode
95
- // 2.0.16 stopped setting `filetype` on a written file's body, so it must not be
96
- // required (requiring it silently stopped every write block from folding).
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 only scrollbox in the TUI that asks to stick to the
143
- // bottom. Staying inside it is what keeps the permission dialog's diff preview
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