@pi-archimedes/diff 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.
package/src/index.ts CHANGED
@@ -5,14 +5,16 @@
5
5
  */
6
6
 
7
7
  import type { ExtensionAPI, ExtensionContext, Theme } from "@earendil-works/pi-coding-agent";
8
- import type { SettingItem } from "@earendil-works/pi-tui";
8
+ import type { Component, SettingItem } from "@earendil-works/pi-tui";
9
9
 
10
10
  import { setConfigGetter as setShikiConfig } from "./shiki.js";
11
11
  import { setConfigGetter as setRenderConfig } from "./render/index.js";
12
12
  import { DiffComponent } from "./diff-component.js";
13
13
  import type { DiffBg, DiffColors } from "./ansi/index.js";
14
14
  import { resetDiffColors } from "./ansi/index.js";
15
- import { registerWriteTool, registerEditTool } from "./tools/index.js";
15
+ import { installWriteTool, installEditTool } from "./tools/index.js";
16
+ import { diag } from "./log.js";
17
+ import { reportPiSupport } from "./pi-support.js";
16
18
 
17
19
  // ---------------------------------------------------------------------------
18
20
  // Re-exports
@@ -81,33 +83,58 @@ export function getDiffSettingsItems(overrideConfig?: DiffConfig): SettingItem[]
81
83
  // Extension entry point
82
84
  // ---------------------------------------------------------------------------
83
85
 
84
- // Load SDK dependencies at module evaluation time (top-level await) so they are
85
- // ready before any session_start event fires. If these imports fail the
86
- // extension will skip diff tool registration gracefully.
87
- let _sdk: { createWriteTool: any; createEditTool: any } | null = null;
88
- let _TextComponent: any = null;
86
+ // `Text` is the component the `write` row reuses across repaints. It is loaded
87
+ // at module evaluation time (top-level await) so it is ready before any
88
+ // `session_start`; a failure here means the rows are skipped rather than
89
+ // half-installed.
90
+ let _TextComponent:
91
+ | (new (text?: string, paddingX?: number, paddingY?: number) => Component)
92
+ | null = null;
89
93
 
