headlesscode 1.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/ATTRIBUTION.md +53 -0
- package/CODE_OF_CONDUCT.md +130 -0
- package/CONTRIBUTING.md +107 -0
- package/LICENSE +202 -0
- package/README.md +486 -0
- package/SECURITY.md +211 -0
- package/bin/headlesscode.mjs +83 -0
- package/package.json +63 -0
- package/shared/prompts/review-mode-prompt-short.md +93 -0
- package/shared/prompts/review-mode-prompt.md +281 -0
- package/shared/rules-code/rules.md +22 -0
- package/shared/stacks/cpp/rules.md +30 -0
- package/shared/stacks/fastapi/rules.md +30 -0
- package/shared/stacks/javascript/rules.md +37 -0
- package/shared/stacks/postgresql/rules.md +31 -0
- package/shared/stacks/python/rules.md +35 -0
- package/shared/stacks/react/rules.md +11 -0
- package/shared/stacks/typescript/rules.md +10 -0
- package/src/budget/budget.ts +221 -0
- package/src/budget/concurrency.ts +126 -0
- package/src/budget/cost.ts +309 -0
- package/src/budget/index.ts +8 -0
- package/src/checkpoints/cli.ts +256 -0
- package/src/checkpoints/service.ts +227 -0
- package/src/cli.ts +1535 -0
- package/src/cloud/docker-provider.ts +334 -0
- package/src/cloud/provider.ts +300 -0
- package/src/codeintel/call-graph.ts +78 -0
- package/src/codeintel/find-references.ts +123 -0
- package/src/codeintel/go-to-definition.ts +193 -0
- package/src/codeintel/handlers.ts +190 -0
- package/src/codeintel/import-graph.ts +173 -0
- package/src/codeintel/outline.ts +180 -0
- package/src/codeintel/position.ts +77 -0
- package/src/codeintel/program.ts +350 -0
- package/src/codeintel/rename-symbol.ts +213 -0
- package/src/codeintel/tools.ts +280 -0
- package/src/codemap/build.ts +135 -0
- package/src/codemap/cli.ts +190 -0
- package/src/codemap/extract.ts +339 -0
- package/src/codemap/files.ts +236 -0
- package/src/codemap/fingerprint.ts +65 -0
- package/src/codemap/flows.ts +62 -0
- package/src/codemap/html.ts +451 -0
- package/src/codemap/lock.ts +80 -0
- package/src/codemap/types.ts +101 -0
- package/src/codesearch/airunner-embedder.ts +185 -0
- package/src/codesearch/chunk.ts +339 -0
- package/src/codesearch/cli.ts +223 -0
- package/src/codesearch/embedder.ts +332 -0
- package/src/codesearch/files.ts +280 -0
- package/src/codesearch/index.ts +469 -0
- package/src/codesearch/ollama-embedder.ts +205 -0
- package/src/codesearch/search.ts +141 -0
- package/src/codesearch/types.ts +100 -0
- package/src/config/mode-models.ts +218 -0
- package/src/dashboard/aggregate.ts +364 -0
- package/src/dashboard/chat-thread.ts +141 -0
- package/src/dashboard/checkpoints.ts +124 -0
- package/src/dashboard/cli.ts +193 -0
- package/src/dashboard/codemap.ts +44 -0
- package/src/dashboard/files.ts +121 -0
- package/src/dashboard/page.ts +2803 -0
- package/src/dashboard/self-improvement-metrics.ts +282 -0
- package/src/dashboard/server.ts +1103 -0
- package/src/dashboard/session-launch.ts +310 -0
- package/src/dashboard/timeline.ts +273 -0
- package/src/dashboard/tool-exec.ts +107 -0
- package/src/dashboard/trend-cli.ts +141 -0
- package/src/dashboard/trend.ts +413 -0
- package/src/decision-proxy/cli.ts +261 -0
- package/src/decision-proxy/proxy.ts +569 -0
- package/src/deploy/gate-cli.ts +147 -0
- package/src/deploy/gate.ts +254 -0
- package/src/engine/condense.ts +512 -0
- package/src/engine/events.ts +428 -0
- package/src/engine/handoff.ts +71 -0
- package/src/engine/lazy-tools.ts +160 -0
- package/src/engine/local-explore.ts +653 -0
- package/src/engine/logger.ts +96 -0
- package/src/engine/loop.ts +5517 -0
- package/src/engine/parser.ts +347 -0
- package/src/engine/prompt.ts +860 -0
- package/src/engine/reports.ts +47 -0
- package/src/engine/stacks.ts +448 -0
- package/src/engine/types.ts +291 -0
- package/src/engine/usage.ts +186 -0
- package/src/github/app-auth.ts +161 -0
- package/src/github/cli.ts +448 -0
- package/src/github/installations.ts +133 -0
- package/src/github/pr.ts +321 -0
- package/src/github/provision.ts +118 -0
- package/src/github/push.ts +122 -0
- package/src/index-util.ts +50 -0
- package/src/index.ts +81 -0
- package/src/init/cli.ts +248 -0
- package/src/init/gitignore.ts +74 -0
- package/src/llm/ollama.ts +308 -0
- package/src/llm/openrouter.ts +868 -0
- package/src/llm/preflight.ts +367 -0
- package/src/llm/transcript-capture.ts +84 -0
- package/src/memory/embed.ts +110 -0
- package/src/memory/index.ts +22 -0
- package/src/memory/local.ts +259 -0
- package/src/memory/summarizer.ts +283 -0
- package/src/memory/types.ts +153 -0
- package/src/memory/uwuchat.ts +157 -0
- package/src/migrate/cli.ts +115 -0
- package/src/orchestrator/analyze-cli.ts +104 -0
- package/src/orchestrator/auto-split.ts +206 -0
- package/src/orchestrator/cleanup.ts +1003 -0
- package/src/orchestrator/cli.ts +3571 -0
- package/src/orchestrator/cost-estimate.ts +564 -0
- package/src/orchestrator/cost-history-cli.ts +242 -0
- package/src/orchestrator/cost-history.ts +397 -0
- package/src/orchestrator/git-sync.ts +250 -0
- package/src/orchestrator/index.ts +153 -0
- package/src/orchestrator/log-analysis.ts +0 -0
- package/src/orchestrator/merge-check.ts +108 -0
- package/src/orchestrator/pipeline.ts +411 -0
- package/src/orchestrator/resume.ts +1940 -0
- package/src/orchestrator/reviewer.ts +503 -0
- package/src/orchestrator/split.ts +296 -0
- package/src/orchestrator/state.ts +542 -0
- package/src/orchestrator/status.ts +697 -0
- package/src/orchestrator/verification-gate.ts +134 -0
- package/src/orchestrator/watch.ts +898 -0
- package/src/permissions/commands.ts +1083 -0
- package/src/permissions/config.ts +241 -0
- package/src/permissions/index.ts +12 -0
- package/src/permissions/protected-files.ts +96 -0
- package/src/permissions/store-protection.ts +272 -0
- package/src/project-store.ts +648 -0
- package/src/projects/cli.ts +382 -0
- package/src/qa/qa.ts +487 -0
- package/src/tools/browser/handler.ts +346 -0
- package/src/tools/browser/service.ts +406 -0
- package/src/tools/browser/smoke.ts +78 -0
- package/src/tools/browser/tool.ts +99 -0
- package/src/tools/executor.ts +2575 -0
- package/src/tools/language-detect.ts +183 -0
- package/src/tools/output-summarizer.ts +369 -0
- package/src/tools/run-tests.ts +302 -0
- package/src/tools/set-indentation-tool.ts +49 -0
- package/src/tools/test-selection.ts +160 -0
- package/src/vendor/tests/smoke.ts +103 -0
- package/src/vendor/zoo-code/VENDOR-NOTES.md +213 -0
- package/src/vendor/zoo-code/shim/anthropic.ts +71 -0
- package/src/vendor/zoo-code/shim/openai.d.ts +60 -0
- package/src/vendor/zoo-code/shim/os-name.ts +18 -0
- package/src/vendor/zoo-code/shim/strip-bom.ts +14 -0
- package/src/vendor/zoo-code/shim/vscode.ts +76 -0
- package/src/vendor/zoo-code/src/core/config/CustomModesManager.ts +1015 -0
- package/src/vendor/zoo-code/src/core/diff/strategies/multi-search-replace.ts +670 -0
- package/src/vendor/zoo-code/src/core/prompts/sections/capabilities.ts +46 -0
- package/src/vendor/zoo-code/src/core/prompts/sections/custom-instructions.ts +559 -0
- package/src/vendor/zoo-code/src/core/prompts/sections/index.ts +10 -0
- package/src/vendor/zoo-code/src/core/prompts/sections/markdown-formatting.ts +7 -0
- package/src/vendor/zoo-code/src/core/prompts/sections/modes.ts +35 -0
- package/src/vendor/zoo-code/src/core/prompts/sections/objective.ts +13 -0
- package/src/vendor/zoo-code/src/core/prompts/sections/rules.ts +95 -0
- package/src/vendor/zoo-code/src/core/prompts/sections/skills.ts +105 -0
- package/src/vendor/zoo-code/src/core/prompts/sections/system-info.ts +30 -0
- package/src/vendor/zoo-code/src/core/prompts/sections/tool-use-guidelines.ts +9 -0
- package/src/vendor/zoo-code/src/core/prompts/sections/tool-use.ts +7 -0
- package/src/vendor/zoo-code/src/core/prompts/system.ts +176 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/access_mcp_resource.ts +41 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/apply_diff.ts +40 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/apply_patch.ts +61 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/ask_followup_question.ts +62 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/attempt_completion.ts +33 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/codebase_search.ts +43 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/converters.ts +109 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/edit.ts +48 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/edit_file.ts +72 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/execute_command.ts +54 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/generate_image.ts +51 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/index.ts +75 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/list_files.ts +41 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/mcp_server.ts +75 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/new_task.ts +39 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/read_command_output.ts +81 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/read_file.ts +169 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/run_slash_command.ts +31 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/search_files.ts +50 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/search_replace.ts +51 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/skill.ts +33 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/switch_mode.ts +31 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/update_todo_list.ts +54 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/write_to_file.ts +40 -0
- package/src/vendor/zoo-code/src/core/prompts/types.ts +12 -0
- package/src/vendor/zoo-code/src/i18n/index.ts +19 -0
- package/src/vendor/zoo-code/src/integrations/misc/extract-text.ts +81 -0
- package/src/vendor/zoo-code/src/services/checkpoints/RepoPerTaskCheckpointService.ts +15 -0
- package/src/vendor/zoo-code/src/services/checkpoints/ShadowCheckpointService.ts +553 -0
- package/src/vendor/zoo-code/src/services/checkpoints/excludes.ts +212 -0
- package/src/vendor/zoo-code/src/services/checkpoints/index.ts +3 -0
- package/src/vendor/zoo-code/src/services/checkpoints/types.ts +35 -0
- package/src/vendor/zoo-code/src/services/code-index/manager.ts +19 -0
- package/src/vendor/zoo-code/src/services/mcp/McpHub.ts +36 -0
- package/src/vendor/zoo-code/src/services/roo-config/index.ts +441 -0
- package/src/vendor/zoo-code/src/services/search/file-search.ts +143 -0
- package/src/vendor/zoo-code/src/services/skills/SkillsManager.ts +20 -0
- package/src/vendor/zoo-code/src/shared/globalFileNames.ts +9 -0
- package/src/vendor/zoo-code/src/shared/language.ts +43 -0
- package/src/vendor/zoo-code/src/shared/modes.ts +257 -0
- package/src/vendor/zoo-code/src/shared/tools.ts +385 -0
- package/src/vendor/zoo-code/src/utils/fs.ts +39 -0
- package/src/vendor/zoo-code/src/utils/globalContext.ts +22 -0
- package/src/vendor/zoo-code/src/utils/json-schema.ts +16 -0
- package/src/vendor/zoo-code/src/utils/logging.ts +21 -0
- package/src/vendor/zoo-code/src/utils/mcp-name.ts +190 -0
- package/src/vendor/zoo-code/src/utils/object.ts +18 -0
- package/src/vendor/zoo-code/src/utils/path.ts +94 -0
- package/src/vendor/zoo-code/src/utils/shell.ts +376 -0
- package/src/vendor/zoo-code/src/utils/text-normalization.ts +99 -0
- package/src/vendor/zoo-code/types/global-settings.ts +19 -0
- package/src/vendor/zoo-code/types/index.ts +22 -0
- package/src/vendor/zoo-code/types/message.ts +375 -0
- package/src/vendor/zoo-code/types/mode.ts +241 -0
- package/src/vendor/zoo-code/types/todo.ts +19 -0
- package/src/vendor/zoo-code/types/tool-params.ts +116 -0
- package/src/vendor/zoo-code/types/tool.ts +67 -0
- package/src/vendor/zoo-code/types/vscode.ts +84 -0
- package/src/vision/describe.ts +242 -0
- package/src/vision/tool.ts +91 -0
- package/src/watcher/cli.ts +369 -0
- package/src/watcher/github.ts +304 -0
- package/src/watcher/index.ts +59 -0
- package/src/watcher/state.ts +254 -0
- package/src/watcher/watch.ts +562 -0
- package/tsconfig.json +18 -0
|
@@ -0,0 +1,670 @@
|
|
|
1
|
+
import { distance } from "fastest-levenshtein"
|
|
2
|
+
|
|
3
|
+
import { ToolProgressStatus, DEFAULT_DIFF_FUZZY_THRESHOLD } from "../../../../types"
|
|
4
|
+
|
|
5
|
+
import { addLineNumbers, everyLineHasLineNumbers, stripLineNumbers } from "../../../integrations/misc/extract-text"
|
|
6
|
+
import { ToolUse, DiffStrategy, DiffResult } from "../../../shared/tools"
|
|
7
|
+
import { normalizeString } from "../../../utils/text-normalization"
|
|
8
|
+
|
|
9
|
+
const BUFFER_LINES = 40 // Number of extra context lines to show before and after matches
|
|
10
|
+
|
|
11
|
+
function getSimilarity(original: string, search: string): number {
|
|
12
|
+
// Empty searches are no longer supported
|
|
13
|
+
if (search === "") {
|
|
14
|
+
return 0
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// Use the normalizeString utility to handle smart quotes and other special characters
|
|
18
|
+
const normalizedOriginal = normalizeString(original)
|
|
19
|
+
const normalizedSearch = normalizeString(search)
|
|
20
|
+
|
|
21
|
+
if (normalizedOriginal === normalizedSearch) {
|
|
22
|
+
return 1
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Calculate Levenshtein distance using fastest-levenshtein's distance function
|
|
26
|
+
const dist = distance(normalizedOriginal, normalizedSearch)
|
|
27
|
+
|
|
28
|
+
// Calculate similarity ratio (0 to 1, where 1 is an exact match)
|
|
29
|
+
const maxLength = Math.max(normalizedOriginal.length, normalizedSearch.length)
|
|
30
|
+
return 1 - dist / maxLength
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Performs a "middle-out" search of `lines` (between [startIndex, endIndex]) to find
|
|
35
|
+
* the slice that is most similar to `searchChunk`. Returns the best score, index, and matched text.
|
|
36
|
+
*/
|
|
37
|
+
function fuzzySearch(lines: string[], searchChunk: string, startIndex: number, endIndex: number) {
|
|
38
|
+
let bestScore = 0
|
|
39
|
+
let bestMatchIndex = -1
|
|
40
|
+
let bestMatchContent = ""
|
|
41
|
+
const searchLen = searchChunk.split(/\r?\n/).length
|
|
42
|
+
|
|
43
|
+
// Middle-out from the midpoint
|
|
44
|
+
const midPoint = Math.floor((startIndex + endIndex) / 2)
|
|
45
|
+
let leftIndex = midPoint
|
|
46
|
+
let rightIndex = midPoint + 1
|
|
47
|
+
|
|
48
|
+
while (leftIndex >= startIndex || rightIndex <= endIndex - searchLen) {
|
|
49
|
+
if (leftIndex >= startIndex) {
|
|
50
|
+
const originalChunk = lines.slice(leftIndex, leftIndex + searchLen).join("\n")
|
|
51
|
+
const similarity = getSimilarity(originalChunk, searchChunk)
|
|
52
|
+
if (similarity > bestScore) {
|
|
53
|
+
bestScore = similarity
|
|
54
|
+
bestMatchIndex = leftIndex
|
|
55
|
+
bestMatchContent = originalChunk
|
|
56
|
+
}
|
|
57
|
+
leftIndex--
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (rightIndex <= endIndex - searchLen) {
|
|
61
|
+
const originalChunk = lines.slice(rightIndex, rightIndex + searchLen).join("\n")
|
|
62
|
+
const similarity = getSimilarity(originalChunk, searchChunk)
|
|
63
|
+
if (similarity > bestScore) {
|
|
64
|
+
bestScore = similarity
|
|
65
|
+
bestMatchIndex = rightIndex
|
|
66
|
+
bestMatchContent = originalChunk
|
|
67
|
+
}
|
|
68
|
+
rightIndex++
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return { bestScore, bestMatchIndex, bestMatchContent }
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export class MultiSearchReplaceDiffStrategy implements DiffStrategy {
|
|
76
|
+
private fuzzyThreshold: number
|
|
77
|
+
private bufferLines: number
|
|
78
|
+
|
|
79
|
+
getName(): string {
|
|
80
|
+
return "MultiSearchReplace"
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
constructor(fuzzyThreshold?: number, bufferLines?: number) {
|
|
84
|
+
// Use provided threshold or default to exact matching (1.0)
|
|
85
|
+
// A value of 0.9 means 90% similarity is required for a match,
|
|
86
|
+
// but the default remains 1.0 (exact match). Users can opt in
|
|
87
|
+
// to relaxed matching via diffFuzzyThreshold in settings.
|
|
88
|
+
// Clamp the threshold to [0.5, 1.0] as a defence-in-depth guard.
|
|
89
|
+
const thresholdVal = fuzzyThreshold ?? DEFAULT_DIFF_FUZZY_THRESHOLD
|
|
90
|
+
this.fuzzyThreshold = Math.max(0.5, Math.min(1.0, thresholdVal))
|
|
91
|
+
this.bufferLines = bufferLines ?? BUFFER_LINES
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
private unescapeMarkers(content: string): string {
|
|
95
|
+
return content
|
|
96
|
+
.replace(/^\\<<<<<<</gm, "<<<<<<<")
|
|
97
|
+
.replace(/^\\=======/gm, "=======")
|
|
98
|
+
.replace(/^\\>>>>>>>/gm, ">>>>>>>")
|
|
99
|
+
.replace(/^\\-------/gm, "-------")
|
|
100
|
+
.replace(/^\\:end_line:/gm, ":end_line:")
|
|
101
|
+
.replace(/^\\:start_line:/gm, ":start_line:")
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
private validateMarkerSequencing(diffContent: string): { success: boolean; error?: string } {
|
|
105
|
+
enum State {
|
|
106
|
+
START,
|
|
107
|
+
AFTER_SEARCH,
|
|
108
|
+
AFTER_SEPARATOR,
|
|
109
|
+
}
|
|
110
|
+
const state = { current: State.START, line: 0 }
|
|
111
|
+
|
|
112
|
+
// Pattern allows optional '>' after SEARCH to handle AI-generated diffs
|
|
113
|
+
// (e.g., Sonnet 4 sometimes adds an extra '>')
|
|
114
|
+
const SEARCH_PATTERN = /^<<<<<<< SEARCH>?$/
|
|
115
|
+
const SEARCH = SEARCH_PATTERN.source.replace(/[\^$]/g, "") // Remove regex anchors for display
|
|
116
|
+
const SEP = "======="
|
|
117
|
+
const REPLACE = ">>>>>>> REPLACE"
|
|
118
|
+
const SEARCH_PREFIX = "<<<<<<<"
|
|
119
|
+
const REPLACE_PREFIX = ">>>>>>>"
|
|
120
|
+
|
|
121
|
+
const reportMergeConflictError = (found: string, _expected: string) => ({
|
|
122
|
+
success: false,
|
|
123
|
+
error:
|
|
124
|
+
`ERROR: Special marker '${found}' found in your diff content at line ${state.line}:\n` +
|
|
125
|
+
"\n" +
|
|
126
|
+
`When removing merge conflict markers like '${found}' from files, you MUST escape them\n` +
|
|
127
|
+
"in your SEARCH section by prepending a backslash (\\) at the beginning of the line:\n" +
|
|
128
|
+
"\n" +
|
|
129
|
+
"CORRECT FORMAT:\n\n" +
|
|
130
|
+
"<<<<<<< SEARCH\n" +
|
|
131
|
+
"content before\n" +
|
|
132
|
+
`\\${found} <-- Note the backslash here in this example\n` +
|
|
133
|
+
"content after\n" +
|
|
134
|
+
"=======\n" +
|
|
135
|
+
"replacement content\n" +
|
|
136
|
+
">>>>>>> REPLACE\n" +
|
|
137
|
+
"\n" +
|
|
138
|
+
"Without escaping, the system confuses your content with diff syntax markers.\n" +
|
|
139
|
+
"You may use multiple diff blocks in a single diff request, but ANY of ONLY the following separators that occur within SEARCH or REPLACE content must be escaped, as follows:\n" +
|
|
140
|
+
`\\${SEARCH}\n` +
|
|
141
|
+
`\\${SEP}\n` +
|
|
142
|
+
`\\${REPLACE}\n`,
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
const reportInvalidDiffError = (found: string, expected: string) => ({
|
|
146
|
+
success: false,
|
|
147
|
+
error:
|
|
148
|
+
`ERROR: Diff block is malformed: marker '${found}' found in your diff content at line ${state.line}. Expected: ${expected}\n` +
|
|
149
|
+
"\n" +
|
|
150
|
+
"CORRECT FORMAT:\n\n" +
|
|
151
|
+
"<<<<<<< SEARCH\n" +
|
|
152
|
+
":start_line: (required) The line number of original content where the search block starts.\n" +
|
|
153
|
+
"-------\n" +
|
|
154
|
+
"[exact content to find including whitespace]\n" +
|
|
155
|
+
"=======\n" +
|
|
156
|
+
"[new content to replace with]\n" +
|
|
157
|
+
">>>>>>> REPLACE\n",
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
const reportLineMarkerInReplaceError = (marker: string) => ({
|
|
161
|
+
success: false,
|
|
162
|
+
error:
|
|
163
|
+
`ERROR: Invalid line marker '${marker}' found in REPLACE section at line ${state.line}\n` +
|
|
164
|
+
"\n" +
|
|
165
|
+
"Line markers (:start_line: and :end_line:) are only allowed in SEARCH sections.\n" +
|
|
166
|
+
"\n" +
|
|
167
|
+
"CORRECT FORMAT:\n" +
|
|
168
|
+
"<<<<<<< SEARCH\n" +
|
|
169
|
+
":start_line:5\n" +
|
|
170
|
+
"content to find\n" +
|
|
171
|
+
"=======\n" +
|
|
172
|
+
"replacement content\n" +
|
|
173
|
+
">>>>>>> REPLACE\n" +
|
|
174
|
+
"\n" +
|
|
175
|
+
"INCORRECT FORMAT:\n" +
|
|
176
|
+
"<<<<<<< SEARCH\n" +
|
|
177
|
+
"content to find\n" +
|
|
178
|
+
"=======\n" +
|
|
179
|
+
":start_line:5 <-- Invalid location\n" +
|
|
180
|
+
"replacement content\n" +
|
|
181
|
+
">>>>>>> REPLACE\n",
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
const lines = diffContent.split("\n")
|
|
185
|
+
const searchCount = lines.filter((l) => SEARCH_PATTERN.test(l.trim())).length
|
|
186
|
+
const sepCount = lines.filter((l) => l.trim() === SEP).length
|
|
187
|
+
const replaceCount = lines.filter((l) => l.trim() === REPLACE).length
|
|
188
|
+
|
|
189
|
+
const likelyBadStructure = searchCount !== replaceCount || sepCount < searchCount
|
|
190
|
+
|
|
191
|
+
for (const line of diffContent.split("\n")) {
|
|
192
|
+
state.line++
|
|
193
|
+
const marker = line.trim()
|
|
194
|
+
|
|
195
|
+
// Check for line markers in REPLACE sections (but allow escaped ones)
|
|
196
|
+
if (state.current === State.AFTER_SEPARATOR) {
|
|
197
|
+
if (marker.startsWith(":start_line:") && !line.trim().startsWith("\\:start_line:")) {
|
|
198
|
+
return reportLineMarkerInReplaceError(":start_line:")
|
|
199
|
+
}
|
|
200
|
+
if (marker.startsWith(":end_line:") && !line.trim().startsWith("\\:end_line:")) {
|
|
201
|
+
return reportLineMarkerInReplaceError(":end_line:")
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
switch (state.current) {
|
|
206
|
+
case State.START:
|
|
207
|
+
if (marker === SEP)
|
|
208
|
+
return likelyBadStructure
|
|
209
|
+
? reportInvalidDiffError(SEP, SEARCH)
|
|
210
|
+
: reportMergeConflictError(SEP, SEARCH)
|
|
211
|
+
if (marker === REPLACE) return reportInvalidDiffError(REPLACE, SEARCH)
|
|
212
|
+
if (marker.startsWith(REPLACE_PREFIX)) return reportMergeConflictError(marker, SEARCH)
|
|
213
|
+
if (SEARCH_PATTERN.test(marker)) state.current = State.AFTER_SEARCH
|
|
214
|
+
else if (marker.startsWith(SEARCH_PREFIX)) return reportMergeConflictError(marker, SEARCH)
|
|
215
|
+
break
|
|
216
|
+
|
|
217
|
+
case State.AFTER_SEARCH:
|
|
218
|
+
if (SEARCH_PATTERN.test(marker)) return reportInvalidDiffError(SEARCH_PATTERN.source, SEP)
|
|
219
|
+
if (marker.startsWith(SEARCH_PREFIX)) return reportMergeConflictError(marker, SEARCH)
|
|
220
|
+
if (marker === REPLACE) return reportInvalidDiffError(REPLACE, SEP)
|
|
221
|
+
if (marker.startsWith(REPLACE_PREFIX)) return reportMergeConflictError(marker, SEARCH)
|
|
222
|
+
if (marker === SEP) state.current = State.AFTER_SEPARATOR
|
|
223
|
+
break
|
|
224
|
+
|
|
225
|
+
case State.AFTER_SEPARATOR:
|
|
226
|
+
if (SEARCH_PATTERN.test(marker)) return reportInvalidDiffError(SEARCH_PATTERN.source, REPLACE)
|
|
227
|
+
if (marker.startsWith(SEARCH_PREFIX)) return reportMergeConflictError(marker, REPLACE)
|
|
228
|
+
if (marker === SEP)
|
|
229
|
+
return likelyBadStructure
|
|
230
|
+
? reportInvalidDiffError(SEP, REPLACE)
|
|
231
|
+
: reportMergeConflictError(SEP, REPLACE)
|
|
232
|
+
if (marker === REPLACE) state.current = State.START
|
|
233
|
+
else if (marker.startsWith(REPLACE_PREFIX)) return reportMergeConflictError(marker, REPLACE)
|
|
234
|
+
break
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
return state.current === State.START
|
|
239
|
+
? { success: true }
|
|
240
|
+
: {
|
|
241
|
+
success: false,
|
|
242
|
+
error: `ERROR: Unexpected end of sequence: Expected '${
|
|
243
|
+
state.current === State.AFTER_SEARCH ? "=======" : ">>>>>>> REPLACE"
|
|
244
|
+
}' was not found.`,
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Repairs truncated diffs (common with Grok) by adding missing ======= and >>>>>>> REPLACE markers.
|
|
250
|
+
* When the model's output gets cut off mid-stream, the diff may end after SEARCH content
|
|
251
|
+
* without the separator or closing marker. This method detects that pattern and appends
|
|
252
|
+
* the missing markers so the diff can still be parsed and applied.
|
|
253
|
+
*/
|
|
254
|
+
private repairTruncatedDiff(diffContent: string): string {
|
|
255
|
+
// Only repair if the diff has at least one SEARCH marker
|
|
256
|
+
if (!/(?<!\\)<<<<<<< SEARCH/.test(diffContent)) {
|
|
257
|
+
return diffContent
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Split into blocks based on SEARCH markers
|
|
261
|
+
const blocks = diffContent.split(/(?=(?<!\\)<<<<<<< SEARCH)/)
|
|
262
|
+
|
|
263
|
+
let repaired = ""
|
|
264
|
+
|
|
265
|
+
for (let i = 0; i < blocks.length; i++) {
|
|
266
|
+
const block = blocks[i]
|
|
267
|
+
|
|
268
|
+
if (block.trim() === "") {
|
|
269
|
+
continue
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// Skip prefix blocks that don't contain a SEARCH marker
|
|
273
|
+
// (e.g., the filename line before the first <<<<<<< SEARCH)
|
|
274
|
+
if (!/(?<!\\)<<<<<<< SEARCH/.test(block)) {
|
|
275
|
+
repaired += block
|
|
276
|
+
continue
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// Check if this block is complete (has both ======= and >>>>>>> REPLACE)
|
|
280
|
+
const hasSeparator = /(?<=\n)(?<!\\)=======\s*\n/.test(block)
|
|
281
|
+
const hasCloser = /(?<=\n)(?<!\\)>>>>>>> REPLACE(?=\n|$)/.test(block)
|
|
282
|
+
|
|
283
|
+
if (hasSeparator && hasCloser) {
|
|
284
|
+
// Block is complete — emit verbatim (keeps its own trailing separator)
|
|
285
|
+
repaired += block
|
|
286
|
+
continue
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// Block needs repair. Build a clean block ending at >>>>>>> REPLACE, then
|
|
290
|
+
// re-add an inter-block separator if more (non-empty) blocks follow, so the
|
|
291
|
+
// appended closer never gets glued to the next "<<<<<<< SEARCH".
|
|
292
|
+
const isLast = blocks.slice(i + 1).every((b) => b.trim() === "")
|
|
293
|
+
const separator = isLast ? "" : "\n\n"
|
|
294
|
+
|
|
295
|
+
if (hasSeparator && !hasCloser) {
|
|
296
|
+
// Has ======= but missing >>>>>>> REPLACE — append closing marker
|
|
297
|
+
const body = block.replace(/\s+$/, "")
|
|
298
|
+
repaired += body + "\n>>>>>>> REPLACE" + separator
|
|
299
|
+
} else if (hasCloser && !hasSeparator) {
|
|
300
|
+
// Has >>>>>>> REPLACE but missing the ======= separator. Don't synthesize a
|
|
301
|
+
// second closer; splice the separator in right before the existing closer so
|
|
302
|
+
// everything above it becomes the SEARCH section.
|
|
303
|
+
const body = block.replace(/\s+$/, "")
|
|
304
|
+
repaired += body.replace(/(\n)(>>>>>>> REPLACE)(?=\n|$)/, "$1=======\n$2") + separator
|
|
305
|
+
} else {
|
|
306
|
+
// Missing both ======= and >>>>>>> REPLACE.
|
|
307
|
+
const searchMatch = block.match(/^<<<<<<< SEARCH\n?([\s\S]*)$/)
|
|
308
|
+
let content = (searchMatch?.[1] ?? "").replace(/\s+$/, "")
|
|
309
|
+
|
|
310
|
+
// Peel off any leading Grok header directives (:start_line:, :end_line:, -------)
|
|
311
|
+
// so the "first line is SEARCH" heuristic sees real content, not metadata. The
|
|
312
|
+
// directives are preserved as a header on the SEARCH section.
|
|
313
|
+
let header = ""
|
|
314
|
+
const directiveLine = /^(?::start_line:\s*\d+|:end_line:\s*\d+|-------)\s*$/
|
|
315
|
+
let nlIdx: number
|
|
316
|
+
while ((nlIdx = content.indexOf("\n")) !== -1 && directiveLine.test(content.slice(0, nlIdx))) {
|
|
317
|
+
header += content.slice(0, nlIdx + 1)
|
|
318
|
+
content = content.slice(nlIdx + 1)
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
const firstNewlineIdx = content.indexOf("\n")
|
|
322
|
+
if (firstNewlineIdx !== -1) {
|
|
323
|
+
// First line is SEARCH content, rest is REPLACE content
|
|
324
|
+
const searchContent = content.substring(0, firstNewlineIdx)
|
|
325
|
+
const replaceContent = content.substring(firstNewlineIdx + 1)
|
|
326
|
+
repaired +=
|
|
327
|
+
"<<<<<<< SEARCH\n" +
|
|
328
|
+
header +
|
|
329
|
+
searchContent +
|
|
330
|
+
"\n=======\n" +
|
|
331
|
+
replaceContent +
|
|
332
|
+
"\n>>>>>>> REPLACE" +
|
|
333
|
+
separator
|
|
334
|
+
} else if (header) {
|
|
335
|
+
// Only a directive header plus a single content line: that line is the SEARCH
|
|
336
|
+
// target (the user pinned it with start_line); the REPLACE section is empty.
|
|
337
|
+
repaired += "<<<<<<< SEARCH\n" + header + content + "\n=======\n\n>>>>>>> REPLACE" + separator
|
|
338
|
+
} else {
|
|
339
|
+
// Single line — treat as empty SEARCH with content as REPLACE
|
|
340
|
+
repaired += "<<<<<<< SEARCH\n=======\n" + content + "\n>>>>>>> REPLACE" + separator
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
return repaired || diffContent
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
async applyDiff(
|
|
349
|
+
originalContent: string,
|
|
350
|
+
diffContent: string,
|
|
351
|
+
_paramStartLine?: number,
|
|
352
|
+
_paramEndLine?: number,
|
|
353
|
+
): Promise<DiffResult> {
|
|
354
|
+
// Repair truncated diffs before validation (common with Grok and other models
|
|
355
|
+
// whose output gets cut off mid-stream, leaving missing ======= and >>>>>>> REPLACE markers)
|
|
356
|
+
const repairedDiff = this.repairTruncatedDiff(diffContent)
|
|
357
|
+
|
|
358
|
+
const validseq = this.validateMarkerSequencing(repairedDiff)
|
|
359
|
+
if (!validseq.success) {
|
|
360
|
+
return {
|
|
361
|
+
success: false,
|
|
362
|
+
error: validseq.error!,
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/*
|
|
367
|
+
Regex parts:
|
|
368
|
+
|
|
369
|
+
1. (?:^|\n)
|
|
370
|
+
Ensures the first marker starts at the beginning of the file or right after a newline.
|
|
371
|
+
|
|
372
|
+
2. (?<!\\)<<<<<<< SEARCH\s*\n
|
|
373
|
+
Matches the line "<<<<<<< SEARCH" (ignoring any trailing spaces) – the negative lookbehind makes sure it isn't escaped.
|
|
374
|
+
|
|
375
|
+
3. ((?:\:start_line:\s*(\d+)\s*\n))?
|
|
376
|
+
Optionally matches a ":start_line:" line. The outer capturing group is group 1 and the inner (\d+) is group 2.
|
|
377
|
+
|
|
378
|
+
4. ((?:\:end_line:\s*(\d+)\s*\n))?
|
|
379
|
+
Optionally matches a ":end_line:" line. Group 3 is the whole match and group 4 is the digits.
|
|
380
|
+
|
|
381
|
+
5. ((?<!\\)-------\s*\n)?
|
|
382
|
+
Optionally matches the "-------" marker line (group 5).
|
|
383
|
+
|
|
384
|
+
6. ([\s\S]*?)(?:\n)?
|
|
385
|
+
Non‐greedy match for the "search content" (group 6) up to the next marker.
|
|
386
|
+
|
|
387
|
+
7. (?:(?<=\n)(?<!\\)=======\s*\n)
|
|
388
|
+
Matches the "=======" marker on its own line.
|
|
389
|
+
|
|
390
|
+
8. ([\s\S]*?)(?:\n)?
|
|
391
|
+
Non‐greedy match for the "replace content" (group 7).
|
|
392
|
+
|
|
393
|
+
9. (?:(?<=\n)(?<!\\)>>>>>>> REPLACE)(?=\n|$)
|
|
394
|
+
Matches the final ">>>>>>> REPLACE" marker on its own line (and requires a following newline or the end of file).
|
|
395
|
+
*/
|
|
396
|
+
|
|
397
|
+
const matches = [
|
|
398
|
+
...repairedDiff.matchAll(
|
|
399
|
+
/(?:^|\n)(?<!\\)<<<<<<< SEARCH>?\s*\n((?:\:start_line:\s*(\d+)\s*\n))?((?:\:end_line:\s*(\d+)\s*\n))?((?<!\\)-------\s*\n)?([\s\S]*?)(?:\n)?(?:(?<=\n)(?<!\\)=======\s*\n)([\s\S]*?)(?:\n)?(?:(?<=\n)(?<!\\)>>>>>>> REPLACE)(?=\n|$)/g,
|
|
400
|
+
),
|
|
401
|
+
]
|
|
402
|
+
|
|
403
|
+
if (matches.length === 0) {
|
|
404
|
+
return {
|
|
405
|
+
success: false,
|
|
406
|
+
error: `Invalid diff format - missing required sections\n\nDebug Info:\n- Expected Format: <<<<<<< SEARCH\\n:start_line: start line\\n-------\\n[search content]\\n=======\\n[replace content]\\n>>>>>>> REPLACE\n- Tip: Make sure to include start_line/SEARCH/=======/REPLACE sections with correct markers on new lines`,
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
// Detect line ending from original content
|
|
410
|
+
const lineEnding = originalContent.includes("\r\n") ? "\r\n" : "\n"
|
|
411
|
+
let resultLines = originalContent.split(/\r?\n/)
|
|
412
|
+
let delta = 0
|
|
413
|
+
const diffResults: DiffResult[] = []
|
|
414
|
+
let appliedCount = 0
|
|
415
|
+
const replacements = matches
|
|
416
|
+
.map((match) => ({
|
|
417
|
+
startLine: Number(match[2] ?? 0),
|
|
418
|
+
searchContent: match[6],
|
|
419
|
+
replaceContent: match[7],
|
|
420
|
+
}))
|
|
421
|
+
.sort((a, b) => a.startLine - b.startLine)
|
|
422
|
+
|
|
423
|
+
for (const replacement of replacements) {
|
|
424
|
+
let { searchContent, replaceContent } = replacement
|
|
425
|
+
let startLine = replacement.startLine + (replacement.startLine === 0 ? 0 : delta)
|
|
426
|
+
|
|
427
|
+
// First unescape any escaped markers in the content
|
|
428
|
+
searchContent = this.unescapeMarkers(searchContent)
|
|
429
|
+
replaceContent = this.unescapeMarkers(replaceContent)
|
|
430
|
+
|
|
431
|
+
// Strip line numbers from search and replace content if every line starts with a line number
|
|
432
|
+
const hasAllLineNumbers =
|
|
433
|
+
(everyLineHasLineNumbers(searchContent) && everyLineHasLineNumbers(replaceContent)) ||
|
|
434
|
+
(everyLineHasLineNumbers(searchContent) && replaceContent.trim() === "")
|
|
435
|
+
|
|
436
|
+
if (hasAllLineNumbers && startLine === 0) {
|
|
437
|
+
startLine = parseInt(searchContent.split("\n")[0].split("|")[0])
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
if (hasAllLineNumbers) {
|
|
441
|
+
searchContent = stripLineNumbers(searchContent)
|
|
442
|
+
replaceContent = stripLineNumbers(replaceContent)
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// Validate that search and replace content are not identical
|
|
446
|
+
if (searchContent === replaceContent) {
|
|
447
|
+
diffResults.push({
|
|
448
|
+
success: false,
|
|
449
|
+
error:
|
|
450
|
+
`Search and replace content are identical - no changes would be made\n\n` +
|
|
451
|
+
`Debug Info:\n` +
|
|
452
|
+
`- Search and replace must be different to make changes\n` +
|
|
453
|
+
`- Use read_file to verify the content you want to change`,
|
|
454
|
+
})
|
|
455
|
+
continue
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// Split content into lines, handling both \n and \r\n
|
|
459
|
+
let searchLines = searchContent === "" ? [] : searchContent.split(/\r?\n/)
|
|
460
|
+
let replaceLines = replaceContent === "" ? [] : replaceContent.split(/\r?\n/)
|
|
461
|
+
|
|
462
|
+
// Validate that search content is not empty
|
|
463
|
+
if (searchLines.length === 0) {
|
|
464
|
+
diffResults.push({
|
|
465
|
+
success: false,
|
|
466
|
+
error: `Empty search content is not allowed\n\nDebug Info:\n- Search content cannot be empty\n- For insertions, provide a specific line using :start_line: and include content to search for\n- For example, match a single line to insert before/after it`,
|
|
467
|
+
})
|
|
468
|
+
continue
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
const endLine = replacement.startLine + searchLines.length - 1
|
|
472
|
+
|
|
473
|
+
// Initialize search variables
|
|
474
|
+
let matchIndex = -1
|
|
475
|
+
let bestMatchScore = 0
|
|
476
|
+
let bestMatchContent = ""
|
|
477
|
+
const searchChunk = searchLines.join("\n")
|
|
478
|
+
|
|
479
|
+
// Determine search bounds
|
|
480
|
+
let searchStartIndex = 0
|
|
481
|
+
let searchEndIndex = resultLines.length
|
|
482
|
+
|
|
483
|
+
// Validate and handle line range if provided
|
|
484
|
+
if (startLine) {
|
|
485
|
+
// Convert to 0-based index
|
|
486
|
+
const exactStartIndex = startLine - 1
|
|
487
|
+
const searchLen = searchLines.length
|
|
488
|
+
const exactEndIndex = exactStartIndex + searchLen - 1
|
|
489
|
+
|
|
490
|
+
// Try exact match first
|
|
491
|
+
const originalChunk = resultLines.slice(exactStartIndex, exactEndIndex + 1).join("\n")
|
|
492
|
+
const similarity = getSimilarity(originalChunk, searchChunk)
|
|
493
|
+
if (similarity >= this.fuzzyThreshold) {
|
|
494
|
+
matchIndex = exactStartIndex
|
|
495
|
+
bestMatchScore = similarity
|
|
496
|
+
bestMatchContent = originalChunk
|
|
497
|
+
} else {
|
|
498
|
+
// Set bounds for buffered search
|
|
499
|
+
searchStartIndex = Math.max(0, startLine - (this.bufferLines + 1))
|
|
500
|
+
searchEndIndex = Math.min(resultLines.length, startLine + searchLines.length + this.bufferLines)
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
// If no match found yet, try middle-out search within bounds
|
|
505
|
+
if (matchIndex === -1) {
|
|
506
|
+
const {
|
|
507
|
+
bestScore,
|
|
508
|
+
bestMatchIndex,
|
|
509
|
+
bestMatchContent: midContent,
|
|
510
|
+
} = fuzzySearch(resultLines, searchChunk, searchStartIndex, searchEndIndex)
|
|
511
|
+
matchIndex = bestMatchIndex
|
|
512
|
+
bestMatchScore = bestScore
|
|
513
|
+
bestMatchContent = midContent
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
// Try aggressive line number stripping as a fallback if regular matching fails
|
|
517
|
+
if (matchIndex === -1 || bestMatchScore < this.fuzzyThreshold) {
|
|
518
|
+
// Strip both search and replace content once (simultaneously)
|
|
519
|
+
const aggressiveSearchContent = stripLineNumbers(searchContent, true)
|
|
520
|
+
const aggressiveReplaceContent = stripLineNumbers(replaceContent, true)
|
|
521
|
+
|
|
522
|
+
const aggressiveSearchLines = aggressiveSearchContent ? aggressiveSearchContent.split(/\r?\n/) : []
|
|
523
|
+
const aggressiveSearchChunk = aggressiveSearchLines.join("\n")
|
|
524
|
+
|
|
525
|
+
// Try middle-out search again with aggressive stripped content (respecting the same search bounds)
|
|
526
|
+
const {
|
|
527
|
+
bestScore,
|
|
528
|
+
bestMatchIndex,
|
|
529
|
+
bestMatchContent: aggContent,
|
|
530
|
+
} = fuzzySearch(resultLines, aggressiveSearchChunk, searchStartIndex, searchEndIndex)
|
|
531
|
+
if (bestMatchIndex !== -1 && bestScore >= this.fuzzyThreshold) {
|
|
532
|
+
matchIndex = bestMatchIndex
|
|
533
|
+
bestMatchScore = bestScore
|
|
534
|
+
bestMatchContent = aggContent
|
|
535
|
+
// Replace the original search/replace with their stripped versions
|
|
536
|
+
searchContent = aggressiveSearchContent
|
|
537
|
+
replaceContent = aggressiveReplaceContent
|
|
538
|
+
searchLines = aggressiveSearchLines
|
|
539
|
+
replaceLines = replaceContent ? replaceContent.split(/\r?\n/) : []
|
|
540
|
+
} else {
|
|
541
|
+
// No match found with either method
|
|
542
|
+
const originalContentSection =
|
|
543
|
+
startLine && endLine
|
|
544
|
+
? `\n\nOriginal Content:\n${addLineNumbers(
|
|
545
|
+
resultLines
|
|
546
|
+
.slice(
|
|
547
|
+
Math.max(0, startLine - 1 - this.bufferLines),
|
|
548
|
+
Math.min(resultLines.length, endLine + this.bufferLines),
|
|
549
|
+
)
|
|
550
|
+
.join("\n"),
|
|
551
|
+
Math.max(1, startLine - this.bufferLines),
|
|
552
|
+
)}`
|
|
553
|
+
: `\n\nOriginal Content:\n${addLineNumbers(
|
|
554
|
+
resultLines
|
|
555
|
+
.slice(
|
|
556
|
+
Math.max(0, (matchIndex >= 0 ? matchIndex : 0) - this.bufferLines),
|
|
557
|
+
Math.min(
|
|
558
|
+
resultLines.length,
|
|
559
|
+
(matchIndex >= 0 ? matchIndex : 0) +
|
|
560
|
+
searchLines.length +
|
|
561
|
+
this.bufferLines,
|
|
562
|
+
),
|
|
563
|
+
)
|
|
564
|
+
.join("\n"),
|
|
565
|
+
Math.max(1, (matchIndex >= 0 ? matchIndex : 0) - this.bufferLines + 1),
|
|
566
|
+
)}`
|
|
567
|
+
|
|
568
|
+
const bestMatchSection = bestMatchContent
|
|
569
|
+
? `\n\nBest Match Found:\n${addLineNumbers(bestMatchContent, matchIndex + 1)}`
|
|
570
|
+
: `\n\nBest Match Found:\n(no match)`
|
|
571
|
+
|
|
572
|
+
const lineRange = startLine ? ` at line: ${startLine}` : ""
|
|
573
|
+
|
|
574
|
+
const levenDist = bestMatchContent
|
|
575
|
+
? distance(normalizeString(searchChunk), normalizeString(bestMatchContent))
|
|
576
|
+
: -1
|
|
577
|
+
|
|
578
|
+
diffResults.push({
|
|
579
|
+
success: false,
|
|
580
|
+
error: `No sufficiently similar match found${lineRange} (${Math.floor(bestMatchScore * 100)}% similar, needs ${Math.floor(this.fuzzyThreshold * 100)}%)\n\nDebug Info:\n- Similarity Score: ${Math.floor(bestMatchScore * 100)}%\n- Required Threshold: ${Math.floor(this.fuzzyThreshold * 100)}%\n- Search Range: ${startLine ? `starting at line ${startLine}` : "start to end"}\n- Levenshtein Distance: ${levenDist >= 0 ? `${levenDist} characters` : "N/A"}\n- Search Length: ${searchChunk.length} characters\n- Best Match Length: ${bestMatchContent ? bestMatchContent.length : 0} characters\n- Tried both standard and aggressive line number stripping\n- Tip: Use the read_file tool to get the latest content of the file before attempting to use the apply_diff tool again, as the file content may have changed\n\nSearch Content:\n${searchChunk}${bestMatchSection}${originalContentSection}`,
|
|
581
|
+
})
|
|
582
|
+
continue
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
// Get the matched lines from the original content
|
|
587
|
+
const matchedLines = resultLines.slice(matchIndex, matchIndex + searchLines.length)
|
|
588
|
+
|
|
589
|
+
// Get the exact indentation (preserving tabs/spaces) of each line
|
|
590
|
+
const originalIndents = matchedLines.map((line) => {
|
|
591
|
+
const match = line.match(/^[\t ]*/)
|
|
592
|
+
return match ? match[0] : ""
|
|
593
|
+
})
|
|
594
|
+
|
|
595
|
+
// Get the exact indentation of each line in the search block
|
|
596
|
+
const searchIndents = searchLines.map((line) => {
|
|
597
|
+
const match = line.match(/^[\t ]*/)
|
|
598
|
+
return match ? match[0] : ""
|
|
599
|
+
})
|
|
600
|
+
|
|
601
|
+
// Apply the replacement while preserving exact indentation
|
|
602
|
+
const indentedReplaceLines = replaceLines.map((line) => {
|
|
603
|
+
// Get the matched line's exact indentation
|
|
604
|
+
const matchedIndent = originalIndents[0] || ""
|
|
605
|
+
|
|
606
|
+
// Get the current line's indentation relative to the search content
|
|
607
|
+
const currentIndentMatch = line.match(/^[\t ]*/)
|
|
608
|
+
const currentIndent = currentIndentMatch ? currentIndentMatch[0] : ""
|
|
609
|
+
const searchBaseIndent = searchIndents[0] || ""
|
|
610
|
+
|
|
611
|
+
// Calculate the relative indentation level
|
|
612
|
+
const searchBaseLevel = searchBaseIndent.length
|
|
613
|
+
const currentLevel = currentIndent.length
|
|
614
|
+
const relativeLevel = currentLevel - searchBaseLevel
|
|
615
|
+
|
|
616
|
+
// If relative level is negative, remove indentation from matched indent
|
|
617
|
+
// If positive, add to matched indent
|
|
618
|
+
const finalIndent =
|
|
619
|
+
relativeLevel < 0
|
|
620
|
+
? matchedIndent.slice(0, Math.max(0, matchedIndent.length + relativeLevel))
|
|
621
|
+
: matchedIndent + currentIndent.slice(searchBaseLevel)
|
|
622
|
+
|
|
623
|
+
return finalIndent + line.trim()
|
|
624
|
+
})
|
|
625
|
+
|
|
626
|
+
// Construct the final content
|
|
627
|
+
const beforeMatch = resultLines.slice(0, matchIndex)
|
|
628
|
+
const afterMatch = resultLines.slice(matchIndex + searchLines.length)
|
|
629
|
+
resultLines = [...beforeMatch, ...indentedReplaceLines, ...afterMatch]
|
|
630
|
+
delta = delta - matchedLines.length + replaceLines.length
|
|
631
|
+
appliedCount++
|
|
632
|
+
}
|
|
633
|
+
const finalContent = resultLines.join(lineEnding)
|
|
634
|
+
if (appliedCount === 0) {
|
|
635
|
+
return {
|
|
636
|
+
success: false,
|
|
637
|
+
failParts: diffResults,
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
return {
|
|
641
|
+
success: true,
|
|
642
|
+
content: finalContent,
|
|
643
|
+
failParts: diffResults,
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
getProgressStatus(toolUse: ToolUse, result?: DiffResult): ToolProgressStatus {
|
|
648
|
+
const diffContent = toolUse.params.diff
|
|
649
|
+
if (diffContent) {
|
|
650
|
+
const icon = "diff-multiple"
|
|
651
|
+
if (toolUse.partial) {
|
|
652
|
+
if (Math.floor(diffContent.length / 10) % 10 === 0) {
|
|
653
|
+
const searchBlockCount = (diffContent.match(/SEARCH/g) || []).length
|
|
654
|
+
return { icon, text: `${searchBlockCount}` }
|
|
655
|
+
}
|
|
656
|
+
} else if (result) {
|
|
657
|
+
const searchBlockCount = (diffContent.match(/SEARCH/g) || []).length
|
|
658
|
+
if (result.failParts?.length) {
|
|
659
|
+
return {
|
|
660
|
+
icon,
|
|
661
|
+
text: `${searchBlockCount - result.failParts.length}/${searchBlockCount}`,
|
|
662
|
+
}
|
|
663
|
+
} else {
|
|
664
|
+
return { icon, text: `${searchBlockCount}` }
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
return {}
|
|
669
|
+
}
|
|
670
|
+
}
|