@nghyane/arcane 0.1.13 → 0.1.15

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 (303) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/package.json +21 -70
  3. package/scripts/format-prompts.ts +1 -3
  4. package/src/cli/args.ts +2 -7
  5. package/src/cli/config-cli.ts +1 -1
  6. package/src/cli/plugin-cli.ts +1 -1
  7. package/src/cli/setup-cli.ts +1 -1
  8. package/src/cli/update-cli.ts +1 -1
  9. package/src/cli/web-search-cli.ts +1 -1
  10. package/src/cli.ts +0 -1
  11. package/src/commands/config.ts +1 -1
  12. package/src/commands/grep.ts +1 -1
  13. package/src/commands/jupyter.ts +1 -1
  14. package/src/commands/plugin.ts +1 -1
  15. package/src/commands/setup.ts +1 -1
  16. package/src/commands/shell.ts +1 -1
  17. package/src/commands/ssh.ts +1 -1
  18. package/src/commands/stats.ts +1 -1
  19. package/src/commands/update.ts +1 -1
  20. package/src/config/model-registry.ts +3 -4
  21. package/src/config/model-resolver.ts +36 -9
  22. package/src/config/prompt-templates.ts +1 -9
  23. package/src/config/settings-schema.ts +32 -88
  24. package/src/config/settings.ts +3 -4
  25. package/src/debug/index.ts +1 -1
  26. package/src/debug/log-formatting.ts +1 -1
  27. package/src/debug/log-viewer.ts +2 -2
  28. package/src/discovery/helpers.ts +13 -3
  29. package/src/exa/index.ts +1 -35
  30. package/src/exa/render.ts +30 -190
  31. package/src/export/html/index.ts +1 -1
  32. package/src/extensibility/custom-tools/loader.ts +1 -1
  33. package/src/extensibility/custom-tools/types.ts +5 -1
  34. package/src/extensibility/custom-tools/wrapper.ts +1 -1
  35. package/src/extensibility/extensions/runner.ts +1 -1
  36. package/src/extensibility/extensions/types.ts +1 -1
  37. package/src/extensibility/extensions/wrapper.ts +7 -15
  38. package/src/extensibility/hooks/runner.ts +1 -1
  39. package/src/extensibility/hooks/types.ts +1 -1
  40. package/src/extensibility/plugins/doctor.ts +1 -1
  41. package/src/index.ts +13 -13
  42. package/src/lsp/index.ts +77 -24
  43. package/src/lsp/render.ts +34 -583
  44. package/src/lsp/types.ts +3 -3
  45. package/src/lsp/utils.ts +1 -1
  46. package/src/main.ts +1 -1
  47. package/src/mcp/tool-bridge.ts +1 -24
  48. package/src/modes/components/assistant-message.ts +7 -7
  49. package/src/modes/components/bash-execution.ts +50 -112
  50. package/src/modes/components/bordered-loader.ts +1 -1
  51. package/src/modes/components/branch-summary-message.ts +16 -10
  52. package/src/modes/components/compaction-summary-message.ts +20 -12
  53. package/src/modes/components/context-group.ts +106 -0
  54. package/src/modes/components/custom-message.ts +4 -5
  55. package/src/modes/components/diff.ts +2 -2
  56. package/src/modes/components/dynamic-border.ts +1 -1
  57. package/src/modes/components/extensions/extension-dashboard.ts +1 -1
  58. package/src/modes/components/extensions/extension-list.ts +1 -1
  59. package/src/modes/components/extensions/inspector-panel.ts +1 -1
  60. package/src/modes/components/footer.ts +2 -2
  61. package/src/modes/components/history-search.ts +1 -1
  62. package/src/modes/components/hook-editor.ts +1 -1
  63. package/src/modes/components/hook-input.ts +1 -1
  64. package/src/modes/components/hook-message.ts +4 -5
  65. package/src/modes/components/hook-selector.ts +1 -1
  66. package/src/modes/components/index.ts +0 -2
  67. package/src/modes/components/keybinding-hints.ts +1 -1
  68. package/src/modes/components/login-dialog.ts +1 -1
  69. package/src/modes/components/mcp-add-wizard.ts +1 -1
  70. package/src/modes/components/model-selector.ts +1 -1
  71. package/src/modes/components/oauth-selector.ts +1 -1
  72. package/src/modes/components/plugin-settings.ts +1 -1
  73. package/src/modes/components/python-execution.ts +51 -91
  74. package/src/modes/components/queue-mode-selector.ts +1 -1
  75. package/src/modes/components/session-selector.ts +1 -1
  76. package/src/modes/components/settings-defs.ts +5 -10
  77. package/src/modes/components/settings-selector.ts +1 -1
  78. package/src/modes/components/show-images-selector.ts +1 -1
  79. package/src/modes/components/skill-message.ts +4 -4
  80. package/src/modes/components/status-line/segments.ts +2 -2
  81. package/src/modes/components/status-line/separators.ts +1 -1
  82. package/src/modes/components/status-line-segment-editor.ts +1 -1
  83. package/src/modes/components/status-line.ts +1 -1
  84. package/src/modes/components/theme-selector.ts +1 -1
  85. package/src/modes/components/thinking-selector.ts +1 -1
  86. package/src/modes/components/todo-display.ts +2 -4
  87. package/src/modes/components/todo-reminder.ts +4 -4
  88. package/src/modes/components/tool-execution.ts +118 -440
  89. package/src/modes/components/tool-image-display.ts +107 -0
  90. package/src/modes/components/tree-selector.ts +2 -2
  91. package/src/modes/components/ttsr-notification.ts +4 -17
  92. package/src/modes/components/user-message-selector.ts +1 -1
  93. package/src/modes/components/user-message.ts +9 -10
  94. package/src/modes/components/welcome.ts +1 -1
  95. package/src/modes/controllers/command-controller.ts +1 -1
  96. package/src/modes/controllers/event-controller.ts +58 -187
  97. package/src/modes/controllers/extension-ui-controller.ts +1 -1
  98. package/src/modes/controllers/input-controller.ts +3 -1
  99. package/src/modes/controllers/mcp-command-controller.ts +1 -1
  100. package/src/modes/controllers/selector-controller.ts +3 -26
  101. package/src/modes/controllers/ssh-command-controller.ts +1 -1
  102. package/src/modes/interactive-mode.ts +3 -7
  103. package/src/modes/print-mode.ts +5 -5
  104. package/src/modes/rpc/rpc-mode.ts +1 -1
  105. package/src/modes/types.ts +1 -2
  106. package/src/modes/utils/ui-helpers.ts +34 -32
  107. package/src/patch/edit-tool.ts +742 -0
  108. package/src/patch/index.ts +32 -898
  109. package/src/patch/schemas.ts +208 -0
  110. package/src/patch/shared.ts +83 -151
  111. package/src/prompts/agents/explore.md +22 -37
  112. package/src/prompts/agents/init.md +1 -1
  113. package/src/prompts/agents/librarian.md +29 -20
  114. package/src/prompts/agents/oracle.md +9 -2
  115. package/src/prompts/agents/reviewer.md +14 -48
  116. package/src/prompts/agents/task.md +16 -8
  117. package/src/prompts/compaction/branch-summary.md +4 -1
  118. package/src/prompts/compaction/compaction-summary.md +4 -1
  119. package/src/prompts/system/subagent-system-prompt.md +1 -1
  120. package/src/prompts/system/system-prompt.md +162 -178
  121. package/src/prompts/system/verification-reminder.md +6 -0
  122. package/src/sdk.ts +0 -9
  123. package/src/session/agent-session.ts +244 -1459
  124. package/src/session/model-controller.ts +406 -0
  125. package/src/session/retry-utils.ts +71 -0
  126. package/src/session/session-manager.ts +22 -186
  127. package/src/session/session-types.ts +312 -0
  128. package/src/session/stats.ts +387 -0
  129. package/src/session/streaming-edit.ts +258 -0
  130. package/src/session/ttsr.ts +213 -0
  131. package/src/slash-commands/builtin-registry.ts +0 -8
  132. package/src/stt/recorder.ts +2 -2
  133. package/src/system-prompt.ts +1 -14
  134. package/src/task/agents.ts +7 -33
  135. package/src/task/executor.ts +50 -438
  136. package/src/task/index.ts +104 -71
  137. package/src/task/progress-tracker.ts +390 -0
  138. package/src/task/render.ts +371 -187
  139. package/src/task/subprocess-tool-registry.ts +1 -1
  140. package/src/task/types.ts +14 -47
  141. package/src/tools/ask.ts +31 -42
  142. package/src/tools/bash-interactive.ts +2 -2
  143. package/src/tools/bash-interceptor.ts +2 -2
  144. package/src/tools/bash-normalize.ts +1 -1
  145. package/src/tools/bash-skill-urls.ts +2 -2
  146. package/src/tools/bash.ts +87 -136
  147. package/src/tools/browser.ts +54 -84
  148. package/src/tools/create-tools.ts +186 -0
  149. package/src/tools/default-renderer.ts +104 -0
  150. package/src/tools/explore.ts +11 -10
  151. package/src/tools/fetch.ts +24 -114
  152. package/src/tools/find.ts +48 -132
  153. package/src/tools/gemini-image.ts +5 -15
  154. package/src/tools/github.ts +450 -0
  155. package/src/tools/grep.ts +43 -179
  156. package/src/tools/index.ts +35 -198
  157. package/src/tools/json-tree.ts +3 -3
  158. package/src/tools/librarian.ts +18 -18
  159. package/src/tools/list-limit.ts +2 -2
  160. package/src/tools/notebook.ts +35 -87
  161. package/src/tools/oracle.ts +25 -25
  162. package/src/tools/output-meta.ts +89 -4
  163. package/src/tools/output-utils.ts +2 -2
  164. package/src/tools/python.ts +86 -637
  165. package/src/tools/read.ts +36 -119
  166. package/src/tools/reviewer-tool.ts +19 -21
  167. package/src/tools/search-code.ts +128 -0
  168. package/src/tools/ssh.ts +67 -126
  169. package/src/tools/subagent-tool.ts +197 -123
  170. package/src/tools/todo-write.ts +15 -31
  171. package/src/tools/tool-errors.ts +0 -30
  172. package/src/tools/undo-edit.ts +30 -67
  173. package/src/tools/write.ts +78 -127
  174. package/src/tui/code-cell.ts +4 -4
  175. package/src/tui/file-list.ts +2 -2
  176. package/src/tui/output-block.ts +1 -1
  177. package/src/tui/status-line.ts +1 -1
  178. package/src/tui/tree-list.ts +2 -2
  179. package/src/tui/types.ts +1 -1
  180. package/src/tui/utils.ts +1 -1
  181. package/src/{tools → ui}/render-utils.ts +87 -126
  182. package/src/utils/external-editor.ts +4 -4
  183. package/src/utils/file-mentions.ts +1 -1
  184. package/src/utils/index.ts +30 -0
  185. package/src/utils/tools-manager.ts +9 -19
  186. package/src/web/github-client.ts +290 -0
  187. package/src/web/scrapers/github.ts +11 -62
  188. package/src/web/search/auth.ts +1 -3
  189. package/src/web/search/index.ts +82 -46
  190. package/src/web/search/provider.ts +11 -16
  191. package/src/web/search/providers/grep.ts +160 -0
  192. package/src/web/search/render.ts +48 -235
  193. package/src/web/search/types.ts +1 -1
  194. package/src/commands/commit.ts +0 -36
  195. package/src/commit/agentic/agent.ts +0 -311
  196. package/src/commit/agentic/fallback.ts +0 -96
  197. package/src/commit/agentic/index.ts +0 -359
  198. package/src/commit/agentic/prompts/analyze-file.md +0 -22
  199. package/src/commit/agentic/prompts/session-user.md +0 -25
  200. package/src/commit/agentic/prompts/split-confirm.md +0 -1
  201. package/src/commit/agentic/prompts/system.md +0 -38
  202. package/src/commit/agentic/state.ts +0 -69
  203. package/src/commit/agentic/tools/analyze-file.ts +0 -118
  204. package/src/commit/agentic/tools/git-file-diff.ts +0 -194
  205. package/src/commit/agentic/tools/git-hunk.ts +0 -50
  206. package/src/commit/agentic/tools/git-overview.ts +0 -84
  207. package/src/commit/agentic/tools/index.ts +0 -56
  208. package/src/commit/agentic/tools/propose-changelog.ts +0 -128
  209. package/src/commit/agentic/tools/propose-commit.ts +0 -154
  210. package/src/commit/agentic/tools/recent-commits.ts +0 -81
  211. package/src/commit/agentic/tools/split-commit.ts +0 -280
  212. package/src/commit/agentic/topo-sort.ts +0 -44
  213. package/src/commit/agentic/trivial.ts +0 -51
  214. package/src/commit/agentic/validation.ts +0 -200
  215. package/src/commit/analysis/conventional.ts +0 -165
  216. package/src/commit/analysis/index.ts +0 -4
  217. package/src/commit/analysis/scope.ts +0 -242
  218. package/src/commit/analysis/summary.ts +0 -112
  219. package/src/commit/analysis/validation.ts +0 -66
  220. package/src/commit/changelog/detect.ts +0 -37
  221. package/src/commit/changelog/generate.ts +0 -110
  222. package/src/commit/changelog/index.ts +0 -234
  223. package/src/commit/changelog/parse.ts +0 -44
  224. package/src/commit/cli.ts +0 -93
  225. package/src/commit/git/diff.ts +0 -148
  226. package/src/commit/git/errors.ts +0 -9
  227. package/src/commit/git/index.ts +0 -211
  228. package/src/commit/git/operations.ts +0 -54
  229. package/src/commit/index.ts +0 -5
  230. package/src/commit/map-reduce/index.ts +0 -64
  231. package/src/commit/map-reduce/map-phase.ts +0 -178
  232. package/src/commit/map-reduce/reduce-phase.ts +0 -145
  233. package/src/commit/map-reduce/utils.ts +0 -9
  234. package/src/commit/message.ts +0 -11
  235. package/src/commit/model-selection.ts +0 -69
  236. package/src/commit/pipeline.ts +0 -243
  237. package/src/commit/prompts/analysis-system.md +0 -148
  238. package/src/commit/prompts/analysis-user.md +0 -38
  239. package/src/commit/prompts/changelog-system.md +0 -50
  240. package/src/commit/prompts/changelog-user.md +0 -18
  241. package/src/commit/prompts/file-observer-system.md +0 -24
  242. package/src/commit/prompts/file-observer-user.md +0 -8
  243. package/src/commit/prompts/reduce-system.md +0 -50
  244. package/src/commit/prompts/reduce-user.md +0 -17
  245. package/src/commit/prompts/summary-retry.md +0 -3
  246. package/src/commit/prompts/summary-system.md +0 -38
  247. package/src/commit/prompts/summary-user.md +0 -13
  248. package/src/commit/prompts/types-description.md +0 -2
  249. package/src/commit/types.ts +0 -109
  250. package/src/commit/utils/exclusions.ts +0 -42
  251. package/src/mcp/render.ts +0 -123
  252. package/src/modes/components/agent-dashboard.ts +0 -1130
  253. package/src/modes/components/codemode-group.ts +0 -369
  254. package/src/modes/components/read-tool-group.ts +0 -119
  255. package/src/modes/components/visual-truncate.ts +0 -63
  256. package/src/prompts/system/subagent-user-prompt.md +0 -8
  257. package/src/prompts/tools/ask.md +0 -44
  258. package/src/prompts/tools/bash.md +0 -24
  259. package/src/prompts/tools/browser.md +0 -33
  260. package/src/prompts/tools/calculator.md +0 -12
  261. package/src/prompts/tools/explore.md +0 -29
  262. package/src/prompts/tools/fetch.md +0 -16
  263. package/src/prompts/tools/find.md +0 -18
  264. package/src/prompts/tools/gemini-image.md +0 -23
  265. package/src/prompts/tools/grep.md +0 -28
  266. package/src/prompts/tools/hashline.md +0 -232
  267. package/src/prompts/tools/librarian.md +0 -24
  268. package/src/prompts/tools/lsp.md +0 -28
  269. package/src/prompts/tools/oracle.md +0 -26
  270. package/src/prompts/tools/patch.md +0 -74
  271. package/src/prompts/tools/python.md +0 -66
  272. package/src/prompts/tools/read.md +0 -36
  273. package/src/prompts/tools/replace.md +0 -38
  274. package/src/prompts/tools/reviewer.md +0 -41
  275. package/src/prompts/tools/ssh.md +0 -51
  276. package/src/prompts/tools/task-summary.md +0 -28
  277. package/src/prompts/tools/task.md +0 -146
  278. package/src/prompts/tools/todo-write.md +0 -65
  279. package/src/prompts/tools/undo-edit.md +0 -7
  280. package/src/prompts/tools/web-search.md +0 -19
  281. package/src/prompts/tools/write.md +0 -18
  282. package/src/task/batch.ts +0 -102
  283. package/src/task/discovery.ts +0 -126
  284. package/src/task/parallel.ts +0 -84
  285. package/src/task/template.ts +0 -32
  286. package/src/tools/calculator.ts +0 -537
  287. package/src/tools/jtd-to-typescript.ts +0 -198
  288. package/src/tools/renderers.ts +0 -60
  289. package/src/tools/tool-result.ts +0 -86
  290. /package/src/{modes/theme → theme}/dark.json +0 -0
  291. /package/src/{modes/theme → theme}/defaults/dark-catppuccin.json +0 -0
  292. /package/src/{modes/theme → theme}/defaults/dark-dracula.json +0 -0
  293. /package/src/{modes/theme → theme}/defaults/dark-gruvbox.json +0 -0
  294. /package/src/{modes/theme → theme}/defaults/dark-solarized.json +0 -0
  295. /package/src/{modes/theme → theme}/defaults/dark-tokyo-night.json +0 -0
  296. /package/src/{modes/theme → theme}/defaults/index.ts +0 -0
  297. /package/src/{modes/theme → theme}/defaults/light-catppuccin.json +0 -0
  298. /package/src/{modes/theme → theme}/defaults/light-github.json +0 -0
  299. /package/src/{modes/theme → theme}/defaults/light-solarized.json +0 -0
  300. /package/src/{modes/theme → theme}/light.json +0 -0
  301. /package/src/{modes/theme → theme}/mermaid-cache.ts +0 -0
  302. /package/src/{modes/theme → theme}/theme-schema.json +0 -0
  303. /package/src/{modes/theme → theme}/theme.ts +0 -0
