@gajae-code/coding-agent 0.12.21 → 0.13.0

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 (222) hide show
  1. package/CHANGELOG.md +67 -2
  2. package/dist/types/cli/args.d.ts +5 -2
  3. package/dist/types/cli/root-flags.d.ts +246 -0
  4. package/dist/types/cli.d.ts +11 -19
  5. package/dist/types/commands/daemon.d.ts +0 -30
  6. package/dist/types/commands/launch.d.ts +11 -19
  7. package/dist/types/commands/quick-lane.d.ts +32 -0
  8. package/dist/types/commands/sdk.d.ts +21 -15
  9. package/dist/types/config/file-lock-gc.d.ts +2 -0
  10. package/dist/types/config/model-equivalence.d.ts +20 -0
  11. package/dist/types/config/model-profile-activation.d.ts +62 -17
  12. package/dist/types/config/model-profiles.d.ts +10 -0
  13. package/dist/types/config/model-registry.d.ts +68 -1
  14. package/dist/types/config/model-resolver.d.ts +15 -4
  15. package/dist/types/config/models-config-schema.d.ts +7 -2
  16. package/dist/types/config/provider-selection-policy.d.ts +62 -0
  17. package/dist/types/config/settings-schema.d.ts +36 -78
  18. package/dist/types/config/settings.d.ts +2 -0
  19. package/dist/types/edit/renderer.d.ts +5 -2
  20. package/dist/types/edit/streaming.d.ts +13 -0
  21. package/dist/types/extensibility/custom-tools/types.d.ts +2 -0
  22. package/dist/types/extensibility/extensions/compact-handler.d.ts +2 -1
  23. package/dist/types/extensibility/extensions/types.d.ts +3 -0
  24. package/dist/types/gjc-runtime/gc-runtime.d.ts +19 -5
  25. package/dist/types/internal-urls/local-protocol.d.ts +1 -0
  26. package/dist/types/modes/components/model-selector.d.ts +0 -2
  27. package/dist/types/modes/components/provider-order-context.d.ts +45 -0
  28. package/dist/types/modes/components/provider-order-editor.d.ts +24 -0
  29. package/dist/types/modes/components/settings-defs.d.ts +5 -1
  30. package/dist/types/modes/components/settings-selector.d.ts +2 -0
  31. package/dist/types/quick-lane/classify.d.ts +40 -0
  32. package/dist/types/runtime/stt-settings-setup.d.ts +18 -0
  33. package/dist/types/sdk/broker/broker.d.ts +56 -2
  34. package/dist/types/sdk/broker/operation-receipt-ledger.d.ts +25 -0
  35. package/dist/types/sdk/broker/session-index.d.ts +83 -2
  36. package/dist/types/sdk/broker/state-version.d.ts +1 -1
  37. package/dist/types/sdk/bus/notification-service.d.ts +6 -0
  38. package/dist/types/sdk/bus/prompt-reconciliation.d.ts +8 -1
  39. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +4 -1
  40. package/dist/types/sdk/bus/telegram-daemon.d.ts +26 -0
  41. package/dist/types/sdk/cli/rows.d.ts +98 -0
  42. package/dist/types/sdk/cli/session-cli.d.ts +35 -3
  43. package/dist/types/sdk/elevation/allowlist.d.ts +20 -0
  44. package/dist/types/sdk/elevation/audit.d.ts +93 -0
  45. package/dist/types/sdk/elevation/capability.d.ts +3 -0
  46. package/dist/types/sdk/elevation/digest.d.ts +72 -0
  47. package/dist/types/sdk/elevation/dispatch-receipt.d.ts +85 -0
  48. package/dist/types/sdk/elevation/grant-ledger.d.ts +163 -0
  49. package/dist/types/sdk/elevation/owner.d.ts +48 -0
  50. package/dist/types/sdk/guides/cache.d.ts +98 -0
  51. package/dist/types/sdk/guides/catalog.d.ts +156 -0
  52. package/dist/types/sdk/guides/cli.d.ts +39 -0
  53. package/dist/types/sdk/guides/manifest.d.ts +66 -0
  54. package/dist/types/sdk/guides/verify.d.ts +66 -0
  55. package/dist/types/sdk/host/control/dispatch.d.ts +8 -0
  56. package/dist/types/sdk/host/control/index.d.ts +1 -1
  57. package/dist/types/sdk/host/control/operations.d.ts +3 -2
  58. package/dist/types/sdk/host/query/cursor.d.ts +10 -0
  59. package/dist/types/sdk/host/query/handlers.d.ts +21 -0
  60. package/dist/types/sdk/host/session-runtime.d.ts +9 -0
  61. package/dist/types/sdk/mcp/server.d.ts +2 -2
  62. package/dist/types/sdk/prompt-status.d.ts +11 -0
  63. package/dist/types/sdk/session.d.ts +4 -0
  64. package/dist/types/sdk/transport/index.d.ts +1 -0
  65. package/dist/types/sdk/transport/relay.d.ts +1 -0
  66. package/dist/types/sdk/transport/serve-cli.d.ts +20 -1
  67. package/dist/types/session/agent-session.d.ts +32 -18
  68. package/dist/types/session/blob-store.d.ts +13 -7
  69. package/dist/types/session/fallback-chain-controller.d.ts +15 -0
  70. package/dist/types/session/internal/managed-session-storage.d.ts +25 -1
  71. package/dist/types/session/internal/session-memory-sidecar.d.ts +5 -5
  72. package/dist/types/session/session-manager.d.ts +139 -10
  73. package/dist/types/session/session-storage.d.ts +48 -0
  74. package/dist/types/setup/provider-onboarding.d.ts +7 -1
  75. package/dist/types/slash-commands/builtin-registry.d.ts +14 -0
  76. package/dist/types/stt/setup.d.ts +2 -0
  77. package/dist/types/task/executor.d.ts +10 -2
  78. package/dist/types/tools/browser/runtime-diagnostics.d.ts +65 -0
  79. package/dist/types/tools/browser/tab-protocol.d.ts +4 -0
  80. package/dist/types/tools/browser/tab-supervisor.d.ts +2 -0
  81. package/dist/types/tools/browser/tab-worker.d.ts +2 -0
  82. package/dist/types/tools/browser.d.ts +2 -0
  83. package/dist/types/tools/image-gen.d.ts +14 -18
  84. package/dist/types/tools/index.d.ts +2 -0
  85. package/dist/types/tools/resource-gc.d.ts +2 -0
  86. package/dist/types/utils/pasted-image-path.d.ts +6 -0
  87. package/package.json +9 -9
  88. package/scripts/generate-sdk-operation-inventory.ts +8 -0
  89. package/scripts/run-test-manifest.ts +61 -21
  90. package/scripts/verify-gjc-sdk-canonicalization.ts +9 -4
  91. package/src/cli/args.ts +62 -9
  92. package/src/cli/completion-cli.ts +1 -0
  93. package/src/cli/fast-help.ts +1 -0
  94. package/src/cli/root-flags.ts +100 -0
  95. package/src/cli/setup-cli.ts +6 -2
  96. package/src/cli.ts +42 -65
  97. package/src/commands/acp.ts +1 -1
  98. package/src/commands/daemon.ts +2 -31
  99. package/src/commands/gc.ts +1 -1
  100. package/src/commands/launch.ts +8 -129
  101. package/src/commands/quick-lane.ts +51 -0
  102. package/src/commands/sdk.ts +200 -26
  103. package/src/commands/setup.ts +4 -2
  104. package/src/config/file-lock-gc.ts +4 -1
  105. package/src/config/model-equivalence.ts +76 -7
  106. package/src/config/model-profile-activation.ts +1115 -265
  107. package/src/config/model-profiles.ts +134 -2
  108. package/src/config/model-registry.ts +352 -50
  109. package/src/config/model-resolver.ts +289 -38
  110. package/src/config/models-config-schema.ts +11 -4
  111. package/src/config/provider-selection-policy.ts +146 -0
  112. package/src/config/settings-schema.ts +50 -77
  113. package/src/config/settings.ts +40 -0
  114. package/src/coordinator-mcp/server.ts +32 -2
  115. package/src/edit/renderer.ts +175 -223
  116. package/src/edit/streaming.ts +108 -1
  117. package/src/export/html/index.ts +57 -7
  118. package/src/extensibility/custom-tools/types.ts +2 -0
  119. package/src/extensibility/extensions/compact-handler.ts +3 -2
  120. package/src/extensibility/extensions/runner.ts +6 -0
  121. package/src/extensibility/extensions/types.ts +3 -0
  122. package/src/gjc-runtime/gc-runtime.ts +379 -20
  123. package/src/gjc-runtime/launch-worktree.ts +5 -1
  124. package/src/gjc-runtime/team-runtime.ts +59 -2
  125. package/src/gjc-runtime/tmux-owner-isolation.ts +19 -0
  126. package/src/gjc-runtime/ultragoal-receipt-freshness.ts +16 -1
  127. package/src/internal-urls/docs-index.generated.ts +17 -13
  128. package/src/internal-urls/local-protocol.ts +1 -0
  129. package/src/main.ts +40 -3
  130. package/src/memories/index.ts +4 -2
  131. package/src/modes/acp/acp-agent.ts +235 -91
  132. package/src/modes/components/model-selector.ts +206 -42
  133. package/src/modes/components/provider-order-context.ts +197 -0
  134. package/src/modes/components/provider-order-editor.ts +441 -0
  135. package/src/modes/components/settings-defs.ts +21 -1
  136. package/src/modes/components/settings-selector.ts +45 -2
  137. package/src/modes/components/tool-execution.ts +47 -15
  138. package/src/modes/controllers/command-controller.ts +6 -3
  139. package/src/modes/controllers/extension-ui-controller.ts +2 -0
  140. package/src/modes/controllers/input-controller.ts +73 -2
  141. package/src/modes/controllers/selector-controller.ts +260 -153
  142. package/src/modes/prompt-suggestion-controller.ts +1 -1
  143. package/src/modes/runtime-init.ts +1 -0
  144. package/src/modes/utils/ui-helpers.ts +18 -0
  145. package/src/prompts/tools/browser.md +1 -0
  146. package/src/quick-lane/classify.ts +257 -0
  147. package/src/rlm/index.ts +1 -1
  148. package/src/runtime/stt-settings-setup.ts +57 -0
  149. package/src/sdk/acp/adapter.ts +2 -1
  150. package/src/sdk/broker/broker.ts +780 -12
  151. package/src/sdk/broker/lifecycle.ts +70 -29
  152. package/src/sdk/broker/operation-receipt-ledger.ts +144 -0
  153. package/src/sdk/broker/session-index.ts +463 -48
  154. package/src/sdk/broker/state-version.ts +13 -9
  155. package/src/sdk/broker/transport.ts +14 -1
  156. package/src/sdk/bus/index.ts +34 -13
  157. package/src/sdk/bus/kind-aware-reconciliation.ts +21 -2
  158. package/src/sdk/bus/notification-service.ts +47 -1
  159. package/src/sdk/bus/prompt-reconciliation.ts +22 -3
  160. package/src/sdk/bus/telegram-daemon-contract.ts +4 -1
  161. package/src/sdk/bus/telegram-daemon.ts +54 -3
  162. package/src/sdk/cli/rows.ts +247 -0
  163. package/src/sdk/cli/session-cli.ts +930 -79
  164. package/src/sdk/elevation/allowlist.ts +54 -0
  165. package/src/sdk/elevation/audit.ts +208 -0
  166. package/src/sdk/elevation/capability.ts +41 -0
  167. package/src/sdk/elevation/digest.ts +129 -0
  168. package/src/sdk/elevation/dispatch-receipt.ts +184 -0
  169. package/src/sdk/elevation/grant-ledger.ts +1142 -0
  170. package/src/sdk/elevation/owner.ts +109 -0
  171. package/src/sdk/guides/cache.ts +363 -0
  172. package/src/sdk/guides/catalog.ts +554 -0
  173. package/src/sdk/guides/cli.ts +235 -0
  174. package/src/sdk/guides/manifest.ts +152 -0
  175. package/src/sdk/guides/verify.ts +178 -0
  176. package/src/sdk/host/control/dispatch.ts +49 -1
  177. package/src/sdk/host/control/index.ts +1 -0
  178. package/src/sdk/host/control/operations.ts +8 -1
  179. package/src/sdk/host/query/cursor.ts +47 -3
  180. package/src/sdk/host/query/handlers.ts +155 -2
  181. package/src/sdk/host/sdk-surface-parity.test.ts +1 -0
  182. package/src/sdk/host/session-runtime.test.ts +391 -1
  183. package/src/sdk/host/session-runtime.ts +308 -73
  184. package/src/sdk/host/surface-policy.ts +3 -0
  185. package/src/sdk/host/websocket-transport.ts +7 -1
  186. package/src/sdk/mcp/server.ts +88 -15
  187. package/src/sdk/prompt-status.ts +8 -0
  188. package/src/sdk/protocol/operation-inventory.generated.json +62 -0
  189. package/src/sdk/protocol/operation-registry.ts +3 -2
  190. package/src/sdk/session.ts +87 -39
  191. package/src/sdk/transport/index.ts +1 -0
  192. package/src/sdk/transport/relay.ts +3 -0
  193. package/src/sdk/transport/serve-cli.ts +111 -24
  194. package/src/secrets/obfuscator.ts +92 -12
  195. package/src/session/agent-session.ts +322 -93
  196. package/src/session/blob-store.ts +60 -8
  197. package/src/session/fallback-chain-controller.ts +35 -0
  198. package/src/session/internal/managed-session-scope.ts +25 -3
  199. package/src/session/internal/managed-session-storage.ts +574 -73
  200. package/src/session/internal/session-memory-sidecar.ts +19 -29
  201. package/src/session/session-manager.ts +2166 -743
  202. package/src/session/session-storage.ts +715 -48
  203. package/src/setup/provider-onboarding.ts +19 -3
  204. package/src/setup/provider-presets.json +3 -1
  205. package/src/slash-commands/builtin-registry.ts +85 -3
  206. package/src/stt/downloader.ts +2 -3
  207. package/src/stt/setup.ts +29 -4
  208. package/src/task/executor.ts +22 -18
  209. package/src/task/index.ts +24 -1
  210. package/src/tools/archive-reader.ts +11 -2
  211. package/src/tools/browser/runtime-diagnostics.ts +249 -0
  212. package/src/tools/browser/tab-protocol.ts +4 -0
  213. package/src/tools/browser/tab-supervisor.ts +5 -0
  214. package/src/tools/browser/tab-worker.ts +36 -1
  215. package/src/tools/browser.ts +7 -0
  216. package/src/tools/image-gen.ts +110 -166
  217. package/src/tools/index.ts +2 -0
  218. package/src/tools/read.ts +1 -1
  219. package/src/tools/resource-gc.ts +6 -1
  220. package/src/tools/tool-catalog.generated.ts +5 -1
  221. package/src/utils/pasted-image-path.ts +93 -0
  222. package/src/web/search/index.ts +3 -3
