diff --git a/packages/ai/src/models.generated.ts b/packages/ai/src/models.generated.ts index d3b8adb1..f5a0c9b8 100644 --- a/packages/ai/src/models.generated.ts +++ b/packages/ai/src/models.generated.ts @@ -5967,9 +5967,9 @@ export const MODELS = { contextWindow: 32768, maxTokens: 4096, } satisfies Model<"openai-completions">, - "anthropic/claude-3.5-haiku": { - id: "anthropic/claude-3.5-haiku", - name: "Anthropic: Claude 3.5 Haiku", + "anthropic/claude-3.5-haiku-20241022": { + id: "anthropic/claude-3.5-haiku-20241022", + name: "Anthropic: Claude 3.5 Haiku (2024-10-22)", api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", @@ -5984,9 +5984,9 @@ export const MODELS = { contextWindow: 200000, maxTokens: 8192, } satisfies Model<"openai-completions">, - "anthropic/claude-3.5-haiku-20241022": { - id: "anthropic/claude-3.5-haiku-20241022", - name: "Anthropic: Claude 3.5 Haiku (2024-10-22)", + "anthropic/claude-3.5-haiku": { + id: "anthropic/claude-3.5-haiku", + name: "Anthropic: Claude 3.5 Haiku", api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", @@ -6018,23 +6018,6 @@ export const MODELS = { contextWindow: 200000, maxTokens: 8192, } satisfies Model<"openai-completions">, - "mistralai/ministral-3b": { - id: "mistralai/ministral-3b", - name: "Mistral: Ministral 3B", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text"], - cost: { - input: 0.04, - output: 0.04, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 4096, - } satisfies Model<"openai-completions">, "mistralai/ministral-8b": { id: "mistralai/ministral-8b", name: "Mistral: Ministral 8B", @@ -6052,6 +6035,23 @@ export const MODELS = { contextWindow: 131072, maxTokens: 4096, } satisfies Model<"openai-completions">, + "mistralai/ministral-3b": { + id: "mistralai/ministral-3b", + name: "Mistral: Ministral 3B", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: false, + input: ["text"], + cost: { + input: 0.04, + output: 0.04, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 131072, + maxTokens: 4096, + } satisfies Model<"openai-completions">, "nvidia/llama-3.1-nemotron-70b-instruct": { id: "nvidia/llama-3.1-nemotron-70b-instruct", name: "NVIDIA: Llama 3.1 Nemotron 70B Instruct", @@ -6137,23 +6137,6 @@ export const MODELS = { contextWindow: 32768, maxTokens: 4096, } satisfies Model<"openai-completions">, - "cohere/command-r-plus-08-2024": { - id: "cohere/command-r-plus-08-2024", - name: "Cohere: Command R+ (08-2024)", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text"], - cost: { - input: 2.5, - output: 10, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 4000, - } satisfies Model<"openai-completions">, "cohere/command-r-08-2024": { id: "cohere/command-r-08-2024", name: "Cohere: Command R (08-2024)", @@ -6171,6 +6154,23 @@ export const MODELS = { contextWindow: 128000, maxTokens: 4000, } satisfies Model<"openai-completions">, + "cohere/command-r-plus-08-2024": { + id: "cohere/command-r-plus-08-2024", + name: "Cohere: Command R+ (08-2024)", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: false, + input: ["text"], + cost: { + input: 2.5, + output: 10, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 4000, + } satisfies Model<"openai-completions">, "sao10k/l3.1-euryale-70b": { id: "sao10k/l3.1-euryale-70b", name: "Sao10K: Llama 3.1 Euryale 70B v2.2", @@ -6222,6 +6222,23 @@ export const MODELS = { contextWindow: 128000, maxTokens: 16384, } satisfies Model<"openai-completions">, + "meta-llama/llama-3.1-8b-instruct": { + id: "meta-llama/llama-3.1-8b-instruct", + name: "Meta: Llama 3.1 8B Instruct", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: false, + input: ["text"], + cost: { + input: 0.02, + output: 0.03, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 131072, + maxTokens: 16384, + } satisfies Model<"openai-completions">, "meta-llama/llama-3.1-405b-instruct": { id: "meta-llama/llama-3.1-405b-instruct", name: "Meta: Llama 3.1 405B Instruct", @@ -6256,23 +6273,6 @@ export const MODELS = { contextWindow: 131072, maxTokens: 4096, } satisfies Model<"openai-completions">, - "meta-llama/llama-3.1-8b-instruct": { - id: "meta-llama/llama-3.1-8b-instruct", - name: "Meta: Llama 3.1 8B Instruct", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text"], - cost: { - input: 0.02, - output: 0.03, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 16384, - } satisfies Model<"openai-completions">, "mistralai/mistral-nemo": { id: "mistralai/mistral-nemo", name: "Mistral: Mistral Nemo", @@ -6460,23 +6460,6 @@ export const MODELS = { contextWindow: 128000, maxTokens: 64000, } satisfies Model<"openai-completions">, - "meta-llama/llama-3-8b-instruct": { - id: "meta-llama/llama-3-8b-instruct", - name: "Meta: Llama 3 8B Instruct", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text"], - cost: { - input: 0.03, - output: 0.06, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 8192, - maxTokens: 16384, - } satisfies Model<"openai-completions">, "meta-llama/llama-3-70b-instruct": { id: "meta-llama/llama-3-70b-instruct", name: "Meta: Llama 3 70B Instruct", @@ -6494,6 +6477,23 @@ export const MODELS = { contextWindow: 8192, maxTokens: 16384, } satisfies Model<"openai-completions">, + "meta-llama/llama-3-8b-instruct": { + id: "meta-llama/llama-3-8b-instruct", + name: "Meta: Llama 3 8B Instruct", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: false, + input: ["text"], + cost: { + input: 0.03, + output: 0.06, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 8192, + maxTokens: 16384, + } satisfies Model<"openai-completions">, "mistralai/mixtral-8x22b-instruct": { id: "mistralai/mixtral-8x22b-instruct", name: "Mistral: Mixtral 8x22B Instruct", diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index a6dc00f4..35c689b6 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Fixed + +- Fixed TUI performance regression caused by Box component lacking render caching. Built-in tools now use Text directly (like v0.22.5), and Box has proper caching for custom tool rendering. + ## [0.23.0] - 2025-12-17 ### Added diff --git a/packages/coding-agent/docs/custom-tools.md b/packages/coding-agent/docs/custom-tools.md index cb33133d..66beb71d 100644 --- a/packages/coding-agent/docs/custom-tools.md +++ b/packages/coding-agent/docs/custom-tools.md @@ -7,7 +7,7 @@ Custom tools extend pi with new capabilities beyond the built-in read/write/edit Create a file `~/.pi/agent/tools/hello.ts`: ```typescript -import { Type } from "@mariozechner/pi-coding-agent"; +import { Type } from "@sinclair/typebox"; import type { CustomToolFactory } from "@mariozechner/pi-coding-agent"; const factory: CustomToolFactory = (pi) => ({ @@ -47,7 +47,7 @@ The tool is automatically discovered and available in your next pi session. ## Tool Definition ```typescript -import { Type } from "@mariozechner/pi-coding-agent"; +import { Type } from "@sinclair/typebox"; import { StringEnum } from "@mariozechner/pi-ai"; import { Text } from "@mariozechner/pi-tui"; import type { CustomToolFactory, ToolSessionEvent } from "@mariozechner/pi-coding-agent"; diff --git a/packages/coding-agent/examples/custom-tools/README.md b/packages/coding-agent/examples/custom-tools/README.md index 6c8aa08b..522f193e 100644 --- a/packages/coding-agent/examples/custom-tools/README.md +++ b/packages/coding-agent/examples/custom-tools/README.md @@ -43,7 +43,7 @@ See [docs/custom-tools.md](../../docs/custom-tools.md) for full documentation. **Factory pattern:** ```typescript -import { Type } from "@mariozechner/pi-coding-agent"; +import { Type } from "@sinclair/typebox"; import { StringEnum } from "@mariozechner/pi-ai"; import { Text } from "@mariozechner/pi-tui"; import type { CustomToolFactory } from "@mariozechner/pi-coding-agent"; diff --git a/packages/coding-agent/examples/custom-tools/hello.ts b/packages/coding-agent/examples/custom-tools/hello.ts index 405ddd2d..a599e756 100644 --- a/packages/coding-agent/examples/custom-tools/hello.ts +++ b/packages/coding-agent/examples/custom-tools/hello.ts @@ -1,4 +1,4 @@ -import { Type } from "@mariozechner/pi-coding-agent"; +import { Type } from "@sinclair/typebox"; import type { CustomToolFactory } from "@mariozechner/pi-coding-agent"; const factory: CustomToolFactory = (pi) => ({ diff --git a/packages/coding-agent/examples/custom-tools/question.ts b/packages/coding-agent/examples/custom-tools/question.ts index d7787042..c21add67 100644 --- a/packages/coding-agent/examples/custom-tools/question.ts +++ b/packages/coding-agent/examples/custom-tools/question.ts @@ -2,7 +2,7 @@ * Question Tool - Let the LLM ask the user a question with options */ -import { Type } from "@mariozechner/pi-coding-agent"; +import { Type } from "@sinclair/typebox"; import { Text } from "@mariozechner/pi-tui"; import type { CustomAgentTool, CustomToolFactory } from "@mariozechner/pi-coding-agent"; diff --git a/packages/coding-agent/examples/custom-tools/todo.ts b/packages/coding-agent/examples/custom-tools/todo.ts index e88dcf03..aa14003e 100644 --- a/packages/coding-agent/examples/custom-tools/todo.ts +++ b/packages/coding-agent/examples/custom-tools/todo.ts @@ -8,7 +8,7 @@ * The onSession callback reconstructs state by scanning past tool results. */ -import { Type } from "@mariozechner/pi-coding-agent"; +import { Type } from "@sinclair/typebox"; import { StringEnum } from "@mariozechner/pi-ai"; import { Text } from "@mariozechner/pi-tui"; import type { CustomAgentTool, CustomToolFactory, ToolSessionEvent } from "@mariozechner/pi-coding-agent"; diff --git a/packages/coding-agent/src/core/custom-tools/loader.ts b/packages/coding-agent/src/core/custom-tools/loader.ts index 77ba3cac..ccd5f814 100644 --- a/packages/coding-agent/src/core/custom-tools/loader.ts +++ b/packages/coding-agent/src/core/custom-tools/loader.ts @@ -4,13 +4,35 @@ import { spawn } from "node:child_process"; import * as fs from "node:fs"; +import { createRequire } from "node:module"; import * as os from "node:os"; import * as path from "node:path"; +import { fileURLToPath } from "node:url"; import { createJiti } from "jiti"; import { getAgentDir } from "../../config.js"; import type { HookUIContext } from "../hooks/types.js"; import type { CustomToolFactory, CustomToolsLoadResult, ExecResult, LoadedCustomTool, ToolAPI } from "./types.js"; +// Create require function to resolve module paths at runtime +const require = createRequire(import.meta.url); + +// Lazily computed aliases - resolved at runtime to handle global installs +let _aliases: Record | null = null; +function getAliases(): Record { + if (_aliases) return _aliases; + + const __dirname = path.dirname(fileURLToPath(import.meta.url)); + const packageIndex = path.resolve(__dirname, "../..", "index.js"); + + _aliases = { + "@mariozechner/pi-coding-agent": packageIndex, + "@mariozechner/pi-tui": require.resolve("@mariozechner/pi-tui"), + "@mariozechner/pi-ai": require.resolve("@mariozechner/pi-ai"), + "@sinclair/typebox": require.resolve("@sinclair/typebox"), + }; + return _aliases; +} + const UNICODE_SPACES = /[\u00A0\u2000-\u200A\u202F\u205F\u3000]/g; function normalizeUnicodeSpaces(str: string): string { @@ -109,7 +131,11 @@ async function loadTool( try { // Create jiti instance for TypeScript/ESM loading - const jiti = createJiti(import.meta.url); + // Use aliases to resolve package imports since tools are loaded from user directories + // (e.g. ~/.pi/agent/tools) but import from packages installed with pi-coding-agent + const jiti = createJiti(import.meta.url, { + alias: getAliases(), + }); // Import the module const module = await jiti.import(resolvedPath, { default: true }); diff --git a/packages/coding-agent/src/index.ts b/packages/coding-agent/src/index.ts index 2fef905e..89f606cf 100644 --- a/packages/coding-agent/src/index.ts +++ b/packages/coding-agent/src/index.ts @@ -1,7 +1,4 @@ // Core session management - -// Re-export Type from typebox for custom tools -export { Type } from "@sinclair/typebox"; export { AgentSession, type AgentSessionConfig, diff --git a/packages/coding-agent/src/modes/interactive/components/tool-execution.ts b/packages/coding-agent/src/modes/interactive/components/tool-execution.ts index 8bd10424..6d2d7d2d 100644 --- a/packages/coding-agent/src/modes/interactive/components/tool-execution.ts +++ b/packages/coding-agent/src/modes/interactive/components/tool-execution.ts @@ -40,8 +40,8 @@ export interface ToolExecutionOptions { * Component that renders a tool call with its result (updateable) */ export class ToolExecutionComponent extends Container { - private contentBox: Box; - private contentText: Text; // For built-in tools + private contentBox?: Box; // Only used for custom tools + private contentText: Text; // For built-in tools (with its own padding/bg) private imageComponents: Image[] = []; private toolName: string; private args: any; @@ -64,12 +64,16 @@ export class ToolExecutionComponent extends Container { this.addChild(new Spacer(1)); - // Box wraps content with padding and background - this.contentBox = new Box(1, 1, (text: string) => theme.bg("toolPendingBg", text)); - this.addChild(this.contentBox); - - // Text component for built-in tool rendering - this.contentText = new Text("", 0, 0); + if (customTool) { + // Custom tools use Box for flexible component rendering + this.contentBox = new Box(1, 1, (text: string) => theme.bg("toolPendingBg", text)); + this.addChild(this.contentBox); + this.contentText = new Text("", 0, 0); // Fallback only + } else { + // Built-in tools use Text directly (has caching, better perf) + this.contentText = new Text("", 1, 1, (text: string) => theme.bg("toolPendingBg", text)); + this.addChild(this.contentText); + } this.updateDisplay(); } @@ -110,11 +114,12 @@ export class ToolExecutionComponent extends Container { ? (text: string) => theme.bg("toolErrorBg", text) : (text: string) => theme.bg("toolSuccessBg", text); - this.contentBox.setBgFn(bgFn); - this.contentBox.clear(); - // Check for custom tool rendering - if (this.customTool) { + if (this.customTool && this.contentBox) { + // Custom tools use Box for flexible component rendering + this.contentBox.setBgFn(bgFn); + this.contentBox.clear(); + // Render call component if (this.customTool.renderCall) { try { @@ -157,9 +162,9 @@ export class ToolExecutionComponent extends Container { } } } else { - // Built-in tool: use existing formatToolExecution + // Built-in tools: use Text directly with caching + this.contentText.setCustomBgFn(bgFn); this.contentText.setText(this.formatToolExecution()); - this.contentBox.addChild(this.contentText); } // Handle images (same for both custom and built-in) diff --git a/packages/coding-agent/tsconfig.examples.json b/packages/coding-agent/tsconfig.examples.json index a94bb560..453240bf 100644 --- a/packages/coding-agent/tsconfig.examples.json +++ b/packages/coding-agent/tsconfig.examples.json @@ -5,7 +5,10 @@ "baseUrl": ".", "paths": { "@mariozechner/pi-coding-agent": ["./src/index.ts"], - "@mariozechner/pi-coding-agent/hooks": ["./src/core/hooks/index.ts"] + "@mariozechner/pi-coding-agent/hooks": ["./src/core/hooks/index.ts"], + "@mariozechner/pi-tui": ["../tui/src/index.ts"], + "@mariozechner/pi-ai": ["../ai/src/index.ts"], + "@sinclair/typebox": ["../../node_modules/@sinclair/typebox"] }, "skipLibCheck": true }, diff --git a/packages/tui/src/components/box.ts b/packages/tui/src/components/box.ts index ede2aa14..3d4f2a88 100644 --- a/packages/tui/src/components/box.ts +++ b/packages/tui/src/components/box.ts @@ -10,6 +10,12 @@ export class Box implements Component { private paddingY: number; private bgFn?: (text: string) => string; + // Cache for rendered output + private cachedWidth?: number; + private cachedChildLines?: string; + private cachedBgSample?: string; + private cachedLines?: string[]; + constructor(paddingX = 1, paddingY = 1, bgFn?: (text: string) => string) { this.paddingX = paddingX; this.paddingY = paddingY; @@ -18,24 +24,36 @@ export class Box implements Component { addChild(component: Component): void { this.children.push(component); + this.invalidateCache(); } removeChild(component: Component): void { const index = this.children.indexOf(component); if (index !== -1) { this.children.splice(index, 1); + this.invalidateCache(); } } clear(): void { this.children = []; + this.invalidateCache(); } setBgFn(bgFn?: (text: string) => string): void { this.bgFn = bgFn; + // Don't invalidate here - we'll detect bgFn changes by sampling output + } + + private invalidateCache(): void { + this.cachedWidth = undefined; + this.cachedChildLines = undefined; + this.cachedBgSample = undefined; + this.cachedLines = undefined; } invalidate(): void { + this.invalidateCache(); for (const child of this.children) { child.invalidate?.(); } @@ -62,6 +80,20 @@ export class Box implements Component { return []; } + // Check if bgFn output changed by sampling + const bgSample = this.bgFn ? this.bgFn("test") : undefined; + + // Check cache validity + const childLinesKey = childLines.join("\n"); + if ( + this.cachedLines && + this.cachedWidth === width && + this.cachedChildLines === childLinesKey && + this.cachedBgSample === bgSample + ) { + return this.cachedLines; + } + // Apply background and padding const result: string[] = []; @@ -80,6 +112,12 @@ export class Box implements Component { result.push(this.applyBg("", width)); } + // Update cache + this.cachedWidth = width; + this.cachedChildLines = childLinesKey; + this.cachedBgSample = bgSample; + this.cachedLines = result; + return result; } diff --git a/tsconfig.json b/tsconfig.json index 815531f2..54923e61 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -12,6 +12,7 @@ "@mariozechner/pi-coding-agent": ["./packages/coding-agent/src/index.ts"], "@mariozechner/pi-coding-agent/hooks": ["./packages/coding-agent/src/core/hooks/index.ts"], "@mariozechner/pi-coding-agent/*": ["./packages/coding-agent/src/*"], + "@sinclair/typebox": ["./node_modules/@sinclair/typebox"], "@mariozechner/pi-mom": ["./packages/mom/src/index.ts"], "@mariozechner/pi-mom/*": ["./packages/mom/src/*"], "@mariozechner/pi": ["./packages/pods/src/index.ts"],