chore(vendor): import pi 1.3.0

Imported as a linear vendor commit so forge rebases do not replay the upstream root tree.

git-subtree-dir: third_party/pi

git-subtree-split: 71023aec1546d6d677a65669c5665c0927ce0dd0
This commit is contained in:
2026-10-15 17:58:48 -04:00
parent 719370c8f7
commit 6081e3e325
1743 changed files with 205460 additions and 131705 deletions

View File

@@ -1,7 +1,8 @@
import { join, resolve } from "node:path";
import { Text, type TUI, type TuiMouseEvent } from "@earendil-works/pi-tui";
import { resetCapabilitiesCache, setCapabilities, Text, type TUI, type TuiMouseEvent } from "@earendil-works/pi-tui";
import { Type } from "typebox";
import { beforeAll, describe, expect, test } from "vitest";
import { afterEach, beforeAll, describe, expect, test, vi } from "vitest";
import { getReadmePath } from "../src/config.ts";
import type { ToolDefinition } from "../src/core/extensions/types.ts";
import { type BashOperations, createBashToolDefinition } from "../src/core/tools/bash.ts";
@@ -25,6 +26,10 @@ function createBaseToolDefinition(name = "custom_tool"): ToolDefinition {
};
}
// Small 2x2 blue JPEG image
const TINY_JPEG =
"/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAMCAgMCAgMDAwMEAwMEBQgFBQQEBQoHBwYIDAoMDAsKCwsNDhIQDQ4RDgsLEBYQERMUFRUVDA8XGBYUGBIUFRT/2wBDAQMEBAUEBQkFBQkUDQsNFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBT/wAARCAACAAIDAREAAhEBAxEB/8QAFAABAAAAAAAAAAAAAAAAAAAACf/EABQQAQAAAAAAAAAAAAAAAAAAAAD/xAAVAQEBAAAAAAAAAAAAAAAAAAAGCf/EABQRAQAAAAAAAAAAAAAAAAAAAAD/2gAMAwEAAhEDEQA/AD3VTB3/2Q==";
function createFakeTui(): TUI {
return {
requestRender: () => {},
@@ -35,6 +40,30 @@ describe("ToolExecutionComponent parity", () => {
beforeAll(() => {
initTheme("dark");
});
afterEach(() => {
resetCapabilitiesCache();
vi.useRealTimers();
});
// Issue #10292: the component loads the PNG transcoder itself, so this works in any TUI host.
// Issue #8577: a replaced partial image must not resurface.
test("converts non-PNG tool images once the transcoder loads", async () => {
setCapabilities({ images: "kitty", trueColor: true, hyperlinks: true });
const component = new ToolExecutionComponent("tool", "id", {}, {}, undefined, createFakeTui(), process.cwd());
component.updateResult(
{ content: [{ type: "image", data: "cGFydGlhbA==", mimeType: "image/jpeg" }], isError: false },
true,
);
component.updateResult({ content: [{ type: "image", data: TINY_JPEG, mimeType: "image/jpeg" }], isError: false });
await vi.waitFor(() => expect(component.render(120).join("\n")).toContain(";iVBORw0KGgo"));
const rendered = component.render(120).join("\n");
expect(rendered).not.toContain("cGFydGlhbA==");
// Invalidation reuses the converted Image, so the Kitty image ID stays the same.
component.invalidate();
expect(component.render(120).join("\n")).toBe(rendered);
});
test("stacks custom call and result renderers like the old implementation", () => {
const toolDefinition: ToolDefinition = {
@@ -192,6 +221,74 @@ describe("ToolExecutionComponent parity", () => {
expect(rendered).not.toContain("[Showing lines 2001-4000 of 4000. Full output:");
});
// Issue #9628: keep short durations precise and make long shell durations readable.
test.each([
{ ms: 0, formatted: "0.0s" },
{ ms: 4_200, formatted: "4.2s" },
{ ms: 59_900, formatted: "59.9s" },
{ ms: 59_999, formatted: "60.0s" },
{ ms: 60_000, formatted: "1m 0s" },
{ ms: 90_900, formatted: "1m 30s" },
{ ms: 1_592_200, formatted: "26m 32s" },
{ ms: 3_599_999, formatted: "59m 59s" },
{ ms: 3_600_000, formatted: "1h 0m 0s" },
{ ms: 7_384_900, formatted: "2h 3m 4s" },
])("bash renderer formats $ms ms as $formatted while running and after completion", ({ ms, formatted }) => {
vi.useFakeTimers();
vi.setSystemTime(0);
const component = new ToolExecutionComponent(
"bash",
"tool-bash-duration",
{ command: "long-running-command" },
{},
createBashToolDefinition(process.cwd(), { exposeSessionEnvironment: false }),
createFakeTui(),
process.cwd(),
);
component.markExecutionStarted();
component.updateResult({ content: [], isError: false }, true);
vi.advanceTimersByTime(ms);
component.invalidate();
const running = stripAnsi(component.render(120).join("\n"));
component.updateResult({ content: [], isError: false }, false);
const completed = stripAnsi(component.render(120).join("\n"));
vi.advanceTimersByTime(1_000);
component.invalidate();
expect(stripAnsi(component.render(120).join("\n"))).toBe(completed);
expect(running).toContain(`Elapsed ${formatted}`);
expect(completed).toContain(`Took ${formatted}`);
});
// #10549
test("bash renderer shows a result's recorded duration, also for a result restored without a live start", () => {
const render = (live: boolean): string => {
vi.useFakeTimers();
vi.setSystemTime(0);
const component = new ToolExecutionComponent(
"bash",
"tool-bash-recorded",
{ command: "sleep 4" },
{},
createBashToolDefinition(process.cwd(), { exposeSessionEnvironment: false }),
createFakeTui(),
process.cwd(),
);
if (live) {
component.markExecutionStarted();
component.updateResult({ content: [], isError: false }, true);
// The wall clock jumps; the recorded duration does not.
vi.advanceTimersByTime(3_600_000);
}
component.updateResult({ content: [], isError: false, durationMs: 4_200 }, false);
return stripAnsi(component.render(120).join("\n"));
};
expect(render(true)).toContain("Took 4.2s");
expect(render(false)).toContain("Took 4.2s");
});
test("does not duplicate built-in headers when passed the active built-in definition", () => {
const component = new ToolExecutionComponent(
"read",
@@ -207,6 +304,22 @@ describe("ToolExecutionComponent parity", () => {
expect(rendered.match(/\bread\b/g)?.length ?? 0).toBe(1);
});
// Issue #9996: strict tool schemas make models send null for omitted optional fields.
test("renders read calls with null offset and limit as full-file reads", () => {
const component = new ToolExecutionComponent(
"read",
"tool-read-null-range",
{ path: "src/example.ts", offset: null, limit: null },
{},
createReadToolDefinition(process.cwd()),
createFakeTui(),
process.cwd(),
);
const rendered = stripAnsi(component.render(120).join("\n"));
expect(rendered).toContain("read src/example.ts");
expect(rendered).not.toContain("src/example.ts:");
});
test("inherits missing built-in result renderer slot from the built-in tool", () => {
const overrideDefinition: ToolDefinition = {
...createBaseToolDefinition("read"),
@@ -345,6 +458,33 @@ describe("ToolExecutionComponent parity", () => {
expect(rendered).toContain("arg:bar");
});
test("shows arguments in the fallback call header", () => {
const longValue = "x".repeat(200);
const component = new ToolExecutionComponent(
"custom_tool",
"tool-args",
{ query: "pi", long: longValue, text: "line one\nline two" },
{},
createBaseToolDefinition(),
createFakeTui(),
process.cwd(),
);
const collapsed = stripAnsi(component.render(300).join("\n"));
expect(collapsed).toContain('custom_tool query="pi" long="xxx');
expect(collapsed).toContain("...");
expect(collapsed).not.toContain(longValue);
component.setExpanded(true);
const expanded = stripAnsi(component.render(300).join("\n"));
expect(expanded).toContain(" query: pi");
expect(expanded).toContain(longValue);
const expandedLines = expanded.split("\n").map((line) => line.trimEnd());
const textLine = expandedLines.findIndex((line) => line.endsWith(" text: line one"));
expect(textLine).toBeGreaterThan(-1);
expect(expandedLines[textLine + 1]).toMatch(/^\s+ {4}line two$/);
});
test("collapses fallback results until expanded", () => {
const toolDefinition: ToolDefinition = {
...createBaseToolDefinition(),