@zhuxixi/pi-agent-board 0.8.0 → 0.9.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 +20 -0
- package/README.md +12 -11
- package/docs/RELEASE.md +148 -0
- package/docs/superpowers/plans/2026-09-23-a5-mid-stream-assertion.md +25 -0
- package/docs/superpowers/plans/2026-09-23-cwd-browse-tab-drill.md +393 -0
- package/docs/superpowers/plans/2026-09-23-terminal-query-forwarding.md +51 -0
- package/docs/superpowers/plans/2026-09-23-unconditional-detach-key.md +58 -0
- package/docs/superpowers/plans/2026-09-24-control-reconcile-flake.md +698 -0
- package/docs/superpowers/plans/2026-09-24-probe-attached-clients.md +605 -0
- package/docs/superpowers/specs/2026-09-23-a5-mid-stream-assertion-design.md +43 -0
- package/docs/superpowers/specs/2026-09-23-cwd-browse-tab-drill-design.md +95 -0
- package/docs/superpowers/specs/2026-09-23-terminal-query-forwarding-design.md +63 -0
- package/docs/superpowers/specs/2026-09-23-unconditional-detach-key-design.md +57 -0
- package/docs/superpowers/specs/2026-09-24-control-reconcile-flake-design.md +98 -0
- package/docs/superpowers/specs/2026-09-24-probe-attached-clients-design.md +190 -0
- package/package.json +1 -1
- package/runner/pty-runner.mjs +34 -21
- package/scripts/release.mjs +446 -0
- package/src/core/host-protocol.mjs +43 -5
- package/src/core/launch-options.mjs +37 -0
- package/src/core/terminal-attach-client.mjs +11 -5
- package/src/core/terminal-query-sequences.mjs +132 -0
- package/src/ui/dashboard.ts +20 -8
- package/src/ui/pty-attach.ts +84 -2
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,26 @@ conventional commits by `scripts/release_helper.mjs`. Entries are
|
|
|
5
5
|
forward-only: they begin with the first release after this file landed —
|
|
6
6
|
for earlier history, see the git log and the pull-request list.
|
|
7
7
|
|
|
8
|
+
## [0.9.0] - 2026-09-25
|
|
9
|
+
|
|
10
|
+
### Features
|
|
11
|
+
|
|
12
|
+
- **attach**: forward terminal queries to the real terminal — automatic theme works in attach sessions (issue #128) (#138)
|
|
13
|
+
- **attach**: unconditional Ctrl+\ detach key, terminal-independent (issue #126) (#137)
|
|
14
|
+
|
|
15
|
+
### Fixes
|
|
16
|
+
|
|
17
|
+
- **runner**: probes leave attachedClients at hello (issue #130) (#142)
|
|
18
|
+
- browse-mode Tab completes and drills down directories in cwd picker (issue #127) (#141)
|
|
19
|
+
|
|
20
|
+
### Changes
|
|
21
|
+
|
|
22
|
+
- **release**: orchestrate releases and publish from CI (#144)
|
|
23
|
+
- control-reconcile flaky assertions — windowed recorder + invariant realignment (issue #140) (#143)
|
|
24
|
+
- **attach**: scope A5 mid-stream seq assertion to post-snapshot window (issue #132) (#139)
|
|
25
|
+
|
|
26
|
+
[0.9.0]: https://github.com/zhuxixi/pi-agent-board/compare/v0.8.0...v0.9.0
|
|
27
|
+
|
|
8
28
|
## [0.8.0] - 2026-09-23
|
|
9
29
|
|
|
10
30
|
### Features
|
package/README.md
CHANGED
|
@@ -93,7 +93,7 @@ From the board:
|
|
|
93
93
|
- In Peek, press `r` to reply without attaching.
|
|
94
94
|
- Press `v` for a read-only transcript, or `e` for evidence and diagnostics.
|
|
95
95
|
- Press `Enter`, `Right`, or `>` to attach to the real Pi session.
|
|
96
|
-
- In PTY attach mode, press `Ctrl+Left` to return to the board at any time (even mid-draft). `Left` detaches only while the child Pi reports an empty editor; otherwise (draft, or unknown editor state) it is forwarded to the child. `Ctrl+]` is not a detach key — it is passed through to the child Pi editor. When the host is disconnected, `Left` always exits.
|
|
96
|
+
- In PTY attach mode, press `Ctrl+Left` to return to the board at any time (even mid-draft), or `Ctrl+\` — the unconditional escape: it detaches regardless of editor state on any terminal that forwards the raw 0x1c byte (raw, kitty, and modifyOtherKeys keyboard modes all work). `Left` detaches only while the child Pi reports an empty editor; otherwise (draft, or unknown editor state) it is forwarded to the child. `Ctrl+]` is not a detach key — it is passed through to the child Pi editor. When the host is disconnected, `Left` always exits. `Ctrl+\` caveats: on keyboard layouts where `\` sits on AltGr (e.g. German) the chord may never produce the byte — use `Ctrl+Left` there; in legacy keyboard modes `Ctrl+4` emits the same byte and also detaches; and the key is captured from the child while attached, so the child's own `Ctrl+\` binding (e.g. the vim terminal-mode prefix) is unavailable. Automatic themes also resolve correctly in attach sessions: the child's background probe (OSC 11) and color-scheme change notifications are forwarded to your real terminal, replies flow back to the child when your terminal answers, and attach re-probes the background once when it settles so the theme is right from the first frame. Set `AGENT_BOARD_FORWARD_TERMINAL_QUERIES=0` to disable this (default on); keyboard-protocol negotiation (kitty/DA) is not forwarded — it happens once at child spawn, before attach exists.
|
|
97
97
|
|
|
98
98
|
## Dashboard Workflow
|
|
99
99
|
|
|
@@ -192,7 +192,9 @@ The `e` view shows durable session evidence, including changed files, commands a
|
|
|
192
192
|
|
|
193
193
|
### PTY attach
|
|
194
194
|
|
|
195
|
-
PTY attach opens the real interactive Pi session. `Ctrl+Left` detaches and returns to the board regardless of editor state. `Left` detaches only while the child Pi reports an empty editor; while you are editing text — or when the child's editor state is unknown (e.g. the child extension is missing) — `Left` is forwarded to the Pi editor. A disconnected host can always be exited with `Left`. `Ctrl+]` is not a detach key — it is passed through to the child Pi editor. While attached, `PageUp`, `PageDown`, `Home`, `End`, and the mouse wheel scroll local scrollback. Mouse drag or double-click selects and copies text, clicks open detected links, and middle-click paste is available on systems with the required X11 tooling.
|
|
195
|
+
PTY attach opens the real interactive Pi session. `Ctrl+Left` detaches and returns to the board regardless of editor state, and `Ctrl+\` is the unconditional escape — a single raw ASCII byte (0x1c) that terminals forward unchanged in raw mode (kitty CSI-u and modifyOtherKeys encodings included), honored no matter what the child editor reports. `Left` detaches only while the child Pi reports an empty editor; while you are editing text — or when the child's editor state is unknown (e.g. the child extension is missing) — `Left` is forwarded to the Pi editor. A disconnected host can always be exited with `Left`. `Ctrl+]` is not a detach key — it is passed through to the child Pi editor. `Ctrl+\` caveats: on keyboard layouts where `\` sits on AltGr (e.g. German) the chord may never produce the byte — use `Ctrl+Left` there; in legacy keyboard modes `Ctrl+4` emits the same byte and also detaches; and the key is captured from the child while attached, so the child's own `Ctrl+\` binding (e.g. the vim terminal-mode prefix) is unavailable. While attached, `PageUp`, `PageDown`, `Home`, `End`, and the mouse wheel scroll local scrollback. Mouse drag or double-click selects and copies text, clicks open detected links, and middle-click paste is available on systems with the required X11 tooling.
|
|
196
|
+
|
|
197
|
+
Automatic light/dark themes work in attach sessions too. The child Pi's background-color probe (OSC 11) and its color-scheme change notification switch are forwarded to your real terminal; replies and notifications flow back to the child when your terminal answers — a terminal that never answers keeps the dark fallback. Attach also re-probes the background once when it settles, so the theme is correct from the first frame without re-selecting it in `/settings`. All of this is on by default; set `AGENT_BOARD_FORWARD_TERMINAL_QUERIES=0` to disable it. Not covered: keyboard-protocol negotiation (kitty flags / DA1) is not forwarded — the child negotiates once at spawn, before any attach client exists, and forwarding it would retune the shared real-terminal keyboard stack — so modifier-key encoding can still differ from a directly-run Pi (the modifyOtherKeys fallback applies).
|
|
196
198
|
|
|
197
199
|
The attach surface can forward terminal clipboard and image/file passthrough sequences. These behaviors can be disabled individually in [Configuration](#configuration). Cold hosts may briefly show a loading/reconnect surface while their PTY becomes ready.
|
|
198
200
|
|
|
@@ -383,19 +385,17 @@ npm run verify
|
|
|
383
385
|
|
|
384
386
|
## Publishing
|
|
385
387
|
|
|
386
|
-
|
|
388
|
+
Releases run through `scripts/release.mjs` and are published by CI when a GitHub Release is created. The flow checks the parts that are easy to get wrong — the changelog range, tag ancestry against `main`, and drift from a late merge — instead of assuming them. See [docs/RELEASE.md](docs/RELEASE.md) for the full process, the one-time npm trusted-publisher setup, and failure recovery.
|
|
387
389
|
|
|
388
390
|
```bash
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
npm version patch # bumps package.json, commits, and tags vX.Y.Z
|
|
395
|
-
npm publish
|
|
391
|
+
node scripts/release.mjs precheck # gate: clean main, nothing in flight
|
|
392
|
+
node scripts/release.mjs prepare minor # changelog + verify + bump + tag + release branch
|
|
393
|
+
node scripts/release.mjs open-pr 0.9.0 # push the branch and tag, open the release PR
|
|
394
|
+
# merge the PR with a merge commit, then:
|
|
395
|
+
node scripts/release.mjs finish 0.9.0 # GitHub Release → CI publishes to npm
|
|
396
396
|
```
|
|
397
397
|
|
|
398
|
-
|
|
398
|
+
After publishing, users install the scoped package with:
|
|
399
399
|
|
|
400
400
|
```bash
|
|
401
401
|
pi install npm:@zhuxixi/pi-agent-board
|
|
@@ -406,6 +406,7 @@ The Pi package gallery uses the `pi.video` and `pi.image` URLs from `package.jso
|
|
|
406
406
|
## Further Reading
|
|
407
407
|
|
|
408
408
|
- [Manual verification](VERIFY.md) — static checks, Pi loading, provider authentication, persistence, and dashboard flows.
|
|
409
|
+
- [Release process](docs/RELEASE.md) — release commands, trusted publishing setup, and failure recovery.
|
|
409
410
|
- [Product requirements](PRD.md) — original product scope and design context.
|
|
410
411
|
- [Progress log](PROGRESS.md) — implementation checkpoints and known environment notes.
|
|
411
412
|
- [Exploration notes](docs/EXPLORATION.md) — Pi API and integration research.
|
package/docs/RELEASE.md
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
# Release process
|
|
2
|
+
|
|
3
|
+
Releases are driven by `scripts/release.mjs` and published by CI when a GitHub
|
|
4
|
+
Release is created. Four commands and one merge:
|
|
5
|
+
|
|
6
|
+
```
|
|
7
|
+
precheck → prepare <bump> → open-pr <version> → merge (merge commit!)
|
|
8
|
+
→ finish <version> → .github/workflows/publish.yml publishes to npm
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
`main` is protected, so a release lands as a pull request — but the npm publish
|
|
12
|
+
is not tied to that PR: it is tied to the GitHub Release, which is created only
|
|
13
|
+
after the PR is merged and the tag is confirmed to be on `main`.
|
|
14
|
+
|
|
15
|
+
## One-time setup: the npm trusted publisher
|
|
16
|
+
|
|
17
|
+
`publish.yml` publishes with **trusted publishing** (OIDC), so no `NPM_TOKEN`
|
|
18
|
+
secret is stored in the repository. npm has to be told which workflow may
|
|
19
|
+
publish the package — this is a one-time, per-package web step:
|
|
20
|
+
|
|
21
|
+
1. Open <https://www.npmjs.com/package/@zhuxixi/pi-agent-board> and sign in as
|
|
22
|
+
the package owner.
|
|
23
|
+
2. Go to **Settings** → **Trusted Publisher** → **GitHub Actions**.
|
|
24
|
+
3. Fill in: organization `zhuxixi`, repository `pi-agent-board`, workflow
|
|
25
|
+
filename `publish.yml`, environment left empty.
|
|
26
|
+
4. Save.
|
|
27
|
+
|
|
28
|
+
Until this is configured, the CI publish step fails with an authentication
|
|
29
|
+
error. Publishing locally (the pre-2026-09 flow, see "Local publish fallback"
|
|
30
|
+
below) still works, because the npm account keeps its normal credentials.
|
|
31
|
+
|
|
32
|
+
## Commands
|
|
33
|
+
|
|
34
|
+
### `node scripts/release.mjs precheck`
|
|
35
|
+
|
|
36
|
+
Read-only gate. Run it before anything else; it fails closed when the tree is
|
|
37
|
+
dirty, `main` is not in sync with `origin/main`, a `release/*` PR is still open,
|
|
38
|
+
the last tag is not an ancestor of `origin/main` (an orphan tag from a past
|
|
39
|
+
squash merge), or there is nothing functional to release.
|
|
40
|
+
|
|
41
|
+
### `node scripts/release.mjs prepare <patch|minor|major|X.Y.Z> [--dry-run]`
|
|
42
|
+
|
|
43
|
+
Runs the gate, then:
|
|
44
|
+
|
|
45
|
+
1. renders the new CHANGELOG section from the conventional commits since the
|
|
46
|
+
last `v*` tag,
|
|
47
|
+
2. commits it as `docs(changelog): add <version> section`,
|
|
48
|
+
3. runs `npm version <bump>` — one commit plus the annotated `v<version>` tag,
|
|
49
|
+
4. switches to `release/v<version>`.
|
|
50
|
+
|
|
51
|
+
Nothing leaves the machine. `--dry-run` prints the section and the plan without
|
|
52
|
+
writing anything; review the preview before running it for real. Pick the bump
|
|
53
|
+
the same way as before: features are a `minor`, fixes alone are a `patch`.
|
|
54
|
+
|
|
55
|
+
### `node scripts/release.mjs open-pr <version> [--dry-run]`
|
|
56
|
+
|
|
57
|
+
Pushes the release branch and the tag, then opens the PR with the CHANGELOG
|
|
58
|
+
section as its body. The tag is pushed separately because tags are not covered
|
|
59
|
+
by the branch protection rule.
|
|
60
|
+
|
|
61
|
+
### Merge the PR with a merge commit
|
|
62
|
+
|
|
63
|
+
**Do not squash the release PR.** `npm version` created the tag on a commit that
|
|
64
|
+
only exists on the release branch; a merge commit brings that commit into
|
|
65
|
+
`main`, a squash does not, and the tag becomes unreachable from `main`
|
|
66
|
+
(`git describe` then misreads the changelog range of the next release). Use the
|
|
67
|
+
**Merge pull request** button, or `gh pr merge --merge`.
|
|
68
|
+
|
|
69
|
+
### `node scripts/release.mjs finish <version> [--dry-run] [--sync-main]`
|
|
70
|
+
|
|
71
|
+
Run it after the merge. It fetches, verifies that `v<version>` is an ancestor of
|
|
72
|
+
`origin/main`, re-checks the CHANGELOG against any functional PR merged after the
|
|
73
|
+
bump, and then creates the GitHub Release with the CHANGELOG section as its
|
|
74
|
+
notes. That release is the publish trigger. `--sync-main` also resets a clean
|
|
75
|
+
local `main` to `origin/main`, dropping the local copy of the two release
|
|
76
|
+
commits (they are content-equal to what the merge brought in).
|
|
77
|
+
|
|
78
|
+
To publish without the orchestration, the underlying commands are
|
|
79
|
+
`gh release create v<version> --title v<version> --notes-file <notes>` where the
|
|
80
|
+
notes are `node scripts/release.mjs notes <version>`.
|
|
81
|
+
|
|
82
|
+
## What CI does
|
|
83
|
+
|
|
84
|
+
`.github/workflows/publish.yml` runs on `release: published`, checks out the
|
|
85
|
+
tag, and:
|
|
86
|
+
|
|
87
|
+
1. asserts npm is at least 11.5.1 (trusted publishing needs it; Node 24 ships
|
|
88
|
+
a newer one),
|
|
89
|
+
2. asserts the tag equals `package.json`'s version,
|
|
90
|
+
3. runs `npm run typecheck` and `npm test`,
|
|
91
|
+
4. skips the publish when that version is already on npm, and otherwise
|
|
92
|
+
5. runs `npm publish --provenance --access public`.
|
|
93
|
+
|
|
94
|
+
Provenance means npm records which repository, workflow, and commit built the
|
|
95
|
+
tarball, and shows it on the package page.
|
|
96
|
+
|
|
97
|
+
## Recovering from a broken release
|
|
98
|
+
|
|
99
|
+
**Orphan tag** (`finish` refuses with "not an ancestor of origin/main"). The
|
|
100
|
+
release PR was squash-merged. Re-point the tag at the content-equivalent commit
|
|
101
|
+
on `main`, then retry:
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
git fetch origin
|
|
105
|
+
git tag -f v0.9.0 <sha on origin/main with the same content>
|
|
106
|
+
git push origin v0.9.0 --force
|
|
107
|
+
node scripts/release.mjs finish 0.9.0 --sync-main
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
**CHANGELOG drift** (`finish` refuses with missing PR numbers, or
|
|
111
|
+
`node scripts/release_helper.mjs verify` exits 1). A functional PR merged after
|
|
112
|
+
the changelog section was generated. Add the missing entries to the top section
|
|
113
|
+
in a `docs(changelog)` PR, merge it, then retry — the release notes are read
|
|
114
|
+
again at that point, so the fix is picked up.
|
|
115
|
+
|
|
116
|
+
**The publish failed** (npm outage, trusted publisher not configured yet).
|
|
117
|
+
Re-run the same tag without recreating the release: Actions → **Publish to
|
|
118
|
+
npm** → **Run workflow** → enter the tag.
|
|
119
|
+
|
|
120
|
+
**`npm view <package>` returns 404 right after a successful publish.** That is
|
|
121
|
+
the registry's view index lagging, not a failed publish. Compare endpoints: the
|
|
122
|
+
tarball, `/<package>/latest`, and `/-/package/<package>/dist-tags` answer
|
|
123
|
+
immediately while the root document 404s for a few minutes. Wait; re-publishing
|
|
124
|
+
the same version is rejected with a 403 and is never the fix.
|
|
125
|
+
|
|
126
|
+
**Local publish fallback.** When the trusted publisher is not configured and a
|
|
127
|
+
release cannot wait:
|
|
128
|
+
|
|
129
|
+
```bash
|
|
130
|
+
npm run verify
|
|
131
|
+
npm publish --registry=https://registry.npmjs.org --access public
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
The registry must be passed explicitly: this machine defaults to
|
|
135
|
+
`registry.npmmirror.com`. This path skips provenance and the CI checks, so
|
|
136
|
+
prefer fixing the CI path.
|
|
137
|
+
|
|
138
|
+
## CHANGELOG conventions
|
|
139
|
+
|
|
140
|
+
`scripts/release_helper.mjs` renders the section from conventional commits in
|
|
141
|
+
the range `<last v* tag>..HEAD`. `feat`/`fix`/`perf` become Features / Fixes /
|
|
142
|
+
Performance, everything else (including `test` and `docs`) lands under Changes,
|
|
143
|
+
merges and `npm version` squash subjects are skipped, and `docs(changelog):`
|
|
144
|
+
subjects are ignored so changelog fixes do not feed back into the verify check.
|
|
145
|
+
|
|
146
|
+
Every entry carries the PR number parsed from the trailing `(#N)`. `verify`
|
|
147
|
+
fails when a functional commit's PR is missing from the top section — that is
|
|
148
|
+
what keeps a late merge from shipping without release notes.
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# Plan: A5 mid-stream 断言窗口修正(issue #132)
|
|
2
|
+
|
|
3
|
+
Spec: `docs/superpowers/specs/2026-09-23-a5-mid-stream-assertion-design.md`(commit 5b6ec00)
|
|
4
|
+
Base: main 1e832a1
|
|
5
|
+
|
|
6
|
+
## Task 1: 断言修正 + stress 验证(A1, A2, A3, A4)
|
|
7
|
+
|
|
8
|
+
**Files:** `test/terminal-snapshot.integration.test.mjs`(唯一,A5 mid-stream test case 体内,不动 helper/其它用例)
|
|
9
|
+
|
|
10
|
+
**Changes:**
|
|
11
|
+
1. **D1(A1)**: `:241` 附近——`seqsB` 构造改为窗口限定:`messagesB.filter((m) => m.type === "output" && m.seq >= endB.nextSeq).map((m) => m.seq)`;断言注释补一句为什么(订阅前 legacy 行 seq ≤ snapshotSeq < nextSeq,被过滤——引用 runner/pty-runner.mjs 广播分支行号)
|
|
12
|
+
2. **D2(A2)**: `:227` 的 clientA waitFor 之后,加 `await waitFor(() => messagesB.filter((m) => m.type === "output" && m.seq >= endB.nextSeq).length >= ticks)`——显式等 socket B 追平再断言(沿用文件既有 waitFor 默认——15s,:29;A5-burst 的显式 30s 参数不在本 case)
|
|
13
|
+
3. 断言本体不变(严格连续形态保留——窗口限定后就是纯协议流)
|
|
14
|
+
4. **A3 stress 验证**:在 worktree 里跑 3 轮并行全量 `node --test test/*.test.mjs`(背靠背,高争用)——记录每轮 A5 mid-stream 结果;**若任何一轮 A5 mid-stream 仍失败且错误为 "strictly contiguous"** → 立即回炉报告,不许带病交付;账本 flake(subscribe_terminal / stop-finalize / A5-burst)照常隔离复跑不算回炉条件
|
|
15
|
+
5. **A4**:全量其余断言绿
|
|
16
|
+
|
|
17
|
+
**Verify:** `node --test test/terminal-snapshot.integration.test.mjs`(定向)+ 3 轮并行全量 + `npm run typecheck`
|
|
18
|
+
|
|
19
|
+
**Commit:** `test(attach): scope A5 mid-stream seq assertion to post-snapshot window, wait for socket B catch-up explicitly (issue #132)`
|
|
20
|
+
|
|
21
|
+
## Review 焦点预设
|
|
22
|
+
|
|
23
|
+
- 窗口下限为什么是 `endB.nextSeq` 而非 `beginB.snapshotSeq + 1`(二者相等,用 nextSeq 更贴近协议游标语义)
|
|
24
|
+
- D2 等待的 30s 天花板来源(文件 waitFor 默认),与 A5-burst 的显式 30s 参数区分
|
|
25
|
+
- stress 3 轮的统计学诚实度(概率验证非根除证明,报告如实记录)
|
|
@@ -0,0 +1,393 @@
|
|
|
1
|
+
# cwd 选择器 browse 模式 Tab 补全 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:** 让 Start session 的 cwd 选择器在文件系统浏览(browse)模式下 Tab 键可用:补全高亮候选、连续 Tab 像 shell 一样逐层下钻(issue #127,选项 B)。
|
|
6
|
+
|
|
7
|
+
**Architecture:** 新语义全部沉到 `src/core/launch-options.mjs` 的两个纯函数(`sameResolvedDir`、`browseTabCompletion`);`src/ui/dashboard.ts` 的 Tab 分支删掉 favorites-only 模式守卫,browse 走新 helper,favorites 逻辑一字不动。探针式集成测试(test-support/*.ts + `node --experimental-transform-types`)覆盖接线层,沿用 `navigation-wrap.ts` 既有模式。
|
|
8
|
+
|
|
9
|
+
**Tech Stack:** Node.js (node:test)、TypeScript(dashboard.ts,strip-types 经 transform 跑)、纯 JSDoc 的 .mjs core 层。
|
|
10
|
+
|
|
11
|
+
**Worktree(所有作业路径基准,记作 $WT):** `/home/elling/git-repo/github/pi-agent-board/.pi/worktrees/issue-127-cwd-browse-tab-drill`
|
|
12
|
+
|
|
13
|
+
**Spec:** `$WT/docs/superpowers/specs/2026-09-23-cwd-browse-tab-drill-design.md`(已提交,含验收矩阵 A1–A6/U1)
|
|
14
|
+
|
|
15
|
+
## Global Constraints
|
|
16
|
+
|
|
17
|
+
- 只在 $WT 内作业;git 操作用 `git -C $WT`;**禁碰主 checkout**。
|
|
18
|
+
- 测试命令:`node --test test/<file>.test.mjs`(在 $WT 下跑);全量 `npm test`(= `node --test test/*.test.mjs`)。
|
|
19
|
+
- dashboard.ts 用了 TS parameter properties,测试必须经 `node --experimental-transform-types` 子进程跑探针;test-support/ 不在 tsconfig 内、不做 typecheck。
|
|
20
|
+
- 纯函数边界是实现硬约束:`sameResolvedDir` / `browseTabCompletion` 不得依赖 dashboard 状态;dashboard.ts 只接线。
|
|
21
|
+
- favorites 模式的既有 Tab 语义(补全进输入框、回车确认)一行不改。
|
|
22
|
+
- 不做 `~` 展开进收藏匹配(spec 已确认范围外);不做 fuzzy 匹配。
|
|
23
|
+
- commit message 用 conventional commits(`fix:` / `feat:` / `test:`),按文件 `git add`,禁用 `git add -A`。
|
|
24
|
+
|
|
25
|
+
## 验收追溯(spec 验收 ID ↔ task)
|
|
26
|
+
|
|
27
|
+
| 验收 ID | 归属 task |
|
|
28
|
+
|---------|-----------|
|
|
29
|
+
| A4 纯函数 helper 边界 | Task 1 |
|
|
30
|
+
| A1 browse Tab 补全 + 重算 | Task 2 |
|
|
31
|
+
| A2 连续 Tab 逐层下钻 | Task 2 |
|
|
32
|
+
| A3 favorites Tab 不回归 | Task 2 |
|
|
33
|
+
| A5 browse 提示含 tab | Task 2 |
|
|
34
|
+
| A6 全量测试 + typecheck 无回归 | Task 3 |
|
|
35
|
+
| U1 真实 TUI 手感(用户实测) | Task 4(post-implementation manual verification) |
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
### Task 1: 纯函数 helpers —— `sameResolvedDir` + `browseTabCompletion`
|
|
40
|
+
|
|
41
|
+
**Files:**
|
|
42
|
+
- Modify: `$WT/src/core/launch-options.mjs`(在 `filterCwdCandidates` 附近追加;`expandHome` 定义于 :279,同文件可直接调用;`path` 已 import)
|
|
43
|
+
- Test: `$WT/test/launch-options.test.mjs`(现有 import 行 :6 追加两个函数名;文件已 import `node:assert/strict`、`node:test`;另需 `import os from "node:os";` 与 `import path from "node:path";`,若已有则复用)
|
|
44
|
+
|
|
45
|
+
**Interfaces:**
|
|
46
|
+
- Produces:
|
|
47
|
+
- `sameResolvedDir(a: string, b: string): boolean` — 两路径串是否解析到同一目录;空白输入永不匹配。
|
|
48
|
+
- `browseTabCompletion(query: string, suggestions: string[], index: number): { query: string, completed: string, usedIndex: number } | null` — browse 模式 Tab 补全计算;suggestions 为空返回 null。
|
|
49
|
+
- Consumes: 无(首个 task)。
|
|
50
|
+
|
|
51
|
+
- [ ] **Step 1: 写失败测试**(追加到 `$WT/test/launch-options.test.mjs`;先确认文件头部的 import 行,缺 `os`/`path` 就补)
|
|
52
|
+
|
|
53
|
+
```js
|
|
54
|
+
test("sameResolvedDir: matches across trailing separator and ~; blank never matches", () => {
|
|
55
|
+
const home = os.homedir();
|
|
56
|
+
assert.equal(sameResolvedDir("~/work", `${home}/work/`), true);
|
|
57
|
+
assert.equal(sameResolvedDir("/a/b", "/a/c"), false);
|
|
58
|
+
assert.equal(sameResolvedDir("", "/a"), false);
|
|
59
|
+
assert.equal(sameResolvedDir(" ", "/a"), false);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test("browseTabCompletion: completes highlighted suggestion with trailing separator", () => {
|
|
63
|
+
const res = browseTabCompletion("/tmp/x/wo", ["/tmp/x/work"], 0);
|
|
64
|
+
assert.deepEqual(res, { query: `/tmp/x/work${path.sep}`, completed: "/tmp/x/work", usedIndex: 0 });
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test("browseTabCompletion: no-progress highlight advances cyclically", () => {
|
|
68
|
+
const s = ["/tmp/x/work", "/tmp/x/work/app", "/tmp/x/work/notes"];
|
|
69
|
+
const res = browseTabCompletion(`/tmp/x/work${path.sep}`, s, 0);
|
|
70
|
+
assert.equal(res.usedIndex, 1);
|
|
71
|
+
assert.equal(res.query, `/tmp/x/work/app${path.sep}`);
|
|
72
|
+
// wraps from the last entry back to the first
|
|
73
|
+
const res2 = browseTabCompletion("/tmp/x/work/notes", ["/tmp/x/work", "/tmp/x/work/notes"], 1);
|
|
74
|
+
assert.equal(res2.usedIndex, 0);
|
|
75
|
+
assert.equal(res2.query, `/tmp/x/work${path.sep}`);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test("browseTabCompletion: single self suggestion stays put (drill naturally stops)", () => {
|
|
79
|
+
const res = browseTabCompletion(`/tmp/x/work${path.sep}`, ["/tmp/x/work"], 0);
|
|
80
|
+
assert.equal(res.query, `/tmp/x/work${path.sep}`);
|
|
81
|
+
assert.equal(res.usedIndex, 0);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test("browseTabCompletion: empty suggestions -> null; out-of-range index clamps", () => {
|
|
85
|
+
assert.equal(browseTabCompletion("x", [], 0), null);
|
|
86
|
+
assert.equal(browseTabCompletion("x", null, 0), null);
|
|
87
|
+
const res = browseTabCompletion("x", ["/a"], 7);
|
|
88
|
+
assert.equal(res.usedIndex, 0);
|
|
89
|
+
});
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
- [ ] **Step 2: 跑测试确认失败**
|
|
93
|
+
|
|
94
|
+
Run: `cd $WT && node --test test/launch-options.test.mjs`
|
|
95
|
+
Expected: FAIL(`sameResolvedDir is not defined` / import 报错)
|
|
96
|
+
|
|
97
|
+
- [ ] **Step 3: 实现 helpers**(追加到 `$WT/src/core/launch-options.mjs`,放在 `filterCwdCandidates` 之后、`nextCwdPickerState` 之前)
|
|
98
|
+
|
|
99
|
+
```js
|
|
100
|
+
/**
|
|
101
|
+
* Whether two path strings resolve to the same directory. Blank inputs never
|
|
102
|
+
* match (path.resolve("") would silently equal process.cwd()).
|
|
103
|
+
* @param {string} a
|
|
104
|
+
* @param {string} b
|
|
105
|
+
* @returns {boolean}
|
|
106
|
+
*/
|
|
107
|
+
export function sameResolvedDir(a, b) {
|
|
108
|
+
const na = String(a ?? "").trim();
|
|
109
|
+
const nb = String(b ?? "").trim();
|
|
110
|
+
if (!na || !nb) return false;
|
|
111
|
+
return path.resolve(expandHome(na)) === path.resolve(expandHome(nb));
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Browse-mode Tab completion for the cwd picker. Completes to the highlighted
|
|
116
|
+
* suggestion with a trailing separator (so the next recompute lists that
|
|
117
|
+
* directory's children); when the highlight already equals the current query
|
|
118
|
+
* (no progress), advances cyclically so repeated Tab drills downward. A lone
|
|
119
|
+
* self suggestion stays put, so drilling stops naturally at leaf directories.
|
|
120
|
+
* @param {string} query
|
|
121
|
+
* @param {string[]} suggestions
|
|
122
|
+
* @param {number} index
|
|
123
|
+
* @returns {{query: string, completed: string, usedIndex: number} | null}
|
|
124
|
+
*/
|
|
125
|
+
export function browseTabCompletion(query, suggestions, index) {
|
|
126
|
+
if (!suggestions || suggestions.length === 0) return null;
|
|
127
|
+
const len = suggestions.length;
|
|
128
|
+
let usedIndex = ((index % len) + len) % len;
|
|
129
|
+
let completed = suggestions[usedIndex];
|
|
130
|
+
if (len > 1 && sameResolvedDir(completed, query)) {
|
|
131
|
+
usedIndex = (usedIndex + 1) % len;
|
|
132
|
+
completed = suggestions[usedIndex];
|
|
133
|
+
}
|
|
134
|
+
return { query: completed + path.sep, completed, usedIndex };
|
|
135
|
+
}
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
- [ ] **Step 4: 跑测试确认通过**
|
|
139
|
+
|
|
140
|
+
Run: `cd $WT && node --test test/launch-options.test.mjs`
|
|
141
|
+
Expected: PASS(新旧用例全绿)
|
|
142
|
+
|
|
143
|
+
- [ ] **Step 5: Commit**
|
|
144
|
+
|
|
145
|
+
```bash
|
|
146
|
+
git -C $WT add src/core/launch-options.mjs test/launch-options.test.mjs
|
|
147
|
+
git -C $WT commit -m "feat: add browse-mode Tab completion helpers for cwd picker (issue #127)"
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
---
|
|
151
|
+
|
|
152
|
+
### Task 2: dashboard 接线 + browse 提示文案 + 探针集成测试
|
|
153
|
+
|
|
154
|
+
**Files:**
|
|
155
|
+
- Create: `$WT/test-support/cwd-browse-tab-probe.ts`
|
|
156
|
+
- Create: `$WT/test/dashboard-cwd-tab.test.mjs`
|
|
157
|
+
- Modify: `$WT/src/ui/dashboard.ts`(import 行;Tab 分支 :598-608;提示文案 :1586)
|
|
158
|
+
|
|
159
|
+
**Interfaces:**
|
|
160
|
+
- Consumes: Task 1 的 `sameResolvedDir`(不直接用)、`browseTabCompletion`(dashboard.ts 引用)。
|
|
161
|
+
- Produces: 无新对外接口(行为修复)。
|
|
162
|
+
|
|
163
|
+
- [ ] **Step 1: 写失败探针与测试**
|
|
164
|
+
|
|
165
|
+
Create `$WT/test-support/cwd-browse-tab-probe.ts`:
|
|
166
|
+
|
|
167
|
+
```ts
|
|
168
|
+
// cwd browse-Tab probe (issue #127): drive Tab through the public handleInput()
|
|
169
|
+
// in browse and favorites picker modes and report picker state as JSON.
|
|
170
|
+
// Run via `node --experimental-transform-types` (dashboard.ts uses TS
|
|
171
|
+
// parameter properties). Not typechecked (tsconfig excludes test-support).
|
|
172
|
+
import { mkdirSync, mkdtempSync } from "node:fs";
|
|
173
|
+
import { tmpdir } from "node:os";
|
|
174
|
+
import { join, sep } from "node:path";
|
|
175
|
+
import { createService } from "../src/runtime/service.mjs";
|
|
176
|
+
import { DashboardComponent } from "../src/ui/dashboard.ts";
|
|
177
|
+
import { nextCwdPickerState } from "../src/core/launch-options.mjs";
|
|
178
|
+
|
|
179
|
+
const root = mkdtempSync(join(tmpdir(), "agentview-cwd-tab-"));
|
|
180
|
+
const work = join(root, "work");
|
|
181
|
+
mkdirSync(join(work, "app"), { recursive: true });
|
|
182
|
+
mkdirSync(join(work, "notes"), { recursive: true });
|
|
183
|
+
|
|
184
|
+
const service = createService({
|
|
185
|
+
root,
|
|
186
|
+
runnerScript: "/no/runner.mjs",
|
|
187
|
+
piCommand: "pi",
|
|
188
|
+
piArgsPrefix: [],
|
|
189
|
+
defaultCwd: root,
|
|
190
|
+
launch: () => ({ pid: null, configPath: "/no/config.json" }),
|
|
191
|
+
launchHost: () => ({ pid: null, configPath: "/no/host-config.json" }),
|
|
192
|
+
launchTitle: () => ({ pid: null, configPath: "/no/title-config.json" }),
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
const tui = {
|
|
196
|
+
terminal: { rows: 24, cols: 80, columns: 80, write: () => {} },
|
|
197
|
+
requestRender: () => {},
|
|
198
|
+
};
|
|
199
|
+
const theme = { fg: (_c, t) => t, bold: (t) => t };
|
|
200
|
+
|
|
201
|
+
const dash = new DashboardComponent(tui, theme, {}, () => {}, {
|
|
202
|
+
service,
|
|
203
|
+
root,
|
|
204
|
+
defaultCwd: root,
|
|
205
|
+
availableModels: [],
|
|
206
|
+
currentModel: null,
|
|
207
|
+
currentThinkingLevel: "off",
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
// TS-private is runtime-accessible; white-box the launch dialog open
|
|
211
|
+
// (input must be non-empty or openLaunchDialog bails back to list mode).
|
|
212
|
+
dash.input = "go";
|
|
213
|
+
dash.openLaunchDialog();
|
|
214
|
+
dash.openLaunchPicker("cwd", join(root, "wo"));
|
|
215
|
+
|
|
216
|
+
const out = { root, work };
|
|
217
|
+
out.mode0 = dash.launch.cwdPickerMode;
|
|
218
|
+
out.sugg0 = dash.launch.cwdSuggestions;
|
|
219
|
+
|
|
220
|
+
// Browse Tab 1: completes the highlighted candidate with a trailing separator.
|
|
221
|
+
dash.handleInput("\t");
|
|
222
|
+
out.afterTab1 = {
|
|
223
|
+
query: dash.launch.cwdQuery,
|
|
224
|
+
mode: dash.launch.cwdPickerMode,
|
|
225
|
+
suggestions: dash.launch.cwdSuggestions,
|
|
226
|
+
index: dash.launch.cwdSuggestionIndex,
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
// Browse Tab 2: highlight sits on the just-completed dir (no progress) -> drill into first child.
|
|
230
|
+
dash.handleInput("\t");
|
|
231
|
+
out.afterTab2 = { query: dash.launch.cwdQuery, mode: dash.launch.cwdPickerMode };
|
|
232
|
+
|
|
233
|
+
// Browse Tab 3: leaf dir has a lone self suggestion -> stable no-op.
|
|
234
|
+
dash.handleInput("\t");
|
|
235
|
+
out.afterTab3 = dash.launch.cwdQuery;
|
|
236
|
+
|
|
237
|
+
// Browse hint advertises tab completion (render while still in browse mode).
|
|
238
|
+
out.browseHint = dash.render(80).join("\n").includes("type to filter folders · tab complete");
|
|
239
|
+
|
|
240
|
+
// Favorites regression: ranked hit keeps the old complete-into-input semantics (no trailing separator).
|
|
241
|
+
const launch = dash.launch;
|
|
242
|
+
launch.cwdRanked = [{ path: work, count: 3 }];
|
|
243
|
+
launch.cwdQuery = "wo";
|
|
244
|
+
const st = nextCwdPickerState("wo", launch.cwdRanked, launch.cwd);
|
|
245
|
+
launch.cwdPickerMode = st.mode;
|
|
246
|
+
launch.cwdSuggestions = st.suggestions;
|
|
247
|
+
launch.cwdSuggestionIndex = 0;
|
|
248
|
+
dash.handleInput("\t");
|
|
249
|
+
out.fav = { query: dash.launch.cwdQuery, mode: dash.launch.cwdPickerMode };
|
|
250
|
+
|
|
251
|
+
dash.dispose();
|
|
252
|
+
console.log(JSON.stringify(out));
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
Create `$WT/test/dashboard-cwd-tab.test.mjs`:
|
|
256
|
+
|
|
257
|
+
```js
|
|
258
|
+
import assert from "node:assert/strict";
|
|
259
|
+
import { execFileSync } from "node:child_process";
|
|
260
|
+
import { fileURLToPath } from "node:url";
|
|
261
|
+
import { join, sep } from "node:path";
|
|
262
|
+
import test from "node:test";
|
|
263
|
+
|
|
264
|
+
const ROOT_DIR = fileURLToPath(new URL("../", import.meta.url));
|
|
265
|
+
const PROBE = join(ROOT_DIR, "test-support", "cwd-browse-tab-probe.ts");
|
|
266
|
+
|
|
267
|
+
test("cwd picker: browse-mode Tab completes and drills; favorites Tab unchanged (issue #127)", () => {
|
|
268
|
+
// dashboard.ts uses TS parameter properties, which strip-only mode rejects;
|
|
269
|
+
// --experimental-transform-types handles them (Node 22.7+ / 24).
|
|
270
|
+
const out = execFileSync(process.execPath, ["--experimental-transform-types", PROBE], {
|
|
271
|
+
encoding: "utf8",
|
|
272
|
+
timeout: 30_000,
|
|
273
|
+
});
|
|
274
|
+
const r = JSON.parse(out);
|
|
275
|
+
const work = r.work;
|
|
276
|
+
|
|
277
|
+
// A1: browse-mode Tab completes the highlighted candidate into the query.
|
|
278
|
+
assert.equal(r.mode0, "browse");
|
|
279
|
+
assert.deepEqual(r.sugg0, [work]);
|
|
280
|
+
assert.equal(r.afterTab1.query, `${work}${sep}`);
|
|
281
|
+
assert.equal(r.afterTab1.mode, "browse");
|
|
282
|
+
assert.equal(r.afterTab1.suggestions[0], work);
|
|
283
|
+
|
|
284
|
+
// A2: repeated Tab drills down like a shell (work/ -> work/app/), stopping at the leaf.
|
|
285
|
+
assert.equal(r.afterTab2.query, `${join(work, "app")}${sep}`);
|
|
286
|
+
assert.equal(r.afterTab2.mode, "browse");
|
|
287
|
+
assert.equal(r.afterTab3, `${join(work, "app")}${sep}`);
|
|
288
|
+
|
|
289
|
+
// A5: browse-mode hint advertises tab completion.
|
|
290
|
+
assert.equal(r.browseHint, true);
|
|
291
|
+
|
|
292
|
+
// A3: favorites mode keeps the original semantics (bare path, mode stays favorites).
|
|
293
|
+
assert.equal(r.fav.query, work);
|
|
294
|
+
assert.equal(r.fav.mode, "favorites");
|
|
295
|
+
});
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
- [ ] **Step 2: 跑测试确认失败(复现 bug)**
|
|
299
|
+
|
|
300
|
+
Run: `cd $WT && node --test test/dashboard-cwd-tab.test.mjs`
|
|
301
|
+
Expected: FAIL —— `r.afterTab1.query` 仍是探针 seed 的 `join(root, "wo")`(browse 模式 Tab 被静默吞掉),A1 断言先挂。
|
|
302
|
+
|
|
303
|
+
- [ ] **Step 3: 实现修复**
|
|
304
|
+
|
|
305
|
+
a) `$WT/src/ui/dashboard.ts` 顶部 import(:24 附近,现有 `nextCwdPickerState` 同一条 import 里)追加 `browseTabCompletion`。
|
|
306
|
+
|
|
307
|
+
b) 替换 Tab 分支(:598-608)为:
|
|
308
|
+
|
|
309
|
+
```ts
|
|
310
|
+
if (launch.picker === "cwd" && matchesKey(data, Key.tab)) {
|
|
311
|
+
if (launch.cwdSuggestions.length > 0) {
|
|
312
|
+
if (launch.cwdPickerMode === "browse") {
|
|
313
|
+
const completion = browseTabCompletion(launch.cwdQuery, launch.cwdSuggestions, launch.cwdSuggestionIndex);
|
|
314
|
+
if (completion) {
|
|
315
|
+
launch.cwdQuery = completion.query;
|
|
316
|
+
const state = nextCwdPickerState(launch.cwdQuery, launch.cwdRanked, launch.cwd);
|
|
317
|
+
launch.cwdPickerMode = state.mode;
|
|
318
|
+
launch.cwdSuggestions = state.suggestions;
|
|
319
|
+
launch.cwdSuggestionIndex = Math.max(0, state.suggestions.indexOf(completion.completed));
|
|
320
|
+
}
|
|
321
|
+
} else {
|
|
322
|
+
const completed = launch.cwdSuggestions[launch.cwdSuggestionIndex] ?? launch.cwdSuggestions[0];
|
|
323
|
+
launch.cwdQuery = completed;
|
|
324
|
+
const state = nextCwdPickerState(launch.cwdQuery, launch.cwdRanked, launch.cwd);
|
|
325
|
+
launch.cwdPickerMode = state.mode;
|
|
326
|
+
launch.cwdSuggestions = state.suggestions;
|
|
327
|
+
launch.cwdSuggestionIndex = Math.max(0, state.suggestions.indexOf(completed));
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
c) browse 提示文案(:1586):
|
|
335
|
+
|
|
336
|
+
```ts
|
|
337
|
+
lines.push(t.fg("dim", "type to filter folders · tab complete · enter choose · esc back"));
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
- [ ] **Step 4: 跑测试确认通过**
|
|
341
|
+
|
|
342
|
+
Run: `cd $WT && node --test test/dashboard-cwd-tab.test.mjs`
|
|
343
|
+
Expected: PASS(A1/A2/A3/A5 全绿)
|
|
344
|
+
|
|
345
|
+
- [ ] **Step 5: Commit**
|
|
346
|
+
|
|
347
|
+
```bash
|
|
348
|
+
git -C $WT add src/ui/dashboard.ts test-support/cwd-browse-tab-probe.ts test/dashboard-cwd-tab.test.mjs
|
|
349
|
+
git -C $WT commit -m "fix: browse-mode Tab completes and drills down directories in cwd picker (issue #127)"
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
---
|
|
353
|
+
|
|
354
|
+
### Task 3: 全量回归 + typecheck(A6)
|
|
355
|
+
|
|
356
|
+
**Files:** 无改动(纯验证)。
|
|
357
|
+
|
|
358
|
+
**Interfaces:**
|
|
359
|
+
- Consumes: Task 1、Task 2 的全部产物。
|
|
360
|
+
- Produces: 无。
|
|
361
|
+
|
|
362
|
+
- [ ] **Step 1: 全量测试**
|
|
363
|
+
|
|
364
|
+
Run: `cd $WT && npm test`
|
|
365
|
+
Expected: PASS(`node --test test/*.test.mjs` 全绿;特别关注 launch-options / dashboard 相关文件无回归)
|
|
366
|
+
|
|
367
|
+
- [ ] **Step 2: typecheck**
|
|
368
|
+
|
|
369
|
+
Run: `cd $WT && npm run typecheck`
|
|
370
|
+
Expected: PASS(`tsc --noEmit` 无错;dashboard.ts 对 `browseTabCompletion` 的调用类型正确,含 null 收窄)
|
|
371
|
+
|
|
372
|
+
- [ ] **Step 3: 如全量中有与本改动无关的既存 flake(对照 issue #95/#140 清单)失败,隔离复跑该文件确认;与本改动相关则回到对应 task 修**
|
|
373
|
+
|
|
374
|
+
---
|
|
375
|
+
|
|
376
|
+
### Task 4: 用户实测 U1(post-implementation manual verification)
|
|
377
|
+
|
|
378
|
+
**Files:** 无改动。
|
|
379
|
+
|
|
380
|
+
- [ ] **Step 1: 请用户实测**(PR 合并前执行;自动化测试通过不能替代本项)
|
|
381
|
+
|
|
382
|
+
操作步骤:
|
|
383
|
+
1. 从本 worktree 构建/链接后打开 agent-board 面板,进入 Start session。
|
|
384
|
+
2. 聚焦 cwd 字段,输入 `~/wo`(或任意 `~` 开头路径片段),确认候选列表出现后按 Tab。
|
|
385
|
+
3. 连续按 Tab 观察逐层下钻;用 ↑/↓ 调整高亮后再按 Tab;Enter 确认并启动 session。
|
|
386
|
+
|
|
387
|
+
观察结果(记录):
|
|
388
|
+
- Tab 是否补全高亮候选进输入框;连续 Tab 是否逐层深入;叶子目录是否自然停止。
|
|
389
|
+
- 启动的 session cwd 是否为最终确认的目录。
|
|
390
|
+
|
|
391
|
+
通过标准:补全/下钻手感符合 shell 习惯;session cwd 正确;favorites 场景(输入收藏目录名片段)Tab 行为与 main 一致。
|
|
392
|
+
|
|
393
|
+
实测结果:**pending**(待用户执行)
|