@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.
Files changed (46) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +60 -0
  3. package/package.json +44 -0
  4. package/src/bash/index.ts +10 -0
  5. package/src/bash/renderer.test.ts +424 -0
  6. package/src/bash/renderer.ts +301 -0
  7. package/src/bash/tool.test.ts +26 -0
  8. package/src/bash/tool.ts +17 -0
  9. package/src/codemode/index.ts +20 -0
  10. package/src/codemode/loader.test.ts +139 -0
  11. package/src/codemode/loader.ts +115 -0
  12. package/src/codemode/renderer.test.ts +717 -0
  13. package/src/codemode/renderer.ts +399 -0
  14. package/src/codemode/tool.test.ts +149 -0
  15. package/src/codemode/tool.ts +96 -0
  16. package/src/config.test.ts +199 -0
  17. package/src/config.ts +129 -0
  18. package/src/editor/index.test.ts +820 -0
  19. package/src/editor/index.ts +380 -0
  20. package/src/editor/spin-quips.test.ts +106 -0
  21. package/src/editor/spin-quips.ts +44 -0
  22. package/src/editor/spin.test.ts +763 -0
  23. package/src/editor/spin.ts +493 -0
  24. package/src/index.test.ts +402 -0
  25. package/src/index.ts +235 -0
  26. package/src/migration.test.ts +256 -0
  27. package/src/migration.ts +94 -0
  28. package/src/settings.test.ts +122 -0
  29. package/src/settings.ts +135 -0
  30. package/src/startup/capture.ts +47 -0
  31. package/src/startup/index.test.ts +175 -0
  32. package/src/startup/index.ts +348 -0
  33. package/src/startup/logo.test.ts +260 -0
  34. package/src/startup/logo.ts +85 -0
  35. package/src/startup/sections.test.ts +333 -0
  36. package/src/startup/sections.ts +306 -0
  37. package/src/startup/version.test.ts +98 -0
  38. package/src/startup/version.ts +26 -0
  39. package/src/thinking/patch.test.ts +857 -0
  40. package/src/thinking/patch.ts +407 -0
  41. package/src/thinking/theme.test.ts +216 -0
  42. package/src/thinking/theme.ts +156 -0
  43. package/src/thinking/transform.test.ts +89 -0
  44. package/src/thinking/transform.ts +21 -0
  45. package/src/thinking/unindent.test.ts +74 -0
  46. 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
+ }