@huseynsnmz/pi-board 0.15.3 → 0.17.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 (216) hide show
  1. package/CHANGELOG.md +24 -4
  2. package/CONTRIBUTING.md +7 -0
  3. package/README.md +55 -419
  4. package/SECURITY.md +19 -0
  5. package/docs/how-it-works.md +31 -0
  6. package/docs/reference.md +277 -0
  7. package/package.json +5 -11
  8. package/src/commands/agent-board.ts +26 -2
  9. package/src/commands/attach-flow.ts +4 -0
  10. package/src/core/double-press.mjs +6 -2
  11. package/src/core/slash-commands.mjs +52 -0
  12. package/src/index.ts +1 -1
  13. package/src/runtime/service.mjs +12 -0
  14. package/src/ui/dashboard.ts +59 -25
  15. package/src/ui/pty-attach.ts +8 -3
  16. package/IMPLEMENTATION_PLAN.md +0 -920
  17. package/PRD.md +0 -484
  18. package/PROGRESS.md +0 -142
  19. package/VERIFY.md +0 -115
  20. package/docs/BATCH_SELECTION_READ_FLOW.md +0 -277
  21. package/docs/EXPLORATION.md +0 -187
  22. package/docs/PTY_ATTACH_IMPLEMENTATION_PLAN.md +0 -581
  23. package/docs/RELEASE.md +0 -148
  24. package/docs/superpowers/plans/2026-08-15-screenlog-gc.md +0 -704
  25. package/docs/superpowers/plans/2026-08-16-attach-double-cursor-jiggle-retry.md +0 -499
  26. package/docs/superpowers/plans/2026-08-21-attach-coldstart-jiggle-rearm.md +0 -797
  27. package/docs/superpowers/plans/2026-08-21-dashboard-keypress-lag.md +0 -366
  28. package/docs/superpowers/plans/2026-08-21-issue-14-needs-input-no-auto-done.md +0 -576
  29. package/docs/superpowers/plans/2026-08-22-jiggle-shrink-and-hold.md +0 -337
  30. package/docs/superpowers/plans/2026-08-22-launch-cwd-favorites.md +0 -916
  31. package/docs/superpowers/plans/2026-08-22-question-tool-grouping.md +0 -211
  32. package/docs/superpowers/plans/2026-08-22-rename-state-labels.md +0 -182
  33. package/docs/superpowers/plans/2026-08-22-stable-list-order.md +0 -51
  34. package/docs/superpowers/plans/2026-08-27-locks-acquirelock-spin.md +0 -614
  35. package/docs/superpowers/plans/2026-08-27-pty-runner-test-flaky.md +0 -32
  36. package/docs/superpowers/plans/2026-08-29-code-refs-badges.md +0 -223
  37. package/docs/superpowers/plans/2026-08-30-circular-navigation.md +0 -308
  38. package/docs/superpowers/plans/2026-08-30-post-exit-timing-fix.md +0 -37
  39. package/docs/superpowers/plans/2026-08-30-pty-attach-quality-debt.md +0 -311
  40. package/docs/superpowers/plans/2026-08-30-readme-v2.md +0 -294
  41. package/docs/superpowers/plans/2026-09-01-attach-detach-gate-cursor-anchor.md +0 -284
  42. package/docs/superpowers/plans/2026-09-02-attach-detach-editor-state.md +0 -722
  43. package/docs/superpowers/plans/2026-09-03-code-refs-pr-backlink-narrow.md +0 -551
  44. package/docs/superpowers/plans/2026-09-03-detach-gate-glyph-fallback.md +0 -146
  45. package/docs/superpowers/plans/2026-09-04-evidence-outputpreview.md +0 -209
  46. package/docs/superpowers/plans/2026-09-04-warm-host-reclaim.md +0 -796
  47. package/docs/superpowers/plans/2026-09-05-issue-13-drainnextfollowup-pty-probe.md +0 -114
  48. package/docs/superpowers/plans/2026-09-05-issue-38-windows-wezterm-ime-cursor.md +0 -73
  49. package/docs/superpowers/plans/2026-09-05-issue-39-truncate-codepoint-boundary.md +0 -143
  50. package/docs/superpowers/plans/2026-09-05-issue-61-mention-fallback-guards.md +0 -226
  51. package/docs/superpowers/plans/2026-09-05-issue-63-flaky-manual-completion.md +0 -87
  52. package/docs/superpowers/plans/2026-09-05-issue-64-changelog-release-helper.md +0 -53
  53. package/docs/superpowers/plans/2026-09-05-pty-host-stacking-sock-race.md +0 -731
  54. package/docs/superpowers/plans/2026-09-08-attach-ctrl-left-detach.md +0 -30
  55. package/docs/superpowers/plans/2026-09-08-dashboard-shrink-repaint.md +0 -68
  56. package/docs/superpowers/plans/2026-09-08-issue-11-attach-runtime-desync-heal.md +0 -917
  57. package/docs/superpowers/plans/2026-09-08-legacy-stale-host-recovery.md +0 -125
  58. package/docs/superpowers/plans/2026-09-08-spawn-async-error-swallow.md +0 -56
  59. package/docs/superpowers/plans/2026-09-08-stale-model-attach-guard.md +0 -96
  60. package/docs/superpowers/plans/2026-09-09-claimpid-blocks-replace.md +0 -267
  61. package/docs/superpowers/plans/2026-09-09-harden-runner-architecture.md +0 -603
  62. package/docs/superpowers/plans/2026-09-09-single-writer-completion.md +0 -252
  63. package/docs/superpowers/plans/2026-09-10-reader-consistency.md +0 -115
  64. package/docs/superpowers/plans/2026-09-14-attach-cursor-dectcem-gate.md +0 -469
  65. package/docs/superpowers/plans/2026-09-14-attach-snapshot.md +0 -92
  66. package/docs/superpowers/plans/2026-09-14-host-meta-orphan-lock.md +0 -771
  67. package/docs/superpowers/plans/2026-09-14-issue-106-terminal-frame-cognition.md +0 -299
  68. package/docs/superpowers/plans/2026-09-14-issue-113-foreground-preview-race.md +0 -609
  69. package/docs/superpowers/plans/2026-09-14-terminal-model.md +0 -145
  70. package/docs/superpowers/plans/2026-09-15-coordinator-pipe-root-normalize.md +0 -224
  71. package/docs/superpowers/plans/2026-09-15-lease-publish-eprem-reclaim.md +0 -341
  72. package/docs/superpowers/plans/2026-09-18-detach-anchor-reporter-endpoint.md +0 -875
  73. package/docs/superpowers/plans/2026-09-20-control-lifecycle.md +0 -116
  74. package/docs/superpowers/plans/2026-09-20-issue-121-perf-gate-out-of-coverage.md +0 -517
  75. package/docs/superpowers/plans/2026-09-23-a5-mid-stream-assertion.md +0 -25
  76. package/docs/superpowers/plans/2026-09-23-cwd-browse-tab-drill.md +0 -393
  77. package/docs/superpowers/plans/2026-09-23-terminal-query-forwarding.md +0 -51
  78. package/docs/superpowers/plans/2026-09-23-unconditional-detach-key.md +0 -58
  79. package/docs/superpowers/plans/2026-09-24-control-reconcile-flake.md +0 -698
  80. package/docs/superpowers/plans/2026-09-24-probe-attached-clients.md +0 -605
  81. package/docs/superpowers/plans/2026-09-29-issue-145-holding-semantic-state.md +0 -1183
  82. package/docs/superpowers/plans/2026-09-29-issue-145-user-verification.md +0 -66
  83. package/docs/superpowers/plans/2026-09-30-board-delete-key-x-y-confirm.md +0 -649
  84. package/docs/superpowers/plans/2026-09-30-issue-153-runner-stop-boot-window.md +0 -618
  85. package/docs/superpowers/plans/2026-09-30-issue-95-test-discipline.md +0 -555
  86. package/docs/superpowers/plans/2026-09-30-publish-dispatch-tag-check.md +0 -259
  87. package/docs/superpowers/plans/2026-10-06-attach-settle-scheme-delivery.md +0 -751
  88. package/docs/superpowers/plans/2026-10-09-issue-167-attach-middle-paste.md +0 -509
  89. package/docs/superpowers/plans/2026-10-09-issue-169-fullscreen-mouse-ownership.md +0 -346
  90. package/docs/superpowers/plans/2026-10-10-attach-delete-in-place.md +0 -832
  91. package/docs/superpowers/plans/2026-10-10-issue-174-selection-mirror-paste-source.md +0 -510
  92. package/docs/superpowers/plans/2026-10-14-issue-170-attach-reply-guard.md +0 -771
  93. package/docs/superpowers/plans/2026-10-14-issue-178-accept-registration-race.md +0 -1257
  94. package/docs/superpowers/plans/2026-10-14-issue-184-snapshot-window-contract.md +0 -657
  95. package/docs/superpowers/plans/2026-10-14-issue-185-attach-wheel-alt-screen.md +0 -47
  96. package/docs/superpowers/plans/2026-10-14-issue-187-motionless-drag.md +0 -931
  97. package/docs/superpowers/plans/2026-10-14-issue-189-mixed-input-segments.md +0 -823
  98. package/docs/superpowers/plans/2026-10-15-issue-188-x10-mouse-release.md +0 -526
  99. package/docs/superpowers/plans/2026-10-16-issue-198-attach-wheel-forward-fallback.md +0 -32
  100. package/docs/superpowers/plans/2026-10-18-attach-wheel-replay-pseudo-scrollback.md +0 -437
  101. package/docs/superpowers/plans/2026-10-19-issue-213-reply-guard-csi-key.md +0 -257
  102. package/docs/superpowers/plans/2026-10-19-issue-214-attach-forward-alt-modifier.md +0 -202
  103. package/docs/superpowers/plans/2026-10-21-issue-221-ask-user-question.md +0 -198
  104. package/docs/superpowers/plans/2026-10-23-issue-222-attach-ready.md +0 -290
  105. package/docs/superpowers/plans/2026-10-23-issue-225-monotonic-foreground-state.md +0 -450
  106. package/docs/superpowers/plans/2026-10-23-issue-229-vim-jkl.md +0 -449
  107. package/docs/superpowers/plans/2026-10-23-issue-232-attach-bg-only-cells.md +0 -153
  108. package/docs/superpowers/plans/2026-10-23-lease-heartbeat-staleness.md +0 -537
  109. package/docs/superpowers/plans/2026-10-23-selected-row-highlight.md +0 -405
  110. package/docs/superpowers/plans/2026-10-24-issue-217-windows-pid-reuse-stopping.md +0 -367
  111. package/docs/superpowers/plans/2026-10-24-issue-230-warm-host-background-runs.md +0 -806
  112. package/docs/superpowers/plans/2026-10-24-issue-238-windows-process-identity-token.md +0 -490
  113. package/docs/superpowers/plans/2026-10-24-issue-241-coordinator-root-exit.md +0 -461
  114. package/docs/superpowers/plans/2026-10-25-issue-239-coordinator-replacement-identity.md +0 -504
  115. package/docs/superpowers/plans/2026-10-26-issue-245-key-hints-doc-sync.md +0 -244
  116. package/docs/superpowers/plans/2026-10-27-issue-172-fullscreen-selection-clear.md +0 -344
  117. package/docs/superpowers/plans/2026-10-27-issue-252-select-key-hints.md +0 -209
  118. package/docs/superpowers/plans/2026-10-28-issue-255-coordinator-endpoint-zombie.md +0 -442
  119. package/docs/superpowers/plans/2026-10-28-issue-257-legacy-pty-runner-endpoint-cleanup.md +0 -256
  120. package/docs/superpowers/plans/2026-11-03-cwd-favorites-full-pool.md +0 -266
  121. package/docs/superpowers/specs/2026-08-15-screenlog-gc-design.md +0 -105
  122. package/docs/superpowers/specs/2026-08-16-attach-double-cursor-jiggle-retry-design.md +0 -142
  123. package/docs/superpowers/specs/2026-08-21-attach-coldstart-jiggle-rearm-design.md +0 -110
  124. package/docs/superpowers/specs/2026-08-21-dashboard-keypress-lag-design.md +0 -59
  125. package/docs/superpowers/specs/2026-08-21-issue-14-needs-input-no-auto-done-design.md +0 -109
  126. package/docs/superpowers/specs/2026-08-22-cwd-favorites-design.md +0 -131
  127. package/docs/superpowers/specs/2026-08-22-jiggle-shrink-and-hold-design.md +0 -111
  128. package/docs/superpowers/specs/2026-08-22-question-tool-grouping-design.md +0 -114
  129. package/docs/superpowers/specs/2026-08-22-rename-state-labels-design.md +0 -80
  130. package/docs/superpowers/specs/2026-08-27-locks-acquirelock-spin-design.md +0 -97
  131. package/docs/superpowers/specs/2026-08-27-pty-runner-test-flaky-design.md +0 -35
  132. package/docs/superpowers/specs/2026-08-29-code-refs-badges-design.md +0 -128
  133. package/docs/superpowers/specs/2026-08-30-circular-navigation-design.md +0 -47
  134. package/docs/superpowers/specs/2026-08-30-eprm-atomicwrite-race-design.md +0 -77
  135. package/docs/superpowers/specs/2026-08-30-post-exit-timing-fix-design.md +0 -80
  136. package/docs/superpowers/specs/2026-08-30-pty-attach-quality-debt-design.md +0 -105
  137. package/docs/superpowers/specs/2026-08-30-readme-v2-design.md +0 -115
  138. package/docs/superpowers/specs/2026-09-01-attach-detach-gate-cursor-anchor-design.md +0 -78
  139. package/docs/superpowers/specs/2026-09-02-attach-detach-editor-state-design.md +0 -120
  140. package/docs/superpowers/specs/2026-09-03-code-refs-pr-backlink-narrow-design.md +0 -92
  141. package/docs/superpowers/specs/2026-09-03-detach-gate-glyph-fallback-design.md +0 -99
  142. package/docs/superpowers/specs/2026-09-04-evidence-outputpreview-design.md +0 -50
  143. package/docs/superpowers/specs/2026-09-04-warm-host-reclaim-design.md +0 -106
  144. package/docs/superpowers/specs/2026-09-05-issue-13-drainnextfollowup-pty-probe-design.md +0 -64
  145. package/docs/superpowers/specs/2026-09-05-issue-38-windows-wezterm-ime-design.md +0 -48
  146. package/docs/superpowers/specs/2026-09-05-issue-39-truncate-codepoint-boundary-design.md +0 -64
  147. package/docs/superpowers/specs/2026-09-05-issue-61-mention-fallback-design.md +0 -71
  148. package/docs/superpowers/specs/2026-09-05-issue-63-flaky-manual-completion-design.md +0 -49
  149. package/docs/superpowers/specs/2026-09-05-issue-64-changelog-helper-design.md +0 -76
  150. package/docs/superpowers/specs/2026-09-05-pty-host-stacking-sock-race-design.md +0 -510
  151. package/docs/superpowers/specs/2026-09-07-issue-11-attach-runtime-desync-heal-design.md +0 -130
  152. package/docs/superpowers/specs/2026-09-08-attach-ctrl-left-detach-design.md +0 -58
  153. package/docs/superpowers/specs/2026-09-08-dashboard-shrink-repaint-design.md +0 -52
  154. package/docs/superpowers/specs/2026-09-08-legacy-stale-host-recovery-design.md +0 -87
  155. package/docs/superpowers/specs/2026-09-08-spawn-async-error-swallow-design.md +0 -56
  156. package/docs/superpowers/specs/2026-09-08-stale-model-attach-guard-design.md +0 -79
  157. package/docs/superpowers/specs/2026-09-09-claimpid-blocks-replace-design.md +0 -147
  158. package/docs/superpowers/specs/2026-09-09-harden-runner-architecture-design.md +0 -304
  159. package/docs/superpowers/specs/2026-09-14-attach-cursor-dectcem-gate-design.md +0 -114
  160. package/docs/superpowers/specs/2026-09-14-host-meta-orphan-lock-design.md +0 -120
  161. package/docs/superpowers/specs/2026-09-14-issue-106-terminal-frame-cognition-design.md +0 -146
  162. package/docs/superpowers/specs/2026-09-14-issue-113-foreground-preview-race-design.md +0 -116
  163. package/docs/superpowers/specs/2026-09-15-coordinator-pipe-root-normalize-design.md +0 -84
  164. package/docs/superpowers/specs/2026-09-15-lease-publish-eprem-reclaim-design.md +0 -92
  165. package/docs/superpowers/specs/2026-09-18-detach-anchor-reporter-endpoint-design.md +0 -123
  166. package/docs/superpowers/specs/2026-09-20-issue-121-perf-gate-out-of-coverage-design.md +0 -204
  167. package/docs/superpowers/specs/2026-09-23-a5-mid-stream-assertion-design.md +0 -43
  168. package/docs/superpowers/specs/2026-09-23-cwd-browse-tab-drill-design.md +0 -95
  169. package/docs/superpowers/specs/2026-09-23-terminal-query-forwarding-design.md +0 -63
  170. package/docs/superpowers/specs/2026-09-23-unconditional-detach-key-design.md +0 -57
  171. package/docs/superpowers/specs/2026-09-24-control-reconcile-flake-design.md +0 -98
  172. package/docs/superpowers/specs/2026-09-24-probe-attached-clients-design.md +0 -190
  173. package/docs/superpowers/specs/2026-09-29-issue-145-holding-semantic-state-design.md +0 -394
  174. package/docs/superpowers/specs/2026-09-30-board-delete-key-x-y-confirm-design.md +0 -86
  175. package/docs/superpowers/specs/2026-09-30-issue-153-runner-stop-boot-window-design.md +0 -75
  176. package/docs/superpowers/specs/2026-09-30-issue-95-test-discipline-design.md +0 -226
  177. package/docs/superpowers/specs/2026-09-30-publish-dispatch-tag-check-design.md +0 -129
  178. package/docs/superpowers/specs/2026-10-06-attach-settle-scheme-delivery-design.md +0 -196
  179. package/docs/superpowers/specs/2026-10-09-issue-167-attach-middle-paste-design.md +0 -125
  180. package/docs/superpowers/specs/2026-10-09-issue-169-fullscreen-mouse-ownership-design.md +0 -130
  181. package/docs/superpowers/specs/2026-10-10-attach-delete-in-place-design.md +0 -224
  182. package/docs/superpowers/specs/2026-10-10-issue-174-selection-mirror-paste-source-design.md +0 -154
  183. package/docs/superpowers/specs/2026-10-14-issue-170-attach-reply-guard-design.md +0 -195
  184. package/docs/superpowers/specs/2026-10-14-issue-178-accept-registration-race-design.md +0 -287
  185. package/docs/superpowers/specs/2026-10-14-issue-184-snapshot-window-contract-design.md +0 -288
  186. package/docs/superpowers/specs/2026-10-14-issue-185-attach-wheel-alt-screen-design.md +0 -126
  187. package/docs/superpowers/specs/2026-10-14-issue-187-motionless-drag-design.md +0 -269
  188. package/docs/superpowers/specs/2026-10-14-issue-189-mixed-input-segments-design.md +0 -369
  189. package/docs/superpowers/specs/2026-10-15-issue-188-x10-mouse-release-design.md +0 -252
  190. package/docs/superpowers/specs/2026-10-16-issue-198-attach-wheel-forward-fallback-design.md +0 -67
  191. package/docs/superpowers/specs/2026-10-18-attach-wheel-replay-pseudo-scrollback-design.md +0 -250
  192. package/docs/superpowers/specs/2026-10-19-issue-213-reply-guard-csi-key-design.md +0 -165
  193. package/docs/superpowers/specs/2026-10-19-issue-214-attach-forward-alt-modifier-design.md +0 -131
  194. package/docs/superpowers/specs/2026-10-20-issue-221-ask-user-question-design.md +0 -270
  195. package/docs/superpowers/specs/2026-10-22-issue-225-monotonic-foreground-state-design.md +0 -225
  196. package/docs/superpowers/specs/2026-10-23-issue-222-attach-ready-design.md +0 -204
  197. package/docs/superpowers/specs/2026-10-23-issue-229-vim-jkl-design.md +0 -90
  198. package/docs/superpowers/specs/2026-10-23-issue-232-attach-bg-only-cells-design.md +0 -146
  199. package/docs/superpowers/specs/2026-10-23-lease-heartbeat-staleness-design.md +0 -271
  200. package/docs/superpowers/specs/2026-10-23-selected-row-highlight-design.md +0 -191
  201. package/docs/superpowers/specs/2026-10-24-issue-217-windows-pid-reuse-stopping-design.md +0 -334
  202. package/docs/superpowers/specs/2026-10-24-issue-230-warm-host-background-runs-design.md +0 -283
  203. package/docs/superpowers/specs/2026-10-24-issue-238-windows-process-identity-token-design.md +0 -286
  204. package/docs/superpowers/specs/2026-10-24-issue-241-coordinator-root-exit-design.md +0 -381
  205. package/docs/superpowers/specs/2026-10-25-issue-239-coordinator-replacement-identity-design.md +0 -331
  206. package/docs/superpowers/specs/2026-10-26-issue-245-key-hints-doc-sync-design.md +0 -72
  207. package/docs/superpowers/specs/2026-10-27-issue-172-fullscreen-selection-clear-design.md +0 -248
  208. package/docs/superpowers/specs/2026-10-27-issue-252-select-key-hints-design.md +0 -95
  209. package/docs/superpowers/specs/2026-10-28-issue-255-coordinator-endpoint-zombie-design.md +0 -380
  210. package/docs/superpowers/specs/2026-10-28-issue-257-legacy-pty-runner-endpoint-cleanup-design.md +0 -197
  211. package/docs/superpowers/specs/2026-11-03-cwd-favorites-full-pool-design.md +0 -106
  212. package/scripts/budget-audit.mjs +0 -135
  213. package/scripts/release.mjs +0 -446
  214. package/scripts/release_helper.mjs +0 -309
  215. package/scripts/run-perf-gate.mjs +0 -40
  216. package/scripts/run-tests.mjs +0 -375
