@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.
- package/CHANGELOG.md +13 -4
- package/CONTRIBUTING.md +7 -0
- package/README.md +53 -419
- package/SECURITY.md +19 -0
- package/docs/how-it-works.md +31 -0
- package/docs/reference.md +276 -0
- package/package.json +5 -11
- package/src/index.ts +1 -1
- package/src/runtime/service.mjs +12 -0
- package/src/ui/dashboard.ts +14 -8
- 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,823 +0,0 @@
|
|
|
1
|
-
# Issue #189 — mixed-input segment dispatch: Implementation Plan
|
|
2
|
-
|
|
3
|
-
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
|
4
|
-
|
|
5
|
-
**Goal:** In the legacy (regular-TUI) attach path, dispatch one input read segment by segment — mouse
|
|
6
|
-
runs to the existing mouse path, terminal-report runs to a pass-through that touches neither the
|
|
7
|
-
selection nor the viewport, everything else to today's keyboard chain.
|
|
8
|
-
|
|
9
|
-
**Architecture:** A new pure `splitAttachInput(data)` in `src/core/pty-scroll.mjs` (built on the
|
|
10
|
-
existing `parseMouseEventPrefix` plus a small, explicitly grounded report table) returns an ordered
|
|
11
|
-
array of `mouse | report | key` segments; `PtyAttachComponent.handleInput` becomes a dispatch loop
|
|
12
|
-
and the existing keyboard chain moves verbatim into a private `handleKeyInput`. All new behaviour is
|
|
13
|
-
covered by pure unit cases plus one component-level scenario (real runner socket + fake PTY + fake
|
|
14
|
-
TUI) that drives the public `handleInput` seam.
|
|
15
|
-
|
|
16
|
-
**Tech Stack:** Node.js ESM (`.mjs` pure layer), TypeScript (`src/ui/pty-attach.ts`, compiled with
|
|
17
|
-
`tsc --noEmit`), `node:test` + `assert/strict`, scenario harnesses run with
|
|
18
|
-
`node --experimental-transform-types`.
|
|
19
|
-
|
|
20
|
-
**Spec:** `docs/superpowers/specs/2026-10-14-issue-189-mixed-input-segments-design.md` (issue #189,
|
|
21
|
-
cross-reviewed twice, converged; sha256 `042e4efc508bab3b497083076c8359d70f259ae79fe4dd2b6758033d19084e15`).
|
|
22
|
-
|
|
23
|
-
## Global Constraints
|
|
24
|
-
|
|
25
|
-
- **Worktree only:** every change lands in
|
|
26
|
-
`/home/elling/git-repo/github/pi-agent-board/.pi/worktrees/issue-189-mixed-input-segments` (branch
|
|
27
|
-
`issue-189-mixed-input-segments`). Never touch the main checkout; stage files by name (no
|
|
28
|
-
`git add -A`).
|
|
29
|
-
- **Indentation:** tabs. **Comments and commit messages:** English. Conventional commit subjects
|
|
30
|
-
(`feat`/`fix`/`docs`/`test`/`chore`/`refactor`) with the issue reference `(issue #189)`.
|
|
31
|
-
- **`parseMouseInputChunk` keeps its literal contract** (all-or-nothing, `null` on any non-mouse
|
|
32
|
-
byte): existing assertions in `test/pty-scroll.test.mjs:83-99` and sibling issue #188's planned
|
|
33
|
-
cases depend on it. It is re-expressed on a new private `parseMouseRun` but must not change.
|
|
34
|
-
- **Do not touch** `parseMouseEventPrefix`, its SGR/X10 branches, `parseMouseEvent`,
|
|
35
|
-
`mouseWheelDirection`, `handleLocalMouseEvent`, `handleMouseInputChunk`, `handleMouse`, or any
|
|
36
|
-
selection/copy/viewport logic. Sibling issues #187/#188 own edits there.
|
|
37
|
-
- **Testability split (hard constraint from spec §7):** all classification lives in the pure module;
|
|
38
|
-
the component only switches on the segment kind; modules and helpers touched by the change use
|
|
39
|
-
English JSDoc, and no test-only export may be added.
|
|
40
|
-
- **No new env knob, dependency, or I/O surface.** No README/CHANGELOG editing (the release tooling
|
|
41
|
-
owns the changelog).
|
|
42
|
-
- **Definition of done:** `npm run verify` exits 0 (typecheck + perf gate + tests + coverage
|
|
43
|
-
thresholds lines 85 / funcs 80 / branches 70 + `pack:dry`).
|
|
44
|
-
- **Node ≥ 24** is required by the toolchain; the `node_modules` symlink in the worktree points at
|
|
45
|
-
the main checkout's install (gitignored — never commit it).
|
|
46
|
-
|
|
47
|
-
---
|
|
48
|
-
|
|
49
|
-
### Task 1: Pure segment classifier + report table (`splitAttachInput`)
|
|
50
|
-
|
|
51
|
-
Spec acceptance: **A1** (segment decision table), **A2** (report positives + deliberate negatives).
|
|
52
|
-
|
|
53
|
-
**Files:**
|
|
54
|
-
- Modify: `src/core/pty-scroll.mjs` (insert after `parseMouseInputChunk`, which ends at line 95)
|
|
55
|
-
- Test: `test/pty-scroll.test.mjs` (add `splitAttachInput` to the import block at lines 3-16, append
|
|
56
|
-
two tests at the end of the file)
|
|
57
|
-
|
|
58
|
-
**Interfaces:**
|
|
59
|
-
- Consumes: existing private `parseMouseEventPrefix(data, offset)` (`src/core/pty-scroll.mjs:31`).
|
|
60
|
-
- Produces (Task 2 relies on these exact names/shapes):
|
|
61
|
-
- `splitAttachInput(data: string)` → `Array<{kind:"mouse", events: Array<{length:number, raw:string, mouse:{encoding:string, button:number, col:number, row:number, action:string}}>} | {kind:"report", data:string} | {kind:"key", data:string}>`; returns `[]` for falsy input; consecutive same-kind segments are merged; unrecognised/partial escape sequences stay `key`.
|
|
62
|
-
- `parseMouseInputChunk(data)` → unchanged behaviour, now implemented via a private `parseMouseRun(data, offset)`.
|
|
63
|
-
|
|
64
|
-
- [ ] **Step 1: Write the failing A1 test (segmentation decision table)**
|
|
65
|
-
|
|
66
|
-
Append to `test/pty-scroll.test.mjs` (and add `splitAttachInput,` to the import list, keeping it
|
|
67
|
-
alphabetical between `shouldOwnOuterMouseMode` and `mouseWheelDirection`-independent entries):
|
|
68
|
-
|
|
69
|
-
```js
|
|
70
|
-
test("splitAttachInput decodes ordered mouse / report / key segments (issue #189)", () => {
|
|
71
|
-
const kinds = (input) => splitAttachInput(input).map((segment) => segment.kind);
|
|
72
|
-
const data = (input) => splitAttachInput(input).map((segment) => (segment.kind === "mouse" ? segment.events.map((entry) => entry.raw) : segment.data));
|
|
73
|
-
|
|
74
|
-
assert.deepEqual(splitAttachInput(""), []);
|
|
75
|
-
assert.deepEqual(kinds("\x1b[<64;10;20M\x1b[<65;10;20M"), ["mouse"]);
|
|
76
|
-
assert.equal(splitAttachInput("\x1b[<64;10;20M\x1b[<65;10;20M")[0].events.length, 2);
|
|
77
|
-
|
|
78
|
-
// the issue's repro, both orders
|
|
79
|
-
assert.deepEqual(kinds("\x1b[<0;16;2m\x1b[I"), ["mouse", "report"]);
|
|
80
|
-
assert.deepEqual(data("\x1b[<0;16;2m\x1b[I"), ["\x1b[<0;16;2m", "\x1b[I"]);
|
|
81
|
-
assert.deepEqual(kinds("\x1b[I\x1b[<0;16;2m"), ["report", "mouse"]);
|
|
82
|
-
assert.deepEqual(kinds("\x1b[I\x1b[O\x1b[<0;16;2m"), ["report", "mouse"]);
|
|
83
|
-
assert.deepEqual(data("\x1b[I\x1b[O\x1b[<0;16;2m"), ["\x1b[I\x1b[O", "\x1b[<0;16;2m"]);
|
|
84
|
-
|
|
85
|
-
// mouse/key and report/key adjacency, both orders
|
|
86
|
-
assert.deepEqual(kinds("\x1b[<0;16;2mx"), ["mouse", "key"]);
|
|
87
|
-
assert.deepEqual(kinds("x\x1b[<0;16;2m"), ["key", "mouse"]);
|
|
88
|
-
assert.deepEqual(kinds("\x1b[Ix"), ["report", "key"]);
|
|
89
|
-
assert.deepEqual(kinds("x\x1b[I"), ["key", "report"]);
|
|
90
|
-
|
|
91
|
-
assert.deepEqual(kinds("hello"), ["key"]);
|
|
92
|
-
assert.deepEqual(data("hello"), ["hello"]);
|
|
93
|
-
|
|
94
|
-
// adjacent key runs merge: a key is only recognised when it is the whole read (spec §5)
|
|
95
|
-
assert.deepEqual(kinds("\x1b[5~\x1b[6~"), ["key"]);
|
|
96
|
-
assert.deepEqual(data("\x1b[5~\x1b[6~"), ["\x1b[5~\x1b[6~"]);
|
|
97
|
-
|
|
98
|
-
// truncated sequences are not recognised and stay keyboard text
|
|
99
|
-
assert.deepEqual(kinds("\x1b[<0;16"), ["key"]);
|
|
100
|
-
assert.deepEqual(kinds("\x1b[?997;1"), ["key"]);
|
|
101
|
-
assert.deepEqual(kinds("\x1b[<0;16\x1b[I"), ["key", "report"]);
|
|
102
|
-
|
|
103
|
-
// the pre-existing all-or-nothing contract is untouched
|
|
104
|
-
assert.equal(parseMouseInputChunk("\x1b[<64;10;20Mx"), null);
|
|
105
|
-
assert.equal(parseMouseInputChunk("x\x1b[<64;10;20M"), null);
|
|
106
|
-
assert.equal(parseMouseInputChunk(""), null);
|
|
107
|
-
});
|
|
108
|
-
```
|
|
109
|
-
|
|
110
|
-
- [ ] **Step 2: Run it and watch it fail**
|
|
111
|
-
|
|
112
|
-
Run: `node --test test/pty-scroll.test.mjs`
|
|
113
|
-
Expected: FAIL — `SyntaxError: The requested module '../src/core/pty-scroll.mjs' does not provide an export named 'splitAttachInput'`.
|
|
114
|
-
|
|
115
|
-
- [ ] **Step 3: Write the failing A2 test (report positives + deliberate negatives)**
|
|
116
|
-
|
|
117
|
-
Append:
|
|
118
|
-
|
|
119
|
-
```js
|
|
120
|
-
test("splitAttachInput classifies grounded terminal reports and keeps keys as keys (issue #189)", () => {
|
|
121
|
-
const reports = [
|
|
122
|
-
"\x1b[I",
|
|
123
|
-
"\x1b[O",
|
|
124
|
-
"\x1b[?62;4;6;22c",
|
|
125
|
-
"\x1b[>0;95;0c",
|
|
126
|
-
"\x1b[?1u",
|
|
127
|
-
"\x1b[?997;1n",
|
|
128
|
-
"\x1b[?997;2n",
|
|
129
|
-
"\x1b[6;24;80t",
|
|
130
|
-
"\x1b]11;rgb:0d0d/0d0d/0d0d\x07",
|
|
131
|
-
"\x1b]11;rgb:0d0d/0d0d/0d0d\x1b\\",
|
|
132
|
-
"\x1b]10;#ffffff\x07",
|
|
133
|
-
"\x1b]4;3;rgb:0000/0000/0000\x07",
|
|
134
|
-
"\x1b]7501;?\x1b\\",
|
|
135
|
-
];
|
|
136
|
-
for (const sequence of reports) {
|
|
137
|
-
assert.deepEqual(splitAttachInput(sequence), [{ kind: "report", data: sequence }], `report: ${JSON.stringify(sequence)}`);
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
// deliberately not reports: a keystroke can look like them (spec §3.1(b))
|
|
141
|
-
const keys = ["a", "\x1b[A", "\x1b[1;2R", "\x1b[97;5u", "\x1b[1;5D", "\x1b[200~paste\x1b[201~", "\x1bP1+r6375;6375\x1b\\"];
|
|
142
|
-
for (const sequence of keys) {
|
|
143
|
-
assert.equal(splitAttachInput(sequence)[0].kind, "key", `key: ${JSON.stringify(sequence)}`);
|
|
144
|
-
}
|
|
145
|
-
});
|
|
146
|
-
```
|
|
147
|
-
|
|
148
|
-
- [ ] **Step 4: Run it and watch it fail**
|
|
149
|
-
|
|
150
|
-
Run: `node --test test/pty-scroll.test.mjs`
|
|
151
|
-
Expected: FAIL — same missing-export error.
|
|
152
|
-
|
|
153
|
-
- [ ] **Step 5: Implement the pure helpers**
|
|
154
|
-
|
|
155
|
-
In `src/core/pty-scroll.mjs`, replace the current `parseMouseInputChunk` block (lines 79-95) with
|
|
156
|
-
the code below (the existing doc comment on `parseMouseInputChunk` must keep stating the
|
|
157
|
-
all-or-nothing contract):
|
|
158
|
-
|
|
159
|
-
```js
|
|
160
|
-
/**
|
|
161
|
-
* Read a maximal run of concatenated mouse reports starting at `offset`. Returns the decoded events
|
|
162
|
-
* with their exact raw byte sequences plus the consumed length, or null when no complete report
|
|
163
|
-
* starts there.
|
|
164
|
-
*/
|
|
165
|
-
function parseMouseRun(data, offset = 0) {
|
|
166
|
-
const events = [];
|
|
167
|
-
let position = offset;
|
|
168
|
-
for (;;) {
|
|
169
|
-
const parsed = parseMouseEventPrefix(data, position);
|
|
170
|
-
if (!parsed) break;
|
|
171
|
-
events.push(parsed);
|
|
172
|
-
position += parsed.length;
|
|
173
|
-
}
|
|
174
|
-
return events.length > 0 ? { events, length: position - offset } : null;
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
/**
|
|
178
|
-
* Parse an input chunk that consists entirely of one or more concatenated mouse reports.
|
|
179
|
-
* Returns each decoded event with its exact raw byte sequence, or null if any non-mouse
|
|
180
|
-
* bytes are present in the chunk (all-or-nothing contract, pinned by unit tests and by the
|
|
181
|
-
* X10 acceptance of issue #188 — do not relax it).
|
|
182
|
-
*/
|
|
183
|
-
export function parseMouseInputChunk(data) {
|
|
184
|
-
if (!data) return null;
|
|
185
|
-
const run = parseMouseRun(data);
|
|
186
|
-
return run && run.length === data.length ? run.events : null;
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
/**
|
|
190
|
-
* Issue #189: terminal *report* sequences, i.e. bytes the real terminal sends in answer to a query
|
|
191
|
-
* instead of in answer to the user. Every query this stack forwards to the real terminal (child
|
|
192
|
-
* OSC 11 probes, the host's colour batch, kitty protocol negotiation) is answered on the same stdin
|
|
193
|
-
* stream the user types on, so a report is routinely coalesced with keys or mouse reports into one
|
|
194
|
-
* read. Reports carry no user intent: they must reach the child unchanged, but they must not be
|
|
195
|
-
* treated as typing (no selection clear, no viewport reset).
|
|
196
|
-
*
|
|
197
|
-
* Grounding of each pattern (pi-tui 1.x, the face this extension runs against):
|
|
198
|
-
* - focus in/out: DECSET 1004, enabled by pi's fullscreen TUI; reachable in regular mode only
|
|
199
|
-
* through leaked terminal state.
|
|
200
|
-
* - `CSI ? ... c`: DA1 reply — the host sends `\x1b[c` as the trailing sentinel of its colour
|
|
201
|
-
* batch (`pi-tui tui.js:57`).
|
|
202
|
-
* - `CSI ? ... u`: kitty keyboard flag reply to `\x1b[?u` (`pi-tui terminal.js:210`).
|
|
203
|
-
* - `CSI ? ... n`: mode/status reports, including the colour-scheme report `\x1b[?997;1n` that Pi
|
|
204
|
-
* parses (`pi-tui terminal-colors.js:19`).
|
|
205
|
-
* - `CSI ... t`: XTWINOPS replies — the host asks for the cell size with `\x1b[16t`
|
|
206
|
-
* (`pi-tui tui.js:603`) and consumes `CSI 6;h;wt`.
|
|
207
|
-
* - `OSC ... BEL|ST`: colour replies (OSC 10/11/12, OSC 4) and other reply bodies such as the
|
|
208
|
-
* program-status reply (OSC 7501); bodies are capped at 4096 bytes like the query carry.
|
|
209
|
-
* - `CSI > ... c` (DA2) has **no** source in this stack — kept as a defensive extra only, because
|
|
210
|
-
* no keystroke encodes as a `>`-prefixed CSI.
|
|
211
|
-
*
|
|
212
|
-
* Deliberately NOT matched, because a keystroke can look like them: `CSI ... R` (cursor-position
|
|
213
|
-
* report but also modifier-F3), any `u` final without the `?` prefix (kitty key events),
|
|
214
|
-
* `CSI ... $ y` (DECRPM — nothing in this stack queries modes), and DCS replies (no in-repo DCS
|
|
215
|
-
* query). Unrecognised bytes keep today's keyboard semantics.
|
|
216
|
-
*/
|
|
217
|
-
const REPORT_PREFIXES = [
|
|
218
|
-
/^\x1b\[[IO]/,
|
|
219
|
-
/^\x1b\[\?\d+(?:;\d+)*c/,
|
|
220
|
-
/^\x1b\[>\d+(?:;\d+)*c/,
|
|
221
|
-
/^\x1b\[\?\d+(?:;\d+)*u/,
|
|
222
|
-
/^\x1b\[\?\d+(?:;\d+)*n/,
|
|
223
|
-
/^\x1b\[\d+(?:;\d+)*t/,
|
|
224
|
-
/^\x1b\][^\x07\x1b]{0,4096}(?:\x07|\x1b\\)/,
|
|
225
|
-
];
|
|
226
|
-
|
|
227
|
-
/** Issue #189: the complete report sequence starting at `offset`, or null. */
|
|
228
|
-
function parseReportPrefix(data, offset = 0) {
|
|
229
|
-
const input = data.slice(offset);
|
|
230
|
-
for (const pattern of REPORT_PREFIXES) {
|
|
231
|
-
const match = pattern.exec(input);
|
|
232
|
-
if (match) return match[0];
|
|
233
|
-
}
|
|
234
|
-
return null;
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
/** Issue #189: the maximal run of concatenated report sequences at `offset`, or null. */
|
|
238
|
-
function parseReportRun(data, offset = 0) {
|
|
239
|
-
let position = offset;
|
|
240
|
-
for (;;) {
|
|
241
|
-
const report = parseReportPrefix(data, position);
|
|
242
|
-
if (!report) break;
|
|
243
|
-
position += report.length;
|
|
244
|
-
}
|
|
245
|
-
return position > offset ? data.slice(offset, position) : null;
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
/** Merge a segment into the previous one when it has the same kind, else append it. */
|
|
249
|
-
function pushSegment(segments, segment) {
|
|
250
|
-
const last = segments[segments.length - 1];
|
|
251
|
-
if (last && last.kind === segment.kind) {
|
|
252
|
-
if (last.kind === "mouse") last.events.push(...segment.events);
|
|
253
|
-
else last.data += segment.data;
|
|
254
|
-
return;
|
|
255
|
-
}
|
|
256
|
-
segments.push(segment);
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
/**
|
|
260
|
-
* Issue #189: split one input read into ordered segments so the attach surface can dispatch per
|
|
261
|
-
* kind instead of all-or-nothing. Mouse reports, terminal reports and everything else are returned
|
|
262
|
-
* in arrival order, consecutive segments of the same kind merged.
|
|
263
|
-
*
|
|
264
|
-
* A partial escape sequence (a sequence split across reads) is not recognised and becomes keyboard
|
|
265
|
-
* text — the same bytes the all-or-nothing parser used to forward.
|
|
266
|
-
*
|
|
267
|
-
* @param {string} data
|
|
268
|
-
* @returns {Array<{kind: "mouse", events: Array<{length: number, raw: string, mouse: {encoding: string, button: number, col: number, row: number, action: string}}>} | {kind: "report", data: string} | {kind: "key", data: string}>}
|
|
269
|
-
*/
|
|
270
|
-
export function splitAttachInput(data) {
|
|
271
|
-
if (!data) return [];
|
|
272
|
-
const segments = [];
|
|
273
|
-
let offset = 0;
|
|
274
|
-
while (offset < data.length) {
|
|
275
|
-
const mouse = parseMouseRun(data, offset);
|
|
276
|
-
if (mouse) {
|
|
277
|
-
pushSegment(segments, { kind: "mouse", events: mouse.events });
|
|
278
|
-
offset += mouse.length;
|
|
279
|
-
continue;
|
|
280
|
-
}
|
|
281
|
-
const report = parseReportRun(data, offset);
|
|
282
|
-
if (report) {
|
|
283
|
-
pushSegment(segments, { kind: "report", data: report });
|
|
284
|
-
offset += report.length;
|
|
285
|
-
continue;
|
|
286
|
-
}
|
|
287
|
-
// Neither a mouse report nor a terminal report: keyboard text up to the next escape.
|
|
288
|
-
const next = data.indexOf("\x1b", offset + 1);
|
|
289
|
-
const end = next === -1 ? data.length : next;
|
|
290
|
-
pushSegment(segments, { kind: "key", data: data.slice(offset, end) });
|
|
291
|
-
offset = end;
|
|
292
|
-
}
|
|
293
|
-
return segments;
|
|
294
|
-
}
|
|
295
|
-
```
|
|
296
|
-
|
|
297
|
-
- [ ] **Step 6: Run the tests**
|
|
298
|
-
|
|
299
|
-
Run: `node --test test/pty-scroll.test.mjs`
|
|
300
|
-
Expected: PASS — the two new tests plus every pre-existing case (including the three
|
|
301
|
-
`parseMouseInputChunk` assertions) are green.
|
|
302
|
-
|
|
303
|
-
- [ ] **Step 7: Typecheck**
|
|
304
|
-
|
|
305
|
-
Run: `npm run typecheck`
|
|
306
|
-
Expected: exit 0 (the `.mjs` inference must keep `splitAttachInput`'s union narrowable).
|
|
307
|
-
|
|
308
|
-
- [ ] **Step 8: Commit**
|
|
309
|
-
|
|
310
|
-
```bash
|
|
311
|
-
git add src/core/pty-scroll.mjs test/pty-scroll.test.mjs
|
|
312
|
-
git commit -m "feat(attach): split legacy input reads into mouse/report/key segments (issue #189)"
|
|
313
|
-
```
|
|
314
|
-
|
|
315
|
-
---
|
|
316
|
-
|
|
317
|
-
### Task 2: Component dispatch + component-level scenario
|
|
318
|
-
|
|
319
|
-
Spec acceptance: **A3** (both orders of the issue's repro), **A4** (report-only reads), **A5**
|
|
320
|
-
(viewport/wheel on a scrollable buffer), **A6** (keyboard semantics + classifier boundary), **A9**
|
|
321
|
-
(mid-read detach guard).
|
|
322
|
-
|
|
323
|
-
**Files:**
|
|
324
|
-
- Modify: `src/ui/pty-attach.ts` (import line 15; `handleInput` at lines 297-376)
|
|
325
|
-
- Create: `test-support/pty-attach-mixed-input-scenario.ts`
|
|
326
|
-
- Create: `test/pty-attach-mixed-input.test.mjs`
|
|
327
|
-
|
|
328
|
-
**Interfaces:**
|
|
329
|
-
- Consumes: `splitAttachInput(data)` from Task 1 with the exact return shape documented there.
|
|
330
|
-
- Produces: `PtyAttachComponent.handleInput(data)` (public, unchanged signature) and a private
|
|
331
|
-
`handleKeyInput(data: string): void` holding the former keyboard chain.
|
|
332
|
-
|
|
333
|
-
- [ ] **Step 1: Write the failing component test**
|
|
334
|
-
|
|
335
|
-
Create `test/pty-attach-mixed-input.test.mjs`:
|
|
336
|
-
|
|
337
|
-
```js
|
|
338
|
-
import assert from "node:assert/strict";
|
|
339
|
-
import test from "node:test";
|
|
340
|
-
import { execFileSync } from "node:child_process";
|
|
341
|
-
import { fileURLToPath } from "node:url";
|
|
342
|
-
import { join } from "node:path";
|
|
343
|
-
|
|
344
|
-
const ROOT_DIR = fileURLToPath(new URL("../", import.meta.url));
|
|
345
|
-
const SCENARIO = join(ROOT_DIR, "test-support", "pty-attach-mixed-input-scenario.ts");
|
|
346
|
-
|
|
347
|
-
// Component-level coverage (issue #189): the REAL PtyAttachComponent against a REAL runner socket
|
|
348
|
-
// and a fake PTY. Asserts that a read mixing mouse reports with terminal reports is dispatched per
|
|
349
|
-
// segment: the gesture still copies (OSC 52) and keeps its selection, no raw mouse bytes reach the
|
|
350
|
-
// child, a spontaneous report clears neither selection nor viewport, and the mid-read detach guard
|
|
351
|
-
// stops the rest of the read.
|
|
352
|
-
test("attach mixed input: per-segment dispatch keeps copy/selection/viewport; reports pass through", () => {
|
|
353
|
-
const out = execFileSync(process.execPath, ["--experimental-transform-types", SCENARIO], {
|
|
354
|
-
encoding: "utf8",
|
|
355
|
-
timeout: 120_000,
|
|
356
|
-
});
|
|
357
|
-
const parsed = JSON.parse(out.trim().split("\n").filter(Boolean).pop());
|
|
358
|
-
assert.equal(parsed.error, null, `scenario error: ${parsed.error}`);
|
|
359
|
-
for (const [name, passed] of Object.entries(parsed.checks)) {
|
|
360
|
-
assert.equal(passed, true, `check failed: ${name}`);
|
|
361
|
-
}
|
|
362
|
-
});
|
|
363
|
-
```
|
|
364
|
-
|
|
365
|
-
- [ ] **Step 2: Run it and watch it fail**
|
|
366
|
-
|
|
367
|
-
Run: `node --test test/pty-attach-mixed-input.test.mjs`
|
|
368
|
-
Expected: FAIL — the scenario file does not exist yet (`Cannot find module ... pty-attach-mixed-input-scenario.ts`).
|
|
369
|
-
|
|
370
|
-
- [ ] **Step 3: Write the scenario harness**
|
|
371
|
-
|
|
372
|
-
Create `test-support/pty-attach-mixed-input-scenario.ts`:
|
|
373
|
-
|
|
374
|
-
```ts
|
|
375
|
-
// Component-level coverage (issue #189): one input read that mixes mouse reports with terminal
|
|
376
|
-
// reports must be dispatched per segment — the gesture is still processed (OSC 52 copy + selection
|
|
377
|
-
// kept), no raw mouse bytes reach the child, and a spontaneous report clears neither the selection
|
|
378
|
-
// nor the viewport. Modelled on test-support/pty-attach-mouse-scenario.ts (real runner socket, fake
|
|
379
|
-
// PTY from test-support/fake-pty-pi.mjs, fake TUI capturing terminal.write).
|
|
380
|
-
// Run via `node --experimental-transform-types`; prints one JSON line: { error, checks }.
|
|
381
|
-
import { chmodSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
382
|
-
import { Socket } from "node:net";
|
|
383
|
-
import { tmpdir } from "node:os";
|
|
384
|
-
import { join, resolve } from "node:path";
|
|
385
|
-
import { spawn } from "node:child_process";
|
|
386
|
-
import { atomicWriteJson } from "../src/core/atomic.mjs";
|
|
387
|
-
import * as P from "../src/core/paths.mjs";
|
|
388
|
-
import { createView, readHost } from "../src/core/store.mjs";
|
|
389
|
-
import { PtyAttachComponent } from "../src/ui/pty-attach.ts";
|
|
390
|
-
|
|
391
|
-
const root = mkdtempSync(join(tmpdir(), "agentview-attach-mixed-"));
|
|
392
|
-
const viewId = "mixed-e2e";
|
|
393
|
-
const SELECTED_TEXT = "fake pi ready";
|
|
394
|
-
const OSC52_COPY = `\x1b]52;c;${Buffer.from(SELECTED_TEXT, "utf8").toString("base64")}\x07`;
|
|
395
|
-
const OSC11_REPLY = "\x1b]11;rgb:0d0d/0d0d/0d0d\x07";
|
|
396
|
-
|
|
397
|
-
// Hermetic xclip stub: the scenario must never touch the host clipboard.
|
|
398
|
-
const binDir = join(root, "bin");
|
|
399
|
-
mkdirSync(binDir);
|
|
400
|
-
writeFileSync(join(binDir, "xclip"), "#!/bin/sh\nexit 0\n");
|
|
401
|
-
chmodSync(join(binDir, "xclip"), 0o755);
|
|
402
|
-
process.env.PATH = `${binDir}:${process.env.PATH}`;
|
|
403
|
-
|
|
404
|
-
function isAlive(pid: number): boolean {
|
|
405
|
-
try {
|
|
406
|
-
process.kill(pid, 0);
|
|
407
|
-
return true;
|
|
408
|
-
} catch {
|
|
409
|
-
return false;
|
|
410
|
-
}
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
const sleep = (ms: number) => new Promise<void>((r) => setTimeout(r, ms));
|
|
414
|
-
|
|
415
|
-
async function waitFor(predicate: () => boolean, timeoutMs: number): Promise<boolean> {
|
|
416
|
-
const start = Date.now();
|
|
417
|
-
while (Date.now() - start < timeoutMs) {
|
|
418
|
-
if (predicate()) return true;
|
|
419
|
-
await sleep(25);
|
|
420
|
-
}
|
|
421
|
-
return false;
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
const meta = createView(root, { id: viewId, name: "mixed attach e2e", cwd: root });
|
|
425
|
-
const configPath = P.hostConfigPath(root, viewId);
|
|
426
|
-
atomicWriteJson(configPath, {
|
|
427
|
-
root,
|
|
428
|
-
viewId,
|
|
429
|
-
sessionFile: meta.sessionFile,
|
|
430
|
-
cwd: process.cwd(),
|
|
431
|
-
initialPrompt: null,
|
|
432
|
-
piCommand: process.execPath,
|
|
433
|
-
piArgsPrefix: [resolve("test-support/fake-pty-pi.mjs")],
|
|
434
|
-
model: null,
|
|
435
|
-
tools: null,
|
|
436
|
-
env: { AGENT_BOARD_ALLOW_PIPE_FALLBACK: "1" },
|
|
437
|
-
cols: 80,
|
|
438
|
-
rows: 22,
|
|
439
|
-
});
|
|
440
|
-
|
|
441
|
-
const runner = spawn(process.execPath, [resolve("runner/pty-runner.mjs"), configPath], {
|
|
442
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
443
|
-
});
|
|
444
|
-
runner.stderr.resume();
|
|
445
|
-
|
|
446
|
-
const checks: Record<string, boolean> = {};
|
|
447
|
-
const result = { error: null as string | null, checks };
|
|
448
|
-
|
|
449
|
-
try {
|
|
450
|
-
const ready = await waitFor(() => {
|
|
451
|
-
const host = readHost(root, viewId);
|
|
452
|
-
return !!host && host.state === "alive" && !!host.socketPath && !!host.childPid && isAlive(host.runnerPid) && isAlive(host.childPid);
|
|
453
|
-
}, 10_000);
|
|
454
|
-
if (!ready) throw new Error("host never became ready");
|
|
455
|
-
|
|
456
|
-
// Observe component→runner frames so forwarded input is visible.
|
|
457
|
-
const sent: string[] = [];
|
|
458
|
-
const socketProto = Socket.prototype as any;
|
|
459
|
-
const origWrite = socketProto.write;
|
|
460
|
-
socketProto.write = function (data: any, ...rest: any[]) {
|
|
461
|
-
if (typeof data === "string") sent.push(data);
|
|
462
|
-
return origWrite.call(this, data, ...rest);
|
|
463
|
-
};
|
|
464
|
-
const forwardedInput = (): string[] =>
|
|
465
|
-
sent
|
|
466
|
-
.map((line) => {
|
|
467
|
-
try {
|
|
468
|
-
return JSON.parse(line);
|
|
469
|
-
} catch {
|
|
470
|
-
return null;
|
|
471
|
-
}
|
|
472
|
-
})
|
|
473
|
-
.filter((msg) => !!msg && msg.type === "input" && typeof msg.data === "string")
|
|
474
|
-
.map((msg) => msg.data as string);
|
|
475
|
-
|
|
476
|
-
// Observe terminal writes so OSC 52 copies are visible.
|
|
477
|
-
const writes: string[] = [];
|
|
478
|
-
const tui = {
|
|
479
|
-
terminal: { rows: 24, cols: 80, columns: 80, write: (data: string) => writes.push(data) },
|
|
480
|
-
requestRender: () => {},
|
|
481
|
-
};
|
|
482
|
-
const theme = { fg: (_color: string, text: string) => text, bold: (text: string) => text };
|
|
483
|
-
const component: any = new PtyAttachComponent(
|
|
484
|
-
tui as never,
|
|
485
|
-
theme,
|
|
486
|
-
{} as never,
|
|
487
|
-
() => {},
|
|
488
|
-
{
|
|
489
|
-
socketPath: readHost(root, viewId).socketPath,
|
|
490
|
-
screenLogPath: P.screenLogPath(root, viewId),
|
|
491
|
-
title: "mixed attach e2e",
|
|
492
|
-
} as never,
|
|
493
|
-
);
|
|
494
|
-
|
|
495
|
-
const rendered = () => (component.render(80) ?? []).join("\n");
|
|
496
|
-
const firstBurstLine = (): number | null => {
|
|
497
|
-
const match = /burstline-(\d{4})/.exec(rendered());
|
|
498
|
-
return match ? Number(match[1]) : null;
|
|
499
|
-
};
|
|
500
|
-
const copies = () => writes.filter((write) => write === OSC52_COPY).length;
|
|
501
|
-
const selectionPresent = () => component.selection !== null;
|
|
502
|
-
const drag = (fromCol: number, toCol: number) => {
|
|
503
|
-
component.handleInput(`\x1b[<0;${fromCol};2M`);
|
|
504
|
-
component.handleInput(`\x1b[<32;${toCol};2M`);
|
|
505
|
-
};
|
|
506
|
-
|
|
507
|
-
checks.sawContent = await waitFor(() => rendered().includes(SELECTED_TEXT), 8000);
|
|
508
|
-
if (!checks.sawContent) throw new Error("session content never rendered");
|
|
509
|
-
|
|
510
|
-
// A3 control: a canonical drag in separate reads still copies.
|
|
511
|
-
const controlCopies = copies();
|
|
512
|
-
drag(1, 16);
|
|
513
|
-
component.handleInput("\x1b[<0;16;2m");
|
|
514
|
-
await waitFor(() => copies() > controlCopies, 3000);
|
|
515
|
-
checks.controlCopies = copies() === controlCopies + 1;
|
|
516
|
-
|
|
517
|
-
// A3 case 1 (the issue's repro): release + focus report coalesced into ONE read.
|
|
518
|
-
const mixedCopies = copies();
|
|
519
|
-
const mixedFrames = forwardedInput().length;
|
|
520
|
-
drag(1, 16);
|
|
521
|
-
component.handleInput("\x1b[<0;16;2m\x1b[I");
|
|
522
|
-
await waitFor(() => copies() > mixedCopies, 3000);
|
|
523
|
-
checks.mixedCopies = copies() === mixedCopies + 1;
|
|
524
|
-
checks.mixedForwardedOnlyReport = forwardedInput().slice(mixedFrames).every((frame) => frame === "\x1b[I");
|
|
525
|
-
// The pending-click window (DOUBLE_CLICK_MS = 260) is what used to wipe the selection: nothing to
|
|
526
|
-
// await here, so this wait is the assertion's own window.
|
|
527
|
-
await sleep(400);
|
|
528
|
-
checks.selectionSurvivesMixed = selectionPresent();
|
|
529
|
-
|
|
530
|
-
// A3 case 2: report FIRST, release after, still ONE read.
|
|
531
|
-
const reportFirstCopies = copies();
|
|
532
|
-
const reportFirstFrames = forwardedInput().length;
|
|
533
|
-
drag(1, 16);
|
|
534
|
-
component.handleInput("\x1b[I\x1b[<0;16;2m");
|
|
535
|
-
await waitFor(() => copies() > reportFirstCopies, 3000);
|
|
536
|
-
checks.reportFirstCopies = copies() === reportFirstCopies + 1;
|
|
537
|
-
checks.reportFirstForwardedOnlyReport = forwardedInput().slice(reportFirstFrames).every((frame) => frame === "\x1b[I");
|
|
538
|
-
|
|
539
|
-
// A4: a report alone mid-drag keeps the selection and copies nothing.
|
|
540
|
-
const reportOnlyCopies = copies();
|
|
541
|
-
const reportOnlyFrames = forwardedInput().length;
|
|
542
|
-
drag(1, 16);
|
|
543
|
-
component.handleInput("\x1b[I");
|
|
544
|
-
await sleep(150);
|
|
545
|
-
checks.reportOnlyKeepsSelection = selectionPresent();
|
|
546
|
-
checks.reportOnlyNoCopy = copies() === reportOnlyCopies;
|
|
547
|
-
checks.reportOnlyForwarded = forwardedInput().slice(reportOnlyFrames).every((frame) => frame === "\x1b[I");
|
|
548
|
-
|
|
549
|
-
// A4 variant: an OSC 11 reply (an answer to a forwarded child query) behaves the same.
|
|
550
|
-
const oscCopies = copies();
|
|
551
|
-
const oscFrames = forwardedInput().length;
|
|
552
|
-
drag(1, 16);
|
|
553
|
-
component.handleInput(OSC11_REPLY);
|
|
554
|
-
await sleep(150);
|
|
555
|
-
checks.oscReplyKeepsSelection = selectionPresent();
|
|
556
|
-
checks.oscReplyNoCopy = copies() === oscCopies;
|
|
557
|
-
checks.oscReplyForwarded = forwardedInput().slice(oscFrames).every((frame) => frame === OSC11_REPLY);
|
|
558
|
-
|
|
559
|
-
// A5: viewport semantics on a scrollable buffer. "burst" makes the fixture emit 200 numbered lines.
|
|
560
|
-
component.handleInput("burst");
|
|
561
|
-
await waitFor(() => firstBurstLine() === 200, 10_000);
|
|
562
|
-
const settle = async (): Promise<number | null> => {
|
|
563
|
-
let last = firstBurstLine();
|
|
564
|
-
for (;;) {
|
|
565
|
-
await sleep(60);
|
|
566
|
-
const now = firstBurstLine();
|
|
567
|
-
if (now === last) return now;
|
|
568
|
-
last = now;
|
|
569
|
-
}
|
|
570
|
-
};
|
|
571
|
-
const atBottom = await settle();
|
|
572
|
-
for (let i = 0; i < 3; i++) component.handleInput("\x1b[<64;10;3M");
|
|
573
|
-
const scrolled = (await waitFor(() => firstBurstLine() !== null && firstBurstLine()! < (atBottom ?? 0), 3000)) ? firstBurstLine() : null;
|
|
574
|
-
component.handleInput("\x1b[I");
|
|
575
|
-
// Nothing to await: the assertion is that a spontaneous report moves nothing.
|
|
576
|
-
await sleep(150);
|
|
577
|
-
checks.viewportKeptOnReport = scrolled !== null && firstBurstLine() === scrolled;
|
|
578
|
-
|
|
579
|
-
const wheelFrames = forwardedInput().length;
|
|
580
|
-
const beforeWheel = firstBurstLine();
|
|
581
|
-
component.handleInput("\x1b[<64;10;3M\x1b[I");
|
|
582
|
-
checks.wheelScrolledInMixedRead = await waitFor(() => firstBurstLine() !== null && firstBurstLine()! < (beforeWheel ?? 0), 3000);
|
|
583
|
-
checks.wheelMixedForwardedOnlyReport = forwardedInput().slice(wheelFrames).every((frame) => frame === "\x1b[I");
|
|
584
|
-
|
|
585
|
-
// A6: a real key still clears the selection (typing semantics unchanged) …
|
|
586
|
-
const keyCopies = copies();
|
|
587
|
-
drag(1, 16);
|
|
588
|
-
component.handleInput("x");
|
|
589
|
-
await sleep(80);
|
|
590
|
-
checks.keyStillClears = !selectionPresent() && copies() === keyCopies;
|
|
591
|
-
|
|
592
|
-
// … and ambiguous escape shapes stay keys (the classifier boundary).
|
|
593
|
-
drag(1, 16);
|
|
594
|
-
component.handleInput("\x1b[1;2R");
|
|
595
|
-
await sleep(80);
|
|
596
|
-
const clearedByShiftF3 = !selectionPresent();
|
|
597
|
-
drag(1, 16);
|
|
598
|
-
component.handleInput("\x1b[97;5u");
|
|
599
|
-
await sleep(80);
|
|
600
|
-
checks.ambiguousEscapesStayKeys = clearedByShiftF3 && !selectionPresent() && copies() === keyCopies;
|
|
601
|
-
|
|
602
|
-
// A9 (last: it closes the component): a detach chord matched in a mixed read must stop the rest
|
|
603
|
-
// of that read. The trailing segment is a KEY, not a report: once detach() closes the socket a
|
|
604
|
-
// trailing report's send() is a silent no-op, so only a key can prove the loop stopped (it would
|
|
605
|
-
// otherwise clearSelection() and wipe the highlight). The read is three segments — detach chord /
|
|
606
|
-
// focus report / trailing key — because adjacent key runs merge: a bare `"\x1b[1;5D" + "x"` is
|
|
607
|
-
// ONE key segment `"\x1b[1;5Dx"` and the chord would never match.
|
|
608
|
-
drag(1, 16);
|
|
609
|
-
component.handleInput("\x1b[1;5D" + "\x1b[I" + "x");
|
|
610
|
-
await sleep(150);
|
|
611
|
-
checks.detachStopsRead = component.closed === true;
|
|
612
|
-
checks.detachTrailingSegmentStopped = selectionPresent();
|
|
613
|
-
|
|
614
|
-
try {
|
|
615
|
-
component.dispose();
|
|
616
|
-
} catch {}
|
|
617
|
-
} catch (err) {
|
|
618
|
-
result.error = err instanceof Error ? err.message : String(err);
|
|
619
|
-
}
|
|
620
|
-
|
|
621
|
-
try {
|
|
622
|
-
const pid = readHost(root, viewId)?.childPid;
|
|
623
|
-
if (pid) process.kill(pid, "SIGKILL");
|
|
624
|
-
} catch {}
|
|
625
|
-
try {
|
|
626
|
-
runner.kill("SIGKILL");
|
|
627
|
-
} catch {}
|
|
628
|
-
await sleep(50);
|
|
629
|
-
try {
|
|
630
|
-
rmSync(root, { recursive: true, force: true, maxRetries: 3, retryDelay: 30 });
|
|
631
|
-
} catch {}
|
|
632
|
-
process.stdout.write(JSON.stringify(result) + "\n");
|
|
633
|
-
process.exit(0);
|
|
634
|
-
```
|
|
635
|
-
|
|
636
|
-
- [ ] **Step 4: Run the scenario to confirm it fails on the current code**
|
|
637
|
-
|
|
638
|
-
Run: `node --test test/pty-attach-mixed-input.test.mjs`
|
|
639
|
-
Expected: FAIL with the failing checks (`mixedCopies`, `reportFirstCopies`, `selectionSurvivesMixed`,
|
|
640
|
-
`reportOnlyKeepsSelection`, `oscReplyKeepsSelection`, `viewportKeptOnReport`,
|
|
641
|
-
`wheelScrolledInMixedRead`) — that is the reproduced defect on `f81949d`.
|
|
642
|
-
|
|
643
|
-
- [ ] **Step 5: Implement the dispatch loop**
|
|
644
|
-
|
|
645
|
-
In `src/ui/pty-attach.ts`:
|
|
646
|
-
|
|
647
|
-
(a) Line 15 — swap the import (`splitAttachInput` replaces `parseMouseInputChunk`):
|
|
648
|
-
|
|
649
|
-
```ts
|
|
650
|
-
import { clampInt, resolveAttachMouseAction, resolvePasteSelectionSource, resolveWheelLines, scrollViewportTop, selectionDragScrollLines, shouldMirrorSelectionToPrimary, shouldOwnOuterMouseMode, splitAttachInput, wheelScrollLinesUp } from "../core/pty-scroll.mjs";
|
|
651
|
-
```
|
|
652
|
-
|
|
653
|
-
(b) Replace the first two statements of `handleInput` (lines 297-298) with the dispatch loop, and
|
|
654
|
-
open the extracted keyboard handler — the rest of the old body (every `matchesKey` branch, including
|
|
655
|
-
the `detach()` early returns and the final default branch) stays **verbatim** inside it:
|
|
656
|
-
|
|
657
|
-
```ts
|
|
658
|
-
handleInput(data: string): void {
|
|
659
|
-
// Issue #189: dispatch per segment instead of all-or-nothing. A read that mixes mouse
|
|
660
|
-
// reports with other bytes used to fall through to the keyboard branch as a whole: the
|
|
661
|
-
// gesture was dropped, an in-progress selection cleared, the viewport reset, and the raw
|
|
662
|
-
// mouse bytes forwarded into the hosted session as typed input.
|
|
663
|
-
//
|
|
664
|
-
// Issue #170/#194 composition (post-merge with main): the inbound reply guard runs per
|
|
665
|
-
// non-mouse segment, so a recognized report can never absorb keys coalesced into the same
|
|
666
|
-
// read (a still-fragmented head keeps the bounded #194 absorption: 64 bytes / 250ms).
|
|
667
|
-
for (const segment of splitAttachInput(data)) {
|
|
668
|
-
if (segment.kind === "mouse") {
|
|
669
|
-
this.handleMouseInputChunk(segment.events);
|
|
670
|
-
} else if (!this.consumeByReplyGuard(segment.data)) {
|
|
671
|
-
if (segment.kind === "report") this.send({ type: "input", data: segment.data });
|
|
672
|
-
else this.handleKeyInput(segment.data);
|
|
673
|
-
}
|
|
674
|
-
// A detach/close inside a segment must stop the rest of this read.
|
|
675
|
-
if (this.closed) return;
|
|
676
|
-
}
|
|
677
|
-
}
|
|
678
|
-
|
|
679
|
-
/** Issue #170/#194: run the inbound reply guard over one non-mouse segment. Returns true when
|
|
680
|
-
* the guard took it (complete protocol reply dropped / colour reply forwarded whole). */
|
|
681
|
-
private consumeByReplyGuard(data: string): boolean {
|
|
682
|
-
const guard = this.replyGuard;
|
|
683
|
-
if (!guard) return false;
|
|
684
|
-
const decision = guard.feed(data, this.nowFn());
|
|
685
|
-
if (guard.pendingSince() !== null) this.armReplyGuardTimer();
|
|
686
|
-
if (decision.action === "forward") {
|
|
687
|
-
this.send({ type: "input", data: decision.data });
|
|
688
|
-
return true;
|
|
689
|
-
}
|
|
690
|
-
return decision.action === "swallow";
|
|
691
|
-
}
|
|
692
|
-
|
|
693
|
-
/** Keyboard branch of `handleInput` (issue #189: one segment of the read at a time). Terminal
|
|
694
|
-
* *reports* never arrive here — `handleInput` forwards them without touching the selection or
|
|
695
|
-
* the viewport, because they carry no user intent (focus events, query replies). */
|
|
696
|
-
private handleKeyInput(data: string): void {
|
|
697
|
-
if (matchesKey(data, Key.pageUp)) {
|
|
698
|
-
```
|
|
699
|
-
|
|
700
|
-
The report path is deliberately just a guard pass-through plus `send(...)`: no `clearPendingClick()`,
|
|
701
|
-
no `clearSelection()`, no `scrollToBottom()` — that omission is the fix for the second half of the
|
|
702
|
-
issue (spec §3.2). Complete protocol replies stay dropped by the #194 guard and colour replies are
|
|
703
|
-
forwarded whole by it; #189 only changes *what* the guard is fed (one segment at a time).
|
|
704
|
-
|
|
705
|
-
- [ ] **Step 6: Run the component test and the unit tests**
|
|
706
|
-
|
|
707
|
-
Run: `node --test test/pty-attach-mixed-input.test.mjs test/pty-scroll.test.mjs`
|
|
708
|
-
Expected: PASS — every check in the scenario plus both new unit blocks.
|
|
709
|
-
|
|
710
|
-
- [ ] **Step 7: Run the pre-existing attach suites (regression)**
|
|
711
|
-
|
|
712
|
-
Run: `node --test test/pty-attach-mouse.test.mjs test/pty-attach-wheel.test.mjs test/pty-attach-selection-mirror.test.mjs test/pty-attach-mouse-mode.test.mjs`
|
|
713
|
-
Expected: PASS (these cover #167 middle-click dispatch, the wheel, #174 PRIMARY mirroring and #173
|
|
714
|
-
mouse-mode ownership through the same seams).
|
|
715
|
-
|
|
716
|
-
- [ ] **Step 8: Typecheck**
|
|
717
|
-
|
|
718
|
-
Run: `npm run typecheck`
|
|
719
|
-
Expected: exit 0.
|
|
720
|
-
|
|
721
|
-
- [ ] **Step 9: Commit**
|
|
722
|
-
|
|
723
|
-
```bash
|
|
724
|
-
git add src/ui/pty-attach.ts test-support/pty-attach-mixed-input-scenario.ts test/pty-attach-mixed-input.test.mjs
|
|
725
|
-
git commit -m "fix(attach): dispatch mixed input reads per segment and pass terminal reports through (issue #189)"
|
|
726
|
-
```
|
|
727
|
-
|
|
728
|
-
---
|
|
729
|
-
|
|
730
|
-
### Task 3: Full verification and traceability sweep
|
|
731
|
-
|
|
732
|
-
Spec acceptance: **A7** (typecheck/build/pack), **A8** (no regression across the repo).
|
|
733
|
-
|
|
734
|
-
**Files:** none (verification only).
|
|
735
|
-
|
|
736
|
-
- [ ] **Step 1: Run the full repo verification**
|
|
737
|
-
|
|
738
|
-
Run: `npm run verify`
|
|
739
|
-
Expected: exit 0 — typecheck, perf gate, `node --test test/*.test.mjs`, coverage thresholds
|
|
740
|
-
(lines 85 / funcs 80 / branches 70), `pack:dry`.
|
|
741
|
-
|
|
742
|
-
- [ ] **Step 2: Confirm the coverage numbers**
|
|
743
|
-
|
|
744
|
-
Run: `npm run test:coverage 2>&1 | tail -20`
|
|
745
|
-
Expected: thresholds met; `src/core/pty-scroll.mjs` and `src/ui/pty-attach.ts` do not drop below the
|
|
746
|
-
configured floors (if they do, extend the unit cases — never lower the thresholds).
|
|
747
|
-
|
|
748
|
-
- [ ] **Step 3: Byte-level spec/plan traceability sweep**
|
|
749
|
-
|
|
750
|
-
Run:
|
|
751
|
-
|
|
752
|
-
```bash
|
|
753
|
-
grep -n "splitAttachInput\|handleKeyInput" src/core/pty-scroll.mjs src/ui/pty-attach.ts
|
|
754
|
-
grep -n "is not recognised and becomes keyboard text\|all-or-nothing contract" src/core/pty-scroll.mjs
|
|
755
|
-
```
|
|
756
|
-
|
|
757
|
-
Expected: `splitAttachInput` exported exactly once and imported once by the component;
|
|
758
|
-
`parseMouseInputChunk` still present with its all-or-nothing doc comment; no other call site changed.
|
|
759
|
-
|
|
760
|
-
- [ ] **Step 4: Re-read the spec's acceptance matrix and record the evidence**
|
|
761
|
-
|
|
762
|
-
For every row A1–A9 write down the command that produced the evidence and its result in the issue
|
|
763
|
-
comment prepared for step 9/10 (spec §6). `U1`/`U2` stay pending until Task 4 runs.
|
|
764
|
-
|
|
765
|
-
- [ ] **Step 5: Commit (only if Step 1-3 forced changes)**
|
|
766
|
-
|
|
767
|
-
```bash
|
|
768
|
-
git add -p # stage only the fixes made in this task
|
|
769
|
-
git commit -m "test(attach): keep the segment coverage at the configured floors (issue #189)"
|
|
770
|
-
```
|
|
771
|
-
|
|
772
|
-
---
|
|
773
|
-
|
|
774
|
-
### Task 4: Manual verification on a real terminal (post-merge, operator-run)
|
|
775
|
-
|
|
776
|
-
Spec acceptance: **U1** (byte injection in regular TUI mode), **U2** (optional daily-use check).
|
|
777
|
-
|
|
778
|
-
This task runs **after** the PR is merged (the board loads from a git package checkout, so a
|
|
779
|
-
pre-merge worktree would not be the loaded code). It is the operator's step; the agent prepares the
|
|
780
|
-
commands and records the result — it must be reported as `pending` until actually executed.
|
|
781
|
-
|
|
782
|
-
- [ ] **Step 1: Deploy the merged code into the loaded package**
|
|
783
|
-
|
|
784
|
-
```bash
|
|
785
|
-
git -C ~/.pi/agent/git/github.com/zhuxixi/pi-agent-board stash # local package-lock.json change
|
|
786
|
-
git -C ~/.pi/agent/git/github.com/zhuxixi/pi-agent-board pull
|
|
787
|
-
git -C ~/.pi/agent/git/github.com/zhuxixi/pi-agent-board log --oneline -1
|
|
788
|
-
```
|
|
789
|
-
|
|
790
|
-
Then restart `pi --tui-mode regular` (the legacy path only runs in regular TUI mode).
|
|
791
|
-
|
|
792
|
-
- [ ] **Step 2: Inject the exact mixed byte stream inside tmux**
|
|
793
|
-
|
|
794
|
-
```bash
|
|
795
|
-
tmux send-keys -H 1b 5b 3c 30 3b 31 3b 32 4d # press (col 1, row 2)
|
|
796
|
-
tmux send-keys -H 1b 5b 3c 33 32 3b 38 3b 32 4d # move (col 8, row 2)
|
|
797
|
-
tmux send-keys -H 1b 5b 3c 30 3b 31 36 3b 32 6d 1b 5b 49 # release + focus-in, ONE frame
|
|
798
|
-
xclip -o -selection primary
|
|
799
|
-
```
|
|
800
|
-
|
|
801
|
-
Expected: the highlight from the release survives until the next click (#172 contract), the selected
|
|
802
|
-
text (`fake pi ready`) is in the clipboard/PRIMARY, and the hosted session shows no echoed mouse
|
|
803
|
-
bytes. If tmux splits the third injection into several reads, record that honestly and mark the row
|
|
804
|
-
`pending` — A3 (component level) remains the primary evidence.
|
|
805
|
-
|
|
806
|
-
- [ ] **Step 3: Record U1/U2 in the issue**
|
|
807
|
-
|
|
808
|
-
Comment the observed result (pass/fail/pending with the exact commands and outputs). U2 (daily use:
|
|
809
|
-
a selection is no longer wiped by a spontaneous report) is optional and non-blocking; report it as
|
|
810
|
-
`pending` if not exercised.
|
|
811
|
-
|
|
812
|
-
---
|
|
813
|
-
|
|
814
|
-
## Self-review checklist (run before handing the plan to implementers)
|
|
815
|
-
|
|
816
|
-
- **Spec coverage:** A1/A2 → Task 1; A3–A6/A9 → Task 2; A7/A8 → Task 3; U1/U2 → Task 4. Spec §3.1
|
|
817
|
-
(pure layer) → Task 1 Steps 1-8; §3.2 (component wiring) → Task 2 Steps 5-9; §7 testability split →
|
|
818
|
-
Global Constraints + both test files; §8 boundaries → Global Constraints (no `parseMouseEventPrefix`
|
|
819
|
-
/ `handleLocalMouseEvent` edits).
|
|
820
|
-
- **Placeholder scan:** every step carries real code or a real command with an expected result.
|
|
821
|
-
- **Type consistency:** `splitAttachInput`'s `{kind, events|data}` discriminated union is used
|
|
822
|
-
identically in the JSDoc (Task 1), the dispatch loop (Task 2 Step 5) and the unit tests
|
|
823
|
-
(Task 1 Step 1).
|