@huseynsnmz/pi-board 0.15.3 → 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 (211) hide show
  1. package/CHANGELOG.md +13 -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 +5 -11
  8. package/src/index.ts +1 -1
  9. package/src/runtime/service.mjs +12 -0
  10. package/src/ui/dashboard.ts +14 -8
  11. package/IMPLEMENTATION_PLAN.md +0 -920
  12. package/PRD.md +0 -484
  13. package/PROGRESS.md +0 -142
  14. package/VERIFY.md +0 -115
  15. package/docs/BATCH_SELECTION_READ_FLOW.md +0 -277
  16. package/docs/EXPLORATION.md +0 -187
  17. package/docs/PTY_ATTACH_IMPLEMENTATION_PLAN.md +0 -581
  18. package/docs/RELEASE.md +0 -148
  19. package/docs/superpowers/plans/2026-08-15-screenlog-gc.md +0 -704
  20. package/docs/superpowers/plans/2026-08-16-attach-double-cursor-jiggle-retry.md +0 -499
  21. package/docs/superpowers/plans/2026-08-21-attach-coldstart-jiggle-rearm.md +0 -797
  22. package/docs/superpowers/plans/2026-08-21-dashboard-keypress-lag.md +0 -366
  23. package/docs/superpowers/plans/2026-08-21-issue-14-needs-input-no-auto-done.md +0 -576
  24. package/docs/superpowers/plans/2026-08-22-jiggle-shrink-and-hold.md +0 -337
  25. package/docs/superpowers/plans/2026-08-22-launch-cwd-favorites.md +0 -916
  26. package/docs/superpowers/plans/2026-08-22-question-tool-grouping.md +0 -211
  27. package/docs/superpowers/plans/2026-08-22-rename-state-labels.md +0 -182
  28. package/docs/superpowers/plans/2026-08-22-stable-list-order.md +0 -51
  29. package/docs/superpowers/plans/2026-08-27-locks-acquirelock-spin.md +0 -614
  30. package/docs/superpowers/plans/2026-08-27-pty-runner-test-flaky.md +0 -32
  31. package/docs/superpowers/plans/2026-08-29-code-refs-badges.md +0 -223
  32. package/docs/superpowers/plans/2026-08-30-circular-navigation.md +0 -308
  33. package/docs/superpowers/plans/2026-08-30-post-exit-timing-fix.md +0 -37
  34. package/docs/superpowers/plans/2026-08-30-pty-attach-quality-debt.md +0 -311
  35. package/docs/superpowers/plans/2026-08-30-readme-v2.md +0 -294
  36. package/docs/superpowers/plans/2026-09-01-attach-detach-gate-cursor-anchor.md +0 -284
  37. package/docs/superpowers/plans/2026-09-02-attach-detach-editor-state.md +0 -722
  38. package/docs/superpowers/plans/2026-09-03-code-refs-pr-backlink-narrow.md +0 -551
  39. package/docs/superpowers/plans/2026-09-03-detach-gate-glyph-fallback.md +0 -146
  40. package/docs/superpowers/plans/2026-09-04-evidence-outputpreview.md +0 -209
  41. package/docs/superpowers/plans/2026-09-04-warm-host-reclaim.md +0 -796
  42. package/docs/superpowers/plans/2026-09-05-issue-13-drainnextfollowup-pty-probe.md +0 -114
  43. package/docs/superpowers/plans/2026-09-05-issue-38-windows-wezterm-ime-cursor.md +0 -73
  44. package/docs/superpowers/plans/2026-09-05-issue-39-truncate-codepoint-boundary.md +0 -143
  45. package/docs/superpowers/plans/2026-09-05-issue-61-mention-fallback-guards.md +0 -226
  46. package/docs/superpowers/plans/2026-09-05-issue-63-flaky-manual-completion.md +0 -87
  47. package/docs/superpowers/plans/2026-09-05-issue-64-changelog-release-helper.md +0 -53
  48. package/docs/superpowers/plans/2026-09-05-pty-host-stacking-sock-race.md +0 -731
  49. package/docs/superpowers/plans/2026-09-08-attach-ctrl-left-detach.md +0 -30
  50. package/docs/superpowers/plans/2026-09-08-dashboard-shrink-repaint.md +0 -68
  51. package/docs/superpowers/plans/2026-09-08-issue-11-attach-runtime-desync-heal.md +0 -917
  52. package/docs/superpowers/plans/2026-09-08-legacy-stale-host-recovery.md +0 -125
  53. package/docs/superpowers/plans/2026-09-08-spawn-async-error-swallow.md +0 -56
  54. package/docs/superpowers/plans/2026-09-08-stale-model-attach-guard.md +0 -96
  55. package/docs/superpowers/plans/2026-09-09-claimpid-blocks-replace.md +0 -267
  56. package/docs/superpowers/plans/2026-09-09-harden-runner-architecture.md +0 -603
  57. package/docs/superpowers/plans/2026-09-09-single-writer-completion.md +0 -252
  58. package/docs/superpowers/plans/2026-09-10-reader-consistency.md +0 -115
  59. package/docs/superpowers/plans/2026-09-14-attach-cursor-dectcem-gate.md +0 -469
  60. package/docs/superpowers/plans/2026-09-14-attach-snapshot.md +0 -92
  61. package/docs/superpowers/plans/2026-09-14-host-meta-orphan-lock.md +0 -771
  62. package/docs/superpowers/plans/2026-09-14-issue-106-terminal-frame-cognition.md +0 -299
  63. package/docs/superpowers/plans/2026-09-14-issue-113-foreground-preview-race.md +0 -609
  64. package/docs/superpowers/plans/2026-09-14-terminal-model.md +0 -145
  65. package/docs/superpowers/plans/2026-09-15-coordinator-pipe-root-normalize.md +0 -224
  66. package/docs/superpowers/plans/2026-09-15-lease-publish-eprem-reclaim.md +0 -341
  67. package/docs/superpowers/plans/2026-09-18-detach-anchor-reporter-endpoint.md +0 -875
  68. package/docs/superpowers/plans/2026-09-20-control-lifecycle.md +0 -116
  69. package/docs/superpowers/plans/2026-09-20-issue-121-perf-gate-out-of-coverage.md +0 -517
  70. package/docs/superpowers/plans/2026-09-23-a5-mid-stream-assertion.md +0 -25
  71. package/docs/superpowers/plans/2026-09-23-cwd-browse-tab-drill.md +0 -393
  72. package/docs/superpowers/plans/2026-09-23-terminal-query-forwarding.md +0 -51
  73. package/docs/superpowers/plans/2026-09-23-unconditional-detach-key.md +0 -58
  74. package/docs/superpowers/plans/2026-09-24-control-reconcile-flake.md +0 -698
  75. package/docs/superpowers/plans/2026-09-24-probe-attached-clients.md +0 -605
  76. package/docs/superpowers/plans/2026-09-29-issue-145-holding-semantic-state.md +0 -1183
  77. package/docs/superpowers/plans/2026-09-29-issue-145-user-verification.md +0 -66
  78. package/docs/superpowers/plans/2026-09-30-board-delete-key-x-y-confirm.md +0 -649
  79. package/docs/superpowers/plans/2026-09-30-issue-153-runner-stop-boot-window.md +0 -618
  80. package/docs/superpowers/plans/2026-09-30-issue-95-test-discipline.md +0 -555
  81. package/docs/superpowers/plans/2026-09-30-publish-dispatch-tag-check.md +0 -259
  82. package/docs/superpowers/plans/2026-10-06-attach-settle-scheme-delivery.md +0 -751
  83. package/docs/superpowers/plans/2026-10-09-issue-167-attach-middle-paste.md +0 -509
  84. package/docs/superpowers/plans/2026-10-09-issue-169-fullscreen-mouse-ownership.md +0 -346
  85. package/docs/superpowers/plans/2026-10-10-attach-delete-in-place.md +0 -832
  86. package/docs/superpowers/plans/2026-10-10-issue-174-selection-mirror-paste-source.md +0 -510
  87. package/docs/superpowers/plans/2026-10-14-issue-170-attach-reply-guard.md +0 -771
  88. package/docs/superpowers/plans/2026-10-14-issue-178-accept-registration-race.md +0 -1257
  89. package/docs/superpowers/plans/2026-10-14-issue-184-snapshot-window-contract.md +0 -657
  90. package/docs/superpowers/plans/2026-10-14-issue-185-attach-wheel-alt-screen.md +0 -47
  91. package/docs/superpowers/plans/2026-10-14-issue-187-motionless-drag.md +0 -931
  92. package/docs/superpowers/plans/2026-10-14-issue-189-mixed-input-segments.md +0 -823
  93. package/docs/superpowers/plans/2026-10-15-issue-188-x10-mouse-release.md +0 -526
  94. package/docs/superpowers/plans/2026-10-16-issue-198-attach-wheel-forward-fallback.md +0 -32
  95. package/docs/superpowers/plans/2026-10-18-attach-wheel-replay-pseudo-scrollback.md +0 -437
  96. package/docs/superpowers/plans/2026-10-19-issue-213-reply-guard-csi-key.md +0 -257
  97. package/docs/superpowers/plans/2026-10-19-issue-214-attach-forward-alt-modifier.md +0 -202
  98. package/docs/superpowers/plans/2026-10-21-issue-221-ask-user-question.md +0 -198
  99. package/docs/superpowers/plans/2026-10-23-issue-222-attach-ready.md +0 -290
  100. package/docs/superpowers/plans/2026-10-23-issue-225-monotonic-foreground-state.md +0 -450
  101. package/docs/superpowers/plans/2026-10-23-issue-229-vim-jkl.md +0 -449
  102. package/docs/superpowers/plans/2026-10-23-issue-232-attach-bg-only-cells.md +0 -153
  103. package/docs/superpowers/plans/2026-10-23-lease-heartbeat-staleness.md +0 -537
  104. package/docs/superpowers/plans/2026-10-23-selected-row-highlight.md +0 -405
  105. package/docs/superpowers/plans/2026-10-24-issue-217-windows-pid-reuse-stopping.md +0 -367
  106. package/docs/superpowers/plans/2026-10-24-issue-230-warm-host-background-runs.md +0 -806
  107. package/docs/superpowers/plans/2026-10-24-issue-238-windows-process-identity-token.md +0 -490
  108. package/docs/superpowers/plans/2026-10-24-issue-241-coordinator-root-exit.md +0 -461
  109. package/docs/superpowers/plans/2026-10-25-issue-239-coordinator-replacement-identity.md +0 -504
  110. package/docs/superpowers/plans/2026-10-26-issue-245-key-hints-doc-sync.md +0 -244
  111. package/docs/superpowers/plans/2026-10-27-issue-172-fullscreen-selection-clear.md +0 -344
  112. package/docs/superpowers/plans/2026-10-27-issue-252-select-key-hints.md +0 -209
  113. package/docs/superpowers/plans/2026-10-28-issue-255-coordinator-endpoint-zombie.md +0 -442
  114. package/docs/superpowers/plans/2026-10-28-issue-257-legacy-pty-runner-endpoint-cleanup.md +0 -256
  115. package/docs/superpowers/plans/2026-11-03-cwd-favorites-full-pool.md +0 -266
  116. package/docs/superpowers/specs/2026-08-15-screenlog-gc-design.md +0 -105
  117. package/docs/superpowers/specs/2026-08-16-attach-double-cursor-jiggle-retry-design.md +0 -142
  118. package/docs/superpowers/specs/2026-08-21-attach-coldstart-jiggle-rearm-design.md +0 -110
  119. package/docs/superpowers/specs/2026-08-21-dashboard-keypress-lag-design.md +0 -59
  120. package/docs/superpowers/specs/2026-08-21-issue-14-needs-input-no-auto-done-design.md +0 -109
  121. package/docs/superpowers/specs/2026-08-22-cwd-favorites-design.md +0 -131
  122. package/docs/superpowers/specs/2026-08-22-jiggle-shrink-and-hold-design.md +0 -111
  123. package/docs/superpowers/specs/2026-08-22-question-tool-grouping-design.md +0 -114
  124. package/docs/superpowers/specs/2026-08-22-rename-state-labels-design.md +0 -80
  125. package/docs/superpowers/specs/2026-08-27-locks-acquirelock-spin-design.md +0 -97
  126. package/docs/superpowers/specs/2026-08-27-pty-runner-test-flaky-design.md +0 -35
  127. package/docs/superpowers/specs/2026-08-29-code-refs-badges-design.md +0 -128
  128. package/docs/superpowers/specs/2026-08-30-circular-navigation-design.md +0 -47
  129. package/docs/superpowers/specs/2026-08-30-eprm-atomicwrite-race-design.md +0 -77
  130. package/docs/superpowers/specs/2026-08-30-post-exit-timing-fix-design.md +0 -80
  131. package/docs/superpowers/specs/2026-08-30-pty-attach-quality-debt-design.md +0 -105
  132. package/docs/superpowers/specs/2026-08-30-readme-v2-design.md +0 -115
  133. package/docs/superpowers/specs/2026-09-01-attach-detach-gate-cursor-anchor-design.md +0 -78
  134. package/docs/superpowers/specs/2026-09-02-attach-detach-editor-state-design.md +0 -120
  135. package/docs/superpowers/specs/2026-09-03-code-refs-pr-backlink-narrow-design.md +0 -92
  136. package/docs/superpowers/specs/2026-09-03-detach-gate-glyph-fallback-design.md +0 -99
  137. package/docs/superpowers/specs/2026-09-04-evidence-outputpreview-design.md +0 -50
  138. package/docs/superpowers/specs/2026-09-04-warm-host-reclaim-design.md +0 -106
  139. package/docs/superpowers/specs/2026-09-05-issue-13-drainnextfollowup-pty-probe-design.md +0 -64
  140. package/docs/superpowers/specs/2026-09-05-issue-38-windows-wezterm-ime-design.md +0 -48
  141. package/docs/superpowers/specs/2026-09-05-issue-39-truncate-codepoint-boundary-design.md +0 -64
  142. package/docs/superpowers/specs/2026-09-05-issue-61-mention-fallback-design.md +0 -71
  143. package/docs/superpowers/specs/2026-09-05-issue-63-flaky-manual-completion-design.md +0 -49
  144. package/docs/superpowers/specs/2026-09-05-issue-64-changelog-helper-design.md +0 -76
  145. package/docs/superpowers/specs/2026-09-05-pty-host-stacking-sock-race-design.md +0 -510
  146. package/docs/superpowers/specs/2026-09-07-issue-11-attach-runtime-desync-heal-design.md +0 -130
  147. package/docs/superpowers/specs/2026-09-08-attach-ctrl-left-detach-design.md +0 -58
  148. package/docs/superpowers/specs/2026-09-08-dashboard-shrink-repaint-design.md +0 -52
  149. package/docs/superpowers/specs/2026-09-08-legacy-stale-host-recovery-design.md +0 -87
  150. package/docs/superpowers/specs/2026-09-08-spawn-async-error-swallow-design.md +0 -56
  151. package/docs/superpowers/specs/2026-09-08-stale-model-attach-guard-design.md +0 -79
  152. package/docs/superpowers/specs/2026-09-09-claimpid-blocks-replace-design.md +0 -147
  153. package/docs/superpowers/specs/2026-09-09-harden-runner-architecture-design.md +0 -304
  154. package/docs/superpowers/specs/2026-09-14-attach-cursor-dectcem-gate-design.md +0 -114
  155. package/docs/superpowers/specs/2026-09-14-host-meta-orphan-lock-design.md +0 -120
  156. package/docs/superpowers/specs/2026-09-14-issue-106-terminal-frame-cognition-design.md +0 -146
  157. package/docs/superpowers/specs/2026-09-14-issue-113-foreground-preview-race-design.md +0 -116
  158. package/docs/superpowers/specs/2026-09-15-coordinator-pipe-root-normalize-design.md +0 -84
  159. package/docs/superpowers/specs/2026-09-15-lease-publish-eprem-reclaim-design.md +0 -92
  160. package/docs/superpowers/specs/2026-09-18-detach-anchor-reporter-endpoint-design.md +0 -123
  161. package/docs/superpowers/specs/2026-09-20-issue-121-perf-gate-out-of-coverage-design.md +0 -204
  162. package/docs/superpowers/specs/2026-09-23-a5-mid-stream-assertion-design.md +0 -43
  163. package/docs/superpowers/specs/2026-09-23-cwd-browse-tab-drill-design.md +0 -95
  164. package/docs/superpowers/specs/2026-09-23-terminal-query-forwarding-design.md +0 -63
  165. package/docs/superpowers/specs/2026-09-23-unconditional-detach-key-design.md +0 -57
  166. package/docs/superpowers/specs/2026-09-24-control-reconcile-flake-design.md +0 -98
  167. package/docs/superpowers/specs/2026-09-24-probe-attached-clients-design.md +0 -190
  168. package/docs/superpowers/specs/2026-09-29-issue-145-holding-semantic-state-design.md +0 -394
  169. package/docs/superpowers/specs/2026-09-30-board-delete-key-x-y-confirm-design.md +0 -86
  170. package/docs/superpowers/specs/2026-09-30-issue-153-runner-stop-boot-window-design.md +0 -75
  171. package/docs/superpowers/specs/2026-09-30-issue-95-test-discipline-design.md +0 -226
  172. package/docs/superpowers/specs/2026-09-30-publish-dispatch-tag-check-design.md +0 -129
  173. package/docs/superpowers/specs/2026-10-06-attach-settle-scheme-delivery-design.md +0 -196
  174. package/docs/superpowers/specs/2026-10-09-issue-167-attach-middle-paste-design.md +0 -125
  175. package/docs/superpowers/specs/2026-10-09-issue-169-fullscreen-mouse-ownership-design.md +0 -130
  176. package/docs/superpowers/specs/2026-10-10-attach-delete-in-place-design.md +0 -224
  177. package/docs/superpowers/specs/2026-10-10-issue-174-selection-mirror-paste-source-design.md +0 -154
  178. package/docs/superpowers/specs/2026-10-14-issue-170-attach-reply-guard-design.md +0 -195
  179. package/docs/superpowers/specs/2026-10-14-issue-178-accept-registration-race-design.md +0 -287
  180. package/docs/superpowers/specs/2026-10-14-issue-184-snapshot-window-contract-design.md +0 -288
  181. package/docs/superpowers/specs/2026-10-14-issue-185-attach-wheel-alt-screen-design.md +0 -126
  182. package/docs/superpowers/specs/2026-10-14-issue-187-motionless-drag-design.md +0 -269
  183. package/docs/superpowers/specs/2026-10-14-issue-189-mixed-input-segments-design.md +0 -369
  184. package/docs/superpowers/specs/2026-10-15-issue-188-x10-mouse-release-design.md +0 -252
  185. package/docs/superpowers/specs/2026-10-16-issue-198-attach-wheel-forward-fallback-design.md +0 -67
  186. package/docs/superpowers/specs/2026-10-18-attach-wheel-replay-pseudo-scrollback-design.md +0 -250
  187. package/docs/superpowers/specs/2026-10-19-issue-213-reply-guard-csi-key-design.md +0 -165
  188. package/docs/superpowers/specs/2026-10-19-issue-214-attach-forward-alt-modifier-design.md +0 -131
  189. package/docs/superpowers/specs/2026-10-20-issue-221-ask-user-question-design.md +0 -270
  190. package/docs/superpowers/specs/2026-10-22-issue-225-monotonic-foreground-state-design.md +0 -225
  191. package/docs/superpowers/specs/2026-10-23-issue-222-attach-ready-design.md +0 -204
  192. package/docs/superpowers/specs/2026-10-23-issue-229-vim-jkl-design.md +0 -90
  193. package/docs/superpowers/specs/2026-10-23-issue-232-attach-bg-only-cells-design.md +0 -146
  194. package/docs/superpowers/specs/2026-10-23-lease-heartbeat-staleness-design.md +0 -271
  195. package/docs/superpowers/specs/2026-10-23-selected-row-highlight-design.md +0 -191
  196. package/docs/superpowers/specs/2026-10-24-issue-217-windows-pid-reuse-stopping-design.md +0 -334
  197. package/docs/superpowers/specs/2026-10-24-issue-230-warm-host-background-runs-design.md +0 -283
  198. package/docs/superpowers/specs/2026-10-24-issue-238-windows-process-identity-token-design.md +0 -286
  199. package/docs/superpowers/specs/2026-10-24-issue-241-coordinator-root-exit-design.md +0 -381
  200. package/docs/superpowers/specs/2026-10-25-issue-239-coordinator-replacement-identity-design.md +0 -331
  201. package/docs/superpowers/specs/2026-10-26-issue-245-key-hints-doc-sync-design.md +0 -72
  202. package/docs/superpowers/specs/2026-10-27-issue-172-fullscreen-selection-clear-design.md +0 -248
  203. package/docs/superpowers/specs/2026-10-27-issue-252-select-key-hints-design.md +0 -95
  204. package/docs/superpowers/specs/2026-10-28-issue-255-coordinator-endpoint-zombie-design.md +0 -380
  205. package/docs/superpowers/specs/2026-10-28-issue-257-legacy-pty-runner-endpoint-cleanup-design.md +0 -197
  206. package/docs/superpowers/specs/2026-11-03-cwd-favorites-full-pool-design.md +0 -106
  207. package/scripts/budget-audit.mjs +0 -135
  208. package/scripts/release.mjs +0 -446
  209. package/scripts/release_helper.mjs +0 -309
  210. package/scripts/run-perf-gate.mjs +0 -40
  211. 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.