@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 +13 -1
- package/package.json +7 -7
- package/src/commit/git/index.ts +3 -4
- package/src/commit/model-selection.ts +1 -19
- package/src/config/model-registry.ts +19 -3
- package/src/config/model-resolver.ts +21 -0
- package/src/main.ts +1 -0
- package/src/patch/shared.ts +28 -3
- package/src/tools/auto-generated-guard.ts +1 -1
- package/src/tools/render-utils.ts +2 -2
- package/src/utils/title-generator.ts +4 -8
- package/src/web/search/index.ts +1 -36
- package/src/web/search/types.ts +0 -2
- package/src/prompts/tools/code-search.md +0 -45
- package/src/web/search/code-search.ts +0 -208
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.
|
|
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.
|
|
46
|
-
"@oh-my-pi/pi-agent-core": "13.17.
|
|
47
|
-
"@oh-my-pi/pi-ai": "13.17.
|
|
48
|
-
"@oh-my-pi/pi-natives": "13.17.
|
|
49
|
-
"@oh-my-pi/pi-tui": "13.17.
|
|
50
|
-
"@oh-my-pi/pi-utils": "13.17.
|
|
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",
|
package/src/commit/git/index.ts
CHANGED
|
@@ -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
|
-
.
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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);
|
package/src/patch/shared.ts
CHANGED
|
@@ -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 {
|
|
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").
|
|
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("
|
|
386
|
-
if (removed > 0) parts.push(theme.fg("
|
|
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 {
|
|
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
|
|
32
|
-
|
|
33
|
-
|
|
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) {
|
package/src/web/search/index.ts
CHANGED
|
@@ -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
|
|
257
|
+
return [webSearchCustomTool];
|
|
293
258
|
}
|
|
294
259
|
|
|
295
260
|
export { getSearchProvider, setPreferredSearchProvider } from "./provider";
|
package/src/web/search/types.ts
CHANGED
|
@@ -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
|
-
}
|