@huseynsnmz/pi-board 0.15.1 → 0.16.0

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 (215) hide show
  1. package/CHANGELOG.md +26 -4
  2. package/CONTRIBUTING.md +7 -0
  3. package/README.md +53 -419
  4. package/SECURITY.md +19 -0
  5. package/docs/how-it-works.md +31 -0
  6. package/docs/reference.md +276 -0
  7. package/package.json +18 -19
  8. package/src/index.ts +9 -1
  9. package/src/runtime/service.mjs +12 -0
  10. package/src/ui/dashboard.ts +14 -8
  11. package/src/ui/double-left.ts +5 -1
  12. package/src/ui/focus.ts +7 -1
  13. package/src/ui/waiting.ts +10 -5
  14. package/IMPLEMENTATION_PLAN.md +0 -920
  15. package/PRD.md +0 -484
  16. package/PROGRESS.md +0 -142
  17. package/VERIFY.md +0 -115
  18. package/docs/BATCH_SELECTION_READ_FLOW.md +0 -277
  19. package/docs/EXPLORATION.md +0 -187
  20. package/docs/PTY_ATTACH_IMPLEMENTATION_PLAN.md +0 -581
  21. package/docs/RELEASE.md +0 -148
  22. package/docs/superpowers/plans/2026-08-15-screenlog-gc.md +0 -704
  23. package/docs/superpowers/plans/2026-08-16-attach-double-cursor-jiggle-retry.md +0 -499
  24. package/docs/superpowers/plans/2026-08-21-attach-coldstart-jiggle-rearm.md +0 -797
  25. package/docs/superpowers/plans/2026-08-21-dashboard-keypress-lag.md +0 -366
  26. package/docs/superpowers/plans/2026-08-21-issue-14-needs-input-no-auto-done.md +0 -576
  27. package/docs/superpowers/plans/2026-08-22-jiggle-shrink-and-hold.md +0 -337
  28. package/docs/superpowers/plans/2026-08-22-launch-cwd-favorites.md +0 -916
  29. package/docs/superpowers/plans/2026-08-22-question-tool-grouping.md +0 -211
  30. package/docs/superpowers/plans/2026-08-22-rename-state-labels.md +0 -182
  31. package/docs/superpowers/plans/2026-08-22-stable-list-order.md +0 -51
  32. package/docs/superpowers/plans/2026-08-27-locks-acquirelock-spin.md +0 -614
  33. package/docs/superpowers/plans/2026-08-27-pty-runner-test-flaky.md +0 -32
  34. package/docs/superpowers/plans/2026-08-29-code-refs-badges.md +0 -223
  35. package/docs/superpowers/plans/2026-08-30-circular-navigation.md +0 -308
  36. package/docs/superpowers/plans/2026-08-30-post-exit-timing-fix.md +0 -37
  37. package/docs/superpowers/plans/2026-08-30-pty-attach-quality-debt.md +0 -311
  38. package/docs/superpowers/plans/2026-08-30-readme-v2.md +0 -294
  39. package/docs/superpowers/plans/2026-09-01-attach-detach-gate-cursor-anchor.md +0 -284
  40. package/docs/superpowers/plans/2026-09-02-attach-detach-editor-state.md +0 -722
  41. package/docs/superpowers/plans/2026-09-03-code-refs-pr-backlink-narrow.md +0 -551
  42. package/docs/superpowers/plans/2026-09-03-detach-gate-glyph-fallback.md +0 -146
  43. package/docs/superpowers/plans/2026-09-04-evidence-outputpreview.md +0 -209
  44. package/docs/superpowers/plans/2026-09-04-warm-host-reclaim.md +0 -796
  45. package/docs/superpowers/plans/2026-09-05-issue-13-drainnextfollowup-pty-probe.md +0 -114
  46. package/docs/superpowers/plans/2026-09-05-issue-38-windows-wezterm-ime-cursor.md +0 -73
  47. package/docs/superpowers/plans/2026-09-05-issue-39-truncate-codepoint-boundary.md +0 -143
  48. package/docs/superpowers/plans/2026-09-05-issue-61-mention-fallback-guards.md +0 -226
  49. package/docs/superpowers/plans/2026-09-05-issue-63-flaky-manual-completion.md +0 -87
  50. package/docs/superpowers/plans/2026-09-05-issue-64-changelog-release-helper.md +0 -53
  51. package/docs/superpowers/plans/2026-09-05-pty-host-stacking-sock-race.md +0 -731
  52. package/docs/superpowers/plans/2026-09-08-attach-ctrl-left-detach.md +0 -30
  53. package/docs/superpowers/plans/2026-09-08-dashboard-shrink-repaint.md +0 -68
  54. package/docs/superpowers/plans/2026-09-08-issue-11-attach-runtime-desync-heal.md +0 -917
  55. package/docs/superpowers/plans/2026-09-08-legacy-stale-host-recovery.md +0 -125
  56. package/docs/superpowers/plans/2026-09-08-spawn-async-error-swallow.md +0 -56
  57. package/docs/superpowers/plans/2026-09-08-stale-model-attach-guard.md +0 -96
  58. package/docs/superpowers/plans/2026-09-09-claimpid-blocks-replace.md +0 -267
  59. package/docs/superpowers/plans/2026-09-09-harden-runner-architecture.md +0 -603
  60. package/docs/superpowers/plans/2026-09-09-single-writer-completion.md +0 -252
  61. package/docs/superpowers/plans/2026-09-10-reader-consistency.md +0 -115
  62. package/docs/superpowers/plans/2026-09-14-attach-cursor-dectcem-gate.md +0 -469
  63. package/docs/superpowers/plans/2026-09-14-attach-snapshot.md +0 -92
  64. package/docs/superpowers/plans/2026-09-14-host-meta-orphan-lock.md +0 -771
  65. package/docs/superpowers/plans/2026-09-14-issue-106-terminal-frame-cognition.md +0 -299
  66. package/docs/superpowers/plans/2026-09-14-issue-113-foreground-preview-race.md +0 -609
  67. package/docs/superpowers/plans/2026-09-14-terminal-model.md +0 -145
  68. package/docs/superpowers/plans/2026-09-15-coordinator-pipe-root-normalize.md +0 -224
  69. package/docs/superpowers/plans/2026-09-15-lease-publish-eprem-reclaim.md +0 -341
  70. package/docs/superpowers/plans/2026-09-18-detach-anchor-reporter-endpoint.md +0 -875
  71. package/docs/superpowers/plans/2026-09-20-control-lifecycle.md +0 -116
  72. package/docs/superpowers/plans/2026-09-20-issue-121-perf-gate-out-of-coverage.md +0 -517
  73. package/docs/superpowers/plans/2026-09-23-a5-mid-stream-assertion.md +0 -25
  74. package/docs/superpowers/plans/2026-09-23-cwd-browse-tab-drill.md +0 -393
  75. package/docs/superpowers/plans/2026-09-23-terminal-query-forwarding.md +0 -51
  76. package/docs/superpowers/plans/2026-09-23-unconditional-detach-key.md +0 -58
  77. package/docs/superpowers/plans/2026-09-24-control-reconcile-flake.md +0 -698
  78. package/docs/superpowers/plans/2026-09-24-probe-attached-clients.md +0 -605
  79. package/docs/superpowers/plans/2026-09-29-issue-145-holding-semantic-state.md +0 -1183
  80. package/docs/superpowers/plans/2026-09-29-issue-145-user-verification.md +0 -66
  81. package/docs/superpowers/plans/2026-09-30-board-delete-key-x-y-confirm.md +0 -649
  82. package/docs/superpowers/plans/2026-09-30-issue-153-runner-stop-boot-window.md +0 -618
  83. package/docs/superpowers/plans/2026-09-30-issue-95-test-discipline.md +0 -555
  84. package/docs/superpowers/plans/2026-09-30-publish-dispatch-tag-check.md +0 -259
  85. package/docs/superpowers/plans/2026-10-06-attach-settle-scheme-delivery.md +0 -751
  86. package/docs/superpowers/plans/2026-10-09-issue-167-attach-middle-paste.md +0 -509
  87. package/docs/superpowers/plans/2026-10-09-issue-169-fullscreen-mouse-ownership.md +0 -346
  88. package/docs/superpowers/plans/2026-10-10-attach-delete-in-place.md +0 -832
  89. package/docs/superpowers/plans/2026-10-10-issue-174-selection-mirror-paste-source.md +0 -510
  90. package/docs/superpowers/plans/2026-10-14-issue-170-attach-reply-guard.md +0 -771
  91. package/docs/superpowers/plans/2026-10-14-issue-178-accept-registration-race.md +0 -1257
  92. package/docs/superpowers/plans/2026-10-14-issue-184-snapshot-window-contract.md +0 -657
  93. package/docs/superpowers/plans/2026-10-14-issue-185-attach-wheel-alt-screen.md +0 -47
  94. package/docs/superpowers/plans/2026-10-14-issue-187-motionless-drag.md +0 -931
  95. package/docs/superpowers/plans/2026-10-14-issue-189-mixed-input-segments.md +0 -823
  96. package/docs/superpowers/plans/2026-10-15-issue-188-x10-mouse-release.md +0 -526
  97. package/docs/superpowers/plans/2026-10-16-issue-198-attach-wheel-forward-fallback.md +0 -32
  98. package/docs/superpowers/plans/2026-10-18-attach-wheel-replay-pseudo-scrollback.md +0 -437
  99. package/docs/superpowers/plans/2026-10-19-issue-213-reply-guard-csi-key.md +0 -257
  100. package/docs/superpowers/plans/2026-10-19-issue-214-attach-forward-alt-modifier.md +0 -202
  101. package/docs/superpowers/plans/2026-10-21-issue-221-ask-user-question.md +0 -198
  102. package/docs/superpowers/plans/2026-10-23-issue-222-attach-ready.md +0 -290
  103. package/docs/superpowers/plans/2026-10-23-issue-225-monotonic-foreground-state.md +0 -450
  104. package/docs/superpowers/plans/2026-10-23-issue-229-vim-jkl.md +0 -449
  105. package/docs/superpowers/plans/2026-10-23-issue-232-attach-bg-only-cells.md +0 -153
  106. package/docs/superpowers/plans/2026-10-23-lease-heartbeat-staleness.md +0 -537
  107. package/docs/superpowers/plans/2026-10-23-selected-row-highlight.md +0 -405
  108. package/docs/superpowers/plans/2026-10-24-issue-217-windows-pid-reuse-stopping.md +0 -367
  109. package/docs/superpowers/plans/2026-10-24-issue-230-warm-host-background-runs.md +0 -806
  110. package/docs/superpowers/plans/2026-10-24-issue-238-windows-process-identity-token.md +0 -490
  111. package/docs/superpowers/plans/2026-10-24-issue-241-coordinator-root-exit.md +0 -461
  112. package/docs/superpowers/plans/2026-10-25-issue-239-coordinator-replacement-identity.md +0 -504
  113. package/docs/superpowers/plans/2026-10-26-issue-245-key-hints-doc-sync.md +0 -244
  114. package/docs/superpowers/plans/2026-10-27-issue-172-fullscreen-selection-clear.md +0 -344
  115. package/docs/superpowers/plans/2026-10-27-issue-252-select-key-hints.md +0 -209
  116. package/docs/superpowers/plans/2026-10-28-issue-255-coordinator-endpoint-zombie.md +0 -442
  117. package/docs/superpowers/plans/2026-10-28-issue-257-legacy-pty-runner-endpoint-cleanup.md +0 -256
  118. package/docs/superpowers/plans/2026-11-03-cwd-favorites-full-pool.md +0 -266
  119. package/docs/superpowers/specs/2026-08-15-screenlog-gc-design.md +0 -105
  120. package/docs/superpowers/specs/2026-08-16-attach-double-cursor-jiggle-retry-design.md +0 -142
  121. package/docs/superpowers/specs/2026-08-21-attach-coldstart-jiggle-rearm-design.md +0 -110
  122. package/docs/superpowers/specs/2026-08-21-dashboard-keypress-lag-design.md +0 -59
  123. package/docs/superpowers/specs/2026-08-21-issue-14-needs-input-no-auto-done-design.md +0 -109
  124. package/docs/superpowers/specs/2026-08-22-cwd-favorites-design.md +0 -131
  125. package/docs/superpowers/specs/2026-08-22-jiggle-shrink-and-hold-design.md +0 -111
  126. package/docs/superpowers/specs/2026-08-22-question-tool-grouping-design.md +0 -114
  127. package/docs/superpowers/specs/2026-08-22-rename-state-labels-design.md +0 -80
  128. package/docs/superpowers/specs/2026-08-27-locks-acquirelock-spin-design.md +0 -97
  129. package/docs/superpowers/specs/2026-08-27-pty-runner-test-flaky-design.md +0 -35
  130. package/docs/superpowers/specs/2026-08-29-code-refs-badges-design.md +0 -128
  131. package/docs/superpowers/specs/2026-08-30-circular-navigation-design.md +0 -47
  132. package/docs/superpowers/specs/2026-08-30-eprm-atomicwrite-race-design.md +0 -77
  133. package/docs/superpowers/specs/2026-08-30-post-exit-timing-fix-design.md +0 -80
  134. package/docs/superpowers/specs/2026-08-30-pty-attach-quality-debt-design.md +0 -105
  135. package/docs/superpowers/specs/2026-08-30-readme-v2-design.md +0 -115
  136. package/docs/superpowers/specs/2026-09-01-attach-detach-gate-cursor-anchor-design.md +0 -78
  137. package/docs/superpowers/specs/2026-09-02-attach-detach-editor-state-design.md +0 -120
  138. package/docs/superpowers/specs/2026-09-03-code-refs-pr-backlink-narrow-design.md +0 -92
  139. package/docs/superpowers/specs/2026-09-03-detach-gate-glyph-fallback-design.md +0 -99
  140. package/docs/superpowers/specs/2026-09-04-evidence-outputpreview-design.md +0 -50
  141. package/docs/superpowers/specs/2026-09-04-warm-host-reclaim-design.md +0 -106
  142. package/docs/superpowers/specs/2026-09-05-issue-13-drainnextfollowup-pty-probe-design.md +0 -64
  143. package/docs/superpowers/specs/2026-09-05-issue-38-windows-wezterm-ime-design.md +0 -48
  144. package/docs/superpowers/specs/2026-09-05-issue-39-truncate-codepoint-boundary-design.md +0 -64
  145. package/docs/superpowers/specs/2026-09-05-issue-61-mention-fallback-design.md +0 -71
  146. package/docs/superpowers/specs/2026-09-05-issue-63-flaky-manual-completion-design.md +0 -49
  147. package/docs/superpowers/specs/2026-09-05-issue-64-changelog-helper-design.md +0 -76
  148. package/docs/superpowers/specs/2026-09-05-pty-host-stacking-sock-race-design.md +0 -510
  149. package/docs/superpowers/specs/2026-09-07-issue-11-attach-runtime-desync-heal-design.md +0 -130
  150. package/docs/superpowers/specs/2026-09-08-attach-ctrl-left-detach-design.md +0 -58
  151. package/docs/superpowers/specs/2026-09-08-dashboard-shrink-repaint-design.md +0 -52
  152. package/docs/superpowers/specs/2026-09-08-legacy-stale-host-recovery-design.md +0 -87
  153. package/docs/superpowers/specs/2026-09-08-spawn-async-error-swallow-design.md +0 -56
  154. package/docs/superpowers/specs/2026-09-08-stale-model-attach-guard-design.md +0 -79
  155. package/docs/superpowers/specs/2026-09-09-claimpid-blocks-replace-design.md +0 -147
  156. package/docs/superpowers/specs/2026-09-09-harden-runner-architecture-design.md +0 -304
  157. package/docs/superpowers/specs/2026-09-14-attach-cursor-dectcem-gate-design.md +0 -114
  158. package/docs/superpowers/specs/2026-09-14-host-meta-orphan-lock-design.md +0 -120
  159. package/docs/superpowers/specs/2026-09-14-issue-106-terminal-frame-cognition-design.md +0 -146
  160. package/docs/superpowers/specs/2026-09-14-issue-113-foreground-preview-race-design.md +0 -116
  161. package/docs/superpowers/specs/2026-09-15-coordinator-pipe-root-normalize-design.md +0 -84
  162. package/docs/superpowers/specs/2026-09-15-lease-publish-eprem-reclaim-design.md +0 -92
  163. package/docs/superpowers/specs/2026-09-18-detach-anchor-reporter-endpoint-design.md +0 -123
  164. package/docs/superpowers/specs/2026-09-20-issue-121-perf-gate-out-of-coverage-design.md +0 -204
  165. package/docs/superpowers/specs/2026-09-23-a5-mid-stream-assertion-design.md +0 -43
  166. package/docs/superpowers/specs/2026-09-23-cwd-browse-tab-drill-design.md +0 -95
  167. package/docs/superpowers/specs/2026-09-23-terminal-query-forwarding-design.md +0 -63
  168. package/docs/superpowers/specs/2026-09-23-unconditional-detach-key-design.md +0 -57
  169. package/docs/superpowers/specs/2026-09-24-control-reconcile-flake-design.md +0 -98
  170. package/docs/superpowers/specs/2026-09-24-probe-attached-clients-design.md +0 -190
  171. package/docs/superpowers/specs/2026-09-29-issue-145-holding-semantic-state-design.md +0 -394
  172. package/docs/superpowers/specs/2026-09-30-board-delete-key-x-y-confirm-design.md +0 -86
  173. package/docs/superpowers/specs/2026-09-30-issue-153-runner-stop-boot-window-design.md +0 -75
  174. package/docs/superpowers/specs/2026-09-30-issue-95-test-discipline-design.md +0 -226
  175. package/docs/superpowers/specs/2026-09-30-publish-dispatch-tag-check-design.md +0 -129
  176. package/docs/superpowers/specs/2026-10-06-attach-settle-scheme-delivery-design.md +0 -196
  177. package/docs/superpowers/specs/2026-10-09-issue-167-attach-middle-paste-design.md +0 -125
  178. package/docs/superpowers/specs/2026-10-09-issue-169-fullscreen-mouse-ownership-design.md +0 -130
  179. package/docs/superpowers/specs/2026-10-10-attach-delete-in-place-design.md +0 -224
  180. package/docs/superpowers/specs/2026-10-10-issue-174-selection-mirror-paste-source-design.md +0 -154
  181. package/docs/superpowers/specs/2026-10-14-issue-170-attach-reply-guard-design.md +0 -195
  182. package/docs/superpowers/specs/2026-10-14-issue-178-accept-registration-race-design.md +0 -287
  183. package/docs/superpowers/specs/2026-10-14-issue-184-snapshot-window-contract-design.md +0 -288
  184. package/docs/superpowers/specs/2026-10-14-issue-185-attach-wheel-alt-screen-design.md +0 -126
  185. package/docs/superpowers/specs/2026-10-14-issue-187-motionless-drag-design.md +0 -269
  186. package/docs/superpowers/specs/2026-10-14-issue-189-mixed-input-segments-design.md +0 -369
  187. package/docs/superpowers/specs/2026-10-15-issue-188-x10-mouse-release-design.md +0 -252
  188. package/docs/superpowers/specs/2026-10-16-issue-198-attach-wheel-forward-fallback-design.md +0 -67
  189. package/docs/superpowers/specs/2026-10-18-attach-wheel-replay-pseudo-scrollback-design.md +0 -250
  190. package/docs/superpowers/specs/2026-10-19-issue-213-reply-guard-csi-key-design.md +0 -165
  191. package/docs/superpowers/specs/2026-10-19-issue-214-attach-forward-alt-modifier-design.md +0 -131
  192. package/docs/superpowers/specs/2026-10-20-issue-221-ask-user-question-design.md +0 -270
  193. package/docs/superpowers/specs/2026-10-22-issue-225-monotonic-foreground-state-design.md +0 -225
  194. package/docs/superpowers/specs/2026-10-23-issue-222-attach-ready-design.md +0 -204
  195. package/docs/superpowers/specs/2026-10-23-issue-229-vim-jkl-design.md +0 -90
  196. package/docs/superpowers/specs/2026-10-23-issue-232-attach-bg-only-cells-design.md +0 -146
  197. package/docs/superpowers/specs/2026-10-23-lease-heartbeat-staleness-design.md +0 -271
  198. package/docs/superpowers/specs/2026-10-23-selected-row-highlight-design.md +0 -191
  199. package/docs/superpowers/specs/2026-10-24-issue-217-windows-pid-reuse-stopping-design.md +0 -334
  200. package/docs/superpowers/specs/2026-10-24-issue-230-warm-host-background-runs-design.md +0 -283
  201. package/docs/superpowers/specs/2026-10-24-issue-238-windows-process-identity-token-design.md +0 -286
  202. package/docs/superpowers/specs/2026-10-24-issue-241-coordinator-root-exit-design.md +0 -381
  203. package/docs/superpowers/specs/2026-10-25-issue-239-coordinator-replacement-identity-design.md +0 -331
  204. package/docs/superpowers/specs/2026-10-26-issue-245-key-hints-doc-sync-design.md +0 -72
  205. package/docs/superpowers/specs/2026-10-27-issue-172-fullscreen-selection-clear-design.md +0 -248
  206. package/docs/superpowers/specs/2026-10-27-issue-252-select-key-hints-design.md +0 -95
  207. package/docs/superpowers/specs/2026-10-28-issue-255-coordinator-endpoint-zombie-design.md +0 -380
  208. package/docs/superpowers/specs/2026-10-28-issue-257-legacy-pty-runner-endpoint-cleanup-design.md +0 -197
  209. package/docs/superpowers/specs/2026-11-03-cwd-favorites-full-pool-design.md +0 -106
  210. package/scripts/budget-audit.mjs +0 -135
  211. package/scripts/patch-vulns.mjs +0 -59
  212. package/scripts/release.mjs +0 -446
  213. package/scripts/release_helper.mjs +0 -309
  214. package/scripts/run-perf-gate.mjs +0 -40
  215. package/scripts/run-tests.mjs +0 -375
