@zhuxixi/pi-agent-board 0.10.0 → 0.10.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +19 -0
- package/docs/superpowers/plans/2026-10-06-attach-settle-scheme-delivery.md +751 -0
- package/docs/superpowers/plans/2026-10-09-issue-167-attach-middle-paste.md +509 -0
- package/docs/superpowers/plans/2026-10-09-issue-169-fullscreen-mouse-ownership.md +346 -0
- package/docs/superpowers/specs/2026-10-06-attach-settle-scheme-delivery-design.md +196 -0
- package/docs/superpowers/specs/2026-10-09-issue-167-attach-middle-paste-design.md +125 -0
- package/docs/superpowers/specs/2026-10-09-issue-169-fullscreen-mouse-ownership-design.md +130 -0
- package/package.json +3 -3
- package/src/commands/agent-board.ts +11 -0
- package/src/commands/attach-flow.ts +11 -0
- package/src/core/pty-scroll.mjs +25 -0
- package/src/core/terminal-query-sequences.mjs +81 -0
- package/src/ui/pty-attach.ts +125 -41
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
# Issue #169 Fullscreen Outer-Mouse Ownership 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:** Stop `PtyAttachComponent` from writing ANY outer-terminal mouse-mode sequence in fullscreen mode (where `TuiAltScreen` owns reporting), fixing the post-detach mouse kill and the mid-attach mode downgrade, while keeping regular-mode behavior byte-identical.
|
|
6
|
+
|
|
7
|
+
**Architecture:** A pure decision helper (`shouldOwnOuterMouseMode(mode, mouseEnabled)` in `src/core/pty-scroll.mjs`) decides ownership; the component's two write choke points (`enableMouseScroll`, `disableMouseScroll`) both early-return when it says no. All existing call sites (ctor, connect, resize, close) route through those two methods, so no other code changes.
|
|
8
|
+
|
|
9
|
+
**Tech Stack:** TypeScript (tabs), node:test + assert/strict, node --experimental-transform-types scenario with a fake-TUI write spy and a nonexistent control socket.
|
|
10
|
+
|
|
11
|
+
**Spec:** `docs/superpowers/specs/2026-10-09-issue-169-fullscreen-mouse-ownership-design.md` (acceptance IDs A1–A4, U1 referenced per task). Cross-review was waived by user decision (recorded in the issue research dir).
|
|
12
|
+
|
|
13
|
+
## Global Constraints
|
|
14
|
+
|
|
15
|
+
- Work ONLY inside the worktree `<WT>` (branch `issue-169-fullscreen-mouse-ownership`); never touch main.
|
|
16
|
+
- Tabs for indentation; English comments and commit messages; conventional commit subjects.
|
|
17
|
+
- Pure-function split is a hard spec constraint: `shouldOwnOuterMouseMode` must not read `process.env` or `this.*` (call site resolves both args); must not be re-inlined into the component.
|
|
18
|
+
- `enableMouseScroll` / `disableMouseScroll` must remain the ONLY places that write `MOUSE_ENABLE` / `MOUSE_DISABLE` / `XTSHIFTESCAPE_SELECT` to the outer terminal.
|
|
19
|
+
- Regular-mode behavior must stay byte-identical (ctor disable→enable→refresh, 0/50/250ms refresh timers, connect/resize re-enable, close disable) — only the ownership gate is new.
|
|
20
|
+
- No behavior change for wheel/scroll/selection/middle-paste inside attach views; no changes outside the two choke points.
|
|
21
|
+
- Tests use `node:test` with self-contained fixtures under temp dirs; never the host repository.
|
|
22
|
+
- `test/` and `test-support/` are EXCLUDED from tsc — scenario files are runtime-transformed only; `src/` must stay `npm run typecheck`-clean (pi-tui 1.1.0 typings provide `TUI.mode`).
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
### Task 1: Pure decision helper `shouldOwnOuterMouseMode` (A1)
|
|
27
|
+
|
|
28
|
+
**Files:**
|
|
29
|
+
- Modify: `src/core/pty-scroll.mjs` (add export next to `resolveAttachMouseAction`, near the other mouse helpers)
|
|
30
|
+
- Test: `test/pty-scroll.test.mjs` (extend the existing import block and append tests)
|
|
31
|
+
|
|
32
|
+
**Interfaces:**
|
|
33
|
+
- Consumes: none.
|
|
34
|
+
- Produces: `shouldOwnOuterMouseMode(mode, mouseEnabled) → boolean` where `mode` is `Tui["mode"] | undefined` (string `"regular" | "fullscreen" | undefined`). Task 2's component imports this from `../core/pty-scroll.mjs`.
|
|
35
|
+
|
|
36
|
+
- [ ] **Step 1: Write the failing test**
|
|
37
|
+
|
|
38
|
+
In `test/pty-scroll.test.mjs`, extend the existing import from `../src/core/pty-scroll.mjs` with `shouldOwnOuterMouseMode`, then append:
|
|
39
|
+
|
|
40
|
+
```js
|
|
41
|
+
test("shouldOwnOuterMouseMode: fullscreen never owns; regular/undefined follow the switch", () => {
|
|
42
|
+
assert.equal(shouldOwnOuterMouseMode("fullscreen", true), false);
|
|
43
|
+
assert.equal(shouldOwnOuterMouseMode("fullscreen", false), false);
|
|
44
|
+
assert.equal(shouldOwnOuterMouseMode("regular", true), true);
|
|
45
|
+
assert.equal(shouldOwnOuterMouseMode("regular", false), false);
|
|
46
|
+
// Old pi runtimes have no tui.mode — must behave like regular.
|
|
47
|
+
assert.equal(shouldOwnOuterMouseMode(undefined, true), true);
|
|
48
|
+
assert.equal(shouldOwnOuterMouseMode(undefined, false), false);
|
|
49
|
+
});
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
- [ ] **Step 2: Run test to verify it fails**
|
|
53
|
+
|
|
54
|
+
Run: `node --test test/pty-scroll.test.mjs`
|
|
55
|
+
Expected: FAIL — `shouldOwnOuterMouseMode` is not exported / not a function.
|
|
56
|
+
|
|
57
|
+
- [ ] **Step 3: Implement**
|
|
58
|
+
|
|
59
|
+
In `src/core/pty-scroll.mjs`, next to `resolveAttachMouseAction`:
|
|
60
|
+
|
|
61
|
+
```js
|
|
62
|
+
/**
|
|
63
|
+
* Issue #169: whether the attach surface should manage the OUTER terminal's
|
|
64
|
+
* mouse mode. Only in regular TUI mode — in fullscreen, TuiAltScreen owns
|
|
65
|
+
* mouse reporting (asserted once at startup, never re-asserted), so any write
|
|
66
|
+
* from the attach surface can only downgrade or kill it. Old runtimes without
|
|
67
|
+
* tui.mode behave as regular. Pure: both arguments are resolved at the call
|
|
68
|
+
* site (mode from the TUI, the flag from the env-derived helper).
|
|
69
|
+
*/
|
|
70
|
+
export function shouldOwnOuterMouseMode(mode, mouseEnabled) {
|
|
71
|
+
return mode !== "fullscreen" && !!mouseEnabled;
|
|
72
|
+
}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
- [ ] **Step 4: Run test to verify it passes**
|
|
76
|
+
|
|
77
|
+
Run: `node --test test/pty-scroll.test.mjs`
|
|
78
|
+
Expected: PASS (new + all pre-existing cases).
|
|
79
|
+
|
|
80
|
+
- [ ] **Step 5: Commit**
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
git add src/core/pty-scroll.mjs test/pty-scroll.test.mjs
|
|
84
|
+
git commit -m "feat(attach): pure outer-mouse-mode ownership decision helper"
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
---
|
|
88
|
+
|
|
89
|
+
### Task 2: Component gating + runner-free ownership scenario (A2)
|
|
90
|
+
|
|
91
|
+
**Files:**
|
|
92
|
+
- Modify: `src/ui/pty-attach.ts` (import line ~13; new private method near `mouseScrollEnabled` (~:862); guards in `enableMouseScroll` (~:852) and `disableMouseScroll` (~:882))
|
|
93
|
+
- Create: `test-support/pty-attach-mouse-mode-scenario.ts`
|
|
94
|
+
- Test: `test/pty-attach-mouse-mode.test.mjs`
|
|
95
|
+
|
|
96
|
+
**Interfaces:**
|
|
97
|
+
- Consumes: `shouldOwnOuterMouseMode(mode, mouseEnabled)` from Task 1; `this.tui.mode` (typed `readonly mode: TuiMode` by pi-tui 1.1.0; absent at runtime on old hosts); `this.mouseScrollEnabled()` (existing private).
|
|
98
|
+
- Produces: private `ownsOuterMouseMode(): boolean`; gated `enableMouseScroll`/`disableMouseScroll`.
|
|
99
|
+
|
|
100
|
+
- [ ] **Step 1: Write the failing test + scenario**
|
|
101
|
+
|
|
102
|
+
Create `test/pty-attach-mouse-mode.test.mjs`:
|
|
103
|
+
|
|
104
|
+
```js
|
|
105
|
+
import assert from "node:assert/strict";
|
|
106
|
+
import test from "node:test";
|
|
107
|
+
import { execFileSync } from "node:child_process";
|
|
108
|
+
import { fileURLToPath } from "node:url";
|
|
109
|
+
import { join } from "node:path";
|
|
110
|
+
|
|
111
|
+
const ROOT_DIR = fileURLToPath(new URL("../", import.meta.url));
|
|
112
|
+
const SCENARIO = join(ROOT_DIR, "test-support", "pty-attach-mouse-mode-scenario.ts");
|
|
113
|
+
|
|
114
|
+
// Component-level outer-mouse-mode ownership coverage (issue #169): the REAL
|
|
115
|
+
// PtyAttachComponent against a fake TUI write spy and a nonexistent control
|
|
116
|
+
// socket. Asserts zero mouse-mode writes in fullscreen across ctor + refresh
|
|
117
|
+
// window + close, the preserved regular-mode pairing, zero writes under the
|
|
118
|
+
// kill switch (incl. close — the previously unguarded disable), and old-runtime
|
|
119
|
+
// (mode absent) behaving like regular.
|
|
120
|
+
test("attach mouse-mode ownership: fullscreen writes nothing; regular pairing kept; kill switch silent", () => {
|
|
121
|
+
const out = execFileSync(process.execPath, ["--experimental-transform-types", SCENARIO], {
|
|
122
|
+
encoding: "utf8",
|
|
123
|
+
timeout: 60_000,
|
|
124
|
+
});
|
|
125
|
+
const parsed = JSON.parse(out.trim().split("\n").filter(Boolean).pop());
|
|
126
|
+
assert.equal(parsed.error, null, `scenario error: ${parsed.error}`);
|
|
127
|
+
assert.equal(parsed.fullscreen.writes, 0, "fullscreen: zero mouse-mode/XTSHIFTESCAPE writes across ctor+timers+close");
|
|
128
|
+
assert.equal(parsed.fullscreen.enableAtCtor, false, "fullscreen: no MOUSE_ENABLE at ctor");
|
|
129
|
+
assert.equal(parsed.fullscreen.disableAtClose, false, "fullscreen: no MOUSE_DISABLE at close");
|
|
130
|
+
assert.equal(parsed.regular.enableAtCtor, true, "regular: MOUSE_ENABLE at ctor preserved");
|
|
131
|
+
assert.equal(parsed.regular.disableAtClose, true, "regular: MOUSE_DISABLE at close preserved");
|
|
132
|
+
assert.equal(parsed.off.writes, 0, "AGENT_BOARD_ATTACH_MOUSE=0: zero writes including close");
|
|
133
|
+
assert.equal(parsed.legacy.enableAtCtor, true, "old runtime (no mode): behaves like regular");
|
|
134
|
+
assert.equal(parsed.legacy.disableAtClose, true, "old runtime (no mode): close disables like regular");
|
|
135
|
+
});
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
Create `test-support/pty-attach-mouse-mode-scenario.ts`:
|
|
139
|
+
|
|
140
|
+
```ts
|
|
141
|
+
// Component-level outer-mouse-mode ownership coverage (issue #169): the REAL
|
|
142
|
+
// PtyAttachComponent with a fake TUI whose terminal.write collects bytes, run
|
|
143
|
+
// against a nonexistent control socket (on non-Windows, connect() short-
|
|
144
|
+
// circuits to its reconnect loop before creating a socket — no socket churn).
|
|
145
|
+
// Four cases:
|
|
146
|
+
// 1. mode "fullscreen" → ZERO mouse-mode/XTSHIFTESCAPE writes across
|
|
147
|
+
// ctor + the 0/50/250ms refresh window + close() (TuiAltScreen owns it);
|
|
148
|
+
// 2. mode "regular" → today's pairing preserved (MOUSE_ENABLE at
|
|
149
|
+
// ctor, MOUSE_DISABLE at close);
|
|
150
|
+
// 3. mode "regular" + AGENT_BOARD_ATTACH_MOUSE=0 → zero writes including
|
|
151
|
+
// close() (fixes the previously unguarded disable);
|
|
152
|
+
// 4. mode absent (old pi) → behaves like regular.
|
|
153
|
+
// Run via `node --experimental-transform-types`.
|
|
154
|
+
import { mkdtempSync, rmSync } from "node:fs";
|
|
155
|
+
import { tmpdir } from "node:os";
|
|
156
|
+
import { join } from "node:path";
|
|
157
|
+
import { PtyAttachComponent } from "../src/ui/pty-attach.ts";
|
|
158
|
+
|
|
159
|
+
const sleep = (ms: number) => new Promise<void>((r) => setTimeout(r, ms));
|
|
160
|
+
|
|
161
|
+
const MOUSE_BYTE_RE = /\x1b\[\?(1000|1002|1003|1004|1006)[hl]/;
|
|
162
|
+
const XTSHIFTESCAPE_RE = /\x1b\[>0s/;
|
|
163
|
+
|
|
164
|
+
interface CaseResult {
|
|
165
|
+
writes: number;
|
|
166
|
+
enableAtCtor: boolean;
|
|
167
|
+
disableAtClose: boolean;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
async function runCase(mode: string | undefined, env: Record<string, string> | undefined): Promise<CaseResult> {
|
|
171
|
+
const writes: string[] = [];
|
|
172
|
+
const tui = {
|
|
173
|
+
mode,
|
|
174
|
+
terminal: { rows: 24, cols: 80, columns: 80, write: (s: string) => { writes.push(s); } },
|
|
175
|
+
requestRender: () => {},
|
|
176
|
+
};
|
|
177
|
+
const theme = { fg: (_c: string, t: string) => t, bold: (t: string) => t };
|
|
178
|
+
const root = mkdtempSync(join(tmpdir(), "agentview-mouse-mode-"));
|
|
179
|
+
const socketPath = join(root, "nonexistent.sock");
|
|
180
|
+
const prevEnv: Record<string, string | undefined> = {};
|
|
181
|
+
for (const [k, v] of Object.entries(env ?? {})) {
|
|
182
|
+
prevEnv[k] = process.env[k];
|
|
183
|
+
process.env[k] = v;
|
|
184
|
+
}
|
|
185
|
+
let component: PtyAttachComponent | null = null;
|
|
186
|
+
try {
|
|
187
|
+
component = new PtyAttachComponent(
|
|
188
|
+
tui as never,
|
|
189
|
+
theme as never,
|
|
190
|
+
{} as never,
|
|
191
|
+
() => {},
|
|
192
|
+
{ socketPath, title: "mouse mode e2e" } as never,
|
|
193
|
+
);
|
|
194
|
+
const enableAtCtor = writes.some((s) => s.includes("\x1b[?1000h"));
|
|
195
|
+
await sleep(400); // past the 250ms refresh window
|
|
196
|
+
try { component.dispose(); } catch {}
|
|
197
|
+
await sleep(100);
|
|
198
|
+
const disableAtClose = writes.some((s) => s.includes("\x1b[?1000l"));
|
|
199
|
+
return {
|
|
200
|
+
writes: writes.filter((s) => MOUSE_BYTE_RE.test(s) || XTSHIFTESCAPE_RE.test(s)).length,
|
|
201
|
+
enableAtCtor,
|
|
202
|
+
disableAtClose,
|
|
203
|
+
};
|
|
204
|
+
} finally {
|
|
205
|
+
try { component?.dispose(); } catch {}
|
|
206
|
+
for (const [k, v] of Object.entries(prevEnv)) {
|
|
207
|
+
if (v === undefined) delete process.env[k];
|
|
208
|
+
else process.env[k] = v;
|
|
209
|
+
}
|
|
210
|
+
try { rmSync(root, { recursive: true, force: true, maxRetries: 3, retryDelay: 30 }); } catch {}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const result = {
|
|
215
|
+
error: null as string | null,
|
|
216
|
+
fullscreen: null as CaseResult | null,
|
|
217
|
+
regular: null as CaseResult | null,
|
|
218
|
+
off: null as CaseResult | null,
|
|
219
|
+
legacy: null as CaseResult | null,
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
try {
|
|
223
|
+
result.fullscreen = await runCase("fullscreen", undefined);
|
|
224
|
+
result.regular = await runCase("regular", undefined);
|
|
225
|
+
result.off = await runCase("regular", { AGENT_BOARD_ATTACH_MOUSE: "0" });
|
|
226
|
+
result.legacy = await runCase(undefined, undefined);
|
|
227
|
+
} catch (err) {
|
|
228
|
+
result.error = err instanceof Error ? err.message : String(err);
|
|
229
|
+
}
|
|
230
|
+
console.log(JSON.stringify(result));
|
|
231
|
+
process.exit(0);
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
- [ ] **Step 2: Run the test to verify it fails**
|
|
235
|
+
|
|
236
|
+
Run: `node --test test/pty-attach-mouse-mode.test.mjs`
|
|
237
|
+
Expected: FAIL — `parsed.fullscreen.writes` > 0 (ctor writes `MOUSE_DISABLE`+`XTSHIFTESCAPE`+`MOUSE_ENABLE`, refresh timers re-enable) and/or `parsed.off.writes` > 0 (unguarded disable). The `regular`/`legacy` assertions may already pass — the failure must be on the fullscreen/off fields.
|
|
238
|
+
|
|
239
|
+
- [ ] **Step 3: Implement the gating**
|
|
240
|
+
|
|
241
|
+
In `src/ui/pty-attach.ts`:
|
|
242
|
+
|
|
243
|
+
Import line (~13) — extend the core import (keep alphabetical order):
|
|
244
|
+
|
|
245
|
+
```ts
|
|
246
|
+
import { clampInt, parseMouseInputChunk, resolveAttachMouseAction, resolveWheelLines, scrollViewportTop, selectionDragScrollLines, shouldOwnOuterMouseMode } from "../core/pty-scroll.mjs";
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
Near `mouseScrollEnabled()` (~:862), add the private method:
|
|
250
|
+
|
|
251
|
+
```ts
|
|
252
|
+
/** Issue #169: in fullscreen mode TuiAltScreen owns outer-terminal mouse
|
|
253
|
+
* reporting (asserted once at startup, never re-asserted) — writing any
|
|
254
|
+
* mouse-mode sequence from here can only downgrade or kill it. tui.mode is
|
|
255
|
+
* typed non-optional but old pi runtimes lack it; the strict inequality
|
|
256
|
+
* then keeps today's (regular) behavior. */
|
|
257
|
+
private ownsOuterMouseMode(): boolean {
|
|
258
|
+
return shouldOwnOuterMouseMode(this.tui.mode, this.mouseScrollEnabled());
|
|
259
|
+
}
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
In `enableMouseScroll()` (~:852), replace the guard:
|
|
263
|
+
|
|
264
|
+
```ts
|
|
265
|
+
private enableMouseScroll(): void {
|
|
266
|
+
if (!this.ownsOuterMouseMode()) return;
|
|
267
|
+
try {
|
|
268
|
+
this.tui.terminal.write(XTSHIFTESCAPE_SELECT);
|
|
269
|
+
this.tui.terminal.write(MOUSE_ENABLE);
|
|
270
|
+
} catch {
|
|
271
|
+
/* best-effort: some terminals reject these sequences; mouse reporting is optional */
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
In `disableMouseScroll()` (~:882), add the guard as the first statement:
|
|
277
|
+
|
|
278
|
+
```ts
|
|
279
|
+
private disableMouseScroll(): void {
|
|
280
|
+
if (!this.ownsOuterMouseMode()) return;
|
|
281
|
+
try {
|
|
282
|
+
this.tui.terminal.write(MOUSE_DISABLE);
|
|
283
|
+
} catch {
|
|
284
|
+
/* best-effort: terminal may already be gone at teardown */
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
Do NOT change any other call site (ctor :278-280, connect :487, resize :1203, close :1524 route through these methods unchanged).
|
|
290
|
+
|
|
291
|
+
- [ ] **Step 4: Run the test to verify it passes**
|
|
292
|
+
|
|
293
|
+
Run: `node --test test/pty-attach-mouse-mode.test.mjs`
|
|
294
|
+
Expected: PASS.
|
|
295
|
+
|
|
296
|
+
- [ ] **Step 5: Run the unit tests + typecheck**
|
|
297
|
+
|
|
298
|
+
Run: `node --test test/pty-scroll.test.mjs && npm run typecheck`
|
|
299
|
+
Expected: PASS both.
|
|
300
|
+
|
|
301
|
+
- [ ] **Step 6: Commit**
|
|
302
|
+
|
|
303
|
+
```bash
|
|
304
|
+
git add src/ui/pty-attach.ts test-support/pty-attach-mouse-mode-scenario.ts test/pty-attach-mouse-mode.test.mjs
|
|
305
|
+
git commit -m "fix(attach): stop writing outer-terminal mouse mode in fullscreen (issue #169)"
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
---
|
|
309
|
+
|
|
310
|
+
### Task 3: Full verify + coverage gate (A3, A4)
|
|
311
|
+
|
|
312
|
+
**Files:**
|
|
313
|
+
- Modify: whatever Tasks 1–2 fallout requires (expected: none).
|
|
314
|
+
|
|
315
|
+
**Interfaces:**
|
|
316
|
+
- Consumes: everything from Tasks 1–2.
|
|
317
|
+
- Produces: green `npm run verify` — the PR-ready state.
|
|
318
|
+
|
|
319
|
+
- [ ] **Step 1: Run the full verify pipeline**
|
|
320
|
+
|
|
321
|
+
Run: `npm run verify` (= typecheck + perf gate + tests + coverage + pack:dry)
|
|
322
|
+
Expected: PASS end-to-end. Coverage thresholds: lines 85 / funcs 80 / branches 70 — `shouldOwnOuterMouseMode` is fully unit-covered (Task 1); the component gate lines are exercised by the Task 2 scenario at runtime (c8 does not instrument test-support, same as #171 — no coverage hacks).
|
|
323
|
+
|
|
324
|
+
- [ ] **Step 2: Fix any fallout, then re-run**
|
|
325
|
+
|
|
326
|
+
Run: `npm run verify`
|
|
327
|
+
Expected: PASS. If a pre-existing flaky real-process test blocks (issue #95), re-run that single test once; if still red, STOP and report BLOCKED with the failure output.
|
|
328
|
+
|
|
329
|
+
- [ ] **Step 3: Commit (only if Step 2 needed changes)**
|
|
330
|
+
|
|
331
|
+
```bash
|
|
332
|
+
git add <changed files>
|
|
333
|
+
git commit -m "test: verify fallout fixes for issue #169"
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
---
|
|
337
|
+
|
|
338
|
+
## Acceptance traceability
|
|
339
|
+
|
|
340
|
+
| Spec ID | Covered by |
|
|
341
|
+
| --- | --- |
|
|
342
|
+
| A1 (unit decision table) | Task 1 |
|
|
343
|
+
| A2 (component-level ownership scenario) | Task 2 |
|
|
344
|
+
| A3 (static/build) | Task 3 (+ typecheck in Task 2 Step 5) |
|
|
345
|
+
| A4 (existing suite regression) | Task 3 |
|
|
346
|
+
| U1 (user manual: detach-then-select survives; 5-step checklist) | Post-implementation checklist for the user — executed after merge or from the worktree install |
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
# Spec: 修复 attach 落定的亮暗交付(issue #161)
|
|
2
|
+
|
|
3
|
+
Drafted: zai-coding-cn/glm-5.3 (selected) · zai-coding-cn/glm-5.3 (physical) @ 2026-10-04T23:59:27+08:00
|
|
4
|
+
Revised: zai-coding-cn/glm-5.3 (selected) · zai-coding-cn/glm-5.3 (physical) @ 2026-10-05T23:16:10+08:00
|
|
5
|
+
Revised: zai-coding-cn/glm-5.3 (selected) · zai-coding-cn/glm-5.3 (physical) @ 2026-10-06T22:29:11+08:00
|
|
6
|
+
|
|
7
|
+
> 状态:v3(第 2 轮复核收敛:真缺口 0,细化 R2-D1~D3 已处置)——⏸ 等待用户确认设计,确认后进步 5(worktree)。
|
|
8
|
+
> 依据:issue #161 正文 + 三轮调研(`research/root-cause-probe-api-rename.md`、
|
|
9
|
+
> `research/fix-surface-api-availability.md`、`research/observability-hook-points.md`)。
|
|
10
|
+
|
|
11
|
+
## 1. 根因(已证实)
|
|
12
|
+
|
|
13
|
+
**落定探测调用的 pi-tui 方法在 pi ≥ 0.99 已改名,调用抛 `TypeError` 被 `catch {}` 吞掉,
|
|
14
|
+
probe 从未执行——settle 时刻的亮暗交付在 pi ≥ 0.99 上恒为 no-op。**
|
|
15
|
+
|
|
16
|
+
证据链:
|
|
17
|
+
|
|
18
|
+
1. `src/ui/pty-attach.ts:664`(origin/main,#165 后)调用
|
|
19
|
+
`this.tui.queryTerminalBackgroundColor({ timeoutMs })`;665-667 行 `catch {}` 静默。
|
|
20
|
+
2. pi-tui 版本考古(npm tarball `tui.d.ts` 核对):0.79.8 / 0.87.1 仅有旧名;
|
|
21
|
+
0.99.0 / 0.99.2 / 1.0.0 / 1.0.2 仅有 `queryTerminalColors`。改名点 = 0.87.1 → 0.99.0。
|
|
22
|
+
3. `pi-coding-agent@0.99.2` 依赖 pi-tui ^0.99.2 → 事发环境(2026-10-01,pi 0.99.2)已改名,
|
|
23
|
+
probe 当时即 no-op;当前本机 pi 1.0.2 亦然。
|
|
24
|
+
4. CI 绿的原因:repo devDeps 钉 0.79.8(旧名在,typecheck 过),测试 fake 只带旧名;
|
|
25
|
+
运行时扩展副本 node_modules/@earendil-works 为空,peer 依赖由宿主 pi 解析(仅新名)
|
|
26
|
+
→ 开发面与运行面漂移。
|
|
27
|
+
5. 排除备选:settle 未触发 / 发送链路断——#165 修的回声残渣证明 settle 钩子在跑;
|
|
28
|
+
「应答被僵尸条目吃掉」是旧实现的失败模式,且根本走不到那一步(调用即抛)。
|
|
29
|
+
6. (复核补强)扩展组件拿到的 tui 是 `createInteractiveTuiReference` 返回的透明 Proxy
|
|
30
|
+
(pi 1.0.2 tui-renderer.js:37-66,get/set/has 全量转发底层 TuiBase)→ 旧名经
|
|
31
|
+
`Reflect.get` 得 undefined、调用即 TypeError(与根因自洽);新名 `queryTerminalColors`
|
|
32
|
+
真实存在且可调(TuiBase 实现,tui.js:1056)——修复假设实现级成立。
|
|
33
|
+
|
|
34
|
+
**#165 合并后的现状**:settle 交付只剩 probe 一条路(D3 已删;D2 桥只转发宿主消费到的
|
|
35
|
+
scheme 事件,救不了落定即错的子 pi)→ 任何「子 pi spawn 时拿不到真实亮暗」的 attach 必现
|
|
36
|
+
恒 dark,直到外部 997 推送。比 issue 提交时更严重(当时 D3 至少还会发裸查询)。
|
|
37
|
+
|
|
38
|
+
## 2. 修复设计
|
|
39
|
+
|
|
40
|
+
### 2.1 决策表
|
|
41
|
+
|
|
42
|
+
| 决策点 | 选择 | 理由 | 被拒备选 |
|
|
43
|
+
|---|---|---|---|
|
|
44
|
+
| 探测 API | feature-detect:优先 `queryTerminalColors`(新),回退 `queryTerminalBackgroundColor`(旧),皆无则不发探测 | peerDependencies 为 `*`,须同时覆盖新旧宿主;#165 后 settle 只剩这条路 | 只用新名(砍掉 ≤0.87 宿主);维持旧名(= 现状,坏的) |
|
|
45
|
+
| 迟到应答 | 新 API 传 `onLateReply`;到达时若组件未 closed **且 scheme 不同于已发送值**才补发一次 997;相同则跳过 | 上游已把迟到应答导向回调(tui.js:1064-1068);997 幂等但重复发送是诊断噪音、违背 #148「一次探测」克制精神 | 忽略迟到(慢链路首拍仍 dark);无去重(每次 attach 可能发两条相同 997) |
|
|
46
|
+
| 「读已解析状态」 | 不做 | 扩展面 TUI 接口无同步 getter(1.0.2 tui.d.ts 无 getTerminalTheme 暴露);宿主进程直连真实终端、队列干净、WezTerm 毫秒级答 OSC 11,一次查询等价可靠 | issue 方向 2 原案(无 API 可用) |
|
|
47
|
+
| scheme 事件缓存重发 | 本期不做 | scheme 事件只在外部推送时触发;board 重启场景宿主是新进程、缓存必空,边际价值低 | 记为后续可选加固(开放问题 2) |
|
|
48
|
+
| 可观测性 | 组件 `onDiagnostic` 发 **DiagnosticEvent 兼容 patch**(code/source/level/message/details),两处构造点 wiring 直传 `appendDiagnostic(root, viewId, patch)` | `normalizeDiagnostic` 只保留 level/code/runId/source/message/details 白名单,其余字段会被丢弃——事件必须在源头就按既有 schema 构造 | 组件直写文件(层穿透、难单测);自定义字段名(会被白名单丢弃,R1-G1) |
|
|
49
|
+
| devDeps 0.79.8 → 1.0.x | 本期不做 | 全量类型面迁移,独立 PR;本期用 feature-detect + 本地结构类型 + 局部 cast | 混入会放大回归面 |
|
|
50
|
+
| kill switch | 保留 `AGENT_BOARD_FORWARD_TERMINAL_QUERIES=0` | 既有回退语义(probe + D2 桥同关) | — |
|
|
51
|
+
|
|
52
|
+
### 2.2 数据流(settle 时刻)
|
|
53
|
+
|
|
54
|
+
```
|
|
55
|
+
finishAttachTransition(现有,不变)
|
|
56
|
+
└─ probeAndReportRealTerminalColorScheme(重写编排)
|
|
57
|
+
├─ resolveProbeApi(this.tui) [新纯函数,terminal-query-sequences.mjs]
|
|
58
|
+
│ api=colors → tui.queryTerminalColors({timeoutMs, onLateReply})
|
|
59
|
+
│ → colors.background → RgbColor
|
|
60
|
+
│ api=background→ tui.queryTerminalBackgroundColor({timeoutMs}) → RgbColor
|
|
61
|
+
│ api=none → 仅诊断事件,不发探测
|
|
62
|
+
├─ colorSchemeForBackgroundRgb(rgb) [现有纯函数,不动]
|
|
63
|
+
├─ toColorSchemeReport(scheme) [现有纯函数,不动]
|
|
64
|
+
├─ this.send({type:"input", data:997…}) [现有通道,不动]
|
|
65
|
+
└─ onDiagnostic(DiagnosticEvent 兼容 patch) [新回调,缺省 no-op]
|
|
66
|
+
onLateReply(colors) →
|
|
67
|
+
closed → 不发送,记 outcome=dropped_closed
|
|
68
|
+
scheme 与已发送值相同 → 不发送,记 outcome=duplicate_skipped
|
|
69
|
+
否则 → 补发一次 997,记 outcome=reported + late:true
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### 2.3 组件契约与改动面
|
|
73
|
+
|
|
74
|
+
- `src/core/terminal-query-sequences.mjs`(落点钉死,与 `colorSchemeForBackgroundRgb` 同域):
|
|
75
|
+
- `resolveProbeApi(tuiLike) → {api:"colors"|"background"|"none", invoke(tuiLike, timeoutMs, onLateReply?) → Promise<RgbColor|undefined>}`
|
|
76
|
+
—— 纯检测 + 适配器,输入输出 plain object,零副作用。
|
|
77
|
+
- `backgroundRgbFromTerminalColors(colors) → RgbColor|undefined`——纯映射。
|
|
78
|
+
- **编译期类型约束**:本仓编译面是 pi-tui 0.79.8,没有 `TerminalColors` 类型也没有
|
|
79
|
+
`queryTerminalColors` 方法 → 上述两个函数使用**本地定义的最小结构类型**
|
|
80
|
+
(如 `interface TerminalColorsLike { background?: RgbColor }`),**禁止
|
|
81
|
+
`import type { TerminalColors }` 自 pi-tui**;`typeof` 检测与必要的 cast 封装在
|
|
82
|
+
`invoke` 内部,不外溢到编排层。
|
|
83
|
+
- `src/ui/pty-attach.ts`:
|
|
84
|
+
- `PtyAttachOptions`(pty-attach.ts:24,实名)增加可选
|
|
85
|
+
`onDiagnostic?: (event: AttachSettleDiagnosticPatch) => void`(缺省 no-op)。
|
|
86
|
+
- `probeAndReportRealTerminalColorScheme` 重写为上述编排;每步失败写诊断后静默返回
|
|
87
|
+
(保持 #148 的 best-effort 语义,但可观测)。
|
|
88
|
+
- **两处构造点都要注入**(当前为同构重复的两个 `openPtyAttach`,是否去重由 plan 决定,
|
|
89
|
+
去重本身非目标):
|
|
90
|
+
- `src/commands/attach-flow.ts:33-37`(export 的 `openPtyAttach`,供 `attach()` /
|
|
91
|
+
键盘回退路径);
|
|
92
|
+
- `src/commands/agent-board.ts:226-251`(私有 `openPtyAttach`,供 dashboard attach
|
|
93
|
+
动作,构造在 238-240 行)。
|
|
94
|
+
两处工厂闭包均持有 `root` 与 `viewId`,把 `onDiagnostic` 接到
|
|
95
|
+
`appendDiagnostic(root, viewId, patch)`(与现有 `screenLogPath(root, viewId)` 同源)。
|
|
96
|
+
- **诊断事件契约(映射到既有 `DiagnosticEvent` schema,字段名必须落在
|
|
97
|
+
`normalizeDiagnostic` 白名单内,否则会被静默丢弃)**:
|
|
98
|
+
- `code: "attach_settle_scheme"`(snake_case,对齐 `host_crashed` /
|
|
99
|
+
`host_crash_owner_changed` 等现有 code 命名)
|
|
100
|
+
- `source: "attach"`
|
|
101
|
+
- `level`: **「预期交付但失败」才 `"warn"`**——`timeout` / `error` / `no_background` /
|
|
102
|
+
`no_probe_api` / `dropped_disconnected`;正常/预期操作用 `"info"`——`reported` / `duplicate_skipped` /
|
|
103
|
+
`dropped_closed` / `suppressed`(后三者是慢链路正常去重、用户快速 detach、用户主动
|
|
104
|
+
kill switch;标 warn 会污染 `summarizeDiagnostics` 的 warningCount——diagnostics.mjs:60
|
|
105
|
+
计数、store.mjs:425 并入行摘要被 dashboard 消费)
|
|
106
|
+
- `message`: 人类可读摘要(如 `attach settle scheme reported via queryTerminalColors (light)`)
|
|
107
|
+
- `details: { probeApi: "colors"|"background"|"none", outcome:
|
|
108
|
+
"reported"|"timeout"|"error"|"no_background"|"no_probe_api"|"dropped_closed"|
|
|
109
|
+
"dropped_disconnected"|"duplicate_skipped"|"suppressed", report?: "997;1"|"997;2", late?: true }`
|
|
110
|
+
(camelCase,对齐既有 details 键风格如 `ownerChanged`;`report` 仅在发送 997 时携带;
|
|
111
|
+
kill switch 的 suppressed 事件不填 `probeApi`)
|
|
112
|
+
- 组件的 `onDiagnostic` 即以上述 patch 形状发出,wiring 层直接透传给
|
|
113
|
+
`appendDiagnostic(root, viewId, patch)`,不做二次映射。
|
|
114
|
+
- **outcome 判定规则(实现与 A9 的对齐依据)**:
|
|
115
|
+
(a) `timeout` 与 `no_background` 的区分——api=colors 时,返回对象**任一字段**
|
|
116
|
+
(foreground / background / palette 任一项)有值但 `background` 缺失 →
|
|
117
|
+
`no_background`(终端应答了但没答 OSC 11);返回对象**完全为空** → `timeout`
|
|
118
|
+
(pi-tui 1.0.2 tui.d.ts:376-378:未应答的颜色为 undefined,两种情形在返回值层面
|
|
119
|
+
仅靠「是否有任何字段有值」区分);api=background 时 `undefined` → `timeout`
|
|
120
|
+
(旧 API 只查 OSC 11,无法细分)。
|
|
121
|
+
(b) 去重基准 `sentScheme` 初始为 `null`;`null` 不命中 duplicate 判定——保证
|
|
122
|
+
「超时 → 迟到应答补发」这条主路径不被去重吞掉。
|
|
123
|
+
|
|
124
|
+
### 2.4 降级路径
|
|
125
|
+
|
|
126
|
+
- probe 超时 / 异常 / 无 background → 诊断 `outcome=timeout|error|no_background`
|
|
127
|
+
(level `"warn"`;判定规则见 §2.3 outcome 判定规则),不发 997。
|
|
128
|
+
- 无任何 probe API → 诊断 `details.probeApi="none"`、`outcome=no_probe_api`,不发探测。
|
|
129
|
+
- `onLateReply` 到达且组件已 closed → 不发送,诊断 `outcome=dropped_closed`(level `"info"`)。
|
|
130
|
+
- `onLateReply` 的 scheme 与已发送值(`sentScheme`,初始 `null`)相同 → 不发送,诊断
|
|
131
|
+
`outcome=duplicate_skipped`(level `"info"`);不同(含尚未发送过任何值的 `null` 情形,
|
|
132
|
+
即「超时 → 迟到补发」主路径)→ 补发一次 997,诊断 `outcome=reported`、`late:true`。
|
|
133
|
+
- kill switch(`AGENT_BOARD_FORWARD_TERMINAL_QUERIES=0`)→ 全链路跳过,诊断
|
|
134
|
+
`outcome=suppressed`(level `"info"`,不填 `probeApi`),行为回到修复前。
|
|
135
|
+
- 控制.socket 未连接时 settle 交付命中 send() 静默 no-op(硬超时落定、宿主连接慢的角落)→
|
|
136
|
+
不没 sentScheme、不发 997,诊断 `outcome=dropped_disconnected`(level `"warn"`)——同 scheme
|
|
137
|
+
的迟到应答之后重试而非被去重吞掉。(CR round-1 advisory 采纳)
|
|
138
|
+
- 以上降级均不抛错、不影响 attach 主流程。
|
|
139
|
+
|
|
140
|
+
### 2.5 非目标
|
|
141
|
+
|
|
142
|
+
- pi-tui 上游 `pendingTerminalColorQueries` 超时条目累积的队列卫生(上游问题)。
|
|
143
|
+
- WezTerm mode 2031 支持(上游 wezterm/wezterm#6454)。
|
|
144
|
+
- 子 pi 侧 OSC 11 僵尸条目泄漏(上游)。
|
|
145
|
+
- devDependencies 升级到 pi 1.0.x(独立 issue/PR,见开放问题 1)。
|
|
146
|
+
- 复活 #165 删除的 D3 raw replay。
|
|
147
|
+
- 两个 `openPtyAttach` 同构重复的去重(由 plan 自行决定,不作为本期验收项)。
|
|
148
|
+
|
|
149
|
+
## 3. 验收矩阵
|
|
150
|
+
|
|
151
|
+
| ID | 功能点 | 验收方式 | 具体验证 | 通过标准 |
|
|
152
|
+
|----|--------|----------|----------|----------|
|
|
153
|
+
| A1 | 新 API 探测与 997 交付 | 自动化验证(unit) | `node --test test/pty-attach-settle-scheme.test.mjs`:fake tui 仅带 `queryTerminalColors`,light/dark 两个 background 值 | probe 被调用;向 child 各发一次 `997;2` / `997;1` |
|
|
154
|
+
| A2 | 旧 API 回退不回归 | 自动化验证(unit) | 同文件:fake tui 仅带 `queryTerminalBackgroundColor` | 旧路径行为与 #149 语义一致(997 照发) |
|
|
155
|
+
| A3 | 双 API 皆缺 | 自动化验证(unit) | 同文件:fake tui 两者皆无 | 不抛错、不发探测;`onDiagnostic` 收到 `details.probeApi:"none"`、`outcome:"no_probe_api"` |
|
|
156
|
+
| A4 | 颜色→亮暗映射 | 自动化验证(unit) | `colorSchemeForBackgroundRgb` 现有边界用例保持 + `backgroundRgbFromTerminalColors` 新用例 | 亮度阈值与 pi 对齐;`background` 缺失返回 `undefined` |
|
|
157
|
+
| A5 | 迟到应答补发与去重 | 自动化验证(unit) | fake:resolve 后再触发 `onLateReply`(scheme 相同 / 不同两组)+ closed 场景 | scheme 不同补发一次 997 且 `late:true`;相同跳过且 `outcome:"duplicate_skipped"`;closed 后不发送且 `outcome:"dropped_closed"` |
|
|
158
|
+
| A6 | 诊断事件流(schema 兼容) | 自动化验证(unit) | fake `onDiagnostic` 捕获各阶段事件,并**将 patch 过一遍 `normalizeDiagnostic(viewId, patch)` 直证字段不丢** | 事件 `code:"attach_settle_scheme"`、`source:"attach"`、`details.{probeApi,outcome,report?}` 全部在 normalize 后存活;kill switch 下 `details.outcome:"suppressed"` |
|
|
159
|
+
| A7 | 真实 PTY 端到端 | 自动化验证(integration) | 扩展 `test-support/detach-gate-smoke.ts`:新增「tui 仅新 API」场景,真实 node-pty 子进程 | 子 PTY 输入流收到正确 997 恰一次 |
|
|
160
|
+
| A8 | 既有面无回归 | 自动化验证(static + build + integration) | `npm run verify`(= typecheck + perf gate + tests + coverage 阈值 lines 85 / funcs 80 / branches 70 + pack:dry,见 AGENTS.md:20-24) | 全绿;c8 分支阈值由 A1/A2/A3 三向覆盖满足;#165 的 `attach-startup-color-smoke` 不受影响 |
|
|
161
|
+
| A9 | 失败分支不发 997 | 自动化验证(unit) | A1 同文件:fake 分别返回 timeout(resolve 完全为空对象 `{}`,不触发 late)/ throw / 有应答但无 `background`(resolve 含 foreground 的对象) | 三种降级下 sent 均为空;`details.outcome` 分别为 `timeout` / `error` / `no_background`(判定规则见 §2.3) |
|
|
162
|
+
| U1 | 真机 board 重启场景(两条入口路径各验一次) | 用户实测 | 前置(必须):① 快进运行时副本 `git -C ~/.pi/agent/git/github.com/zhuxixi/pi-agent-board pull --ff-only`(至含本修复的 main);② **重启宿主 pi**(扩展随宿主启动加载,不重启跑的是旧代码)。然后:浅色 WezTerm 下宿主 pi 起 board → board 级重启 → **两次 attach 各走一条入口**:① dashboard 选中 view 回车 attach(验 agent-board.ts 构造点);② detach 后用 ← 键盘路径或 bg 命令再 attach(验 attach-flow.ts 构造点);观察子 pi 首帧配色 + 两次各自 view 的 `diagnostics.jsonl` | 两次首帧均 light-warm 系(userMsg ≈ `#f0f0f0` 而非 `#343541`);两次的 diagnostics.jsonl 均出现 `code:"attach_settle_scheme"`、`details.probeApi:"colors"`、`details.report:"997;2"` |
|
|
163
|
+
| U2 | kill switch 回退 | 用户实测 | 同 U1 前置两步 + `AGENT_BOARD_FORWARD_TERMINAL_QUERIES=0` 起宿主,重复 U1 场景 | 无探测(diagnostics `details.outcome:"suppressed"`)、无报错,行为回到修复前(dark) |
|
|
164
|
+
|
|
165
|
+
分层说明:A1-A6/A9 用 unit(行为可由注入 fake 完全证明,不跨进程);A7 必须 fake tui + 真实 PTY
|
|
166
|
+
跨组件,用 integration;A8 是类型/构建/既有集成回归(`npm run verify` 是仓约定的 done 门)。
|
|
167
|
+
U1/U2 依赖真实终端 + 真实宿主 pi 进程与 board 运行时,无法在自动化环境稳定复现(需要真实
|
|
168
|
+
OSC 11 应答方与 board 级重启),故为用户实测;可执行时机 = 修复合入 main 后按前置两步更新
|
|
169
|
+
运行时副本并重启宿主 pi。
|
|
170
|
+
|
|
171
|
+
## 4. 可测性拆分设计(硬约束)
|
|
172
|
+
|
|
173
|
+
- `resolveProbeApi`:纯检测器,落点 `src/core/terminal-query-sequences.mjs`。三种 tui
|
|
174
|
+
形状(仅新 / 仅旧 / 皆无)直测 → 形成 A1/A2/A3 的测试边界:**API 形状判定不依赖真实
|
|
175
|
+
pi-tui**;参数与返回使用本地结构类型(`TerminalColorsLike` 等),禁止 import pi-tui 的
|
|
176
|
+
`TerminalColors`。
|
|
177
|
+
- `backgroundRgbFromTerminalColors`:纯映射函数,独立于探测与发送 → A4 边界。
|
|
178
|
+
- `probeAndReportRealTerminalColorScheme`:编排层,副作用序列(query→map→send→diagnose)。
|
|
179
|
+
unit 测试以注入 fake(tui、send 捕获、onDiagnostic 捕获)断言 → A1/A3/A5/A6/A9 边界:
|
|
180
|
+
**编排测试不碰真 PTY / 真 socket**。
|
|
181
|
+
- 诊断 wiring(两处 openPtyAttach → appendDiagnostic):A6 用 `normalizeDiagnostic` 直证
|
|
182
|
+
组件发出的 patch schema 兼容(字段在白名单内存活);`appendDiagnostic` 本身已有 core
|
|
183
|
+
层测试,不在本期改动范围。
|
|
184
|
+
- A7 的真实 PTY 边界:复用 detach-gate-smoke harness(fake tui + 真 node-pty 子进程),
|
|
185
|
+
不新造 harness。
|
|
186
|
+
- 实现不得把 resolveProbeApi / backgroundRgbFromTerminalColors 的逻辑内联回编排函数
|
|
187
|
+
(保持可测边界);不得在编排层引入对 pi-tui 版本的硬编码判断。
|
|
188
|
+
|
|
189
|
+
## 5. 开放问题
|
|
190
|
+
|
|
191
|
+
1. devDeps 0.79.8 → 1.0.x 迁移:单独开 issue(类型面变化大:TUI 接口、扩展 API 均有改动;
|
|
192
|
+
迁移后本 spec 的本地结构类型与 cast 可去除)。是否现在开,等用户确认。
|
|
193
|
+
2. 「宿主已知 scheme 缓存 + settle 重发」(issue 方向 3 保险丝):本期不做;若 U1 实测发现
|
|
194
|
+
WezTerm 下探测超时率不为零,再评估。
|
|
195
|
+
3. `pendingTerminalColorQueries` 超时条目是否出队(上游细节):不影响本期决策(每 attach
|
|
196
|
+
一次 + onLateReply 的用量与 pi 自身启动探测同量级);如后续观察到吞应答再上游报 bug。
|