@oh-my-pi/pi-coding-agent 13.17.0 → 13.17.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/CHANGELOG.md CHANGED
@@ -2,6 +2,18 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [13.17.1] - 2026-04-01
6
+ ### Removed
7
+
8
+ - Removed `code_search` tool for code snippet and documentation search
9
+
10
+ ### Fixed
11
+
12
+ - Fixed edit tool diff rendering to wrap long diff lines with continuation gutters instead of truncating them at terminal width ([#578](https://github.com/can1357/oh-my-pi/issues/578))
13
+ - Fixed `--list-models` and `/model` provider filtering to hide models from disabled providers ([#588](https://github.com/can1357/oh-my-pi/issues/588))
14
+ - Fixed edit tool diffstats to use diff-specific add/remove theme colors instead of success/error status colors ([#589](https://github.com/can1357/oh-my-pi/issues/589))
15
+
16
+
5
17
  ## [13.17.0] - 2026-03-30
6
18
 
7
19
  ### Added
@@ -6451,4 +6463,4 @@ Initial public release.
6451
6463
  - Git branch display in footer
6452
6464
  - Message queueing during streaming responses
6453
6465
  - OAuth integration for Gmail and Google Calendar access
6454
- - HTML export with syntax highlighting and collapsible sections
6466
+ - HTML export with syntax highlighting and collapsible sections
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@oh-my-pi/pi-coding-agent",
4
- "version": "13.17.0",
4
+ "version": "13.17.1",
5
5
  "description": "Coding agent CLI with read, bash, edit, write tools and session management",
6
6
  "homepage": "https://github.com/can1357/oh-my-pi",
7
7
  "author": "Can Boluk",
@@ -42,12 +42,12 @@
42
42
  "dependencies": {
43
43
  "@agentclientprotocol/sdk": "0.16.1",
44
44
  "@mozilla/readability": "^0.6",
45
- "@oh-my-pi/omp-stats": "13.17.0",
46
- "@oh-my-pi/pi-agent-core": "13.17.0",
47
- "@oh-my-pi/pi-ai": "13.17.0",
48
- "@oh-my-pi/pi-natives": "13.17.0",
49
- "@oh-my-pi/pi-tui": "13.17.0",
50
- "@oh-my-pi/pi-utils": "13.17.0",
45
+ "@oh-my-pi/omp-stats": "13.17.1",
46
+ "@oh-my-pi/pi-agent-core": "13.17.1",
47
+ "@oh-my-pi/pi-ai": "13.17.1",
48
+ "@oh-my-pi/pi-natives": "13.17.1",
49
+ "@oh-my-pi/pi-tui": "13.17.1",
50
+ "@oh-my-pi/pi-utils": "13.17.1",
51
51
  "@sinclair/typebox": "^0.34",
52
52
  "@xterm/headless": "^6.0",
53
53
  "ajv": "^8.18",
@@ -189,12 +189,11 @@ function extractFileHeader(diff: string): string {
189
189
  return headerLines.join("\n");
190
190
  }
191
191
 
192
- function joinPatch(parts: string[]): string {
193
- return parts
192
+ export function joinPatch(parts: string[]): string {
193
+ return `${parts
194
194
  .map(part => (part.endsWith("\n") ? part : `${part}\n`))
195
195
  .join("\n")
196
- .trimEnd()
197
- .concat("\n");
196
+ .replace(/\n+$/, "")}\n`;
198
197
  }
199
198
 
200
199
  function selectHunks(file: FileHunks, selector: HunkSelection["hunks"]): FileHunks["hunks"] {
@@ -1,7 +1,7 @@
1
1
  import type { ThinkingLevel } from "@oh-my-pi/pi-agent-core";
2
2
  import type { Api, Model } from "@oh-my-pi/pi-ai";
3
3
  import { MODEL_ROLE_IDS } from "../config/model-registry";
4
- import { parseModelPattern, resolveModelRoleValue } from "../config/model-resolver";
4
+ import { parseModelPattern, resolveModelRoleValue, resolveRoleSelection } from "../config/model-resolver";
5
5
  import type { Settings } from "../config/settings";
6
6
  import MODEL_PRIO from "../priority.json" with { type: "json" };
7
7
 
@@ -11,24 +11,6 @@ export interface ResolvedCommitModel {
11
11
  thinkingLevel?: ThinkingLevel;
12
12
  }
13
13
 
14
- function resolveRoleSelection(
15
- roles: readonly string[],
16
- settings: Settings,
17
- availableModels: Model<Api>[],
18
- ): { model: Model<Api>; thinkingLevel?: ThinkingLevel } | undefined {
19
- const matchPreferences = { usageOrder: settings.getStorage()?.getModelUsageOrder() };
20
- for (const role of roles) {
21
- const resolved = resolveModelRoleValue(settings.getModelRole(role), availableModels, {
22
- settings,
23
- matchPreferences,
24
- });
25
- if (resolved.model) {
26
- return { model: resolved.model, thinkingLevel: resolved.thinkingLevel };
27
- }
28
- }
29
- return undefined;
30
- }
31
-
32
14
  export async function resolvePrimaryModel(
33
15
  override: string | undefined,
34
16
  settings: Settings,
@@ -31,7 +31,7 @@ import { type ConfigError, ConfigFile } from "../config";
31
31
  import { parseModelString } from "../config/model-resolver";
32
32
  import { isValidThemeColor, type ThemeColor } from "../modes/theme/theme";
33
33
  import type { AuthStorage, OAuthCredential } from "../session/auth-storage";
34
- import type { Settings } from "./settings";
34
+ import { type Settings, settings } from "./settings";
35
35
 
36
36
  export const kNoAuth = "N/A";
37
37
 
@@ -730,6 +730,14 @@ function normalizeSuppressedSelector(selector: string): string {
730
730
  return `${parsed.provider}/${parsed.id}`;
731
731
  }
732
732
 
733
+ function getDisabledProviderIdsFromSettings(): Set<string> {
734
+ try {
735
+ return new Set(settings.get("disabledProviders"));
736
+ } catch {
737
+ return new Set();
738
+ }
739
+ }
740
+
733
741
  /**
734
742
  * Model registry - loads and manages models, resolves API keys via AuthStorage.
735
743
  */
@@ -1670,11 +1678,19 @@ export class ModelRegistry {
1670
1678
  * This is a fast check that doesn't refresh OAuth tokens.
1671
1679
  */
1672
1680
  getAvailable(): Model<Api>[] {
1673
- return this.#models.filter(m => this.#keylessProviders.has(m.provider) || this.authStorage.hasAuth(m.provider));
1681
+ const disabledProviders = getDisabledProviderIdsFromSettings();
1682
+ return this.#models.filter(
1683
+ m =>
1684
+ !disabledProviders.has(m.provider) &&
1685
+ (this.#keylessProviders.has(m.provider) || this.authStorage.hasAuth(m.provider)),
1686
+ );
1674
1687
  }
1675
1688
 
1676
1689
  getDiscoverableProviders(): string[] {
1677
- return this.#discoverableProviders.map(provider => provider.provider);
1690
+ const disabledProviders = getDisabledProviderIdsFromSettings();
1691
+ return this.#discoverableProviders
1692
+ .filter(provider => !disabledProviders.has(provider.provider))
1693
+ .map(provider => provider.provider);
1678
1694
  }
1679
1695
 
1680
1696
  getProviderDiscoveryState(provider: string): ProviderDiscoveryState | undefined {
@@ -587,6 +587,27 @@ export function resolveModelOverride(
587
587
  return { explicitThinkingLevel: false };
588
588
  }
589
589
 
590
+ /**
591
+ * Resolve a list of role patterns to the first matching model.
592
+ */
593
+ export function resolveRoleSelection(
594
+ roles: readonly string[],
595
+ settings: Settings,
596
+ availableModels: Model<Api>[],
597
+ ): { model: Model<Api>; thinkingLevel?: ThinkingLevel } | undefined {
598
+ const matchPreferences = { usageOrder: settings.getStorage()?.getModelUsageOrder() };
599
+ for (const role of roles) {
600
+ const resolved = resolveModelRoleValue(settings.getModelRole(role), availableModels, {
601
+ settings,
602
+ matchPreferences,
603
+ });
604
+ if (resolved.model) {
605
+ return { model: resolved.model, thinkingLevel: resolved.thinkingLevel };
606
+ }
607
+ }
608
+ return undefined;
609
+ }
610
+
590
611
  /**
591
612
  * Resolve model patterns to actual Model objects with optional thinking levels
592
613
  * Format: "pattern:level" where :level is optional
package/src/main.ts CHANGED
@@ -536,6 +536,7 @@ export async function runRootCommand(parsed: Args, rawArgs: string[]): Promise<v
536
536
  }
537
537
 
538
538
  if (parsedArgs.listModels !== undefined) {
539
+ await logger.timeAsync("settings:init:list-models", () => Settings.init({ cwd: getProjectDir() }));
539
540
  await modelRegistry.refresh("online");
540
541
  const searchPattern = typeof parsedArgs.listModels === "string" ? parsedArgs.listModels : undefined;
541
542
  await listModels(modelRegistry, searchPattern);
@@ -3,7 +3,7 @@
3
3
  */
4
4
  import type { ToolCallContext } from "@oh-my-pi/pi-agent-core";
5
5
  import type { Component } from "@oh-my-pi/pi-tui";
6
- import { Text } from "@oh-my-pi/pi-tui";
6
+ import { Text, visibleWidth, wrapTextWithAnsi } from "@oh-my-pi/pi-tui";
7
7
  import type { RenderResultOptions } from "../extensibility/custom-tools/types";
8
8
  import type { FileDiagnosticsResult } from "../lsp";
9
9
  import { renderDiff as renderDiffColored } from "../modes/components/diff";
@@ -21,7 +21,7 @@ import {
21
21
  shortenPath,
22
22
  truncateDiffByHunk,
23
23
  } from "../tools/render-utils";
24
- import { Ellipsis, Hasher, type RenderCache, renderStatusLine, truncateToWidth } from "../tui";
24
+ import { Hasher, type RenderCache, renderStatusLine, truncateToWidth } from "../tui";
25
25
  import type { HashlineToolEdit } from "./index";
26
26
  import type { DiffError, DiffResult, Operation } from "./types";
27
27
 
@@ -222,6 +222,31 @@ function renderDiffSection(
222
222
  return text;
223
223
  }
224
224
 
225
+ function wrapEditRendererLine(line: string, width: number): string[] {
226
+ if (width <= 0) return [line];
227
+ if (line.length === 0) return [""];
228
+
229
+ const startAnsi = line.match(/^((?:\x1b\[[0-9;]*m)*)/)?.[1] ?? "";
230
+ const bodyWithReset = line.slice(startAnsi.length);
231
+ const body = bodyWithReset.endsWith("\x1b[39m") ? bodyWithReset.slice(0, -"\x1b[39m".length) : bodyWithReset;
232
+ const diffMatch = /^([+\-\s])(\s*\d+)\|(.*)$/s.exec(body);
233
+
234
+ if (!diffMatch) {
235
+ return wrapTextWithAnsi(line, width);
236
+ }
237
+
238
+ const [, marker, lineNum, content] = diffMatch;
239
+ const prefix = `${marker}${lineNum}|`;
240
+ const prefixWidth = visibleWidth(prefix);
241
+ const contentWidth = Math.max(1, width - prefixWidth);
242
+ const continuationPrefix = `${" ".repeat(Math.max(0, prefixWidth - 1))}|`;
243
+ const wrappedContent = wrapTextWithAnsi(content, contentWidth);
244
+
245
+ return wrappedContent.map(
246
+ (segment, index) => `${startAnsi}${index === 0 ? prefix : continuationPrefix}${segment}\x1b[39m`,
247
+ );
248
+ }
249
+
225
250
  export const editToolRenderer = {
226
251
  mergeCallAndResult: true,
227
252
 
@@ -357,7 +382,7 @@ export const editToolRenderer = {
357
382
  }
358
383
 
359
384
  const lines =
360
- width > 0 ? text.split("\n").map(line => truncateToWidth(line, width, Ellipsis.Omit)) : text.split("\n");
385
+ width > 0 ? text.split("\n").flatMap(line => wrapEditRendererLine(line, width)) : text.split("\n");
361
386
  cached = { key, lines };
362
387
  return lines;
363
388
  },
@@ -16,7 +16,7 @@ const CHECK_BYTE_COUNT = 1024;
16
16
  const HEADER_LINE_LIMIT = 40;
17
17
 
18
18
  const KNOWN_GENERATOR_PATTERN =
19
- "(?:protoc(?:-gen-[\\w-]+)?|sqlc|buf|swagger(?:-codegen)?|openapi(?:-generator)?|grpc-gateway|mockery|stringer|easyjson|deepcopy-gen|defaulter-gen|conversion-gen|client-gen|lister-gen|informer-gen|kysely-codegen)";
19
+ "(?:protoc(?:-gen-[\\w-]+)?|sqlc|buf|swagger(?:-codegen)?|openapi(?:-generator)?|grpc-gateway|mockery|stringer|easyjson|deepcopy-gen|defaulter-gen|conversion-gen|client-gen|lister-gen|informer-gen|kysely-codegen|napi-rs)";
20
20
 
21
21
  /**
22
22
  * Strong marker patterns for generated-file headers.
@@ -382,8 +382,8 @@ export function getDiffStats(diffText: string): DiffStats {
382
382
 
383
383
  export function formatDiffStats(added: number, removed: number, hunks: number, theme: Theme): string {
384
384
  const parts: string[] = [];
385
- if (added > 0) parts.push(theme.fg("success", `+${added}`));
386
- if (removed > 0) parts.push(theme.fg("error", `-${removed}`));
385
+ if (added > 0) parts.push(theme.fg("toolDiffAdded", `+${added}`));
386
+ if (removed > 0) parts.push(theme.fg("toolDiffRemoved", `-${removed}`));
387
387
  if (hunks > 0) parts.push(theme.fg("dim", `${hunks} hunk${hunks !== 1 ? "s" : ""}`));
388
388
  return parts.join(theme.fg("dim", " / "));
389
389
  }
@@ -7,7 +7,7 @@ import type { Api, Model } from "@oh-my-pi/pi-ai";
7
7
  import { completeSimple } from "@oh-my-pi/pi-ai";
8
8
  import { logger } from "@oh-my-pi/pi-utils";
9
9
  import type { ModelRegistry } from "../config/model-registry";
10
- import { resolveModelRoleValue } from "../config/model-resolver";
10
+ import { resolveRoleSelection } from "../config/model-resolver";
11
11
  import { renderPromptTemplate } from "../config/prompt-templates";
12
12
  import type { Settings } from "../config/settings";
13
13
  import titleSystemPrompt from "../prompts/system/title-system.md" with { type: "text" };
@@ -28,13 +28,9 @@ function getTitleModel(
28
28
  const availableModels = registry.getAvailable();
29
29
  if (availableModels.length === 0) return undefined;
30
30
 
31
- const matchPreferences = { usageOrder: settings.getStorage()?.getModelUsageOrder() };
32
- const configuredSmol = resolveModelRoleValue(settings.getModelRole("smol"), availableModels, {
33
- settings,
34
- matchPreferences,
35
- });
36
- if (configuredSmol.model) {
37
- return { model: configuredSmol.model, thinkingLevel: configuredSmol.thinkingLevel };
31
+ const titleModel = resolveRoleSelection(["commit", "smol"], settings, availableModels);
32
+ if (titleModel) {
33
+ return { model: titleModel.model, thinkingLevel: titleModel.thinkingLevel };
38
34
  }
39
35
 
40
36
  if (currentModel) {
@@ -4,7 +4,6 @@
4
4
  * Single tool supporting Anthropic, Perplexity, Exa, Brave, Jina, Kimi, Gemini, Codex, Tavily, Kagi, Z.AI, and Synthetic
5
5
  * providers with provider-specific parameters exposed conditionally.
6
6
  *
7
- * Code search is also supported via the code_search tool.
8
7
  */
9
8
  import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
10
9
  import { StringEnum } from "@oh-my-pi/pi-ai";
@@ -13,17 +12,9 @@ import { renderPromptTemplate } from "../../config/prompt-templates";
13
12
  import type { CustomTool, CustomToolContext, RenderResultOptions } from "../../extensibility/custom-tools/types";
14
13
  import type { Theme } from "../../modes/theme/theme";
15
14
  import webSearchSystemPrompt from "../../prompts/system/web-search.md" with { type: "text" };
16
- import codeSearchDescription from "../../prompts/tools/code-search.md" with { type: "text" };
17
15
  import webSearchDescription from "../../prompts/tools/web-search.md" with { type: "text" };
18
16
  import type { ToolSession } from "../../tools";
19
17
  import { formatAge } from "../../tools/render-utils";
20
- import {
21
- type CodeSearchRenderDetails,
22
- type CodeSearchToolParams,
23
- executeCodeSearch,
24
- renderCodeSearchCall,
25
- renderCodeSearchResult,
26
- } from "./code-search";
27
18
  import { getSearchProvider, resolveProviderChain, type SearchProvider } from "./provider";
28
19
  import { renderSearchCall, renderSearchResult, type SearchRenderDetails } from "./render";
29
20
  import type { SearchProviderId, SearchResponse } from "./types";
@@ -262,34 +253,8 @@ export const webSearchCustomTool: CustomTool<typeof webSearchSchema, SearchRende
262
253
  },
263
254
  };
264
255
 
265
- /** Schema for code context search */
266
- const codeSearchParameters = Type.Object({
267
- query: Type.String({ description: "Grep-style code search query; use exact tokens or short quoted phrases" }),
268
- code_context: Type.Optional(Type.String({ description: "Optional disambiguation tokens only, not a sentence" })),
269
- });
270
-
271
- /** Code context search - optimized for code snippets and documentation */
272
- export const codeSearchTool: CustomTool<typeof codeSearchParameters, CodeSearchRenderDetails> = {
273
- name: "code_search",
274
- label: "Code Search",
275
- description: renderPromptTemplate(codeSearchDescription),
276
- parameters: codeSearchParameters,
277
-
278
- async execute(_toolCallId, params, _onUpdate, _ctx, _signal) {
279
- return executeCodeSearch(params);
280
- },
281
-
282
- renderCall(args: CodeSearchToolParams, options: RenderResultOptions, theme: Theme) {
283
- return renderCodeSearchCall(args, options, theme);
284
- },
285
-
286
- renderResult(result, options, theme) {
287
- return renderCodeSearchResult(result, options, theme);
288
- },
289
- };
290
-
291
256
  export function getSearchTools(): CustomTool<any, any>[] {
292
- return [webSearchCustomTool, codeSearchTool];
257
+ return [webSearchCustomTool];
293
258
  }
294
259
 
295
260
  export { getSearchProvider, setPreferredSearchProvider } from "./provider";
@@ -20,8 +20,6 @@ export type SearchProviderId =
20
20
  | "kagi"
21
21
  | "synthetic";
22
22
 
23
- export type CodeSearchProviderId = "exa";
24
-
25
23
  export function isSearchProviderId(value: string): value is SearchProviderId {
26
24
  return [
27
25
  "exa",
@@ -1,45 +0,0 @@
1
- Search code snippets, and technical content.
2
- This tool behaves more like grep than natural-language web search.
3
-
4
- <instruction>
5
- - Query with exact symbols, identifiers, error strings, CLI flags, filenames, import paths, and short code fragments
6
- - Start with the smallest distinctive token; widen or add one nearby token only if the first query is too broad
7
- - Prefer exact syntax when punctuation carries meaning, such as `Promise.withResolvers`, `useEffect(`, `--watch`, or `"direnv loading"`
8
- - Keep `query` terse; remove filler words, prose, and request framing
9
- - Use `code_context` only for a few disambiguating tokens such as language, library, framework, repo, runtime, or API name
10
- - If a multi-word literal matters exactly, quote the shortest stable phrase first, then refine
11
- - When looking for usage examples of a specific API, search the symbol first; add surrounding call syntax only when needed
12
- </instruction>
13
-
14
- <parameters>
15
- - query: Grep-style code search query; use exact tokens, short fragments, or short quoted phrases
16
- - code_context: Optional disambiguation tokens only, not a sentence
17
- </parameters>
18
-
19
- <examples>
20
- Good queries:
21
- - `Promise.withResolvers`
22
- - `DIRENV_LOG_FORMAT`
23
- - `"direnv loading"`
24
- - `useState` with `code_context: react hooks`
25
- - `app.get(` with `code_context: express`
26
- - `ERR_REQUIRE_ESM` with `code_context: node`
27
-
28
- Bad queries:
29
- - `Need the official or source-backed way to silence direnv loading output`
30
- - `How do I use Promise.withResolvers in Bun?`
31
- - `find examples of React state hooks in TypeScript projects`
32
- - `search GitHub for express routing docs`
33
- </examples>
34
-
35
- <avoid>
36
- - Do not use this tool for broad conceptual research, comparisons, or authoritative sourcing; use `web_search`, `web_search_deep`, or `fetch` instead
37
- - Do not put full-sentence instructions into `query` or `code_context`
38
- - Do not pack many weak terms into one query; one strong token plus minimal context usually works better
39
- </avoid>
40
-
41
- <critical>
42
- - `query` should be grep-style code search, not a natural-language request
43
- - `code_context` is optional and should stay short
44
- - If you need explanations, best practices, or comprehensive answers, use broader web search tools instead of this one
45
- </critical>
@@ -1,208 +0,0 @@
1
- import type { Component } from "@oh-my-pi/pi-tui";
2
- import { Text } from "@oh-my-pi/pi-tui";
3
- import { callExaTool, findApiKey as findExaKey, formatSearchResults, isSearchResponse } from "../../exa/mcp-client";
4
- import type { CustomToolResult, RenderResultOptions } from "../../extensibility/custom-tools/types";
5
- import type { Theme } from "../../modes/theme/theme";
6
- import {
7
- formatCount,
8
- formatExpandHint,
9
- formatMoreItems,
10
- formatStatusIcon,
11
- replaceTabs,
12
- truncateToWidth,
13
- } from "../../tools/render-utils";
14
- import type { CodeSearchProviderId } from "./types";
15
-
16
- export interface CodeSearchToolParams {
17
- query: string;
18
- code_context?: string;
19
- }
20
-
21
- export interface CodeSearchSource {
22
- title: string;
23
- url: string;
24
- repository: string;
25
- path: string;
26
- branch: string;
27
- snippet?: string;
28
- totalMatches?: string;
29
- }
30
-
31
- export interface CodeSearchResponse {
32
- provider: CodeSearchProviderId;
33
- query: string;
34
- totalResults?: number;
35
- sources: CodeSearchSource[];
36
- }
37
-
38
- export interface CodeSearchRenderDetails {
39
- response?: CodeSearchResponse;
40
- error?: string;
41
- provider: CodeSearchProviderId;
42
- }
43
-
44
- function stringifyExaCodeResponse(payload: unknown): string {
45
- if (typeof payload === "string") return payload;
46
- if (typeof payload === "number" || typeof payload === "boolean") return String(payload);
47
- if (payload === null || payload === undefined) return "";
48
- const serialized = JSON.stringify(payload, null, 2);
49
- return typeof serialized === "string" ? serialized : "";
50
- }
51
-
52
- function normalizeExaCodeSearchResponse(
53
- params: CodeSearchToolParams,
54
- payload: unknown,
55
- formattedSearchResponse?: string,
56
- ): CodeSearchResponse {
57
- const snippet = formattedSearchResponse ?? stringifyExaCodeResponse(payload);
58
- return {
59
- provider: "exa",
60
- query: params.query,
61
- sources: [
62
- {
63
- title: params.query,
64
- url: "https://exa.ai/",
65
- repository: "exa",
66
- path: "code-search",
67
- branch: "public-mcp",
68
- snippet: snippet.length > 0 ? snippet : undefined,
69
- },
70
- ],
71
- };
72
- }
73
-
74
- async function searchCodeWithExa(params: CodeSearchToolParams): Promise<CodeSearchResponse> {
75
- const exaParams = params.code_context
76
- ? { query: params.query, code_context: params.code_context }
77
- : { query: params.query };
78
- const response = await callExaTool("get_code_context_exa", exaParams, findExaKey());
79
- if (isSearchResponse(response)) {
80
- return normalizeExaCodeSearchResponse(params, response, formatSearchResults(response));
81
- }
82
-
83
- return normalizeExaCodeSearchResponse(params, response);
84
- }
85
-
86
- export function formatCodeSearchForLlm(response: CodeSearchResponse): string {
87
- const parts: string[] = [];
88
- const summaryParts: string[] = [response.provider];
89
- if (response.totalResults !== undefined) {
90
- summaryParts.push(`${response.totalResults.toLocaleString()} total matches`);
91
- }
92
- parts.push(`Code search via ${summaryParts.join(" · ")}`);
93
-
94
- if (response.sources.length === 0) {
95
- parts.push("No results found.");
96
- return parts.join("\n");
97
- }
98
-
99
- for (const [index, source] of response.sources.entries()) {
100
- const metadata: string[] = [source.repository, source.path];
101
- if (source.totalMatches) metadata.push(`${source.totalMatches} matches`);
102
- parts.push(`[${index + 1}] ${metadata.join(" · ")}`);
103
- parts.push(` ${source.url}`);
104
- if (source.snippet) {
105
- for (const line of source.snippet.split("\n").slice(0, 8)) {
106
- parts.push(` ${line}`);
107
- }
108
- }
109
- }
110
-
111
- return parts.join("\n");
112
- }
113
-
114
- export async function executeCodeSearch(
115
- params: CodeSearchToolParams,
116
- ): Promise<CustomToolResult<CodeSearchRenderDetails>> {
117
- try {
118
- const response = await searchCodeWithExa(params);
119
-
120
- return {
121
- content: [{ type: "text", text: formatCodeSearchForLlm(response) }],
122
- details: { provider: response.provider, response },
123
- };
124
- } catch (error) {
125
- const message = error instanceof Error ? error.message : String(error);
126
- return {
127
- content: [{ type: "text", text: `Error: ${message}` }],
128
- details: { provider: "exa", error: message },
129
- };
130
- }
131
- }
132
-
133
- export function renderCodeSearchCall(
134
- args: CodeSearchToolParams,
135
- _options: RenderResultOptions,
136
- theme: Theme,
137
- ): Component {
138
- let text = `${theme.fg("toolTitle", "Code Search")} ${theme.fg("accent", truncateToWidth(args.query, 80))}`;
139
- if (args.code_context) {
140
- text += ` ${theme.fg("dim", truncateToWidth(args.code_context, 40))}`;
141
- }
142
- return new Text(text, 0, 0);
143
- }
144
-
145
- export function renderCodeSearchResult(
146
- result: { content: Array<{ type: string; text?: string }>; details?: CodeSearchRenderDetails },
147
- options: RenderResultOptions,
148
- uiTheme: Theme,
149
- ): Component {
150
- const details = result.details;
151
- if (details?.error) {
152
- return new Text(
153
- `${formatStatusIcon("error", uiTheme)} ${uiTheme.fg("error", `Error: ${replaceTabs(details.error)}`)}`,
154
- 0,
155
- 0,
156
- );
157
- }
158
-
159
- const response = details?.response;
160
- if (!response) {
161
- return new Text(`${formatStatusIcon("warning", uiTheme)} ${uiTheme.fg("muted", "No code search results")}`, 0, 0);
162
- }
163
-
164
- const resultCount = response.sources.length;
165
- const meta: string[] = [formatCount("result", resultCount), `provider:${response.provider}`];
166
- if (response.totalResults !== undefined) {
167
- meta.push(`${response.totalResults.toLocaleString()} total`);
168
- }
169
- const expandHint = formatExpandHint(uiTheme, options.expanded, resultCount > 1);
170
- let text = `${formatStatusIcon(resultCount > 0 ? "success" : "warning", uiTheme)} ${uiTheme.fg("dim", meta.join(uiTheme.sep.dot))}${expandHint}`;
171
-
172
- if (resultCount === 0) {
173
- text += `\n ${uiTheme.fg("dim", uiTheme.tree.last)} ${uiTheme.fg("muted", "No results")}`;
174
- return new Text(text, 0, 0);
175
- }
176
-
177
- const visibleSources = options.expanded ? response.sources : response.sources.slice(0, 1);
178
- for (const [index, source] of visibleSources.entries()) {
179
- const isLast = index === visibleSources.length - 1;
180
- const branch = isLast ? uiTheme.tree.last : uiTheme.tree.branch;
181
- const cont = isLast ? " " : uiTheme.tree.vertical;
182
- text += `\n ${uiTheme.fg("dim", branch)} ${uiTheme.fg("accent", truncateToWidth(replaceTabs(source.title), 100))}`;
183
- text += `\n ${uiTheme.fg("dim", cont)} ${uiTheme.fg("dim", uiTheme.tree.hook)} ${uiTheme.fg("mdLinkUrl", source.url)}`;
184
-
185
- if (source.totalMatches) {
186
- text += `\n ${uiTheme.fg("dim", cont)} ${uiTheme.fg("dim", uiTheme.tree.hook)} ${uiTheme.fg("muted", `Matches: ${source.totalMatches}`)}`;
187
- }
188
-
189
- if (source.snippet) {
190
- const snippetLines = source.snippet.split("\n").slice(0, options.expanded ? 6 : 3);
191
- for (const line of snippetLines) {
192
- text += `\n ${uiTheme.fg("dim", cont)} ${uiTheme.fg("dim", uiTheme.tree.hook)} ${uiTheme.fg(
193
- "toolOutput",
194
- truncateToWidth(replaceTabs(line), 100),
195
- )}`;
196
- }
197
- }
198
- }
199
-
200
- if (!options.expanded && response.sources.length > visibleSources.length) {
201
- text += `\n ${uiTheme.fg("dim", uiTheme.tree.last)} ${uiTheme.fg(
202
- "muted",
203
- formatMoreItems(response.sources.length - visibleSources.length, "result"),
204
- )}`;
205
- }
206
-
207
- return new Text(text, 0, 0);
208
- }