@zhuxixi/pi-agent-board 0.7.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 +46 -0
- package/README.md +16 -12
- package/VERIFY.md +2 -1
- package/docs/PTY_ATTACH_IMPLEMENTATION_PLAN.md +3 -1
- package/docs/RELEASE.md +148 -0
- package/docs/superpowers/plans/2026-09-10-reader-consistency.md +115 -0
- package/docs/superpowers/plans/2026-09-14-attach-cursor-dectcem-gate.md +469 -0
- package/docs/superpowers/plans/2026-09-14-attach-snapshot.md +92 -0
- package/docs/superpowers/plans/2026-09-14-host-meta-orphan-lock.md +771 -0
- package/docs/superpowers/plans/2026-09-14-issue-106-terminal-frame-cognition.md +299 -0
- package/docs/superpowers/plans/2026-09-14-issue-113-foreground-preview-race.md +609 -0
- package/docs/superpowers/plans/2026-09-14-terminal-model.md +145 -0
- package/docs/superpowers/plans/2026-09-15-coordinator-pipe-root-normalize.md +224 -0
- package/docs/superpowers/plans/2026-09-15-lease-publish-eprem-reclaim.md +341 -0
- package/docs/superpowers/plans/2026-09-18-detach-anchor-reporter-endpoint.md +875 -0
- package/docs/superpowers/plans/2026-09-20-control-lifecycle.md +116 -0
- package/docs/superpowers/plans/2026-09-20-issue-121-perf-gate-out-of-coverage.md +517 -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-14-attach-cursor-dectcem-gate-design.md +114 -0
- package/docs/superpowers/specs/2026-09-14-host-meta-orphan-lock-design.md +120 -0
- package/docs/superpowers/specs/2026-09-14-issue-106-terminal-frame-cognition-design.md +146 -0
- package/docs/superpowers/specs/2026-09-14-issue-113-foreground-preview-race-design.md +116 -0
- package/docs/superpowers/specs/2026-09-15-coordinator-pipe-root-normalize-design.md +84 -0
- package/docs/superpowers/specs/2026-09-15-lease-publish-eprem-reclaim-design.md +92 -0
- package/docs/superpowers/specs/2026-09-18-detach-anchor-reporter-endpoint-design.md +123 -0
- package/docs/superpowers/specs/2026-09-20-issue-121-perf-gate-out-of-coverage-design.md +204 -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 +3 -2
- package/runner/job-runner.mjs +8 -7
- package/runner/pty-runner.mjs +633 -31
- package/runner/state-coordinator.mjs +43 -17
- package/runner/state-runner.mjs +6 -5
- package/scripts/release.mjs +446 -0
- package/scripts/run-perf-gate.mjs +40 -0
- package/src/commands/agent-board.ts +8 -8
- package/src/commands/attach-flow.ts +5 -5
- package/src/core/control-protocol.mjs +482 -0
- package/src/core/editor-state-reporter.mjs +11 -1
- package/src/core/foreground-preview-cache.mjs +117 -0
- package/src/core/host-protocol.mjs +62 -0
- package/src/core/launch-options.mjs +37 -0
- package/src/core/locks.mjs +68 -14
- package/src/core/paths.mjs +35 -3
- package/src/core/pid.mjs +32 -1
- package/src/core/pty-attach-jiggle-controller.mjs +27 -3
- package/src/core/pty-attach-reconnect.mjs +13 -6
- package/src/core/pty-attach-render.mjs +20 -0
- package/src/core/state-commands.mjs +88 -6
- package/src/core/status-consistency.mjs +98 -0
- package/src/core/store.mjs +59 -13
- package/src/core/terminal-attach-client.mjs +809 -0
- package/src/core/terminal-attach-protocol.mjs +252 -0
- package/src/core/terminal-model.mjs +222 -0
- package/src/core/terminal-query-sequences.mjs +132 -0
- package/src/core/terminal-snapshot.mjs +440 -0
- package/src/index.ts +12 -4
- package/src/runtime/service.mjs +284 -16
- package/src/ui/dashboard.ts +61 -41
- package/src/ui/pty-attach.ts +318 -72
- package/src/core/pty-input.mjs +0 -47
|
@@ -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**(待用户执行)
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# Plan: attach 终端能力透传(issue #128)
|
|
2
|
+
|
|
3
|
+
Spec: `docs/superpowers/specs/2026-09-23-terminal-query-forwarding-design.md`(commit a3a7374)
|
|
4
|
+
Base: main 4ccb427
|
|
5
|
+
|
|
6
|
+
## Task 1: 提取器与纯函数(A1, A2)
|
|
7
|
+
|
|
8
|
+
**Files:**
|
|
9
|
+
- 新建 `src/core/terminal-query-sequences.mjs`:
|
|
10
|
+
- `extractOscQuerySequences(input) → { sequences, carry }` — 转发 `\x1b]11;?` + BEL(`\x07`)/ST(`\x1b\\`) 定界、`\x1b[?2031h`、`\x1b[?2031l`;**排除** `\x1b]11;rgb:...` 设置形态与 kitty/DA(`\x1b[>7u`/`\x1b[?u`/`\x1b[c`);carry 模式照抄 `extractOsc52Sequences` 结构(部分前缀保留、carry 上限)
|
|
11
|
+
- `toColorSchemeReport(scheme) → string` — light/dark → 颜色方案报告序列(997);**格式先读 pi-tui `parseTerminalColorSchemeReport`(node_modules/@earendil-works/pi-tui/dist/tui.js)源码对齐**,来源在测试注释标注
|
|
12
|
+
- `OSC_QUERY_CARRY_MAX_BYTES` 常量(对齐既有家族取值)
|
|
13
|
+
- 新建 `test/terminal-query-sequences.test.mjs`(unit):
|
|
14
|
+
- A1: 查询 BEL/ST 各转发一次;设置形态不转发;2031h/l 转发;跨 chunk(查询截半 → carry 拼接后识别);carry 不含完整序列时丢弃防积压
|
|
15
|
+
- A2: toColorSchemeReport light/dark 两态输出与 pi-tui parser 双向验证(序列喂 parser 能解回 scheme)
|
|
16
|
+
|
|
17
|
+
**Verify:** `node --test test/terminal-query-sequences.test.mjs` 全绿
|
|
18
|
+
|
|
19
|
+
**Constraints:** 纯函数零副作用;不 import pi-tui(格式对齐靠测试里 parser 交叉验证,生产代码硬编码序列——避免对 pi-tui 内部结构的运行时依赖)
|
|
20
|
+
|
|
21
|
+
## Task 2: attach 组件接线(A3, A4, A5, A6, A7)
|
|
22
|
+
|
|
23
|
+
**Files:**
|
|
24
|
+
- `src/ui/pty-attach.ts`:
|
|
25
|
+
- import 提取器;`forwardTerminalProtocols` 增加第三段(独立 `oscQueryCarry` 字段,kill switch `AGENT_BOARD_FORWARD_TERMINAL_QUERIES !== "0"` 默认开)
|
|
26
|
+
- `attachColorSchemeBridge()` — settle 时 `this.tui.onTerminalColorSchemeChange(...)` 注册,回调 `this.send({type:"input", data: toColorSchemeReport(scheme)})`;`done()`/detach 路径退订(A7 对称)
|
|
27
|
+
- `replayBackgroundQuery()` — settle 后一次性 `this.tui.terminal.write("\x1b]11;?\x07")`;不截获应答(靠既有兜底穿透);仅在本次 attach 从未发过时发(防重复 settle 重放)
|
|
28
|
+
- settle 路径接线两方法;确认 settle 判定处(attach loading banner 消失点)只调一次
|
|
29
|
+
- `test-support/detach-gate-smoke.ts` 扩展场景(P8-P11)+ `test/pty-attach-detach-gate.test.mjs` 断言:
|
|
30
|
+
- A3: 远端 output 含 `\x1b]11;?\x07` → localTerminalWrites 含之;含 `\x1b]11;rgb:ffff/ffff/ffff\x07` → 不写
|
|
31
|
+
- A4: 伪 tui 触发 colorScheme listener("light") → socket 收到 `type:"input"` + 997 报告;模拟 detach 后再触发 → 不发(A7)
|
|
32
|
+
- A5: attach settle → localTerminalWrites 恰一条 `\x1b]11;?\x07`;kill switch=0 时零条
|
|
33
|
+
- A6: 远端 output 含 `\x1b[>7u\x1b[?u\x1b[c` → localTerminalWrites 不含任何一段
|
|
34
|
+
|
|
35
|
+
**Verify:** 定向 `node --test test/pty-attach-detach-gate.test.mjs` + 全量 `node --test test/*.test.mjs`(账本 flake 除外)+ `npm run typecheck`
|
|
36
|
+
|
|
37
|
+
**Constraints:** 转发写失败静默(沿用既有 catch);桥/回放不影响 attach 主流程;kill switch 覆盖全部新行为
|
|
38
|
+
|
|
39
|
+
## Task 3: 文档同步(A 矩阵文案面, U1-U3 步骤引用)
|
|
40
|
+
|
|
41
|
+
**Files:**
|
|
42
|
+
- `README.md` attach 特性两段:新增「终端能力透传」小节——亮暗探测转发/桥接/首帧回放的行为描述 + kill switch + 已知未覆盖(kitty/DA 协商、OSC 10/12)
|
|
43
|
+
- spec 验收矩阵 U1/U3 的实测步骤引用 README 步骤
|
|
44
|
+
|
|
45
|
+
**Verify:** 文案与实现行为一致(reviewer 对照);无自动化新增
|
|
46
|
+
|
|
47
|
+
## Review 焦点预设
|
|
48
|
+
|
|
49
|
+
- T1: carry 边界(截半在定界符处/前缀处)、设置形态误匹配(`\x1b]11;?` 前缀是 rgb 设置的前缀吗——不是,`?` vs `r` 第一字符即分岔,测试钉死)
|
|
50
|
+
- T2: settle 只触发一次的路径证明;桥退订覆盖所有 done 路径(exit/error/detach/kill);kill switch 三处(转发/桥/回放)全覆盖
|
|
51
|
+
- T3: 文案不过度承诺(OSC 11「正常场景穿透」而非「总是」——本地 pending>0 边角如实)
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# Plan: 终端无关的无条件 detach 逃生键 Ctrl+\(issue #126)
|
|
2
|
+
|
|
3
|
+
Spec: docs/superpowers/specs/2026-09-23-unconditional-detach-key-design.md(首个 commit 69eb7d6,含验收矩阵与可测性拆分)。
|
|
4
|
+
|
|
5
|
+
## Verified ground truth (do not re-litigate)
|
|
6
|
+
|
|
7
|
+
- `matchesKey` 对三种编码全部原生匹配(spec review 实测):裸 `\x1c`、kitty `\x1b[92;5u`、modifyOtherKeys `\x1b[27;5;92~`。键名字符串 `"ctrl+\\"`(JS 源码双反斜杠)。
|
|
8
|
+
- pi 本体 dist / 本仓 src·runner·test 无 `ctrl+\` 占用。
|
|
9
|
+
- 集成点:`src/ui/pty-attach.ts` handleInput 的 `Key.ctrl("left")` 分支(当前 :290)**之前**插新分支;header(:343 附近 `"Ctrl+← detach"`)与 loading banner(:359 附近 `"Ctrl+← to detach"`)文案更新。
|
|
10
|
+
- smoke harness:`test-support/detach-gate-smoke.ts` 现有场景驱动真组件;`connected=true` 显式模式;Ctrl+← 场景为 L/M/N(对照模板)。
|
|
11
|
+
- README 按键段两处(:96 与 :195 附近),Phase 6 已改为 D1 契约文案,本次在其上补无条件键 + 已知代价(键盘布局限制、Ctrl+4 legacy 别名、child 抢键面)。
|
|
12
|
+
- 全量测试命令是 `node --test test/*.test.mjs`(目录形式会异常失败);CI 会跑同款。
|
|
13
|
+
|
|
14
|
+
## Tasks
|
|
15
|
+
|
|
16
|
+
### Task 1: Ctrl+\ 分支 + 四象限/三编码 smoke 场景(TDD)— covers A1, A2, A3
|
|
17
|
+
|
|
18
|
+
**Files:**
|
|
19
|
+
- Modify: `src/ui/pty-attach.ts`(handleInput 新分支)
|
|
20
|
+
- Modify: `test-support/detach-gate-smoke.ts`(新增场景)
|
|
21
|
+
- Modify: `test/pty-attach-detach-gate.test.mjs`(新断言接线)
|
|
22
|
+
|
|
23
|
+
**Steps:**
|
|
24
|
+
1. Red:smoke 新增场景组——四象限(editorEmpty=true/false/null × connected=true)+ disconnected 一条;每象限注入裸 `\x1c`,断言 `didDetach()` 为真(A1/A2)。另加编码形态场景:`\x1b[92;5u`(kitty)与 `\x1b[27;5;92~`(modifyOtherKeys)各一条 detach 断言;冲突隔离:`\x1b[1;5D` 仍 detach、`\x1b[D` 按 D1 门禁、可打印键 `q` 转发、`ctrl+]` 透传(A3)。先跑确认新场景红。
|
|
25
|
+
2. Green:handleInput 在 `Key.ctrl("left")` 分支**之前**插入:
|
|
26
|
+
|
|
27
|
+
```ts
|
|
28
|
+
if (matchesKey(data, "ctrl+\\")) {
|
|
29
|
+
// Unconditional, terminal-independent escape (issue #126): 0x1c is a
|
|
30
|
+
// single raw ASCII byte every terminal forwards unchanged in raw
|
|
31
|
+
// mode — no modifier-encoding dependency like Ctrl+Left (macOS
|
|
32
|
+
// Terminal.app never emits it, WezTerm consumes it for tab
|
|
33
|
+
// switching). Only caveat (documented in README): layouts where
|
|
34
|
+
// `\` sits on AltGr may never produce this byte; the escape ladder
|
|
35
|
+
// still covers those. matchesKey also covers the kitty CSI-u and
|
|
36
|
+
// modifyOtherKeys encodings of the same chord.
|
|
37
|
+
this.detach();
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
3. 定向测试全绿:`node --test test/pty-attach-detach-gate.test.mjs`。
|
|
43
|
+
|
|
44
|
+
### Task 2: 文案与 README 同步 — covers A4
|
|
45
|
+
|
|
46
|
+
**Files:**
|
|
47
|
+
- Modify: `src/ui/pty-attach.ts`(header + loading banner)
|
|
48
|
+
- Modify: `test/pty-attach-detach-gate.test.mjs`(header 断言扩到 Ctrl+\)
|
|
49
|
+
- Modify: `README.md`(两处按键段)
|
|
50
|
+
|
|
51
|
+
**Steps:**
|
|
52
|
+
1. header:`Ctrl+← detach` → `Ctrl+←/Ctrl+\\ detach`(注意渲染字符串里的反斜杠转义);loading banner 同步。detach-gate 测试的 header 断言补 Ctrl+\ 出现(沿用 `headerMentions*` 模式)。
|
|
53
|
+
2. README 两处按键段:补「`Ctrl+\` 是无条件退出键(终端无关)」+ 已知代价三条(键盘布局 AltGr 限制、Ctrl+4 legacy 别名、child 抢键面 = child 内 vim terminal-mode Ctrl+\ 前缀)。
|
|
54
|
+
3. 定向测试绿;全量 `node --test test/*.test.mjs` + `npm run typecheck`。
|
|
55
|
+
|
|
56
|
+
## Review discipline
|
|
57
|
+
|
|
58
|
+
每 task 完成派 reviewer;final whole-branch review 后 PR;Zima CR 收敛后 merge。验收逐项对账(spec 矩阵):A1-A4 自动化记录命令与结果;U1 mac 实测标 pending;U1b 列给用户执行。
|