@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/thinking/patch.ts
CHANGED
|
@@ -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
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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
|
-
|
|
89
|
+
diag("ui", `[archimedes] Re-patching thinking renderer: pi version changed ${patchVersion} → ${currentVersion}`);
|
|
74
90
|
}
|
|
75
91
|
}
|
|
76
92
|
|
package/src/bash/tool.test.ts
DELETED
|
@@ -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
|
-
});
|
package/src/codemode/loader.ts
DELETED
|
@@ -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
|
-
});
|
package/src/codemode/tool.ts
DELETED
|
@@ -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
|
-
}
|
package/src/startup/capture.ts
DELETED
|
@@ -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
|
-
}
|