90
94
  try {
91
- const [sdk, tui] = await Promise.all([
92
- import("@earendil-works/pi-coding-agent"),
93
- import("@earendil-works/pi-tui"),
94
- ]);
95
- _sdk = {
96
- createWriteTool: sdk.createWriteTool,
97
- createEditTool: sdk.createEditTool,
98
- };
95
+ const tui = await import("@earendil-works/pi-tui");
99
96
  _TextComponent = tui.Text;
100
97
  } catch (error) {
101
- console.error(
102
- `[diff] failed to load Pi SDK: ${error instanceof Error ? error.message : String(error)}`,
98
+ // A missing SDK means the diff tool is not registered at all, which is a
99
+ // developer-facing structural failure — the log file, never the console, and
100
+ // with the stack: this error is swallowed, so nothing else ever prints it.
101
+ // (Inlined rather than imported: `describeError` lives in core, which a
102
+ // standalone install does not have — see `pi-support.ts`.)
103
+ diag(
104
+ "diff",
105
+ `[diff] failed to load Pi SDK: ${error instanceof Error ? error.stack ?? error.message : String(error)}`,
103
106
  );
104
107
  }
105
108
 
109
+ /**
110
+ * Install the diff-drawing `edit` and `write` rows.
111
+ *
112
+ * Idempotent per `pi` handle, and it has to be: pi keeps a LIST of resolvers and
113
+ * of event handlers per extension, so registering twice on the same extension
114
+ * stacks a duplicate. It is keyed to the handle rather than to this module
115
+ * because the two lifetimes differ — pi caches the compiled extension MODULE
116
+ * across sessions while every session replacement (`/new`, `/fork`, `/resume`)
117
+ * builds a fresh extension and hands registration a new handle. A module-wide
118
+ * flag would then install for the first session only, and every session after
119
+ * it would get pi's native `edit`/`write` rows and none of the result details
120
+ * the rows read. The theme and config readers are per-session, so those are
121
+ * refreshed on every call.
122
+ */
123
+ const _installedExtensions = new WeakSet<object>();
124
+
106
125
  export function registerDiffTools(
107
126
  pi: ExtensionAPI,
108
127
  _getTheme: () => Theme,
109
128
  readConfig: () => DiffConfig,
110
129
  ): void {
130
+ // Floor check first, before anything can fail: pi 1.x APIs this package now
131
+ // relies on are not guaranteed below 1.2.0, and a peer range cannot warn
132
+ // anyone because pi never installs or solves peers (ADR 0026). Warn and
133
+ // carry on — a `diff` that declined to load would make /plugins report an
134
+ // enabled package that registered nothing. Idempotent per package, and this
135
+ // runs again on every session_start.
136
+ reportPiSupport("diff", "1.2.0");
137
+
111
138
  _readConfig = readConfig;
112
139
 
113
140
  // Reset theme-derived colors so they are re-derived from the current theme
@@ -117,19 +144,33 @@ export function registerDiffTools(
117
144
  setShikiConfig(() => getConfig());
118
145
  setRenderConfig(() => getConfig());
119
146
 
120
- if (!_sdk?.createWriteTool || !_sdk?.createEditTool || !_TextComponent) {
121
- console.warn("[diff] SDK not available, skipping diff tool registration");
147
+ // Already installed on this extension: re-registering would stack a duplicate
148
+ // resolver and handler set. A different handle is a different extension, so
149
+ // it installs even when this module instance is a reused one.
150
+ if (_installedExtensions.has(pi)) return;
151
+
152
+ if (!_TextComponent) {
153
+ diag("diff", "[diff] SDK not available, skipping diff tool registration");
122
154
  return;
123
155
  }
124
156
 
125
157
  const cwd = process.cwd();
126
158
  const home = process.env.HOME ?? "";
127
159
 
128
- // Register write tool override
129
- registerWriteTool(pi, cwd, home, _sdk.createWriteTool, _TextComponent);
160
+ // Capability check, not a version wish: without renderer resolution there is
161
+ // nothing to install, and pi's own rows stand in. The floor check above is
162
+ // what tells the user why.
163
+ if (typeof pi.registerToolRenderer !== "function" || typeof pi.on !== "function") {
164
+ diag("diff", "[diff] pi has no registerToolRenderer(), skipping diff tool installation");
165
+ return;
166
+ }
130
167
 
131
- // Register edit tool override
132
- registerEditTool(pi, cwd, home, _sdk.createEditTool);
168
+ // The rows are chosen by pi's renderer resolution, and the result details
169
+ // each row reads arrive through `tool_result` (ADR 0029). Neither registers a
170
+ // tool, so pi's own `edit` and `write` stay in place.
171
+ installEditTool(pi, cwd, home);
172
+ installWriteTool(pi, cwd, home, _TextComponent);
173
+ _installedExtensions.add(pi);
133
174
  }
134
175
 
135
176
  // ── Default export (for standalone pi.extensions loading) ─────────────────
@@ -0,0 +1,68 @@
1
+ import { describe, it, expect, afterEach } from "vitest";
2
+ import { existsSync, mkdirSync, mkdtempSync, readFileSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import { tmpdir } from "node:os";
5
+ import { randomUUID } from "node:crypto";
6
+
7
+ import { diag } from "./log.js";
8
+
9
+ // Same contract as `@pi-archimedes/core/log` — see `packages/core/src/log.test.ts`.
10
+ // Kept in sync deliberately: this package is standalone, so the sink is a
11
+ // sibling implementation, and a contract test is what keeps them siblings
12
+ // rather than drift.
13
+ const SANDBOX = mkdtempSync(join(tmpdir(), `archimedes-diff-log-${randomUUID()}-`));
14
+ const ENV = "PI_ARCHIMEDES_LOG";
15
+ const previous = process.env[ENV];
16
+
17
+ afterEach(() => {
18
+ if (previous === undefined) delete process.env[ENV];
19
+ else process.env[ENV] = previous;
20
+ });
21
+
22
+ describe("diff diag — the diagnostics sink", () => {
23
+ it("writes nothing when the env var is unset", () => {
24
+ const target = join(SANDBOX, "unset.log");
25
+ process.env[ENV] = target;
26
+ diag("diff", "enabled");
27
+
28
+ delete process.env[ENV];
29
+ expect(() => diag("diff", "a message")).not.toThrow();
30
+ expect(readFileSync(target, "utf8").trimEnd().split("\n")).toHaveLength(1);
31
+ });
32
+
33
+ it("appends a timestamped, scoped line", () => {
34
+ const target = join(SANDBOX, "write.log");
35
+ process.env[ENV] = target;
36
+
37
+ diag("diff", "SDK not available");
38
+
39
+ const lines = readFileSync(target, "utf8").trimEnd().split("\n");
40
+ expect(lines).toHaveLength(1);
41
+ const match = /^\[(\d{4}-\d{2}-\d{2}T[\d:.]+Z?)\] \[(.+?)\] (.*)$/.exec(lines[0]!);
42
+ expect(match, `unexpected line shape: ${lines[0]}`).not.toBeNull();
43
+ expect(match![2]).toBe("diff");
44
+ expect(match![3]).toBe("SDK not available");
45
+ });
46
+
47
+ it("appends across calls", () => {
48
+ const target = join(SANDBOX, "append.log");
49
+ process.env[ENV] = target;
50
+
51
+ diag("diff", "one");
52
+ diag("diff", "two");
53
+
54
+ expect(readFileSync(target, "utf8").trimEnd().split("\n")).toHaveLength(2);
55
+ });
56
+
57
+ it("never throws on an unwritable target, and creates no directories", () => {
58
+ const missing = join(SANDBOX, "nope", "log.txt");
59
+ process.env[ENV] = missing;
60
+ expect(() => diag("diff", "ENOENT")).not.toThrow();
61
+ expect(existsSync(join(SANDBOX, "nope"))).toBe(false);
62
+
63
+ const dir = join(SANDBOX, "directory");
64
+ mkdirSync(dir, { recursive: true });
65
+ process.env[ENV] = dir;
66
+ expect(() => diag("diff", "EISDIR")).not.toThrow();
67
+ });
68
+ });
package/src/log.ts ADDED
@@ -0,0 +1,34 @@
1
+ import { appendFileSync } from "node:fs";
2
+
3
+ // ── Diagnostics sink (sibling of `@pi-archimedes/core/log`) ─────────────────
4
+ //
5
+ // Nothing in this package writes to the console: on fullscreen screen a stray
6
+ // write lands inside the differential frame and smears the viewport, and inside
7
+ // a subagent child it is discarded anyway (`subagent/src/stream.ts` drops every
8
+ // non-JSON stdout line). Diagnostics therefore go to `$PI_ARCHIMEDES_LOG`, and
9
+ // messages the user must see go to the notification surface.
10
+ //
11
+ // Why a copy rather than the import: `diff` is standalone, and `pi-support.ts`
12
+ // is the one place that states what that costs and why it is worth it. The
13
+ // canonical implementation is `packages/core/src/log.ts`; keep the two in step
14
+ // (pinned here by `log.test.ts`, which mirrors `core/src/log.test.ts`).
15
+
16
+ /**
17
+ * Append one diagnostic line to `$PI_ARCHIMEDES_LOG`.
18
+ *
19
+ * No-op when the variable is unset or empty. Never throws: losing a log line is
20
+ * always cheaper than failing a render or a tool call, which is what these
21
+ * calls sit next to.
22
+ *
23
+ * @param scope the package the line came from (`"diff"`)
24
+ * @param message the diagnostic text
25
+ */
26
+ export function diag(scope: string, message: string): void {
27
+ const file = process.env.PI_ARCHIMEDES_LOG;
28
+ if (!file) return;
29
+ try {
30
+ appendFileSync(file, `[${new Date().toISOString()}] [${scope}] ${message}\n`);
31
+ } catch {
32
+ // Logging must never be the reason something failed.
33
+ }
34
+ }
@@ -0,0 +1,113 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
2
+
3
+ // ── What this file pins ─────────────────────────────────────────────────────
4
+ //
5
+ // `diff` carries its own copy of the pi-version guard because it is a standalone
6
+ // package with no dependency on `@pi-archimedes/core` (see the header of
7
+ // `pi-support.ts`). A copy is only safe if it is pinned twice: once here, and
8
+ // once in `packages/core/src/pi-support.test.ts`. The two ends of the contract
9
+ // that matters — the `Symbol.for` key and the entry shape — are asserted in both
10
+ // files, so the copies cannot drift apart silently and leave `diff`'s warning
11
+ // out of the suite's aggregated notice.
12
+
13
+ const host = vi.hoisted(() => ({ version: "1.2.0" }));
14
+
15
+ vi.mock("@earendil-works/pi-coding-agent", () => ({
16
+ get VERSION() {
17
+ return host.version;
18
+ },
19
+ }));
20
+
21
+ import { reportPiSupport, piSupportWarnings } from "./pi-support.js";
22
+
23
+ const WARNINGS_KEY = Symbol.for("archimedes:piSupportWarnings");
24
+ const globals = globalThis as Record<symbol, unknown>;
25
+
26
+ function wipeRegistry(): void {
27
+ delete globals[WARNINGS_KEY];
28
+ }
29
+
30
+ beforeEach(() => {
31
+ wipeRegistry();
32
+ host.version = "1.2.0";
33
+ });
34
+
35
+ afterEach(() => {
36
+ wipeRegistry();
37
+ });
38
+
39
+ describe("diff's pi-support guard", () => {
40
+ it("says nothing on a supported host, at the floor or above", () => {
41
+ host.version = "1.2.0";
42
+ reportPiSupport("diff", "1.2.0");
43
+ host.version = "1.10.0";
44
+ reportPiSupport("diff", "1.2.0");
45
+ expect(piSupportWarnings()).toEqual([]);
46
+ });
47
+
48
+ it("records the package, the floor and the running version below the floor", () => {
49
+ host.version = "0.99.2";
50
+ reportPiSupport("diff", "1.2.0");
51
+ expect(piSupportWarnings()).toEqual([{ pkg: "diff", min: "1.2.0", actual: "0.99.2" }]);
52
+ });
53
+
54
+ it("orders by number, not by string (1.10.0 is not older than 1.2.0)", () => {
55
+ host.version = "1.10.0";
56
+ reportPiSupport("diff", "1.2.0");
57
+ expect(piSupportWarnings()).toEqual([]);
58
+ });
59
+
60
+ it("is idempotent per package — session_start re-registers on /new and /reload", () => {
61
+ host.version = "0.99.2";
62
+ reportPiSupport("diff", "1.2.0");
63
+ reportPiSupport("diff", "1.2.0");
64
+ expect(piSupportWarnings()).toHaveLength(1);
65
+ });
66
+
67
+ it("never throws, whatever the host reports as its version", () => {
68
+ // A guard that can throw can break registration, which is the one thing it
69
+ // must never do.
70
+ host.version = "nonsense";
71
+ expect(() => reportPiSupport("diff", "1.2.0")).not.toThrow();
72
+ delete (host as { version?: string }).version;
73
+ expect(() => reportPiSupport("diff", "1.2.0")).not.toThrow();
74
+ });
75
+
76
+ it("writes nothing to the console, on either path", () => {
77
+ const log = vi.spyOn(console, "log").mockImplementation(() => {});
78
+ const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
79
+ const error = vi.spyOn(console, "error").mockImplementation(() => {});
80
+
81
+ host.version = "1.2.0";
82
+ reportPiSupport("diff", "1.2.0");
83
+ host.version = "0.99.2";
84
+ reportPiSupport("diff", "1.2.0");
85
+
86
+ expect(log).not.toHaveBeenCalled();
87
+ expect(warn).not.toHaveBeenCalled();
88
+ expect(error).not.toHaveBeenCalled();
89
+ log.mockRestore();
90
+ warn.mockRestore();
91
+ error.mockRestore();
92
+ });
93
+ });
94
+
95
+ describe("the registry contract with @pi-archimedes/core", () => {
96
+ it("writes into the same Symbol.for key and entry shape core reads", () => {
97
+ host.version = "0.99.2";
98
+ reportPiSupport("diff", "1.2.0");
99
+ const registry = globals[WARNINGS_KEY] as Record<string, { pkg: string; min: string; actual: string }>;
100
+ expect(registry).toBeTypeOf("object");
101
+ // The exact shape `core/pi-support.ts` builds its aggregated notice from —
102
+ // if this changes, meta stops naming `diff` and no test would notice
103
+ // without this assertion.
104
+ expect(registry["diff"]).toEqual({ pkg: "diff", min: "1.2.0", actual: "0.99.2" });
105
+ });
106
+
107
+ it("leaves entries written by another package alone (co-existence, not ownership)", () => {
108
+ globals[WARNINGS_KEY] = { ui: { pkg: "ui", min: "1.2.0", actual: "0.99.2" } };
109
+ host.version = "0.99.2";
110
+ reportPiSupport("diff", "1.2.0");
111
+ expect(piSupportWarnings().map((w) => w.pkg)).toEqual(["ui", "diff"]);
112
+ });
113
+ });
@@ -0,0 +1,98 @@
1
+ import { VERSION } from "@earendil-works/pi-coding-agent";
2
+ import { diag } from "./log.js";
3
+
4
+ // ── pi-version support guard (sibling of `@pi-archimedes/core/pi-support`) ───
5
+ //
6
+ // `diff` is a standalone, independently installable package: it declares no
7
+ // dependency on `@pi-archimedes/core` (AGENTS.md, README), so anything it needs
8
+ // from the suite it carries itself. That trade now has exactly three entries —
9
+ // the xterm colour table in `ansi/colors.ts`, the diagnostics sink in `log.ts`,
10
+ // and this version guard. This file is the one place that states the reason, so
11
+ // the other two point here rather than each repeating it; the reason is simply
12
+ // that `pi install @pi-archimedes/diff` must keep working with nothing else
13
+ // from the suite present.
14
+ //
15
+ // Why the check exists at all: a `peerDependencies` range reaches nobody. `pi
16
+ // install` omits peers, and the loader aliases host specifiers to pi's own
17
+ // copies, so the only thing that can tell a user their pi is older than this
18
+ // package was built for is a comparison against the `VERSION` the running host
19
+ // exports (ADR 0026). The answer is a warning, never a refusal — a `diff` that
20
+ // declined to load would make `/plugins` report an enabled package that
21
+ // registered nothing.
22
+ //
23
+ // How the warning is seen: this writes into the same `globalThis` registry core
24
+ // owns, under the same `Symbol.for` key and the same plain-object entry shape.
25
+ // A symbol-keyed global is the only shared memory a standalone package and the
26
+ // suite have, and it means an installed suite still aggregates `diff` into its
27
+ // one notice while a standalone `diff` leaves the entry for `piSupportWarnings()`
28
+ // and the log file. `pi-support.test.ts` here and
29
+ // `packages/core/src/pi-support.test.ts` pin both ends of that contract; keep
30
+ // the two copies in step.
31
+
32
+ /** Below-floor report. Shape shared with `core/pi-support.ts` — do not drift. */
33
+ export interface PiSupportWarning {
34
+ pkg: string;
35
+ min: string;
36
+ actual: string;
37
+ }
38
+
39
+ /** The key `@pi-archimedes/core/pi-support` reads to build its notice. */
40
+ const WARNINGS_KEY = Symbol.for("archimedes:piSupportWarnings");
41
+
42
+ type Registry = Record<string, PiSupportWarning>;
43
+
44
+ function registry(): Registry {
45
+ const existing = (globalThis as Record<symbol, unknown>)[WARNINGS_KEY];
46
+ return (existing as Registry | undefined) ?? {};
47
+ }
48
+
49
+ /**
50
+ * Numeric three-component comparison, copied from
51
+ * `packages/core/src/pi-support.ts`: -1 / 0 / 1, leading `v` and missing
52
+ * components tolerated, a non-numeric component counting as 0 rather than NaN.
53
+ * Numeric, not lexical, because `1.10.0` must outrank `1.2.0`.
54
+ */
55
+ function compareVersions(a: string, b: string): number {
56
+ const part = (v: string, i: number): number => {
57
+ const n = Number(v.replace(/^v/, "").split(".")[i] ?? "0");
58
+ return Number.isFinite(n) ? n : 0;
59
+ };
60
+ for (let i = 0; i < 3; i++) {
61
+ const left = part(a, i);
62
+ const right = part(b, i);
63
+ if (left > right) return 1;
64
+ if (left < right) return -1;
65
+ }
66
+ return 0;
67
+ }
68
+
69
+ /**
70
+ * Record a below-floor warning. Idempotent per package, silent by construction
71
+ * (no console write on either path), and never throws — this runs at the top of
72
+ * registration, where the only acceptable failure mode is "nothing happened,
73
+ * carry on registering".
74
+ */
75
+ export function reportPiSupport(pkg: string, min: string): void {
76
+ try {
77
+ const actual = typeof VERSION === "string" ? VERSION : "";
78
+ if (compareVersions(actual, min) >= 0) return;
79
+ const current = registry();
80
+ if (pkg in current) return;
81
+ current[pkg] = { pkg, min, actual };
82
+ (globalThis as Record<symbol, unknown>)[WARNINGS_KEY] = current;
83
+ // A standalone install has no aggregator, so the log file is the only other
84
+ // place this can go.
85
+ diag(pkg, `pi ${actual} is below this package's floor (>=${min}); loaded anyway`);
86
+ } catch {
87
+ // A guard that can throw is a guard that can break registration.
88
+ }
89
+ }
90
+
91
+ /** Recorded warnings, in report order — the standalone view of the registry. */
92
+ export function piSupportWarnings(): PiSupportWarning[] {
93
+ try {
94
+ return Object.values(registry());
95
+ } catch {
96
+ return [];
97
+ }
98
+ }
@@ -0,0 +1,81 @@
1
+ /**
2
+ * The `registerDiffTools` install guard (ADR 0029).
3
+ *
4
+ * pi keeps a LIST of resolvers and of event handlers per extension, so a second
5
+ * registration on the SAME extension would stack a duplicate. But a session
6
+ * replacement (`/new`, `/fork`, `/resume`) reuses pi's cached extension MODULE
7
+ * while creating a FRESH extension — meta's own comment records exactly that —
8
+ * so installation has to be keyed to the `pi` handle it is registering on, not
9
+ * to the module. A module-wide flag hands every session after the first pi's
10
+ * native `edit`/`write` rows and no result details at all.
11
+ */
12
+
13
+ import { describe, expect, it, vi } from "vitest";
14
+ import { registerDiffTools } from "./index.js";
15
+
16
+ /** A `pi` handle that records what this registration asked for. */
17
+ function fakePi() {
18
+ const renderers: ((name: string, next: () => unknown) => unknown)[] = [];
19
+ const events: string[] = [];
20
+ return {
21
+ pi: {
22
+ registerTool: vi.fn(),
23
+ registerToolRenderer: (r: (name: string, next: () => unknown) => unknown) => {
24
+ renderers.push(r);
25
+ },
26
+ on: (event: string) => {
27
+ events.push(event);
28
+ return () => {};
29
+ },
30
+ },
31
+ renderers,
32
+ events,
33
+ };
34
+ }
35
+
36
+ const themeGetter = () => ({}) as never;
37
+ const configGetter = () => ({
38
+ diffTheme: "github-dark",
39
+ diffSplitMinWidth: 150,
40
+ diffSplitMinCodeWidth: 60,
41
+ });
42
+
43
+ describe("registerDiffTools install scope", () => {
44
+ it("does not stack a second set of renderers on the same pi handle", () => {
45
+ const a = fakePi();
46
+ // Two `session_start`s on one extension (the case the guard exists for).
47
+ registerDiffTools(a.pi as never, themeGetter, configGetter);
48
+ registerDiffTools(a.pi as never, themeGetter, configGetter);
49
+
50
+ expect(a.renderers).toHaveLength(2); // edit + write, once each
51
+ expect(a.events.filter((e) => e === "tool_result")).toHaveLength(2);
52
+ expect(a.events.filter((e) => e === "tool_call")).toHaveLength(1);
53
+ });
54
+
55
+ it("installs again for a fresh pi handle sharing the cached module", () => {
56
+ const first = fakePi();
57
+ registerDiffTools(first.pi as never, themeGetter, configGetter);
58
+
59
+ // A session replacement: same imported module, brand new extension.
60
+ const second = fakePi();
61
+ registerDiffTools(second.pi as never, themeGetter, configGetter);
62
+
63
+ expect(second.renderers).toHaveLength(2);
64
+ expect(second.events.filter((e) => e === "tool_result")).toHaveLength(2);
65
+ expect(second.events.filter((e) => e === "tool_call")).toHaveLength(1);
66
+
67
+ // The renderers the new extension was handed actually resolve, so the rows
68
+ // are the Archimedes ones rather than pi's natives.
69
+ const native = Object.freeze({ native: true });
70
+ for (const name of ["edit", "write"]) {
71
+ const resolved = second.renderers.map((r) => r(name, () => native));
72
+ const mine = resolved.filter((x) => x !== native);
73
+ expect(mine).toHaveLength(1);
74
+ expect(typeof (mine[0] as { renderCall?: unknown }).renderCall).toBe("function");
75
+ expect(typeof (mine[0] as { renderResult?: unknown }).renderResult).toBe("function");
76
+ // Tools this extension does not own fall through to the next resolver.
77
+ const other = second.renderers.map((r) => r("read", () => native));
78
+ expect(other.every((x) => x === native)).toBe(true);
79
+ }
80
+ });
81
+ });