@zhuxixi/pi-agent-board 0.5.0 → 0.5.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. package/PROGRESS.md +18 -3
  2. package/README.md +298 -76
  3. package/VERIFY.md +3 -3
  4. package/docs/PTY_ATTACH_IMPLEMENTATION_PLAN.md +3 -3
  5. package/docs/superpowers/plans/2026-08-30-circular-navigation.md +308 -0
  6. package/docs/superpowers/plans/2026-08-30-pty-attach-quality-debt.md +311 -0
  7. package/docs/superpowers/plans/2026-08-30-readme-v2.md +294 -0
  8. package/docs/superpowers/plans/2026-09-01-attach-detach-gate-cursor-anchor.md +284 -0
  9. package/docs/superpowers/plans/2026-09-02-attach-detach-editor-state.md +722 -0
  10. package/docs/superpowers/plans/2026-09-03-detach-gate-glyph-fallback.md +146 -0
  11. package/docs/superpowers/specs/2026-08-21-attach-coldstart-jiggle-rearm-design.md +4 -0
  12. package/docs/superpowers/specs/2026-08-22-jiggle-shrink-and-hold-design.md +4 -0
  13. package/docs/superpowers/specs/2026-08-30-circular-navigation-design.md +47 -0
  14. package/docs/superpowers/specs/2026-08-30-pty-attach-quality-debt-design.md +105 -0
  15. package/docs/superpowers/specs/2026-08-30-readme-v2-design.md +115 -0
  16. package/docs/superpowers/specs/2026-09-01-attach-detach-gate-cursor-anchor-design.md +78 -0
  17. package/docs/superpowers/specs/2026-09-02-attach-detach-editor-state-design.md +120 -0
  18. package/docs/superpowers/specs/2026-09-03-detach-gate-glyph-fallback-design.md +99 -0
  19. package/package.json +1 -1
  20. package/runner/pty-runner.mjs +64 -17
  21. package/src/core/code-refs-store.mjs +3 -0
  22. package/src/core/editor-state-reporter.mjs +102 -0
  23. package/src/core/launch.mjs +6 -0
  24. package/src/core/pty-attach-jiggle-controller.mjs +71 -17
  25. package/src/core/pty-input.mjs +32 -0
  26. package/src/core/pty-scroll.mjs +4 -3
  27. package/src/core/repo.mjs +3 -0
  28. package/src/core/worktree.mjs +1 -0
  29. package/src/index.ts +12 -1
  30. package/src/ui/dashboard.ts +6 -2
  31. package/src/ui/pty-attach.ts +148 -32
