@oh-my-pi/pi-coding-agent 14.5.3 → 14.5.6

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 (68) hide show
  1. package/CHANGELOG.md +49 -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 +103 -8
  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 +1039 -841
  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 +7 -7
  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/utils/ui-helpers.ts +31 -7
  33. package/src/prompts/agents/explore.md +1 -1
  34. package/src/prompts/agents/librarian.md +2 -2
  35. package/src/prompts/agents/plan.md +2 -2
  36. package/src/prompts/agents/reviewer.md +1 -1
  37. package/src/prompts/agents/task.md +2 -2
  38. package/src/prompts/system/plan-mode-active.md +1 -1
  39. package/src/prompts/system/system-prompt.md +116 -60
  40. package/src/prompts/tools/apply-patch.md +0 -2
  41. package/src/prompts/tools/atom.md +81 -63
  42. package/src/prompts/tools/bash.md +7 -4
  43. package/src/prompts/tools/checkpoint.md +1 -1
  44. package/src/prompts/tools/find.md +6 -1
  45. package/src/prompts/tools/hashline.md +10 -11
  46. package/src/prompts/tools/patch.md +13 -13
  47. package/src/prompts/tools/read.md +4 -4
  48. package/src/prompts/tools/replace.md +3 -3
  49. package/src/prompts/tools/{grep.md → search.md} +4 -4
  50. package/src/sdk.ts +19 -9
  51. package/src/session/agent-session.ts +65 -0
  52. package/src/system-prompt.ts +15 -5
  53. package/src/task/executor.ts +5 -0
  54. package/src/task/index.ts +10 -1
  55. package/src/tools/ast-edit.ts +4 -6
  56. package/src/tools/ast-grep.ts +4 -6
  57. package/src/tools/bash.ts +1 -1
  58. package/src/tools/file-recorder.ts +6 -6
  59. package/src/tools/find.ts +11 -13
  60. package/src/tools/index.ts +7 -7
  61. package/src/tools/path-utils.ts +31 -4
  62. package/src/tools/read.ts +12 -6
  63. package/src/tools/renderers.ts +2 -2
  64. package/src/tools/{grep.ts → search.ts} +32 -40
  65. package/src/tools/write.ts +8 -4
  66. package/src/web/search/index.ts +1 -1
  67. package/src/edit/block.ts +0 -308
  68. package/src/edit/indent.ts +0 -150
@@ -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,16 +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
17
  import { formatGroupedFiles } from "./grouped-file-output";
18
18
  import { formatMatchLine } from "./match-line-format";
19
19
  import { formatFullOutputReference, type OutputMeta } from "./output-meta";
