@oh-my-pi/pi-coding-agent 14.5.2 → 14.5.5
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 +70 -0
- package/examples/extensions/plan-mode.ts +1 -1
- package/examples/sdk/README.md +1 -1
- package/package.json +7 -7
- package/src/config/prompt-templates.ts +104 -6
- package/src/config/settings-schema.ts +14 -13
- package/src/config/settings.ts +1 -1
- package/src/cursor.ts +4 -4
- package/src/edit/index.ts +111 -109
- package/src/edit/line-hash.ts +33 -3
- package/src/edit/modes/apply-patch.ts +6 -4
- package/src/edit/modes/atom.lark +27 -0
- package/src/edit/modes/atom.ts +1094 -642
- package/src/edit/modes/hashline.ts +9 -10
- package/src/edit/modes/patch.ts +23 -19
- package/src/edit/modes/replace.ts +19 -15
- package/src/edit/renderer.ts +65 -8
- package/src/edit/streaming.ts +47 -77
- package/src/extensibility/extensions/types.ts +11 -11
- package/src/extensibility/hooks/types.ts +6 -6
- package/src/lsp/edits.ts +8 -5
- package/src/lsp/index.ts +4 -4
- package/src/lsp/utils.ts +13 -43
- package/src/mcp/discoverable-tool-metadata.ts +1 -1
- package/src/mcp/manager.ts +3 -3
- package/src/mcp/tool-bridge.ts +4 -4
- package/src/memories/index.ts +1 -1
- package/src/modes/acp/acp-event-mapper.ts +1 -1
- package/src/modes/components/session-observer-overlay.ts +1 -1
- package/src/modes/components/settings-defs.ts +3 -3
- package/src/modes/components/tree-selector.ts +2 -2
- package/src/modes/controllers/event-controller.ts +12 -0
- package/src/modes/utils/ui-helpers.ts +31 -7
- package/src/prompts/agents/explore.md +1 -1
- package/src/prompts/agents/librarian.md +2 -2
- package/src/prompts/agents/plan.md +2 -2
- package/src/prompts/agents/reviewer.md +1 -1
- package/src/prompts/agents/task.md +2 -2
- package/src/prompts/system/plan-mode-active.md +1 -1
- package/src/prompts/system/system-prompt.md +34 -31
- package/src/prompts/tools/apply-patch.md +0 -2
- package/src/prompts/tools/atom.md +88 -97
- package/src/prompts/tools/bash.md +7 -4
- package/src/prompts/tools/checkpoint.md +1 -1
- package/src/prompts/tools/find.md +6 -1
- package/src/prompts/tools/hashline.md +10 -11
- package/src/prompts/tools/patch.md +13 -13
- package/src/prompts/tools/read.md +5 -5
- package/src/prompts/tools/replace.md +3 -3
- package/src/prompts/tools/{grep.md → search.md} +4 -4
- package/src/sdk.ts +19 -9
- package/src/session/agent-session.ts +69 -1
- package/src/system-prompt.ts +15 -5
- package/src/task/executor.ts +5 -0
- package/src/task/index.ts +10 -1
- package/src/tools/ast-edit.ts +27 -50
- package/src/tools/ast-grep.ts +22 -48
- package/src/tools/bash.ts +1 -1
- package/src/tools/file-recorder.ts +6 -6
- package/src/tools/find.ts +11 -13
- package/src/tools/grouped-file-output.ts +96 -0
- package/src/tools/index.ts +7 -7
- package/src/tools/path-utils.ts +31 -4
- package/src/tools/read.ts +12 -6
- package/src/tools/renderers.ts +2 -2
- package/src/tools/{grep.ts → search.ts} +43 -86
- package/src/tools/todo-write.ts +0 -1
- package/src/tools/write.ts +8 -4
- package/src/web/search/index.ts +1 -1
package/src/system-prompt.ts
CHANGED
|
@@ -384,6 +384,8 @@ export async function loadSystemPromptFiles(options: LoadContextFilesOptions = {
|
|
|
384
384
|
export interface SystemPromptToolMetadata {
|
|
385
385
|
label: string;
|
|
386
386
|
description: string;
|
|
387
|
+
/** Tool name the model sees on the provider wire. Defaults to the internal tool name. */
|
|
388
|
+
wireName?: string;
|
|
387
389
|
}
|
|
388
390
|
|
|
389
391
|
export function buildSystemPromptToolMetadata(
|
|
@@ -394,12 +396,16 @@ export function buildSystemPromptToolMetadata(
|
|
|
394
396
|
Array.from(tools.entries(), ([name, tool]) => {
|
|
395
397
|
const toolRecord = tool as AgentTool & { label?: string; description?: string };
|
|
396
398
|
const override = overrides[name];
|
|
399
|
+
const wireName =
|
|
400
|
+
override?.wireName ??
|
|
401
|
+
(typeof toolRecord.customWireName === "string" ? toolRecord.customWireName : undefined);
|
|
397
402
|
return [
|
|
398
403
|
name,
|
|
399
404
|
{
|
|
400
405
|
label: override?.label ?? (typeof toolRecord.label === "string" ? toolRecord.label : ""),
|
|
401
406
|
description:
|
|
402
407
|
override?.description ?? (typeof toolRecord.description === "string" ? toolRecord.description : ""),
|
|
408
|
+
wireName,
|
|
403
409
|
},
|
|
404
410
|
] as const;
|
|
405
411
|
}),
|
|
@@ -570,14 +576,17 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
|
|
|
570
576
|
}
|
|
571
577
|
}
|
|
572
578
|
|
|
573
|
-
// Build tool descriptions for system prompt rendering
|
|
579
|
+
// Build tool descriptions for system prompt rendering.
|
|
580
|
+
const toolPromptNames = new Map<string, string>(toolNames.map(name => [name, tools?.get(name)?.wireName ?? name]));
|
|
581
|
+
const toolRefs = Object.fromEntries(toolPromptNames.entries());
|
|
574
582
|
const toolInfo = toolNames.map(name => ({
|
|
575
|
-
name,
|
|
583
|
+
name: toolPromptNames.get(name) ?? name,
|
|
584
|
+
internalName: name,
|
|
576
585
|
label: tools?.get(name)?.label ?? "",
|
|
577
586
|
description: tools?.get(name)?.description ?? "",
|
|
578
587
|
}));
|
|
579
588
|
|
|
580
|
-
// Filter skills to only include those with read tool
|
|
589
|
+
// Filter skills to only include those with read tool.
|
|
581
590
|
const hasRead = tools?.has("read");
|
|
582
591
|
const filteredSkills = hasRead ? skills : [];
|
|
583
592
|
|
|
@@ -589,6 +598,7 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
|
|
|
589
598
|
const injectedAlwaysApplyRules = dedupeAlwaysApplyRules(alwaysApplyRules, promptSources);
|
|
590
599
|
|
|
591
600
|
const environment = await logger.time("getEnvironmentInfo", getEnvironmentInfo);
|
|
601
|
+
const reportToolIssueToolName = toolPromptNames.get("report_tool_issue") ?? "report_tool_issue";
|
|
592
602
|
const data = {
|
|
593
603
|
systemPromptCustomization: effectiveSystemPromptCustomization,
|
|
594
604
|
customPrompt: resolvedCustomPrompt,
|
|
@@ -596,6 +606,7 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
|
|
|
596
606
|
tools: toolNames,
|
|
597
607
|
toolInfo,
|
|
598
608
|
repeatToolDescriptions,
|
|
609
|
+
toolRefs,
|
|
599
610
|
environment,
|
|
600
611
|
contextFiles,
|
|
601
612
|
agentsMdSearch,
|
|
@@ -617,8 +628,7 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
|
|
|
617
628
|
|
|
618
629
|
// When autoqa is active the report_tool_issue tool is in the tool set — nudge the agent.
|
|
619
630
|
if (toolNames.includes("report_tool_issue")) {
|
|
620
|
-
rendered +=
|
|
621
|
-
"\n\n<critical>\nThe `report_tool_issue` tool is available for automated QA. If ANY tool you call returns output that is unexpected, incorrect, malformed, or otherwise inconsistent with what you anticipated given the tool's described behavior and your parameters, call `report_tool_issue` with the tool name and a concise description of the discrepancy. Do not hesitate to report — false positives are acceptable.\n</critical>";
|
|
631
|
+
rendered += `\n\n<critical>\nThe \`${reportToolIssueToolName}\` tool is available for automated QA. If ANY tool you call returns output that is unexpected, incorrect, malformed, or otherwise inconsistent with what you anticipated given the tool's described behavior and your parameters, call \`${reportToolIssueToolName}\` with the tool name and a concise description of the discrepancy. Do not hesitate to report — false positives are acceptable.\n</critical>`;
|
|
622
632
|
}
|
|
623
633
|
|
|
624
634
|
return rendered;
|
package/src/task/executor.ts
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Runs each subagent on the main thread and forwards AgentEvents for progress tracking.
|
|
5
5
|
*/
|
|
6
|
+
|
|
6
7
|
import path from "node:path";
|
|
7
8
|
import type { AgentEvent, ThinkingLevel } from "@oh-my-pi/pi-agent-core";
|
|
8
9
|
import { logger, prompt, untilAborted } from "@oh-my-pi/pi-utils";
|
|
@@ -16,6 +17,7 @@ import { SETTINGS_SCHEMA, type SettingPath } from "../config/settings-schema";
|
|
|
16
17
|
import type { CustomTool } from "../extensibility/custom-tools/types";
|
|
17
18
|
import { runExtensionCompact, runExtensionSetModel } from "../extensibility/extensions/compact-handler";
|
|
18
19
|
import type { Skill } from "../extensibility/skills";
|
|
20
|
+
import type { LocalProtocolOptions } from "../internal-urls";
|
|
19
21
|
import { callTool } from "../mcp/client";
|
|
20
22
|
import type { MCPManager } from "../mcp/manager";
|
|
21
23
|
import subagentSystemPromptTemplate from "../prompts/system/subagent-system-prompt.md" with { type: "text" };
|
|
@@ -159,6 +161,8 @@ export interface ExecutorOptions {
|
|
|
159
161
|
authStorage?: AuthStorage;
|
|
160
162
|
modelRegistry?: ModelRegistry;
|
|
161
163
|
settings?: Settings;
|
|
164
|
+
/** Override local:// protocol options so subagent shares parent's local:// root */
|
|
165
|
+
localProtocolOptions?: LocalProtocolOptions;
|
|
162
166
|
}
|
|
163
167
|
|
|
164
168
|
function parseStringifiedJson(value: unknown): unknown {
|
|
@@ -987,6 +991,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
987
991
|
enableMCP,
|
|
988
992
|
mcpManager: options.mcpManager,
|
|
989
993
|
customTools: mcpProxyTools.length > 0 ? mcpProxyTools : undefined,
|
|
994
|
+
localProtocolOptions: options.localProtocolOptions,
|
|
990
995
|
});
|
|
991
996
|
|
|
992
997
|
activeSession = session;
|
package/src/task/index.ts
CHANGED
|
@@ -28,6 +28,7 @@ import taskSummaryTemplate from "../prompts/tools/task-summary.md" with { type:
|
|
|
28
28
|
import { formatBytes, formatDuration } from "../tools/render-utils";
|
|
29
29
|
// Import review tools for side effects (registers subagent tool handlers)
|
|
30
30
|
import "../tools/review";
|
|
31
|
+
import type { LocalProtocolOptions } from "../internal-urls";
|
|
31
32
|
import { generateCommitMessage } from "../utils/commit-message-generator";
|
|
32
33
|
import * as git from "../utils/git";
|
|
33
34
|
import { discoverAgents, getAgent } from "./discovery";
|
|
@@ -567,7 +568,7 @@ export class TaskTool implements AgentTool<TSchema, TaskToolDetails, Theme> {
|
|
|
567
568
|
}
|
|
568
569
|
|
|
569
570
|
const planModeState = this.session.getPlanModeState?.();
|
|
570
|
-
const planModeTools = ["read", "
|
|
571
|
+
const planModeTools = ["read", "search", "find", "lsp", "web_search"];
|
|
571
572
|
const effectiveAgent: typeof agent = planModeState?.enabled
|
|
572
573
|
? {
|
|
573
574
|
...agent,
|
|
@@ -715,6 +716,12 @@ export class TaskTool implements AgentTool<TSchema, TaskToolDetails, Theme> {
|
|
|
715
716
|
const tempArtifactsDir = artifactsDir ? null : path.join(os.tmpdir(), `omp-task-${Snowflake.next()}`);
|
|
716
717
|
const effectiveArtifactsDir = artifactsDir || tempArtifactsDir!;
|
|
717
718
|
|
|
719
|
+
// Share the parent session's local:// root with subagents so they read/write the same scratch space
|
|
720
|
+
const localProtocolOptions: LocalProtocolOptions = {
|
|
721
|
+
getArtifactsDir: this.session.getArtifactsDir ?? (() => null),
|
|
722
|
+
getSessionId: this.session.getSessionId ?? (() => null),
|
|
723
|
+
};
|
|
724
|
+
|
|
718
725
|
// Initialize progress tracking
|
|
719
726
|
const progressMap = new Map<number, AgentProgress>();
|
|
720
727
|
|
|
@@ -856,6 +863,7 @@ export class TaskTool implements AgentTool<TSchema, TaskToolDetails, Theme> {
|
|
|
856
863
|
contextFiles,
|
|
857
864
|
skills: availableSkills,
|
|
858
865
|
promptTemplates,
|
|
866
|
+
localProtocolOptions,
|
|
859
867
|
});
|
|
860
868
|
}
|
|
861
869
|
|
|
@@ -909,6 +917,7 @@ export class TaskTool implements AgentTool<TSchema, TaskToolDetails, Theme> {
|
|
|
909
917
|
contextFiles,
|
|
910
918
|
skills: availableSkills,
|
|
911
919
|
promptTemplates,
|
|
920
|
+
localProtocolOptions,
|
|
912
921
|
});
|
|
913
922
|
if (mergeMode === "branch" && result.exitCode === 0) {
|
|
914
923
|
try {
|
package/src/tools/ast-edit.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import * as path from "node:path";
|
|
2
1
|
import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
|
|
3
2
|
import { type AstReplaceChange, astEdit } from "@oh-my-pi/pi-natives";
|
|
4
3
|
import type { Component } from "@oh-my-pi/pi-tui";
|
|
@@ -13,8 +12,10 @@ import { Ellipsis, Hasher, type RenderCache, renderStatusLine, renderTreeList, t
|
|
|
13
12
|
import { resolveFileDisplayMode } from "../utils/file-display-mode";
|
|
14
13
|
import type { ToolSession } from ".";
|
|
15
14
|
import { createFileRecorder, formatResultPath } from "./file-recorder";
|
|
15
|
+
import { formatGroupedFiles } from "./grouped-file-output";
|
|
16
16
|
import type { OutputMeta } from "./output-meta";
|
|
17
17
|
import {
|
|
18
|
+
formatPathRelativeToCwd,
|
|
18
19
|
hasGlobPathChars,
|
|
19
20
|
normalizePathLikeInput,
|
|
20
21
|
parseSearchPath,
|
|
@@ -105,10 +106,7 @@ export class AstEditTool implements AgentTool<typeof astEditSchema, AstEditToolD
|
|
|
105
106
|
const normalizedRewrites = Object.fromEntries(ops);
|
|
106
107
|
const maxFiles = $envpos("PI_MAX_AST_FILES", 1000);
|
|
107
108
|
|
|
108
|
-
const formatScopePath = (targetPath: string): string =>
|
|
109
|
-
const relative = path.relative(this.session.cwd, targetPath).replace(/\\/g, "/");
|
|
110
|
-
return relative.length === 0 ? "." : relative;
|
|
111
|
-
};
|
|
109
|
+
const formatScopePath = (targetPath: string): string => formatPathRelativeToCwd(targetPath, this.session.cwd);
|
|
112
110
|
let searchPath: string | undefined;
|
|
113
111
|
let scopePath: string | undefined;
|
|
114
112
|
let globFilter: string | undefined;
|
|
@@ -163,7 +161,8 @@ export class AstEditTool implements AgentTool<typeof astEditSchema, AstEditToolD
|
|
|
163
161
|
});
|
|
164
162
|
|
|
165
163
|
const dedupedParseErrors = dedupeParseErrors(result.parseErrors);
|
|
166
|
-
const formatPath = (filePath: string): string =>
|
|
164
|
+
const formatPath = (filePath: string): string =>
|
|
165
|
+
formatResultPath(filePath, isDirectory, resolvedSearchPath, this.session.cwd);
|
|
167
166
|
|
|
168
167
|
const { record: recordFile, list: fileList } = createFileRecorder();
|
|
169
168
|
const fileReplacementCounts = new Map<string, number>();
|
|
@@ -204,7 +203,9 @@ export class AstEditTool implements AgentTool<typeof astEditSchema, AstEditToolD
|
|
|
204
203
|
const useHashLines = resolveFileDisplayMode(this.session).hashLines;
|
|
205
204
|
const outputLines: string[] = [];
|
|
206
205
|
const displayLines: string[] = [];
|
|
207
|
-
const renderChangesForFile = (relativePath: string) => {
|
|
206
|
+
const renderChangesForFile = (relativePath: string): { model: string[]; display: string[] } => {
|
|
207
|
+
const modelOut: string[] = [];
|
|
208
|
+
const displayOut: string[] = [];
|
|
208
209
|
const fileChanges = changesByFile.get(relativePath) ?? [];
|
|
209
210
|
const lineNumberWidth = fileChanges.reduce(
|
|
210
211
|
(width, change) => Math.max(width, String(change.startLine).length),
|
|
@@ -222,55 +223,31 @@ export class AstEditTool implements AgentTool<typeof astEditSchema, AstEditToolD
|
|
|
222
223
|
? `${change.startLine}${computeLineHash(change.startLine, afterFirstLine)}`
|
|
223
224
|
: `${change.startLine}:${change.startColumn}`;
|
|
224
225
|
const lineSeparator = useHashLines ? HASHLINE_CONTENT_SEPARATOR : " ";
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
226
|
+
modelOut.push(`-${beforeRef}${lineSeparator}${beforeLine}`);
|
|
227
|
+
modelOut.push(`+${afterRef}${lineSeparator}${afterLine}`);
|
|
228
|
+
displayOut.push(formatCodeFrameLine("-", change.startLine, beforeLine, lineNumberWidth));
|
|
229
|
+
displayOut.push(formatCodeFrameLine("+", change.startLine, afterLine, lineNumberWidth));
|
|
229
230
|
}
|
|
231
|
+
return { model: modelOut, display: displayOut };
|
|
230
232
|
};
|
|
231
233
|
|
|
232
234
|
if (isDirectory) {
|
|
233
|
-
const
|
|
234
|
-
|
|
235
|
-
const
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
if (outputLines.length > 0) {
|
|
245
|
-
outputLines.push("");
|
|
246
|
-
displayLines.push("");
|
|
247
|
-
}
|
|
248
|
-
const count = fileReplacementCounts.get(relativePath) ?? 0;
|
|
249
|
-
const header = `# ${path.basename(relativePath)} (${formatCount("replacement", count)})`;
|
|
250
|
-
outputLines.push(header);
|
|
251
|
-
displayLines.push(header);
|
|
252
|
-
renderChangesForFile(relativePath);
|
|
253
|
-
}
|
|
254
|
-
continue;
|
|
255
|
-
}
|
|
256
|
-
if (outputLines.length > 0) {
|
|
257
|
-
outputLines.push("");
|
|
258
|
-
displayLines.push("");
|
|
259
|
-
}
|
|
260
|
-
const dirHeader = `# ${directory}`;
|
|
261
|
-
outputLines.push(dirHeader);
|
|
262
|
-
displayLines.push(dirHeader);
|
|
263
|
-
for (const relativePath of directoryFiles) {
|
|
264
|
-
const count = fileReplacementCounts.get(relativePath) ?? 0;
|
|
265
|
-
const fileHeader = `## └─ ${path.basename(relativePath)} (${formatCount("replacement", count)})`;
|
|
266
|
-
outputLines.push(fileHeader);
|
|
267
|
-
displayLines.push(fileHeader);
|
|
268
|
-
renderChangesForFile(relativePath);
|
|
269
|
-
}
|
|
270
|
-
}
|
|
235
|
+
const grouped = formatGroupedFiles(fileList, relativePath => {
|
|
236
|
+
const rendered = renderChangesForFile(relativePath);
|
|
237
|
+
const count = fileReplacementCounts.get(relativePath) ?? 0;
|
|
238
|
+
return {
|
|
239
|
+
headerSuffix: ` (${formatCount("replacement", count)})`,
|
|
240
|
+
modelLines: rendered.model,
|
|
241
|
+
displayLines: rendered.display,
|
|
242
|
+
};
|
|
243
|
+
});
|
|
244
|
+
outputLines.push(...grouped.model);
|
|
245
|
+
displayLines.push(...grouped.display);
|
|
271
246
|
} else {
|
|
272
247
|
for (const relativePath of fileList) {
|
|
273
|
-
renderChangesForFile(relativePath);
|
|
248
|
+
const rendered = renderChangesForFile(relativePath);
|
|
249
|
+
outputLines.push(...rendered.model);
|
|
250
|
+
displayLines.push(...rendered.display);
|
|
274
251
|
}
|
|
275
252
|
}
|
|
276
253
|
|
package/src/tools/ast-grep.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import * as path from "node:path";
|
|
2
1
|
import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
|
|
3
2
|
import { type AstFindMatch, astGrep } from "@oh-my-pi/pi-natives";
|
|
4
3
|
import type { Component } from "@oh-my-pi/pi-tui";
|
|
@@ -12,9 +11,11 @@ import { Ellipsis, Hasher, type RenderCache, renderStatusLine, renderTreeList, t
|
|
|
12
11
|
import { resolveFileDisplayMode } from "../utils/file-display-mode";
|
|
13
12
|
import type { ToolSession } from ".";
|
|
14
13
|
import { createFileRecorder, formatResultPath } from "./file-recorder";
|
|
14
|
+
import { formatGroupedFiles } from "./grouped-file-output";
|
|
15
15
|
import { formatMatchLine } from "./match-line-format";
|
|
16
16
|
import type { OutputMeta } from "./output-meta";
|
|
17
17
|
import {
|
|
18
|
+
formatPathRelativeToCwd,
|
|
18
19
|
hasGlobPathChars,
|
|
19
20
|
normalizePathLikeInput,
|
|
20
21
|
parseSearchPath,
|
|
@@ -86,10 +87,7 @@ export class AstGrepTool implements AgentTool<typeof astGrepSchema, AstGrepToolD
|
|
|
86
87
|
if (!Number.isFinite(skip) || skip < 0) {
|
|
87
88
|
throw new ToolError("skip must be a non-negative number");
|
|
88
89
|
}
|
|
89
|
-
const formatScopePath = (targetPath: string): string =>
|
|
90
|
-
const relative = path.relative(this.session.cwd, targetPath).replace(/\\/g, "/");
|
|
91
|
-
return relative.length === 0 ? "." : relative;
|
|
92
|
-
};
|
|
90
|
+
const formatScopePath = (targetPath: string): string => formatPathRelativeToCwd(targetPath, this.session.cwd);
|
|
93
91
|
let searchPath: string | undefined;
|
|
94
92
|
let scopePath: string | undefined;
|
|
95
93
|
let globFilter: string | undefined;
|
|
@@ -146,7 +144,8 @@ export class AstGrepTool implements AgentTool<typeof astGrepSchema, AstGrepToolD
|
|
|
146
144
|
return parseError?.[1] ?? error;
|
|
147
145
|
});
|
|
148
146
|
const dedupedParseErrors = dedupeParseErrors(normalizedParseErrors);
|
|
149
|
-
const formatPath = (filePath: string): string =>
|
|
147
|
+
const formatPath = (filePath: string): string =>
|
|
148
|
+
formatResultPath(filePath, isDirectory, resolvedSearchPath, this.session.cwd);
|
|
150
149
|
|
|
151
150
|
const { record: recordFile, list: fileList } = createFileRecorder();
|
|
152
151
|
const fileMatchCounts = new Map<string, number>();
|
|
@@ -184,7 +183,9 @@ export class AstGrepTool implements AgentTool<typeof astGrepSchema, AstGrepToolD
|
|
|
184
183
|
const useHashLines = resolveFileDisplayMode(this.session).hashLines;
|
|
185
184
|
const outputLines: string[] = [];
|
|
186
185
|
const displayLines: string[] = [];
|
|
187
|
-
const renderMatchesForFile = (relativePath: string) => {
|
|
186
|
+
const renderMatchesForFile = (relativePath: string): { model: string[]; display: string[] } => {
|
|
187
|
+
const modelOut: string[] = [];
|
|
188
|
+
const displayOut: string[] = [];
|
|
188
189
|
const fileMatches = matchesByFile.get(relativePath) ?? [];
|
|
189
190
|
const lineNumberWidth = fileMatches.reduce((width, match) => {
|
|
190
191
|
const lineCount = match.text.split("\n").length;
|
|
@@ -197,61 +198,34 @@ export class AstGrepTool implements AgentTool<typeof astGrepSchema, AstGrepToolD
|
|
|
197
198
|
const lineNumber = match.startLine + index;
|
|
198
199
|
const isMatch = index === 0;
|
|
199
200
|
const line = matchLines[index] ?? "";
|
|
200
|
-
|
|
201
|
-
|
|
201
|
+
modelOut.push(formatMatchLine(lineNumber, line, isMatch, { useHashLines }));
|
|
202
|
+
displayOut.push(formatCodeFrameLine(isMatch ? "*" : " ", lineNumber, line, lineNumberWidth));
|
|
202
203
|
}
|
|
203
204
|
if (match.metaVariables && Object.keys(match.metaVariables).length > 0) {
|
|
204
205
|
const serializedMeta = Object.entries(match.metaVariables)
|
|
205
206
|
.sort(([left], [right]) => left.localeCompare(right))
|
|
206
207
|
.map(([key, value]) => `${key}=${value}`)
|
|
207
208
|
.join(", ");
|
|
208
|
-
|
|
209
|
-
|
|
209
|
+
modelOut.push(` meta: ${serializedMeta}`);
|
|
210
|
+
displayOut.push(` meta: ${serializedMeta}`);
|
|
210
211
|
}
|
|
211
212
|
fileMatchCounts.set(relativePath, (fileMatchCounts.get(relativePath) ?? 0) + 1);
|
|
212
213
|
}
|
|
214
|
+
return { model: modelOut, display: displayOut };
|
|
213
215
|
};
|
|
214
216
|
|
|
215
217
|
if (isDirectory) {
|
|
216
|
-
const
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
filesByDirectory.get(directory)!.push(relativePath);
|
|
223
|
-
}
|
|
224
|
-
for (const [directory, directoryFiles] of filesByDirectory) {
|
|
225
|
-
if (directory === ".") {
|
|
226
|
-
for (const relativePath of directoryFiles) {
|
|
227
|
-
if (outputLines.length > 0) {
|
|
228
|
-
outputLines.push("");
|
|
229
|
-
displayLines.push("");
|
|
230
|
-
}
|
|
231
|
-
const header = `# ${path.basename(relativePath)}`;
|
|
232
|
-
outputLines.push(header);
|
|
233
|
-
displayLines.push(header);
|
|
234
|
-
renderMatchesForFile(relativePath);
|
|
235
|
-
}
|
|
236
|
-
continue;
|
|
237
|
-
}
|
|
238
|
-
if (outputLines.length > 0) {
|
|
239
|
-
outputLines.push("");
|
|
240
|
-
displayLines.push("");
|
|
241
|
-
}
|
|
242
|
-
const dirHeader = `# ${directory}`;
|
|
243
|
-
outputLines.push(dirHeader);
|
|
244
|
-
displayLines.push(dirHeader);
|
|
245
|
-
for (const relativePath of directoryFiles) {
|
|
246
|
-
const fileHeader = `## └─ ${path.basename(relativePath)}`;
|
|
247
|
-
outputLines.push(fileHeader);
|
|
248
|
-
displayLines.push(fileHeader);
|
|
249
|
-
renderMatchesForFile(relativePath);
|
|
250
|
-
}
|
|
251
|
-
}
|
|
218
|
+
const grouped = formatGroupedFiles(fileList, relativePath => {
|
|
219
|
+
const rendered = renderMatchesForFile(relativePath);
|
|
220
|
+
return { modelLines: rendered.model, displayLines: rendered.display };
|
|
221
|
+
});
|
|
222
|
+
outputLines.push(...grouped.model);
|
|
223
|
+
displayLines.push(...grouped.display);
|
|
252
224
|
} else {
|
|
253
225
|
for (const relativePath of fileList) {
|
|
254
|
-
renderMatchesForFile(relativePath);
|
|
226
|
+
const rendered = renderMatchesForFile(relativePath);
|
|
227
|
+
outputLines.push(...rendered.model);
|
|
228
|
+
displayLines.push(...rendered.display);
|
|
255
229
|
}
|
|
256
230
|
}
|
|
257
231
|
|
package/src/tools/bash.ts
CHANGED
|
@@ -269,7 +269,7 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
|
|
269
269
|
autoBackgroundThresholdSeconds: Math.max(0, Math.floor(this.#autoBackgroundThresholdMs / 1000)),
|
|
270
270
|
hasAstGrep: this.session.settings.get("astGrep.enabled"),
|
|
271
271
|
hasAstEdit: this.session.settings.get("astEdit.enabled"),
|
|
272
|
-
|
|
272
|
+
hasSearch: this.session.settings.get("search.enabled"),
|
|
273
273
|
hasFind: this.session.settings.get("find.enabled"),
|
|
274
274
|
});
|
|
275
275
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import * as path from "node:path";
|
|
2
|
+
import { formatPathRelativeToCwd } from "./path-utils";
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* Creates a deduplicating recorder for relative file paths.
|
|
@@ -22,14 +23,13 @@ export function createFileRecorder(): {
|
|
|
22
23
|
}
|
|
23
24
|
|
|
24
25
|
/**
|
|
25
|
-
* Strip
|
|
26
|
-
*
|
|
27
|
-
* so tool output does not leak absolute paths.
|
|
26
|
+
* Strip native virtual-root prefixes and format file paths relative to cwd when
|
|
27
|
+
* they are inside cwd. Paths outside cwd remain absolute.
|
|
28
28
|
*/
|
|
29
|
-
export function formatResultPath(filePath: string, isDirectory: boolean): string {
|
|
29
|
+
export function formatResultPath(filePath: string, isDirectory: boolean, basePath: string, cwd: string): string {
|
|
30
30
|
const cleanPath = filePath.startsWith("/") ? filePath.slice(1) : filePath;
|
|
31
31
|
if (isDirectory) {
|
|
32
|
-
return
|
|
32
|
+
return formatPathRelativeToCwd(path.resolve(basePath, cleanPath), cwd);
|
|
33
33
|
}
|
|
34
|
-
return
|
|
34
|
+
return formatPathRelativeToCwd(basePath, cwd);
|
|
35
35
|
}
|
package/src/tools/find.ts
CHANGED
|
@@ -23,7 +23,13 @@ import {
|
|
|
23
23
|
import type { ToolSession } from ".";
|
|
24
24
|
import { applyListLimit } from "./list-limit";
|
|
25
25
|
import { formatFullOutputReference, type OutputMeta } from "./output-meta";
|
|
26
|
-
import {
|
|
26
|
+
import {
|
|
27
|
+
formatPathRelativeToCwd,
|
|
28
|
+
normalizePathLikeInput,
|
|
29
|
+
parseFindPattern,
|
|
30
|
+
resolveMultiFindPattern,
|
|
31
|
+
resolveToCwd,
|
|
32
|
+
} from "./path-utils";
|
|
27
33
|
import { formatCount, formatEmptyMessage, formatErrorMessage, PREVIEW_LIMITS } from "./render-utils";
|
|
28
34
|
import { ToolAbortError, ToolError, throwIfAborted } from "./tool-errors";
|
|
29
35
|
import { toolResult } from "./tool-result";
|
|
@@ -101,10 +107,7 @@ export class FindTool implements AgentTool<typeof findSchema, FindToolDetails> {
|
|
|
101
107
|
const { pattern, limit, hidden } = params;
|
|
102
108
|
|
|
103
109
|
return untilAborted(signal, async () => {
|
|
104
|
-
const formatScopePath = (targetPath: string): string =>
|
|
105
|
-
const relative = path.relative(this.session.cwd, targetPath).replace(/\\/g, "/");
|
|
106
|
-
return relative.length === 0 ? "." : relative;
|
|
107
|
-
};
|
|
110
|
+
const formatScopePath = (targetPath: string): string => formatPathRelativeToCwd(targetPath, this.session.cwd);
|
|
108
111
|
const normalizedPattern = normalizePathLikeInput(pattern).replace(/\\/g, "/");
|
|
109
112
|
if (!normalizedPattern) {
|
|
110
113
|
throw new ToolError("Pattern must not be empty");
|
|
@@ -132,14 +135,9 @@ export class FindTool implements AgentTool<typeof findSchema, FindToolDetails> {
|
|
|
132
135
|
const formatMatchPath = (matchPath: string, fileType?: natives.FileType): string => {
|
|
133
136
|
const hadTrailingSlash = matchPath.endsWith("/") || matchPath.endsWith("\\");
|
|
134
137
|
const absolutePath = path.isAbsolute(matchPath) ? matchPath : path.resolve(searchPath, matchPath);
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
}
|
|
139
|
-
if ((fileType === natives.FileType.Dir || hadTrailingSlash) && !relativePath.endsWith("/")) {
|
|
140
|
-
relativePath += "/";
|
|
141
|
-
}
|
|
142
|
-
return relativePath;
|
|
138
|
+
return formatPathRelativeToCwd(absolutePath, this.session.cwd, {
|
|
139
|
+
trailingSlash: fileType === natives.FileType.Dir || hadTrailingSlash,
|
|
140
|
+
});
|
|
143
141
|
};
|
|
144
142
|
|
|
145
143
|
const buildResult = (files: string[]): AgentToolResult<FindToolDetails> => {
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* One file's contribution to a grouped file output. The header itself is generated
|
|
5
|
+
* by `formatGroupedFiles` (single `#` for root files, `##` for files inside a dir);
|
|
6
|
+
* use `headerSuffix` to tack on extras like ` (1 replacement)`.
|
|
7
|
+
*/
|
|
8
|
+
export interface GroupedFileSection {
|
|
9
|
+
/** Optional suffix appended to the file header. */
|
|
10
|
+
headerSuffix?: string;
|
|
11
|
+
/** Body lines emitted into the textual model output. */
|
|
12
|
+
modelLines: string[];
|
|
13
|
+
/** Body lines emitted into the display output. Defaults to `modelLines`. */
|
|
14
|
+
displayLines?: string[];
|
|
15
|
+
/** When true, the file (and its header) is omitted entirely. */
|
|
16
|
+
skip?: boolean;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface GroupedFilesOutput {
|
|
20
|
+
model: string[];
|
|
21
|
+
display: string[];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Render a list of files as directory-grouped sections shared by grep, ast-grep,
|
|
26
|
+
* ast-edit, and the LSP diagnostic formatter.
|
|
27
|
+
*
|
|
28
|
+
* Layout:
|
|
29
|
+
* # dir/
|
|
30
|
+
* ## file.ts
|
|
31
|
+
* …body…
|
|
32
|
+
*
|
|
33
|
+
* # otherdir/
|
|
34
|
+
* ## other.ts
|
|
35
|
+
* …body…
|
|
36
|
+
*
|
|
37
|
+
* Files in the project root (directory `.`) become single-`#` headers without a
|
|
38
|
+
* `## file` line, matching the existing convention.
|
|
39
|
+
*/
|
|
40
|
+
export function formatGroupedFiles(
|
|
41
|
+
files: string[],
|
|
42
|
+
renderFile: (filePath: string) => GroupedFileSection,
|
|
43
|
+
): GroupedFilesOutput {
|
|
44
|
+
const filesByDirectory = new Map<string, string[]>();
|
|
45
|
+
for (const filePath of files) {
|
|
46
|
+
const directory = path.dirname(filePath).replace(/\\/g, "/");
|
|
47
|
+
if (!filesByDirectory.has(directory)) {
|
|
48
|
+
filesByDirectory.set(directory, []);
|
|
49
|
+
}
|
|
50
|
+
filesByDirectory.get(directory)!.push(filePath);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const model: string[] = [];
|
|
54
|
+
const display: string[] = [];
|
|
55
|
+
|
|
56
|
+
const pushSeparatorIfNeeded = () => {
|
|
57
|
+
if (model.length > 0) {
|
|
58
|
+
model.push("");
|
|
59
|
+
display.push("");
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
for (const [directory, dirFiles] of filesByDirectory) {
|
|
64
|
+
if (directory === ".") {
|
|
65
|
+
for (const filePath of dirFiles) {
|
|
66
|
+
const section = renderFile(filePath);
|
|
67
|
+
if (section.skip) continue;
|
|
68
|
+
pushSeparatorIfNeeded();
|
|
69
|
+
const header = `# ${path.basename(filePath)}${section.headerSuffix ?? ""}`;
|
|
70
|
+
model.push(header, ...section.modelLines);
|
|
71
|
+
display.push(header, ...(section.displayLines ?? section.modelLines));
|
|
72
|
+
}
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const sections: Array<{ filePath: string; section: GroupedFileSection }> = [];
|
|
77
|
+
for (const filePath of dirFiles) {
|
|
78
|
+
const section = renderFile(filePath);
|
|
79
|
+
if (section.skip) continue;
|
|
80
|
+
sections.push({ filePath, section });
|
|
81
|
+
}
|
|
82
|
+
if (sections.length === 0) continue;
|
|
83
|
+
|
|
84
|
+
pushSeparatorIfNeeded();
|
|
85
|
+
const dirHeader = `# ${directory}/`;
|
|
86
|
+
model.push(dirHeader);
|
|
87
|
+
display.push(dirHeader);
|
|
88
|
+
for (const { filePath, section } of sections) {
|
|
89
|
+
const fileHeader = `## ${path.basename(filePath)}${section.headerSuffix ?? ""}`;
|
|
90
|
+
model.push(fileHeader, ...section.modelLines);
|
|
91
|
+
display.push(fileHeader, ...(section.displayLines ?? section.modelLines));
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return { model, display };
|
|
96
|
+
}
|
package/src/tools/index.ts
CHANGED
|
@@ -18,7 +18,7 @@ import type { ToolChoiceQueue } from "../session/tool-choice-queue";
|
|
|
18
18
|
import { TaskTool } from "../task";
|
|
19
19
|
import type { AgentOutputManager } from "../task/output-manager";
|
|
20
20
|
import type { EventBus } from "../utils/event-bus";
|
|
21
|
-
import {
|
|
21
|
+
import { WebSearchTool } from "../web/search";
|
|
22
22
|
import { AskTool } from "./ask";
|
|
23
23
|
import { AstEditTool } from "./ast-edit";
|
|
24
24
|
import { AstGrepTool } from "./ast-grep";
|
|
@@ -30,7 +30,6 @@ import { DebugTool } from "./debug";
|
|
|
30
30
|
import { ExitPlanModeTool } from "./exit-plan-mode";
|
|
31
31
|
import { FindTool } from "./find";
|
|
32
32
|
import { GithubTool } from "./gh";
|
|
33
|
-
import { GrepTool } from "./grep";
|
|
34
33
|
import { InspectImageTool } from "./inspect-image";
|
|
35
34
|
import { IrcTool } from "./irc";
|
|
36
35
|
import { JobTool } from "./job";
|
|
@@ -42,6 +41,7 @@ import { RenderMermaidTool } from "./render-mermaid";
|
|
|
42
41
|
import { createReportToolIssueTool, isAutoQaEnabled } from "./report-tool-issue";
|
|
43
42
|
import { ResolveTool } from "./resolve";
|
|
44
43
|
import { reportFindingTool } from "./review";
|
|
44
|
+
import { SearchTool } from "./search";
|
|
45
45
|
import { SearchToolBm25Tool } from "./search-tool-bm25";
|
|
46
46
|
import { loadSshTool } from "./ssh";
|
|
47
47
|
import { type TodoPhase, TodoWriteTool } from "./todo-write";
|
|
@@ -68,7 +68,6 @@ export * from "./debug";
|
|
|
68
68
|
export * from "./exit-plan-mode";
|
|
69
69
|
export * from "./find";
|
|
70
70
|
export * from "./gh";
|
|
71
|
-
export * from "./grep";
|
|
72
71
|
export * from "./image-gen";
|
|
73
72
|
export * from "./inspect-image";
|
|
74
73
|
export * from "./irc";
|
|
@@ -80,6 +79,7 @@ export * from "./render-mermaid";
|
|
|
80
79
|
export * from "./report-tool-issue";
|
|
81
80
|
export * from "./resolve";
|
|
82
81
|
export * from "./review";
|
|
82
|
+
export * from "./search";
|
|
83
83
|
export * from "./search-tool-bm25";
|
|
84
84
|
export * from "./ssh";
|
|
85
85
|
export * from "./todo-write";
|
|
@@ -214,7 +214,7 @@ export const BUILTIN_TOOLS: Record<string, ToolFactory> = {
|
|
|
214
214
|
edit: s => new EditTool(s),
|
|
215
215
|
github: GithubTool.createIf,
|
|
216
216
|
find: s => new FindTool(s),
|
|
217
|
-
|
|
217
|
+
search: s => new SearchTool(s),
|
|
218
218
|
lsp: LspTool.createIf,
|
|
219
219
|
notebook: s => new NotebookTool(s),
|
|
220
220
|
read: s => new ReadTool(s),
|
|
@@ -226,7 +226,7 @@ export const BUILTIN_TOOLS: Record<string, ToolFactory> = {
|
|
|
226
226
|
job: JobTool.createIf,
|
|
227
227
|
irc: IrcTool.createIf,
|
|
228
228
|
todo_write: s => new TodoWriteTool(s),
|
|
229
|
-
web_search: s => new
|
|
229
|
+
web_search: s => new WebSearchTool(s),
|
|
230
230
|
search_tool_bm25: SearchToolBm25Tool.createIf,
|
|
231
231
|
write: s => new WriteTool(s),
|
|
232
232
|
};
|
|
@@ -357,7 +357,7 @@ export async function createTools(session: ToolSession, toolNames?: string[]): P
|
|
|
357
357
|
// Auto-include AST counterparts when their text-based sibling is present
|
|
358
358
|
if (requestedTools) {
|
|
359
359
|
if (
|
|
360
|
-
requestedTools.includes("
|
|
360
|
+
requestedTools.includes("search") &&
|
|
361
361
|
!requestedTools.includes("ast_grep") &&
|
|
362
362
|
session.settings.get("astGrep.enabled")
|
|
363
363
|
) {
|
|
@@ -379,7 +379,7 @@ export async function createTools(session: ToolSession, toolNames?: string[]): P
|
|
|
379
379
|
if (name === "debug") return session.settings.get("debug.enabled");
|
|
380
380
|
if (name === "todo_write") return !includeYield && session.settings.get("todo.enabled");
|
|
381
381
|
if (name === "find") return session.settings.get("find.enabled");
|
|
382
|
-
if (name === "
|
|
382
|
+
if (name === "search") return session.settings.get("search.enabled");
|
|
383
383
|
if (name === "github") return session.settings.get("github.enabled");
|
|
384
384
|
if (name === "ast_grep") return session.settings.get("astGrep.enabled");
|
|
385
385
|
if (name === "ast_edit") return session.settings.get("astEdit.enabled");
|