@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
@@ -156,7 +156,6 @@ const locSchema = Type.Union(
156
156
 
157
157
  export const hashlineEditSchema = Type.Object(
158
158
  {
159
- path: Type.Optional(Type.String({ description: "File path (omit to use top-level `path`)" })),
160
159
  loc: Type.Optional(locSchema),
161
160
  content: Type.Optional(linesSchema),
162
161
  },
@@ -165,7 +164,7 @@ export const hashlineEditSchema = Type.Object(
165
164
 
166
165
  export const hashlineEditParamsSchema = Type.Object(
167
166
  {
168
- path: Type.Optional(Type.String({ description: "Default file path used when an edit omits its own `path`" })),
167
+ path: Type.String({ description: "file path for edits" }),
169
168
  edits: Type.Array(hashlineEditSchema, { description: "edits" }),
170
169
  },
171
170
  { additionalProperties: false },
@@ -566,8 +565,8 @@ export class HashlineMismatchError extends Error {
566
565
 
567
566
  const sorted = [...displayLines].sort((a, b) => a - b);
568
567
  const out: string[] = [
569
- `Edit rejected: ${mismatches.length} line${mismatches.length > 1 ? "s have" : " has"} changed since the last read. The edit was NOT applied.`,
570
- "Realign your edit to the file state shown below. Copy the full anchors exactly as shown (for example `160sr`, not just `sr`).",
568
+ `Edit rejected: ${mismatches.length} line${mismatches.length > 1 ? "s have" : " has"} changed since the last read (marked *).`,
569
+ "The edit was NOT applied, please use the updated file content shown below, and issue another edit tool-call.",
571
570
  "",
572
571
  ];
573
572
 
@@ -603,8 +602,8 @@ export class HashlineMismatchError extends Error {
603
602
  const lines: string[] = [];
604
603
 
605
604
  lines.push(
606
- `Edit rejected: ${mismatches.length} line${mismatches.length > 1 ? "s have" : " has"} changed since the last read. The edit was NOT applied.`,
607
- "Use the updated anchors shown below (`*` marks changed lines, leading space marks context) and retry the edit.",
605
+ `Edit rejected: ${mismatches.length} line${mismatches.length > 1 ? "s have" : " has"} changed since the last read (marked *).`,
606
+ "The edit was NOT applied, please use the updated file content shown below, and issue another edit tool-call.",
608
607
  );
609
608
  lines.push("");
610
609
 
@@ -1015,7 +1014,7 @@ export interface CompactHashlineDiffOptions {
1015
1014
  }
1016
1015
 
1017
1016
  const NUMBERED_DIFF_LINE_RE = /^([ +-])(\s*\d+)\|(.*)$/;
1018
- const HASHLINE_PREVIEW_PLACEHOLDER = " ";
1017
+ const HASHLINE_PREVIEW_PLACEHOLDER = " ";
1019
1018
 
1020
1019
  type DiffRunKind = " " | "+" | "-" | "meta";
1021
1020
  type DiffRun = { kind: DiffRunKind; lines: string[] };
@@ -1142,7 +1141,7 @@ function collapseFromStart(lines: string[], maxLines: number, label: string): st
1142
1141
  return [...lines.slice(0, maxLines), ` ... ${hidden} more ${label} lines`];
1143
1142
  }
1144
1143
 
1145
- function collapseFromEnd(lines: string[], maxLines: number, label: string): string[] {
1144
+ function _collapseFromEnd(lines: string[], maxLines: number, label: string): string[] {
1146
1145
  if (lines.length <= maxLines) return lines;
1147
1146
  const hidden = lines.length - maxLines;
1148
1147
  return [` ... ${hidden} more ${label} lines`, ...lines.slice(-maxLines)];
@@ -1191,11 +1190,11 @@ export function buildCompactHashlineDiffPreview(
1191
1190
  break;
1192
1191
  case " ":
1193
1192
  if (runIndex === 0) {
1194
- out.push(...collapseFromEnd(run.lines, maxUnchangedRun, "unchanged"));
1193
+ out.push(...run.lines.slice(-maxUnchangedRun));
1195
1194
  break;
1196
1195
  }
1197
1196
  if (runIndex === runs.length - 1) {
1198
- out.push(...collapseFromStart(run.lines, maxUnchangedRun, "unchanged"));
1197
+ out.push(...run.lines.slice(0, maxUnchangedRun));
1199
1198
  break;
1200
1199
  }
1201
1200
  out.push(...collapseFromMiddle(run.lines, maxUnchangedRun, "unchanged"));
@@ -1576,27 +1576,33 @@ export async function computePatchDiff(
1576
1576
  }
1577
1577
  }
1578
1578
 
1579
- export const patchEditEntrySchema = Type.Object({
1580
- path: Type.Optional(Type.String({ description: "File path (omit to use top-level `path`)" })),
1581
- op: Type.Optional(
1582
- StringEnum(["create", "delete", "update"], {
1583
- description: "Operation (default: update)",
1584
- }),
1585
- ),
1586
- rename: Type.Optional(Type.String({ description: "New path for move" })),
1587
- diff: Type.Optional(Type.String({ description: "Diff hunks (update) or full content (create)" })),
1588
- });
1589
-
1590
- export const patchEditSchema = Type.Object({
1591
- path: Type.Optional(Type.String({ description: "Default file path used when an edit omits its own `path`" })),
1592
- edits: Type.Array(patchEditEntrySchema, { description: "Patch operations", minItems: 1 }),
1593
- });
1579
+ export const patchEditEntrySchema = Type.Object(
1580
+ {
1581
+ op: Type.Optional(
1582
+ StringEnum(["create", "delete", "update"], {
1583
+ description: "Operation (default: update)",
1584
+ }),
1585
+ ),
1586
+ rename: Type.Optional(Type.String({ description: "New path for move" })),
1587
+ diff: Type.Optional(Type.String({ description: "Diff hunks (update) or full content (create)" })),
1588
+ },
1589
+ { additionalProperties: false },
1590
+ );
1591
+
1592
+ export const patchEditSchema = Type.Object(
1593
+ {
1594
+ path: Type.String({ description: "file path for edits" }),
1595
+ edits: Type.Array(patchEditEntrySchema, { description: "Patch operations", minItems: 1 }),
1596
+ },
1597
+ { additionalProperties: false },
1598
+ );
1594
1599
 
1595
1600
  export type PatchEditEntry = Static<typeof patchEditEntrySchema>;
1596
1601
  export type PatchParams = Static<typeof patchEditSchema>;
1597
1602
 
1598
1603
  export interface ExecutePatchSingleOptions {
1599
1604
  session: ToolSession;
1605
+ path: string;
1600
1606
  params: PatchEditEntry;
1601
1607
  signal?: AbortSignal;
1602
1608
  batchRequest?: LspBatchRequest;
@@ -1694,6 +1700,7 @@ export async function executePatchSingle(
1694
1700
  ): Promise<AgentToolResult<EditToolDetails, typeof patchEditEntrySchema>> {
1695
1701
  const {
1696
1702
  session,
1703
+ path,
1697
1704
  params,
1698
1705
  signal,
1699
1706
  batchRequest,
@@ -1702,10 +1709,7 @@ export async function executePatchSingle(
1702
1709
  writethrough,
1703
1710
  beginDeferredDiagnosticsForPath,
1704
1711
  } = options;
1705
- const { path, op: rawOp, rename, diff } = params;
1706
- if (typeof path !== "string" || path.length === 0) {
1707
- throw new Error("patch edit: missing `path`. Provide `path` on the edit or supply a top-level `path`.");
1708
- }
1712
+ const { op: rawOp, rename, diff } = params;
1709
1713
 
1710
1714
  const op: Operation = rawOp === "create" || rawOp === "delete" ? rawOp : "update";
1711
1715
 
@@ -976,23 +976,29 @@ export function findContextLine(
976
976
  return { index: undefined, confidence: bestScore };
977
977
  }
978
978
 
979
- export const replaceEditEntrySchema = Type.Object({
980
- path: Type.Optional(Type.String({ description: "File path (omit to use top-level `path`)" })),
981
- old_text: Type.String({ description: "Text to find (fuzzy whitespace matching enabled)" }),
982
- new_text: Type.String({ description: "Replacement text" }),
983
- all: Type.Optional(Type.Boolean({ description: "Replace all occurrences (default: unique match required)" })),
984
- });
985
-
986
- export const replaceEditSchema = Type.Object({
987
- path: Type.Optional(Type.String({ description: "Default file path used when an edit omits its own `path`" })),
988
- edits: Type.Array(replaceEditEntrySchema, { description: "Replacements", minItems: 1 }),
989
- });
979
+ export const replaceEditEntrySchema = Type.Object(
980
+ {
981
+ old_text: Type.String({ description: "Text to find (fuzzy whitespace matching enabled)" }),
982
+ new_text: Type.String({ description: "Replacement text" }),
983
+ all: Type.Optional(Type.Boolean({ description: "Replace all occurrences (default: unique match required)" })),
984
+ },
985
+ { additionalProperties: false },
986
+ );
987
+
988
+ export const replaceEditSchema = Type.Object(
989
+ {
990
+ path: Type.String({ description: "file path for edits" }),
991
+ edits: Type.Array(replaceEditEntrySchema, { description: "Replacements", minItems: 1 }),
992
+ },
993
+ { additionalProperties: false },
994
+ );
990
995
 
991
996
  export type ReplaceEditEntry = Static<typeof replaceEditEntrySchema>;
992
997
  export type ReplaceParams = Static<typeof replaceEditSchema>;
993
998
 
994
999
  export interface ExecuteReplaceSingleOptions {
995
1000
  session: ToolSession;
1001
+ path: string;
996
1002
  params: ReplaceEditEntry;
997
1003
  signal?: AbortSignal;
998
1004
  batchRequest?: LspBatchRequest;
@@ -1007,6 +1013,7 @@ export async function executeReplaceSingle(
1007
1013
  ): Promise<AgentToolResult<EditToolDetails, typeof replaceEditEntrySchema>> {
1008
1014
  const {
1009
1015
  session,
1016
+ path,
1010
1017
  params,
1011
1018
  signal,
1012
1019
  batchRequest,
@@ -1015,10 +1022,7 @@ export async function executeReplaceSingle(
1015
1022
  writethrough,
1016
1023
  beginDeferredDiagnosticsForPath,
1017
1024
  } = options;
1018
- const { path, old_text, new_text, all } = params;
1019
- if (typeof path !== "string" || path.length === 0) {
1020
- throw new Error("replace edit: missing `path`. Provide `path` on the edit or supply a top-level `path`.");
1021
- }
1025
+ const { old_text, new_text, all } = params;
1022
1026
 
1023
1027
  enforcePlanModeWrite(session, path);
1024
1028
 
@@ -28,8 +28,8 @@ import { Hasher, type RenderCache, renderStatusLine, truncateToWidth } from "../
28
28
  import type { EditMode } from "../utils/edit-mode";
29
29
  import type { VimToolDetails } from "../vim/types";
30
30
  import type { DiffError, DiffResult } from "./diff";
31
- import { expandApplyPatchToEntries, expandApplyPatchToPreviewEntries } from "./modes/apply-patch";
32
- import type { Operation, PatchEditEntry } from "./modes/patch";
31
+ import { type ApplyPatchEntry, expandApplyPatchToEntries, expandApplyPatchToPreviewEntries } from "./modes/apply-patch";
32
+ import type { Operation } from "./modes/patch";
33
33
  import type { PerFileDiffPreview } from "./streaming";
34
34
 
35
35
  // ═══════════════════════════════════════════════════════════════════════════
@@ -106,8 +106,12 @@ type EditRenderEntry = {
106
106
  op?: Operation;
107
107
  };
108
108
 
109
+ interface AtomRenderSummary {
110
+ entries: Array<{ path: string }>;
111
+ }
112
+
109
113
  interface ApplyPatchRenderSummary {
110
- entries: PatchEditEntry[];
114
+ entries: ApplyPatchEntry[];
111
115
  error?: string;
112
116
  }
113
117
 
@@ -305,8 +309,54 @@ function getCallPreview(
305
309
  }
306
310
 
307
311
  const MISSING_APPLY_PATCH_END_ERROR = "The last line of the patch must be '*** End Patch'";
312
+ const ATOM_HEADER_PREFIX = "---";
313
+
314
+ function normalizeAtomPreviewPath(rawPath: string): string {
315
+ const trimmed = rawPath.trim();
316
+ if (trimmed.length < 2) return trimmed;
317
+ const first = trimmed[0];
318
+ const last = trimmed[trimmed.length - 1];
319
+ if ((first === '"' || first === "'") && first === last) {
320
+ return trimmed.slice(1, -1);
321
+ }
322
+ return trimmed;
323
+ }
324
+
325
+ function parseAtomPreviewHeader(line: string): string | null {
326
+ if (!line.startsWith(ATOM_HEADER_PREFIX)) return null;
327
+ let body = line.slice(ATOM_HEADER_PREFIX.length);
328
+ if (body.startsWith(" ")) body = body.slice(1);
329
+ const previewPath = normalizeAtomPreviewPath(body);
330
+ return previewPath.length > 0 ? previewPath : null;
331
+ }
332
+
333
+ function getAtomInputPaths(input: string): string[] {
334
+ const stripped = input.startsWith("\uFEFF") ? input.slice(1) : input;
335
+ const paths: string[] = [];
336
+ for (const rawLine of stripped.split("\n")) {
337
+ const line = rawLine.replace(/\r$/, "");
338
+ const path = parseAtomPreviewHeader(line);
339
+ if (path) paths.push(path);
340
+ }
341
+ return paths;
342
+ }
343
+
344
+ function getAtomRenderSummary(args: EditRenderArgs, editMode: EditMode | undefined): AtomRenderSummary | undefined {
345
+ if (editMode !== "atom" || typeof args.input !== "string") {
346
+ return undefined;
347
+ }
348
+ return { entries: getAtomInputPaths(args.input).map(path => ({ path })) };
349
+ }
350
+
351
+ function getApplyPatchRenderSummary(
352
+ args: EditRenderArgs,
353
+ isPartial: boolean,
354
+ editMode: EditMode | undefined,
355
+ ): ApplyPatchRenderSummary | undefined {
356
+ if (editMode !== undefined && editMode !== "apply_patch") {
357
+ return undefined;
358
+ }
308
359
 
309
- function getApplyPatchRenderSummary(args: EditRenderArgs, isPartial: boolean): ApplyPatchRenderSummary | undefined {
310
360
  if (typeof args.input !== "string") {
311
361
  return undefined;
312
362
  }
@@ -397,8 +447,10 @@ export const editToolRenderer = {
397
447
  }
398
448
 
399
449
  const editArgs = args as EditRenderArgs;
400
- const applyPatchSummary = getApplyPatchRenderSummary(editArgs, options.isPartial);
450
+ const atomSummary = getAtomRenderSummary(editArgs, renderContext?.editMode);
451
+ const applyPatchSummary = getApplyPatchRenderSummary(editArgs, options.isPartial, renderContext?.editMode);
401
452
  const firstApplyPatchEntry = applyPatchSummary?.entries[0];
453
+ const firstAtomEntry = atomSummary?.entries[0];
402
454
  // Extract path from first edit entry when top-level path is absent (new schema)
403
455
  const firstEdit = Array.isArray(editArgs.edits) && editArgs.edits.length > 0 ? editArgs.edits[0] : undefined;
404
456
  const rawPath =
@@ -406,6 +458,7 @@ export const editToolRenderer = {
406
458
  editArgs.path ||
407
459
  filePathFromEditEntry(firstEdit?.path) ||
408
460
  getPartialJsonEditPath(editArgs) ||
461
+ firstAtomEntry?.path ||
409
462
  firstApplyPatchEntry?.path ||
410
463
  "";
411
464
  const rename = editArgs.rename || firstEdit?.rename || firstEdit?.move || firstApplyPatchEntry?.rename;
@@ -415,9 +468,10 @@ export const editToolRenderer = {
415
468
  options?.spinnerFrame !== undefined ? formatStatusIcon("running", uiTheme, options.spinnerFrame) : "";
416
469
  let text = `${formatTitle(getOperationTitle(op), uiTheme)} ${spinner ? `${spinner} ` : ""}${description}`;
417
470
  // Show file count hint for multi-file edits
418
- const fileCount = Array.isArray(editArgs.edits)
419
- ? countEditFiles(editArgs.edits)
420
- : (applyPatchSummary?.entries.length ?? 0);
471
+ let fileCount = atomSummary?.entries.length ?? applyPatchSummary?.entries.length ?? 0;
472
+ if (Array.isArray(editArgs.edits)) {
473
+ fileCount = countEditFiles(editArgs.edits);
474
+ }
421
475
  if (fileCount > 1) {
422
476
  text += uiTheme.fg("dim", ` (+${fileCount - 1} more)`);
423
477
  }
@@ -465,11 +519,14 @@ function renderSingleFileResult(
465
519
  const details = result.details;
466
520
  const isError = result.isError ?? (details && "isError" in details ? details.isError : false);
467
521
  const firstEdit = args?.edits?.[0];
522
+ const atomSummary = getAtomRenderSummary(args ?? {}, options.renderContext?.editMode);
523
+ const firstAtomEntry = atomSummary?.entries[0];
468
524
  const rawPath =
469
525
  args?.file_path ||
470
526
  args?.path ||
471
527
  filePathFromEditEntry(firstEdit?.path) ||
472
528
  (details && "path" in details ? details.path : "") ||
529
+ firstAtomEntry?.path ||
473
530
  "";
474
531
  const op = args?.op || firstEdit?.op || details?.op;
475
532
  const rename = args?.rename || firstEdit?.rename || firstEdit?.move || details?.move;
@@ -15,7 +15,7 @@
15
15
  import type { Theme } from "../modes/theme/theme";
16
16
  import { type EditMode, resolveEditMode } from "../utils/edit-mode";
17
17
  import { computeEditDiff, type DiffError, type DiffResult } from "./diff";
18
- import { expandApplyPatchToEntries, expandApplyPatchToPreviewEntries } from "./modes/apply-patch";
18
+ import { type ApplyPatchEntry, expandApplyPatchToEntries, expandApplyPatchToPreviewEntries } from "./modes/apply-patch";
19
19
  import { computeHashlineDiff, type HashlineToolEdit } from "./modes/hashline";
20
20
  import { computePatchDiff, type PatchEditEntry } from "./modes/patch";
21
21
  import type { ReplaceEditEntry } from "./modes/replace";
@@ -126,33 +126,19 @@ export function dropIncompleteLastEdit<T>(edits: readonly T[], partialJson: stri
126
126
  }
127
127
 
128
128
  // -----------------------------------------------------------------------------
129
- // Multi-file grouping
129
+ // Apply_patch remains multi-file because the Codex envelope carries paths per hunk.
130
130
  // -----------------------------------------------------------------------------
131
131
 
132
- /** Cap on how many distinct files a streaming preview will render diffs for. */
133
- const MAX_PREVIEW_FILES = 5;
132
+ function groupApplyPatchEntriesByPath(entries: readonly ApplyPatchEntry[]): Map<string, ApplyPatchEntry[]> {
133
+ const groups = new Map<string, ApplyPatchEntry[]>();
134
134
 
135
- /**
136
- * Group a list of edits by their effective `path` (per-edit `path` falls back
137
- * to the top-level `args.path`). Insertion order is preserved and the number of
138
- * distinct buckets is capped at {@link MAX_PREVIEW_FILES}.
139
- */
140
- function groupEditsByPath<T extends { path?: string }>(
141
- edits: readonly T[],
142
- fallbackPath: string | undefined,
143
- ): Map<string, Array<T & { path: string }>> {
144
- const groups = new Map<string, Array<T & { path: string }>>();
145
- for (const edit of edits) {
146
- if (!edit) continue;
147
- const editPath = edit.path ?? fallbackPath;
148
- if (!editPath) continue;
149
- let bucket = groups.get(editPath);
135
+ for (const entry of entries) {
136
+ let bucket = groups.get(entry.path);
150
137
  if (!bucket) {
151
- if (groups.size >= MAX_PREVIEW_FILES) continue;
152
138
  bucket = [];
153
- groups.set(editPath, bucket);
139
+ groups.set(entry.path, bucket);
154
140
  }
155
- bucket.push({ ...edit, path: editPath });
141
+ bucket.push(entry);
156
142
  }
157
143
  return groups;
158
144
  }
@@ -173,26 +159,21 @@ const replaceStrategy: EditStreamingStrategy<ReplaceArgs> = {
173
159
  return { ...args, edits: dropIncompleteLastEdit(args.edits, partialJson, "edits") };
174
160
  },
175
161
  async computeDiffPreview(args, ctx) {
176
- const groups = groupEditsByPath(args.edits ?? [], args.path);
177
- if (groups.size === 0) return null;
178
- const previews: PerFileDiffPreview[] = [];
179
- for (const [path, fileEdits] of groups) {
180
- const first = fileEdits[0];
181
- if (!first || first.old_text === undefined || first.new_text === undefined) continue;
182
- ctx.signal.throwIfAborted();
183
- const result = await computeEditDiff(
184
- path,
185
- first.old_text,
186
- first.new_text,
187
- ctx.cwd,
188
- ctx.allowFuzzy ?? true,
189
- first.all,
190
- ctx.fuzzyThreshold,
191
- );
192
- ctx.signal.throwIfAborted();
193
- previews.push(toPerFilePreview(path, result));
194
- }
195
- return previews.length > 0 ? previews : null;
162
+ if (!args.path) return null;
163
+ const first = args.edits?.[0];
164
+ if (!first || first.old_text === undefined || first.new_text === undefined) return null;
165
+ ctx.signal.throwIfAborted();
166
+ const result = await computeEditDiff(
167
+ args.path,
168
+ first.old_text,
169
+ first.new_text,
170
+ ctx.cwd,
171
+ ctx.allowFuzzy ?? true,
172
+ first.all,
173
+ ctx.fuzzyThreshold,
174
+ );
175
+ ctx.signal.throwIfAborted();
176
+ return [toPerFilePreview(args.path, result)];
196
177
  },
197
178
  renderStreamingFallback() {
198
179
  return "";
@@ -211,22 +192,17 @@ const patchStrategy: EditStreamingStrategy<PatchArgs> = {
211
192
  return { ...args, edits: dropIncompleteLastEdit(args.edits, partialJson, "edits") };
212
193
  },
213
194
  async computeDiffPreview(args, ctx) {
214
- const groups = groupEditsByPath(args.edits ?? [], args.path);
215
- if (groups.size === 0) return null;
216
- const previews: PerFileDiffPreview[] = [];
217
- for (const [path, fileEdits] of groups) {
218
- const first = fileEdits[0];
219
- if (!first) continue;
220
- ctx.signal.throwIfAborted();
221
- const result = await computePatchDiff(
222
- { path, op: first.op ?? "update", rename: first.rename, diff: first.diff },
223
- ctx.cwd,
224
- { fuzzyThreshold: ctx.fuzzyThreshold, allowFuzzy: ctx.allowFuzzy },
225
- );
226
- ctx.signal.throwIfAborted();
227
- previews.push(toPerFilePreview(path, result));
228
- }
229
- return previews.length > 0 ? previews : null;
195
+ if (!args.path) return null;
196
+ const first = args.edits?.[0];
197
+ if (!first) return null;
198
+ ctx.signal.throwIfAborted();
199
+ const result = await computePatchDiff(
200
+ { path: args.path, op: first.op ?? "update", rename: first.rename, diff: first.diff },
201
+ ctx.cwd,
202
+ { fuzzyThreshold: ctx.fuzzyThreshold, allowFuzzy: ctx.allowFuzzy },
203
+ );
204
+ ctx.signal.throwIfAborted();
205
+ return [toPerFilePreview(args.path, result)];
230
206
  },
231
207
  renderStreamingFallback() {
232
208
  return "";
@@ -245,16 +221,11 @@ const hashlineStrategy: EditStreamingStrategy<HashlineArgs> = {
245
221
  return { ...args, edits: dropIncompleteLastEdit(args.edits, partialJson, "edits") };
246
222
  },
247
223
  async computeDiffPreview(args, ctx) {
248
- const groups = groupEditsByPath(args.edits ?? [], args.path);
249
- if (groups.size === 0) return null;
250
- const previews: PerFileDiffPreview[] = [];
251
- for (const [path, fileEdits] of groups) {
252
- ctx.signal.throwIfAborted();
253
- const result = await computeHashlineDiff({ path, edits: fileEdits }, ctx.cwd);
254
- ctx.signal.throwIfAborted();
255
- previews.push(toPerFilePreview(path, result));
256
- }
257
- return previews;
224
+ if (!args.path || !args.edits?.length) return null;
225
+ ctx.signal.throwIfAborted();
226
+ const result = await computeHashlineDiff({ path: args.path, edits: args.edits }, ctx.cwd);
227
+ ctx.signal.throwIfAborted();
228
+ return [toPerFilePreview(args.path, result)];
258
229
  },
259
230
  renderStreamingFallback() {
260
231
  return "";
@@ -272,7 +243,7 @@ const applyPatchStrategy: EditStreamingStrategy<ApplyPatchArgs> = {
272
243
  },
273
244
  async computeDiffPreview(args, ctx) {
274
245
  if (typeof args.input !== "string" || args.input.length === 0) return null;
275
- let entries: PatchEditEntry[];
246
+ let entries: ApplyPatchEntry[];
276
247
  try {
277
248
  entries = expandApplyPatchToEntries({ input: args.input });
278
249
  } catch {
@@ -282,7 +253,7 @@ const applyPatchStrategy: EditStreamingStrategy<ApplyPatchArgs> = {
282
253
  return [{ path: "", error: err instanceof Error ? err.message : String(err) }];
283
254
  }
284
255
  }
285
- const groups = groupEditsByPath(entries, undefined);
256
+ const groups = groupApplyPatchEntriesByPath(entries);
286
257
  if (groups.size === 0) return null;
287
258
  const previews: PerFileDiffPreview[] = [];
288
259
  for (const [path, fileEntries] of groups) {
@@ -319,18 +290,17 @@ const vimStrategy: EditStreamingStrategy<unknown> = {
319
290
  };
320
291
 
321
292
  interface AtomArgs {
322
- path?: string;
323
- edits?: unknown[];
293
+ input?: string;
294
+ __partialJson?: string;
324
295
  }
325
296
 
326
297
  const atomStrategy: EditStreamingStrategy<AtomArgs> = {
327
- extractCompleteEdits(args, partialJson) {
328
- if (!args.edits) return args;
329
- return { ...args, edits: dropIncompleteLastEdit(args.edits, partialJson, "edits") };
298
+ extractCompleteEdits(args) {
299
+ return args;
330
300
  },
331
301
  async computeDiffPreview() {
332
- // Atom edits are line-anchored and validated against live file hashes; a
333
- // streaming preview without that validation could mislead. Skip for now.
302
+ // Atom edits can target file headers plus compact diff statements.
303
+ // We intentionally avoid speculative parsing while args are partial.
334
304
  return null;
335
305
  },
336
306
  renderStreamingFallback() {
@@ -46,10 +46,10 @@ import type {
46
46
  BashToolInput,
47
47
  FindToolDetails,
48
48
  FindToolInput,
49
- GrepToolDetails,
50
- GrepToolInput,
51
49
  ReadToolDetails,
52
50
  ReadToolInput,
51
+ SearchToolDetails,
52
+ SearchToolInput,
53
53
  WriteToolInput,
54
54
  } from "../../tools";
55
55
  import type { TodoItem } from "../../tools/todo-write";
@@ -683,9 +683,9 @@ export interface WriteToolCallEvent extends ToolCallEventBase {
683
683
  input: WriteToolInput;
684
684
  }
685
685
 
686
- export interface GrepToolCallEvent extends ToolCallEventBase {
687
- toolName: "grep";
688
- input: GrepToolInput;
686
+ export interface SearchToolCallEvent extends ToolCallEventBase {
687
+ toolName: "search";
688
+ input: SearchToolInput;
689
689
  }
690
690
 
691
691
  export interface FindToolCallEvent extends ToolCallEventBase {
@@ -704,7 +704,7 @@ export type ToolCallEvent =
704
704
  | ReadToolCallEvent
705
705
  | EditToolCallEvent
706
706
  | WriteToolCallEvent
707
- | GrepToolCallEvent
707
+ | SearchToolCallEvent
708
708
  | FindToolCallEvent
709
709
  | CustomToolCallEvent;
710
710
 
@@ -736,9 +736,9 @@ export interface WriteToolResultEvent extends ToolResultEventBase {
736
736
  details: undefined;
737
737
  }
738
738
 
739
- export interface GrepToolResultEvent extends ToolResultEventBase {
740
- toolName: "grep";
741
- details: GrepToolDetails | undefined;
739
+ export interface SearchToolResultEvent extends ToolResultEventBase {
740
+ toolName: "search";
741
+ details: SearchToolDetails | undefined;
742
742
  }
743
743
 
744
744
  export interface FindToolResultEvent extends ToolResultEventBase {
@@ -757,7 +757,7 @@ export type ToolResultEvent =
757
757
  | ReadToolResultEvent
758
758
  | EditToolResultEvent
759
759
  | WriteToolResultEvent
760
- | GrepToolResultEvent
760
+ | SearchToolResultEvent
761
761
  | FindToolResultEvent
762
762
  | CustomToolResultEvent;
763
763
 
@@ -785,7 +785,7 @@ export function isToolCallEventType(toolName: "bash", event: ToolCallEvent): eve
785
785
  export function isToolCallEventType(toolName: "read", event: ToolCallEvent): event is ReadToolCallEvent;
786
786
  export function isToolCallEventType(toolName: "edit", event: ToolCallEvent): event is EditToolCallEvent;
787
787
  export function isToolCallEventType(toolName: "write", event: ToolCallEvent): event is WriteToolCallEvent;
788
- export function isToolCallEventType(toolName: "grep", event: ToolCallEvent): event is GrepToolCallEvent;
788
+ export function isToolCallEventType(toolName: "search", event: ToolCallEvent): event is SearchToolCallEvent;
789
789
  export function isToolCallEventType(toolName: "find", event: ToolCallEvent): event is FindToolCallEvent;
790
790
  export function isToolCallEventType<TName extends string, TInput extends Record<string, unknown>>(
791
791
  toolName: TName,
@@ -21,7 +21,7 @@ import type {
21
21
  SessionEntry,
22
22
  SessionManager,
23
23
  } from "../../session/session-manager";
24
- import type { BashToolDetails, FindToolDetails, GrepToolDetails, ReadToolDetails } from "../../tools";
24
+ import type { BashToolDetails, FindToolDetails, ReadToolDetails, SearchToolDetails } from "../../tools";
25
25
  import type { TodoItem } from "../../tools/todo-write";
26
26
 
27
27
  // Re-export for backward compatibility
@@ -494,10 +494,10 @@ export interface WriteToolResultEvent extends ToolResultEventBase {
494
494
  details: undefined;
495
495
  }
496
496
 
497
- /** Tool result event for grep tool */
498
- export interface GrepToolResultEvent extends ToolResultEventBase {
499
- toolName: "grep";
500
- details: GrepToolDetails | undefined;
497
+ /** Tool result event for search tool */
498
+ export interface SearchToolResultEvent extends ToolResultEventBase {
499
+ toolName: "search";
500
+ details: SearchToolDetails | undefined;
501
501
  }
502
502
 
503
503
  /** Tool result event for find tool */
@@ -522,7 +522,7 @@ export type ToolResultEvent =
522
522
  | ReadToolResultEvent
523
523
  | EditToolResultEvent
524
524
  | WriteToolResultEvent
525
- | GrepToolResultEvent
525
+ | SearchToolResultEvent
526
526
  | FindToolResultEvent
527
527
  | CustomToolResultEvent;
528
528