@oh-my-pi/pi-coding-agent 14.1.0 → 14.1.1

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 (82) hide show
  1. package/CHANGELOG.md +79 -0
  2. package/package.json +8 -8
  3. package/src/async/job-manager.ts +43 -10
  4. package/src/commit/agentic/tools/analyze-file.ts +1 -2
  5. package/src/config/mcp-schema.json +1 -1
  6. package/src/config/model-equivalence.ts +1 -0
  7. package/src/config/model-registry.ts +63 -34
  8. package/src/config/model-resolver.ts +111 -15
  9. package/src/config/settings-schema.ts +4 -3
  10. package/src/config/settings.ts +1 -1
  11. package/src/cursor.ts +64 -23
  12. package/src/edit/index.ts +254 -89
  13. package/src/edit/modes/chunk.ts +336 -57
  14. package/src/edit/modes/hashline.ts +51 -26
  15. package/src/edit/modes/patch.ts +16 -10
  16. package/src/edit/modes/replace.ts +15 -7
  17. package/src/edit/renderer.ts +248 -94
  18. package/src/export/html/template.generated.ts +1 -1
  19. package/src/export/html/template.js +6 -4
  20. package/src/extensibility/custom-tools/types.ts +0 -3
  21. package/src/extensibility/extensions/loader.ts +16 -0
  22. package/src/extensibility/extensions/runner.ts +2 -7
  23. package/src/extensibility/extensions/types.ts +8 -4
  24. package/src/internal-urls/docs-index.generated.ts +3 -3
  25. package/src/ipy/executor.ts +447 -52
  26. package/src/ipy/kernel.ts +39 -13
  27. package/src/lsp/client.ts +54 -0
  28. package/src/lsp/index.ts +8 -0
  29. package/src/lsp/types.ts +6 -0
  30. package/src/main.ts +0 -1
  31. package/src/modes/acp/acp-agent.ts +4 -1
  32. package/src/modes/components/bash-execution.ts +16 -4
  33. package/src/modes/components/status-line/presets.ts +17 -6
  34. package/src/modes/components/status-line/segments.ts +15 -0
  35. package/src/modes/components/status-line-segment-editor.ts +1 -0
  36. package/src/modes/components/status-line.ts +7 -1
  37. package/src/modes/components/tool-execution.ts +145 -75
  38. package/src/modes/controllers/command-controller.ts +24 -1
  39. package/src/modes/controllers/event-controller.ts +4 -1
  40. package/src/modes/controllers/extension-ui-controller.ts +28 -5
  41. package/src/modes/controllers/input-controller.ts +9 -3
  42. package/src/modes/controllers/selector-controller.ts +4 -1
  43. package/src/modes/interactive-mode.ts +19 -3
  44. package/src/modes/print-mode.ts +13 -4
  45. package/src/modes/prompt-action-autocomplete.ts +3 -5
  46. package/src/modes/rpc/rpc-mode.ts +8 -2
  47. package/src/modes/shared.ts +2 -2
  48. package/src/modes/types.ts +1 -0
  49. package/src/modes/utils/ui-helpers.ts +1 -0
  50. package/src/prompts/tools/bash.md +2 -2
  51. package/src/prompts/tools/chunk-edit.md +191 -163
  52. package/src/prompts/tools/hashline.md +11 -11
  53. package/src/prompts/tools/patch.md +10 -5
  54. package/src/prompts/tools/{await.md → poll.md} +1 -1
  55. package/src/prompts/tools/read-chunk.md +3 -3
  56. package/src/prompts/tools/task.md +2 -2
  57. package/src/prompts/tools/vim.md +98 -0
  58. package/src/sdk.ts +754 -724
  59. package/src/session/agent-session.ts +164 -34
  60. package/src/session/session-manager.ts +50 -4
  61. package/src/slash-commands/builtin-registry.ts +17 -0
  62. package/src/task/executor.ts +4 -4
  63. package/src/task/index.ts +3 -5
  64. package/src/task/types.ts +2 -2
  65. package/src/tools/bash.ts +26 -8
  66. package/src/tools/find.ts +5 -2
  67. package/src/tools/grep.ts +77 -8
  68. package/src/tools/index.ts +48 -19
  69. package/src/tools/{await-tool.ts → poll-tool.ts} +36 -30
  70. package/src/tools/python.ts +293 -278
  71. package/src/tools/submit-result.ts +5 -2
  72. package/src/tools/todo-write.ts +8 -2
  73. package/src/tools/vim.ts +966 -0
  74. package/src/utils/edit-mode.ts +2 -1
  75. package/src/utils/session-color.ts +55 -0
  76. package/src/utils/title-generator.ts +15 -6
  77. package/src/vim/buffer.ts +309 -0
  78. package/src/vim/commands.ts +382 -0
  79. package/src/vim/engine.ts +2426 -0
  80. package/src/vim/parser.ts +151 -0
  81. package/src/vim/render.ts +252 -0
  82. package/src/vim/types.ts +197 -0
