@pi-archimedes/ui 2.10.4 → 2.11.1

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.
@@ -1,6 +1,7 @@
1
1
  import type { Theme } from "@earendil-works/pi-coding-agent";
2
2
  import { AssistantMessageComponent, VERSION } from "@earendil-works/pi-coding-agent";
3
3
  import { Markdown, type MarkdownOptions, type MarkdownTheme, MouseRegion, Spacer, Text, TruncatedText } from "@earendil-works/pi-tui";
4
+ import { diag } from "@pi-archimedes/core/log";
4
5
  import type { ThinkingStyle } from "../config.js";
5
6
  import { buildMutedMarkdownTheme } from "./theme.js";
6
7
 
@@ -21,6 +22,11 @@ const THINKING_STATES_KEY = Symbol.for("archimedes:thinkingStateOverrides");
21
22
  * @param config Optional labelText/labelColor overrides for the thinking
22
23
  * block header, and autoCollapseThinking flag. When omitted (or empty/invalid),
23
24
  * the original defaults are used.
25
+ * @param notify The user-facing surface (`ctx.ui.notify`), injected because
26
+ * this module has no `ExtensionContext` of its own. A skipped patch is a
27
+ * silent feature loss — the muted thinking theme simply stops applying — so
28
+ * it is the one diagnostic here the user has to hear about. Omit it and the
29
+ * notice is dropped: the detail still reaches the log.
24
30
  */
