@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,751 @@
|
|
|
1
|
+
# Attach Settle Scheme Delivery 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:** Make attach settle deliver the real terminal's light/dark scheme to the child Pi on every host pi version (issue #161), with full observability.
|
|
6
|
+
|
|
7
|
+
**Architecture:** Feature-detect the terminal color query API on the attach TUI (`queryTerminalColors` on pi-tui ≥0.99, `queryTerminalBackgroundColor` on ≤0.87), probe once at settle, map the background RGB to a scheme, deliver it as an unconditional 997 report on the child PTY, and emit DiagnosticEvent-compatible patches through a new `onDiagnostic` option wired into both `openPtyAttach` construction sites. Late replies (pi-tui `onLateReply`) deliver only when the scheme differs from what was already sent.
|
|
8
|
+
|
|
9
|
+
**Tech Stack:** TypeScript (`src/ui/pty-attach.ts`, `src/commands/*.ts`), JSDoc-typed ESM (`src/core/terminal-query-sequences.mjs`, `src/core/diagnostics.mjs`), `node:test` + driver-spawned TS smoke scripts (`--experimental-transform-types`), real `node-pty` for the integration scenario.
|
|
10
|
+
|
|
11
|
+
**Spec:** `docs/superpowers/specs/2026-10-06-attach-settle-scheme-delivery-design.md` (v3, sha256 `4488581928e75f20a2ecfd4750a4949480e95a9b51f73520f7b76221a552a534`). The plan argues from the spec; executors read both.
|
|
12
|
+
|
|
13
|
+
**Plan-level refinement of one internal signature (spec §2.3):** `resolveProbeApi().invoke` returns `Promise<{ rgb?: RgbColor, anyColors: boolean }>` instead of bare `RgbColor|undefined` — the spec's own outcome rule (§2.3, timeout vs no_background distinguished by "any color field answered") requires the distinction to cross this boundary. Observable contracts (A1–A9) are unchanged.
|
|
14
|
+
|
|
15
|
+
## Acceptance-ID ↔ Task Map (bidirectional)
|
|
16
|
+
|
|
17
|
+
| Acceptance ID | Task(s) | | Task | Acceptance ID(s) |
|
|
18
|
+
|---|---|---|---|---|
|
|
19
|
+
| A1 | 2 | | 1 | A4, parts of A6 (patch builder), feeds A1–A3 |
|
|
20
|
+
| A2 | 2 | | 2 | A1, A2, A3, A9, A6 |
|
|
21
|
+
| A3 | 2 | | 3 | A5 |
|
|
22
|
+
| A4 | 1 | | 4 | wiring for U1/U2; A8 (typecheck) |
|
|
23
|
+
| A5 | 3 | | 5 | A7 |
|
|
24
|
+
| A6 | 1 (builder), 2 (emission) | | 6 | A8, U1/U2 manual checklist |
|
|
25
|
+
| A7 | 5 | | | |
|
|
26
|
+
| A8 | 6 (gate), 2–5 (targeted runs) | | | |
|
|
27
|
+
| A9 | 2 | | | |
|
|
28
|
+
| U1, U2 | 6 (post-implementation manual verification) | | | |
|
|
29
|
+
|
|
30
|
+
## Global Constraints
|
|
31
|
+
|
|
32
|
+
- Tabs for indentation; comments and commit messages in English.
|
|
33
|
+
- Tests use `node:test` + `assert/strict`; component tests are driver `.test.mjs` files spawning a `test-support/*.ts` script with `node --experimental-transform-types` (existing pattern: `test/pty-attach-startup-color.test.mjs`).
|
|
34
|
+
- **Compile face is pi-tui 0.79.8**: never `import type { TerminalColors }` or reference `queryTerminalColors` on the typed `TUI` interface; use local structural typedefs in the `.mjs` and `as never` casts at the pty-attach boundary.
|
|
35
|
+
- Diagnostic patches must land inside `normalizeDiagnostic`'s whitelist (`source/level/code/runId/message/details`); `code:"attach_settle_scheme"`; `level` warn only for `timeout/error/no_background/no_probe_api`; suppressed events omit `probeApi`.
|
|
36
|
+
- Do NOT inline `resolveProbeApi`, `backgroundRgbFromTerminalColors`, or `buildSettleSchemePatch` logic into the orchestration (spec §4 hard constraint).
|
|
37
|
+
- Kill switch `AGENT_BOARD_FORWARD_TERMINAL_QUERIES=0` skips probe AND bridge, emits one `suppressed` (info) event.
|
|
38
|
+
- `sentScheme` starts `null`; `null` never matches the duplicate check (the timeout→late-delivery path must deliver).
|
|
39
|
+
- One probe per attach (settle is single-shot) — never add retries.
|
|
40
|
+
- No `git push` / PR creation without explicit user permission (flow step 9 gate).
|
|
41
|
+
- Done gate is `npm run verify` (typecheck + perf gate + tests + coverage lines 85 / funcs 80 / branches 70 + pack:dry).
|
|
42
|
+
|
|
43
|
+
## Review Focus
|
|
44
|
+
|
|
45
|
+
1. **Kill switch set at startup** → no probe, no 997, exactly one info `suppressed` event — pinned in Task 2 Step 1 (killSwitch scenario).
|
|
46
|
+
2. **Terminal answers nothing, then answers late** → timeout event first, late reply still delivers (sentScheme null) — pinned in Task 3 Step 1 (lateAfterTimeout scenario).
|
|
47
|
+
3. **Old-pi host (only the old API)** keeps #149 behavior (997 still sent) — pinned in Task 2 Step 1 (oldApi scenario).
|
|
48
|
+
4. **TUI with neither API** must not fall back to an unowned raw query (#165 echo regression) — pinned in Task 2 Step 1 (noApi scenario asserts `writes` empty) and Task 6 (existing `attach-startup-color-smoke` stays green).
|
|
49
|
+
5. **Late reply racing detach (component closed)** → dropped, recorded as `dropped_closed`, nothing written to the dead transport — pinned in Task 3 Step 1 (lateAfterClose scenario).
|
|
50
|
+
6. **Routine outcomes must not pollute warningCount** — pinned in Task 1 Step 1 (builder level mapping) and Task 2 Step 1 (scenario level assertions).
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
### Task 1: Core pure helpers (probe API detection, color mapping, diagnostic patch builder)
|
|
55
|
+
|
|
56
|
+
**Files:**
|
|
57
|
+
- Modify: `src/core/terminal-query-sequences.mjs` (append after `colorSchemeForBackgroundRgb`)
|
|
58
|
+
- Test: `test/terminal-query-sequences.test.mjs` (append)
|
|
59
|
+
|
|
60
|
+
**Interfaces:**
|
|
61
|
+
- Consumes: nothing new (existing `RgbColor`-shaped `{r,g,b}` values).
|
|
62
|
+
- Produces (JSDoc-typed, all plain-data, zero side effects — the `invoke` lambdas only call the passed-in surface):
|
|
63
|
+
- `resolveProbeApi(tuiLike) → { api: "colors"|"background"|"none", invoke(tui, timeoutMs, onLateReply?) → Promise<{rgb?: {r,g,b}, anyColors: boolean}> }`
|
|
64
|
+
- `backgroundRgbFromTerminalColors(colors) → {r,g,b}|undefined`
|
|
65
|
+
- `buildSettleSchemePatch({probeApi?, outcome, report?, late?}) → { source:"attach", level:"info"|"warn", code:"attach_settle_scheme", message:string, details:{outcome, probeApi?, report?, late?} }`
|
|
66
|
+
|
|
67
|
+
- [ ] **Step 1: Write the failing tests**
|
|
68
|
+
|
|
69
|
+
Append to `test/terminal-query-sequences.test.mjs` (import the three new exports plus `normalizeDiagnostic` from `../src/core/diagnostics.mjs` at the top of the appended block; reuse this file's existing light/dark RGB constants if present, else `{ r: 0.94, g: 0.94, b: 0.94 }` for light and `{ r: 0.16, g: 0.16, b: 0.21 }` for dark — both sit clearly either side of pi's `>= 0.5` luminance threshold):
|
|
70
|
+
|
|
71
|
+
```js
|
|
72
|
+
import { normalizeDiagnostic } from "../src/core/diagnostics.mjs";
|
|
73
|
+
import {
|
|
74
|
+
backgroundRgbFromTerminalColors,
|
|
75
|
+
buildSettleSchemePatch,
|
|
76
|
+
resolveProbeApi,
|
|
77
|
+
} from "../src/core/terminal-query-sequences.mjs";
|
|
78
|
+
|
|
79
|
+
test("resolveProbeApi detects colors-only / background-only / neither surfaces", async () => {
|
|
80
|
+
const light = { r: 0.94, g: 0.94, b: 0.94 };
|
|
81
|
+
const colorsOnly = { queryTerminalColors: async () => ({ background: light }) };
|
|
82
|
+
const backgroundOnly = { queryTerminalBackgroundColor: async () => light };
|
|
83
|
+
const neither = {};
|
|
84
|
+
|
|
85
|
+
const a = resolveProbeApi(colorsOnly);
|
|
86
|
+
assert.equal(a.api, "colors");
|
|
87
|
+
assert.deepEqual(await a.invoke(colorsOnly, 500), { rgb: light, anyColors: true });
|
|
88
|
+
|
|
89
|
+
const b = resolveProbeApi(backgroundOnly);
|
|
90
|
+
assert.equal(b.api, "background");
|
|
91
|
+
assert.deepEqual(await b.invoke(backgroundOnly, 500), { rgb: light, anyColors: true });
|
|
92
|
+
|
|
93
|
+
const c = resolveProbeApi(neither);
|
|
94
|
+
assert.equal(c.api, "none");
|
|
95
|
+
assert.deepEqual(await c.invoke(neither, 500), { rgb: undefined, anyColors: false });
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test("resolveProbeApi colors adapter distinguishes empty from answered-but-no-background", async () => {
|
|
99
|
+
const empty = { queryTerminalColors: async () => ({}) };
|
|
100
|
+
const answered = { queryTerminalColors: async () => ({ foreground: { r: 0.1, g: 0.1, b: 0.1 } }) };
|
|
101
|
+
const probe = resolveProbeApi(empty);
|
|
102
|
+
assert.deepEqual(await probe.invoke(empty, 500), { rgb: undefined, anyColors: false });
|
|
103
|
+
const probe2 = resolveProbeApi(answered);
|
|
104
|
+
assert.deepEqual(await probe2.invoke(answered, 500), { rgb: undefined, anyColors: true });
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test("resolveProbeApi forwards onLateReply through the colors adapter", async () => {
|
|
108
|
+
let captured;
|
|
109
|
+
const tui = { queryTerminalColors: async (opts) => { captured = opts; return {}; } };
|
|
110
|
+
const probe = resolveProbeApi(tui);
|
|
111
|
+
const onLate = () => {};
|
|
112
|
+
await probe.invoke(tui, 500, onLate);
|
|
113
|
+
assert.equal(captured.onLateReply, onLate);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test("backgroundRgbFromTerminalColors maps defensively", () => {
|
|
117
|
+
assert.equal(backgroundRgbFromTerminalColors(undefined), undefined);
|
|
118
|
+
assert.equal(backgroundRgbFromTerminalColors({}), undefined);
|
|
119
|
+
assert.deepEqual(backgroundRgbFromTerminalColors({ background: { r: 1, g: 2, b: 3 } }), { r: 1, g: 2, b: 3 });
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
test("buildSettleSchemePatch maps levels and survives normalizeDiagnostic", () => {
|
|
123
|
+
const failure = buildSettleSchemePatch({ probeApi: "colors", outcome: "timeout" });
|
|
124
|
+
assert.equal(failure.level, "warn");
|
|
125
|
+
const routine = buildSettleSchemePatch({ probeApi: "colors", outcome: "duplicate_skipped" });
|
|
126
|
+
assert.equal(routine.level, "info");
|
|
127
|
+
const reported = buildSettleSchemePatch({ probeApi: "colors", outcome: "reported", report: "997;2", late: true });
|
|
128
|
+
assert.equal(reported.level, "info");
|
|
129
|
+
const suppressed = buildSettleSchemePatch({ outcome: "suppressed" });
|
|
130
|
+
assert.equal(suppressed.details.probeApi, undefined);
|
|
131
|
+
|
|
132
|
+
for (const patch of [failure, routine, reported, suppressed]) {
|
|
133
|
+
const normalized = normalizeDiagnostic("view-test", patch);
|
|
134
|
+
assert.equal(normalized.code, "attach_settle_scheme");
|
|
135
|
+
assert.equal(normalized.source, "attach");
|
|
136
|
+
assert.equal(normalized.details.outcome, patch.details.outcome);
|
|
137
|
+
assert.deepEqual(normalized.details, patch.details);
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
- [ ] **Step 2: Run tests to verify they fail**
|
|
143
|
+
|
|
144
|
+
Run: `node --test test/terminal-query-sequences.test.mjs`
|
|
145
|
+
Expected: FAIL — the three exports do not exist (SyntaxError / undefined is not a function).
|
|
146
|
+
|
|
147
|
+
- [ ] **Step 3: Implement the helpers**
|
|
148
|
+
|
|
149
|
+
Append to `src/core/terminal-query-sequences.mjs` (tabs, English comments):
|
|
150
|
+
|
|
151
|
+
```js
|
|
152
|
+
// --- Attach settle scheme probe (issue #161) -------------------------------
|
|
153
|
+
// pi-tui >= 0.99 renamed queryTerminalBackgroundColor -> queryTerminalColors.
|
|
154
|
+
// This repo compiles against pi-tui 0.79.8 but the extension runs against the
|
|
155
|
+
// host pi's pi-tui (peerDependencies "*"), so the probe feature-detects at
|
|
156
|
+
// runtime. Local structural typedefs only — TerminalColors does not exist on
|
|
157
|
+
// the 0.79.8 compile face and must not be imported.
|
|
158
|
+
|
|
159
|
+
/** @typedef {{ r: number, g: number, b: number }} SettleRgb */
|
|
160
|
+
/** @typedef {{ background?: SettleRgb, foreground?: SettleRgb, palette?: SettleRgb[] }} TerminalColorsLike */
|
|
161
|
+
/** @typedef {{ queryTerminalColors?: (opts: { timeoutMs: number, onLateReply?: (colors: TerminalColorsLike) => void }) => Promise<TerminalColorsLike>, queryTerminalBackgroundColor?: (opts: { timeoutMs: number }) => Promise<SettleRgb|undefined> }} SettleProbeSurface */
|
|
162
|
+
/** @typedef {{ rgb?: SettleRgb, anyColors: boolean }} SettleProbeResult */
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Issue #161: detect which terminal color query API a TUI surface exposes.
|
|
166
|
+
* Detection is pure; the returned adapter only calls the passed-in surface.
|
|
167
|
+
* @param {SettleProbeSurface} tuiLike
|
|
168
|
+
* @returns {{ api: "colors"|"background"|"none", invoke: (tui: SettleProbeSurface, timeoutMs: number, onLateReply?: (colors: TerminalColorsLike) => void) => Promise<SettleProbeResult> }}
|
|
169
|
+
*/
|
|
170
|
+
export function resolveProbeApi(tuiLike) {
|
|
171
|
+
if (tuiLike && typeof tuiLike.queryTerminalColors === "function") {
|
|
172
|
+
return {
|
|
173
|
+
api: "colors",
|
|
174
|
+
invoke: async (tui, timeoutMs, onLateReply) => {
|
|
175
|
+
const colors = await tui.queryTerminalColors({ timeoutMs, onLateReply });
|
|
176
|
+
return { rgb: backgroundRgbFromTerminalColors(colors), anyColors: hasAnyColor(colors) };
|
|
177
|
+
},
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
if (tuiLike && typeof tuiLike.queryTerminalBackgroundColor === "function") {
|
|
181
|
+
return {
|
|
182
|
+
api: "background",
|
|
183
|
+
invoke: async (tui, timeoutMs) => {
|
|
184
|
+
const rgb = await tui.queryTerminalBackgroundColor({ timeoutMs });
|
|
185
|
+
return { rgb, anyColors: rgb !== undefined };
|
|
186
|
+
},
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
return { api: "none", invoke: async () => ({ rgb: undefined, anyColors: false }) };
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** @param {TerminalColorsLike} colors @returns {SettleRgb|undefined} */
|
|
193
|
+
export function backgroundRgbFromTerminalColors(colors) {
|
|
194
|
+
return colors && typeof colors === "object" ? colors.background : undefined;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/** @param {TerminalColorsLike} colors @returns {boolean} */
|
|
198
|
+
function hasAnyColor(colors) {
|
|
199
|
+
if (!colors || typeof colors !== "object") return false;
|
|
200
|
+
if (colors.background !== undefined || colors.foreground !== undefined) return true;
|
|
201
|
+
return Array.isArray(colors.palette) && colors.palette.some((c) => c !== undefined);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const SETTLE_SCHEME_WARN_OUTCOMES = new Set(["timeout", "error", "no_background", "no_probe_api"]);
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Issue #161: build the DiagnosticEvent-compatible patch for the settle scheme
|
|
208
|
+
* flow. Field names MUST stay inside normalizeDiagnostic's whitelist
|
|
209
|
+
* (level/code/runId/source/message/details) — anything else is silently
|
|
210
|
+
* dropped. Warn only when delivery was expected but failed; routine outcomes
|
|
211
|
+
* (dedup, quick detach, kill switch) stay info so warningCount stays meaningful.
|
|
212
|
+
* @param {{ probeApi?: "colors"|"background"|"none", outcome: "reported"|"timeout"|"error"|"no_background"|"no_probe_api"|"dropped_closed"|"duplicate_skipped"|"suppressed", report?: string, late?: boolean }} details
|
|
213
|
+
*/
|
|
214
|
+
export function buildSettleSchemePatch(details) {
|
|
215
|
+
const parts = [details.outcome];
|
|
216
|
+
if (details.probeApi) parts.push(`via ${details.probeApi}`);
|
|
217
|
+
if (details.report) parts.push(details.report);
|
|
218
|
+
if (details.late) parts.push("late");
|
|
219
|
+
const patch = {
|
|
220
|
+
source: "attach",
|
|
221
|
+
level: SETTLE_SCHEME_WARN_OUTCOMES.has(details.outcome) ? "warn" : "info",
|
|
222
|
+
code: "attach_settle_scheme",
|
|
223
|
+
message: `attach settle scheme: ${parts.join(" ")}`,
|
|
224
|
+
details: { outcome: details.outcome },
|
|
225
|
+
};
|
|
226
|
+
if (details.probeApi !== undefined) patch.details.probeApi = details.probeApi;
|
|
227
|
+
if (details.report !== undefined) patch.details.report = details.report;
|
|
228
|
+
if (details.late !== undefined) patch.details.late = details.late;
|
|
229
|
+
return patch;
|
|
230
|
+
}
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
- [ ] **Step 4: Run tests to verify they pass**
|
|
234
|
+
|
|
235
|
+
Run: `node --test test/terminal-query-sequences.test.mjs`
|
|
236
|
+
Expected: PASS (new + existing tests).
|
|
237
|
+
|
|
238
|
+
- [ ] **Step 5: Commit**
|
|
239
|
+
|
|
240
|
+
```bash
|
|
241
|
+
git add src/core/terminal-query-sequences.mjs test/terminal-query-sequences.test.mjs
|
|
242
|
+
git commit -m "fix(core): add settle probe API detection and diagnostic patch helpers"
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
---
|
|
246
|
+
|
|
247
|
+
### Task 2: Settle orchestration rewrite + scenario tests (A1, A2, A3, A9, A6)
|
|
248
|
+
|
|
249
|
+
**Files:**
|
|
250
|
+
- Modify: `src/ui/pty-attach.ts` — replace `reportRealTerminalColorScheme` / `probeAndReportRealTerminalColorScheme` (currently around lines 647–672); extend `PtyAttachOptions` (line 24); add `sentScheme` field near the settle-timer fields (~line 226); extend the import from `../core/terminal-query-sequences.mjs` (line 14).
|
|
251
|
+
- Create: `test-support/attach-settle-scheme-smoke.ts`
|
|
252
|
+
- Create: `test/pty-attach-settle-scheme.test.mjs`
|
|
253
|
+
|
|
254
|
+
**Interfaces:**
|
|
255
|
+
- Consumes: Task 1's `resolveProbeApi`, `backgroundRgbFromTerminalColors`, `buildSettleSchemePatch`; existing `colorSchemeForBackgroundRgb`, `toColorSchemeReport`, `RgbColor`, `TerminalColorScheme`, `REAL_TERMINAL_SCHEME_PROBE_TIMEOUT_MS`, `this.send`, `this.closed`.
|
|
256
|
+
- Produces: `PtyAttachOptions.onDiagnostic?: (event: AttachSettleDiagnosticPatch) => void`; `export interface AttachSettleDiagnosticPatch { source: string; level: "info"|"warn"; code: string; message: string; details: { probeApi?: string; outcome: string; report?: string; late?: boolean } }` (exported from `src/ui/pty-attach.ts` — Task 4's wiring imports it implicitly via the options type).
|
|
257
|
+
|
|
258
|
+
- [ ] **Step 1: Write the failing tests**
|
|
259
|
+
|
|
260
|
+
Create `test-support/attach-settle-scheme-smoke.ts` (scenarios run the real component with fake TUIs; the driver asserts on the JSON):
|
|
261
|
+
|
|
262
|
+
```ts
|
|
263
|
+
// Attach settle scheme probe scenarios (issue #161). Driver: test/pty-attach-settle-scheme.test.mjs.
|
|
264
|
+
import { PtyAttachComponent } from "../src/ui/pty-attach.ts";
|
|
265
|
+
|
|
266
|
+
const LIGHT = { r: 0.94, g: 0.94, b: 0.94 };
|
|
267
|
+
const DARK = { r: 0.16, g: 0.16, b: 0.21 };
|
|
268
|
+
|
|
269
|
+
interface Captures { sent: string[]; diagnostics: any[]; writes: string[]; }
|
|
270
|
+
|
|
271
|
+
function makeTui(overrides: Record<string, unknown>): Record<string, unknown> {
|
|
272
|
+
return {
|
|
273
|
+
terminal: { rows: 24, cols: 80, columns: 80, write: (_d: string) => {} },
|
|
274
|
+
requestRender: () => {},
|
|
275
|
+
onTerminalColorSchemeChange: () => () => {},
|
|
276
|
+
...overrides,
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
async function runSettle(tui: Record<string, unknown>, opts: { env?: Record<string, string> } = {}): Promise<Captures> {
|
|
281
|
+
const captures: Captures = { sent: [], diagnostics: [], writes: [] };
|
|
282
|
+
(tui.terminal as { write: (d: string) => void }).write = (d) => captures.writes.push(d);
|
|
283
|
+
const theme = { fg: (_c: string, t: string) => t, bold: (t: string) => t };
|
|
284
|
+
for (const [k, v] of Object.entries(opts.env ?? {})) process.env[k] = v;
|
|
285
|
+
try {
|
|
286
|
+
const attach = new PtyAttachComponent(tui as never, theme as never, {} as never, () => {}, {
|
|
287
|
+
socketPath: "/nonexistent/settle-scheme.sock",
|
|
288
|
+
title: "settle-scheme",
|
|
289
|
+
onDiagnostic: (event) => captures.diagnostics.push(event),
|
|
290
|
+
});
|
|
291
|
+
const internals = attach as unknown as { send: (m: { type: string; data?: string }) => void; finishAttachTransition: () => void };
|
|
292
|
+
internals.send = (m) => { if (m?.type === "input" && m.data) captures.sent.push(m.data); };
|
|
293
|
+
internals.finishAttachTransition();
|
|
294
|
+
await new Promise((r) => setImmediate(r));
|
|
295
|
+
return captures;
|
|
296
|
+
} finally {
|
|
297
|
+
for (const k of Object.keys(opts.env ?? {})) delete process.env[k];
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
const out: Record<string, unknown> = {};
|
|
302
|
+
const dump = (c: Captures) => ({ sent: c.sent, diagnostics: c.diagnostics, writes: c.writes });
|
|
303
|
+
|
|
304
|
+
// A1: new API, light and dark backgrounds.
|
|
305
|
+
out.newApiLight = dump(await runSettle(makeTui({ queryTerminalColors: async () => ({ background: LIGHT }) })));
|
|
306
|
+
out.newApiDark = dump(await runSettle(makeTui({ queryTerminalColors: async () => ({ background: DARK }) })));
|
|
307
|
+
// A2: old API fallback keeps #149 behavior.
|
|
308
|
+
out.oldApi = dump(await runSettle(makeTui({ queryTerminalBackgroundColor: async () => LIGHT })));
|
|
309
|
+
// A3: neither API — no probe, no raw write.
|
|
310
|
+
out.noApi = dump(await runSettle(makeTui({})));
|
|
311
|
+
// A9: three failure branches.
|
|
312
|
+
out.timeout = dump(await runSettle(makeTui({ queryTerminalColors: async () => ({}) })));
|
|
313
|
+
out.error = dump(await runSettle(makeTui({ queryTerminalColors: async () => { throw new Error("boom"); } })));
|
|
314
|
+
out.noBackground = dump(await runSettle(makeTui({ queryTerminalColors: async () => ({ foreground: DARK }) })));
|
|
315
|
+
// Kill switch: suppressed, info level, no probeApi.
|
|
316
|
+
out.killSwitch = dump(await runSettle(makeTui({ queryTerminalColors: async () => ({ background: LIGHT }) }), { env: { AGENT_BOARD_FORWARD_TERMINAL_QUERIES: "0" } }));
|
|
317
|
+
|
|
318
|
+
console.log(JSON.stringify(out));
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
Create `test/pty-attach-settle-scheme.test.mjs`:
|
|
322
|
+
|
|
323
|
+
```js
|
|
324
|
+
import assert from "node:assert/strict";
|
|
325
|
+
import { execFileSync } from "node:child_process";
|
|
326
|
+
import { fileURLToPath } from "node:url";
|
|
327
|
+
import test from "node:test";
|
|
328
|
+
|
|
329
|
+
const script = fileURLToPath(new URL("../test-support/attach-settle-scheme-smoke.ts", import.meta.url));
|
|
330
|
+
|
|
331
|
+
test("attach settle scheme probe: new/old/none APIs, failures, kill switch", () => {
|
|
332
|
+
const out = JSON.parse(execFileSync(process.execPath, ["--experimental-transform-types", script], {
|
|
333
|
+
encoding: "utf-8",
|
|
334
|
+
timeout: 20000,
|
|
335
|
+
}));
|
|
336
|
+
|
|
337
|
+
// A1 — new API delivers light and dark once each.
|
|
338
|
+
assert.deepEqual(out.newApiLight.sent, ["\x1b[?997;2n"], "A1 light: one 997;2");
|
|
339
|
+
assert.deepEqual(out.newApiDark.sent, ["\x1b[?997;1n"], "A1 dark: one 997;1");
|
|
340
|
+
assert.equal(out.newApiLight.diagnostics[0].details.probeApi, "colors");
|
|
341
|
+
assert.equal(out.newApiLight.diagnostics[0].details.outcome, "reported");
|
|
342
|
+
assert.equal(out.newApiLight.diagnostics[0].details.report, "997;2");
|
|
343
|
+
assert.equal(out.newApiLight.diagnostics[0].level, "info");
|
|
344
|
+
|
|
345
|
+
// A2 — old API fallback still delivers.
|
|
346
|
+
assert.deepEqual(out.oldApi.sent, ["\x1b[?997;2n"], "A2 old API delivers");
|
|
347
|
+
assert.equal(out.oldApi.diagnostics[0].details.probeApi, "background");
|
|
348
|
+
|
|
349
|
+
// A3 — neither API: no probe, no raw write, no send.
|
|
350
|
+
assert.deepEqual(out.noApi.sent, [], "A3 sends nothing");
|
|
351
|
+
assert.deepEqual(out.noApi.writes, [], "A3 never writes an unowned raw query");
|
|
352
|
+
assert.equal(out.noApi.diagnostics[0].details.probeApi, "none");
|
|
353
|
+
assert.equal(out.noApi.diagnostics[0].details.outcome, "no_probe_api");
|
|
354
|
+
assert.equal(out.noApi.diagnostics[0].level, "warn");
|
|
355
|
+
|
|
356
|
+
// A9 — failure branches send nothing and classify.
|
|
357
|
+
for (const [key, outcome] of [["timeout", "timeout"], ["error", "error"], ["noBackground", "no_background"]]) {
|
|
358
|
+
assert.deepEqual(out[key].sent, [], `${key}: no 997 on failure`);
|
|
359
|
+
assert.equal(out[key].diagnostics[0].details.outcome, outcome);
|
|
360
|
+
assert.equal(out[key].diagnostics[0].level, "warn");
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// Kill switch — suppressed, info, no probeApi.
|
|
364
|
+
assert.deepEqual(out.killSwitch.sent, [], "kill switch sends nothing");
|
|
365
|
+
assert.equal(out.killSwitch.diagnostics[0].details.outcome, "suppressed");
|
|
366
|
+
assert.equal(out.killSwitch.diagnostics[0].details.probeApi, undefined);
|
|
367
|
+
assert.equal(out.killSwitch.diagnostics[0].level, "info");
|
|
368
|
+
});
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
- [ ] **Step 2: Run tests to verify they fail**
|
|
372
|
+
|
|
373
|
+
Run: `node --test test/pty-attach-settle-scheme.test.mjs`
|
|
374
|
+
Expected: FAIL — `onDiagnostic` is not read yet, so `newApiLight.diagnostics` is empty (probe still calls the old API name on the fake → TypeError swallowed → no sends). Assertions on `sent` fail.
|
|
375
|
+
|
|
376
|
+
- [ ] **Step 3: Implement the orchestration**
|
|
377
|
+
|
|
378
|
+
In `src/ui/pty-attach.ts`:
|
|
379
|
+
|
|
380
|
+
(a) Extend the core import (line 14):
|
|
381
|
+
|
|
382
|
+
```ts
|
|
383
|
+
import {
|
|
384
|
+
backgroundRgbFromTerminalColors,
|
|
385
|
+
buildSettleSchemePatch,
|
|
386
|
+
colorSchemeForBackgroundRgb,
|
|
387
|
+
extractOscQuerySequences,
|
|
388
|
+
resolveProbeApi,
|
|
389
|
+
toColorSchemeReport,
|
|
390
|
+
} from "../core/terminal-query-sequences.mjs";
|
|
391
|
+
```
|
|
392
|
+
|
|
393
|
+
(b) Add to `PtyAttachOptions` (line 24 region) and export the patch type above it:
|
|
394
|
+
|
|
395
|
+
```ts
|
|
396
|
+
/** Diagnostic patch emitted at attach settle (DiagnosticEvent-compatible; built by buildSettleSchemePatch). */
|
|
397
|
+
export interface AttachSettleDiagnosticPatch {
|
|
398
|
+
source: string;
|
|
399
|
+
level: "info" | "warn";
|
|
400
|
+
code: string;
|
|
401
|
+
message: string;
|
|
402
|
+
details: { probeApi?: string; outcome: string; report?: string; late?: boolean };
|
|
403
|
+
}
|
|
404
|
+
```
|
|
405
|
+
|
|
406
|
+
and inside the interface:
|
|
407
|
+
|
|
408
|
+
```ts
|
|
409
|
+
/** Issue #161: receives settle-scheme diagnostic patches. Default: no-op. */
|
|
410
|
+
onDiagnostic?: (event: AttachSettleDiagnosticPatch) => void;
|
|
411
|
+
```
|
|
412
|
+
|
|
413
|
+
(c) Add a field near `attachSettleTimer` (~line 226):
|
|
414
|
+
|
|
415
|
+
```ts
|
|
416
|
+
// Last scheme delivered to the child at settle (issue #161). Starts null:
|
|
417
|
+
// null never matches the duplicate check, so a timeout-then-late sequence
|
|
418
|
+
// still delivers. Single-shot per attach — settle runs at most once.
|
|
419
|
+
private sentScheme: TerminalColorScheme | null = null;
|
|
420
|
+
```
|
|
421
|
+
|
|
422
|
+
(d) Replace `reportRealTerminalColorScheme` and `probeAndReportRealTerminalColorScheme` (keep and update the existing doc comment: #148's probe design, #161's API-rename root cause, one probe only):
|
|
423
|
+
|
|
424
|
+
```ts
|
|
425
|
+
/** Issue #148/#161: at settle, ask the REAL terminal itself (feature-detected
|
|
426
|
+
* public query API — pi-tui >= 0.99 renamed it to queryTerminalColors) and
|
|
427
|
+
* hand the child an unconditional 997 color-scheme report. One probe per
|
|
428
|
+
* attach; the pre-#148 silence on failure is now observable via
|
|
429
|
+
* onDiagnostic. Same kill switch as #128 D1-D3. */
|
|
430
|
+
private reportRealTerminalColorScheme(): void {
|
|
431
|
+
if (process.env.AGENT_BOARD_FORWARD_TERMINAL_QUERIES === "0") {
|
|
432
|
+
this.opts.onDiagnostic?.(buildSettleSchemePatch({ outcome: "suppressed" }));
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
435
|
+
void this.probeAndReportRealTerminalColorScheme();
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
private async probeAndReportRealTerminalColorScheme(): Promise<void> {
|
|
439
|
+
const probe = resolveProbeApi(this.tui as never);
|
|
440
|
+
if (probe.api === "none") {
|
|
441
|
+
this.opts.onDiagnostic?.(buildSettleSchemePatch({ probeApi: "none", outcome: "no_probe_api" }));
|
|
442
|
+
return;
|
|
443
|
+
}
|
|
444
|
+
let result: { rgb?: RgbColor; anyColors: boolean };
|
|
445
|
+
try {
|
|
446
|
+
result = await probe.invoke(
|
|
447
|
+
this.tui as never,
|
|
448
|
+
REAL_TERMINAL_SCHEME_PROBE_TIMEOUT_MS,
|
|
449
|
+
(colors) => this.handleLateSettleColors(probe.api, colors),
|
|
450
|
+
);
|
|
451
|
+
} catch {
|
|
452
|
+
/* best-effort: a failed probe keeps the pre-#148 behavior, but recorded */
|
|
453
|
+
this.opts.onDiagnostic?.(buildSettleSchemePatch({ probeApi: probe.api, outcome: "error" }));
|
|
454
|
+
return;
|
|
455
|
+
}
|
|
456
|
+
const rgb = result?.rgb;
|
|
457
|
+
if (!rgb) {
|
|
458
|
+
// Outcome rule (spec §2.3): any color answered but no background =>
|
|
459
|
+
// no_background; nothing answered at all => timeout. The old API only
|
|
460
|
+
// queries OSC 11, so its undefined is always a timeout.
|
|
461
|
+
const outcome = result && result.anyColors ? "no_background" : "timeout";
|
|
462
|
+
this.opts.onDiagnostic?.(buildSettleSchemePatch({ probeApi: probe.api, outcome }));
|
|
463
|
+
return;
|
|
464
|
+
}
|
|
465
|
+
this.deliverSettleScheme(colorSchemeForBackgroundRgb(rgb) ?? undefined, probe.api, false);
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
/** pi-tui >= 0.99 late replies: deliver only when the scheme differs from
|
|
469
|
+
* what was already sent (sentScheme starts null and never matches, so the
|
|
470
|
+
* timeout-then-late path delivers). Closed => dropped, but recorded. */
|
|
471
|
+
private handleLateSettleColors(api: "colors" | "background" | "none", colors: unknown): void {
|
|
472
|
+
if (this.closed) {
|
|
473
|
+
this.opts.onDiagnostic?.(buildSettleSchemePatch({ probeApi: api, outcome: "dropped_closed" }));
|
|
474
|
+
return;
|
|
475
|
+
}
|
|
476
|
+
const rgb = backgroundRgbFromTerminalColors(colors as never);
|
|
477
|
+
const scheme = rgb ? colorSchemeForBackgroundRgb(rgb) ?? undefined : undefined;
|
|
478
|
+
if (!scheme) return;
|
|
479
|
+
if (this.sentScheme === scheme) {
|
|
480
|
+
this.opts.onDiagnostic?.(buildSettleSchemePatch({ probeApi: api, outcome: "duplicate_skipped" }));
|
|
481
|
+
return;
|
|
482
|
+
}
|
|
483
|
+
this.deliverSettleScheme(scheme, api, true);
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
private deliverSettleScheme(scheme: TerminalColorScheme | undefined, api: "colors" | "background" | "none", late: boolean): void {
|
|
487
|
+
if (!scheme || this.closed) return;
|
|
488
|
+
const data = toColorSchemeReport(scheme);
|
|
489
|
+
if (!data) return;
|
|
490
|
+
this.sentScheme = scheme;
|
|
491
|
+
this.send({ type: "input", data });
|
|
492
|
+
const details: { probeApi: "colors" | "background" | "none"; outcome: "reported"; report: string; late?: boolean } = {
|
|
493
|
+
probeApi: api,
|
|
494
|
+
outcome: "reported",
|
|
495
|
+
report: scheme === "light" ? "997;2" : "997;1",
|
|
496
|
+
};
|
|
497
|
+
if (late) details.late = true;
|
|
498
|
+
this.opts.onDiagnostic?.(buildSettleSchemePatch(details));
|
|
499
|
+
}
|
|
500
|
+
```
|
|
501
|
+
|
|
502
|
+
- [ ] **Step 4: Run tests to verify they pass**
|
|
503
|
+
|
|
504
|
+
Run: `node --test test/pty-attach-settle-scheme.test.mjs && npm run typecheck`
|
|
505
|
+
Expected: PASS both.
|
|
506
|
+
|
|
507
|
+
- [ ] **Step 5: Commit**
|
|
508
|
+
|
|
509
|
+
```bash
|
|
510
|
+
git add src/ui/pty-attach.ts test-support/attach-settle-scheme-smoke.ts test/pty-attach-settle-scheme.test.mjs
|
|
511
|
+
git commit -m "fix(attach): settle scheme probe uses the detected terminal query API"
|
|
512
|
+
```
|
|
513
|
+
|
|
514
|
+
---
|
|
515
|
+
|
|
516
|
+
### Task 3: Late-reply path (A5) — dedup, closed guard, timeout-then-late
|
|
517
|
+
|
|
518
|
+
**Files:**
|
|
519
|
+
- Modify: `test-support/attach-settle-scheme-smoke.ts` (add scenarios)
|
|
520
|
+
- Modify: `test/pty-attach-settle-scheme.test.mjs` (add assertions)
|
|
521
|
+
|
|
522
|
+
**Interfaces:**
|
|
523
|
+
- Consumes: Task 2's `handleLateSettleColors` behavior (no signature change — this task only proves it).
|
|
524
|
+
- Produces: none (behavior verification only).
|
|
525
|
+
|
|
526
|
+
- [ ] **Step 1: Write the failing tests**
|
|
527
|
+
|
|
528
|
+
Add to `attach-settle-scheme-smoke.ts` (before the `console.log`); these need control over the late callback, so they inline the setup instead of using `runSettle`:
|
|
529
|
+
|
|
530
|
+
```ts
|
|
531
|
+
async function runLateScenario(kind: "same" | "different" | "afterTimeout" | "closed") {
|
|
532
|
+
const captures: Captures = { sent: [], diagnostics: [], writes: [] };
|
|
533
|
+
// firstReply: "same"/"different"/"closed" deliver LIGHT up front; "afterTimeout"
|
|
534
|
+
// answers nothing so the late reply is the only source.
|
|
535
|
+
const firstReply = kind === "afterTimeout" ? {} : { background: LIGHT };
|
|
536
|
+
let late: ((colors: unknown) => void) | undefined;
|
|
537
|
+
const tui = {
|
|
538
|
+
terminal: { rows: 24, cols: 80, columns: 80, write: (d: string) => captures.writes.push(d) },
|
|
539
|
+
requestRender: () => {},
|
|
540
|
+
onTerminalColorSchemeChange: () => () => {},
|
|
541
|
+
queryTerminalColors: async (opts: { onLateReply?: (c: unknown) => void }) => {
|
|
542
|
+
// Capture the late callback instead of auto-firing it: the "closed"
|
|
543
|
+
// scenario must close the component BEFORE the late reply arrives.
|
|
544
|
+
late = opts.onLateReply;
|
|
545
|
+
return firstReply;
|
|
546
|
+
},
|
|
547
|
+
};
|
|
548
|
+
const theme = { fg: (_c: string, t: string) => t, bold: (t: string) => t };
|
|
549
|
+
const attach = new PtyAttachComponent(tui as never, theme as never, {} as never, () => {}, {
|
|
550
|
+
socketPath: "/nonexistent/settle-scheme.sock",
|
|
551
|
+
title: "settle-scheme-late",
|
|
552
|
+
onDiagnostic: (event) => captures.diagnostics.push(event),
|
|
553
|
+
});
|
|
554
|
+
const internals = attach as unknown as { send: (m: { type: string; data?: string }) => void; finishAttachTransition: () => void };
|
|
555
|
+
internals.send = (m) => { if (m?.type === "input" && m.data) captures.sent.push(m.data); };
|
|
556
|
+
internals.finishAttachTransition();
|
|
557
|
+
await new Promise((r) => setImmediate(r));
|
|
558
|
+
if (kind === "closed") (attach as unknown as { close(): void }).close();
|
|
559
|
+
// Late payload: same scheme as delivered for "same"; a dark scheme for
|
|
560
|
+
// "different"/"afterTimeout" (differs from the LIGHT already sent — or from
|
|
561
|
+
// nothing, since sentScheme starts null); anything for "closed" (must drop).
|
|
562
|
+
const latePayload = kind === "same" ? { background: LIGHT } : { background: { r: 0.16, g: 0.16, b: 0.21 } };
|
|
563
|
+
late?.(latePayload);
|
|
564
|
+
await new Promise((r) => setImmediate(r));
|
|
565
|
+
return captures;
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
out.lateSame = dump(await runLateScenario("same"));
|
|
569
|
+
out.lateDifferent = dump(await runLateScenario("different"));
|
|
570
|
+
out.lateAfterTimeout = dump(await runLateScenario("afterTimeout"));
|
|
571
|
+
out.lateClosed = dump(await runLateScenario("closed"));
|
|
572
|
+
```
|
|
573
|
+
|
|
574
|
+
Add to the driver's test body:
|
|
575
|
+
|
|
576
|
+
```js
|
|
577
|
+
// A5 — late replies: dedup, closed guard, timeout-then-late delivery.
|
|
578
|
+
assert.deepEqual(out.lateSame.sent, ["\x1b[?997;2n"], "same-scheme late reply is deduped");
|
|
579
|
+
assert.equal(out.lateSame.diagnostics.at(-1).details.outcome, "duplicate_skipped");
|
|
580
|
+
assert.equal(out.lateSame.diagnostics.at(-1).level, "info");
|
|
581
|
+
|
|
582
|
+
assert.deepEqual(out.lateDifferent.sent, ["\x1b[?997;2n", "\x1b[?997;1n"], "different-scheme late reply delivers");
|
|
583
|
+
const lateDiff = out.lateDifferent.diagnostics.at(-1);
|
|
584
|
+
assert.equal(lateDiff.details.outcome, "reported");
|
|
585
|
+
assert.equal(lateDiff.details.late, true);
|
|
586
|
+
assert.equal(lateDiff.details.report, "997;1");
|
|
587
|
+
|
|
588
|
+
assert.deepEqual(out.lateAfterTimeout.sent, ["\x1b[?997;1n"], "timeout-then-late still delivers (sentScheme null)");
|
|
589
|
+
assert.equal(out.lateAfterTimeout.diagnostics[0].details.outcome, "timeout");
|
|
590
|
+
assert.equal(out.lateAfterTimeout.diagnostics.at(-1).details.outcome, "reported");
|
|
591
|
+
|
|
592
|
+
assert.deepEqual(out.lateClosed.sent, ["\x1b[?997;2n"], "closed late reply sends nothing beyond the initial delivery");
|
|
593
|
+
assert.equal(out.lateClosed.diagnostics.at(-1).details.outcome, "dropped_closed");
|
|
594
|
+
assert.equal(out.lateClosed.diagnostics.at(-1).level, "info");
|
|
595
|
+
```
|
|
596
|
+
|
|
597
|
+
- [ ] **Step 2: Run tests to verify they fail**
|
|
598
|
+
|
|
599
|
+
Run: `node --test test/pty-attach-settle-scheme.test.mjs`
|
|
600
|
+
Expected: FAIL on `lateSame`/`lateDifferent`/`lateClosed` — Task 2's implementation wires `onLateReply` already, so if all pass immediately, verify the assertions actually exercise the path (temporarily break `handleLateSettleColors`'s dedup line and re-run; it must fail). Document the mutation check in the commit-free scratch, then restore.
|
|
601
|
+
|
|
602
|
+
- [ ] **Step 3: Fix only what the failures show (expected: none — this task proves Task 2's late path)**
|
|
603
|
+
|
|
604
|
+
If a scenario fails, fix `handleLateSettleColors` / `deliverSettleScheme` in `src/ui/pty-attach.ts` until green. Do not weaken assertions.
|
|
605
|
+
|
|
606
|
+
- [ ] **Step 4: Run tests to verify they pass**
|
|
607
|
+
|
|
608
|
+
Run: `node --test test/pty-attach-settle-scheme.test.mjs && npm run typecheck`
|
|
609
|
+
Expected: PASS.
|
|
610
|
+
|
|
611
|
+
- [ ] **Step 5: Commit**
|
|
612
|
+
|
|
613
|
+
```bash
|
|
614
|
+
git add test-support/attach-settle-scheme-smoke.ts test/pty-attach-settle-scheme.test.mjs
|
|
615
|
+
git commit -m "test(attach): cover late color reply dedup, closed guard, timeout-then-late"
|
|
616
|
+
```
|
|
617
|
+
|
|
618
|
+
---
|
|
619
|
+
|
|
620
|
+
### Task 4: Wire diagnostics into both openPtyAttach construction sites
|
|
621
|
+
|
|
622
|
+
**Files:**
|
|
623
|
+
- Modify: `src/commands/attach-flow.ts:24-45` (exported `openPtyAttach` — keyboard/bg paths)
|
|
624
|
+
- Modify: `src/commands/agent-board.ts:226-251` (private `openPtyAttach` — dashboard path)
|
|
625
|
+
|
|
626
|
+
**Interfaces:**
|
|
627
|
+
- Consumes: Task 2's `PtyAttachOptions.onDiagnostic`; existing `appendDiagnostic(root, viewId, patch)` from `src/core/diagnostics.mjs`; both factories already hold `root` and `viewId`.
|
|
628
|
+
- Produces: diagnostics.jsonl entries per attach for U1/U2.
|
|
629
|
+
|
|
630
|
+
- [ ] **Step 1: Implement the wiring (no unit — U1 verifies end-to-end; A8's typecheck gates the compile)**
|
|
631
|
+
|
|
632
|
+
In BOTH files: add the import next to the existing `screenLogPath` import:
|
|
633
|
+
|
|
634
|
+
```ts
|
|
635
|
+
import { appendDiagnostic } from "../core/diagnostics.mjs";
|
|
636
|
+
```
|
|
637
|
+
|
|
638
|
+
and extend the options object passed to `new PtyAttachComponent(...)` identically in both factories:
|
|
639
|
+
|
|
640
|
+
```ts
|
|
641
|
+
{
|
|
642
|
+
socketPath,
|
|
643
|
+
screenLogPath: root ? screenLogPath(root, viewId) : undefined,
|
|
644
|
+
title: name,
|
|
645
|
+
onDiagnostic: root
|
|
646
|
+
? (event) => {
|
|
647
|
+
// Best effort, like every other diagnostics writer.
|
|
648
|
+
try {
|
|
649
|
+
appendDiagnostic(root, viewId, event as never);
|
|
650
|
+
} catch {
|
|
651
|
+
/* diagnostics must never break attach */
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
: undefined,
|
|
655
|
+
},
|
|
656
|
+
```
|
|
657
|
+
|
|
658
|
+
- [ ] **Step 2: Run the gates**
|
|
659
|
+
|
|
660
|
+
Run: `npm run typecheck && node --test test/attach-flow.test.mjs test/pty-attach-detach-gate.test.mjs`
|
|
661
|
+
Expected: PASS both (no behavior change without onDiagnostic consumers).
|
|
662
|
+
|
|
663
|
+
- [ ] **Step 3: Commit**
|
|
664
|
+
|
|
665
|
+
```bash
|
|
666
|
+
git add src/commands/attach-flow.ts src/commands/agent-board.ts
|
|
667
|
+
git commit -m "fix(attach): record settle scheme diagnostics in the view's diagnostics.jsonl"
|
|
668
|
+
```
|
|
669
|
+
|
|
670
|
+
---
|
|
671
|
+
|
|
672
|
+
### Task 5: Real-PTY integration scenario with the new API (A7)
|
|
673
|
+
|
|
674
|
+
**Files:**
|
|
675
|
+
- Modify: `test-support/detach-gate-smoke.ts` (the settle-probe scenario block — search `settleProbeReportsRealTerminalScheme`, currently around lines 544–561)
|
|
676
|
+
- Modify: `test/pty-attach-detach-gate.test.mjs` (add one assertion)
|
|
677
|
+
|
|
678
|
+
**Interfaces:**
|
|
679
|
+
- Consumes: existing harness (fake tui + real `node-pty` child + captured `sent`); Task 2's component behavior.
|
|
680
|
+
- Produces: `out.settleProbeNewApiReportsRealTerminalScheme` boolean in the smoke JSON.
|
|
681
|
+
|
|
682
|
+
- [ ] **Step 1: Write the failing test**
|
|
683
|
+
|
|
684
|
+
In the driver (`test/pty-attach-detach-gate.test.mjs`), add to the existing detach-gate test's assertions:
|
|
685
|
+
|
|
686
|
+
```js
|
|
687
|
+
assert.equal(parsed.settleProbeNewApiReportsRealTerminalScheme, true, "A7: new-API tui delivers a 997;2 to the real child PTY exactly once");
|
|
688
|
+
```
|
|
689
|
+
|
|
690
|
+
- [ ] **Step 2: Run to verify it fails**
|
|
691
|
+
|
|
692
|
+
Run: `node --test test/pty-attach-detach-gate.test.mjs`
|
|
693
|
+
Expected: FAIL — `settleProbeNewApiReportsRealTerminalScheme` is `undefined` in the smoke JSON.
|
|
694
|
+
|
|
695
|
+
- [ ] **Step 3: Add the scenario to the smoke script**
|
|
696
|
+
|
|
697
|
+
Mirror the existing `settleProbeReportsRealTerminalScheme` scenario: same real-`node-pty` child and `sent` capture, but the fake tui exposes ONLY the new API (reuse the file's existing light RGB constant, e.g. the Catppuccin Latte background used by the current scenario):
|
|
698
|
+
|
|
699
|
+
```ts
|
|
700
|
+
// A7 (issue #161): tui with only the new pi-tui API (>= 0.99) still delivers
|
|
701
|
+
// the settle scheme to the real child PTY — exactly one 997;2 report.
|
|
702
|
+
{
|
|
703
|
+
const newApiTui = makeSmokeTui({
|
|
704
|
+
queryTerminalColors: async () => ({ background: LIGHT_BACKGROUND_RGB }),
|
|
705
|
+
});
|
|
706
|
+
const sent = await runSettleProbeScenario(newApiTui);
|
|
707
|
+
out.settleProbeNewApiReportsRealTerminalScheme =
|
|
708
|
+
sent.length === 1 && sent[0] === "\x1b[?997;2n";
|
|
709
|
+
}
|
|
710
|
+
```
|
|
711
|
+
|
|
712
|
+
(Use the actual helper/constant names from the file — `makeSmokeTui`/`runSettleProbeScenario`/`LIGHT_BACKGROUND_RGB` stand for the existing local helpers; if the scenario block is inlined rather than helper-based, copy the block and swap the fake's query method, keeping everything else identical.)
|
|
713
|
+
|
|
714
|
+
- [ ] **Step 4: Run to verify it passes**
|
|
715
|
+
|
|
716
|
+
Run: `node --test test/pty-attach-detach-gate.test.mjs`
|
|
717
|
+
Expected: PASS.
|
|
718
|
+
|
|
719
|
+
- [ ] **Step 5: Commit**
|
|
720
|
+
|
|
721
|
+
```bash
|
|
722
|
+
git add test-support/detach-gate-smoke.ts test/pty-attach-detach-gate.test.mjs
|
|
723
|
+
git commit -m "test(attach): new query API settle delivery over a real PTY"
|
|
724
|
+
```
|
|
725
|
+
|
|
726
|
+
---
|
|
727
|
+
|
|
728
|
+
### Task 6: Full gate + post-implementation manual verification (A8, U1/U2)
|
|
729
|
+
|
|
730
|
+
**Files:**
|
|
731
|
+
- No code files. Produces the U1/U2 execution record (kept for the PR body / step 9's acceptance reconciliation).
|
|
732
|
+
|
|
733
|
+
- [ ] **Step 1: Run the full gate**
|
|
734
|
+
|
|
735
|
+
Run: `npm run verify`
|
|
736
|
+
Expected: typecheck + perf gate + tests + coverage (lines 85 / funcs 80 / branches 70 — the three-way API detection and all outcome branches are covered by Tasks 1–3) + pack:dry, all green. Fix whatever it reports; re-run until green.
|
|
737
|
+
|
|
738
|
+
- [ ] **Step 2: Reconcile the acceptance matrix (automated part)**
|
|
739
|
+
|
|
740
|
+
For each of A1–A9, record the command actually run and its result (from Tasks 1–5 runs + `npm run verify`). Any A-item that did not actually run is NOT done — do not claim it.
|
|
741
|
+
|
|
742
|
+
- [ ] **Step 3: U1/U2 manual verification (user-executed, after merge)**
|
|
743
|
+
|
|
744
|
+
Post these steps to the user (they require the merged main and a real light WezTerm):
|
|
745
|
+
|
|
746
|
+
- U1 前置: `git -C ~/.pi/agent/git/github.com/zhuxixi/pi-agent-board pull --ff-only` + restart host pi. Then light WezTerm → start board → board-level restart → attach twice (① dashboard enter on a view; ② detach, ← keyboard or bg path). Pass: both first frames light-warm (userMsg ≈ `#f0f0f0`), both views' `diagnostics.jsonl` contain `code:"attach_settle_scheme"` with `details.probeApi:"colors"`, `details.report:"997;2"`.
|
|
747
|
+
- U2: same preconditions + `AGENT_BOARD_FORWARD_TERMINAL_QUERIES=0`; pass: no probe (diagnostics `details.outcome:"suppressed"`), no errors, dark child (pre-fix behavior).
|
|
748
|
+
|
|
749
|
+
Record results as they come; `pending` until then — automated greens do NOT replace U1/U2.
|
|
750
|
+
|
|
751
|
+
- [ ] **Step 4: No commit (verification task)**
|