@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,67 +0,0 @@
1
- # Issue #198 — attach wheel forward-fallback (production topology)
2
-
3
- - Issue: https://github.com/zhuxixi/pi-agent-board/issues/198
4
- - Kind: bug — root-cause report + fix design (follow-up to #183 / #185)
5
- - Baseline: `main` @ `eab48d0` · worktree `issue-198-attach-wheel-forward-fallback`
6
- - Drafted: deepseek/deepseek-flash (selected) · deepseek/deepseek-flash (physical) @ 2026-10-16T00:1xZ
7
-
8
- ## 1. Problem
9
-
10
- #185 shipped and the fullscreen wheel was still dead in the reporter's real session, while `PageUp`/`PageDown` and middle-click paste both worked. That combination rules out the input channel (mouse events reach `handleMouse`, keys reach the child) and points at the wheel's own routing decision.
11
-
12
- ## 2. Root cause
13
-
14
- `#185` routes on `this.term.buffer.active.type`, and that signal is structurally unavailable in production:
15
-
16
- 1. **The screen-log replay is a 100 KB tail** (`SCREEN_LOG_REPLAY_BYTES = 100_000`, `src/core/screen-log.mjs:15`). A real fullscreen child sends `\x1b[?1049h` once at startup, long before the window.
17
- 2. **The snapshot path carries no alternate-screen state.** `src/core/terminal-model.mjs`, `src/core/terminal-snapshot.mjs` and `src/core/terminal-attach-protocol.mjs` have no `alternate` / `1049` / `altScreen` handling; the synthesized frame carries rendered content only.
18
- 3. **Measured:** the reporter's live `screen.log` (420 KB) contains **zero** `\x1b[?1049h`; the new fallback scenario pins the consequence — with the enable outside the window the attach reads `buffer.active.type === "normal"` with `bottomViewportTop(bodyHeight) === 0`.
19
-
20
- So the wheel routes `"local"`, finds no attach-local scrollback (a fullscreen TUI paints with absolute positioning, so the normal buffer never scrolls and `baseY` stays 0), and #183's contract returns `undefined`: pi then scrolls the transcript hidden behind the overlay. The `"alternate"` → `"child"` route is effectively **unreachable in production** today; #185's local test only saw it because its fixture re-asserted the enable on every tick, which put the sequence back into the live stream. Removing that hack is part of this fix.
21
-
22
- ## 3. Fix design
23
-
24
- Route order inside `handleMouse` (`src/ui/pty-attach.ts`), unchanged helpers (`wheelScrollLinesUp`, `resolveAttachWheelRoute`, `encodeWheelReport`):
25
-
26
- | Condition | Behaviour |
27
- | --- | --- |
28
- | `wheelLinesUp === 0`, or the kill switches are off (`route === "none"`) | unchanged: `undefined` |
29
- | `route === "local"` and `tryScrollBy(linesUp)` moved | unchanged #183: `{handled:true, render:false}` |
30
- | `route === "child"`, or `route === "local"` with a viewport that cannot move | **new**: forward one synthesized SGR report — the same fallback the raw-SGR path and `PageUp`/`PageDown` already use |
31
- | any forward, attach disconnected | unchanged #185 CR rule: `undefined` (a forward `send()` would drop is not an effect) |
32
-
33
- The `"child"` fast path is kept: when the type *is* known, skipping the inert local viewport avoids a two-line jitter before the child takes over. Correctness no longer depends on it.
34
-
35
- README (`PTY attach` paragraph) states the parity rule: local scrollback while it can move, otherwise forwarded to the session.
36
-
37
- ## 4. Testability split
38
-
39
- No new pure helper: the decision is a reordering of existing branches. Coverage instead pins the two topologies:
40
-
41
- | Scenario | Topology | Asserts |
42
- | --- | --- | --- |
43
- | `test-support/pty-attach-wheel-fallback-scenario.ts` (**new**) | child enters the alternate screen once, floods >130 KB with absolute-positioned frames, then the attach connects — i.e. the enable is outside the replay window | `buffer.type === "normal"`, local room `0`, wheel up/down forwarded as `\x1b[<64;4;3M` / `\x1b[<65;4;3M`, `{handled:true, render:false}`, viewport stays `null`, kill switch falls through |
44
- | `test-support/pty-attach-alt-wheel-scenario.ts` (updated) | same child, young log, **legacy replay path forced** (`AGENT_BOARD_TERMINAL_SNAPSHOT=0`) so the enable is replayed | the `"child"` route still skips the local viewport; documents *why* it needs the legacy path |
45
- | `test-support/pty-attach-wheel-scenario.ts` (#183, updated comment) | socketless/ disconnected | the disconnected half of the rule: falls through instead of dropping a forward |
46
-
47
- ## 5. Acceptance matrix
48
-
49
- | ID | 功能点 | 验收方式 | 具体验证 | 通过标准 |
50
- | --- | --- | --- | --- | --- |
51
- | A1 | 生产拓扑下滚轮转发 | 自动化(integration) | `node --test test/pty-attach-wheel-fallback.test.mjs` | 拓扑断言(normal + room 0)成立;up/down 各转发一条精确 SGR 报文;`{handled:true,render:false}`;视口保持 null |
52
- | A2 | `"child"` 分支仍覆盖 | 自动化(integration) | `node --test test/pty-attach-alt-wheel.test.mjs` | legacy 重放路径下 `bufferType === "alternate"`,视口不动,报文转发 |
53
- | A3 | 断连不消费 | 自动化(integration) | `node --test test/pty-attach-wheel.test.mjs` | socketless 场景仍返回 `undefined` |
54
- | A4 | 既有行为不回归 | 自动化(integration) | `npm test`(mouse / mouse-mode / selection-mirror / wheel 套件) | 全绿 |
55
- | A5 | 门禁 | 自动化(static + suite) | `npm run verify` | typecheck / perf / tests / coverage / pack:dry 全过 |
56
- | A6 | 文档同步 | 自动化(static) | `rg -n "mouse wheel" README.md` | 描述为「本地能滚就滚本地,滚不动就转发」 |
57
- | U1 | 真实会话里滚轮可用 | 用户实测 | 合并 → `git pull` → 重启 pi → attach 长会话滚轮上下 | 与 `PageUp`/`PageDown` 等效;中键与拖选不变 |
58
-
59
- ## 6. Non-goals / boundaries
60
-
61
- - Not fixing the signal itself (teaching the runner's terminal model + snapshot DTO the alternate-screen flag) — a protocol change that is no longer needed for correctness; recorded as a possible follow-up.
62
- - The `"alternate"` route stays as a fast path; in today's production it is usually not taken, which is documented here and in the scenario header.
63
- - No change to `AGENT_BOARD_WHEEL_LINES` semantics, the kill switches, or the middle-click/selection paths.
64
-
65
- ## 7. Next step
66
-
67
- Step 5 of `github-issue-driven`: land this spec in the worktree, plan, implement (done, TDD red → green), local CR, PR with Zima single-bot CR.
@@ -1,250 +0,0 @@
1
- Drafted: deepseek/deepseek-flash (selected) · deepseek/deepseek-flash (physical) @ 2026-10-18T13:34:00+08:00
2
- Revised: deepseek/deepseek-flash (selected) · deepseek/deepseek-flash (physical) @ 2026-10-18T14:05:00+08:00 — 第 1 轮复核处置(R1-G1 公式修正、R1-G2 测试接缝、R1-D1..D9)
3
- Revised: deepseek/deepseek-flash (selected) · deepseek/deepseek-flash (physical) @ 2026-10-18T14:20:00+08:00 — 第 2 轮复核处置(R2-G1 非有限数归零、R2-G2 reset 接线与验收、R2-D1 帧投递接缝、R2-D2 记号)
4
- Revised: deepseek/deepseek-flash (selected) · deepseek/deepseek-flash (physical) @ 2026-10-18T14:35:00+08:00 — 第 3 轮复核处置(收敛轮,0 真缺口;R3-D1..D5 细化)
5
-
6
- # issue #205 根因报告与修复设计:attach 滚轮被「快照帧溢出」的伪 scrollback 吞掉
7
-
8
- 仓库:zhuxixi/pi-agent-board · 基线:`origin/main` @ `b837286`(含 #204)
9
-
10
- ## 1. 症状(报告者环境)
11
-
12
- pi fullscreen TUI → `/agent-board` → attach 会话:
13
-
14
- - 滚轮能"动",但滚动出来的是**旧屏幕碎片**(同一 editor 行重复),不是子会话 transcript
15
- - PageUp/PageDown 正常滚动子会话
16
- - `AGENT_BOARD_WHEEL_LINES=3` 在 fullscreen 下无效(文档已注明,非根因)
17
- - 表现随终端高度变化,矮终端更严重
18
-
19
- ## 2. 根因
20
-
21
- ### 2.1 结论
22
-
23
- `PtyAttachComponent` 把 **hydrate 快照帧时滚出本地视口的行**当成了"会话历史",
24
- 于是 `tryScrollBy()` 恒为真,#199 的 `forwardWheelToChild()` 兜底永不触发。
25
-
26
- ### 2.2 机制
27
-
28
- 协议路径(`attachMode === "protocol"`)下:
29
-
30
- 1. 构造期 `replayScreenLog()` 先写入 60 KB legacy 重放(本问题无关,见 §2.4)
31
- 2. runner 用**它自己的尺寸**(子进程 PTY 尺寸,如 `300x65`)`captureTerminalSnapshot()` 并合成
32
- `synthesizeFullRedrawFrame()`(65 行 viewport + 0 行 scrollback)
33
- 3. attach 收到 `snapshot_begin { cols: 300, rows: 65 }`,发现与自身尺寸(`rows = terminal.rows - 2 = 38`)不符,
34
- 只 `sendResize()`,**仍然接受并按 38 行终端 hydrate 这个 65 行的 frame**
35
- 4. `synthesizeFullRedrawFrame` 前导 `\x1b[2J\x1b[3J` 清了 viewport 与 scrollback
36
- (`src/core/terminal-snapshot.mjs:329-335`),但随后写入的 65 行 > 38 行,
37
- xterm 自然滚动出 **`65 - 38 = 27` 行** → `buffer.active.baseY = 27`
38
- 5. `bottomViewportTop(38) = min(65, 27 + 38) - 38 = 27` → `tryScrollBy()` 认为有 27 行历史可滚
39
- 6. 这 27 行是**当前屏幕顶部若干行**(不是历史),用户滚轮看到的就是它们 → "滚的是输入框内容的历史"
40
-
41
- **行溢出不是唯一来源**:列宽不匹配(300 列的绝对定位帧写入更窄的本地 xterm)会按列换行,
42
- 同样推高 `baseY`。因此判据不能只算 `rows` 差,必须取**实际写入后的 `baseY` 增量**。
43
-
44
- 核心关系:
45
-
46
- ```
47
- baseY_after_hydrate = 真实历史行(runner 快照的 scrollback)+ 溢出伪历史行(帧写入时被挤出视口的屏幕顶部行)
48
- 真实历史行 = snapshot.scrollback.length ← runner 唯一知道,需随协议下发
49
- 可向上滚的行数 = max(0, baseY - historyFloor)
50
- historyFloor = baseY_after_hydrate > scrollbackLines ? baseY_after_hydrate : 0
51
- ```
52
-
53
- **为什么是"全有或全无"**:`synthesizeFullRedrawFrame` 先写真实 scrollback 行、再写 viewport 行
54
- (`src/core/terminal-snapshot.mjs:336-351`),所以 hydrate 之后本地 buffer 的布局是
55
-
56
- ```
57
- [0 .. S) 真实历史(最旧优先)
58
- [S .. S+k) 制造行:被挤出视口的屏幕顶部 k 行
59
- [S+k ..) 视口
60
- baseY_after = S + k
61
- ```
62
-
63
- 制造行**夹在真实历史和视口之间**,任何本地滚动都必然先穿过它们;单一 floor 常量无法跳过中间块。
64
- 因此只要发生溢出就把 floor 设为 `baseY`(本地历史整体禁用、滚轮全部转发),不牺牲最旧的真实行。
65
-
66
- ### 2.3 实证(真实链路,无 mock 组件)
67
-
68
- 一次性探针:假 runner 用**完整** `screen.log` 构建 `createTerminalModel()`(模拟真实 pty-runner),
69
- `captureTerminalSnapshot()` + `synthesizeFullRedrawFrame()` 原样回包;attach 侧用真实
70
- `TuiAltScreen` + 真实 `PtyAttachComponent`,喂真实 `ESC[<64;10;10M`。
71
-
72
- ```
73
- protocolReady: true, attachMode: "protocol"
74
- runnerType: normal, runnerBaseY: 0, runnerLength: 65
75
- snapshotScrollback: 0, snapshotViewportRows: 65
76
- attachType: normal, attachBaseY: 27, attachLength: 65
77
- bodyHeight: 38, bottomViewportTop: 27
78
- → firstForwardAt: 28 (第 28 个滚轮事件才到达子会话)
79
- ```
80
-
81
- 该探针是一次性脚本(未入库),**A4 的 scenario 落地后必须复现同组数值**(见 §5/§6)。
82
- legacy(无 protocol)路径同源:`rows=30` 时测得 `firstForwardAt: 65`。
83
-
84
- ### 2.4 与 legacy 重放的关系
85
-
86
- 构造函数里的 60 KB 重放(`ATTACH_REPLAY_BYTES = 60_000`)在 protocol 路径会被
87
- `\x1b[3J` 清空,**不是**本次根因;它只在 pre-protocol runner / `AGENT_BOARD_TERMINAL_SNAPSHOT=0`
88
- 降级路径下贡献同类伪历史。本修复**不覆盖**该路径(见 §4 非目标)。
89
-
90
- ## 3. 修复设计
91
-
92
- ### 3.1 核心:`historyFloor` 基线
93
-
94
- 组件维护 `historyFloor`("早于它的一切都是伪历史"的 baseY 基线),
95
- "能否向上滚"的判定为 `maxScrollUp = max(0, baseY - historyFloor)`。
96
-
97
- 更新时机(唯一三处):
98
-
99
- | 时机 | 动作 |
100
- | --- | --- |
101
- | `pushOutput(frame, { authoritative: true })` 的 `term.write` 回调 | `historyFloor = historyFloorAfterHydrate(baseY, scrollbackLines)`,`scrollbackLines` 取自本次 `snapshot_ready` 的 payload(见 §3.5),缺省 0 |
102
- | `term.reset()`(`snap.empty` / `resnapshot`) | `historyFloor = 0`;接线位置见 §3.3,验收见 A6b |
103
- | `term.resize()` 之后(`resizeIfNeeded`) | `historyFloor = buffer.active.baseY`(见 §3.4 保证边界) |
104
-
105
- **不使用 `snapshot_begin.rows` 推算溢出**:行差会被列换行放大,`baseY` 是实测值,唯一可靠。
106
-
107
- ### 3.2 纯函数拆分(`src/core/pty-scroll.mjs`)
108
-
109
- ```js
110
- /**
111
- * baseY 在 hydrate 之后包含了「本次写入强制滚出的行」。runner 快照里真正属于
112
- * 历史的只有 scrollbackLines 行,其余一律是伪历史。返回可视为历史的起点。
113
- */
114
- export function historyFloorAfterHydrate(baseYAfter, scrollbackLines)
115
-
116
- /** max(0, baseY - historyFloor):真正可以往回滚的历史行数。 */
117
- export function scrollableHistoryLines(baseY, historyFloor)
118
-
119
- /** 现有函数增加 floor 参数(视口起点下限),默认 0 时与原实现逐例等价。 */
120
- export function scrollViewportTop(viewportTop, bottom, linesUp, floor = 0)
121
- ```
122
-
123
- `scrollViewportTop` 语义扩展:
124
-
125
- ```
126
- safeBottom = max(0, floor(bottom))
127
- safeFloor = clampInt(floor, 0, safeBottom)
128
- current = viewportTop == null ? safeBottom : clampInt(viewportTop, safeFloor, safeBottom)
129
- next = clampInt(current - linesUp, safeFloor, safeBottom)
130
- → { viewportTop: next >= safeBottom ? null : next, changed: next !== current }
131
- ```
132
-
133
- `historyFloorAfterHydrate(baseYAfter, scrollbackLines)`:
134
-
135
- ```
136
- // 非有限数(undefined / NaN / Infinity)一律归 0——老 runner 不发送该字段
137
- safeBase = Number.isFinite(baseYAfter) ? Math.max(0, Math.floor(baseYAfter)) : 0
138
- safeScrollback = Number.isFinite(scrollbackLines) ? Math.max(0, Math.floor(scrollbackLines)) : 0
139
- // 溢出(baseY > scrollback)=> 制造行夹在真实历史与视口之间 => 整体禁用本地历史
140
- // 未溢出 => 视口之上的每一行都是真实历史 => floor 0,全部可滚
141
- return safeBase > safeScrollback ? safeBase : 0
142
- ```
143
-
144
- 语义:`baseYAfter` 至少为 0;`scrollbackLines` 只用来判断"是否溢出",不用来计算"保留多少行历史"。
145
- **不得**用 `max(0, floor(x))` 处理未校验的输入(`Math.floor(undefined) === NaN`,会污染整条判定链)。
146
-
147
- ### 3.3 组件接线(`src/ui/pty-attach.ts`)
148
-
149
- - `pushOutput(data, opts)` 增加可选 `opts.scrollbackLines?: number`;
150
- `term.write` 回调里 `if (opts.authoritative) this.historyFloor = historyFloorAfterHydrate(this.term.buffer.active.baseY, opts.scrollbackLines)`(回调内没有 `buf` 绑定,必须重新取 `this.term.buffer.active`)
151
- - `handleAttachEvent("snapshotReady")` 的 `else if (snap.empty || snap.resnapshot)` 分支(`src/ui/pty-attach.ts:1597-1601`):`this.term.reset()` 之后**必须** `this.historyFloor = 0`,否则 host 重启/重订阅后的 `baseY` 已归零而 floor 仍是旧值,前 floor 行无法本地滚动
152
- - `handleAttachEvent("snapshotReady")`:`this.pushOutput(snap.frame, { authoritative: true, scrollbackLines: snap.scrollbackLines })`
153
- (不再在组件里计算 overflow,也不引入 `pendingFrameOverflow` / `snapshotRows`)
154
- - 新增 `private scrollFloorStart(height)`:
155
- `bottom = bottomViewportTop(height)`;`floorStart = max(0, bottom - scrollableHistoryLines(baseY, historyFloor))`
156
- - `tryScrollBy` / `tryScrollToTop` / `clampViewportTop` 改用 `scrollFloorStart()` 作为下限
157
- - `resizeIfNeeded` 的 `term.resize()` 后:`this.historyFloor = buffer.active.baseY`
158
-
159
- ### 3.4 保证边界
160
-
161
- - **hydration 溢出即禁用本地历史**:只要 `baseY > scrollbackLines`,`historyFloor = baseY`,本地一行都滚不动、滚轮全部转发给子会话。制造行夹在真实历史与视口之间(见 §2.2),这是唯一不牺牲最旧真实行的选择。
162
- - **resize 之后本地滚动历史清零**:`historyFloor` 被设为当前 `baseY`,"宁可暂时不能本地滚,也不把伪历史当历史滚"。resize 之后的真实历史只保留在子会话侧(可转发滚动)。
163
- - **老 runner 没有 `scrollbackLines`**:按 0 处理 → 本地历史被禁,滚轮全部转发给子会话(对 fullscreen 子会话是正确的;对 regular 子会话是本地滚动能力的退化,记录在 §4)。
164
- - frame 解析是异步的:解析完成前的滚轮事件按旧 floor 处理(最坏情况 = 现状),解析完成后收敛。
165
-
166
- ### 3.5 协议扩展(向后兼容)
167
-
168
- `src/core/terminal-attach-protocol.mjs`:`snapshot_end` 增加
169
- `scrollbackLines: dto.scrollback.length`(runner 侧 `captureTerminalSnapshot()` 已经产出该数组)。
170
-
171
- `src/core/terminal-attach-client.mjs`:把 `snapshot_end.scrollbackLines` 并入 `snapshotReady` payload,
172
- **仅当它是 number 时写入该键,否则省略该键**(不写 `scrollbackLines: undefined`)——
173
- `test/terminal-attach-client.test.mjs:94-95` 的 `deepEqual` 钉死了 `snapshotReady` 的精确形状,
174
- 无条件写键会让既有 happy-path 用例变红;同时更新该文件 `:85` 处 payload 形状的头部注释。
175
-
176
- 老 runner 不发该字段 → 客户端省略键 → `snap.scrollbackLines === undefined` → §3.3 按 0 处理。**无版本闸门**(字段可选,无需 frameVersion 变更)。
177
-
178
- ## 4. 非目标
179
-
180
- - **不改变 legacy(pre-protocol runner)路径的本地滚动**:那条路径的 60 KB 重放是唯一历史来源,
181
- `historyFloor` 保持 0,行为与现状逐例等价。该路径的伪历史问题不在本 issue 范围。
182
- - 不修改 `AGENT_BOARD_WHEEL_LINES` 在 fullscreen 下的语义(上游 `fullscreenWheelScrollLines` 已负责步长)
183
- - 不修正 #198 的 buffer-type 探测(本修复不再依赖它)
184
- - 不新增 `frameVersion` 变更(`scrollbackLines` 是可选字段,缺失即降级)
185
-
186
- ## 5. 验收矩阵
187
-
188
- | ID | 功能点 | 验收方式 | 具体验证 | 通过标准 |
189
- | --- | --- | --- | --- | --- |
190
- | A1 | `historyFloorAfterHydrate` 纯函数 | 自动化(unit) | `test/pty-scroll.test.mjs` 新增用例 | `(27,0)=27`(溢出)、`(127,100)=127`(溢出→禁用)、`(5,10)=0`(未溢出)、`(undefined,0)=0`、`(27,undefined)=27` |
191
- | A2 | `scrollableHistoryLines` 纯函数 | 自动化(unit) | 同上 | `(27,27)=0`、`(127,27)=100`、`(0,0)=0` |
192
- | A3 | `scrollViewportTop` floor 语义 | 自动化(unit) | 同上(含对拍) | floor=bottom 时 `changed=false`;floor=0 时对现有全部输入逐例与旧实现一致 |
193
- | A4 | protocol 快照溢出后首次滚轮即转发 | 自动化(integration) | 新增 `test-support/pty-attach-frame-overflow-scenario.ts` + `test/pty-attach-frame-overflow.test.mjs`:假 runner 用真实 `captureTerminalSnapshot`/`synthesizeFullRedrawFrame`(**runner 模型 rows=65,attach rows=38,scrollback=0**),attach 侧真实 `PtyAttachComponent`,直接调 `component.handleMouse` 驱动 | `attachBaseY===27`、`historyFloor===27`、`scrollableHistoryLines===0`;首个 wheel 返回 `{handled:true,render:false}` 并发出 `{type:"input","data":"\x1b[<64;…M"}`;`viewportTop` 保持 `null` |
194
- | A5 | 溢出时禁用本地历史而不是牺牲最旧真实行 | 自动化(integration) | 同 scenario 变体:runner 先写 100 行普通文本再快照(`scrollbackLines=100`,帧 165 行写入 38 行终端 → 溢出) | `historyFloor === baseY`、`scrollableHistoryLines === 0`,首个 wheel 即转发(未溢出且带历史的情形由 A1 的 `(5,10)=0` 覆盖) |
195
- | A6 | resize 后基线重置(含保证边界) | 自动化(integration) | 同 scenario:hydrate 后 `component.render(<更窄宽度>)` 触发 `term.resize` | `historyFloor === baseY`、`scrollableHistoryLines===0`,下一次 wheel 立即转发 |
196
- | A6b | `term.reset()` 后基线归零 | 自动化(integration) | 同 scenario 的 `snap.empty`(或 `resnapshot`)变体:hydration 后投递一次 empty 基线 | `historyFloor === 0` 且 `baseY === 0`;随后以超过 `bodyHeight` 行的 live 输出填满视口,wheel 可本地滚动(floor 不再残留旧值) |
197
- | A7 | `scrollbackLines` 缺失降级 | 自动化(unit + integration) | unit:`historyFloorAfterHydrate(27, undefined)=27`;integration:scenario 不发 `scrollbackLines` 的变体 | 与 A4 同结果(本地不滚、直接转发),不抛错 |
198
- | A8 | 真实 fullscreen 调度 smoke | 自动化(integration,附加保护) | 同 scenario 额外用例:真实 `TuiAltScreen` + `showOverlay` + `(tui as any).handleTerminalInput("\x1b[<64;10;10M")`(`handleTerminalInput` 在运行时可达,private 只是 TS 类型层;一次性探针已验证可调用) | `handleMouse` 被调用且首个 notch 即转发 |
199
- | A9 | legacy 路径不回归 | 自动化(integration) | 现有 `pty-attach-wheel`、`pty-attach-alt-wheel`、`pty-attach-wheel-fallback` 套件 | 全部通过 |
200
- | A10 | 协议字段兼容 | 自动化(integration) | `test/terminal-attach-protocol*.test.mjs` / client 套件新增断言;同步更新 `test/terminal-attach-client.test.mjs:94-95` 的 `deepEqual` 与 `:85` 形状注释 | `snapshot_end` 带 `scrollbackLines`;client 透传到 `snapshotReady`;`scrollbackLines` 为非 number 时**省略该键**且不报错 |
201
- | A11 | 类型与构建 | 自动化(static/build) | `npm run typecheck`、`npm run verify` | 通过(含 perf gate + coverage 阈值) |
202
- | U1 | 真实终端滚轮 | 用户实测 | 见 §5.1 | 面板立即显示子会话 transcript 的上一行(而非输入框碎片);PageUp 行为保持一致 |
203
-
204
- ### 5.1 U1 执行时机与部署前置
205
-
206
- 1. A1–A11 全绿、PR 合并到 `main`
207
- 2. `cd C:/Users/27499/proj/opensource/pi-agent-board && git pull`
208
- 3. **刷新 pi 的 git 包缓存**(pi 加载的是 `~/.pi/agent/git/github.com/zhuxixi/pi-agent-board`,
209
- 不是本工作区):`pi update`(或重启 pi 让其重新拉取),并确认
210
- `git -C ~/.pi/agent/git/github.com/zhuxixi/pi-agent-board log --oneline -1` 含本 PR 提交
211
- 4. 完整重启 pi(旧进程内的模块缓存不会热更新)
212
- 5. pi fullscreen → `/agent-board` → attach 一个输出超过 60 KB 的长会话 → 拨一次滚轮
213
- 6. 通过标准:第一次滚轮即显示子会话 transcript 的上一行;不出现输入框/旧屏幕碎片;PageUp 仍正常
214
-
215
- ## 6. 可测性拆分设计
216
-
217
- - **决策逻辑全部下沉到 `src/core/pty-scroll.mjs` 纯函数**:`historyFloorAfterHydrate`、
218
- `scrollableHistoryLines`、`scrollViewportTop(…, floor)`。无 I/O、无环境变量读取,可直接单测。
219
- - **组件只承担接线与状态**:`historyFloor` 一个字段 + 三个赋值点(hydrate 回调 / reset / resize),
220
- 不新增任何"计算"逻辑,避免把已拆分的纯函数重新耦合回组件。
221
- - **帧投递接缝(A4–A7 的构造前提)**:scenario 必须让 65 行帧**原样**送达组件,不得触发 runner 侧重新 capture。具体两种合法写法(选一并在场景注释里写明):
222
- 1. 走完整协议链路,但**假 runner 忽略 `snapshot_begin` 之后到达的 `resize` 控制消息**,按固定 65 行 `captureTerminalSnapshot()` 原样回包;
223
- 2. 跳过协议层,直接 `component.pushOutput(frame, { authoritative: true, scrollbackLines })` 投递由真实 `synthesizeFullRedrawFrame()` 生成的帧。
224
- 若假 runner 响应 resize 并重新 capture(38 行),`attachBaseY === 27` 的前提塌陷,A4 永远不成立。
225
- - **连接与发送可观测(A4–A7 的第二个前提)**:`forwardWheelToChild` 只在 `this.socket && this.connected` 为真时才真正 `send`
226
- (`src/ui/pty-attach.ts:474-476`)。scenario 必须建立连接并拦截发送,复用仓库既有模式:
227
- 真实 host socket / 假 runner 监听 pipe,并拦截 `Socket.prototype.write` 收集 `{type:"input"}`
228
- (见 `test-support/pty-attach-wheel-fallback-scenario.ts:113-119`)。用 disconnected 的假 TUI(例如
229
- `nonexistent.sock`)会让 forward 静默 no-op 并返回 `undefined`,A4 永远失败。
230
- - **await 解析完成再断言**:`historyFloor` 在 `term.write` 的回调里才写入(§3.3),而 `@xterm/headless` 是异步解析。
231
- A4/A6b 断言前必须 `await` 写入回调,沿用既有模式
232
- (`test-support/pty-attach-wheel-scenario.ts:75` 的 `await new Promise((resolve) => component.term.write(..., resolve))`)或轮询到回调完成。
233
- - **测试边界**:
234
- - `test/pty-scroll.test.mjs` —— 纯函数(A1–A3、A7 的 unit 部分)
235
- - `test-support/pty-attach-frame-overflow-scenario.ts` —— 真实帧合成 + 真实组件 + 直接 `handleMouse`(A4–A7、A6b)
236
- - 同一 scenario 的真实 `TuiAltScreen` 用例 —— 附加保护(A8),断言"事件确实到达 `handleMouse`"
237
- - 协议/client 套件 —— 字段透传(A10)
238
- - 现有 wheel / alt-wheel / wheel-fallback 套件 —— legacy 无回归(A9)
239
- - A8 用到的 `handleTerminalInput` 是运行时可达的公开实例方法(`private` 只在 TS 类型层不可见),
240
- 通过 `(tui as unknown as { handleTerminalInput(d: string): void }).handleTerminalInput(...)` 构造;
241
- 场景落地时若该签名变化,A8 允许退化为直接调 `dispatchMouseToOverlay`,但**不得**删掉该用例而不记录。
242
-
243
- ## 7. 风险
244
-
245
- | 风险 | 缓解 |
246
- | --- | --- |
247
- | `scrollbackLines` 语义与 frame 内容不一致(runner 快照后又有新 chunk 被 flush) | `snapshot_end` 的 `scrollbackLines` 取自同一次 `captureTerminalSnapshot()` 的 dto,与 frame 同源;catch-up flush 的 chunk 是增量(live output),不影响已 hydrate 的行数 |
248
- | `historyFloor` 让真实历史被判成伪历史 | 只在 hydrate/reset/resize 三处设置;`scrollbackLines` 由 runner 权威给出;resize 场景见 §3.4 明示边界 |
249
- | 老 runner 造成本地滚动退化 | 明确写入 §3.4 / §4:对 fullscreen 子会话是正确行为,对 regular 子会话是能力退化;`pi update` 后消失 |
250
- | 现有测试直接调 `handleMouse`,改动可能未被覆盖 | A4 用真实帧合成覆盖 hydrate 路径;A8 补真实调度链路 |
@@ -1,165 +0,0 @@
1
- Drafted: deepseek/deepseek-flash (selected) · deepseek/deepseek-flash (physical) @ 2026-10-19T12:40:00+08:00
2
- Revised: deepseek/deepseek-flash (selected) · deepseek/deepseek-flash (physical) @ 2026-10-19T13:05:00+08:00 — 第 1 轮复核处置(R1-G1/G2/G3 + D1/D2/D3)
3
- Revised: deepseek/deepseek-flash (selected) · deepseek/deepseek-flash (physical) @ 2026-10-19T13:15:00+08:00 — 第 2 轮复核处置(R2-G1 + D1/D2/D3)
4
- Revised: deepseek/deepseek-flash (selected) · deepseek/deepseek-flash (physical) @ 2026-10-19T13:25:00+08:00 — 第 3 轮复核处置(收敛轮,R3-D1/D2/D3 细化)
5
- Revised: deepseek/deepseek-flash (selected) · deepseek/deepseek-flash (physical) @ 2026-10-19T14:20:00+08:00 — 代码评审批次修复(A8③ 空转 grep、REPLY_HEADS 注释未改、armed 路径未覆盖)
6
-
7
- # issue #213 根因报告与修复设计:reply guard 的 cell-size 前缀吞掉 kitty 编码的 PageDown
8
-
9
- 仓库:zhuxixi/pi-agent-board · 基线:`origin/main` @ `ef4317f`
10
-
11
- ## 1. 症状
12
-
13
- `v0.12.0` + WezTerm 20240203 + pi fullscreen 1.3.0 → `/agent-board` → attach:
14
-
15
- | 键 | 结果 |
16
- | --- | --- |
17
- | `PageUp` | 正常(本地滚动 / 转发) |
18
- | `PageDown` | **完全无反应** |
19
- | 鼠标滚轮 | 正常 |
20
-
21
- 不对称本身就是线索:两个键在 `handleKeyInput` 里走**完全相同**的
22
- `tryScrollBy(±pageSize)` → `send()` 兜底,所以差异只能出在更上游的输入分类层。
23
-
24
- ## 2. 根因
25
-
26
- `src/core/terminal-reply-guard.mjs` 的 `REPLY_HEADS` 含:
27
-
28
- ```js
29
- { family: "cell-size", prefix: "\x1b[6;" }
30
- ```
31
-
32
- 本意是吸收被拆碎的 XTWINOPS cell-size 回复(`CSI 6;<rows>;<cols>t`)。
33
- 但 **PageDown 的 legacy 功能键编号正是 6**:kitty 键盘协议下带修饰键的 PageDown 编码为
34
- `CSI 6;<mod>u`(`\x1b[6;1u`),modifyOtherKeys 回退下为 `CSI 6;<mod>~`(`\x1b[6;5~`)。
35
- 两者都以 `\x1b[6;` 开头 → `classifyReplyEvent` 判成 `reply-head` → guard 进入 armed
36
- → 250ms/64 字节吸收窗口把按键吞掉。PageUp 的编号是 5(`\x1b[5;1u`),永不命中,所以不受影响。
37
-
38
- 附带发现的**文档与代码矛盾**:`REPLY_HEADS` 上方的注释写着
39
- 「Deliberately absent: … `\x1b[5`/`\x1b[6` (PageUp/PageDown heads)」,
40
- 但列表里实际存在 `\x1b[6;` —— 注释想表达的是"不吸收 `\x1b[6` 开头的裸键位",
41
- 实现却用了一个更宽的前缀,正是这条矛盾掩盖了本次冲突。
42
-
43
- 引入点:`2148360 fix(attach): stop terminal reply fragments leaking into the attached child's input (#194)`。
44
-
45
- ## 3. 证据(对仓库当前代码实测)
46
-
47
- ```
48
- "\x1b[6;1u" → { kind: "reply-head", family: "cell-size" } ❌ PageDown 被吞
49
- "\x1b[6;5~" → { kind: "reply-head", family: "cell-size" } ❌ modifyOtherKeys PageDown 被吞
50
- "\x1b[5;1u" → { kind: "other" } ✓ PageUp
51
- "\x1b[6~" → { kind: "other" } ✓ 无修饰的 legacy PageDown
52
- "\x1b[57422u" → { kind: "other" } ✓ kitty codepoint 形式
53
- "\x1b[6;" → { kind: "reply-head", family: "cell-size" } ✓ 真 fragment 仍 arm
54
- "\x1b[6;24" → { kind: "reply-head", family: "cell-size" } ✓
55
- "\x1b[6;24;80t" → { kind: "reply-complete", action: "drop" } ✓
56
- ```
57
-
58
- 现有测试(15 个 `test()` / 63 条 assert,全绿)**没有覆盖** `\x1b[6;<mod>u` / `\x1b[6;<mod>~` 形态 —— 这是回归缺口。
59
- `test/terminal-reply-guard.test.mjs:58` 只测了裸 `\x1b[5~` / `\x1b[6~`,
60
- `:96` 测了 `\x1b[6;` 仍需 arm。
61
-
62
- ## 4. 修复
63
-
64
- **在 head 前缀之前**识别"完整的 CSI 键序列":真 cell-size fragment 没有 `u`/`~` 终结符,
65
- 所以完整键序列可以安全地判成用户输入。
66
-
67
- ```js
68
- // Kitty / modifyOtherKeys keys: `CSI <num>[;<mod>…]{u,~}`. PageDown's legacy number
69
- // is 6, which shares the `\x1b[6;` prefix with the cell-size reply head.
70
- const CSI_KEY_SEQUENCE = /^\x1b\[\d+(?:[;:]\d+)*[u~]$/;
71
-
72
- export function classifyReplyEvent(data) {
73
- …DROP/FORWARD 判定不变…
74
- // A COMPLETE CSI key is user input, never a reply fragment.
75
- if (CSI_KEY_SEQUENCE.test(data)) return { kind: "other" };
76
- for (const head of REPLY_HEADS) { … }
77
- …
78
- }
79
- ```
80
-
81
- **正则比 issue 原文更宽一点**:issue 给的 `/^\x1b\[\d+(?:;\d+)?(?::\d+)?[u~]$/` 覆盖不了
82
- kitty 的"交替键"编码 `\x1b[6;1:2;65u`(`;65` 在主修饰符之后),会重新落回 reply-head。
83
- `(?:[;:]\d+)*` 覆盖全部 `CSI <num>;[<mod>[:<event>][;<alternate>]]u` 形态。
84
-
85
- 保持不变的行为:
86
-
87
- - `\x1b[6;`、`\x1b[6;24`(无终结符)→ 仍然 `reply-head`(arm)
88
- - `\x1b[6;24;80t` → 仍然 `reply-complete` drop(DROP_SHAPES 命中在前)
89
- - 其余 `REPLY_HEADS`(`\x1b[?`、`\x1b[>`、`\x1bP`、`\x1b]1`、`\x1b]4`)无键位冲突,不动
90
-
91
- 顺带修掉 `REPLY_HEADS` 上方与新语义矛盾的注释(说明 `\x1b[6;` 只能靠"完整键序列优先"来消解冲突)。
92
-
93
- ### 4.1 推翻既有设计文档的 pinned 不变式(R1-G3)
94
-
95
- `docs/superpowers/specs/2026-10-14-issue-170-attach-reply-guard-design.md:106` 钉死了:
96
-
97
- > 窗口只由 `reply-head` 武装,只有「**永不可能是按键/鼠标编码前缀**」的形状才武装:
98
- > `ESC[?`、`ESC[>`、**`ESC[6;`**、`ESC P`、`ESC]1`、`ESC]4`。
99
-
100
- 本次根因正是这条论断的反例:kitty 功能键编号形态 `ESC[6;1u`(PageDown)打破「永不可能是按键前缀」。
101
- 因此本修复**显式推翻**该不变式,替换为:
102
-
103
- > 完整 CSI 键序列(`CSI <num>[;<mod>[:<event>][;<alt>]]{u,~}`)优先于 head 前缀判定;
104
- > 只有**不带** `u`/`~` 终结符的片段才按 head 武装。
105
-
106
- 同步要求(实现阶段必做):
107
-
108
- - **改掉 `REPLY_HEADS` 上方那段与新语义矛盾的注释**(源码 :76-79,「can never be a key/mouse/paste
109
- encoding」「Deliberately absent: … `[5`/`[6`(PageUp/PageDown heads)」):不得再出现
110
- 绝对断言,改为说明「完整键序列优先,只有不带终结符的片段才武装」。
111
- 模块 header 用**英文**写同一条规则(`complete CSI key sequence … beats the head prefixes`),
112
- 中文仅在 #170 文档里作为跨引用出现。
113
- - 在 #170 设计文档 §3.3 的「设计要点」清单里补上「完整 CSI 键序列优先于 head 前缀」这一新类别,
114
- 并把 L106 的论断标注为已被 #213 修正(追加而非改写历史)
115
- - #170 文档 L112 与 A4 清单里「`ESC[6;` 必须武装」仍然成立(本修复只对**完整**键序列例外)
116
-
117
- ## 5. 边界与风险
118
-
119
- | 风险 | 说明 / 缓解 |
120
- | --- | --- |
121
- | 真 cell-size 回复被误判成键 | 不可能:cell-size 以 `t` 结尾,不带 `u`/`~` |
122
- | kitty 其他功能键(`\x1b[5;1u` 等) | 本就未被 head 命中,行为不变 |
123
- | modifyOtherKeys 其他键(`\x1b[3;5~`) | 不命中 `\x1b[6;`,行为不变 |
124
- | 极少数终端把 PageDown 编码成 `\x1b[6;`(截断) | 仍会 arm 并吞掉;这是 issue #170 的保护方向,保留 |
125
- | 安全方向 | 修复只是让**用户按键**更早地判定为非回复;不会让回复碎片更容易漏进子会话 |
126
-
127
- ## 6. 验收矩阵
128
-
129
- | ID | 功能点 | 验收方式 | 具体验证 | 通过标准 |
130
- | --- | --- | --- | --- | --- |
131
- | A1 | kitty/modifyOtherKeys PageDown 判为键 | 自动化(unit) | `test/terminal-reply-guard.test.mjs`:`\x1b[6;1u`、`\x1b[6;5u`、`\x1b[6;1:2u`、`\x1b[6;1:2;65u`、`\x1b[6;5~` | 全部 `{ kind: "other" }` |
132
- | A2 | PageUp 与其他键不受影响 | 自动化(unit) | 同文件:`\x1b[5;1u`、`\x1b[5;5~`、`\x1b[57422u`、`\x1b[57421u`、`\x1b[3;5~` | 全部 `{ kind: "other" }` |
133
- | A3 | 真 cell-size fragment 仍 arm | 自动化(unit) | 同文件:`\x1b[6;`、`\x1b[6;24` | 均 `{ kind: "reply-head", family: "cell-size" }` |
134
- | A4 | 完整 cell-size 回复仍 drop | 自动化(unit) | 同文件:`\x1b[6;24;80t` | `{ kind: "reply-complete", family: "cell-size", action: "drop" }` |
135
- | A5 | guard 状态机不回归 | 自动化(unit) | 现有 `terminal-reply-guard` 全部用例 | 全绿 |
136
- | A6 | attach 按键真的转发到子会话(不被 guard 吞) | 自动化(integration) | `test-support/attach-reply-guard-smoke.ts` 新增:`handleInput("")` / `handleInput("[6;5~")` 后 `sent` 含该字节;断言归 `test/pty-attach-reply-guard.test.mjs` | `sent` 收到完整键序列;且随后的普通文本仍正常转发 |
137
- | A7 | 语法/静态检查 | 自动化(static) | `npm run typecheck`(`tsconfig` 对 `.mjs` 是 `allowJs` + `checkJs:false`,只做语法解析)+ `npm run pack:dry` | 均通过 |
138
- | A8 | §4.1 的文档同步真的落地 | 自动化(static) | 三条独立 grep,全部锚定**英文** marker(源码是英文注释,中文只出现在 #170 文档):① `grep -c "complete CSI key" src/core/terminal-reply-guard.mjs`(≥1,且位于模块顶部注释区);② `grep -n "完整 CSI 键序列优先.*#213" docs/superpowers/specs/2026-10-14-issue-170-attach-reply-guard-design.md`(≥1);③ `grep -c "can never be a key" src/core/terminal-reply-guard.mjs`(**必须为 0** —— 旧的绝对断言已删除) | ①② ≥1、③ ==0 |
139
- | U1 | 真实终端 PageDown | 用户实测 | 见 §6.1 三步前置 + WezTerm + pi fullscreen → attach → 按 PageDown | 面板按页向后滚动(或转发给子会话),与 PageUp 对称 |
140
-
141
- ### 6.1 U1 执行时机与部署前置
142
-
143
- 仓库加载补丁 checkout 的方式见 `README.md:54`(`pi install "$(pwd)"`)或 `README.md:68`(symlink 到
144
- `~/.pi/agent/extensions/agent-board`)。U1 必须在以下三步之后执行,否则测的是旧代码:
145
-
146
- 1. 用上述任一方式把**当前 worktree/checkout** 加载到 pi(不是 `~/.pi/agent/git/...` 的缓存克隆)
147
- 2. 完整重启 pi(模块缓存不会热更新),并新开一个 attach 会话
148
- 3. 时机:本地 `npm run typecheck` + 目标套件全绿之后、合并之前
149
-
150
- > **验收 ID 作用域**:#213 的 `A1–A8` 仅指本 issue。新增测试名一律带 `#213-` 前缀
151
- > (如 `test("#213-A1: kitty PageDown is a key, not a reply head")`),
152
- > 因为 `test/terminal-reply-guard.test.mjs` 里已有 #170 遗留的 `A1:`/`A2:`/… 测试名,
153
- > 直接用 `--test-name-pattern="A1"` 会同时命中两套语义不同的用例。
154
-
155
- ## 7. 可测性拆分设计
156
-
157
- - **判定逻辑已经是纯函数**(`classifyReplyEvent`),零改动地单测;本次只新增一个正则常量。
158
- - 测试边界:`test/terminal-reply-guard.test.mjs` 覆盖 A1–A5(纯函数层);`test-support/attach-reply-guard-smoke.ts` + `test/pty-attach-reply-guard.test.mjs` 覆盖 A6(真实 `PtyAttachComponent.handleInput` → `sent` 转发,只替换 socket)。`test/pty-scroll.test.mjs` 与本次改动无关,不作为护栏。
159
- - 正则常量导出与否:**不导出**(内部实现细节),测试通过 `classifyReplyEvent` 的公开行为验证,避免把实现形状钉进测试。
160
-
161
- ## 8. 非目标
162
-
163
- - 不改变 guard 的吸收窗口(250ms / 64 字节)与 kill switch 语义
164
- - 不改动其他 `REPLY_HEADS` 条目
165
- - 不处理 `PageUp`/`PageDown` 在 legacy 路径的行为差异(本就正常)
@@ -1,131 +0,0 @@
1
- Drafted: deepseek/deepseek-flash (selected) · deepseek/deepseek-flash (physical) @ 2026-10-19T13:58:00+08:00
2
- Revised: deepseek/deepseek-flash (selected) · deepseek/deepseek-flash (physical) @ 2026-10-19T14:10:00+08:00 — 第 1 轮复核处置(收敛轮,R1-D1..D4 细化)
3
-
4
- # issue #214 根因报告与修复设计:attach 转发丢 Alt 修饰位,Alt+滚轮退化为 1 行
5
-
6
- 仓库:zhuxixi/pi-agent-board · 基线:`origin/main` @ `64bca89`
7
-
8
- ## 1. 症状
9
-
10
- pi fullscreen 1.3.0 + WezTerm,在 attach 一个 fullscreen 子会话时:
11
-
12
- | 手势 | 正常 fullscreen | attach 内 |
13
- | --- | --- | --- |
14
- | 滚轮 | 1 notch = 加速后的行数 | 相同(转发给子会话) |
15
- | **Alt + 滚轮** | **5×**(`ALT_WHEEL_SCROLL_MULTIPLIER = 5`) | **1×** —— Alt 被静默丢弃 |
16
-
17
- 其余滚轮行为(普通滚轮、PageUp/PageDown、本地 scrollback)都正常。
18
-
19
- ## 2. 根因
20
-
21
- fullscreen 子会话的滚轮走**转发**路径(alternate buffer 无 scrollback,见 #185/#198/#199/#205)。该路径**从零重建** SGR 报告,只保留方向:
22
-
23
- `src/ui/pty-attach.ts:1477`:
24
-
25
- ```ts
26
- private forwardWheelToChild(linesUp: number, event: TuiMouseEvent): void {
27
- this.send({
28
- type: "input",
29
- data: encodeWheelReport(linesUp > 0 ? "up" : "down", {
30
- col: clampInt(event.x + 1, 1, this.cols),
31
- row: clampInt(event.y, 1, Math.max(1, this.bodyHeight())),
32
- // ← event.alt 没有传
33
- }),
34
- });
35
- }
36
- ```
37
-
38
- `src/core/pty-scroll.mjs:370`:
39
-
40
- ```js
41
- export function encodeWheelReport(direction, { col, row } = {}) {
42
- const button = direction === "up" ? 64 : 65; // 永远不置 SGR bit 3 (值 8 = Alt)
43
- ...
44
- }
45
- ```
46
-
47
- 真实终端用 SGR **bit 3(值 8)** 表示 Alt:Alt+wheel-up = `72 = 64 + 8`,Alt+wheel-down = `73 = 65 + 8`。
48
- 子会话的 pi-tui 正是读这一位来决定 5× 加速
49
- (`@earendil-works/pi-tui/dist/tui-alt-screen.js` `handleViewportInput`):
50
-
51
- ```js
52
- const wheelDelta = wheelEvent.direction * ((wheelEvent.button & 8) !== 0 ? lines * ALT_WHEEL_SCROLL_MULTIPLIER : lines);
53
- ```
54
-
55
- attach 覆盖层**本来就拿到了** Alt:pi-tui 的 `TuiMouseEvent` 带 `alt: boolean`(`createMouseEvent` 里 `alt: (button & 8) !== 0`)。它只是没被传下去。
56
-
57
- ## 3. 证据(对仓库当前代码实测)
58
-
59
- ```
60
- encodeWheelReport("up", {col:5,row:3}) → "\x1b[<64;5;3M"
61
- encodeWheelReport("up", {col:5,row:3,alt:true}) → "\x1b[<64;5;3M" ❌ alt 被忽略(应为 72)
62
- encodeWheelReport("down", {col:5,row:3,alt:true}) → "\x1b[<65;5;3M" ❌(应为 73)
63
- ```
64
-
65
- 子会话侧对照:
66
-
67
- | 子会话收到的报告 | 结果 |
68
- | --- | --- |
69
- | `\x1b[<64;5;3M`(plain) | 1× |
70
- | `\x1b[<72;5;3M`(Alt+up,**当前永远收不到**) | 5× |
71
- | `\x1b[<65;5;3M`(plain) | 1× |
72
- | `\x1b[<73;5;3M`(Alt+down,**当前永远收不到**) | 5× |
73
-
74
- ## 4. 修复
75
-
76
- 把覆盖层的 `alt` 透传进编码器,置 SGR bit 3:
77
-
78
- `src/core/pty-scroll.mjs`:
79
-
80
- ```js
81
- export function encodeWheelReport(direction, { col, row, alt = false } = {}) {
82
- const base = direction === "up" ? 64 : 65;
83
- const button = alt === true ? base + 8 : base;
84
- const cell = (v) => (Number.isFinite(v) ? Math.max(1, Math.floor(v)) : 1);
85
- return `\x1b[<${button};${cell(col)};${cell(row)}M`;
86
- }
87
- ```
88
-
89
- `src/ui/pty-attach.ts` `forwardWheelToChild`:在参数对象里补 `alt: event.alt === true`。
90
-
91
- ## 5. 边界与风险
92
-
93
- | 风险 | 说明 / 缓解 |
94
- | --- | --- |
95
- | 影响本地滚动 | 不影响:本地路径用 `wheelDelta`(外层 pi 已加速的值),不经 `encodeWheelReport` |
96
- | 影响 legacy regular-mode 路径 | 不影响:该路径(`handleMouseInputChunk`,`src/ui/pty-attach.ts:1110`)把原始 SGR `raw` **原样**转发,从不重建报告,因此天然保留 alt 位,本次不动它 |
97
- | `event.alt` 缺失(老 pi-tui) | `alt = false` 默认值兜底;`=== true` 显式比较,`undefined` 不会误开位 |
98
- | Alt 被终端吞掉 | 不在本次范围:终端若不发 Alt 位,覆盖层也拿不到;本次只保证"拿到就转发" |
99
- | shift / ctrl 修饰位 | **非目标**:pi-tui 只消费 Alt 位做加速,转发其他位不会改变行为却扩大改动面;若将来需要,同一参数可扩展 |
100
- | 与 #170 reply guard 的交互 | 无关:本次只改出站编码,入站分类不动 |
101
- | 与 #213 的交互 | 无关:`\x1b[<` 开头是 SGR mouse,不命中 `CSI_KEY_SEQUENCE` 也不命中任何 reply head |
102
-
103
- ## 6. 验收矩阵
104
-
105
- | ID | 功能点 | 验收方式 | 具体验证 | 通过标准 |
106
- | --- | --- | --- | --- | --- |
107
- | A1 | Alt+wheel-up 编码为 72 | 自动化(unit) | `test/pty-scroll.test.mjs`:`encodeWheelReport("up", { col: 5, row: 3, alt: true })` | `"\x1b[<72;5;3M"` |
108
- | A2 | Alt+wheel-down 编码为 73 | 自动化(unit) | 同上 | `"\x1b[<73;5;3M"` |
109
- | A3 | 无 Alt 时行为不变,且严格比较(`=== true`) | 自动化(unit) | 同上:`alt: false` / 缺省 / `alt: undefined` / **`alt: 1`** / **`alt: "yes"`**(truthy 非布尔) | 全部为 64 / 65,无一置 bit 3 —— 锁死 `event.alt === true` 的严格比较契约 |
110
- | A4 | 转发链路真的把 `event.alt` 传下去 | 自动化(integration) | `test-support/pty-attach-wheel-fallback-scenario.ts` 增加 `alt: true` 的 wheel 事件,断言 runner 收到 `\x1b[<72;…M` | 收到的报告含 `72`(up)/ `73`(down) |
111
- | A5 | 本地滚动路径不受影响 | 自动化(integration) | 既有 `pty-attach-wheel` / `pty-attach-frame-overflow` / `pty-attach-alt-wheel` 套件 | 全绿 |
112
- | A6 | 静态检查 | 自动化(static) | `npm run typecheck` + `npm run pack:dry` | 通过 |
113
- | U1 | 真实终端 Alt+滚轮 | 用户实测 | 部署前置见 §6.1;attach 一个 fullscreen 子会话,按住 Alt 滚一格 | 子会话滚动 5 行(与 attach 外一致) |
114
-
115
- ### 6.1 U1 执行时机与部署前置
116
-
117
- 1. 用 `pi install "$(pwd)"`(`README.md:54`)或 symlink 到 `~/.pi/agent/extensions/agent-board` 加载**当前 checkout**,而不是 `~/.pi/agent/git/...` 缓存
118
- 2. 完整重启 pi,新开一个 attach 会话
119
- 3. 时机:目标套件全绿之后、合并之前
120
-
121
- ## 7. 可测性拆分设计
122
-
123
- - **编码逻辑是纯函数**(`encodeWheelReport`),签名扩展一个可选参数即可单测;无 I/O。
124
- - **接线在 `forwardWheelToChild`**(薄壳),只多传一个字段;集成测试落在既有的 `pty-attach-wheel-fallback-scenario.ts`(真实 `Socket.prototype.write` 拦截),与 #198/#205 的断言模式一致。
125
- - **测试边界**:A1–A3 在 `test/pty-scroll.test.mjs`(纯函数);A4 在 `test/pty-attach-wheel-fallback.test.mjs` + 对应 scenario;A5 复用既有套件;不新增测试文件。
126
-
127
- ## 8. 非目标
128
-
129
- - 不转发 shift / ctrl 修饰位(见 §5)
130
- - 不改本地滚动路径的任何行为
131
- - 不改 reply guard / 输入分类