@@ -0,0 +1,146 @@
1
+ # Detach-Gate Tier-2 Glyph Fallback Tightening 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:** Tighten the attach-surface `←` detach gate's tier-2 glyph fallback so content glyph lines (markdown table rows / quotes / drafts) no longer trap the user when `editor_state` is unavailable (issue #69).
6
+
7
+ **Architecture:** One-line condition change in `PtyAttachComponent.childInputLooksEmpty()` (src/ui/pty-attach.ts): the tier-2 loop now only treats an EMPTY prompt-glyph line as proof of an empty editor (`isProbablyPiInputLine(line) && isProbablyEmptyPiInputLine(line)`), skipping content glyph lines; loop-end escape stays authoritative. Tier-1 (inverse fake-cursor anchor), the `editor_state` authoritative path (#71), and all protocol/public API surfaces are untouched. Behavior is pinned by two new detach-gate smoke scenarios (K1/K2) asserted inside the existing `test/pty-attach-detach-gate.test.mjs` case.
8
+
9
+ **Tech Stack:** TypeScript (Node `--experimental-transform-types`), node:test, @xterm/headless in-memory smoke harness.
10
+
11
+ **Spec:** `docs/superpowers/specs/2026-09-03-detach-gate-glyph-fallback-design.md` (accepted 2026-09-03, Option B).
12
+
13
+ **Acceptance traceability:** Task 1 → A1, A2, A3, A4 (automated). Task 2 → U1, U2 (manual, human-executed after push, before merge). No orphan tasks, no orphan acceptance IDs.
14
+
15
+ ## Global Constraints
16
+
17
+ - Work only inside this worktree: `/home/elling/git-repo/github/pi-agent-board/.pi/worktrees/issue-69-detach-gate-glyph-fallback`. Never edit the main checkout at `/home/elling/git-repo/github/pi-agent-board` (`node_modules` there is only the read-only symlink target).
18
+ - Commit messages: English, conventional commits, suffix `(issue #69)`.
19
+ - Stage files explicitly (`git add <file> ...`); never `git add -A`.
20
+ - Do NOT touch: tier-1 inverse anchor, `editor_state` path (`resolveEditorEmpty`, reporter, runner), README.md, or the pure helpers in `src/core/pty-input.mjs` (spec non-goals).
21
+ - Do NOT extract new pure functions — the tightened tier-2 is constant-true by design; the discriminating tests live at smoke level (spec §4).
22
+ - Every new online-gate smoke scenario MUST pin `connected=true` explicitly before `handleInput` (B-section convention; otherwise the disconnected-escape semantics fire instead of the online gate).
23
+ - `node --test` case count stays 437: the new smoke keys are assertions inside the existing test case in `test/pty-attach-detach-gate.test.mjs`, not new `test()` blocks.
24
+ - Verification commands: `npm test` (expect 437 pass, 0 fail) and `npm run typecheck` (expect 0 errors). Faster smoke-only loop: `node --experimental-transform-types test-support/detach-gate-smoke.ts` (prints a JSON of scenario keys).
25
+
26
+ ---
27
+
28
+ ### Task 1: Tighten tier-2 glyph fallback (TDD) — covers A1, A2, A3, A4
29
+
30
+ **Files:**
31
+ - Modify: `src/ui/pty-attach.ts` (the tier-2 loop inside `childInputLooksEmpty()`, right after the `// Fallback: Pi variants that render no fake cursor` comment, line 359)
32
+ - Test: `test-support/detach-gate-smoke.ts` (insert two scenarios after the J-scenario block, before the `// E2.` comment)
33
+ - Test: `test/pty-attach-detach-gate.test.mjs` (two assertions after the `leftHelloNullResetsStaleEditorState` assertion)
34
+
35
+ **Interfaces:**
36
+ - Consumes: `isProbablyPiInputLine(line)`, `isProbablyEmptyPiInputLine(line)` from `src/core/pty-input.mjs` (both already imported in `pty-attach.ts`); smoke harness helpers `makeAttach()`, `writeToTerm(attach, data)` (already defined in the smoke file).
37
+ - Produces: smoke output keys `leftDetachesOnTableRowsWithoutFakeCursor` (A1) and `leftDetachesOnContentGlyphFallback` (A2) in the JSON printed by `test-support/detach-gate-smoke.ts`.
38
+
39
+ - [ ] **Step 1: Write the failing smoke scenarios (A1, A2)**
40
+
41
+ In `test-support/detach-gate-smoke.ts`, insert these two blocks immediately BEFORE the line starting with `// E2. A terminal at the minimum supported size`:
42
+
43
+ ```ts
44
+ // K1. Issue #69 real-world shape: zero inverse cells anywhere in the buffer,
45
+ // the chat area carries a markdown table row (`│ … │`) and a quote line
46
+ // (`> …`) that isProbablyPiInputLine misreads as a draft-bearing input line,
47
+ // and editor_state never arrives (editorEmpty stays null — child without the
48
+ // reporter). tier-2 must skip content glyph lines and ← must detach: the
49
+ // gate philosophy is "never trap the user" (issues #42/#48).
50
+ {
51
+ const { attach, sent, didDetach } = makeAttach();
52
+ await writeToTerm(attach, "chat content\r\n│ Issue #778 │ open │\r\n> quote line\r\n");
53
+ (attach as unknown as { connected: boolean }).connected = true;
54
+ attach.handleInput("\x1b[D");
55
+ out.leftDetachesOnTableRowsWithoutFakeCursor = didDetach() && sent.length === 1 && sent[0].type === "detach";
56
+ attach.dispose();
57
+ }
58
+
59
+ // K2. The deliberate flip side of K1 — pair with scenario B: the SAME draft
60
+ // shape (`> draft`) is gated when the fake cursor is present (tier-1, scenario
61
+ // B) but detaches when the buffer carries no inverse cells (tier-2 fallback
62
+ // cannot tell a real draft from a table row; a spurious detach beats a trapped
63
+ // user, and detach never loses the draft — the child session keeps running).
64
+ // This pins the intentional loss of fallback draft protection (issue #69);
65
+ // restoring it needs the mid-term dock-structure anchor, not a revert.
66
+ {
67
+ const { attach, sent, didDetach } = makeAttach();
68
+ await writeToTerm(attach, "chat content\r\n> draft\r\n");
69
+ (attach as unknown as { connected: boolean }).connected = true;
70
+ attach.handleInput("\x1b[D");
71
+ out.leftDetachesOnContentGlyphFallback = didDetach() && sent.length === 1 && sent[0].type === "detach";
72
+ attach.dispose();
73
+ }
74
+ ```
75
+
76
+ In `test/pty-attach-detach-gate.test.mjs`, insert these two assertions immediately AFTER the `assert.equal(parsed.leftHelloNullResetsStaleEditorState, true, ...)` line:
77
+
78
+ ```js
79
+ assert.equal(parsed.leftDetachesOnTableRowsWithoutFakeCursor, true, "← must detach when a zero-inverse buffer holds only table/quote glyph lines and editor_state is unknown (issue #69)");
80
+ assert.equal(parsed.leftDetachesOnContentGlyphFallback, true, "← must detach on a content glyph line in the no-fake-cursor fallback — spurious detach beats trapping (issue #69)");
81
+ ```
82
+
83
+ - [ ] **Step 2: Run to verify both fail (A1, A2 red)**
84
+
85
+ Run: `node --experimental-transform-types test-support/detach-gate-smoke.ts`
86
+ Expected: JSON contains `"leftDetachesOnTableRowsWithoutFakeCursor":false` and `"leftDetachesOnContentGlyphFallback":false` (all pre-existing keys stay `true`).
87
+
88
+ - [ ] **Step 3: Implement the tier-2 tightening (minimal change)**
89
+
90
+ In `src/ui/pty-attach.ts`, inside `childInputLooksEmpty()`, replace this exact block:
91
+
92
+ ```ts
93
+ // Fallback: Pi variants that render no fake cursor — look for a
94
+ // prompt-glyph line.
95
+ for (let y = active.baseY + active.length - 1; y >= active.baseY; y--) {
96
+ const line = active.getLine(y)?.translateToString(true) ?? "";
97
+ if (isProbablyPiInputLine(line)) return isProbablyEmptyPiInputLine(line);
98
+ }
99
+ ```
100
+
101
+ with:
102
+
103
+ ```ts
104
+ // Fallback: Pi variants that render no fake cursor — look for an EMPTY
105
+ // prompt-glyph line. Only an empty glyph line proves an empty editor:
106
+ // content glyph lines (markdown table rows `│ … │`, quotes `> …`, or a
107
+ // real draft in a no-fake-cursor Pi variant) cannot be told apart, and
108
+ // trapping the user is worse than a spurious detach (issue #69) — skip
109
+ // them and keep scanning; the loop-end escape below stays authoritative.
110
+ for (let y = active.baseY + active.length - 1; y >= active.baseY; y--) {
111
+ const line = active.getLine(y)?.translateToString(true) ?? "";
112
+ if (isProbablyPiInputLine(line) && isProbablyEmptyPiInputLine(line)) return true;
113
+ }
114
+ ```
115
+
116
+ - [ ] **Step 4: Run smoke to verify the fix (A1, A2 green)**
117
+
118
+ Run: `node --experimental-transform-types test-support/detach-gate-smoke.ts`
119
+ Expected: EVERY key in the JSON is `true`, including the two new ones.
120
+
121
+ - [ ] **Step 5: Full automated acceptance (A3, A4)**
122
+
123
+ Run: `npm test`
124
+ Expected: `pass 437`, `fail 0` (count unchanged — the new keys are assertions inside the existing test case).
125
+
126
+ Run: `npm run typecheck`
127
+ Expected: exit 0, no output errors.
128
+
129
+ - [ ] **Step 6: Commit**
130
+
131
+ ```bash
132
+ git add src/ui/pty-attach.ts test-support/detach-gate-smoke.ts test/pty-attach-detach-gate.test.mjs
133
+ git commit -m "fix: tighten ← detach tier-2 glyph fallback to empty-only lines (issue #69)"
134
+ ```
135
+
136
+ ### Task 2: Manual verification U1/U2 (human-executed; after PR branch is pushed, before merge) — covers U1, U2
137
+
138
+ > Not a subagent task. The controller surfaces this checklist to the human partner at the pre-merge gate.
139
+
140
+ **Environment:** the running extension copy is the git-source clone at `~/.pi/agent/git/github.com/zhuxixi/pi-agent-board` (currently at 0.5.1 / fac9e91). Steps:
141
+
142
+ - [ ] **Step 1:** `git -C ~/.pi/agent/git/github.com/zhuxixi/pi-agent-board fetch origin && git -C ~/.pi/agent/git/github.com/zhuxixi/pi-agent-board checkout issue-69-detach-gate-glyph-fallback`
143
+ - [ ] **Step 2:** Restart pi IN ANOTHER TERMINAL (restarting kills the session running this agent — do not run inside the controlling session's pi instance).
144
+ - [ ] **Step 3 (U1):** Open dashboard → attach a warm session whose chat area contains markdown table output (`│ … │` rows) → with an empty input box press `←`. Pass = returns to dashboard, not trapped.
145
+ - [ ] **Step 4 (U2):** In the same or another reporter-active hosted session, type a draft → press `←`. Pass = cursor moves left inside the draft, no detach (same as #67 U3).
146
+ - [ ] **Step 5:** `git -C ~/.pi/agent/git/github.com/zhuxixi/pi-agent-board checkout main` and restart pi to restore the running copy.
@@ -1,5 +1,9 @@
1
1
  # Spec:attach 冷启动双光标根治 — jiggle 重试链编排修复(issue #10)
2
2
 
3
+ > **历史设计,已被 issue #25 的 shrink-and-hold 及 issue #42 的 G6
4
+ > post-restore verify supersede。** 当前实现和测试以
5
+ > `src/core/pty-attach-jiggle-controller.mjs` 为准。
6
+
3
7
  ## 日期
4
8
  2026-08-21(v2:评审后修订,补三项编排严谨性修正)
5
9
 
@@ -1,5 +1,9 @@
1
1
  # Spec:attach jiggle 协议改造 — shrink-and-hold(issue #25)
2
2
 
3
+ > **历史设计,部分内容已被 issue #42 superseded。** 当前实现保留 G1–G5,
4
+ > 并在首帧快速 restore 后增加 G6 post-restore verify:900ms 内没有 clear
5
+ > 时重新 shrink;attach detach 只使用空输入时的 `←`,`ctrl+]` 透传给 Pi。
6
+
3
7
  ## 日期
4
8
  2026-08-22
5
9
 
@@ -0,0 +1,47 @@
1
+ # Spec: dashboard 列表方向键导航循环回绕(issue #52)
2
+
3
+ 日期:2026-08-30 · 状态:draft(等用户确认)
4
+
5
+ ## 根因报告
6
+
7
+ **现象**:dashboard 主列表 ↑/↓ 移动选中项,到达首/尾边界后按键无效,无法循环。
8
+
9
+ **根因**(已通过代码直读确认,systematic-debugging Phase 1 完成):
10
+ `src/ui/dashboard.ts` `moveSelection()`(L250-258)用 `Math.max(0, Math.min(len-1, ...))` 钳制新索引,而非取模回绕。该写法来自 MVP commit `72c0d8f`,非有意设计。
11
+
12
+ **调用链**:`handleListKey`(L334-335,normal 模式)与 `handleSelectKey`(L370-371,multi-select 模式)→ `moveSelection(±1)`。
13
+
14
+ ## 设计
15
+
16
+ ### 改动点
17
+
18
+ | # | 位置 | 改动 |
19
+ |---|------|------|
20
+ | 1 | `moveSelection()` L250-258 | 钳制 → 取模回绕:`next = ((base + delta) % len + len) % len` |
21
+ | 2 | `peekStep()` L1094-1101 | 同上,保持与主列表一致 |
22
+
23
+ ### 决策表
24
+
25
+ | 决策点 | 决定 | 理由 |
26
+ |--------|------|------|
27
+ | `cur < 0`(selectedId 不在列表中) | 取模语义:按 ↓ 得 index 1(第二条,与旧实现一致);按 ↑ 得 index len-1(最后一条,**与旧实现不同**——旧钳制得第一条)。有意为之:与回绕语义一致;该路径实际不可达(`refresh()` 不变量保证按键处理时 selectedId ∈ orderedIds,或 selectedId=null 走 base 0) | 修正记录(Zima CR round 1):旧版写"保持现状语义"不属实,↑ 方向兜底行为确有变化 |
28
+ | 单条列表(len=1) | 取模后索引恒为 0,`nextId === selectedId` early return 挡掉无效更新 | 现有兜底继续生效,无需特判 |
29
+ | 空列表 | 现有 `length === 0` early return 保留 | 不变 |
30
+ | 滚动跟随 | **不改**——`windowBody()` 渲染时强制选中行可见,方向无关,回绕自动跟随 | 已验证 |
31
+ | 按键热路径性能 | 改动只涉及一次取模运算,不影响 #9/PR #12 的 prewarm debounce 设计 | 调研结论 |
32
+ | launch picker(cwd/model/thinking,L539-547) | **本 issue 不改**(非目标),另开 issue 跟踪 | 弹窗内短列表,回绕收益低;避免一次 PR 混两个行为变更 |
33
+
34
+ ### 非目标
35
+
36
+ - launch 对话框 picker 的回绕(另议)
37
+ - 任何渲染层、滚动层改动
38
+ - 其他模式的按键行为
39
+
40
+ ### 测试
41
+
42
+ 目前 `moveSelection`/`peekStep` 无测试覆盖。补一个针对 Dashboard 的轻量测试(参考 `test/ui-smoke.test.mjs` 的实例化方式):构造 3 条 orderedIds,断言 尾→↓→首、首→↑→尾 的回绕行为,以及单条/空列表不炸。
43
+
44
+ ## 验证
45
+
46
+ 1. `npm test` 全绿(含新增用例)
47
+ 2. 手动跑 dashboard:多 session 列表,尾按 ↓ 回首、首按 ↑ 回尾,peek 模式同样验证
@@ -0,0 +1,105 @@
1
+ # Spec: pty-attach.ts legacy quality debt cleanup (issue #8)
2
+
3
+ - **Date**: 2026-08-30
4
+ - **Issue**: zhuxixi/pi-agent-board#8
5
+ - **Type**: chore — zero behavior change (comments + signature trim only)
6
+ - **Status**: approved and implemented (final review clean; see docs/superpowers/plans/2026-08-30-pty-attach-quality-debt.md)
7
+
8
+ ## Background
9
+
10
+ pi-lens flagged 10 legacy quality issues in `src/ui/pty-attach.ts` on 2026-08-21
11
+ (upstream original author's style, pre-existing). The issue tracked them for
12
+ separate cleanup so they wouldn't pollute feature PRs. Since then, #45 and #48
13
+ modified the file, so every line number in the issue has drifted. A fresh scan
14
+ of current main (1324 lines) found:
15
+
16
+ - **9 empty `catch {}` blocks** (issue said 8): L484, 510, 724, 745, 755, 766,
17
+ 924, 951, 1010
18
+ - **1 `as unknown as` cast without a SAFETY comment**: L795 (`currentSize()`)
19
+ - **1 unused parameter**: L970 (`project(height, width)` — `width` never read)
20
+
21
+ Two other catches are out of scope: L902 (`onSocketData`, multi-line catch with
22
+ an explanatory comment) and L1048 (`openExternalTarget`, catch returns `false`).
23
+
24
+ ## Goals
25
+
26
+ 1. Every empty catch documents *why* silence is correct (intentional vs forgot).
27
+ 2. The as-cast states the invariant that makes it safe.
28
+ 3. No unused parameters in `project()`.
29
+ 4. Zero behavior change: no logic, no logging, no reformatting beyond the edits.
30
+
31
+ ## Non-goals
32
+
33
+ - No logging infrastructure (no logger is imported in pty-attach today; these
34
+ failures have no consumer; `forwardTerminalProtocols` runs at frame frequency
35
+ and would spam).
36
+ - No changes outside `src/ui/pty-attach.ts`.
37
+ - No touching L902/L1048 (already documented/behavioral).
38
+ - No drive-by refactors of nearby code.
39
+
40
+ ## Decision: empty catches stay silent + explanatory comment (Option A)
41
+
42
+ Rejected alternative (Option B): debug-level logging — needs new UI-layer log
43
+ plumbing, has no consumer, and high-frequency paths would flood output.
44
+
45
+ Precedent in this repo: `dashboard.ts` uses `/* best effort: stats must never
46
+ block dispatch */` style comments for the same pattern.
47
+
48
+ Per-site comment text (implementation is mechanical):
49
+
50
+ | Line | Method | Failure tolerated | Comment to add |
51
+ |------|--------|-------------------|----------------|
52
+ | 484 | `enableMouseScroll()` | `terminal.write(XTSHIFTESCAPE/MOUSE_ENABLE)` | `/* best-effort: some terminals reject these sequences; mouse reporting is optional */` |
53
+ | 510 | `disableMouseScroll()` | `terminal.write(MOUSE_DISABLE)` | `/* best-effort: terminal may already be gone at teardown */` |
54
+ | 724 | `copySelectionToClipboard()` | OSC52 write | `/* best-effort: OSC52 clipboard support is optional */` |
55
+ | 745 | `pastePrimarySelection()` inner timer | `child.kill("SIGKILL")` | `/* the child may have already exited before the timeout fired */` |
56
+ | 755 | `pastePrimarySelection()` outer | `spawn("xclip")` | `/* silent no-op when xclip is absent — documented contract of this helper */` |
57
+ | 766 | `writePrimarySelection()` | `spawn("xclip")` | `/* silent no-op when xclip is absent */` |
58
+ | 924 | `forwardTerminalProtocols()` | per-sequence `terminal.write` | `/* best-effort: forwarded sequences are enhancements, never critical */` |
59
+ | 951 | `replayScreenLog()` | screen.log read/replay | `/* best-effort: a missing or racing screen.log must not block attach */` |
60
+ | 1010 | `close()` | `socket.destroy()` | `/* best-effort teardown: socket may already be destroyed */` |
61
+
62
+ ## Decision: SAFETY comment for the as-cast (L795)
63
+
64
+ `currentSize()` reads `this.tui.terminal as unknown as { cols?: number;
65
+ columns?: number; rows?: number } | undefined`. Comment to add above the line:
66
+
67
+ ```
68
+ // SAFETY: duck-typed read — Pi TUI's Terminal type does not consistently expose
69
+ // cols/columns/rows across versions (see resizeIfNeeded below). Runtime
70
+ // fallbacks (120/24) keep this safe when the fields are absent.
71
+ ```
72
+
73
+ ## Decision: delete `width` param (not `_` prefix)
74
+
75
+ `private project(height: number, width: number)` → `private project(height: number)`;
76
+ single call site L271 `this.project(bodyHeight, width)` → `this.project(bodyHeight)`.
77
+ Deleting is cleaner than `_width`: private method, exactly one caller, no
78
+ interface stability concerns.
79
+
80
+ ## Verification
81
+
82
+ 1. `npm run typecheck` — clean.
83
+ 2. `npm test` — all pass (attach-related suites must stay green).
84
+ 3. `grep -c "catch {}" src/ui/pty-attach.ts` — 0 (each expanded to a documented
85
+ 3-line catch block).
86
+ 4. `grep -n "project(" src/ui/pty-attach.ts` — signature and call site both
87
+ single-arg.
88
+ 5. Coverage thresholds unaffected (comments + signature trim don't move lines/funcs/branches).
89
+
90
+ ## Risks & mitigations
91
+
92
+ - **Line drift vs this spec**: implementation re-locates sites by method name
93
+ (as in the table), not by line number.
94
+ - **Untracked files in main checkout** (`scratch/`, `test-support/*`,
95
+ `test/pty-attach-*.test.mjs`): all work happens in a worktree; staging is
96
+ per-file, never `git add -A`.
97
+ - **Zero-behavior guarantee**: no statement is added/removed except the param
98
+ deletion and its call-site argument; review diff must show comments + two-line
99
+ signature/call-site change only.
100
+
101
+ ## Rollout
102
+
103
+ - Worktree: `issue-8-pty-attach-quality-debt` (from main).
104
+ - Spec lands in worktree `docs/superpowers/specs/2026-08-30-pty-attach-quality-debt-design.md`
105
+ as the first commit, then plan → implement → local CR → PR.
@@ -0,0 +1,115 @@
1
+ # Design: User-first README v2 for Pi Agent Board
2
+
3
+ **Issue:** #51
4
+ **Date:** 2026-08-30
5
+ **Status:** Approved
6
+ **Language:** English
7
+
8
+ ## Outcome
9
+
10
+ Rewrite `README.md` into a user-first English guide that accurately describes the currently shipped Pi Agent Board package, verified by current source, package metadata, tests, and manual verification notes.
11
+
12
+ The README will take a new user from installation to a first background session, then serve as a practical reference for dashboard actions, attach mode, filters, configuration, limitations, troubleshooting, and maintainer entry points.
13
+
14
+ This is documentation-only. No product behavior changes are part of the work.
15
+
16
+ ## Source of truth
17
+
18
+ - `package.json` is authoritative for package identity, version, Node engine, repository, scripts, and Pi package metadata.
19
+ - `src/index.ts`, `src/commands/*`, `src/ui/dashboard.ts`, `src/ui/pty-attach.ts`, `src/runtime/service.mjs`, and `src/core/*` are authoritative for shipped behavior.
20
+ - `PRD.md`, `PROGRESS.md`, `REMAINING_WORK.md`, and dated design/plan documents are historical or planning material; they must not advertise disabled or planned behavior.
21
+ - Every command, shortcut, environment variable, default, and limitation must be checked against an exact source location or a verified command.
22
+ - Clearly distinguish shipped behavior, fallback behavior, disabled behavior, and planned/internal behavior.
23
+ - Avoid hard-coded test counts; state that CI and `npm run verify` are authoritative.
24
+ - Use `AGENT_BOARD_*` names for new configuration. Mention selected `AGENT_VIEW_*` names only as migration aliases.
25
+
26
+ ## Information architecture
27
+
28
+ Use this task-oriented structure:
29
+
30
+ 1. Title, package links, value proposition
31
+ 2. What it does / when to use it
32
+ 3. Requirements
33
+ 4. Installation
34
+ 5. Quick start
35
+ 6. Entry points
36
+ 7. Dashboard workflow
37
+ 8. Views and actions
38
+ 9. States, grouping, and filters
39
+ 10. Attach mode
40
+ 11. Persistence, safety, and limitations
41
+ 12. Configuration
42
+ 13. Troubleshooting
43
+ 14. Development
44
+ 15. Publishing
45
+ 16. Further reading
46
+
47
+ The first half should be readable without knowing Pi internals. Advanced QA and implementation details should be linked rather than expanded inline.
48
+
49
+ ## Content requirements
50
+
51
+ ### Positioning
52
+
53
+ State that Agent Board is a full-screen TUI dashboard for dispatching, monitoring, inspecting, replying to, attaching to, and managing multiple durable background Pi sessions. Emphasize global cross-project visibility, resumability, dashboard triage, inline reply/evidence, and PTY/JSON fallback. Do not imply cloud execution, multi-user sharing, automatic worktree isolation, or full Claude parity.
54
+
55
+ ### Requirements and installation
56
+
57
+ Include Pi, Node.js 20+, working Pi provider authentication, and PTY support for live attach/start-and-attach. Use `pi install npm:@zhuxixi/pi-agent-board` everywhere. Keep local path installation and symlink discovery as separate alternatives. Include a short one-shot auth sanity check and link detailed checks to `VERIFY.md`.
58
+
59
+ ### Quick start and entry points
60
+
61
+ Use a concrete five-step first-task flow: `i` INSERT mode → type task → `Enter` to open the Start session dialog → review cwd/model/thinking/action → `Enter` on Start session to launch. Explain Space Peek, `r` in Peek, `v`, `e`, attach with Enter/Right/`>`, and that PTY detach with `Left` is gated by the child input line: it detaches on empty input, is forwarded while editing, and remains unconditional when disconnected. `Ctrl+]` is not a detach key — it is passed through to the child Pi editor.
62
+
63
+ Document `/agent-board`, `pi /agent-board`, `pi --agent-board`, and `/bg [prompt]`, including that `--agent-board` startup cannot attach and normal `/agent-board` is required for attach.
64
+
65
+ ### Dashboard and actions
66
+
67
+ Explain Normal vs INSERT mode, draft-vs-empty `Enter`, Ctrl+N entering INSERT mode with a pre-filled `hello` prompt (the next Enter opens the launch flow), cwd favorites/path completion, model/thinking/action fields, persisted launch preferences, and PTY-dependent start-and-attach fallback.
68
+
69
+ Document exact destructive semantics: `d` confirms inactive Done; manual completion is default; Ctrl+X twice quickly archives; archive preserves the session file; X deletes inactive rows in the selected state; `m` batch selection supports Space/a/u/d/Ctrl+X.
70
+
71
+ Keep shortcut reference separated by view. State that `r` is available from Peek/Transcript/Evidence, not directly from the main list, and pending Pi questions must be answered via attach rather than inline reply.
72
+
73
+ ### States, views, and filters
74
+
75
+ Document the seven labels: Queued, Running, Needs answer, Needs instructions, Done, Failed, Stopped. Explain separate process liveness, state grouping, folder grouping, pinned-first stable creation ordering, unread indicators, Peek, read-only transcript, Evidence/Diagnostics, durable FIFO follow-up queue, and `qN`.
76
+
77
+ Document filter syntax: `s:`, `review:ready`, `diag:stalled`, `evidence:error`, `queued:true|yes|1`, `steer:`, and free-text AND over name/summary/cwd. Explain aliases and case-insensitivity. Caveat that `diag:stalled` can consume persisted diagnostics but there is no general current provider-stall detector.
78
+
79
+ Explain locally extracted issue/PR badges and optional per-root `providers.json`, without asserting an unverified custom schema.
80
+
81
+ ### Attach and persistence
82
+
83
+ Describe PTY attach, conditional `Left` detach (empty child input detaches; edited input forwards the key; disconnected hosts can always be exited), `Ctrl+]` passthrough to the child Pi editor, PageUp/PageDown/Home/End/mouse wheel scrollback, link opening, drag/double-click copy, optional X11 middle-click paste, clipboard/image passthrough, cold-host loading/reconnect, warm host pool, and Windows named-pipe/hidden-console behavior without promising terminal-emulator parity.
84
+
85
+ Explain PTY vs JSON fallback, adopted external-session PTY requirement, and `!` diagnostics. Document the default store at `~/.pi/agent/agent-board/`, high-level artifacts, persistence across reload/restart/worker exit, and stale-row reconciliation.
86
+
87
+ Prominently state that worktree isolation is currently disabled and not automatically created; same-repository concurrent writes are unsafe unless the user manually avoids overlap or supplies isolation. Also state no cloud/multi-user coordination, row deletion preserves session files, auth remains required, startup attach limitation, and PTY native-dependency limitation.
88
+
89
+ ### Configuration
90
+
91
+ List supported user-facing settings with exact defaults/disable values: ROOT, AUTO_STATE, AUTO_STATE_MODEL, AUTO_STATE_NO_DONE, SUMMARY_MODEL, TITLE_MODEL, TITLE_THINKING_LEVEL, CODE_REFS, DISABLE_PTY, FORCE_PTY, ATTACH_MOUSE, ENABLE_MOUSE_SCROLL, WHEEL_LINES, MAX_WARM_HOSTS, WARM_HOST_TTL_MS, ATTACH_NATIVE_PASTE, FORWARD_OSC52, and FORWARD_IMAGES.
92
+
93
+ Do not list internal child markers. Do not advertise `AGENT_BOARD_ALLOW_PIPE_FALLBACK` as a normal user toggle because current service dispatch does not pass the ambient variable into the injected PTY runner config. Mention selected legacy `AGENT_VIEW_*` aliases only as compatibility paths.
94
+
95
+ ### Troubleshooting and maintainer sections
96
+
97
+ Troubleshoot stuck Running/auth, `node-pty unavailable`, slow/reconnecting attach, start-and-attach fallback, rejected inline replies, and same-repo conflicts. Link `VERIFY.md`.
98
+
99
+ Keep Development to `npm install` and `npm run verify`; explain verify briefly. Keep Publishing to verify, version bump, and publish. Link further reading (`VERIFY.md`, `PRD.md`, `PROGRESS.md`, and relevant design docs) without turning README into a historical log.
100
+
101
+ ## Supporting documentation
102
+
103
+ Correct the old unscoped install command in `VERIFY.md` from `pi install npm:pi-agent-board` to `pi install npm:@zhuxixi/pi-agent-board`, because README links to it. Make no other supporting-doc changes.
104
+
105
+ ## Validation
106
+
107
+ - Line-by-line compare the final README with source and package metadata.
108
+ - Check Markdown link targets and stale package names/status wording.
109
+ - Run targeted documentation scans.
110
+ - Run verification in the isolated worktree; do not count the main session's untracked PTY tests as evidence.
111
+ - Final diff should contain README, this approved spec, the implementation plan, and the one-line VERIFY correction only.
112
+
113
+ ## Non-goals
114
+
115
+ No runtime behavior, worktree implementation, plan-approval UI, provider-stall detection, docs generator, changelog, or broad PRD/progress rewrite.
@@ -0,0 +1,78 @@
1
+ # Issue #66 Spec:← detach 门禁锚点重构(反白假光标)
2
+
3
+ > Draft:2026-09-01。state: **pending user review**(⏸ 等用户确认后再进 worktree)
4
+
5
+ ## 1. 根因(调研结论,详见 issue 评论)
6
+
7
+ `childInputLooksEmpty()` 以 **xterm 硬件光标所在行**为锚点(`baseY + cursorY`),而 pi 渲染时:
8
+ - streaming:差分帧只重绘 Working 行,光标**持续**停在 `⠙ Working...` 行(~120ms/帧);
9
+ - attach 完成瞬间:光标停在输出区/提示行。
10
+
11
+ 两态下输入框为空但光标行非空 → `isProbablyEmptyPiInputLine` 判非空 → `←` 被转发给子进程。
12
+ v0.5.1 起 `ctrl+]` 已透传 Pi,`←` 是 attach 表面**唯一 detach 键** → 用户被困。
13
+
14
+ **可靠锚点(实证)**:pi 输入行恒渲染**反白假光标**(`\x1b[7m...\x1b[27m`,pi-tui `input.js` render + 本机 screen.log 双重确认),且反白 cell 在 xterm buffer 中**持久存在**(差分帧不重绘也保留)。从 buffer 底部向上扫"含 inverse cell 的行"即可定位输入行,**不依赖光标位置**。
15
+
16
+ ## 2. 修复设计
17
+
18
+ ### 2.1 `src/core/pty-input.mjs`
19
+
20
+ 新增纯函数(现有 `isProbablyEmptyPiInputLine` 不变):
21
+
22
+ ```js
23
+ export function isProbablyPiInputLine(line) // 行首(trim 左空白)为 prompt/continuation 字形
24
+ ```
25
+
26
+ 字形集:`›>┃│|┆╎╏:`(与 isProbablyEmptyPiInputLine 的 trim 字符集一致,兼容有字形 pi 版本)。
27
+
28
+ ### 2.2 `src/ui/pty-attach.ts` — `childInputLooksEmpty()` 重构
29
+
30
+ 三层判定,按优先级:
31
+
32
+ 1. **反白假光标锚点**(主):从 `active.baseY + active.length - 1` 向上扫,找第一个含 `isInverse()` cell 的行 → 返回 `isProbablyEmptyPiInputLine(line.translateToString(true))`;
33
+ 2. **字形行 fallback**(兼容无反白光标渲染的 pi 变体):找不到反白行时,同向扫 `isProbablyPiInputLine` 行 → 判空;
34
+ 3. **逃生兜底**:都找不到(损坏 buffer / replay 窗口无输入行帧)→ 视为空 → `←` 可 detach。
35
+
36
+ 行为矩阵:
37
+
38
+ | 状态 | 反白行 | 字形行 | 判定 | ← 行为 |
39
+ |------|--------|--------|------|--------|
40
+ | 输入框空(attach 后 / streaming 中) | 空行 | — | 空 | detach ✓ |
41
+ | 输入框有草稿 | 非空行 | — | 非空 | 转发(不抢编辑键)✓ |
42
+ | 损坏残影 buffer | 无 | 无 | 空 | detach(逃生)✓ |
43
+ | 有字形 pi 版本 | 空行(`> `) | `> ` | 空 | detach ✓ |
44
+
45
+ ## 3. 非目标
46
+
47
+ - ❌ ← 无条件 detach(违背"编辑中不抢键"产品意图,attach-flow.ts 亦保留门禁)
48
+ - ❌ 控制 socket 编辑器状态查询(pi 无此协议)
49
+ - ❌ 修改 `ctrl+]` 语义(v0.5.1 已透传 Pi,保持)
50
+
51
+ ## 4. 验收矩阵
52
+
53
+ | ID | 功能点 | 验收方式 | 具体验证 | 通过标准 |
54
+ |----|--------|----------|----------|----------|
55
+ | A1 | `isProbablyPiInputLine` 纯函数行为 | 自动化验证(unit) | `node --test test/pty-input.test.mjs` | 字形行 true;内容行/空行 false |
56
+ | A2 | 反白锚点:空输入行(光标漂移)→ detach | 自动化验证(unit) | `node --test test/pty-attach-detach-gate.test.mjs`(smoke B3) | `leftDetachesWhenCursorOffEmptyInputLine === true` |
57
+ | A3 | 反白锚点:草稿行 → 转发 | 自动化验证(unit) | 同上(smoke B) | `leftStaysGatedOnNonEmptyLine === true` |
58
+ | A4 | 反白锚点:无假光标行 + 空行 fallback | 自动化验证(unit) | 同上(smoke 新增) | `leftDetachesOnEmptyInput === true` 保持 |
59
+ | A5 | 损坏 buffer(无输入行)→ 逃生 | 自动化验证(unit) | 同上(smoke B1) | `leftEscapesOnGarbledBuffer === true` |
60
+ | A6 | 全套回归 + 覆盖率门禁 | 自动化验证(build/static + unit) | `npm run verify`(typecheck + 全部测试 + coverage 门禁 lines85/funcs80/branches70 + pack:dry,Node 22/24) | 全绿;无既有测试被改语义 |
61
+ | U1 | attach 后输入框空按 ← 回退 | 用户实测 | attach 进入 pi session → 立即按 ← | 回退到 dashboard,无需 ↑↓/输入删除 |
62
+ | U2 | pi 思考中(Working...)按 ← 回退 | 用户实测 | 触发 pi 思考(Working 动画)→ 按 ← | 回退到 dashboard |
63
+
64
+ ## 5. 可测性拆分设计
65
+
66
+ | 函数 | 位置 | 职责 | 测试边界 |
67
+ |------|------|------|----------|
68
+ | `isProbablyPiInputLine(line)` | pty-input.mjs(纯函数) | 行首字形判定 | 输入字符串 → boolean;不碰 buffer/term |
69
+ | `findLastInverseCellLine(active)` | pty-attach.ts(私有,仅依赖 xterm buffer 接口) | 底部向上扫含 inverse cell 的行号 | 输入 fake buffer({baseY, length, getLine})→ 行号/null;**不依赖 cursorY** |
70
+ | `childInputLooksEmpty()` | pty-attach.ts | 三层组合判定 | 输入 buffer 状态 → boolean;经 smoke 场景验证 |
71
+
72
+ 约束:`findLastInverseCellLine` 只读 buffer(无副作用);`isProbablyPiInputLine` 无状态;smoke 场景构造 xterm 渲染序列(`\x1b[7m \x1b[27m` 等)验证端到端判定,不 mock 内部函数。
73
+
74
+ ## 6. 风险
75
+
76
+ - pi 未来版本假光标不再反白渲染 → 字形/兜底 fallback 接管(判定仍可用,只是可能放宽为"视为空")
77
+ - 多行编辑器最后一行空 → 判空 detach(与现状光标行逻辑一致,非回归)
78
+ - buffer 中其他反白元素(选中文本等)在输入行下方 → 极罕见;底部扫描以"最靠下"优先,选中文本在输出区(输入行上方)不干扰
@@ -0,0 +1,120 @@
1
+ # Issue #68 Spec:editor_state 推送 —— detach 门禁从渲染启发式迁移到子 pi 编辑器真实状态
2
+
3
+ > Draft:2026-09-02。state: **pending user review**(⏸ 等用户确认后再进 worktree)
4
+
5
+ ## 1. 背景与根因
6
+
7
+ #66 的三层渲染启发式(反色假光标锚点 → 字形行 → 逃生兜底)在真实 pi 渲染下全部失效:
8
+ - 空编辑器行行首**无 prompt 字形**;attach buffer 常**无反色 cell**(差分渲染不重绘编辑器行);
9
+ - 结果 tier-1 落空 → tier-2 误命中聊天区 markdown 表格行(`│ ... │`)/ 冒号行(`:` 开头)→ 判"有草稿" → `←` 被吞(#68/#69 实锤)。
10
+
11
+ 渲染流**不是**编辑器状态的可靠载体。根治方案(本 spec):子 pi 扩展直接读取 `ctx.ui.getEditorText()`(pi 官方 API,权威状态),经现有 control socket 推送给 runner → broadcast 给 attach 面板 → 门禁直接用。渲染启发式降级为 fallback。
12
+
13
+ ## 2. 架构设计
14
+
15
+ ```
16
+ 子 pi 扩展(isHostedChild 时 session_start 启动上报循环)
17
+ └─ 每 100ms 轮询 ctx.ui.getEditorText(),文本变化时:
18
+ → {type:"editor_state", empty} → control socket(JSONL client)
19
+ pty-runner(handleClientLine 新 case "editor_state")
20
+ ├─ 缓存 host.editorEmpty + persist 到 host.json(可选,见 §7)
21
+ ├─ broadcast({type:"editor_state", empty}) 给所有 attach clients
22
+ └─ hello 消息带初始 editorEmpty(新 attach 立即同步)
23
+ attach 面板(pty-attach.ts)
24
+ ├─ 缓存 this.editorEmpty: boolean | null(null = 未知)
25
+ ├─ onSocketData 新 case "editor_state" 更新缓存
26
+ └─ ← 判定:resolveEditorEmpty(editorEmpty, 启发式结果)
27
+ editorEmpty !== null ? editorEmpty : 现有启发式(fallback)
28
+ ```
29
+
30
+ ### 2.1 协议(JSONL,向后兼容)
31
+
32
+ ```jsonc
33
+ // 子 pi 扩展 → runner
34
+ { "type": "editor_state", "empty": true }
35
+
36
+ // runner → attach clients(广播 + hello 初始值)
37
+ { "type": "editor_state", "empty": true }
38
+ { "type": "hello", "status": {...}, "editorEmpty": true }
39
+ ```
40
+
41
+ 旧端忽略未知字段/消息(现有 parser 已忽略未知 type)→ 协议向后兼容。
42
+
43
+ ### 2.2 子 pi 扩展上报循环(新文件 `src/core/editor-state-reporter.mjs`)
44
+
45
+ - 激活条件:`process.env.AGENT_BOARD_CHILD === "1" || process.env.AGENT_VIEW_CHILD === "1"`(与 index.ts 同判据)
46
+ - socket 路径:`controlSocketPathFor(process.platform, root, viewId)`(复用 `src/core/paths.mjs`)
47
+ - 连接策略:runner **先 spawn 子 pi 后 listen** → 初始连接带重试(退避 1s→2s→…封顶 5s,永久重试);断开后同样重连
48
+ - 轮询:`setInterval(100ms)` 读 `getEditorText()`,`text !== lastText` 才发送(dedupe,避免常发)
49
+ - 生命周期:session_start 启动、进程退出自然回收;`stop()` 供测试
50
+ - 依赖注入设计(可测性,§5):`createEditorStateReporter({ getEditorText, connect, intervalMs = 100, scheduler = defaultScheduler })` → `{ start, stop }`;`scheduler = { interval(fn, ms), timeout(fn, ms), clear(handle) }`(默认 setInterval/setTimeout 包装);生产接线在 index.ts
51
+
52
+ ### 2.3 runner 改动(`runner/pty-runner.mjs`)
53
+
54
+ - `handleClientLine` switch 加 `case "editor_state"`:`host.editorEmpty = !!msg.empty; broadcast({type:"editor_state", empty: host.editorEmpty})`(不 persist,见 §7)
55
+ - `hello` 消息:`{ type:"hello", status: host, editorEmpty: host.editorEmpty ?? null }`
56
+ - 子 pi 断开(child exit)→ editorEmpty 复位 null 并 broadcast(attach 端退回启发式)
57
+
58
+ ### 2.4 attach 面板改动(`src/ui/pty-attach.ts`)
59
+
60
+ - 字段 `private editorEmpty: boolean | null = null`
61
+ - `onSocketData`:`case "editor_state"` → `this.editorEmpty = !!msg.empty`;`case "hello"` → 读 `msg.editorEmpty`
62
+ - `←` 判定(handleInput):`shouldEscapeAttach(this.connected, this.resolveEditorEmpty())`
63
+ - 纯函数 `resolveEditorEmpty(editorEmpty, heuristic)`(放 `src/core/pty-input.mjs` 或新纯函数文件):
64
+ `editorEmpty === null ? heuristic : editorEmpty`
65
+ - `ctrl+]` 语义不变;现有三层启发式**原样保留**(fallback)
66
+
67
+ ## 3. 行为矩阵
68
+
69
+ | 场景 | editorEmpty | 判定 | ← 行为 |
70
+ |------|-------------|------|--------|
71
+ | 空输入(attach/streaming/任意时刻) | true | 空 | detach ✓ |
72
+ | 草稿(多行/单行/autocomplete 后/↑ 召回) | false | 非空 | 转发(编辑保护)✓ |
73
+ | 提交瞬间 | true(清空) | 空 | detach ✓ |
74
+ | 子 pi 扩展缺失/socket 断流 | null | 启发式 | 现状 fallback 行为 |
75
+ | 损坏 buffer + editor_state 正常 | true/false | 权威 | 正确判定(不再被残影误导)✓ |
76
+
77
+ ## 4. 非目标
78
+
79
+ - ❌ 删除现有渲染启发式(保留为 fallback)
80
+ - ❌ #69 的短期 tier-2 收紧(#68 长期方案落地后启发式仅 fallback;#69 由维护者按需处理)
81
+ - ❌ RPC 模式特判(子 pi 恒 PTY/TUI;RPC 下 getEditorText 恒 "" → empty 恒 true → ← 恒 detach,无 TUI 草稿概念,方向安全)
82
+ - ❌ 击键级拦截(评论 2 已排除:会覆盖编辑器光标行为,hack 且危险)
83
+
84
+ ## 5. 可测性拆分设计
85
+
86
+ | 单元 | 位置 | 职责 | 测试边界 |
87
+ |------|------|------|----------|
88
+ | `createEditorStateReporter({getEditorText, connect, intervalMs, scheduler})` | src/core/editor-state-reporter.mjs | 轮询、变化 dedupe、断线重连退避、stop | 依赖全注入(scheduler 为 {interval, timeout, clear} 手动时钟);fake connect/getEditorText/scheduler → 断言 send 序列与时机;不碰真实 socket/interval |
89
+ | `resolveEditorEmpty(editorEmpty, heuristic)` | src/core/pty-input.mjs | 判定优先级 | 纯函数:4 种输入组合 → boolean |
90
+ | runner `editor_state` case + hello 字段 | runner/pty-runner.mjs | 缓存、broadcast、复位 | 现有 integration harness(pty-runner.integration.test.mjs 模式):注入 client line → 断言 broadcast 与 hello 载荷 |
91
+ | attach `editorEmpty` 缓存 + 判定 | src/ui/pty-attach.ts | 消息 case、字段、← 判定 | detach-gate-smoke 场景 H(editor_state 消息 + 草稿 buffer → detach);场景 I(草稿 + editor_state:false → 转发) |
92
+
93
+ 约束:reporter 的生产接线只做「注入真实依赖」,纯逻辑全在工厂函数内;实现不得把轮询/重连逻辑重新耦合进 index.ts。
94
+
95
+ ## 6. 验收矩阵
96
+
97
+ | ID | 功能点 | 验收方式 | 具体验证 | 通过标准 |
98
+ |----|--------|----------|----------|----------|
99
+ | A1 | reporter:轮询变化才上报、dedupe、stop | 自动化验证(unit) | `node --test test/editor-state-reporter.test.mjs` | 变化序列上报次数/内容精确匹配;无变化零上报 |
100
+ | A2 | reporter:初始连接重试退避 + 断线重连 | 自动化验证(unit) | 同上 | fake connect 先拒后通 → 按退避序列重试并在连通后恢复上报 |
101
+ | A3 | runner:editor_state case 缓存 + broadcast + hello 初始值 + exit 复位 | 自动化验证(integration) | `node --test test/pty-runner.integration.test.mjs` | 注入 editor_state line → 其他 client 收到广播;新 client hello 带 editorEmpty;exit 后 editorEmpty=null 广播 |
102
+ | A4 | attach:editor_state 消息更新缓存、判定优先级(null 走启发式) | 自动化验证(unit) | detach-gate-smoke 场景 H/I | 空输入+editorEmpty=true → detach;草稿+editorEmpty=false → 转发;无消息 → 走启发式(既有场景全绿) |
103
+ | A5 | 纯函数 resolveEditorEmpty | 自动化验证(unit) | `node --test test/pty-input.test.mjs` | 4 组合精确匹配 |
104
+ | A6 | 全套回归 + 覆盖率门禁 | 自动化验证(build/static + unit) | `npm run verify`(typecheck + 全测 + c8 门禁 85/80/70 + pack:dry);CI Node 22/24 | 全绿;既有 430+ 测试无语义改动 |
105
+ | U1 | 空输入 attach 后 ← 回退 | 用户实测 | attach 进入 → 立即 ← | 回 dashboard |
106
+ | U2 | Working... 中 ← 回退 | 用户实测 | 思考动画中 ← | 回 dashboard |
107
+ | U3 | 草稿中 ← 左移不被抢 | 用户实测 | 输入草稿 → ← | 光标左移,不 detach |
108
+ | U4 | 断流 fallback(子 pi 扩展被禁用时仍可退出) | 用户实测 | 临时禁用扩展的 editor_state 上报(env 开关)→ ← | 仍可回退(启发式兜底) |
109
+
110
+ ## 7. 决策与风险
111
+
112
+ - **不 persist editorEmpty**(首版):host.json 是行状态持久化,editor 状态是瞬态(毫秒级),重启后 attach 的 hello 里 null → 启发式兜底 1-2s 内收到首个 editor_state 修正。避免 host.json 写放大(100ms 级变化频率)。
113
+ - **100ms 轮询开销**:getEditorText 是内存 join,忽略不计;socket 仅在变化时写。
114
+ - **风险:editorEmpty=false 但用户实际想退出**(草稿场景):与既有"编辑中不抢键"哲学一致(attach-flow 同样);用户清空草稿即可退出。
115
+ - **风险:子 pi 扩展未安装/旧版**(fallback 启发式接管,U4 验证)。
116
+ - **风险:runner 老版本**(不认识 editor_state case,忽略)→ attach 收不到 → 启发式兜底,向后兼容。
117
+
118
+ ## 8. 关联
119
+
120
+ - #68(本 issue 长期方案)、#66/#67(启发式起源与失效)、#69(同症状短期方案,由维护者处理)、#42/#48(门禁可靠性原则:视图必须始终可退出)