25
31
  export function patchThinkingRenderer(
26
32
  getTheme: () => Theme,
@@ -30,6 +36,7 @@ export function patchThinkingRenderer(
30
36
  autoCollapseThinking?: boolean;
31
37
  thinkingStyle?: ThinkingStyle;
32
38
  },
39
+ notify?: (message: string) => void,
33
40
  ): void {
34
41
  if (!AssistantMessageComponent) return;
35
42
 
@@ -52,12 +59,21 @@ export function patchThinkingRenderer(
52
59
  const hasThinkingCheck = /content\.type\s*===\s*["']thinking["']/.test(src);
53
60
  const hasMarkdownTheme = src.includes("this.markdownTheme");
54
61
  if (!hasThinkingCheck || !hasMarkdownTheme) {
55
- console.warn(
56
- `[archimedes] Skipping thinking renderer patch — signature mismatch
57
- hasThinkingCheck: ${hasThinkingCheck}, hasMarkdownTheme: ${hasMarkdownTheme}
58
- This likely means pi's AssistantMessageComponent changed. The muted theme
59
- for thinking blocks will not be applied.`,
62
+ // The user loses the muted thinking theme and cannot see why, so this one
63
+ // goes to the notification surface — one line — while the probe result a
64
+ // developer needs goes to the log. Neither goes to the console: a write
65
+ // smears a fullscreen frame and is discarded inside a subagent child.
66
+ diag(
67
+ "ui",
68
+ `[archimedes] Skipping thinking renderer patch — signature mismatch: hasThinkingCheck: ${hasThinkingCheck}, hasMarkdownTheme: ${hasMarkdownTheme}. This likely means pi's AssistantMessageComponent changed.`,
60
69
  );
70
+ try {
71
+ notify?.(
72
+ "Thinking renderer patch skipped: pi's AssistantMessageComponent changed, so thinking blocks keep pi's default styling.",
73
+ );
74
+ } catch {
75
+ // A surface that cannot deliver the notice must not fail the session.
76
+ }
61
77
  return;
62
78
  }
63
79
 
@@ -70,7 +86,7 @@ export function patchThinkingRenderer(
70
86
  } else {
71
87
  // First patch or version changed — warn if version mismatch
72
88
  if ((proto as any)[PATCHED_KEY] && patchVersion && patchVersion !== currentVersion) {
73
- console.warn(`[archimedes] Re-patching thinking renderer: pi version changed ${patchVersion} → ${currentVersion}`);
89
+ diag("ui", `[archimedes] Re-patching thinking renderer: pi version changed ${patchVersion} → ${currentVersion}`);
74
90
  }
75
91
  }
76
92
 
@@ -1,26 +0,0 @@
1
- import { describe, it, expect, vi } from "vitest";
2
- import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
3
- import { registerBashToolOverride } from "./tool.js";
4
- import { renderBashCall, renderBashResult } from "./renderer.js";
5
-
6
- describe("registerBashToolOverride", () => {
7
- it("registers bash tool with custom renderCall and renderResult while preserving definition", () => {
8
- let capturedTool: any = null;
9
- const pi = {
10
- registerTool: vi.fn((tool: any) => {
11
- capturedTool = tool;
12
- }),
13
- } as unknown as ExtensionAPI;
14
-
15
- registerBashToolOverride(pi, "/test/cwd");
16
-
17
- expect(pi.registerTool).toHaveBeenCalledTimes(1);
18
- expect(capturedTool).toBeDefined();
19
- expect(capturedTool.name).toBe("bash");
20
- expect(capturedTool.label).toBe("bash");
21
- expect(capturedTool.parameters).toBeDefined();
22
- expect(typeof capturedTool.execute).toBe("function");
23
- expect(capturedTool.renderCall).toBe(renderBashCall);
24
- expect(capturedTool.renderResult).toBe(renderBashResult);
25
- });
26
- });
package/src/bash/tool.ts DELETED
@@ -1,17 +0,0 @@
1
- import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
- import { createBashToolDefinition } from "@earendil-works/pi-coding-agent";
3
- import { renderBashCall, renderBashResult } from "./renderer.js";
4
-
5
- /**
6
- * Register custom styled bash tool override.
7
- * Preserves core execution, parameters, timeouts, and PI_* environment injection
8
- * while replacing the presentation with Archimedes styled rendering.
9
- */
10
- export function registerBashToolOverride(pi: ExtensionAPI, cwd: string): void {
11
- const def = createBashToolDefinition(cwd);
12
- pi.registerTool({
13
- ...def,
14
- renderCall: renderBashCall,
15
- renderResult: renderBashResult,
16
- });
17
- }
@@ -1,139 +0,0 @@
1
- import { describe, it, expect, vi, afterEach } from "vitest";
2
- import { mkdtempSync, writeFileSync, mkdirSync, rmSync } from "node:fs";
3
- import { tmpdir } from "node:os";
4
- import path from "node:path";
5
- import {
6
- findAgentPackageRoot,
7
- resolveCodemodeModuleFiles,
8
- loadCodemodeModule,
9
- } from "./loader.js";
10
-
11
- const createdDirs: string[] = [];
12
-
13
- function makeTree(
14
- rootName: string,
15
- files: Record<string, string>,
16
- ): string {
17
- const root = mkdtempSync(path.join(tmpdir(), `archimedes-${rootName}-`));
18
- createdDirs.push(root);
19
- for (const [rel, content] of Object.entries(files)) {
20
- const file = path.join(root, rel);
21
- mkdirSync(path.dirname(file), { recursive: true });
22
- writeFileSync(file, content);
23
- }
24
- return root;
25
- }
26
-
27
- afterEach(() => {
28
- for (const dir of createdDirs.splice(0)) {
29
- rmSync(dir, { recursive: true, force: true });
30
- }
31
- vi.unstubAllEnvs();
32
- });
33
-
34
- describe("findAgentPackageRoot", () => {
35
- it("finds the package root from a nested dist file", () => {
36
- const root = makeTree("agent", {
37
- "package.json": JSON.stringify({ name: "@earendil-works/pi-coding-agent" }),
38
- "dist/index.js": "",
39
- "dist/extensions/codemode/tool.js": "",
40
- });
41
- expect(
42
- findAgentPackageRoot(path.join(root, "dist", "extensions", "codemode")),
43
- ).toBe(root);
44
- expect(findAgentPackageRoot(path.join(root, "dist"))).toBe(root);
45
- });
46
-
47
- it("returns undefined for another package", () => {
48
- const root = makeTree("other", {
49
- "package.json": JSON.stringify({ name: "@pi-archimedes/ui" }),
50
- "src/index.ts": "",
51
- });
52
- expect(findAgentPackageRoot(path.join(root, "src"))).toBeUndefined();
53
- });
54
-
55
- it("returns undefined when there is no package.json at all", () => {
56
- const root = makeTree("loose", { "some/file.js": "" });
57
- expect(findAgentPackageRoot(root)).toBeUndefined();
58
- });
59
- });
60
-
61
- describe("resolveCodemodeModuleFiles", () => {
62
- it("resolves the codemode module from the CLI entry script (argv[1])", () => {
63
- const root = makeTree("agent", {
64
- "package.json": JSON.stringify({ name: "@earendil-works/pi-coding-agent" }),
65
- "dist/bundle/cli.js": "",
66
- "dist/extensions/codemode/tool.js": "",
67
- });
68
- const files = resolveCodemodeModuleFiles(
69
- path.join(root, "dist", "bundle", "cli.js"),
70
- );
71
- expect(files).toContain(
72
- path.join(root, "dist", "extensions", "codemode", "tool.js"),
73
- );
74
- });
75
-
76
- it("returns no argv[1] candidate when the entry is not in the agent package", () => {
77
- const files = resolveCodemodeModuleFiles("/usr/bin/some-other-cli");
78
- // Only the local-copy candidate (if any) may remain.
79
- for (const file of files) {
80
- expect(file).not.toContain("/usr/bin/some-other-cli");
81
- }
82
- });
83
-
84
- it("returns an empty list without argv[1] and without a local copy", () => {
85
- // createRequire fallback: the local pi-coding-agent copy in this repo has
86
- // no dist/extensions/codemode in older versions, but the resolution
87
- // itself must not throw.
88
- expect(() => resolveCodemodeModuleFiles(undefined)).not.toThrow();
89
- });
90
-
91
- it("never builds a dist/dist path for the local-copy candidate", () => {
92
- // require.resolve() returns …/pi-coding-agent/dist/index.js — its
93
- // directory IS the dist dir, so joining another `dist` segment would
94
- // produce a nonexistent …/dist/dist/… path and silently disable the
95
- // override for launcher entries outside the agent package.
96
- const files = resolveCodemodeModuleFiles(undefined);
97
- for (const file of files) {
98
- expect(file).not.toContain(path.join("dist", "dist"));
99
- }
100
- });
101
- });
102
-
103
- describe("loadCodemodeModule", () => {
104
- it("loads the module when the file exists and exports the factory", async () => {
105
- const root = makeTree("agent", {
106
- "package.json": JSON.stringify({ name: "@earendil-works/pi-coding-agent" }),
107
- "dist/index.js": "",
108
- "dist/extensions/codemode/tool.js": `
109
- export const codemodeSchema = { type: "object" };
110
- export function createCodemodeToolDefinition(options) {
111
- return { options, parameters: codemodeSchema };
112
- }
113
- `,
114
- });
115
- const mod = await loadCodemodeModule(
116
- path.join(root, "dist", "index.js"),
117
- );
118
- expect(mod).toBeDefined();
119
- expect(mod!.createCodemodeToolDefinition).toBeTypeOf("function");
120
- expect(mod!.codemodeSchema).toEqual({ type: "object" });
121
- });
122
-
123
- it("returns undefined when the module file is missing", async () => {
124
- const mod = await loadCodemodeModule("/does/not/exist/cli.js");
125
- expect(mod).toBeUndefined();
126
- });
127
-
128
- it("returns undefined when the module lacks the factory", async () => {
129
- const root = makeTree("agent", {
130
- "package.json": JSON.stringify({ name: "@earendil-works/pi-coding-agent" }),
131
- "dist/index.js": "",
132
- "dist/extensions/codemode/tool.js": `
133
- export const somethingElse = 1;
134
- `,
135
- });
136
- const mod = await loadCodemodeModule(path.join(root, "dist", "index.js"));
137
- expect(mod).toBeUndefined();
138
- });
139
- });
@@ -1,115 +0,0 @@
1
- import { existsSync, readFileSync, realpathSync } from "node:fs";
2
- import path from "node:path";
3
-
4
- export interface CodemodeToolModule {
5
- createCodemodeToolDefinition: (options?: Record<string, unknown>) => unknown;
6
- codemodeSchema: unknown;
7
- }
8
-
9
- const AGENT_PACKAGE_NAME = "@earendil-works/pi-coding-agent";
10
- /** Candidate 1: relative to the package ROOT (the CLI-install walk-up). */
11
- const CODEMODE_MODULE_FROM_ROOT = path.join(
12
- "dist",
13
- "extensions",
14
- "codemode",
15
- "tool.js",
16
- );
17
- /** Candidate 2: relative to the `dist/` directory the package entry lives in. */
18
- const CODEMODE_MODULE_FROM_DIST = path.join(
19
- "extensions",
20
- "codemode",
21
- "tool.js",
22
- );
23
-
24
- /**
25
- * Walk up from `startDir` to the package root (the nearest ancestor holding a
26
- * package.json) and return it when it is the pi-coding-agent package.
27
- */
28
- export function findAgentPackageRoot(startDir: string): string | undefined {
29
- let dir = startDir;
30
- while (true) {
31
- const pkgPath = path.join(dir, "package.json");
32
- if (existsSync(pkgPath)) {
33
- try {
34
- const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
35
- if (pkg.name === AGENT_PACKAGE_NAME) {
36
- return dir;
37
- }
38
- } catch {
39
- // Unreadable package.json — keep walking up.
40
- }
41
- // A package boundary without a matching name: stop (we are inside some
42
- // other package's tree, not the agent's).
43
- return undefined;
44
- }
45
- const parent = path.dirname(dir);
46
- if (parent === dir) return undefined;
47
- dir = parent;
48
- }
49
- }
50
-
51
- /**
52
- * Candidate files for the codemode tool module, in preference order:
53
- *
54
- * 1. The running pi CLI's own install. `process.argv[1]` is the CLI entry
55
- * script (e.g. `…/pi-coding-agent/dist/bundle/cli.js`); walking up to the
56
- * package root finds the `dist` the CLI is actually running from. Pi loads
57
- * extensions with jiti, which shares the native module instance for `.js`
58
- * files, so when the CLI runs unbundled this is the very same module the
59
- * CLI's built-in codemode extension loaded — same schema object, same
60
- * executor.
61
- * 2. Our own node_modules copy (resolved via `import.meta.resolve`). A
62
- * fallback for launchers whose argv[1] is not inside a pi-coding-agent
63
- * install. Older pi versions have no `dist/extensions/codemode` at all,
64
- * in which case the file does not exist and the candidate is skipped.
65
- */
66
- export function resolveCodemodeModuleFiles(argv1?: string): string[] {
67
- const candidates: string[] = [];
68
- const entry = argv1 ?? process.argv[1];
69
- if (typeof entry === "string" && entry.length > 0) {
70
- let root: string | undefined;
71
- try {
72
- root = findAgentPackageRoot(path.dirname(realpathSync(entry)));
73
- } catch {
74
- root = undefined;
75
- }
76
- if (root) {
77
- candidates.push(path.join(root, CODEMODE_MODULE_FROM_ROOT));
78
- }
79
- }
80
- try {
81
- // `import.meta.resolve` (the `import` condition) — the agent package's
82
- // exports map has no `require` condition, so a CJS `require.resolve`
83
- // would fail on newer pi versions.
84
- const agentEntry = new URL(import.meta.resolve(AGENT_PACKAGE_NAME)).pathname;
85
- // `agentEntry` is `…/pi-coding-agent/dist/index.js` — its directory IS the
86
- // `dist` dir, so join the module path WITHOUT another `dist` segment.
87
- candidates.push(path.join(path.dirname(agentEntry), CODEMODE_MODULE_FROM_DIST));
88
- } catch {
89
- // No local copy of the agent package — nothing else to try.
90
- }
91
- return candidates;
92
- }
93
-
94
- /**
95
- * Load the codemode tool module from the running pi install (see
96
- * {@link resolveCodemodeModuleFiles}). Returns `undefined` when the module
97
- * cannot be found or loaded (e.g. a pi version without the codemode
98
- * extension), so the caller can fall back to pi's native rendering.
99
- */
100
- export async function loadCodemodeModule(
101
- argv1?: string,
102
- ): Promise<CodemodeToolModule | undefined> {
103
- for (const file of resolveCodemodeModuleFiles(argv1)) {
104
- if (!existsSync(file)) continue;
105
- try {
106
- const mod = await import(file);
107
- if (typeof mod?.createCodemodeToolDefinition === "function") {
108
- return mod as CodemodeToolModule;
109
- }
110
- } catch {
111
- // Try the next candidate.
112
- }
113
- }
114
- return undefined;
115
- }
@@ -1,149 +0,0 @@
1
- import { describe, it, expect, vi, beforeEach } from "vitest";
2
- import { registerCodemodeToolOverride } from "./tool.js";
3
-
4
- const nativeSchema = { type: "object", properties: { code: {} } };
5
- const defFromFactory = {
6
- name: "codemode",
7
- label: "codemode",
8
- description: "native description",
9
- parameters: { type: "object", properties: { code: {} } },
10
- execute: async () => ({}),
11
- prepareLoadout: (loadout: unknown) => loadout,
12
- exposure: "model-only",
13
- constrainedSampling: { type: "grammar" },
14
- };
15
-
16
- function makePi(
17
- overrides: {
18
- allTools?: Array<{ name: string; parameters?: unknown; namespace?: unknown }>;
19
- getSettings?: () => {
20
- codemode?: { mode?: string; inlineBudget?: unknown };
21
- };
22
- } = {},
23
- ) {
24
- const registerTool = vi.fn();
25
- const appendEntry = vi.fn();
26
- const pi = {
27
- registerTool,
28
- appendEntry,
29
- getAllTools: vi.fn(() => overrides.allTools ?? []),
30
- ...(overrides.getSettings ? { getSettings: overrides.getSettings } : {}),
31
- };
32
- return { pi, registerTool, appendEntry } as const;
33
- }
34
-
35
- type MockModule = {
36
- createCodemodeToolDefinition: (options: Record<string, unknown>) => unknown;
37
- codemodeSchema: unknown;
38
- };
39
-
40
- // `vi.mock` is hoisted above the imports, so the mutable module handle must
41
- // live in a hoisted scope (referencing a plain `let` would hit the TDZ when
42
- // the mock factory first runs during the static import of ./tool.js).
43
- const state = vi.hoisted(() => ({
44
- module: undefined as MockModule | undefined,
45
- }));
46
-
47
- vi.mock("./loader.js", () => ({
48
- loadCodemodeModule: vi.fn(async () => state.module),
49
- }));
50
-
51
- beforeEach(() => {
52
- state.module = {
53
- createCodemodeToolDefinition: (options) => ({ ...defFromFactory, options }),
54
- codemodeSchema: nativeSchema,
55
- };
56
- });
57
-
58
- describe("registerCodemodeToolOverride", () => {
59
- it("registers the native definition with the Archimedes renderers", async () => {
60
- const { pi, registerTool } = makePi();
61
- const ok = await registerCodemodeToolOverride(pi as never);
62
-
63
- expect(ok).toBe(true);
64
- expect(registerTool).toHaveBeenCalledTimes(1);
65
- const registered = registerTool.mock.calls[0]![0] as Record<string, unknown>;
66
- // Native definition fields are preserved.
67
- expect(registered.name).toBe("codemode");
68
- expect(registered.execute).toBe(defFromFactory.execute);
69
- expect(registered.prepareLoadout).toBe(defFromFactory.prepareLoadout);
70
- expect(registered.exposure).toBe("model-only");
71
- // The built-in registers codemode inactive; the override must match.
72
- expect(registered.defaultActive).toBe(false);
73
- // The presentation is replaced.
74
- expect(typeof registered.renderCall).toBe("function");
75
- expect(typeof registered.renderResult).toBe("function");
76
- });
77
-
78
- it("preserves the running CLI's schema reference when the built-in is still registered", async () => {
79
- const { pi, registerTool } = makePi({
80
- allTools: [{ name: "codemode", parameters: nativeSchema }],
81
- });
82
- await registerCodemodeToolOverride(pi as never);
83
-
84
- const registered = registerTool.mock.calls[0]![0] as Record<string, unknown>;
85
- expect(registered.parameters).toBe(nativeSchema);
86
- });
87
-
88
- it("keeps the factory schema when no built-in registration is visible", async () => {
89
- const { pi, registerTool } = makePi();
90
- await registerCodemodeToolOverride(pi as never);
91
-
92
- const registered = registerTool.mock.calls[0]![0] as Record<string, unknown>;
93
- expect(registered.parameters).toBe(defFromFactory.parameters);
94
- });
95
-
96
- it("wires appendEntry, models, and the settings-backed options", async () => {
97
- const settings = { codemode: { mode: "only", inlineBudget: 1500 } };
98
- const { pi, registerTool, appendEntry } = makePi({
99
- getSettings: () => settings,
100
- });
101
- await registerCodemodeToolOverride(pi as never);
102
-
103
- const registered = registerTool.mock.calls[0]![0] as Record<string, unknown>;
104
- const options = (registered as { options?: Record<string, unknown> }).options!;
105
- const getMode = options.getMode as () => string;
106
- const getInlineBudget = options.getInlineBudget as () => number | undefined;
107
- expect(options.models).toBe(true);
108
- expect(typeof options.appendEntry).toBe("function");
109
- expect(typeof options.getToolNamespace).toBe("function");
110
- expect(getMode()).toBe("only");
111
- expect(getInlineBudget()).toBe(1500);
112
-
113
- // appendEntry forwards to pi.appendEntry.
114
- await (options.appendEntry as (t: string, d: unknown) => void)(
115
- "codemode-store",
116
- { set: {} },
117
- );
118
- expect(appendEntry).toHaveBeenCalledWith("codemode-store", { set: {} });
119
- });
120
-
121
- it("defaults mode to on and inlineBudget to undefined without settings", async () => {
122
- const { pi, registerTool } = makePi();
123
- await registerCodemodeToolOverride(pi as never);
124
-
125
- const registered = registerTool.mock.calls[0]![0] as Record<string, unknown>;
126
- const options = (registered as { options?: Record<string, unknown> }).options!;
127
- expect((options.getMode as () => string)()).toBe("on");
128
- expect((options.getInlineBudget as () => number | undefined)()).toBeUndefined();
129
- });
130
-
131
- it("treats a non-finite inlineBudget as undefined", async () => {
132
- const { pi, registerTool } = makePi({
133
- getSettings: () => ({ codemode: { inlineBudget: Number.NaN } }),
134
- });
135
- await registerCodemodeToolOverride(pi as never);
136
-
137
- const registered = registerTool.mock.calls[0]![0] as Record<string, unknown>;
138
- const options = (registered as { options?: Record<string, unknown> }).options!;
139
- expect((options.getInlineBudget as () => number | undefined)()).toBeUndefined();
140
- });
141
-
142
- it("returns false and registers nothing when the module cannot be loaded", async () => {
143
- state.module = undefined;
144
- const { pi, registerTool } = makePi();
145
- const ok = await registerCodemodeToolOverride(pi as never);
146
- expect(ok).toBe(false);
147
- expect(registerTool).not.toHaveBeenCalled();
148
- });
149
- });
@@ -1,96 +0,0 @@
1
- import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
- import { loadCodemodeModule } from "./loader.js";
3
- import {
4
- renderCodemodeCall,
5
- renderCodemodeResult,
6
- clearActiveCodemodeIntervals,
7
- } from "./renderer.js";
8
-
9
- export { clearActiveCodemodeIntervals } from "./renderer.js";
10
-
11
- /**
12
- * Register an Archimedes-styled override of the `codemode` tool.
13
- *
14
- * `codemode` is a replaceable pi built-in extension: registering a tool with
15
- * the same name takes over completely (pi omits the built-in one), so the
16
- * override must reproduce the built-in's full definition — the QuickJS
17
- * sandbox executor, the dynamic `prepareLoadout` description, the
18
- * `model-only` exposure, and `defaultActive: false` (the built-in registers
19
- * codemode inactive; it is activated via `--tools`, `defaultTools`, or MCP
20
- * auto-enable). Only the presentation is replaced.
21
- *
22
- * The definition is built from the running CLI's own copy of the codemode
23
- * module (see {@link loadCodemodeModule}) so the executor matches what the
24
- * CLI expects. The `parameters` reference is preserved from the built-in's
25
- * registration when it is still visible (`pi.getAllTools()`): the CLI's
26
- * MCP and tool-search extensions recognise the codemode tool by schema
27
- * identity (`isCodemodeTool`), and a fresh schema object from a second
28
- * module instance would defeat those checks.
29
- *
30
- * Returns `false` when the codemode module cannot be loaded (e.g. a pi
31
- * version without the codemode extension) — pi's native rendering then
32
- * stands in.
33
- *
34
- * Note: the take-over makes pi print a startup notice ("built-in extension
35
- * `codemode` was not loaded") — the same notice a third-party MCP extension
36
- * triggers when it replaces the built-in MCP extension. It is informational:
37
- * the tool still works, now with this package's renderers. Set
38
- * `codemodeToolStyling` to `false` to run the native tool instead.
39
- */
40
- export async function registerCodemodeToolOverride(
41
- pi: ExtensionAPI,
42
- ): Promise<boolean> {
43
- const mod = await loadCodemodeModule();
44
- if (!mod) return false;
45
-
46
- const def = mod.createCodemodeToolDefinition({
47
- appendEntry: (customType: string, data: unknown) =>
48
- pi.appendEntry(customType, data),
49
- models: true,
50
- getToolNamespace: (name: string) =>
51
- (pi.getAllTools().find((t) => t.name === name) as {
52
- namespace?: unknown;
53
- } | undefined)?.namespace,
54
- // `getSettings` is newer than the oldest pi this package type-checks
55
- // against; read it defensively (the built-in extension's wiring).
56
- getMode: () =>
57
- readSettings(pi)?.codemode?.mode === "only" ? "only" : "on",
58
- getInlineBudget: () => {
59
- const budget = readSettings(pi)?.codemode?.inlineBudget;
60
- return typeof budget === "number" && Number.isFinite(budget) && budget >= 0
61
- ? budget
62
- : undefined;
63
- },
64
- }) as Record<string, unknown>;
65
-
66
- // The built-in (when loaded) is still registered at this point: keep its
67
- // schema object reference so `isCodemodeTool` identity checks keep working.
68
- const native = pi.getAllTools().find((t) => t.name === "codemode");
69
- if (native?.parameters !== undefined) {
70
- def.parameters = native.parameters;
71
- }
72
-
73
- pi.registerTool({
74
- ...def,
75
- renderCall: renderCodemodeCall,
76
- renderResult: renderCodemodeResult,
77
- defaultActive: false,
78
- } as never);
79
- return true;
80
- }
81
-
82
- function readSettings(
83
- pi: ExtensionAPI,
84
- ): { codemode?: { mode?: string; inlineBudget?: unknown } } | undefined {
85
- const getSettings = (
86
- pi as unknown as { getSettings?: () => unknown }
87
- ).getSettings;
88
- if (typeof getSettings !== "function") return undefined;
89
- try {
90
- return getSettings.call(pi) as
91
- | { codemode?: { mode?: string; inlineBudget?: unknown } }
92
- | undefined;
93
- } catch {
94
- return undefined;
95
- }
96
- }
@@ -1,47 +0,0 @@
1
- const g: Record<string | symbol, unknown> = globalThis as unknown as typeof global & Record<string | symbol, unknown>;
2
-
3
- const MODEL_SCOPE_RE = /Model scope:\s*(.+)/;
4
- export const CAPTURED_MODELS = Symbol.for("splashscreen:capturedModels");
5
- export const PATCHED_LOG = Symbol.for("splashscreen:logPatched");
6
-
7
- // Store original console.log for restoration
8
- let _origLog: typeof console.log | undefined;
9
-
10
- export function patchConsoleLog(): void {
11
- if (g[PATCHED_LOG]) return;
12
- g[PATCHED_LOG] = true;
13
- _origLog = console.log;
14
-
15
- console.log = (...args: unknown[]) => {
16
- try {
17
- if (args.length === 1 && typeof args[0] === "string") {
18
- const plain = (args[0] as string).replace(/\x1b\[[0-9;]*[a-zA-Z]/g, "");
19
- const m = MODEL_SCOPE_RE.exec(plain);
20
- if (m) {
21
- const raw = m[1]!.replace(/\s*\(Ctrl\+\w[\w\s]*\)/gi, "");
22
- g[CAPTURED_MODELS] = raw
23
- .split(",")
24
- .map((s: string) => s.trim())
25
- .filter(Boolean);
26
- // Unpatch once captured — no need to keep intercepting
27
- unpatchConsoleLog();
28
- return;
29
- }
30
- }
31
- } catch (err) {
32
- // Log the error but don't suppress the original call
33
- console.error("[archimedes] Error in console.log patch:", err);
34
- }
35
- _origLog!.apply(console, args);
36
- };
37
- }
38
-
39
- /** Restore original console.log — call on session_shutdown. */
40
- export function unpatchConsoleLog(): void {
41
- if (!g[PATCHED_LOG]) return;
42
- g[PATCHED_LOG] = false;
43
- if (_origLog) {
44
- console.log = _origLog;
45
- _origLog = undefined;
46
- }
47
- }