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:
@@ -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(),
|
||||
|
||||
Reference in New Issue
Block a user