@@ -1,449 +0,0 @@
1
- # Vim-style j/k/l Board Keys 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:** Bind `j`/`k` navigation and `l` attach in the board's list/select/peek/evidence modes so vim users' muscle memory works, without touching text-input modes or the `h` = Hold contract (issue #229).
6
-
7
- **Architecture:** Pure additive key branches in the five per-mode handlers of `src/ui/dashboard.ts`, reusing the exact code paths of the arrow keys (`moveSelection`, `peekStep`, `attachSelected`, `attachPeek`); copy sync in the `?` help overlay, the NORMAL/SELECT hint lines, and the four README view tables. Behavior is verified by a `test-support` probe driving the public `handleInput()` with fake service/done injection.
8
-
9
- **Tech Stack:** TypeScript (runtime), `node --test` + `--experimental-transform-types` probes (test).
10
-
11
- **Spec:** `docs/superpowers/specs/2026-10-23-issue-229-vim-jkl-design.md` (acceptance IDs A1–A9, U1–U2 referenced below)
12
-
13
- ## Global Constraints
14
-
15
- - Tabs for indentation; English comments and commit messages (AGENTS.md).
16
- - Tests use `node:test` with `assert/strict`; probes run via `node --experimental-transform-types` and live in `test-support/` (excluded from tsconfig).
17
- - Conventional commit subjects `feat`/`fix`/`...` with `(issue #229)` reference; PR number appended at merge time.
18
- - `npm run verify` (typecheck + perf gate + tests + coverage thresholds lines 85 / funcs 80 / branches 70 + pack:dry) must pass before the PR.
19
- - `j`/`k` inherit **wrap-around** semantics via `moveSelection`/`peekStep` — do NOT implement clamp (spec D2).
20
- - `h` = Hold (PR #151 contract), dispatch/filter/rename/reply stay literal — off-limits (spec D4/D6).
21
- - Worktree discipline: all edits under `/home/elling/work/git-repo/pi-agent-board/.pi/worktrees/issue-229-vim-jkl-board-list` (= `$WT`), git via `git -C $WT`.
22
-
23
- ## Review Focus
24
-
25
- 1. **Uppercase `J`/`K`/`L` stay unbound** — list mode still shows the printable fallback toast for them. Expected: unchanged current behavior, pinned by a probe assertion (Task 1, Step 1).
26
- 2. **`l` on a busy row** (`hostActive=false && agentBusy=true`) must take the same `confirm-stop-first` branch as `→`, not attach directly. Pinned by the `busyL` probe segment (Task 1, Step 1).
27
- 3. **Help rows got longer** — at 80 cols `renderCenteredBox` clips (existing fallback, `dashboard.ts:1846`); full wording must be visible at 120 cols. Pinned by the render assertions (Task 2, Step 1).
28
- 4. **`j`/`k` in select mode trigger `prewarmScheduler.schedule()`** — inherited `moveSelection` side effect, select is in the `prewarmSelected` allow-list, no behavior change. No test; reviewers confirm no new coupling.
29
- 5. **Bracketed paste containing `j`/`k`/`l` must not move the selection** (multi-byte paste string never equals a single-byte key). Pinned by the paste segment (Task 1, Step 1).
30
-
31
- ---
32
-
33
- ### Task 1: Key bindings in the five mode handlers
34
-
35
- **Files:**
36
- - Create: `test-support/vim-keys-wrap.ts`
37
- - Create: `test/dashboard-vim-keys.test.mjs`
38
- - Modify: `src/ui/dashboard.ts` (handleListKey :359, handleSelectKey :400, handlePeekKey :427, handleEvidenceKey :484 — line refs are main @ 759d659)
39
-
40
- **Interfaces:**
41
- - Consumes: existing `moveSelection(delta)`, `peekStep(delta)`, `attachSelected()`, `attachPeek()`, `planDashboardAttach` (src/ui/dashboard-decisions.mjs:34).
42
- - Produces: no new symbols; later tasks rely on the probe's `out.copy` field added in Task 2.
43
-
44
- **Spec acceptance IDs:** A1, A2, A3, A4, A5, A6, A9 (+ Review Focus 1, 2, 5). Spec D3 dead-code cleanup folded in (Step 4).
45
-
46
- - [ ] **Step 1: Write the failing probe and test**
47
-
48
- Create `test-support/vim-keys-wrap.ts`:
49
-
50
- ```ts
51
- // Vim-keys probe: drive j/k/l through the public handleInput() across modes
52
- // and report observations as JSON (issue #229, spec A1-A9 + review-focus pins).
53
- // Run via `node --experimental-transform-types`. Not typechecked.
54
- import { mkdtempSync } from "node:fs";
55
- import { tmpdir } from "node:os";
56
- import { join } from "node:path";
57
- import { createService } from "../src/runtime/service.mjs";
58
- import { createView } from "../src/core/store.mjs";
59
- import { DashboardComponent } from "../src/ui/dashboard.ts";
60
-
61
- const root = mkdtempSync(join(tmpdir(), "agentview-vim-keys-"));
62
- createView(root, { id: "v1", name: "one", cwd: root });
63
- createView(root, { id: "v2", name: "two", cwd: root });
64
- createView(root, { id: "v3", name: "three", cwd: root });
65
-
66
- const service = createService({
67
- root,
68
- runnerScript: "/no/runner.mjs",
69
- piCommand: "pi",
70
- piArgsPrefix: [],
71
- defaultCwd: root,
72
- launch: () => ({ pid: null, configPath: "/no/config.json" }),
73
- launchHost: () => ({ pid: null, configPath: "/no/host-config.json" }),
74
- launchTitle: () => ({ pid: null, configPath: "/no/title-config.json" }),
75
- });
76
-
77
- const tui = { terminal: { rows: 24, cols: 80, columns: 80, write: () => {} }, requestRender: () => {} };
78
- const theme = { fg: (_c, t) => t, bold: (t) => t, bg: (_c, t) => t };
79
- const doneCalls = [];
80
- const dash = new DashboardComponent(tui, theme, {}, (r) => doneCalls.push(r), {
81
- service, root, defaultCwd: root,
82
- availableModels: [], currentModel: null, currentThinkingLevel: "off",
83
- });
84
-
85
- // TS-private is runtime-accessible; read white-box state for assertions.
86
- const ids = dash.orderedIds;
87
- const out = { ids, list: [], select: [], peek: [], attach: [], negative: [], scroll: [], text: {}, notices: [], busyL: null };
88
-
89
- // A1: list j/k == arrow keys (wrap at both ends); j must not pop the INSERT toast.
90
- dash.handleInput("j"); out.list.push(dash.selectedId);
91
- out.notices.push(dash.inputNotice === null);
92
- dash.handleInput("j"); out.list.push(dash.selectedId);
93
- dash.handleInput("j"); out.list.push(dash.selectedId); // wrap last -> first
94
- dash.handleInput("k"); out.list.push(dash.selectedId); // wrap first -> last
95
- // RF1: uppercase J stays on the printable fallback (current behavior pinned).
96
- dash.handleInput("J"); out.notices.push(dash.inputNotice !== null);
97
- // RF5: bracketed paste containing j/k/l must not move the selection.
98
- dash.handleInput("\x1b[200~jkl\x1b[201~"); out.list.push(dash.selectedId);
99
-
100
- // A2: select mode j/k move the selection (wrap).
101
- dash.mode = "select"; dash.selectedId = ids[0];
102
- dash.handleInput("j"); out.select.push(dash.selectedId);
103
- dash.handleInput("k"); out.select.push(dash.selectedId);
104
- dash.handleInput("k"); out.select.push(dash.selectedId); // wrap first -> last
105
-
106
- // A9 negative: select "l" must not attach / leave the mode.
107
- const doneBeforeSelect = doneCalls.length;
108
- dash.handleInput("l");
109
- out.negative.push(["select-l", dash.mode, doneCalls.length - doneBeforeSelect]);
110
- dash.mode = "list";
111
-
112
- // A3: peek j/k == peekStep (wrap).
113
- dash.mode = "peek"; dash.peekId = ids[0]; dash.selectedId = ids[0];
114
- dash.handleInput("j"); out.peek.push(dash.peekId);
115
- dash.handleInput("k"); out.peek.push(dash.peekId);
116
- dash.handleInput("k"); out.peek.push(dash.peekId); // wrap first -> last
117
-
118
- // A4: l attaches in list / peek / evidence (non-busy rows -> direct done).
119
- dash.mode = "list"; dash.selectedId = ids[0];
120
- dash.handleInput("l"); out.attach.push(doneCalls.at(-1));
121
- dash.mode = "peek"; dash.peekId = ids[1]; dash.selectedId = ids[1];
122
- dash.handleInput("l"); out.attach.push(doneCalls.at(-1));
123
- dash.mode = "evidence"; dash.selectedId = ids[2];
124
- dash.handleInput("l"); out.attach.push(doneCalls.at(-1));
125
-
126
- // A9 negative: session "l" must not attach / leave the mode.
127
- dash.mode = "session"; dash.selectedId = ids[0];
128
- const doneBeforeSession = doneCalls.length;
129
- dash.handleInput("l");
130
- out.negative.push(["session-l", dash.mode, doneCalls.length - doneBeforeSession]);
131
-
132
- // A6: session / evidence j/k still scroll (regression guard).
133
- dash.mode = "session";
134
- dash.handleInput("j"); out.scroll.push(dash.sessionScrollTop);
135
- dash.handleInput("k"); out.scroll.push(dash.sessionScrollTop);
136
- dash.mode = "evidence";
137
- dash.handleInput("j"); out.scroll.push(dash.evidenceScrollTop);
138
- dash.handleInput("k"); out.scroll.push(dash.evidenceScrollTop);
139
-
140
- // A5: text modes keep j/k/l literal.
141
- for (const m of ["dispatch", "filter", "rename", "reply"]) {
142
- dash.mode = m; dash.input = "";
143
- dash.handleInput("j"); dash.handleInput("k"); dash.handleInput("l");
144
- out.text[m] = dash.input;
145
- }
146
- dash.mode = "list"; dash.input = "";
147
-
148
- // RF2: busy row + l -> confirm branch (same as →), not a direct attach.
149
- const busyService = {
150
- rows: () => [{
151
- meta: { id: "b1", name: "busy", cwd: "/", sessionFile: "/s/b1.jsonl" },
152
- state: { semanticState: "working", processState: "alive", pendingQuestions: [] },
153
- alive: true, hostAlive: false, hostActive: false, hostReady: false, host: null,
154
- }],
155
- reconcile: async () => 0,
156
- };
157
- const done2 = [];
158
- const dash2 = new DashboardComponent(tui, theme, {}, (r) => done2.push(r), {
159
- service: busyService, root, defaultCwd: root,
160
- availableModels: [], currentModel: null, currentThinkingLevel: "off",
161
- });
162
- dash2.selectedId = "b1";
163
- dash2.handleInput("l");
164
- out.busyL = { mode: dash2.mode, done: done2.length, prompt: dash2.pending?.prompt ?? null };
165
- dash2.dispose();
166
-
167
- dash.dispose();
168
- console.log(JSON.stringify(out));
169
- ```
170
-
171
- Create `test/dashboard-vim-keys.test.mjs`:
172
-
173
- ```js
174
- import assert from "node:assert/strict";
175
- import { execFileSync } from "node:child_process";
176
- import { fileURLToPath } from "node:url";
177
- import { join } from "node:path";
178
- import test from "node:test";
179
-
180
- const ROOT_DIR = fileURLToPath(new URL("../", import.meta.url));
181
- const PROBE = join(ROOT_DIR, "test-support", "vim-keys-wrap.ts");
182
-
183
- test("vim j/k/l bindings mirror the arrow keys across board modes (issue #229)", () => {
184
- const out = JSON.parse(execFileSync(process.execPath, ["--experimental-transform-types", PROBE], { encoding: "utf8", timeout: 30_000 }));
185
- const { ids } = out;
186
- assert.equal(ids.length, 3, "probe must see 3 rows");
187
- // A1: list j/k wrap like the arrows; j/k never pop the INSERT toast.
188
- assert.deepEqual(out.list, [ids[1], ids[2], ids[0], ids[2], ids[2]], "j down x2, wrap; k wrap; paste no-move");
189
- assert.deepEqual(out.notices, [true, true], "j: no INSERT toast; J: keeps the printable fallback");
190
- // A2: select mode j/k move (wrap).
191
- assert.deepEqual(out.select, [ids[1], ids[0], ids[2]]);
192
- // A3: peek j/k step (wrap).
193
- assert.deepEqual(out.peek, [ids[1], ids[0], ids[2]]);
194
- // A4: l attaches in list / peek / evidence.
195
- assert.deepEqual(out.attach.map((r) => [r.action, r.viewId, r.stopFirst]), [
196
- ["attach", ids[0], false],
197
- ["attach", ids[1], false],
198
- ["attach", ids[2], false],
199
- ]);
200
- // A9: l stays unbound in select / session.
201
- assert.deepEqual(out.negative, [["select-l", "select", 0], ["session-l", "session", 0]]);
202
- // A6: session / evidence j/k still scroll.
203
- assert.deepEqual(out.scroll, [1, 0, 1, 0]);
204
- // A5: text modes keep j/k/l literal.
205
- for (const m of ["dispatch", "filter", "rename", "reply"]) assert.equal(out.text[m], "jkl", m);
206
- // RF2: busy row + l -> confirm branch, no direct attach.
207
- assert.equal(out.busyL.mode, "confirm");
208
- assert.equal(out.busyL.done, 0);
209
- assert.match(out.busyL.prompt, /Interrupt and attach/);
210
- });
211
- ```
212
-
213
- - [ ] **Step 2: Run the test to verify it fails**
214
-
215
- Run: `cd $WT && node --test test/dashboard-vim-keys.test.mjs`
216
- Expected: FAIL — list assertion shows `j` does not move the selection (falls into the printable fallback instead).
217
-
218
- - [ ] **Step 3: Implement the key bindings**
219
-
220
- In `src/ui/dashboard.ts`:
221
-
222
- `handleListKey` — extend the three branches:
223
-
224
- ```ts
225
- if (matchesKey(data, Key.up) || data === "k") return void this.moveSelection(-1);
226
- if (matchesKey(data, Key.down) || data === "j") return void this.moveSelection(1);
227
- if (matchesKey(data, Key.right) || data === ">" || data === "l") return this.attachSelected();
228
- ```
229
-
230
- `handleSelectKey`:
231
-
232
- ```ts
233
- if (matchesKey(data, Key.up) || data === "k") return void this.moveSelection(-1);
234
- if (matchesKey(data, Key.down) || data === "j") return void this.moveSelection(1);
235
- ```
236
-
237
- `handlePeekKey`:
238
-
239
- ```ts
240
- if (matchesKey(data, Key.right) || data === ">" || data === "l") return this.attachPeek();
241
- // ...
242
- if (matchesKey(data, Key.up) || data === "k") {
243
- this.peekStep(-1);
244
- return;
245
- }
246
- if (matchesKey(data, Key.down) || data === "j") {
247
- this.peekStep(1);
248
- return;
249
- }
250
- ```
251
-
252
- `handleEvidenceKey`:
253
-
254
- ```ts
255
- if (data === "a" || matchesKey(data, Key.enter) || matchesKey(data, Key.right) || data === "l") return this.attachSelected();
256
- ```
257
-
258
- - [ ] **Step 4: Run the test to verify it passes, then remove the dead duplicate lines (spec D3)**
259
-
260
- Run: `cd $WT && node --test test/dashboard-vim-keys.test.mjs`
261
- Expected: PASS.
262
-
263
- Then in `handleListKey` delete the duplicated lines (zero behavior change, re-run the test after):
264
-
265
- ```ts
266
- if (data === "d") return this.confirmDone();
267
- if (data === "d") return this.confirmDone(); // DELETE this duplicate
268
- ```
269
- ```ts
270
- if (matchesKey(data, Key.ctrl("x"))) return this.handleDeleteKey();
271
- if (matchesKey(data, Key.ctrl("x"))) return this.handleDeleteKey(); // DELETE this duplicate
272
- ```
273
-
274
- Run: `cd $WT && node --test test/dashboard-vim-keys.test.mjs test/dashboard-delete-key.test.mjs test/dashboard-hold-key.test.mjs`
275
- Expected: all PASS (no regression in the neighboring key paths).
276
-
277
- - [ ] **Step 5: Typecheck and commit**
278
-
279
- Run: `cd $WT && npm run typecheck && node --test test/dashboard-navigation.test.mjs` (existing wrap test still passes)
280
- Then:
281
-
282
- ```bash
283
- git -C $WT add test-support/vim-keys-wrap.ts test/dashboard-vim-keys.test.mjs src/ui/dashboard.ts
284
- git -C $WT commit -m "feat(ui): vim-style j/k navigation and l attach in board modes (issue #229)"
285
- ```
286
-
287
- ---
288
-
289
- ### Task 2: Help overlay and hint-line copy
290
-
291
- **Files:**
292
- - Modify: `test-support/vim-keys-wrap.ts` (append render segment)
293
- - Modify: `test/dashboard-vim-keys.test.mjs` (append A7 test)
294
- - Modify: `src/ui/dashboard.ts` (renderHelp rows :1701-1729, listHints :1342-1364)
295
-
296
- **Interfaces:**
297
- - Consumes: Task 1's probe file and `dash.render(width): string[]` (public, :1212).
298
- - Produces: probe `out.copy = { help120, listRender, selectRender, help80Lines }`.
299
-
300
- **Spec acceptance IDs:** A7 (+ Review Focus 3). Covers spec §4 items 1-3 including the D5 doc-debt rows.
301
-
302
- - [ ] **Step 1: Extend the probe with render capture and write the failing A7 test**
303
-
304
- Append before `dash.dispose();` in `test-support/vim-keys-wrap.ts`:
305
-
306
- ```ts
307
- // A7: help overlay + hint lines copy.
308
- dash.mode = "help"; dash.helpReturnMode = "list";
309
- const help120 = dash.render(120).join("\n");
310
- const help80 = dash.render(80); // must not throw; renderCenteredBox clips
311
- dash.mode = "list";
312
- const listRender = dash.render(120).join("\n");
313
- dash.mode = "select";
314
- const selectRender = dash.render(120).join("\n");
315
- dash.mode = "list";
316
- out.copy = { help120, listRender, selectRender, help80Lines: help80.length };
317
- ```
318
-
319
- Append to `test/dashboard-vim-keys.test.mjs`:
320
-
321
- ```js
322
- test("help overlay and hint lines document j/k/l (issue #229 A7)", () => {
323
- const out = JSON.parse(execFileSync(process.execPath, ["--experimental-transform-types", PROBE], { encoding: "utf8", timeout: 30_000 }));
324
- const { help120, listRender, selectRender, help80Lines } = out.copy;
325
- assert.match(help120, /↑\/↓ or j\/k/);
326
- assert.match(help120, /→ \/ > \/ l/);
327
- assert.match(help120, /In select.*j\/k/);
328
- assert.match(help120, /In peek.*↑↓\/j\/k/);
329
- assert.match(help120, /In peek.*→\/l attach/);
330
- assert.match(help120, /In session.*↑↓\/j\/k scroll/);
331
- assert.match(help120, /In evidence.*↑↓\/j\/k scroll/);
332
- assert.match(help120, /In evidence.*→\/l attach/);
333
- // Negative: stale pre-change wording is gone from every row that named bare arrows.
334
- assert.doesNotMatch(help120, /In peek.*· ↑↓ adjacent/);
335
- assert.doesNotMatch(help120, /In session.*· ↑↓ scroll/);
336
- assert.doesNotMatch(help120, /In evidence.*· ↑↓ scroll/);
337
- assert.match(listRender, /→\/l attach/);
338
- assert.match(selectRender, /↑↓\/j\/k move/);
339
- assert.ok(help80Lines > 0, "80-col help render must not throw (RF3)");
340
- });
341
- ```
342
-
343
- - [ ] **Step 2: Run the test to verify it fails**
344
-
345
- Run: `cd $WT && node --test test/dashboard-vim-keys.test.mjs`
346
- Expected: FAIL — `↑/↓ or j/k` not found in the help render.
347
-
348
- - [ ] **Step 3: Update the help overlay rows and hint lines**
349
-
350
- In `renderHelp` (`src/ui/dashboard.ts:1701-1729`), change these rows:
351
-
352
- ```ts
353
- ["↑/↓ or j/k", "Normal: move selection; Insert: move cursor"],
354
- ["→ / > / l", "Attach to the selected real Pi session"],
355
- ["In select", "j/k move · a select all · u clear · d move done · ctrl+x delete · esc cancel"],
356
- ["In peek", "→/l attach · v transcript · e evidence · r reply · ↑↓/j/k adjacent"],
357
- ["In session", "← back · ↑↓/j/k scroll · e evidence · enter attach · r reply"],
358
- ["In evidence", "← back · ↑↓/j/k scroll · →/l attach · r reply · v transcript · x clear diag"],
359
- ```
360
-
361
- (Note the trailing `x clear diag` — shortened from `x clear diagnostics` so the longer row stays readable; `renderCenteredBox` clips at narrow widths regardless, RF3.)
362
-
363
- In `listHints` (:1342-1364): SELECT branch gains `"↑↓/j/k move"` as the first hint after the count/unread spread; NORMAL row changes `"→ attach"` to `"→/l attach"`.
364
-
365
- - [ ] **Step 4: Run the tests to verify they pass**
366
-
367
- Run: `cd $WT && node --test test/dashboard-vim-keys.test.mjs`
368
- Expected: PASS (both tests).
369
-
370
- - [ ] **Step 5: Commit**
371
-
372
- ```bash
373
- git -C $WT add test-support/vim-keys-wrap.ts test/dashboard-vim-keys.test.mjs src/ui/dashboard.ts
374
- git -C $WT commit -m "docs(ui): document j/k/l in help overlay and hint lines (issue #229)"
375
- ```
376
-
377
- ---
378
-
379
- ### Task 3: README key tables and full verification gate
380
-
381
- **Files:**
382
- - Modify: `README.md` (Main list table, Peek table, Transcript table, Evidence table)
383
-
384
- **Interfaces:**
385
- - Consumes: Task 1/2 behavior as implemented.
386
- - Produces: docs matching behavior; A8 green run.
387
-
388
- **Spec acceptance IDs:** A8 (+ U2 groundwork — the tables are what U2 checks against).
389
-
390
- - [ ] **Step 1: Update the four README tables**
391
-
392
- In `README.md`:
393
-
394
- Main list table:
395
- - `| `Up` / `Down` | Move the selection. |` → `| `Up` / `Down` / `j` / `k` | Move the selection. |`
396
- - `| `Right` / `>` | Attach to the selected session. |` → `| `Right` / `>` / `l` | Attach to the selected session. |`
397
-
398
- Peek table:
399
- - `| `a`, `Right`, or `>` | Attach to the session. |` → `| `a`, `Right`, `>`, or `l` | Attach to the session. |`
400
- - `| `Up` / `Down` | Move to the previous or next session. |` → `| `Up` / `Down` / `j` / `k` | Move to the previous or next session. |`
401
-
402
- Transcript table:
403
- - `| `Up` / `Down` | Scroll one line. |` → `| `Up` / `Down` / `j` / `k` | Scroll one line. |`
404
-
405
- Evidence table:
406
- - `| `Up` / `Down` | Scroll one line. |` → `| `Up` / `Down` / `j` / `k` | Scroll one line. |`
407
- - `| `Enter`, `a`, or `Right` | Attach to the session. |` → `| `Enter`, `a`, `Right`, or `l` | Attach to the session. |`
408
-
409
- - [ ] **Step 2: Run the full verification gate**
410
-
411
- Run: `cd $WT && npm run verify`
412
- Expected: PASS — typecheck, perf gate, all tests, coverage thresholds (lines 85 / funcs 80 / branches 70), pack:dry.
413
-
414
- - [ ] **Step 3: Commit**
415
-
416
- ```bash
417
- git -C $WT add README.md
418
- git -C $WT commit -m "docs: document j/k/l keys in the README view tables (issue #229)"
419
- ```
420
-
421
- ---
422
-
423
- ### Task 4: Post-implementation manual verification (pre-merge, by user)
424
-
425
- **Spec acceptance IDs:** U1, U2. No code changes; this task is the executable checklist.
426
-
427
- - [ ] **Step 1: U1 — real dashboard feel**
428
-
429
- Deploy prelude (prevents loading stale code): `pi list` to record the current install source (this machine: `git:github.com/zhuxixi/pi-agent-board`) → `pi remove <that source>` → `pi install /home/elling/work/git-repo/pi-agent-board/.pi/worktrees/issue-229-vim-jkl-board-list` → `pi /agent-board`.
430
-
431
- Checklist: ① `j`/`k` walk list rows incl. wrap at both ends ② `j` no longer pops the INSERT toast ③ `l` attaches the selected session ④ after detach, `?` shows the new bindings (incl. the evidence row's `→/l attach`) ⑤ in INSERT mode `j`/`k`/`l` stay literal characters.
432
-
433
- Cleanup: `pi remove /home/elling/work/git-repo/pi-agent-board/.pi/worktrees/issue-229-vim-jkl-board-list` → reinstall the recorded original source.
434
-
435
- - [ ] **Step 2: U2 — README vs behavior**
436
-
437
- Walk the four README tables row by row against the live dashboard; every listed key works, no key is missing.
438
-
439
- - [ ] **Step 3: Record results**
440
-
441
- Comment the U1/U2 outcomes on issue #229 before merge. If any item fails or cannot run, mark it `pending` — do not claim full acceptance.
442
-
443
- ---
444
-
445
- ## Self-Review Notes
446
-
447
- - **Spec coverage:** §3 bindings → Task 1; §4 copy items 1-3 → Task 2, items 4-5 → Task 3; D3 dead-code → Task 1 Step 4; A1-A9 → Task 1/2; A8 → Task 3; U1/U2 → Task 4. Every acceptance ID has a task; every task cites IDs.
448
- - **Placeholder scan:** none — all code and commands are concrete.
449
- - **Type consistency:** probe field names (`out.list/select/peek/attach/negative/scroll/text/notices/busyL/copy`) match between the probe and both tests; `DashboardResult` shape `{action, viewId, stopFirst}` matches `src/ui/dashboard.ts:58`.
@@ -1,153 +0,0 @@
1
- # Attach Background-Only Cells 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:** Stop `lineToAnsi()` from trimming cells that carry only a non-default background, so full-screen TUIs (nvim, lazygit, htop) keep their theme background in the attach view.
6
-
7
- **Architecture:** The per-line scan in `lineToAnsi()` already walks every cell; it gains a `lastBg` cursor (last cell with `!isBgDefault()`), and the render bound becomes `renderEnd = max(last, lastBg)`. The main loop, the empty-line guard, and the cursor-past-content check all switch from `last` to `renderEnd`. Default-background screens keep the existing cheap path (`""` for blank lines, no padding). A component-level smoke fixture drives a real `@xterm/headless` terminal through `PtyAttachComponent.render()` and asserts the emitted ANSI.
8
-
9
- **Tech Stack:** TypeScript (`src/ui`), Node 22/24 `node:test`, `node --experimental-transform-types` fixtures, `@xterm/headless`.
10
-
11
- **Spec:** `docs/superpowers/specs/2026-10-23-issue-232-attach-bg-only-cells-design.md`
12
-
13
- **Worktree:** `/home/elling/work/git-repo/pi-agent-board/.pi/worktrees/issue-232-attach-bg-only-cells` — every path below is relative to it; never edit the main checkout.
14
-
15
- ## Global Constraints
16
-
17
- - Tabs for indentation; English comments; conventional commit subjects (`fix(attach): …`) with `(#232)` only on the PR title.
18
- - Do not change selection/copy semantics, OSC-8 link handling, mouse handling, or the wide-char skip (`getWidth() === 0`).
19
- - Padding must never be emitted for default-background cells — the cheap path for ordinary shells stays byte-identical.
20
- - `git add <explicit files>` only; never `git add -A`.
21
- - `npm run verify` (typecheck + perf + test + coverage + pack:dry) is the gate before claiming done.
22
- - Fixture cells that must be background-only are produced **only** by erase sequences (`\x1b[K` / `\x1b[2J`) under an active bg; literal spaces keep `getChars()` non-empty and make the test pass on unpatched code (spec A8).
23
-
24
- ## Review Focus
25
-
26
- 1. **Blank painted line** — all cells `getChars() === ""`, `isBgDefault() === false`; the line must render as an SGR run plus padding, not `""`. → A1.
27
- 2. **Trailing paint after content** — characters end at col 1, paint starts at col 40; cols 2–39 stay default, 40–79 painted. → A2.
28
- 3. **Default-background regression** — plain text screens still serialize blank lines to `""` (no padding, no SGR). → A3.
29
- 4. **Wide chars** — `中` (cols 0–1, continuation cell skipped) followed by painted erase cells; display width must stay 80 with 78 padding spaces. → A4.
30
- 5. **Cursor inside the padded region** — exactly one `CURSOR_MARKER` and one inverse block, no double block. → A5.
31
- 6. **Selection over padding** — padded cells inside a selection render inverse (`7`); copied text is unaffected. → A9.
32
-
33
- ---
34
-
35
- ### Task 1: Fixture and failing tests (red)
36
-
37
- **Files:**
38
- - Create: `test-support/attach-bg-paint-smoke.ts`
39
- - Create: `test/pty-attach-bg-paint.test.mjs`
40
-
41
- **Interfaces:**
42
- - Consumes: `PtyAttachComponent` (`src/ui/pty-attach.ts:162`), constructor `(tui, theme, keybindings, done, opts)`; `render(width: number): string[]` returns `[header, ...body(rows-2), footer]`.
43
- - Produces: JSON on stdout from the fixture:
44
-
45
- ```ts
46
- {
47
- blankPainted: string[]; // 10 body lines, whole screen erased with bg #1e1e2e
48
- contentPainted: string[]; // one row: "AB" + 38 default spaces + 40 painted cells
49
- plain: string[]; // ordinary text screen (default bg)
50
- wide: string[]; // "中" + 78 painted cells
51
- cursor: string[]; // painted blank row, cursor visible at col 30
52
- selection: string[]; // painted blank row, selection cols 20–60
53
- }
54
- ```
55
-
56
- - [ ] **Step 1: Write the fixture.** Model it on `test-support/hyperlink-cell-smoke.ts` (stub `tui` with `terminal: { rows: 12, cols: 80, columns: 80, write: () => {} }`, identity `theme`, `socketPath: "/no/such/socket"`, `dispose()` in `finally`). Cast the component to expose `term`, `receivedOutput`, `finishAttachTransition`, and `selection`.
57
-
58
- Per scenario:
59
- - `blankPainted`: `\x1b[48;2;30;30;46m\x1b[2J` then `\x1b[?25l` (hide cursor).
60
- - `contentPainted`: default-bg rows, then `AB\x1b[41G\x1b[48;2;30;30;46m\x1b[K` on one row; hide cursor.
61
- - `plain`: `hello\r\nworld` only; hide cursor.
62
- - `wide`: `中\x1b[48;2;30;30;46m\x1b[K`; hide cursor.
63
- - `cursor`: painted blank row (`\x1b[48;2;30;30;46m\x1b[2J`), then park the cursor visibly at row 5 col 30 via `\x1b[6;31H`; do **not** send `\x1b[?25l`.
64
- - `selection`: painted blank screen; set the private `selection = { anchor: { line: 5, col: 20 }, focus: { line: 5, col: 60 } }` (type `MouseSelection`, `src/ui/pty-attach.ts:159`) before `finishAttachTransition()`.
65
-
66
- Return `attach.render(80).slice(1, -1)` per scenario (the component's body lines; height comes from `tui.terminal.rows - 2`).
67
-
68
- - [ ] **Step 2: Write the assertions** in `test/pty-attach-bg-paint.test.mjs`, spawning the fixture with `execFileSync(process.execPath, ["--experimental-transform-types", FIXTURE], { encoding: "utf8", timeout: 30_000 })` exactly like `test/pty-attach-hyperlinks.test.mjs`:
69
- - A1: every `blankPainted` line is non-empty and includes `48;2;30;30;46`.
70
- - A2: the `contentPainted` line containing `AB` has exactly 38 spaces before the next SGR, and the paint run covers 40 cells.
71
- - A3: in `plain`, the blank body lines are exactly `""`.
72
- - A4: the `wide` line contains `中`, ends with 78 spaces plus `\x1b[0m`, and has display width 80.
73
- - A5: the cursor row contains exactly one `\x1b_pi:c\x07` and one `\x1b[7m \x1b[0m`.
74
- - A9: the selection row contains an SGR that includes `7` (e.g. `\x1b[0;7;`) inside the padded region.
75
-
76
- - [ ] **Step 3: Run the tests and record the red output.**
77
-
78
- ```bash
79
- node --test test/pty-attach-bg-paint.test.mjs
80
- ```
81
-
82
- Expected: A1, A2, A4, A5, A9 fail against the unpatched `lineToAnsi` (blank painted lines are `""`, padding absent). A3 passes. Save the output — it is the A8 "red" evidence for the PR description.
83
-
84
- ---
85
-
86
- ### Task 2: Patch `lineToAnsi()` (green)
87
-
88
- **Files:**
89
- - Modify: `src/ui/pty-attach.ts` (`lineToAnsi`, `:1962-2026`)
90
-
91
- **Interfaces:**
92
- - Consumes: `BufferCellLike.isBgDefault()` (`:142`), already on the interface.
93
- - Produces: no signature change.
94
-
95
- - [ ] **Step 1: Add the painted-extent scan (C1).** After the `if (!line)` early return, declare `let lastBg = -1;`, and inside the scan loop add `if (!cell.isBgDefault()) lastBg = x;` after the `getChars()` check. Compute `const renderEnd = Math.max(last, lastBg);` and make the empty-line guard test `renderEnd < 0`.
96
-
97
- - [ ] **Step 2: Render through the painted extent (C2).** Change the main loop bound to `x <= renderEnd`.
98
-
99
- - [ ] **Step 3: Fix the cursor bound (C3).** Change the post-loop check to `cursor!.col > renderEnd` and update the adjacent comment to say "painted region" rather than "line content".
100
-
101
- - [ ] **Step 4: Add the explanatory comment** above `lastBg` (English): full-screen TUIs paint an explicit theme bg over empty regions; trimming at the last character drops it and lets the host terminal's default bg show through (issue #232). Keep it to 3–4 lines.
102
-
103
- - [ ] **Step 5: Run the new tests.**
104
-
105
- ```bash
106
- node --test test/pty-attach-bg-paint.test.mjs
107
- ```
108
-
109
- Expected: all pass, including A3 (plain screen unchanged).
110
-
111
- - [ ] **Step 6: Commit.**
112
-
113
- ```bash
114
- git add src/ui/pty-attach.ts test-support/attach-bg-paint-smoke.ts test/pty-attach-bg-paint.test.mjs
115
- git commit -m "fix(attach): render cells that only carry a background (#232)"
116
- ```
117
-
118
- ---
119
-
120
- ### Task 3: Red-green mutation proof (A8)
121
-
122
- - [ ] **Step 1:** Temporarily change the main loop bound back to `x <= last` (leave `renderEnd` in place).
123
- - [ ] **Step 2:** Run `node --test test/pty-attach-bg-paint.test.mjs`; expect A1/A2/A4 to fail.
124
- - [ ] **Step 3:** Restore `x <= renderEnd`; re-run; expect all pass.
125
- - [ ] **Step 4:** Record both outputs (commands + failure names) for the PR description. Do not commit the reverted state.
126
-
127
- ---
128
-
129
- ### Task 4: Full gate
130
-
131
- - [ ] **Step 1:** `npm run typecheck` → exit 0 (A6; note `test-support/` is outside `tsconfig`, so fixture types surface through Task 1 instead).
132
- - [ ] **Step 2:** `npm run verify` → typecheck + perf gate + full suite + coverage thresholds (lines 85 / funcs 80 / branches 70) + pack dry-run (A7).
133
-
134
- ---
135
-
136
- ### Task 5: Manual acceptance (U1, after the PR branch is installable)
137
-
138
- - [ ] **Step 1:** Confirm the worktree HEAD equals the PR head (`git log -1`).
139
- - [ ] **Step 2:** `npm install` in the worktree; load the checkout with `pi install "$(pwd)"`; **restart pi**.
140
- - [ ] **Step 3:** `/agent-board` → attach to an nvim session with catppuccin-mocha; observe the empty buffer area (`#1e1e2e`) and the cursorline (highlight spans the full row).
141
- - [ ] **Step 4:** `pi remove "$(pwd)"`; report the observation in the PR/issue.
142
-
143
- ---
144
-
145
- ## Acceptance traceability
146
-
147
- | Spec ID | Plan task |
148
- | --- | --- |
149
- | A1, A2, A3, A4, A5, A9 | Task 1 (red) + Task 2 Step 5 (green) |
150
- | A8 | Task 3 (+ Task 1 Step 3 red output) |
151
- | A6 | Task 4 Step 1 |
152
- | A7 | Task 4 Step 2 |
153
- | U1 | Task 5 |