@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,224 +0,0 @@
|
|
|
1
|
-
# Spec: attach 会话内就地删除(issue #147)
|
|
2
|
-
|
|
3
|
-
Drafted: deepseek/deepseek-flash (selected) · deepseek/deepseek-flash (physical) @ 2026-10-09T23:09:00+08:00
|
|
4
|
-
Revised: deepseek/deepseek-flash (selected) · deepseek/deepseek-flash (physical) @ 2026-10-10T22:04:00+08:00
|
|
5
|
-
Revised: deepseek/deepseek-flash (selected) · deepseek/deepseek-flash (physical) @ 2026-10-10T22:12:00+08:00
|
|
6
|
-
Revised: deepseek/deepseek-flash (selected) · deepseek/deepseek-flash (physical) @ 2026-10-10T22:25:00+08:00
|
|
7
|
-
|
|
8
|
-
> 状态:v4 —— 第 3 轮复核收敛(真缺口 0、细化 2 已处置)+ **用户裁决两项开放问题已写入**;待用户确认设计
|
|
9
|
-
> **行号基准:`884040f`(0.10.2)**——`pty-attach.ts` 随 main 前进(0.10.1 → 0.10.2,issue #167/#169 新增 `handleMouse`),
|
|
10
|
-
> 全文行号已按新基准刷新;`agent-board.ts`、`dashboard.ts`、`service.mjs`、`core/*` 在该区间无 diff。
|
|
11
|
-
> 依据:issue #147 正文 + 三轮调研(`research/round1-key-choice-and-occupancy.md`、
|
|
12
|
-
> `round2-delete-path-and-return-channel.md`、`round3-ui-surface-and-verification.md`)
|
|
13
|
-
> + 2026-10-09 用户拍板(键位 + 四个设计点:顶栏整行顶替 / 非确认键取消并透传 / 断连无条件可用 / 选中落相邻行)
|
|
14
|
-
> + `research/spec-review-round-1.md`、`research/spec-review-round-2.md`(各轮发现的处置见对应修订摘要)
|
|
15
|
-
|
|
16
|
-
## 1. 目标与非目标
|
|
17
|
-
|
|
18
|
-
### 1.1 目标
|
|
19
|
-
|
|
20
|
-
1. attach 视图内一个手势完成「退出 attach → 归档该行(停 host、停 run)→ 回 board」。
|
|
21
|
-
2. 删除语义与 board 完全一致:同一 `service.archive()`,软删除(session 文件保留)、busy 行走协调器 fence。
|
|
22
|
-
3. 回到 board 后:被删行消失、有删除提示、选中落在相邻行。
|
|
23
|
-
|
|
24
|
-
### 1.2 非目标
|
|
25
|
-
|
|
26
|
-
- 不改 board 侧键位与语义(`x` → `y` 与 `Ctrl+X` ×2 均不动)。
|
|
27
|
-
- 不做 `ctx.switchSession`(PTY 不可用)路径的删除——那条路径下「要删的正是用户当前所在的会话」,语义不同,单独讨论。
|
|
28
|
-
- 不给 attach 增加 `Ctrl+X` ×2 别名(避免第三种删除路径;attach 从无删除键,无旧肌肉记忆要照顾)。
|
|
29
|
-
- 不改 `service.archive` / `archiveView` / 状态协调器。
|
|
30
|
-
- 不同步孤儿 `src/commands/attach-flow.ts`(属 `REMAINING_WORK.md:102` T14;其现有映射 `result.action === "closed" ? "closed" : "detached"` 对新态天然落到 `detached`,不产生错误行为)。
|
|
31
|
-
- 不做键位可配置化。
|
|
32
|
-
|
|
33
|
-
## 2. 现状审计(实现时按此定位)
|
|
34
|
-
|
|
35
|
-
| 事实 | 位置 |
|
|
36
|
-
| --- | --- |
|
|
37
|
-
| attach 按键拦截链(mouse → pageUp/Down/Home/End → `Ctrl+\` → `Ctrl+←` → `←` 门控 → 兜底透传) | `src/ui/pty-attach.ts:289` |
|
|
38
|
-
| 鼠标事件的**第二通道**(fullscreen 模式走规范化 `TuiMouseEvent`,**不进** `handleInput`) | `src/ui/pty-attach.ts:376-385`(issue #167/#169,0.10.2 新增) |
|
|
39
|
-
| 顶部状态行渲染(标题 + `status · 键位提示`) | `src/ui/pty-attach.ts:399-401` |
|
|
40
|
-
| 结果类型(现为 `detached` \| `closed`)与 `done` 回调 | `src/ui/pty-attach.ts:17`、`:265` |
|
|
41
|
-
| 线上消费点(唯一) | `src/commands/agent-board.ts:187-192`(`resolveAttachTarget` → `openPtyAttach`) |
|
|
42
|
-
| attach 循环(重开 board、带 `selectedId`) | `src/commands/agent-board.ts:139-161` |
|
|
43
|
-
| `service.archive` → `archiveView` | `src/runtime/service.mjs:2088` → `:753-779` |
|
|
44
|
-
| board 确认引擎(`y`/`Y`/Enter 确认,其它键取消) | `src/ui/dashboard.ts:509` |
|
|
45
|
-
| board 删除文案 `Deleted "name"` | `src/ui/dashboard.ts:1044`、`:1067` |
|
|
46
|
-
| board 选中兜底(悬空 → `orderedIds[0]`) | `src/ui/dashboard.ts:196-199` |
|
|
47
|
-
| 排序来源 `groupRowsByFolder` | `src/core/rows.mjs:233`(dashboard 从 `:31` 导入) |
|
|
48
|
-
| `openDashboard` deps 现有能力(只有 `initialSelectedId`) | `src/ui/dashboard.ts:116-124`;`agent-board.ts:104` |
|
|
49
|
-
| 孤儿 `attach-flow.ts`(无 import、`setOpenDashboardFn` 从未调用) | `src/commands/attach-flow.ts:201` |
|
|
50
|
-
|
|
51
|
-
## 3. 设计
|
|
52
|
-
|
|
53
|
-
### 3.1 决策表
|
|
54
|
-
|
|
55
|
-
| 决策点 | 选择 | 理由 | 被拒备选 |
|
|
56
|
-
| --- | --- | --- | --- |
|
|
57
|
-
| 触发键 | `Ctrl+X`(0x18),**单击进入确认态** | 单字节 raw、终端无关(#126 标准);与 board `x`→`y` 心智一致 | `Ctrl+X` ×2(#150 已否决双击形态);`Ctrl+^`/`Ctrl+_`/`Ctrl+Space`(难按 / IME 抢);字母键(必须透传);`Ctrl+\`(已是 detach) |
|
|
58
|
-
| 形态 | 单击 → `y/N` 确认,第一次按下**不透传** | 单击不删、抢键不致命;子 Pi 不会同步收到 `message.copy` | 透传+第二次才删(产生真实 copy 副作用);延迟透传(每次 `Ctrl+X` 都延迟) |
|
|
59
|
-
| 提示落点 | **顶栏整行顶替**,不改变渲染行数 | 顶栏已存在 1 行,替换文本不触发 PTY resize(jiggle/resize 竞态面);body 与 loading 不受影响 | 底部覆盖条(会盖住子 Pi 编辑器行);只在 status 位追加(争宽、可见性弱) |
|
|
60
|
-
| 非确认键 | 取消确认态,**然后按既有链路继续处理该键** | 不吞用户输入;确认态只影响一次按键 | 吞掉(用户丢字);模态保持(模糊态重) |
|
|
61
|
-
| 断连 / host exited | arm 与 confirm **无条件可用** | archive 不依赖子进程存活;与 `←` 的 always-exitable 同性质;断连正是最想清掉该行的场景 | 仅连接正常时可用 |
|
|
62
|
-
| 删除语义 | 复用 `service.archive(viewId)`,零改动 | 与 board 单一实现;busy 行走协调器 fence;避免两条停机路径 | 另写停机逻辑 |
|
|
63
|
-
| 回 board 选中 | **相邻可见行**(无下一个则上一个;都没有则 `null` → 落 `orderedIds[0]`) | 单行删除语义明确,行多时不跳走 | 沿用 `orderedIds[0]` 兜底 |
|
|
64
|
-
| 删除提示 | `openDashboard` 新增 `initialNotice`,带到重开的 board | 重开的 board 是新建组件、内联 notice 带不过去 | 不显示提示 |
|
|
65
|
-
| archive 失败 | `ctx.ui.notify(res.error ?? "Delete failed", "error")`,仍回 board | 与 board 的 `notice(..., "error")` 同义;不静默 | 吞掉错误 |
|
|
66
|
-
| `session-switch` 路径 | 本期不做 | 「要删的正是当前所在会话」,语义不同 | 一起做(语义风险) |
|
|
67
|
-
| 孤儿 `attach-flow.ts` | 不同步 | 无 import、`setOpenDashboardFn` 从未被调用(T14 单列);新态在其映射下天然落到 `detached` | 顺手同步(扩大改动面) |
|
|
68
|
-
|
|
69
|
-
### 3.2 交互(状态机)
|
|
70
|
-
|
|
71
|
-
状态:`confirmArmed: boolean`(组件字段,初始 `false`,attach 期间不持久化;无定时器、无窗口过期语义)。
|
|
72
|
-
|
|
73
|
-
| 当前状态 | 按键 | 动作 |
|
|
74
|
-
| --- | --- | --- |
|
|
75
|
-
| 未 armed | `Ctrl+X` | **arm**:置位 + 重绘顶栏(提示);**不**发 socket;**不** archive |
|
|
76
|
-
| armed | `y` / `Y` / Enter | **confirm**:先 `close(true)` 做本地拆解(清定时器与监听、`restoreAndStop()` 恢复被 hold 的 PTY 尺寸、`socket.end()` 保序关闭),再 `done({ action: "delete" })`;**不**发送 `detach`/`terminate` 控制帧;**不** archive(由消费点 archive) |
|
|
77
|
-
| armed | `Ctrl+X` 二次按下 | **cancel-swallow**:清位,**吞掉该字节**(不透传——避免向子 Pi 注入一次 `message.copy`;用户裁决 2026-10-10) |
|
|
78
|
-
| armed | 其它任何按键(含 `n`、`Esc`、字母、方向键、pageUp/Down) | **cancel**:清位,然后**按既有链路继续处理该键**(输入不丢;`Ctrl+\` 仍 detach、`←` 仍按门控、普通字符仍进子 Pi) |
|
|
79
|
-
| armed | 鼠标事件(**两通道**:常规模式的 `handleInput` 鼠标 chunk / fullscreen 模式的 `handleMouse` 事件) | 按既有鼠标逻辑处理,并清位。**两个入口都要清**(`handleInput` 鼠标分支命中即 return、`handleMouse` 不经过 `handleInput`,纯函数两者都看不到) |
|
|
80
|
-
| 未 armed | 其它按键 | 既有链路,无变化 |
|
|
81
|
-
|
|
82
|
-
顶栏文案(armed 时整行替换,经既有 `clip(header, width)` 裁剪):
|
|
83
|
-
|
|
84
|
-
```
|
|
85
|
-
Delete "NAME"? y confirms · any other key cancels
|
|
86
|
-
```
|
|
87
|
-
|
|
88
|
-
`NAME` 经 `truncateToWidth` 截断到 ≤24 列(该函数已在该文件 import),保证 `y confirms` 子串在任何终端宽度下完整可见。
|
|
89
|
-
|
|
90
|
-
### 3.3 数据流
|
|
91
|
-
|
|
92
|
-
```
|
|
93
|
-
[attach 视图] PtyAttachComponent.handleInput
|
|
94
|
-
mouse chunk (handleInput) → 既有鼠标处理 + 清 confirmArmed(该分支命中即 return)
|
|
95
|
-
handleMouse(event) → 入口先清 confirmArmed,再走既有鼠标逻辑(fullscreen 通道;纯函数看不到)
|
|
96
|
-
resolveAttachDeleteKey({confirmArmed}, data) ← 纯函数(新)
|
|
97
|
-
arm → confirmArmed = true; scheduleRender(); return(不 send)
|
|
98
|
-
confirm → close(true) → done({ action: "delete" })(本地拆解;不发控制帧,不 archive)
|
|
99
|
-
cancel → confirmArmed = false; 继续既有链路
|
|
100
|
-
cancel-swallow → confirmArmed = false; return(吞掉该字节,不 send)
|
|
101
|
-
forward → 继续既有链路
|
|
102
|
-
既有链路(pageUp/Down/Home/End → Ctrl+\ → Ctrl+← → ← 门控 → 兜底 send)
|
|
103
|
-
|
|
104
|
-
[命令层] attach()(agent-board.ts,open-pty 分支)
|
|
105
|
-
result.action === "delete"
|
|
106
|
-
neighbor = nextVisibleIdAfterRemoval(service.rows(), viewId, Date.now()) ← 纯函数(新)
|
|
107
|
-
name = service.row(viewId)?.meta.name ?? ""
|
|
108
|
-
await service.archive(viewId) → 失败则 ctx.ui.notify(error, "error")
|
|
109
|
-
return { action: "detached", afterDelete: { nextSelectedId: neighbor, notice: `Deleted "${name}"` } }
|
|
110
|
-
|
|
111
|
-
[命令层] dashboardAttachLoop
|
|
112
|
-
下一轮 openDashboard({ initialSelectedId: afterDelete.nextSelectedId ?? selectedId,
|
|
113
|
-
initialNotice: afterDelete.notice })
|
|
114
|
-
|
|
115
|
-
[dashboard] DashboardComponent 构造:deps.initialNotice 存在 → this.notice(text, "info")
|
|
116
|
-
```
|
|
117
|
-
|
|
118
|
-
顺序约束:**必须先退出 attach 组件(`close(true)` + `done()`)、再由命令层 archive**。反了会把 host 被杀过程渲染给用户(`status` → `host exited` / `disconnected`,`pty-attach.ts:508`、`:1312`)。
|
|
119
|
-
|
|
120
|
-
### 3.4 组件契约(逐文件)
|
|
121
|
-
|
|
122
|
-
1. **`src/core/attach-delete.mjs`(新)**
|
|
123
|
-
- `export function resolveAttachDeleteKey(state: { confirmArmed: boolean }, data: string): { action: "arm" | "confirm" | "cancel" | "cancel-swallow" | "forward"; confirmArmed: boolean }`
|
|
124
|
-
- 判定:`matchesKey(data, Key.ctrl("x"))`(覆盖 0x18 / kitty CSI-u / modifyOtherKeys);确认键 = `data === "y" || data === "Y" || matchesKey(data, Key.enter)`;**armed 时 `Ctrl+X` 二次按下 → `cancel-swallow`**;其余按键在 armed 时为 `cancel`、未 armed 时为 `forward`。不读 socket / 连接 / 编辑器状态。
|
|
125
|
-
|
|
126
|
-
2. **`src/core/rows.mjs`**
|
|
127
|
-
- `export function nextVisibleIdAfterRemoval(rows, id, now): string | null` —— 用与 board 相同的排序(`groupRowsByFolder(rows, now)` → flatMap id)取被删 id 的下一个可见 id;末位取前一个;不存在该 id 或列表里没有其它行 → `null`。
|
|
128
|
-
|
|
129
|
-
3. **`src/ui/pty-attach.ts`**
|
|
130
|
-
- `PtyAttachResult` 增加第三态:`| { action: "delete" }`(`:17`)。
|
|
131
|
-
- 新字段 `private confirmArmed = false`。
|
|
132
|
-
- `handleInput`(`:289`):鼠标分支之后、`PageUp` 分支之前调用纯函数;`arm` / `confirm` / `cancel` / `forward` 按 3.2 执行。
|
|
133
|
-
- `handleInput` 的鼠标分支(`:290-291`)内清 `confirmArmed`——该分支命中即 `return`,纯函数永远看不到鼠标 chunk。
|
|
134
|
-
- **`handleMouse`(`:376-385`)入口同样清 `confirmArmed`**——fullscreen 模式鼠标只走这里(#167/#169);清位发生在 `resolveAttachMouseAction` 之前,覆盖返回 `paste-primary` 与返回 `undefined` 的全部路径。
|
|
135
|
-
- **confirm 的拆解**(照 `detach()`(`:435-447`)的形状,去掉控制帧):`this.close(true)` → `this.done({ action: "delete" })`。`close()`(`:1522`)自带全部本地清理并调用 `jiggleRetry.restoreAndStop()`(`:1533`;控制器 `pty-attach-jiggle-controller.mjs:325-329`),`true` 走 `socket.end()` 保序(`:1562`)、失败由 unref 计时器兜底(`:1563-1567`);**不发送** `detach` 帧(host 将由 archive 终止)。`close()` 会置 `closed`(`:1529`),而 `scheduleReconnect` 首行即 `if (this.closed || this.retryTimer) return`(`:546-547`)→ 拆解后不会再重连。
|
|
136
|
-
- `render`(`:399-401`):armed 时顶栏整行替换为确认提示(`NAME` 截断 ≤24 列,保证 `y confirms` 可见);**行数与未 armed 时一致**。
|
|
137
|
-
|
|
138
|
-
4. **`src/commands/agent-board.ts`**
|
|
139
|
-
- `AttachOutcome` 增加旁带信息:`{ action: "detached" | "closed" | "switched" | "none"; afterDelete?: { nextSelectedId: string | null; notice: string } }`(`:137`)。
|
|
140
|
-
- `attach()` open-pty 分支:按 3.3 处理 `delete`(`:187-192`;`attach()` 本体起于 `:163`)。
|
|
141
|
-
- `dashboardAttachLoop`(`:139-161`):把 `afterDelete` 带到下一轮 `openDashboard`。
|
|
142
|
-
- `openDashboard` options(`:77`)与 `DashboardComponent` deps 透传 `initialNotice`。
|
|
143
|
-
|
|
144
|
-
5. **`src/ui/dashboard.ts`**
|
|
145
|
-
- deps 类型(`:116-124`)加 `initialNotice?: string`;构造函数中 `this.selectedId` 初始化后(`:175`)若存在则 `this.notice(deps.initialNotice, "info")`。
|
|
146
|
-
- 不改 `refresh()` 的兜底语义。
|
|
147
|
-
|
|
148
|
-
6. **`README.md`**
|
|
149
|
-
- attach 两处叙述(`:96`、`:201`)补 `Ctrl+X` → `y` 说明与 caveat:attach 期间子 Pi 的 `Ctrl+X`(`app.message.copy` / `app.models.clearAll`)被接管;与 board 的 `Ctrl+X` ×2 不是同一件事。
|
|
150
|
-
|
|
151
|
-
### 3.5 降级与边界
|
|
152
|
-
|
|
153
|
-
- **行数不变**:确认提示不改变渲染行数,避免 PTY resize(jiggle 控制器竞态面)。
|
|
154
|
-
- **无窗口语义**:不引入定时器,不存在「窗口过期作废」的模糊态(这是相对 #147 原案 `Ctrl+X` ×2 的关键改动)。
|
|
155
|
-
- **二次 `Ctrl+X`**:**取消 + 吞掉**(用户 2026-10-10 裁决)——`Ctrl+X` 在本视图内是确认开关,再按一次只是关掉提示:不透传、不会向子 Pi 注入 `message.copy`。这是「非确认键一律透传」的唯一例外;README caveat 据此写成「`Ctrl+X` 在 attach 期间被接管」。
|
|
156
|
-
- **邻居退化**:`nextSelectedId` 为 `null` 或该行在删除前已消失 → board 的 `refresh()` 兜底到 `orderedIds[0]`(既有行为)。
|
|
157
|
-
- **archive 失败**:不静默,`ctx.ui.notify(..., "error")`;仍回 board。
|
|
158
|
-
- **confirm 与 detach 的差别**:只做本地拆解(`close(true)`),不发送 `detach` 控制帧(host 由 archive 终止)。`detach()` 里那句显式 `restoreAndStop()` 是为了在**发送 detach 帧之前**把 restore 包排进缓冲(`:435-447`),本路径无控制帧,故不需要额外调用——`close()` 内部已经做(`:1533`)。
|
|
159
|
-
|
|
160
|
-
## 4. 验收矩阵
|
|
161
|
-
|
|
162
|
-
| ID | 功能点 | 验收方式 | 具体验证 | 通过标准 |
|
|
163
|
-
| --- | --- | --- | --- | --- |
|
|
164
|
-
| A1 | `resolveAttachDeleteKey` 全分支 | 自动化(unit) | `node --test test/attach-delete-key.test.mjs` | 未 armed + `\x18` → `arm`;armed + `y`/`Y`/`\r` → `confirm`;armed + `n`/`\x1b`/`a`/方向键 → `cancel`(透传);**armed + `\x18` → `cancel-swallow`(吞掉)**;未 armed + 其它 → `forward`;返回的 `confirmArmed` 与动作自洽 |
|
|
165
|
-
| A2 | `nextVisibleIdAfterRemoval` | 自动化(unit) | `node --test test/rows.test.mjs`(或该模块既有测试文件) | 中间行 → 下一可见行;末行 → 上一行;单行 / 空列表 / 未知 id → `null`;跨分组取全局下一个可见行 |
|
|
166
|
-
| A3 | 组件级 arm/confirm/cancel、拆解与不透传 | 自动化(integration,组件探针) | `node --test test/pty-attach-delete-gate.test.mjs`(沿用 `test-support/detach-gate-smoke.ts` 新增场景:真 `PtyAttachComponent` + 假 socket server) | arm 后顶栏含 `y` 子串与 session 名(含长名字场景:`y confirms` 不被裁);arm 期间 socket 未收到该 `\x18`;`y` → `done` 结果为 `{action:"delete"}`;socket **未收到 detach/terminate 帧**,但假 server 观察到客户端 socket 关闭且**此后无新连接**(无重连);legacy 模式下 confirm 发出 restore resize 且先于 socket end(对照既有 `leftDetachRestoresBeforeGracefulEnd` 场景);`n` 后 socket 收到该字节;**armed + 第二次 `\x18` → socket 未收到该字节且 `done` 未被调用(cancel-swallow)**;armed + `handleInput` 滚轮 chunk + `y` → 不 delete 且 `y` 被透传;**armed + `handleMouse` 事件(直接调 `handleMouse({type:"press",button:"middle",…})`,形状照 `test-support/pty-attach-mouse-scenario.ts:142-152`)+ `y` → 同样不 delete 且 `y` 被透传**(fullscreen 通道清位);`host exited` / 断连状态下 arm 与 confirm 仍成立 |
|
|
167
|
-
| A4 | 编码形态兼容 | 自动化(integration,同上探针) | 同 A3 文件 | 0x18、kitty CSI-u(`\x1b[120;5u`)、modifyOtherKeys(`\x1b[27;5;120~`)三种形态均能 arm |
|
|
168
|
-
| A5 | 命令层闭环:archive + board 重开参数 | 自动化(integration,新 harness) | `node --test test/attach-delete-flow.test.mjs`:stub ctx(替身面见 §5)+ 真 temp-root service + **打桩 `service.resolveAttachTarget` 返回 `{kind:"pty", socketPath:"<temp>/fake.sock"}`**,驱动 `dashboardAttachLoop`;`ui.custom` 为**两段式**:第 1 段(board)调用 factory 构造 `DashboardComponent` 并以 wrappedDone 推入 `{action:"attach", viewId, stopFirst:false}`,第 2 段(PTY)不调 factory、直接 resolve `{action:"delete"}` | 被删 view `meta.archived === true`;其 session 文件仍存在;`service.rows()` 不含该 id;第 2 轮 board 的 `DashboardComponent` deps 收到 `initialSelectedId === 相邻 id` 与 `initialNotice === 'Deleted "name"'` |
|
|
169
|
-
| A6 | archive 失败不静默 | 自动化(integration,同上 harness) | 同 A5,`service.archive` 打桩返回 `{ok:false, error:"boom"}` | `ctx.ui.notify` 收到 error 级提示;仍回到 board;无未捕获 rejection |
|
|
170
|
-
| A7 | 文案与类型 | 自动化(static) | `npm run typecheck` + `rg -n "Ctrl\+X" README.md` 的 static 断言 | attach 两处段落含 `Ctrl+X` → `y`;board 的 `x` / `Ctrl+X` ×2 文案未被误改;typecheck 0 error |
|
|
171
|
-
| A8 | 全量回归 | 自动化(build/test) | `npm run verify`(typecheck + perf gate + 全量测试 + coverage 阈值 + pack:dry) | 全绿,无新增失败 |
|
|
172
|
-
| U1 | 真机交互(删除路径) | 用户实测 | 起 board → attach 一个空闲会话 → `Ctrl+X`(看提示)→ `y` | 行从 board 消失、`Deleted "name"` 可见、选中落相邻行、session 文件仍在、host 已停(无残留 pid) |
|
|
173
|
-
| U2 | 真机(不删除路径) | 用户实测 | attach → `Ctrl+X` → `n`;再 attach → `Ctrl+X` → 继续打字 | 会话未被删;继续打字字符不丢(取消透传生效) |
|
|
174
|
-
| U3 | 真机(Working 会话) | 用户实测 | attach 一个 Working 会话 → `Ctrl+X` → `y` | 先停 run 再归档、无「删除后 runner 仍写状态」竞态;回 board 无 `host exited` / 重连横幅闪现 |
|
|
175
|
-
| U4 | 真机(断连场景) | 用户实测 | attach 后杀掉 host 进程(或等其 exited)→ `Ctrl+X` → `y` | 仍能删除并回 board |
|
|
176
|
-
|
|
177
|
-
**U 类统一前置(可执行时机)**:先确认运行面是待测 checkout——`pi install "$(pwd)"` 指向该 checkout,或 `~/.pi/agent/agent-board` 经 dev auto-discovery 指向它(README:42-63);改动落地后重启 pi 会话再执行,否则会拿旧安装副本验收、误报「功能不生效」。
|
|
178
|
-
|
|
179
|
-
## 5. 可测性拆分设计(必答)
|
|
180
|
-
|
|
181
|
-
**新增纯函数(副作用全部外推)**
|
|
182
|
-
|
|
183
|
-
1. `src/core/attach-delete.mjs` → `resolveAttachDeleteKey(state, data)`
|
|
184
|
-
- 依赖:pi-tui `matchesKey`(纯),不读 `this` / socket / 连接 / 编辑器状态。
|
|
185
|
-
- 测试边界:A1 覆盖全分支;三种真实编码形态由 A4 在组件层钉住(纯函数层只保证不自行解析转义序列)。
|
|
186
|
-
2. `src/core/rows.mjs` → `nextVisibleIdAfterRemoval(rows, id, now)`
|
|
187
|
-
- 依赖:同文件 `groupRowsByFolder`(既有纯函数)——**与 board 的 `orderedIds` 同一排序来源,避免两套排序漂移**。
|
|
188
|
-
- 测试边界:A2。
|
|
189
|
-
|
|
190
|
-
**副作用隔离**
|
|
191
|
-
|
|
192
|
-
- 组件层 `PtyAttachComponent`:只做「调用纯函数 → 置位 / 渲染顶栏 / `done()`」;无 `service` 引用,不 archive。
|
|
193
|
-
- 命令层 `attach()`:只做「取 name → 调纯函数算邻居 → `await service.archive` → 返回旁带信息」;notice 文案在此拼装(与 board 同一字面量)。
|
|
194
|
-
- dashboard:只消费 `initialNotice`,不动 `refresh()` 兜底语义。
|
|
195
|
-
|
|
196
|
-
**测试边界(不得越界)**
|
|
197
|
-
|
|
198
|
-
- 不测 `service.archive` 内部(`test/service.test.mjs` 已覆盖)。
|
|
199
|
-
- 不测 pi-tui `matchesKey` 实现(上游职责)。
|
|
200
|
-
- A5/A6 的 stub ctx 面**按真实调用枚举**(依据 `src/commands/agent-board.ts`):`ctx.ui.custom/notify/setHeader/setFooter/setWorkingVisible/setTitle`、`ctx.modelRegistry.getAvailable`、`ctx.model`、`ctx.cwd`、`ctx.sessionManager.getSessionFile`(返回 `null` 即可——测试不在 attach 会话内)。`attach()` 的 session-switch 分支不在 A5 路径上,`ctx.switchSession` 无需替身。
|
|
201
|
-
- **A5 进入被测分支的合法路径**:`attach()` 在开 PTY 前先跑 `planAttachResolved(await service.resolveAttachTarget(viewId))`(`:187`);真 temp-root service 在无 host 时会真实 `ensureHostImpl` spawn host 并轮询探活(`service.mjs:1236+`),PTY 不可用时回落 `kind:"session"` → session-switch(与「不在 A5 路径上」矛盾)。因此 **A5 允许打桩 `service.resolveAttachTarget` 返回 `{kind:"pty", socketPath:<temp 路径>}`**——stub 的 `ui.custom` 不会真连 socket,被测逻辑(archive / 邻居 / notice 拼装)不依赖 host 存活。除此以外 service 用真 temp-root 实现(A6 例外:额外打桩 `service.archive` 返回值)。
|
|
202
|
-
|
|
203
|
-
## 6. 风险
|
|
204
|
-
|
|
205
|
-
| 风险 | 处置 |
|
|
206
|
-
| --- | --- |
|
|
207
|
-
| 抢键回归(子 Pi 的 `Ctrl+X` = `message.copy` / `models.clearAll`) | 确认式(单击不删)+ README caveat;U2 钉「不删除路径」 |
|
|
208
|
-
| 顶栏提示与 status 争宽 / 行数变化引发 resize / 长名字裁掉确认键提示 | 整行顶替、行数不变、`NAME` 截断 ≤24 列;A3 断言顶栏含 `y` 子串 |
|
|
209
|
-
| 确认态吞输入 / armed 悬挂导致后续 `y` 误删(含 fullscreen 鼠标通道) | 取消一律透传(U2 验收);`handleInput` 鼠标分支 **与** `handleMouse` 入口双清位 + A3 两条断言(两条通道各一条) |
|
|
210
|
-
| confirm 漏拆解(定时器泄漏 / PTY 尺寸残留 / 退出后仍消费 socket 事件) | §3.4.3 契约定死 `close(true)` → `done()`;A3 断言 socket 关闭后无新连接 + legacy 有 restore 且先于 end |
|
|
211
|
-
| 二次 `Ctrl+X` 被吞(用户在子 Pi 侧暂时失去 `message.copy`) | 用户裁决「取消 + 吞掉」;README 明确 `Ctrl+X` 在 attach 期间整体被接管 |
|
|
212
|
-
| 命令层无既有 harness(仓库首个 stub ctx 测试) | 独立 plan task;若成本超预期,降级为 static 断言 + U1 并在 plan 记录 |
|
|
213
|
-
| 孤儿 `attach-flow.ts` 与实际接口漂移 | 非目标;T14 单列 |
|
|
214
|
-
| 半重构双副本漏改导致入口行为不一致 | 线上只有 `agent-board.ts` 一份映射(R2 已证实),风险解除 |
|
|
215
|
-
|
|
216
|
-
## 7. 开放问题
|
|
217
|
-
|
|
218
|
-
1. ~~armed 状态下第二次 `Ctrl+X`~~ **已裁决(用户 2026-10-10):取消 + 吞掉**(不再向子 Pi 透传,避免注入 `message.copy`)——已写入 §3.2 / §3.3 / §3.4.1 / §3.5 与 A1 / A3。
|
|
219
|
-
2. ~~顶栏文案是否含 busy 提示~~ **已裁决(用户 2026-10-10):不含**——保持 `Delete "NAME"? y confirms · any other key cancels`;Working 场景的行为由 U3 真机验收覆盖。
|
|
220
|
-
|
|
221
|
-
## 参考
|
|
222
|
-
|
|
223
|
-
- issue #147;#150(board 确认式删除,main `444199d`);#126 / PR #137(`Ctrl+\` 选键标准与「抢键 + caveat」文案先例);#103 / #119(attach 按键判定只信权威状态)
|
|
224
|
-
- 调研:`research/round1-key-choice-and-occupancy.md`、`round2-delete-path-and-return-channel.md`、`round3-ui-surface-and-verification.md`
|
|
@@ -1,154 +0,0 @@
|
|
|
1
|
-
Drafted: zai-coding-cn/glm-5.3 (selected) · zai-coding-cn/glm-5.3 (physical) @ 2026-10-10T00:31:00Z
|
|
2
|
-
|
|
3
|
-
# Spec: #174 — middle-click paste in fullscreen reads a stale X11 PRIMARY: mirror pi's selection to PRIMARY (B2) + paste-source override (C)
|
|
4
|
-
|
|
5
|
-
Status: DRAFT (awaiting spec-cross-review or user waiver, then user approval — github-issue-driven step 4 gates)
|
|
6
|
-
Issue: https://github.com/zhuxixi/pi-agent-board/issues/174
|
|
7
|
-
Repo state baseline: main @ `0fd1dc5` (post-#173)
|
|
8
|
-
|
|
9
|
-
## Problem (root cause, verified live + from source)
|
|
10
|
-
|
|
11
|
-
In fullscreen, drag-select belongs to `TuiAltScreen` and its copy-on-select writes
|
|
12
|
-
**CLIPBOARD** only; nothing in a pi session ever writes X11 **PRIMARY**. Middle-click
|
|
13
|
-
paste (#171) reads **PRIMARY** → serves a fossil (live evidence 2026-10-10: PRIMARY held
|
|
14
|
-
`assert_contains(doc, contracts)` while the user's fresh selection sat in CLIPBOARD).
|
|
15
|
-
Pre-1.0 the loop closed because the component's own selection mirrored to PRIMARY
|
|
16
|
-
(`writePrimarySelection`, a1c5f08) — dead in fullscreen since pi owns left-drag.
|
|
17
|
-
|
|
18
|
-
Design-critical ordering fact (pi-tui 1.1.0 `handleMouseEvent`): overlays are dispatched
|
|
19
|
-
**before** pi's selection finalization for the same event, so the mirror must read the
|
|
20
|
-
selection **deferred** (`process.nextTick` after the release/click).
|
|
21
|
-
|
|
22
|
-
## Goal / non-goals
|
|
23
|
-
|
|
24
|
-
**Goal**: (B2) when a selection gesture ends inside the attach view in fullscreen, mirror
|
|
25
|
-
pi's final selection text into X11 PRIMARY so the classic select → middle-click-paste
|
|
26
|
-
loop works again; (C) opt-in `AGENT_BOARD_ATTACH_PASTE_SOURCE=primary|clipboard` for
|
|
27
|
-
environments where PRIMARY never updates. Both governed by the existing
|
|
28
|
-
`AGENT_BOARD_ATTACH_NATIVE_PASTE` kill switch. pi's own selection behavior (visuals,
|
|
29
|
-
CLIPBOARD copy) is untouched — B2 observes, never consumes.
|
|
30
|
-
|
|
31
|
-
**Non-goals**:
|
|
32
|
-
- Selections released outside the attach overlay (dashboard / other pi UI) — outside the
|
|
33
|
-
component's event window; pi-wide coverage belongs upstream (earendil-works/pi).
|
|
34
|
-
- Any change to #169's ownership model, #171's paste decision table, or regular-mode
|
|
35
|
-
behavior (mirror is a no-op there: `getActiveSelectionText` doesn't exist on
|
|
36
|
-
regular-mode TUIs; paste source defaults to primary as today).
|
|
37
|
-
|
|
38
|
-
## Design
|
|
39
|
-
|
|
40
|
-
### 1. Pure decision helpers (`src/core/pty-scroll.mjs`)
|
|
41
|
-
|
|
42
|
-
```js
|
|
43
|
-
/** Issue #174 (C): which X11 selection buffer middle-click paste reads.
|
|
44
|
-
* "primary" (default, classic X11 semantics) | "clipboard" (escape hatch for
|
|
45
|
-
* environments where PRIMARY never updates, e.g. WSLg). Unknown values fall
|
|
46
|
-
* back to "primary". Pure: the env value is read at the call site. */
|
|
47
|
-
resolvePasteSelectionSource(value) → "primary" | "clipboard"
|
|
48
|
-
|
|
49
|
-
/** Issue #174 (B2): whether a normalized mouse event should trigger mirroring
|
|
50
|
-
* pi's (final) selection text into X11 PRIMARY. Only selection-gesture endings
|
|
51
|
-
* (release/click of the left button) with native paste enabled. Pure. */
|
|
52
|
-
shouldMirrorSelectionToPrimary(event, { nativePasteEnabled }) → "mirror-primary" | null
|
|
53
|
-
```
|
|
54
|
-
|
|
55
|
-
Decision table for `shouldMirrorSelectionToPrimary`:
|
|
56
|
-
|
|
57
|
-
| type | button | nativePasteEnabled | result |
|
|
58
|
-
| --- | --- | --- | --- |
|
|
59
|
-
| release / click | left | true | `"mirror-primary"` |
|
|
60
|
-
| release / click | left | false | `null` |
|
|
61
|
-
| press/move/drag/wheel | any | * | `null` |
|
|
62
|
-
| release / click | middle/right/none | * | `null` |
|
|
63
|
-
|
|
64
|
-
### 2. Component wiring (`src/ui/pty-attach.ts`)
|
|
65
|
-
|
|
66
|
-
- `handleMouse(event)` gains one branch BEFORE the paste branch (order irrelevant; events
|
|
67
|
-
are disjoint): if `shouldMirrorSelectionToPrimary(event, {nativePasteEnabled})` →
|
|
68
|
-
schedule `process.nextTick(() => this.mirrorFullscreenSelectionToPrimary())` and
|
|
69
|
-
**return `undefined`** (observe only — pi keeps the event). The paste branch is
|
|
70
|
-
unchanged (`press + middle` → paste, `{handled:true, render:false}`).
|
|
71
|
-
- New private method:
|
|
72
|
-
|
|
73
|
-
```ts
|
|
74
|
-
/** Issue #174 (B2): after a selection gesture ends inside this overlay, copy
|
|
75
|
-
* pi's final selection text into the X11 PRIMARY selection so the classic
|
|
76
|
-
* select-then-middle-click-paste loop works in fullscreen. Runs on nextTick
|
|
77
|
-
* because overlays are dispatched before TuiAltScreen finalizes the selection
|
|
78
|
-
* for the same event. No-op in regular mode (no getActiveSelectionText) and
|
|
79
|
-
* under the native-paste kill switch. */
|
|
80
|
-
private mirrorFullscreenSelectionToPrimary(): void {
|
|
81
|
-
if (this.closed) return;
|
|
82
|
-
const alt = this.tui as { getActiveSelectionText?: () => string | undefined };
|
|
83
|
-
const text = alt.getActiveSelectionText?.();
|
|
84
|
-
if (typeof text === "string" && text.length > 0) this.writePrimarySelection(text);
|
|
85
|
-
}
|
|
86
|
-
```
|
|
87
|
-
|
|
88
|
-
(`writePrimarySelection` already guards `AGENT_BOARD_ATTACH_NATIVE_PASTE === "0"`;
|
|
89
|
-
the decision helper guards the scheduling side — belt and suspenders, no behavior
|
|
90
|
-
difference.)
|
|
91
|
-
- `pastePrimarySelection()`: the spawn args become
|
|
92
|
-
`xclip -o -selection ${resolvePasteSelectionSource(process.env.AGENT_BOARD_ATTACH_PASTE_SOURCE)}`.
|
|
93
|
-
Everything else (800 ms kill timer, forward-as-input, silent no-op) unchanged.
|
|
94
|
-
|
|
95
|
-
### Resulting behavior matrix
|
|
96
|
-
|
|
97
|
-
| Scenario | Behavior |
|
|
98
|
-
| --- | --- |
|
|
99
|
-
| fullscreen: drag-select inside attach → middle-click in attach | selection lands in PRIMARY (B2) AND CLIPBOARD (pi); middle paste = the selection ✅ |
|
|
100
|
-
| fullscreen: select in another X11 app → middle-click in attach | pastes that app's PRIMARY (unchanged, correct) |
|
|
101
|
-
| fullscreen: select elsewhere in pi (dashboard) → middle-click in attach | NOT mirrored (no event window) — known limitation; middle paste reads whatever PRIMARY holds |
|
|
102
|
-
| `AGENT_BOARD_ATTACH_PASTE_SOURCE=clipboard` | middle paste reads CLIPBOARD regardless of PRIMARY |
|
|
103
|
-
| `AGENT_BOARD_ATTACH_NATIVE_PASTE=0` | no mirror, no paste (kill switch governs both sides) |
|
|
104
|
-
| regular mode | mirror no-ops (no `getActiveSelectionText`), paste reads primary (default) — unchanged |
|
|
105
|
-
|
|
106
|
-
## Acceptance matrix
|
|
107
|
-
|
|
108
|
-
| ID | 功能点 | 验收方式 | 具体验证 | 通过标准 |
|
|
109
|
-
|----|--------|----------|----------|----------|
|
|
110
|
-
| A1 | 两个纯决策表(mirror 触发条件 × 开关;paste source 解析) | 自动化验证(unit) | `node --test test/pty-scroll.test.mjs`(新增用例直接调两个 helper:mirror 表全部行 + source 的 unset/"primary"/"clipboard"/garbage) | 新增用例全绿,既有用例不回归 |
|
|
111
|
-
| A2 | B2 接线:release/click(left) → nextTick 后 PRIMARY 收到 pi 的选中文本;observe 不消费;空选择不写;kill switch 不写 | 自动化验证(integration,组件级场景) | 新增 `test-support/pty-attach-selection-mirror-scenario.ts`(fake tui `mode:"fullscreen"` + `getActiveSelectionText` fixture + PATH-stub `xclip` 把 args/stdin 记日志),由 `test/pty-attach-selection-mirror.test.mjs` 运行断言 | ① `handleMouse({release,left})` 返回 `undefined` 且 nextTick 后 stub 日志含 `-selection primary` 写、stdin=fixture;② `getActiveSelectionText→undefined` 时无写;③ NATIVE_PASTE=0 无写;④ `press+left`/`move` 不触发 |
|
|
112
|
-
| A3 | C 接线:paste source 覆盖 | 自动化验证(integration,同一场景文件加 case) | 同场景:`AGENT_BOARD_ATTACH_PASTE_SOURCE=clipboard` + `handleMouse({press,middle})` | stub 日志显示 `-o -selection clipboard`;fixture 文本经 socket 以 `{type:"input"}` 发出(复用 #171 场景缝) |
|
|
113
|
-
| A4 | 类型与构建 | 自动化验证(static/build) | `npm run typecheck`、`npm run verify` | 全部通过 |
|
|
114
|
-
| A5 | 既有行为不回归(#171 中键粘贴、#169 ownership、regular 路径) | 自动化验证(unit+integration,既有套件) | `npm test` / `npm run test:coverage` | 既有测试全绿,覆盖阈值不降 |
|
|
115
|
-
| U1 | 真机:pi 内选中 → 中键粘贴闭环 | 用户实测 | ① fullscreen attach 内拖选一段文字;② 中键 → 应粘贴刚选的内容;③ 在别的 X11 应用选中 → 中键 → 粘贴该内容;④ `AGENT_BOARD_ATTACH_PASTE_SOURCE=clipboard` 后中键粘贴读剪贴板;⑤ `AGENT_BOARD_ATTACH_NATIVE_PASTE=0` 后中键无反应、选中不再镜像 PRIMARY | ①–⑤ 全部符合 |
|
|
116
|
-
|
|
117
|
-
## Testability split design (hard constraint for implementation)
|
|
118
|
-
|
|
119
|
-
- **Pure core**: `resolvePasteSelectionSource(value)` and
|
|
120
|
-
`shouldMirrorSelectionToPrimary(event, {nativePasteEnabled})` — plain functions, no env
|
|
121
|
-
reads inside, no side effects. Unit decision tables (A1).
|
|
122
|
-
- **I/O shell**: `PtyAttachComponent.handleMouse` maps decisions to side effects
|
|
123
|
-
(`process.nextTick` scheduling → `mirrorFullscreenSelectionToPrimary()` → existing
|
|
124
|
-
`writePrimarySelection`; paste branch spawns xclip with the resolved source).
|
|
125
|
-
Component-level tests (A2/A3) drive only public seams (`handleMouse`, ticks,
|
|
126
|
-
`dispose()`) and observe the PATH-stubbed `xclip` log + attach-socket messages; the
|
|
127
|
-
stub handles both directions (`-o` prints a fixture; write mode records stdin/args).
|
|
128
|
-
- The pure helpers must not be re-inlined into the component; `handleMouse` must remain
|
|
129
|
-
the only mirror scheduling point; `writePrimarySelection`/`pastePrimarySelection`
|
|
130
|
-
remain the only xclip touch points.
|
|
131
|
-
|
|
132
|
-
## Risks / notes
|
|
133
|
-
|
|
134
|
-
- nextTick timing assumes pi's release handling is synchronous in the same tick (verified
|
|
135
|
-
in tui-alt-screen.js: overlay dispatch → `handleSelectionMouseEvent` → copy, no awaits
|
|
136
|
-
before selection state is final; `copySelectionToClipboard` is async but only writes
|
|
137
|
-
CLIPBOARD — irrelevant to our read). If a future pi-tui defers selection state, the
|
|
138
|
-
mirror lags one gesture — acceptable degradation, not corruption.
|
|
139
|
-
- `getActiveSelectionText` typing: structural cast in the component (declared private in
|
|
140
|
-
pi-tui 1.1.0's `tui-alt-screen.d.ts` but runtime-reachable, hence the cast; absent on
|
|
141
|
-
regular TUIs and old runtimes → optional access → no-op).
|
|
142
|
-
- Double-click word / triple-click line selections: press path sets the full range
|
|
143
|
-
immediately; release/click mirror picks up the same text — covered by the same code
|
|
144
|
-
path, no special casing.
|
|
145
|
-
- The stub-xclip scenario must restore PATH and clean temp dirs (self-contained).
|
|
146
|
-
|
|
147
|
-
## Deliverables
|
|
148
|
-
|
|
149
|
-
- `src/core/pty-scroll.mjs`: + `resolvePasteSelectionSource`, `shouldMirrorSelectionToPrimary`
|
|
150
|
-
- `src/ui/pty-attach.ts`: mirror branch in `handleMouse`, `mirrorFullscreenSelectionToPrimary()`,
|
|
151
|
-
paste source in `pastePrimarySelection`
|
|
152
|
-
- `test/pty-scroll.test.mjs`: decision-table cases
|
|
153
|
-
- `test-support/pty-attach-selection-mirror-scenario.ts` + `test/pty-attach-selection-mirror.test.mjs`
|
|
154
|
-
- CHANGELOG entry via release tooling at release time (not in this PR)
|
|
@@ -1,195 +0,0 @@
|
|
|
1
|
-
# Spec — Issue #170: attach 入站应答护栏(终端应答碎片不再漏进子进程输入框)
|
|
2
|
-
|
|
3
|
-
Drafted: deepseek/deepseek-flash (selected) · deepseek/deepseek-flash (physical) @ 2026-10-14T07:53:40+08:00
|
|
4
|
-
Revised: deepseek/deepseek-flash (selected) · deepseek/deepseek-flash (physical) @ 2026-10-14T08:02:08+08:00(round-1:R1-G1/G2 接受并改;R1-D1/D2 接受并改;R1-D3 部分接受)
|
|
5
|
-
Revised: deepseek/deepseek-flash (selected) · deepseek/deepseek-flash (physical) @ 2026-10-14T08:12:00+08:00(round-2:R2-D1/D2/D3 全部接受并改)
|
|
6
|
-
|
|
7
|
-
> 状态:**DRAFT v3 — 交叉复核已收敛(第 2 轮真缺口 0)→ ⏸ 等用户确认设计**
|
|
8
|
-
> 认领:zhuxixi(2026-10-14);基线 HEAD:`acc40fd`(main;origin/main 已到 `f81949d`,本 issue 涉及文件一致)
|
|
9
|
-
> 路由:bug → `systematic-debugging`(Phase 1 根因调查已在 research 完成,本文档是 Phase 2-4 的产出)
|
|
10
|
-
> 调研:`research/01-in-repo-mechanism.md`(本仓机制与硬约束)、`research/02-upstream-wire-evidence.md`(上游源码级链路与真实字节)、`research/03-test-infrastructure.md`(可测性落点)
|
|
11
|
-
> 上游:earendil-works/pi#10657(OPEN,同族 #10256 OPEN、#8603 closed、#10250 closed)
|
|
12
|
-
> 关联:#128(终端能力透传)、#148/#161(settle 探针)、#180(子进程就绪门)
|
|
13
|
-
|
|
14
|
-
## 1. 问题陈述
|
|
15
|
-
|
|
16
|
-
**每次 attach 进一个会话,子进程输入框会被塞进字面垃圾**(本机稳定为 `c7c`,zsh 子进程的提示行与 pi 子进程的 composer 都中招)。垃圾来自真实终端对 DA1 查询的应答碎片,经 pi-tui 的输入管线落到 attach 覆盖层,再由覆盖层「未识别输入一律透传」的兜底送进子进程。
|
|
17
|
-
|
|
18
|
-
影响面有两个,第二个是本 issue 立题以来新确认的:
|
|
19
|
-
|
|
20
|
-
1. **子进程输入被污染**:用户 attach 后立即要按退格清掉垃圾;自动化子进程(headless worker、脚本)会把这段文本当成 prompt 内容的一部分。
|
|
21
|
-
2. **本地终端探测恒超时**:碎片化的 DA1 永远不会作为单一事件到达 pi-tui 的 `consumeTerminalColorResponse`,所以 `queryTerminalColors` 只能走超时路径——这是本机每条 attach 都记 `attach_settle_scheme: timeout` 的直接原因之一。
|
|
22
|
-
|
|
23
|
-
真实字节(上游 #10657 记录,WezTerm 20240203 + WSL2/ConPTY):DA1 应答 28 字节 `ESC[?61;6;7;22;23;24;28;32;42c`,以 8 字节读交付,间隔 >50ms:`ESC[?61;6;` / `7;22;23;` / `24;28;32` / `;42c`。
|
|
24
|
-
|
|
25
|
-
## 2. 根因
|
|
26
|
-
|
|
27
|
-
**根因是两个超时阈值与一张「只覆盖 OSC/DCS/APC、不覆盖 CSI」的排除表:CSI 形状的应答头在 50ms 后被 `StdinBuffer` 当作输入事件发出,再过 150ms 被 `ProcessTerminal` 当作普通输入 flush;应答尾部逐字符发出,落到覆盖层的兜底透传。**
|
|
28
|
-
|
|
29
|
-
四段链路(每段已在 pi 1.2.0 安装产物中核到行):
|
|
30
|
-
|
|
31
|
-
| # | 层 | 行为 | 位置 |
|
|
32
|
-
|---|---|---|---|
|
|
33
|
-
| 1 | 终端 | 只答 DA1,应答被切成 4 个 8 字节读 | WezTerm + ConPTY 实测 |
|
|
34
|
-
| 2 | `stdin-buffer.js` | `DEFAULT_SEQUENCE_TIMEOUT_MS = 50`;`TERMINAL_REPLY_HEADER_PATTERN` 只匹配 OSC/DCS/APC,**CSI 不在内** → 前缀武装 50ms 超时后被当 `data` 事件发出 | `:21`、`:27`、`:344-352` |
|
|
35
|
-
| 3 | `terminal.js` | `KEYBOARD_PROTOCOL_RESPONSE_FRAGMENT_TIMEOUT_MS = 150`;只认 `^\x1b\[\?(\d+)u$`(kitty flags)与 `^\x1b\[\?[\d;]*c$`(DA)为完整;前缀缓冲 150ms 后 `flushKeyboardProtocolNegotiationBufferAsInput()` **当普通输入转发** | `:13`、`:16-28`、`:239-263`、`:274-287` |
|
|
36
|
-
| 4 | 本仓覆盖层 | 尾部碎片不以 `ESC` 开头 → `stdin-buffer` 走 not-escape 分支逐字符发出 → `PtyAttachComponent.handleInput` 兜底 `send({type:"input"})` | `src/ui/pty-attach.ts:295`(函数体末尾) |
|
|
37
|
-
|
|
38
|
-
**为什么修在覆盖层**(Phase 2 模式分析):
|
|
39
|
-
|
|
40
|
-
- **工作样例就在上游**:`readKeyboardProtocolNegotiationSequence` 已经实现了「前缀缓冲 + 拼接补全」,`stdin-buffer.js` 的 `TERMINAL_REPLY_HEADER_PATTERN` 已经实现了「可识别应答头永不 flush」。两处都证明了这套模式可行,缺的只是**覆盖面**(CSI 家族 + 覆盖层这一侧)。本改动是这两个模式的嵌入方超集,不是新发明。
|
|
41
|
-
- **上游不可控且不在本仓范围内**:#10657 至今 OPEN;patch `node_modules` 的机制在本仓已退役(升级即失效)。
|
|
42
|
-
- **损坏发生在子进程可见面上**:无论上游将来是否修好,覆盖层丢弃「对子进程无意义的应答」都是纵深防御;且本仓的兜底透传是**所有**未识别输入的必经之路,只有它能统一兜住。
|
|
43
|
-
|
|
44
|
-
## 3. 设计
|
|
45
|
-
|
|
46
|
-
### 3.1 模块契约(纯函数,零依赖)
|
|
47
|
-
|
|
48
|
-
新模块 `src/core/terminal-reply-guard.mjs`(与 `terminal-query-sequences.mjs` 同层同风格:纯函数、无 I/O、无定时器、不 import pi-tui)。
|
|
49
|
-
|
|
50
|
-
```js
|
|
51
|
-
export const REPLY_GUARD_WINDOW_MS = 250;
|
|
52
|
-
export const REPLY_GUARD_MAX_ABSORB_BYTES = 64;
|
|
53
|
-
|
|
54
|
-
/** 单事件分类,无状态。 */
|
|
55
|
-
export function classifyReplyEvent(data) => Classification;
|
|
56
|
-
// { kind: "reply-head", family } ← 需要进入暂存态
|
|
57
|
-
// { kind: "reply-complete", family, action: "forward" | "drop" }
|
|
58
|
-
// { kind: "plain" } ← 可能是应答尾部
|
|
59
|
-
// { kind: "other" } ← 键/鼠标/paste 等(非纯文本)
|
|
60
|
-
|
|
61
|
-
/** 纯状态机:不持定时器,时间由调用方注入。 */
|
|
62
|
-
export function createReplyGuard({ windowMs = REPLY_GUARD_WINDOW_MS, maxAbsorbBytes = REPLY_GUARD_MAX_ABSORB_BYTES } = {}) => ({
|
|
63
|
-
feed(data, now) => Decision, // {action:"pass"} | {action:"swallow"} | {action:"forward", data}
|
|
64
|
-
expire(now) => Decision | null, // 窗口到期时调用:{action:"swallow"},此后回到 idle 返回 null
|
|
65
|
-
pendingSince() => number | null, // 供接线层决定是否武装定时器;idle 为 null
|
|
66
|
-
reset() => void,
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
/** 分类表(家族 → 完成形状 → 动作)。 */
|
|
70
|
-
// drop : ESC[?…c (DA1/DA3) · ESC[>…c (DA2) · ESC[?<n>u (kitty flags) · ESC[6;<h>;<w>t (cell size) · ESC P … ST (DCS)
|
|
71
|
-
// forward : ESC[?997;<n>n (color-scheme report;单数字值均接受,子进程解析器自行拒绝未知值) · ESC]4;N;… / ]10;… / ]11;… / ]12;… (BEL|ST)
|
|
72
|
-
```
|
|
73
|
-
|
|
74
|
-
### 3.2 分类与动作表
|
|
75
|
-
|
|
76
|
-
| 家族 | 完成形状 | 动作 | 理由 |
|
|
77
|
-
|---|---|---|---|
|
|
78
|
-
| DA1 / DA3 | `ESC[?<params>c` | **drop** | #128 已决策不转发 kitty/DA;子进程的协商发生在 spawn 时,attach 后收到的 DA 对应答对它无意义 |
|
|
79
|
-
| DA2 | `ESC[><params>c` | **drop** | 同上 |
|
|
80
|
-
| kitty flags | `ESC[?<n>u` | **drop** | 同上(转发会污染共享终端的 kitty 栈顶) |
|
|
81
|
-
| cell size | `ESC[6;<h>;<w>t` | **drop** | XTWINOPS 应答,子进程在 attach 后不消费 |
|
|
82
|
-
| DCS | `ESC P … ST` | **drop** | XTVersion 等,子进程不消费 |
|
|
83
|
-
| color-scheme report | `ESC[?997;<n>n` | **forward(整条)** | #128/#180 链路:子进程的 pi-tui 解析器消费它(正则锚定 `^…$`,必须整条到达)。单数字值(含未知的 `3`)都整条转发:本 guard 不做值域校验,子进程解析器自行拒绝未知值 |
|
|
84
|
-
| OSC 彩色应答 | `ESC]4;N;…`、`ESC]10;…`、`ESC]11;…`、`ESC]12;…`(BEL 或 ST 终止) | **forward(整条)** | #128 的自动主题依赖整条转发(KB `202609232016086714`) |
|
|
85
|
-
| 其它一切 | — | **pass** | 交回 `handleInput` 原链路(mouse → 键位拦截 → 兜底透传),语义不变 |
|
|
86
|
-
|
|
87
|
-
### 3.3 状态机
|
|
88
|
-
|
|
89
|
-
只有两个状态:`idle` 与 `armed`。
|
|
90
|
-
|
|
91
|
-
| 当前态 | 输入 | 下一态 | 动作 |
|
|
92
|
-
|---|---|---|---|
|
|
93
|
-
| idle | `reply-head` | armed(记 `since=now`,缓冲=该事件) | swallow |
|
|
94
|
-
| idle | `reply-complete` | idle | 按动作表:drop → swallow;forward → 整条转发 |
|
|
95
|
-
| idle | `plain` / `other` | idle | pass |
|
|
96
|
-
| armed | `plain` 且 缓冲+事件 ≤ `maxAbsorbBytes` | armed | swallow(追加缓冲) |
|
|
97
|
-
| armed | `plain` 且超上限 | idle | swallow(丢弃缓冲与该事件,**有界**损失) |
|
|
98
|
-
| armed | 拼接后命中 `reply-complete` | idle | 按动作表(`forward` 时转发**拼接后的整条**,`drop` 时 swallow) |
|
|
99
|
-
| armed | **新 `reply-head`(未与缓冲拼接命中)** | armed(重新武装) | 丢弃旧缓冲,以新事件为缓冲并把 `since` 重算为 `now`,swallow |
|
|
100
|
-
| armed | **`reply-complete` 且未与缓冲拼接命中**(自身即完整应答) | idle | 丢弃旧缓冲,按**该事件自身**的动作表处理(`forward` → 整条转发;`drop` → swallow) |
|
|
101
|
-
| armed | `other`(键/鼠标/paste 等) | idle | 丢弃缓冲,然后 **pass 该事件**(保住用户意图) |
|
|
102
|
-
| armed | `expire(now)` 且 `now - since ≥ windowMs` | idle | swallow(丢弃不完整缓冲) |
|
|
103
|
-
|
|
104
|
-
设计要点与理由:
|
|
105
|
-
|
|
106
|
-
1. **窗口只由 `reply-head` 武装**,只有「永不可能是按键/鼠标编码前缀」的形状才武装:`ESC[?`、`ESC[>`、`ESC[6;`、`ESC P`、`ESC]1`、`ESC]4`。`ESC` 单独、`ESC[`、`ESC[5`/`ESC[6`(PageUp/PageDown 头)、`ESC[<`(SGR mouse)、`ESC O`(SS3)**一律不武装**——否则会在 250ms 内吞掉用户的按键。
|
|
107
|
-
2. **250ms 窗口覆盖的是「head 交付之后、尾碎片陆续到达」的跨度**(不是上游阈值):head 到达 `handleInput` 时上游两级超时(50ms + 150ms)已经消耗完,窗口是从 head 到达的那一刻起算,要吸收的是之后各段尾碎片的读间隔总和(上游记录 >50ms/段,4 段证据约 150ms)再留余量。
|
|
108
|
-
3. **上限 64 字节**:真实应答尾 ≤ 32 字节(DA1 尾部 20 字符),上限只用于把「用户快速输入」的损失限定成有界量,避免无界吞输入。
|
|
109
|
-
4. **不完整就丢**:窗口到期仍在 armed → 丢弃。扔掉的是一段永远补不全的应答前缀,对子进程只有害处(半条 OSC 可能被行规程回显成可见字样,正是 #180 记录过的形态)。
|
|
110
|
-
5. **`other` 事件先结束事件再放行**:避免「用户按了 PageUp 却被吞掉」这类不可接受的体验;代价是极端情况下把应答尾留下一小部分(用户按键导致 state 结束,后续尾巴变成 pass → 落进兜底透传)。这是**有意的边界**:宁可少吞一点垃圾,不可吞用户按键。
|
|
111
|
-
|
|
112
|
-
6. **近失形状的归属**(R2-D3):完整但参数为空的 DA(`ESC[?c`)按完成形状处理(drop);不完整前缀(`ESC[?1;`、`ESC[6;`、`ESC[6;24;80`、`ESC]11;`、`ESC P`)一律武装(swallow);未知第三值的 997 报告按 forward 家族整条转发。这三类由 A1/A4 的「近失形状」清单锁死,不得靠实现者自由发挥。
|
|
113
|
-
|
|
114
|
-
7. **修正(issue #213)**:第 1 条「`ESC[6;` 永不可能是按键前缀」已被推翻 —— kitty 功能键编码
|
|
115
|
-
把 PageDown 放在 `CSI 6;<mods>u`(modifyOtherKeys 为 `\x1b[6;<mods>~`),与 cell-size 的
|
|
116
|
-
`ESC[6;` 共享前缀。「完整 CSI 键序列优先」(#213)是现行规则:以 `u`/`~` 终结的完整键序列
|
|
117
|
-
先于 head 前缀判定,不带终结符的片段仍然武装。实现见 `CSI_KEY_SEQUENCE`。
|
|
118
|
-
|
|
119
|
-
### 3.4 接线点
|
|
120
|
-
|
|
121
|
-
`src/ui/pty-attach.ts` 的 `handleInput(data)` **最前面**(在既有 mouse/key 拦截之前):
|
|
122
|
-
|
|
123
|
-
```ts
|
|
124
|
-
handleInput(data: string): void {
|
|
125
|
-
const decision = this.replyGuard.feed(data, Date.now());
|
|
126
|
-
if (decision.action === "forward") { this.send({ type: "input", data: decision.data }); return; }
|
|
127
|
-
if (decision.action === "swallow") return;
|
|
128
|
-
// ...既有逻辑(parseMouseInputChunk → PageUp/Down/Home/End → Ctrl+\ → Ctrl+← → ← → 兜底透传)原样不动
|
|
129
|
-
}
|
|
130
|
-
```
|
|
131
|
-
|
|
132
|
-
- 定时器:`feed` 返回后若 `pendingSince() !== null` 则武装一个 `windowMs` 定时器(重复 feed 不重复武装),回调里 `expire(Date.now())`;`close()`/`done()` 走 `reset()` 并清定时器(与 `detachColorSchemeBridge` 同样的清理纪律)。
|
|
133
|
-
- kill switch:`AGENT_BOARD_REPLY_GUARD=0`(也接受 `off`/`false`)时**不创建 guard**,`handleInput` 走原逻辑(guard 为 null 即跳过),语义等价于改动前。该开关必须在 README 登记(与 `AGENT_BOARD_FORWARD_TERMINAL_QUERIES` 同处:环境变量表行或配置段落),验收项见 A9。
|
|
134
|
-
- **与 #180 无重叠**:#180 的 `childInputReady` 门在 `deliverSettleScheme`(send 侧、出站),guard 在入口侧(入站);两者不共享状态。
|
|
135
|
-
- **与 #128 的既有转发是加法关系**:guard 的 `forward` 走与今天兜底完全相同的 `send({type:"input"})`,只是多了一条「碎片拼接后整条转发」的路径。
|
|
136
|
-
|
|
137
|
-
### 3.5 决策表(被否方案)
|
|
138
|
-
|
|
139
|
-
| 方案 | 否决理由 |
|
|
140
|
-
|---|---|
|
|
141
|
-
| 把兜底改成白名单(只放行可打印字符) | 会挡掉 Alt 组合、SS3、kitty/modifyOtherKeys 修饰键等合法输入;`ESC[27;5;92~` 这类是用户按键而非应答 |
|
|
142
|
-
| 只丢弃 `ESC` 开头的未知序列,不管尾碎片 | 尾碎片(真凶)恰恰不以 `ESC` 开头 |
|
|
143
|
-
| 在子进程侧治理(如往子 PTY 写前过滤) | 子进程是任意程序(zsh 也中招),本仓无法控制 |
|
|
144
|
-
| patch `node_modules` 里的 pi-tui | patch 机制已退役,升级即失效 |
|
|
145
|
-
| 只文档化 + 等上游 #10657 | 上游 OPEN 且无时间表;护栏是纵深防御,不与上游修复冲突 |
|
|
146
|
-
| 把窗口做成「永久吸收直到超时无上限」 | 无界吞输入不可接受;有界损失 + kill switch 是更小的恶 |
|
|
147
|
-
|
|
148
|
-
## 4. 边界与非目标
|
|
149
|
-
|
|
150
|
-
**明确不做**:不修上游(不动 `node_modules`);不改本地 `queryTerminalColors` 的超时行为(它的超时是上游碎片化导致的**结果**,本改动不声称修复它);不做应答合成/重放;除一个需在 README 登记的 kill switch 外不引入新配置项。
|
|
151
|
-
|
|
152
|
-
**已知边界**:窗口内的快速打字可能被吞(≤64 字节,250ms,可 kill switch 关闭);**尾碎片晚于窗口到达时仍会漏**——窗口从 head 到达起算,若终端读间隔异常拉长(更慢的 ConPTY、网络终端、重负载),最后一段尾碎片可能落在窗口之外并被兜底透传进子进程;U1 的 ≥5 次 attach 观察就是这条边界的探测手段(属于「降低发生率」而非「数学上消除」);DSR/CPR(`ESC[<r>;<c>R`)家族**不在本次范围**——它需要「`ESC[` + 数字」这类与按键编码(`ESC[6~`、`ESC[27;5;92~`)共享前缀的武装规则,风险与本改动不同量级,另案处理(本次先记录)。`ESC[` 单独作为 negotiation 前缀是 pi-tui 自己的行为,本 guard 不重复。
|
|
153
|
-
|
|
154
|
-
## 5. 验收矩阵
|
|
155
|
-
|
|
156
|
-
**U 项共享部署前置(R1-G2)**:先让被测 pi 会话真的加载本改动——按 `VERIFY.md:55` 的方式 `pi install "$(pwd)"`(指向含本改动的 checkout;合并发布后可改 `pi install npm:@zhuxixi/pi-agent-board`)→ 重启 pi,或按 `VERIFY.md:101` 的 `/reload` → 确认扩展加载无报错、`/agent-board` 可打开 → 再执行 U1–U3。可执行时机:worktree 实现完成且 `npm run verify` 通过后在合并前试点;合并发布后可复验。缺这一步会在旧安装上得到假阴性「通过」。
|
|
157
|
-
|
|
158
|
-
| ID | 功能点 | 验收方式 | 具体验证 | 通过标准 |
|
|
159
|
-
|---|---|---|---|---|
|
|
160
|
-
| A1 | 完整应答形状的分类与动作 | 自动化(unit) | `node --test --test-name-pattern="A1" test/terminal-reply-guard.test.mjs` | drop 家族(DA1/DA2/DA3、kitty flags、cell size、DCS)全部返回 drop;forward 家族(997 报告、OSC 4/10/11/12 的 BEL 与 ST 两种终止)返回 forward。**近失形状**:空参 DA1 `ESC[?c` → drop;未知第三值报告 `ESC[?997;3n` → forward;完整但未知的 CSI(如 `ESC[1;2R`)→ pass(不误判为任何家族)(R2-D3) |
|
|
161
|
-
| A2 | 碎片重组:4×8 字节 DA1 | 自动化(unit) | `node --test --test-name-pattern="A2" …` | 四段依次 feed 后:前三段全部 swallow、第四段 swallow,`sent`/forward 次数为 0;缓冲与状态回到 idle |
|
|
162
|
-
| A3 | 窗口语义、有界损失与 armed 态的按键放行 | 自动化(unit) | `node --test --test-name-pattern="A3" …` | ① 窗口未到期的 `plain` 被吸收;② 到期 `expire` 丢弃并回 idle;③ 超上限时丢弃并回 idle;④ `reset()` 立即回 idle 且不带残留;⑤ 两次独立事件不串味;⑥ **armed 态收到 `other`**(`ESC[5~`、`ESC[<35;20;5M`、`ESC[200~…ESC[201~`)→ 该事件 **pass**、缓冲被丢弃、`pendingSince()` 回 null、其后的 `plain` 不再被吸收(R1-G1);⑦ **armed 态收到新的 `reply-head`** → 保持 armed 且 `since` 以新事件重算,旧缓冲不再参与拼接(R1-D2);⑧ **armed 态收到「自身完整但未与缓冲拼接命中」的 `reply-complete`**(如 250ms 内整条到达的 OSC 11 回复)→ 旧缓冲被丢弃、按该事件自身动作表处理(forward 时逐字节断言内容相等)、回 idle、`pendingSince()` 回 null(R2-D2) |
|
|
163
|
-
| A4 | 非应答输入不被吞(idle 态基线) | 自动化(unit) | `node --test --test-name-pattern="A4" …` | `ESC[5~`、`ESC[6~`、`ESC[H`、`ESC[F`、`ESC[3~`、`ESC[<35;20;5M`、`ESCOA`、`ESCOB`、`ESC`、`ESC`+字母、`ESC[27;5;92~`、`ESC[92;5u`、`ESC[200~`、普通文本 全部 pass;且这些事件之后 `pendingSince()` 仍为 null(未被武装)。本项是 idle 态基线,armed 态下的同类事件见 A3⑥。**近失前缀**:`ESC[?1;`、`ESC[6;`、`ESC[6;24;80`、`ESC]11;`、`ESC P` 全部武装(`pendingSince()` 非 null)且不发任何帧;而完整的非应答 CSI(`ESC[1;2R`)pass 且不武装(R2-D3) |
|
|
164
|
-
| A5 | 彩色应答整条转发(含碎片形态) | 自动化(unit) | `node --test --test-name-pattern="A5" …` | 完整 OSC 11 回复(BEL/ST)与 `ESC[?997;1n` / `ESC[?997;2n` → forward 且内容逐字节相等;碎片化的 OSC 11 回复被拼接后整条 forward |
|
|
165
|
-
| A6 | 组件接线:`sent` 帧里不再有垃圾 | 自动化(component smoke) | `node --test test/pty-attach-reply-guard.test.mjs` | 把 4×8 字节 DA1 经真实 `handleInput` 喂入:捕获的 `input` 帧为 0(改动前会捕获到 4 帧);喂入 OSC 11 回复:捕获到 1 帧且逐字节相等 |
|
|
166
|
-
| A7 | kill switch 与清理 | 自动化(component smoke + unit) | `node --test test/pty-attach-reply-guard.test.mjs` | `AGENT_BOARD_REPLY_GUARD=0` 时 4×8 字节 DA1 全部照旧透传(捕获 4 帧);`close()` 后残留定时器不再产生任何 `send` |
|
|
167
|
-
| A8 | 静态与回归 | 自动化(static + 全量) | `npm run typecheck && npm test` | 「类型检查通过」且「既有测试全绿(无回归)」 |
|
|
168
|
-
| A9 | kill switch 的可发现性(文档契约) | 自动化(static) | `rg -n "AGENT_BOARD_REPLY_GUARD" README.md` 且人工核对落点 | **硬性落点**:`README.md` 的 `## Configuration` 段(:302 起)变量表新增一行(默认启用;`0`/`off`/`false` 关闭;写明 ≤64 字节 / 250ms 吞入边界与逃生场景);功能散文里另提可作补充但不满足本条。**不得**以 `AGENT_BOARD_FORWARD_TERMINAL_QUERIES` 为锚定先例——后者只登记在功能散文(`README.md:96`、`:203`),不在 Configuration 表内,而表内已含其余全部布尔开关(R1-D3 实质 + R2-D1 修正) |
|
|
169
|
-
| U1 | 真机:attach 不再出现碎片垃圾 | 用户实测 | 在会慢速应答的终端(WezTerm+WSL2/ConPTY、mintty 等)attach 进一个 pi 子进程与一个 zsh 子进程,观察 composer/提示行 | 连续 attach ≥5 次,子进程输入框**不出现** `c7c` 一类的字面碎片 |
|
|
170
|
-
| U2 | 真机:自动主题无回归 | 用户实测 | 浅色终端里 attach 一个配 `theme: light-warm/dark` 的子进程 | 子进程主题随终端亮暗正确;`attach_settle_scheme` 诊断仍有 outcome(允许 timeout 形态保留,超时不是本次验收目标) |
|
|
171
|
-
| U3 | 真机:输入可用性无回归 | 用户实测 | attach 后依次执行:正常打字、PageUp/PageDown 滚动、中键粘贴、拖选拷贝、250ms 内快速连打 10 个字符 | 五类操作行为与改动前一致;快速连打最多损失不超过 64 字节(不符合则记录实际形态) |
|
|
172
|
-
|
|
173
|
-
## 6. 可测性拆分设计
|
|
174
|
-
|
|
175
|
-
判定逻辑与副作用严格分离,测试边界如下:
|
|
176
|
-
|
|
177
|
-
| 单元 | 形态 | 测什么 | 不测什么 |
|
|
178
|
-
|---|---|---|---|
|
|
179
|
-
| `classifyReplyEvent(data)` | 纯函数(无状态、无时间) | 单事件分类与动作(A1、A4 的 pass 侧、A5 的完整形态) | 状态、窗口、转发 |
|
|
180
|
-
| `createReplyGuard().feed/expire/pendingSince/reset` | 纯状态机(时间由参数注入) | 碎片重组、窗口、上限、reset(A2、A3、A5 的碎片形态) | 定时器、socket、UI |
|
|
181
|
-
| `PtyAttachComponent.handleInput` 接线 | 薄接线(三行分支 + 定时器武装) | 三态决策是否正确落到 `send`/return(A6、A7) | 字节矩阵(已在纯模块层覆盖) |
|
|
182
|
-
|
|
183
|
-
- 因此组件层 smoke **不重复**纯模块的字节矩阵:它只验「四个碎片 → 0 帧」与「OSC 回复 → 1 帧」两条端到端事实,加 kill switch 与 close 清理。
|
|
184
|
-
- 测试基座(模板):纯模块层用 `test/terminal-reply-guard.test.mjs`(风格照 `test/terminal-query-sequences.test.mjs`);组件层用 `test-support/attach-reply-guard-smoke.ts` + `test/pty-attach-reply-guard.test.mjs`(形态照 #180 的 `attach-scheme-readiness-smoke.ts` + `test/pty-attach-scheme-readiness.test.mjs`:真实组件 + 只替换 `send`/`connected`,`execFileSync('--experimental-transform-types')` 取回 JSON)。
|
|
185
|
-
- fixture 基准:上游 #10657 记录的真实 28 字节 DA1 与 4×8 切分;合成变体(DA2 `ESC[>41;330;0c`、单字符尾、`ESC[?997;2n`、OSC 11 BEL/ST、`ESC[6;24;80t`)。**全部字节字面量写死在测试里**(生产代码不得 import pi-tui)。
|
|
186
|
-
|
|
187
|
-
## 7. 风险与回滚
|
|
188
|
-
|
|
189
|
-
| 风险 | 缓解 |
|
|
190
|
-
|---|---|
|
|
191
|
-
| 窗口内吞掉用户输入(最坏 64 字节 / 250ms) | 有界 + 只在应答头之后 + `AGENT_BOARD_REPLY_GUARD=0` kill switch;U3 真机确认 |
|
|
192
|
-
| 误判导致彩色应答被丢 → 自动主题失效 | forward 家族单测锁死(A5)+ U2 真机确认;kill switch 兜底 |
|
|
193
|
-
| 半条应答在前缀判定上开洞(如 `ESC[?1;` 被判成 drop 家族混入) | 分类表以「完成形状」为准,前缀只用于武装不用于动作;近失形状(空参 DA、未知 997 值、不完整前缀、完整但未知的 CSI)逐条列入 A1/A4 并写明期望分类(R2-D3) |
|
|
194
|
-
| 与上游将来修复叠加产生双重处理 | 上游修复发生在 pi-tui 内部(更早的层),本 guard 只在「上游已放弃」的碎片上生效,两者叠加时 guard 无输入可处理,无冲突 |
|
|
195
|
-
| 定时器泄漏 | A7 覆盖 close 路径;接线层与 `detachColorSchemeBridge` 同纪律 |
|