@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.
- package/CHANGELOG.md +28 -0
- package/package.json +21 -70
- package/scripts/format-prompts.ts +1 -3
- package/src/cli/args.ts +2 -7
- package/src/cli/config-cli.ts +1 -1
- package/src/cli/plugin-cli.ts +1 -1
- package/src/cli/setup-cli.ts +1 -1
- package/src/cli/update-cli.ts +1 -1
- package/src/cli/web-search-cli.ts +1 -1
- package/src/cli.ts +0 -1
- package/src/commands/config.ts +1 -1
- package/src/commands/grep.ts +1 -1
- package/src/commands/jupyter.ts +1 -1
- package/src/commands/plugin.ts +1 -1
- package/src/commands/setup.ts +1 -1
- package/src/commands/shell.ts +1 -1
- package/src/commands/ssh.ts +1 -1
- package/src/commands/stats.ts +1 -1
- package/src/commands/update.ts +1 -1
- package/src/config/model-registry.ts +3 -4
- package/src/config/model-resolver.ts +36 -9
- package/src/config/prompt-templates.ts +1 -9
- package/src/config/settings-schema.ts +32 -88
- package/src/config/settings.ts +3 -4
- package/src/debug/index.ts +1 -1
- package/src/debug/log-formatting.ts +1 -1
- package/src/debug/log-viewer.ts +2 -2
- package/src/discovery/helpers.ts +13 -3
- package/src/exa/index.ts +1 -35
- package/src/exa/render.ts +30 -190
- package/src/export/html/index.ts +1 -1
- package/src/extensibility/custom-tools/loader.ts +1 -1
- package/src/extensibility/custom-tools/types.ts +5 -1
- package/src/extensibility/custom-tools/wrapper.ts +1 -1
- package/src/extensibility/extensions/runner.ts +1 -1
- package/src/extensibility/extensions/types.ts +1 -1
- package/src/extensibility/extensions/wrapper.ts +7 -15
- package/src/extensibility/hooks/runner.ts +1 -1
- package/src/extensibility/hooks/types.ts +1 -1
- package/src/extensibility/plugins/doctor.ts +1 -1
- package/src/index.ts +13 -13
- package/src/lsp/index.ts +77 -24
- package/src/lsp/render.ts +34 -583
- package/src/lsp/types.ts +3 -3
- package/src/lsp/utils.ts +1 -1
- package/src/main.ts +1 -1
- package/src/mcp/tool-bridge.ts +1 -24
- package/src/modes/components/assistant-message.ts +7 -7
- package/src/modes/components/bash-execution.ts +50 -112
- package/src/modes/components/bordered-loader.ts +1 -1
- package/src/modes/components/branch-summary-message.ts +16 -10
- package/src/modes/components/compaction-summary-message.ts +20 -12
- package/src/modes/components/context-group.ts +106 -0
- package/src/modes/components/custom-message.ts +4 -5
- package/src/modes/components/diff.ts +2 -2
- package/src/modes/components/dynamic-border.ts +1 -1
- package/src/modes/components/extensions/extension-dashboard.ts +1 -1
- package/src/modes/components/extensions/extension-list.ts +1 -1
- package/src/modes/components/extensions/inspector-panel.ts +1 -1
- package/src/modes/components/footer.ts +2 -2
- package/src/modes/components/history-search.ts +1 -1
- package/src/modes/components/hook-editor.ts +1 -1
- package/src/modes/components/hook-input.ts +1 -1
- package/src/modes/components/hook-message.ts +4 -5
- package/src/modes/components/hook-selector.ts +1 -1
- package/src/modes/components/index.ts +0 -2
- package/src/modes/components/keybinding-hints.ts +1 -1
- package/src/modes/components/login-dialog.ts +1 -1
- package/src/modes/components/mcp-add-wizard.ts +1 -1
- package/src/modes/components/model-selector.ts +1 -1
- package/src/modes/components/oauth-selector.ts +1 -1
- package/src/modes/components/plugin-settings.ts +1 -1
- package/src/modes/components/python-execution.ts +51 -91
- package/src/modes/components/queue-mode-selector.ts +1 -1
- package/src/modes/components/session-selector.ts +1 -1
- package/src/modes/components/settings-defs.ts +5 -10
- package/src/modes/components/settings-selector.ts +1 -1
- package/src/modes/components/show-images-selector.ts +1 -1
- package/src/modes/components/skill-message.ts +4 -4
- package/src/modes/components/status-line/segments.ts +2 -2
- package/src/modes/components/status-line/separators.ts +1 -1
- package/src/modes/components/status-line-segment-editor.ts +1 -1
- package/src/modes/components/status-line.ts +1 -1
- package/src/modes/components/theme-selector.ts +1 -1
- package/src/modes/components/thinking-selector.ts +1 -1
- package/src/modes/components/todo-display.ts +2 -4
- package/src/modes/components/todo-reminder.ts +4 -4
- package/src/modes/components/tool-execution.ts +118 -440
- package/src/modes/components/tool-image-display.ts +107 -0
- package/src/modes/components/tree-selector.ts +2 -2
- package/src/modes/components/ttsr-notification.ts +4 -17
- package/src/modes/components/user-message-selector.ts +1 -1
- package/src/modes/components/user-message.ts +9 -10
- package/src/modes/components/welcome.ts +1 -1
- package/src/modes/controllers/command-controller.ts +1 -1
- package/src/modes/controllers/event-controller.ts +58 -187
- package/src/modes/controllers/extension-ui-controller.ts +1 -1
- package/src/modes/controllers/input-controller.ts +3 -1
- package/src/modes/controllers/mcp-command-controller.ts +1 -1
- package/src/modes/controllers/selector-controller.ts +3 -26
- package/src/modes/controllers/ssh-command-controller.ts +1 -1
- package/src/modes/interactive-mode.ts +3 -7
- package/src/modes/print-mode.ts +5 -5
- package/src/modes/rpc/rpc-mode.ts +1 -1
- package/src/modes/types.ts +1 -2
- package/src/modes/utils/ui-helpers.ts +34 -32
- package/src/patch/edit-tool.ts +742 -0
- package/src/patch/index.ts +32 -898
- package/src/patch/schemas.ts +208 -0
- package/src/patch/shared.ts +83 -151
- package/src/prompts/agents/explore.md +22 -37
- package/src/prompts/agents/init.md +1 -1
- package/src/prompts/agents/librarian.md +29 -20
- package/src/prompts/agents/oracle.md +9 -2
- package/src/prompts/agents/reviewer.md +14 -48
- package/src/prompts/agents/task.md +16 -8
- package/src/prompts/compaction/branch-summary.md +4 -1
- package/src/prompts/compaction/compaction-summary.md +4 -1
- package/src/prompts/system/subagent-system-prompt.md +1 -1
- package/src/prompts/system/system-prompt.md +162 -178
- package/src/prompts/system/verification-reminder.md +6 -0
- package/src/sdk.ts +0 -9
- package/src/session/agent-session.ts +244 -1459
- package/src/session/model-controller.ts +406 -0
- package/src/session/retry-utils.ts +71 -0
- package/src/session/session-manager.ts +22 -186
- package/src/session/session-types.ts +312 -0
- package/src/session/stats.ts +387 -0
- package/src/session/streaming-edit.ts +258 -0
- package/src/session/ttsr.ts +213 -0
- package/src/slash-commands/builtin-registry.ts +0 -8
- package/src/stt/recorder.ts +2 -2
- package/src/system-prompt.ts +1 -14
- package/src/task/agents.ts +7 -33
- package/src/task/executor.ts +50 -438
- package/src/task/index.ts +104 -71
- package/src/task/progress-tracker.ts +390 -0
- package/src/task/render.ts +371 -187
- package/src/task/subprocess-tool-registry.ts +1 -1
- package/src/task/types.ts +14 -47
- package/src/tools/ask.ts +31 -42
- package/src/tools/bash-interactive.ts +2 -2
- package/src/tools/bash-interceptor.ts +2 -2
- package/src/tools/bash-normalize.ts +1 -1
- package/src/tools/bash-skill-urls.ts +2 -2
- package/src/tools/bash.ts +87 -136
- package/src/tools/browser.ts +54 -84
- package/src/tools/create-tools.ts +186 -0
- package/src/tools/default-renderer.ts +104 -0
- package/src/tools/explore.ts +11 -10
- package/src/tools/fetch.ts +24 -114
- package/src/tools/find.ts +48 -132
- package/src/tools/gemini-image.ts +5 -15
- package/src/tools/github.ts +450 -0
- package/src/tools/grep.ts +43 -179
- package/src/tools/index.ts +35 -198
- package/src/tools/json-tree.ts +3 -3
- package/src/tools/librarian.ts +18 -18
- package/src/tools/list-limit.ts +2 -2
- package/src/tools/notebook.ts +35 -87
- package/src/tools/oracle.ts +25 -25
- package/src/tools/output-meta.ts +89 -4
- package/src/tools/output-utils.ts +2 -2
- package/src/tools/python.ts +86 -637
- package/src/tools/read.ts +36 -119
- package/src/tools/reviewer-tool.ts +19 -21
- package/src/tools/search-code.ts +128 -0
- package/src/tools/ssh.ts +67 -126
- package/src/tools/subagent-tool.ts +197 -123
- package/src/tools/todo-write.ts +15 -31
- package/src/tools/tool-errors.ts +0 -30
- package/src/tools/undo-edit.ts +30 -67
- package/src/tools/write.ts +78 -127
- package/src/tui/code-cell.ts +4 -4
- package/src/tui/file-list.ts +2 -2
- package/src/tui/output-block.ts +1 -1
- package/src/tui/status-line.ts +1 -1
- package/src/tui/tree-list.ts +2 -2
- package/src/tui/types.ts +1 -1
- package/src/tui/utils.ts +1 -1
- package/src/{tools → ui}/render-utils.ts +87 -126
- package/src/utils/external-editor.ts +4 -4
- package/src/utils/file-mentions.ts +1 -1
- package/src/utils/index.ts +30 -0
- package/src/utils/tools-manager.ts +9 -19
- package/src/web/github-client.ts +290 -0
- package/src/web/scrapers/github.ts +11 -62
- package/src/web/search/auth.ts +1 -3
- package/src/web/search/index.ts +82 -46
- package/src/web/search/provider.ts +11 -16
- package/src/web/search/providers/grep.ts +160 -0
- package/src/web/search/render.ts +48 -235
- package/src/web/search/types.ts +1 -1
- package/src/commands/commit.ts +0 -36
- package/src/commit/agentic/agent.ts +0 -311
- package/src/commit/agentic/fallback.ts +0 -96
- package/src/commit/agentic/index.ts +0 -359
- package/src/commit/agentic/prompts/analyze-file.md +0 -22
- package/src/commit/agentic/prompts/session-user.md +0 -25
- package/src/commit/agentic/prompts/split-confirm.md +0 -1
- package/src/commit/agentic/prompts/system.md +0 -38
- package/src/commit/agentic/state.ts +0 -69
- package/src/commit/agentic/tools/analyze-file.ts +0 -118
- package/src/commit/agentic/tools/git-file-diff.ts +0 -194
- package/src/commit/agentic/tools/git-hunk.ts +0 -50
- package/src/commit/agentic/tools/git-overview.ts +0 -84
- package/src/commit/agentic/tools/index.ts +0 -56
- package/src/commit/agentic/tools/propose-changelog.ts +0 -128
- package/src/commit/agentic/tools/propose-commit.ts +0 -154
- package/src/commit/agentic/tools/recent-commits.ts +0 -81
- package/src/commit/agentic/tools/split-commit.ts +0 -280
- package/src/commit/agentic/topo-sort.ts +0 -44
- package/src/commit/agentic/trivial.ts +0 -51
- package/src/commit/agentic/validation.ts +0 -200
- package/src/commit/analysis/conventional.ts +0 -165
- package/src/commit/analysis/index.ts +0 -4
- package/src/commit/analysis/scope.ts +0 -242
- package/src/commit/analysis/summary.ts +0 -112
- package/src/commit/analysis/validation.ts +0 -66
- package/src/commit/changelog/detect.ts +0 -37
- package/src/commit/changelog/generate.ts +0 -110
- package/src/commit/changelog/index.ts +0 -234
- package/src/commit/changelog/parse.ts +0 -44
- package/src/commit/cli.ts +0 -93
- package/src/commit/git/diff.ts +0 -148
- package/src/commit/git/errors.ts +0 -9
- package/src/commit/git/index.ts +0 -211
- package/src/commit/git/operations.ts +0 -54
- package/src/commit/index.ts +0 -5
- package/src/commit/map-reduce/index.ts +0 -64
- package/src/commit/map-reduce/map-phase.ts +0 -178
- package/src/commit/map-reduce/reduce-phase.ts +0 -145
- package/src/commit/map-reduce/utils.ts +0 -9
- package/src/commit/message.ts +0 -11
- package/src/commit/model-selection.ts +0 -69
- package/src/commit/pipeline.ts +0 -243
- package/src/commit/prompts/analysis-system.md +0 -148
- package/src/commit/prompts/analysis-user.md +0 -38
- package/src/commit/prompts/changelog-system.md +0 -50
- package/src/commit/prompts/changelog-user.md +0 -18
- package/src/commit/prompts/file-observer-system.md +0 -24
- package/src/commit/prompts/file-observer-user.md +0 -8
- package/src/commit/prompts/reduce-system.md +0 -50
- package/src/commit/prompts/reduce-user.md +0 -17
- package/src/commit/prompts/summary-retry.md +0 -3
- package/src/commit/prompts/summary-system.md +0 -38
- package/src/commit/prompts/summary-user.md +0 -13
- package/src/commit/prompts/types-description.md +0 -2
- package/src/commit/types.ts +0 -109
- package/src/commit/utils/exclusions.ts +0 -42
- package/src/mcp/render.ts +0 -123
- package/src/modes/components/agent-dashboard.ts +0 -1130
- package/src/modes/components/codemode-group.ts +0 -369
- package/src/modes/components/read-tool-group.ts +0 -119
- package/src/modes/components/visual-truncate.ts +0 -63
- package/src/prompts/system/subagent-user-prompt.md +0 -8
- package/src/prompts/tools/ask.md +0 -44
- package/src/prompts/tools/bash.md +0 -24
- package/src/prompts/tools/browser.md +0 -33
- package/src/prompts/tools/calculator.md +0 -12
- package/src/prompts/tools/explore.md +0 -29
- package/src/prompts/tools/fetch.md +0 -16
- package/src/prompts/tools/find.md +0 -18
- package/src/prompts/tools/gemini-image.md +0 -23
- package/src/prompts/tools/grep.md +0 -28
- package/src/prompts/tools/hashline.md +0 -232
- package/src/prompts/tools/librarian.md +0 -24
- package/src/prompts/tools/lsp.md +0 -28
- package/src/prompts/tools/oracle.md +0 -26
- package/src/prompts/tools/patch.md +0 -74
- package/src/prompts/tools/python.md +0 -66
- package/src/prompts/tools/read.md +0 -36
- package/src/prompts/tools/replace.md +0 -38
- package/src/prompts/tools/reviewer.md +0 -41
- package/src/prompts/tools/ssh.md +0 -51
- package/src/prompts/tools/task-summary.md +0 -28
- package/src/prompts/tools/task.md +0 -146
- package/src/prompts/tools/todo-write.md +0 -65
- package/src/prompts/tools/undo-edit.md +0 -7
- package/src/prompts/tools/web-search.md +0 -19
- package/src/prompts/tools/write.md +0 -18
- package/src/task/batch.ts +0 -102
- package/src/task/discovery.ts +0 -126
- package/src/task/parallel.ts +0 -84
- package/src/task/template.ts +0 -32
- package/src/tools/calculator.ts +0 -537
- package/src/tools/jtd-to-typescript.ts +0 -198
- package/src/tools/renderers.ts +0 -60
- package/src/tools/tool-result.ts +0 -86
- /package/src/{modes/theme → theme}/dark.json +0 -0
- /package/src/{modes/theme → theme}/defaults/dark-catppuccin.json +0 -0
- /package/src/{modes/theme → theme}/defaults/dark-dracula.json +0 -0
- /package/src/{modes/theme → theme}/defaults/dark-gruvbox.json +0 -0
- /package/src/{modes/theme → theme}/defaults/dark-solarized.json +0 -0
- /package/src/{modes/theme → theme}/defaults/dark-tokyo-night.json +0 -0
- /package/src/{modes/theme → theme}/defaults/index.ts +0 -0
- /package/src/{modes/theme → theme}/defaults/light-catppuccin.json +0 -0
- /package/src/{modes/theme → theme}/defaults/light-github.json +0 -0
- /package/src/{modes/theme → theme}/defaults/light-solarized.json +0 -0
- /package/src/{modes/theme → theme}/light.json +0 -0
- /package/src/{modes/theme → theme}/mermaid-cache.ts +0 -0
- /package/src/{modes/theme → theme}/theme-schema.json +0 -0
- /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
|
+
}
|