@huseynsnmz/pi-board 0.15.3 → 0.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +24 -4
- package/CONTRIBUTING.md +7 -0
- package/README.md +55 -419
- package/SECURITY.md +19 -0
- package/docs/how-it-works.md +31 -0
- package/docs/reference.md +277 -0
- package/package.json +5 -11
- package/src/commands/agent-board.ts +26 -2
- package/src/commands/attach-flow.ts +4 -0
- package/src/core/double-press.mjs +6 -2
- package/src/core/slash-commands.mjs +52 -0
- package/src/index.ts +1 -1
- package/src/runtime/service.mjs +12 -0
- package/src/ui/dashboard.ts +59 -25
- package/src/ui/pty-attach.ts +8 -3
- package/IMPLEMENTATION_PLAN.md +0 -920
- package/PRD.md +0 -484
- package/PROGRESS.md +0 -142
- package/VERIFY.md +0 -115
- package/docs/BATCH_SELECTION_READ_FLOW.md +0 -277
- package/docs/EXPLORATION.md +0 -187
- package/docs/PTY_ATTACH_IMPLEMENTATION_PLAN.md +0 -581
- package/docs/RELEASE.md +0 -148
- package/docs/superpowers/plans/2026-08-15-screenlog-gc.md +0 -704
- package/docs/superpowers/plans/2026-08-16-attach-double-cursor-jiggle-retry.md +0 -499
- package/docs/superpowers/plans/2026-08-21-attach-coldstart-jiggle-rearm.md +0 -797
- package/docs/superpowers/plans/2026-08-21-dashboard-keypress-lag.md +0 -366
- package/docs/superpowers/plans/2026-08-21-issue-14-needs-input-no-auto-done.md +0 -576
- package/docs/superpowers/plans/2026-08-22-jiggle-shrink-and-hold.md +0 -337
- package/docs/superpowers/plans/2026-08-22-launch-cwd-favorites.md +0 -916
- package/docs/superpowers/plans/2026-08-22-question-tool-grouping.md +0 -211
- package/docs/superpowers/plans/2026-08-22-rename-state-labels.md +0 -182
- package/docs/superpowers/plans/2026-08-22-stable-list-order.md +0 -51
- package/docs/superpowers/plans/2026-08-27-locks-acquirelock-spin.md +0 -614
- package/docs/superpowers/plans/2026-08-27-pty-runner-test-flaky.md +0 -32
- package/docs/superpowers/plans/2026-08-29-code-refs-badges.md +0 -223
- package/docs/superpowers/plans/2026-08-30-circular-navigation.md +0 -308
- package/docs/superpowers/plans/2026-08-30-post-exit-timing-fix.md +0 -37
- package/docs/superpowers/plans/2026-08-30-pty-attach-quality-debt.md +0 -311
- package/docs/superpowers/plans/2026-08-30-readme-v2.md +0 -294
- package/docs/superpowers/plans/2026-09-01-attach-detach-gate-cursor-anchor.md +0 -284
- package/docs/superpowers/plans/2026-09-02-attach-detach-editor-state.md +0 -722
- package/docs/superpowers/plans/2026-09-03-code-refs-pr-backlink-narrow.md +0 -551
- package/docs/superpowers/plans/2026-09-03-detach-gate-glyph-fallback.md +0 -146
- package/docs/superpowers/plans/2026-09-04-evidence-outputpreview.md +0 -209
- package/docs/superpowers/plans/2026-09-04-warm-host-reclaim.md +0 -796
- package/docs/superpowers/plans/2026-09-05-issue-13-drainnextfollowup-pty-probe.md +0 -114
- package/docs/superpowers/plans/2026-09-05-issue-38-windows-wezterm-ime-cursor.md +0 -73
- package/docs/superpowers/plans/2026-09-05-issue-39-truncate-codepoint-boundary.md +0 -143
- package/docs/superpowers/plans/2026-09-05-issue-61-mention-fallback-guards.md +0 -226
- package/docs/superpowers/plans/2026-09-05-issue-63-flaky-manual-completion.md +0 -87
- package/docs/superpowers/plans/2026-09-05-issue-64-changelog-release-helper.md +0 -53
- package/docs/superpowers/plans/2026-09-05-pty-host-stacking-sock-race.md +0 -731
- package/docs/superpowers/plans/2026-09-08-attach-ctrl-left-detach.md +0 -30
- package/docs/superpowers/plans/2026-09-08-dashboard-shrink-repaint.md +0 -68
- package/docs/superpowers/plans/2026-09-08-issue-11-attach-runtime-desync-heal.md +0 -917
- package/docs/superpowers/plans/2026-09-08-legacy-stale-host-recovery.md +0 -125
- package/docs/superpowers/plans/2026-09-08-spawn-async-error-swallow.md +0 -56
- package/docs/superpowers/plans/2026-09-08-stale-model-attach-guard.md +0 -96
- package/docs/superpowers/plans/2026-09-09-claimpid-blocks-replace.md +0 -267
- package/docs/superpowers/plans/2026-09-09-harden-runner-architecture.md +0 -603
- package/docs/superpowers/plans/2026-09-09-single-writer-completion.md +0 -252
- package/docs/superpowers/plans/2026-09-10-reader-consistency.md +0 -115
- package/docs/superpowers/plans/2026-09-14-attach-cursor-dectcem-gate.md +0 -469
- package/docs/superpowers/plans/2026-09-14-attach-snapshot.md +0 -92
- package/docs/superpowers/plans/2026-09-14-host-meta-orphan-lock.md +0 -771
- package/docs/superpowers/plans/2026-09-14-issue-106-terminal-frame-cognition.md +0 -299
- package/docs/superpowers/plans/2026-09-14-issue-113-foreground-preview-race.md +0 -609
- package/docs/superpowers/plans/2026-09-14-terminal-model.md +0 -145
- package/docs/superpowers/plans/2026-09-15-coordinator-pipe-root-normalize.md +0 -224
- package/docs/superpowers/plans/2026-09-15-lease-publish-eprem-reclaim.md +0 -341
- package/docs/superpowers/plans/2026-09-18-detach-anchor-reporter-endpoint.md +0 -875
- package/docs/superpowers/plans/2026-09-20-control-lifecycle.md +0 -116
- package/docs/superpowers/plans/2026-09-20-issue-121-perf-gate-out-of-coverage.md +0 -517
- package/docs/superpowers/plans/2026-09-23-a5-mid-stream-assertion.md +0 -25
- package/docs/superpowers/plans/2026-09-23-cwd-browse-tab-drill.md +0 -393
- package/docs/superpowers/plans/2026-09-23-terminal-query-forwarding.md +0 -51
- package/docs/superpowers/plans/2026-09-23-unconditional-detach-key.md +0 -58
- package/docs/superpowers/plans/2026-09-24-control-reconcile-flake.md +0 -698
- package/docs/superpowers/plans/2026-09-24-probe-attached-clients.md +0 -605
- package/docs/superpowers/plans/2026-09-29-issue-145-holding-semantic-state.md +0 -1183
- package/docs/superpowers/plans/2026-09-29-issue-145-user-verification.md +0 -66
- package/docs/superpowers/plans/2026-09-30-board-delete-key-x-y-confirm.md +0 -649
- package/docs/superpowers/plans/2026-09-30-issue-153-runner-stop-boot-window.md +0 -618
- package/docs/superpowers/plans/2026-09-30-issue-95-test-discipline.md +0 -555
- package/docs/superpowers/plans/2026-09-30-publish-dispatch-tag-check.md +0 -259
- package/docs/superpowers/plans/2026-10-06-attach-settle-scheme-delivery.md +0 -751
- package/docs/superpowers/plans/2026-10-09-issue-167-attach-middle-paste.md +0 -509
- package/docs/superpowers/plans/2026-10-09-issue-169-fullscreen-mouse-ownership.md +0 -346
- package/docs/superpowers/plans/2026-10-10-attach-delete-in-place.md +0 -832
- package/docs/superpowers/plans/2026-10-10-issue-174-selection-mirror-paste-source.md +0 -510
- package/docs/superpowers/plans/2026-10-14-issue-170-attach-reply-guard.md +0 -771
- package/docs/superpowers/plans/2026-10-14-issue-178-accept-registration-race.md +0 -1257
- package/docs/superpowers/plans/2026-10-14-issue-184-snapshot-window-contract.md +0 -657
- package/docs/superpowers/plans/2026-10-14-issue-185-attach-wheel-alt-screen.md +0 -47
- package/docs/superpowers/plans/2026-10-14-issue-187-motionless-drag.md +0 -931
- package/docs/superpowers/plans/2026-10-14-issue-189-mixed-input-segments.md +0 -823
- package/docs/superpowers/plans/2026-10-15-issue-188-x10-mouse-release.md +0 -526
- package/docs/superpowers/plans/2026-10-16-issue-198-attach-wheel-forward-fallback.md +0 -32
- package/docs/superpowers/plans/2026-10-18-attach-wheel-replay-pseudo-scrollback.md +0 -437
- package/docs/superpowers/plans/2026-10-19-issue-213-reply-guard-csi-key.md +0 -257
- package/docs/superpowers/plans/2026-10-19-issue-214-attach-forward-alt-modifier.md +0 -202
- package/docs/superpowers/plans/2026-10-21-issue-221-ask-user-question.md +0 -198
- package/docs/superpowers/plans/2026-10-23-issue-222-attach-ready.md +0 -290
- package/docs/superpowers/plans/2026-10-23-issue-225-monotonic-foreground-state.md +0 -450
- package/docs/superpowers/plans/2026-10-23-issue-229-vim-jkl.md +0 -449
- package/docs/superpowers/plans/2026-10-23-issue-232-attach-bg-only-cells.md +0 -153
- package/docs/superpowers/plans/2026-10-23-lease-heartbeat-staleness.md +0 -537
- package/docs/superpowers/plans/2026-10-23-selected-row-highlight.md +0 -405
- package/docs/superpowers/plans/2026-10-24-issue-217-windows-pid-reuse-stopping.md +0 -367
- package/docs/superpowers/plans/2026-10-24-issue-230-warm-host-background-runs.md +0 -806
- package/docs/superpowers/plans/2026-10-24-issue-238-windows-process-identity-token.md +0 -490
- package/docs/superpowers/plans/2026-10-24-issue-241-coordinator-root-exit.md +0 -461
- package/docs/superpowers/plans/2026-10-25-issue-239-coordinator-replacement-identity.md +0 -504
- package/docs/superpowers/plans/2026-10-26-issue-245-key-hints-doc-sync.md +0 -244
- package/docs/superpowers/plans/2026-10-27-issue-172-fullscreen-selection-clear.md +0 -344
- package/docs/superpowers/plans/2026-10-27-issue-252-select-key-hints.md +0 -209
- package/docs/superpowers/plans/2026-10-28-issue-255-coordinator-endpoint-zombie.md +0 -442
- package/docs/superpowers/plans/2026-10-28-issue-257-legacy-pty-runner-endpoint-cleanup.md +0 -256
- package/docs/superpowers/plans/2026-11-03-cwd-favorites-full-pool.md +0 -266
- package/docs/superpowers/specs/2026-08-15-screenlog-gc-design.md +0 -105
- package/docs/superpowers/specs/2026-08-16-attach-double-cursor-jiggle-retry-design.md +0 -142
- package/docs/superpowers/specs/2026-08-21-attach-coldstart-jiggle-rearm-design.md +0 -110
- package/docs/superpowers/specs/2026-08-21-dashboard-keypress-lag-design.md +0 -59
- package/docs/superpowers/specs/2026-08-21-issue-14-needs-input-no-auto-done-design.md +0 -109
- package/docs/superpowers/specs/2026-08-22-cwd-favorites-design.md +0 -131
- package/docs/superpowers/specs/2026-08-22-jiggle-shrink-and-hold-design.md +0 -111
- package/docs/superpowers/specs/2026-08-22-question-tool-grouping-design.md +0 -114
- package/docs/superpowers/specs/2026-08-22-rename-state-labels-design.md +0 -80
- package/docs/superpowers/specs/2026-08-27-locks-acquirelock-spin-design.md +0 -97
- package/docs/superpowers/specs/2026-08-27-pty-runner-test-flaky-design.md +0 -35
- package/docs/superpowers/specs/2026-08-29-code-refs-badges-design.md +0 -128
- package/docs/superpowers/specs/2026-08-30-circular-navigation-design.md +0 -47
- package/docs/superpowers/specs/2026-08-30-eprm-atomicwrite-race-design.md +0 -77
- package/docs/superpowers/specs/2026-08-30-post-exit-timing-fix-design.md +0 -80
- package/docs/superpowers/specs/2026-08-30-pty-attach-quality-debt-design.md +0 -105
- package/docs/superpowers/specs/2026-08-30-readme-v2-design.md +0 -115
- package/docs/superpowers/specs/2026-09-01-attach-detach-gate-cursor-anchor-design.md +0 -78
- package/docs/superpowers/specs/2026-09-02-attach-detach-editor-state-design.md +0 -120
- package/docs/superpowers/specs/2026-09-03-code-refs-pr-backlink-narrow-design.md +0 -92
- package/docs/superpowers/specs/2026-09-03-detach-gate-glyph-fallback-design.md +0 -99
- package/docs/superpowers/specs/2026-09-04-evidence-outputpreview-design.md +0 -50
- package/docs/superpowers/specs/2026-09-04-warm-host-reclaim-design.md +0 -106
- package/docs/superpowers/specs/2026-09-05-issue-13-drainnextfollowup-pty-probe-design.md +0 -64
- package/docs/superpowers/specs/2026-09-05-issue-38-windows-wezterm-ime-design.md +0 -48
- package/docs/superpowers/specs/2026-09-05-issue-39-truncate-codepoint-boundary-design.md +0 -64
- package/docs/superpowers/specs/2026-09-05-issue-61-mention-fallback-design.md +0 -71
- package/docs/superpowers/specs/2026-09-05-issue-63-flaky-manual-completion-design.md +0 -49
- package/docs/superpowers/specs/2026-09-05-issue-64-changelog-helper-design.md +0 -76
- package/docs/superpowers/specs/2026-09-05-pty-host-stacking-sock-race-design.md +0 -510
- package/docs/superpowers/specs/2026-09-07-issue-11-attach-runtime-desync-heal-design.md +0 -130
- package/docs/superpowers/specs/2026-09-08-attach-ctrl-left-detach-design.md +0 -58
- package/docs/superpowers/specs/2026-09-08-dashboard-shrink-repaint-design.md +0 -52
- package/docs/superpowers/specs/2026-09-08-legacy-stale-host-recovery-design.md +0 -87
- package/docs/superpowers/specs/2026-09-08-spawn-async-error-swallow-design.md +0 -56
- package/docs/superpowers/specs/2026-09-08-stale-model-attach-guard-design.md +0 -79
- package/docs/superpowers/specs/2026-09-09-claimpid-blocks-replace-design.md +0 -147
- package/docs/superpowers/specs/2026-09-09-harden-runner-architecture-design.md +0 -304
- package/docs/superpowers/specs/2026-09-14-attach-cursor-dectcem-gate-design.md +0 -114
- package/docs/superpowers/specs/2026-09-14-host-meta-orphan-lock-design.md +0 -120
- package/docs/superpowers/specs/2026-09-14-issue-106-terminal-frame-cognition-design.md +0 -146
- package/docs/superpowers/specs/2026-09-14-issue-113-foreground-preview-race-design.md +0 -116
- package/docs/superpowers/specs/2026-09-15-coordinator-pipe-root-normalize-design.md +0 -84
- package/docs/superpowers/specs/2026-09-15-lease-publish-eprem-reclaim-design.md +0 -92
- package/docs/superpowers/specs/2026-09-18-detach-anchor-reporter-endpoint-design.md +0 -123
- package/docs/superpowers/specs/2026-09-20-issue-121-perf-gate-out-of-coverage-design.md +0 -204
- package/docs/superpowers/specs/2026-09-23-a5-mid-stream-assertion-design.md +0 -43
- package/docs/superpowers/specs/2026-09-23-cwd-browse-tab-drill-design.md +0 -95
- package/docs/superpowers/specs/2026-09-23-terminal-query-forwarding-design.md +0 -63
- package/docs/superpowers/specs/2026-09-23-unconditional-detach-key-design.md +0 -57
- package/docs/superpowers/specs/2026-09-24-control-reconcile-flake-design.md +0 -98
- package/docs/superpowers/specs/2026-09-24-probe-attached-clients-design.md +0 -190
- package/docs/superpowers/specs/2026-09-29-issue-145-holding-semantic-state-design.md +0 -394
- package/docs/superpowers/specs/2026-09-30-board-delete-key-x-y-confirm-design.md +0 -86
- package/docs/superpowers/specs/2026-09-30-issue-153-runner-stop-boot-window-design.md +0 -75
- package/docs/superpowers/specs/2026-09-30-issue-95-test-discipline-design.md +0 -226
- package/docs/superpowers/specs/2026-09-30-publish-dispatch-tag-check-design.md +0 -129
- package/docs/superpowers/specs/2026-10-06-attach-settle-scheme-delivery-design.md +0 -196
- package/docs/superpowers/specs/2026-10-09-issue-167-attach-middle-paste-design.md +0 -125
- package/docs/superpowers/specs/2026-10-09-issue-169-fullscreen-mouse-ownership-design.md +0 -130
- package/docs/superpowers/specs/2026-10-10-attach-delete-in-place-design.md +0 -224
- package/docs/superpowers/specs/2026-10-10-issue-174-selection-mirror-paste-source-design.md +0 -154
- package/docs/superpowers/specs/2026-10-14-issue-170-attach-reply-guard-design.md +0 -195
- package/docs/superpowers/specs/2026-10-14-issue-178-accept-registration-race-design.md +0 -287
- package/docs/superpowers/specs/2026-10-14-issue-184-snapshot-window-contract-design.md +0 -288
- package/docs/superpowers/specs/2026-10-14-issue-185-attach-wheel-alt-screen-design.md +0 -126
- package/docs/superpowers/specs/2026-10-14-issue-187-motionless-drag-design.md +0 -269
- package/docs/superpowers/specs/2026-10-14-issue-189-mixed-input-segments-design.md +0 -369
- package/docs/superpowers/specs/2026-10-15-issue-188-x10-mouse-release-design.md +0 -252
- package/docs/superpowers/specs/2026-10-16-issue-198-attach-wheel-forward-fallback-design.md +0 -67
- package/docs/superpowers/specs/2026-10-18-attach-wheel-replay-pseudo-scrollback-design.md +0 -250
- package/docs/superpowers/specs/2026-10-19-issue-213-reply-guard-csi-key-design.md +0 -165
- package/docs/superpowers/specs/2026-10-19-issue-214-attach-forward-alt-modifier-design.md +0 -131
- package/docs/superpowers/specs/2026-10-20-issue-221-ask-user-question-design.md +0 -270
- package/docs/superpowers/specs/2026-10-22-issue-225-monotonic-foreground-state-design.md +0 -225
- package/docs/superpowers/specs/2026-10-23-issue-222-attach-ready-design.md +0 -204
- package/docs/superpowers/specs/2026-10-23-issue-229-vim-jkl-design.md +0 -90
- package/docs/superpowers/specs/2026-10-23-issue-232-attach-bg-only-cells-design.md +0 -146
- package/docs/superpowers/specs/2026-10-23-lease-heartbeat-staleness-design.md +0 -271
- package/docs/superpowers/specs/2026-10-23-selected-row-highlight-design.md +0 -191
- package/docs/superpowers/specs/2026-10-24-issue-217-windows-pid-reuse-stopping-design.md +0 -334
- package/docs/superpowers/specs/2026-10-24-issue-230-warm-host-background-runs-design.md +0 -283
- package/docs/superpowers/specs/2026-10-24-issue-238-windows-process-identity-token-design.md +0 -286
- package/docs/superpowers/specs/2026-10-24-issue-241-coordinator-root-exit-design.md +0 -381
- package/docs/superpowers/specs/2026-10-25-issue-239-coordinator-replacement-identity-design.md +0 -331
- package/docs/superpowers/specs/2026-10-26-issue-245-key-hints-doc-sync-design.md +0 -72
- package/docs/superpowers/specs/2026-10-27-issue-172-fullscreen-selection-clear-design.md +0 -248
- package/docs/superpowers/specs/2026-10-27-issue-252-select-key-hints-design.md +0 -95
- package/docs/superpowers/specs/2026-10-28-issue-255-coordinator-endpoint-zombie-design.md +0 -380
- package/docs/superpowers/specs/2026-10-28-issue-257-legacy-pty-runner-endpoint-cleanup-design.md +0 -197
- package/docs/superpowers/specs/2026-11-03-cwd-favorites-full-pool-design.md +0 -106
- package/scripts/budget-audit.mjs +0 -135
- package/scripts/release.mjs +0 -446
- package/scripts/release_helper.mjs +0 -309
- package/scripts/run-perf-gate.mjs +0 -40
- package/scripts/run-tests.mjs +0 -375
|
@@ -1,126 +0,0 @@
|
|
|
1
|
-
# Issue #185 — attach wheel cannot scroll a fullscreen child session (alternate buffer)
|
|
2
|
-
|
|
3
|
-
- Issue: https://github.com/zhuxixi/pi-agent-board/issues/185
|
|
4
|
-
- Kind: bug — root-cause report + fix design (delta on top of the merged #183)
|
|
5
|
-
- Repo: `zhuxixi/pi-agent-board` · baseline: `main` @ `f81949d5` (#183) · worktree `issue-185-attach-wheel-alt-screen`
|
|
6
|
-
- Drafted: deepseek/deepseek-flash (selected) · deepseek/deepseek-flash (physical) @ 2026-10-14T00:06:01Z
|
|
7
|
-
- Revised: deepseek/deepseek-flash (selected) · deepseek/deepseek-flash (physical) @ 2026-10-14T00:23:30Z — after round-1 review (mode: degraded same-model, user-confirmed; findings R1-D1..R1-D5 in `research/spec-review-round-1.md`)
|
|
8
|
-
- Revised: deepseek/deepseek-flash (selected) · deepseek/deepseek-flash (physical) @ 2026-10-14T00:52:00Z — after local code-review (workflow `code-review`, run `code-review-mv7dz229-mrt1ay`): the disconnected alternate-buffer wheel now falls through instead of consuming a drop-bound forward (consume-only-on-effect invariant restored); §3.1/§8 updated
|
|
9
|
-
|
|
10
|
-
## 1. Problem
|
|
11
|
-
|
|
12
|
-
`#183` fixed the fullscreen wheel by consuming it in `handleMouse` and scrolling the attach's own xterm
|
|
13
|
-
viewport, falling through when that viewport cannot move. For a child that holds the alternate screen —
|
|
14
|
-
i.e. **every session this board launches by default** — the attach-local scrollback collapses to about
|
|
15
|
-
two lines, so the wheel travels those two lines and then falls through to a transcript scroll hidden
|
|
16
|
-
behind the overlay. To the user the wheel still looks dead while `PageUp`/`PageDown` keep working.
|
|
17
|
-
|
|
18
|
-
Out of scope here: the normal-buffer case, which #183 already fixed and pins with tests.
|
|
19
|
-
|
|
20
|
-
## 2. Root cause
|
|
21
|
-
|
|
22
|
-
**2.1 Alternate buffers have no scrollback.** Measured with the extension's own `@xterm/headless` and the live 36×120 geometry (`scrollback: 2000`, `bodyHeight = rows - 2 = 34`):
|
|
23
|
-
|
|
24
|
-
| buffer | `baseY` | `length` | `min(length, baseY + rows) - bodyHeight` |
|
|
25
|
-
| --- | --- | --- | --- |
|
|
26
|
-
| normal | 65 | 101 | 67 |
|
|
27
|
-
| alternate (`\x1b[?1049h` + 200 lines) | 0 | 36 | **2** |
|
|
28
|
-
|
|
29
|
-
`bottomViewportTop()` (`src/ui/pty-attach.ts:1340-1344`) and the projection (`:854`, `:1121`, `:1142`, `:1237`) all read `this.term.buffer.active`, so an alternate-screen child is exactly the state where the attach's local scrolling is inert.
|
|
30
|
-
|
|
31
|
-
**2.2 The hosted child is in that state.** Board host-configs carry no `--tui-mode` and `env: {}`, so the child inherits `tuiMode: "fullscreen"`. A probe pi spawned under the same env/settings in a 36×120 PTY emitted `\x1b[?1049h`, `\x1b[?1000h`, `\x1b[?1006h`, `\x1b[?2004h` at startup.
|
|
32
|
-
|
|
33
|
-
**2.3 #183's fall-through ends in an invisible scroll.** `src/ui/pty-attach.ts:386-395`: `wheelScrollLinesUp(event)` → `if (this.mouseScrollEnabled() && this.tryScrollBy(wheelLinesUp)) return {handled:true, render:false}` → `return undefined`. Returning `undefined` hands the event to pi's `routeWheel`, which scrolls the transcript underneath the full-screen overlay — no visible effect. The behaviour is pinned as intentional (`test/pty-attach-wheel.test.mjs:29`, `test-support/pty-attach-wheel-scenario.ts:16,92`), so this issue must not regress it for the normal-buffer case.
|
|
34
|
-
|
|
35
|
-
**2.4 The keyboard path already forwards, the wheel does not.** `handleInput`'s `PageUp`/`PageDown` branches (`src/ui/pty-attach.ts:300`, `:307`) call `tryScrollBy` and, when it returns false, `this.send({ type: "input", data })` — the child scrolls its own viewport and its redraw flows back through the attach. The raw-SGR wheel path does the same (`:967`). The fullscreen wheel branch is the only scroll path without that fallback.
|
|
36
|
-
|
|
37
|
-
## 3. Fix design
|
|
38
|
-
|
|
39
|
-
### 3.1 Behaviour contract — `PtyAttachComponent.handleMouse`, wheel branch (`src/ui/pty-attach.ts:386-395`)
|
|
40
|
-
|
|
41
|
-
| Condition | Behaviour |
|
|
42
|
-
| --- | --- |
|
|
43
|
-
| `wheelLinesUp === 0` (not a wheel, or unusable delta) | unchanged: `undefined` (falls through) |
|
|
44
|
-
| `mouseScrollEnabled() === false` (`AGENT_BOARD_ATTACH_MOUSE=0` / `AGENT_BOARD_ENABLE_MOUSE_SCROLL=0`) | unchanged: `undefined`, no forwarding |
|
|
45
|
-
| active buffer is `alternate` | **new**: skip the local viewport entirely; forward one synthesized SGR wheel report to the child; `return { handled: true, render: false }` |
|
|
46
|
-
| active buffer is `normal`, `tryScrollBy(linesUp)` moved | unchanged #183: `{ handled: true, render: false }` |
|
|
47
|
-
| active buffer is `normal`, viewport cannot move | unchanged #183: `undefined` |
|
|
48
|
-
|
|
49
|
-
The alternate case deliberately skips `tryScrollBy`: letting the local viewport travel its two lines would pin the attach's projection while the child repaints its own scrolled frame, producing a two-line-jump artefact rather than a scroll.
|
|
50
|
-
|
|
51
|
-
Two edge notes on the alternate branch:
|
|
52
|
-
|
|
53
|
-
- While the attach is disconnected, the branch falls through (`undefined`) instead of forwarding — `send()` would silently drop the report (`src/ui/pty-attach.ts`), and the pre-#185 branch likewise consumed the wheel only when it had an effect. The event goes back to pi, whose own handling is equally inert behind the overlay; acceptable, since the attach shows a reconnect surface.
|
|
54
|
-
- `README.md:201`'s sentence ("While attached, `PageUp`, `PageDown`, `Home`, `End`, and the mouse wheel scroll local scrollback") must be updated in the same PR: in the alternate case the wheel scrolls the child's own viewport, same as `PageUp`/`PageDown`.
|
|
55
|
-
|
|
56
|
-
### 3.2 Wire format and coordinates
|
|
57
|
-
|
|
58
|
-
- Report shape: `\x1b[<64;{col};{row}M` (up) / `\x1b[<65;{col};{row}M` (down) — the plain SGR wheel report a terminal sends; the child's own TuiAltScreen (or any mouse-aware alt-screen program) parses it exactly as it parses the real terminal's.
|
|
59
|
-
- One report per received event, not one per line, so the child applies its own acceleration (`WheelScrollAccelerator`); `#183`'s `wheelScrollLinesUp()` decides direction and validity, and `AGENT_BOARD_WHEEL_LINES` stays a regular-mode-only knob as #183 documented.
|
|
60
|
-
- Coordinates are the **child screen** frame, not the attach buffer's absolute row: `handleMouse` events are zero-based local (pi-tui `TuiMouseEvent`), the attach body starts at local row 1 (header row 0, footer last row), and the child's screen is the body 1:1. So `col = clampInt(event.x + 1, 1, this.cols)` and `row = clampInt(event.y, 1, Math.max(1, this.bodyHeight()))` (SGR is one-based). This mirrors what the raw path assumes when it maps a 1-based SGR column to `col - 1` (`:1090`).
|
|
61
|
-
|
|
62
|
-
### 3.3 Testability split (implementation constraint)
|
|
63
|
-
|
|
64
|
-
| Unit | Kind | Contract |
|
|
65
|
-
| --- | --- | --- |
|
|
66
|
-
| `resolveAttachWheelRoute({ bufferType, wheelLinesUp, mouseEnabled })` (**new**, `src/core/pty-scroll.mjs`) | pure | `"none"` when the switch is off or `wheelLinesUp === 0`; `"child"` when `bufferType === "alternate"`; else `"local"`. The `"local"`→`"none"` downgrade when `tryScrollBy` cannot move stays in the shell (it needs the live viewport). |
|
|
67
|
-
| `encodeWheelReport(direction, { col, row })` (**new**, same module) | pure | `"up" \| "down"` → the SGR string above, coordinates clamped to ≥ 1. |
|
|
68
|
-
| `handleMouse` wheel branch | shell | `resolveAttachWheelRoute` → `"child"`: `send({type:"input", data: encodeWheelReport(...)})`; `"local"`: `tryScrollBy(...) ? handled : undefined`; `"none"`: `undefined`. |
|
|
69
|
-
|
|
70
|
-
Both helpers are pure and belong next to `wheelScrollLinesUp()` (`src/core/pty-scroll.mjs:175`), which stays as the delta gate.
|
|
71
|
-
|
|
72
|
-
Seams for the component-level tests: `test-support/pty-attach-mouse-scenario.ts` already provides a real runner plus a `Socket.prototype.write` interception that observes component→runner `{type:"input"}` messages, plus `as any` private access — the same harness that asserts the middle-click paste payload reaches the runner. The alternate-screen branch needs one new additive fixture mode in `test-support/fake-pty-pi.mjs` (`FAKE_PTY_STREAM_MODE=alt`, writing `\x1b[?1049h` before its lines — same additive shape as the existing `steady`/`firehose`/`malformed` modes) — a test-support change, no runtime impact. The new scenario's fake TUI declares `mode: "fullscreen"` (same as `test-support/pty-attach-wheel-scenario.ts`), so the component is exercised in production's dispatch shape.
|
|
73
|
-
|
|
74
|
-
## 4. Rejected alternatives
|
|
75
|
-
|
|
76
|
-
| Alternative | Evidence against |
|
|
77
|
-
| --- | --- |
|
|
78
|
-
| Keep the local scroll first and forward only after the two lines | The attach projection would pin at `viewportTop = 0` while the child scrolls its own frame — a two-line artefact plus divergent frames; also invisible in practice. |
|
|
79
|
-
| Forward at the normal-buffer boundary too (full parity with the raw path) | Changes a contract #183 pinned (`test/pty-attach-wheel.test.mjs:29`) for a case where local scrolling does work; keep it as a separate decision, not part of this fix. |
|
|
80
|
-
| Mirror the child's transcript into attach-local history for alternate-screen children | Large redesign of the projection model; unnecessary once forwarding matches the keyboard path. |
|
|
81
|
-
| Ask the user to run sessions with `--tui-mode regular` | Workaround; leaves the default topology broken. |
|
|
82
|
-
| Re-assert the outer mouse mode / bypass `handleMouse` | #169/#173 already settled that `TuiAltScreen` owns the mode in fullscreen. |
|
|
83
|
-
|
|
84
|
-
## 5. Acceptance matrix
|
|
85
|
-
|
|
86
|
-
| ID | 功能点 | 验收方式 | 具体验证 | 通过标准 |
|
|
87
|
-
| --- | --- | --- | --- | --- |
|
|
88
|
-
| A1 | 路由决策与报文编码(纯函数) | 自动化验证(unit) | `node --test test/pty-scroll.test.mjs` | `resolveAttachWheelRoute`:`mouseEnabled:false` → `"none"`;`wheelLinesUp:0` → `"none"`;`alternate` → `"child"`;`normal` → `"local"`。`encodeWheelReport`:up → `"\x1b[<64;5;3M"`、down → `"\x1b[<65;5;3M"`,`col/row < 1` 夹到 1 |
|
|
89
|
-
| A2 | alternate 子进程:转发报文而非本地滚动 | 自动化验证(integration) | 新场景 `test-support/pty-attach-alt-wheel-scenario.ts` + `test/pty-attach-alt-wheel.test.mjs`(真实 runner + `Socket.prototype.write` 拦截 + 新 `FAKE_PTY_STREAM_MODE=alt`) | 子进程收到 `{"type":"input","data":"\x1b[<64;…M"}`(下滑为 65,坐标为 1-based 且落在 `[1, cols] × [1, bodyHeight]`);`handleMouse` 返回 `{handled:true, render:false}`;`viewportTop` 保持 `null` |
|
|
90
|
-
| A3 | normal 子进程:不回归 #183 | 自动化验证(integration) | `npm test`(`test/pty-attach-wheel.test.mjs` 原样) | 既有多条断言全绿,含 `atBottomFallsThrough === true` |
|
|
91
|
-
| A4 | 关闭开关与不可用 delta 不转发 | 自动化验证(integration) | A2 场景内设 `AGENT_BOARD_ATTACH_MOUSE=0` / `AGENT_BOARD_ENABLE_MOUSE_SCROLL=0` / 无 delta 的 wheel | 三者都返回 `undefined` 且 socket 上无新的 `{type:"input"}` |
|
|
92
|
-
| A5 | 既有鼠标行为不回归 | 自动化验证(integration) | `npm test`(`pty-attach-mouse`、`pty-attach-mouse-mode`、`pty-attach-selection-mirror`) | 全绿:中键粘贴、fullscreen 零 mouse-mode 写入、selection mirror |
|
|
93
|
-
| A6 | 类型、构建与全量门禁 | 自动化验证(static + suite) | `npm run verify` | typecheck / perf gate / `npm test` / coverage 阈值(lines 85 / funcs 80 / branches 70)/ `pack:dry` 全过 |
|
|
94
|
-
| A7 | 文档同步(README 的滚轮声明) | 自动化验证(static) | `rg -n "mouse wheel" README.md` | `README.md:201` 的句子不再只承诺 "scroll local scrollback"——必须说明滚轮与 PageUp/PageDown 一致:本地有 scrollback 滚本地,否则转发给子进程滚它自己的视口 |
|
|
95
|
-
| U1 | 真实 fullscreen attach 里滚轮与键盘一致 | 用户实测 | 合并并入 git checkout 后重启 pi:attach 一个长输出会话,先 `PageUp`/`PageDown` 再滚轮上下 | 滚轮能连续回看历史并回到跟随底部,与 `PageUp`/`PageDown` 一致;无两行跳变或闪烁;时机:PR 合并 → `git pull` → 重启 pi 后任意时刻 |
|
|
96
|
-
| U2 | 中键粘贴与拖拽选择无回归 | 用户实测 | 同一会话中键粘贴 + 左键拖选 | 与改动前一致(X11 PRIMARY 粘贴内容正确、选中高亮) |
|
|
97
|
-
| U3 | regular 模式冒烟(本次改动未触碰的路径) | 用户实测 | 全局 `tuiMode: regular` 重启外层 pi 后 attach 一个长会话并滚轮 | 滚轮走原始路径(本地 scrollback + 边界转发)不变;时机同 U1 |
|
|
98
|
-
|
|
99
|
-
## 6. Verification path (deployment truth)
|
|
100
|
-
|
|
101
|
-
- The outer pi loads the extension from `~/.pi/agent/settings.json` → `git:github.com/zhuxixi/pi-agent-board` → `~/.pi/agent/git/github.com/zhuxixi/pi-agent-board`. U-items need that checkout at the merge commit **and** a pi restart (the module is loaded at startup); no build step (`main: ./index.ts`).
|
|
102
|
-
- Implementation baseline: the dev checkout `/home/elling/git-repo/github/pi-agent-board` is at `acc40fd`, i.e. **behind #183**; the worktree must be created from a refreshed `main` (fetch first) so the change builds on `f81949d5` rather than reintroducing the previous wheel branch.
|
|
103
|
-
- A2's `FAKE_PTY_STREAM_MODE=alt` is new fixture surface (`test-support/fake-pty-pi.mjs`, alongside `steady`/`firehose`/`malformed`); keep it additive so existing fixture consumers are unaffected.
|
|
104
|
-
|
|
105
|
-
## 7. Non-goals
|
|
106
|
-
|
|
107
|
-
- Changing the normal-buffer boundary behaviour #183 pinned (fall-through) or `AGENT_BOARD_WHEEL_LINES` semantics.
|
|
108
|
-
- Mirroring child history into attach-local scrollback; horizontal wheel; touch/kinetic input.
|
|
109
|
-
- Any pi-tui change (the child parses standard SGR reports; nothing new is required upstream).
|
|
110
|
-
|
|
111
|
-
## 8. Guarantee boundaries
|
|
112
|
-
|
|
113
|
-
- The forwarded report is synthesized, not a byte replay: coordinates are best-effort (the child uses them for scroll-view hit testing and falls back to its primary scroll view) and modifier bits are not carried; the child applies its own acceleration.
|
|
114
|
-
- In the alternate case there is deliberately no local fallback: if the child ignores mouse reports, the wheel does nothing in that session (the same class of limitation `PageUp` has today if the child ignores paging keys).
|
|
115
|
-
- The attach cannot observe whether the child actually scrolled; parity with `PageUp` is the contract, not a guarantee about the child's own limits.
|
|
116
|
-
- In the disconnected state the alternate branch falls through rather than consuming the wheel (see §3.1): a forward that `send()` would silently drop is not an effect, so the event is handed back — restoring the pre-#185 consume-only-on-effect invariant.
|
|
117
|
-
|
|
118
|
-
## 9. Risks
|
|
119
|
-
|
|
120
|
-
- Sending an escape sequence into a child that does not parse mouse reports (non-pi alt-screen programs with mouse mode off) — accepted, scoped to the alternate buffer, and identical in kind to what the raw path already forwards for wheel/keys in regular mode.
|
|
121
|
-
- A quickly spinning wheel now produces one forwarded report per event, which the child's accelerator may amplify up to ~6 lines (×5 with Alt) per event — same as a real terminal, and #183's README note already explains the fullscreen line count is the TUI's decision.
|
|
122
|
-
- Momentum bursts arriving while the child repaints can interleave; the attach's projection re-renders from the buffer, so no state is lost.
|
|
123
|
-
|
|
124
|
-
## 10. Next step after approval
|
|
125
|
-
|
|
126
|
-
Step 5 of `github-issue-driven`: create worktree `issue-185-attach-wheel-alt-screen` from refreshed `main`, land this spec as the first commit at `docs/superpowers/specs/2026-10-14-issue-185-attach-wheel-alt-screen-design.md`, then plan (`writing-plans`) → implement (`subagent-driven-development`) → local CR → PR with Zima single-bot CR.
|
|
@@ -1,269 +0,0 @@
|
|
|
1
|
-
Drafted: deepseek/deepseek-flash (selected) · deepseek/deepseek-flash (physical) @ 2026-10-14T12:29:14Z
|
|
2
|
-
Revised: deepseek/deepseek-flash (selected) · deepseek/deepseek-flash (physical) @ 2026-10-14T12:48:00Z (round-1 review: R1-G1 accepted — classify the gesture in SCREEN coordinates; R1-D1 accepted — double-click path gets its own case; R1-D2 accepted — A2 gains the content-wait precondition and contains-semantics wording)
|
|
3
|
-
Revised: deepseek/deepseek-flash (selected) · deepseek/deepseek-flash (physical) @ 2026-10-14T13:05:00Z (round-2 review: R2-G1 accepted — U3 stops demanding double-click-under-drift and the boundary is documented; R2-D1 accepted — A4/A5 gain the press-created-selection precondition; R2-D2 accepted — A6 asserts an observable signal)
|
|
4
|
-
Revised: deepseek/deepseek-flash (selected) · deepseek/deepseek-flash (physical) @ 2026-10-14T13:22:00Z (round-3 review: converged with R3-D1 as the only detail — the raw/unclamped screen-cell semantics now have an explicit A1 case and an implementation note; the round-3 hash is the converged version plus this detail fix)
|
|
5
|
-
|
|
6
|
-
# Spec: #187 — attach(legacy): a motion-less drag must be classified by geometry, not by the move flag
|
|
7
|
-
|
|
8
|
-
Status: DRAFT (awaiting spec-cross-review, then user approval — github-issue-driven step 4 gate)
|
|
9
|
-
Issue: https://github.com/zhuxixi/pi-agent-board/issues/187
|
|
10
|
-
Repo state baseline: main @ `f81949d`
|
|
11
|
-
Scope: **#187 only.** #188 (X10 release parsed as press) and #189 (mixed input chunks) stay open and
|
|
12
|
-
untouched; the fix must not assume they are fixed.
|
|
13
|
-
|
|
14
|
-
## Problem (root cause, verified in code + reproduced at runtime)
|
|
15
|
-
|
|
16
|
-
`src/ui/pty-attach.ts` → `handleLocalMouseEvent` (legacy/regular-mode mouse path; the fullscreen path
|
|
17
|
-
is `handleMouse` and never reaches this handler):
|
|
18
|
-
|
|
19
|
-
```ts
|
|
20
|
-
// press (:1035) — 1-cell selection, and the drag flag is cleared
|
|
21
|
-
this.selection = { anchor: point, focus: point };
|
|
22
|
-
this.selectionDragging = false;
|
|
23
|
-
// move (:1045) — the ONLY place the drag flag is ever set
|
|
24
|
-
this.selectionDragging = true;
|
|
25
|
-
// release(:1054) — drag inferred from the flag
|
|
26
|
-
const shouldCopy = this.selectionDragging;
|
|
27
|
-
```
|
|
28
|
-
|
|
29
|
-
The flag answers "did a motion event arrive since the press", not "did the gesture move". Warp
|
|
30
|
-
delivers fast drags as **press + release with zero motion events** (byte capture in the issue), so
|
|
31
|
-
`shouldCopy` is `false` and the release takes the plain-click path (`:1079`
|
|
32
|
-
`schedulePendingClick(point)`): `copySelectionToClipboard()` never runs, and the 1-cell selection is
|
|
33
|
-
wiped by the pending-click timer 260 ms later. That timer is the reported symptom — *"drag-select
|
|
34
|
-
doesn't select at all; the moment I release, the selection is gone"*.
|
|
35
|
-
|
|
36
|
-
Runtime reproduction (throwaway harness modeled on `test-support/pty-attach-mouse-scenario.ts`: real
|
|
37
|
-
`PtyAttachComponent` + real runner socket + fake PTY, fake TUI capturing `terminal.write`):
|
|
38
|
-
|
|
39
|
-
| Gesture (row 2 = first body row) | OSC 52 payload | `selection` after 400 ms |
|
|
40
|
-
|---|---|---|
|
|
41
|
-
| press (col 1) → release (col 16), **zero motion** | none | `null` (pending-click timer) |
|
|
42
|
-
| press (col 1) → move (col 8) → release (col 16) | `["fake pi ready"]` | retained |
|
|
43
|
-
| press (col 1) → release (col 1) (same point) | none | `null` (expected click behavior) |
|
|
44
|
-
|
|
45
|
-
### Why the decision must be taken in SCREEN coordinates, not buffer coordinates
|
|
46
|
-
|
|
47
|
-
`mousePointForEvent` (`:1088-1089`) maps a screen cell to a buffer line through the *current*
|
|
48
|
-
viewport base: `start = this.viewportTop ?? this.bottomViewportTop(height)`. While following the
|
|
49
|
-
bottom (`viewportTop === null`, the normal case) `bottomViewportTop` (`:1340-1343`) is
|
|
50
|
-
`max(0, min(buf.length, baseY + rows) - height)`, which **grows as the session streams new lines**
|
|
51
|
-
and `clampViewportTop` (`:1346-1348`) deliberately does not pin it.
|
|
52
|
-
|
|
53
|
-
So `anchor` and `focus` are buffer coordinates captured at two different times against two different
|
|
54
|
-
bases: on a streaming session (the primary attach use case — watching a running agent) a stationary
|
|
55
|
-
click maps to `focus.line > anchor.line` purely because output arrived in between. A buffer-space
|
|
56
|
-
comparison would therefore classify every stationary click on a live session as a drag: the single
|
|
57
|
-
click would stop opening links, the double-click would stop selecting words, and each click would
|
|
58
|
-
write the clipboard. The gesture must be classified from the pointer's **screen cells**
|
|
59
|
-
(`mouse.row` / `mouse.col` of the parsed report, 1-based), which is exactly the data the terminal
|
|
60
|
-
sent and is immune to viewport drift.
|
|
61
|
-
|
|
62
|
-
## Goal / non-goals
|
|
63
|
-
|
|
64
|
-
**Goal**: in the legacy path, a release is treated as a drag when the gesture actually moved —
|
|
65
|
-
either a motion event was seen (`selectionDragging`) **or** the release's screen cell differs from
|
|
66
|
-
the press's screen cell. The motion-less gesture then lands on the same branch as the canonical
|
|
67
|
-
drag: copy + highlight retained until the next click. Stationary clicks keep the click path
|
|
68
|
-
regardless of how much output arrived between press and release.
|
|
69
|
-
|
|
70
|
-
**Non-goals**:
|
|
71
|
-
|
|
72
|
-
- #172 (clear the highlight after copy) — the drag-copy branch deliberately keeps the highlight;
|
|
73
|
-
#187 must reproduce that contract, not change it.
|
|
74
|
-
- #186 UX umbrella items (selection background color, no 1-cell press selection, clipboard
|
|
75
|
-
fallback/`pbcopy`, Shift+drag docs).
|
|
76
|
-
- #188 (X10 `Cb=3` release parsed as press) and #189 (all-or-nothing chunk parsing): separate
|
|
77
|
-
issues, separate acceptance. The fix works with both still broken.
|
|
78
|
-
- Fullscreen mode / `handleMouse` / pi-tui behavior (upstream concern).
|
|
79
|
-
- Press/move branches, selection autoscroll, middle-click paste, PRIMARY mirroring, wheel handling.
|
|
80
|
-
- No change to how the selection range itself is stored (still buffer coordinates) — only the
|
|
81
|
-
drag/click classification moves to screen space.
|
|
82
|
-
- No new env knob, no new dependency, no new I/O surface.
|
|
83
|
-
|
|
84
|
-
## Design
|
|
85
|
-
|
|
86
|
-
### 1. Pure helpers (`src/core/pty-scroll.mjs`)
|
|
87
|
-
|
|
88
|
-
```js
|
|
89
|
-
/** Issue #187: two mouse points are "the same" within the terminals' 1-cell coordinate slack.
|
|
90
|
-
* Moved here from pty-attach.ts (it was already pure) so double-click detection and the
|
|
91
|
-
* screen-space gesture comparison share one definition each. Buffer-space comparison. Pure. */
|
|
92
|
-
export function sameMousePoint(a, b) {
|
|
93
|
-
return a.line === b.line && Math.abs(a.col - b.col) <= 1;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
/** Issue #187: two SCREEN cells are "the same" within the same 1-cell slack — the terminal
|
|
97
|
-
* reports cells, and a genuine click must survive a ±1 sub-cell report. Screen-space
|
|
98
|
-
* comparison; see the spec for why the gesture is classified here and not in buffer space. Pure. */
|
|
99
|
-
export function sameScreenCell(a, b) {
|
|
100
|
-
return a.row === b.row && Math.abs(a.col - b.col) <= 1;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
/** Issue #187: whether a mouse release ends a real selection drag. Terminals such as Warp
|
|
104
|
-
* deliver a fast drag as press + release with zero motion events, so the move flag alone is not
|
|
105
|
-
* sufficient — the gesture geometry decides. Points are SCREEN cells, so a viewport that drifts
|
|
106
|
-
* between press and release (streaming session, follow-bottom) cannot turn a click into a drag.
|
|
107
|
-
* Missing geometry (no screen anchor recorded) degrades to the pre-fix answer: not a drag. Pure. */
|
|
108
|
-
export function selectionReleaseIsDrag({ dragging, pressScreen, releaseScreen }) {
|
|
109
|
-
if (dragging) return true;
|
|
110
|
-
if (!pressScreen || !releaseScreen) return false;
|
|
111
|
-
return !sameScreenCell(pressScreen, releaseScreen);
|
|
112
|
-
}
|
|
113
|
-
```
|
|
114
|
-
|
|
115
|
-
Decision table for `selectionReleaseIsDrag`:
|
|
116
|
-
|
|
117
|
-
| dragging | pressScreen / releaseScreen (screen cells) | result |
|
|
118
|
-
|---|---|---|
|
|
119
|
-
| `true` | any (incl. missing) | `true` |
|
|
120
|
-
| `false` | same row, `\|Δcol\| <= 1` | `false` (click) |
|
|
121
|
-
| `false` | same row, `\|Δcol\| >= 2` | `true` (motion-less drag) |
|
|
122
|
-
| `false` | different row | `true` (motion-less drag) |
|
|
123
|
-
| `false` | either missing | `false` (degrade to pre-fix behavior) |
|
|
124
|
-
|
|
125
|
-
Buffer-space points (`anchor`/`focus`) are deliberately **not** inputs: they are only meaningful
|
|
126
|
-
against the viewport base that produced them.
|
|
127
|
-
|
|
128
|
-
### 2. Component wiring (`src/ui/pty-attach.ts`)
|
|
129
|
-
|
|
130
|
-
- The selection object carries the gesture anchor in screen space, so its lifetime is bound to the
|
|
131
|
-
selection and no extra reset site is introduced:
|
|
132
|
-
|
|
133
|
-
```ts
|
|
134
|
-
// press (:1035): record the raw screen cell alongside the buffer-space anchor
|
|
135
|
-
this.selection = { anchor: point, focus: point, screenAnchor: { row: mouse.row, col: mouse.col } };
|
|
136
|
-
```
|
|
137
|
-
|
|
138
|
-
`screenAnchor` is optional: `selectWordAtPoint` (double-click) creates a selection without one,
|
|
139
|
-
and a missing anchor degrades to "not a drag".
|
|
140
|
-
|
|
141
|
-
**Both sides of the comparison must be the raw reported cells** (`mouse.row` / `mouse.col` as
|
|
142
|
-
parsed from the terminal report) — never the clamped buffer point `mousePointForEvent` returns, and
|
|
143
|
-
never `this.selection.focus`. A release outside the body is still classified by where the pointer
|
|
144
|
-
actually went; only the *selection focus* is clamped (unchanged). A1 carries an out-of-body case
|
|
145
|
-
for exactly this reason.
|
|
146
|
-
|
|
147
|
-
- Release branch — the decision, and nothing else, changes:
|
|
148
|
-
|
|
149
|
-
```ts
|
|
150
|
-
if (mouse.action === "release") {
|
|
151
|
-
this.clearSelectionAutoScroll();
|
|
152
|
-
const point = this.mousePointForEvent(mouse.row, mouse.col, true); // unchanged
|
|
153
|
-
if (point) this.selection.focus = point; // unchanged
|
|
154
|
-
const shouldCopy = selectionReleaseIsDrag({ // was: this.selectionDragging
|
|
155
|
-
dragging: this.selectionDragging,
|
|
156
|
-
pressScreen: this.selection.screenAnchor,
|
|
157
|
-
releaseScreen: { row: mouse.row, col: mouse.col },
|
|
158
|
-
});
|
|
159
|
-
this.selectionDragging = false; // unchanged
|
|
160
|
-
// ...copy branch / click branch unchanged
|
|
161
|
-
}
|
|
162
|
-
```
|
|
163
|
-
|
|
164
|
-
Plus: `sameMousePoint` is deleted from `pty-attach.ts` (`:1658`) and imported from
|
|
165
|
-
`../core/pty-scroll.mjs` together with `selectionReleaseIsDrag` (one import line, `:20`).
|
|
166
|
-
`isDoubleClickCandidate` (`:1096`) keeps calling `sameMousePoint` on buffer points, unchanged.
|
|
167
|
-
|
|
168
|
-
### Resulting behavior matrix
|
|
169
|
-
|
|
170
|
-
| Gesture | Before | After |
|
|
171
|
-
|---|---|---|
|
|
172
|
-
| press + release, same screen cell | click path: pending link-open, clear after 260 ms | unchanged |
|
|
173
|
-
| press + release, `\|Δcol\| <= 1` on the same screen row | click path | unchanged (documented boundary) |
|
|
174
|
-
| press + release, same screen cell, **session streamed output in between** | click path | **unchanged** (this is the R1-G1 regression the screen-space decision prevents) |
|
|
175
|
-
| press + move + release | copy + highlight retained | unchanged |
|
|
176
|
-
| press + release, `\|Δcol\| >= 2` or different screen row (motion-less drag) | **click path: no copy, selection wiped after 260 ms** | **copy + highlight retained (same branch as the canonical drag)** |
|
|
177
|
-
| drag released outside the body | clamped selection focus, drag if the flag was set | clamped selection focus, drag by screen geometry too |
|
|
178
|
-
| middle press / non-primary press / wheel | unchanged | unchanged |
|
|
179
|
-
| fullscreen TUI | different handler (`handleMouse`) | unchanged |
|
|
180
|
-
|
|
181
|
-
Side effect of the fix worth stating: a motion-less drag now cancels a pending click from an
|
|
182
|
-
earlier press, exactly as the canonical drag path already does (`clearPendingClick()` in the copy
|
|
183
|
-
branch). That is intended — a drag is not a click.
|
|
184
|
-
|
|
185
|
-
## Acceptance matrix
|
|
186
|
-
|
|
187
|
-
| ID | 功能点 | 验收方式 | 具体验证 | 通过标准 |
|
|
188
|
-
|----|--------|----------|----------|----------|
|
|
189
|
-
| A1 | 纯决策 `selectionReleaseIsDrag` 决策表(屏幕坐标)+ `sameScreenCell` / `sameMousePoint` 边界 | 自动化验证(unit) | `node --test test/pty-scroll.test.mjs`(新增用例:dragging=true;同屏幕单元 → false;`\|Δcol\| = 1` → false;`\|Δcol\| = 2` → true;不同屏幕行 → true;pressScreen/releaseScreen 缺省 → false;**body 外的原始屏幕单元**(`pressScreen={row:2,col:1}`、`releaseScreen={row:30,col:16}`)→ true(按**原始**屏幕值判定,不复用 `mousePointForEvent` 的 clamp 结果);sameMousePoint 的行相等与 1 格容差;sameScreenCell 的行相等与 1 格容差) | 新增用例全绿,既有用例不回归 |
|
|
190
|
-
| A2 | 组件接线:零 motion 的 press→release 走复制分支(本 issue 的确切复现) | 自动化验证(integration,组件级场景:真 runner socket + 默认模式 fake PTY + fake TUI 捕获 `terminal.write`) | 新增 `test-support/pty-attach-legacy-drag-scenario.ts` + `test/pty-attach-legacy-drag.test.mjs`;case 1:先照 `test-support/pty-attach-mouse-scenario.ts` 的 `sawContent` 轮询等到 `fake pi ready` 渲染完成(**前置条件,防空缓冲竞态**),再注入 `\x1b[<0;1;2M` + `\x1b[<0;16;2m`(零 move) | ① `terminal.write` 收到的序列中包含 `\x1b]52;c;<base64("fake pi ready")>\x07`(**包含语义**:同一通道还有鼠标模式控制序列,不得用等值断言);② 400 ms 后组件 selection 非 null(未被待点击定时器清除);③ 无 `{type:"input"}` 转发到 socket |
|
|
191
|
-
| A3 | 等价性:零 motion 拖拽与常规拖拽复制同一份文本 | 自动化验证(integration,同场景) | 同场景 case 2:`press(col1,row2)` + `move(col8,row2)` + `release(col16,row2)`;与 case 1 的 OSC 52 payload 比对 | 两个 case 提取出的 base64 payload 逐字节相同 |
|
|
192
|
-
| A4 | 负向:同屏幕单元的 press+release 仍走点击路径(不复制、260 ms 后清选中) | 自动化验证(integration,同场景) | 同场景 case 3:`press(col1,row2)` + `release(col1,row2)`,等 400 ms | ① press 注入后组件 selection **非 null**(证明 press 路径确实建立了选区,否则本用例空转过绿、无判别力);② 无 OSC 52 写出;③ 400 ms 后组件 selection 为 null |
|
|
193
|
-
| A5 | 漂移负向控制(R1-G1):流式输出导致的视口漂移不得把定身点击变成拖拽 | 自动化验证(integration,组件级场景:`FAKE_PTY_STREAM_MODE=steady` 的 fake PTY,缓冲区在 press→release 之间确实增长) | 新增 `test-support/pty-attach-legacy-drag-drift-scenario.ts` + `test/pty-attach-legacy-drag-drift.test.mjs`:等到内容渲染后,在同一屏幕单元 `press(col1,row2)`,等 ≥150 ms(期间 steady 流持续写入),再 `release(col1,row2)` | ① 断言期间缓冲区行数确实增长(否则该用例无判别力,视为失败);② press 注入后组件 selection 非 null(同 A4 判据①);③ 无 OSC 52 写出;④ 400 ms 后组件 selection 为 null(点击路径未被改判) |
|
|
194
|
-
| A6 | 点击路径未被改判:双击选词仍复制(该路径修复前无任何既有测试覆盖) | 自动化验证(integration,A2 场景) | 同场景 case 4:同屏幕单元两次快速 `press`+`release`(间隔 < 260 ms,第二次 release 触发 `isDoubleClickCandidate`)。默认模式 PTY 在此期间无新输出,故缓冲坐标不漂移——与 U3 的真实流式环境形成对照 | ① `terminal.write` 中包含以该单元格所在词为内容的 OSC 52 payload(fixture 下预期 `fake`);② 400 ms 后组件 selection 仍为该词的选区(未被 pending-click 定时器清空) |
|
|
195
|
-
| A7 | 类型 / 构建 / 打包 | 自动化验证(static + build) | `npm run typecheck`;`npm run verify` | 全部通过(含 perf gate、coverage 阈值 lines 85 / funcs 80 / branches 70、`pack:dry`) |
|
|
196
|
-
| A8 | 既有行为不回归(#167 鼠标分发、#171 中键粘贴、#174 选中镜像、滚轮) | 自动化验证(unit + integration,既有套件) | `npm run test:coverage`(含 `test/pty-scroll.test.mjs`、`test/pty-attach-mouse.test.mjs`、`test/pty-attach-selection-mirror.test.mjs`、`test/pty-attach-wheel.test.mjs`) | 既有测试全绿,覆盖率不低于阈值。注:双击/待点击路径**此前无既有覆盖**(`grep -rn 'isDoubleClickCandidate\|selectWordAtPoint\|schedulePendingClick' test/ test-support/` 空),其不回归由 A6 承担 |
|
|
197
|
-
| U1 | 真机(终端原生,回归):常规模式 attach 内拖选照旧复制 | 用户实测 | ① 把本机加载的扩展更新到合并后的 main:`git -C ~/.pi/agent/git/github.com/zhuxixi/pi-agent-board pull`(该 checkout 当前 HEAD=f81949d、有 `package-lock.json` 本地改动,先确认或 stash;扩展经 `~/.pi/agent/settings.json` 的 `git:github.com/zhuxixi/pi-agent-board` 注册,入口 `./index.ts` 免构建);② 重启 `pi --tui-mode regular`(legacy 路径只在常规模式生效)→ `/agent-board` → attach 到任一 session;③ 左键拖选一段文字后松开;④ 中键粘贴 | ④ 中键粘贴内容与 ③ 选中的文字一致;高亮状态与 #172 现状一致(保留到下一次点击) |
|
|
198
|
-
| U2 | 真机(精确字节流):注入 Warp 的 press+release 字节,复现本 issue 的确切输入 | 用户实测(tmux 3.4 已在本机;不可用时标 pending) | ① tmux 内跑 `pi --tui-mode regular` → `/agent-board` → attach;② 注入原始字节:`tmux send-keys -H 1b 5b 3c 30 3b 31 3b 32 4d`(press col1,row2)然后 `tmux send-keys -H 1b 5b 3c 30 3b 31 36 3b 32 6d`(release col16,row2);③ 观察 attach 视图高亮与该行文本的剪贴板/PRIMARY 内容 | 注入后该行文本进入剪贴板(OSC 52 或 xclip PRIMARY),高亮保留到下一次点击;若常规模式下的真实 TUI 不把该字节流交给 attach 组件(与组件级 A2 环境不同),如实记录观察结果并标 pending,不改 A2 结论 |
|
|
199
|
-
| U3 | 真机(流式会话,R1-G1 回归):输出持续滚动时单击**不得**被改判为拖拽 | 用户实测 | ① 承接 U1 的环境;② 让被 attach 的 session 持续输出(例如 `while :; do date; sleep 0.2; done`);③ 输出滚动期间单击一段**无链接**文本数次;④ 停掉持续输出、让 session 空闲后,双击某词作对照 | ③ 单击期间剪贴板内容不变(无 OSC 52 写入迹象)、不误开链接——这是本次修复的核心回归项;④ 输出停止后的双击仍能选中该词并复制。**输出正好滚动时双击失配属既有行为(见保证边界),不计入本项判定** |
|
|
200
|
-
|
|
201
|
-
## Testability split design (hard constraint for implementation)
|
|
202
|
-
|
|
203
|
-
- **Pure core** (`src/core/pty-scroll.mjs`): `sameMousePoint(a, b)` (buffer space, shared with
|
|
204
|
-
double-click detection), `sameScreenCell(a, b)` (screen cells) and
|
|
205
|
-
`selectionReleaseIsDrag({dragging, pressScreen, releaseScreen})` — plain functions, no env reads,
|
|
206
|
-
no I/O, no imports from the component. Unit decision tables (A1) call them directly.
|
|
207
|
-
- **I/O shell** (`PtyAttachComponent.handleLocalMouseEvent`): the only caller of the decision. It
|
|
208
|
-
keeps owning selection state (buffer space), the screen anchor recording, the pending-click timer,
|
|
209
|
-
the clamp, and `copySelectionToClipboard()`. No new private method is introduced and no new
|
|
210
|
-
side-effect surface is added — the copy branch already exists and is reused verbatim.
|
|
211
|
-
- **Screen anchor ownership**: the anchor is recorded in the press branch as a field of
|
|
212
|
-
`this.selection`, so it shares the selection's lifetime — there is no second piece of state to
|
|
213
|
-
invalidate in `clearSelection()`, the pending-click timer, or `selectWordAtPoint()`.
|
|
214
|
-
- **Test seam** (component-level, A2–A6): the public `handleInput` legacy seam + a fake
|
|
215
|
-
`tui.terminal.write` capture + a real runner socket with the fake PTY, i.e. exactly the seams
|
|
216
|
-
`test/pty-attach-mouse.test.mjs` and `test/pty-attach-selection-mirror.test.mjs` already use.
|
|
217
|
-
No test-only export, no new env knob, no injection hook.
|
|
218
|
-
- **Fixtures**: default-mode `test-support/fake-pty-pi.mjs` prints exactly one stable line
|
|
219
|
-
(`fake pi ready`), so the expected OSC 52 payload is deterministic; `steady` mode is the drift
|
|
220
|
-
source for A5. A PATH-stub `xclip` (logs args, swallows stdin) keeps scenarios off the host
|
|
221
|
-
clipboard.
|
|
222
|
-
- **Boundaries to preserve**: each comparison keeps exactly one definition (`sameMousePoint` for
|
|
223
|
-
buffer points, `sameScreenCell` for screen cells, both in `pty-scroll.mjs`, both unit-tested);
|
|
224
|
-
the component must not re-inline either comparison, and the drag decision must not be split
|
|
225
|
-
between the helper and the component.
|
|
226
|
-
|
|
227
|
-
## Risks / guarantee boundaries
|
|
228
|
-
|
|
229
|
-
- **Coordinates are cell-quantized.** A gesture that starts and ends within the same screen cell (or
|
|
230
|
-
one column apart) is still a click; the fix cannot see sub-cell intent. Both comparison helpers
|
|
231
|
-
apply the same 1-cell slack used today for double-click detection, so classification stays
|
|
232
|
-
consistent.
|
|
233
|
-
- **Release outside the body** is clamped into the body before the selection focus is updated
|
|
234
|
-
(existing behavior, unchanged); the drag decision itself uses the raw screen cells, so an
|
|
235
|
-
out-of-body release is still classified as a drag when the pointer really moved.
|
|
236
|
-
- **Screen coordinates are only used for classification.** The selection range stays in buffer
|
|
237
|
-
coordinates; if output scrolls the buffer mid-gesture, the copied text is whatever the current
|
|
238
|
-
viewport mapping yields — unchanged behavior, not addressed here.
|
|
239
|
-
- **Double-click matching still uses buffer coordinates.** `isDoubleClickCandidate` (`:1096`)
|
|
240
|
-
compares `lastClickPoint` (captured at the first release) against the second release point, both
|
|
241
|
-
in buffer space, so output arriving between the two clicks makes a double-click miss — a
|
|
242
|
-
pre-existing behaviour that #187 deliberately does not change (same drift mechanism as above,
|
|
243
|
-
different call site). Fixing it means moving that comparison to screen cells too; that is a
|
|
244
|
-
separate change with its own acceptance and deserves its own issue.
|
|
245
|
-
- **#188 / #189 are not fixed here.** X10-only terminals still misparse the release as a press, and
|
|
246
|
-
mixed input chunks still bypass the mouse path; in those scenarios the selection is still lost.
|
|
247
|
-
This is explicitly out of scope for #187.
|
|
248
|
-
- **No claim about terminal clipboard support.** The fix guarantees the copy is *attempted* on this
|
|
249
|
-
path (OSC 52 write + PRIMARY mirror); whether the outer terminal honors OSC 52 is
|
|
250
|
-
terminal-dependent (the reporter verified Warp does).
|
|
251
|
-
- **No claim about fullscreen mode.** pi ≥1.0 fullscreen selection/copy is pi-tui's; this handler
|
|
252
|
-
does not run there.
|
|
253
|
-
|
|
254
|
-
## Deliverables
|
|
255
|
-
|
|
256
|
-
- `src/core/pty-scroll.mjs`: + `sameMousePoint` (moved), + `sameScreenCell`, + `selectionReleaseIsDrag`
|
|
257
|
-
- `src/ui/pty-attach.ts`: import both comparators + the decision; delete the local `sameMousePoint`;
|
|
258
|
-
record `screenAnchor` in the press branch; release-branch decision
|
|
259
|
-
- `test/pty-scroll.test.mjs`: decision-table cases (A1)
|
|
260
|
-
- `test-support/pty-attach-legacy-drag-scenario.ts` + `test/pty-attach-legacy-drag.test.mjs` (A2–A4, A6)
|
|
261
|
-
- `test-support/pty-attach-legacy-drag-drift-scenario.ts` + `test/pty-attach-legacy-drag-drift.test.mjs` (A5)
|
|
262
|
-
- `docs/superpowers/specs/2026-10-14-issue-187-motionless-drag-design.md`: this spec, first commit in the worktree (step 5)
|
|
263
|
-
- CHANGELOG entry via the release tooling at release time (not in this PR)
|
|
264
|
-
|
|
265
|
-
## Open question for the design gate
|
|
266
|
-
|
|
267
|
-
U2 depends on tmux + regular TUI mode being available in the operator's environment (tmux 3.4 is
|
|
268
|
-
present on this machine, verified). If the operator confirms it is not available, U2 is recorded as
|
|
269
|
-
`pending` (per the acceptance-reporting rule) and A2 stays the evidence for the fix.
|