@oh-my-pi/pi-coding-agent 12.16.0 → 12.17.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/CHANGELOG.md CHANGED
@@ -2,6 +2,17 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [12.17.0] - 2026-02-21
6
+ ### Added
7
+
8
+ - Added timeout protection (5 seconds) for system prompt preparation with graceful fallback to minimal context on timeout
9
+
10
+ ### Changed
11
+
12
+ - Replaced glob-based AGENTS.md discovery with depth-limited directory traversal (depth 1-4) for improved performance and control
13
+ - Refactored system prompt preparation to parallelize file loading operations with a 5-second timeout to prevent startup hangs
14
+ - Unified `renderCall` signatures to `(args, options, theme)` across all tool renderers and extension types
15
+
5
16
  ## [12.16.0] - 2026-02-21
6
17
 
7
18
  ### Added
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oh-my-pi/pi-coding-agent",
3
- "version": "12.16.0",
3
+ "version": "12.17.0",
4
4
  "description": "Coding agent CLI with read, bash, edit, write tools and session management",
5
5
  "type": "module",
6
6
  "bin": {
@@ -85,12 +85,12 @@
85
85
  },
86
86
  "dependencies": {
87
87
  "@mozilla/readability": "0.6.0",
88
- "@oh-my-pi/omp-stats": "12.16.0",
89
- "@oh-my-pi/pi-agent-core": "12.16.0",
90
- "@oh-my-pi/pi-ai": "12.16.0",
91
- "@oh-my-pi/pi-natives": "12.16.0",
92
- "@oh-my-pi/pi-tui": "12.16.0",
93
- "@oh-my-pi/pi-utils": "12.16.0",
88
+ "@oh-my-pi/omp-stats": "12.17.0",
89
+ "@oh-my-pi/pi-agent-core": "12.17.0",
90
+ "@oh-my-pi/pi-ai": "12.17.0",
91
+ "@oh-my-pi/pi-natives": "12.17.0",
92
+ "@oh-my-pi/pi-tui": "12.17.0",
93
+ "@oh-my-pi/pi-utils": "12.17.0",
94
94
  "@sinclair/typebox": "^0.34.48",
95
95
  "@xterm/headless": "^6.0.0",
96
96
  "ajv": "^8.18.0",
@@ -182,7 +182,7 @@ export interface CustomTool<TParams extends TSchema = TSchema, TDetails = any> {
182
182
  /** Called on session lifecycle events - use to reconstruct state or cleanup resources */
183
183
  onSession?: (event: CustomToolSessionEvent, ctx: CustomToolContext) => void | Promise<void>;
184
184
  /** Custom rendering for tool call display - return a Component */
185
- renderCall?: (args: Static<TParams>, theme: Theme) => Component;
185
+ renderCall?: (args: Static<TParams>, options: RenderResultOptions, theme: Theme) => Component;
186
186
 
187
187
  /** Custom rendering for tool result display - return a Component */
188
188
  renderResult?: (
@@ -304,7 +304,7 @@ export interface ToolDefinition<TParams extends TSchema = TSchema, TDetails = un
304
304
  onSession?: (event: ToolSessionEvent, ctx: ExtensionContext) => void | Promise<void>;
305
305
 
306
306
  /** Custom rendering for tool call display */
307
- renderCall?: (args: Static<TParams>, theme: Theme) => Component;
307
+ renderCall?: (args: Static<TParams>, options: ToolRenderResultOptions, theme: Theme) => Component;
308
308
 
309
309
  /** Custom rendering for tool result display */
310
310
  renderResult?: (
@@ -18,7 +18,7 @@ export class RegisteredToolAdapter implements AgentTool<any, any, any> {
18
18
  declare parameters: any;
19
19
  declare label: string;
20
20
 
21
- renderCall?: (args: any, theme: any) => any;
21
+ renderCall?: (args: any, options: any, theme: any) => any;
22
22
  renderResult?: (result: any, options: any, theme: any, args?: any) => any;
23
23
 
24
24
  constructor(
@@ -32,7 +32,8 @@ export class RegisteredToolAdapter implements AgentTool<any, any, any> {
32
32
  // enters the custom-renderer path, gets undefined back, and silently
33
33
  // discards tool result text (extensions without renderers show blank).
34
34
  if (registeredTool.definition.renderCall) {
35
- this.renderCall = (args: any, theme: any) => registeredTool.definition.renderCall!(args, theme as Theme);
35
+ this.renderCall = (args: any, options: any, theme: any) =>
36
+ registeredTool.definition.renderCall!(args, options, theme as Theme);
36
37
  }
37
38
  if (registeredTool.definition.renderResult) {
38
39
  this.renderResult = (result: any, options: any, theme: any, args?: any) =>
package/src/lsp/render.ts CHANGED
@@ -31,7 +31,7 @@ import type { LspParams, LspToolDetails } from "./types";
31
31
  * Render the LSP tool call in the TUI.
32
32
  * Shows: "lsp <operation> <file/filecount>"
33
33
  */
34
- export function renderCall(args: LspParams, theme: Theme): Text {
34
+ export function renderCall(args: LspParams, _options: RenderResultOptions, theme: Theme): Text {
35
35
  const actionLabel = (args.action ?? "request").replace(/_/g, " ");
36
36
  const queryPreview = args.query ? truncateToWidth(args.query, TRUNCATE_LENGTHS.SHORT) : undefined;
37
37
 
@@ -198,7 +198,7 @@ export class MCPTool implements CustomTool<TSchema, MCPToolDetails> {
198
198
  this.mcpServerName = connection.name;
199
199
  }
200
200
 
201
- renderCall(args: unknown, theme: Theme) {
201
+ renderCall(args: unknown, _options: RenderResultOptions, theme: Theme) {
202
202
  return renderMCPCall((args ?? {}) as Record<string, unknown>, theme, this.label);
203
203
  }
204
204
 
@@ -304,7 +304,7 @@ export class DeferredMCPTool implements CustomTool<TSchema, MCPToolDetails> {
304
304
  this.#fallbackProviderName = source?.providerName;
305
305
  }
306
306
 
307
- renderCall(args: unknown, theme: Theme) {
307
+ renderCall(args: unknown, _options: RenderResultOptions, theme: Theme) {
308
308
  return renderMCPCall((args ?? {}) as Record<string, unknown>, theme, this.label);
309
309
  }
310
310
 
@@ -391,7 +391,7 @@ export class ToolExecutionComponent extends Container {
391
391
  const shouldRenderCall = !this.#result || !mergeCallAndResult;
392
392
  if (shouldRenderCall && tool.renderCall) {
393
393
  try {
394
- const callComponent = tool.renderCall(this.#getCallArgsForRender(), theme);
394
+ const callComponent = tool.renderCall(this.#getCallArgsForRender(), this.#renderState, theme);
395
395
  if (callComponent) {
396
396
  this.#contentBox.addChild(ensureInvalidate(callComponent));
397
397
  }
@@ -453,7 +453,7 @@ export class ToolExecutionComponent extends Container {
453
453
  if (shouldRenderCall) {
454
454
  // Render call component
455
455
  try {
456
- const callComponent = renderer.renderCall(this.#getCallArgsForRender(), theme, this.#renderState);
456
+ const callComponent = renderer.renderCall(this.#getCallArgsForRender(), this.#renderState, theme);
457
457
  if (callComponent) {
458
458
  this.#contentBox.addChild(ensureInvalidate(callComponent));
459
459
  }
@@ -19,7 +19,6 @@ import {
19
19
  ToolUIKit,
20
20
  truncateDiffByHunk,
21
21
  } from "../tools/render-utils";
22
- import type { RenderCallOptions } from "../tools/renderers";
23
22
  import { Ellipsis, Hasher, type RenderCache, renderStatusLine, truncateToWidth } from "../tui";
24
23
  import type { DiffError, DiffResult, Operation } from "./types";
25
24
 
@@ -254,7 +253,7 @@ function renderDiffSection(
254
253
  export const editToolRenderer = {
255
254
  mergeCallAndResult: true,
256
255
 
257
- renderCall(args: EditRenderArgs, uiTheme: Theme, options?: RenderCallOptions): Component {
256
+ renderCall(args: EditRenderArgs, options: RenderResultOptions, uiTheme: Theme): Component {
258
257
  const ui = new ToolUIKit(uiTheme);
259
258
  const rawPath = args.file_path || args.path || "";
260
259
  const filePath = shortenPath(rawPath);
@@ -1,8 +1,11 @@
1
1
  /**
2
2
  * System prompt construction and project context loading
3
3
  */
4
+
5
+ import * as fs from "node:fs";
4
6
  import * as os from "node:os";
5
- import { $env, hasFsCode, isEnoent, logger } from "@oh-my-pi/pi-utils";
7
+ import * as path from "node:path";
8
+ import { $env, hasFsCode, isEnoent, logger, untilAborted } from "@oh-my-pi/pi-utils";
6
9
  import { getGpuCachePath, getProjectDir } from "@oh-my-pi/pi-utils/dirs";
7
10
  import { $ } from "bun";
8
11
  import { contextFileCapability } from "./capability/context-file";
@@ -49,47 +52,38 @@ async function loadPreloadedSkillContents(preloadedSkills: Skill[]): Promise<Pre
49
52
  * Returns structured git data or null if not in a git repo.
50
53
  */
51
54
  export async function loadGitContext(cwd: string): Promise<GitContext | null> {
52
- const runGit = async (args: string[], timeoutMs = 1500): Promise<string | null> => {
55
+ const timeout = 3000;
56
+ const abortSignal = AbortSignal.timeout(timeout);
57
+
58
+ const git = async (...args: string[]): Promise<string | null> => {
53
59
  const proc = Bun.spawn(["git", ...args], {
54
60
  cwd,
55
61
  stdout: "pipe",
56
62
  stderr: "ignore",
63
+ timeout: timeout,
64
+ });
65
+ return untilAborted(abortSignal, async () => {
66
+ const exitCode = await proc.exited;
67
+ const stdout = await proc.stdout.text();
68
+ return exitCode === 0 ? stdout.trim() : null;
57
69
  });
58
- const stdoutPromise = proc.stdout ? new Response(proc.stdout).text() : Promise.resolve("");
59
- const race = await Promise.race([
60
- proc.exited.then(() => "exited" as const),
61
- Bun.sleep(timeoutMs).then(() => "timeout" as const),
62
- ]);
63
-
64
- if (race === "timeout") {
65
- proc.kill();
66
- await stdoutPromise.catch(() => null);
67
- logger.debug("Git context command timed out", { cwd, args, timeoutMs });
68
- return null;
69
- }
70
-
71
- const exitCode = await proc.exited;
72
- const stdout = await stdoutPromise.catch(() => "");
73
- if (exitCode !== 0) return null;
74
-
75
- const trimmed = stdout.trim();
76
- return trimmed.length > 0 ? trimmed : "";
77
70
  };
71
+
78
72
  // Check if inside a git repo
79
- const isGitRepo = await runGit(["rev-parse", "--is-inside-work-tree"]);
73
+ const isGitRepo = await git("rev-parse", "--is-inside-work-tree");
80
74
  if (isGitRepo !== "true") return null;
81
- const currentBranch = await runGit(["rev-parse", "--abbrev-ref", "HEAD"]);
75
+ const currentBranch = await git("rev-parse", "--abbrev-ref", "HEAD");
82
76
  if (!currentBranch) return null;
83
77
  let mainBranch = "main";
84
- const mainExists = await runGit(["rev-parse", "--verify", "main"]);
78
+ const mainExists = await git("rev-parse", "--verify", "main");
85
79
  if (mainExists === null) {
86
- const masterExists = await runGit(["rev-parse", "--verify", "master"]);
80
+ const masterExists = await git("rev-parse", "--verify", "master");
87
81
  if (masterExists !== null) mainBranch = "master";
88
82
  }
89
83
 
90
84
  const [status, commits] = await Promise.all([
91
- runGit(["status", "--porcelain", "--untracked-files=no"], 2000),
92
- runGit(["log", "--oneline", "-5"]),
85
+ git("status", "--porcelain", "--untracked-files=no"),
86
+ git("log", "--oneline", "-5"),
93
87
  ]);
94
88
  return {
95
89
  isRepo: true,
@@ -117,8 +111,11 @@ function parseWmicTable(output: string, header: string): string | null {
117
111
  return filtered[0] ?? null;
118
112
  }
119
113
 
120
- const AGENTS_MD_PATTERN = "**/AGENTS.md";
114
+ const AGENTS_MD_MIN_DEPTH = 1;
115
+ const AGENTS_MD_MAX_DEPTH = 4;
121
116
  const AGENTS_MD_LIMIT = 200;
117
+ const SYSTEM_PROMPT_PREP_TIMEOUT_MS = 5000;
118
+ const AGENTS_MD_EXCLUDED_DIRS = new Set(["node_modules", ".git"]);
122
119
 
123
120
  interface AgentsMdSearch {
124
121
  scopePath: string;
@@ -131,27 +128,75 @@ function normalizePath(value: string): string {
131
128
  return value.replace(/\\/g, "/");
132
129
  }
133
130
 
134
- function listAgentsMdFiles(root: string, limit: number): string[] {
131
+ function shouldSkipAgentsDir(name: string): boolean {
132
+ if (AGENTS_MD_EXCLUDED_DIRS.has(name)) return true;
133
+ return name.startsWith(".");
134
+ }
135
+
136
+ async function collectAgentsMdFiles(
137
+ root: string,
138
+ dir: string,
139
+ depth: number,
140
+ limit: number,
141
+ discovered: Set<string>,
142
+ ): Promise<void> {
143
+ if (depth > AGENTS_MD_MAX_DEPTH || discovered.size >= limit) {
144
+ return;
145
+ }
146
+
147
+ let entries: fs.Dirent[];
135
148
  try {
136
- const entries = Array.from(
137
- new Bun.Glob(AGENTS_MD_PATTERN).scanSync({ cwd: root, onlyFiles: true, dot: false, absolute: false }),
138
- );
139
- const normalized = entries
140
- .map(entry => normalizePath(entry))
141
- .filter(entry => entry.length > 0 && !entry.includes("node_modules"))
142
- .sort();
143
- return normalized.length > limit ? normalized.slice(0, limit) : normalized;
149
+ entries = await fs.promises.readdir(dir, { withFileTypes: true });
150
+ } catch {
151
+ return;
152
+ }
153
+
154
+ if (depth >= AGENTS_MD_MIN_DEPTH) {
155
+ const hasAgentsMd = entries.some(entry => entry.isFile() && entry.name === "AGENTS.md");
156
+ if (hasAgentsMd) {
157
+ const relPath = normalizePath(path.relative(root, path.join(dir, "AGENTS.md")));
158
+ if (relPath.length > 0) {
159
+ discovered.add(relPath);
160
+ }
161
+ if (discovered.size >= limit) {
162
+ return;
163
+ }
164
+ }
165
+ }
166
+
167
+ if (depth === AGENTS_MD_MAX_DEPTH) {
168
+ return;
169
+ }
170
+
171
+ const childDirs = entries
172
+ .filter(entry => entry.isDirectory() && !shouldSkipAgentsDir(entry.name))
173
+ .map(entry => entry.name)
174
+ .sort();
175
+
176
+ await Promise.all(
177
+ childDirs.map(async child => {
178
+ if (discovered.size >= limit) return;
179
+ await collectAgentsMdFiles(root, path.join(dir, child), depth + 1, limit, discovered);
180
+ }),
181
+ );
182
+ }
183
+
184
+ async function listAgentsMdFiles(root: string, limit: number): Promise<string[]> {
185
+ try {
186
+ const discovered = new Set<string>();
187
+ await collectAgentsMdFiles(root, root, 0, limit, discovered);
188
+ return Array.from(discovered).sort().slice(0, limit);
144
189
  } catch {
145
190
  return [];
146
191
  }
147
192
  }
148
193
 
149
- function buildAgentsMdSearch(cwd: string): AgentsMdSearch {
150
- const files = listAgentsMdFiles(cwd, AGENTS_MD_LIMIT);
194
+ async function buildAgentsMdSearch(cwd: string): Promise<AgentsMdSearch> {
195
+ const files = await listAgentsMdFiles(cwd, AGENTS_MD_LIMIT);
151
196
  return {
152
197
  scopePath: ".",
153
198
  limit: AGENTS_MD_LIMIT,
154
- pattern: AGENTS_MD_PATTERN,
199
+ pattern: `AGENTS.md depth ${AGENTS_MD_MIN_DEPTH}-${AGENTS_MD_MAX_DEPTH}`,
155
200
  files,
156
201
  };
157
202
  }
@@ -463,12 +508,114 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
463
508
  rules,
464
509
  } = options;
465
510
  const resolvedCwd = cwd ?? getProjectDir();
466
- const resolvedCustomPrompt = await resolvePromptInput(customPrompt, "system prompt");
467
- const resolvedAppendPrompt = await resolvePromptInput(appendSystemPrompt, "append system prompt");
511
+ const preloadedSkills = providedPreloadedSkills;
512
+
513
+ const prepPromise = (async () => {
514
+ const systemPromptCustomizationPromise = (async () => {
515
+ const customization = await loadSystemPromptFiles({ cwd: resolvedCwd });
516
+ debugStartup("system-prompt:loadSystemPromptFiles:done");
517
+ return customization;
518
+ })();
519
+ const contextFilesPromise = providedContextFiles
520
+ ? Promise.resolve(providedContextFiles)
521
+ : loadProjectContextFiles({ cwd: resolvedCwd });
522
+ const agentsMdSearchPromise = buildAgentsMdSearch(resolvedCwd);
523
+ const skillsPromise: Promise<Skill[]> =
524
+ providedSkills !== undefined
525
+ ? Promise.resolve(providedSkills)
526
+ : skillsSettings?.enabled !== false
527
+ ? loadSkills({ ...skillsSettings, cwd: resolvedCwd }).then(result => result.skills)
528
+ : Promise.resolve([]);
529
+ const preloadedSkillContentsPromise = (async () => {
530
+ debugStartup("system-prompt:loadPreloadedSkills:start");
531
+ const loaded = preloadedSkills ? await loadPreloadedSkillContents(preloadedSkills) : [];
532
+ debugStartup("system-prompt:loadPreloadedSkills:done");
533
+ return loaded;
534
+ })();
535
+ const gitPromise = (async () => {
536
+ debugStartup("system-prompt:loadGitContext:start");
537
+ const loaded = await loadGitContext(resolvedCwd);
538
+ debugStartup("system-prompt:loadGitContext:done");
539
+ return loaded;
540
+ })();
541
+
542
+ const [
543
+ resolvedCustomPrompt,
544
+ resolvedAppendPrompt,
545
+ systemPromptCustomization,
546
+ contextFiles,
547
+ agentsMdSearch,
548
+ skills,
549
+ preloadedSkillContents,
550
+ git,
551
+ ] = await Promise.all([
552
+ resolvePromptInput(customPrompt, "system prompt"),
553
+ resolvePromptInput(appendSystemPrompt, "append system prompt"),
554
+ systemPromptCustomizationPromise,
555
+ contextFilesPromise,
556
+ agentsMdSearchPromise,
557
+ skillsPromise,
558
+ preloadedSkillContentsPromise,
559
+ gitPromise,
560
+ ]);
468
561
 
469
- // Load SYSTEM.md customization (prepended to prompt)
470
- const systemPromptCustomization = await loadSystemPromptFiles({ cwd: resolvedCwd });
471
- debugStartup("system-prompt:loadSystemPromptFiles:done");
562
+ return {
563
+ resolvedCustomPrompt,
564
+ resolvedAppendPrompt,
565
+ systemPromptCustomization,
566
+ contextFiles,
567
+ agentsMdSearch,
568
+ skills,
569
+ preloadedSkillContents,
570
+ git,
571
+ };
572
+ })();
573
+
574
+ const prepResult = await Promise.race([
575
+ prepPromise
576
+ .then(value => ({ type: "ready" as const, value }))
577
+ .catch(error => ({ type: "error" as const, error })),
578
+ Bun.sleep(SYSTEM_PROMPT_PREP_TIMEOUT_MS).then(() => ({ type: "timeout" as const })),
579
+ ]);
580
+
581
+ let resolvedCustomPrompt: string | undefined;
582
+ let resolvedAppendPrompt: string | undefined;
583
+ let systemPromptCustomization: string | null = null;
584
+ let contextFiles: Array<{ path: string; content: string; depth?: number }> = providedContextFiles ?? [];
585
+ let agentsMdSearch: AgentsMdSearch = {
586
+ scopePath: ".",
587
+ limit: AGENTS_MD_LIMIT,
588
+ pattern: `AGENTS.md depth ${AGENTS_MD_MIN_DEPTH}-${AGENTS_MD_MAX_DEPTH}`,
589
+ files: [],
590
+ };
591
+ let skills: Skill[] = providedSkills ?? [];
592
+ let preloadedSkillContents: PreloadedSkill[] = [];
593
+ let git: GitContext | null = null;
594
+
595
+ if (prepResult.type === "timeout") {
596
+ logger.warn("System prompt preparation timed out; using minimal startup context", {
597
+ cwd: resolvedCwd,
598
+ timeoutMs: SYSTEM_PROMPT_PREP_TIMEOUT_MS,
599
+ });
600
+ process.stderr.write(
601
+ `Warning: system prompt preparation timed out after ${SYSTEM_PROMPT_PREP_TIMEOUT_MS}ms; using minimal startup context.\n`,
602
+ );
603
+ } else if (prepResult.type === "error") {
604
+ logger.warn("System prompt preparation failed; using minimal startup context", {
605
+ cwd: resolvedCwd,
606
+ error: String(prepResult.error),
607
+ });
608
+ process.stderr.write("Warning: system prompt preparation failed; using minimal startup context.\n");
609
+ } else {
610
+ resolvedCustomPrompt = prepResult.value.resolvedCustomPrompt;
611
+ resolvedAppendPrompt = prepResult.value.resolvedAppendPrompt;
612
+ systemPromptCustomization = prepResult.value.systemPromptCustomization;
613
+ contextFiles = prepResult.value.contextFiles;
614
+ agentsMdSearch = prepResult.value.agentsMdSearch;
615
+ skills = prepResult.value.skills;
616
+ preloadedSkillContents = prepResult.value.preloadedSkillContents;
617
+ git = prepResult.value.git;
618
+ }
472
619
 
473
620
  const now = new Date();
474
621
  const date = now.toLocaleDateString("en-CA", {
@@ -487,10 +634,6 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
487
634
  timeZoneName: "short",
488
635
  });
489
636
 
490
- // Resolve context files: use provided or discover
491
- const contextFiles = providedContextFiles ?? (await loadProjectContextFiles({ cwd: resolvedCwd }));
492
- const agentsMdSearch = buildAgentsMdSearch(resolvedCwd);
493
-
494
637
  // Build tool descriptions array
495
638
  // Priority: toolNames (explicit list) > tools (Map) > defaults
496
639
  // Default includes both bash and python; actual availability determined by settings in createTools
@@ -512,19 +655,6 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
512
655
  name,
513
656
  description: tools?.get(name)?.description ?? "",
514
657
  }));
515
- // Resolve skills: use provided or discover
516
- const skills =
517
- providedSkills ??
518
- (skillsSettings?.enabled !== false ? (await loadSkills({ ...skillsSettings, cwd: resolvedCwd })).skills : []);
519
- const preloadedSkills = providedPreloadedSkills;
520
- debugStartup("system-prompt:loadPreloadedSkills:start");
521
- const preloadedSkillContents = preloadedSkills ? await loadPreloadedSkillContents(preloadedSkills) : [];
522
- debugStartup("system-prompt:loadPreloadedSkills:done");
523
-
524
- // Get git context
525
- debugStartup("system-prompt:loadGitContext:start");
526
- const git = await loadGitContext(resolvedCwd);
527
- debugStartup("system-prompt:loadGitContext:done");
528
658
 
529
659
  // Filter skills to only include those with read tool
530
660
  const hasRead = tools?.has("read");
@@ -445,7 +445,7 @@ function formatOutputInline(data: unknown, theme: Theme, maxWidth = 80): string
445
445
  /**
446
446
  * Render the tool call arguments.
447
447
  */
448
- export function renderCall(args: TaskParams, theme: Theme): Component {
448
+ export function renderCall(args: TaskParams, _options: RenderResultOptions, theme: Theme): Component {
449
449
  const lines: string[] = [];
450
450
  lines.push(renderStatusLine({ icon: "pending", title: "Task", description: args.agent }, theme));
451
451
 
package/src/tools/ask.ts CHANGED
@@ -380,7 +380,7 @@ interface AskRenderArgs {
380
380
  }
381
381
 
382
382
  export const askToolRenderer = {
383
- renderCall(args: AskRenderArgs, uiTheme: Theme): Component {
383
+ renderCall(args: AskRenderArgs, _options: RenderResultOptions, uiTheme: Theme): Component {
384
384
  const ui = new ToolUIKit(uiTheme);
385
385
  const label = ui.title("Ask");
386
386
 
package/src/tools/bash.ts CHANGED
@@ -237,7 +237,7 @@ function formatBashCommand(args: BashRenderArgs, _uiTheme: Theme): string {
237
237
  export const BASH_PREVIEW_LINES = 10;
238
238
 
239
239
  export const bashToolRenderer = {
240
- renderCall(args: BashRenderArgs, uiTheme: Theme): Component {
240
+ renderCall(args: BashRenderArgs, _options: RenderResultOptions, uiTheme: Theme): Component {
241
241
  const cmdText = formatBashCommand(args, uiTheme);
242
242
  const text = renderStatusLine({ icon: "pending", title: "Bash", description: cmdText }, uiTheme);
243
243
  return new Text(text, 0, 0);
@@ -444,7 +444,7 @@ export const calculatorToolRenderer = {
444
444
  * Render the tool call header showing the first expression and count.
445
445
  * Format: "Calc <expression> (N calcs)"
446
446
  */
447
- renderCall(args: CalculatorRenderArgs, uiTheme: Theme): Component {
447
+ renderCall(args: CalculatorRenderArgs, _options: RenderResultOptions, uiTheme: Theme): Component {
448
448
  const count = args.calculations?.length ?? 0;
449
449
  const firstExpression = args.calculations?.[0]?.expression;
450
450
  const description = firstExpression ? truncateToWidth(firstExpression, TRUNCATE_LENGTHS.TITLE) : undefined;
@@ -967,6 +967,7 @@ function countNonEmptyLines(text: string): number {
967
967
  /** Render fetch call (URL preview) */
968
968
  export function renderFetchCall(
969
969
  args: { url?: string; timeout?: number; raw?: boolean },
970
+ _options: RenderResultOptions,
970
971
  uiTheme: Theme = theme,
971
972
  ): Component {
972
973
  const url = args.url ?? "";
package/src/tools/find.ts CHANGED
@@ -407,7 +407,7 @@ const COLLAPSED_LIST_LIMIT = PREVIEW_LIMITS.COLLAPSED_ITEMS;
407
407
 
408
408
  export const findToolRenderer = {
409
409
  inline: true,
410
- renderCall(args: FindRenderArgs, uiTheme: Theme): Component {
410
+ renderCall(args: FindRenderArgs, _options: RenderResultOptions, uiTheme: Theme): Component {
411
411
  const meta: string[] = [];
412
412
  if (args.limit !== undefined) meta.push(`limit:${args.limit}`);
413
413
 
package/src/tools/grep.ts CHANGED
@@ -309,7 +309,7 @@ const COLLAPSED_TEXT_LIMIT = PREVIEW_LIMITS.COLLAPSED_LINES * 2;
309
309
 
310
310
  export const grepToolRenderer = {
311
311
  inline: true,
312
- renderCall(args: GrepRenderArgs, uiTheme: Theme): Component {
312
+ renderCall(args: GrepRenderArgs, _options: RenderResultOptions, uiTheme: Theme): Component {
313
313
  const meta: string[] = [];
314
314
  if (args.path) meta.push(`in ${args.path}`);
315
315
  if (args.glob) meta.push(`glob:${args.glob}`);
@@ -203,7 +203,7 @@ interface NotebookRenderArgs {
203
203
  const COLLAPSED_TEXT_LIMIT = PREVIEW_LIMITS.COLLAPSED_LINES * 2;
204
204
 
205
205
  export const notebookToolRenderer = {
206
- renderCall(args: NotebookRenderArgs, uiTheme: Theme): Component {
206
+ renderCall(args: NotebookRenderArgs, _options: RenderResultOptions, uiTheme: Theme): Component {
207
207
  const meta: string[] = [];
208
208
  const notebookPath = args.notebookPath ?? args.notebook_path;
209
209
  const cellNumber = args.cellNumber ?? args.cell_index;
@@ -834,7 +834,7 @@ function formatCellOutputLines(
834
834
  }
835
835
 
836
836
  export const pythonToolRenderer = {
837
- renderCall(args: PythonRenderArgs, uiTheme: Theme): Component {
837
+ renderCall(args: PythonRenderArgs, _options: RenderResultOptions, uiTheme: Theme): Component {
838
838
  const ui = new ToolUIKit(uiTheme);
839
839
  const cells = args.cells ?? [];
840
840
  const cwd = getProjectDir();
package/src/tools/read.ts CHANGED
@@ -1077,7 +1077,7 @@ interface ReadRenderArgs {
1077
1077
  }
1078
1078
 
1079
1079
  export const readToolRenderer = {
1080
- renderCall(args: ReadRenderArgs, uiTheme: Theme): Component {
1080
+ renderCall(args: ReadRenderArgs, _options: RenderResultOptions, uiTheme: Theme): Component {
1081
1081
  const rawPath = args.file_path || args.path || "";
1082
1082
  const filePath = shortenPath(rawPath);
1083
1083
  const offset = args.offset;
@@ -23,12 +23,8 @@ import { sshToolRenderer } from "./ssh";
23
23
  import { todoWriteToolRenderer } from "./todo-write";
24
24
  import { writeToolRenderer } from "./write";
25
25
 
26
- export interface RenderCallOptions {
27
- spinnerFrame?: number;
28
- }
29
-
30
26
  type ToolRenderer = {
31
- renderCall: (args: unknown, theme: Theme, options?: RenderCallOptions) => Component;
27
+ renderCall: (args: unknown, options: RenderResultOptions, theme: Theme) => Component;
32
28
  renderResult: (
33
29
  result: { content: Array<{ type: string; text?: string }>; details?: unknown; isError?: boolean },
34
30
  options: RenderResultOptions & { renderContext?: Record<string, unknown> },
@@ -104,7 +104,7 @@ export const reportFindingTool: AgentTool<typeof ReportFindingParams, ReportFind
104
104
  };
105
105
  },
106
106
 
107
- renderCall(args, theme): Component {
107
+ renderCall(args, _options, theme): Component {
108
108
  const { label, icon, color } = getPriorityDisplay(args.priority, theme);
109
109
  const titleText = String(args.title).replace(/^\[P\d\]\s*/, "");
110
110
  return new Text(
package/src/tools/ssh.ts CHANGED
@@ -229,7 +229,7 @@ interface SshRenderContext {
229
229
  }
230
230
 
231
231
  export const sshToolRenderer = {
232
- renderCall(args: SshRenderArgs, uiTheme: Theme): Component {
232
+ renderCall(args: SshRenderArgs, _options: RenderResultOptions, uiTheme: Theme): Component {
233
233
  const host = args.host || "…";
234
234
  const command = args.command || "…";
235
235
  const text = renderStatusLine({ icon: "pending", title: "SSH", description: `[${host}] $ ${command}` }, uiTheme);
@@ -214,7 +214,7 @@ interface TodoWriteRenderArgs {
214
214
  }
215
215
 
216
216
  export const todoWriteToolRenderer = {
217
- renderCall(args: TodoWriteRenderArgs, uiTheme: Theme): Component {
217
+ renderCall(args: TodoWriteRenderArgs, _options: RenderResultOptions, uiTheme: Theme): Component {
218
218
  const count = args.todos?.length ?? 0;
219
219
  const meta = count > 0 ? [`${count} items`] : ["empty"];
220
220
  const text = renderStatusLine({ icon: "pending", title: "Todo Write", meta }, uiTheme);
@@ -28,7 +28,6 @@ import {
28
28
  shortenPath,
29
29
  ToolUIKit,
30
30
  } from "./render-utils";
31
- import type { RenderCallOptions } from "./renderers";
32
31
 
33
32
  const writeSchema = Type.Object({
34
33
  path: Type.String({ description: "Path to the file to write (relative or absolute)" }),
@@ -189,7 +188,7 @@ function renderContentPreview(content: string, expanded: boolean, uiTheme: Theme
189
188
  }
190
189
 
191
190
  export const writeToolRenderer = {
192
- renderCall(args: WriteRenderArgs, uiTheme: Theme, options?: RenderCallOptions): Component {
191
+ renderCall(args: WriteRenderArgs, options: RenderResultOptions, uiTheme: Theme): Component {
193
192
  const ui = new ToolUIKit(uiTheme);
194
193
  const rawPath = args.file_path || args.path || "";
195
194
  const filePath = shortenPath(rawPath);
@@ -285,8 +285,8 @@ export const webSearchCustomTool: CustomTool<typeof webSearchSchema, SearchRende
285
285
  return executeSearch(toolCallId, params);
286
286
  },
287
287
 
288
- renderCall(args: SearchParams, theme: Theme) {
289
- return renderSearchCall(args, theme);
288
+ renderCall(args: SearchParams, options: RenderResultOptions, theme: Theme) {
289
+ return renderSearchCall(args, options, theme);
290
290
  },
291
291
 
292
292
  renderResult(result, options: RenderResultOptions, theme: Theme) {
@@ -405,7 +405,7 @@ Parameters:
405
405
  return executeExaTool("web_search_exa", args, "web_search_deep");
406
406
  },
407
407
 
408
- renderCall(args, theme) {
408
+ renderCall(args, _options, theme) {
409
409
  return renderExaCall(args as Record<string, unknown>, "Deep Search", theme);
410
410
  },
411
411
 
@@ -436,7 +436,7 @@ Parameters:
436
436
  return executeExaTool("get_code_context_exa", params as Record<string, unknown>, "web_search_code_context");
437
437
  },
438
438
 
439
- renderCall(args, theme) {
439
+ renderCall(args, _options, theme) {
440
440
  return renderExaCall(args as Record<string, unknown>, "Code Search", theme);
441
441
  },
442
442
 
@@ -464,7 +464,7 @@ Parameters:
464
464
  return executeExaTool("crawling", params as Record<string, unknown>, "web_search_crawl");
465
465
  },
466
466
 
467
- renderCall(args, theme) {
467
+ renderCall(args, _options, theme) {
468
468
  const url = (args as { url: string }).url;
469
469
  return renderExaCall({ query: url }, "Crawl URL", theme);
470
470
  },
@@ -495,7 +495,7 @@ Parameters:
495
495
  return executeExaTool("linkedin_search", params as Record<string, unknown>, "web_search_linkedin");
496
496
  },
497
497
 
498
- renderCall(args, theme) {
498
+ renderCall(args, _options, theme) {
499
499
  return renderExaCall(args as Record<string, unknown>, "LinkedIn Search", theme);
500
500
  },
501
501
 
@@ -525,7 +525,7 @@ Parameters:
525
525
  return executeExaTool("company_research", params as Record<string, unknown>, "web_search_company");
526
526
  },
527
527
 
528
- renderCall(args, theme) {
528
+ renderCall(args, _options, theme) {
529
529
  const name = (args as { company_name: string }).company_name;
530
530
  return renderExaCall({ query: name }, "Company Research", theme);
531
531
  },
@@ -283,6 +283,7 @@ export function renderSearchResult(
283
283
  /** Render web search call (query preview) */
284
284
  export function renderSearchCall(
285
285
  args: { query?: string; provider?: string; [key: string]: unknown },
286
+ _options: RenderResultOptions,
286
287
  theme: Theme,
287
288
  ): Component {
288
289
  const provider = args.provider ?? "auto";