@pi-archimedes/ui 2.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +60 -0
- package/package.json +44 -0
- package/src/bash/index.ts +10 -0
- package/src/bash/renderer.test.ts +424 -0
- package/src/bash/renderer.ts +301 -0
- package/src/bash/tool.test.ts +26 -0
- package/src/bash/tool.ts +17 -0
- package/src/codemode/index.ts +20 -0
- package/src/codemode/loader.test.ts +139 -0
- package/src/codemode/loader.ts +115 -0
- package/src/codemode/renderer.test.ts +717 -0
- package/src/codemode/renderer.ts +399 -0
- package/src/codemode/tool.test.ts +149 -0
- package/src/codemode/tool.ts +96 -0
- package/src/config.test.ts +199 -0
- package/src/config.ts +129 -0
- package/src/editor/index.test.ts +820 -0
- package/src/editor/index.ts +380 -0
- package/src/editor/spin-quips.test.ts +106 -0
- package/src/editor/spin-quips.ts +44 -0
- package/src/editor/spin.test.ts +763 -0
- package/src/editor/spin.ts +493 -0
- package/src/index.test.ts +402 -0
- package/src/index.ts +235 -0
- package/src/migration.test.ts +256 -0
- package/src/migration.ts +94 -0
- package/src/settings.test.ts +122 -0
- package/src/settings.ts +135 -0
- package/src/startup/capture.ts +47 -0
- package/src/startup/index.test.ts +175 -0
- package/src/startup/index.ts +348 -0
- package/src/startup/logo.test.ts +260 -0
- package/src/startup/logo.ts +85 -0
- package/src/startup/sections.test.ts +333 -0
- package/src/startup/sections.ts +306 -0
- package/src/startup/version.test.ts +98 -0
- package/src/startup/version.ts +26 -0
- package/src/thinking/patch.test.ts +857 -0
- package/src/thinking/patch.ts +407 -0
- package/src/thinking/theme.test.ts +216 -0
- package/src/thinking/theme.ts +156 -0
- package/src/thinking/transform.test.ts +89 -0
- package/src/thinking/transform.ts +21 -0
- package/src/thinking/unindent.test.ts +74 -0
- package/src/thinking/unindent.ts +74 -0
|
@@ -0,0 +1,402 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
2
|
+
import type {
|
|
3
|
+
ExtensionAPI,
|
|
4
|
+
ExtensionContext,
|
|
5
|
+
Theme,
|
|
6
|
+
KeybindingsManager,
|
|
7
|
+
} from "@earendil-works/pi-coding-agent";
|
|
8
|
+
import type { TUI, EditorTheme, Component } from "@earendil-works/pi-tui";
|
|
9
|
+
|
|
10
|
+
// ── Mock modules ─────────────────────────────────────────────────────────────
|
|
11
|
+
|
|
12
|
+
vi.mock("@pi-archimedes/core/settings-io", () => ({
|
|
13
|
+
loadConfig: vi.fn(),
|
|
14
|
+
saveConfig: vi.fn(),
|
|
15
|
+
removeConfig: vi.fn(),
|
|
16
|
+
}));
|
|
17
|
+
|
|
18
|
+
vi.mock("./config.js", async (importOriginal) => {
|
|
19
|
+
const actual = await importOriginal<typeof import("./config.js")>();
|
|
20
|
+
return {
|
|
21
|
+
...actual,
|
|
22
|
+
loadUIConfig: vi.fn(),
|
|
23
|
+
};
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
vi.mock("./startup/capture.js", async (importOriginal) => {
|
|
27
|
+
const actual = await importOriginal<typeof import("./startup/capture.js")>();
|
|
28
|
+
return {
|
|
29
|
+
...actual,
|
|
30
|
+
patchConsoleLog: vi.fn(),
|
|
31
|
+
unpatchConsoleLog: vi.fn(),
|
|
32
|
+
};
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
vi.mock("./thinking/patch.js", () => ({
|
|
36
|
+
patchThinkingRenderer: vi.fn(),
|
|
37
|
+
}));
|
|
38
|
+
|
|
39
|
+
vi.mock("./thinking/transform.js", () => ({
|
|
40
|
+
transformThinkingContent: vi.fn(),
|
|
41
|
+
}));
|
|
42
|
+
|
|
43
|
+
vi.mock("./startup/index.js", async (importOriginal) => {
|
|
44
|
+
const actual = await importOriginal<typeof import("./startup/index.js")>();
|
|
45
|
+
return {
|
|
46
|
+
...actual,
|
|
47
|
+
renderHeader: vi.fn(() => ["header line"]),
|
|
48
|
+
patchStartupListing: vi.fn(),
|
|
49
|
+
};
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
vi.mock("./migration.js", () => ({
|
|
53
|
+
migrateCoreToUIConfig: vi.fn(),
|
|
54
|
+
migrateCompactThinkingToStyle: vi.fn(),
|
|
55
|
+
migrateRemovedToolPatch: vi.fn(),
|
|
56
|
+
UI_CONFIG_KEYS: [],
|
|
57
|
+
}));
|
|
58
|
+
|
|
59
|
+
vi.mock("./bash/index.js", () => ({
|
|
60
|
+
registerBashToolOverride: vi.fn(),
|
|
61
|
+
clearActiveBashIntervals: vi.fn(),
|
|
62
|
+
}));
|
|
63
|
+
|
|
64
|
+
vi.mock("./codemode/index.js", () => ({
|
|
65
|
+
registerCodemodeToolOverride: vi.fn(async () => true),
|
|
66
|
+
clearActiveCodemodeIntervals: vi.fn(),
|
|
67
|
+
}));
|
|
68
|
+
|
|
69
|
+
import defaultExport, {
|
|
70
|
+
registerUI,
|
|
71
|
+
unpatchConsoleLog,
|
|
72
|
+
getUISettingsItems,
|
|
73
|
+
loadUIConfig,
|
|
74
|
+
saveUIConfig,
|
|
75
|
+
DEFAULT_UI_CONFIG,
|
|
76
|
+
ANIMATION_STYLES,
|
|
77
|
+
} from "./index.js";
|
|
78
|
+
import { patchConsoleLog } from "./startup/capture.js";
|
|
79
|
+
import { migrateCoreToUIConfig } from "./migration.js";
|
|
80
|
+
import { registerBashToolOverride, clearActiveBashIntervals } from "./bash/index.js";
|
|
81
|
+
import { registerCodemodeToolOverride, clearActiveCodemodeIntervals } from "./codemode/index.js";
|
|
82
|
+
import { patchThinkingRenderer } from "./thinking/patch.js";
|
|
83
|
+
import { transformThinkingContent } from "./thinking/transform.js";
|
|
84
|
+
import { renderHeader, patchStartupListing } from "./startup/index.js";
|
|
85
|
+
|
|
86
|
+
// ── Test helpers ─────────────────────────────────────────────────────────────
|
|
87
|
+
|
|
88
|
+
interface UiSpies {
|
|
89
|
+
setHeader: ReturnType<typeof vi.fn>;
|
|
90
|
+
setEditorComponent: ReturnType<typeof vi.fn>;
|
|
91
|
+
setWorkingVisible: ReturnType<typeof vi.fn>;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function makeCtx(hasUI = true): { ctx: ExtensionContext; ui: UiSpies } {
|
|
95
|
+
const ui: UiSpies = {
|
|
96
|
+
setHeader: vi.fn(),
|
|
97
|
+
setEditorComponent: vi.fn(),
|
|
98
|
+
setWorkingVisible: vi.fn(),
|
|
99
|
+
};
|
|
100
|
+
const ctx = {
|
|
101
|
+
hasUI,
|
|
102
|
+
cwd: "/workspace/test-project",
|
|
103
|
+
ui: { ...ui, theme: {} as Theme },
|
|
104
|
+
isIdle: vi.fn(() => true),
|
|
105
|
+
shutdown: vi.fn(),
|
|
106
|
+
} as unknown as ExtensionContext;
|
|
107
|
+
return { ctx, ui };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function setupHarness(): {
|
|
111
|
+
pi: ExtensionAPI;
|
|
112
|
+
triggerStart: (ctx: ExtensionContext) => void;
|
|
113
|
+
triggerShutdown: (ctx: ExtensionContext) => void;
|
|
114
|
+
triggerMessageEnd: (msg: unknown) => void;
|
|
115
|
+
} {
|
|
116
|
+
const listeners: Record<string, ((event: any, ctx: any) => void)[]> = {};
|
|
117
|
+
const pi = {
|
|
118
|
+
on: vi.fn((event: string, handler: (event: any, ctx: any) => void) => {
|
|
119
|
+
listeners[event] = listeners[event] ?? [];
|
|
120
|
+
listeners[event]!.push(handler);
|
|
121
|
+
}),
|
|
122
|
+
registerTool: vi.fn(),
|
|
123
|
+
} as unknown as ExtensionAPI;
|
|
124
|
+
|
|
125
|
+
registerUI(pi);
|
|
126
|
+
|
|
127
|
+
return {
|
|
128
|
+
pi,
|
|
129
|
+
triggerStart: (ctx: ExtensionContext) => {
|
|
130
|
+
for (const h of listeners["session_start"] ?? []) {
|
|
131
|
+
h(null, ctx);
|
|
132
|
+
}
|
|
133
|
+
},
|
|
134
|
+
triggerShutdown: (ctx: ExtensionContext) => {
|
|
135
|
+
for (const h of listeners["session_shutdown"] ?? []) {
|
|
136
|
+
h(null, ctx);
|
|
137
|
+
}
|
|
138
|
+
},
|
|
139
|
+
triggerMessageEnd: (msg: unknown) => {
|
|
140
|
+
for (const h of listeners["message_end"] ?? []) {
|
|
141
|
+
h({ message: msg }, {} as any);
|
|
142
|
+
}
|
|
143
|
+
},
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
describe("packages/ui lifecycle and registration", () => {
|
|
148
|
+
beforeEach(() => {
|
|
149
|
+
vi.clearAllMocks();
|
|
150
|
+
vi.mocked(loadUIConfig).mockReturnValue({ ...DEFAULT_UI_CONFIG });
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
afterEach(() => {
|
|
154
|
+
vi.restoreAllMocks();
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
describe("exports", () => {
|
|
158
|
+
it("re-exports expected functions and constants", () => {
|
|
159
|
+
expect(typeof unpatchConsoleLog).toBe("function");
|
|
160
|
+
expect(typeof getUISettingsItems).toBe("function");
|
|
161
|
+
expect(typeof loadUIConfig).toBe("function");
|
|
162
|
+
expect(typeof saveUIConfig).toBe("function");
|
|
163
|
+
expect(DEFAULT_UI_CONFIG).toBeDefined();
|
|
164
|
+
expect(ANIMATION_STYLES).toBeDefined();
|
|
165
|
+
expect(typeof defaultExport).toBe("function");
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it("default export delegates to registerUI", () => {
|
|
169
|
+
const pi = { on: vi.fn() } as unknown as ExtensionAPI;
|
|
170
|
+
defaultExport(pi);
|
|
171
|
+
expect(migrateCoreToUIConfig).toHaveBeenCalled();
|
|
172
|
+
expect(patchConsoleLog).toHaveBeenCalled();
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
describe("registerUI initialization", () => {
|
|
177
|
+
it("calls migrateCoreToUIConfig and patchConsoleLog", () => {
|
|
178
|
+
const { pi } = setupHarness();
|
|
179
|
+
expect(migrateCoreToUIConfig).toHaveBeenCalledTimes(1);
|
|
180
|
+
expect(patchConsoleLog).toHaveBeenCalledTimes(1);
|
|
181
|
+
expect(pi.on).toHaveBeenCalledWith("session_shutdown", expect.any(Function));
|
|
182
|
+
expect(pi.on).toHaveBeenCalledWith("session_start", expect.any(Function));
|
|
183
|
+
});
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
describe("session_start with interactive UI (hasUI: true)", () => {
|
|
187
|
+
it("configures bash tool override, codemode tool override, header, editor, and thinking renderer", () => {
|
|
188
|
+
const { triggerStart } = setupHarness();
|
|
189
|
+
const { ctx, ui } = makeCtx(true);
|
|
190
|
+
|
|
191
|
+
triggerStart(ctx);
|
|
192
|
+
|
|
193
|
+
// Bash tool override
|
|
194
|
+
expect(registerBashToolOverride).toHaveBeenCalledWith(expect.anything(), "/workspace/test-project");
|
|
195
|
+
|
|
196
|
+
// Codemode tool override (fire-and-forget)
|
|
197
|
+
expect(registerCodemodeToolOverride).toHaveBeenCalledWith(expect.anything());
|
|
198
|
+
|
|
199
|
+
// Header installation
|
|
200
|
+
expect(ui.setHeader).toHaveBeenCalledTimes(1);
|
|
201
|
+
const headerFactory = ui.setHeader.mock.calls[0]![0];
|
|
202
|
+
const fakeTui = { terminal: { rows: 24 } } as TUI;
|
|
203
|
+
const fakeTheme = {} as Theme;
|
|
204
|
+
const comp = headerFactory(fakeTui, fakeTheme);
|
|
205
|
+
expect(typeof comp.render).toBe("function");
|
|
206
|
+
comp.render(80);
|
|
207
|
+
expect(renderHeader).toHaveBeenCalledWith(fakeTheme, expect.any(Object), 80, 21);
|
|
208
|
+
expect(patchStartupListing).toHaveBeenCalledWith(fakeTui, fakeTheme, expect.any(Object));
|
|
209
|
+
|
|
210
|
+
// Working visibility and Editor component
|
|
211
|
+
expect(ui.setWorkingVisible).toHaveBeenCalledWith(false); // spinFlag is true by default
|
|
212
|
+
expect(ui.setEditorComponent).toHaveBeenCalledTimes(1);
|
|
213
|
+
const editorFactory = ui.setEditorComponent.mock.calls[0]![0];
|
|
214
|
+
const editorInstance = editorFactory(fakeTui, { borderColor: (s: string) => s } as EditorTheme, {} as KeybindingsManager);
|
|
215
|
+
expect(editorInstance).toBeDefined();
|
|
216
|
+
(editorInstance as any).dispose?.();
|
|
217
|
+
|
|
218
|
+
// Thinking renderer
|
|
219
|
+
expect(patchThinkingRenderer).toHaveBeenCalledWith(
|
|
220
|
+
expect.any(Function),
|
|
221
|
+
expect.objectContaining({
|
|
222
|
+
labelText: "Thinking...",
|
|
223
|
+
labelColor: "255,215,0",
|
|
224
|
+
autoCollapseThinking: false,
|
|
225
|
+
thinkingStyle: "Full",
|
|
226
|
+
}),
|
|
227
|
+
);
|
|
228
|
+
|
|
229
|
+
// Master gate (default toolStyle is Minimal): both overrides registered
|
|
230
|
+
expect(registerBashToolOverride).toHaveBeenCalled();
|
|
231
|
+
expect(registerCodemodeToolOverride).toHaveBeenCalled();
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
it("skips the tool overrides entirely when toolStyle is Native", () => {
|
|
235
|
+
vi.mocked(loadUIConfig).mockReturnValue({
|
|
236
|
+
...DEFAULT_UI_CONFIG,
|
|
237
|
+
toolStyle: "Native",
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
const { triggerStart } = setupHarness();
|
|
241
|
+
const { ctx, ui } = makeCtx(true);
|
|
242
|
+
|
|
243
|
+
triggerStart(ctx);
|
|
244
|
+
|
|
245
|
+
// Native = no archimedes tool styling: neither override is registered
|
|
246
|
+
// (pi's native rendering stands, no built-in-takeover notice).
|
|
247
|
+
expect(registerBashToolOverride).not.toHaveBeenCalled();
|
|
248
|
+
expect(registerCodemodeToolOverride).not.toHaveBeenCalled();
|
|
249
|
+
// The rest of the UI still comes up.
|
|
250
|
+
expect(ui.setHeader).toHaveBeenCalledTimes(1);
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
it("respects editorSpinBorder: false by leaving workingVisible=true and not installing editor component", () => {
|
|
254
|
+
vi.mocked(loadUIConfig).mockReturnValue({
|
|
255
|
+
...DEFAULT_UI_CONFIG,
|
|
256
|
+
editorSpinBorder: false,
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
const { triggerStart } = setupHarness();
|
|
260
|
+
const { ctx, ui } = makeCtx(true);
|
|
261
|
+
|
|
262
|
+
triggerStart(ctx);
|
|
263
|
+
|
|
264
|
+
expect(ui.setWorkingVisible).toHaveBeenCalledWith(true);
|
|
265
|
+
expect(ui.setEditorComponent).not.toHaveBeenCalled();
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
it("skips bashToolOverride when bashToolStyling is false", () => {
|
|
269
|
+
vi.mocked(loadUIConfig).mockReturnValue({
|
|
270
|
+
...DEFAULT_UI_CONFIG,
|
|
271
|
+
bashToolStyling: false,
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
const { triggerStart } = setupHarness();
|
|
275
|
+
const { ctx } = makeCtx(true);
|
|
276
|
+
|
|
277
|
+
triggerStart(ctx);
|
|
278
|
+
|
|
279
|
+
expect(registerBashToolOverride).not.toHaveBeenCalled();
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
it("skips the codemode tool override when codemodeToolStyling is false", () => {
|
|
283
|
+
vi.mocked(loadUIConfig).mockReturnValue({
|
|
284
|
+
...DEFAULT_UI_CONFIG,
|
|
285
|
+
codemodeToolStyling: false,
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
const { triggerStart } = setupHarness();
|
|
289
|
+
const { ctx } = makeCtx(true);
|
|
290
|
+
|
|
291
|
+
triggerStart(ctx);
|
|
292
|
+
|
|
293
|
+
expect(registerCodemodeToolOverride).not.toHaveBeenCalled();
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
it("does not accumulate message_end listeners on repeated session_start (simulating /reload)", () => {
|
|
297
|
+
vi.mocked(loadUIConfig).mockReturnValue({
|
|
298
|
+
...DEFAULT_UI_CONFIG,
|
|
299
|
+
codeUnindent: true,
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
const { triggerStart, triggerMessageEnd } = setupHarness();
|
|
303
|
+
const { ctx } = makeCtx(true);
|
|
304
|
+
|
|
305
|
+
triggerStart(ctx);
|
|
306
|
+
triggerStart(ctx); // Simulate second session_start
|
|
307
|
+
|
|
308
|
+
const msg = { role: "assistant", content: [] };
|
|
309
|
+
triggerMessageEnd(msg);
|
|
310
|
+
|
|
311
|
+
expect(transformThinkingContent).toHaveBeenCalledTimes(1);
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
it("skips transformThinkingContent on message_end if codeUnindent is false", () => {
|
|
315
|
+
vi.mocked(loadUIConfig).mockReturnValue({
|
|
316
|
+
...DEFAULT_UI_CONFIG,
|
|
317
|
+
codeUnindent: false,
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
const { triggerStart, triggerMessageEnd } = setupHarness();
|
|
321
|
+
const { ctx } = makeCtx(true);
|
|
322
|
+
|
|
323
|
+
triggerStart(ctx);
|
|
324
|
+
|
|
325
|
+
const msg = { role: "assistant", content: [] };
|
|
326
|
+
triggerMessageEnd(msg);
|
|
327
|
+
|
|
328
|
+
expect(transformThinkingContent).not.toHaveBeenCalled();
|
|
329
|
+
});
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
describe("session_start in non-interactive mode (hasUI: false)", () => {
|
|
333
|
+
it("still registers bashToolOverride but skips UI setup", () => {
|
|
334
|
+
const { triggerStart } = setupHarness();
|
|
335
|
+
const { ctx, ui } = makeCtx(false);
|
|
336
|
+
|
|
337
|
+
triggerStart(ctx);
|
|
338
|
+
|
|
339
|
+
expect(registerBashToolOverride).toHaveBeenCalledWith(expect.anything(), "/workspace/test-project");
|
|
340
|
+
expect(ui.setHeader).not.toHaveBeenCalled();
|
|
341
|
+
expect(ui.setEditorComponent).not.toHaveBeenCalled();
|
|
342
|
+
expect(patchThinkingRenderer).not.toHaveBeenCalled();
|
|
343
|
+
});
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
describe("session_shutdown teardown", () => {
|
|
347
|
+
it("restores unpatchConsoleLog, workingVisible, and clears editor component", () => {
|
|
348
|
+
const { triggerStart, triggerShutdown } = setupHarness();
|
|
349
|
+
const { ctx, ui } = makeCtx(true);
|
|
350
|
+
|
|
351
|
+
triggerStart(ctx);
|
|
352
|
+
|
|
353
|
+
// Verify startup
|
|
354
|
+
expect(ui.setWorkingVisible).toHaveBeenCalledWith(false);
|
|
355
|
+
|
|
356
|
+
// Now trigger shutdown
|
|
357
|
+
triggerShutdown(ctx);
|
|
358
|
+
|
|
359
|
+
expect(unpatchConsoleLog).toHaveBeenCalledTimes(1);
|
|
360
|
+
expect(clearActiveBashIntervals).toHaveBeenCalledTimes(1);
|
|
361
|
+
expect(clearActiveCodemodeIntervals).toHaveBeenCalledTimes(1);
|
|
362
|
+
expect(ui.setWorkingVisible).toHaveBeenLastCalledWith(true);
|
|
363
|
+
expect(ui.setEditorComponent).toHaveBeenLastCalledWith(undefined);
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
it("cleans up patched TUI container children on shutdown", () => {
|
|
367
|
+
const { triggerStart, triggerShutdown } = setupHarness();
|
|
368
|
+
const { ctx, ui } = makeCtx(true);
|
|
369
|
+
|
|
370
|
+
triggerStart(ctx);
|
|
371
|
+
|
|
372
|
+
const headerFactory = ui.setHeader.mock.calls[0]![0];
|
|
373
|
+
const PATCHED_LISTING = Symbol.for("splashscreen:listingPatched");
|
|
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;
|
|
391
|
+
|
|
392
|
+
headerFactory(fakeTui, {} as Theme);
|
|
393
|
+
|
|
394
|
+
// Now trigger shutdown
|
|
395
|
+
triggerShutdown(ctx);
|
|
396
|
+
|
|
397
|
+
expect(patchedChild.addChild).toBe(origAddChildFn);
|
|
398
|
+
expect(patchedChild[PATCHED_LISTING]).toBe(false);
|
|
399
|
+
expect(patchedChild[ORIG_ADD_CHILD]).toBeUndefined();
|
|
400
|
+
});
|
|
401
|
+
});
|
|
402
|
+
});
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ExtensionAPI,
|
|
3
|
+
ExtensionContext,
|
|
4
|
+
KeybindingsManager,
|
|
5
|
+
Theme,
|
|
6
|
+
} from "@earendil-works/pi-coding-agent";
|
|
7
|
+
import type {
|
|
8
|
+
TUI,
|
|
9
|
+
EditorTheme,
|
|
10
|
+
Component,
|
|
11
|
+
} from "@earendil-works/pi-tui";
|
|
12
|
+
|
|
13
|
+
import { HephaestusEditor } from "./editor/index.js";
|
|
14
|
+
import { renderHeader, patchStartupListing, type ListingRef } from "./startup/index.js";
|
|
15
|
+
import { patchConsoleLog, unpatchConsoleLog } from "./startup/capture.js";
|
|
16
|
+
import { patchThinkingRenderer } from "./thinking/patch.js";
|
|
17
|
+
import { transformThinkingContent } from "./thinking/transform.js";
|
|
18
|
+
import {
|
|
19
|
+
loadUIConfig,
|
|
20
|
+
saveUIConfig,
|
|
21
|
+
DEFAULT_UI_CONFIG,
|
|
22
|
+
ANIMATION_STYLES,
|
|
23
|
+
normalizeThinkingStyle,
|
|
24
|
+
normalizeToolStyle,
|
|
25
|
+
type UIConfig,
|
|
26
|
+
type CoreConfig,
|
|
27
|
+
} from "./config.js";
|
|
28
|
+
import { getUISettingsItems } from "./settings.js";
|
|
29
|
+
import { migrateCoreToUIConfig, migrateCompactThinkingToStyle, migrateRemovedToolPatch } from "./migration.js";
|
|
30
|
+
import { registerBashToolOverride, clearActiveBashIntervals } from "./bash/index.js";
|
|
31
|
+
import { registerCodemodeToolOverride, clearActiveCodemodeIntervals } from "./codemode/index.js";
|
|
32
|
+
|
|
33
|
+
// Re-exports
|
|
34
|
+
export { unpatchConsoleLog } from "./startup/capture.js";
|
|
35
|
+
export { getUISettingsItems } from "./settings.js";
|
|
36
|
+
export {
|
|
37
|
+
loadUIConfig,
|
|
38
|
+
saveUIConfig,
|
|
39
|
+
DEFAULT_UI_CONFIG,
|
|
40
|
+
ANIMATION_STYLES,
|
|
41
|
+
normalizeThinkingStyle,
|
|
42
|
+
normalizeToolStyle,
|
|
43
|
+
THINKING_STYLE_VALUES,
|
|
44
|
+
TOOL_STYLE_VALUES,
|
|
45
|
+
type UIConfig,
|
|
46
|
+
type CoreConfig,
|
|
47
|
+
type ThinkingStyle,
|
|
48
|
+
type ToolStyle,
|
|
49
|
+
} from "./config.js";
|
|
50
|
+
|
|
51
|
+
// Module-level state for session lifecycle
|
|
52
|
+
let uiRef: ListingRef | undefined;
|
|
53
|
+
let uiCtx: ExtensionContext | undefined;
|
|
54
|
+
let uiTui: TUI | undefined;
|
|
55
|
+
|
|
56
|
+
let spinFlag = false;
|
|
57
|
+
let spinInterval: ReturnType<typeof setInterval> | undefined;
|
|
58
|
+
|
|
59
|
+
function clearSpinInterval(): void {
|
|
60
|
+
if (spinInterval) {
|
|
61
|
+
clearInterval(spinInterval);
|
|
62
|
+
spinInterval = undefined;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function registerUI(pi: ExtensionAPI): void {
|
|
67
|
+
// One-time migration of any legacy core config to UI config
|
|
68
|
+
migrateCoreToUIConfig();
|
|
69
|
+
migrateCompactThinkingToStyle();
|
|
70
|
+
// The toolStyle "Full" auto-expand patch was removed: drop any wrapper a
|
|
71
|
+
// previous version left on the shared prototype (upgrade + /reload case).
|
|
72
|
+
migrateRemovedToolPatch();
|
|
73
|
+
|
|
74
|
+
// Patch console.log for model scope capture
|
|
75
|
+
patchConsoleLog();
|
|
76
|
+
|
|
77
|
+
pi.on("message_end", (event, _ctx) => {
|
|
78
|
+
if (loadUIConfig().codeUnindent) {
|
|
79
|
+
transformThinkingContent(event.message as any);
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
// session_shutdown handler (top-level to prevent accumulation on /reload)
|
|
84
|
+
pi.on("session_shutdown", (_event, ctx) => {
|
|
85
|
+
unpatchConsoleLog();
|
|
86
|
+
clearActiveBashIntervals();
|
|
87
|
+
clearActiveCodemodeIntervals();
|
|
88
|
+
|
|
89
|
+
const targetCtx = ctx?.hasUI ? ctx : uiCtx;
|
|
90
|
+
if (targetCtx?.hasUI) {
|
|
91
|
+
targetCtx.ui.setWorkingVisible(true);
|
|
92
|
+
targetCtx.ui.setEditorComponent(undefined);
|
|
93
|
+
}
|
|
94
|
+
clearSpinInterval();
|
|
95
|
+
spinFlag = false;
|
|
96
|
+
|
|
97
|
+
if (uiRef) {
|
|
98
|
+
uiRef.settled = true;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const PATCHED_LISTING = Symbol.for("splashscreen:listingPatched");
|
|
102
|
+
const ORIG_ADD_CHILD = Symbol.for("splashscreen:origAddChild");
|
|
103
|
+
if (uiTui) {
|
|
104
|
+
try {
|
|
105
|
+
for (const child of uiTui.children) {
|
|
106
|
+
const cc = child as any;
|
|
107
|
+
if (cc[PATCHED_LISTING] && cc[ORIG_ADD_CHILD]) {
|
|
108
|
+
cc.addChild = cc[ORIG_ADD_CHILD];
|
|
109
|
+
cc[PATCHED_LISTING] = false;
|
|
110
|
+
cc[ORIG_ADD_CHILD] = undefined;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
for (const topChild of uiTui.children) {
|
|
114
|
+
const tc = topChild as any;
|
|
115
|
+
if (!tc.children) continue;
|
|
116
|
+
for (const child of tc.children) {
|
|
117
|
+
const cc = child as any;
|
|
118
|
+
if (cc[PATCHED_LISTING] && cc[ORIG_ADD_CHILD]) {
|
|
119
|
+
cc.addChild = cc[ORIG_ADD_CHILD];
|
|
120
|
+
cc[PATCHED_LISTING] = false;
|
|
121
|
+
cc[ORIG_ADD_CHILD] = undefined;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const ANIM_INTERVAL = Symbol.for("splashscreen:animInterval");
|
|
127
|
+
const DEBOUNCE_TIMER = Symbol.for("splashscreen:debounceTimer");
|
|
128
|
+
for (const child of uiTui.children) {
|
|
129
|
+
const cc = child as any;
|
|
130
|
+
if (cc[ANIM_INTERVAL]) clearInterval(cc[ANIM_INTERVAL]);
|
|
131
|
+
if (cc[DEBOUNCE_TIMER]) clearTimeout(cc[DEBOUNCE_TIMER]);
|
|
132
|
+
}
|
|
133
|
+
for (const topChild of uiTui.children) {
|
|
134
|
+
const tc = topChild as any;
|
|
135
|
+
if (!tc.children) continue;
|
|
136
|
+
for (const child of tc.children) {
|
|
137
|
+
const cc = child as any;
|
|
138
|
+
if (cc[ANIM_INTERVAL]) clearInterval(cc[ANIM_INTERVAL]);
|
|
139
|
+
if (cc[DEBOUNCE_TIMER]) clearTimeout(cc[DEBOUNCE_TIMER]);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
} catch {
|
|
143
|
+
/* TUI structure may have changed — ignore */
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
// session_start handler
|
|
149
|
+
pi.on("session_start", (_event, ctx: ExtensionContext) => {
|
|
150
|
+
uiCtx = ctx;
|
|
151
|
+
|
|
152
|
+
const config = loadUIConfig();
|
|
153
|
+
const toolStyle = normalizeToolStyle(config.toolStyle);
|
|
154
|
+
|
|
155
|
+
// Master gate: `Minimal` = archimedes tool styling (the per-tool
|
|
156
|
+
// toggles fine-tune it); `Native` = no archimedes tool styling at all
|
|
157
|
+
// — the overrides are not registered, so pi's native tool rendering
|
|
158
|
+
// stands (and no built-in-takeover startup notice).
|
|
159
|
+
if (toolStyle === "Minimal") {
|
|
160
|
+
if (config.bashToolStyling !== false) {
|
|
161
|
+
registerBashToolOverride(pi, ctx.cwd);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// The codemode override replaces pi's built-in codemode extension
|
|
165
|
+
// (the same take-over the bash override does for the core bash
|
|
166
|
+
// tool). It loads the native definition from the running CLI and
|
|
167
|
+
// swaps in the Archimedes renderers; it is a no-op on pi versions
|
|
168
|
+
// without the codemode extension.
|
|
169
|
+
if (config.codemodeToolStyling !== false) {
|
|
170
|
+
void registerCodemodeToolOverride(pi);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (ctx.hasUI) {
|
|
175
|
+
spinFlag = config.editorSpinBorder;
|
|
176
|
+
|
|
177
|
+
// Set animated header
|
|
178
|
+
uiRef = {
|
|
179
|
+
sections: [],
|
|
180
|
+
frame: 0,
|
|
181
|
+
revealed: false,
|
|
182
|
+
revealedAt: 0,
|
|
183
|
+
scaffoldAt: 0,
|
|
184
|
+
settled: false,
|
|
185
|
+
};
|
|
186
|
+
const ref = uiRef;
|
|
187
|
+
const headerFactory = (tui: TUI, theme: Theme): Component & { dispose?(): void } => {
|
|
188
|
+
uiTui = tui;
|
|
189
|
+
const comp: Component & { dispose?(): void } = {
|
|
190
|
+
invalidate(): void {},
|
|
191
|
+
render(width: number): string[] {
|
|
192
|
+
return renderHeader(theme, ref, width, tui.terminal.rows - 3);
|
|
193
|
+
},
|
|
194
|
+
};
|
|
195
|
+
patchStartupListing(tui, theme, ref);
|
|
196
|
+
return comp;
|
|
197
|
+
};
|
|
198
|
+
ctx.ui.setHeader(headerFactory);
|
|
199
|
+
|
|
200
|
+
clearSpinInterval();
|
|
201
|
+
ctx.ui.setWorkingVisible(!spinFlag);
|
|
202
|
+
|
|
203
|
+
if (spinFlag) {
|
|
204
|
+
ctx.ui.setEditorComponent(
|
|
205
|
+
(tui: TUI, editorTheme: EditorTheme, keybindings: KeybindingsManager) => {
|
|
206
|
+
const theme = ctx.ui.theme;
|
|
207
|
+
return new HephaestusEditor(tui, editorTheme, keybindings, {
|
|
208
|
+
getTheme: () => theme,
|
|
209
|
+
isIdle: () => ctx.isIdle(),
|
|
210
|
+
shutdown: () => ctx.shutdown(),
|
|
211
|
+
spin: true,
|
|
212
|
+
spinSpeed: config.editorSpinSpeed,
|
|
213
|
+
spinStyle: config.editorSpinStyle,
|
|
214
|
+
spinLabel: config.editorSpinLabel,
|
|
215
|
+
onSpinInterval: (i) => {
|
|
216
|
+
spinInterval = i;
|
|
217
|
+
},
|
|
218
|
+
});
|
|
219
|
+
},
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
patchThinkingRenderer(() => ctx.ui.theme, {
|
|
224
|
+
labelText: config.labelText,
|
|
225
|
+
labelColor: config.labelColor,
|
|
226
|
+
autoCollapseThinking: config.autoCollapseThinking,
|
|
227
|
+
thinkingStyle: normalizeThinkingStyle(config.thinkingStyle),
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
export default function (pi: ExtensionAPI): void {
|
|
234
|
+
registerUI(pi);
|
|
235
|
+
}
|