@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.
Files changed (69) hide show
  1. package/CHANGELOG.md +70 -0
  2. package/examples/extensions/plan-mode.ts +1 -1
  3. package/examples/sdk/README.md +1 -1
  4. package/package.json +7 -7
  5. package/src/config/prompt-templates.ts +104 -6
  6. package/src/config/settings-schema.ts +14 -13
  7. package/src/config/settings.ts +1 -1
  8. package/src/cursor.ts +4 -4
  9. package/src/edit/index.ts +111 -109
  10. package/src/edit/line-hash.ts +33 -3
  11. package/src/edit/modes/apply-patch.ts +6 -4
  12. package/src/edit/modes/atom.lark +27 -0
  13. package/src/edit/modes/atom.ts +1094 -642
  14. package/src/edit/modes/hashline.ts +9 -10
  15. package/src/edit/modes/patch.ts +23 -19
  16. package/src/edit/modes/replace.ts +19 -15
  17. package/src/edit/renderer.ts +65 -8
  18. package/src/edit/streaming.ts +47 -77
  19. package/src/extensibility/extensions/types.ts +11 -11
  20. package/src/extensibility/hooks/types.ts +6 -6
  21. package/src/lsp/edits.ts +8 -5
  22. package/src/lsp/index.ts +4 -4
  23. package/src/lsp/utils.ts +13 -43
  24. package/src/mcp/discoverable-tool-metadata.ts +1 -1
  25. package/src/mcp/manager.ts +3 -3
  26. package/src/mcp/tool-bridge.ts +4 -4
  27. package/src/memories/index.ts +1 -1
  28. package/src/modes/acp/acp-event-mapper.ts +1 -1
  29. package/src/modes/components/session-observer-overlay.ts +1 -1
  30. package/src/modes/components/settings-defs.ts +3 -3
  31. package/src/modes/components/tree-selector.ts +2 -2
  32. package/src/modes/controllers/event-controller.ts +12 -0
  33. package/src/modes/utils/ui-helpers.ts +31 -7
  34. package/src/prompts/agents/explore.md +1 -1
  35. package/src/prompts/agents/librarian.md +2 -2
  36. package/src/prompts/agents/plan.md +2 -2
  37. package/src/prompts/agents/reviewer.md +1 -1
  38. package/src/prompts/agents/task.md +2 -2
  39. package/src/prompts/system/plan-mode-active.md +1 -1
  40. package/src/prompts/system/system-prompt.md +34 -31
  41. package/src/prompts/tools/apply-patch.md +0 -2
  42. package/src/prompts/tools/atom.md +88 -97
  43. package/src/prompts/tools/bash.md +7 -4
  44. package/src/prompts/tools/checkpoint.md +1 -1
  45. package/src/prompts/tools/find.md +6 -1
  46. package/src/prompts/tools/hashline.md +10 -11
  47. package/src/prompts/tools/patch.md +13 -13
  48. package/src/prompts/tools/read.md +5 -5
  49. package/src/prompts/tools/replace.md +3 -3
  50. package/src/prompts/tools/{grep.md → search.md} +4 -4
  51. package/src/sdk.ts +19 -9
  52. package/src/session/agent-session.ts +69 -1
  53. package/src/system-prompt.ts +15 -5
  54. package/src/task/executor.ts +5 -0
  55. package/src/task/index.ts +10 -1
  56. package/src/tools/ast-edit.ts +27 -50
  57. package/src/tools/ast-grep.ts +22 -48
  58. package/src/tools/bash.ts +1 -1
  59. package/src/tools/file-recorder.ts +6 -6
  60. package/src/tools/find.ts +11 -13
  61. package/src/tools/grouped-file-output.ts +96 -0
  62. package/src/tools/index.ts +7 -7
  63. package/src/tools/path-utils.ts +31 -4
  64. package/src/tools/read.ts +12 -6
  65. package/src/tools/renderers.ts +2 -2
  66. package/src/tools/{grep.ts → search.ts} +43 -86
  67. package/src/tools/todo-write.ts +0 -1
  68. package/src/tools/write.ts +8 -4
  69. package/src/web/search/index.ts +1 -1
