@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,369 +0,0 @@
1
- Drafted: deepseek/deepseek-flash (selected) · deepseek/deepseek-flash (physical) @ 2026-10-14T12:45:00Z
2
- Revised: deepseek/deepseek-flash (selected) · deepseek/deepseek-flash (physical) @ 2026-10-14T13:10:00Z (round-1 findings R1-G1/G2/G3/D1/D2 all accepted)
3
- Revised: deepseek/deepseek-flash (selected) · deepseek/deepseek-flash (physical) @ 2026-10-14T13:22:00Z (round-2 finding R2-D1 accepted: report table row 3 relabelled as defensive-only)
4
- Revised: zai-coding-cn/glm-5.3-flash (selected) · zai-coding-cn/glm-5.3-flash (physical) @ 2026-10-15T00:24:50Z (round-2 review follow-up: A9 pinning strengthened)
5
- Revised: deepseek/deepseek-flash (selected) · deepseek/deepseek-flash (physical) @ 2026-10-16T00:30:00Z (post-merge composition with main's #194 inbound reply guard; zima CR advisory on PR #196)
6
-
7
- # Spec: #189 — attach(legacy): dispatch a mixed input read per segment, and stop treating terminal reports as user intent
8
-
9
- - Issue: https://github.com/zhuxixi/pi-agent-board/issues/189
10
- - Kind: bug — root-cause report + fix design
11
- - Repo: `zhuxixi/pi-agent-board` · baseline `main` @ `f81949d` · planned worktree `issue-189-mixed-input-segments`
12
- - Route: bug → `systematic-debugging` (Phase 1 root cause + runtime repro in `research/`, this document is the design)
13
- - Research: `~/.claude/github-issue-driven/zhuxixi/pi-agent-board/issue-189/research/`
14
- (`01-root-cause-and-reachability.md`, `03-fix-hypothesis-validated.md`, prototype `02-prototype.patch`)
15
- - Scope: **#189 only.** #187 (motion-less drag) and #188 (X10 release) are separate, in-flight
16
- issues; this fix must work with both still broken. #186 stays the UX umbrella.
17
-
18
- ## 1. Problem
19
-
20
- In the legacy (regular-TUI) attach path the whole input read is classified all-or-nothing:
21
-
22
- | Spot | Location | Behaviour |
23
- |---|---|---|
24
- | `parseMouseInputChunk` | `src/core/pty-scroll.mjs:84` | Returns the decoded mouse events only when **every** byte of the chunk is a mouse report; one stray byte → `null`. |
25
- | `handleInput` head | `src/ui/pty-attach.ts:297-298` | `null` means "not a mouse chunk" → the whole chunk enters the keyboard chain. |
26
- | `handleInput` default branch | `src/ui/pty-attach.ts:373-376` | `clearPendingClick(); clearSelection(); scrollToBottom(); send({ type: "input", data })` — the **whole** chunk, mouse bytes included, is forwarded into the hosted session. |
27
-
28
- Terminals coalesce a mouse release with bytes they send for their own reasons — focus in/out
29
- reports (`\x1b[I`/`\x1b[O`), query replies, colour-scheme reports, OSC responses. When that happens
30
- the mouse events are dropped (no copy, selection cleared, viewport reset) **and** the raw mouse
31
- bytes are typed into the hosted session.
32
-
33
- The keyboard chain has no branch of its own for terminal *reports* either: a spontaneously arriving
34
- report takes the same default branch, so it destroys an in-progress selection and snaps the reading
35
- position to the bottom although it carries no user intent.
36
-
37
- ### Runtime reproduction (this session, 2026-10-14, main `f81949d`)
38
-
39
- Real `PtyAttachComponent` + real runner socket + fake PTY, OSC 52 observed on a fake
40
- `tui.terminal.write`, `{type:"input"}` frames observed on a `Socket.write` tap
41
- (`research/01-root-cause-and-reachability.md`, probes 1+2):
42
-
43
- | Input (one `handleInput` call per step) | OSC 52 | selection | forwarded as input |
44
- |---|---|---|---|
45
- | control: press / move / release, separate chunks | `fake pi ready` | retained | — |
46
- | press, move, then `"\x1b[<0;16;2m\x1b[I"` (release + focus-in) | none | **`null`** | **whole chunk** |
47
- | press, move, then `"\x1b[I\x1b[<0;16;2m"` (report first) | none | **`null`** | **whole chunk** |
48
- | press, move, then `"\x1b[I"` (report alone) | none | **`null`** | `"\x1b[I"` |
49
- | `"\x1b[<64;10;3M\x1b[I"` (wheel + report) on a scrollable buffer | none | **`null`** | **whole chunk**, wheel never applied |
50
- | `"\x1b]11;rgb:0d0d/0d0d/0d0d\x07"` (OSC 11 reply) mid-drag | none | **`null`** | the reply (intended) |
51
- | `"\x1b[I"` while scrolled back (200-line buffer) | — | — | viewport `178` → **`null`** |
52
-
53
- Two findings beyond the issue text, both reproduced: the split must work in **both orders** (a
54
- report can precede the mouse report in the same read), and a coalesced **wheel** event both stops
55
- scrolling and leaks its bytes into the child.
56
-
57
- Why the offending bytes are realistically there: the host pi's own reply filters are whole-chunk
58
- anchored (`/^\x1b\[\?[\d;]*c$/`, `/^\x1b\[6;(\d+);(\d+)t$/`, `COLOR_SCHEME_REPORT_PATTERN`,
59
- `pi-tui terminal-colors.js` OSC pattern), so coalescing defeats them and the bytes land on the
60
- attach component's keyboard path; and the board **deliberately** forwards the child's terminal
61
- queries (`extractOscQuerySequences`, #128) whose replies have no local owner — the code comment at
62
- `src/ui/pty-attach.ts:658-662` states that they fall through `handleInput` into the child PTY.
63
-
64
- Not a regression: `git log -S parseMouseInputChunk -- src/` → single hit `ffc2c8c` (2026-08-30).
65
-
66
- ## 2. Goal / non-goals
67
-
68
- **Goal.** One input read is dispatched segment by segment in arrival order: mouse runs go to the
69
- existing mouse path, terminal *reports* are passed through to the child without touching selection
70
- or viewport, everything else keeps today's keyboard semantics byte-for-byte. The issue's exact
71
- mixed chunk (either order) copies the selection again and leaks nothing into the child.
72
-
73
- **Non-goals.**
74
-
75
- - Mouse decoding itself: `parseMouseEventPrefix`, the SGR/X10 branches, wheel classification. #188
76
- owns the X10 release bug; #187 owns the motion-less drag classification. Neither is assumed fixed.
77
- - The drag/click/copy/highlight contract (`shouldCopy`, pending click, #172 highlight retention) and
78
- `handleLocalMouseEvent` — untouched.
79
- - Fullscreen host mode / `handleMouse` / pi-tui selection: in fullscreen `TuiAltScreen` consumes raw
80
- mouse sequences and focus reports as an input listener before any overlay, so the legacy parser
81
- never sees them (`tui-alt-screen.js:105,475-527`). Scope is regular TUI mode (and pi < 1.0).
82
- - #186 UX items (selection background colour, 1-cell press selection, `pbcopy` fallback, Shift+drag
83
- docs).
84
- - Forwarding policy for reports (they are forwarded as today), `childInputReady`/startup echo
85
- ordering (#165/#180), the run of a *key* clearing the selection and jumping to the bottom (stays).
86
- - No new env knob, no new dependency, no new I/O surface, no change to `parseMouseInputChunk`'s
87
- contract (its unit tests and #188's planned cases depend on it).
88
-
89
- ## 3. Design
90
-
91
- ### 3.1 Pure decision layer — `src/core/pty-scroll.mjs`
92
-
93
- **(a) `parseMouseRun(data, offset)`** — module-private: reads a maximal run of concatenated mouse
94
- reports using the existing `parseMouseEventPrefix`. Returns `{ events, length }` or `null`.
95
- `parseMouseInputChunk` is re-expressed on top of it and keeps its exact contract:
96
-
97
- ```js
98
- export function parseMouseInputChunk(data) {
99
- if (!data) return null;
100
- const run = parseMouseRun(data);
101
- return run && run.length === data.length ? run.events : null;
102
- }
103
- ```
104
-
105
- **(b) Report table + `parseReportPrefix` / `parseReportRun`** — module-private. A *report* is a
106
- complete byte sequence the real terminal sends in answer to a query, never in answer to a key.
107
- Every pattern is grounded in a query this stack actually issues — except row 3, which is kept as a
108
- defensive extra and labelled as such; anything not listed keeps today's keyboard semantics
109
- (fail-safe direction: an unknown sequence is typing).
110
-
111
- | # | Pattern | What produces it |
112
- |---|---|---|
113
- | 1 | `\x1b[I` / `\x1b[O` | focus in/out, DECSET 1004 (pi's fullscreen TUI enables it; regular mode only via leaked terminal state) |
114
- | 2 | `CSI ? <digits[;digits]> c` | DA1 reply to `\x1b[c` — the trailing sentinel of Pi's colour batch (`pi-tui tui.js:57`) |
115
- | 3 | `CSI > <digits[;digits]> c` | **not grounded (defensive only)**: this stack sends no DA2 query — the only DA query is `\x1b[c` (`pi-tui terminal.js:15`), whose reply row 2 covers — and XTVERSION replies are DCS-shaped, i.e. deliberately excluded below. Kept because no keystroke encodes as a `>`-prefixed CSI, and a stray DA2 reply would otherwise reach the keyboard path. |
116
- | 4 | `CSI ? <digits[;digits]> u` | kitty keyboard flag reply to `\x1b[?u` (`pi-tui terminal.js:210`) |
117
- | 5 | `CSI ? <digits[;digits]> n` | mode/status reports, incl. the colour-scheme report `\x1b[?997;1n` Pi parses (`terminal-colors.js:19`) |
118
- | 6 | `CSI <digits[;digits]> t` | XTWINOPS replies — the host asks for the cell size with `\x1b[16t` (`tui.js:603`) and consumes `CSI 6;h;wt` |
119
- | 7 | `OSC <body ≤4096 bytes> BEL\|ST` | colour replies (OSC 10/11/12, OSC 4) and other reply bodies (e.g. program status OSC 7501) |
120
-
121
- Deliberately **not** matched (a keystroke can look like them, so they keep key semantics, and the
122
- exclusion is part of the contract): `CSI … R` (cursor-position report, but also modifier-F3),
123
- `u`-final CSI without `?` (kitty key events), `CSI … $ y` (DECRPM — nothing in this stack queries
124
- modes), DCS replies (no in-repo DCS query).
125
-
126
- **(c) `splitAttachInput(data)`** — the new public function (the only one):
127
-
128
- ```js
129
- /** @returns {Array<{kind:"mouse", events:[…]} | {kind:"report", data:string} | {kind:"key", data:string}>} */
130
- export function splitAttachInput(data) {
131
- if (!data) return [];
132
- const segments = [];
133
- let offset = 0;
134
- while (offset < data.length) {
135
- const mouse = parseMouseRun(data, offset);
136
- if (mouse) { pushSegment(segments, { kind: "mouse", events: mouse.events }); offset += mouse.length; continue; }
137
- const report = parseReportRun(data, offset);
138
- if (report) { pushSegment(segments, { kind: "report", data: report }); offset += report.length; continue; }
139
- // Neither: keyboard text up to the next ESC.
140
- const next = data.indexOf("\x1b", offset + 1);
141
- const end = next === -1 ? data.length : next;
142
- pushSegment(segments, { kind: "key", data: data.slice(offset, end) });
143
- offset = end;
144
- }
145
- return segments;
146
- }
147
- ```
148
-
149
- Decision table (consecutive same-kind segments are merged; a partial/trailing escape sequence is
150
- never recognised and becomes `key`, i.e. the bytes the all-or-nothing parser used to forward):
151
-
152
- | Input (one read) | Segments |
153
- |---|---|
154
- | `"\x1b[<64;10;20M\x1b[<65;10;20M"` | `[mouse(2)]` |
155
- | `"\x1b[<0;16;2m\x1b[I"` | `[mouse(1), report]` |
156
- | `"\x1b[I\x1b[<0;16;2m"` | `[report, mouse(1)]` |
157
- | `"\x1b[I\x1b[O\x1b[<0;16;2m"` | `[report ("\x1b[I\x1b[O"), mouse(1)]` |
158
- | `"\x1b[<0;16;2mx"` / `"x\x1b[<0;16;2m"` | `[mouse(1), key "x"]` / `[key "x", mouse(1)]` |
159
- | `"\x1b[Ix"` / `"x\x1b[I"` | `[report, key "x"]` / `[key "x", report]` |
160
- | `"\x1b[5~\x1b[6~"` (adjacent key runs **merge**) | `[key "\x1b[5~\x1b[6~"]` — one segment, so `matchesKey` sees the same concatenation as today |
161
- | `"hello"` | `[key "hello"]` |
162
- | `""` | `[]` |
163
- | `"\x1b[<0;16"` (truncated) | `[key "\x1b[<0;16"]` |
164
- | `"\x1b[?997;1"` (truncated report) | `[key "\x1b[?997;1"]` |
165
- | `"\x1b[1;2R"`, `"\x1b[97;5u"`, `"\x1bP1+r…\x1b\\"` (kept as keys) | `[key …]` |
166
-
167
- ### 3.2 Component wiring — `src/ui/pty-attach.ts`
168
-
169
- `handleInput` becomes a dispatch loop; the existing keyboard chain moves verbatim into a private
170
- `handleKeyInput(segment)` (same branches, same `return`s — no logic change):
171
-
172
- ```ts
173
- handleInput(data: string): void {
174
- for (const segment of splitAttachInput(data)) {
175
- if (segment.kind === "mouse") {
176
- this.handleMouseInputChunk(segment.events);
177
- } else if (!this.consumeByReplyGuard(segment.data)) {
178
- if (segment.kind === "report") this.send({ type: "input", data: segment.data });
179
- else this.handleKeyInput(segment.data);
180
- }
181
- // A detach/close inside a segment must stop the rest of this read.
182
- if (this.closed) return;
183
- }
184
- }
185
- ```
186
-
187
- `consumeByReplyGuard(data)` (new private method) runs the issue #170/#194 inbound reply guard over one
188
- non-mouse **segment** and returns true when the guard took it — a complete protocol reply (DA1/DA2,
189
- kitty flags, cell-size, DCS) is dropped, a colour reply is forwarded as one whole sequence. Feeding
190
- segments instead of raw reads is what keeps a recognized report segment from absorbing keys that
191
- were coalesced into the same read — the very case #189 exists for. A still-fragmented head keeps
192
- the bounded #194 absorption (64 bytes / 250 ms, kill-switchable).
193
-
194
- - `mouse` → the existing `handleMouseInputChunk` (wheel branch + `handleLocalMouseEvent`), unchanged.
195
- - `report` → passed through the reply guard first; whatever the guard does not own is forwarded to
196
- the child (it may be the answer the child's own query is waiting for) but **without**
197
- `clearPendingClick()` / `clearSelection()` / `scrollToBottom()`: it arrives without user intent.
198
- - the reply guard (#170/#194) keeps its drop/forward policy per segment: reports are *not* always
199
- forwarded unchanged — complete protocol replies stay dropped as before, and colour replies are
200
- forwarded whole by the guard itself.
201
- - `key` → the existing keyboard chain (`handleKeyInput`), semantics unchanged: typing still clears
202
- the selection and jumps to the bottom.
203
- - `if (this.closed) return;` after each segment: `detach()` sets `closed` (`:461`, `close():1609`),
204
- so a detach chord mid-read cannot make the loop act on a closed component. This is **new reachable
205
- behaviour**: today `"\x1b[1;5D\x1b[I"` (detach chord + report in one read) matches no key as a
206
- whole chunk and is merely forwarded; after the fix the first segment is an exact `Key.ctrl("left")`
207
- hit → `detach()`. A9 pins it.
208
-
209
- The fullscreen path (`handleMouse`) and everything below `handleKeyInput` are untouched.
210
-
211
- ### 3.3 Alternatives considered
212
-
213
- | Alternative | Verdict |
214
- |---|---|
215
- | Split only a **leading** mouse prefix (the issue's `splitMouseInputPrefix` sketch), forward the remainder through the keyboard path | Rejected: reproduced counter-example — a report *first* (`"\x1b[I\x1b[<0;16;2m"`) still loses the release (probe 1, case D). The split must be order-agnostic. |
216
- | Teach `parseMouseInputChunk` to skip non-mouse bytes | Rejected: breaks its pinned all-or-nothing contract (`test/pty-scroll.test.mjs:83-99`) and #188's acceptance, and leaves the keyboard path unable to tell a report from typing. |
217
- | Route reports into a new dedicated branch of the keyboard chain (check before `matchesKey`) instead of segmenting them | Rejected: reports and keys can share one read; a branch that runs once for the whole chunk cannot keep "report does not clear, key immediately after does" correct. |
218
- | Keep reports on the keyboard path but wrap `clearSelection`/`scrollToBottom` in a flag | Rejected: adds state to the component for a pure input-classification fact, and does not fix the dropped mouse events. |
219
- | Decode the report bytes ourselves / forward only known-good replies | Rejected: out of scope — the child owns reply semantics; forwarding is today's contract. |
220
-
221
- ## 4. Data flow
222
-
223
- ```
224
- outer terminal ── stdin ──▶ pi host TUI ──(focused component)──▶ PtyAttachComponent.handleInput(data)
225
- │
226
- splitAttachInput(data) ← new (pure, pty-scroll.mjs)
227
- │
228
- ┌───────────────────────────┬──────────────────────────────┴───────────────┐
229
- ▼ ▼ ▼
230
- mouse segment report segment key segment
231
- handleMouseInputChunk send({type:"input"}) (pass-through) handleKeyInput (unchanged chain)
232
- wheel / handleLocalMouseEvent no selection or viewport change clear selection + scroll to bottom
233
- │ │
234
- └───────────────▶ this.send({type:"input"}) ──▶ runner socket ──▶ child PTY
235
- ```
236
-
237
- ## 5. Behaviour matrix (before → after)
238
-
239
- | Read | before | after |
240
- |---|---|---|
241
- | pure mouse reports | mouse path | unchanged |
242
- | mouse + any non-mouse bytes (either order) | keyboard default: gesture dropped, selection cleared, viewport reset, whole read forwarded to the child | mouse run processed; non-mouse run dispatched by kind |
243
- | report-only read with a selection in progress / scrolled back | selection cleared, viewport snapped to bottom | forwarded; local state untouched |
244
- | report followed/preceded by a real key | whole read treated as typing | report forwarded, key handled as typing |
245
- | complete DA1/DA2/kitty/cell-size reply (issue #194 guard) | dropped by the guard | **unchanged**: the guard still owns complete protocol replies, now fed one segment at a time |
246
- | colour reply (OSC 4/10/11/12, 997 report) | forwarded whole by the guard | **unchanged** (the guard forwards it before the kind dispatch) |
247
- | reply head coalesced with keys (guard window) | guard could absorb the keys into its window | keys are a separate segment, so a head can never absorb them |
248
- | wheel coalesced with a report | wheel not applied, bytes leaked to the child | wheel scrolls locally; only the report is forwarded |
249
- | two keys in one read (`"\x1b[5~\x1b[6~"`) | neither recognised → both forwarded to the child | **unchanged**: adjacent key runs merge into one segment, so `matchesKey` still sees the concatenation (a key is only recognised when it is the whole read) |
250
- | key-only read | typing semantics | unchanged |
251
- | truncated/partial escape sequence | forwarded as input | unchanged (becomes `key`) |
252
- | fullscreen host mode | `handleMouse` owns mouse; focus reports consumed by `TuiAltScreen` | unchanged |
253
-
254
- ## 6. Acceptance matrix
255
-
256
- | ID | 功能点 | 验收方式 | 具体验证 | 通过标准 |
257
- |----|--------|----------|----------|----------|
258
- | A1 | 分段决策 `splitAttachInput` 决策表(§3.1 表全部行,含两种顺序、合并同类(含 report\|key 邻接两行)、键段合并、截断序列、`""`) | 自动化验证(unit) | `node --test test/pty-scroll.test.mjs`(新增用例组) | 每行 `kind` 序列与 `data`/`events` 内容逐字匹配;`parseMouseInputChunk` 既有 3 条断言(`:83-99`)不变仍绿 |
259
- | A2 | 报告序列判定表(§3.1 七类正例 + 刻意排除的反例) | 自动化验证(unit,同一文件;只经 `splitAttachInput` 断言,不新增 test-only 导出) | `node --test test/pty-scroll.test.mjs` | ① 七类正例(含 BEL 与 ST 两种 OSC 终止符、`\x1b[?1;2c`、`\x1b[>0;95;0c`、`\x1b[?1u`、`\x1b[?997;1n`、`\x1b[6;24;80t`)每条 → 恰好一个 `{kind:"report"}` 且 `data` 逐字相等;② 反例 `\x1b[1;2R`(shift-F3)、`\x1b[97;5u`(kitty ctrl+a)、`\x1bP1+r…\x1b\\`(DCS)→ `{kind:"key"}`;③ 正例中的 `\x1b[>0;95;0c` 属 §3.1(b) 表第 3 行的防御性保留项,断言它仍为 report |
260
- | A3 | 本 issue 的确切复现:release 与 report 同 chunk(两种顺序,含 OSC 11 reply 变体) | 自动化验证(integration,组件级:真 runner socket + fake PTY + fake TUI 捕获 `terminal.write`) | 新增 `test-support/pty-attach-mixed-input-scenario.ts` + `test/pty-attach-mixed-input.test.mjs`(`node --experimental-transform-types`,模式同 `test/pty-attach-mouse.test.mjs`) | ① 恰好一次 OSC 52 `\x1b]52;c;<base64("fake pi ready")>\x07`;② socket 收到的 input 帧里**不含任何** `\x1b[<` 鼠标字节,report 字节本身仍在;③ 组件 selection 仍非 null(场景 harness 已是白盒 `(component as any)`,另记渲染行仍带 inverse 选中 SGR 作辅助信号) |
261
- | A4 | 纯 report 读取不碰选择:`"\x1b[I"`、`"\x1b]11;rgb:…\x07"` 在拖选中途到达 | 自动化验证(integration,同场景) | 同场景 case 组 | ① selection 保持非 null;② 无 OSC 52 写出;③ report 仍以 `{type:"input"}` 转发给子进程 |
262
- | A5 | 视口语义(需可滚动缓冲:fixture 的 `burst` 输入 → 200 行):① 读回滚时 report 不跳底;② wheel+report 同 chunk 时 wheel 本地生效 | 自动化验证(integration,同场景) | 同场景 case 组;断言只用公开可见量:`render()` 首行可见的 `burstline-####` 编号(回滚后应为较旧的编号) | ① report 到过后首行编号不变(不是 `burstline-0200`);② wheel+report 后首行编号比之前更旧(回滚 1 行),且转发的 input 帧只有 report |
263
- | A6 | 键盘语义不回归 + 分类边界:真键 `"x"` 仍清选择并跳底;`"\x1b[1;2R"`(shift-F3)、`"\x1b[97;5u"`(kitty ctrl+a)仍按键入处理 | 自动化验证(integration,同场景) | 同场景 case 组 | ① 真键后 selection 为 null 且该键被转发;② 两个歧义序列都走键入路径(清选择) |
264
- | A7 | 类型 / 构建 / 打包 | 自动化验证(static + build) | `npm run typecheck`;`npm run verify` | exit 0(含 perf gate、coverage 阈值 lines 85 / funcs 80 / branches 70、`pack:dry`) |
265
- | A8 | 既有行为不回归(#167 鼠标分发、#171 中键粘贴、#174 选中镜像、滚轮、attach e2e) | 自动化验证(integration,既有套件) | `npm run test:coverage`(`test/pty-scroll.test.mjs`、`test/pty-attach-mouse.test.mjs`、`test/pty-attach-wheel.test.mjs`、`test/pty-attach-selection-mirror.test.mjs`、`test/pty-attach-*.test.mjs`) | 既有测试全绿,覆盖率不低于阈值 |
266
- | A9 | mid-read detach 守卫(§3.2 的 `if (this.closed) return;`,本轮新识别的可达行为) | 自动化验证(integration,同 A3 场景) | 同场景 case:一次读入三段 `"\x1b[1;5D" + "\x1b[I" + "x"`(ctrl+← / focus report / 尾随 key;report 分隔必不可少——相邻键段会合并(A1),无分隔时 chord 与 key 并成一段键文本、chord 永不匹配) | ① `closed === true`;② 该读取的后续段未被处理(用尾随 key 段作探针,断言 selection 仍存在;无守卫时尾随 key 会 `clearSelection()` 而尾随 report 的 `send()` 只是静默 no-op、无法区分);③ 无 throw、无渲染报错 |
267
- | U1 | 真机:常规模式下注入本 issue 的确切字节流(两种顺序) | 用户实测(可执行:本机有 tmux + xclip + `DISPLAY=:0`;缺 tmux 时标 `pending`) | **部署前置(必须先做)**:把被测 checkout 装进 pi——合并后首选 `git -C ~/.pi/agent/git/github.com/zhuxixi/pi-agent-board stash`(该 checkout 有本地 `package-lock.json` 改动)→ `git pull` → 重启 `pi --tui-mode regular`;合并前试点则按 README:62-75 开发自发现路径 `ln -s <worktree> ~/.pi/agent/extensions/agent-board`,并临时从 `~/.pi/agent/settings.json` 的 `packages` 移除 `git:github.com/zhuxixi/pi-agent-board`(避免同一扩展双加载),测后恢复。**然后**:① tmux 内跑 `pi --tui-mode regular` → `/agent-board` → attach 到任一 session;② 在**一次** `tmux send-keys -H` 里传入全部字节以合成一次读取:`1b 5b 3c 30 3b 31 3b 32 4d`(press)、`1b 5b 3c 33 32 3b 38 3b 32 4d`(move)、`1b 5b 3c 30 3b 31 36 3b 32 6d 1b 5b 49`(release+focus-in);③ 观察高亮与剪贴板/PRIMARY(复用 `xclip -o -selection primary` 检查) | 高亮保留到下一次点击(#172 契约),选中文本进入剪贴板/PRIMARY,子 session 不出现鼠标字节回显;若 tmux 把字节拆成多帧(合成失败),如实记录分帧观察结果并把该行标 `pending`,A3 仍是主证据(步骤 ①②③ 中仅 ② 依赖 tmux) |
268
- | U2 | 真机(可选):日常使用中混合读取不再清掉选中 | 用户实测(可选,非阻塞) | 常规模式 attach 内拖选,同时触发焦点切换/主题探测 | 选中与阅读位置不再被无意图的 report 打断;失败如实记录 |
269
-
270
- Notes:
271
-
272
- - A1–A2 are the lowest level that can prove the classification rules (pure, no TUI/socket). A3–A6
273
- and A9 are the lowest level that can prove the *effects*, because the dispatch lives behind a private
274
- handler with TUI + socket + xterm dependencies (§7). A8/A7 are the repo's definition of done.
275
- - A3 is the issue's own reproduction; A4/A5 cover the second half of the issue's suggested fix
276
- (reports must not cancel an in-progress selection), A6 pins the fail-safe boundary (ambiguous
277
- sequences stay keys).
278
-
279
- ## 7. Testability split design (hard constraint for implementation)
280
-
281
- - **Pure core** (`src/core/pty-scroll.mjs`): `splitAttachInput` is the single new export; the run
282
- readers and the report table stay module-private. No env reads, no I/O, no component import, no
283
- use of `Date.now()`. A1/A2 call it directly; the only other export touched is
284
- `parseMouseInputChunk`, whose behaviour must be literally unchanged (re-expressed on
285
- `parseMouseRun`, contract re-pinned by the existing assertions).
286
- - **I/O shell** (`PtyAttachComponent`): owns the socket, the xterm model, selection state, the
287
- pending-click timer and the viewport. The dispatch loop is the only new logic; `handleKeyInput` is
288
- the old body moved verbatim (a pure refactor — no branch, guard or ordering may change inside it).
289
- The report branch adds exactly one `send()` and no state.
290
- - **Test seam** (A3–A6): the public `handleInput` legacy seam + a fake `tui.terminal.write` capture +
291
- a real runner socket with the fake PTY — the seams `test/pty-attach-mouse.test.mjs` and
292
- `test/pty-attach-selection-mirror.test.mjs` already use. No test-only export, no injection hook,
293
- no new env knob. The scenario is white-box by construction (it already casts the component to
294
- `any` to reach `handleMouse`), so reading the private `selection` field to prove "the highlight
295
- survived" is allowed there; the behavioural proofs (OSC 52 payload, socket frames) and the
296
- viewport proof (rendered content) stay black-box and are the primary assertions.
297
- - **Fixtures**: `test-support/fake-pty-pi.mjs` default mode prints exactly one stable line
298
- (`fake pi ready`) → the expected OSC 52 payload is deterministic; the `burst` input channel emits
299
- 200 numbered lines → the scrollable-viewport cases (A5) need no new fixture. A PATH-stubbed `xclip`
300
- keeps the scenario off the host clipboard.
301
- - **Boundary to preserve**: "what is a report" lives in exactly one place (the report table in
302
- `pty-scroll.mjs`). The component must not re-inline report or mouse detection; it only switches on
303
- the segment kind.
304
-
305
- ## 8. Boundaries, interactions, risks
306
-
307
- **Sibling issues (must not be pre-empted).** #187 edits the release branch of
308
- `handleLocalMouseEvent` and moves `sameMousePoint` into `pty-scroll.mjs`; #188 edits the X10 branch of
309
- `parseMouseEventPrefix` and extends the `parseMouseInputChunk` unit block. #189 touches neither
310
- function body: it adds new private helpers plus one export, and its component edits sit in
311
- `handleInput` (top of the class) — disjoint from #187's release branch. Whichever merges second
312
- rebases and re-runs `npm run verify`; with #187/#188 unfixed this issue's cases still pass because it
313
- does not rely on their fixes (X10-only terminals still lose the copy — documented as their scope).
314
-
315
- **Documented guarantee boundaries.**
316
-
317
- - Report-shaped bytes inside a bracketed paste are split out as a report run: the byte stream and its
318
- order are preserved (the child sees the same bytes), and the only local difference is that the
319
- pasted report-shaped text does not clear the selection. **Mouse-report-shaped bytes are different**
320
- and are stated here honestly: a pasted `\x1b[<…M` run is consumed by the local mouse path and is
321
- **not** forwarded to the child (today the whole pasted chunk was forwarded). Pasting a captured
322
- terminal log that contains mouse reports can therefore create a local selection/click instead of
323
- reaching the child — a documented consequence of segmenting, not a paste-content loss (nothing is
324
- dropped from the byte stream for report runs; mouse runs are consumed by design, exactly as they
325
- are when typed by the terminal).
326
- - A report or mouse sequence **split across two reads** is not recognised and is forwarded as typing
327
- — exactly today's behaviour for partial sequences; the outer terminal writes complete sequences.
328
- - Unknown/unlisted reply forms (CPR `CSI … R`, DECRPM `CSI … $ y`, DCS replies) keep key semantics:
329
- they still clear the selection as today. Adding a form later is one line in the report table.
330
- - Reports the reply guard (#170/#194) does not own are forwarded to the child, including while the
331
- child has not reached raw/bracketed-paste readiness — that is today's behaviour and belongs to
332
- #165/#180, not here. Complete protocol replies stay dropped by the guard, and colour replies are
333
- forwarded whole by the guard; #189 does not change that policy, it only feeds it segments.
334
- - No clipboard claim: the copy path (OSC 52 + PRIMARY mirror) is unchanged.
335
- - No claim about fullscreen mode: `handleMouse`/pi-tui still own that path.
336
-
337
- **Risks.**
338
-
339
- | Risk | Assessment / mitigation |
340
- |---|---|
341
- | The classifier misses a real report → selection still cleared | Fail-safe direction: the child still receives the bytes; the local state difference is a highlight/viewport artefact. A2 pins the excluded forms explicitly, and the table is documented as the extension point. |
342
- | The classifier swallows a key → selection no longer clears on typing | Patterns are grounded in queries this stack issues; A2/A6 pin `\x1b[1;2R`, `\x1b[97;5u`, `\x1bP…` as keys. |
343
- | `handleKeyInput` extraction changes keyboard behaviour | Pure move; A8 + `npm run verify` (the prototype already ran 1161 tests green on the patched tree — `research/03-fix-hypothesis-validated.md`). |
344
- | Extra per-read work (segment scanner) on large pastes | Only ESC positions are probed: one `parseMouseEventPrefix` (single regex) plus the report table (≤7 regexes, all anchored) per ESC, and one linear `indexOf` per key run. |
345
- | Shared file conflicts with #187/#188 | Disjoint functions; whichever lands second rebases and re-runs `npm run verify`. |
346
- | A5's viewport assertion touches component internals | Observed through the public `render()` output (the `burstline-####`编号 of the first visible body line); no private API is added or changed, and the private `selection` read (A3) is a white-box convenience with the OSC 52 payload as the primary evidence. |
347
-
348
- ## 9. Deliverables
349
-
350
- - `src/core/pty-scroll.mjs`: `parseMouseRun` (private) + `parseMouseInputChunk` re-expressed on it;
351
- report table + `parseReportPrefix` / `parseReportRun` / `pushSegment` (private); `splitAttachInput`
352
- (new export + JSDoc typedef).
353
- - `src/ui/pty-attach.ts`: import `splitAttachInput` (dropping `parseMouseInputChunk`); `handleInput`
354
- becomes the dispatch loop; keyboard chain moved into private `handleKeyInput`.
355
- - `test/pty-scroll.test.mjs`: segmentation + report-classification cases (A1, A2).
356
- - `test-support/pty-attach-mixed-input-scenario.ts` + `test/pty-attach-mixed-input.test.mjs`:
357
- component-level cases (A3–A6, A9), including the `burst`-based viewport cases.
358
- - `docs/superpowers/specs/2026-10-14-issue-189-mixed-input-segments-design.md`: this spec, first
359
- commit in the worktree (step 5).
360
- - CHANGELOG entry via the release tooling at release time (not in this PR).
361
-
362
- ## 10. Open question for the design gate
363
-
364
- The report pass-through is the second half of the issue's own suggested fix and is reproduced as a
365
- real symptom (selection cleared + reading position lost). If the operator prefers a smaller blast
366
- radius, the dispatch loop alone still fixes the *copy* (the mouse run is processed), and the report
367
- rows can be trimmed to fewer forms (A2/A4/A5 shrink accordingly) — but then the remaining forms
368
- still wipe the just-created highlight and snap the viewport, which is exactly the second symptom of
369
- this issue.
@@ -1,252 +0,0 @@
1
- # Issue #188 — X10 mouse release (`Cb=3`) parsed as press: selection cleared, never copied
2
-
3
- - Issue: https://github.com/zhuxixi/pi-agent-board/issues/188
4
- - Kind: bug — root-cause report + fix design
5
- - Repo: `zhuxixi/pi-agent-board` · baseline `main` @ `2148360` (drafted and cross-reviewed against `f81949d`; rebases are citation-only) · worktree `issue-188-x10-release`
6
- - Drafted: deepseek/deepseek-flash (selected) · deepseek/deepseek-flash (physical) @ 2026-10-14T12:34:15Z
7
- - Revised: deepseek/deepseek-flash (selected) · deepseek/deepseek-flash (physical) @ 2026-10-14T12:52:00Z — after round-1 review (R1-G1/R1-G2/R1-D1 accepted; findings in `research/spec-review-round-1.md`)
8
- - Revised: deepseek/deepseek-flash (selected) · deepseek/deepseek-flash (physical) @ 2026-10-14T13:03:00Z — after round-2 review (R2-D1 accepted; 0 real gaps, converged; findings in `research/spec-review-round-2.md`)
9
- - Revised: deepseek/deepseek-flash (selected) · deepseek/deepseek-flash (physical) @ 2026-10-15T00:00:00Z — rebased onto `main` @ `7cf0853` (#190/#191 merged): line citations moved (+4 from `handleInput`, +25 from `handleMouseInputChunk`, osc52 +42); no design or boundary change, the X10 branch and the legacy handler are byte-identical to `f81949d`
10
- - Revised: deepseek/deepseek-flash (selected) · deepseek/deepseek-flash (physical) @ 2026-10-16T12:35:00Z — rebased onto `main` @ `074123c`, where the two sibling issues landed (#189 @ `8a5966e`, #187 @ `074123c`). §6.2/§8 updated with the realised interactions: the X10 release now reaches #187's geometry-based drag decision (so a motion-less X10 gesture also copies — #187's acceptance, not asserted here), and #189 kept `parseMouseInputChunk`'s all-or-nothing contract (the 7-byte row still holds; `splitAttachInput` owns mixed reads). One both-added-tests conflict in `test/pty-scroll.test.mjs` was resolved by keeping both sides.
11
- - Research: `research/01-root-cause-and-reachability.md` (root cause, reachability, fix validation)
12
- - Route: bug → `systematic-debugging` (Phase 1 root cause + live repro done in `research/`; this document is the design)
13
-
14
- ## 1. Problem
15
-
16
- In the legacy (regular-TUI) mouse path, an X10/normal-encoded release — `Cb = 3` — is decoded as
17
- `action: "press"`. `handleLocalMouseEvent` then takes its non-primary press branch and clears the
18
- pending click and the selection, so the real release branch (the one that calls
19
- `copySelectionToClipboard()`) never runs: on a terminal that speaks only X10 the user drags, sees the
20
- highlight, and loses it the instant the button comes up — nothing is copied.
21
-
22
- Affected scope is narrow but real: X10 bytes reach the parser only from terminals that ignore the
23
- `?1006` SGR request while still reporting `?1000`/`?1002` mouse events (`MOUSE_ENABLE`,
24
- `src/ui/pty-attach.ts:45`), and only in regular TUI mode (in fullscreen, pi-tui's `TuiAltScreen`
25
- consumes raw mouse input and its own parser accepts SGR only). The defect is latent, not a
26
- regression: the X10 branch has been unchanged since it was introduced in `b1ee950`
27
- (2026-06-01).
28
-
29
- ## 2. Root cause (evidence in `research/01-root-cause-and-reachability.md`)
30
-
31
- X10/normal encoding is `ESC [ M Cb Cx Cy`, one byte per field, each byte = value + 32. Per xterm
32
- ctlseqs, for the low two bits of `Cb`: `0 = MB1 pressed, 1 = MB2 pressed, 2 = MB3 pressed,
33
- 3 = release` — release is a *button code*, not a bit, and it carries no button identity. Bits 2–4
34
- are shift/meta/ctrl, bit 5 (`32`) is motion, bit 6 (`64`) is wheel (wheel releases are never
35
- reported).
36
-
37
- `parseMouseEventPrefix` (`src/core/pty-scroll.mjs:53-66`) maps every X10 event that lacks the motion
38
- bit to `"press"`. A `Cb = 3` event therefore enters `handleLocalMouseEvent`
39
- (`src/ui/pty-attach.ts:1033`) with `button = 3`: `primary = (3 & 3) === 0` is `false`,
40
- `middleButton` is `false`, so the press branch runs `!primary` → `clearPendingClick()` +
41
- `clearSelection()` (`:1047-1051`).
42
-
43
- Live component-level repro on `f81949d` (real `PtyAttachComponent` + real runner socket + fake PTY,
44
- OSC 52 observed on `tui.terminal.write`): canonical SGR press → move → release copies
45
- (`osc52 = ["fake pi ready"]`); the same gesture encoded as X10 emits **no** OSC 52 and the selection
46
- is `null` synchronously at the release.
47
-
48
- ## 3. Fix design
49
-
50
- ### 3.1 Classification contract for the X10 branch (`parseMouseEventPrefix`)
51
-
52
- | `Cb` bits | `Cb` example | `action` |
53
- |---|---|---|
54
- | `(button & 96) === 0` and `(button & 3) === 3` | 3, 7 (shift), 19 (ctrl) | `"release"` |
55
- | `button & 32` (motion), not the above | 32, 35, 64\|32… | `"move"` |
56
- | otherwise | 0, 1, 2, 64, 65 | `"press"` |
57
-
58
- Bit 6 (`64`) is excluded from the release rule: wheel events (`64`/`65`, with or without modifiers)
59
- keep the `"press"` classification that the wheel path reads. Bit 5 (`32`) is excluded as well, so
60
- "motion with no button" (`35`) stays `"move"`. Modifier bits 4/8/16 leave `button & 3 === 3` intact,
61
- so a modifier-carrying release is still a release. The SGR branch (`M`/`m` suffix) and the passive
62
- branch are unchanged — see §6.
63
-
64
- Byte literals used from here on follow that encoding: `ESC [ M` + `chr(Cb + 32)` + `chr(col + 32)` +
65
- `chr(row + 32)`, six bytes total. Worked examples for body row 0 (= terminal row 2, `chr(34) = "`):
66
- press at col 1 = `\x1b[M !"`, motion at col 8 = `\x1b[M@("` (`"(" = 40 = 8 + 32`), release at col 20 =
67
- `\x1b[M#4"` (`"4" = 52 = 20 + 32`). A release report is therefore exactly six bytes — a seven-byte
68
- string is not an X10 report and `parseMouseInputChunk` rejects the whole chunk (all-or-nothing, §6.2).
69
-
70
- Intended implementation shape (behaviour above is normative; the exact shape may be adjusted in the
71
- plan as long as the decision table holds):
72
-
73
- ```js
74
- // X10/normal mouse: ESC [ M Cb Cx Cy. Cb is encoded as button + 32.
75
- if (input.startsWith("\x1b[M") && input.length >= 6) {
76
- // Cb packs the button (bits 0-1), modifiers (2-4), motion (5) and wheel (6).
77
- // Bit value 3 in bits 0-1 is the protocol's release code: it carries no
78
- // button identity, so treating it as a press cleared the drag selection on
79
- // release instead of copying it (issue #188).
80
- const button = input.charCodeAt(3) - 32;
81
- const motion = (button & 32) !== 0;
82
- const wheel = (button & 64) !== 0;
83
- const release = !motion && !wheel && (button & 3) === 3;
84
- return {
85
- length: 6,
86
- raw: input.slice(0, 6),
87
- mouse: {
88
- encoding: "x10",
89
- button,
90
- col: input.charCodeAt(4) - 32,
91
- row: input.charCodeAt(5) - 32,
92
- action: release ? "release" : motion ? "move" : "press",
93
- },
94
- };
95
- }
96
- ```
97
-
98
- ### 3.2 Why the parser layer, not the handler
99
-
100
- `mouse.action` has exactly three consumers, all in `handleLocalMouseEvent`
101
- (`src/ui/pty-attach.ts:1036`, `:1065`, `:1075`), and the SGR branch already expresses release
102
- through this same field. Classifying in the parser keeps one source of truth for "what did the
103
- terminal report", leaves the handler's protocol-agnostic branches untouched, and keeps the decision
104
- inside a pure function (see §7). A handler-side patch (e.g. treating `button === 3` presses as
105
- releases) is rejected: it would make the same fact true in two places and would misfire on the SGR
106
- path, where `button` is carried on release with its real identity.
107
-
108
- ### 3.3 Alternatives considered
109
-
110
- | Alternative | Verdict |
111
- |---|---|
112
- | Classify in the parser with the bit test above (chosen) | One edit site, pure, testable at the existing unit seam |
113
- | Detect release in `handleLocalMouseEvent` by `mouse.button === 3` | Rejected: duplicates truth, misfires for SGR releases that carry `button = 0/1/2`, and hides the protocol fact from the parser's unit tests |
114
- | Infer "release" from a stateful "a press is pending" flag in the component | Rejected: adds state to fix a stateless decoding bug; a lost press (e.g. mixed-chunk drop, #189) would invert the mapping |
115
- | Treat any non-primary press as a release | Rejected: right/middle presses are real events and must keep clearing/acting as they do today |
116
-
117
- ## 4. Data flow
118
-
119
- `outer terminal → pi input pipeline → PtyAttachComponent.handleInput(data)` (`:301`) →
120
- `parseMouseInputChunk` (`src/core/pty-scroll.mjs:84`) → `parseMouseEventPrefix` per report →
121
- `{ raw, mouse }` → `handleMouseInputChunk` (`:981`) → wheel branch by `button & 64` **or**
122
- `handleLocalMouseEvent` by `action`. The fix changes only the `action` value produced for `Cb = 3`;
123
- everything downstream is unchanged.
124
-
125
- ## 5. Acceptance matrix
126
-
127
- | ID | Functional point | Verification | Concrete check | Pass criterion |
128
- |---|---|---|---|---|
129
- | A1 | X10 press / motion / release / modifier-release classification | automated — **unit** | extend `test/pty-scroll.test.mjs` `parseMouseEvent` cases: `\x1b[M !"` → press, `\x1b[M@!"` → move, `\x1b[M#!"` → release, `\x1b[M'!"` (7 = shift+release) → release, `\x1b[M3!"` (19 = ctrl+release) → release; run `node --test test/pty-scroll.test.mjs` | every case decodes `button`, `col`, `row` and the `action` from §3.1 |
130
- | A2 | Failure and unchanged branches do not shift | automated — **unit** | same file: X10 wheel `\x1b[M\`!"` (64) and `\x1b[Ma!"` (65) → press and `mouseWheelDirection` still +1/−1; middle button `\x1b[M!!"` (1) → press, button 1 (backs §6.1's middle-press claim: the component path it feeds — `middleButton` → `pastePrimarySelection` — is already driven SGR-encoded by `test/pty-attach-mouse.test.mjs`, so classification is the only X10-specific link); motion-with-button-3 `\x1b[MC!"` (35) → move; right button `\x1b[M"!"` (2) → press; `parseMouseEvent("x")` → null; passive `\x1b[?32;7;8M` → move and `\x1b[?0;7;8m` → release; run `node --test test/pty-scroll.test.mjs` | values identical to `f81949d` except the intended release rows |
131
- | A3 | Multi-report X10 chunk decodes in order | automated — **unit** | same file: `parseMouseInputChunk` over the concatenation of three X10 reports — press col 1 row 2 `\x1b[M !"`, motion col 8 row 2 `\x1b[M@("`, release col 20 row 2 `\x1b[M#4"` (byte rule in §3.1) | 3 entries with exact `raw` slices and `action` = press / move / release |
132
- | A4 | X10 drag release copies the selection | automated — **component E2E** | new `test-support/pty-attach-x10-scenario.ts` (real `PtyAttachComponent` + real runner socket + fake PTY, modelled on `test-support/pty-attach-mouse-scenario.ts`) driven by new `test/pty-attach-x10.test.mjs` via `node --experimental-transform-types`; feed the same three reports as A3 (`\x1b[M !"` → `\x1b[M@("` → `\x1b[M#4"`) through `handleInput`, waiting for the fixture line to render first | exactly one OSC 52 (`\x1b]52;c;<base64>\x07`) write, whose payload is the selected line text (`fake pi ready`) |
133
- | A5 | Non-drag branches keep today's behaviour | automated — **component E2E** | same harness: same-point X10 click (`\x1b[M !"` → `\x1b[M#!"`), X10 right press + release (`\x1b[M"!"` → `\x1b[M#!"`), X10 wheel (`\x1b[M\`!"`) with a viewport that cannot scroll | click and right-button gestures emit **no** OSC 52; the click's 1-cell highlight is gone after the pending-click window; the wheel sequence is forwarded to the child as `{type:"input"}` and the process stays alive |
134
- | A6 | No regression across the repo, build integrity | automated — **static + full suite** | `npm run verify` (`package.json`) | exit code 0 (typecheck, perf gate, `node --test test/*.test.mjs`, coverage thresholds, `pack:dry`) |
135
- | U1 | Real-world X10-only terminal behaviour | manual — **optional, non-blocking** | see §6.3 | drag in the attach view of a terminal that reports X10 copies on release; recorded as `pending` unless executed |
136
-
137
- Notes on the matrix:
138
-
139
- - A1–A3 are the lowest-cost level that can prove the decoding rule; A4/A5 are the lowest level that
140
- can prove the *effect* (copy on release), because the effect lives behind a private handler with
141
- TUI + socket + xterm dependencies (see §7).
142
- - A4 asserts the copy effect only. Highlight persistence after a copy is today's behaviour and the
143
- subject of the open bug #172, so #188 neither pins it nor changes it (see §6.2).
144
- - Failure branches are covered by A2 (unrecognised input → `null`, unchanged classifications) and A5
145
- (click / right button / wheel).
146
- - A6 is the repo's definition of done (`AGENTS.md`); it is not a substitute for A4/A5, which pin the
147
- new behaviour.
148
-
149
- ## 6. Boundaries and non-goals
150
-
151
- ### 6.1 Explicitly not changed
152
-
153
- - **Wheel** (`Cb` with bit 6), including wheel-with-modifier: classification stays `"press"`, which
154
- is what the wheel branch (`button & 64`) and `mouseWheelDirection` read.
155
- - **Click semantics**: same-point press + release still schedules the pending click and clears the
156
- 1-cell highlight after `DOUBLE_CLICK_MS` (260 ms) — the release branch's `shouldCopy === false`
157
- path is untouched.
158
- - **Middle press** (PRIMARY paste) and **right press** (selection clear) keep their behaviour; a
159
- right release (`Cb = 3` after `Cb = 2`) now reaches the release branch, which, with no selection,
160
- returns at `:1064` (`if (!this.selection) return;`) — same observable result as before (nothing
161
- copied), verified by A5.
162
- - **SGR and passive branches.** SGR releases are already marked by the `m` suffix. The passive form
163
- (`CSI ? … M/m`, contour-terminal passive mouse tracking, `CSI ? 2029 h`) marks release with the
164
- lowercase `m` as well and a passive terminal "will ONLY allow SGR transport mode", so the X10
165
- button-code rule does not apply there — A2 pins that the passive rows are unchanged.
166
- - **Fullscreen path** (`handleMouse`, pi-tui `TuiAltScreen`): untouched. pi-tui parses SGR only, so
167
- X10 non-wheel reports are dropped upstream in that mode; that is an upstream limitation, not this
168
- issue.
169
- - **Cutting back the X10 branch, or adding 1005/1015 encodings**: no.
170
-
171
- ### 6.2 Interaction with sibling issues (must not be pre-empted)
172
-
173
- - **#187 (motion-less drags) — landed on `main` @ `074123c` while this PR was in flight.** The
174
- release branch now decides "was this a drag?" by screen geometry (`selectionReleaseIsDrag`) instead
175
- of the move flag alone. #188 still owns only the decoding of `Cb = 3`; the geometry rule stays
176
- #187's. Interaction as landed: an X10 gesture *with* motion copies (A4), a same-point X10 release is
177
- still a click (A5), and an X10 gesture *without* motion events now also copies — that last row is
178
- #187's acceptance, not #188's, and is deliberately not asserted here. #188's acceptance never
179
- depended on #187 landing.
180
- - **#172 (highlight survives a copy-on-release)** is an open bug asking to *clear* the selection in
181
- the very `shouldCopy` branch this issue exercises. Retention is
182
- today's behaviour, not a contract; #188 must not add an assertion that pins it and must not add the
183
- clearing either — that call belongs to #172. A4 therefore asserts the copy effect only. Whichever
184
- of the two merges second rebases and re-runs A6.
185
- - **#189 (mixed chunks) — landed on `main` @ `8a5966e` while this PR was in flight.** It kept
186
- `parseMouseInputChunk`'s all-or-nothing contract (verified after the rebase: the 7-byte chunk still
187
- returns `null`) and added `splitAttachInput` for mixed reads, so A3's assertion is unchanged and the
188
- component scenario's pure-mouse reports keep taking the same path. A4's three reports are exactly
189
- six bytes each for this reason.
190
-
191
- ### 6.3 Guarantee boundary and the manual item U1
192
-
193
- - **No release, no copy**: terminals in X10 *compatibility* mode (`?9`, press-only reports) never
194
- send a release, so nothing changes for them; xterm encodes MB3 presses there as `Cb = 2`
195
- ("`Cb` is button−1"), so the new release rule cannot capture a real press.
196
- - The mapping in §3.1 is the protocol definition, not a heuristic: xterm ctlseqs states the low two
197
- bits are `0 = MB1 pressed, 1 = MB2 pressed, 2 = MB3 pressed, 3 = release`.
198
- - **U1 (optional, non-blocking, expected status `pending`)**: the operator has no X10-only terminal,
199
- so the real-terminal check cannot gate this change. Recipe if it is ever run: start the board
200
- (`pi`) inside a PTY wrapper that strips `\x1b[?1006h` from the byte stream on its way to the real
201
- terminal (so the terminal falls back to the X10/normal encoding while the board still reports
202
- `?1000`/`?1002`), then drag-select in an attach view and paste. Prerequisites: (a) the wrapper
203
- (about 30 lines using the repo's `node-pty` dependency), a terminal that accepts `?1000`/`?1002`,
204
- regular TUI mode (`tuiMode: "regular"`), and a session with selectable text; (b) the code under
205
- test — point the dev symlink at the #188 worktree (`ln -s "$(pwd)" ~/.pi/agent/extensions/agent-board`,
206
- `README.md:68`) or run U1 after the merge on a release that contains it, otherwise the run validates
207
- whatever checkout the symlink already points at. Observation: on release the OSC 52 copy lands in
208
- the clipboard (this is the acceptance); whether the highlight then stays is today's behaviour and an
209
- open bug (#172), so it is not an acceptance criterion here. Failing an execution window, the
210
- component-level E2E (A4/A5) drives the exact byte stream through the real component and socket, and
211
- is the strongest evidence available.
212
-
213
- ### 6.4 Rollback
214
-
215
- Single-commit revert of the parser change; no persisted state, no config, no env flag, no migration.
216
- Reverting restores today's (broken) X10 release classification and changes nothing else.
217
-
218
- ## 7. Testability split design
219
-
220
- - **Pure decision layer (must stay pure)**: the rule lives inside `parseMouseEventPrefix`
221
- (`src/core/pty-scroll.mjs:31`), reachable through the already-exported pure functions
222
- `parseMouseEvent(data)` and `parseMouseInputChunk(data)`. A1–A3 drive those directly: no I/O, no
223
- TUI, no socket. The implementation must not move the classification out of the parser (no new
224
- export, no new helper needed, no new env flag or config key).
225
- - **Effect layer (private, covered at the component seam)**: `handleLocalMouseEvent`
226
- (`src/ui/pty-attach.ts:1033`) is private and depends on the TUI, the runner socket and the inner
227
- xterm instance; it is not unit-testable and gets no new seam. A4/A5 cover it through the public
228
- `handleInput(data)` entry point with observable effects only: OSC 52 written to
229
- `tui.terminal.write`, `{type:"input"}` frames written to the socket, and `render()` output.
230
- Selection *state* is deliberately not asserted through private fields, and A4 asserts the copy
231
- (OSC 52 payload) rather than the highlight's fate — its persistence is #172's contested subject
232
- (§6.2). In A5, "the click's highlight is gone" is observed through the pending-click window
233
- (`DOUBLE_CLICK_MS = 260`, `src/ui/pty-attach.ts:48`) plus the absence of a copy.
234
- - **Test boundary**: parser classification (A1–A3) / component reaction (A4–A5). The split mirrors
235
- the existing pair `test/pty-scroll.test.mjs` (pure) + `test-support/pty-attach-mouse-scenario.ts`
236
- with `test/pty-attach-mouse.test.mjs` (component, `node --experimental-transform-types` scenario
237
- printing one JSON line consumed by the test). Naming follows those files
238
- (`test-support/pty-attach-x10-scenario.ts`, `test/pty-attach-x10.test.mjs`).
239
- - **Fixture realism**: the scenario reuses `test-support/fake-pty-pi.mjs` (default mode prints
240
- `fake pi ready`, stable, one body line) and a PATH-stubbed `xclip` so `writePrimarySelection()`
241
- never touches the host clipboard.
242
-
243
- ## 8. Risks
244
-
245
- | Risk | Assessment / mitigation |
246
- |---|---|
247
- | Misclassifying a real press as release | `Cb = 3` is defined as release by xterm; X10 compatibility mode encodes MB3 presses as 2 and sends no releases. A2 pins right-button press. |
248
- | Wheel regression | The release rule excludes bit 6; A2 pins both wheel directions and `mouseWheelDirection`. |
249
- | Changing shared files (`src/core/pty-scroll.mjs`, `test/pty-scroll.test.mjs`) while #187/#189 were in flight | Realised: both landed first (#189 @ `8a5966e`, #187 @ `074123c`). Rebase resolved one both-added-tests conflict in `test/pty-scroll.test.mjs`; the parser change auto-merged cleanly and `parseMouseInputChunk`'s contract survived #189. A6 re-run on the rebased head. |
250
- | A4's OSC 52 payload depends on the fixture line | The harness waits for the fixture line to render before the gesture (same pattern as the existing mouse scenario) and asserts the exact payload string. |
251
- | #172 lands and clears the selection in the copy branch | A4 carries no retention clause, so #172 does not invalidate it; only the rebase (same file, `src/ui/pty-attach.ts` release branch region) and a re-run of A6 are needed. |
252
- | Coverage thresholds (`npm run verify`) | New pure branches are exercised by A1/A2; the scenario adds one integration test. If coverage dips, the parser cases (not the assertions) are extended. |