@oh-my-pi/pi-coding-agent 6.1.0 → 6.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +56 -0
- package/docs/sdk.md +1 -1
- package/package.json +5 -5
- package/scripts/generate-template.ts +6 -6
- package/src/cli/args.ts +3 -0
- package/src/core/agent-session.ts +39 -0
- package/src/core/bash-executor.ts +3 -3
- package/src/core/cursor/exec-bridge.ts +95 -88
- package/src/core/custom-commands/bundled/review/index.ts +142 -145
- package/src/core/custom-commands/bundled/wt/index.ts +68 -66
- package/src/core/custom-commands/loader.ts +4 -6
- package/src/core/custom-tools/index.ts +2 -2
- package/src/core/custom-tools/loader.ts +66 -61
- package/src/core/custom-tools/types.ts +4 -4
- package/src/core/custom-tools/wrapper.ts +61 -25
- package/src/core/event-bus.ts +19 -47
- package/src/core/extensions/index.ts +8 -4
- package/src/core/extensions/loader.ts +160 -120
- package/src/core/extensions/types.ts +4 -4
- package/src/core/extensions/wrapper.ts +149 -100
- package/src/core/hooks/index.ts +1 -1
- package/src/core/hooks/tool-wrapper.ts +96 -70
- package/src/core/hooks/types.ts +1 -2
- package/src/core/index.ts +1 -0
- package/src/core/mcp/index.ts +6 -2
- package/src/core/mcp/json-rpc.ts +88 -0
- package/src/core/mcp/loader.ts +22 -4
- package/src/core/mcp/manager.ts +202 -48
- package/src/core/mcp/tool-bridge.ts +143 -55
- package/src/core/mcp/tool-cache.ts +122 -0
- package/src/core/python-executor.ts +3 -9
- package/src/core/sdk.ts +33 -32
- package/src/core/session-manager.ts +30 -0
- package/src/core/settings-manager.ts +34 -1
- package/src/core/ssh/ssh-executor.ts +6 -84
- package/src/core/streaming-output.ts +107 -53
- package/src/core/tools/ask.ts +92 -93
- package/src/core/tools/bash.ts +103 -94
- package/src/core/tools/calculator.ts +41 -26
- package/src/core/tools/complete.ts +76 -66
- package/src/core/tools/context.ts +25 -25
- package/src/core/tools/exa/index.ts +1 -1
- package/src/core/tools/exa/mcp-client.ts +56 -101
- package/src/core/tools/find.ts +250 -253
- package/src/core/tools/git.ts +39 -33
- package/src/core/tools/grep.ts +440 -427
- package/src/core/tools/index.ts +62 -61
- package/src/core/tools/ls.ts +119 -114
- package/src/core/tools/lsp/clients/biome-client.ts +5 -7
- package/src/core/tools/lsp/clients/index.ts +4 -4
- package/src/core/tools/lsp/clients/lsp-linter-client.ts +5 -7
- package/src/core/tools/lsp/config.ts +2 -2
- package/src/core/tools/lsp/index.ts +824 -639
- package/src/core/tools/notebook.ts +121 -119
- package/src/core/tools/output.ts +163 -147
- package/src/core/tools/patch/applicator.ts +1100 -0
- package/src/core/tools/patch/diff.ts +362 -0
- package/src/core/tools/patch/fuzzy.ts +647 -0
- package/src/core/tools/patch/index.ts +430 -0
- package/src/core/tools/patch/normalize.ts +220 -0
- package/src/core/tools/patch/normative.ts +49 -0
- package/src/core/tools/patch/parser.ts +528 -0
- package/src/core/tools/patch/shared.ts +228 -0
- package/src/core/tools/patch/types.ts +244 -0
- package/src/core/tools/python.ts +139 -136
- package/src/core/tools/read.ts +237 -216
- package/src/core/tools/render-utils.ts +196 -77
- package/src/core/tools/renderers.ts +1 -1
- package/src/core/tools/ssh.ts +99 -80
- package/src/core/tools/task/executor.ts +11 -7
- package/src/core/tools/task/index.ts +352 -343
- package/src/core/tools/task/worker.ts +13 -23
- package/src/core/tools/todo-write.ts +74 -59
- package/src/core/tools/web-fetch.ts +54 -47
- package/src/core/tools/web-search/index.ts +27 -16
- package/src/core/tools/write.ts +89 -41
- package/src/core/ttsr.ts +106 -152
- package/src/core/voice.ts +49 -39
- package/src/index.ts +16 -12
- package/src/lib/worktree/index.ts +1 -9
- package/src/modes/interactive/components/diff.ts +15 -8
- package/src/modes/interactive/components/settings-defs.ts +24 -0
- package/src/modes/interactive/components/tool-execution.ts +34 -6
- package/src/modes/interactive/controllers/event-controller.ts +6 -19
- package/src/modes/interactive/controllers/input-controller.ts +1 -1
- package/src/modes/interactive/utils/ui-helpers.ts +5 -1
- package/src/modes/rpc/rpc-mode.ts +99 -81
- package/src/prompts/tools/patch.md +76 -0
- package/src/prompts/tools/read.md +1 -1
- package/src/prompts/tools/{edit.md → replace.md} +1 -0
- package/src/utils/shell.ts +0 -40
- package/src/core/tools/edit-diff.ts +0 -574
- package/src/core/tools/edit.ts +0 -326
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared utilities for edit tool TUI rendering.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { ToolCallContext } from "@oh-my-pi/pi-agent-core";
|
|
6
|
+
import type { Component } from "@oh-my-pi/pi-tui";
|
|
7
|
+
import { Text } from "@oh-my-pi/pi-tui";
|
|
8
|
+
import { getLanguageFromPath, type Theme } from "../../../modes/interactive/theme/theme";
|
|
9
|
+
import type { RenderResultOptions } from "../../custom-tools/types";
|
|
10
|
+
import type { FileDiagnosticsResult } from "../lsp/index";
|
|
11
|
+
import {
|
|
12
|
+
createToolUIKit,
|
|
13
|
+
formatExpandHint,
|
|
14
|
+
getDiffStats,
|
|
15
|
+
shortenPath,
|
|
16
|
+
type ToolUIKit,
|
|
17
|
+
truncateDiffByHunk,
|
|
18
|
+
} from "../render-utils";
|
|
19
|
+
import type { DiffError, DiffResult, Operation } from "./types";
|
|
20
|
+
|
|
21
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
22
|
+
// LSP Batching
|
|
23
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
24
|
+
|
|
25
|
+
const LSP_BATCH_TOOLS = new Set(["edit", "write"]);
|
|
26
|
+
|
|
27
|
+
export function getLspBatchRequest(toolCall: ToolCallContext | undefined): { id: string; flush: boolean } | undefined {
|
|
28
|
+
if (!toolCall) {
|
|
29
|
+
return undefined;
|
|
30
|
+
}
|
|
31
|
+
const hasOtherWrites = toolCall.toolCalls.some(
|
|
32
|
+
(call, index) => index !== toolCall.index && LSP_BATCH_TOOLS.has(call.name),
|
|
33
|
+
);
|
|
34
|
+
if (!hasOtherWrites) {
|
|
35
|
+
return undefined;
|
|
36
|
+
}
|
|
37
|
+
const hasLaterWrites = toolCall.toolCalls.slice(toolCall.index + 1).some((call) => LSP_BATCH_TOOLS.has(call.name));
|
|
38
|
+
return { id: toolCall.batchId, flush: !hasLaterWrites };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
42
|
+
// Tool Details Types
|
|
43
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
44
|
+
|
|
45
|
+
export interface EditToolDetails {
|
|
46
|
+
/** Unified diff of the changes made */
|
|
47
|
+
diff: string;
|
|
48
|
+
/** Line number of the first change in the new file (for editor navigation) */
|
|
49
|
+
firstChangedLine?: number;
|
|
50
|
+
/** Diagnostic result (if available) */
|
|
51
|
+
diagnostics?: FileDiagnosticsResult;
|
|
52
|
+
/** Operation type (patch mode only) */
|
|
53
|
+
op?: Operation;
|
|
54
|
+
/** New path after move/rename (patch mode only) */
|
|
55
|
+
rename?: string;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
59
|
+
// TUI Renderer
|
|
60
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
61
|
+
|
|
62
|
+
interface EditRenderArgs {
|
|
63
|
+
path?: string;
|
|
64
|
+
file_path?: string;
|
|
65
|
+
oldText?: string;
|
|
66
|
+
newText?: string;
|
|
67
|
+
patch?: string;
|
|
68
|
+
all?: boolean;
|
|
69
|
+
// Patch mode fields
|
|
70
|
+
op?: Operation;
|
|
71
|
+
rename?: string;
|
|
72
|
+
diff?: string;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Extended context for edit tool rendering */
|
|
76
|
+
export interface EditRenderContext {
|
|
77
|
+
/** Pre-computed diff preview (computed before tool executes) */
|
|
78
|
+
editDiffPreview?: DiffResult | DiffError;
|
|
79
|
+
/** Function to render diff text with syntax highlighting */
|
|
80
|
+
renderDiff?: (diffText: string, options?: { filePath?: string }) => string;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const EDIT_DIFF_PREVIEW_HUNKS = 2;
|
|
84
|
+
const EDIT_DIFF_PREVIEW_LINES = 24;
|
|
85
|
+
|
|
86
|
+
function countLines(text: string): number {
|
|
87
|
+
if (!text) return 0;
|
|
88
|
+
return text.split("\n").length;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function formatMetadataLine(lineCount: number | null, language: string | undefined, uiTheme: Theme): string {
|
|
92
|
+
const icon = uiTheme.getLangIcon(language);
|
|
93
|
+
if (lineCount !== null) {
|
|
94
|
+
return uiTheme.fg("dim", `${icon} ${lineCount} lines`);
|
|
95
|
+
}
|
|
96
|
+
return uiTheme.fg("dim", `${icon}`);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function renderDiffSection(
|
|
100
|
+
diff: string,
|
|
101
|
+
rawPath: string,
|
|
102
|
+
expanded: boolean,
|
|
103
|
+
uiTheme: Theme,
|
|
104
|
+
ui: ToolUIKit,
|
|
105
|
+
renderDiffFn: (t: string, o?: { filePath?: string }) => string,
|
|
106
|
+
): string {
|
|
107
|
+
let text = "";
|
|
108
|
+
const diffStats = getDiffStats(diff);
|
|
109
|
+
text += `\n${uiTheme.fg("dim", uiTheme.format.bracketLeft)}${ui.formatDiffStats(
|
|
110
|
+
diffStats.added,
|
|
111
|
+
diffStats.removed,
|
|
112
|
+
diffStats.hunks,
|
|
113
|
+
)}${uiTheme.fg("dim", uiTheme.format.bracketRight)}`;
|
|
114
|
+
|
|
115
|
+
const {
|
|
116
|
+
text: truncatedDiff,
|
|
117
|
+
hiddenHunks,
|
|
118
|
+
hiddenLines,
|
|
119
|
+
} = expanded
|
|
120
|
+
? { text: diff, hiddenHunks: 0, hiddenLines: 0 }
|
|
121
|
+
: truncateDiffByHunk(diff, EDIT_DIFF_PREVIEW_HUNKS, EDIT_DIFF_PREVIEW_LINES);
|
|
122
|
+
|
|
123
|
+
text += `\n\n${renderDiffFn(truncatedDiff, { filePath: rawPath })}`;
|
|
124
|
+
if (!expanded && (hiddenHunks > 0 || hiddenLines > 0)) {
|
|
125
|
+
const remainder: string[] = [];
|
|
126
|
+
if (hiddenHunks > 0) remainder.push(`${hiddenHunks} more hunks`);
|
|
127
|
+
if (hiddenLines > 0) remainder.push(`${hiddenLines} more lines`);
|
|
128
|
+
text += uiTheme.fg(
|
|
129
|
+
"toolOutput",
|
|
130
|
+
`\n${uiTheme.format.ellipsis} (${remainder.join(", ")}) ${formatExpandHint(uiTheme)}`,
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
return text;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export const editToolRenderer = {
|
|
137
|
+
mergeCallAndResult: true,
|
|
138
|
+
|
|
139
|
+
renderCall(args: EditRenderArgs, uiTheme: Theme): Component {
|
|
140
|
+
const ui = createToolUIKit(uiTheme);
|
|
141
|
+
const rawPath = args.file_path || args.path || "";
|
|
142
|
+
const filePath = shortenPath(rawPath);
|
|
143
|
+
const editLanguage = getLanguageFromPath(rawPath) ?? "text";
|
|
144
|
+
const editIcon = uiTheme.fg("muted", uiTheme.getLangIcon(editLanguage));
|
|
145
|
+
let pathDisplay = filePath ? uiTheme.fg("accent", filePath) : uiTheme.fg("toolOutput", uiTheme.format.ellipsis);
|
|
146
|
+
|
|
147
|
+
// Add arrow for move/rename operations
|
|
148
|
+
if (args.rename) {
|
|
149
|
+
pathDisplay += ` ${uiTheme.fg("dim", "→")} ${uiTheme.fg("accent", shortenPath(args.rename))}`;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Show operation type for patch mode
|
|
153
|
+
const opTitle = args.op === "create" ? "Create" : args.op === "delete" ? "Delete" : "Edit";
|
|
154
|
+
const text = `${ui.title(opTitle)} ${editIcon} ${pathDisplay}`;
|
|
155
|
+
return new Text(text, 0, 0);
|
|
156
|
+
},
|
|
157
|
+
|
|
158
|
+
renderResult(
|
|
159
|
+
result: { content: Array<{ type: string; text?: string }>; details?: EditToolDetails; isError?: boolean },
|
|
160
|
+
options: RenderResultOptions & { renderContext?: EditRenderContext },
|
|
161
|
+
uiTheme: Theme,
|
|
162
|
+
args?: EditRenderArgs,
|
|
163
|
+
): Component {
|
|
164
|
+
const ui = createToolUIKit(uiTheme);
|
|
165
|
+
const { expanded, renderContext } = options;
|
|
166
|
+
const rawPath = args?.file_path || args?.path || "";
|
|
167
|
+
const filePath = shortenPath(rawPath);
|
|
168
|
+
const editLanguage = getLanguageFromPath(rawPath) ?? "text";
|
|
169
|
+
const editIcon = uiTheme.fg("muted", uiTheme.getLangIcon(editLanguage));
|
|
170
|
+
const editDiffPreview = renderContext?.editDiffPreview;
|
|
171
|
+
const renderDiffFn = renderContext?.renderDiff ?? ((t: string) => t);
|
|
172
|
+
|
|
173
|
+
// Get op and rename from args or details
|
|
174
|
+
const op = args?.op || result.details?.op;
|
|
175
|
+
const rename = args?.rename || result.details?.rename;
|
|
176
|
+
|
|
177
|
+
// Build path display with line number if available
|
|
178
|
+
let pathDisplay = filePath ? uiTheme.fg("accent", filePath) : uiTheme.fg("toolOutput", uiTheme.format.ellipsis);
|
|
179
|
+
const firstChangedLine =
|
|
180
|
+
(editDiffPreview && "firstChangedLine" in editDiffPreview ? editDiffPreview.firstChangedLine : undefined) ||
|
|
181
|
+
(result.details && !result.isError ? result.details.firstChangedLine : undefined);
|
|
182
|
+
if (firstChangedLine) {
|
|
183
|
+
pathDisplay += uiTheme.fg("warning", `:${firstChangedLine}`);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Add arrow for rename operations
|
|
187
|
+
if (rename) {
|
|
188
|
+
pathDisplay += ` ${uiTheme.fg("dim", "→")} ${uiTheme.fg("accent", shortenPath(rename))}`;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Show operation type for patch mode
|
|
192
|
+
const opTitle = op === "create" ? "Create" : op === "delete" ? "Delete" : "Edit";
|
|
193
|
+
let text = `${uiTheme.fg("toolTitle", uiTheme.bold(opTitle))} ${editIcon} ${pathDisplay}`;
|
|
194
|
+
|
|
195
|
+
// Skip metadata line for delete operations
|
|
196
|
+
if (op !== "delete") {
|
|
197
|
+
const editLineCount = countLines(args?.newText ?? args?.oldText ?? args?.diff ?? args?.patch ?? "");
|
|
198
|
+
text += `\n${formatMetadataLine(editLineCount, editLanguage, uiTheme)}`;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (result.isError) {
|
|
202
|
+
// Show error from result
|
|
203
|
+
const errorText = result.content?.find((c) => c.type === "text")?.text ?? "";
|
|
204
|
+
if (errorText) {
|
|
205
|
+
text += `\n\n${uiTheme.fg("error", errorText)}`;
|
|
206
|
+
}
|
|
207
|
+
} else if (result.details?.diff) {
|
|
208
|
+
// Prefer actual diff after execution
|
|
209
|
+
text += renderDiffSection(result.details.diff, rawPath, expanded, uiTheme, ui, renderDiffFn);
|
|
210
|
+
} else if (editDiffPreview) {
|
|
211
|
+
// Use cached diff preview when no actual diff is available
|
|
212
|
+
if ("error" in editDiffPreview) {
|
|
213
|
+
text += `\n\n${uiTheme.fg("error", editDiffPreview.error)}`;
|
|
214
|
+
} else if (editDiffPreview.diff) {
|
|
215
|
+
text += renderDiffSection(editDiffPreview.diff, rawPath, expanded, uiTheme, ui, renderDiffFn);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Show LSP diagnostics if available
|
|
220
|
+
if (result.details?.diagnostics) {
|
|
221
|
+
text += ui.formatDiagnostics(result.details.diagnostics, expanded, (fp: string) =>
|
|
222
|
+
uiTheme.getLangIcon(getLanguageFromPath(fp)),
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
return new Text(text, 0, 0);
|
|
227
|
+
},
|
|
228
|
+
};
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared types for the edit tool module.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
6
|
+
// File System Abstraction
|
|
7
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
8
|
+
|
|
9
|
+
/** Abstraction for file system operations to support LSP writethrough */
|
|
10
|
+
export interface FileSystem {
|
|
11
|
+
exists(path: string): Promise<boolean>;
|
|
12
|
+
read(path: string): Promise<string>;
|
|
13
|
+
readBinary?: (path: string) => Promise<Uint8Array>;
|
|
14
|
+
write(path: string, content: string): Promise<void>;
|
|
15
|
+
delete(path: string): Promise<void>;
|
|
16
|
+
mkdir(path: string): Promise<void>;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
20
|
+
// Fuzzy Matching Types
|
|
21
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
22
|
+
|
|
23
|
+
/** Result of a fuzzy match operation */
|
|
24
|
+
export interface FuzzyMatch {
|
|
25
|
+
/** The actual text that was matched */
|
|
26
|
+
actualText: string;
|
|
27
|
+
/** Character index where the match starts */
|
|
28
|
+
startIndex: number;
|
|
29
|
+
/** Line number where the match starts (1-indexed) */
|
|
30
|
+
startLine: number;
|
|
31
|
+
/** Confidence score (0-1, where 1 is exact match) */
|
|
32
|
+
confidence: number;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Outcome of attempting to find a match */
|
|
36
|
+
export interface MatchOutcome {
|
|
37
|
+
/** The match if found with sufficient confidence */
|
|
38
|
+
match?: FuzzyMatch;
|
|
39
|
+
/** The closest match found (may be below threshold) */
|
|
40
|
+
closest?: FuzzyMatch;
|
|
41
|
+
/** Number of occurrences if multiple exact matches found */
|
|
42
|
+
occurrences?: number;
|
|
43
|
+
/** Number of fuzzy matches above threshold */
|
|
44
|
+
fuzzyMatches?: number;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Result of a sequence search */
|
|
48
|
+
export interface SequenceSearchResult {
|
|
49
|
+
/** Starting line index of the match (0-indexed) */
|
|
50
|
+
index: number | undefined;
|
|
51
|
+
/** Confidence score (1.0 for exact match, lower for fuzzy) */
|
|
52
|
+
confidence: number;
|
|
53
|
+
/** Number of matches at the same confidence level (for ambiguity detection) */
|
|
54
|
+
matchCount?: number;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Result of a context line search */
|
|
58
|
+
export interface ContextLineResult {
|
|
59
|
+
/** Index of the matching line (0-indexed) */
|
|
60
|
+
index: number | undefined;
|
|
61
|
+
/** Confidence score (1.0 for exact match, lower for fuzzy) */
|
|
62
|
+
confidence: number;
|
|
63
|
+
/** Number of matches at the same confidence level (for ambiguity detection) */
|
|
64
|
+
matchCount?: number;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
68
|
+
// Patch Types
|
|
69
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
70
|
+
|
|
71
|
+
export type Operation = "create" | "delete" | "update";
|
|
72
|
+
|
|
73
|
+
/** Input for a patch operation */
|
|
74
|
+
export interface PatchInput {
|
|
75
|
+
/** File path (relative or absolute) */
|
|
76
|
+
path: string;
|
|
77
|
+
/** Operation type */
|
|
78
|
+
op: Operation;
|
|
79
|
+
/** New path for rename (update only) */
|
|
80
|
+
rename?: string;
|
|
81
|
+
/** File content (create) or diff hunks (update) */
|
|
82
|
+
diff?: string;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Normalized patch input used internally by the applicator. */
|
|
86
|
+
export interface NormalizedPatchInput {
|
|
87
|
+
path: string;
|
|
88
|
+
op: Operation;
|
|
89
|
+
rename?: string;
|
|
90
|
+
diff?: string;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function normalizePatchInput(input: PatchInput): NormalizedPatchInput {
|
|
94
|
+
return {
|
|
95
|
+
path: input.path,
|
|
96
|
+
op: input.op ?? "update",
|
|
97
|
+
rename: input.rename,
|
|
98
|
+
diff: input.diff,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** A single hunk/chunk in a diff */
|
|
103
|
+
export interface DiffHunk {
|
|
104
|
+
/** Context line to narrow down position (e.g., class/method definition) */
|
|
105
|
+
changeContext?: string;
|
|
106
|
+
/** 1-based line hint from unified diff headers (old file) */
|
|
107
|
+
oldStartLine?: number;
|
|
108
|
+
/** 1-based line hint from unified diff headers (new file) */
|
|
109
|
+
newStartLine?: number;
|
|
110
|
+
/** True if the hunk contains context lines (space-prefixed) */
|
|
111
|
+
hasContextLines: boolean;
|
|
112
|
+
/** Lines to be replaced (old content) */
|
|
113
|
+
oldLines: string[];
|
|
114
|
+
/** Lines to replace with (new content) */
|
|
115
|
+
newLines: string[];
|
|
116
|
+
/** If true, oldLines must occur at end of file */
|
|
117
|
+
isEndOfFile: boolean;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** Describes a change made to a file */
|
|
121
|
+
export interface FileChange {
|
|
122
|
+
type: Operation;
|
|
123
|
+
path: string;
|
|
124
|
+
newPath?: string;
|
|
125
|
+
oldContent?: string;
|
|
126
|
+
newContent?: string;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** Result of applying a patch */
|
|
130
|
+
export interface ApplyPatchResult {
|
|
131
|
+
change: FileChange;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** Options for applying a patch */
|
|
135
|
+
export interface ApplyPatchOptions {
|
|
136
|
+
/** Working directory for resolving relative paths */
|
|
137
|
+
cwd: string;
|
|
138
|
+
/** Dry run - compute changes without writing */
|
|
139
|
+
dryRun?: boolean;
|
|
140
|
+
/** Similarity threshold for fuzzy matching */
|
|
141
|
+
fuzzyThreshold?: number;
|
|
142
|
+
/** Allow fuzzy/partial matching when applying hunks */
|
|
143
|
+
allowFuzzy?: boolean;
|
|
144
|
+
/** File system abstraction (defaults to Bun-based implementation) */
|
|
145
|
+
fs?: FileSystem;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
149
|
+
// Diff Generation Types
|
|
150
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
151
|
+
|
|
152
|
+
/** Result of generating a diff */
|
|
153
|
+
export interface DiffResult {
|
|
154
|
+
/** The unified diff string */
|
|
155
|
+
diff: string;
|
|
156
|
+
/** Line number of the first change in the new file */
|
|
157
|
+
firstChangedLine: number | undefined;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** Error from diff computation */
|
|
161
|
+
export interface DiffError {
|
|
162
|
+
error: string;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
166
|
+
// Error Classes
|
|
167
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
168
|
+
|
|
169
|
+
export class ParseError extends Error {
|
|
170
|
+
constructor(
|
|
171
|
+
message: string,
|
|
172
|
+
public readonly lineNumber?: number,
|
|
173
|
+
) {
|
|
174
|
+
super(lineNumber !== undefined ? `Line ${lineNumber}: ${message}` : message);
|
|
175
|
+
this.name = "ParseError";
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export class ApplyPatchError extends Error {
|
|
180
|
+
constructor(message: string) {
|
|
181
|
+
super(message);
|
|
182
|
+
this.name = "ApplyPatchError";
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export class EditMatchError extends Error {
|
|
187
|
+
constructor(
|
|
188
|
+
public readonly path: string,
|
|
189
|
+
public readonly searchText: string,
|
|
190
|
+
public readonly closest: FuzzyMatch | undefined,
|
|
191
|
+
public readonly options: { allowFuzzy: boolean; threshold: number; fuzzyMatches?: number },
|
|
192
|
+
) {
|
|
193
|
+
super(EditMatchError.formatMessage(path, searchText, closest, options));
|
|
194
|
+
this.name = "EditMatchError";
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
static formatMessage(
|
|
198
|
+
path: string,
|
|
199
|
+
searchText: string,
|
|
200
|
+
closest: FuzzyMatch | undefined,
|
|
201
|
+
options: { allowFuzzy: boolean; threshold: number; fuzzyMatches?: number },
|
|
202
|
+
): string {
|
|
203
|
+
if (!closest) {
|
|
204
|
+
return options.allowFuzzy
|
|
205
|
+
? `Could not find a close enough match in ${path}.`
|
|
206
|
+
: `Could not find the exact text in ${path}. The old text must match exactly including all whitespace and newlines.`;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const similarity = Math.round(closest.confidence * 100);
|
|
210
|
+
const searchLines = searchText.split("\n");
|
|
211
|
+
const actualLines = closest.actualText.split("\n");
|
|
212
|
+
const { oldLine, newLine } = findFirstDifferentLine(searchLines, actualLines);
|
|
213
|
+
const thresholdPercent = Math.round(options.threshold * 100);
|
|
214
|
+
|
|
215
|
+
const hint = options.allowFuzzy
|
|
216
|
+
? options.fuzzyMatches && options.fuzzyMatches > 1
|
|
217
|
+
? `Found ${options.fuzzyMatches} high-confidence matches. Provide more context to make it unique.`
|
|
218
|
+
: `Closest match was below the ${thresholdPercent}% similarity threshold.`
|
|
219
|
+
: "Fuzzy matching is disabled. Enable 'Edit fuzzy match' in settings to accept high-confidence matches.";
|
|
220
|
+
|
|
221
|
+
return [
|
|
222
|
+
options.allowFuzzy
|
|
223
|
+
? `Could not find a close enough match in ${path}.`
|
|
224
|
+
: `Could not find the exact text in ${path}.`,
|
|
225
|
+
``,
|
|
226
|
+
`Closest match (${similarity}% similar) at line ${closest.startLine}:`,
|
|
227
|
+
` - ${oldLine}`,
|
|
228
|
+
` + ${newLine}`,
|
|
229
|
+
hint,
|
|
230
|
+
].join("\n");
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function findFirstDifferentLine(oldLines: string[], newLines: string[]): { oldLine: string; newLine: string } {
|
|
235
|
+
const max = Math.max(oldLines.length, newLines.length);
|
|
236
|
+
for (let i = 0; i < max; i++) {
|
|
237
|
+
const oldLine = oldLines[i] ?? "";
|
|
238
|
+
const newLine = newLines[i] ?? "";
|
|
239
|
+
if (oldLine !== newLine) {
|
|
240
|
+
return { oldLine, newLine };
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
return { oldLine: oldLines[0] ?? "", newLine: newLines[0] ?? "" };
|
|
244
|
+
}
|