@@ -157,6 +157,27 @@ export function resolveToCwd(filePath: string, cwd: string): string {
157
157
  return path.resolve(cwd, expanded);
158
158
  }
159
159
 
160
+ export function formatPathRelativeToCwd(
161
+ filePath: string,
162
+ cwd: string,
163
+ options: { trailingSlash?: boolean } = {},
164
+ ): string {
165
+ const resolvedCwd = path.resolve(cwd);
166
+ const normalized = normalizeLocalScheme(filePath);
167
+ if (isInternalUrlPath(normalized)) {
168
+ return normalized;
169
+ }
170
+ const expanded = expandPath(normalized);
171
+ const resolvedPath = path.isAbsolute(expanded) ? path.resolve(expanded) : path.resolve(cwd, expanded);
172
+ const relative = path.relative(resolvedCwd, resolvedPath);
173
+ const isWithinCwd = relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative));
174
+ let displayPath = normalizePosixPath(isWithinCwd ? relative || "." : resolvedPath);
175
+ if (options.trailingSlash && displayPath !== "." && !displayPath.endsWith("/")) {
176
+ displayPath += "/";
177
+ }
178
+ return displayPath;
179
+ }
180
+
160
181
  /**
161
182
  * Strip matching surrounding double quotes from a path string.
162
183
  * Common when users paste quoted paths from Windows Explorer or shell copy-paste.
@@ -381,8 +402,14 @@ function findCommonBasePath(paths: string[]): string {
381
402
  return joined || path.parse(path.resolve(paths[0])).root;
382
403
  }
383
404
 
384
- function toScopeDisplay(items: string[]): string {
385
- return items.map(item => normalizePosixPath(item)).join(", ");
405
+ function toScopeDisplay(items: string[], cwd: string): string {
406
+ return items
407
+ .map(item =>
408
+ formatPathRelativeToCwd(item, cwd, {
409
+ trailingSlash: item.endsWith("/") || item.endsWith("\\"),
410
+ }),
411
+ )
412
+ .join(", ");
386
413
  }
387
414
 
388
415
  function looksLikeDelimitedPathToken(token: string): boolean {
@@ -533,7 +560,7 @@ export async function resolveMultiSearchPath(
533
560
  return {
534
561
  basePath: commonBasePath,
535
562
  glob: buildBraceUnion(combinedPatterns),
536
- scopePath: toScopeDisplay(pathItems),
563
+ scopePath: toScopeDisplay(pathItems, cwd),
537
564
  exactFilePaths: allExactFiles ? parsedItems.map(item => item.absoluteBasePath) : undefined,
538
565
  };
539
566
  }
@@ -571,7 +598,7 @@ export async function resolveMultiFindPattern(
571
598
  return {
572
599
  basePath: commonBasePath,
573
600
  globPattern: buildBraceUnion(combinedPatterns) ?? "**/*",
574
- scopePath: toScopeDisplay(patternItems),
601
+ scopePath: toScopeDisplay(patternItems, cwd),
575
602
  };
576
603
  }
577
604
 
package/src/tools/read.ts CHANGED
@@ -40,7 +40,7 @@ import {
40
40
  } from "./fetch";
41
41
  import { applyListLimit } from "./list-limit";
42
42
  import { formatFullOutputReference, formatStyledTruncationWarning, type OutputMeta } from "./output-meta";
43
- import { expandPath, resolveReadPath } from "./path-utils";
43
+ import { expandPath, formatPathRelativeToCwd, resolveReadPath } from "./path-utils";
44
44
  import { formatAge, formatBytes, shortenPath, wrapBrackets } from "./render-utils";
45
45
  import {
46
46
  executeReadQuery,
@@ -1046,7 +1046,10 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
1046
1046
  ? "- Alpha: no"
1047
1047
  : "- Alpha: unknown",
1048
1048
  "",
1049
- `If you want to analyze the image, call inspect_image with path="${readPath}" and a question describing what to inspect and the desired output format.`,
1049
+ `If you want to analyze the image, call inspect_image with path="${formatPathRelativeToCwd(
1050
+ absolutePath,
1051
+ this.session.cwd,
1052
+ )}" and a question describing what to inspect and the desired output format.`,
1050
1053
  ];
