@zhuxixi/pi-agent-board 0.5.0 → 0.5.2

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 (31) hide show
  1. package/PROGRESS.md +18 -3
  2. package/README.md +298 -76
  3. package/VERIFY.md +3 -3
  4. package/docs/PTY_ATTACH_IMPLEMENTATION_PLAN.md +3 -3
  5. package/docs/superpowers/plans/2026-08-30-circular-navigation.md +308 -0
  6. package/docs/superpowers/plans/2026-08-30-pty-attach-quality-debt.md +311 -0
  7. package/docs/superpowers/plans/2026-08-30-readme-v2.md +294 -0
  8. package/docs/superpowers/plans/2026-09-01-attach-detach-gate-cursor-anchor.md +284 -0
  9. package/docs/superpowers/plans/2026-09-02-attach-detach-editor-state.md +722 -0
  10. package/docs/superpowers/plans/2026-09-03-detach-gate-glyph-fallback.md +146 -0
  11. package/docs/superpowers/specs/2026-08-21-attach-coldstart-jiggle-rearm-design.md +4 -0
  12. package/docs/superpowers/specs/2026-08-22-jiggle-shrink-and-hold-design.md +4 -0
  13. package/docs/superpowers/specs/2026-08-30-circular-navigation-design.md +47 -0
  14. package/docs/superpowers/specs/2026-08-30-pty-attach-quality-debt-design.md +105 -0
  15. package/docs/superpowers/specs/2026-08-30-readme-v2-design.md +115 -0
  16. package/docs/superpowers/specs/2026-09-01-attach-detach-gate-cursor-anchor-design.md +78 -0
  17. package/docs/superpowers/specs/2026-09-02-attach-detach-editor-state-design.md +120 -0
  18. package/docs/superpowers/specs/2026-09-03-detach-gate-glyph-fallback-design.md +99 -0
  19. package/package.json +1 -1
  20. package/runner/pty-runner.mjs +64 -17
  21. package/src/core/code-refs-store.mjs +3 -0
  22. package/src/core/editor-state-reporter.mjs +102 -0
  23. package/src/core/launch.mjs +6 -0
  24. package/src/core/pty-attach-jiggle-controller.mjs +71 -17
  25. package/src/core/pty-input.mjs +32 -0
  26. package/src/core/pty-scroll.mjs +4 -3
  27. package/src/core/repo.mjs +3 -0
  28. package/src/core/worktree.mjs +1 -0
  29. package/src/index.ts +12 -1
  30. package/src/ui/dashboard.ts +6 -2
  31. package/src/ui/pty-attach.ts +148 -32
