@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,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.