1051
1054
  content = [{ type: "text", text: metadataLines.join("\n") }];
1052
1055
  details = {};
@@ -1110,12 +1113,15 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
1110
1113
  const effectiveLimit = limit ?? DEFAULT_LIMIT;
1111
1114
  const maxLinesToCollect = Math.min(effectiveLimit, DEFAULT_MAX_LINES);
1112
1115
  const selectedLineLimit = effectiveLimit;
1116
+ // Scale byte budget with line limit so the configured line count actually fits.
1117
+ // Assume ~512 bytes/line average; never go below the shared default.
1118
+ const maxBytesForRead = Math.max(DEFAULT_MAX_BYTES, maxLinesToCollect * 512);
1113
1119
 
1114
1120
  const streamResult = await streamLinesFromFile(
1115
1121
  absolutePath,
1116
1122
  startLine,
1117
1123
  maxLinesToCollect,
1118
- DEFAULT_MAX_BYTES,
1124
+ maxBytesForRead,
1119
1125
  selectedLineLimit,
1120
1126
  signal,
1121
1127
  );
@@ -1146,7 +1152,7 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
1146
1152
  const totalSelectedLines = totalFileLines - startLine;
1147
1153
  const totalSelectedBytes = collectedBytes;
1148
1154
  const wasTruncated = collectedLines.length < totalSelectedLines || stoppedByByteLimit;
1149
- const firstLineExceedsLimit = firstLineByteLength !== undefined && firstLineByteLength > DEFAULT_MAX_BYTES;
1155
+ const firstLineExceedsLimit = firstLineByteLength !== undefined && firstLineByteLength > maxBytesForRead;
1150
1156
 
1151
1157
  const truncation: TruncationResult = {
1152
1158
  content: selectedContent,
@@ -1178,14 +1184,14 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
1178
1184
  if (shouldAddHashLines) {
1179
1185
  outputText = `[Line ${startLineDisplay} is ${formatBytes(
1180
1186
  firstLineBytes,
1181
- )}, exceeds ${formatBytes(DEFAULT_MAX_BYTES)} limit. Hashline output requires full lines; cannot compute hashes for a truncated preview.]`;
1187
+ )}, exceeds ${formatBytes(maxBytesForRead)} limit. Hashline output requires full lines; cannot compute hashes for a truncated preview.]`;
1182
1188
  } else {
1183
1189
  outputText = formatText(snippet.text, startLineDisplay);
1184
1190
  }
1185
1191
  if (snippet.text.length === 0) {
1186
1192
  outputText = `[Line ${startLineDisplay} is ${formatBytes(
1187
1193
  firstLineBytes,
1188
- )}, exceeds ${formatBytes(DEFAULT_MAX_BYTES)} limit. Unable to display a valid UTF-8 snippet.]`;
1194
+ )}, exceeds ${formatBytes(maxBytesForRead)} limit. Unable to display a valid UTF-8 snippet.]`;
1189
1195
  }
1190
1196
  details = { truncation };
1191
1197
  sourcePath = absolutePath;
@@ -18,13 +18,13 @@ import { calculatorToolRenderer } from "./calculator";
18
18
  import { debugToolRenderer } from "./debug";
19
19
  import { findToolRenderer } from "./find";
20
20
  import { githubToolRenderer } from "./gh-renderer";
21
- import { grepToolRenderer } from "./grep";
22
21
  import { inspectImageToolRenderer } from "./inspect-image-renderer";
23
22
  import { jobToolRenderer } from "./job";
24
23
  import { notebookToolRenderer } from "./notebook";
25
24
  import { pythonToolRenderer } from "./python";
26
25
  import { readToolRenderer } from "./read";
27
26
  import { resolveToolRenderer } from "./resolve";
27
+ import { searchToolRenderer } from "./search";
28
28
  import { searchToolBm25Renderer } from "./search-tool-bm25";
29
29
  import { sshToolRenderer } from "./ssh";
30
30
  import { todoWriteToolRenderer } from "./todo-write";
