@pi-archimedes/ui 2.10.3 → 2.11.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/README.md +11 -7
- package/package.json +8 -8
- package/src/bash/index.ts +0 -2
- package/src/bash/renderer.test.ts +50 -5
- package/src/bash/renderer.ts +28 -30
- package/src/codemode/index.ts +0 -9
- package/src/codemode/renderer.test.ts +32 -31
- package/src/codemode/renderer.ts +29 -61
- package/src/config.test.ts +62 -1
- package/src/config.ts +45 -1
- package/src/editor/index.test.ts +118 -2
- package/src/editor/index.ts +34 -3
- package/src/editor/spin-quips.ts +1 -1
- package/src/index.screen-mode.test.ts +411 -0
- package/src/index.test.ts +506 -101
- package/src/index.ts +252 -104
- package/src/settings.test.ts +37 -2
- package/src/settings.ts +16 -3
- package/src/startup/index.test.ts +239 -129
- package/src/startup/index.ts +179 -272
- package/src/startup/pi-listing.test.ts +290 -0
- package/src/startup/sections.test.ts +38 -208
- package/src/startup/sections.ts +83 -236
- package/src/startup-splash-gate.test.ts +546 -0
- package/src/thinking/patch.test.ts +81 -1
- package/src/thinking/patch.ts +22 -6
- package/src/bash/tool.test.ts +0 -26
- package/src/bash/tool.ts +0 -17
- package/src/codemode/loader.test.ts +0 -139
- package/src/codemode/loader.ts +0 -115
- package/src/codemode/tool.test.ts +0 -149
- package/src/codemode/tool.ts +0 -96
- package/src/startup/capture.ts +0 -47
- package/src/startup/version.test.ts +0 -98
- package/src/startup/version.ts +0 -26
package/src/index.test.ts
CHANGED
|
@@ -9,26 +9,25 @@ import type { TUI, EditorTheme, Component } from "@earendil-works/pi-tui";
|
|
|
9
9
|
|
|
10
10
|
// ── Mock modules ─────────────────────────────────────────────────────────────
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
vi.mock("./config.js", async (importOriginal) => {
|
|
19
|
-
const actual = await importOriginal<typeof import("./config.js")>();
|
|
12
|
+
// Real `readEffectiveSettings`, so the harness's `pi.getSettings()` is what the
|
|
13
|
+
// splash's `quietStartup` read actually goes through; the namespace helpers are
|
|
14
|
+
// stubbed because every other settings read in these tests is about `ui`'s own
|
|
15
|
+
// config.
|
|
16
|
+
vi.mock("@pi-archimedes/core/settings-io", async (importOriginal) => {
|
|
17
|
+
const actual = await importOriginal<typeof import("@pi-archimedes/core/settings-io")>();
|
|
20
18
|
return {
|
|
21
19
|
...actual,
|
|
22
|
-
|
|
20
|
+
loadConfig: vi.fn(),
|
|
21
|
+
saveConfig: vi.fn(),
|
|
22
|
+
removeConfig: vi.fn(),
|
|
23
23
|
};
|
|
24
24
|
});
|
|
25
25
|
|
|
26
|
-
vi.mock("./
|
|
27
|
-
const actual = await importOriginal<typeof import("./
|
|
26
|
+
vi.mock("./config.js", async (importOriginal) => {
|
|
27
|
+
const actual = await importOriginal<typeof import("./config.js")>();
|
|
28
28
|
return {
|
|
29
29
|
...actual,
|
|
30
|
-
|
|
31
|
-
unpatchConsoleLog: vi.fn(),
|
|
30
|
+
loadUIConfig: vi.fn(),
|
|
32
31
|
};
|
|
33
32
|
});
|
|
34
33
|
|
|
@@ -46,6 +45,8 @@ vi.mock("./startup/index.js", async (importOriginal) => {
|
|
|
46
45
|
...actual,
|
|
47
46
|
renderHeader: vi.fn(() => ["header line"]),
|
|
48
47
|
patchStartupListing: vi.fn(),
|
|
48
|
+
revealStartupListing: vi.fn(),
|
|
49
|
+
stopStartupListing: vi.fn(),
|
|
49
50
|
};
|
|
50
51
|
});
|
|
51
52
|
|
|
@@ -56,32 +57,27 @@ vi.mock("./migration.js", () => ({
|
|
|
56
57
|
UI_CONFIG_KEYS: [],
|
|
57
58
|
}));
|
|
58
59
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
clearActiveBashIntervals: vi.fn(),
|
|
62
|
-
}));
|
|
63
|
-
|
|
64
|
-
vi.mock("./codemode/index.js", () => ({
|
|
65
|
-
registerCodemodeToolOverride: vi.fn(async () => true),
|
|
66
|
-
clearActiveCodemodeIntervals: vi.fn(),
|
|
67
|
-
}));
|
|
68
|
-
|
|
60
|
+
// The renderers stay real: the resolver hands them straight to pi, so the
|
|
61
|
+
// tests can assert on their identity.
|
|
69
62
|
import defaultExport, {
|
|
70
63
|
registerUI,
|
|
71
|
-
unpatchConsoleLog,
|
|
72
64
|
getUISettingsItems,
|
|
73
65
|
loadUIConfig,
|
|
74
66
|
saveUIConfig,
|
|
75
67
|
DEFAULT_UI_CONFIG,
|
|
76
68
|
ANIMATION_STYLES,
|
|
77
69
|
} from "./index.js";
|
|
78
|
-
import { patchConsoleLog } from "./startup/capture.js";
|
|
79
70
|
import { migrateCoreToUIConfig } from "./migration.js";
|
|
80
|
-
import {
|
|
81
|
-
import {
|
|
71
|
+
import { renderBashCall, renderBashResult } from "./bash/index.js";
|
|
72
|
+
import { renderCodemodeCall, renderCodemodeResult } from "./codemode/index.js";
|
|
82
73
|
import { patchThinkingRenderer } from "./thinking/patch.js";
|
|
83
74
|
import { transformThinkingContent } from "./thinking/transform.js";
|
|
84
|
-
import {
|
|
75
|
+
import {
|
|
76
|
+
renderHeader,
|
|
77
|
+
patchStartupListing,
|
|
78
|
+
revealStartupListing,
|
|
79
|
+
stopStartupListing,
|
|
80
|
+
} from "./startup/index.js";
|
|
85
81
|
|
|
86
82
|
// ── Test helpers ─────────────────────────────────────────────────────────────
|
|
87
83
|
|
|
@@ -89,6 +85,9 @@ interface UiSpies {
|
|
|
89
85
|
setHeader: ReturnType<typeof vi.fn>;
|
|
90
86
|
setEditorComponent: ReturnType<typeof vi.fn>;
|
|
91
87
|
setWorkingVisible: ReturnType<typeof vi.fn>;
|
|
88
|
+
setWorkingIndicator: ReturnType<typeof vi.fn>;
|
|
89
|
+
notify: ReturnType<typeof vi.fn>;
|
|
90
|
+
setHiddenThinkingLabel: ReturnType<typeof vi.fn>;
|
|
92
91
|
}
|
|
93
92
|
|
|
94
93
|
function makeCtx(hasUI = true): { ctx: ExtensionContext; ui: UiSpies } {
|
|
@@ -96,6 +95,9 @@ function makeCtx(hasUI = true): { ctx: ExtensionContext; ui: UiSpies } {
|
|
|
96
95
|
setHeader: vi.fn(),
|
|
97
96
|
setEditorComponent: vi.fn(),
|
|
98
97
|
setWorkingVisible: vi.fn(),
|
|
98
|
+
setWorkingIndicator: vi.fn(),
|
|
99
|
+
notify: vi.fn(),
|
|
100
|
+
setHiddenThinkingLabel: vi.fn(),
|
|
99
101
|
};
|
|
100
102
|
const ctx = {
|
|
101
103
|
hasUI,
|
|
@@ -103,29 +105,67 @@ function makeCtx(hasUI = true): { ctx: ExtensionContext; ui: UiSpies } {
|
|
|
103
105
|
ui: { ...ui, theme: {} as Theme },
|
|
104
106
|
isIdle: vi.fn(() => true),
|
|
105
107
|
shutdown: vi.fn(),
|
|
108
|
+
scopedModels: [],
|
|
106
109
|
} as unknown as ExtensionContext;
|
|
107
110
|
return { ctx, ui };
|
|
108
111
|
}
|
|
109
112
|
|
|
110
|
-
|
|
113
|
+
/** Stand-in for what `next()` hands back: pi's own renderers for the tool. */
|
|
114
|
+
const NATIVE = Object.freeze({ native: true });
|
|
115
|
+
|
|
116
|
+
/** What a resolver resolves for a tool name, and whether it called `next()`. */
|
|
117
|
+
function resolve(
|
|
118
|
+
resolver: ((name: string, next: () => any) => any) | undefined,
|
|
119
|
+
toolName: string,
|
|
120
|
+
): { result: unknown; nextCalls: number } {
|
|
121
|
+
let nextCalls = 0;
|
|
122
|
+
const result = resolver?.(toolName, () => {
|
|
123
|
+
nextCalls++;
|
|
124
|
+
return NATIVE;
|
|
125
|
+
});
|
|
126
|
+
return { result, nextCalls };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** A `pi` handle whose `getSettings()` answers what pi would apply. */
|
|
130
|
+
function makePi(settings: Record<string, unknown> = {}): {
|
|
111
131
|
pi: ExtensionAPI;
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
triggerMessageEnd: (msg: unknown) => void;
|
|
132
|
+
listeners: Record<string, ((event: any, ctx: any) => void)[]>;
|
|
133
|
+
renderers: ((name: string, next: () => any) => any)[];
|
|
115
134
|
} {
|
|
116
135
|
const listeners: Record<string, ((event: any, ctx: any) => void)[]> = {};
|
|
136
|
+
const renderers: ((name: string, next: () => any) => any)[] = [];
|
|
117
137
|
const pi = {
|
|
118
138
|
on: vi.fn((event: string, handler: (event: any, ctx: any) => void) => {
|
|
119
139
|
listeners[event] = listeners[event] ?? [];
|
|
120
140
|
listeners[event]!.push(handler);
|
|
121
141
|
}),
|
|
122
142
|
registerTool: vi.fn(),
|
|
143
|
+
registerToolRenderer: vi.fn(
|
|
144
|
+
(resolver: (name: string, next: () => any) => any) => {
|
|
145
|
+
renderers.push(resolver);
|
|
146
|
+
},
|
|
147
|
+
),
|
|
148
|
+
getSettings: vi.fn(() => settings),
|
|
123
149
|
} as unknown as ExtensionAPI;
|
|
150
|
+
return { pi, listeners, renderers };
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function setupHarness(
|
|
154
|
+
settings: Record<string, unknown> = {},
|
|
155
|
+
): {
|
|
156
|
+
pi: ExtensionAPI;
|
|
157
|
+
renderers: ((name: string, next: () => any) => any)[];
|
|
158
|
+
triggerStart: (ctx: ExtensionContext) => void;
|
|
159
|
+
triggerShutdown: (ctx: ExtensionContext) => void;
|
|
160
|
+
triggerMessageEnd: (msg: unknown) => void;
|
|
161
|
+
} {
|
|
162
|
+
const { pi, listeners, renderers } = makePi(settings);
|
|
124
163
|
|
|
125
164
|
registerUI(pi);
|
|
126
165
|
|
|
127
166
|
return {
|
|
128
167
|
pi,
|
|
168
|
+
renderers,
|
|
129
169
|
triggerStart: (ctx: ExtensionContext) => {
|
|
130
170
|
for (const h of listeners["session_start"] ?? []) {
|
|
131
171
|
h(null, ctx);
|
|
@@ -147,7 +187,11 @@ function setupHarness(): {
|
|
|
147
187
|
describe("packages/ui lifecycle and registration", () => {
|
|
148
188
|
beforeEach(() => {
|
|
149
189
|
vi.clearAllMocks();
|
|
150
|
-
|
|
190
|
+
// The splash is opt-in (`startupSplash` defaults to off), and this file is
|
|
191
|
+
// where the splash's installation and teardown are asserted — so the
|
|
192
|
+
// harness asks for it. The default-off path, and the whole
|
|
193
|
+
// `startupSplash` × `quietStartup` table, is `startup-splash-gate.test.ts`.
|
|
194
|
+
vi.mocked(loadUIConfig).mockReturnValue({ ...DEFAULT_UI_CONFIG, startupSplash: true });
|
|
151
195
|
});
|
|
152
196
|
|
|
153
197
|
afterEach(() => {
|
|
@@ -156,7 +200,6 @@ describe("packages/ui lifecycle and registration", () => {
|
|
|
156
200
|
|
|
157
201
|
describe("exports", () => {
|
|
158
202
|
it("re-exports expected functions and constants", () => {
|
|
159
|
-
expect(typeof unpatchConsoleLog).toBe("function");
|
|
160
203
|
expect(typeof getUISettingsItems).toBe("function");
|
|
161
204
|
expect(typeof loadUIConfig).toBe("function");
|
|
162
205
|
expect(typeof saveUIConfig).toBe("function");
|
|
@@ -166,35 +209,146 @@ describe("packages/ui lifecycle and registration", () => {
|
|
|
166
209
|
});
|
|
167
210
|
|
|
168
211
|
it("default export delegates to registerUI", () => {
|
|
169
|
-
const pi = { on: vi.fn() } as unknown as ExtensionAPI;
|
|
212
|
+
const pi = { on: vi.fn(), getSettings: vi.fn(() => ({})) } as unknown as ExtensionAPI;
|
|
170
213
|
defaultExport(pi);
|
|
171
214
|
expect(migrateCoreToUIConfig).toHaveBeenCalled();
|
|
172
|
-
expect(patchConsoleLog).toHaveBeenCalled();
|
|
173
215
|
});
|
|
174
216
|
});
|
|
175
217
|
|
|
176
218
|
describe("registerUI initialization", () => {
|
|
177
|
-
it("calls migrateCoreToUIConfig and
|
|
219
|
+
it("calls migrateCoreToUIConfig and leaves the console alone", () => {
|
|
178
220
|
const { pi } = setupHarness();
|
|
179
221
|
expect(migrateCoreToUIConfig).toHaveBeenCalledTimes(1);
|
|
180
|
-
|
|
222
|
+
// Nobody may wrap console.log to watch pi's startup output: the scrape is
|
|
223
|
+
// gone, and a wrapped log both smears the frame and hides pi's own line.
|
|
224
|
+
expect(console.log.name).toBe("log");
|
|
181
225
|
expect(pi.on).toHaveBeenCalledWith("session_shutdown", expect.any(Function));
|
|
182
226
|
expect(pi.on).toHaveBeenCalledWith("session_start", expect.any(Function));
|
|
183
227
|
});
|
|
184
228
|
});
|
|
185
229
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
230
|
+
// ADR 0029: presentation is resolved through `pi.registerToolRenderer()`,
|
|
231
|
+
// so the styling settings choose between our row and pi's native row inside
|
|
232
|
+
// one resolver, instead of re-registering the tool.
|
|
233
|
+
describe("tool renderer resolution", () => {
|
|
234
|
+
it("resolves bash to the Archimedes renderers when styling is on", () => {
|
|
235
|
+
const { renderers, triggerStart } = setupHarness();
|
|
236
|
+
const { ctx } = makeCtx(true);
|
|
190
237
|
|
|
191
238
|
triggerStart(ctx);
|
|
192
239
|
|
|
193
|
-
|
|
194
|
-
|
|
240
|
+
expect(renderers.length).toBe(1);
|
|
241
|
+
const { result, nextCalls } = resolve(renderers[0], "bash");
|
|
242
|
+
expect(result).toMatchObject({
|
|
243
|
+
renderCall: renderBashCall,
|
|
244
|
+
renderResult: renderBashResult,
|
|
245
|
+
});
|
|
246
|
+
// `next()` is consulted and spread first, so a key the built-in
|
|
247
|
+
// definition declares besides our two — `renderShell` is the one that
|
|
248
|
+
// matters today — survives resolution instead of being dropped.
|
|
249
|
+
expect(nextCalls).toBe(1);
|
|
250
|
+
expect(result).toHaveProperty("native", true);
|
|
251
|
+
});
|
|
195
252
|
|
|
196
|
-
|
|
197
|
-
|
|
253
|
+
it("gives back pi's native bash row when bashToolStyling is off", () => {
|
|
254
|
+
vi.mocked(loadUIConfig).mockReturnValue({
|
|
255
|
+
...DEFAULT_UI_CONFIG,
|
|
256
|
+
bashToolStyling: false,
|
|
257
|
+
});
|
|
258
|
+
const { renderers, triggerStart } = setupHarness();
|
|
259
|
+
const { ctx } = makeCtx(true);
|
|
260
|
+
|
|
261
|
+
triggerStart(ctx);
|
|
262
|
+
|
|
263
|
+
const { result, nextCalls } = resolve(renderers[0], "bash");
|
|
264
|
+
expect(result).toBe(NATIVE);
|
|
265
|
+
expect(nextCalls).toBe(1);
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
it("gives back pi's native bash row when toolStyle is Native", () => {
|
|
269
|
+
vi.mocked(loadUIConfig).mockReturnValue({
|
|
270
|
+
...DEFAULT_UI_CONFIG,
|
|
271
|
+
toolStyle: "Native",
|
|
272
|
+
});
|
|
273
|
+
const { renderers, triggerStart } = setupHarness();
|
|
274
|
+
const { ctx } = makeCtx(true);
|
|
275
|
+
|
|
276
|
+
triggerStart(ctx);
|
|
277
|
+
|
|
278
|
+
expect(resolve(renderers[0], "bash")).toEqual({ result: NATIVE, nextCalls: 1 });
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
it("follows a settings change made between sessions", () => {
|
|
282
|
+
const { renderers, triggerStart } = setupHarness();
|
|
283
|
+
const { ctx } = makeCtx(true);
|
|
284
|
+
|
|
285
|
+
triggerStart(ctx);
|
|
286
|
+
expect(resolve(renderers[0], "bash").result).toMatchObject({ renderCall: renderBashCall });
|
|
287
|
+
|
|
288
|
+
// The user turns bash styling off in `/archimedes` and starts a session.
|
|
289
|
+
vi.mocked(loadUIConfig).mockReturnValue({
|
|
290
|
+
...DEFAULT_UI_CONFIG,
|
|
291
|
+
bashToolStyling: false,
|
|
292
|
+
});
|
|
293
|
+
triggerStart(ctx);
|
|
294
|
+
|
|
295
|
+
expect(resolve(renderers[0], "bash")).toEqual({ result: NATIVE, nextCalls: 1 });
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
it("leaves every other tool to the remaining resolvers", () => {
|
|
299
|
+
const { renderers, triggerStart } = setupHarness();
|
|
300
|
+
const { ctx } = makeCtx(true);
|
|
301
|
+
|
|
302
|
+
triggerStart(ctx);
|
|
303
|
+
|
|
304
|
+
for (const name of ["read", "edit", "write", "grep", "todo"]) {
|
|
305
|
+
expect(resolve(renderers[0], name)).toEqual({ result: NATIVE, nextCalls: 1 });
|
|
306
|
+
}
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
it("resolves codemode to the Archimedes renderers when styling is on", () => {
|
|
310
|
+
const { renderers, triggerStart } = setupHarness();
|
|
311
|
+
const { ctx } = makeCtx(true);
|
|
312
|
+
|
|
313
|
+
triggerStart(ctx);
|
|
314
|
+
|
|
315
|
+
expect(resolve(renderers[0], "codemode").result).toMatchObject({
|
|
316
|
+
renderCall: renderCodemodeCall,
|
|
317
|
+
renderResult: renderCodemodeResult,
|
|
318
|
+
native: true,
|
|
319
|
+
});
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
it("gives back pi's own codemode row when codemodeToolStyling is off", () => {
|
|
323
|
+
vi.mocked(loadUIConfig).mockReturnValue({
|
|
324
|
+
...DEFAULT_UI_CONFIG,
|
|
325
|
+
codemodeToolStyling: false,
|
|
326
|
+
});
|
|
327
|
+
const { renderers, triggerStart } = setupHarness();
|
|
328
|
+
const { ctx } = makeCtx(true);
|
|
329
|
+
|
|
330
|
+
triggerStart(ctx);
|
|
331
|
+
|
|
332
|
+
expect(resolve(renderers[0], "codemode")).toEqual({ result: NATIVE, nextCalls: 1 });
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
it("registers the resolver once even across repeated session_start (/reload)", () => {
|
|
336
|
+
const { renderers, triggerStart } = setupHarness();
|
|
337
|
+
const { ctx } = makeCtx(true);
|
|
338
|
+
|
|
339
|
+
triggerStart(ctx);
|
|
340
|
+
triggerStart(ctx);
|
|
341
|
+
|
|
342
|
+
expect(renderers.length).toBe(1);
|
|
343
|
+
});
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
describe("session_start with interactive UI (hasUI: true)", () => {
|
|
347
|
+
it("configures the header, editor and thinking renderer", () => {
|
|
348
|
+
const { renderers, triggerStart } = setupHarness();
|
|
349
|
+
const { ctx, ui } = makeCtx(true);
|
|
350
|
+
|
|
351
|
+
triggerStart(ctx);
|
|
198
352
|
|
|
199
353
|
// Header installation
|
|
200
354
|
expect(ui.setHeader).toHaveBeenCalledTimes(1);
|
|
@@ -204,11 +358,16 @@ describe("packages/ui lifecycle and registration", () => {
|
|
|
204
358
|
const comp = headerFactory(fakeTui, fakeTheme);
|
|
205
359
|
expect(typeof comp.render).toBe("function");
|
|
206
360
|
comp.render(80);
|
|
207
|
-
expect(renderHeader).toHaveBeenCalledWith(fakeTheme, expect.any(Object), 80
|
|
208
|
-
expect(patchStartupListing).toHaveBeenCalledWith(fakeTui,
|
|
209
|
-
|
|
210
|
-
// Working
|
|
211
|
-
|
|
361
|
+
expect(renderHeader).toHaveBeenCalledWith(fakeTheme, expect.any(Object), 80);
|
|
362
|
+
expect(patchStartupListing).toHaveBeenCalledWith(fakeTui, expect.any(Object));
|
|
363
|
+
|
|
364
|
+
// Working indicator and Editor component. pi's working row ("Working",
|
|
365
|
+
// plus its own animated indicator) and our border spinner say the same
|
|
366
|
+
// thing — `editorSpinLabel` defaults to "Working" too — so with the spinner
|
|
367
|
+
// on the row is hidden through pi's own API rather than left showing an
|
|
368
|
+
// indicator-less label.
|
|
369
|
+
expect(ui.setWorkingVisible).toHaveBeenCalledWith(false);
|
|
370
|
+
expect(ui.setWorkingIndicator).toHaveBeenCalledWith(undefined);
|
|
212
371
|
expect(ui.setEditorComponent).toHaveBeenCalledTimes(1);
|
|
213
372
|
const editorFactory = ui.setEditorComponent.mock.calls[0]![0];
|
|
214
373
|
const editorInstance = editorFactory(fakeTui, { borderColor: (s: string) => s } as EditorTheme, {} as KeybindingsManager);
|
|
@@ -224,33 +383,41 @@ describe("packages/ui lifecycle and registration", () => {
|
|
|
224
383
|
autoCollapseThinking: false,
|
|
225
384
|
thinkingStyle: "Full",
|
|
226
385
|
}),
|
|
386
|
+
// The user-facing surface is injected: `patch.ts` has no ctx of its
|
|
387
|
+
// own, and a skipped patch must not be a silent feature loss.
|
|
388
|
+
expect.any(Function),
|
|
227
389
|
);
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
expect(
|
|
231
|
-
|
|
390
|
+
const patchNotify = vi.mocked(patchThinkingRenderer).mock.calls[0]![2] as (m: string) => void;
|
|
391
|
+
patchNotify("patch skipped");
|
|
392
|
+
expect(ui.notify).toHaveBeenCalledWith("patch skipped", "warning");
|
|
393
|
+
|
|
394
|
+
// Master gate (default toolStyle is Minimal): both rows are ours.
|
|
395
|
+
expect(resolve(renderers[0], "bash").result).toMatchObject({ renderCall: renderBashCall });
|
|
396
|
+
expect(resolve(renderers[0], "codemode").result).toMatchObject({
|
|
397
|
+
renderCall: renderCodemodeCall,
|
|
398
|
+
});
|
|
232
399
|
});
|
|
233
400
|
|
|
234
|
-
it("
|
|
401
|
+
it("yields pi's native rows for every tool when toolStyle is Native", () => {
|
|
235
402
|
vi.mocked(loadUIConfig).mockReturnValue({
|
|
236
403
|
...DEFAULT_UI_CONFIG,
|
|
237
404
|
toolStyle: "Native",
|
|
238
405
|
});
|
|
239
406
|
|
|
240
|
-
const { triggerStart } = setupHarness();
|
|
407
|
+
const { renderers, triggerStart } = setupHarness();
|
|
241
408
|
const { ctx, ui } = makeCtx(true);
|
|
242
409
|
|
|
243
410
|
triggerStart(ctx);
|
|
244
411
|
|
|
245
|
-
// Native = no archimedes tool styling:
|
|
246
|
-
// (pi's
|
|
247
|
-
expect(
|
|
248
|
-
expect(
|
|
412
|
+
// Native = no archimedes tool styling: both rows go back through
|
|
413
|
+
// `next()`, so pi's own rendering stands and nothing is taken over.
|
|
414
|
+
expect(resolve(renderers[0], "bash")).toEqual({ result: NATIVE, nextCalls: 1 });
|
|
415
|
+
expect(resolve(renderers[0], "codemode")).toEqual({ result: NATIVE, nextCalls: 1 });
|
|
249
416
|
// The rest of the UI still comes up.
|
|
250
417
|
expect(ui.setHeader).toHaveBeenCalledTimes(1);
|
|
251
418
|
});
|
|
252
419
|
|
|
253
|
-
it("respects editorSpinBorder: false by leaving
|
|
420
|
+
it("respects editorSpinBorder: false by leaving pi's indicator running and installing no editor component", () => {
|
|
254
421
|
vi.mocked(loadUIConfig).mockReturnValue({
|
|
255
422
|
...DEFAULT_UI_CONFIG,
|
|
256
423
|
editorSpinBorder: false,
|
|
@@ -261,22 +428,144 @@ describe("packages/ui lifecycle and registration", () => {
|
|
|
261
428
|
|
|
262
429
|
triggerStart(ctx);
|
|
263
430
|
|
|
431
|
+
// No border spinner, so pi's own working row comes back, looking exactly
|
|
432
|
+
// like pi's default: visible, with its own animated indicator (omitting the
|
|
433
|
+
// argument is how pi restores the default frames).
|
|
264
434
|
expect(ui.setWorkingVisible).toHaveBeenCalledWith(true);
|
|
435
|
+
expect(ui.setWorkingIndicator).toHaveBeenCalledWith(undefined);
|
|
265
436
|
expect(ui.setEditorComponent).not.toHaveBeenCalled();
|
|
266
437
|
});
|
|
267
438
|
|
|
268
|
-
it("
|
|
439
|
+
it("puts the configured thinking label on hidden blocks through pi", () => {
|
|
269
440
|
vi.mocked(loadUIConfig).mockReturnValue({
|
|
270
441
|
...DEFAULT_UI_CONFIG,
|
|
271
|
-
|
|
442
|
+
labelText: "Pondering...",
|
|
272
443
|
});
|
|
273
444
|
|
|
274
445
|
const { triggerStart } = setupHarness();
|
|
446
|
+
const { ctx, ui } = makeCtx(true);
|
|
447
|
+
|
|
448
|
+
triggerStart(ctx);
|
|
449
|
+
|
|
450
|
+
// The hidden row is pi's own (`AssistantMessageComponent` reads the label
|
|
451
|
+
// pi was given), so the setting is handed to pi rather than rebuilt here.
|
|
452
|
+
expect(ui.setHiddenThinkingLabel).toHaveBeenCalledWith("Pondering...");
|
|
453
|
+
});
|
|
454
|
+
|
|
455
|
+
it("hands the hidden-thinking label back to pi when the setting is empty", () => {
|
|
456
|
+
vi.mocked(loadUIConfig).mockReturnValue({
|
|
457
|
+
...DEFAULT_UI_CONFIG,
|
|
458
|
+
labelText: " ",
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
const { triggerStart } = setupHarness();
|
|
462
|
+
const { ctx, ui } = makeCtx(true);
|
|
463
|
+
|
|
464
|
+
triggerStart(ctx);
|
|
465
|
+
|
|
466
|
+
expect(ui.setHiddenThinkingLabel).toHaveBeenCalledWith(undefined);
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
it("gives the working row and the thinking label back to pi on shutdown", () => {
|
|
470
|
+
const { triggerStart, triggerShutdown } = setupHarness();
|
|
471
|
+
const { ctx, ui } = makeCtx(true);
|
|
472
|
+
|
|
473
|
+
triggerStart(ctx);
|
|
474
|
+
triggerShutdown(ctx);
|
|
475
|
+
|
|
476
|
+
expect(ui.setWorkingIndicator).toHaveBeenLastCalledWith(undefined);
|
|
477
|
+
expect(ui.setHiddenThinkingLabel).toHaveBeenLastCalledWith(undefined);
|
|
478
|
+
expect(ui.setWorkingVisible).toHaveBeenLastCalledWith(true);
|
|
479
|
+
});
|
|
480
|
+
|
|
481
|
+
// The working row is pi's "Working" line above the editor; our border spinner
|
|
482
|
+
// lives in the editor's own top border and its default label is "Working" as
|
|
483
|
+
// well. With both on screen the word appeared twice, which is what the
|
|
484
|
+
// maintainer's screenshot shows. The setting that decides is
|
|
485
|
+
// `editorSpinBorder`, and the row is handed back and taken through pi's own
|
|
486
|
+
// `setWorkingVisible()` — `types.d.ts:88`.
|
|
487
|
+
describe("pi's working row and the border spinner", () => {
|
|
488
|
+
it("hides pi's working row while the border spinner carries the announcement", () => {
|
|
489
|
+
vi.mocked(loadUIConfig).mockReturnValue({ ...DEFAULT_UI_CONFIG, editorSpinBorder: true });
|
|
490
|
+
const { triggerStart } = setupHarness();
|
|
491
|
+
const { ctx, ui } = makeCtx(true);
|
|
492
|
+
|
|
493
|
+
triggerStart(ctx);
|
|
494
|
+
|
|
495
|
+
expect(ui.setWorkingVisible).toHaveBeenLastCalledWith(false);
|
|
496
|
+
// No indicator configuration is needed once the row is hidden — and
|
|
497
|
+
// nothing here asks pi for `frames: []` any more, so the row that *is*
|
|
498
|
+
// shown (spinner off) is pi's default down to its spinner.
|
|
499
|
+
expect(ui.setWorkingIndicator).toHaveBeenCalledWith(undefined);
|
|
500
|
+
expect(ui.setWorkingIndicator).not.toHaveBeenCalledWith({ frames: [] });
|
|
501
|
+
});
|
|
502
|
+
|
|
503
|
+
it("shows pi's working row, with pi's own indicator, when the spinner is off", () => {
|
|
504
|
+
vi.mocked(loadUIConfig).mockReturnValue({ ...DEFAULT_UI_CONFIG, editorSpinBorder: false });
|
|
505
|
+
const { triggerStart } = setupHarness();
|
|
506
|
+
const { ctx, ui } = makeCtx(true);
|
|
507
|
+
|
|
508
|
+
triggerStart(ctx);
|
|
509
|
+
|
|
510
|
+
expect(ui.setWorkingVisible).toHaveBeenLastCalledWith(true);
|
|
511
|
+
expect(ui.setWorkingIndicator).toHaveBeenCalledWith(undefined);
|
|
512
|
+
});
|
|
513
|
+
|
|
514
|
+
it("re-reads the setting for every session, so a replaced session gets the row back", () => {
|
|
515
|
+
// pi resets the extension UI on session replacement (`resetExtensionUI()`,
|
|
516
|
+
// interactive-mode.js:1921 sets `workingVisible = true` again), and the
|
|
517
|
+
// setting can change between sessions, so the answer has to be read from
|
|
518
|
+
// the config each time rather than remembered from the first install.
|
|
519
|
+
vi.mocked(loadUIConfig).mockReturnValue({ ...DEFAULT_UI_CONFIG, editorSpinBorder: true });
|
|
520
|
+
const { triggerStart } = setupHarness();
|
|
521
|
+
const spinnerOn = makeCtx(true);
|
|
522
|
+
triggerStart(spinnerOn.ctx);
|
|
523
|
+
|
|
524
|
+
vi.mocked(loadUIConfig).mockReturnValue({ ...DEFAULT_UI_CONFIG, editorSpinBorder: false });
|
|
525
|
+
const spinnerOff = makeCtx(true);
|
|
526
|
+
triggerStart(spinnerOff.ctx);
|
|
527
|
+
|
|
528
|
+
// Two sessions, two answers: the row went away and came back.
|
|
529
|
+
const calls = spinnerOn.ui.setWorkingVisible.mock.calls.map((c) => c[0]);
|
|
530
|
+
expect(calls).toEqual([false]);
|
|
531
|
+
expect(spinnerOff.ui.setWorkingVisible.mock.calls.map((c) => c[0])).toEqual([true]);
|
|
532
|
+
});
|
|
533
|
+
|
|
534
|
+
it("hides the row for a session installed on a second pi handle", () => {
|
|
535
|
+
// pi caches an extension's factory and only clears that cache on some
|
|
536
|
+
// reloads: `/new`, `/fork` and `/resume` build a fresh loader and run the
|
|
537
|
+
// same module instance against a NEW handle. Nothing the first handle did
|
|
538
|
+
// may suppress the second one's answer (#106 learned this the hard way).
|
|
539
|
+
vi.mocked(loadUIConfig).mockReturnValue({ ...DEFAULT_UI_CONFIG, editorSpinBorder: true });
|
|
540
|
+
const first = setupHarness();
|
|
541
|
+
const firstCtx = makeCtx(true);
|
|
542
|
+
first.triggerStart(firstCtx.ctx);
|
|
543
|
+
|
|
544
|
+
const second = setupHarness();
|
|
545
|
+
const secondCtx = makeCtx(true);
|
|
546
|
+
second.triggerStart(secondCtx.ctx);
|
|
547
|
+
|
|
548
|
+
expect(firstCtx.ui.setWorkingVisible).toHaveBeenLastCalledWith(false);
|
|
549
|
+
expect(secondCtx.ui.setWorkingVisible).toHaveBeenLastCalledWith(false);
|
|
550
|
+
});
|
|
551
|
+
});
|
|
552
|
+
|
|
553
|
+
it("registers no bash tool and yields pi's native row when bashToolStyling is false", () => {
|
|
554
|
+
vi.mocked(loadUIConfig).mockReturnValue({
|
|
555
|
+
...DEFAULT_UI_CONFIG,
|
|
556
|
+
bashToolStyling: false,
|
|
557
|
+
});
|
|
558
|
+
|
|
559
|
+
const { pi, renderers, triggerStart } = setupHarness();
|
|
275
560
|
const { ctx } = makeCtx(true);
|
|
276
561
|
|
|
277
562
|
triggerStart(ctx);
|
|
278
563
|
|
|
279
|
-
|
|
564
|
+
// The take-over is gone: nothing re-registers `bash`, and the resolver
|
|
565
|
+
// hands the row back through `next()` — the first time this setting can
|
|
566
|
+
// genuinely give back pi's own rendering.
|
|
567
|
+
expect(pi.registerTool).not.toHaveBeenCalled();
|
|
568
|
+
expect(resolve(renderers[0], "bash")).toEqual({ result: NATIVE, nextCalls: 1 });
|
|
280
569
|
});
|
|
281
570
|
|
|
282
571
|
it("skips the codemode tool override when codemodeToolStyling is false", () => {
|
|
@@ -285,12 +574,15 @@ describe("packages/ui lifecycle and registration", () => {
|
|
|
285
574
|
codemodeToolStyling: false,
|
|
286
575
|
});
|
|
287
576
|
|
|
288
|
-
const { triggerStart } = setupHarness();
|
|
577
|
+
const { pi, renderers, triggerStart } = setupHarness();
|
|
289
578
|
const { ctx } = makeCtx(true);
|
|
290
579
|
|
|
291
580
|
triggerStart(ctx);
|
|
292
581
|
|
|
293
|
-
|
|
582
|
+
// Nothing re-registers `codemode`, so pi's built-in extension stays
|
|
583
|
+
// loaded; the resolver hands the row to pi's own renderer.
|
|
584
|
+
expect(pi.registerTool).not.toHaveBeenCalled();
|
|
585
|
+
expect(resolve(renderers[0], "codemode")).toEqual({ result: NATIVE, nextCalls: 1 });
|
|
294
586
|
});
|
|
295
587
|
|
|
296
588
|
it("does not accumulate message_end listeners on repeated session_start (simulating /reload)", () => {
|
|
@@ -330,73 +622,186 @@ describe("packages/ui lifecycle and registration", () => {
|
|
|
330
622
|
});
|
|
331
623
|
|
|
332
624
|
describe("session_start in non-interactive mode (hasUI: false)", () => {
|
|
333
|
-
it("
|
|
334
|
-
const { triggerStart } = setupHarness();
|
|
625
|
+
it("registers no tool and skips UI setup", () => {
|
|
626
|
+
const { pi, triggerStart } = setupHarness();
|
|
335
627
|
const { ctx, ui } = makeCtx(false);
|
|
336
628
|
|
|
337
629
|
triggerStart(ctx);
|
|
338
630
|
|
|
339
|
-
expect(
|
|
631
|
+
expect(pi.registerTool).not.toHaveBeenCalled();
|
|
340
632
|
expect(ui.setHeader).not.toHaveBeenCalled();
|
|
341
633
|
expect(ui.setEditorComponent).not.toHaveBeenCalled();
|
|
342
634
|
expect(patchThinkingRenderer).not.toHaveBeenCalled();
|
|
343
635
|
});
|
|
344
636
|
});
|
|
345
637
|
|
|
638
|
+
describe("startup header: the splash is opt-in (default off)", () => {
|
|
639
|
+
// `startup-splash-gate.test.ts` drives the real animation module; this uses
|
|
640
|
+
// the stubbed one so the claim "nothing of the splash runs" is the absence
|
|
641
|
+
// of calls into it, which is the direct form of "no logo work":
|
|
642
|
+
// `renderHeader` is what asks `logo.ts` for a frame, and the two listing
|
|
643
|
+
// calls are what arm the animation.
|
|
644
|
+
it("hands the header to pi and touches no part of the splash module", () => {
|
|
645
|
+
// The default config, unmodified: this is what a settings file with no
|
|
646
|
+
// `startupSplash` key resolves to.
|
|
647
|
+
vi.mocked(loadUIConfig).mockReturnValue({ ...DEFAULT_UI_CONFIG });
|
|
648
|
+
const { triggerStart, triggerShutdown } = setupHarness({});
|
|
649
|
+
const { ctx, ui } = makeCtx(true);
|
|
650
|
+
|
|
651
|
+
triggerStart(ctx);
|
|
652
|
+
|
|
653
|
+
expect(ui.setHeader).toHaveBeenCalledTimes(1);
|
|
654
|
+
expect(ui.setHeader).toHaveBeenCalledWith(undefined);
|
|
655
|
+
expect(renderHeader).not.toHaveBeenCalled();
|
|
656
|
+
expect(patchStartupListing).not.toHaveBeenCalled();
|
|
657
|
+
expect(revealStartupListing).not.toHaveBeenCalled();
|
|
658
|
+
|
|
659
|
+
// Teardown still runs its one unconditional stop — cheap and idempotent —
|
|
660
|
+
// while the ref freeze is guarded on there being a ref, so an off session
|
|
661
|
+
// cannot resurrect an animation that was never built.
|
|
662
|
+
triggerShutdown(ctx);
|
|
663
|
+
expect(stopStartupListing).toHaveBeenCalledTimes(1);
|
|
664
|
+
expect(renderHeader).not.toHaveBeenCalled();
|
|
665
|
+
});
|
|
666
|
+
});
|
|
667
|
+
|
|
668
|
+
describe("startup header: quietStartup and ctx.scopedModels", () => {
|
|
669
|
+
// pi 1.x owns the startup screen: `quietStartup` (`false` / `"header"` /
|
|
670
|
+
// `true`) decides what is shown, and `ctx.scopedModels` is the session's
|
|
671
|
+
// model scope — the same set `/scoped-models` lists. Both are read, neither
|
|
672
|
+
// is scraped any more.
|
|
673
|
+
const scopedModels = [
|
|
674
|
+
{ model: { id: "claude-sonnet-4-5" } },
|
|
675
|
+
{ model: { id: "gpt-5" }, thinkingLevel: "high" },
|
|
676
|
+
];
|
|
677
|
+
let fetchSpy: ReturnType<typeof vi.fn>;
|
|
678
|
+
|
|
679
|
+
beforeEach(() => {
|
|
680
|
+
fetchSpy = vi.fn(async () => ({ ok: true, json: async () => ({}) }) as never);
|
|
681
|
+
vi.stubGlobal("fetch", fetchSpy);
|
|
682
|
+
});
|
|
683
|
+
|
|
684
|
+
afterEach(() => {
|
|
685
|
+
vi.unstubAllGlobals();
|
|
686
|
+
});
|
|
687
|
+
|
|
688
|
+
/** Register, start a session and invoke the header factory pi was given. */
|
|
689
|
+
function startHeader(settings: Record<string, unknown>, models: unknown[] = scopedModels) {
|
|
690
|
+
const { triggerStart } = setupHarness(settings);
|
|
691
|
+
const { ctx, ui } = makeCtx(true);
|
|
692
|
+
(ctx as unknown as { scopedModels: unknown[] }).scopedModels = models;
|
|
693
|
+
|
|
694
|
+
triggerStart(ctx);
|
|
695
|
+
const headerFactory = ui.setHeader.mock.calls[0]![0];
|
|
696
|
+
const tui = { terminal: { rows: 24 }, requestRender: vi.fn() } as unknown as TUI;
|
|
697
|
+
headerFactory(tui, {} as Theme);
|
|
698
|
+
return { ui, tui };
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
it("reveals the model scope from ctx.scopedModels with console.log untouched", () => {
|
|
702
|
+
const { tui } = startHeader({});
|
|
703
|
+
|
|
704
|
+
expect(revealStartupListing).toHaveBeenCalledTimes(1);
|
|
705
|
+
expect(revealStartupListing).toHaveBeenCalledWith(tui, expect.any(Object), [
|
|
706
|
+
{ name: "Models", items: ["claude-sonnet-4-5", "gpt-5:high"] },
|
|
707
|
+
]);
|
|
708
|
+
expect(console.log.name).toBe("log");
|
|
709
|
+
});
|
|
710
|
+
|
|
711
|
+
it('quietStartup: "header" keeps the splash and drops the model scope', () => {
|
|
712
|
+
startHeader({ quietStartup: "header" });
|
|
713
|
+
|
|
714
|
+
// The header is ours (the splash stays), but the details half is not.
|
|
715
|
+
expect(revealStartupListing).toHaveBeenCalledTimes(1);
|
|
716
|
+
expect(revealStartupListing).toHaveBeenCalledWith(expect.anything(), expect.any(Object), []);
|
|
717
|
+
});
|
|
718
|
+
|
|
719
|
+
it("quietStartup: true hands the header back to pi and starts no splash", () => {
|
|
720
|
+
const { triggerStart } = setupHarness({ quietStartup: true });
|
|
721
|
+
const { ctx, ui } = makeCtx(true);
|
|
722
|
+
|
|
723
|
+
triggerStart(ctx);
|
|
724
|
+
|
|
725
|
+
// pi replaces its own header with an empty component for a quiet start;
|
|
726
|
+
// restoring ours is what leaves that silence in place.
|
|
727
|
+
expect(ui.setHeader).toHaveBeenCalledWith(undefined);
|
|
728
|
+
expect(patchStartupListing).not.toHaveBeenCalled();
|
|
729
|
+
expect(revealStartupListing).not.toHaveBeenCalled();
|
|
730
|
+
});
|
|
731
|
+
|
|
732
|
+
it("reveals nothing but the version block when the session has no model scope", () => {
|
|
733
|
+
startHeader({}, []);
|
|
734
|
+
|
|
735
|
+
expect(revealStartupListing).toHaveBeenCalledWith(expect.anything(), expect.any(Object), []);
|
|
736
|
+
});
|
|
737
|
+
|
|
738
|
+
it("asks the network nothing at startup", () => {
|
|
739
|
+
startHeader({});
|
|
740
|
+
expect(fetchSpy).not.toHaveBeenCalled();
|
|
741
|
+
});
|
|
742
|
+
});
|
|
743
|
+
|
|
346
744
|
describe("session_shutdown teardown", () => {
|
|
347
|
-
it("
|
|
745
|
+
it("stops the splash, restores workingVisible, and clears editor component", () => {
|
|
348
746
|
const { triggerStart, triggerShutdown } = setupHarness();
|
|
349
747
|
const { ctx, ui } = makeCtx(true);
|
|
350
748
|
|
|
351
749
|
triggerStart(ctx);
|
|
352
750
|
|
|
353
|
-
// Verify startup
|
|
354
|
-
expect(ui.setWorkingVisible).
|
|
751
|
+
// Verify startup: with the border spinner on, pi's working row was hidden.
|
|
752
|
+
expect(ui.setWorkingVisible).toHaveBeenLastCalledWith(false);
|
|
355
753
|
|
|
356
754
|
// Now trigger shutdown
|
|
357
755
|
triggerShutdown(ctx);
|
|
358
756
|
|
|
359
|
-
expect(
|
|
360
|
-
expect(clearActiveBashIntervals).toHaveBeenCalledTimes(1);
|
|
361
|
-
expect(clearActiveCodemodeIntervals).toHaveBeenCalledTimes(1);
|
|
757
|
+
expect(stopStartupListing).toHaveBeenCalledTimes(1);
|
|
362
758
|
expect(ui.setWorkingVisible).toHaveBeenLastCalledWith(true);
|
|
363
759
|
expect(ui.setEditorComponent).toHaveBeenLastCalledWith(undefined);
|
|
364
760
|
});
|
|
365
761
|
|
|
366
|
-
it("
|
|
762
|
+
it("stops the splash when pi disposes the header (header replaced, session restart)", () => {
|
|
367
763
|
const { triggerStart, triggerShutdown } = setupHarness();
|
|
368
764
|
const { ctx, ui } = makeCtx(true);
|
|
369
765
|
|
|
370
766
|
triggerStart(ctx);
|
|
371
|
-
|
|
372
767
|
const headerFactory = ui.setHeader.mock.calls[0]![0];
|
|
373
|
-
const
|
|
374
|
-
const ORIG_ADD_CHILD = Symbol.for("splashscreen:origAddChild");
|
|
375
|
-
const ANIM_INTERVAL = Symbol.for("splashscreen:animInterval");
|
|
376
|
-
const DEBOUNCE_TIMER = Symbol.for("splashscreen:debounceTimer");
|
|
377
|
-
|
|
378
|
-
const origAddChildFn = vi.fn();
|
|
379
|
-
const patchedChild = {
|
|
380
|
-
[PATCHED_LISTING]: true,
|
|
381
|
-
[ORIG_ADD_CHILD]: origAddChildFn,
|
|
382
|
-
[ANIM_INTERVAL]: setInterval(() => {}, 10000),
|
|
383
|
-
[DEBOUNCE_TIMER]: setTimeout(() => {}, 10000),
|
|
384
|
-
addChild: vi.fn(),
|
|
385
|
-
};
|
|
386
|
-
|
|
387
|
-
const fakeTui = {
|
|
388
|
-
terminal: { rows: 24 },
|
|
389
|
-
children: [patchedChild],
|
|
390
|
-
} as unknown as TUI;
|
|
768
|
+
const comp = headerFactory({ terminal: { rows: 24 } } as TUI, {} as Theme);
|
|
391
769
|
|
|
392
|
-
|
|
770
|
+
expect(typeof comp.dispose).toBe("function");
|
|
771
|
+
comp.dispose!();
|
|
772
|
+
expect(stopStartupListing).toHaveBeenCalledTimes(1);
|
|
393
773
|
|
|
394
|
-
// Now trigger shutdown
|
|
395
774
|
triggerShutdown(ctx);
|
|
775
|
+
});
|
|
776
|
+
});
|
|
777
|
+
|
|
778
|
+
// The header component's own contract with pi, independent of lifecycle: pi
|
|
779
|
+
// tells a component to drop cached rendering state by calling
|
|
780
|
+
// `invalidate()` — `onThemeChange()` does it for the whole tree
|
|
781
|
+
// (`interactive-mode.js:792`), as does the theme file watcher — and the
|
|
782
|
+
// splash caches its finished lines, which carry the theme's colours.
|
|
783
|
+
describe("header component contract", () => {
|
|
784
|
+
it("drops the cached header lines when pi invalidates it (theme change)", () => {
|
|
785
|
+
const { triggerStart } = setupHarness();
|
|
786
|
+
const { ctx, ui } = makeCtx(true);
|
|
787
|
+
|
|
788
|
+
triggerStart(ctx);
|
|
789
|
+
const headerFactory = ui.setHeader.mock.calls[0]![0];
|
|
790
|
+
const comp = headerFactory({ terminal: { rows: 24 } } as TUI, {} as Theme);
|
|
791
|
+
|
|
792
|
+
comp.render(80);
|
|
793
|
+
const ref = vi.mocked(renderHeader).mock.calls.at(-1)![1] as {
|
|
794
|
+
settled: boolean;
|
|
795
|
+
cachedLines?: string[];
|
|
796
|
+
};
|
|
797
|
+
// The state a finished splash leaves behind (the mocked `renderHeader`
|
|
798
|
+
// does not fill it in, so the test does what the real one would).
|
|
799
|
+
ref.settled = true;
|
|
800
|
+
ref.cachedLines = ["\x1b[90mv1.2.0\x1b[0m"];
|
|
801
|
+
|
|
802
|
+
comp.invalidate();
|
|
396
803
|
|
|
397
|
-
expect(
|
|
398
|
-
expect(patchedChild[PATCHED_LISTING]).toBe(false);
|
|
399
|
-
expect(patchedChild[ORIG_ADD_CHILD]).toBeUndefined();
|
|
804
|
+
expect(ref.cachedLines).toBeUndefined();
|
|
400
805
|
});
|
|
401
806
|
});
|
|
402
807
|
});
|