@@ -0,0 +1,742 @@
1
+ import * as fs from "node:fs/promises";
2
+ import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@nghyane/arcane-agent";
3
+ import {
4
+ createLspWritethrough,
5
+ type FileDiagnosticsResult,
6
+ flushLspWritethroughBatch,
7
+ type WritethroughCallback,
8
+ writethroughNoop,
9
+ } from "../lsp";
10
+ import { renderDiff } from "../modes/components/diff";
11
+ import type { Theme } from "../theme/theme";
12
+ import type { ToolSession } from "../tools";
13
+ import {
14
+ invalidateFsScanAfterDelete,
15
+ invalidateFsScanAfterRename,
16
+ invalidateFsScanAfterWrite,
17
+ } from "../tools/fs-cache-invalidation";
18
+ import { outputMeta } from "../tools/output-meta";
19
+ import { resolveToCwd } from "../tools/path-utils";
20
+ import { saveForUndo } from "../tools/undo-history";
21
+ import { applyPatch } from "./applicator";
22
+ import {
23
+ computeEditDiff,
24
+ computeHashlineDiff,
25
+ computePatchDiff,
26
+ generateDiffString,
27
+ generateUnifiedDiffString,
28
+ replaceText,
29
+ } from "./diff";
30
+ import { findMatch } from "./fuzzy";
31
+ import {
32
+ applyHashlineEdits,
33
+ computeLineHash,
34
+ type HashlineEdit,
35
+ type LineTag,
36
+ parseTag,
37
+ type ReplaceTextEdit,
38
+ } from "./hashline";
39
+ import { detectLineEnding, normalizeToLF, restoreLineEndings, stripBom } from "./normalize";
40
+ import { buildNormativeUpdateInput } from "./normative";
41
+ import {
42
+ DEFAULT_EDIT_MODE,
43
+ type EditMode,
44
+ type HashlineParams,
45
+ hashlineEditSchema,
46
+ hashlineParseContent,
47
+ hashlineParseContentString,
48
+ normalizeEditMode,
49
+ type PatchParams,
50
+ patchEditSchema,
51
+ type ReplaceParams,
52
+ replaceEditSchema,
53
+ type TInput,
54
+ } from "./schemas";
55
+ import { type EditToolDetails, editToolRenderer, getLspBatchRequest } from "./shared";
56
+ import type { DiffError, DiffResult, FileSystem, Operation, PatchInput } from "./types";
57
+ import { EditMatchError } from "./types";
58
+
59
+ class LspFileSystem implements FileSystem {
60
+ #lastDiagnostics: FileDiagnosticsResult | undefined;
61
+ #fileCache: Record<string, Bun.BunFile> = {};
62
+
63
+ constructor(
64
+ private readonly writethrough: (
65
+ dst: string,
66
+ content: string,
67
+ signal?: AbortSignal,
68
+ file?: import("bun").BunFile,
69
+ batch?: { id: string; flush: boolean },
70
+ ) => Promise<FileDiagnosticsResult | undefined>,
71
+ private readonly signal?: AbortSignal,
72
+ private readonly batchRequest?: { id: string; flush: boolean },
73
+ ) {}
74
+
75
+ #getFile(path: string): Bun.BunFile {
76
+ if (this.#fileCache[path]) {
77
+ return this.#fileCache[path];
78
+ }
79
+ const file = Bun.file(path);
80
+ this.#fileCache[path] = file;
81
+ return file;
82
+ }
83
+
84
+ async exists(path: string): Promise<boolean> {
85
+ return this.#getFile(path).exists();
86
+ }
87
+
88
+ async read(path: string): Promise<string> {
89
+ return this.#getFile(path).text();
90
+ }
91
+
92
+ async readBinary(path: string): Promise<Uint8Array> {
93
+ const buffer = await this.#getFile(path).arrayBuffer();
94
+ return new Uint8Array(buffer);
95
+ }
96
+
97
+ async write(path: string, content: string): Promise<void> {
98
+ const file = this.#getFile(path);
99
+ const result = await this.writethrough(path, content, this.signal, file, this.batchRequest);
100
+ if (result) {
101
+ this.#lastDiagnostics = result;
102
+ }
103
+ }
104
+
105
+ async delete(path: string): Promise<void> {
106
+ await this.#getFile(path).unlink();
107
+ }
108
+
109
+ async mkdir(path: string): Promise<void> {
110
+ await fs.mkdir(path, { recursive: true });
111
+ }
112
+
113
+ getDiagnostics(): FileDiagnosticsResult | undefined {
114
+ return this.#lastDiagnostics;
115
+ }
116
+ }
117
+
118
+ function mergeDiagnosticsWithWarnings(
119
+ diagnostics: FileDiagnosticsResult | undefined,
120
+ warnings: string[],
121
+ ): FileDiagnosticsResult | undefined {
122
+ if (warnings.length === 0) return diagnostics;
123
+ const warningMessages = warnings.map(warning => `patch: ${warning}`);
124
+ if (!diagnostics) {
125
+ return {
126
+ server: "patch",
127
+ messages: warningMessages,
128
+ summary: `Patch warnings: ${warnings.length}`,
129
+ errored: false,
130
+ };
131
+ }
132
+ return {
133
+ ...diagnostics,
134
+ messages: [...warningMessages, ...diagnostics.messages],
135
+ summary: `${diagnostics.summary}; Patch warnings: ${warnings.length}`,
136
+ };
137
+ }
138
+
139
+ export class EditTool implements AgentTool<TInput, any, Theme> {
140
+ readonly name = "edit";
141
+ readonly label = "Edit";
142
+ readonly nonAbortable = true;
143
+ readonly concurrency = "exclusive";
144
+ readonly renderCall = editToolRenderer.renderCall as unknown as AgentTool<TInput, any, Theme>["renderCall"];
145
+ readonly renderResult = editToolRenderer.renderResult as unknown as AgentTool<TInput, any, Theme>["renderResult"];
146
+
147
+ readonly #allowFuzzy: boolean;
148
+ readonly #fuzzyThreshold: number;
149
+ readonly #writethrough: WritethroughCallback;
150
+ readonly #editMode?: EditMode | null;
151
+
152
+ constructor(private readonly session: ToolSession) {
153
+ const {
154
+ ARCANE_EDIT_FUZZY: editFuzzy = "auto",
155
+ ARCANE_EDIT_FUZZY_THRESHOLD: editFuzzyThreshold = "auto",
156
+ ARCANE_EDIT_VARIANT: envEditVariant = "auto",
157
+ } = Bun.env;
158
+
159
+ if (envEditVariant && envEditVariant !== "auto") {
160
+ const editMode = normalizeEditMode(envEditVariant);
161
+ if (!editMode) {
162
+ throw new Error(`Invalid ARCANE_EDIT_VARIANT: ${envEditVariant}`);
163
+ }
164
+ this.#editMode = editMode;
165
+ }
166
+
167
+ switch (editFuzzy) {
168
+ case "true":
169
+ case "1":
170
+ this.#allowFuzzy = true;
171
+ break;
172
+ case "false":
173
+ case "0":
174
+ this.#allowFuzzy = false;
175
+ break;
176
+ case "auto":
177
+ this.#allowFuzzy = session.settings.get("edit.fuzzyMatch");
178
+ break;
179
+ default:
180
+ throw new Error(`Invalid ARCANE_EDIT_FUZZY: ${editFuzzy}`);
181
+ }
182
+ switch (editFuzzyThreshold) {
183
+ case "auto":
184
+ this.#fuzzyThreshold = session.settings.get("edit.fuzzyThreshold");
185
+ break;
186
+ default:
187
+ this.#fuzzyThreshold = parseFloat(editFuzzyThreshold);
188
+ if (Number.isNaN(this.#fuzzyThreshold) || this.#fuzzyThreshold < 0 || this.#fuzzyThreshold > 1) {
189
+ throw new Error(`Invalid ARCANE_EDIT_FUZZY_THRESHOLD: ${editFuzzyThreshold}`);
190
+ }
191
+ break;
192
+ }
193
+
194
+ const enableLsp = session.enableLsp ?? true;
195
+ const enableDiagnostics = enableLsp && session.settings.get("lsp.diagnosticsOnEdit");
196
+ const enableFormat = enableLsp && session.settings.get("lsp.formatOnWrite");
197
+ this.#writethrough = enableLsp
198
+ ? createLspWritethrough(session.cwd, { enableFormat, enableDiagnostics })
199
+ : writethroughNoop;
200
+ }
201
+
202
+ /**
203
+ * Determine edit mode dynamically based on current model.
204
+ * This is re-evaluated on each access so tool definitions stay current when model changes.
205
+ */
206
+ get mode(): EditMode {
207
+ if (this.#editMode) return this.#editMode;
208
+ const activeModel = this.session.getActiveModelString?.();
209
+ const editVariant =
210
+ this.session.settings.getEditVariantForModel(activeModel) ??
211
+ normalizeEditMode(this.session.settings.get("edit.mode"));
212
+ return editVariant ?? DEFAULT_EDIT_MODE;
213
+ }
214
+
215
+ description =
216
+ "Apply edits to existing files (create, update, delete, rename). The diff is shown to the user, so do not repeat or summarize the changes.";
217
+
218
+ /**
219
+ * Dynamic parameters schema based on current edit mode (which depends on current model).
220
+ */
221
+ get parameters(): TInput {
222
+ switch (this.mode) {
223
+ case "patch":
224
+ return patchEditSchema;
225
+ case "hashline":
226
+ return hashlineEditSchema;
227
+ default:
228
+ return replaceEditSchema;
229
+ }
230
+ }
231
+
232
+ async execute(
233
+ _toolCallId: string,
234
+ params: ReplaceParams | PatchParams | HashlineParams,
235
+ signal?: AbortSignal,
236
+ _onUpdate?: AgentToolUpdateCallback<EditToolDetails, TInput>,
237
+ context?: AgentToolContext,
238
+ ): Promise<AgentToolResult<EditToolDetails, TInput>> {
239
+ const batchRequest = getLspBatchRequest(context?.toolCall);
240
+
241
+ // ─────────────────────────────────────────────────────────────────
242
+ // Hashline mode execution
243
+ // ─────────────────────────────────────────────────────────────────
244
+ if (this.mode === "hashline") {
245
+ const { path, edits, delete: deleteFile, rename } = params as HashlineParams;
246
+
247
+ if (path.endsWith(".ipynb") && edits?.length > 0) {
248
+ throw new Error("Cannot edit Jupyter notebooks with the Edit tool. Use the NotebookEdit tool instead.");
249
+ }
250
+
251
+ const absolutePath = resolveToCwd(path, this.session.cwd);
252
+ const resolvedRename = rename ? resolveToCwd(rename, this.session.cwd) : undefined;
253
+ const file = Bun.file(absolutePath);
254
+
255
+ if (deleteFile) {
256
+ if (await file.exists()) {
257
+ await file.unlink();
258
+ }
259
+ invalidateFsScanAfterDelete(absolutePath);
260
+ return {
261
+ content: [{ type: "text", text: `Deleted ${path}` }],
262
+ details: {
263
+ diff: "",
264
+ op: "delete",
265
+ meta: outputMeta().get(),
266
+ },
267
+ };
268
+ }
269
+
270
+ if (!(await file.exists())) {
271
+ const content: string[] = [];
272
+ for (const edit of edits) {
273
+ switch (edit.op) {
274
+ case "append": {
275
+ if (edit.after) {
276
+ throw new Error(`File not found: ${path}`);
277
+ }
278
+ content.push(...hashlineParseContent(edit.content));
279
+ break;
280
+ }
281
+ case "prepend": {
282
+ if (edit.before) {
283
+ throw new Error(`File not found: ${path}`);
284
+ }
285
+ content.unshift(...hashlineParseContent(edit.content));
286
+ break;
287
+ }
288
+ default: {
289
+ throw new Error(`File not found: ${path}`);
290
+ }
291
+ }
292
+ }
293
+ await file.write(content.join("\n"));
294
+ return {
295
+ content: [{ type: "text", text: `Created ${path}` }],
296
+ details: {
297
+ diff: "",
298
+ op: "create",
299
+ meta: outputMeta().get(),
300
+ },
301
+ };
302
+ }
303
+
304
+ const anchorEdits: HashlineEdit[] = [];
305
+ const replaceEdits: ReplaceTextEdit[] = [];
306
+ for (const edit of edits) {
307
+ switch (edit.op) {
308
+ case "set": {
309
+ const { tag, content } = edit;
310
+ anchorEdits.push({
311
+ op: "set",
312
+ tag: parseTag(tag),
313
+ content: hashlineParseContent(content),
314
+ });
315
+ break;
316
+ }
317
+ case "replace": {
318
+ const { first, last, content } = edit;
319
+ anchorEdits.push({
320
+ op: "replace",
321
+ first: parseTag(first),
322
+ last: parseTag(last),
323
+ content: hashlineParseContent(content),
324
+ });
325
+ break;
326
+ }
327
+ case "append": {
328
+ const { after, content } = edit;
329
+ anchorEdits.push({
330
+ op: "append",
331
+ ...(after ? { after: parseTag(after) } : {}),
332
+ content: hashlineParseContent(content),
333
+ });
334
+ break;
335
+ }
336
+ case "prepend": {
337
+ const { before, content } = edit;
338
+ anchorEdits.push({
339
+ op: "prepend",
340
+ ...(before ? { before: parseTag(before) } : {}),
341
+ content: hashlineParseContent(content),
342
+ });
343
+ break;
344
+ }
345
+ case "insert": {
346
+ const { before, after, content } = edit;
347
+ if (before && !after) {
348
+ anchorEdits.push({
349
+ op: "prepend",
350
+ before: parseTag(before),
351
+ content: hashlineParseContent(content),
352
+ });
353
+ } else if (after && !before) {
354
+ anchorEdits.push({
355
+ op: "append",
356
+ after: parseTag(after),
357
+ content: hashlineParseContent(content),
358
+ });
359
+ } else if (before && after) {
360
+ anchorEdits.push({
361
+ op: "insert",
362
+ before: parseTag(before),
363
+ after: parseTag(after),
364
+ content: hashlineParseContent(content),
365
+ });
366
+ } else {
367
+ throw new Error(`Insert must have both before and after tags.`);
368
+ }
369
+ break;
370
+ }
371
+ case "replaceText": {
372
+ const { old_text, new_text, all } = edit;
373
+ replaceEdits.push({
374
+ op: "replaceText",
375
+ old_text: old_text,
376
+ new_text: hashlineParseContentString(new_text),
377
+ all: all ?? false,
378
+ });
379
+ break;
380
+ }
381
+ default:
382
+ throw new Error(`Invalid edit operation: ${JSON.stringify(edit)}`);
383
+ }
384
+ }
385
+
386
+ const rawContent = await file.text();
387
+ const { bom, text: content } = stripBom(rawContent);
388
+ const originalEnding = detectLineEnding(content);
389
+ const originalNormalized = normalizeToLF(content);
390
+ let normalizedContent = originalNormalized;
391
+
392
+ // Apply anchor-based edits first (set, set_range, insert)
393
+ const anchorResult = applyHashlineEdits(normalizedContent, anchorEdits);
394
+ normalizedContent = anchorResult.content;
395
+
396
+ // Apply content-replace edits (substr-style fuzzy replace)
397
+ for (const r of replaceEdits) {
398
+ if (r.old_text.length === 0) {
399
+ throw new Error("old_text must not be empty.");
400
+ }
401
+ const rep = replaceText(normalizedContent, r.old_text, r.new_text, {
402
+ fuzzy: this.#allowFuzzy,
403
+ all: r.all ?? false,
404
+ threshold: this.#fuzzyThreshold,
405
+ });
406
+ normalizedContent = rep.content;
407
+ }
408
+
409
+ const result = {
410
+ content: normalizedContent,
411
+ firstChangedLine: anchorResult.firstChangedLine,
412
+ warnings: anchorResult.warnings,
413
+ noopEdits: anchorResult.noopEdits,
414
+ };
415
+ if (originalNormalized === result.content && !rename) {
416
+ let diagnostic = `No changes made to ${path}. The edits produced identical content.`;
417
+ if (result.noopEdits && result.noopEdits.length > 0) {
418
+ const details = result.noopEdits
419
+ .map(
420
+ e =>
421
+ `Edit ${e.editIndex}: replacement for ${e.loc} is identical to current content:\n ${e.loc}| ${e.currentContent}`,
422
+ )
423
+ .join("\n");
424
+ diagnostic += `\n${details}`;
425
+ diagnostic +=
426
+ "\nYour content must differ from what the file already contains. Re-read the file to see the current state.";
427
+ } else {
428
+ // Edits were not literally identical but heuristics normalized them back
429
+ const lines = result.content.split("\n");
430
+ const targetLines: string[] = [];
431
+ const refs: LineTag[] = [];
432
+ for (const edit of anchorEdits) {
433
+ refs.length = 0;
434
+ switch (edit.op) {
435
+ case "set":
436
+ refs.push(edit.tag);
437
+ break;
438
+ case "replace":
439
+ refs.push(edit.first, edit.last);
440
+ break;
441
+ case "append":
442
+ if (edit.after) refs.push(edit.after);
443
+ break;
444
+ case "prepend":
445
+ if (edit.before) refs.push(edit.before);
446
+ break;
447
+ case "insert":
448
+ refs.push(edit.after, edit.before);
449
+ break;
450
+ default:
451
+ break;
452
+ }
453
+
454
+ for (const ref of refs) {
455
+ try {
456
+ if (ref.line >= 1 && ref.line <= lines.length) {
457
+ const lineContent = lines[ref.line - 1];
458
+ const hash = computeLineHash(ref.line, lineContent);
459
+ targetLines.push(`${ref.line}#${hash}:${lineContent}`);
460
+ }
461
+ } catch {
462
+ /* skip malformed refs */
463
+ }
464
+ }
465
+ }
466
+ if (targetLines.length > 0) {
467
+ const preview = [...new Set(targetLines)].slice(0, 5).join("\n");
468
+ diagnostic += `\nThe file currently contains these lines:\n${preview}\nYour edits were normalized back to the original content (whitespace-only differences are preserved as-is). Ensure your replacement changes actual code, not just formatting.`;
469
+ }
470
+ }
471
+ throw new Error(diagnostic);
472
+ }
473
+
474
+ const finalContent = bom + restoreLineEndings(result.content, originalEnding);
475
+ const writePath = resolvedRename ?? absolutePath;
476
+ saveForUndo(absolutePath, rawContent);
477
+ const diagnostics = await this.#writethrough(
478
+ writePath,
479
+ finalContent,
480
+ signal,
481
+ Bun.file(writePath),
482
+ batchRequest,
483
+ );
484
+ if (resolvedRename && resolvedRename !== absolutePath) {
485
+ await file.unlink();
486
+ invalidateFsScanAfterRename(absolutePath, resolvedRename);
487
+ } else {
488
+ invalidateFsScanAfterWrite(absolutePath);
489
+ }
490
+ const diffResult = generateDiffString(originalNormalized, result.content);
491
+
492
+ const normative = buildNormativeUpdateInput({
493
+ path,
494
+ ...(rename ? { rename } : {}),
495
+ oldContent: rawContent,
496
+ newContent: finalContent,
497
+ });
498
+
499
+ const meta = outputMeta()
500
+ .diagnostics(diagnostics?.summary ?? "", diagnostics?.messages ?? [])
501
+ .get();
502
+
503
+ const resultText = rename ? `Updated and moved ${path} to ${rename}` : `Updated ${path}`;
504
+ return {
505
+ content: [
506
+ {
507
+ type: "text",
508
+ text: `${resultText}${result.warnings?.length ? `\n\nWarnings:\n${result.warnings.join("\n")}` : ""}`,
509
+ },
510
+ ],
511
+ details: {
512
+ diff: diffResult.diff,
513
+ firstChangedLine: result.firstChangedLine ?? diffResult.firstChangedLine,
514
+ diagnostics,
515
+ op: "update",
516
+ rename,
517
+ meta,
518
+ },
519
+ $normative: normative,
520
+ };
521
+ }
522
+
523
+ // ─────────────────────────────────────────────────────────────────
524
+ // Patch mode execution
525
+ // ─────────────────────────────────────────────────────────────────
526
+ if (this.mode === "patch") {
527
+ const { path, op: rawOp, rename, diff } = params as PatchParams;
528
+
529
+ // Normalize unrecognized operations to "update"
530
+ const op: Operation = rawOp === "create" || rawOp === "delete" ? rawOp : "update";
531
+
532
+ const resolvedPath = resolveToCwd(path, this.session.cwd);
533
+ const resolvedRename = rename ? resolveToCwd(rename, this.session.cwd) : undefined;
534
+
535
+ if (path.endsWith(".ipynb")) {
536
+ throw new Error("Cannot edit Jupyter notebooks with the Edit tool. Use the NotebookEdit tool instead.");
537
+ }
538
+ if (rename?.endsWith(".ipynb")) {
539
+ throw new Error("Cannot edit Jupyter notebooks with the Edit tool. Use the NotebookEdit tool instead.");
540
+ }
541
+
542
+ const input: PatchInput = {
543
+ path: resolvedPath,
544
+ op,
545
+ rename: resolvedRename,
546
+ diff,
547
+ };
548
+ const fs = new LspFileSystem(this.#writethrough, signal, batchRequest);
549
+ const result = await applyPatch(input, {
550
+ cwd: this.session.cwd,
551
+ fs,
552
+ fuzzyThreshold: this.#fuzzyThreshold,
553
+ allowFuzzy: this.#allowFuzzy,
554
+ });
555
+ if (result.change.oldContent !== undefined) {
556
+ saveForUndo(resolvedPath, result.change.oldContent);
557
+ }
558
+ if (resolvedRename) {
559
+ invalidateFsScanAfterRename(resolvedPath, resolvedRename);
560
+ } else if (result.change.type === "delete") {
561
+ invalidateFsScanAfterDelete(resolvedPath);
562
+ } else {
563
+ invalidateFsScanAfterWrite(resolvedPath);
564
+ }
565
+ const effRename = result.change.newPath ? rename : undefined;
566
+
567
+ // Generate diff for display
568
+ let diffResult = {
569
+ diff: "",
570
+ firstChangedLine: undefined as number | undefined,
571
+ };
572
+ if (result.change.type === "update" && result.change.oldContent && result.change.newContent) {
573
+ const normalizedOld = normalizeToLF(stripBom(result.change.oldContent).text);
574
+ const normalizedNew = normalizeToLF(stripBom(result.change.newContent).text);
575
+ diffResult = generateUnifiedDiffString(normalizedOld, normalizedNew);
576
+ }
577
+
578
+ let resultText: string;
579
+ switch (result.change.type) {
580
+ case "create":
581
+ resultText = `Created ${path}`;
582
+ break;
583
+ case "delete":
584
+ resultText = `Deleted ${path}`;
585
+ break;
586
+ case "update":
587
+ resultText = effRename ? `Updated and moved ${path} to ${effRename}` : `Updated ${path}`;
588
+ break;
589
+ }
590
+
591
+ let diagnostics = fs.getDiagnostics();
592
+ if (op === "delete" && batchRequest?.flush) {
593
+ const flushedDiagnostics = await flushLspWritethroughBatch(batchRequest.id, this.session.cwd, signal);
594
+ diagnostics ??= flushedDiagnostics;
595
+ }
596
+ const patchWarnings = result.warnings ?? [];
597
+ const mergedDiagnostics = mergeDiagnosticsWithWarnings(diagnostics, patchWarnings);
598
+
599
+ const meta = outputMeta()
600
+ .diagnostics(mergedDiagnostics?.summary ?? "", mergedDiagnostics?.messages ?? [])
601
+ .get();
602
+
603
+ return {
604
+ content: [{ type: "text", text: resultText }],
605
+ details: {
606
+ diff: diffResult.diff,
607
+ firstChangedLine: diffResult.firstChangedLine,
608
+ diagnostics: mergedDiagnostics,
609
+ op,
610
+ rename: effRename,
611
+ meta,
612
+ },
613
+ };
614
+ }
615
+
616
+ // ─────────────────────────────────────────────────────────────────
617
+ // Replace mode execution
618
+ // ─────────────────────────────────────────────────────────────────
619
+ const { path, old_text, new_text, all } = params as ReplaceParams;
620
+
621
+ if (path.endsWith(".ipynb")) {
622
+ throw new Error("Cannot edit Jupyter notebooks with the Edit tool. Use the NotebookEdit tool instead.");
623
+ }
624
+
625
+ if (old_text.length === 0) {
626
+ throw new Error("old_text must not be empty.");
627
+ }
628
+
629
+ const absolutePath = resolveToCwd(path, this.session.cwd);
630
+ const file = Bun.file(absolutePath);
631
+
632
+ if (!(await file.exists())) {
633
+ throw new Error(`File not found: ${path}`);
634
+ }
635
+
636
+ const rawContent = await file.text();
637
+ const { bom, text: content } = stripBom(rawContent);
638
+ const originalEnding = detectLineEnding(content);
639
+ const normalizedContent = normalizeToLF(content);
640
+ const normalizedOldText = normalizeToLF(old_text);
641
+ const normalizedNewText = normalizeToLF(new_text);
642
+
643
+ const result = replaceText(normalizedContent, normalizedOldText, normalizedNewText, {
644
+ fuzzy: this.#allowFuzzy,
645
+ all: all ?? false,
646
+ threshold: this.#fuzzyThreshold,
647
+ });
648
+
649
+ if (result.count === 0) {
650
+ // Get error details
651
+ const matchOutcome = findMatch(normalizedContent, normalizedOldText, {
652
+ allowFuzzy: this.#allowFuzzy,
653
+ threshold: this.#fuzzyThreshold,
654
+ });
655
+
656
+ if (matchOutcome.occurrences && matchOutcome.occurrences > 1) {
657
+ const previews = matchOutcome.occurrencePreviews?.join("\n\n") ?? "";
658
+ const moreMsg = matchOutcome.occurrences > 5 ? ` (showing first 5 of ${matchOutcome.occurrences})` : "";
659
+ throw new Error(
660
+ `Found ${matchOutcome.occurrences} occurrences in ${path}${moreMsg}:\n\n${previews}\n\n` +
661
+ `Add more context lines to disambiguate.`,
662
+ );
663
+ }
664
+
665
+ throw new EditMatchError(path, normalizedOldText, matchOutcome.closest, {
666
+ allowFuzzy: this.#allowFuzzy,
667
+ threshold: this.#fuzzyThreshold,
668
+ fuzzyMatches: matchOutcome.fuzzyMatches,
669
+ });
670
+ }
671
+
672
+ if (normalizedContent === result.content) {
673
+ throw new Error(
674
+ `No changes made to ${path}. The replacement produced identical content. This might indicate an issue with special characters or the text not existing as expected.`,
675
+ );
676
+ }
677
+
678
+ const finalContent = bom + restoreLineEndings(result.content, originalEnding);
679
+ saveForUndo(absolutePath, rawContent);
680
+ const diagnostics = await this.#writethrough(absolutePath, finalContent, signal, file, batchRequest);
681
+ invalidateFsScanAfterWrite(absolutePath);
682
+ const diffResult = generateDiffString(normalizedContent, result.content);
683
+
684
+ const resultText =
685
+ result.count > 1
686
+ ? `Successfully replaced ${result.count} occurrences in ${path}.`
687
+ : `Successfully replaced text in ${path}.`;
688
+
689
+ const meta = outputMeta()
690
+ .diagnostics(diagnostics?.summary ?? "", diagnostics?.messages ?? [])
691
+ .get();
692
+
693
+ return {
694
+ content: [{ type: "text", text: resultText }],
695
+ details: {
696
+ diff: diffResult.diff,
697
+ firstChangedLine: diffResult.firstChangedLine,
698
+ diagnostics,
699
+ meta,
700
+ },
701
+ };
702
+ }
703
+
704
+ async onArgsComplete(args: unknown, cwd: string): Promise<DiffResult | DiffError | undefined> {
705
+ const a = args as Record<string, unknown>;
706
+ const path = a.path as string | undefined;
707
+ if (!path) return undefined;
708
+
709
+ const op = a.op as string | undefined;
710
+ const diff = a.diff as string | undefined;
711
+ const rename = a.rename as string | undefined;
712
+ const edits = a.edits as HashlineEdit[] | undefined;
713
+ const oldText = a.old_text as string | undefined;
714
+ const newText = a.new_text as string | undefined;
715
+ const all = a.all as boolean | undefined;
716
+
717
+ try {
718
+ if (op) {
719
+ return await computePatchDiff({ path, op: op as "create" | "delete" | "update", rename, diff }, cwd, {
720
+ fuzzyThreshold: this.#fuzzyThreshold,
721
+ allowFuzzy: this.#allowFuzzy,
722
+ });
723
+ }
724
+ if (Array.isArray(edits)) {
725
+ return await computeHashlineDiff({ path, edits }, cwd);
726
+ }
727
+ if (oldText !== undefined && newText !== undefined) {
728
+ return await computeEditDiff(path, oldText, newText, cwd, true, all, this.#fuzzyThreshold);
729
+ }
730
+ } catch {
731
+ return undefined;
732
+ }
733
+ return undefined;
734
+ }
735
+
736
+ buildRenderContext(info: { toolState?: unknown }): Record<string, unknown> {
737
+ return {
738
+ editDiffPreview: info.toolState,
739
+ renderDiff,
740
+ };
741
+ }
742
+ }