@zhuxixi/pi-agent-board 0.5.1 → 0.6.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 +32 -0
- package/README.md +41 -3
- package/docs/superpowers/plans/2026-09-01-attach-detach-gate-cursor-anchor.md +284 -0
- package/docs/superpowers/plans/2026-09-02-attach-detach-editor-state.md +722 -0
- package/docs/superpowers/plans/2026-09-03-code-refs-pr-backlink-narrow.md +551 -0
- package/docs/superpowers/plans/2026-09-03-detach-gate-glyph-fallback.md +146 -0
- package/docs/superpowers/plans/2026-09-04-evidence-outputpreview.md +209 -0
- package/docs/superpowers/plans/2026-09-04-warm-host-reclaim.md +796 -0
- package/docs/superpowers/plans/2026-09-05-issue-13-drainnextfollowup-pty-probe.md +114 -0
- package/docs/superpowers/plans/2026-09-05-issue-38-windows-wezterm-ime-cursor.md +73 -0
- package/docs/superpowers/plans/2026-09-05-issue-39-truncate-codepoint-boundary.md +143 -0
- package/docs/superpowers/plans/2026-09-05-issue-61-mention-fallback-guards.md +226 -0
- package/docs/superpowers/plans/2026-09-05-issue-63-flaky-manual-completion.md +87 -0
- package/docs/superpowers/plans/2026-09-05-issue-64-changelog-release-helper.md +53 -0
- package/docs/superpowers/plans/2026-09-05-pty-host-stacking-sock-race.md +731 -0
- package/docs/superpowers/specs/2026-08-29-code-refs-badges-design.md +1 -1
- package/docs/superpowers/specs/2026-09-01-attach-detach-gate-cursor-anchor-design.md +78 -0
- package/docs/superpowers/specs/2026-09-02-attach-detach-editor-state-design.md +120 -0
- package/docs/superpowers/specs/2026-09-03-code-refs-pr-backlink-narrow-design.md +92 -0
- package/docs/superpowers/specs/2026-09-03-detach-gate-glyph-fallback-design.md +99 -0
- package/docs/superpowers/specs/2026-09-04-evidence-outputpreview-design.md +50 -0
- package/docs/superpowers/specs/2026-09-04-warm-host-reclaim-design.md +106 -0
- package/docs/superpowers/specs/2026-09-05-issue-13-drainnextfollowup-pty-probe-design.md +64 -0
- package/docs/superpowers/specs/2026-09-05-issue-38-windows-wezterm-ime-design.md +48 -0
- package/docs/superpowers/specs/2026-09-05-issue-39-truncate-codepoint-boundary-design.md +64 -0
- package/docs/superpowers/specs/2026-09-05-issue-61-mention-fallback-design.md +71 -0
- package/docs/superpowers/specs/2026-09-05-issue-63-flaky-manual-completion-design.md +49 -0
- package/docs/superpowers/specs/2026-09-05-issue-64-changelog-helper-design.md +76 -0
- package/docs/superpowers/specs/2026-09-05-pty-host-stacking-sock-race-design.md +510 -0
- package/package.json +83 -81
- package/runner/job-runner.mjs +2 -2
- package/runner/pty-runner.mjs +585 -4
- package/runner/state-runner.mjs +3 -0
- package/runner/title-runner.mjs +1 -1
- package/scripts/release_helper.mjs +277 -0
- package/src/commands/agent-board.ts +38 -35
- package/src/commands/attach-decision.mjs +66 -0
- package/src/commands/attach-flow.ts +45 -39
- package/src/core/code-refs.mjs +85 -33
- package/src/core/editor-state-reporter.mjs +102 -0
- package/src/core/evidence.mjs +2 -2
- package/src/core/heuristics.mjs +40 -2
- package/src/core/host-coordination.mjs +159 -0
- package/src/core/host-crash.mjs +43 -3
- package/src/core/host-probe.mjs +196 -0
- package/src/core/launch.mjs +3 -1
- package/src/core/locks.mjs +196 -1
- package/src/core/paths.mjs +24 -0
- package/src/core/pty-input.mjs +32 -0
- package/src/core/store.mjs +164 -5
- package/src/core/types.mjs +17 -1
- package/src/core/warm-host-sweeper.mjs +150 -0
- package/src/index.ts +41 -2
- package/src/runtime/service.mjs +967 -109
- package/src/ui/dashboard-decisions.mjs +55 -0
- package/src/ui/dashboard.ts +26 -12
- package/src/ui/pty-attach.ts +55 -7
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project are documented here, generated from
|
|
4
|
+
conventional commits by `scripts/release_helper.mjs`. Entries are
|
|
5
|
+
forward-only: they begin with the first release after this file landed —
|
|
6
|
+
for earlier history, see the git log and the pull-request list.
|
|
7
|
+
|
|
8
|
+
## [0.6.0] - 2026-09-07
|
|
9
|
+
|
|
10
|
+
### Features
|
|
11
|
+
|
|
12
|
+
- conventional-commit driven CHANGELOG via release_helper.mjs (issue #64) (#83)
|
|
13
|
+
|
|
14
|
+
### Fixes
|
|
15
|
+
|
|
16
|
+
- pty host stacking and control.sock cross-deletion via per-instance ownership protocol (issue #70) (#84)
|
|
17
|
+
- guard mention fallback against placeholders, code spans, and pr-context (issue #61) (#82)
|
|
18
|
+
- truncate never splits surrogate pairs or emits lone surrogates (issue #39) (#81)
|
|
19
|
+
- extract evidence outputPreview from AgentToolResult content (issue #41) (#77)
|
|
20
|
+
- reclaim idle warm PTY hosts via periodic sweep + lifecycle cleanup (issue #75) (#76)
|
|
21
|
+
- narrow code-refs PR back-link extraction by evidence context (issue #65) (#73)
|
|
22
|
+
|
|
23
|
+
### Performance
|
|
24
|
+
|
|
25
|
+
- use cached PTY probe on the reconcile drain path (issue #13) (#79)
|
|
26
|
+
|
|
27
|
+
### Changes
|
|
28
|
+
|
|
29
|
+
- Windows WezTerm IME needs a visible hardware cursor (issue #38) (#80)
|
|
30
|
+
- poll markCompleted to success instead of racing the persist window (issue #63) (#78)
|
|
31
|
+
|
|
32
|
+
[0.6.0]: https://github.com/zhuxixi/pi-agent-board/compare/v0.5.2...v0.6.0
|
package/README.md
CHANGED
|
@@ -236,6 +236,26 @@ steer:awaiting-approval
|
|
|
236
236
|
|
|
237
237
|
Evidence is collected locally from session events. Agent Board can extract issue and pull-request references from that evidence and show badges such as `#40` or `▸#45` on rows; Peek includes the provider, confidence, source, and URL when available. Built-in GitHub/GitLab-style providers are available, and an optional per-store `providers.json` can extend the provider rules. The `AGENT_BOARD_CODE_REFS=off` setting disables extraction.
|
|
238
238
|
|
|
239
|
+
For internal code platforms (non-github/gitlab hosts) or custom CLIs, add a per-store `providers.json` so claim/action rules exist and sessions stop depending on the low-confidence mention fallback. Example — an internal CLI (`acli`) with claim-strength issue rules:
|
|
240
|
+
|
|
241
|
+
```json
|
|
242
|
+
{
|
|
243
|
+
"providers": [
|
|
244
|
+
{
|
|
245
|
+
"name": "acode",
|
|
246
|
+
"hosts": ["acode.internal.example.com"],
|
|
247
|
+
"rules": [
|
|
248
|
+
{ "pattern": "acli\\s+issue\\s+update\\s+#?(\\d+)(?=[\\s\\S]*--assignee)", "kind": "issue", "strength": "claim" },
|
|
249
|
+
{ "pattern": "acli\\s+issue\\s+(?:note|comment|close)\\s+#?(\\d+)", "kind": "issue", "strength": "action" },
|
|
250
|
+
{ "pattern": "acli\\s+issue\\s+(?:show|view)\\s+#?(\\d+)", "kind": "issue", "strength": "view" }
|
|
251
|
+
]
|
|
252
|
+
}
|
|
253
|
+
]
|
|
254
|
+
}
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
`hosts` matches the repo's remote host; rules follow the same `pattern`/`kind`/`strength` shape as the built-in `gh`/`glab` tables (`strength`: `claim` > `action` > `view`), and the `#N` capture group supplies the number. Validation errors from a broken file surface in the diagnostics panel and never break extraction — an invalid file is simply ignored.
|
|
258
|
+
|
|
239
259
|
## Attach and Fallback Behavior
|
|
240
260
|
|
|
241
261
|
When PTY support is healthy, Agent Board uses an interactive PTY host for attach and start-and-attach. If PTY support is unavailable, eligible managed sessions can still run in the background through the JSON runner; start-and-attach falls back to background launch with a warning. Adopted external foreground sessions require PTY to continue safely. Press `!` in the dashboard for diagnosis and repair hints.
|
|
@@ -319,6 +339,20 @@ Press `!` in the dashboard to open the diagnostic panel and follow its repair hi
|
|
|
319
339
|
|
|
320
340
|
A cold PTY host may briefly show a loading or reconnecting surface while it starts. Check the PTY status in the dashboard with `!`; stale hosts are diagnosed separately from active task workers. If the host never becomes healthy, repair `node-pty` or use background mode for eligible managed sessions.
|
|
321
341
|
|
|
342
|
+
### IME candidate window is stuck at the window edge (Windows WezTerm)
|
|
343
|
+
|
|
344
|
+
On Windows WezTerm with a WSL2 backend, the IME candidate window may stay pinned to the right edge instead of following the text cursor in an attached session. Windows WezTerm only tracks the IME candidate position from the visible hardware cursor, and Pi hides the hardware cursor by default — the block cursor you see in the editor is drawn content, not the hardware cursor. Linux terminals are not affected.
|
|
345
|
+
|
|
346
|
+
Make the hardware cursor visible, either way:
|
|
347
|
+
|
|
348
|
+
```bash
|
|
349
|
+
export PI_HARDWARE_CURSOR=1 # machine-local, e.g. ~/.zshrc.local
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
or set `"showHardwareCursor": true` in Pi's `settings.json` (syncs across machines if the config is version-controlled; harmless on Linux).
|
|
353
|
+
|
|
354
|
+
Trade-off: the real terminal cursor becomes visible inside the TUI. This is cosmetic only.
|
|
355
|
+
|
|
322
356
|
### Start & attach falls back to background
|
|
323
357
|
|
|
324
358
|
Start & attach requires PTY support. When PTY is unavailable, the task is still dispatched in the background and the dashboard displays a warning. Repair PTY and retry attach from the normal `/agent-board` command path.
|
|
@@ -346,15 +380,19 @@ npm run verify
|
|
|
346
380
|
|
|
347
381
|
## Publishing
|
|
348
382
|
|
|
349
|
-
Before publishing a release, verify the package,
|
|
383
|
+
Before publishing a release, verify the package, generate the changelog, then bump, and publish — the changelog must be generated **before** `npm version`, because `npm version` commits and tags the bump, which would empty the generation range:
|
|
350
384
|
|
|
351
385
|
```bash
|
|
352
386
|
npm run verify
|
|
353
|
-
npm version patch
|
|
387
|
+
npm run changelog -- --dry-run # preview the next section (version = current + patch)
|
|
388
|
+
npm run changelog -- patch # inserts the section into CHANGELOG.md
|
|
389
|
+
node scripts/release_helper.mjs verify # must exit 0: no functional PR missing from the top section
|
|
390
|
+
git add CHANGELOG.md && git commit -m "docs(changelog): <version>"
|
|
391
|
+
npm version patch # bumps package.json, commits, and tags vX.Y.Z
|
|
354
392
|
npm publish
|
|
355
393
|
```
|
|
356
394
|
|
|
357
|
-
Use `
|
|
395
|
+
Use `minor`/`major` in both the changelog and `npm version` steps when appropriate. The changelog is generated from conventional commits since the last `vX.Y.Z` tag (`scripts/release_helper.mjs`, ported from the jfox release flow); review the preview before committing. The `verify` step guards against PRs merged after the changelog was generated — rerun it after any late merge and, after removing the stale top section, re-run `npm run changelog -- <bump>` if it reports missing PRs (apply refuses when the section already exists — remove the stale section first). Release notes for the GitHub Release are the top CHANGELOG section. After publishing, users install the scoped package with:
|
|
358
396
|
|
|
359
397
|
```bash
|
|
360
398
|
pi install npm:@zhuxixi/pi-agent-board
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
# ← Detach Gate Cursor-Anchor Fix 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:** Fix issue #66 — `←` must detach when the Pi editor line is empty even while the terminal cursor rests on working/output lines (attach / streaming).
|
|
6
|
+
|
|
7
|
+
**Architecture:** Replace the cursor-line anchor in `PtyAttachComponent.childInputLooksEmpty()` with a bottom-up scan for Pi's inverse-video fake-cursor cell (`ESC[7m`, persists in the xterm buffer across differential frames), with a prompt-glyph fallback and a "treat as empty" escape fallback. Add a pure helper `isProbablyPiInputLine` for glyph detection.
|
|
8
|
+
|
|
9
|
+
**Tech Stack:** TypeScript (pty-attach.ts, run via `node --experimental-transform-types` in tests), plain ESM (pty-input.mjs), `node:test` runner, @xterm/headless.
|
|
10
|
+
|
|
11
|
+
**Spec:** `docs/superpowers/specs/2026-09-01-attach-detach-gate-cursor-anchor-design.md`
|
|
12
|
+
|
|
13
|
+
## Global Constraints
|
|
14
|
+
|
|
15
|
+
- Coverage gates: lines 85 / funcs 80 / branches 70 (c8, `npm run test:coverage`); Node 22/24 both green (CI).
|
|
16
|
+
- `isProbablyPiInputLine` glyph set must stay identical to `isProbablyEmptyPiInputLine`'s trim charset: `›>┃│|┆╎╏:`.
|
|
17
|
+
- Do not change `ctrl+]` semantics (passes through to Pi since v0.5.1) and do not make `←` unconditionally detach (keeps the edit-protection gate — spec §3).
|
|
18
|
+
- Do not modify `src/core/pty-input.mjs`'s existing `isProbablyEmptyPiInputLine` behavior.
|
|
19
|
+
- All edits inside worktree `WT=.pi/worktrees/issue-66-attach-detach-gate-cursor-anchor`; git ops via `git -C $WT`.
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
### Task 1: Add `isProbablyPiInputLine` pure helper + unit tests
|
|
24
|
+
|
|
25
|
+
**Files:**
|
|
26
|
+
- Modify: `src/core/pty-input.mjs` (append glyph const + function after existing helper)
|
|
27
|
+
- Test: `test/pty-input.test.mjs`
|
|
28
|
+
|
|
29
|
+
**Interfaces:**
|
|
30
|
+
- Produces: `export function isProbablyPiInputLine(line: string): boolean` — true iff the line, after trimming leading whitespace, starts with a prompt/continuation glyph.
|
|
31
|
+
|
|
32
|
+
- [ ] **Step 1: Write the failing test**
|
|
33
|
+
|
|
34
|
+
Append to `test/pty-input.test.mjs` (keep existing tests untouched):
|
|
35
|
+
|
|
36
|
+
```js
|
|
37
|
+
import { isProbablyEmptyPiInputLine, isProbablyPiInputLine } from "../src/core/pty-input.mjs";
|
|
38
|
+
|
|
39
|
+
test("isProbablyPiInputLine recognizes Pi prompt / continuation lines", () => {
|
|
40
|
+
assert.equal(isProbablyPiInputLine("> "), true);
|
|
41
|
+
assert.equal(isProbablyPiInputLine(" ┃ edit me"), true);
|
|
42
|
+
assert.equal(isProbablyPiInputLine(" │ second line"), true);
|
|
43
|
+
assert.equal(isProbablyPiInputLine("› draft"), true);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test("isProbablyPiInputLine rejects content lines and empty lines", () => {
|
|
47
|
+
assert.equal(isProbablyPiInputLine("chat content"), false);
|
|
48
|
+
assert.equal(isProbablyPiInputLine("────── ◊◊ ──────"), false);
|
|
49
|
+
assert.equal(isProbablyPiInputLine(""), false);
|
|
50
|
+
assert.equal(isProbablyPiInputLine(" "), false);
|
|
51
|
+
});
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
- [ ] **Step 2: Run test to verify it fails**
|
|
55
|
+
|
|
56
|
+
Run: `node --test test/pty-input.test.mjs`
|
|
57
|
+
Expected: FAIL — `isProbablyPiInputLine is not a function` (import error).
|
|
58
|
+
|
|
59
|
+
- [ ] **Step 3: Write minimal implementation**
|
|
60
|
+
|
|
61
|
+
Append to `src/core/pty-input.mjs` (before `isProbablyEmptyPiInputLine` or after — any top-level position):
|
|
62
|
+
|
|
63
|
+
```js
|
|
64
|
+
/** Glyphs Pi uses to render editor prompt / continuation lines (`>` main prompt,
|
|
65
|
+
* `›`/`┃`/`│` and variants in older releases). Must stay in sync with the
|
|
66
|
+
* trim charset of isProbablyEmptyPiInputLine below. */
|
|
67
|
+
const PROMPT_GLYPHS = "›>┃│|┆╎╏:";
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Whether the given terminal line looks like a Pi editor input line: leading
|
|
71
|
+
* whitespace followed by a prompt/continuation glyph. The attach surface uses
|
|
72
|
+
* this to locate the editor line inside the buffer instead of trusting the
|
|
73
|
+
* terminal cursor, which wanders onto output/working lines while Pi streams
|
|
74
|
+
* (issue #66).
|
|
75
|
+
* @param {string} line
|
|
76
|
+
* @returns {boolean}
|
|
77
|
+
*/
|
|
78
|
+
export function isProbablyPiInputLine(line) {
|
|
79
|
+
const withoutLeftPadding = String(line || "").replace(/^[\s\u00a0]+/u, "");
|
|
80
|
+
return withoutLeftPadding.length > 0 && PROMPT_GLYPHS.includes(withoutLeftPadding[0]);
|
|
81
|
+
}
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
- [ ] **Step 4: Run test to verify it passes**
|
|
85
|
+
|
|
86
|
+
Run: `node --test test/pty-input.test.mjs`
|
|
87
|
+
Expected: PASS (4 tests: 2 existing + 2 new).
|
|
88
|
+
|
|
89
|
+
- [ ] **Step 5: Commit**
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
git add test/pty-input.test.mjs src/core/pty-input.mjs
|
|
93
|
+
git commit -m "feat: add isProbablyPiInputLine helper (issue #66)"
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
---
|
|
97
|
+
|
|
98
|
+
### Task 2: Rework `childInputLooksEmpty()` anchor + smoke scenarios
|
|
99
|
+
|
|
100
|
+
**Files:**
|
|
101
|
+
- Modify: `src/ui/pty-attach.ts` (import line ~8; `childInputLooksEmpty()` ~line 319; add private helper near it)
|
|
102
|
+
- Modify: `test-support/detach-gate-smoke.ts` (scenarios B/B1/B2/B3 + new E)
|
|
103
|
+
- Modify: `test/pty-attach-detach-gate.test.mjs` (assertions)
|
|
104
|
+
|
|
105
|
+
**Interfaces:**
|
|
106
|
+
- Consumes: `isProbablyPiInputLine` from Task 1; existing `isProbablyEmptyPiInputLine`.
|
|
107
|
+
- Produces: private `findLastInverseCellLine(active): number | null` — bottom-most line index containing an `isInverse()` cell; `null` when none.
|
|
108
|
+
|
|
109
|
+
- [ ] **Step 1: Write the failing smoke scenarios**
|
|
110
|
+
|
|
111
|
+
In `test-support/detach-gate-smoke.ts`:
|
|
112
|
+
|
|
113
|
+
1. Change scenario **B** (line ~76) so the draft line carries a fake cursor, then add B3 (cursor off the empty input line) and E (empty input line without fake cursor). Replace the current B block and append:
|
|
114
|
+
|
|
115
|
+
```ts
|
|
116
|
+
// B. ← must NOT detach while attached with a draft in the editor line (child
|
|
117
|
+
// is mid-draft and ← is also the editor's cursor-left key). The draft line
|
|
118
|
+
// carries Pi's inverse-video fake cursor (ESC[7m).
|
|
119
|
+
{
|
|
120
|
+
const { attach, sent, didDetach } = makeAttach();
|
|
121
|
+
await writeToTerm(attach, "chat content\r\n> \x1b[7m草\x1b[27m稿");
|
|
122
|
+
(attach as unknown as { connected: boolean }).connected = true;
|
|
123
|
+
attach.handleInput("\x1b[D");
|
|
124
|
+
out.leftStaysGatedOnNonEmptyLine = !didDetach() && sent.length === 1 && sent[0].type === "input" && sent[0].data === "\x1b[D";
|
|
125
|
+
attach.dispose();
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// B3. The streaming case from issue #66: the editor line is empty (bottom of
|
|
129
|
+
// the buffer, with its fake cursor) but the terminal cursor rests on the
|
|
130
|
+
// working line because Pi's differential frames only repaint the changed
|
|
131
|
+
// line. The gate must be judged from the fake-cursor line, not the cursor.
|
|
132
|
+
{
|
|
133
|
+
const { attach, sent, didDetach } = makeAttach();
|
|
134
|
+
await writeToTerm(attach, "chat content\r\n> \x1b[7m \x1b[27m");
|
|
135
|
+
await writeToTerm(attach, "\x1b[2;1H⠙ Working...");
|
|
136
|
+
(attach as unknown as { connected: boolean }).connected = true;
|
|
137
|
+
attach.handleInput("\x1b[D");
|
|
138
|
+
out.leftDetachesWhenCursorOffEmptyInputLine = didDetach() && sent.length === 0;
|
|
139
|
+
attach.dispose();
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// E. Empty input line rendered WITHOUT a fake cursor: no inverse cell and no
|
|
143
|
+
// glyph anywhere, and the terminal cursor sits on a non-empty output line.
|
|
144
|
+
// Falls through to the escape fallback — treat as empty, detach.
|
|
145
|
+
{
|
|
146
|
+
const { attach, sent, didDetach } = makeAttach();
|
|
147
|
+
await writeToTerm(attach, "chat content\r\n");
|
|
148
|
+
await writeToTerm(attach, "\x1b[1;1H"); // park the cursor on the non-empty line
|
|
149
|
+
(attach as unknown as { connected: boolean }).connected = true;
|
|
150
|
+
attach.handleInput("\x1b[D");
|
|
151
|
+
out.leftDetachesOnEmptyInputWithoutFakeCursor = didDetach() && sent.length === 0;
|
|
152
|
+
attach.dispose();
|
|
153
|
+
}
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
2. Update the B1 header comment (scenario text already fits) and ensure B1 (`────── ◊◊ ──────` garbled buffer) stays as-is — it now asserts escape-on-garbled-buffer.
|
|
157
|
+
|
|
158
|
+
- [ ] **Step 2: Run smoke to verify new scenarios fail**
|
|
159
|
+
|
|
160
|
+
Run: `node --experimental-transform-types test-support/detach-gate-smoke.ts`
|
|
161
|
+
Expected: `leftDetachesWhenCursorOffEmptyInputLine` is `false` (old cursor-line anchor reads the Working line) and `leftDetachesOnEmptyInputWithoutFakeCursor` is `false` (old anchor reads last non-empty line `chat content`? — verify output); `leftStaysGatedOnNonEmptyLine` true.
|
|
162
|
+
|
|
163
|
+
- [ ] **Step 3: Write the implementation**
|
|
164
|
+
|
|
165
|
+
In `src/ui/pty-attach.ts`:
|
|
166
|
+
|
|
167
|
+
1. Update import (line ~8):
|
|
168
|
+
|
|
169
|
+
```ts
|
|
170
|
+
import { isProbablyEmptyPiInputLine, isProbablyPiInputLine } from "../core/pty-input.mjs";
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
2. Replace the `childInputLooksEmpty()` method (currently ~line 319) and add the helper above it:
|
|
174
|
+
|
|
175
|
+
```ts
|
|
176
|
+
/** Bottom-most line whose cells include an inverse-video cell — Pi renders
|
|
177
|
+
* its editor cursor as an inverse "fake cursor" (`ESC[7m`), and the cell
|
|
178
|
+
* persists in the buffer even while streaming differential frames skip
|
|
179
|
+
* repainting the editor line. */
|
|
180
|
+
private findLastInverseCellLine(active: {
|
|
181
|
+
baseY: number;
|
|
182
|
+
length: number;
|
|
183
|
+
getLine(index: number): BufferLineLike | undefined;
|
|
184
|
+
}): number | null {
|
|
185
|
+
for (let y = active.baseY + active.length - 1; y >= active.baseY; y--) {
|
|
186
|
+
const line = active.getLine(y);
|
|
187
|
+
if (!line) continue;
|
|
188
|
+
for (let x = 0; x < line.length; x++) {
|
|
189
|
+
if (line.getCell(x)?.isInverse()) return y;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
return null;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
private childInputLooksEmpty(): boolean {
|
|
196
|
+
if (!this.receivedOutput) return true;
|
|
197
|
+
const active = this.term.buffer.active;
|
|
198
|
+
// The terminal cursor is not a reliable anchor for the editor line:
|
|
199
|
+
// while Pi streams output (or right after attach) the cursor rests on
|
|
200
|
+
// working/output lines, never the input line, so a genuinely empty
|
|
201
|
+
// editor was misread as non-empty and ← stopped detaching (issue #66).
|
|
202
|
+
// Pi's editor line always carries an inverse-video fake-cursor cell,
|
|
203
|
+
// so anchor on that instead.
|
|
204
|
+
const fakeCursorLine = this.findLastInverseCellLine(active);
|
|
205
|
+
if (fakeCursorLine !== null) {
|
|
206
|
+
const line = active.getLine(fakeCursorLine)?.translateToString(true) ?? "";
|
|
207
|
+
return isProbablyEmptyPiInputLine(line);
|
|
208
|
+
}
|
|
209
|
+
// Fallback: Pi variants that render no fake cursor — look for a
|
|
210
|
+
// prompt-glyph line.
|
|
211
|
+
for (let y = active.baseY + active.length - 1; y >= active.baseY; y--) {
|
|
212
|
+
const line = active.getLine(y)?.translateToString(true) ?? "";
|
|
213
|
+
if (isProbablyPiInputLine(line)) return isProbablyEmptyPiInputLine(line);
|
|
214
|
+
}
|
|
215
|
+
// No editor line recoverable (e.g. a garbled replay buffer): treat the
|
|
216
|
+
// input as empty — ← is the only detach key left on the attach surface,
|
|
217
|
+
// so it must always escape rather than trap the user.
|
|
218
|
+
return true;
|
|
219
|
+
}
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
- [ ] **Step 4: Update gate assertions**
|
|
223
|
+
|
|
224
|
+
In `test/pty-attach-detach-gate.test.mjs`, add to the existing test (after `leftDetachesOnEmptyInput`):
|
|
225
|
+
|
|
226
|
+
```js
|
|
227
|
+
assert.equal(parsed.leftDetachesWhenCursorOffEmptyInputLine, true, "← must detach when the editor line is empty even if the cursor sits on a working line");
|
|
228
|
+
assert.equal(parsed.leftDetachesOnEmptyInputWithoutFakeCursor, true, "← must detach when an empty editor line renders no fake cursor");
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
- [ ] **Step 5: Run tests to verify pass**
|
|
232
|
+
|
|
233
|
+
Run: `node --test test/pty-attach-detach-gate.test.mjs test/pty-input.test.mjs`
|
|
234
|
+
Expected: PASS — all gate assertions true, including the three new ones.
|
|
235
|
+
|
|
236
|
+
- [ ] **Step 6: Commit**
|
|
237
|
+
|
|
238
|
+
```bash
|
|
239
|
+
git add src/ui/pty-attach.ts test-support/detach-gate-smoke.ts test/pty-attach-detach-gate.test.mjs
|
|
240
|
+
git commit -m "fix: anchor ← detach gate on Pi's fake-cursor line, not terminal cursor (issue #66)"
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
---
|
|
244
|
+
|
|
245
|
+
### Task 3: Full regression (A6)
|
|
246
|
+
|
|
247
|
+
**Files:** none (verification only).
|
|
248
|
+
|
|
249
|
+
- [ ] **Step 1: Run verify**
|
|
250
|
+
|
|
251
|
+
Run (in worktree): `npm run verify`
|
|
252
|
+
Expected: typecheck clean; all `node --test test/*.test.mjs` pass (325+ tests, none of the pre-existing ones changed semantics); c8 coverage above gates (lines ≥85 / funcs ≥80 / branches ≥70); `npm pack --dry-run` succeeds.
|
|
253
|
+
|
|
254
|
+
- [ ] **Step 2: Commit any incidental fixes**
|
|
255
|
+
|
|
256
|
+
If verify surfaced issues, fix and commit with a message referencing issue #66. Otherwise nothing to commit.
|
|
257
|
+
|
|
258
|
+
---
|
|
259
|
+
|
|
260
|
+
### Task 4: Post-implementation manual verification (U1, U2)
|
|
261
|
+
|
|
262
|
+
**Files:** none (user verification; report back into the PR).
|
|
263
|
+
|
|
264
|
+
- [ ] **Step 1: U1 — attach-then-←**
|
|
265
|
+
|
|
266
|
+
1. Start agent-board, attach into an existing pi session (or create a new one).
|
|
267
|
+
2. As soon as the attach surface paints, press `←`.
|
|
268
|
+
Expected: returns to the dashboard immediately, no ↑/↓ or type-then-delete needed.
|
|
269
|
+
|
|
270
|
+
- [ ] **Step 2: U2 — ← while Pi is thinking**
|
|
271
|
+
|
|
272
|
+
1. Attach into a session and send a prompt that triggers Pi streaming (`⠹ Working...` animation visible).
|
|
273
|
+
2. While the animation is running, press `←`.
|
|
274
|
+
Expected: returns to the dashboard. (Fails before this fix — the exact reported symptom.)
|
|
275
|
+
|
|
276
|
+
- [ ] **Step 3: Sanity — ← does not steal the editor's cursor-left**
|
|
277
|
+
|
|
278
|
+
1. Attach, type a draft in the input box.
|
|
279
|
+
2. Press `←`.
|
|
280
|
+
Expected: cursor moves left inside the draft (key forwarded), NOT detach.
|
|
281
|
+
|
|
282
|
+
- [ ] **Step 4: Record results**
|
|
283
|
+
|
|
284
|
+
Write the U1/U2/U3 outcomes into the PR description (or a PR comment) before merge.
|