@howaboua/pi-codex-conversion 1.0.10 → 1.0.12
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 +6 -1
- package/package.json +1 -1
- package/src/patch/core.ts +69 -24
- package/src/patch/types.ts +22 -0
- package/src/prompt/build-system-prompt.ts +3 -0
- package/src/tools/apply-patch-rendering.ts +13 -13
- package/src/tools/apply-patch-tool.ts +211 -15
- package/src/tools/exec-command-tool.ts +1 -0
- package/src/tools/exec-session-manager.ts +60 -4
- package/src/tools/write-stdin-tool.ts +4 -0
package/README.md
CHANGED
|
@@ -55,11 +55,13 @@ npm run check
|
|
|
55
55
|
- `rg --files src | head -n 50` -> `Explored / List src`
|
|
56
56
|
- `cat README.md` -> `Explored / Read README.md`
|
|
57
57
|
- `exec_command({ cmd: "npm test", yield_time_ms: 1000 })` may return `session_id`, then continue with `write_stdin`
|
|
58
|
-
-
|
|
58
|
+
- for short or non-interactive commands, omitting `yield_time_ms` is preferred; tiny non-interactive waits are clamped upward to avoid unnecessary follow-up calls
|
|
59
|
+
- `write_stdin({ session_id, chars: "" })` renders like `Waited for background terminal` and is meant for occasional polling, not tight repoll loops
|
|
59
60
|
- `write_stdin({ session_id, chars: "y\\n" })` renders like `Interacted with background terminal`
|
|
60
61
|
- `view_image({ path: "/absolute/path/to/screenshot.png" })` is available on image-capable models
|
|
61
62
|
- `web_search` is surfaced only on `openai-codex`, and the adapter rewrites it into the native OpenAI Responses `type: "web_search"` payload instead of executing a local function tool
|
|
62
63
|
- when native web search is available, the adapter shows a one-time session notice; individual searches are not surfaced because Pi does not expose native web-search execution events to extensions
|
|
64
|
+
- `apply_patch` partial failures stay inline in the patch row so successful and failed file entries can be seen together
|
|
63
65
|
|
|
64
66
|
Raw command output is still available by expanding the tool result.
|
|
65
67
|
|
|
@@ -136,7 +138,10 @@ That keeps the prompt much closer to `pi-mono` while still steering the model to
|
|
|
136
138
|
- `view_image` resolves paths against the active session cwd and only exposes `detail: "original"` for Codex-family image-capable models.
|
|
137
139
|
- `web_search` is exposed only for the `openai-codex` provider and is forwarded as the native OpenAI Codex Responses web search tool.
|
|
138
140
|
- `apply_patch` paths stay restricted to the current working directory.
|
|
141
|
+
- partial `apply_patch` failures stay in the original patch block and highlight the failed entry instead of adding a second warning row.
|
|
139
142
|
- `exec_command` / `write_stdin` use a custom PTY-backed session manager via `node-pty` for interactive sessions.
|
|
143
|
+
- tiny `exec_command` waits are clamped for non-interactive commands so short runs do not burn an avoidable follow-up tool call.
|
|
144
|
+
- empty `write_stdin` polls are clamped to a meaningful minimum wait so long-running processes are not repolled too aggressively.
|
|
140
145
|
- PTY output handling applies basic terminal rewrite semantics (`\r`, `\b`, erase-in-line, and common escape cleanup) so interactive redraws replay sensibly.
|
|
141
146
|
- Skills inventory is reintroduced in a Codex-style section when Pi's composed prompt already exposed the underlying Pi skills inventory.
|
|
142
147
|
|
package/package.json
CHANGED
package/src/patch/core.ts
CHANGED
|
@@ -1,8 +1,36 @@
|
|
|
1
|
-
import
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
2
|
import { dirname } from "node:path";
|
|
3
3
|
import { parsePatchActions, parseUpdateFile } from "./parser.ts";
|
|
4
4
|
import { openFileAtPath, pathExists, removeFileAtPath, resolvePatchPath, writeFileAtPath } from "./paths.ts";
|
|
5
|
-
import { DiffError, type ExecutePatchResult, type ParsedPatchAction, type ParserState, type PatchAction } from "./types.ts";
|
|
5
|
+
import { DiffError, ExecutePatchError, type ExecutePatchResult, type ParsedPatchAction, type ParserState, type PatchAction } from "./types.ts";
|
|
6
|
+
|
|
7
|
+
export const patchFsOps = {
|
|
8
|
+
mkdirSync: fs.mkdirSync,
|
|
9
|
+
writeFileSync: fs.writeFileSync,
|
|
10
|
+
unlinkSync: fs.unlinkSync,
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
function buildExecutePatchResult({
|
|
14
|
+
changedFiles,
|
|
15
|
+
createdFiles,
|
|
16
|
+
deletedFiles,
|
|
17
|
+
movedFiles,
|
|
18
|
+
fuzz,
|
|
19
|
+
}: {
|
|
20
|
+
changedFiles: Set<string>;
|
|
21
|
+
createdFiles: Set<string>;
|
|
22
|
+
deletedFiles: Set<string>;
|
|
23
|
+
movedFiles: Set<string>;
|
|
24
|
+
fuzz: number;
|
|
25
|
+
}): ExecutePatchResult {
|
|
26
|
+
return {
|
|
27
|
+
changedFiles: [...changedFiles],
|
|
28
|
+
createdFiles: [...createdFiles],
|
|
29
|
+
deletedFiles: [...deletedFiles],
|
|
30
|
+
movedFiles: [...movedFiles],
|
|
31
|
+
fuzz,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
6
34
|
|
|
7
35
|
function splitFileLines(text: string): string[] {
|
|
8
36
|
const lines = text.split("\n");
|
|
@@ -103,21 +131,23 @@ function applyMove({
|
|
|
103
131
|
const toAbsolutePath = resolvePatchPath({ cwd, patchPath: movePath });
|
|
104
132
|
const destinationExisted = pathExists({ cwd, path: movePath });
|
|
105
133
|
|
|
106
|
-
mkdirSync(dirname(toAbsolutePath), { recursive: true });
|
|
107
|
-
writeFileSync(toAbsolutePath, content, "utf8");
|
|
108
|
-
if (fromAbsolutePath !== toAbsolutePath) {
|
|
109
|
-
unlinkSync(fromAbsolutePath);
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
changedFiles.add(path);
|
|
134
|
+
patchFsOps.mkdirSync(dirname(toAbsolutePath), { recursive: true });
|
|
135
|
+
patchFsOps.writeFileSync(toAbsolutePath, content, "utf8");
|
|
113
136
|
changedFiles.add(movePath);
|
|
114
|
-
movedFiles.add(`${path} -> ${movePath}`);
|
|
115
137
|
if (!destinationExisted) {
|
|
116
138
|
createdFiles.add(movePath);
|
|
117
139
|
}
|
|
140
|
+
|
|
118
141
|
if (fromAbsolutePath !== toAbsolutePath) {
|
|
142
|
+
patchFsOps.unlinkSync(fromAbsolutePath);
|
|
143
|
+
changedFiles.add(path);
|
|
144
|
+
movedFiles.add(`${path} -> ${movePath}`);
|
|
119
145
|
deletedFiles.add(path);
|
|
146
|
+
return;
|
|
120
147
|
}
|
|
148
|
+
|
|
149
|
+
changedFiles.add(path);
|
|
150
|
+
movedFiles.add(`${path} -> ${movePath}`);
|
|
121
151
|
}
|
|
122
152
|
|
|
123
153
|
function applyAction({
|
|
@@ -200,21 +230,36 @@ export function executePatch({ cwd, patchText }: { cwd: string; patchText: strin
|
|
|
200
230
|
let fuzz = 0;
|
|
201
231
|
|
|
202
232
|
for (const action of actions) {
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
233
|
+
try {
|
|
234
|
+
fuzz += applyAction({
|
|
235
|
+
cwd,
|
|
236
|
+
action,
|
|
237
|
+
changedFiles,
|
|
238
|
+
createdFiles,
|
|
239
|
+
deletedFiles,
|
|
240
|
+
movedFiles,
|
|
241
|
+
});
|
|
242
|
+
} catch (error) {
|
|
243
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
244
|
+
throw new ExecutePatchError(
|
|
245
|
+
message,
|
|
246
|
+
buildExecutePatchResult({
|
|
247
|
+
changedFiles,
|
|
248
|
+
createdFiles,
|
|
249
|
+
deletedFiles,
|
|
250
|
+
movedFiles,
|
|
251
|
+
fuzz,
|
|
252
|
+
}),
|
|
253
|
+
action,
|
|
254
|
+
);
|
|
255
|
+
}
|
|
211
256
|
}
|
|
212
257
|
|
|
213
|
-
return {
|
|
214
|
-
changedFiles
|
|
215
|
-
createdFiles
|
|
216
|
-
deletedFiles
|
|
217
|
-
movedFiles
|
|
258
|
+
return buildExecutePatchResult({
|
|
259
|
+
changedFiles,
|
|
260
|
+
createdFiles,
|
|
261
|
+
deletedFiles,
|
|
262
|
+
movedFiles,
|
|
218
263
|
fuzz,
|
|
219
|
-
};
|
|
264
|
+
});
|
|
220
265
|
}
|
package/src/patch/types.ts
CHANGED
|
@@ -42,3 +42,25 @@ export class DiffError extends Error {
|
|
|
42
42
|
this.name = "DiffError";
|
|
43
43
|
}
|
|
44
44
|
}
|
|
45
|
+
|
|
46
|
+
export class ExecutePatchError extends DiffError {
|
|
47
|
+
result: ExecutePatchResult;
|
|
48
|
+
failedAction?: ParsedPatchAction;
|
|
49
|
+
|
|
50
|
+
constructor(message: string, result: ExecutePatchResult, failedAction?: ParsedPatchAction) {
|
|
51
|
+
super(message);
|
|
52
|
+
this.name = "ExecutePatchError";
|
|
53
|
+
this.result = result;
|
|
54
|
+
this.failedAction = failedAction;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
hasPartialSuccess(): boolean {
|
|
58
|
+
return (
|
|
59
|
+
this.result.changedFiles.length > 0 ||
|
|
60
|
+
this.result.createdFiles.length > 0 ||
|
|
61
|
+
this.result.deletedFiles.length > 0 ||
|
|
62
|
+
this.result.movedFiles.length > 0 ||
|
|
63
|
+
this.result.fuzz > 0
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
@@ -6,9 +6,12 @@ export interface PromptSkill {
|
|
|
6
6
|
|
|
7
7
|
const CODEX_GUIDELINES = [
|
|
8
8
|
"Prefer a single `apply_patch` call that updates all related files together when one coherent patch will do.",
|
|
9
|
+
"When making coordinated edits across multiple files, include them in one `apply_patch` call instead of splitting them into separate patches.",
|
|
9
10
|
"When multiple tool calls are independent, emit them together so they can execute in parallel instead of serializing them.",
|
|
10
11
|
"Use `parallel` only when tool calls are independent and can safely run at the same time.",
|
|
11
12
|
"Use `write_stdin` when an exec session returns `session_id`, and continue until `exit_code` is present.",
|
|
13
|
+
"For short or non-interactive commands, prefer the default `exec_command` wait instead of a tiny `yield_time_ms` that forces an extra follow-up call.",
|
|
14
|
+
"When polling a running exec session with empty `chars`, wait meaningfully between polls and do not repeatedly poll by reflex.",
|
|
12
15
|
"Do not request `tty` unless interactive terminal behavior is required.",
|
|
13
16
|
];
|
|
14
17
|
|
|
@@ -24,12 +24,12 @@ export function formatApplyPatchSummary(patchText: string, cwd = process.cwd()):
|
|
|
24
24
|
try {
|
|
25
25
|
actions = parsePatchActions({ text: patchText });
|
|
26
26
|
} catch {
|
|
27
|
-
return "
|
|
27
|
+
return "";
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
const files = actions.map((action) => buildFilePreview(action, cwd));
|
|
31
31
|
if (files.length === 0) {
|
|
32
|
-
return "
|
|
32
|
+
return "";
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
const totalAdded = files.reduce((sum, file) => sum + file.added, 0);
|
|
@@ -38,14 +38,14 @@ export function formatApplyPatchSummary(patchText: string, cwd = process.cwd()):
|
|
|
38
38
|
|
|
39
39
|
if (files.length === 1) {
|
|
40
40
|
const [file] = files;
|
|
41
|
-
lines.push(`${bulletHeader(file.verb,
|
|
41
|
+
lines.push(`${bulletHeader(file.verb, formatPatchTarget(file.path, file.movePath, cwd))} ${renderCounts(file.added, file.removed)}`);
|
|
42
42
|
return lines.join("\n");
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
lines.push(`${bulletHeader("Edited", `${files.length} files`)} ${renderCounts(totalAdded, totalRemoved)}`);
|
|
46
46
|
for (const [index, file] of files.entries()) {
|
|
47
47
|
const prefix = index === 0 ? " └ " : " ";
|
|
48
|
-
lines.push(`${prefix}${
|
|
48
|
+
lines.push(`${prefix}${formatPatchTarget(file.path, file.movePath, cwd)} ${renderCounts(file.added, file.removed)}`);
|
|
49
49
|
}
|
|
50
50
|
|
|
51
51
|
return lines.join("\n");
|
|
@@ -56,12 +56,12 @@ export function formatApplyPatchCall(patchText: string, cwd = process.cwd()): st
|
|
|
56
56
|
try {
|
|
57
57
|
actions = parsePatchActions({ text: patchText });
|
|
58
58
|
} catch {
|
|
59
|
-
return "
|
|
59
|
+
return "";
|
|
60
60
|
}
|
|
61
61
|
|
|
62
62
|
const files = actions.map((action) => buildFilePreview(action, cwd));
|
|
63
63
|
if (files.length === 0) {
|
|
64
|
-
return "
|
|
64
|
+
return "";
|
|
65
65
|
}
|
|
66
66
|
|
|
67
67
|
const totalAdded = files.reduce((sum, file) => sum + file.added, 0);
|
|
@@ -70,7 +70,7 @@ export function formatApplyPatchCall(patchText: string, cwd = process.cwd()): st
|
|
|
70
70
|
|
|
71
71
|
if (files.length === 1) {
|
|
72
72
|
const [file] = files;
|
|
73
|
-
lines.push(`${bulletHeader(file.verb,
|
|
73
|
+
lines.push(`${bulletHeader(file.verb, formatPatchTarget(file.path, file.movePath, cwd))} ${renderCounts(file.added, file.removed)}`);
|
|
74
74
|
lines.push(...file.lines.map((line) => formatPreviewLine(line, file.lines)));
|
|
75
75
|
return lines.join("\n");
|
|
76
76
|
}
|
|
@@ -80,7 +80,7 @@ export function formatApplyPatchCall(patchText: string, cwd = process.cwd()): st
|
|
|
80
80
|
if (index > 0) {
|
|
81
81
|
lines.push("");
|
|
82
82
|
}
|
|
83
|
-
lines.push(` └ ${
|
|
83
|
+
lines.push(` └ ${formatPatchTarget(file.path, file.movePath, cwd)} ${renderCounts(file.added, file.removed)}`);
|
|
84
84
|
lines.push(...file.lines.map((line) => formatPreviewLine(line, file.lines)));
|
|
85
85
|
}
|
|
86
86
|
|
|
@@ -92,12 +92,12 @@ export function renderApplyPatchCall(patchText: string, cwd = process.cwd()): st
|
|
|
92
92
|
try {
|
|
93
93
|
actions = parsePatchActions({ text: patchText });
|
|
94
94
|
} catch {
|
|
95
|
-
return "
|
|
95
|
+
return "";
|
|
96
96
|
}
|
|
97
97
|
|
|
98
98
|
const files = actions.map((action) => buildFilePreview(action, cwd));
|
|
99
99
|
if (files.length === 0) {
|
|
100
|
-
return "
|
|
100
|
+
return "";
|
|
101
101
|
}
|
|
102
102
|
|
|
103
103
|
const totalAdded = files.reduce((sum, file) => sum + file.added, 0);
|
|
@@ -106,7 +106,7 @@ export function renderApplyPatchCall(patchText: string, cwd = process.cwd()): st
|
|
|
106
106
|
|
|
107
107
|
if (files.length === 1) {
|
|
108
108
|
const [file] = files;
|
|
109
|
-
lines.push(`${bulletHeader(file.verb,
|
|
109
|
+
lines.push(`${bulletHeader(file.verb, formatPatchTarget(file.path, file.movePath, cwd))} ${renderCounts(file.added, file.removed)}`);
|
|
110
110
|
lines.push(...renderPreviewLines(file.lines));
|
|
111
111
|
return lines.join("\n");
|
|
112
112
|
}
|
|
@@ -116,7 +116,7 @@ export function renderApplyPatchCall(patchText: string, cwd = process.cwd()): st
|
|
|
116
116
|
if (index > 0) {
|
|
117
117
|
lines.push("");
|
|
118
118
|
}
|
|
119
|
-
lines.push(` └ ${
|
|
119
|
+
lines.push(` └ ${formatPatchTarget(file.path, file.movePath, cwd)} ${renderCounts(file.added, file.removed)}`);
|
|
120
120
|
lines.push(...renderPreviewLines(file.lines));
|
|
121
121
|
}
|
|
122
122
|
|
|
@@ -303,7 +303,7 @@ function findSequence(lines: string[], context: string[], start: number, normali
|
|
|
303
303
|
return -1;
|
|
304
304
|
}
|
|
305
305
|
|
|
306
|
-
function
|
|
306
|
+
export function formatPatchTarget(path: string, movePath: string | undefined, cwd: string): string {
|
|
307
307
|
const from = displayPath(path, cwd);
|
|
308
308
|
if (!movePath) {
|
|
309
309
|
return from;
|
|
@@ -2,8 +2,8 @@ import { Type } from "@sinclair/typebox";
|
|
|
2
2
|
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
3
3
|
import { Container, Text } from "@mariozechner/pi-tui";
|
|
4
4
|
import { executePatch } from "../patch/core.ts";
|
|
5
|
-
import type
|
|
6
|
-
import { formatApplyPatchSummary, renderApplyPatchCall } from "./apply-patch-rendering.ts";
|
|
5
|
+
import { ExecutePatchError, type ExecutePatchResult } from "../patch/types.ts";
|
|
6
|
+
import { formatApplyPatchSummary, formatPatchTarget, renderApplyPatchCall } from "./apply-patch-rendering.ts";
|
|
7
7
|
|
|
8
8
|
const APPLY_PATCH_PARAMETERS = Type.Object({
|
|
9
9
|
input: Type.String({
|
|
@@ -13,10 +13,27 @@ const APPLY_PATCH_PARAMETERS = Type.Object({
|
|
|
13
13
|
|
|
14
14
|
interface ApplyPatchRenderState {
|
|
15
15
|
cwd: string;
|
|
16
|
+
patchText: string;
|
|
16
17
|
collapsed: string;
|
|
17
18
|
expanded: string;
|
|
19
|
+
status: "pending" | "partial_failure" | "failed";
|
|
20
|
+
failedTarget?: string;
|
|
18
21
|
}
|
|
19
22
|
|
|
23
|
+
interface ApplyPatchSuccessDetails {
|
|
24
|
+
status: "success";
|
|
25
|
+
result: ExecutePatchResult;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
interface ApplyPatchPartialFailureDetails {
|
|
29
|
+
status: "partial_failure";
|
|
30
|
+
result: ExecutePatchResult;
|
|
31
|
+
error: string;
|
|
32
|
+
failedTarget?: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
type ApplyPatchToolDetails = ApplyPatchSuccessDetails | ApplyPatchPartialFailureDetails;
|
|
36
|
+
|
|
20
37
|
const applyPatchRenderStates = new Map<string, ApplyPatchRenderState>();
|
|
21
38
|
|
|
22
39
|
interface ApplyPatchRenderContextLike {
|
|
@@ -33,8 +50,138 @@ function parseApplyPatchParams(params: unknown): { patchText: string } {
|
|
|
33
50
|
return { patchText: params.input };
|
|
34
51
|
}
|
|
35
52
|
|
|
36
|
-
function
|
|
37
|
-
return typeof details === "object" && details !== null;
|
|
53
|
+
function isApplyPatchToolDetails(details: unknown): details is ApplyPatchToolDetails {
|
|
54
|
+
return typeof details === "object" && details !== null && "status" in details && "result" in details;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function setApplyPatchRenderState(
|
|
58
|
+
toolCallId: string,
|
|
59
|
+
patchText: string,
|
|
60
|
+
cwd: string,
|
|
61
|
+
status: "pending" | "partial_failure" | "failed" = "pending",
|
|
62
|
+
failedTarget?: string,
|
|
63
|
+
): void {
|
|
64
|
+
const collapsed = formatApplyPatchSummary(patchText, cwd);
|
|
65
|
+
const expanded = renderApplyPatchCall(patchText, cwd);
|
|
66
|
+
applyPatchRenderStates.set(toolCallId, {
|
|
67
|
+
cwd,
|
|
68
|
+
patchText,
|
|
69
|
+
collapsed,
|
|
70
|
+
expanded,
|
|
71
|
+
status,
|
|
72
|
+
failedTarget,
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function markApplyPatchPartialFailure(toolCallId: string, failedTarget?: string): void {
|
|
77
|
+
markApplyPatchFailure(toolCallId, "partial_failure", failedTarget);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function markApplyPatchFailure(toolCallId: string, status: "partial_failure" | "failed", failedTarget?: string): void {
|
|
81
|
+
const existing = applyPatchRenderStates.get(toolCallId);
|
|
82
|
+
if (!existing) {
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
applyPatchRenderStates.set(toolCallId, {
|
|
86
|
+
...existing,
|
|
87
|
+
status,
|
|
88
|
+
failedTarget,
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function renderPartialFailureCall(
|
|
93
|
+
text: string,
|
|
94
|
+
theme: { fg(role: string, text: string): string },
|
|
95
|
+
failedTarget?: string,
|
|
96
|
+
): string {
|
|
97
|
+
const lines = text.split("\n");
|
|
98
|
+
if (lines.length === 0) {
|
|
99
|
+
return theme.fg("warning", "• Edit partially failed");
|
|
100
|
+
}
|
|
101
|
+
lines[0] = lines[0].replace(/^• (Added|Edited|Deleted)\b/, "• Edit partially failed");
|
|
102
|
+
const failedLineIndexes = new Set<number>();
|
|
103
|
+
if (failedTarget) {
|
|
104
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
105
|
+
const failedLine = markFailedTargetLine(lines[i], failedTarget);
|
|
106
|
+
if (failedLine) {
|
|
107
|
+
lines[i] = failedLine;
|
|
108
|
+
failedLineIndexes.add(i);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return lines
|
|
113
|
+
.map((line, index) => {
|
|
114
|
+
if (failedLineIndexes.has(index)) {
|
|
115
|
+
return theme.fg("error", line);
|
|
116
|
+
}
|
|
117
|
+
if (index === 0) {
|
|
118
|
+
return theme.fg("warning", line);
|
|
119
|
+
}
|
|
120
|
+
return line;
|
|
121
|
+
})
|
|
122
|
+
.join("\n");
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function renderFailedCall(
|
|
126
|
+
text: string,
|
|
127
|
+
theme: { fg(role: string, text: string): string },
|
|
128
|
+
failedTarget?: string,
|
|
129
|
+
): string {
|
|
130
|
+
const lines = text.split("\n");
|
|
131
|
+
if (lines.length === 0) {
|
|
132
|
+
return theme.fg("error", "• Edit failed");
|
|
133
|
+
}
|
|
134
|
+
lines[0] = lines[0].replace(/^• (Added|Edited|Deleted)\b/, "• Edit failed");
|
|
135
|
+
const failedLineIndexes = new Set<number>();
|
|
136
|
+
if (failedTarget) {
|
|
137
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
138
|
+
const failedLine = markFailedTargetLine(lines[i], failedTarget);
|
|
139
|
+
if (failedLine) {
|
|
140
|
+
lines[i] = failedLine;
|
|
141
|
+
failedLineIndexes.add(i);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return lines
|
|
146
|
+
.map((line, index) => {
|
|
147
|
+
if (failedLineIndexes.has(index) || index === 0) {
|
|
148
|
+
return theme.fg("error", line);
|
|
149
|
+
}
|
|
150
|
+
return line;
|
|
151
|
+
})
|
|
152
|
+
.join("\n");
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function markFailedTargetLine(line: string, failedTarget: string): string | undefined {
|
|
156
|
+
const suffixMatch = line.match(/ \(\+\d+ -\d+\)$/);
|
|
157
|
+
if (!suffixMatch) {
|
|
158
|
+
return undefined;
|
|
159
|
+
}
|
|
160
|
+
const suffix = suffixMatch[0];
|
|
161
|
+
const prefixAndTarget = line.slice(0, -suffix.length);
|
|
162
|
+
const candidatePrefixes = ["• Edit partially failed ", "• Added ", "• Edited ", "• Deleted ", " └ ", " "];
|
|
163
|
+
for (const prefix of candidatePrefixes) {
|
|
164
|
+
if (prefixAndTarget === `${prefix}${failedTarget}`) {
|
|
165
|
+
return `${prefix}${failedTarget} failed${suffix}`;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
return undefined;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function summarizePatchCounts(result: ExecutePatchResult): string {
|
|
172
|
+
return [
|
|
173
|
+
`changed ${result.changedFiles.length} file${result.changedFiles.length === 1 ? "" : "s"}`,
|
|
174
|
+
`created ${result.createdFiles.length}`,
|
|
175
|
+
`deleted ${result.deletedFiles.length}`,
|
|
176
|
+
`moved ${result.movedFiles.length}`,
|
|
177
|
+
].join(", ");
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function describeFailedAction(error: ExecutePatchError, cwd: string): string | undefined {
|
|
181
|
+
if (!error.failedAction) {
|
|
182
|
+
return undefined;
|
|
183
|
+
}
|
|
184
|
+
return formatPatchTarget(error.failedAction.path, error.failedAction.type === "update" ? error.failedAction.movePath : undefined, cwd);
|
|
38
185
|
}
|
|
39
186
|
|
|
40
187
|
export type { ExecutePatchResult } from "../patch/types.ts";
|
|
@@ -57,7 +204,22 @@ const renderApplyPatchCallWithOptionalContext: any = (
|
|
|
57
204
|
}
|
|
58
205
|
const cached = context?.toolCallId ? applyPatchRenderStates.get(context.toolCallId) : undefined;
|
|
59
206
|
const cwd = context?.cwd ?? cached?.cwd;
|
|
60
|
-
const
|
|
207
|
+
const effectivePatchText = cached?.patchText ?? patchText;
|
|
208
|
+
const baseText = context?.expanded
|
|
209
|
+
? cached?.expanded ?? renderApplyPatchCall(effectivePatchText, cwd)
|
|
210
|
+
: cached?.collapsed ?? formatApplyPatchSummary(effectivePatchText, cwd);
|
|
211
|
+
if (baseText.trim().length === 0) {
|
|
212
|
+
if (cached?.status === "failed") {
|
|
213
|
+
return new Text(theme.fg("error", "• Edit failed"), 0, 0);
|
|
214
|
+
}
|
|
215
|
+
return new Text(`${theme.fg("dim", "•")} ${theme.bold("Patching")}`, 0, 0);
|
|
216
|
+
}
|
|
217
|
+
const text =
|
|
218
|
+
cached?.status === "partial_failure"
|
|
219
|
+
? renderPartialFailureCall(baseText, theme, cached.failedTarget)
|
|
220
|
+
: cached?.status === "failed"
|
|
221
|
+
? renderFailedCall(baseText, theme, cached.failedTarget)
|
|
222
|
+
: baseText;
|
|
61
223
|
return new Text(text, 0, 0);
|
|
62
224
|
};
|
|
63
225
|
|
|
@@ -67,7 +229,10 @@ export function registerApplyPatchTool(pi: ExtensionAPI): void {
|
|
|
67
229
|
label: "apply_patch",
|
|
68
230
|
description: "Use `apply_patch` to edit files. Send the full patch in `input`.",
|
|
69
231
|
promptSnippet: "Edit files with a patch.",
|
|
70
|
-
promptGuidelines: [
|
|
232
|
+
promptGuidelines: [
|
|
233
|
+
"Prefer apply_patch for focused textual edits instead of rewriting whole files.",
|
|
234
|
+
"When one task needs coordinated edits across multiple files, send them in a single apply_patch call when one coherent patch will do.",
|
|
235
|
+
],
|
|
71
236
|
parameters: APPLY_PATCH_PARAMETERS,
|
|
72
237
|
async execute(toolCallId, params, signal, _onUpdate, ctx) {
|
|
73
238
|
if (signal?.aborted) {
|
|
@@ -75,12 +240,36 @@ export function registerApplyPatchTool(pi: ExtensionAPI): void {
|
|
|
75
240
|
}
|
|
76
241
|
|
|
77
242
|
const typedParams = parseApplyPatchParams(params);
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
})
|
|
83
|
-
|
|
243
|
+
setApplyPatchRenderState(toolCallId, typedParams.patchText, ctx.cwd);
|
|
244
|
+
let result: ExecutePatchResult;
|
|
245
|
+
try {
|
|
246
|
+
result = executePatch({ cwd: ctx.cwd, patchText: typedParams.patchText });
|
|
247
|
+
} catch (error) {
|
|
248
|
+
if (error instanceof ExecutePatchError) {
|
|
249
|
+
const partial = error.hasPartialSuccess();
|
|
250
|
+
const failedTarget = describeFailedAction(error, ctx.cwd);
|
|
251
|
+
const prefix = partial
|
|
252
|
+
? `apply_patch partially failed after ${summarizePatchCounts(error.result)}`
|
|
253
|
+
: "apply_patch failed";
|
|
254
|
+
const message = failedTarget ? `${prefix} while patching ${failedTarget}: ${error.message}` : `${prefix}: ${error.message}`;
|
|
255
|
+
if (partial) {
|
|
256
|
+
markApplyPatchPartialFailure(toolCallId, failedTarget);
|
|
257
|
+
return {
|
|
258
|
+
content: [{ type: "text", text: message }],
|
|
259
|
+
details: {
|
|
260
|
+
status: "partial_failure",
|
|
261
|
+
result: error.result,
|
|
262
|
+
error: message,
|
|
263
|
+
failedTarget,
|
|
264
|
+
} satisfies ApplyPatchPartialFailureDetails,
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
markApplyPatchFailure(toolCallId, "failed", failedTarget);
|
|
268
|
+
throw new Error(message);
|
|
269
|
+
}
|
|
270
|
+
markApplyPatchFailure(toolCallId, "failed");
|
|
271
|
+
throw error;
|
|
272
|
+
}
|
|
84
273
|
const summary = [
|
|
85
274
|
"Applied patch successfully.",
|
|
86
275
|
`Changed files: ${result.changedFiles.length}`,
|
|
@@ -92,16 +281,23 @@ export function registerApplyPatchTool(pi: ExtensionAPI): void {
|
|
|
92
281
|
|
|
93
282
|
return {
|
|
94
283
|
content: [{ type: "text", text: summary }],
|
|
95
|
-
details:
|
|
284
|
+
details: {
|
|
285
|
+
status: "success",
|
|
286
|
+
result,
|
|
287
|
+
} satisfies ApplyPatchSuccessDetails,
|
|
96
288
|
};
|
|
97
289
|
},
|
|
98
290
|
renderCall: renderApplyPatchCallWithOptionalContext,
|
|
99
|
-
renderResult(result, { isPartial }, theme) {
|
|
291
|
+
renderResult(result, { isPartial, expanded }, theme) {
|
|
100
292
|
if (isPartial) {
|
|
101
293
|
return new Text(`${theme.fg("dim", "•")} ${theme.bold("Patching")}`, 0, 0);
|
|
102
294
|
}
|
|
103
295
|
|
|
104
|
-
if (!
|
|
296
|
+
if (!isApplyPatchToolDetails(result.details)) {
|
|
297
|
+
return new Container();
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
if (result.details.status === "partial_failure") {
|
|
105
301
|
return new Container();
|
|
106
302
|
}
|
|
107
303
|
|
|
@@ -121,6 +121,7 @@ export function registerExecCommandTool(pi: ExtensionAPI, tracker: ExecCommandTr
|
|
|
121
121
|
promptGuidelines: [
|
|
122
122
|
"Use exec_command for search, listing files, and local text-file reads.",
|
|
123
123
|
"Prefer rg or rg --files when possible.",
|
|
124
|
+
"For short or non-interactive commands, omit `yield_time_ms` so the default wait can avoid unnecessary follow-up calls.",
|
|
124
125
|
"Keep tty disabled unless the command truly needs interactive terminal behavior.",
|
|
125
126
|
],
|
|
126
127
|
parameters: EXEC_COMMAND_PARAMETERS,
|
|
@@ -65,10 +65,19 @@ export interface ExecSessionManager {
|
|
|
65
65
|
shutdown(): void;
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
+
export interface ExecSessionManagerOptions {
|
|
69
|
+
defaultExecYieldTimeMs?: number;
|
|
70
|
+
defaultWriteYieldTimeMs?: number;
|
|
71
|
+
minNonInteractiveExecYieldTimeMs?: number;
|
|
72
|
+
minEmptyWriteYieldTimeMs?: number;
|
|
73
|
+
}
|
|
74
|
+
|
|
68
75
|
const DEFAULT_EXEC_YIELD_TIME_MS = 10_000;
|
|
69
|
-
const DEFAULT_WRITE_YIELD_TIME_MS =
|
|
76
|
+
const DEFAULT_WRITE_YIELD_TIME_MS = 250;
|
|
70
77
|
const DEFAULT_MAX_OUTPUT_TOKENS = 10_000;
|
|
71
78
|
const MIN_YIELD_TIME_MS = 250;
|
|
79
|
+
const MIN_NON_INTERACTIVE_EXEC_YIELD_TIME_MS = 5_000;
|
|
80
|
+
const MIN_EMPTY_WRITE_YIELD_TIME_MS = 5_000;
|
|
72
81
|
const MAX_YIELD_TIME_MS = 30_000;
|
|
73
82
|
const MAX_COMMAND_HISTORY = 256;
|
|
74
83
|
|
|
@@ -139,6 +148,32 @@ function clampYieldTime(yieldTimeMs: number | undefined, fallback: number): numb
|
|
|
139
148
|
return Math.min(MAX_YIELD_TIME_MS, Math.max(MIN_YIELD_TIME_MS, value));
|
|
140
149
|
}
|
|
141
150
|
|
|
151
|
+
function clampExecYieldTime(
|
|
152
|
+
yieldTimeMs: number | undefined,
|
|
153
|
+
fallback: number,
|
|
154
|
+
isInteractive: boolean,
|
|
155
|
+
minNonInteractiveExecYieldTimeMs: number,
|
|
156
|
+
): number {
|
|
157
|
+
const value = clampYieldTime(yieldTimeMs, fallback);
|
|
158
|
+
if (isInteractive) {
|
|
159
|
+
return value;
|
|
160
|
+
}
|
|
161
|
+
return Math.min(MAX_YIELD_TIME_MS, Math.max(minNonInteractiveExecYieldTimeMs, value));
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function clampWriteYieldTime(
|
|
165
|
+
yieldTimeMs: number | undefined,
|
|
166
|
+
fallback: number,
|
|
167
|
+
isEmptyPoll: boolean,
|
|
168
|
+
minEmptyWriteYieldTimeMs: number,
|
|
169
|
+
): number {
|
|
170
|
+
const value = clampYieldTime(yieldTimeMs, fallback);
|
|
171
|
+
if (!isEmptyPoll) {
|
|
172
|
+
return value;
|
|
173
|
+
}
|
|
174
|
+
return Math.min(MAX_YIELD_TIME_MS, Math.max(minEmptyWriteYieldTimeMs, value));
|
|
175
|
+
}
|
|
176
|
+
|
|
142
177
|
function maxCharsForTokens(maxOutputTokens = DEFAULT_MAX_OUTPUT_TOKENS): number {
|
|
143
178
|
return Math.max(256, maxOutputTokens * 4);
|
|
144
179
|
}
|
|
@@ -309,11 +344,21 @@ function registerAbortHandler(signal: AbortSignal | undefined, onAbort: () => vo
|
|
|
309
344
|
return () => signal.removeEventListener("abort", abortListener);
|
|
310
345
|
}
|
|
311
346
|
|
|
312
|
-
export function createExecSessionManager(): ExecSessionManager {
|
|
347
|
+
export function createExecSessionManager(options: ExecSessionManagerOptions = {}): ExecSessionManager {
|
|
313
348
|
let nextSessionId = 1;
|
|
314
349
|
const sessions = new Map<number, ExecSession>();
|
|
315
350
|
const commandHistory = new Map<number, string>();
|
|
316
351
|
const exitListeners = new Set<(sessionId: number, command: string) => void>();
|
|
352
|
+
const defaultExecYieldTimeMs = options.defaultExecYieldTimeMs ?? DEFAULT_EXEC_YIELD_TIME_MS;
|
|
353
|
+
const defaultWriteYieldTimeMs = options.defaultWriteYieldTimeMs ?? DEFAULT_WRITE_YIELD_TIME_MS;
|
|
354
|
+
const minNonInteractiveExecYieldTimeMs = Math.min(
|
|
355
|
+
MAX_YIELD_TIME_MS,
|
|
356
|
+
Math.max(MIN_YIELD_TIME_MS, options.minNonInteractiveExecYieldTimeMs ?? MIN_NON_INTERACTIVE_EXEC_YIELD_TIME_MS),
|
|
357
|
+
);
|
|
358
|
+
const minEmptyWriteYieldTimeMs = Math.min(
|
|
359
|
+
MAX_YIELD_TIME_MS,
|
|
360
|
+
Math.max(MIN_YIELD_TIME_MS, options.minEmptyWriteYieldTimeMs ?? MIN_EMPTY_WRITE_YIELD_TIME_MS),
|
|
361
|
+
);
|
|
317
362
|
|
|
318
363
|
function rememberCommand(sessionId: number, command: string): void {
|
|
319
364
|
commandHistory.set(sessionId, command);
|
|
@@ -494,7 +539,10 @@ export function createExecSessionManager(): ExecSessionManager {
|
|
|
494
539
|
sessions.set(session.id, session);
|
|
495
540
|
rememberCommand(session.id, session.command);
|
|
496
541
|
|
|
497
|
-
const waitedMs = await waitForExitOrTimeout(
|
|
542
|
+
const waitedMs = await waitForExitOrTimeout(
|
|
543
|
+
session,
|
|
544
|
+
clampExecYieldTime(input.yield_time_ms, defaultExecYieldTimeMs, session.interactive, minNonInteractiveExecYieldTimeMs),
|
|
545
|
+
);
|
|
498
546
|
return makeResult(session, waitedMs, input.max_output_tokens);
|
|
499
547
|
},
|
|
500
548
|
write: async (input) => {
|
|
@@ -512,7 +560,15 @@ export function createExecSessionManager(): ExecSessionManager {
|
|
|
512
560
|
}
|
|
513
561
|
const waitedMs =
|
|
514
562
|
session.exitCode === undefined
|
|
515
|
-
? await waitForExitOrTimeout(
|
|
563
|
+
? await waitForExitOrTimeout(
|
|
564
|
+
session,
|
|
565
|
+
clampWriteYieldTime(
|
|
566
|
+
input.yield_time_ms,
|
|
567
|
+
defaultWriteYieldTimeMs,
|
|
568
|
+
!input.chars || input.chars.length === 0,
|
|
569
|
+
minEmptyWriteYieldTimeMs,
|
|
570
|
+
),
|
|
571
|
+
)
|
|
516
572
|
: 0;
|
|
517
573
|
return makeResult(session, waitedMs, input.max_output_tokens);
|
|
518
574
|
},
|
|
@@ -110,6 +110,10 @@ export function registerWriteStdinTool(pi: ExtensionAPI, sessions: ExecSessionMa
|
|
|
110
110
|
label: "write_stdin",
|
|
111
111
|
description: "Writes characters to an existing unified exec session and returns recent output.",
|
|
112
112
|
promptSnippet: "Write to an exec session.",
|
|
113
|
+
promptGuidelines: [
|
|
114
|
+
"Use empty `chars` only to poll a running exec session.",
|
|
115
|
+
"When polling with empty `chars`, wait meaningfully between polls and do not repeatedly poll by reflex.",
|
|
116
|
+
],
|
|
113
117
|
parameters: WRITE_STDIN_PARAMETERS,
|
|
114
118
|
async execute(_toolCallId, params) {
|
|
115
119
|
const typed = parseWriteStdinParams(params);
|