package/src/cursor.ts CHANGED
@@ -13,6 +13,7 @@ import type {
13
13
  CursorExecHandlers as ICursorExecHandlers,
14
14
  ToolResultMessage,
15
15
  } from "@oh-my-pi/pi-ai";
16
+ import { sanitizeText } from "@oh-my-pi/pi-natives";
16
17
  import { resolveToCwd } from "./tools/path-utils";
17
18
 
18
19
  interface CursorExecBridgeOptions {
@@ -65,12 +66,16 @@ async function executeTool(
65
66
 
66
67
  const onUpdate: AgentToolUpdateCallback<unknown> | undefined = options.emitEvent
67
68
  ? partialResult => {
69
+ const sanitizedResult: AgentToolResult<unknown> = {
70
+ content: partialResult.content.map(c => (c.type === "text" ? { ...c, text: sanitizeText(c.text) } : c)),
71
+ details: partialResult.details,
72
+ };
68
73
  options.emitEvent?.({
69
74
  type: "tool_execution_update",
70
75
  toolCallId,
71
76
  toolName,
72
77
  args,
73
- partialResult,
78
+ partialResult: sanitizedResult,
74
79
  });
75
80
  }
76
81
  : undefined;
@@ -89,7 +94,11 @@ async function executeTool(
89
94
  isError = true;
90
95
  }
91
96
 
92
- options.emitEvent?.({ type: "tool_execution_end", toolCallId, toolName, result, isError });
97
+ const sanitizedFinalResult: AgentToolResult<unknown> = {
98
+ content: result.content.map(c => (c.type === "text" ? { ...c, text: sanitizeText(c.text) } : c)),
99
+ details: result.details,
100
+ };
101
+ options.emitEvent?.({ type: "tool_execution_end", toolCallId, toolName, result: sanitizedFinalResult, isError });
93
102
 
94
103
  return createToolResultMessage(toolCallId, toolName, result, isError);
95
104
  }
@@ -233,21 +242,43 @@ export class CursorExecHandlers implements ICursorExecHandlers {
233
242
  let result: AgentToolResult<unknown>;
234
243
  let isError = false;
235
244
 
236
- // Track previously streamed text so we only forward deltas.
237
- let streamedLen = 0;
245
+ let rawText = "";
246
+ let sanitizedRawText = "";
247
+ let streamedSanitizedText = "";
248
+ let canStreamSanitizedDelta = true;
238
249
  const onUpdate: AgentToolUpdateCallback<unknown> = partialResult => {
250
+ const newRawText = partialResult.content.map(c => (c.type === "text" ? c.text : "")).join("");
251
+ if (newRawText === rawText) {
252
+ return;
253
+ }
254
+ rawText = newRawText;
255
+ sanitizedRawText = sanitizeText(newRawText);
256
+ const sanitizedPartialResult: AgentToolResult<unknown> = {
257
+ content: [{ type: "text" as const, text: sanitizedRawText }],
258
+ details: partialResult.details,
259
+ };
239
260
  this.options.emitEvent?.({
240
261
  type: "tool_execution_update",
241
262
  toolCallId,
242
263
  toolName,
243
264
  args: toolArgs,
244
- partialResult,
265
+ partialResult: sanitizedPartialResult,
245
266
  });
246
- const text = partialResult.content.map(c => (c.type === "text" ? c.text : "")).join("");
247
- if (text.length > streamedLen) {
248
- callbacks.onStdout(text.slice(streamedLen));
249
- streamedLen = text.length;
267
+ if (!canStreamSanitizedDelta) {
268
+ return;
269
+ }
270
+ if (sanitizedRawText.startsWith(streamedSanitizedText)) {
271
+ const sanitizedDelta = sanitizedRawText.slice(streamedSanitizedText.length);
272
+ streamedSanitizedText = sanitizedRawText;
273
+ if (sanitizedDelta) {
274
+ callbacks.onStdout(sanitizedDelta);
275
+ }
276
+ return;
250
277
  }
278
+ // Cursor's shell-stream callback is append-only. Once the sanitized snapshot
279
+ // stops being a prefix extension, we can no longer repair the stream safely.
280
+ // Keep emitting full snapshots via tool_execution_update, but stop stdout deltas.
281
+ canStreamSanitizedDelta = false;
251
282
  };
252
283
 
253
284
  try {
@@ -260,12 +291,30 @@ export class CursorExecHandlers implements ICursorExecHandlers {
260
291
 
261
292
  // onUpdate may not fire for every chunk — flush any remaining output
262
293
  // from the final result that wasn't already streamed.
263
- const finalText = result.content.map(c => (c.type === "text" ? c.text : "")).join("");
264
- if (finalText.length > streamedLen) {
265
- callbacks.onStdout(finalText.slice(streamedLen));
294
+ const finalRawText = result.content.map(c => (c.type === "text" ? c.text : "")).join("");
295
+ if (finalRawText !== rawText) {
296
+ rawText = finalRawText;
297
+ sanitizedRawText = sanitizeText(finalRawText);
298
+ }
299
+ if (canStreamSanitizedDelta && sanitizedRawText.startsWith(streamedSanitizedText)) {
300
+ const finalDelta = sanitizedRawText.slice(streamedSanitizedText.length);
301
+ streamedSanitizedText = sanitizedRawText;
302
+ if (finalDelta) {
303
+ callbacks.onStdout(finalDelta);
304
+ }
266
305
  }
267
306
 
268
- this.options.emitEvent?.({ type: "tool_execution_end", toolCallId, toolName, result, isError });
307
+ const sanitizedFinalResult: AgentToolResult<unknown> = {
308
+ content: result.content.map(c => (c.type === "text" ? { ...c, text: sanitizeText(c.text) } : c)),
309
+ details: result.details,
310
+ };
311
+ this.options.emitEvent?.({
312
+ type: "tool_execution_end",
313
+ toolCallId,
314
+ toolName,
315
+ result: sanitizedFinalResult,
316
+ isError,
317
+ });
269
318
  return createToolResultMessage(toolCallId, toolName, result, isError);
270
319
  }
271
320
 
@@ -285,16 +334,8 @@ export class CursorExecHandlers implements ICursorExecHandlers {
285
334
  if (!tool) {
286
335
  const availableTools = Array.from(this.options.tools.keys()).filter(name => name.startsWith("mcp_"));
287
336
  const message = formatMcpToolErrorMessage(toolName, availableTools);
288
- const toolResult: ToolResultMessage = {
289
- role: "toolResult",
290
- toolCallId,
291
- toolName,
292
- content: [{ type: "text", text: message }],
293
- details: {},
294
- isError: true,
295
- timestamp: Date.now(),
296
- };
297
- return toolResult;
337
+ const result = buildToolErrorResult(message);
338
+ return createToolResultMessage(toolCallId, toolName, result, true);
298
339
  }
299
340
 
300
341
  const args = Object.keys(call.args ?? {}).length > 0 ? call.args : decodeMcpArgs(call.rawArgs ?? {});
package/src/edit/index.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
2
2
  import { prompt } from "@oh-my-pi/pi-utils";
3
+ import type { Static } from "@sinclair/typebox";
3
4
  import {
4
5
  createLspWritethrough,
5
6
  type FileDiagnosticsResult,
@@ -12,19 +13,41 @@ import hashlineDescription from "../prompts/tools/hashline.md" with { type: "tex
12
13
  import patchDescription from "../prompts/tools/patch.md" with { type: "text" };
13
14
  import replaceDescription from "../prompts/tools/replace.md" with { type: "text" };
14
15
  import type { ToolSession } from "../tools";
16
+ import { VimTool, vimSchema } from "../tools/vim";
15
17
  import { type EditMode, normalizeEditMode, resolveEditMode } from "../utils/edit-mode";
18
+ import type { VimToolDetails } from "../vim/types";
16
19
  import {
17
20
  type ChunkParams,
21
+ type ChunkToolEdit,
18
22
  chunkEditParamsSchema,
19
- executeChunkMode,
23
+ executeChunkSingle,
20
24
  isChunkParams,
25
+ parseChunkEditPath,
21
26
  resolveAnchorStyle,
22
27
  resolveChunkAutoIndent,
23
28
  } from "./modes/chunk";
24
- import { executeHashlineMode, type HashlineParams, hashlineEditParamsSchema, isHashlineParams } from "./modes/hashline";
25
- import { executePatchMode, isPatchParams, type PatchParams, patchEditSchema } from "./modes/patch";
26
- import { executeReplaceMode, isReplaceParams, type ReplaceParams, replaceEditSchema } from "./modes/replace";
27
- import { type EditToolDetails, getLspBatchRequest, type LspBatchRequest } from "./renderer";
29
+ import {
30
+ executeHashlineSingle,
31
+ type HashlineParams,
32
+ type HashlineToolEdit,
33
+ hashlineEditParamsSchema,
34
+ isHashlineParams,
35
+ } from "./modes/hashline";
36
+ import {
37
+ executePatchSingle,
38
+ isPatchParams,
39
+ type PatchEditEntry,
40
+ type PatchParams,
41
+ patchEditSchema,
42
+ } from "./modes/patch";
43
+ import {
44
+ executeReplaceSingle,
45
+ isReplaceParams,
46
+ type ReplaceEditEntry,
47
+ type ReplaceParams,
48
+ replaceEditSchema,
49
+ } from "./modes/replace";
50
+ import { type EditToolDetails, type EditToolPerFileResult, getLspBatchRequest, type LspBatchRequest } from "./renderer";
28
51
 
29
52
  export { DEFAULT_EDIT_MODE, type EditMode, normalizeEditMode } from "../utils/edit-mode";
30
53
  export * from "./diff";
@@ -40,22 +63,25 @@ type TInput =
40
63
  | typeof replaceEditSchema
41
64
  | typeof patchEditSchema
42
65
  | typeof hashlineEditParamsSchema
43
- | typeof chunkEditParamsSchema;
66
+ | typeof chunkEditParamsSchema
67
+ | typeof vimSchema;
44
68
 
45
- type EditParams = ReplaceParams | PatchParams | HashlineParams | ChunkParams;
46
-
47
- type ModeExecutionArgs = {
48
- params: EditParams;
49
- signal: AbortSignal | undefined;
50
- batchRequest: LspBatchRequest | undefined;
51
- };
69
+ type VimParams = Static<typeof vimSchema>;
70
+ type EditParams = ReplaceParams | PatchParams | HashlineParams | ChunkParams | VimParams;
71
+ type EditToolResultDetails = EditToolDetails | VimToolDetails;
52
72
 
53
73
  type EditModeDefinition = {
54
74
  description: (session: ToolSession) => string;
55
75
  parameters: TInput;
56
76
  invalidParamsMessage: string;
57
77
  validate: (params: EditParams) => boolean;
58
- execute: (tool: EditTool, args: ModeExecutionArgs) => Promise<AgentToolResult<EditToolDetails, TInput>>;
78
+ execute: (
79
+ tool: EditTool,
80
+ params: EditParams,
81
+ signal: AbortSignal | undefined,
82
+ batchRequest: LspBatchRequest | undefined,
83
+ onUpdate?: (partialResult: AgentToolResult<EditToolResultDetails, TInput>) => void,
84
+ ) => Promise<AgentToolResult<EditToolResultDetails, TInput>>;
59
85
  };
60
86
 
61
87
  function resolveConfiguredEditMode(rawEditMode: string): EditMode | undefined {
@@ -71,6 +97,10 @@ function resolveConfiguredEditMode(rawEditMode: string): EditMode | undefined {
71
97
  return editMode;
72
98
  }
73
99
 
100
+ function isVimParams(params: EditParams): params is VimParams {
101
+ return typeof params === "object" && params !== null && "file" in params && typeof params.file === "string";
102
+ }
103
+
74
104
  function resolveAllowFuzzy(session: ToolSession, rawValue: string): boolean {
75
105
  switch (rawValue) {
76
106
  case "true":
@@ -106,6 +136,94 @@ function createEditWritethrough(session: ToolSession): WritethroughCallback {
106
136
  return enableLsp ? createLspWritethrough(session.cwd, { enableFormat, enableDiagnostics }) : writethroughNoop;
107
137
  }
108
138
 
139
+ /** Group items by a key, preserving insertion order. */
140
+ function groupBy<T, K>(items: T[], key: (item: T) => K): Map<K, T[]> {
141
+ const map = new Map<K, T[]>();
142
+ for (const item of items) {
143
+ const k = key(item);
144
+ let arr = map.get(k);
145
+ if (!arr) {
146
+ arr = [];
147
+ map.set(k, arr);
148
+ }
149
+ arr.push(item);
150
+ }
151
+ return map;
152
+ }
153
+
154
+ /** Run single-file executors for each file group and aggregate results. */
155
+ async function executePerFile(
156
+ fileEntries: {
157
+ path: string;
158
+ run: (batchRequest: LspBatchRequest | undefined) => Promise<AgentToolResult<EditToolDetails, any>>;
159
+ }[],
160
+ outerBatchRequest: LspBatchRequest | undefined,
161
+ onUpdate?: (partialResult: AgentToolResult<EditToolDetails, TInput>) => void,
162
+ ): Promise<AgentToolResult<EditToolDetails, TInput>> {
163
+ if (fileEntries.length === 1) {
164
+ // Single file — just run directly, no wrapping
165
+ return fileEntries[0].run(outerBatchRequest);
166
+ }
167
+
168
+ const perFileResults: EditToolPerFileResult[] = [];
169
+ const contentTexts: string[] = [];
170
+
171
+ for (let i = 0; i < fileEntries.length; i++) {
172
+ const { path, run } = fileEntries[i];
173
+ const isLast = i === fileEntries.length - 1;
174
+ const batchRequest: LspBatchRequest | undefined = outerBatchRequest
175
+ ? { id: outerBatchRequest.id, flush: isLast && outerBatchRequest.flush }
176
+ : undefined;
177
+
178
+ try {
179
+ const result = await run(batchRequest);
180
+ const details = result.details;
181
+ perFileResults.push({
182
+ path,
183
+ diff: details?.diff ?? "",
184
+ firstChangedLine: details?.firstChangedLine,
185
+ diagnostics: details?.diagnostics,
186
+ op: details?.op,
187
+ move: details?.move,
188
+ meta: details?.meta,
189
+ });
190
+ const text = result.content?.find(c => c.type === "text")?.text ?? "";
191
+ if (text) contentTexts.push(text);
192
+ } catch (err) {
193
+ const errorText = err instanceof Error ? err.message : String(err);
194
+ perFileResults.push({ path, diff: "", isError: true, errorText });
195
+ contentTexts.push(`Error editing ${path}: ${errorText}`);
196
+ }
197
+
198
+ // Emit partial result after each file so UI shows progressive completion
199
+ if (!isLast && onUpdate) {
200
+ onUpdate({
201
+ content: [{ type: "text", text: contentTexts.join("\n") }],
202
+ details: {
203
+ diff: perFileResults
204
+ .map(r => r.diff)
205
+ .filter(Boolean)
206
+ .join("\n"),
207
+ firstChangedLine: perFileResults.find(r => r.firstChangedLine)?.firstChangedLine,
208
+ perFileResults: [...perFileResults],
209
+ },
210
+ });
211
+ }
212
+ }
213
+
214
+ return {
215
+ content: [{ type: "text", text: contentTexts.join("\n") }],
216
+ details: {
217
+ diff: perFileResults
218
+ .map(r => r.diff)
219
+ .filter(Boolean)
220
+ .join("\n"),
221
+ firstChangedLine: perFileResults.find(r => r.firstChangedLine)?.firstChangedLine,
222
+ perFileResults,
223
+ },
224
+ };
225
+ }
226
+
109
227
  export class EditTool implements AgentTool<TInput> {
110
228
  readonly name = "edit";
111
229
  readonly label = "Edit";
@@ -117,6 +235,7 @@ export class EditTool implements AgentTool<TInput> {
117
235
  readonly #fuzzyThreshold: number;
118
236
  readonly #writethrough: WritethroughCallback;
119
237
  readonly #editMode?: EditMode;
238
+ readonly #vimTool: VimTool;
120
239
  readonly #pendingDeferredFetches = new Map<string, AbortController>();
121
240
 
122
241
  constructor(private readonly session: ToolSession) {
@@ -130,6 +249,7 @@ export class EditTool implements AgentTool<TInput> {
130
249
  this.#allowFuzzy = resolveAllowFuzzy(session, editFuzzy);
131
250
  this.#fuzzyThreshold = resolveFuzzyThreshold(session, editFuzzyThreshold);
132
251
  this.#writethrough = createEditWritethrough(session);
252
+ this.#vimTool = new VimTool(session);
133
253
  }
134
254
 
135
255
  get mode(): EditMode {
@@ -145,51 +265,19 @@ export class EditTool implements AgentTool<TInput> {
145
265
  return this.#getModeDefinition().parameters;
146
266
  }
147
267
 
148
- async execute(
149
- _toolCallId: string,
150
- params: ReplaceParams,
151
- signal?: AbortSignal,
152
- _onUpdate?: AgentToolUpdateCallback<EditToolDetails, TInput>,
153
- context?: AgentToolContext,
154
- ): Promise<AgentToolResult<EditToolDetails, TInput>>;
155
- async execute(
156
- _toolCallId: string,
157
- params: PatchParams,
158
- signal?: AbortSignal,
159
- _onUpdate?: AgentToolUpdateCallback<EditToolDetails, TInput>,
160
- context?: AgentToolContext,
161
- ): Promise<AgentToolResult<EditToolDetails, TInput>>;
162
- async execute(
163
- _toolCallId: string,
164
- params: HashlineParams,
165
- signal?: AbortSignal,
166
- _onUpdate?: AgentToolUpdateCallback<EditToolDetails, TInput>,
167
- context?: AgentToolContext,
168
- ): Promise<AgentToolResult<EditToolDetails, TInput>>;
169
- async execute(
170
- _toolCallId: string,
171
- params: ChunkParams,
172
- signal?: AbortSignal,
173
- _onUpdate?: AgentToolUpdateCallback<EditToolDetails, TInput>,
174
- context?: AgentToolContext,
175
- ): Promise<AgentToolResult<EditToolDetails, TInput>>;
176
268
  async execute(
177
269
  _toolCallId: string,
178
270
  params: EditParams,
179
271
  signal?: AbortSignal,
180
- _onUpdate?: AgentToolUpdateCallback<EditToolDetails, TInput>,
272
+ onUpdate?: AgentToolUpdateCallback<EditToolResultDetails, TInput>,
181
273
  context?: AgentToolContext,
182
- ): Promise<AgentToolResult<EditToolDetails, TInput>> {
274
+ ): Promise<AgentToolResult<EditToolResultDetails, TInput>> {
183
275
  const modeDefinition = this.#getModeDefinition();
184
276
  if (!modeDefinition.validate(params)) {
185
277
  throw new Error(modeDefinition.invalidParamsMessage);
186
278
  }
187
279
 
188
- return modeDefinition.execute(this, {
189
- params,
190
- signal,
191
- batchRequest: getLspBatchRequest(context?.toolCall),
192
- });
280
+ return modeDefinition.execute(this, params, signal, getLspBatchRequest(context?.toolCall), onUpdate);
193
281
  }
194
282
 
195
283
  #getModeDefinition(): EditModeDefinition {
@@ -203,15 +291,29 @@ export class EditTool implements AgentTool<TInput> {
203
291
  parameters: chunkEditParamsSchema,
204
292
  invalidParamsMessage: "Invalid edit parameters for chunk mode.",
205
293
  validate: isChunkParams,
206
- async execute(tool: EditTool, args: ModeExecutionArgs) {
207
- return executeChunkMode({
208
- session: tool.session,
209
- params: args.params as ChunkParams,
210
- signal: args.signal,
211
- batchRequest: args.batchRequest,
212
- writethrough: tool.#writethrough,
213
- beginDeferredDiagnosticsForPath: path => tool.#beginDeferredDiagnosticsForPath(path),
214
- });
294
+ execute: (
295
+ tool: EditTool,
296
+ params: EditParams,
297
+ signal: AbortSignal | undefined,
298
+ batchRequest: LspBatchRequest | undefined,
299
+ onUpdate?: (partialResult: AgentToolResult<EditToolDetails, TInput>) => void,
300
+ ) => {
301
+ const { edits } = params as ChunkParams;
302
+ const byFile = groupBy(edits, (e: ChunkToolEdit) => parseChunkEditPath(e.path).filePath);
303
+ const entries = [...byFile.entries()].map(([filePath, fileEdits]) => ({
304
+ path: filePath,
305
+ run: (br: LspBatchRequest | undefined) =>
306
+ executeChunkSingle({
307
+ session: tool.session,
308
+ path: filePath,
309
+ edits: fileEdits,
310
+ signal,
311
+ batchRequest: br,
312
+ writethrough: tool.#writethrough,
313
+ beginDeferredDiagnosticsForPath: p => tool.#beginDeferredDiagnosticsForPath(p),
314
+ }),
315
+ }));
316
+ return executePerFile(entries, batchRequest, onUpdate);
215
317
  },
216
318
  },
217
319
  patch: {
@@ -219,17 +321,29 @@ export class EditTool implements AgentTool<TInput> {
219
321
  parameters: patchEditSchema,
220
322
  invalidParamsMessage: "Invalid edit parameters for patch mode.",
221
323
  validate: isPatchParams,
222
- async execute(tool: EditTool, args: ModeExecutionArgs) {
223
- return executePatchMode({
224
- session: tool.session,
225
- params: args.params as PatchParams,
226
- signal: args.signal,
227
- batchRequest: args.batchRequest,
228
- allowFuzzy: tool.#allowFuzzy,
229
- fuzzyThreshold: tool.#fuzzyThreshold,
230
- writethrough: tool.#writethrough,
231
- beginDeferredDiagnosticsForPath: path => tool.#beginDeferredDiagnosticsForPath(path),
232
- });
324
+ execute: (
325
+ tool: EditTool,
326
+ params: EditParams,
327
+ signal: AbortSignal | undefined,
328
+ batchRequest: LspBatchRequest | undefined,
329
+ onUpdate?: (partialResult: AgentToolResult<EditToolDetails, TInput>) => void,
330
+ ) => {
331
+ const { edits } = params as PatchParams;
332
+ const entries = edits.map((entry: PatchEditEntry) => ({
333
+ path: entry.path,
334
+ run: (br: LspBatchRequest | undefined) =>
335
+ executePatchSingle({
336
+ session: tool.session,
337
+ params: entry,
338
+ signal,
339
+ batchRequest: br,
340
+ allowFuzzy: tool.#allowFuzzy,
341
+ fuzzyThreshold: tool.#fuzzyThreshold,
342
+ writethrough: tool.#writethrough,
343
+ beginDeferredDiagnosticsForPath: p => tool.#beginDeferredDiagnosticsForPath(p),
344
+ }),
345
+ }));
346
+ return executePerFile(entries, batchRequest, onUpdate);
233
347
  },
234
348
  },
235
349
  hashline: {
@@ -237,15 +351,29 @@ export class EditTool implements AgentTool<TInput> {
237
351
  parameters: hashlineEditParamsSchema,
238
352
  invalidParamsMessage: "Invalid edit parameters for hashline mode.",
239
353
  validate: isHashlineParams,
240
- async execute(tool: EditTool, args: ModeExecutionArgs) {
241
- return executeHashlineMode({
242
- session: tool.session,
243
- params: args.params as HashlineParams,
244
- signal: args.signal,
245
- batchRequest: args.batchRequest,
246
- writethrough: tool.#writethrough,
247
- beginDeferredDiagnosticsForPath: path => tool.#beginDeferredDiagnosticsForPath(path),
248
- });
354
+ execute: (
355
+ tool: EditTool,
356
+ params: EditParams,
357
+ signal: AbortSignal | undefined,
358
+ batchRequest: LspBatchRequest | undefined,
359
+ onUpdate?: (partialResult: AgentToolResult<EditToolDetails, TInput>) => void,
360
+ ) => {
361
+ const { edits } = params as HashlineParams;
362
+ const byFile = groupBy(edits, (e: HashlineToolEdit) => e.path);
363
+ const entries = [...byFile.entries()].map(([path, fileEdits]) => ({
364
+ path,
365
+ run: (br: LspBatchRequest | undefined) =>
366
+ executeHashlineSingle({
367
+ session: tool.session,
368
+ path,
369
+ edits: fileEdits,
370
+ signal,
371
+ batchRequest: br,
372
+ writethrough: tool.#writethrough,
373
+ beginDeferredDiagnosticsForPath: p => tool.#beginDeferredDiagnosticsForPath(p),
374
+ }),
375
+ }));
376
+ return executePerFile(entries, batchRequest, onUpdate);
249
377
  },
250
378
  },
251
379
  replace: {
@@ -253,17 +381,54 @@ export class EditTool implements AgentTool<TInput> {
253
381
  parameters: replaceEditSchema,
254
382
  invalidParamsMessage: "Invalid edit parameters for replace mode.",
255
383
  validate: isReplaceParams,
256
- async execute(tool: EditTool, args: ModeExecutionArgs) {
257
- return executeReplaceMode({
258
- session: tool.session,
259
- params: args.params as ReplaceParams,
260
- signal: args.signal,
261
- batchRequest: args.batchRequest,
262
- allowFuzzy: tool.#allowFuzzy,
263
- fuzzyThreshold: tool.#fuzzyThreshold,
264
- writethrough: tool.#writethrough,
265
- beginDeferredDiagnosticsForPath: path => tool.#beginDeferredDiagnosticsForPath(path),
266
- });
384
+ execute: (
385
+ tool: EditTool,
386
+ params: EditParams,
387
+ signal: AbortSignal | undefined,
388
+ batchRequest: LspBatchRequest | undefined,
389
+ onUpdate?: (partialResult: AgentToolResult<EditToolDetails, TInput>) => void,
390
+ ) => {
391
+ const { edits } = params as ReplaceParams;
392
+ const entries = edits.map((entry: ReplaceEditEntry) => ({
393
+ path: entry.path,
394
+ run: (br: LspBatchRequest | undefined) =>
395
+ executeReplaceSingle({
396
+ session: tool.session,
397
+ params: entry,
398
+ signal,
399
+ batchRequest: br,
400
+ allowFuzzy: tool.#allowFuzzy,
401
+ fuzzyThreshold: tool.#fuzzyThreshold,
402
+ writethrough: tool.#writethrough,
403
+ beginDeferredDiagnosticsForPath: p => tool.#beginDeferredDiagnosticsForPath(p),
404
+ }),
405
+ }));
406
+ return executePerFile(entries, batchRequest, onUpdate);
407
+ },
408
+ },
409
+ vim: {
410
+ description: () => this.#vimTool.description,
411
+ parameters: vimSchema,
412
+ invalidParamsMessage: "Invalid edit parameters for vim mode.",
413
+ validate: isVimParams,
414
+ execute: async (
415
+ tool: EditTool,
416
+ params: EditParams,
417
+ signal: AbortSignal | undefined,
418
+ _batchRequest: LspBatchRequest | undefined,
419
+ onUpdate?: (partialResult: AgentToolResult<EditToolResultDetails, TInput>) => void,
420
+ ) => {
421
+ const handleUpdate = onUpdate
422
+ ? (partialResult: AgentToolResult<VimToolDetails>) => {
423
+ onUpdate(partialResult as AgentToolResult<EditToolResultDetails, TInput>);
424
+ }
425
+ : undefined;
426
+ return (await tool.#vimTool.execute(
427
+ "edit",
428
+ params as VimParams,
429
+ signal,
430
+ handleUpdate,
431
+ )) as AgentToolResult<EditToolResultDetails, TInput>;
267
432
  },
268
433
  },
269
434
  }[this.mode];