@cardinal4/opencode-fold-diffs 0.3.6 → 0.3.7

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 +9 -7
  2. package/package.json +1 -1
  3. package/tui.js +19 -2
package/README.md CHANGED
@@ -1,7 +1,7 @@
1
1
  # opencode-fold-diffs
2
2
 
3
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.
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, tested with `opencode 2.2.0`). Install this fork on V2, upstream on V1. Tanner has not reviewed or endorsed it; report bugs here, not upstream.
5
5
 
6
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.
7
7
 
@@ -33,7 +33,9 @@ Click the row to open it. If you set the `key` option, that binding folds or unf
33
33
  opencode plugin add @cardinal4/opencode-fold-diffs
34
34
  ```
35
35
 
36
- 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`):
36
+ That installs the package and adds it to the global `opencode.json`. The package ships a server entry (`index.js`, a no-op that gives the plugin an id) and a TUI entry (`tui.js`); the TUI part loads automatically with the server entry, so no second configuration is needed.
37
+
38
+ If the terminal connects to a **remote server**, list the package in `~/.config/opencode/cli.json` (or `$XDG_CONFIG_HOME/opencode/cli.json`) instead so the local TUI loads the TUI part. Do not list it in both files:
37
39
 
38
40
  ```json
39
41
  {
@@ -42,9 +44,7 @@ Then list it in your CLI config so the terminal loads the TUI entrypoint. Add it
42
44
  }
43
45
  ```
44
46
 
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.
46
-
47
- With options:
47
+ Options go on the plugin entry in either file:
48
48
 
49
49
  ```json
50
50
  {
@@ -115,7 +115,7 @@ Blocks are re-scanned on V2's `session.message.content.updated`, `session.tool.s
115
115
 
116
116
  ## Status
117
117
 
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`.
118
+ Written against **opencode v2.0.24**, re-checked against **v2.0.27** and **v2.2.0**; 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`.
119
119
 
120
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:
121
121
 
@@ -126,6 +126,8 @@ To confirm it loaded, restart (`opencode service restart`) and check `/plugins`
126
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. |
127
127
  | `stats: off` | Folding works but the header could not be restated (Solid owns the label node). |
128
128
 
129
+ The diagnose dump includes up to 80 characters of text from each renderable, so it can contain prompts or tool output. Redact it before sharing; set `dump: false` to skip writing it.
130
+
129
131
  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.
130
132
 
131
133
  ## V2 port notes
@@ -136,7 +138,7 @@ The port follows V2's new plugin API and render tree, so the V1 code could not b
136
138
  - `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
139
  - Event names are V2's `session.*` events; `message.updated` / `message.part.updated` are legacy schemas that never fire.
138
140
  - 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`.
141
+ - `bash` defaults to `false` because V2 trims long commands itself, and config moves from `tui.json` to `opencode.json` (or `cli.json` for a remote TUI).
140
142
 
141
143
  ## License
142
144
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cardinal4/opencode-fold-diffs",
3
- "version": "0.3.6",
3
+ "version": "0.3.7",
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
@@ -221,7 +221,8 @@ function summarise(nodes) {
221
221
  }
222
222
  continue;
223
223
  }
224
- lines += node.content.split("\n").length;
224
+ const content = node.content.endsWith("\n") ? node.content.slice(0, -1) : node.content;
225
+ lines += content === "" ? 0 : content.split("\n").length;
225
226
  }
226
227
  if (diffs) return { size: added + removed, label: `+${added} −${removed}` };
227
228
  return { size: lines, label: `${lines} ${lines === 1 ? "line" : "lines"}` };
@@ -271,6 +272,10 @@ export default {
271
272
  // Folded blocks, by their block renderable. WeakMap so a session switch,
272
273
  // which destroys the renderables, drops the state with them.
273
274
  const known = new WeakMap();
275
+ // WeakMap has no iterator, so keep a strong set of adopted blocks only for
276
+ // cleanup. Destroyed blocks are pruned on the sweep to avoid retaining a
277
+ // closed session's renderables.
278
+ const adopted = new Set();
274
279
  // The mode new blocks adopt. ctrl+o flips it, so "expand everything" also
275
280
  // means "and stop folding what arrives next", the way a verbose toggle works.
276
281
  let folding = opts.folded !== false;
@@ -356,6 +361,7 @@ export default {
356
361
  folded: false,
357
362
  };
358
363
  known.set(block, state);
364
+ adopted.add(block);
359
365
 
360
366
  block.onMouseUp = () => {
361
367
  // Copy-on-select is a drag ending on the block; that is not a click.
@@ -382,6 +388,7 @@ export default {
382
388
  folded: false,
383
389
  };
384
390
  known.set(block, state);
391
+ adopted.add(block);
385
392
 
386
393
  // The block's own onMouseUp belongs to the host here -- it is what expands
387
394
  // the collapsed command/output -- and opentui declares the handler as a
@@ -404,6 +411,7 @@ export default {
404
411
  }
405
412
 
406
413
  function sweep() {
414
+ for (const block of adopted) if (block.isDestroyed) adopted.delete(block);
407
415
  if (context.ui.router.current().type !== "session") return;
408
416
  const box = transcript();
409
417
  if (!box) return;
@@ -515,8 +523,10 @@ export default {
515
523
  if (target) {
516
524
  const lines = [];
517
525
  dumpNode(box ?? context.renderer.root, 0, lines, 600);
518
- dump = target;
519
526
  writeFileSync(target, summary.concat("", lines).join("\n"));
527
+ // Only advertise the path once the write succeeded; the
528
+ // catch below keeps a failed write from taking down diagnose.
529
+ dump = target;
520
530
  }
521
531
  } catch {}
522
532
  context.ui.toast.show({
@@ -550,6 +560,13 @@ export default {
550
560
  clearInterval(timer);
551
561
  if (pending) clearTimeout(pending);
552
562
  for (const off of offs) if (typeof off === "function") off();
563
+ // The host owns these renderables and keeps them after the plugin is
564
+ // disabled, so undo our folds instead of leaving the transcript collapsed.
565
+ for (const block of adopted) {
566
+ const state = known.get(block);
567
+ if (state?.folded) apply(state, false);
568
+ }
569
+ adopted.clear();
553
570
  };
554
571
  },
555
572
  };