@@ -1,722 +0,0 @@
1
- # editor_state Push Detach Gate 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:** Replace the render-heuristic detach gate with the child Pi's authoritative editor state, pushed over the existing control socket (`editor_state`), with the heuristic kept as fallback.
6
-
7
- **Architecture:** Child Pi extension polls `ctx.ui.getEditorText()` every 100ms and reports on change → runner caches + broadcasts `{type:"editor_state", empty}` and seeds it in `hello` → attach panel caches `editorEmpty` and judges `resolveEditorEmpty(editorEmpty, heuristic)`. All pieces are dependency-injected/unit-testable; the production wiring is one hook in `src/index.ts`.
8
-
9
- **Tech Stack:** TypeScript (pty-attach.ts), plain ESM (mjs), `node:test`, @xterm/headless (smoke harness).
10
-
11
- **Spec:** `docs/superpowers/specs/2026-09-02-attach-detach-editor-state-design.md`
12
-
13
- ## Global Constraints
14
-
15
- - Coverage gates: lines 85 / funcs 80 / branches 70 (`npm run test:coverage`); CI Node 22/24 must be green.
16
- - Do NOT remove or change the existing three-tier render heuristic (`findLastInverseCellLine` / glyph fallback / escape fallback) — it becomes the fallback only.
17
- - `ctrl+]` semantics unchanged; `←` keeps its gate (never unconditionally detach while connected).
18
- - Protocol: `{type:"editor_state", empty: boolean}`; `hello` gains `editorEmpty: boolean | null`. Old peers ignore unknown fields — backward compatible.
19
- - `resolveEditorEmpty(editorEmpty, heuristic)`: `editorEmpty === null || editorEmpty === undefined ? heuristic : editorEmpty`.
20
- - All edits inside worktree `WT=.pi/worktrees/issue-68-attach-detach-editor-state`; git ops via `git -C $WT`. You are the only agent; do NOT dispatch subagents.
21
-
22
- ---
23
-
24
- ### Task 1: `resolveEditorEmpty` pure function + unit tests (A5)
25
-
26
- **Files:**
27
- - Modify: `src/core/pty-input.mjs`
28
- - Test: `test/pty-input.test.mjs`
29
-
30
- **Interfaces:**
31
- - Produces: `export function resolveEditorEmpty(editorEmpty: boolean | null | undefined, heuristic: boolean): boolean`
32
-
33
- - [ ] **Step 1: Write the failing test**
34
-
35
- Append to `test/pty-input.test.mjs` (keep existing tests untouched):
36
-
37
- ```js
38
- import { isProbablyEmptyPiInputLine, isProbablyPiInputLine, resolveEditorEmpty } from "../src/core/pty-input.mjs";
39
-
40
- test("resolveEditorEmpty prefers the pushed editor state, falls back on null/undefined", () => {
41
- assert.equal(resolveEditorEmpty(true, false), true);
42
- assert.equal(resolveEditorEmpty(false, true), false);
43
- assert.equal(resolveEditorEmpty(null, true), true);
44
- assert.equal(resolveEditorEmpty(null, false), false);
45
- assert.equal(resolveEditorEmpty(undefined, true), true);
46
- });
47
- ```
48
-
49
- - [ ] **Step 2: Run test to verify it fails**
50
-
51
- Run: `node --test test/pty-input.test.mjs`
52
- Expected: FAIL — `resolveEditorEmpty is not a function` (import error).
53
-
54
- - [ ] **Step 3: Write minimal implementation**
55
-
56
- Append to `src/core/pty-input.mjs`:
57
-
58
- ```js
59
- /**
60
- * Resolve the ← detach gate's emptiness signal: when the child Pi pushes its
61
- * authoritative editor state (boolean), it wins; when it is unknown (null/
62
- * undefined — child extension missing or socket never connected), fall back
63
- * to the render heuristic.
64
- * @param {boolean | null | undefined} editorEmpty
65
- * @param {boolean} heuristic
66
- * @returns {boolean}
67
- */
68
- export function resolveEditorEmpty(editorEmpty, heuristic) {
69
- return editorEmpty === null || editorEmpty === undefined ? heuristic : editorEmpty;
70
- }
71
- ```
72
-
73
- - [ ] **Step 4: Run test to verify it passes**
74
-
75
- Run: `node --test test/pty-input.test.mjs`
76
- Expected: PASS (all pty-input tests including the new one).
77
-
78
- - [ ] **Step 5: Commit**
79
-
80
- ```bash
81
- git add test/pty-input.test.mjs src/core/pty-input.mjs
82
- git commit -m "feat: add resolveEditorEmpty gate resolution helper (issue #68)"
83
- ```
84
-
85
- ---
86
-
87
- ### Task 2: `createEditorStateReporter` + unit tests (A1, A2)
88
-
89
- **Files:**
90
- - Create: `src/core/editor-state-reporter.mjs`
91
- - Test: `test/editor-state-reporter.test.mjs`
92
-
93
- **Interfaces:**
94
- - Produces: `export function createEditorStateReporter({ getEditorText, connect, intervalMs = 100, scheduler = defaultScheduler }): { start(): void, stop(): void }`
95
- - `getEditorText(): string` — returns the child Pi editor text (`""` = empty).
96
- - `connect(): SocketLike` — opens the control socket; throws on failure. `SocketLike` = `{ write(jsonLine: string): void; on?(event: "close" | "error", fn: () => void): void }`.
97
- - `scheduler = { interval(fn, ms): handle; timeout(fn, ms): handle; clear(handle): void }` — defaults to setInterval/setTimeout wrappers.
98
- - Behavior: connect immediately on `start()` (retry with backoff 1s→2s→4s→5s cap, forever); after connect, poll `getEditorText()` every `intervalMs` and send `{type:"editor_state", empty: text.length === 0}` ONLY when the text changed; on socket `close`/`error` (or a write throw), tear down, clear the poll, and re-enter the reconnect backoff (backoff resets to 1s after a successful connect); `stop()` is idempotent and ends everything.
99
-
100
- - [ ] **Step 1: Write the failing tests**
101
-
102
- Create `test/editor-state-reporter.test.mjs`:
103
-
104
- ```js
105
- import assert from "node:assert/strict";
106
- import { test } from "node:test";
107
- import { createEditorStateReporter } from "../src/core/editor-state-reporter.mjs";
108
-
109
- /** Manual-clock scheduler: interval/timeout register callbacks; fire() runs the oldest due one. */
110
- function fakeScheduler() {
111
- const pending = []; // { at, fn, handle }
112
- let nextId = 1;
113
- return {
114
- pending,
115
- interval(fn, ms) { return this._push(fn, ms); },
116
- timeout(fn, ms) { return this._push(fn, ms); },
117
- _push(fn, ms) {
118
- const handle = nextId++;
119
- pending.push({ handle, fn, ms, due: 0 });
120
- return handle;
121
- },
122
- clear(handle) {
123
- const i = pending.findIndex((p) => p.handle === handle);
124
- if (i >= 0) pending.splice(i, 1);
125
- },
126
- /** Run the callback whose (re-registration cadence) is due now; returns it or null. */
127
- fireOne(now) {
128
- const due = pending.filter((p) => p.due <= now).sort((a, b) => a.due - b.due)[0];
129
- if (!due) return null;
130
- const { fn } = due;
131
- this.clear(due.handle);
132
- fn();
133
- return due;
134
- },
135
- };
136
- }
137
-
138
- function fakeSocket() {
139
- const sent = [];
140
- const handlers = {};
141
- return {
142
- sent,
143
- write(jsonLine) { sent.push(jsonLine); },
144
- on(event, fn) { handlers[event] = fn; },
145
- emit(event) { handlers[event]?.(); },
146
- };
147
- }
148
-
149
- test("reporter polls and sends only on text change (A1)", () => {
150
- const sched = fakeScheduler();
151
- let text = "";
152
- const socket = fakeSocket();
153
- const reporter = createEditorStateReporter({
154
- getEditorText: () => text,
155
- connect: () => socket,
156
- scheduler: sched,
157
- intervalMs: 100,
158
- });
159
- reporter.start();
160
- assert.equal(socket.sent.length, 0); // no send before the first poll
161
- sched.fireOne(100); // first poll: "" → lastText was null → change → send empty:true
162
- assert.deepEqual(socket.sent.map((l) => JSON.parse(l)), [{ type: "editor_state", empty: true }]);
163
- text = "draft";
164
- sched.fireOne(200);
165
- assert.equal(socket.sent.length, 2);
166
- assert.deepEqual(JSON.parse(socket.sent[1]), { type: "editor_state", empty: false });
167
- text = "draft 2"; // still non-empty → no send
168
- sched.fireOne(300);
169
- assert.equal(socket.sent.length, 2);
170
- text = "";
171
- sched.fireOne(400);
172
- assert.equal(socket.sent.length, 3);
173
- assert.deepEqual(JSON.parse(socket.sent[2]), { type: "editor_state", empty: true });
174
- reporter.stop();
175
- });
176
-
177
- test("reporter stop is idempotent and ends polling (A1)", () => {
178
- const sched = fakeScheduler();
179
- const socket = fakeSocket();
180
- const reporter = createEditorStateReporter({ getEditorText: () => "", connect: () => socket, scheduler: sched, intervalMs: 100 });
181
- reporter.start();
182
- sched.fireOne(100);
183
- reporter.stop();
184
- reporter.stop();
185
- assert.equal(sched.pending.length, 0); // poll + reconnect timers all cleared
186
- assert.equal(socket.sent.length, 1);
187
- });
188
-
189
- test("reporter retries connect with capped backoff then recovers (A2)", () => {
190
- const sched = fakeScheduler();
191
- const socket = fakeSocket();
192
- let attempts = 0;
193
- let connect = () => { attempts++; throw new Error("not up yet"); };
194
- const reporter = createEditorStateReporter({ getEditorText: () => "", connect: () => connect(), scheduler: sched, intervalMs: 100 });
195
- reporter.start();
196
- assert.equal(attempts, 1); // immediate try
197
- // Backoff sequence: 1s, 2s, 4s, 5s, 5s …
198
- let t = 0;
199
- for (const expectedGap of [1000, 2000, 4000, 5000, 5000]) {
200
- const before = attempts;
201
- const fired = sched.fireOne(t + expectedGap);
202
- assert.ok(fired, "reconnect timer must fire");
203
- assert.equal(attempts, before + 1);
204
- t += expectedGap;
205
- }
206
- // Now the server is up — next reconnect succeeds and polling begins, backoff resets.
207
- connect = () => socket;
208
- sched.fireOne(t + 5000);
209
- assert.equal(attempts, 7);
210
- sched.fireOne(t + 5000 + 100); // first poll after connect
211
- assert.equal(socket.sent.length, 1);
212
- assert.deepEqual(JSON.parse(socket.sent[0]), { type: "editor_state", empty: true });
213
- reporter.stop();
214
- });
215
-
216
- test("reporter reconnects after socket close (A2)", () => {
217
- const sched = fakeScheduler();
218
- const first = fakeSocket();
219
- const second = fakeSocket();
220
- let call = 0;
221
- const reporter = createEditorStateReporter({
222
- getEditorText: () => "",
223
- connect: () => (++call === 1 ? first : second),
224
- scheduler: sched,
225
- intervalMs: 100,
226
- });
227
- reporter.start();
228
- sched.fireOne(100); // poll on first socket
229
- assert.equal(first.sent.length, 1);
230
- first.emit("close");
231
- assert.equal(sched.pending.length, 1); // reconnect timer scheduled (backoff 1s)
232
- sched.fireOne(1100); // reconnect succeeds on second socket
233
- sched.fireOne(1200); // poll resumes
234
- assert.equal(second.sent.length, 1);
235
- reporter.stop();
236
- });
237
- ```
238
-
239
- - [ ] **Step 2: Run test to verify it fails**
240
-
241
- Run: `node --test test/editor-state-reporter.test.mjs`
242
- Expected: FAIL — `Cannot find module '../src/core/editor-state-reporter.mjs'`.
243
-
244
- - [ ] **Step 3: Write minimal implementation**
245
-
246
- Create `src/core/editor-state-reporter.mjs`:
247
-
248
- ```js
249
- /** Child-Pi editor-state reporter (issue #68): polls the child Pi's editor text
250
- * and pushes `{type:"editor_state", empty}` over the control socket whenever the
251
- * text changes, so the attach surface can gate ← on the authoritative state
252
- * instead of render heuristics. Dependency-injected for unit testing. */
253
-
254
- /** @typedef {{ write(jsonLine: string): void; on?(event: "close" | "error", fn: () => void): void }} SocketLike */
255
-
256
- const defaultScheduler = {
257
- interval(fn, ms) { return setInterval(fn, ms); },
258
- timeout(fn, ms) { return setTimeout(fn, ms); },
259
- clear(handle) { clearInterval(handle); clearTimeout(handle); },
260
- };
261
-
262
- export function createEditorStateReporter({ getEditorText, connect, intervalMs = 100, scheduler = defaultScheduler }) {
263
- let started = false;
264
- let stopped = false;
265
- let socket = null;
266
- let pollTimer = null;
267
- let reconnectTimer = null;
268
- let backoffMs = 1000;
269
- let lastText = null;
270
-
271
- function send(json) {
272
- if (!socket) return;
273
- try {
274
- socket.write(JSON.stringify(json) + "\n");
275
- } catch {
276
- teardownSocket();
277
- scheduleReconnect();
278
- }
279
- }
280
-
281
- function teardownSocket() {
282
- if (pollTimer !== null) { scheduler.clear(pollTimer); pollTimer = null; }
283
- const s = socket;
284
- socket = null;
285
- if (s?.on) { try { s.on("close", () => {}); s.on("error", () => {}); } catch { /* already torn down */ } }
286
- }
287
-
288
- function poll() {
289
- if (stopped || !socket) return;
290
- const text = getEditorText();
291
- if (text !== lastText) {
292
- lastText = text;
293
- send({ type: "editor_state", empty: text.length === 0 });
294
- }
295
- }
296
-
297
- function startPolling() {
298
- if (pollTimer !== null) return;
299
- lastText = null; // force a first report after (re)connect
300
- pollTimer = scheduler.interval(poll, intervalMs);
301
- }
302
-
303
- function scheduleReconnect() {
304
- if (stopped) return;
305
- teardownSocket();
306
- reconnectTimer = scheduler.timeout(tryConnect, backoffMs);
307
- backoffMs = Math.min(backoffMs * 2, 5000);
308
- }
309
-
310
- function tryConnect() {
311
- reconnectTimer = null;
312
- if (stopped) return;
313
- let s;
314
- try {
315
- s = connect();
316
- } catch {
317
- scheduleReconnect();
318
- return;
319
- }
320
- socket = s;
321
- backoffMs = 1000;
322
- s?.on?.("close", () => { if (socket === s) scheduleReconnect(); });
323
- s?.on?.("error", () => { if (socket === s) scheduleReconnect(); });
324
- startPolling();
325
- }
326
-
327
- function start() {
328
- if (started) return;
329
- started = true;
330
- stopped = false;
331
- tryConnect();
332
- }
333
-
334
- function stop() {
335
- stopped = true;
336
- if (reconnectTimer !== null) { scheduler.clear(reconnectTimer); reconnectTimer = null; }
337
- teardownSocket();
338
- }
339
-
340
- return { start, stop };
341
- }
342
- ```
343
-
344
- - [ ] **Step 4: Run test to verify it passes**
345
-
346
- Run: `node --test test/editor-state-reporter.test.mjs`
347
- Expected: PASS (4 tests).
348
-
349
- - [ ] **Step 5: Commit**
350
-
351
- ```bash
352
- git add src/core/editor-state-reporter.mjs test/editor-state-reporter.test.mjs
353
- git commit -m "feat: add editor-state reporter with injected scheduler (issue #68)"
354
- ```
355
-
356
- ---
357
-
358
- ### Task 3: runner `editor_state` routing + hello seed + exit reset (A3 code)
359
-
360
- **Files:**
361
- - Modify: `runner/pty-runner.mjs`
362
-
363
- **Interfaces:**
364
- - Consumes: none (protocol only).
365
- - Produces: runner-side `editorEmpty` cache; `hello` payload `{ type:"hello", status, editorEmpty }`; broadcast `{type:"editor_state", empty}`.
366
-
367
- - [ ] **Step 1: Implement**
368
-
369
- In `runner/pty-runner.mjs`:
370
-
371
- 1. Near the top-level state (next to `let crashed = false;` — locate the exact spot when editing; the variables live near `host` init): add
372
-
373
- ```js
374
- /** Authoritative child editor emptiness, pushed by the child Pi extension
375
- * (issue #68). null = unknown (extension missing / not yet reported). */
376
- let editorEmpty = null;
377
- ```
378
-
379
- 2. In the `child.onExit` handler, inside the `if (!crashed) { ... }` block, before `broadcast({ type: "exit", exitCode })`, add:
380
-
381
- ```js
382
- editorEmpty = null;
383
- broadcast({ type: "editor_state", empty: null });
384
- ```
385
-
386
- 3. In `handleClientLine`'s switch:
387
-
388
- ```js
389
- case "hello":
390
- send(socket, { type: "hello", status: host, editorEmpty });
391
- break;
392
- ```
393
-
394
- 4. Add a new case (after `get_status`):
395
-
396
- ```js
397
- case "editor_state": {
398
- editorEmpty = typeof msg.empty === "boolean" ? msg.empty : null;
399
- broadcast({ type: "editor_state", empty: editorEmpty });
400
- break;
401
- }
402
- ```
403
-
404
- - [ ] **Step 2: Run targeted tests to verify no regression**
405
-
406
- Run: `node --test test/pty-runner.integration.test.mjs`
407
- Expected: PASS (existing tests; new assertions come in Task 6).
408
-
409
- - [ ] **Step 3: Commit**
410
-
411
- ```bash
412
- git add runner/pty-runner.mjs
413
- git commit -m "feat: route editor_state through the control socket (issue #68)"
414
- ```
415
-
416
- ---
417
-
418
- ### Task 4: attach panel cache + judgment + smoke scenarios H/I (A4)
419
-
420
- **Files:**
421
- - Modify: `src/ui/pty-attach.ts`
422
- - Modify: `test-support/detach-gate-smoke.ts`
423
- - Modify: `test/pty-attach-detach-gate.test.mjs`
424
-
425
- **Interfaces:**
426
- - Consumes: `resolveEditorEmpty` (Task 1).
427
- - Produces: private `editorEmpty: boolean | null`; smoke keys `leftEditorStateOverridesHeuristicEmpty`, `leftEditorStateBlocksDetachOnDraft`.
428
-
429
- - [ ] **Step 1: Write the failing smoke scenarios + assertions**
430
-
431
- In `test-support/detach-gate-smoke.ts`, after scenario F (use the existing `writeToTerm`/`makeAttach`/`out` pattern), add:
432
-
433
- ```ts
434
- // H. The pushed editor state is authoritative over the render heuristic: the
435
- // buffer looks non-empty (garbled line) but the child reports empty → ← detach.
436
- {
437
- const { attach, sent, didDetach } = makeAttach();
438
- await writeToTerm(attach, "chat content\r\n────── ◊◊ ──────");
439
- (attach as unknown as { onSocketData: (t: string) => void }).onSocketData(JSON.stringify({ type: "editor_state", empty: true }) + "\n");
440
- (attach as unknown as { connected: boolean }).connected = true;
441
- attach.handleInput("\x1b[D");
442
- out.leftEditorStateOverridesHeuristicEmpty = didDetach() && sent.length === 1 && sent[0].type === "detach";
443
- attach.dispose();
444
- }
445
-
446
- // I. The pushed editor state is authoritative the other way: the heuristic
447
- // would say "empty" (nothing in the buffer), but the child reports a draft →
448
- // ← is forwarded (editor protection), NOT detach.
449
- {
450
- const { attach, sent, didDetach } = makeAttach();
451
- await writeToTerm(attach, "chat content\r\n");
452
- (attach as unknown as { onSocketData: (t: string) => void }).onSocketData(JSON.stringify({ type: "editor_state", empty: false }) + "\n");
453
- (attach as unknown as { connected: boolean }).connected = true;
454
- attach.handleInput("\x1b[D");
455
- out.leftEditorStateBlocksDetachOnDraft = !didDetach() && sent.length === 1 && sent[0].type === "input" && sent[0].data === "\x1b[D";
456
- attach.dispose();
457
- }
458
- ```
459
-
460
- In `test/pty-attach-detach-gate.test.mjs`, after the `leftDetachesOnGlyphLineWithoutFakeCursor` assertion, add:
461
-
462
- ```js
463
- assert.equal(parsed.leftEditorStateOverridesHeuristicEmpty, true, "← must detach when editor_state says empty even if the buffer looks non-empty");
464
- assert.equal(parsed.leftEditorStateBlocksDetachOnDraft, true, "← must be forwarded when editor_state reports a draft even if the buffer looks empty");
465
- ```
466
-
467
- - [ ] **Step 2: Run smoke to verify new scenarios fail**
468
-
469
- Run: `node --experimental-transform-types test-support/detach-gate-smoke.ts`
470
- Expected: `leftEditorStateOverridesHeuristicEmpty` and `leftEditorStateBlocksDetachOnDraft` are both `false` (no editor_state handling yet); all 11 existing keys still true.
471
-
472
- - [ ] **Step 3: Implement**
473
-
474
- In `src/ui/pty-attach.ts`:
475
-
476
- 1. Update the pty-input import (line ~8):
477
-
478
- ```ts
479
- import { isProbablyEmptyPiInputLine, isProbablyPiInputLine, resolveEditorEmpty } from "../core/pty-input.mjs";
480
- ```
481
-
482
- 2. Add the cache field next to the other private fields (e.g. right above `private receivedOutput` — find the field block):
483
-
484
- ```ts
485
- /** Authoritative editor emptiness pushed by the child Pi extension via the
486
- * control socket (issue #68). null = unknown — fall back to the heuristic. */
487
- private editorEmpty: boolean | null = null;
488
- ```
489
-
490
- 3. In `onSocketData`, extend the hello/status branch:
491
-
492
- ```ts
493
- if (msg.type === "hello" || msg.type === "status") {
494
- this.status = "attached";
495
- if (msg.type === "hello" && typeof msg.editorEmpty === "boolean") this.editorEmpty = msg.editorEmpty;
496
- } else if (msg.type === "editor_state") {
497
- this.editorEmpty = typeof msg.empty === "boolean" ? msg.empty : null;
498
- } else if (msg.type === "exit") {
499
- ```
500
-
501
- 4. In `handleInput`, the `Key.left` branch — replace the judgment call:
502
-
503
- ```ts
504
- if (shouldEscapeAttach(this.connected, resolveEditorEmpty(this.editorEmpty, this.childInputLooksEmpty()))) {
505
- ```
506
-
507
- - [ ] **Step 4: Run smoke + gate tests to verify pass**
508
-
509
- Run: `node --experimental-transform-types test-support/detach-gate-smoke.ts` then `node --test test/pty-attach-detach-gate.test.mjs test/pty-input.test.mjs test/editor-state-reporter.test.mjs`
510
- Expected: smoke has all 13 keys true (11 existing + H + I); all three test files pass.
511
-
512
- - [ ] **Step 5: Commit**
513
-
514
- ```bash
515
- git add src/ui/pty-attach.ts test-support/detach-gate-smoke.ts test/pty-attach-detach-gate.test.mjs
516
- git commit -m "fix: gate ← on pushed editor_state with heuristic fallback (issue #68)"
517
- ```
518
-
519
- ---
520
-
521
- ### Task 5: production wiring in `src/index.ts` (A2 production end)
522
-
523
- **Files:**
524
- - Modify: `src/index.ts`
525
-
526
- **Interfaces:**
527
- - Consumes: `createEditorStateReporter` (Task 2), `controlSocketPathFor` from `./core/paths.mjs`, `createConnection` from `node:net`.
528
-
529
- - [ ] **Step 1: Implement**
530
-
531
- In `src/index.ts`:
532
-
533
- 1. Add imports at the top:
534
-
535
- ```ts
536
- import { createConnection } from "node:net";
537
- import { controlSocketPathFor } from "./core/paths.mjs";
538
- ```
539
-
540
- 2. Add a module-level reporter handle (next to the other module constants):
541
-
542
- ```ts
543
- let hostedEditorReporter: { stop(): void } | null = null;
544
- ```
545
-
546
- 3. Inside the `pi.on("session_start", ...)` handler, as the FIRST statement (before `updateStatus(ctx)`):
547
-
548
- ```ts
549
- if (isHostedChild && !hostedEditorReporter && typeof ctx.ui?.getEditorText === "function" && hostedViewId) {
550
- const { createEditorStateReporter } = await import("./core/editor-state-reporter.mjs");
551
- hostedEditorReporter = createEditorStateReporter({
552
- getEditorText: () => ctx.ui.getEditorText(),
553
- connect: () => createConnection(controlSocketPathFor(process.platform, root, hostedViewId)),
554
- });
555
- hostedEditorReporter.start();
556
- }
557
- ```
558
-
559
- - [ ] **Step 2: Typecheck**
560
-
561
- Run: `npm run typecheck`
562
- Expected: clean (exit 0).
563
-
564
- - [ ] **Step 3: Commit**
565
-
566
- ```bash
567
- git add src/index.ts
568
- git commit -m "feat: start editor_state reporter in hosted child sessions (issue #68)"
569
- ```
570
-
571
- ---
572
-
573
- ### Task 6: runner integration test for editor_state routing (A3)
574
-
575
- **Files:**
576
- - Test: `test/pty-runner.integration.test.mjs`
577
-
578
- **Interfaces:**
579
- - Consumes: Task 3's runner behavior.
580
-
581
- - [ ] **Step 1: Write the failing test**
582
-
583
- Append to `test/pty-runner.integration.test.mjs` (reusing `freshRoot`/`createView`/`atomicWriteJson`/`hostReady`/`connectWhenReady`/`send`/`waitFor`/`stopRunner`/`reapChild` from the existing helpers):
584
-
585
- ```js
586
- test("pty-runner routes editor_state between clients, seeds hello, resets on exit", async () => {
587
- const root = freshRoot();
588
- let runner;
589
- try {
590
- const meta = createView(root, { id: "v1", name: "editor-state", cwd: process.cwd() });
591
- const configPath = P.hostConfigPath(root, "v1");
592
- atomicWriteJson(configPath, {
593
- root,
594
- viewId: "v1",
595
- sessionFile: meta.sessionFile,
596
- cwd: process.cwd(),
597
- initialPrompt: null,
598
- piCommand: process.execPath,
599
- piArgsPrefix: [resolve("test-support/fake-pty-pi.mjs")],
600
- model: null,
601
- tools: null,
602
- env: { AGENT_BOARD_ALLOW_PIPE_FALLBACK: "1" },
603
- cols: 80,
604
- rows: 24,
605
- });
606
- runner = spawn(process.execPath, [resolve("runner/pty-runner.mjs"), configPath], { stdio: ["ignore", "pipe", "pipe"] });
607
- await waitFor(() => hostReady(root, "v1"));
608
-
609
- const readMessages = (socket) => {
610
- let buf = "";
611
- const messages = [];
612
- socket.on("data", (chunk) => {
613
- buf += chunk.toString();
614
- const lines = buf.split("\n");
615
- buf = lines.pop() ?? "";
616
- for (const line of lines) if (line.trim()) messages.push(JSON.parse(line));
617
- });
618
- return messages;
619
- };
620
-
621
- const client1 = createConnection(P.controlSocketPath(root, "v1"));
622
- await once(client1, "connect");
623
- const messages1 = readMessages(client1);
624
- send(client1, { type: "hello" });
625
- await waitFor(() => messages1.find((m) => m.type === "hello"));
626
-
627
- // A second client must receive the pushed state as a broadcast.
628
- const client2 = createConnection(P.controlSocketPath(root, "v1"));
629
- await once(client2, "connect");
630
- const messages2 = readMessages(client2);
631
- send(client1, { type: "editor_state", empty: false });
632
- await waitFor(() => messages2.find((m) => m.type === "editor_state"));
633
- assert.equal(messages2.find((m) => m.type === "editor_state").empty, false);
634
-
635
- // A client connecting afterwards gets the current state seeded in hello.
636
- const client3 = createConnection(P.controlSocketPath(root, "v1"));
637
- await once(client3, "connect");
638
- const messages3 = readMessages(client3);
639
- send(client3, { type: "hello" });
640
- await waitFor(() => messages3.find((m) => m.type === "hello"));
641
- assert.equal(messages3.find((m) => m.type === "hello").editorEmpty, false);
642
-
643
- // Child exit resets the state to null and broadcasts it.
644
- send(client1, { type: "input", data: "exit\r" });
645
- await waitFor(() => messages2.find((m) => m.type === "editor_state" && m.empty === null));
646
- await waitFor(() => readHost(root, "v1")?.endedAt);
647
-
648
- client1.destroy();
649
- client2.destroy();
650
- client3.destroy();
651
- } finally {
652
- await stopRunner(runner);
653
- reapChild(root, "v1");
654
- await new Promise((r) => setTimeout(r, 50));
655
- rmSync(root, { recursive: true, force: true, maxRetries: 5, retryDelay: 50 });
656
- }
657
- });
658
- ```
659
-
660
- - [ ] **Step 2: Run to verify it fails**
661
-
662
- Run: `node --test test/pty-runner.integration.test.mjs`
663
- Expected: the new test FAILS (no `editor_state` broadcast before Task 3 — if Task 3 is already in the tree this step's test passes; then simply confirm PASS and skip to commit).
664
-
665
- - [ ] **Step 3: (If it failed) — Task 3 is already merged in this branch; re-run to confirm PASS**
666
-
667
- Run: `node --test test/pty-runner.integration.test.mjs`
668
- Expected: PASS (all runner tests, including the new one).
669
-
670
- - [ ] **Step 4: Commit**
671
-
672
- ```bash
673
- git add test/pty-runner.integration.test.mjs
674
- git commit -m "test: cover editor_state routing, hello seed and exit reset (issue #68)"
675
- ```
676
-
677
- ---
678
-
679
- ### Task 7: Full regression (A6)
680
-
681
- **Files:** none (verification only).
682
-
683
- - [ ] **Step 1: Run verify**
684
-
685
- Run (in worktree): `npm run verify`
686
- Expected: typecheck clean; all `node --test test/*.test.mjs` pass (430+ tests, zero pre-existing semantics changed); c8 coverage above gates (lines ≥85 / funcs ≥80 / branches ≥70); `npm pack --dry-run` succeeds.
687
-
688
- - [ ] **Step 2: Commit any incidental fixes**
689
-
690
- If verify surfaced issues, fix and commit referencing issue #68. Otherwise nothing to commit.
691
-
692
- ---
693
-
694
- ### Task 8: Post-implementation manual verification (U1–U4)
695
-
696
- **Files:** none (user verification; report back into the PR).
697
-
698
- - [ ] **Step 1: U1 — attach-then-←**
699
-
700
- 1. Update the local extension checkout to the branch (`~/.pi/agent/git/github.com/zhuxixi/pi-agent-board` → `issue-68-attach-detach-editor-state`), restart Pi.
701
- 2. Attach into a pi session; as soon as the surface paints, press `←`.
702
- Expected: returns to the dashboard immediately.
703
-
704
- - [ ] **Step 2: U2 — ← while Pi is thinking**
705
-
706
- 1. Send a prompt that triggers streaming (`⠹ Working...` visible).
707
- 2. While the animation runs, press `←`.
708
- Expected: returns to the dashboard (the #68/#69 failure mode).
709
-
710
- - [ ] **Step 3: U3 — draft protection**
711
-
712
- 1. Attach, type a draft, press `←`.
713
- Expected: cursor moves left inside the draft (forwarded), NOT detach.
714
-
715
- - [ ] **Step 4: U4 — fallback when the reporter is absent**
716
-
717
- 1. Temporarily set `AGENT_BOARD_DISABLE_EDITOR_STATE=1` is NOT part of this plan's product code; instead verify by attaching while the child extension build predates this branch (or just confirm U1-U3 — the heuristic fallback is covered by the existing smoke suite). If U4 cannot be arranged, record it as covered-by-automation (A4 smoke H/I covers the null path).
718
- Expected: ← still exits (heuristic fallback), recorded in the PR.
719
-
720
- - [ ] **Step 5: Record results**
721
-
722
- Write U1-U4 outcomes into the PR description before merge.