@oh-my-pi/pi-coding-agent 13.18.0 → 14.0.2
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 +316 -1
- package/package.json +86 -24
- package/scripts/format-prompts.ts +2 -2
- package/src/autoresearch/apply-contract-to-state.ts +24 -0
- package/src/autoresearch/contract.ts +0 -44
- package/src/autoresearch/dashboard.ts +1 -2
- package/src/autoresearch/git.ts +116 -30
- package/src/autoresearch/helpers.ts +49 -0
- package/src/autoresearch/index.ts +28 -187
- package/src/autoresearch/prompt.md +26 -9
- package/src/autoresearch/state.ts +0 -6
- package/src/autoresearch/tools/init-experiment.ts +202 -117
- package/src/autoresearch/tools/log-experiment.ts +123 -178
- package/src/autoresearch/tools/run-experiment.ts +48 -10
- package/src/autoresearch/types.ts +2 -2
- package/src/capability/index.ts +4 -2
- package/src/cli/file-processor.ts +3 -3
- package/src/cli/grep-cli.ts +8 -8
- package/src/cli/grievances-cli.ts +78 -0
- package/src/cli/read-cli.ts +67 -0
- package/src/cli/setup-cli.ts +4 -4
- package/src/cli/update-cli.ts +3 -3
- package/src/cli.ts +2 -0
- package/src/commands/grep.ts +6 -1
- package/src/commands/grievances.ts +20 -0
- package/src/commands/read.ts +33 -0
- package/src/commit/agentic/agent.ts +5 -8
- package/src/commit/agentic/index.ts +22 -26
- package/src/commit/agentic/tools/analyze-file.ts +3 -3
- package/src/commit/agentic/tools/git-file-diff.ts +3 -6
- package/src/commit/agentic/tools/git-hunk.ts +3 -3
- package/src/commit/agentic/tools/git-overview.ts +6 -9
- package/src/commit/agentic/tools/index.ts +6 -8
- package/src/commit/agentic/tools/propose-commit.ts +4 -7
- package/src/commit/agentic/tools/recent-commits.ts +3 -3
- package/src/commit/agentic/tools/split-commit.ts +4 -4
- package/src/commit/agentic/validation.ts +1 -1
- package/src/commit/analysis/conventional.ts +4 -4
- package/src/commit/analysis/summary.ts +3 -3
- package/src/commit/changelog/generate.ts +4 -4
- package/src/commit/changelog/index.ts +5 -9
- package/src/commit/map-reduce/map-phase.ts +4 -4
- package/src/commit/map-reduce/reduce-phase.ts +4 -4
- package/src/commit/pipeline.ts +13 -16
- package/src/config/keybindings.ts +7 -6
- package/src/config/prompt-templates.ts +44 -226
- package/src/config/resolve-config-value.ts +4 -2
- package/src/config/settings-schema.ts +98 -2
- package/src/config/settings.ts +25 -26
- package/src/dap/client.ts +674 -0
- package/src/dap/config.ts +150 -0
- package/src/dap/defaults.json +211 -0
- package/src/dap/index.ts +4 -0
- package/src/dap/session.ts +1255 -0
- package/src/dap/types.ts +600 -0
- package/src/debug/log-viewer.ts +3 -2
- package/src/discovery/builtin.ts +1 -2
- package/src/discovery/codex.ts +2 -2
- package/src/discovery/github.ts +2 -1
- package/src/discovery/helpers.ts +2 -2
- package/src/discovery/opencode.ts +2 -2
- package/src/edit/diff.ts +818 -0
- package/src/edit/index.ts +309 -0
- package/src/edit/line-hash.ts +67 -0
- package/src/edit/modes/chunk.ts +454 -0
- package/src/{patch → edit/modes}/hashline.ts +741 -361
- package/src/{patch/applicator.ts → edit/modes/patch.ts} +420 -117
- package/src/{patch/fuzzy.ts → edit/modes/replace.ts} +519 -197
- package/src/{patch → edit}/normalize.ts +97 -76
- package/src/{patch/shared.ts → edit/renderer.ts} +181 -108
- package/src/exec/bash-executor.ts +4 -2
- package/src/exec/idle-timeout-watchdog.ts +126 -0
- package/src/exec/non-interactive-env.ts +5 -0
- package/src/extensibility/custom-commands/bundled/ci-green/index.ts +6 -18
- package/src/extensibility/custom-commands/bundled/review/index.ts +45 -43
- package/src/extensibility/custom-commands/loader.ts +1 -2
- package/src/extensibility/custom-tools/loader.ts +34 -11
- package/src/extensibility/custom-tools/types.ts +1 -1
- package/src/extensibility/extensions/loader.ts +9 -4
- package/src/extensibility/extensions/runner.ts +24 -1
- package/src/extensibility/extensions/types.ts +4 -2
- package/src/extensibility/hooks/loader.ts +5 -6
- package/src/extensibility/hooks/types.ts +2 -2
- package/src/extensibility/plugins/doctor.ts +2 -1
- package/src/extensibility/plugins/marketplace/fetcher.ts +2 -57
- package/src/extensibility/plugins/marketplace/source-resolver.ts +4 -4
- package/src/extensibility/slash-commands.ts +3 -7
- package/src/index.ts +3 -1
- package/src/internal-urls/docs-index.generated.ts +11 -11
- package/src/ipy/executor.ts +58 -17
- package/src/ipy/gateway-coordinator.ts +6 -4
- package/src/ipy/kernel.ts +45 -22
- package/src/ipy/runtime.ts +2 -2
- package/src/lsp/client.ts +7 -4
- package/src/lsp/clients/lsp-linter-client.ts +4 -4
- package/src/lsp/config.ts +2 -2
- package/src/lsp/defaults.json +688 -154
- package/src/lsp/index.ts +234 -45
- package/src/lsp/lspmux.ts +2 -2
- package/src/lsp/startup-events.ts +13 -0
- package/src/lsp/types.ts +12 -1
- package/src/lsp/utils.ts +8 -1
- package/src/main.ts +125 -47
- package/src/memories/index.ts +4 -5
- package/src/modes/acp/acp-agent.ts +563 -163
- package/src/modes/acp/acp-event-mapper.ts +9 -1
- package/src/modes/acp/acp-mode.ts +4 -2
- package/src/modes/components/agent-dashboard.ts +3 -4
- package/src/modes/components/diff.ts +6 -7
- package/src/modes/components/footer.ts +9 -29
- package/src/modes/components/hook-editor.ts +3 -3
- package/src/modes/components/hook-selector.ts +6 -1
- package/src/modes/components/read-tool-group.ts +6 -12
- package/src/modes/components/session-observer-overlay.ts +472 -0
- package/src/modes/components/settings-defs.ts +24 -0
- package/src/modes/components/status-line.ts +15 -61
- package/src/modes/components/tool-execution.ts +1 -1
- package/src/modes/components/welcome.ts +1 -1
- package/src/modes/controllers/btw-controller.ts +2 -2
- package/src/modes/controllers/command-controller.ts +4 -2
- package/src/modes/controllers/event-controller.ts +59 -2
- package/src/modes/controllers/extension-ui-controller.ts +1 -0
- package/src/modes/controllers/input-controller.ts +15 -8
- package/src/modes/controllers/selector-controller.ts +26 -0
- package/src/modes/index.ts +20 -2
- package/src/modes/interactive-mode.ts +278 -69
- package/src/modes/rpc/host-tools.ts +186 -0
- package/src/modes/rpc/rpc-client.ts +178 -13
- package/src/modes/rpc/rpc-mode.ts +73 -3
- package/src/modes/rpc/rpc-types.ts +53 -1
- package/src/modes/session-observer-registry.ts +146 -0
- package/src/modes/shared.ts +0 -42
- package/src/modes/theme/theme.ts +80 -8
- package/src/modes/types.ts +4 -2
- package/src/modes/utils/keybinding-matchers.ts +9 -0
- package/src/prompts/system/custom-system-prompt.md +5 -0
- package/src/prompts/system/system-prompt.md +8 -1
- package/src/prompts/tools/chunk-edit.md +219 -0
- package/src/prompts/tools/debug.md +43 -0
- package/src/prompts/tools/grep.md +3 -0
- package/src/prompts/tools/lsp.md +5 -5
- package/src/prompts/tools/read-chunk.md +17 -0
- package/src/prompts/tools/read.md +19 -5
- package/src/sdk.ts +216 -165
- package/src/secrets/index.ts +1 -1
- package/src/secrets/obfuscator.ts +25 -17
- package/src/session/agent-session.ts +381 -286
- package/src/session/agent-storage.ts +12 -12
- package/src/session/compaction/branch-summarization.ts +3 -3
- package/src/session/compaction/compaction.ts +5 -6
- package/src/session/compaction/utils.ts +3 -3
- package/src/session/history-storage.ts +62 -19
- package/src/session/messages.ts +3 -3
- package/src/session/session-dump-format.ts +203 -0
- package/src/session/session-manager.ts +15 -5
- package/src/session/session-storage.ts +4 -2
- package/src/session/streaming-output.ts +1 -1
- package/src/session/tool-choice-queue.ts +213 -0
- package/src/slash-commands/builtin-registry.ts +56 -8
- package/src/ssh/connection-manager.ts +2 -2
- package/src/ssh/sshfs-mount.ts +5 -5
- package/src/stt/downloader.ts +4 -4
- package/src/stt/recorder.ts +4 -4
- package/src/stt/transcriber.ts +2 -2
- package/src/system-prompt.ts +25 -13
- package/src/task/agents.ts +5 -6
- package/src/task/commands.ts +2 -5
- package/src/task/executor.ts +32 -4
- package/src/task/index.ts +91 -82
- package/src/task/template.ts +2 -2
- package/src/task/types.ts +25 -0
- package/src/task/worktree.ts +131 -149
- package/src/tools/ask.ts +2 -3
- package/src/tools/ast-edit.ts +7 -7
- package/src/tools/ast-grep.ts +7 -7
- package/src/tools/auto-generated-guard.ts +36 -41
- package/src/tools/await-tool.ts +2 -2
- package/src/tools/bash.ts +5 -23
- package/src/tools/browser.ts +4 -5
- package/src/tools/calculator.ts +2 -3
- package/src/tools/cancel-job.ts +2 -2
- package/src/tools/checkpoint.ts +3 -3
- package/src/tools/debug.ts +1007 -0
- package/src/tools/exit-plan-mode.ts +3 -3
- package/src/tools/fetch.ts +67 -3
- package/src/tools/find.ts +4 -5
- package/src/tools/fs-cache-invalidation.ts +5 -0
- package/src/tools/gemini-image.ts +13 -5
- package/src/tools/gh.ts +130 -308
- package/src/tools/grep.ts +57 -9
- package/src/tools/index.ts +44 -22
- package/src/tools/inspect-image.ts +4 -4
- package/src/tools/output-meta.ts +1 -1
- package/src/tools/python.ts +19 -6
- package/src/tools/read.ts +211 -146
- package/src/tools/render-mermaid.ts +2 -3
- package/src/tools/render-utils.ts +20 -6
- package/src/tools/renderers.ts +3 -1
- package/src/tools/report-tool-issue.ts +80 -0
- package/src/tools/resolve.ts +70 -39
- package/src/tools/search-tool-bm25.ts +2 -2
- package/src/tools/ssh.ts +2 -2
- package/src/tools/todo-write.ts +2 -2
- package/src/tools/tool-timeouts.ts +1 -0
- package/src/tools/write.ts +5 -6
- package/src/tui/tree-list.ts +3 -1
- package/src/utils/clipboard.ts +80 -0
- package/src/utils/commit-message-generator.ts +2 -3
- package/src/utils/edit-mode.ts +49 -0
- package/src/utils/external-editor.ts +11 -5
- package/src/utils/file-display-mode.ts +6 -5
- package/src/utils/file-mentions.ts +8 -7
- package/src/utils/git.ts +1400 -0
- package/src/utils/image-loading.ts +98 -0
- package/src/utils/title-generator.ts +2 -3
- package/src/utils/tools-manager.ts +6 -6
- package/src/web/scrapers/choosealicense.ts +1 -1
- package/src/web/search/index.ts +3 -3
- package/src/web/search/render.ts +6 -4
- package/src/autoresearch/command-initialize.md +0 -34
- package/src/commit/git/errors.ts +0 -9
- package/src/commit/git/index.ts +0 -210
- package/src/commit/git/operations.ts +0 -54
- package/src/patch/diff.ts +0 -433
- package/src/patch/index.ts +0 -888
- package/src/patch/parser.ts +0 -532
- package/src/patch/types.ts +0 -292
- package/src/prompts/agents/oracle.md +0 -77
- package/src/tools/gh-cli.ts +0 -125
- package/src/tools/pending-action.ts +0 -49
- package/src/utils/child-process.ts +0 -88
- package/src/utils/frontmatter.ts +0 -117
- package/src/utils/image-input.ts +0 -274
- package/src/utils/mime.ts +0 -53
- package/src/utils/prompt-format.ts +0 -170
package/src/patch/index.ts
DELETED
|
@@ -1,888 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Edit tool module.
|
|
3
|
-
*
|
|
4
|
-
* Supports three modes:
|
|
5
|
-
* - Replace mode (default): oldText/newText replacement with fuzzy matching
|
|
6
|
-
* - Patch mode: structured diff format with explicit operation type
|
|
7
|
-
* - Hashline mode: line-addressed edits using content hashes for integrity
|
|
8
|
-
*
|
|
9
|
-
* The mode is determined by the `edit.mode` setting.
|
|
10
|
-
*/
|
|
11
|
-
import * as fs from "node:fs/promises";
|
|
12
|
-
import * as nodePath from "node:path";
|
|
13
|
-
import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
|
|
14
|
-
import { StringEnum } from "@oh-my-pi/pi-ai";
|
|
15
|
-
import { type Static, Type } from "@sinclair/typebox";
|
|
16
|
-
import { renderPromptTemplate } from "../config/prompt-templates";
|
|
17
|
-
import {
|
|
18
|
-
createLspWritethrough,
|
|
19
|
-
type FileDiagnosticsResult,
|
|
20
|
-
flushLspWritethroughBatch,
|
|
21
|
-
type WritethroughCallback,
|
|
22
|
-
writethroughNoop,
|
|
23
|
-
} from "../lsp";
|
|
24
|
-
import hashlineDescription from "../prompts/tools/hashline.md" with { type: "text" };
|
|
25
|
-
import patchDescription from "../prompts/tools/patch.md" with { type: "text" };
|
|
26
|
-
import replaceDescription from "../prompts/tools/replace.md" with { type: "text" };
|
|
27
|
-
import type { ToolSession } from "../tools";
|
|
28
|
-
import { checkAutoGeneratedFile, checkAutoGeneratedFileContent } from "../tools/auto-generated-guard";
|
|
29
|
-
import {
|
|
30
|
-
invalidateFsScanAfterDelete,
|
|
31
|
-
invalidateFsScanAfterRename,
|
|
32
|
-
invalidateFsScanAfterWrite,
|
|
33
|
-
} from "../tools/fs-cache-invalidation";
|
|
34
|
-
import { outputMeta } from "../tools/output-meta";
|
|
35
|
-
import { enforcePlanModeWrite, resolvePlanPath } from "../tools/plan-mode-guard";
|
|
36
|
-
import { applyPatch } from "./applicator";
|
|
37
|
-
import { generateDiffString, generateUnifiedDiffString, replaceText } from "./diff";
|
|
38
|
-
import { findMatch } from "./fuzzy";
|
|
39
|
-
import {
|
|
40
|
-
type Anchor,
|
|
41
|
-
applyHashlineEdits,
|
|
42
|
-
buildCompactHashlineDiffPreview,
|
|
43
|
-
computeLineHash,
|
|
44
|
-
type HashlineEdit,
|
|
45
|
-
parseTag,
|
|
46
|
-
} from "./hashline";
|
|
47
|
-
import { detectLineEnding, normalizeToLF, restoreLineEndings, stripBom } from "./normalize";
|
|
48
|
-
import { type EditToolDetails, getLspBatchRequest } from "./shared";
|
|
49
|
-
// Internal imports
|
|
50
|
-
import type { FileSystem, Operation, PatchInput } from "./types";
|
|
51
|
-
import { EditMatchError } from "./types";
|
|
52
|
-
|
|
53
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
54
|
-
// Re-exports
|
|
55
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
56
|
-
|
|
57
|
-
// Application
|
|
58
|
-
export { applyPatch, defaultFileSystem, previewPatch } from "./applicator";
|
|
59
|
-
// Diff generation
|
|
60
|
-
export * from "./diff";
|
|
61
|
-
|
|
62
|
-
// Fuzzy matching
|
|
63
|
-
export * from "./fuzzy";
|
|
64
|
-
// Hashline
|
|
65
|
-
export * from "./hashline";
|
|
66
|
-
// Normalization
|
|
67
|
-
export * from "./normalize";
|
|
68
|
-
// Parsing
|
|
69
|
-
export { normalizeCreateContent, normalizeDiff, parseHunks as parseDiffHunks } from "./parser";
|
|
70
|
-
export type { EditRenderContext, EditToolDetails } from "./shared";
|
|
71
|
-
// Rendering
|
|
72
|
-
export { editToolRenderer, getLspBatchRequest } from "./shared";
|
|
73
|
-
export * from "./types";
|
|
74
|
-
|
|
75
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
76
|
-
// Schemas
|
|
77
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
78
|
-
|
|
79
|
-
const replaceEditSchema = Type.Object({
|
|
80
|
-
path: Type.String({ description: "File path (relative or absolute)" }),
|
|
81
|
-
old_text: Type.String({ description: "Text to find (fuzzy whitespace matching enabled)" }),
|
|
82
|
-
new_text: Type.String({ description: "Replacement text" }),
|
|
83
|
-
all: Type.Optional(Type.Boolean({ description: "Replace all occurrences (default: unique match required)" })),
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
const patchEditSchema = Type.Object({
|
|
87
|
-
path: Type.String({ description: "File path" }),
|
|
88
|
-
op: Type.Optional(
|
|
89
|
-
StringEnum(["create", "delete", "update"], {
|
|
90
|
-
description: "Operation (default: update)",
|
|
91
|
-
}),
|
|
92
|
-
),
|
|
93
|
-
rename: Type.Optional(Type.String({ description: "New path for move" })),
|
|
94
|
-
diff: Type.Optional(Type.String({ description: "Diff hunks (update) or full content (create)" })),
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
export type ReplaceParams = Static<typeof replaceEditSchema>;
|
|
98
|
-
export type PatchParams = Static<typeof patchEditSchema>;
|
|
99
|
-
|
|
100
|
-
/**
|
|
101
|
-
* Pattern matching hashline display format prefixes: `LINE#ID:CONTENT`, `#ID:CONTENT`, and `+ID:CONTENT`.
|
|
102
|
-
* A plus-prefixed form appears in diff-like output and should be treated as hashline metadata too.
|
|
103
|
-
*/
|
|
104
|
-
const HASHLINE_PREFIX_RE = /^\s*(?:>>>|>>)?\s*(?:\+?\s*(?:\d+\s*#\s*|#\s*)|\+)\s*[ZPMQVRWSNKTXJBYH]{2}:/;
|
|
105
|
-
const HASHLINE_PREFIX_PLUS_RE = /^\s*(?:>>>|>>)?\s*\+\s*(?:\d+\s*#\s*|#\s*)?[ZPMQVRWSNKTXJBYH]{2}:/;
|
|
106
|
-
|
|
107
|
-
/** Pattern matching a unified-diff added-line `+` prefix (but not `++`). Does NOT match `-` to avoid corrupting Markdown list items. */
|
|
108
|
-
const DIFF_PLUS_RE = /^[+](?![+])/;
|
|
109
|
-
|
|
110
|
-
/**
|
|
111
|
-
* Strip hashline display prefixes and diff `+` markers from replacement lines.
|
|
112
|
-
*
|
|
113
|
-
* Models frequently copy the `LINE#ID ` prefix from read output into their
|
|
114
|
-
* replacement content, or include unified-diff `+` prefixes. Both corrupt the
|
|
115
|
-
* output file. This strips them heuristically before application.
|
|
116
|
-
*/
|
|
117
|
-
export function stripNewLinePrefixes(lines: string[]): string[] {
|
|
118
|
-
// Hashline prefixes are highly specific to read output and are usually stripped only when
|
|
119
|
-
// *every* non-empty line carries one. If a line is prefixed as `+ID:`, strip that
|
|
120
|
-
// prefix while leaving other `+` lines untouched to avoid corrupting mixed snippets.
|
|
121
|
-
let hashPrefixCount = 0;
|
|
122
|
-
let diffPlusHashPrefixCount = 0;
|
|
123
|
-
let diffPlusCount = 0;
|
|
124
|
-
let nonEmpty = 0;
|
|
125
|
-
for (const l of lines) {
|
|
126
|
-
if (l.length === 0) continue;
|
|
127
|
-
nonEmpty++;
|
|
128
|
-
if (HASHLINE_PREFIX_RE.test(l)) hashPrefixCount++;
|
|
129
|
-
if (HASHLINE_PREFIX_PLUS_RE.test(l)) diffPlusHashPrefixCount++;
|
|
130
|
-
if (DIFF_PLUS_RE.test(l)) diffPlusCount++;
|
|
131
|
-
}
|
|
132
|
-
if (nonEmpty === 0) return lines;
|
|
133
|
-
|
|
134
|
-
const stripHash = hashPrefixCount > 0 && hashPrefixCount === nonEmpty;
|
|
135
|
-
const stripPlus =
|
|
136
|
-
!stripHash && diffPlusHashPrefixCount === 0 && diffPlusCount > 0 && diffPlusCount >= nonEmpty * 0.5;
|
|
137
|
-
if (!stripHash && !stripPlus && diffPlusHashPrefixCount === 0) return lines;
|
|
138
|
-
|
|
139
|
-
return lines.map(l => {
|
|
140
|
-
if (stripHash) return l.replace(HASHLINE_PREFIX_RE, "");
|
|
141
|
-
if (stripPlus) return l.replace(DIFF_PLUS_RE, "");
|
|
142
|
-
if (diffPlusHashPrefixCount > 0 && HASHLINE_PREFIX_PLUS_RE.test(l)) return l.replace(HASHLINE_PREFIX_RE, "");
|
|
143
|
-
return l;
|
|
144
|
-
});
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
/**
|
|
148
|
-
* Strip hashline display prefixes only (no diff markers).
|
|
149
|
-
*
|
|
150
|
-
* Unlike {@link stripNewLinePrefixes} which also handles `+` diff markers,
|
|
151
|
-
* this only strips `LINE#ID:` / `#ID:` prefixes. Used by the write tool
|
|
152
|
-
* where diff markers are not applicable.
|
|
153
|
-
*
|
|
154
|
-
* Returns the original array reference when no stripping is needed.
|
|
155
|
-
*/
|
|
156
|
-
export function stripHashlinePrefixes(lines: string[]): string[] {
|
|
157
|
-
let hashPrefixCount = 0;
|
|
158
|
-
let nonEmpty = 0;
|
|
159
|
-
for (const l of lines) {
|
|
160
|
-
if (l.length === 0) continue;
|
|
161
|
-
nonEmpty++;
|
|
162
|
-
if (HASHLINE_PREFIX_RE.test(l)) hashPrefixCount++;
|
|
163
|
-
}
|
|
164
|
-
if (nonEmpty === 0 || hashPrefixCount !== nonEmpty) return lines;
|
|
165
|
-
return lines.map(l => l.replace(HASHLINE_PREFIX_RE, ""));
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
export function hashlineParseText(edit: string[] | string | null): string[] {
|
|
169
|
-
if (edit === null) return [];
|
|
170
|
-
if (typeof edit === "string") {
|
|
171
|
-
const normalizedEdit = edit.endsWith("\n") ? edit.slice(0, -1) : edit;
|
|
172
|
-
edit = normalizedEdit.replaceAll("\r", "").split("\n");
|
|
173
|
-
}
|
|
174
|
-
return stripNewLinePrefixes(edit);
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
const linesSchema = Type.Union([
|
|
178
|
-
Type.Array(Type.String(), { description: "content (preferred format)" }),
|
|
179
|
-
Type.String(),
|
|
180
|
-
Type.Null(),
|
|
181
|
-
]);
|
|
182
|
-
|
|
183
|
-
const locSchema = Type.Union(
|
|
184
|
-
[
|
|
185
|
-
Type.Literal("append"),
|
|
186
|
-
Type.Literal("prepend"),
|
|
187
|
-
Type.Object({ append: Type.String({ description: "anchor" }) }),
|
|
188
|
-
Type.Object({ prepend: Type.String({ description: "anchor" }) }),
|
|
189
|
-
Type.Object({
|
|
190
|
-
range: Type.Object({
|
|
191
|
-
pos: Type.String({ description: "first line to edit (inclusive)" }),
|
|
192
|
-
end: Type.String({ description: "last line to edit (inclusive)" }),
|
|
193
|
-
}),
|
|
194
|
-
}),
|
|
195
|
-
],
|
|
196
|
-
{ description: "insert location" },
|
|
197
|
-
);
|
|
198
|
-
|
|
199
|
-
const hashlineEditSchema = Type.Object(
|
|
200
|
-
{
|
|
201
|
-
loc: locSchema,
|
|
202
|
-
content: linesSchema,
|
|
203
|
-
},
|
|
204
|
-
{ additionalProperties: false },
|
|
205
|
-
);
|
|
206
|
-
|
|
207
|
-
const hashlineEditParamsSchema = Type.Object(
|
|
208
|
-
{
|
|
209
|
-
path: Type.String({ description: "path" }),
|
|
210
|
-
edits: Type.Array(hashlineEditSchema, { description: "edits over $path" }),
|
|
211
|
-
delete: Type.Optional(Type.Boolean({ description: "If true, delete $path" })),
|
|
212
|
-
move: Type.Optional(Type.String({ description: "If set, move $path to $move" })),
|
|
213
|
-
},
|
|
214
|
-
{ additionalProperties: false },
|
|
215
|
-
);
|
|
216
|
-
|
|
217
|
-
export type HashlineToolEdit = Static<typeof hashlineEditSchema>;
|
|
218
|
-
export type HashlineParams = Static<typeof hashlineEditParamsSchema>;
|
|
219
|
-
|
|
220
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
221
|
-
// Resilient anchor resolution
|
|
222
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
223
|
-
|
|
224
|
-
/**
|
|
225
|
-
* Map loc/content tool-schema edits into typed HashlineEdit objects.
|
|
226
|
-
*
|
|
227
|
-
* Each edit entry has a `loc` (where to edit) and `content` (what to insert/replace).
|
|
228
|
-
* loc can be:
|
|
229
|
-
* - "append" / "prepend" — file-level insert
|
|
230
|
-
* - { append: anchor } / { prepend: anchor } — insert relative to anchor
|
|
231
|
-
* - { range: { pos, end } } — replace inclusive range
|
|
232
|
-
*/
|
|
233
|
-
function resolveEditAnchors(edits: HashlineToolEdit[]): HashlineEdit[] {
|
|
234
|
-
const result: HashlineEdit[] = [];
|
|
235
|
-
for (const edit of edits) {
|
|
236
|
-
const lines = hashlineParseText(edit.content);
|
|
237
|
-
const loc = edit.loc;
|
|
238
|
-
|
|
239
|
-
if (loc === "append") {
|
|
240
|
-
result.push({ op: "append_file", lines });
|
|
241
|
-
} else if (loc === "prepend") {
|
|
242
|
-
result.push({ op: "prepend_file", lines });
|
|
243
|
-
} else if (typeof loc === "object") {
|
|
244
|
-
if ("append" in loc) {
|
|
245
|
-
const anchor = tryParseTag(loc.append);
|
|
246
|
-
if (!anchor) throw new Error("append requires a valid anchor.");
|
|
247
|
-
result.push({ op: "append_at", pos: anchor, lines });
|
|
248
|
-
} else if ("prepend" in loc) {
|
|
249
|
-
const anchor = tryParseTag(loc.prepend);
|
|
250
|
-
if (!anchor) throw new Error("prepend requires a valid anchor.");
|
|
251
|
-
result.push({ op: "prepend_at", pos: anchor, lines });
|
|
252
|
-
} else if ("range" in loc) {
|
|
253
|
-
const posAnchor = tryParseTag(loc.range.pos);
|
|
254
|
-
const endAnchor = tryParseTag(loc.range.end);
|
|
255
|
-
if (!posAnchor || !endAnchor) throw new Error("range requires valid pos and end anchors.");
|
|
256
|
-
result.push({ op: "replace_range", pos: posAnchor, end: endAnchor, lines });
|
|
257
|
-
} else {
|
|
258
|
-
throw new Error("Unknown loc shape. Expected append, prepend, or range.");
|
|
259
|
-
}
|
|
260
|
-
} else {
|
|
261
|
-
throw new Error(`Invalid loc value: ${JSON.stringify(loc)}`);
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
return result;
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
/** Parse a tag, returning undefined instead of throwing on garbage. */
|
|
268
|
-
function tryParseTag(raw: string): Anchor | undefined {
|
|
269
|
-
try {
|
|
270
|
-
return parseTag(raw);
|
|
271
|
-
} catch {
|
|
272
|
-
return undefined;
|
|
273
|
-
}
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
277
|
-
// LSP FileSystem for patch mode
|
|
278
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
279
|
-
|
|
280
|
-
class LspFileSystem implements FileSystem {
|
|
281
|
-
#lastDiagnostics: FileDiagnosticsResult | undefined;
|
|
282
|
-
#fileCache: Record<string, Bun.BunFile> = {};
|
|
283
|
-
|
|
284
|
-
constructor(
|
|
285
|
-
private readonly writethrough: (
|
|
286
|
-
dst: string,
|
|
287
|
-
content: string,
|
|
288
|
-
signal?: AbortSignal,
|
|
289
|
-
file?: import("bun").BunFile,
|
|
290
|
-
batch?: { id: string; flush: boolean },
|
|
291
|
-
) => Promise<FileDiagnosticsResult | undefined>,
|
|
292
|
-
private readonly signal?: AbortSignal,
|
|
293
|
-
private readonly batchRequest?: { id: string; flush: boolean },
|
|
294
|
-
) {}
|
|
295
|
-
|
|
296
|
-
#getFile(path: string): Bun.BunFile {
|
|
297
|
-
if (this.#fileCache[path]) {
|
|
298
|
-
return this.#fileCache[path];
|
|
299
|
-
}
|
|
300
|
-
const file = Bun.file(path);
|
|
301
|
-
this.#fileCache[path] = file;
|
|
302
|
-
return file;
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
async exists(path: string): Promise<boolean> {
|
|
306
|
-
return this.#getFile(path).exists();
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
async read(path: string): Promise<string> {
|
|
310
|
-
return this.#getFile(path).text();
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
async readBinary(path: string): Promise<Uint8Array> {
|
|
314
|
-
const buffer = await this.#getFile(path).arrayBuffer();
|
|
315
|
-
return new Uint8Array(buffer);
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
async write(path: string, content: string): Promise<void> {
|
|
319
|
-
const file = this.#getFile(path);
|
|
320
|
-
const result = await this.writethrough(path, content, this.signal, file, this.batchRequest);
|
|
321
|
-
if (result) {
|
|
322
|
-
this.#lastDiagnostics = result;
|
|
323
|
-
}
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
async delete(path: string): Promise<void> {
|
|
327
|
-
await this.#getFile(path).unlink();
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
async mkdir(path: string): Promise<void> {
|
|
331
|
-
await fs.mkdir(path, { recursive: true });
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
getDiagnostics(): FileDiagnosticsResult | undefined {
|
|
335
|
-
return this.#lastDiagnostics;
|
|
336
|
-
}
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
function mergeDiagnosticsWithWarnings(
|
|
340
|
-
diagnostics: FileDiagnosticsResult | undefined,
|
|
341
|
-
warnings: string[],
|
|
342
|
-
): FileDiagnosticsResult | undefined {
|
|
343
|
-
if (warnings.length === 0) return diagnostics;
|
|
344
|
-
const warningMessages = warnings.map(warning => `patch: ${warning}`);
|
|
345
|
-
if (!diagnostics) {
|
|
346
|
-
return {
|
|
347
|
-
server: "patch",
|
|
348
|
-
messages: warningMessages,
|
|
349
|
-
summary: `Patch warnings: ${warnings.length}`,
|
|
350
|
-
errored: false,
|
|
351
|
-
};
|
|
352
|
-
}
|
|
353
|
-
return {
|
|
354
|
-
...diagnostics,
|
|
355
|
-
messages: [...warningMessages, ...diagnostics.messages],
|
|
356
|
-
summary: `${diagnostics.summary}; Patch warnings: ${warnings.length}`,
|
|
357
|
-
};
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
361
|
-
// Tool Class
|
|
362
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
363
|
-
|
|
364
|
-
type TInput = typeof replaceEditSchema | typeof patchEditSchema | typeof hashlineEditParamsSchema;
|
|
365
|
-
|
|
366
|
-
export type EditMode = "replace" | "patch" | "hashline";
|
|
367
|
-
|
|
368
|
-
export const DEFAULT_EDIT_MODE: EditMode = "hashline";
|
|
369
|
-
|
|
370
|
-
const EDIT_MODES = ["replace", "patch", "hashline"] as const satisfies readonly EditMode[];
|
|
371
|
-
const EDIT_ID = Object.fromEntries(EDIT_MODES.map(mode => [mode, mode])) satisfies Record<string, EditMode>;
|
|
372
|
-
export const normalizeEditMode = (mode?: string | null): EditMode | undefined => EDIT_ID[mode ?? ""];
|
|
373
|
-
|
|
374
|
-
function isHashlineParams(params: ReplaceParams | PatchParams | HashlineParams): params is HashlineParams {
|
|
375
|
-
return "edits" in params;
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
function isReplaceParams(params: ReplaceParams | PatchParams | HashlineParams): params is ReplaceParams {
|
|
379
|
-
return "old_text" in params && "new_text" in params;
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
/**
|
|
383
|
-
* Edit tool implementation.
|
|
384
|
-
*
|
|
385
|
-
* Creates replace-mode, patch-mode, or hashline-mode behavior based on session settings.
|
|
386
|
-
*/
|
|
387
|
-
export class EditTool implements AgentTool<TInput> {
|
|
388
|
-
readonly name = "edit";
|
|
389
|
-
readonly label = "Edit";
|
|
390
|
-
readonly nonAbortable = true;
|
|
391
|
-
readonly concurrency = "exclusive";
|
|
392
|
-
readonly strict = true;
|
|
393
|
-
|
|
394
|
-
readonly #allowFuzzy: boolean;
|
|
395
|
-
readonly #fuzzyThreshold: number;
|
|
396
|
-
readonly #writethrough: WritethroughCallback;
|
|
397
|
-
readonly #editMode?: EditMode | null;
|
|
398
|
-
|
|
399
|
-
constructor(private readonly session: ToolSession) {
|
|
400
|
-
const {
|
|
401
|
-
PI_EDIT_FUZZY: editFuzzy = "auto",
|
|
402
|
-
PI_EDIT_FUZZY_THRESHOLD: editFuzzyThreshold = "auto",
|
|
403
|
-
PI_EDIT_VARIANT: envEditVariant = "auto",
|
|
404
|
-
} = Bun.env;
|
|
405
|
-
|
|
406
|
-
if (envEditVariant && envEditVariant !== "auto") {
|
|
407
|
-
const editMode = normalizeEditMode(envEditVariant);
|
|
408
|
-
if (!editMode) {
|
|
409
|
-
throw new Error(`Invalid PI_EDIT_VARIANT: ${envEditVariant}`);
|
|
410
|
-
}
|
|
411
|
-
this.#editMode = editMode;
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
switch (editFuzzy) {
|
|
415
|
-
case "true":
|
|
416
|
-
case "1":
|
|
417
|
-
this.#allowFuzzy = true;
|
|
418
|
-
break;
|
|
419
|
-
case "false":
|
|
420
|
-
case "0":
|
|
421
|
-
this.#allowFuzzy = false;
|
|
422
|
-
break;
|
|
423
|
-
case "auto":
|
|
424
|
-
this.#allowFuzzy = session.settings.get("edit.fuzzyMatch");
|
|
425
|
-
break;
|
|
426
|
-
default:
|
|
427
|
-
throw new Error(`Invalid PI_EDIT_FUZZY: ${editFuzzy}`);
|
|
428
|
-
}
|
|
429
|
-
switch (editFuzzyThreshold) {
|
|
430
|
-
case "auto":
|
|
431
|
-
this.#fuzzyThreshold = session.settings.get("edit.fuzzyThreshold");
|
|
432
|
-
break;
|
|
433
|
-
default:
|
|
434
|
-
this.#fuzzyThreshold = parseFloat(editFuzzyThreshold);
|
|
435
|
-
if (Number.isNaN(this.#fuzzyThreshold) || this.#fuzzyThreshold < 0 || this.#fuzzyThreshold > 1) {
|
|
436
|
-
throw new Error(`Invalid PI_EDIT_FUZZY_THRESHOLD: ${editFuzzyThreshold}`);
|
|
437
|
-
}
|
|
438
|
-
break;
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
const enableLsp = session.enableLsp ?? true;
|
|
442
|
-
const enableDiagnostics = enableLsp && session.settings.get("lsp.diagnosticsOnEdit");
|
|
443
|
-
const enableFormat = enableLsp && session.settings.get("lsp.formatOnWrite");
|
|
444
|
-
this.#writethrough = enableLsp
|
|
445
|
-
? createLspWritethrough(session.cwd, { enableFormat, enableDiagnostics })
|
|
446
|
-
: writethroughNoop;
|
|
447
|
-
}
|
|
448
|
-
|
|
449
|
-
/**
|
|
450
|
-
* Determine edit mode dynamically based on current model.
|
|
451
|
-
* This is re-evaluated on each access so tool definitions stay current when model changes.
|
|
452
|
-
*/
|
|
453
|
-
get mode(): EditMode {
|
|
454
|
-
if (this.#editMode) return this.#editMode;
|
|
455
|
-
// 1. Check if edit mode is explicitly set for this model
|
|
456
|
-
const activeModel = this.session.getActiveModelString?.();
|
|
457
|
-
const modelVariant = this.session.settings.getEditVariantForModel(activeModel);
|
|
458
|
-
if (modelVariant) return modelVariant;
|
|
459
|
-
// 2. Check if model contains "-spark" substring (default to replace mode)
|
|
460
|
-
if (activeModel?.includes("-spark")) return "replace";
|
|
461
|
-
// 3. Check if edit mode is explicitly set in session settings
|
|
462
|
-
const settingsMode = normalizeEditMode(this.session.settings.get("edit.mode"));
|
|
463
|
-
if (settingsMode) return settingsMode;
|
|
464
|
-
// 4. Default to DEFAULT_EDIT_MODE
|
|
465
|
-
return DEFAULT_EDIT_MODE;
|
|
466
|
-
}
|
|
467
|
-
|
|
468
|
-
/**
|
|
469
|
-
* Dynamic description based on current edit mode (which depends on current model).
|
|
470
|
-
*/
|
|
471
|
-
get description(): string {
|
|
472
|
-
switch (this.mode) {
|
|
473
|
-
case "patch":
|
|
474
|
-
return renderPromptTemplate(patchDescription);
|
|
475
|
-
case "hashline":
|
|
476
|
-
return renderPromptTemplate(hashlineDescription);
|
|
477
|
-
default:
|
|
478
|
-
return renderPromptTemplate(replaceDescription);
|
|
479
|
-
}
|
|
480
|
-
}
|
|
481
|
-
|
|
482
|
-
/**
|
|
483
|
-
* Dynamic parameters schema based on current edit mode (which depends on current model).
|
|
484
|
-
*/
|
|
485
|
-
get parameters(): TInput {
|
|
486
|
-
switch (this.mode) {
|
|
487
|
-
case "patch":
|
|
488
|
-
return patchEditSchema;
|
|
489
|
-
case "hashline":
|
|
490
|
-
return hashlineEditParamsSchema;
|
|
491
|
-
default:
|
|
492
|
-
return replaceEditSchema;
|
|
493
|
-
}
|
|
494
|
-
}
|
|
495
|
-
|
|
496
|
-
async execute(
|
|
497
|
-
_toolCallId: string,
|
|
498
|
-
params: ReplaceParams | PatchParams | HashlineParams,
|
|
499
|
-
signal?: AbortSignal,
|
|
500
|
-
_onUpdate?: AgentToolUpdateCallback<EditToolDetails, TInput>,
|
|
501
|
-
context?: AgentToolContext,
|
|
502
|
-
): Promise<AgentToolResult<EditToolDetails, TInput>> {
|
|
503
|
-
const batchRequest = getLspBatchRequest(context?.toolCall);
|
|
504
|
-
|
|
505
|
-
// ─────────────────────────────────────────────────────────────────
|
|
506
|
-
// Hashline mode execution
|
|
507
|
-
// ─────────────────────────────────────────────────────────────────
|
|
508
|
-
if (this.mode === "hashline") {
|
|
509
|
-
if (!isHashlineParams(params)) {
|
|
510
|
-
throw new Error("Invalid edit parameters for hashline mode.");
|
|
511
|
-
}
|
|
512
|
-
const { path, edits, delete: deleteFile, move } = params;
|
|
513
|
-
|
|
514
|
-
enforcePlanModeWrite(this.session, path, { op: deleteFile ? "delete" : "update", move });
|
|
515
|
-
|
|
516
|
-
if (path.endsWith(".ipynb") && edits?.length > 0) {
|
|
517
|
-
throw new Error("Cannot edit Jupyter notebooks with the Edit tool. Use the NotebookEdit tool instead.");
|
|
518
|
-
}
|
|
519
|
-
|
|
520
|
-
const absolutePath = resolvePlanPath(this.session, path);
|
|
521
|
-
const resolvedMove = move ? resolvePlanPath(this.session, move) : undefined;
|
|
522
|
-
if (resolvedMove === absolutePath) {
|
|
523
|
-
throw new Error("move path is the same as source path");
|
|
524
|
-
}
|
|
525
|
-
const sourceExists = await fs.exists(absolutePath);
|
|
526
|
-
const isMoveOnly = Boolean(resolvedMove) && edits.length === 0;
|
|
527
|
-
|
|
528
|
-
if (deleteFile) {
|
|
529
|
-
if (sourceExists) {
|
|
530
|
-
await fs.unlink(absolutePath);
|
|
531
|
-
}
|
|
532
|
-
invalidateFsScanAfterDelete(absolutePath);
|
|
533
|
-
return {
|
|
534
|
-
content: [{ type: "text", text: `Deleted ${path}` }],
|
|
535
|
-
details: {
|
|
536
|
-
diff: "",
|
|
537
|
-
op: "delete",
|
|
538
|
-
meta: outputMeta().get(),
|
|
539
|
-
},
|
|
540
|
-
};
|
|
541
|
-
}
|
|
542
|
-
|
|
543
|
-
if (isMoveOnly && resolvedMove) {
|
|
544
|
-
if (!sourceExists) {
|
|
545
|
-
throw new Error(`File not found: ${path}`);
|
|
546
|
-
}
|
|
547
|
-
const parentDir = nodePath.dirname(resolvedMove);
|
|
548
|
-
if (parentDir && parentDir !== ".") {
|
|
549
|
-
await fs.mkdir(parentDir, { recursive: true });
|
|
550
|
-
}
|
|
551
|
-
// Preserve exact bytes for move-only operations, including binary files.
|
|
552
|
-
await fs.rename(absolutePath, resolvedMove);
|
|
553
|
-
invalidateFsScanAfterRename(absolutePath, resolvedMove);
|
|
554
|
-
return {
|
|
555
|
-
content: [{ type: "text", text: `Moved ${path} to ${move}` }],
|
|
556
|
-
details: {
|
|
557
|
-
diff: "",
|
|
558
|
-
op: "update",
|
|
559
|
-
move,
|
|
560
|
-
meta: outputMeta().get(),
|
|
561
|
-
},
|
|
562
|
-
};
|
|
563
|
-
}
|
|
564
|
-
|
|
565
|
-
if (!sourceExists) {
|
|
566
|
-
const lines: string[] = [];
|
|
567
|
-
for (const edit of edits) {
|
|
568
|
-
// For file creation, only anchorless appends/prepends are valid
|
|
569
|
-
if (edit.loc === "append") {
|
|
570
|
-
lines.push(...hashlineParseText(edit.content));
|
|
571
|
-
} else if (edit.loc === "prepend") {
|
|
572
|
-
lines.unshift(...hashlineParseText(edit.content));
|
|
573
|
-
} else {
|
|
574
|
-
throw new Error(`File not found: ${path}`);
|
|
575
|
-
}
|
|
576
|
-
}
|
|
577
|
-
await fs.writeFile(absolutePath, lines.join("\n"));
|
|
578
|
-
return {
|
|
579
|
-
content: [{ type: "text", text: `Created ${path}` }],
|
|
580
|
-
details: {
|
|
581
|
-
diff: "",
|
|
582
|
-
op: "create",
|
|
583
|
-
meta: outputMeta().get(),
|
|
584
|
-
},
|
|
585
|
-
};
|
|
586
|
-
}
|
|
587
|
-
|
|
588
|
-
const anchorEdits = resolveEditAnchors(edits);
|
|
589
|
-
|
|
590
|
-
const rawContent = await fs.readFile(absolutePath, "utf-8");
|
|
591
|
-
await checkAutoGeneratedFileContent(rawContent, path);
|
|
592
|
-
const { bom, text } = stripBom(rawContent);
|
|
593
|
-
const originalEnding = detectLineEnding(text);
|
|
594
|
-
const originalNormalized = normalizeToLF(text);
|
|
595
|
-
let normalizedText = originalNormalized;
|
|
596
|
-
|
|
597
|
-
// Apply anchor-based edits first (replace, append_at, prepend_at)
|
|
598
|
-
const anchorResult = applyHashlineEdits(normalizedText, anchorEdits);
|
|
599
|
-
normalizedText = anchorResult.lines;
|
|
600
|
-
|
|
601
|
-
const result = {
|
|
602
|
-
text: normalizedText,
|
|
603
|
-
firstChangedLine: anchorResult.firstChangedLine,
|
|
604
|
-
warnings: anchorResult.warnings,
|
|
605
|
-
noopEdits: anchorResult.noopEdits,
|
|
606
|
-
};
|
|
607
|
-
if (originalNormalized === result.text && !move) {
|
|
608
|
-
let diagnostic = `No changes made to ${path}. The edits produced identical content.`;
|
|
609
|
-
if (result.noopEdits && result.noopEdits.length > 0) {
|
|
610
|
-
const details = result.noopEdits
|
|
611
|
-
.map(
|
|
612
|
-
e =>
|
|
613
|
-
`Edit ${e.editIndex}: replacement for ${e.loc} is identical to current content:\n ${e.loc}| ${e.current}`,
|
|
614
|
-
)
|
|
615
|
-
.join("\n");
|
|
616
|
-
diagnostic += `\n${details}`;
|
|
617
|
-
diagnostic +=
|
|
618
|
-
"\nYour content must differ from what the file already contains. Re-read the file to see the current state.";
|
|
619
|
-
} else {
|
|
620
|
-
// Edits were not literally identical but heuristics normalized them back
|
|
621
|
-
const lines = result.text.split("\n");
|
|
622
|
-
const targetLines: string[] = [];
|
|
623
|
-
const refs: Anchor[] = [];
|
|
624
|
-
for (const edit of anchorEdits) {
|
|
625
|
-
refs.length = 0;
|
|
626
|
-
switch (edit.op) {
|
|
627
|
-
case "replace_line":
|
|
628
|
-
refs.push(edit.pos);
|
|
629
|
-
break;
|
|
630
|
-
case "replace_range":
|
|
631
|
-
refs.push(edit.end, edit.pos);
|
|
632
|
-
break;
|
|
633
|
-
case "append_at":
|
|
634
|
-
case "prepend_at":
|
|
635
|
-
refs.push(edit.pos);
|
|
636
|
-
break;
|
|
637
|
-
case "append_file":
|
|
638
|
-
case "prepend_file":
|
|
639
|
-
break;
|
|
640
|
-
}
|
|
641
|
-
|
|
642
|
-
for (const ref of refs) {
|
|
643
|
-
try {
|
|
644
|
-
if (ref.line >= 1 && ref.line <= lines.length) {
|
|
645
|
-
const text = lines[ref.line - 1];
|
|
646
|
-
const hash = computeLineHash(ref.line, text);
|
|
647
|
-
targetLines.push(`${ref.line}#${hash}:${text}`);
|
|
648
|
-
}
|
|
649
|
-
} catch {
|
|
650
|
-
/* skip malformed refs */
|
|
651
|
-
}
|
|
652
|
-
}
|
|
653
|
-
}
|
|
654
|
-
if (targetLines.length > 0) {
|
|
655
|
-
const preview = [...new Set(targetLines)].slice(0, 5).join("\n");
|
|
656
|
-
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.`;
|
|
657
|
-
}
|
|
658
|
-
}
|
|
659
|
-
throw new Error(diagnostic);
|
|
660
|
-
}
|
|
661
|
-
|
|
662
|
-
const finalContent = bom + restoreLineEndings(result.text, originalEnding);
|
|
663
|
-
const writePath = resolvedMove ?? absolutePath;
|
|
664
|
-
const diagnostics = await this.#writethrough(
|
|
665
|
-
writePath,
|
|
666
|
-
finalContent,
|
|
667
|
-
signal,
|
|
668
|
-
Bun.file(writePath),
|
|
669
|
-
batchRequest,
|
|
670
|
-
);
|
|
671
|
-
if (resolvedMove && resolvedMove !== absolutePath) {
|
|
672
|
-
await fs.unlink(absolutePath);
|
|
673
|
-
invalidateFsScanAfterRename(absolutePath, resolvedMove);
|
|
674
|
-
} else {
|
|
675
|
-
invalidateFsScanAfterWrite(absolutePath);
|
|
676
|
-
}
|
|
677
|
-
const diffResult = generateDiffString(originalNormalized, result.text);
|
|
678
|
-
|
|
679
|
-
const meta = outputMeta()
|
|
680
|
-
.diagnostics(diagnostics?.summary ?? "", diagnostics?.messages ?? [])
|
|
681
|
-
.get();
|
|
682
|
-
|
|
683
|
-
const resultText = move ? `Moved ${path} to ${move}` : `Updated ${path}`;
|
|
684
|
-
const preview = buildCompactHashlineDiffPreview(diffResult.diff);
|
|
685
|
-
const summaryLine = `Changes: +${preview.addedLines} -${preview.removedLines}${preview.preview ? "" : " (no textual diff preview)"}`;
|
|
686
|
-
const warningsBlock = result.warnings?.length ? `\n\nWarnings:\n${result.warnings.join("\n")}` : "";
|
|
687
|
-
const previewBlock = preview.preview ? `\n\nDiff preview:\n${preview.preview}` : "";
|
|
688
|
-
return {
|
|
689
|
-
content: [
|
|
690
|
-
{
|
|
691
|
-
type: "text",
|
|
692
|
-
text: `${resultText}\n${summaryLine}${previewBlock}${warningsBlock}`,
|
|
693
|
-
},
|
|
694
|
-
],
|
|
695
|
-
details: {
|
|
696
|
-
diff: diffResult.diff,
|
|
697
|
-
firstChangedLine: result.firstChangedLine ?? diffResult.firstChangedLine,
|
|
698
|
-
diagnostics,
|
|
699
|
-
op: "update",
|
|
700
|
-
move,
|
|
701
|
-
meta,
|
|
702
|
-
},
|
|
703
|
-
};
|
|
704
|
-
}
|
|
705
|
-
|
|
706
|
-
// ─────────────────────────────────────────────────────────────────
|
|
707
|
-
// Patch mode execution
|
|
708
|
-
// ─────────────────────────────────────────────────────────────────
|
|
709
|
-
if (this.mode === "patch") {
|
|
710
|
-
if (isHashlineParams(params) || isReplaceParams(params)) {
|
|
711
|
-
throw new Error("Invalid edit parameters for patch mode.");
|
|
712
|
-
}
|
|
713
|
-
const { path, op: rawOp, rename, diff } = params;
|
|
714
|
-
|
|
715
|
-
// Normalize unrecognized operations to "update"
|
|
716
|
-
const op: Operation = rawOp === "create" || rawOp === "delete" ? rawOp : "update";
|
|
717
|
-
|
|
718
|
-
enforcePlanModeWrite(this.session, path, { op, move: rename });
|
|
719
|
-
const resolvedPath = resolvePlanPath(this.session, path);
|
|
720
|
-
const resolvedRename = rename ? resolvePlanPath(this.session, rename) : undefined;
|
|
721
|
-
|
|
722
|
-
if (path.endsWith(".ipynb")) {
|
|
723
|
-
throw new Error("Cannot edit Jupyter notebooks with the Edit tool. Use the NotebookEdit tool instead.");
|
|
724
|
-
}
|
|
725
|
-
if (rename?.endsWith(".ipynb")) {
|
|
726
|
-
throw new Error("Cannot edit Jupyter notebooks with the Edit tool. Use the NotebookEdit tool instead.");
|
|
727
|
-
}
|
|
728
|
-
|
|
729
|
-
await checkAutoGeneratedFile(resolvedPath, path);
|
|
730
|
-
|
|
731
|
-
const input: PatchInput = { path: resolvedPath, op, rename: resolvedRename, diff };
|
|
732
|
-
const fs = new LspFileSystem(this.#writethrough, signal, batchRequest);
|
|
733
|
-
const result = await applyPatch(input, {
|
|
734
|
-
cwd: this.session.cwd,
|
|
735
|
-
fs,
|
|
736
|
-
fuzzyThreshold: this.#fuzzyThreshold,
|
|
737
|
-
allowFuzzy: this.#allowFuzzy,
|
|
738
|
-
});
|
|
739
|
-
if (resolvedRename) {
|
|
740
|
-
invalidateFsScanAfterRename(resolvedPath, resolvedRename);
|
|
741
|
-
} else if (result.change.type === "delete") {
|
|
742
|
-
invalidateFsScanAfterDelete(resolvedPath);
|
|
743
|
-
} else {
|
|
744
|
-
invalidateFsScanAfterWrite(resolvedPath);
|
|
745
|
-
}
|
|
746
|
-
const effRename = result.change.newPath ? rename : undefined;
|
|
747
|
-
|
|
748
|
-
// Generate diff for display
|
|
749
|
-
let diffResult: { diff: string; firstChangedLine: number | undefined } = {
|
|
750
|
-
diff: "",
|
|
751
|
-
firstChangedLine: undefined,
|
|
752
|
-
};
|
|
753
|
-
if (result.change.type === "update" && result.change.oldContent && result.change.newContent) {
|
|
754
|
-
const normalizedOld = normalizeToLF(stripBom(result.change.oldContent).text);
|
|
755
|
-
const normalizedNew = normalizeToLF(stripBom(result.change.newContent).text);
|
|
756
|
-
diffResult = generateUnifiedDiffString(normalizedOld, normalizedNew);
|
|
757
|
-
}
|
|
758
|
-
|
|
759
|
-
let resultText: string;
|
|
760
|
-
switch (result.change.type) {
|
|
761
|
-
case "create":
|
|
762
|
-
resultText = `Created ${path}`;
|
|
763
|
-
break;
|
|
764
|
-
case "delete":
|
|
765
|
-
resultText = `Deleted ${path}`;
|
|
766
|
-
break;
|
|
767
|
-
case "update":
|
|
768
|
-
resultText = effRename ? `Updated and moved ${path} to ${effRename}` : `Updated ${path}`;
|
|
769
|
-
break;
|
|
770
|
-
}
|
|
771
|
-
|
|
772
|
-
let diagnostics = fs.getDiagnostics();
|
|
773
|
-
if (op === "delete" && batchRequest?.flush) {
|
|
774
|
-
const flushedDiagnostics = await flushLspWritethroughBatch(batchRequest.id, this.session.cwd, signal);
|
|
775
|
-
diagnostics ??= flushedDiagnostics;
|
|
776
|
-
}
|
|
777
|
-
const patchWarnings = result.warnings ?? [];
|
|
778
|
-
const mergedDiagnostics = mergeDiagnosticsWithWarnings(diagnostics, patchWarnings);
|
|
779
|
-
|
|
780
|
-
const meta = outputMeta()
|
|
781
|
-
.diagnostics(mergedDiagnostics?.summary ?? "", mergedDiagnostics?.messages ?? [])
|
|
782
|
-
.get();
|
|
783
|
-
|
|
784
|
-
return {
|
|
785
|
-
content: [{ type: "text", text: resultText }],
|
|
786
|
-
details: {
|
|
787
|
-
diff: diffResult.diff,
|
|
788
|
-
firstChangedLine: diffResult.firstChangedLine,
|
|
789
|
-
diagnostics: mergedDiagnostics,
|
|
790
|
-
op,
|
|
791
|
-
move: effRename,
|
|
792
|
-
meta,
|
|
793
|
-
},
|
|
794
|
-
};
|
|
795
|
-
}
|
|
796
|
-
|
|
797
|
-
// ─────────────────────────────────────────────────────────────────
|
|
798
|
-
// Replace mode execution
|
|
799
|
-
// ─────────────────────────────────────────────────────────────────
|
|
800
|
-
if (!isReplaceParams(params)) {
|
|
801
|
-
throw new Error("Invalid edit parameters for replace mode.");
|
|
802
|
-
}
|
|
803
|
-
const { path, old_text, new_text, all } = params;
|
|
804
|
-
|
|
805
|
-
enforcePlanModeWrite(this.session, path);
|
|
806
|
-
|
|
807
|
-
if (path.endsWith(".ipynb")) {
|
|
808
|
-
throw new Error("Cannot edit Jupyter notebooks with the Edit tool. Use the NotebookEdit tool instead.");
|
|
809
|
-
}
|
|
810
|
-
|
|
811
|
-
if (old_text.length === 0) {
|
|
812
|
-
throw new Error("old_text must not be empty.");
|
|
813
|
-
}
|
|
814
|
-
|
|
815
|
-
const absolutePath = resolvePlanPath(this.session, path);
|
|
816
|
-
|
|
817
|
-
if (!(await fs.exists(absolutePath))) {
|
|
818
|
-
throw new Error(`File not found: ${path}`);
|
|
819
|
-
}
|
|
820
|
-
|
|
821
|
-
const rawContent = await fs.readFile(absolutePath, "utf-8");
|
|
822
|
-
const { bom, text: content } = stripBom(rawContent);
|
|
823
|
-
const originalEnding = detectLineEnding(content);
|
|
824
|
-
const normalizedContent = normalizeToLF(content);
|
|
825
|
-
const normalizedOldText = normalizeToLF(old_text);
|
|
826
|
-
const normalizedNewText = normalizeToLF(new_text);
|
|
827
|
-
|
|
828
|
-
const result = replaceText(normalizedContent, normalizedOldText, normalizedNewText, {
|
|
829
|
-
fuzzy: this.#allowFuzzy,
|
|
830
|
-
all: all ?? false,
|
|
831
|
-
threshold: this.#fuzzyThreshold,
|
|
832
|
-
});
|
|
833
|
-
|
|
834
|
-
if (result.count === 0) {
|
|
835
|
-
// Get error details
|
|
836
|
-
const matchOutcome = findMatch(normalizedContent, normalizedOldText, {
|
|
837
|
-
allowFuzzy: this.#allowFuzzy,
|
|
838
|
-
threshold: this.#fuzzyThreshold,
|
|
839
|
-
});
|
|
840
|
-
|
|
841
|
-
if (matchOutcome.occurrences && matchOutcome.occurrences > 1) {
|
|
842
|
-
const previews = matchOutcome.occurrencePreviews?.join("\n\n") ?? "";
|
|
843
|
-
const moreMsg = matchOutcome.occurrences > 5 ? ` (showing first 5 of ${matchOutcome.occurrences})` : "";
|
|
844
|
-
throw new Error(
|
|
845
|
-
`Found ${matchOutcome.occurrences} occurrences in ${path}${moreMsg}:\n\n${previews}\n\n` +
|
|
846
|
-
`Add more context lines to disambiguate.`,
|
|
847
|
-
);
|
|
848
|
-
}
|
|
849
|
-
|
|
850
|
-
throw new EditMatchError(path, normalizedOldText, matchOutcome.closest, {
|
|
851
|
-
allowFuzzy: this.#allowFuzzy,
|
|
852
|
-
threshold: this.#fuzzyThreshold,
|
|
853
|
-
fuzzyMatches: matchOutcome.fuzzyMatches,
|
|
854
|
-
});
|
|
855
|
-
}
|
|
856
|
-
|
|
857
|
-
if (normalizedContent === result.content) {
|
|
858
|
-
throw new Error(
|
|
859
|
-
`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.`,
|
|
860
|
-
);
|
|
861
|
-
}
|
|
862
|
-
|
|
863
|
-
const finalContent = bom + restoreLineEndings(result.content, originalEnding);
|
|
864
|
-
const diagnostics = await this.#writethrough(
|
|
865
|
-
absolutePath,
|
|
866
|
-
finalContent,
|
|
867
|
-
signal,
|
|
868
|
-
Bun.file(absolutePath),
|
|
869
|
-
batchRequest,
|
|
870
|
-
);
|
|
871
|
-
invalidateFsScanAfterWrite(absolutePath);
|
|
872
|
-
const diffResult = generateDiffString(normalizedContent, result.content);
|
|
873
|
-
|
|
874
|
-
const resultText =
|
|
875
|
-
result.count > 1
|
|
876
|
-
? `Successfully replaced ${result.count} occurrences in ${path}.`
|
|
877
|
-
: `Successfully replaced text in ${path}.`;
|
|
878
|
-
|
|
879
|
-
const meta = outputMeta()
|
|
880
|
-
.diagnostics(diagnostics?.summary ?? "", diagnostics?.messages ?? [])
|
|
881
|
-
.get();
|
|
882
|
-
|
|
883
|
-
return {
|
|
884
|
-
content: [{ type: "text", text: resultText }],
|
|
885
|
-
details: { diff: diffResult.diff, firstChangedLine: diffResult.firstChangedLine, diagnostics, meta },
|
|
886
|
-
};
|
|
887
|
-
}
|
|
888
|
-
}
|