@@ -54,7 +54,7 @@ export const toolRenderers: Record<string, ToolRenderer> = {
54
54
  edit: editToolRenderer as ToolRenderer,
55
55
  apply_patch: editToolRenderer as ToolRenderer,
56
56
  find: findToolRenderer as ToolRenderer,
57
- grep: grepToolRenderer as ToolRenderer,
57
+ search: searchToolRenderer as ToolRenderer,
58
58
  lsp: lspToolRenderer as ToolRenderer,
59
59
  notebook: notebookToolRenderer as ToolRenderer,
60
60
  inspect_image: inspectImageToolRenderer as ToolRenderer,
@@ -8,15 +8,17 @@ import { prompt, untilAborted } from "@oh-my-pi/pi-utils";
8
8
  import { type Static, Type } from "@sinclair/typebox";
9
9
  import type { RenderResultOptions } from "../extensibility/custom-tools/types";
10
10
  import type { Theme } from "../modes/theme/theme";
11
- import grepDescription from "../prompts/tools/grep.md" with { type: "text" };
11
+ import searchDescription from "../prompts/tools/search.md" with { type: "text" };
12
12
  import { DEFAULT_MAX_COLUMN, type TruncationResult, truncateHead } from "../session/streaming-output";
13
13
  import { Ellipsis, Hasher, type RenderCache, renderStatusLine, renderTreeList, truncateToWidth } from "../tui";
14
14
  import { resolveFileDisplayMode } from "../utils/file-display-mode";
15
15
  import type { ToolSession } from ".";
16
- import { createFileRecorder } from "./file-recorder";
16
+ import { createFileRecorder, formatResultPath } from "./file-recorder";
17
+ import { formatGroupedFiles } from "./grouped-file-output";
17
18
  import { formatMatchLine } from "./match-line-format";
18
19
  import { formatFullOutputReference, type OutputMeta } from "./output-meta";
19
20
  import {
21
+ formatPathRelativeToCwd,
20
22
  hasGlobPathChars,
21
23
  normalizePathLikeInput,
22
24
  parseSearchPath,
@@ -33,7 +35,7 @@ import {
33
35
  import { ToolError } from "./tool-errors";
34
36
  import { toolResult } from "./tool-result";
35
37
 
36
- const grepSchema = Type.Object({
38
+ const searchSchema = Type.Object({
37
39
  pattern: Type.String({ description: "regex pattern", examples: ["function\\s+\\w+", "TODO"] }),
38
40
  path: Type.String({
39
41
  description: "file, directory, glob, comma-separated paths, or internal URL to search",
@@ -44,11 +46,11 @@ const grepSchema = Type.Object({
44
46
  skip: Type.Optional(Type.Number({ description: "matches to skip", default: 0 })),
45
47
  });
46
48
 
47
- export type GrepToolInput = Static<typeof grepSchema>;
49
+ export type SearchToolInput = Static<typeof searchSchema>;
48
50
 
49
51
  const DEFAULT_MATCH_LIMIT = 20;
50
52
 
51
- export interface GrepToolDetails {
53
+ export interface SearchToolDetails {
52
54
  truncation?: TruncationResult;
53
55
  matchLimitReached?: number;
54
56
  resultLimitReached?: number;
@@ -67,18 +69,18 @@ export interface GrepToolDetails {
67
69
  displayContent?: string;
68
70
  }
69
71
 
70
- type GrepParams = Static<typeof grepSchema>;
72
+ type SearchParams = Static<typeof searchSchema>;
71
73
 
72
- export class GrepTool implements AgentTool<typeof grepSchema, GrepToolDetails> {
73
- readonly name = "grep";
74
- readonly label = "Grep";
74
+ export class SearchTool implements AgentTool<typeof searchSchema, SearchToolDetails> {
75
+ readonly name = "search";
76
+ readonly label = "Search";
75
77
  readonly description: string;
76
- readonly parameters = grepSchema;
78
+ readonly parameters = searchSchema;
77
79
  readonly strict = true;
78
80
 
79
81
  constructor(private readonly session: ToolSession) {
80
82
  const displayMode = resolveFileDisplayMode(session);
81
- this.description = prompt.render(grepDescription, {
83
+ this.description = prompt.render(searchDescription, {
82
84
  IS_HASHLINE_MODE: displayMode.hashLines,
83
85
  IS_LINE_NUMBER_MODE: !displayMode.hashLines && displayMode.lineNumbers,
84
86
  });
@@ -86,11 +88,11 @@ export class GrepTool implements AgentTool<typeof grepSchema, GrepToolDetails> {
86
88
 
87
89
  async execute(
88
90
  _toolCallId: string,
89
- params: GrepParams,
91
+ params: SearchParams,
90
92
  signal?: AbortSignal,
91
- _onUpdate?: AgentToolUpdateCallback<GrepToolDetails>,
93
+ _onUpdate?: AgentToolUpdateCallback<SearchToolDetails>,
92
94
  _toolContext?: AgentToolContext,
93
- ): Promise<AgentToolResult<GrepToolDetails>> {
95
+ ): Promise<AgentToolResult<SearchToolDetails>> {
94
96
  const { pattern, path: searchDir, i, gitignore, skip } = params;
95
97
 
96
98
  return untilAborted(signal, async () => {
@@ -103,18 +105,15 @@ export class GrepTool implements AgentTool<typeof grepSchema, GrepToolDetails> {
103
105
  if (normalizedSkip < 0 || !Number.isFinite(normalizedSkip)) {
104
106
  throw new ToolError("Skip must be a non-negative number");
105
107
  }
106
- const normalizedContextBefore = this.session.settings.get("grep.contextBefore");
107
- const normalizedContextAfter = this.session.settings.get("grep.contextAfter");
108
+ const normalizedContextBefore = this.session.settings.get("search.contextBefore");
109
+ const normalizedContextAfter = this.session.settings.get("search.contextAfter");
108
110
  const ignoreCase = i ?? false;
109
111
  const useGitignore = gitignore ?? true;
110
112
  const patternHasNewline = normalizedPattern.includes("\n") || normalizedPattern.includes("\\n");
111
113
  const effectiveMultiline = patternHasNewline;
112
114
 
113
115
  const useHashLines = resolveFileDisplayMode(this.session).hashLines;
114
- const formatScopePath = (targetPath: string): string => {
115
- const relative = path.relative(this.session.cwd, targetPath).replace(/\\/g, "/");
116
- return relative.length === 0 ? "." : relative;
117
- };
116
+ const formatScopePath = (targetPath: string): string => formatPathRelativeToCwd(targetPath, this.session.cwd);
118
117
  let searchPath: string;
119
118
  let scopePath: string;
120
119
  let exactFilePaths: string[] | undefined;
@@ -130,7 +129,7 @@ export class GrepTool implements AgentTool<typeof grepSchema, GrepToolDetails> {
130
129
  }
131
130
  const resource = await internalRouter.resolve(rawPath);
132
131
  if (!resource.sourcePath) {
133
- throw new ToolError(`Cannot grep internal URL without a backing file: ${rawPath}`);
132
+ throw new ToolError(`Cannot search internal URL without a backing file: ${rawPath}`);
134
133
  }
135
134
  searchPath = resource.sourcePath;
136
135
  scopePath = formatScopePath(searchPath);
@@ -224,14 +223,8 @@ export class GrepTool implements AgentTool<typeof grepSchema, GrepToolDetails> {
224
223
  throw err;
225
224
  }
226
225
 
227
- const formatPath = (filePath: string): string => {
228
- // returns paths starting with / (the virtual root)
229
- const cleanPath = filePath.startsWith("/") ? filePath.slice(1) : filePath;
230
- if (isDirectory) {
231
- return cleanPath.replace(/\\/g, "/");
232
- }
233
- return path.basename(cleanPath);
234
- };
226
+ const formatPath = (filePath: string): string =>
227
+ formatResultPath(filePath, isDirectory, searchPath, this.session.cwd);
235
228
 
236
229
  // Build output
237
230
  const roundRobinSelect = (matches: GrepMatch[], limit: number): GrepMatch[] => {
@@ -272,7 +265,7 @@ export class GrepTool implements AgentTool<typeof grepSchema, GrepToolDetails> {
272
265
  const { record: recordFile, list: fileList } = createFileRecorder();
273
266
  const fileMatchCounts = new Map<string, number>();
274
267
  if (selectedMatches.length === 0) {
275
- const details: GrepToolDetails = {
268
+ const details: SearchToolDetails = {
276
269
  scopePath,
277
270
  matchCount: 0,
278
271
  fileCount: 0,
@@ -283,7 +276,6 @@ export class GrepTool implements AgentTool<typeof grepSchema, GrepToolDetails> {
283
276
  }
284
277
  const outputLines: string[] = [];
285
278
  let linesTruncated = false;
286
- const hasContextLines = normalizedContextBefore > 0 || normalizedContextAfter > 0;
287
279
  const matchesByFile = new Map<string, GrepMatch[]>();
288
280
  for (const match of selectedMatches) {
289
281
  const relativePath = formatPath(match.path);
@@ -332,46 +324,16 @@ export class GrepTool implements AgentTool<typeof grepSchema, GrepToolDetails> {
332
324
  return { model: modelOut, display: displayOut };
333
325
  };
334
326
  if (isDirectory) {
335
- const filesByDirectory = new Map<string, string[]>();
336
- for (const relativePath of fileList) {
337
- const directory = path.dirname(relativePath).replace(/\\/g, "/");
338
- if (!filesByDirectory.has(directory)) {
339
- filesByDirectory.set(directory, []);
340
- }
341
- filesByDirectory.get(directory)!.push(relativePath);
342
- }
343
- for (const [directory, directoryFiles] of filesByDirectory) {
344
- if (directory === ".") {
345
- for (const relativePath of directoryFiles) {
346
- const rendered = renderMatchesForFile(relativePath);
347
- if (rendered.model.length === 0) continue;
348
- if (outputLines.length > 0) {
349
- outputLines.push("");
350
- displayLines.push("");
351
- }
352
- const header = `# ${path.basename(relativePath)}`;
353
- outputLines.push(header, ...rendered.model);
354
- displayLines.push(header, ...rendered.display);
355
- }
356
- continue;
357
- }
358
- const renderedFiles = directoryFiles
359
- .map(relativePath => ({ relativePath, rendered: renderMatchesForFile(relativePath) }))
360
- .filter(file => file.rendered.model.length > 0);
361
- if (renderedFiles.length === 0) continue;
362
- if (outputLines.length > 0) {
363
- outputLines.push("");
364
- displayLines.push("");
365
- }
366
- const dirHeader = `# ${directory}`;
367
- outputLines.push(dirHeader);
368
- displayLines.push(dirHeader);
369
- for (const { relativePath, rendered } of renderedFiles) {
370
- const fileHeader = `## └─ ${path.basename(relativePath)}`;
371
- outputLines.push(fileHeader, ...rendered.model);
372
- displayLines.push(fileHeader, ...rendered.display);
373
- }
374
- }
327
+ const grouped = formatGroupedFiles(fileList, relativePath => {
328
+ const rendered = renderMatchesForFile(relativePath);
329
+ return {
330
+ modelLines: rendered.model,
331
+ displayLines: rendered.display,
332
+ skip: rendered.model.length === 0,
333
+ };
334
+ });
335
+ outputLines.push(...grouped.model);
336
+ displayLines.push(...grouped.display);
375
337
  } else {
376
338
  for (const relativePath of fileList) {
377
339
  const rendered = renderMatchesForFile(relativePath);
@@ -379,11 +341,6 @@ export class GrepTool implements AgentTool<typeof grepSchema, GrepToolDetails> {
379
341
  displayLines.push(...rendered.display);
380
342
  }
381
343
  }
382
- if (hasContextLines && outputLines.length > 0) {
383
- outputLines.unshift(
384
- "[grep] '*' marks match lines; leading space marks context. Anchor and content are separated by '|'.",
385
- );
386
- }
387
344
  if (matchLimitReached || result.limitReached) {
388
345
  outputLines.push("", limitMessage);
389
346
  }
@@ -391,7 +348,7 @@ export class GrepTool implements AgentTool<typeof grepSchema, GrepToolDetails> {
391
348
  const truncation = truncateHead(rawOutput, { maxLines: Number.MAX_SAFE_INTEGER });
392
349
  const output = truncation.content;
393
350
  const truncated = Boolean(matchLimitReached || result.limitReached || truncation.truncated || linesTruncated);
394
- const details: GrepToolDetails = {
351
+ const details: SearchToolDetails = {
395
352
  scopePath,
396
353
  matchCount: selectedMatches.length,
397
354
  fileCount: fileList.length,
@@ -422,7 +379,7 @@ export class GrepTool implements AgentTool<typeof grepSchema, GrepToolDetails> {
422
379
  // TUI Renderer
423
380
  // =============================================================================
424
381
 
425
- interface GrepRenderArgs {
382
+ interface SearchRenderArgs {
426
383
  pattern: string;
427
384
  path?: string;
428
385
  i?: boolean;
@@ -432,9 +389,9 @@ interface GrepRenderArgs {
432
389
 
433
390
  const COLLAPSED_TEXT_LIMIT = PREVIEW_LIMITS.COLLAPSED_LINES * 2;
434
391
 
435
- export const grepToolRenderer = {
392
+ export const searchToolRenderer = {
436
393
  inline: true,
437
- renderCall(args: GrepRenderArgs, _options: RenderResultOptions, uiTheme: Theme): Component {
394
+ renderCall(args: SearchRenderArgs, _options: RenderResultOptions, uiTheme: Theme): Component {
438
395
  const meta: string[] = [];
439
396
  if (args.path) meta.push(`in ${args.path}`);
440
397
  if (args.i) meta.push("case:insensitive");
@@ -442,17 +399,17 @@ export const grepToolRenderer = {
442
399
  if (args.skip !== undefined && args.skip > 0) meta.push(`skip:${args.skip}`);
443
400
 
444
401
  const text = renderStatusLine(
445
- { icon: "pending", title: "Grep", description: args.pattern || "?", meta },
402
+ { icon: "pending", title: "Search", description: args.pattern || "?", meta },
446
403
  uiTheme,
447
404
  );
448
405
  return new Text(text, 0, 0);
449
406
  },
450
407
 
451
408
  renderResult(
452
- result: { content: Array<{ type: string; text?: string }>; details?: GrepToolDetails; isError?: boolean },
409
+ result: { content: Array<{ type: string; text?: string }>; details?: SearchToolDetails; isError?: boolean },
453
410
  options: RenderResultOptions,
454
411
  uiTheme: Theme,
455
- args?: GrepRenderArgs,
412
+ args?: SearchRenderArgs,
456
413
  ): Component {
457
414
  const details = result.details;
458
415
 
@@ -471,7 +428,7 @@ export const grepToolRenderer = {
471
428
  const lines = textContent.split("\n").filter(line => line.trim() !== "");
472
429
  const description = args?.pattern ?? undefined;
473
430
  const header = renderStatusLine(
474
- { icon: "success", title: "Grep", description, meta: [formatCount("item", lines.length)] },
431
+ { icon: "success", title: "Search", description, meta: [formatCount("item", lines.length)] },
475
432
  uiTheme,
476
433
  );
477
434
  let cached: RenderCache | undefined;
@@ -511,7 +468,7 @@ export const grepToolRenderer = {
511
468
 
512
469
  if (matchCount === 0) {
513
470
  const header = renderStatusLine(
514
- { icon: "warning", title: "Grep", description: args?.pattern, meta: ["0 matches"] },
471
+ { icon: "warning", title: "Search", description: args?.pattern, meta: ["0 matches"] },
515
472
  uiTheme,
516
473
  );
517
474
  return new Text([header, formatEmptyMessage("No matches found", uiTheme)].join("\n"), 0, 0);
@@ -523,7 +480,7 @@ export const grepToolRenderer = {
523
480
  if (truncated) meta.push(uiTheme.fg("warning", "truncated"));
524
481
  const description = args?.pattern ?? undefined;
525
482
  const header = renderStatusLine(
526
- { icon: truncated ? "warning" : "success", title: "Grep", description, meta },
483
+ { icon: truncated ? "warning" : "success", title: "Search", description, meta },
527
484
  uiTheme,
528
485
  );
529
486
 
@@ -717,7 +717,6 @@ export const todoWriteToolRenderer = {
717
717
  const lines: string[] = [header];
718
718
  for (let p = 0; p < phases.length; p++) {
719
719
  const phase = phases[p];
720
- if (p > 0) lines.push("");
721
720
  if (phases.length > 1) {
722
721
  lines.push(uiTheme.fg("accent", chalk.bold(` ${phase.name}`)));
723
722
  }
@@ -19,6 +19,7 @@ import { parseArchivePathCandidates } from "./archive-reader";
19
19
  import { assertEditableFile } from "./auto-generated-guard";
20
20
  import { invalidateFsScanAfterWrite } from "./fs-cache-invalidation";
21
21
  import { type OutputMeta, outputMeta } from "./output-meta";
22
+ import { formatPathRelativeToCwd } from "./path-utils";
22
23
  import { enforcePlanModeWrite, resolvePlanPath } from "./plan-mode-guard";
23
24
  import {
24
25
  formatDiagnostics,
@@ -212,7 +213,6 @@ export class WriteTool implements AgentTool<typeof writeSchema, WriteToolDetails
212
213
  }
213
214
 
214
215
  async #writeArchiveEntry(
215
- displayPath: string,
216
216
  content: string,
217
217
  resolvedArchivePath: ResolvedArchiveWritePath,
218
218
  ): Promise<AgentToolResult<WriteToolDetails>> {
@@ -278,8 +278,11 @@ export class WriteTool implements AgentTool<typeof writeSchema, WriteToolDetails
278
278
  }
279
279
 
280
280
  invalidateFsScanAfterWrite(resolvedArchivePath.absolutePath);
281
+ const outputPath = `${formatPathRelativeToCwd(resolvedArchivePath.absolutePath, this.session.cwd)}:${
282
+ resolvedArchivePath.archiveSubPath
283
+ }`;
281
284
  return {
282
- content: [{ type: "text", text: `Successfully wrote ${content.length} bytes to ${displayPath}` }],
285
+ content: [{ type: "text", text: `Successfully wrote ${content.length} bytes to ${outputPath}` }],
283
286
  details: {},
284
287
  };
285
288
  }
@@ -426,7 +429,7 @@ export class WriteTool implements AgentTool<typeof writeSchema, WriteToolDetails
426
429
  op: resolvedArchivePath.exists ? "update" : "create",
427
430
  });
428
431
 
429
- const archiveResult = await this.#writeArchiveEntry(path, cleanContent, resolvedArchivePath);
432
+ const archiveResult = await this.#writeArchiveEntry(cleanContent, resolvedArchivePath);
430
433
  if (stripped) {
431
434
  const firstText = archiveResult.content.find(
432
435
  (block): block is { type: "text"; text: string } =>
@@ -468,7 +471,8 @@ export class WriteTool implements AgentTool<typeof writeSchema, WriteToolDetails
468
471
  const diagnostics = await this.#writethrough(absolutePath, cleanContent, signal, undefined, batchRequest);
469
472
  invalidateFsScanAfterWrite(absolutePath);
470
473
 
471
- let resultText = `Successfully wrote ${cleanContent.length} bytes to ${path}`;
474
+ const displayPath = formatPathRelativeToCwd(absolutePath, this.session.cwd);
475
+ let resultText = `Successfully wrote ${cleanContent.length} bytes to ${displayPath}`;
472
476
  if (stripped) {
473
477
  resultText += `\nNote: auto-stripped hashline display prefixes from content before writing.`;
474
478
  }
@@ -205,7 +205,7 @@ export async function runSearchQuery(
205
205
  * Supports Anthropic, Perplexity, Exa, Brave, Jina, Kimi, Gemini, Codex, Z.AI, SearXNG, and Synthetic providers with automatic fallback.
206
206
  * Session is accepted for interface consistency but not used.
207
207
  */
208
- export class SearchTool implements AgentTool<typeof webSearchSchema, SearchRenderDetails> {
208
+ export class WebSearchTool implements AgentTool<typeof webSearchSchema, SearchRenderDetails> {
209
209
  readonly name = "web_search";
210
210
  readonly label = "Web Search";
211
211
  readonly description: string;