@yandy0725/pi-coding-tools 0.1.0 → 0.2.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/README.md +1 -22
- package/index.ts +0 -5
- package/package.json +3 -8
- package/src/config.ts +0 -3
- package/src/apply-patch-tool.ts +0 -589
- package/src/apply.ts +0 -306
- package/src/parse.ts +0 -307
- package/src/render.ts +0 -283
- package/src/write-file-atomic.ts +0 -35
package/README.md
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
# pi-coding-tools
|
|
2
2
|
|
|
3
|
-
Pi package
|
|
3
|
+
Pi package enabling `ls`/`find`/`grep` built-in tools.
|
|
4
4
|
|
|
5
5
|
## Features
|
|
6
6
|
|
|
7
|
-
- **apply_patch**: Apply Codex-style patches to files (add/update/delete/move) using a freeform grammar — no JSON wrapping needed.
|
|
8
7
|
- **ls/find/grep**: Enables these built-in tools that are off by default.
|
|
9
8
|
|
|
10
9
|
## Installation
|
|
@@ -23,7 +22,6 @@ Configuration files control which tools are enabled. All default to `true`.
|
|
|
23
22
|
|
|
24
23
|
```json
|
|
25
24
|
{
|
|
26
|
-
"applyPatch": true,
|
|
27
25
|
"ls": true,
|
|
28
26
|
"find": true,
|
|
29
27
|
"grep": true
|
|
@@ -44,25 +42,6 @@ Configuration files control which tools are enabled. All default to `true`.
|
|
|
44
42
|
|
|
45
43
|
| Field | Default | Description |
|
|
46
44
|
|-------|---------|-------------|
|
|
47
|
-
| `applyPatch` | `true` | Register the `apply_patch` tool |
|
|
48
45
|
| `ls` | `true` | Enable the `ls` built-in tool |
|
|
49
46
|
| `find` | `true` | Enable the `find` built-in tool |
|
|
50
47
|
| `grep` | `true` | Enable the `grep` built-in tool |
|
|
51
|
-
|
|
52
|
-
## Patch Format
|
|
53
|
-
|
|
54
|
-
The `apply_patch` tool uses Codex text format:
|
|
55
|
-
|
|
56
|
-
```
|
|
57
|
-
*** Begin Patch
|
|
58
|
-
*** Add File: new.txt
|
|
59
|
-
+Hello, World!
|
|
60
|
-
*** Update File: existing.ts
|
|
61
|
-
@@ function foo() {
|
|
62
|
-
-old line
|
|
63
|
-
+new line
|
|
64
|
-
*** Delete File: old.txt
|
|
65
|
-
*** End Patch
|
|
66
|
-
```
|
|
67
|
-
|
|
68
|
-
See the [Codex apply_patch documentation](https://github.com/code-yeongyu/pi-apply-patch) for full syntax details.
|
package/index.ts
CHANGED
|
@@ -1,15 +1,10 @@
|
|
|
1
1
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import { createApplyPatchTool } from "./src/apply-patch-tool";
|
|
3
2
|
import { loadConfig } from "./src/config";
|
|
4
3
|
import { enableSearchTools } from "./src/search-tools";
|
|
5
4
|
|
|
6
5
|
export default function (pi: ExtensionAPI) {
|
|
7
6
|
const config = loadConfig();
|
|
8
7
|
|
|
9
|
-
if (config.applyPatch) {
|
|
10
|
-
pi.registerTool(createApplyPatchTool());
|
|
11
|
-
}
|
|
12
|
-
|
|
13
8
|
pi.on("session_start", async (_event, _ctx) => {
|
|
14
9
|
enableSearchTools(pi, config);
|
|
15
10
|
});
|
package/package.json
CHANGED
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
"publishConfig": {
|
|
4
4
|
"access": "public"
|
|
5
5
|
},
|
|
6
|
-
"version": "0.
|
|
7
|
-
"description": "pi package
|
|
6
|
+
"version": "0.2.0",
|
|
7
|
+
"description": "pi package enabling ls/find/grep built-in tools",
|
|
8
8
|
"license": "MIT",
|
|
9
9
|
"repository": {
|
|
10
10
|
"type": "git",
|
|
@@ -35,14 +35,9 @@
|
|
|
35
35
|
"peerDependencies": {
|
|
36
36
|
"@earendil-works/pi-coding-agent": ">=0.74.0"
|
|
37
37
|
},
|
|
38
|
-
"dependencies": {
|
|
39
|
-
"@earendil-works/pi-tui": "^0.79.9",
|
|
40
|
-
"diff": "^9.0.0",
|
|
41
|
-
"typebox": "^1.1.0"
|
|
42
|
-
},
|
|
38
|
+
"dependencies": {},
|
|
43
39
|
"devDependencies": {
|
|
44
40
|
"@biomejs/biome": "^2.5.0",
|
|
45
|
-
"@types/diff": "^8.0.0",
|
|
46
41
|
"@types/node": "^22.0.0",
|
|
47
42
|
"typescript": "~5.7.0",
|
|
48
43
|
"vitest": "^3.0.0"
|
package/src/config.ts
CHANGED
|
@@ -3,14 +3,12 @@ import { resolve } from "node:path";
|
|
|
3
3
|
import { CONFIG_DIR_NAME, getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
4
4
|
|
|
5
5
|
export interface CodingToolsConfig {
|
|
6
|
-
applyPatch: boolean;
|
|
7
6
|
ls: boolean;
|
|
8
7
|
find: boolean;
|
|
9
8
|
grep: boolean;
|
|
10
9
|
}
|
|
11
10
|
|
|
12
11
|
const DEFAULT_CONFIG: CodingToolsConfig = {
|
|
13
|
-
applyPatch: true,
|
|
14
12
|
ls: true,
|
|
15
13
|
find: true,
|
|
16
14
|
grep: true,
|
|
@@ -37,7 +35,6 @@ export function loadConfig(cwd?: string): CodingToolsConfig {
|
|
|
37
35
|
const projectConfig = readJsonFile(resolve(dir, CONFIG_DIR_NAME, "coding-tools.json")) || {};
|
|
38
36
|
|
|
39
37
|
cachedConfig = {
|
|
40
|
-
applyPatch: projectConfig.applyPatch ?? globalConfig.applyPatch ?? DEFAULT_CONFIG.applyPatch,
|
|
41
38
|
ls: projectConfig.ls ?? globalConfig.ls ?? DEFAULT_CONFIG.ls,
|
|
42
39
|
find: projectConfig.find ?? globalConfig.find ?? DEFAULT_CONFIG.find,
|
|
43
40
|
grep: projectConfig.grep ?? globalConfig.grep ?? DEFAULT_CONFIG.grep,
|
package/src/apply-patch-tool.ts
DELETED
|
@@ -1,589 +0,0 @@
|
|
|
1
|
-
import { readFile } from "node:fs/promises";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
import type { AgentToolResult, Theme, ToolDefinition } from "@earendil-works/pi-coding-agent";
|
|
4
|
-
import { defineTool, getLanguageFromPath, highlightCode } from "@earendil-works/pi-coding-agent";
|
|
5
|
-
import * as Diff from "diff";
|
|
6
|
-
|
|
7
|
-
type ThemeBg = "selectedBg" | "userMessageBg" | "customMessageBg" | "toolPendingBg" | "toolSuccessBg" | "toolErrorBg";
|
|
8
|
-
|
|
9
|
-
import { Box, Container, Spacer, Text } from "@earendil-works/pi-tui";
|
|
10
|
-
import { Type } from "typebox";
|
|
11
|
-
import type { ApplyPatchProgress, ApplyPatchResult } from "./apply";
|
|
12
|
-
import { applyParsedPatchDetailed, replaceChunks } from "./apply";
|
|
13
|
-
import type { ParsedPatch } from "./parse";
|
|
14
|
-
import {
|
|
15
|
-
APPLY_PATCH_FREEFORM_DESCRIPTION,
|
|
16
|
-
APPLY_PATCH_LARK_GRAMMAR,
|
|
17
|
-
extractPatchedPaths,
|
|
18
|
-
parseNonEmptyPatch,
|
|
19
|
-
parsePatch,
|
|
20
|
-
} from "./parse";
|
|
21
|
-
import type { ApplyPatchPreview, ApplyPatchPreviewFile } from "./render";
|
|
22
|
-
import {
|
|
23
|
-
createPatchDiff,
|
|
24
|
-
formatInFlightCallText,
|
|
25
|
-
formatLineCountSummary,
|
|
26
|
-
formatPatchFileHeader,
|
|
27
|
-
formatPatchFileSummary,
|
|
28
|
-
formatPatchOperation,
|
|
29
|
-
formatPatchPreview,
|
|
30
|
-
readExistingFileForPreview,
|
|
31
|
-
truncatePreview,
|
|
32
|
-
} from "./render";
|
|
33
|
-
|
|
34
|
-
const APPLY_PATCH_PARAMS = Type.Object({
|
|
35
|
-
input: Type.String({
|
|
36
|
-
description: "The entire contents of the apply_patch command",
|
|
37
|
-
}),
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
type FreeformToolFormat = {
|
|
41
|
-
type: "grammar";
|
|
42
|
-
syntax: "lark";
|
|
43
|
-
definition: string;
|
|
44
|
-
};
|
|
45
|
-
|
|
46
|
-
type ApplyPatchParams = {
|
|
47
|
-
input: string;
|
|
48
|
-
};
|
|
49
|
-
|
|
50
|
-
type ApplyPatchToolDetails = {
|
|
51
|
-
preview?: ApplyPatchPreview;
|
|
52
|
-
progress?: ApplyPatchProgress;
|
|
53
|
-
result?: ApplyPatchResult;
|
|
54
|
-
};
|
|
55
|
-
|
|
56
|
-
type ApplyPatchRenderState = {
|
|
57
|
-
cwd: string;
|
|
58
|
-
patchText: string;
|
|
59
|
-
callText: string;
|
|
60
|
-
collapsed: string;
|
|
61
|
-
expanded: string;
|
|
62
|
-
};
|
|
63
|
-
|
|
64
|
-
const applyPatchRenderStates = new Map<string, ApplyPatchRenderState>();
|
|
65
|
-
|
|
66
|
-
function applyLayeredBackground(theme: Theme, bgName: ThemeBg, text: string): string {
|
|
67
|
-
const marker = "\x1fpi-bg-marker\x1f";
|
|
68
|
-
const wrappedMarker = theme.bg(bgName, marker);
|
|
69
|
-
const markerIndex = wrappedMarker.indexOf(marker);
|
|
70
|
-
if (markerIndex === -1) {
|
|
71
|
-
return theme.bg(bgName, text);
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
const bgStart = wrappedMarker.slice(0, markerIndex);
|
|
75
|
-
const bgEnd = wrappedMarker.slice(markerIndex + marker.length);
|
|
76
|
-
// biome-ignore lint/suspicious/noControlCharactersInRegex: ANSI escape codes for terminal colors
|
|
77
|
-
const restored = text.replace(/\x1b\[([0-9;]*)m/g, (sequence: string, params: string) => {
|
|
78
|
-
if (params === "" || params.split(";").some((param) => param === "0" || param === "49")) {
|
|
79
|
-
return `${sequence}${bgStart}`;
|
|
80
|
-
}
|
|
81
|
-
return sequence;
|
|
82
|
-
});
|
|
83
|
-
return `${bgStart}${restored}${bgEnd}`;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
type RenderableAddedDiffLine = { content: string; kind: "added"; lineNumber: string; sign: "+" };
|
|
87
|
-
type RenderableRemovedDiffLine = { content: string; kind: "removed"; lineNumber: string; sign: "-" };
|
|
88
|
-
type RenderableContextDiffLine = { content: string; kind: "context"; lineNumber: string; sign: " " };
|
|
89
|
-
type RenderableContentDiffLine = RenderableAddedDiffLine | RenderableContextDiffLine | RenderableRemovedDiffLine;
|
|
90
|
-
type RenderableDiffLine = RenderableContentDiffLine | { kind: "meta"; text: string };
|
|
91
|
-
|
|
92
|
-
function parseRenderableDiffLine(line: string): RenderableDiffLine {
|
|
93
|
-
const match = line.match(/^([+\- ])(\s*\d+)\s(.*)$/);
|
|
94
|
-
if (!match) {
|
|
95
|
-
return { kind: "meta", text: line };
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
const sign = match[1];
|
|
99
|
-
const lineNumber = match[2];
|
|
100
|
-
if ((sign !== "+" && sign !== "-" && sign !== " ") || lineNumber === undefined) {
|
|
101
|
-
return { kind: "meta", text: line };
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
const content = match[3] ?? "";
|
|
105
|
-
if (sign === "+") {
|
|
106
|
-
return { content, kind: "added", lineNumber, sign };
|
|
107
|
-
}
|
|
108
|
-
if (sign === "-") {
|
|
109
|
-
return { content, kind: "removed", lineNumber, sign };
|
|
110
|
-
}
|
|
111
|
-
return { content, kind: "context", lineNumber, sign };
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
function replaceTabs(text: string): string {
|
|
115
|
-
return text.replace(/\t/g, " ");
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
function highlightDiffContent(content: string, filePath: string): string {
|
|
119
|
-
const plainContent = replaceTabs(content);
|
|
120
|
-
const language = getLanguageFromPath(filePath);
|
|
121
|
-
try {
|
|
122
|
-
return highlightCode(plainContent, language)[0] ?? plainContent;
|
|
123
|
-
} catch {
|
|
124
|
-
return plainContent;
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
function renderInlineDiff(oldContent: string, newContent: string, theme: Theme): { added: string; removed: string } {
|
|
129
|
-
const parts = Diff.diffWords(replaceTabs(oldContent), replaceTabs(newContent));
|
|
130
|
-
let added = "";
|
|
131
|
-
let removed = "";
|
|
132
|
-
let firstAdded = true;
|
|
133
|
-
let firstRemoved = true;
|
|
134
|
-
|
|
135
|
-
for (const part of parts) {
|
|
136
|
-
if (part.added) {
|
|
137
|
-
let value = part.value;
|
|
138
|
-
if (firstAdded) {
|
|
139
|
-
const leadingWhitespace = value.match(/^(\s*)/)?.[1] ?? "";
|
|
140
|
-
added += leadingWhitespace;
|
|
141
|
-
value = value.slice(leadingWhitespace.length);
|
|
142
|
-
firstAdded = false;
|
|
143
|
-
}
|
|
144
|
-
if (value) {
|
|
145
|
-
added += theme.inverse(value);
|
|
146
|
-
}
|
|
147
|
-
continue;
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
if (part.removed) {
|
|
151
|
-
let value = part.value;
|
|
152
|
-
if (firstRemoved) {
|
|
153
|
-
const leadingWhitespace = value.match(/^(\s*)/)?.[1] ?? "";
|
|
154
|
-
removed += leadingWhitespace;
|
|
155
|
-
value = value.slice(leadingWhitespace.length);
|
|
156
|
-
firstRemoved = false;
|
|
157
|
-
}
|
|
158
|
-
if (value) {
|
|
159
|
-
removed += theme.inverse(value);
|
|
160
|
-
}
|
|
161
|
-
continue;
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
added += part.value;
|
|
165
|
-
removed += part.value;
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
return { added, removed };
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
function renderOpenCodeLikeDiffLine(
|
|
172
|
-
line: RenderableContentDiffLine,
|
|
173
|
-
filePath: string,
|
|
174
|
-
theme: Theme,
|
|
175
|
-
contentOverride?: string,
|
|
176
|
-
): string {
|
|
177
|
-
const lineNumber = theme.fg("muted", line.lineNumber);
|
|
178
|
-
if (line.kind === "context") {
|
|
179
|
-
return `${theme.fg("toolDiffContext", line.sign)}${lineNumber} ${highlightDiffContent(line.content, filePath)}`;
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
const diffColor = line.kind === "added" ? "toolDiffAdded" : "toolDiffRemoved";
|
|
183
|
-
const background = line.kind === "added" ? "toolSuccessBg" : "toolErrorBg";
|
|
184
|
-
const content =
|
|
185
|
-
contentOverride === undefined
|
|
186
|
-
? highlightDiffContent(line.content, filePath)
|
|
187
|
-
: theme.fg(diffColor, replaceTabs(contentOverride));
|
|
188
|
-
const rendered = `${theme.fg(diffColor, line.sign)}${lineNumber} ${content}`;
|
|
189
|
-
return theme.bg(background, rendered);
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
function renderOpenCodeLikeDiff(diffText: string, filePath: string, theme: Theme): string {
|
|
193
|
-
const parsedLines = diffText.split("\n").map(parseRenderableDiffLine);
|
|
194
|
-
const rendered: string[] = [];
|
|
195
|
-
let index = 0;
|
|
196
|
-
|
|
197
|
-
while (index < parsedLines.length) {
|
|
198
|
-
const line = parsedLines[index];
|
|
199
|
-
if (!line) {
|
|
200
|
-
index++;
|
|
201
|
-
continue;
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
if (line.kind !== "removed") {
|
|
205
|
-
rendered.push(
|
|
206
|
-
line.kind === "meta" ? theme.fg("toolDiffContext", line.text) : renderOpenCodeLikeDiffLine(line, filePath, theme),
|
|
207
|
-
);
|
|
208
|
-
index++;
|
|
209
|
-
continue;
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
const removedLines: RenderableRemovedDiffLine[] = [];
|
|
213
|
-
while (parsedLines[index]?.kind === "removed") {
|
|
214
|
-
const removedLine = parsedLines[index];
|
|
215
|
-
if (removedLine?.kind === "removed") {
|
|
216
|
-
removedLines.push(removedLine);
|
|
217
|
-
}
|
|
218
|
-
index++;
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
const addedLines: RenderableAddedDiffLine[] = [];
|
|
222
|
-
while (parsedLines[index]?.kind === "added") {
|
|
223
|
-
const addedLine = parsedLines[index];
|
|
224
|
-
if (addedLine?.kind === "added") {
|
|
225
|
-
addedLines.push(addedLine);
|
|
226
|
-
}
|
|
227
|
-
index++;
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
const pairedCount = Math.min(removedLines.length, addedLines.length);
|
|
231
|
-
for (let pairIndex = 0; pairIndex < pairedCount; pairIndex++) {
|
|
232
|
-
const removedLine = removedLines[pairIndex];
|
|
233
|
-
const addedLine = addedLines[pairIndex];
|
|
234
|
-
if (!removedLine || !addedLine) {
|
|
235
|
-
continue;
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
const inline = renderInlineDiff(removedLine.content, addedLine.content, theme);
|
|
239
|
-
rendered.push(renderOpenCodeLikeDiffLine(removedLine, filePath, theme, inline.removed));
|
|
240
|
-
rendered.push(renderOpenCodeLikeDiffLine(addedLine, filePath, theme, inline.added));
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
for (const removedLine of removedLines.slice(pairedCount)) {
|
|
244
|
-
rendered.push(renderOpenCodeLikeDiffLine(removedLine, filePath, theme));
|
|
245
|
-
}
|
|
246
|
-
for (const addedLine of addedLines.slice(pairedCount)) {
|
|
247
|
-
rendered.push(renderOpenCodeLikeDiffLine(addedLine, filePath, theme));
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
return rendered.join("\n");
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
function renderPatchPreview(preview: ApplyPatchPreview, cwd: string, theme: Theme, expanded: boolean): string {
|
|
255
|
-
if (expanded) {
|
|
256
|
-
try {
|
|
257
|
-
const renderFile = (file: ApplyPatchPreviewFile, headerPrefix: string): string => {
|
|
258
|
-
const header = formatPatchFileHeader(file, cwd);
|
|
259
|
-
if (!file.diff) {
|
|
260
|
-
return headerPrefix.length > 0 ? `${headerPrefix}${formatPatchFileSummary(file, cwd)}` : header;
|
|
261
|
-
}
|
|
262
|
-
const previewDiff = truncatePreview(file.diff);
|
|
263
|
-
const renderedDiff = renderOpenCodeLikeDiff(previewDiff, file.movePath ?? file.filePath, theme);
|
|
264
|
-
if (headerPrefix.length > 0) {
|
|
265
|
-
const nestedHeader = `${headerPrefix}${formatPatchFileSummary(file, cwd)}`;
|
|
266
|
-
return `${nestedHeader}\n${renderedDiff
|
|
267
|
-
.split("\n")
|
|
268
|
-
.map((line) => ` ${line}`)
|
|
269
|
-
.join("\n")}`;
|
|
270
|
-
}
|
|
271
|
-
return `${header}\n${renderedDiff}`;
|
|
272
|
-
};
|
|
273
|
-
|
|
274
|
-
if (preview.files.length === 1) {
|
|
275
|
-
const file = preview.files[0];
|
|
276
|
-
return file ? renderFile(file, "") : "";
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
const noun = "files";
|
|
280
|
-
const renderedFiles = preview.files.map((file) => renderFile(file, " \u2514 ")).join("\n");
|
|
281
|
-
if (renderedFiles.length > 0) {
|
|
282
|
-
return `${formatPatchOperation("update")} ${preview.files.length} ${noun} ${formatLineCountSummary(preview.added, preview.removed)}\n${renderedFiles}`;
|
|
283
|
-
}
|
|
284
|
-
} catch {
|
|
285
|
-
// fall back to manual themed line rendering
|
|
286
|
-
}
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
return formatPatchPreview(preview, cwd, expanded)
|
|
290
|
-
.split("\n")
|
|
291
|
-
.map((line) => {
|
|
292
|
-
const trimmed = line.trimStart();
|
|
293
|
-
if (trimmed.startsWith("+")) {
|
|
294
|
-
return theme.fg("toolDiffAdded", line);
|
|
295
|
-
}
|
|
296
|
-
if (trimmed.startsWith("-")) {
|
|
297
|
-
return theme.fg("toolDiffRemoved", line);
|
|
298
|
-
}
|
|
299
|
-
if (trimmed.startsWith("\u2022")) {
|
|
300
|
-
return theme.fg("toolTitle", theme.bold(line));
|
|
301
|
-
}
|
|
302
|
-
if (trimmed.startsWith("\u2514")) {
|
|
303
|
-
return theme.fg("accent", line);
|
|
304
|
-
}
|
|
305
|
-
return theme.fg("toolDiffContext", line);
|
|
306
|
-
})
|
|
307
|
-
.join("\n");
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
function getApplyPatchRenderState(toolCallId: string, cwd: string, patchText: string): ApplyPatchRenderState {
|
|
311
|
-
const existing = applyPatchRenderStates.get(toolCallId);
|
|
312
|
-
if (existing && existing.cwd === cwd && existing.patchText === patchText) {
|
|
313
|
-
return existing;
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
const callText = formatInFlightCallText(patchText);
|
|
317
|
-
let collapsed = "";
|
|
318
|
-
let expanded = "";
|
|
319
|
-
try {
|
|
320
|
-
const hunks = parsePatch(patchText);
|
|
321
|
-
if (hunks.length > 0) {
|
|
322
|
-
const files = hunks.map((hunk) => {
|
|
323
|
-
const file = {
|
|
324
|
-
filePath: hunk.filePath,
|
|
325
|
-
operation: hunk.type,
|
|
326
|
-
diff: "",
|
|
327
|
-
added: 0,
|
|
328
|
-
removed: 0,
|
|
329
|
-
} satisfies ApplyPatchPreviewFile;
|
|
330
|
-
return hunk.type === "update" && hunk.movePath !== undefined ? { ...file, movePath: hunk.movePath } : file;
|
|
331
|
-
}) satisfies ApplyPatchPreviewFile[];
|
|
332
|
-
const preview: ApplyPatchPreview = { files, added: 0, removed: 0 };
|
|
333
|
-
collapsed = formatPatchPreview(preview, cwd, false);
|
|
334
|
-
expanded = formatPatchPreview(preview, cwd, true);
|
|
335
|
-
}
|
|
336
|
-
} catch {
|
|
337
|
-
// leave summaries empty for partial/incomplete patch text
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
const nextState: ApplyPatchRenderState = { cwd, patchText, callText, collapsed, expanded };
|
|
341
|
-
applyPatchRenderStates.set(toolCallId, nextState);
|
|
342
|
-
return nextState;
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
export function clearApplyPatchRenderState(): void {
|
|
346
|
-
applyPatchRenderStates.clear();
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
function normalizeApplyPatchArguments(args: unknown): ApplyPatchParams {
|
|
350
|
-
if (typeof args === "string") {
|
|
351
|
-
return { input: args };
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
if (args && typeof args === "object" && "input" in args) {
|
|
355
|
-
const input = (args as { input?: unknown }).input;
|
|
356
|
-
if (typeof input === "string") {
|
|
357
|
-
return { input };
|
|
358
|
-
}
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
return { input: "" };
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
async function createPatchPreview(cwd: string, hunks: ParsedPatch[]): Promise<ApplyPatchPreview> {
|
|
365
|
-
const files: ApplyPatchPreviewFile[] = [];
|
|
366
|
-
for (const hunk of hunks) {
|
|
367
|
-
const absolutePath = path.resolve(cwd, hunk.filePath);
|
|
368
|
-
if (hunk.type === "add") {
|
|
369
|
-
const oldContent = await readExistingFileForPreview(absolutePath);
|
|
370
|
-
const diff = createPatchDiff(oldContent, hunk.content);
|
|
371
|
-
files.push({ filePath: hunk.filePath, operation: oldContent.length > 0 ? "update" : "add", ...diff });
|
|
372
|
-
continue;
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
if (hunk.type === "delete") {
|
|
376
|
-
const oldContent = await readFile(absolutePath, "utf-8");
|
|
377
|
-
const diff = createPatchDiff(oldContent, "");
|
|
378
|
-
files.push({ filePath: hunk.filePath, operation: "delete", ...diff });
|
|
379
|
-
continue;
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
const oldContent = await readFile(absolutePath, "utf-8");
|
|
383
|
-
const newContent =
|
|
384
|
-
hunk.chunks.length === 0 ? oldContent : replaceChunks(oldContent, hunk.filePath, hunk.chunks).content;
|
|
385
|
-
const diff = createPatchDiff(oldContent, newContent);
|
|
386
|
-
const file = { filePath: hunk.filePath, operation: "update", ...diff } satisfies ApplyPatchPreviewFile;
|
|
387
|
-
files.push(hunk.movePath !== undefined ? { ...file, movePath: hunk.movePath } : file);
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
return {
|
|
391
|
-
files,
|
|
392
|
-
added: files.reduce((sum, file) => sum + file.added, 0),
|
|
393
|
-
removed: files.reduce((sum, file) => sum + file.removed, 0),
|
|
394
|
-
};
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
function formatPendingPatchPaths(patchText: string): string {
|
|
398
|
-
const paths = extractPatchedPaths(patchText);
|
|
399
|
-
if (paths.length === 0) {
|
|
400
|
-
return "Applying patch...";
|
|
401
|
-
}
|
|
402
|
-
return `Applying patch...\n${paths.map((filePath) => `\u2022 ${filePath}`).join("\n")}`;
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
async function createPendingPatchUpdate(
|
|
406
|
-
cwd: string,
|
|
407
|
-
patchText: string,
|
|
408
|
-
progress?: ApplyPatchProgress,
|
|
409
|
-
previewOverride?: ApplyPatchPreview,
|
|
410
|
-
parsedHunks?: ParsedPatch[],
|
|
411
|
-
): Promise<{ text: string; details: ApplyPatchToolDetails | undefined }> {
|
|
412
|
-
const title = progress
|
|
413
|
-
? `Applying patch (${progress.applied + progress.failed}/${progress.total})...`
|
|
414
|
-
: "Applying patch...";
|
|
415
|
-
if (previewOverride) {
|
|
416
|
-
const details: ApplyPatchToolDetails = { preview: previewOverride };
|
|
417
|
-
if (progress) details.progress = progress;
|
|
418
|
-
return {
|
|
419
|
-
text: `${title}\n${formatPatchPreview(previewOverride, cwd)}`,
|
|
420
|
-
details,
|
|
421
|
-
};
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
try {
|
|
425
|
-
const hunks = parsedHunks ?? parsePatch(patchText);
|
|
426
|
-
if (hunks.length === 0) {
|
|
427
|
-
return { text: title, details: progress ? { progress } : undefined };
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
const preview = await createPatchPreview(cwd, hunks);
|
|
431
|
-
if (preview.files.some((file) => file.diff.trim().length > 0)) {
|
|
432
|
-
const details: ApplyPatchToolDetails = { preview };
|
|
433
|
-
if (progress) details.progress = progress;
|
|
434
|
-
return { text: `${title}\n${formatPatchPreview(preview, cwd)}`, details };
|
|
435
|
-
}
|
|
436
|
-
} catch {
|
|
437
|
-
return {
|
|
438
|
-
text: progress ? title : formatPendingPatchPaths(patchText),
|
|
439
|
-
details: progress ? { progress } : undefined,
|
|
440
|
-
};
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
return { text: progress ? title : formatPendingPatchPaths(patchText), details: progress ? { progress } : undefined };
|
|
444
|
-
}
|
|
445
|
-
|
|
446
|
-
export function createApplyPatchTool(): ToolDefinition<typeof APPLY_PATCH_PARAMS, ApplyPatchToolDetails | undefined> & {
|
|
447
|
-
freeform: FreeformToolFormat;
|
|
448
|
-
} {
|
|
449
|
-
const tool = defineTool<typeof APPLY_PATCH_PARAMS, ApplyPatchToolDetails | undefined>({
|
|
450
|
-
name: "apply_patch",
|
|
451
|
-
label: "ApplyPatch",
|
|
452
|
-
description: APPLY_PATCH_FREEFORM_DESCRIPTION,
|
|
453
|
-
parameters: APPLY_PATCH_PARAMS,
|
|
454
|
-
prepareArguments: normalizeApplyPatchArguments,
|
|
455
|
-
promptSnippet: "Apply Codex-format file patches with apply_patch",
|
|
456
|
-
promptGuidelines: [
|
|
457
|
-
"Use apply_patch for file edits instead of mutating files through bash, Python scripts, heredocs, or shell redirection.",
|
|
458
|
-
"After apply_patch succeeds, do not re-read the edited files just to confirm the patch applied.",
|
|
459
|
-
],
|
|
460
|
-
async execute(
|
|
461
|
-
_toolCallId,
|
|
462
|
-
params,
|
|
463
|
-
_signal,
|
|
464
|
-
onUpdate,
|
|
465
|
-
ctx,
|
|
466
|
-
): Promise<AgentToolResult<ApplyPatchToolDetails | undefined>> {
|
|
467
|
-
const normalizedParams = normalizeApplyPatchArguments(params);
|
|
468
|
-
if (!normalizedParams.input) {
|
|
469
|
-
throw new Error("input is required");
|
|
470
|
-
}
|
|
471
|
-
|
|
472
|
-
let parsedHunks: ParsedPatch[] | undefined;
|
|
473
|
-
try {
|
|
474
|
-
parsedHunks = parseNonEmptyPatch(normalizedParams.input);
|
|
475
|
-
} catch {
|
|
476
|
-
// createPendingPatchUpdate keeps incomplete or invalid patch text renderable.
|
|
477
|
-
}
|
|
478
|
-
const totalOperations = parsedHunks?.length ?? 0;
|
|
479
|
-
const initialProgress = totalOperations > 0 ? { applied: 0, failed: 0, total: totalOperations } : undefined;
|
|
480
|
-
const pendingUpdate = await createPendingPatchUpdate(
|
|
481
|
-
ctx.cwd,
|
|
482
|
-
normalizedParams.input,
|
|
483
|
-
initialProgress,
|
|
484
|
-
undefined,
|
|
485
|
-
parsedHunks,
|
|
486
|
-
);
|
|
487
|
-
onUpdate?.({
|
|
488
|
-
content: [{ type: "text", text: pendingUpdate.text }],
|
|
489
|
-
details: pendingUpdate.details,
|
|
490
|
-
});
|
|
491
|
-
|
|
492
|
-
const preview = pendingUpdate.details?.preview;
|
|
493
|
-
const result = await applyParsedPatchDetailed(
|
|
494
|
-
ctx.cwd,
|
|
495
|
-
parsedHunks ?? parseNonEmptyPatch(normalizedParams.input),
|
|
496
|
-
async (progress) => {
|
|
497
|
-
const progressUpdate = await createPendingPatchUpdate(
|
|
498
|
-
ctx.cwd,
|
|
499
|
-
normalizedParams.input,
|
|
500
|
-
progress,
|
|
501
|
-
preview,
|
|
502
|
-
parsedHunks,
|
|
503
|
-
);
|
|
504
|
-
onUpdate?.({
|
|
505
|
-
content: [{ type: "text", text: progressUpdate.text }],
|
|
506
|
-
details: progressUpdate.details,
|
|
507
|
-
});
|
|
508
|
-
},
|
|
509
|
-
);
|
|
510
|
-
if (result.failures.length > 0) {
|
|
511
|
-
const mustReadFiles = result.recoveryInstructions.mustReadFiles;
|
|
512
|
-
const failed = mustReadFiles.join(", ");
|
|
513
|
-
const mustReadText = mustReadFiles.join(" and ");
|
|
514
|
-
return {
|
|
515
|
-
content: [
|
|
516
|
-
{
|
|
517
|
-
type: "text",
|
|
518
|
-
text: [
|
|
519
|
-
"apply_patch partially failed.",
|
|
520
|
-
`Failed: ${failed}`,
|
|
521
|
-
`Recovery: MUST read ${mustReadText} before retrying.`,
|
|
522
|
-
result.appliedFiles.length > 0
|
|
523
|
-
? "Earlier file actions in this patch were already applied."
|
|
524
|
-
: "No file actions were applied.",
|
|
525
|
-
result.recoveryInstructions.mustNotReadFiles.length > 0
|
|
526
|
-
? "Recovery: MUST NOT reread other files from this patch unless a specific dependency requires it."
|
|
527
|
-
: "",
|
|
528
|
-
]
|
|
529
|
-
.filter((line) => line.length > 0)
|
|
530
|
-
.join("\n"),
|
|
531
|
-
},
|
|
532
|
-
],
|
|
533
|
-
details: { result },
|
|
534
|
-
};
|
|
535
|
-
}
|
|
536
|
-
|
|
537
|
-
return {
|
|
538
|
-
content: [{ type: "text", text: result.summaries.join("\n") }],
|
|
539
|
-
details: { result },
|
|
540
|
-
};
|
|
541
|
-
},
|
|
542
|
-
renderCall(args, theme, context) {
|
|
543
|
-
if (!context.argsComplete) {
|
|
544
|
-
return new Text(theme.fg("toolTitle", theme.bold("apply_patch: Patching")), 0, 0);
|
|
545
|
-
}
|
|
546
|
-
|
|
547
|
-
const normalizedArgs = normalizeApplyPatchArguments(args);
|
|
548
|
-
const renderState = getApplyPatchRenderState(context.toolCallId, context.cwd, normalizedArgs.input);
|
|
549
|
-
const text = renderState.callText.length > 0 ? `apply_patch: ${renderState.callText}` : "apply_patch";
|
|
550
|
-
return new Text(theme.fg("toolTitle", theme.bold(text)), 0, 0);
|
|
551
|
-
},
|
|
552
|
-
renderResult(result: AgentToolResult<ApplyPatchToolDetails | undefined>, options, theme, context) {
|
|
553
|
-
const component = new Container();
|
|
554
|
-
const preview = result.details?.preview;
|
|
555
|
-
if (preview) {
|
|
556
|
-
const bgName = options.isPartial ? "toolPendingBg" : "toolSuccessBg";
|
|
557
|
-
const progress = result.details?.progress;
|
|
558
|
-
const title = progress
|
|
559
|
-
? `Applying patch (${progress.applied + progress.failed}/${progress.total})`
|
|
560
|
-
: "Applying patch";
|
|
561
|
-
const box = new Box(1, 1, (text: string) => applyLayeredBackground(theme, bgName, text));
|
|
562
|
-
box.addChild(new Text(theme.fg("toolTitle", theme.bold(title)), 0, 0));
|
|
563
|
-
box.addChild(new Spacer(1));
|
|
564
|
-
const expanded = options.isPartial ? true : (options.expanded ?? true);
|
|
565
|
-
box.addChild(new Text(renderPatchPreview(preview, context.cwd, theme, expanded), 0, 0));
|
|
566
|
-
component.addChild(box);
|
|
567
|
-
return component;
|
|
568
|
-
}
|
|
569
|
-
|
|
570
|
-
const text = result.content
|
|
571
|
-
.filter((block) => block.type === "text")
|
|
572
|
-
.map((block) => block.text)
|
|
573
|
-
.filter((value): value is string => typeof value === "string" && value.length > 0)
|
|
574
|
-
.join("\n");
|
|
575
|
-
if (text) {
|
|
576
|
-
component.addChild(new Text(theme.fg("toolOutput", text), 0, 0));
|
|
577
|
-
}
|
|
578
|
-
return component;
|
|
579
|
-
},
|
|
580
|
-
});
|
|
581
|
-
|
|
582
|
-
return Object.assign(tool, {
|
|
583
|
-
freeform: {
|
|
584
|
-
type: "grammar",
|
|
585
|
-
syntax: "lark",
|
|
586
|
-
definition: APPLY_PATCH_LARK_GRAMMAR,
|
|
587
|
-
} satisfies FreeformToolFormat,
|
|
588
|
-
});
|
|
589
|
-
}
|