@@ -6,7 +6,6 @@ import type { Component } from "@gajae-code/tui";
6
6
  import { Text, visibleWidth, wrapTextWithAnsi } from "@gajae-code/tui";
7
7
  import { sanitizeText } from "@gajae-code/utils";
8
8
  import type { RenderResultOptions } from "../extensibility/custom-tools/types";
9
- import { HL_FILE_PREFIX } from "../hashline/hash";
10
9
  import type { FileDiagnosticsResult } from "../lsp";
11
10
  import { renderDiff as renderDiffColored } from "../modes/components/diff";
12
11
  import { getLanguageFromPath, type Theme } from "../modes/theme/theme";
@@ -30,9 +29,9 @@ import { fileHyperlink, Hasher, type RenderCache, renderStatusLine, truncateToWi
30
29
  import type { EditMode } from "../utils/edit-mode";
31
30
  import type { VimToolDetails } from "../vim/types";
32
31
  import type { DiffError, DiffResult } from "./diff";
33
- import { type ApplyPatchEntry, expandApplyPatchToEntries, expandApplyPatchToPreviewEntries } from "./modes/apply-patch";
32
+ import { expandApplyPatchToPreviewEntries } from "./modes/apply-patch";
34
33
  import type { Operation } from "./modes/patch";
35
- import type { PerFileDiffPreview } from "./streaming";
34
+ import { getEditRequestTargetInventory, orderedDistinctPaths, type PerFileDiffPreview } from "./streaming";
36
35
 
37
36
  // ═══════════════════════════════════════════════════════════════════════════
38
37
  // LSP Batching
@@ -90,7 +89,7 @@ export interface EditToolDetails {
90
89
  // TUI Renderer
91
90
  // ═══════════════════════════════════════════════════════════════════════════
92
91
 
93
- interface EditRenderArgs {
92
+ export interface EditRenderArgs {
94
93
  path?: string;
95
94
  file_path?: string;
96
95
  oldText?: string;
@@ -116,17 +115,9 @@ type EditRenderEntry = {
116
115
  rename?: string;
117
116
  move?: string;
118
117
  op?: Operation;
118
+ firstChangedLine?: number;
119
119
  };
120
120
 
121
- interface HashlineInputRenderSummary {
122
- entries: Array<{ path: string }>;
123
- }
124
-
125
- interface ApplyPatchRenderSummary {
126
- entries: ApplyPatchEntry[];
127
- error?: string;
128
- }
129
-
130
121
  function isVimRenderArgs(args: EditRenderArgs | VimRenderArgs): args is VimRenderArgs {
131
122
  return (
132
123
  typeof args === "object" &&
@@ -170,58 +161,22 @@ const EDIT_STREAMING_PREVIEW_LINES = 12;
170
161
  const CALL_TEXT_PREVIEW_LINES = 6;
171
162
  const CALL_TEXT_PREVIEW_WIDTH = 80;
172
163
 
173
- /** Extract file path from an edit entry. */
174
- function filePathFromEditEntry(p: string | undefined): string | undefined {
175
- return p ?? undefined;
176
- }
177
-
178
- function decodePartialJsonStringFragment(fragment: string): string {
179
- // Trim a trailing partial escape so JSON.parse sees a well-formed string.
180
- let text = fragment.replace(/\\u[0-9a-fA-F]{0,3}$/, "");
181
- const trailingBackslashes = text.match(/\\+$/)?.[0].length ?? 0;
182
- if (trailingBackslashes % 2 === 1) text = text.slice(0, -1);
183
- try {
184
- return JSON.parse(`"${text}"`) as string;
185
- } catch {
186
- // Streaming fragment isn't a valid JSON string yet; surface it raw rather
187
- // than ad-hoc unescaping that mishandles surrogates and partial escapes.
188
- return text;
189
- }
190
- }
191
-
192
- function extractPartialJsonString(partialJson: string | undefined, key: string): string | undefined {
193
- if (!partialJson) return undefined;
194
- const pattern = new RegExp(`"${key}"\\s*:\\s*"((?:\\\\.|[^"\\\\])*)`, "u");
195
- const match = pattern.exec(partialJson);
196
- if (!match) return undefined;
197
- return decodePartialJsonStringFragment(match[1]);
198
- }
199
-
200
- function getPartialJsonEditPath(args: EditRenderArgs): string | undefined {
201
- return filePathFromEditEntry(extractPartialJsonString(args.__partialJson, "path"));
202
- }
203
-
204
- /** Count distinct file paths in an edits array. */
205
- function countEditFiles(edits: EditRenderEntry[]): number {
206
- return new Set(edits.map(edit => filePathFromEditEntry(edit.path)).filter(Boolean)).size;
207
- }
208
-
209
- function countLines(text: string): number {
210
- if (!text) return 0;
211
- return text.split("\n").length;
212
- }
213
-
214
164
  function getOperationTitle(op: Operation | undefined): string {
215
165
  return op === "create" ? "Create" : op === "delete" ? "Delete" : "Edit";
216
166
  }
217
167
 
168
+ function sanitizeDisplayPath(path: string): string {
169
+ return replaceTabs(sanitizeText(path).replace(/[\r\n]/g, " "));
170
+ }
171
+
218
172
  function formatEditPathDisplay(
219
173
  rawPath: string,
220
174
  uiTheme: Theme,
221
175
  options?: { rename?: string; firstChangedLine?: number },
222
176
  ): string {
223
- let pathDisplay = rawPath
224
- ? fileHyperlink(rawPath, uiTheme.fg("accent", shortenPath(rawPath)))
177
+ const displayPath = sanitizeDisplayPath(rawPath);
178
+ let pathDisplay = displayPath
179
+ ? fileHyperlink(displayPath, uiTheme.fg("accent", shortenPath(displayPath)))
225
180
  : uiTheme.fg("toolOutput", "…");
226
181
 
227
182
  if (options?.firstChangedLine) {
@@ -229,7 +184,8 @@ function formatEditPathDisplay(
229
184
  }
230
185
 
231
186
  if (options?.rename) {
232
- pathDisplay += ` ${uiTheme.fg("dim", "→")} ${fileHyperlink(options.rename, uiTheme.fg("accent", shortenPath(options.rename)))}`;
187
+ const rename = sanitizeDisplayPath(options.rename);
188
+ pathDisplay += ` ${uiTheme.fg("dim", "→")} ${fileHyperlink(rename, uiTheme.fg("accent", shortenPath(rename)))}`;
233
189
  }
234
190
 
235
191
  return pathDisplay;
@@ -276,21 +232,13 @@ function formatStreamingDiff(diff: string, rawPath: string, uiTheme: Theme, labe
276
232
  return text;
277
233
  }
278
234
 
279
- function formatMetadataLine(lineCount: number | null, language: string | undefined, uiTheme: Theme): string {
280
- const icon = uiTheme.getLangIcon(language);
281
- if (lineCount !== null) {
282
- return uiTheme.fg("dim", `${icon} ${lineCount} lines`);
283
- }
284
- return uiTheme.fg("dim", `${icon}`);
285
- }
286
-
287
235
  function formatMultiFileStreamingDiff(previews: PerFileDiffPreview[], uiTheme: Theme): string {
288
236
  const parts: string[] = [];
289
237
  for (const preview of previews) {
290
238
  if (!preview.diff && !preview.error) continue;
291
- const header = uiTheme.fg("dim", `\n\n── ${shortenPath(preview.path)} ──`);
239
+ const header = uiTheme.fg("dim", `\n\n── ${shortenPath(sanitizeDisplayPath(preview.path))} ──`);
292
240
  if (preview.error) {
293
- parts.push(`${header}\n${uiTheme.fg("error", replaceTabs(preview.error, preview.path))}`);
241
+ parts.push(`${header}\n${uiTheme.fg("error", replaceTabs(sanitizeText(preview.error), preview.path))}`);
294
242
  continue;
295
243
  }
296
244
  if (preview.diff) {
@@ -328,75 +276,6 @@ function getCallPreview(
328
276
  return "";
329
277
  }
330
278
 
331
- const MISSING_APPLY_PATCH_END_ERROR = "The last line of the patch must be '*** End Patch'";
332
-
333
- function normalizeHashlineInputPreviewPath(rawPath: string): string {
334
- const trimmed = rawPath.trim();
335
- if (trimmed.length < 2) return trimmed;
336
- const first = trimmed[0];
337
- const last = trimmed[trimmed.length - 1];
338
- if ((first === '"' || first === "'") && first === last) {
339
- return trimmed.slice(1, -1);
340
- }
341
- return trimmed;
342
- }
343
-
344
- function parseHashlineInputPreviewHeader(line: string): string | null {
345
- if (!line.startsWith(HL_FILE_PREFIX)) return null;
346
- // Mirror hashline/input.ts: strip every leading file marker so canonical
347
- // `§ PATH` headers and stray `§§ PATH` / `§§§PATH` runs render clean paths.
348
- let prefixEnd = 0;
349
- while (prefixEnd < line.length && line[prefixEnd] === HL_FILE_PREFIX) prefixEnd++;
350
- const body = line.slice(prefixEnd).trim();
351
- const previewPath = normalizeHashlineInputPreviewPath(body);
352
- return previewPath.length > 0 ? previewPath : null;
353
- }
354
-
355
- function getHashlineInputPaths(input: string): string[] {
356
- const stripped = input.startsWith("\uFEFF") ? input.slice(1) : input;
357
- const paths: string[] = [];
358
- for (const rawLine of stripped.split("\n")) {
359
- const line = rawLine.replace(/\r$/, "");
360
- const path = parseHashlineInputPreviewHeader(line);
361
- if (path) paths.push(path);
362
- }
363
- return paths;
364
- }
365
-
366
- function getHashlineInputRenderSummary(
367
- args: EditRenderArgs,
368
- editMode: EditMode | undefined,
369
- ): HashlineInputRenderSummary | undefined {
370
- if (editMode !== "hashline" || typeof args.input !== "string") {
371
- return undefined;
372
- }
373
- return { entries: getHashlineInputPaths(args.input).map(path => ({ path })) };
374
- }
375
-
376
- function getApplyPatchRenderSummary(
377
- args: EditRenderArgs,
378
- isPartial: boolean,
379
- editMode: EditMode | undefined,
380
- ): ApplyPatchRenderSummary | undefined {
381
- if (editMode !== undefined && editMode !== "apply_patch") {
382
- return undefined;
383
- }
384
-
385
- if (typeof args.input !== "string") {
386
- return undefined;
387
- }
388
-
389
- try {
390
- return { entries: expandApplyPatchToEntries({ input: args.input }) };
391
- } catch (err) {
392
- const error = err instanceof Error ? err.message : String(err);
393
- if (isPartial && error === MISSING_APPLY_PATCH_END_ERROR) {
394
- return { entries: expandApplyPatchToPreviewEntries({ input: args.input }) };
395
- }
396
- return { entries: [], error };
397
- }
398
- }
399
-
400
279
  function renderDiffSection(
401
280
  diff: string,
402
281
  rawPath: string,
@@ -472,39 +351,26 @@ export const editToolRenderer = {
472
351
  }
473
352
 
474
353
  const editArgs = args as EditRenderArgs;
475
- const hashlineInputSummary = getHashlineInputRenderSummary(editArgs, renderContext?.editMode);
476
- const applyPatchSummary = getApplyPatchRenderSummary(editArgs, options.isPartial, renderContext?.editMode);
477
- const firstApplyPatchEntry = applyPatchSummary?.entries[0];
478
- const firstHashlineInputEntry = hashlineInputSummary?.entries[0];
479
- // Extract path from first edit entry when top-level path is absent (new schema)
354
+ const inventory = getEditRequestTargetInventory(editArgs, renderContext?.editMode, {
355
+ isPartial: options.isPartial,
356
+ });
480
357
  const firstEdit = Array.isArray(editArgs.edits) && editArgs.edits.length > 0 ? editArgs.edits[0] : undefined;
481
- const rawPath =
482
- editArgs.file_path ||
483
- editArgs.path ||
484
- filePathFromEditEntry(firstEdit?.path) ||
485
- getPartialJsonEditPath(editArgs) ||
486
- firstHashlineInputEntry?.path ||
487
- firstApplyPatchEntry?.path ||
488
- "";
489
- const rename = editArgs.rename || firstEdit?.rename || firstEdit?.move || firstApplyPatchEntry?.rename;
490
- const op = editArgs.op || firstEdit?.op || firstApplyPatchEntry?.op;
491
- const { description } = formatEditDescription(rawPath, uiTheme, { rename });
358
+ const rawPath = inventory.paths[0] ?? "";
359
+ const rename = editArgs.rename || firstEdit?.rename || firstEdit?.move;
360
+ const op = editArgs.op || firstEdit?.op || inventory.firstOp;
361
+ const { description } = formatEditDescription(rawPath, uiTheme, options.expanded ? { rename } : undefined);
492
362
  const spinner =
493
363
  options?.spinnerFrame !== undefined ? formatStatusIcon("running", uiTheme, options.spinnerFrame) : "";
494
364
  let text = `${formatTitle(getOperationTitle(op), uiTheme)} ${spinner ? `${spinner} ` : ""}${description}`;
495
- // Show file count hint for multi-file edits
496
- let fileCount = hashlineInputSummary?.entries.length ?? applyPatchSummary?.entries.length ?? 0;
497
- if (Array.isArray(editArgs.edits)) {
498
- fileCount = countEditFiles(editArgs.edits);
499
- }
500
- if (fileCount > 1) {
501
- text += uiTheme.fg("dim", ` (+${fileCount - 1} more)`);
365
+ if (inventory.paths.length > 1) {
366
+ text += uiTheme.fg("dim", ` (+${inventory.paths.length - 1} more)`);
502
367
  }
368
+ if (!options.expanded) return new Text(text, 0, 0);
369
+
503
370
  text += getCallPreview(editArgs, rawPath, uiTheme, renderContext);
504
- if (applyPatchSummary?.error) {
505
- text += `\n\n${uiTheme.fg("error", truncateToWidth(replaceTabs(applyPatchSummary.error, rawPath), CALL_TEXT_PREVIEW_WIDTH))}`;
371
+ if (inventory.parseError) {
372
+ text += `\n\n${uiTheme.fg("error", truncateToWidth(replaceTabs(sanitizeText(inventory.parseError), rawPath), CALL_TEXT_PREVIEW_WIDTH))}`;
506
373
  }
507
-
508
374
  return new Text(text, 0, 0);
509
375
  },
510
376
 
@@ -523,14 +389,91 @@ export const editToolRenderer = {
523
389
  }
524
390
 
525
391
  const perFileResults = result.details?.perFileResults;
526
- const totalFiles = args?.edits ? countEditFiles(args.edits) : 0;
527
- if (perFileResults && (perFileResults.length > 1 || totalFiles > 1)) {
528
- return renderMultiFileResult(perFileResults, totalFiles, options, uiTheme);
392
+ const inventory = getEditRequestTargetInventory(args, options.renderContext?.editMode, {
393
+ isPartial: options.isPartial,
394
+ });
395
+ const representedPaths = orderedDistinctPaths(perFileResults?.map(file => file.path) ?? []);
396
+ if (perFileResults && (perFileResults.length > 1 || inventory.paths.length > 1)) {
397
+ return renderMultiFileResult(
398
+ perFileResults,
399
+ inventory.paths.length,
400
+ representedPaths.length,
401
+ options,
402
+ uiTheme,
403
+ args,
404
+ );
529
405
  }
530
406
  return renderSingleFileResult(result, options, uiTheme, args);
531
407
  },
532
408
  };
533
409
 
410
+ function resolveCompletedEditIdentity(
411
+ details: EditToolDetails | EditToolPerFileResult | undefined,
412
+ args: EditRenderArgs | undefined,
413
+ editMode: EditMode | undefined,
414
+ isPartial: boolean,
415
+ ): { path: string; op: Operation | undefined; move: string | undefined; firstChangedLine: number | undefined } {
416
+ const firstEdit = args?.edits?.[0];
417
+ const inventory = getEditRequestTargetInventory(args, editMode, { isPartial });
418
+ const detailsPath = details && "path" in details ? details.path : undefined;
419
+ return {
420
+ path: detailsPath ?? args?.file_path ?? args?.path ?? firstEdit?.path ?? inventory.paths[0] ?? "",
421
+ op: details?.op ?? args?.op ?? firstEdit?.op,
422
+ move: details?.move ?? args?.rename ?? firstEdit?.rename ?? firstEdit?.move,
423
+ firstChangedLine: details?.firstChangedLine ?? firstEdit?.firstChangedLine,
424
+ };
425
+ }
426
+
427
+ export function getPerFileEditRenderArgs(
428
+ args: EditRenderArgs | undefined,
429
+ path: string,
430
+ editMode: EditMode | undefined,
431
+ ): EditRenderArgs | undefined {
432
+ if (!args) return undefined;
433
+ const matchingEdit = args.edits?.find(edit => edit.path === path);
434
+ if (matchingEdit) {
435
+ return {
436
+ ...args,
437
+ path: matchingEdit.path,
438
+ op: matchingEdit.op,
439
+ rename: matchingEdit.rename ?? matchingEdit.move,
440
+ edits: [matchingEdit],
441
+ };
442
+ }
443
+ if (args.path === path || args.file_path === path) return args;
444
+ if (editMode === "apply_patch" && typeof args.input === "string") {
445
+ try {
446
+ const entry = expandApplyPatchToPreviewEntries({ input: args.input }).find(
447
+ candidate => candidate.path === path,
448
+ );
449
+ if (entry) return { path: entry.path, op: entry.op, rename: entry.rename, edits: [entry] };
450
+ } catch {
451
+ // Malformed free-form input has no safe per-file request metadata fallback.
452
+ }
453
+ }
454
+ return undefined;
455
+ }
456
+
457
+ export function getPerFileEditRenderContext(
458
+ renderContext: EditRenderContext | undefined,
459
+ path: string,
460
+ ): EditRenderContext | undefined {
461
+ if (!renderContext) return undefined;
462
+ const matchingPreview = renderContext.perFileDiffPreview?.find(preview => preview.path === path);
463
+ const editDiffPreview = matchingPreview?.error
464
+ ? { error: matchingPreview.error }
465
+ : matchingPreview
466
+ ? { diff: matchingPreview.diff ?? "", firstChangedLine: matchingPreview.firstChangedLine }
467
+ : undefined;
468
+ return { ...renderContext, editDiffPreview };
469
+ }
470
+
471
+ function hasDiagnostics(
472
+ details: EditToolDetails | EditToolPerFileResult | undefined,
473
+ ): details is (EditToolDetails | EditToolPerFileResult) & { diagnostics: FileDiagnosticsResult } {
474
+ return details !== undefined && details.diagnostics !== undefined;
475
+ }
476
+
534
477
  function renderSingleFileResult(
535
478
  result: {
536
479
  content: Array<{ type: string; text?: string }>;
@@ -543,75 +486,74 @@ function renderSingleFileResult(
543
486
  ): Component {
544
487
  const details = result.details;
545
488
  const isError = result.isError ?? (details && "isError" in details ? details.isError : false);
546
- const firstEdit = args?.edits?.[0];
547
- const hashlineInputSummary = getHashlineInputRenderSummary(args ?? {}, options.renderContext?.editMode);
548
- const firstHashlineInputEntry = hashlineInputSummary?.entries[0];
549
- const rawPath =
550
- args?.file_path ||
551
- args?.path ||
552
- filePathFromEditEntry(firstEdit?.path) ||
553
- (details && "path" in details ? details.path : "") ||
554
- firstHashlineInputEntry?.path ||
555
- "";
556
- const op = args?.op || firstEdit?.op || details?.op;
557
- const rename = args?.rename || firstEdit?.rename || firstEdit?.move || details?.move;
558
- const { language } = formatEditDescription(rawPath, uiTheme, { rename });
559
-
560
- const editTextSource = args?.newText ?? args?.oldText ?? args?.diff ?? args?.patch;
561
- const metadataLineCount = editTextSource ? countLines(editTextSource) : null;
562
- const metadataLine = op !== "delete" ? `\n${formatMetadataLine(metadataLineCount, language, uiTheme)}` : "";
563
-
564
489
  const displayErrorText = isError && details && "displayErrorText" in details ? details.displayErrorText : undefined;
565
490
  const errorText = isError
566
491
  ? displayErrorText ||
567
492
  (details && "errorText" in details && details.errorText) ||
568
493
  (result.content?.find(c => c.type === "text")?.text ?? "")
569
494
  : "";
570
-
495
+ const baseIdentity = resolveCompletedEditIdentity(details, args, options.renderContext?.editMode, options.isPartial);
571
496
  let cached: RenderCache | undefined;
572
497
 
573
498
  return {
574
499
  render(width) {
575
500
  const { expanded, renderContext } = options;
576
- const editDiffPreview = renderContext?.editDiffPreview;
577
- const renderDiffFn = renderContext?.renderDiff ?? ((t: string) => t);
578
- const key = new Hasher().bool(expanded).u32(width).digest();
579
- if (cached?.key === key) return cached.lines;
580
-
581
- const firstChangedLine =
582
- (editDiffPreview && "firstChangedLine" in editDiffPreview ? editDiffPreview.firstChangedLine : undefined) ||
583
- (details && !isError ? details.firstChangedLine : undefined);
584
- const { description } = formatEditDescription(rawPath, uiTheme, { rename, firstChangedLine });
585
-
501
+ const preview = renderContext?.editDiffPreview;
502
+ const previewLine = preview && "firstChangedLine" in preview ? preview.firstChangedLine : undefined;
503
+ const identity = {
504
+ ...baseIdentity,
505
+ firstChangedLine: baseIdentity.firstChangedLine ?? previewLine,
506
+ };
507
+ const { description } = formatEditDescription(identity.path, uiTheme, {
508
+ rename: identity.move,
509
+ firstChangedLine: identity.firstChangedLine,
510
+ });
586
511
  const header = renderStatusLine(
587
- {
588
- icon: isError ? "error" : "success",
589
- title: getOperationTitle(op),
590
- description,
591
- },
512
+ { icon: isError ? "error" : "success", title: getOperationTitle(identity.op), description },
592
513
  uiTheme,
593
514
  );
594
- let text = header;
595
- text += metadataLine;
515
+ const key = new Hasher().bool(expanded).u32(width).str(header).digest();
516
+ if (cached?.key === key) return cached.lines;
596
517
 
597
- if (isError) {
598
- if (errorText) {
599
- text += `\n\n${uiTheme.fg("error", replaceTabs(errorText, rawPath))}`;
518
+ let text = header;
519
+ if (!expanded) {
520
+ if (isError) {
521
+ const cause = sanitizeText(errorText)
522
+ .split("\n")
523
+ .map(line => line.trim())
524
+ .find(Boolean);
525
+ if (cause) {
526
+ const availableWidth = Math.max(1, Math.min(80, width > 0 ? width : 80));
527
+ text += `\n${uiTheme.fg("error", truncateToWidth(replaceTabs(cause, identity.path), availableWidth))}`;
528
+ }
529
+ } else if (details?.diff) {
530
+ const stats = getDiffStats(details.diff);
531
+ text += `\n${uiTheme.fg("dim", uiTheme.format.bracketLeft)}${formatDiffStats(
532
+ stats.added,
533
+ stats.removed,
534
+ stats.hunks,
535
+ uiTheme,
536
+ )}${uiTheme.fg("dim", uiTheme.format.bracketRight)}`;
600
537
  }
601
- } else if (details?.diff) {
602
- text += renderDiffSection(details.diff, rawPath, expanded, uiTheme, renderDiffFn);
603
- } else if (editDiffPreview) {
604
- if ("error" in editDiffPreview) {
605
- text += `\n\n${uiTheme.fg("error", replaceTabs(editDiffPreview.error, rawPath))}`;
606
- } else if (editDiffPreview.diff) {
607
- text += renderDiffSection(editDiffPreview.diff, rawPath, expanded, uiTheme, renderDiffFn);
538
+ if (hasDiagnostics(details)) text += `\n${uiTheme.fg("dim", "Diagnostics available")}`;
539
+ } else {
540
+ const renderDiffFn = renderContext?.renderDiff ?? ((diff: string) => diff);
541
+ if (isError) {
542
+ if (errorText) text += `\n\n${uiTheme.fg("error", replaceTabs(sanitizeText(errorText), identity.path))}`;
543
+ } else if (details?.diff) {
544
+ text += renderDiffSection(details.diff, identity.path, true, uiTheme, renderDiffFn);
545
+ } else if (preview) {
546
+ if ("error" in preview) {
547
+ text += `\n\n${uiTheme.fg("error", replaceTabs(sanitizeText(preview.error), identity.path))}`;
548
+ } else if (preview.diff) {
549
+ text += renderDiffSection(preview.diff, identity.path, true, uiTheme, renderDiffFn);
550
+ }
551
+ }
552
+ if (hasDiagnostics(details)) {
553
+ text += formatDiagnostics(details.diagnostics, true, uiTheme, (fp: string) =>
554
+ uiTheme.getLangIcon(getLanguageFromPath(fp)),
555
+ );
608
556
  }
609
- }
610
-
611
- if (details?.diagnostics) {
612
- text += formatDiagnostics(details.diagnostics, expanded, uiTheme, (fp: string) =>
613
- uiTheme.getLangIcon(getLanguageFromPath(fp)),
614
- );
615
557
  }
616
558
 
617
559
  const lines =
@@ -628,13 +570,25 @@ function renderSingleFileResult(
628
570
  function renderMultiFileResult(
629
571
  perFileResults: EditToolPerFileResult[],
630
572
  totalFiles: number,
573
+ representedFiles: number,
631
574
  options: RenderResultOptions & { renderContext?: EditRenderContext },
632
575
  uiTheme: Theme,
576
+ args: EditRenderArgs | undefined,
633
577
  ): Component {
634
- const fileComponents = perFileResults.map(fileResult =>
635
- renderSingleFileResult({ content: [], details: fileResult, isError: fileResult.isError }, options, uiTheme),
636
- );
637
- const remaining = Math.max(0, totalFiles - perFileResults.length);
578
+ const fileComponents = perFileResults.map(fileResult => {
579
+ const fileOptions: RenderResultOptions & { renderContext?: EditRenderContext } = {
580
+ ...options,
581
+ renderContext: getPerFileEditRenderContext(options.renderContext, fileResult.path),
582
+ };
583
+ const fileArgs = getPerFileEditRenderArgs(args, fileResult.path, options.renderContext?.editMode);
584
+ return renderSingleFileResult(
585
+ { content: [], details: fileResult, isError: fileResult.isError },
586
+ fileOptions,
587
+ uiTheme,
588
+ fileArgs,
589
+ );
590
+ });
591
+ const remaining = options.isPartial ? Math.max(0, totalFiles - representedFiles) : 0;
638
592
 
639
593
  let cached: RenderCache | undefined;
640
594
 
@@ -651,7 +605,6 @@ function renderMultiFileResult(
651
605
  allLines.push(...fileComponents[i].render(width));
652
606
  }
653
607
 
654
- // Show pending indicator for files still being processed
655
608
  if (remaining > 0) {
656
609
  if (allLines.length > 0) allLines.push("");
657
610
  const spinnerFrame = options.spinnerFrame;
@@ -667,7 +620,6 @@ function renderMultiFileResult(
667
620
  ),
668
621
  );
669
622
  if (spinner) {
670
- // Replace the pending icon with spinner on the last line
671
623
  allLines[allLines.length - 1] = allLines[allLines.length - 1].replace(/^(?:\x1b\[[^m]*m)*./u, spinner);
672
624
  }
673
625
  }
@@ -31,7 +31,7 @@ import { replaceTabs, truncateToWidth } from "../tools/render-utils";
31
31
  import { type EditMode, resolveEditMode } from "../utils/edit-mode";
32
32
  import { computeEditDiff, type DiffError, type DiffResult } from "./diff";
33
33
  import { type ApplyPatchEntry, expandApplyPatchToEntries, expandApplyPatchToPreviewEntries } from "./modes/apply-patch";
34
- import { computePatchDiff, type PatchEditEntry } from "./modes/patch";
34
+ import { computePatchDiff, type Operation, type PatchEditEntry } from "./modes/patch";
35
35
  import type { ReplaceEditEntry } from "./modes/replace";
36
36
 
37
37
  export interface PerFileDiffPreview {
@@ -74,6 +74,113 @@ export interface EditStreamingStrategy<Args = unknown> {
74
74
  */
75
75
  renderStreamingFallback(args: Args, uiTheme: Theme): string;
76
76
  }
77
+ export interface EditRequestTargetInventory {
78
+ paths: string[];
79
+ /** Operation of the first target (Create/Delete) when derivable from the
80
+ * request payload (apply_patch/hashline free-form envelopes carry op on the
81
+ * parsed entry, not on the top-level args). */
82
+ firstOp?: Operation;
83
+ parseError?: string;
84
+ }
85
+ const MISSING_APPLY_PATCH_END_ERROR = `The last line of the patch must be '${END_PATCH_MARKER}'`;
86
+
87
+ export function orderedDistinctPaths(paths: readonly (string | undefined)[]): string[] {
88
+ const seen = new Set<string>();
89
+ const distinct: string[] = [];
90
+ for (const path of paths) {
91
+ if (!path || seen.has(path)) continue;
92
+ seen.add(path);
93
+ distinct.push(path);
94
+ }
95
+ return distinct;
96
+ }
97
+
98
+ function decodePartialJsonStringFragment(fragment: string): string {
99
+ let text = fragment.replace(/\\u[0-9a-fA-F]{0,3}$/, "");
100
+ const trailingBackslashes = text.match(/\\+$/)?.[0].length ?? 0;
101
+ if (trailingBackslashes % 2 === 1) text = text.slice(0, -1);
102
+ try {
103
+ return JSON.parse(`"${text}"`) as string;
104
+ } catch {
105
+ return text;
106
+ }
107
+ }
108
+
109
+ function extractTrailingPartialJsonPath(partialJson: string | undefined): string | undefined {
110
+ if (!partialJson) return undefined;
111
+ const pattern = /"(?:file_path|path)"\s*:\s*"((?:\\.|[^"\\])*)/gu;
112
+ let trailingPath: string | undefined;
113
+ for (const match of partialJson.matchAll(pattern)) {
114
+ trailingPath = decodePartialJsonStringFragment(match[1]);
115
+ }
116
+ return trailingPath;
117
+ }
118
+
119
+ function normalizeHashlineTargetPath(rawPath: string): string {
120
+ const trimmed = rawPath.trim();
121
+ if (trimmed.length < 2) return trimmed;
122
+ const first = trimmed[0];
123
+ const last = trimmed[trimmed.length - 1];
124
+ return (first === '"' || first === "'") && first === last ? trimmed.slice(1, -1) : trimmed;
125
+ }
126
+
127
+ function getHashlineTargetPaths(input: string): string[] {
128
+ const stripped = input.startsWith("\uFEFF") ? input.slice(1) : input;
129
+ const paths: string[] = [];
130
+ for (const rawLine of stripped.split("\n")) {
131
+ const trimmed = rawLine.replace(/\r$/, "").trimEnd();
132
+ if (trimmed === END_PATCH_MARKER || trimmed === ABORT_MARKER) break;
133
+ if (isHashlineHeaderLine(trimmed)) paths.push(normalizeHashlineTargetPath(parseHashlineHeaderPath(trimmed)));
134
+ }
135
+ return paths.filter(Boolean);
136
+ }
137
+
138
+ export function getEditRequestTargetInventory(
139
+ args: unknown,
140
+ editMode: EditMode | undefined,
141
+ options: { isPartial: boolean },
142
+ ): EditRequestTargetInventory {
143
+ if (editMode === "vim") return { paths: [] };
144
+ const values = args && typeof args === "object" ? (args as Record<string, unknown>) : {};
145
+ const partialJson = typeof values.__partialJson === "string" ? values.__partialJson : undefined;
146
+ const topLevelPath =
147
+ typeof values.file_path === "string"
148
+ ? values.file_path
149
+ : typeof values.path === "string"
150
+ ? values.path
151
+ : undefined;
152
+ const edits = Array.isArray(values.edits) ? values.edits : [];
153
+
154
+ if (editMode === "hashline") {
155
+ const input = typeof values.input === "string" ? values.input : "";
156
+ const headerPaths = getHashlineTargetPaths(input);
157
+ return { paths: orderedDistinctPaths(headerPaths.length > 0 ? headerPaths : [topLevelPath]) };
158
+ }
159
+
160
+ if (editMode === "apply_patch") {
161
+ const input = typeof values.input === "string" ? values.input : "";
162
+ try {
163
+ const entries = expandApplyPatchToEntries({ input });
164
+ return { paths: orderedDistinctPaths(entries.map(entry => entry.path)), firstOp: entries[0]?.op };
165
+ } catch (err) {
166
+ const parseError = err instanceof Error ? err.message : String(err);
167
+ const previewEntries = expandApplyPatchToPreviewEntries({ input });
168
+ const paths = orderedDistinctPaths(previewEntries.map(entry => entry.path));
169
+ if (options.isPartial && parseError === MISSING_APPLY_PATCH_END_ERROR) {
170
+ return { paths, firstOp: previewEntries[0]?.op };
171
+ }
172
+ return { paths, firstOp: previewEntries[0]?.op, parseError };
173
+ }
174
+ }
175
+
176
+ const editPaths = edits.map(edit => {
177
+ if (!edit || typeof edit !== "object") return undefined;
178
+ const entry = edit as Record<string, unknown>;
179
+ return typeof entry.path === "string" ? entry.path : undefined;
180
+ });
181
+ const partialPath = options.isPartial ? extractTrailingPartialJsonPath(partialJson) : undefined;
182
+ return { paths: orderedDistinctPaths([topLevelPath, ...editPaths, partialPath]) };
183
+ }
77
184
 
78
185
  const STREAMING_FALLBACK_LINES = 12;
79
186
  const STREAMING_FALLBACK_WIDTH = 80;