@@ -0,0 +1,308 @@
1
+ # Circular List Navigation Implementation Plan
2
+
3
+ > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
4
+
5
+ **Goal:** Make dashboard list arrow-key navigation wrap around (last ⇄ first) instead of clamping at boundaries.
6
+
7
+ **Architecture:** Change index clamping to modulo wrap in `moveSelection()` and `peekStep()` in `src/ui/dashboard.ts`. Scroll-follow needs no change (`windowBody()` keeps the selected row visible regardless of direction). Behavior is covered by a new child-process test following the `ui-smoke` pattern.
8
+
9
+ **Tech Stack:** Node 24 (`--experimental-transform-types`), node:test, TypeScript (parameter properties, no typecheck on test-support).
10
+
11
+ **Spec:** `docs/superpowers/specs/2026-08-30-circular-navigation-design.md`
12
+
13
+ ## Global Constraints
14
+
15
+ - Work only in the worktree: `/home/elling/git-repo/github/pi-agent-board/.pi/worktrees/issue-52-circular-navigation` (all paths below are relative to it).
16
+ - `npm test` = `node --test test/*.test.mjs`; `npm run typecheck` = `tsc --noEmit`. Both must stay green.
17
+ - `src/ui/dashboard.ts` uses TS parameter properties → it can only be imported from a child process spawned with `--experimental-transform-types` (the `test-support/ui-smoke.ts` pattern). Never import it directly from `test/*.test.mjs`.
18
+ - `tsconfig.json` excludes `test/` and `test-support/` — no type casts needed in the probe script.
19
+ - Do NOT touch the launch dialog pickers (`dashboard.ts` L539-547) — out of scope for this issue.
20
+ - Commit per task with conventional commits; `git add <file>` per file, never `git add -A`.
21
+ - `cur < 0` fallback semantics: out-of-list `selectedId` uses base index 0 — ↓ yields index 1 (second row, same as before); ↑ wraps to the last row (**changed** from the old clamp's first row — intentional, wrap-consistent, and effectively unreachable since `refresh()` keeps `selectedId` ∈ `orderedIds`).
22
+
23
+ ---
24
+
25
+ ### Task 1: `moveSelection()` 取模回绕 + 行为测试
26
+
27
+ **Files:**
28
+ - Modify: `src/ui/dashboard.ts:250-258` (`moveSelection`)
29
+ - Create: `test-support/navigation-wrap.ts` (probe script, prints JSON)
30
+ - Create: `test/dashboard-navigation.test.mjs` (node:test wrapper)
31
+
32
+ **Interfaces:**
33
+ - Consumes: existing `createService` (`src/runtime/service.mjs`), `createView` (`src/core/store.mjs`), `DashboardComponent` (`src/ui/dashboard.ts`), public method `handleInput(data: string)`.
34
+ - Produces: `test-support/navigation-wrap.ts` prints one JSON line `{ ids: string[], seq: (string|null)[] }`; Task 2 extends the same probe with a peek-mode segment (do not rewrite the file structure).
35
+
36
+ - [ ] **Step 1: Worktree dependency setup**
37
+
38
+ Worktree has no `node_modules`. Symlink from the main checkout (instant; postinstall patches already applied there):
39
+
40
+ ```bash
41
+ WT=/home/elling/git-repo/github/pi-agent-board/.pi/worktrees/issue-52-circular-navigation
42
+ ln -s /home/elling/git-repo/github/pi-agent-board/node_modules "$WT/node_modules"
43
+ ls "$WT/node_modules/@mariozechner" # sanity: pi-tui present
44
+ ```
45
+
46
+ (Fallback if the symlink misbehaves: `cd "$WT" && npm ci`.)
47
+
48
+ - [ ] **Step 2: Write the failing test probe**
49
+
50
+ Create `test-support/navigation-wrap.ts`:
51
+
52
+ ```ts
53
+ // Nav wrap probe: construct a dashboard with 3 views, drive arrow keys through
54
+ // the public handleInput(), and report the selection sequence as JSON.
55
+ // Run via `node --experimental-transform-types` (dashboard.ts uses TS
56
+ // parameter properties). Not typechecked (tsconfig excludes test-support).
57
+ import { mkdtempSync } from "node:fs";
58
+ import { tmpdir } from "node:os";
59
+ import { join } from "node:path";
60
+ import { createService } from "../src/runtime/service.mjs";
61
+ import { createView } from "../src/core/store.mjs";
62
+ import { DashboardComponent } from "../src/ui/dashboard.ts";
63
+
64
+ const root = mkdtempSync(join(tmpdir(), "agentview-nav-wrap-"));
65
+ createView(root, { id: "v1", name: "one", cwd: root });
66
+ createView(root, { id: "v2", name: "two", cwd: root });
67
+ createView(root, { id: "v3", name: "three", cwd: root });
68
+
69
+ const service = createService({
70
+ root,
71
+ runnerScript: "/no/runner.mjs",
72
+ piCommand: "pi",
73
+ piArgsPrefix: [],
74
+ defaultCwd: root,
75
+ launch: () => ({ pid: null, configPath: "/no/config.json" }),
76
+ launchHost: () => ({ pid: null, configPath: "/no/host-config.json" }),
77
+ launchTitle: () => ({ pid: null, configPath: "/no/title-config.json" }),
78
+ });
79
+
80
+ const writes = [];
81
+ const tui = {
82
+ terminal: { rows: 24, cols: 80, columns: 80, write: (d) => writes.push(d) },
83
+ requestRender: () => {},
84
+ };
85
+ const theme = { fg: (_c, t) => t, bold: (t) => t };
86
+
87
+ const dash = new DashboardComponent(tui, theme, {}, () => {}, {
88
+ service,
89
+ root,
90
+ defaultCwd: root,
91
+ availableModels: [],
92
+ currentModel: null,
93
+ currentThinkingLevel: "off",
94
+ });
95
+
96
+ // TS-private is runtime-accessible; read white-box state for assertions.
97
+ const ids = dash.orderedIds;
98
+ const seq = [];
99
+ seq.push(dash.selectedId); // initial selection = ids[0]
100
+ dash.handleInput("\x1b[B"); // ↓ -> ids[1]
101
+ seq.push(dash.selectedId);
102
+ dash.handleInput("\x1b[B"); // ↓ -> ids[2]
103
+ seq.push(dash.selectedId);
104
+ dash.handleInput("\x1b[B"); // ↓ at last -> WRAP to ids[0]
105
+ seq.push(dash.selectedId);
106
+ dash.handleInput("\x1b[A"); // ↑ at first -> WRAP to ids[2]
107
+ seq.push(dash.selectedId);
108
+
109
+ dash.dispose();
110
+ console.log(JSON.stringify({ ids, seq }));
111
+ ```
112
+
113
+ - [ ] **Step 3: Write the failing test wrapper**
114
+
115
+ Create `test/dashboard-navigation.test.mjs`:
116
+
117
+ ```js
118
+ import assert from "node:assert/strict";
119
+ import { execFileSync } from "node:child_process";
120
+ import { fileURLToPath } from "node:url";
121
+ import { join } from "node:path";
122
+ import test from "node:test";
123
+
124
+ const ROOT_DIR = fileURLToPath(new URL("../", import.meta.url));
125
+ const PROBE = join(ROOT_DIR, "test-support", "navigation-wrap.ts");
126
+
127
+ test("list arrow-key navigation wraps around at both ends (issue #52)", () => {
128
+ // dashboard.ts uses TS parameter properties, which strip-only mode rejects;
129
+ // --experimental-transform-types handles them (Node 22.7+ / 24).
130
+ const out = execFileSync(process.execPath, ["--experimental-transform-types", PROBE], {
131
+ encoding: "utf8",
132
+ timeout: 30_000,
133
+ });
134
+ const { ids, seq } = JSON.parse(out);
135
+ assert.equal(ids.length, 3, "probe must see 3 rows");
136
+ assert.deepEqual(
137
+ seq,
138
+ [ids[0], ids[1], ids[2], ids[0], ids[2]],
139
+ "down past the last row wraps to first; up past the first wraps to last",
140
+ );
141
+ });
142
+ ```
143
+
144
+ - [ ] **Step 4: Run test to verify it fails**
145
+
146
+ ```bash
147
+ cd /home/elling/git-repo/github/pi-agent-board/.pi/worktrees/issue-52-circular-navigation
148
+ node --test test/dashboard-navigation.test.mjs
149
+ ```
150
+
151
+ Expected: FAIL — `seq[3]` is `ids[2]` (clamped at last) instead of `ids[0]`, and `seq[4]` is `ids[0]` instead of `ids[2]`.
152
+
153
+ If the probe itself crashes (e.g. key sequence mismatch), debug the probe first — `matchesKey(data, Key.up/down)` must match `\x1b[A` / `\x1b[B]`; verify against `@mariozechner/pi-tui` `Key` defaults if not.
154
+
155
+ - [ ] **Step 5: Implement modulo wrap in `moveSelection()`**
156
+
157
+ In `src/ui/dashboard.ts`, replace:
158
+
159
+ ```ts
160
+ private moveSelection(delta: number): void {
161
+ if (this.orderedIds.length === 0) return;
162
+ const cur = this.selectedId ? this.orderedIds.indexOf(this.selectedId) : 0;
163
+ const next = Math.max(0, Math.min(this.orderedIds.length - 1, (cur < 0 ? 0 : cur) + delta));
164
+ ```
165
+
166
+ with:
167
+
168
+ ```ts
169
+ private moveSelection(delta: number): void {
170
+ if (this.orderedIds.length === 0) return;
171
+ const cur = this.selectedId ? this.orderedIds.indexOf(this.selectedId) : 0;
172
+ const len = this.orderedIds.length;
173
+ // Wrap around both ends: down past last -> first, up past first -> last.
174
+ const next = (((cur < 0 ? 0 : cur) + delta) % len + len) % len;
175
+ ```
176
+
177
+ Everything below that line (`const nextId = ...` through the end of the method) stays unchanged — the `nextId === this.selectedId` early return keeps single-row lists a no-op.
178
+
179
+ - [ ] **Step 6: Run test to verify it passes**
180
+
181
+ ```bash
182
+ cd /home/elling/git-repo/github/pi-agent-board/.pi/worktrees/issue-52-circular-navigation
183
+ node --test test/dashboard-navigation.test.mjs
184
+ ```
185
+
186
+ Expected: PASS.
187
+
188
+ - [ ] **Step 7: Full test suite + typecheck**
189
+
190
+ ```bash
191
+ cd /home/elling/git-repo/github/pi-agent-board/.pi/worktrees/issue-52-circular-navigation
192
+ npm test && npm run typecheck
193
+ ```
194
+
195
+ Expected: all green.
196
+
197
+ - [ ] **Step 8: Commit**
198
+
199
+ ```bash
200
+ cd /home/elling/git-repo/github/pi-agent-board/.pi/worktrees/issue-52-circular-navigation
201
+ git add src/ui/dashboard.ts test-support/navigation-wrap.ts test/dashboard-navigation.test.mjs
202
+ git commit -m "fix: wrap list arrow-key navigation at both ends (issue #52)"
203
+ ```
204
+
205
+ ---
206
+
207
+ ### Task 2: `peekStep()` 取模回绕 + peek 段测试
208
+
209
+ **Files:**
210
+ - Modify: `src/ui/dashboard.ts:1094-1101` (`peekStep`)
211
+ - Modify: `test-support/navigation-wrap.ts` (append peek segment)
212
+ - Modify: `test/dashboard-navigation.test.mjs` (assert extended sequence)
213
+
214
+ **Interfaces:**
215
+ - Consumes: Task 1's probe file and test wrapper; `peekId` / `mode` are TS-private fields on `DashboardComponent`, runtime-accessible from the probe (same white-box pattern as `selectedId`).
216
+ - Produces: probe JSON gains nothing new — `seq` grows by one entry (peek-mode wrap result); test asserts the 6-element sequence.
217
+
218
+ - [ ] **Step 1: Extend the probe with a peek-mode segment (failing)**
219
+
220
+ In `test-support/navigation-wrap.ts`, insert immediately BEFORE `dash.dispose();`:
221
+
222
+ ```ts
223
+ // Peek mode: stepping down from the last row wraps to the first.
224
+ dash.peekId = ids[2];
225
+ dash.selectedId = ids[2];
226
+ dash.mode = "peek";
227
+ dash.handleInput("\x1b[B"); // ↓ at last in peek -> WRAP to ids[0]
228
+ seq.push(dash.selectedId);
229
+ ```
230
+
231
+ And in `test/dashboard-navigation.test.mjs`, replace the final assertion with:
232
+
233
+ ```js
234
+ assert.deepEqual(
235
+ seq,
236
+ [ids[0], ids[1], ids[2], ids[0], ids[2], ids[0]],
237
+ "list and peek navigation wrap around at both ends",
238
+ );
239
+ ```
240
+
241
+ - [ ] **Step 2: Run test to verify it fails**
242
+
243
+ ```bash
244
+ cd /home/elling/git-repo/github/pi-agent-board/.pi/worktrees/issue-52-circular-navigation
245
+ node --test test/dashboard-navigation.test.mjs
246
+ ```
247
+
248
+ Expected: FAIL — `seq[5]` is `ids[2]` (peek clamped at last) instead of `ids[0]`.
249
+
250
+ - [ ] **Step 3: Implement modulo wrap in `peekStep()`**
251
+
252
+ In `src/ui/dashboard.ts`, replace:
253
+
254
+ ```ts
255
+ private peekStep(delta: number): void {
256
+ if (!this.peekId) return;
257
+ const idx = this.orderedIds.indexOf(this.peekId);
258
+ if (idx < 0) return;
259
+ const next = Math.max(0, Math.min(this.orderedIds.length - 1, idx + delta));
260
+ ```
261
+
262
+ with:
263
+
264
+ ```ts
265
+ private peekStep(delta: number): void {
266
+ if (!this.peekId) return;
267
+ const idx = this.orderedIds.indexOf(this.peekId);
268
+ if (idx < 0) return;
269
+ const len = this.orderedIds.length;
270
+ // Wrap around both ends, same as moveSelection().
271
+ const next = ((idx + delta) % len + len) % len;
272
+ ```
273
+
274
+ The two lines below (`this.peekId = ...; this.selectedId = ...`) stay unchanged. `len` is guaranteed `>= 1` here because `idx >= 0` implies the id was found in `orderedIds`.
275
+
276
+ - [ ] **Step 4: Run test to verify it passes**
277
+
278
+ ```bash
279
+ cd /home/elling/git-repo/github/pi-agent-board/.pi/worktrees/issue-52-circular-navigation
280
+ node --test test/dashboard-navigation.test.mjs
281
+ ```
282
+
283
+ Expected: PASS.
284
+
285
+ - [ ] **Step 5: Full test suite + typecheck**
286
+
287
+ ```bash
288
+ cd /home/elling/git-repo/github/pi-agent-board/.pi/worktrees/issue-52-circular-navigation
289
+ npm test && npm run typecheck
290
+ ```
291
+
292
+ Expected: all green.
293
+
294
+ - [ ] **Step 6: Commit**
295
+
296
+ ```bash
297
+ cd /home/elling/git-repo/github/pi-agent-board/.pi/worktrees/issue-52-circular-navigation
298
+ git add src/ui/dashboard.ts test-support/navigation-wrap.ts test/dashboard-navigation.test.mjs
299
+ git commit -m "fix: wrap peek-mode stepping at both ends (issue #52)"
300
+ ```
301
+
302
+ ---
303
+
304
+ ## Self-Review 记录
305
+
306
+ - **Spec coverage**:spec 改动点 #1 → Task 1;改动点 #2 → Task 2;滚动跟随"不改"已在 Global Constraints 外无对应任务(无需任务);launch picker 非目标 → Global Constraints 显式禁止。测试要求 → 两任务各带 TDD 循环。✅ 无缺口。
307
+ - **Placeholder scan**:无 TBD/TODO;每个代码步骤含完整可运行代码。✅
308
+ - **Type consistency**:`orderedIds` / `selectedId` / `peekId` / `mode` / `handleInput(data: string)` / `dispose()` 名称与 `src/ui/dashboard.ts` 实际成员一致;probe 的 JSON 形状 `{ ids, seq }` 在两个任务间一致(Task 2 只扩 seq 长度)。✅
@@ -0,0 +1,311 @@
1
+ # pty-attach.ts Legacy Quality Debt Cleanup — Implementation Plan
2
+
3
+ > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
4
+
5
+ **Goal:** Clear the 10 legacy quality issues in `src/ui/pty-attach.ts` flagged by issue #8 — document 9 intentional empty catches, add a SAFETY comment to 1 as-cast, drop 1 unused parameter. Zero behavior change.
6
+
7
+ **Architecture:** Single-file comment/signature cleanup. Empty catches are expanded to the repo's documented-catch house style (see `dashboard.ts` precedent); the as-cast gets an invariant comment; `project()` loses its unused `width` parameter. Existing test suite is the regression net — no new tests (no behavior change to test).
8
+
9
+ **Tech Stack:** TypeScript (strict), `node --test`, tab indentation.
10
+
11
+ **Spec:** `docs/superpowers/specs/2026-08-30-pty-attach-quality-debt-design.md`
12
+
13
+ ## Global Constraints
14
+
15
+ - Touch ONLY `src/ui/pty-attach.ts` (plus this plan file's checkboxes).
16
+ - Zero behavior change: no logic added/removed except the parameter deletion and its call-site argument.
17
+ - Indentation is TABS. Comment style: expand `} catch {}` to multi-line with the comment INSIDE the braces, exactly like `dashboard.ts` (`} catch {` / `\t/* best effort: ... */` / `}`).
18
+ - Locate sites by method name, not line number (lines drift).
19
+ - `git add` per file only; NEVER `git add -A` (main checkout has untracked files that must not be swept — the worktree is clean, but keep the habit).
20
+ - Run all commands from the worktree root: `/home/elling/git-repo/github/pi-agent-board/.pi/worktrees/issue-8-pty-attach-quality-debt`
21
+
22
+ ---
23
+
24
+ ### Task 1: Document the 9 empty catches + the as-cast invariant
25
+
26
+ **Files:**
27
+ - Modify: `src/ui/pty-attach.ts` (9 catch sites + 1 as-cast site, by method)
28
+
29
+ **Interfaces:** none changed (comments only).
30
+
31
+ There are exactly 9 single-line `} catch {}` sites in the file. Each becomes a 3-line documented catch. The edits, by method (old → new). Match surrounding context exactly; indentation is tabs.
32
+
33
+ 1. `enableMouseScroll()` — inside `try { this.tui.terminal.write(XTSHIFTESCAPE_SELECT); this.tui.terminal.write(MOUSE_ENABLE); }`:
34
+
35
+ ```ts
36
+ // OLD
37
+ } catch {}
38
+ }
39
+
40
+ private mouseScrollEnabled(): boolean {
41
+ // NEW
42
+ } catch {
43
+ /* best-effort: some terminals reject these sequences; mouse reporting is optional */
44
+ }
45
+ }
46
+
47
+ private mouseScrollEnabled(): boolean {
48
+ ```
49
+
50
+ 2. `disableMouseScroll()`:
51
+
52
+ ```ts
53
+ // OLD
54
+ private disableMouseScroll(): void {
55
+ try {
56
+ this.tui.terminal.write(MOUSE_DISABLE);
57
+ } catch {}
58
+ }
59
+ // NEW
60
+ private disableMouseScroll(): void {
61
+ try {
62
+ this.tui.terminal.write(MOUSE_DISABLE);
63
+ } catch {
64
+ /* best-effort: terminal may already be gone at teardown */
65
+ }
66
+ }
67
+ ```
68
+
69
+ 3. `copySelectionToClipboard()` — the OSC52 write:
70
+
71
+ ```ts
72
+ // OLD
73
+ if (seq) {
74
+ try {
75
+ this.tui.terminal.write(seq);
76
+ } catch {}
77
+ }
78
+ // NEW
79
+ if (seq) {
80
+ try {
81
+ this.tui.terminal.write(seq);
82
+ } catch {
83
+ /* best-effort: OSC52 clipboard support is optional */
84
+ }
85
+ }
86
+ ```
87
+
88
+ 4. `pastePrimarySelection()` — inner timer kill:
89
+
90
+ ```ts
91
+ // OLD
92
+ const timer = setTimeout(() => {
93
+ try {
94
+ child.kill("SIGKILL");
95
+ } catch {}
96
+ }, 800);
97
+ // NEW
98
+ const timer = setTimeout(() => {
99
+ try {
100
+ child.kill("SIGKILL");
101
+ } catch {
102
+ /* the child may have already exited before the timeout fired */
103
+ }
104
+ }, 800);
105
+ ```
106
+
107
+ 5. `pastePrimarySelection()` — outer catch, at end of method (the `} catch {}` right before the method's closing `}`):
108
+
109
+ ```ts
110
+ // OLD
111
+ child.on("close", () => {
112
+ clearTimeout(timer);
113
+ if (!this.closed && out) this.send({ type: "input", data: out });
114
+ });
115
+ } catch {}
116
+ }
117
+ // NEW
118
+ child.on("close", () => {
119
+ clearTimeout(timer);
120
+ if (!this.closed && out) this.send({ type: "input", data: out });
121
+ });
122
+ } catch {
123
+ /* silent no-op when xclip is absent — documented contract of this helper */
124
+ }
125
+ }
126
+ ```
127
+
128
+ 6. `writePrimarySelection()`:
129
+
130
+ ```ts
131
+ // OLD
132
+ child.stdin?.on("error", () => {});
133
+ child.on("error", () => {});
134
+ child.stdin?.end(text);
135
+ } catch {}
136
+ }
137
+ // NEW
138
+ child.stdin?.on("error", () => {});
139
+ child.on("error", () => {});
140
+ child.stdin?.end(text);
141
+ } catch {
142
+ /* silent no-op when xclip is absent */
143
+ }
144
+ }
145
+ ```
146
+
147
+ 7. `forwardTerminalProtocols()` — the per-sequence write loop:
148
+
149
+ ```ts
150
+ // OLD
151
+ for (const seq of toWrite) {
152
+ try {
153
+ this.tui.terminal.write(seq);
154
+ } catch {}
155
+ }
156
+ // NEW
157
+ for (const seq of toWrite) {
158
+ try {
159
+ this.tui.terminal.write(seq);
160
+ } catch {
161
+ /* best-effort: forwarded sequences are enhancements, never critical */
162
+ }
163
+ }
164
+ ```
165
+
166
+ 8. `replayScreenLog()` — outer catch at end of method:
167
+
168
+ ```ts
169
+ // OLD
170
+ } finally {
171
+ closeSync(fd);
172
+ }
173
+ } catch {}
174
+ }
175
+ // NEW
176
+ } finally {
177
+ closeSync(fd);
178
+ }
179
+ } catch {
180
+ /* best-effort: a missing or racing screen.log must not block attach */
181
+ }
182
+ }
183
+ ```
184
+
185
+ 9. `close()` — socket destroy:
186
+
187
+ ```ts
188
+ // OLD
189
+ try {
190
+ this.socket?.destroy();
191
+ } catch {}
192
+ this.socket = null;
193
+ // NEW
194
+ try {
195
+ this.socket?.destroy();
196
+ } catch {
197
+ /* best-effort teardown: socket may already be destroyed */
198
+ }
199
+ this.socket = null;
200
+ ```
201
+
202
+ 10. `currentSize()` — SAFETY comment above the as-cast:
203
+
204
+ ```ts
205
+ // OLD
206
+ private currentSize(): { cols: number; rows: number } {
207
+ const term = this.tui.terminal as unknown as { cols?: number; columns?: number; rows?: number } | undefined;
208
+ // NEW
209
+ private currentSize(): { cols: number; rows: number } {
210
+ // SAFETY: duck-typed read — Pi TUI's Terminal type does not consistently expose
211
+ // cols/columns/rows across versions (see resizeIfNeeded below). Runtime
212
+ // fallbacks (120/24) keep this safe when the fields are absent.
213
+ const term = this.tui.terminal as unknown as { cols?: number; columns?: number; rows?: number } | undefined;
214
+ ```
215
+
216
+ - [ ] **Step 1: Apply all 10 edits** (by method, exact old→new above)
217
+
218
+ - [ ] **Step 2: Assert no bare `catch {}` remains**
219
+
220
+ Run: `grep -c "catch {}" src/ui/pty-attach.ts`
221
+ Expected: `0`
222
+
223
+ - [ ] **Step 3: Typecheck**
224
+
225
+ Run: `npm run typecheck`
226
+ Expected: clean exit
227
+
228
+ - [ ] **Step 4: Tests**
229
+
230
+ Run: `npm test`
231
+ Expected: all pass (no behavior change)
232
+
233
+ - [ ] **Step 5: Commit**
234
+
235
+ ```bash
236
+ git add src/ui/pty-attach.ts
237
+ git commit -m "chore: document intentional empty catches and as-cast invariant in pty-attach (issue #8)"
238
+ ```
239
+
240
+ ---
241
+
242
+ ### Task 2: Drop the unused `width` parameter from `project()`
243
+
244
+ **Files:**
245
+ - Modify: `src/ui/pty-attach.ts` (signature + single call site)
246
+
247
+ **Interfaces:**
248
+ - Changes: `private project(height: number, width: number)` → `private project(height: number)` (private; one caller)
249
+
250
+ The parameter `width` is never read in the method body. `width` at the call site remains used by `resizeIfNeeded(width)` / `renderLoading(...)` / `clip(...)` — only the `project()` argument goes away.
251
+
252
+ ```ts
253
+ // OLD (signature)
254
+ private project(height: number, width: number): { lines: string[]; cursor: { row: number; col: number } | null } {
255
+ // NEW (signature)
256
+ private project(height: number): { lines: string[]; cursor: { row: number; col: number } | null } {
257
+ ```
258
+
259
+ ```ts
260
+ // OLD (call site, in render(width: number))
261
+ const projected = this.project(bodyHeight, width);
262
+ // NEW (call site)
263
+ const projected = this.project(bodyHeight);
264
+ ```
265
+
266
+ - [ ] **Step 1: Apply both edits**
267
+
268
+ - [ ] **Step 2: Assert single-arg signature and call**
269
+
270
+ Run: `grep -n "project(" src/ui/pty-attach.ts`
271
+ Expected: exactly 2 hits — `render(...)`'s `this.project(bodyHeight);` and `private project(height: number): ...`
272
+
273
+ - [ ] **Step 3: Typecheck**
274
+
275
+ Run: `npm run typecheck`
276
+ Expected: clean exit (a missed call site would fail here)
277
+
278
+ - [ ] **Step 4: Tests**
279
+
280
+ Run: `npm test`
281
+ Expected: all pass
282
+
283
+ - [ ] **Step 5: Commit**
284
+
285
+ ```bash
286
+ git add src/ui/pty-attach.ts
287
+ git commit -m "chore: drop unused width param from PtyAttachComponent.project (issue #8)"
288
+ ```
289
+
290
+ ---
291
+
292
+ ### Task 3: Final verification sweep
293
+
294
+ **Files:** none modified.
295
+
296
+ - [ ] **Step 1: Full verify pipeline (same as CI)**
297
+
298
+ Run: `npm run verify`
299
+ Expected: typecheck + tests + coverage thresholds (lines 85 / funcs 80 / branches 70) + pack dry-run all pass.
300
+
301
+ - [ ] **Step 2: Zero-behavior diff audit**
302
+
303
+ Run: `git diff main...HEAD -- src/ui/pty-attach.ts | grep -E "^[+-]" | grep -vE "^(\+\+\+|---)" | grep -vE "^\+\s*(/\*|//|\*/?)" | grep -vE "^-.*catch \{\}" | grep -vE "^\+\s*} catch \{" | grep -vE "^\+\s*}"`
304
+ Expected: exactly 4 lines — the `project` signature and call site (`-`/`+` pairs). Everything else in the raw diff must be comment additions or `catch {}` expansions; any other code line appearing here means behavior changed — investigate before proceeding.
305
+
306
+ - [ ] **Step 3: Confirm working tree clean**
307
+
308
+ Run: `git status --short`
309
+ Expected: empty (nothing uncommitted, nothing swept in)
310
+
311
+ No commit in this task (verification only).