@oh-my-pi/pi-coding-agent 14.5.3 → 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 (68) hide show
  1. package/CHANGELOG.md +44 -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 +1057 -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 +34 -31
  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
package/CHANGELOG.md CHANGED
@@ -2,6 +2,50 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [14.5.5] - 2026-04-29
6
+ ### Breaking Changes
7
+
8
+ - Rejected atom diffs with unrecognized operations (including lone '-' lines) by throwing parse errors instead of treating them as inserts
9
+
10
+ ### Added
11
+
12
+ - Added duplicate-line post-edit detection that warns on newly introduced adjacent identical lines and auto-removes one duplicate when bracket-balance is restored
13
+ - Added a warning when suspicious adjacent duplicates are introduced after edits so users can review potential stale-line issues
14
+
15
+ ### Changed
16
+
17
+ - Changed anchor rebase handling to fail when multiple mutating anchors would need auto-rebase, preventing silent misapplied contiguous block rewrites
18
+
19
+ ### Fixed
20
+
21
+ - Fixed bracket-corruption caused by botched block rewrites by automatically removing a newly introduced duplicate adjacent line when removing it restores the original `{}`, `()`, and `[]` balance and by warning when automatic removal is unsafe
22
+
23
+ ## [14.5.4] - 2026-04-28
24
+ ### Breaking Changes
25
+
26
+ - Changed the `atom` edit mode from JSON `{ path, edits }` calls to the compact file-oriented `input` patch language that was previously exposed as `atomd`; `atomd` is no longer a separate edit variant
27
+ - Renamed MCP tool identifiers from the `mcp_<server>_<tool>` format to `mcp__<server>_<tool>` so custom tool names, active tool lists, and persisted MCP selections must be updated to the new prefix
28
+ - Renamed the built-in content-search tool from `grep` to `search`, including SDK/tool event names and settings keys (`search.enabled`, `search.contextBefore`, `search.contextAfter`), so integrations using `grep` and `grep.*` references must be updated
29
+
30
+ ### Added
31
+
32
+ - Added internal URL support to the `search` tool, allowing `artifact://`-style paths that resolve to local files to be searched directly
33
+ - Added IRC relay observation in the main agent UI so every IRC exchange between agents is rendered in the main transcript, even when the main agent is not a direct participant
34
+ - Added stateful `href`/`hrefr` prompt helpers that can reuse anchors remembered from prior `hline` helper calls
35
+
36
+ ### Changed
37
+
38
+ - Changed file-path rendering across search, find, AST, LSP, and related edit outputs to display targets as cwd-relative paths when they resolve inside the working directory and keep absolute paths for files outside the cwd
39
+ - Changed system prompt guidance so in-cwd tool paths must be passed as cwd-relative paths and absolute paths only for out-of-cwd targets or `~` expansion
40
+ - Updated `edit` streaming diff previews for `patch`, `replace`, and `hashline` to produce a single request-level preview for the new single-file `path` mode
41
+ - Bumped default `read.defaultLimit` from 300 to 500 lines, and scaled the read tool's byte budget with the line limit (`max(50KB, lines * 512)`) so the configured line count is no longer truncated by the shared 50KB cap
42
+
43
+ ### Fixed
44
+
45
+ - Fixed atom edit streaming previews to use atom headers for file names instead of apply_patch parsing errors.
46
+ - Fixed collapsed search result rendering so summary and truncation rows stay within the collapsed output budget
47
+ - Updated search path handling to support path lists and internal file paths while preserving previous search behavior
48
+
5
49
  ## [14.5.3] - 2026-04-27
6
50
 
7
51
  ### Added
@@ -22,7 +22,7 @@ import type { ExtensionAPI, ExtensionContext } from "@oh-my-pi/pi-coding-agent";
22
22
  import { Key } from "@oh-my-pi/pi-tui";
23
23
 
24
24
  // Read-only tools for plan mode
25
- const PLAN_MODE_TOOLS = ["read", "bash", "grep", "find", "ls"];
25
+ const PLAN_MODE_TOOLS = ["read", "bash", "search", "find"];
26
26
 
27
27
  // Full set of tools for normal mode