20
20
  import {
21
+ formatPathRelativeToCwd,
21
22
  hasGlobPathChars,
22
23
  normalizePathLikeInput,
23
24
  parseSearchPath,
@@ -34,7 +35,7 @@ import {
34
35
  import { ToolError } from "./tool-errors";
35
36
  import { toolResult } from "./tool-result";
36
37
 
37
- const grepSchema = Type.Object({
38
+ const searchSchema = Type.Object({
38
39
  pattern: Type.String({ description: "regex pattern", examples: ["function\\s+\\w+", "TODO"] }),
39
40
  path: Type.String({
40
41
  description: "file, directory, glob, comma-separated paths, or internal URL to search",
@@ -45,11 +46,11 @@ const grepSchema = Type.Object({
45
46
  skip: Type.Optional(Type.Number({ description: "matches to skip", default: 0 })),
46
47
  });
47
48
 
48
- export type GrepToolInput = Static<typeof grepSchema>;
49
+ export type SearchToolInput = Static<typeof searchSchema>;
49
50
 
50
51
  const DEFAULT_MATCH_LIMIT = 20;
51
52
 
52
- export interface GrepToolDetails {
53
+ export interface SearchToolDetails {
53
54
  truncation?: TruncationResult;
54
55
  matchLimitReached?: number;
55
56
  resultLimitReached?: number;
@@ -68,18 +69,18 @@ export interface GrepToolDetails {
68
69
  displayContent?: string;
69
70
  }
70
71
 
71
- type GrepParams = Static<typeof grepSchema>;
72
+ type SearchParams = Static<typeof searchSchema>;
72
73
 
73
- export class GrepTool implements AgentTool<typeof grepSchema, GrepToolDetails> {
74
- readonly name = "grep";
75
- readonly label = "Grep";
74
+ export class SearchTool implements AgentTool<typeof searchSchema, SearchToolDetails> {
75
+ readonly name = "search";
76
+ readonly label = "Search";
76
77
  readonly description: string;
77
- readonly parameters = grepSchema;
78
+ readonly parameters = searchSchema;
78
79
  readonly strict = true;
79
80
 
80
81
  constructor(private readonly session: ToolSession) {
81
82
  const displayMode = resolveFileDisplayMode(session);
82
- this.description = prompt.render(grepDescription, {
83
+ this.description = prompt.render(searchDescription, {
83
84
  IS_HASHLINE_MODE: displayMode.hashLines,
84
85
  IS_LINE_NUMBER_MODE: !displayMode.hashLines && displayMode.lineNumbers,
85
86
  });
@@ -87,11 +88,11 @@ export class GrepTool implements AgentTool<typeof grepSchema, GrepToolDetails> {
87
88
 
88
89
  async execute(
89
90
  _toolCallId: string,
90
- params: GrepParams,
91
+ params: SearchParams,
91
92
  signal?: AbortSignal,
92
- _onUpdate?: AgentToolUpdateCallback<GrepToolDetails>,
93
+ _onUpdate?: AgentToolUpdateCallback<SearchToolDetails>,
93
94
  _toolContext?: AgentToolContext,
94
- ): Promise<AgentToolResult<GrepToolDetails>> {
95
+ ): Promise<AgentToolResult<SearchToolDetails>> {
95
96
  const { pattern, path: searchDir, i, gitignore, skip } = params;
96
97
 
97
98
  return untilAborted(signal, async () => {
@@ -104,18 +105,15 @@ export class GrepTool implements AgentTool<typeof grepSchema, GrepToolDetails> {
104
105
  if (normalizedSkip < 0 || !Number.isFinite(normalizedSkip)) {
105
106
  throw new ToolError("Skip must be a non-negative number");
106
107
  }
107
- const normalizedContextBefore = this.session.settings.get("grep.contextBefore");
108
- 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");
109
110
  const ignoreCase = i ?? false;
110
111
  const useGitignore = gitignore ?? true;
111
112
  const patternHasNewline = normalizedPattern.includes("\n") || normalizedPattern.includes("\\n");
112
113
  const effectiveMultiline = patternHasNewline;
113
114
 
114
115
  const useHashLines = resolveFileDisplayMode(this.session).hashLines;
115
- const formatScopePath = (targetPath: string): string => {
116
- const relative = path.relative(this.session.cwd, targetPath).replace(/\\/g, "/");
117
- return relative.length === 0 ? "." : relative;
118
- };
116
+ const formatScopePath = (targetPath: string): string => formatPathRelativeToCwd(targetPath, this.session.cwd);
119
117
  let searchPath: string;
120
118
  let scopePath: string;
121
119
  let exactFilePaths: string[] | undefined;
@@ -131,7 +129,7 @@ export class GrepTool implements AgentTool<typeof grepSchema, GrepToolDetails> {
131
129
  }
132
130
  const resource = await internalRouter.resolve(rawPath);
133
131
  if (!resource.sourcePath) {
134
- 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}`);
135
133
  }
136
134
  searchPath = resource.sourcePath;
137
135
  scopePath = formatScopePath(searchPath);
@@ -225,14 +223,8 @@ export class GrepTool implements AgentTool<typeof grepSchema, GrepToolDetails> {
225
223
  throw err;
226
224
  }
227
225
 
228
- const formatPath = (filePath: string): string => {
229
- // returns paths starting with / (the virtual root)
230
- const cleanPath = filePath.startsWith("/") ? filePath.slice(1) : filePath;
231
- if (isDirectory) {
232
- return cleanPath.replace(/\\/g, "/");
233
- }
234
- return path.basename(cleanPath);
235
- };
226
+ const formatPath = (filePath: string): string =>
227
+ formatResultPath(filePath, isDirectory, searchPath, this.session.cwd);
236
228
 
237
229
  // Build output
238
230
  const roundRobinSelect = (matches: GrepMatch[], limit: number): GrepMatch[] => {
@@ -273,7 +265,7 @@ export class GrepTool implements AgentTool<typeof grepSchema, GrepToolDetails> {
273
265
  const { record: recordFile, list: fileList } = createFileRecorder();
274
266
  const fileMatchCounts = new Map<string, number>();
275
267
  if (selectedMatches.length === 0) {
276
- const details: GrepToolDetails = {
268
+ const details: SearchToolDetails = {
277
269
  scopePath,
278
270
  matchCount: 0,
279
271
  fileCount: 0,
@@ -356,7 +348,7 @@ export class GrepTool implements AgentTool<typeof grepSchema, GrepToolDetails> {
356
348
  const truncation = truncateHead(rawOutput, { maxLines: Number.MAX_SAFE_INTEGER });
357
349
  const output = truncation.content;
358
350
  const truncated = Boolean(matchLimitReached || result.limitReached || truncation.truncated || linesTruncated);
359
- const details: GrepToolDetails = {
351
+ const details: SearchToolDetails = {
360
352
  scopePath,
361
353
  matchCount: selectedMatches.length,
362
354
  fileCount: fileList.length,
@@ -387,7 +379,7 @@ export class GrepTool implements AgentTool<typeof grepSchema, GrepToolDetails> {
387
379
  // TUI Renderer
388
380
  // =============================================================================
389
381
 
390
- interface GrepRenderArgs {
382
+ interface SearchRenderArgs {
391
383
  pattern: string;
392
384
  path?: string;
393
385
  i?: boolean;
@@ -397,9 +389,9 @@ interface GrepRenderArgs {
397
389
 
398
390
  const COLLAPSED_TEXT_LIMIT = PREVIEW_LIMITS.COLLAPSED_LINES * 2;
399
391
 
400
- export const grepToolRenderer = {
392
+ export const searchToolRenderer = {
401
393
  inline: true,
402
- renderCall(args: GrepRenderArgs, _options: RenderResultOptions, uiTheme: Theme): Component {
394
+ renderCall(args: SearchRenderArgs, _options: RenderResultOptions, uiTheme: Theme): Component {
403
395
  const meta: string[] = [];
404
396
  if (args.path) meta.push(`in ${args.path}`);
405
397
  if (args.i) meta.push("case:insensitive");
@@ -407,17 +399,17 @@ export const grepToolRenderer = {
407
399
  if (args.skip !== undefined && args.skip > 0) meta.push(`skip:${args.skip}`);
408
400
 
409
401
  const text = renderStatusLine(
410
- { icon: "pending", title: "Grep", description: args.pattern || "?", meta },
402
+ { icon: "pending", title: "Search", description: args.pattern || "?", meta },
411
403
  uiTheme,
412
404
  );
413
405
  return new Text(text, 0, 0);
414
406
  },
415
407
 
416
408
  renderResult(
417
- result: { content: Array<{ type: string; text?: string }>; details?: GrepToolDetails; isError?: boolean },
409
+ result: { content: Array<{ type: string; text?: string }>; details?: SearchToolDetails; isError?: boolean },
418
410
  options: RenderResultOptions,
419
411
  uiTheme: Theme,
420
- args?: GrepRenderArgs,
412
+ args?: SearchRenderArgs,
421
413
  ): Component {
422
414
  const details = result.details;
423
415
 
@@ -436,7 +428,7 @@ export const grepToolRenderer = {
436
428
  const lines = textContent.split("\n").filter(line => line.trim() !== "");
437
429
  const description = args?.pattern ?? undefined;
438
430
  const header = renderStatusLine(
439
- { icon: "success", title: "Grep", description, meta: [formatCount("item", lines.length)] },
431
+ { icon: "success", title: "Search", description, meta: [formatCount("item", lines.length)] },
440
432
  uiTheme,
441
433
  );
442
434
  let cached: RenderCache | undefined;
@@ -476,7 +468,7 @@ export const grepToolRenderer = {
476
468
 
477
469
  if (matchCount === 0) {
478
470
  const header = renderStatusLine(
479
- { icon: "warning", title: "Grep", description: args?.pattern, meta: ["0 matches"] },
471
+ { icon: "warning", title: "Search", description: args?.pattern, meta: ["0 matches"] },
480
472
  uiTheme,
481
473
  );
482
474
  return new Text([header, formatEmptyMessage("No matches found", uiTheme)].join("\n"), 0, 0);
@@ -488,7 +480,7 @@ export const grepToolRenderer = {
488
480
  if (truncated) meta.push(uiTheme.fg("warning", "truncated"));
489
481
  const description = args?.pattern ?? undefined;
490
482
  const header = renderStatusLine(
491
- { icon: truncated ? "warning" : "success", title: "Grep", description, meta },
483
+ { icon: truncated ? "warning" : "success", title: "Search", description, meta },
492
484
  uiTheme,
493
485
  );
494
486
 
@@ -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;