28
28
  const NORMAL_MODE_TOOLS = ["read", "bash", "edit", "write"];
@@ -69,7 +69,7 @@ const { session } = await createAgentSession({
69
69
  });
70
70
 
71
71
  // Read-only tools
72
- const { session } = await createAgentSession({ toolNames: ["read", "grep", "find", "ls"], authStorage, modelRegistry });
72
+ const { session } = await createAgentSession({ toolNames: ["read", "search", "find"], authStorage, modelRegistry });
73
73
 
74
74
  // In-memory
75
75
  const { session } = await createAgentSession({
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@oh-my-pi/pi-coding-agent",
4
- "version": "14.5.3",
4
+ "version": "14.5.5",
5
5
  "description": "Coding agent CLI with read, bash, edit, write tools and session management",
6
6
  "homepage": "https://github.com/can1357/oh-my-pi",
7
7
  "author": "Can Boluk",
@@ -46,12 +46,12 @@
46
46
  "dependencies": {
47
47
  "@agentclientprotocol/sdk": "0.20.0",
48
48
  "@mozilla/readability": "^0.6.0",
49
- "@oh-my-pi/omp-stats": "14.5.3",
50
- "@oh-my-pi/pi-agent-core": "14.5.3",
51
- "@oh-my-pi/pi-ai": "14.5.3",
52
- "@oh-my-pi/pi-natives": "14.5.3",
53
- "@oh-my-pi/pi-tui": "14.5.3",
54
- "@oh-my-pi/pi-utils": "14.5.3",
49
+ "@oh-my-pi/omp-stats": "14.5.5",
50
+ "@oh-my-pi/pi-agent-core": "14.5.5",
51
+ "@oh-my-pi/pi-ai": "14.5.5",
52
+ "@oh-my-pi/pi-natives": "14.5.5",
53
+ "@oh-my-pi/pi-tui": "14.5.5",
54
+ "@oh-my-pi/pi-utils": "14.5.5",
55
55
  "@puppeteer/browsers": "^2.13.0",
56
56
  "@sinclair/typebox": "^0.34.49",
57
57
  "@xterm/headless": "^6.0.0",
@@ -43,24 +43,119 @@ function formatHashlineRef(lineNum: unknown, content: unknown): { num: number; t
43
43
  return { num, text, ref };
44
44
  }
45
45
 
46
+ interface HashlineHelperRef {
47
+ line: number;
48
+ ref: string;
49
+ }
50
+
51
+ interface HashlineHelperState {
52
+ last?: HashlineHelperRef;
53
+ byLine: Map<number, HashlineHelperRef>;
54
+ }
55
+
56
+ const HASHLINE_HELPER_STATE = Symbol("hashlineHelperState");
57
+
58
+ interface HashlineHelperStateHolder {
59
+ [HASHLINE_HELPER_STATE]?: HashlineHelperState;
60
+ }
61
+
62
+ function isHelperOptions(value: unknown): value is prompt.HelperOptions {
63
+ return typeof value === "object" && value !== null && "hash" in value;
64
+ }
65
+
66
+ function splitHelperArgs(args: unknown[]): { positional: unknown[]; options?: prompt.HelperOptions } {
67
+ const maybeOptions = args.at(-1);
68
+ if (!isHelperOptions(maybeOptions)) return { positional: args };
69
+ return { positional: args.slice(0, -1), options: maybeOptions };
70
+ }
71
+
72
+ function getHashlineHelperState(context: unknown, options: prompt.HelperOptions | undefined): HashlineHelperState {
73
+ const data = options?.data;
74
+ const root = data?.root;
75
+ const holderTarget = data && typeof data === "object" ? data : root && typeof root === "object" ? root : context;
76
+ if (!holderTarget || typeof holderTarget !== "object") {
77
+ throw new Error("hashline prompt helpers require an object render context");
78
+ }
79
+
80
+ const holder = holderTarget as HashlineHelperStateHolder;
81
+ if (!holder[HASHLINE_HELPER_STATE]) {
82
+ holder[HASHLINE_HELPER_STATE] = { byLine: new Map() };
83
+ }
84
+ return holder[HASHLINE_HELPER_STATE];
85
+ }
86
+
87
+ function isLineNumberArg(value: unknown): boolean {
88
+ const num = typeof value === "number" ? value : Number.parseInt(String(value), 10);
89
+ return Number.isFinite(num);
90
+ }
91
+
92
+ function rememberHashlineRef(state: HashlineHelperState, line: number, ref: string): void {
93
+ const entry = { line, ref };
94
+ state.last = entry;
95
+ state.byLine.set(line, entry);
96
+ }
97
+
98
+ function requireStoredHashlineRef(state: HashlineHelperState, lineArg?: unknown): string {
99
+ if (lineArg === undefined) {
100
+ if (!state.last) {
101
+ throw new Error("{{href}} requires a previous {{hline}} call in the same prompt render");
102
+ }
103
+ return state.last.ref;
104
+ }
105
+
106
+ const line = typeof lineArg === "number" ? lineArg : Number.parseInt(String(lineArg), 10);
107
+ const entry = state.byLine.get(line);
108
+ if (!entry) {
109
+ throw new Error(`{{href ${line}}} requires a previous {{hline ${line} ...}} call in the same prompt render`);
110
+ }
111
+ return entry.ref;
112
+ }
113
+
114
+ function wrapHashlineRef(ref: string, args: unknown[]): string {
115
+ const preStr = typeof args[0] === "string" ? args[0] : "";
116
+ const postStr = typeof args[1] === "string" ? args[1] : "";
117
+ return `${preStr}${ref}${postStr}`;
118
+ }
119
+
120
+ function resolveHashlineRef(state: HashlineHelperState, args: unknown[]): string {
121
+ if (args.length === 0) return requireStoredHashlineRef(state);
122
+ const [first, second, ...rest] = args;
123
+ if (isLineNumberArg(first)) {
124
+ if (second === undefined) return requireStoredHashlineRef(state, first);
125
+ const { ref } = formatHashlineRef(first, second);
126
+ return wrapHashlineRef(ref, rest);
127
+ }
128
+ return wrapHashlineRef(requireStoredHashlineRef(state), args);
129
+ }
130
+
46
131
  /**
47
132
  * {{href lineNum "content"}} — compute a real hashline ref for prompt examples.
48
- * {{href lineNum "content" "[" "]"}} — wrap the ref with pre/post chars (still quoted).
133
+ * {{href lineNum}} — quote the ref remembered by the earlier {{hline lineNum "..."}}
134
+ * {{href}} — quote the ref from the previous {{hline}} call.
135
+ * {{href "[" "]"}} — wrap the previous {{hline}} ref with pre/post chars.
49
136
  * Returns `"lineNumBIGRAM"` (e.g., `"42nd"`), or `"[42nd]"` when pre/post are supplied.
50
137
  */
51
- prompt.registerHelper("href", (lineNum: unknown, content: unknown, pre?: unknown, post?: unknown): string => {
52
- const { ref } = formatHashlineRef(lineNum, content);
53
- const preStr = typeof pre === "string" ? pre : "";
54
- const postStr = typeof post === "string" ? post : "";
55
- return JSON.stringify(`${preStr}${ref}${postStr}`);
138
+ prompt.registerHelper("href", function (this: unknown, ...args: unknown[]): string {
139
+ const { positional, options } = splitHelperArgs(args);
140
+ const state = getHashlineHelperState(this, options);
141
+ return JSON.stringify(resolveHashlineRef(state, positional));
142
+ });
143
+ prompt.registerHelper("hrefr", function (this: unknown, ...args: unknown[]): string {
144
+ const { positional, options } = splitHelperArgs(args);
145
+ const state = getHashlineHelperState(this, options);
146
+ return resolveHashlineRef(state, positional);
56
147
  });
57
148
 
58
149
  /**
59
150
  * {{hline lineNum "content"}} — format a full read-style line with prefix.
60
151
  * Returns `"lineNumBIGRAM|content"` (pipe between anchor and content).
61
152
  */
62
- prompt.registerHelper("hline", (lineNum: unknown, content: unknown): string => {
63
- const { ref, text } = formatHashlineRef(lineNum, content);
153
+ prompt.registerHelper("hline", function (this: unknown, ...args: unknown[]): string {
154
+ const { positional, options } = splitHelperArgs(args);
155
+ const [lineNum, content] = positional;
156
+ const { num, ref, text } = formatHashlineRef(lineNum, content);
157
+ const state = getHashlineHelperState(this, options);
158
+ rememberHashlineRef(state, num, ref);
64
159
  return `${ref}${HASHLINE_CONTENT_SEPARATOR}${text}`;
65
160
  });
66
161
 
@@ -1,5 +1,6 @@
1
1
  import { THINKING_EFFORTS } from "@oh-my-pi/pi-ai";
2
2
  import { TASK_SIMPLE_MODES } from "../task/simple-mode";
3
+ import { EDIT_MODES } from "../utils/edit-mode";
3
4
 
4
5
  /** Unified settings schema - single source of truth for all settings.
5
6
  * Unified settings schema - single source of truth for all settings.
@@ -160,8 +161,8 @@ export const DEFAULT_BASH_INTERCEPTOR_RULES: BashInterceptorRule[] = [
160
161
  },
161
162
  {
162
163
  pattern: "^\\s*(grep|rg|ripgrep|ag|ack)\\s+",
163
- tool: "grep",
164
- message: "Use the `grep` tool instead of grep/rg. It respects .gitignore and provides structured output.",
164
+ tool: "search",
165
+ message: "Use the `search` tool instead of grep/rg. It respects .gitignore and provides structured output.",
165
166
  },
166
167
  {
167
168
  pattern: "^\\s*(find|fd|locate)\\s+.*(-name|-iname|-type|--type|-glob)",
@@ -955,12 +956,12 @@ export const SETTINGS_SCHEMA = {
955
956
  // Edit tool
956
957
  "edit.mode": {
957
958
  type: "enum",
958
- values: ["replace", "patch", "hashline", "vim", "apply_patch", "atom"] as const,
959
+ values: EDIT_MODES,
959
960
  default: "hashline",
960
961
  ui: {
961
962
  tab: "editing",
962
963
  label: "Edit Mode",
963
- description: "Select the edit tool variant (replace, patch, hashline, vim, or apply_patch)",
964
+ description: "Select the edit tool variant (replace, patch, hashline, atom, vim, or apply_patch)",
964
965
  },
965
966
  },
966
967
 
@@ -1027,7 +1028,7 @@ export const SETTINGS_SCHEMA = {
1027
1028
 
1028
1029
  "read.defaultLimit": {
1029
1030
  type: "number",
1030
- default: 300,
1031
+ default: 500,
1031
1032
  ui: {
1032
1033
  tab: "editing",
1033
1034
  label: "Default Read Limit",
@@ -1198,30 +1199,30 @@ export const SETTINGS_SCHEMA = {
1198
1199
  ui: { tab: "tools", label: "Find", description: "Enable the find tool for file searching" },
1199
1200
  },
1200
1201
 
1201
- "grep.enabled": {
1202
+ "search.enabled": {
1202
1203
  type: "boolean",
1203
1204
  default: true,
1204
- ui: { tab: "tools", label: "Grep", description: "Enable the grep tool for content searching" },
1205
+ ui: { tab: "tools", label: "Search", description: "Enable the search tool for content searching" },
1205
1206
  },
1206
1207
 
1207
- "grep.contextBefore": {
1208
+ "search.contextBefore": {
1208
1209
  type: "number",
1209
1210
  default: 1,
1210
1211
  ui: {
1211
1212
  tab: "tools",
1212
- label: "Grep Context Before",
1213
- description: "Lines of context before each grep match",
1213
+ label: "Search Context Before",
1214
+ description: "Lines of context before each search match",
1214
1215
  submenu: true,
1215
1216
  },
1216
1217
  },
1217
1218
 
1218
- "grep.contextAfter": {
1219
+ "search.contextAfter": {
1219
1220
  type: "number",
1220
1221
  default: 3,
1221
1222
  ui: {
1222
1223
  tab: "tools",
1223
- label: "Grep Context After",
1224
- description: "Lines of context after each grep match",
1224
+ label: "Search Context After",
1225
+ description: "Lines of context after each search match",
1225
1226
  submenu: true,
1226
1227
  },
1227
1228
  },
@@ -326,7 +326,7 @@ export class Settings {
326
326
 
327
327
  /**
328
328
  * Get the edit variant for a specific model.
329
- * Returns "patch", "replace", "hashline", "vim", "apply_patch", or null (use global default).
329
+ * Returns "patch", "replace", "hashline", "atom", "vim", "apply_patch", or null (use global default).
330
330
  */
331
331
  getEditVariantForModel(model: string | undefined): EditMode | null {
332
332
  if (!model) return null;
package/src/cursor.ts CHANGED
@@ -177,10 +177,10 @@ export class CursorExecHandlers implements ICursorExecHandlers {
177
177
 
178
178
  async grep(args: Parameters<NonNullable<ICursorExecHandlers["grep"]>>[0]) {
179
179
  const toolCallId = decodeToolCallId(args.toolCallId);
180
- const grepPath = args.glob ? `${args.path || "."}/${args.glob}` : args.path || ".";
181
- const toolResultMessage = await executeTool(this.options, "grep", toolCallId, {
180
+ const searchPath = args.glob ? `${args.path || "."}/${args.glob}` : args.path || ".";
181
+ const toolResultMessage = await executeTool(this.options, "search", toolCallId, {
182
182
  pattern: args.pattern,
183
- path: grepPath,
183
+ path: searchPath,
184
184
  i: args.caseInsensitive || undefined,
185
185
  });
186
186
  return toolResultMessage;
@@ -327,7 +327,7 @@ export class CursorExecHandlers implements ICursorExecHandlers {
327
327
  const toolCallId = decodeToolCallId(call.toolCallId);
328
328
  const tool = this.options.tools.get(toolName);
329
329
  if (!tool) {
330
- const availableTools = Array.from(this.options.tools.keys()).filter(name => name.startsWith("mcp_"));
330
+ const availableTools = Array.from(this.options.tools.keys()).filter(name => name.startsWith("mcp__"));
331
331
  const message = formatMcpToolErrorMessage(toolName, availableTools);
332
332
  const result = buildToolErrorResult(message);
333
333
  return createToolResultMessage(toolCallId, toolName, result, true);
package/src/edit/index.ts CHANGED
@@ -19,13 +19,8 @@ import { type EditMode, normalizeEditMode, resolveEditMode } from "../utils/edit
19
19
  import type { VimToolDetails } from "../vim/types";
20
20
  import { type ApplyPatchParams, applyPatchSchema, expandApplyPatchToEntries } from "./modes/apply-patch";
21
21
  import applyPatchGrammar from "./modes/apply-patch.lark" with { type: "text" };
22
- import {
23
- type AtomParams,
24
- type AtomToolEdit,
25
- atomEditParamsSchema,
26
- executeAtomSingle,
27
- resolveAtomEntryPaths,
28
- } from "./modes/atom";
22
+ import { type AtomParams, atomEditParamsSchema, executeAtomSingle } from "./modes/atom";
23
+ import atomGrammar from "./modes/atom.lark" with { type: "text" };
29
24
  import {
30
25
  executeHashlineSingle,
31
26
  HashlineMismatchError,
@@ -122,45 +117,11 @@ function createEditWritethrough(session: ToolSession): WritethroughCallback {
122
117
  return enableLsp ? createLspWritethrough(session.cwd, { enableFormat, enableDiagnostics }) : writethroughNoop;
123
118
  }
124
119
 
125
- /**
126
- * Resolve per-entry `path` against an optional top-level `path` default.
127
- * If both are absent on an entry, throws a descriptive error.
128
- */
129
- function resolveEntryPaths<T extends { path?: string }>(
130
- edits: readonly T[],
131
- topLevelPath: string | undefined,
132
- ): (T & { path: string })[] {
133
- return edits.map((edit, i) => {
134
- const path = (edit && typeof edit.path === "string" && edit.path) || topLevelPath;
135
- if (!path) {
136
- throw new Error(
137
- `Edit ${i}: missing \`path\`. Provide \`path\` on this edit or supply a top-level \`path\` for the request.`,
138
- );
139
- }
140
- return { ...edit, path };
141
- });
142
- }
143
-
144
- /** Group items by a key, preserving insertion order. */
145
- function groupBy<T, K>(items: T[], key: (item: T) => K): Map<K, T[]> {
146
- const map = new Map<K, T[]>();
147
- for (const item of items) {
148
- const k = key(item);
149
- let arr = map.get(k);
150
- if (!arr) {
151
- arr = [];
152
- map.set(k, arr);
153
- }
154
- arr.push(item);
155
- }
156
- return map;
157
- }
158
-
159
- /** Run single-file executors for each file group and aggregate results. */
160
- async function executePerFile(
120
+ /** Run apply_patch file operations and aggregate their multi-file result. */
121
+ async function executeApplyPatchPerFile(
161
122
  fileEntries: {
162
123
  path: string;
163
- run: (batchRequest: LspBatchRequest | undefined) => Promise<AgentToolResult<EditToolDetails, any>>;
124
+ run: (batchRequest: LspBatchRequest | undefined) => Promise<AgentToolResult<EditToolDetails>>;
164
125
  }[],
165
126
  outerBatchRequest: LspBatchRequest | undefined,
166
127
  onUpdate?: (partialResult: AgentToolResult<EditToolDetails, TInput>) => void,
@@ -230,6 +191,58 @@ async function executePerFile(
230
191
  };
231
192
  }
232
193
 
194
+ async function executeSinglePathEntries(
195
+ path: string,
196
+ runs: ((batchRequest: LspBatchRequest | undefined) => Promise<AgentToolResult<EditToolDetails>>)[],
197
+ outerBatchRequest: LspBatchRequest | undefined,
198
+ onUpdate?: (partialResult: AgentToolResult<EditToolDetails, TInput>) => void,
199
+ ): Promise<AgentToolResult<EditToolDetails, TInput>> {
200
+ if (runs.length === 1) {
201
+ return runs[0](outerBatchRequest);
202
+ }
203
+
204
+ const contentTexts: string[] = [];
205
+ const diffTexts: string[] = [];
206
+ let firstChangedLine: number | undefined;
207
+
208
+ for (let i = 0; i < runs.length; i++) {
209
+ const isLast = i === runs.length - 1;
210
+ const batchRequest: LspBatchRequest | undefined = outerBatchRequest
211
+ ? { id: outerBatchRequest.id, flush: isLast && outerBatchRequest.flush }
212
+ : undefined;
213
+
214
+ try {
215
+ const result = await runs[i](batchRequest);
216
+ const details = result.details;
217
+ if (details?.diff) diffTexts.push(details.diff);
218
+ firstChangedLine ??= details?.firstChangedLine;
219
+ const text = result.content?.find(c => c.type === "text")?.text ?? "";
220
+ if (text) contentTexts.push(text);
221
+ } catch (err) {
222
+ const errorText = err instanceof Error ? err.message : String(err);
223
+ contentTexts.push(`Error editing ${path}: ${errorText}`);
224
+ }
225
+
226
+ if (!isLast && onUpdate) {
227
+ onUpdate({
228
+ content: [{ type: "text", text: contentTexts.join("\n") }],
229
+ details: {
230
+ diff: diffTexts.join("\n"),
231
+ firstChangedLine,
232
+ },
233
+ });
234
+ }
235
+ }
236
+
237
+ return {
238
+ content: [{ type: "text", text: contentTexts.join("\n") }],
239
+ details: {
240
+ diff: diffTexts.join("\n"),
241
+ firstChangedLine,
242
+ },
243
+ };
244
+ }
245
+
233
246
  export class EditTool implements AgentTool<TInput> {
234
247
  readonly name = "edit";
235
248
  readonly label = "Edit";
@@ -278,8 +291,9 @@ export class EditTool implements AgentTool<TInput> {
278
291
  * and fall back to emitting a JSON function tool from `parameters`.
279
292
  */
280
293
  get customFormat(): { syntax: "lark"; definition: string } | undefined {
281
- if (this.mode !== "apply_patch") return undefined;
282
- return { syntax: "lark", definition: applyPatchGrammar };
294
+ if (this.mode === "apply_patch") return { syntax: "lark", definition: applyPatchGrammar };
295
+ if (this.mode === "atom") return { syntax: "lark", definition: atomGrammar };
296
+ return undefined;
283
297
  }
284
298
 
285
299
  /**
@@ -316,13 +330,12 @@ export class EditTool implements AgentTool<TInput> {
316
330
  batchRequest: LspBatchRequest | undefined,
317
331
  onUpdate?: (partialResult: AgentToolResult<EditToolDetails, TInput>) => void,
318
332
  ) => {
319
- const { edits, path: topPath } = params as PatchParams & { path?: string };
320
- const resolved = resolveEntryPaths(edits as PatchEditEntry[], topPath);
321
- const entries = resolved.map(entry => ({
322
- path: entry.path,
323
- run: (br: LspBatchRequest | undefined) =>
333
+ const { edits, path } = params as PatchParams;
334
+ const runs = (edits as PatchEditEntry[]).map(
335
+ entry => (br: LspBatchRequest | undefined) =>
324
336
  executePatchSingle({
325
337
  session: tool.session,
338
+ path,
326
339
  params: entry,
327
340
  signal,
328
341
  batchRequest: br,
@@ -331,8 +344,8 @@ export class EditTool implements AgentTool<TInput> {
331
344
  writethrough: tool.#writethrough,
332
345
  beginDeferredDiagnosticsForPath: p => tool.#beginDeferredDiagnosticsForPath(p),
333
346
  }),
334
- }));
335
- return executePerFile(entries, batchRequest, onUpdate);
347
+ );
348
+ return executeSinglePathEntries(path, runs, batchRequest, onUpdate);
336
349
  },
337
350
  },
338
351
  apply_patch: {
@@ -346,21 +359,25 @@ export class EditTool implements AgentTool<TInput> {
346
359
  onUpdate?: (partialResult: AgentToolResult<EditToolDetails, TInput>) => void,
347
360
  ) => {
348
361
  const entries = expandApplyPatchToEntries(params as ApplyPatchParams);
349
- const perFile = entries.map(entry => ({
350
- path: entry.path!,
351
- run: (br: LspBatchRequest | undefined) =>
352
- executePatchSingle({
353
- session: tool.session,
354
- params: entry,
355
- signal,
356
- batchRequest: br,
357
- allowFuzzy: tool.#allowFuzzy,
358
- fuzzyThreshold: tool.#fuzzyThreshold,
359
- writethrough: tool.#writethrough,
360
- beginDeferredDiagnosticsForPath: p => tool.#beginDeferredDiagnosticsForPath(p),
361
- }),
362
- }));
363
- return executePerFile(perFile, batchRequest, onUpdate);
362
+ const perFile = entries.map(entry => {
363
+ const { path, ...patchParams } = entry;
364
+ return {
365
+ path,
366
+ run: (br: LspBatchRequest | undefined) =>
367
+ executePatchSingle({
368
+ session: tool.session,
369
+ path,
370
+ params: patchParams,
371
+ signal,
372
+ batchRequest: br,
373
+ allowFuzzy: tool.#allowFuzzy,
374
+ fuzzyThreshold: tool.#fuzzyThreshold,
375
+ writethrough: tool.#writethrough,
376
+ beginDeferredDiagnosticsForPath: p => tool.#beginDeferredDiagnosticsForPath(p),
377
+ }),
378
+ };
379
+ });
380
+ return executeApplyPatchPerFile(perFile, batchRequest, onUpdate);
364
381
  },
365
382
  },
366
383
  hashline: {
@@ -371,25 +388,18 @@ export class EditTool implements AgentTool<TInput> {
371
388
  params: EditParams,
372
389
  signal: AbortSignal | undefined,
373
390
  batchRequest: LspBatchRequest | undefined,
374
- onUpdate?: (partialResult: AgentToolResult<EditToolDetails, TInput>) => void,
391
+ _onUpdate?: (partialResult: AgentToolResult<EditToolDetails, TInput>) => void,
375
392
  ) => {
376
- const { edits, path: topPath } = params as HashlineParams & { path?: string };
377
- const resolved = resolveEntryPaths(edits as HashlineToolEdit[], topPath);
378
- const byFile = groupBy(resolved, e => e.path);
379
- const entries = [...byFile.entries()].map(([path, fileEdits]) => ({
393
+ const { edits, path } = params as HashlineParams;
394
+ return executeHashlineSingle({
395
+ session: tool.session,
380
396
  path,
381
- run: (br: LspBatchRequest | undefined) =>
382
- executeHashlineSingle({
383
- session: tool.session,
384
- path,
385
- edits: fileEdits,
386
- signal,
387
- batchRequest: br,
388
- writethrough: tool.#writethrough,
389
- beginDeferredDiagnosticsForPath: p => tool.#beginDeferredDiagnosticsForPath(p),
390
- }),
391
- }));
392
- return executePerFile(entries, batchRequest, onUpdate);
397
+ edits: edits as HashlineToolEdit[],
398
+ signal,
399
+ batchRequest,
400
+ writethrough: tool.#writethrough,
401
+ beginDeferredDiagnosticsForPath: p => tool.#beginDeferredDiagnosticsForPath(p),
402
+ });
393
403
  },
394
404
  },
395
405
  atom: {
@@ -400,25 +410,18 @@ export class EditTool implements AgentTool<TInput> {
400
410
  params: EditParams,
401
411
  signal: AbortSignal | undefined,
402
412
  batchRequest: LspBatchRequest | undefined,
403
- onUpdate?: (partialResult: AgentToolResult<EditToolDetails, TInput>) => void,
413
+ _onUpdate?: (partialResult: AgentToolResult<EditToolDetails, TInput>) => void,
404
414
  ) => {
405
- const { edits, path: topPath } = params as AtomParams & { path?: string };
406
- const resolved = resolveAtomEntryPaths(edits as AtomToolEdit[], topPath);
407
- const byFile = groupBy(resolved, e => e.path);
408
- const entries = [...byFile.entries()].map(([path, fileEdits]) => ({
415
+ const { input, path } = params as AtomParams & { path?: string };
416
+ return executeAtomSingle({
417
+ session: tool.session,
418
+ input,
409
419
  path,
410
- run: (br: LspBatchRequest | undefined) =>
411
- executeAtomSingle({
412
- session: tool.session,
413
- path,
414
- edits: fileEdits,
415
- signal,
416
- batchRequest: br,
417
- writethrough: tool.#writethrough,
418
- beginDeferredDiagnosticsForPath: p => tool.#beginDeferredDiagnosticsForPath(p),
419
- }),
420
- }));
421
- return executePerFile(entries, batchRequest, onUpdate);
420
+ signal,
421
+ batchRequest,
422
+ writethrough: tool.#writethrough,
423
+ beginDeferredDiagnosticsForPath: p => tool.#beginDeferredDiagnosticsForPath(p),
424
+ });
422
425
  },
423
426
  },
424
427
  replace: {
@@ -431,13 +434,12 @@ export class EditTool implements AgentTool<TInput> {
431
434
  batchRequest: LspBatchRequest | undefined,
432
435
  onUpdate?: (partialResult: AgentToolResult<EditToolDetails, TInput>) => void,
433
436
  ) => {
434
- const { edits, path: topPath } = params as ReplaceParams & { path?: string };
435
- const resolved = resolveEntryPaths(edits as ReplaceEditEntry[], topPath);
436
- const entries = resolved.map(entry => ({
437
- path: entry.path,
438
- run: (br: LspBatchRequest | undefined) =>
437
+ const { edits, path } = params as ReplaceParams;
438
+ const runs = (edits as ReplaceEditEntry[]).map(
439
+ entry => (br: LspBatchRequest | undefined) =>
439
440
  executeReplaceSingle({
440
441
  session: tool.session,
442
+ path,
441
443
  params: entry,
442
444
  signal,
443
445
  batchRequest: br,
@@ -446,8 +448,8 @@ export class EditTool implements AgentTool<TInput> {
446
448
  writethrough: tool.#writethrough,
447
449
  beginDeferredDiagnosticsForPath: p => tool.#beginDeferredDiagnosticsForPath(p),
448
450
  }),
449
- }));
450
- return executePerFile(entries, batchRequest, onUpdate);
451
+ );
452
+ return executeSinglePathEntries(path, runs, batchRequest, onUpdate);
451
453
  },
452
454
  },
453
455
  vim: {