@howaboua/pi-codex-conversion 1.0.9 → 1.0.10
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 -0
- package/package.json +3 -3
- package/src/index.ts +26 -4
- package/src/shell/parse-command.ts +0 -2
- package/src/shell/parse.ts +0 -6
- package/src/shell/tokenize.ts +12 -1
- package/src/tools/apply-patch-rendering.ts +352 -0
- package/src/tools/apply-patch-tool.ts +51 -24
- package/src/tools/codex-rendering.ts +57 -10
- package/src/tools/exec-command-state.ts +188 -16
- package/src/tools/exec-command-tool.ts +68 -25
- package/src/tools/web-search-tool.ts +6 -2
- package/src/tools/write-stdin-tool.ts +6 -2
package/README.md
CHANGED
|
@@ -7,6 +7,7 @@ This package replaces Pi's default Codex/GPT experience with a narrower Codex-li
|
|
|
7
7
|
- swaps active tools to `exec_command`, `write_stdin`, `apply_patch`, `view_image`, and native OpenAI Codex Responses `web_search` on `openai-codex`
|
|
8
8
|
- preserves Pi's composed system prompt and applies a narrow Codex-oriented delta on top
|
|
9
9
|
- renders exec activity with Codex-style command and background-terminal labels
|
|
10
|
+
- renders `apply_patch` calls with Codex-style `Added` / `Edited` / `Deleted` diff blocks and Pi-style colored diff lines
|
|
10
11
|
|
|
11
12
|

|
|
12
13
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@howaboua/pi-codex-conversion",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.10",
|
|
4
4
|
"description": "Codex-oriented tool and prompt adapter for pi coding agent",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"repository": {
|
|
@@ -51,8 +51,8 @@
|
|
|
51
51
|
"access": "public"
|
|
52
52
|
},
|
|
53
53
|
"peerDependencies": {
|
|
54
|
-
"@mariozechner/pi-coding-agent": "
|
|
55
|
-
"@mariozechner/pi-tui": "
|
|
54
|
+
"@mariozechner/pi-coding-agent": "^0.62.0",
|
|
55
|
+
"@mariozechner/pi-tui": "^0.62.0",
|
|
56
56
|
"@sinclair/typebox": "*"
|
|
57
57
|
},
|
|
58
58
|
"devDependencies": {
|
package/src/index.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent";
|
|
2
2
|
import { getCodexRuntimeShell } from "./adapter/runtime-shell.ts";
|
|
3
3
|
import { CORE_ADAPTER_TOOL_NAMES, DEFAULT_TOOL_NAMES, STATUS_KEY, STATUS_TEXT, VIEW_IMAGE_TOOL_NAME, WEB_SEARCH_TOOL_NAME } from "./adapter/tool-set.ts";
|
|
4
|
-
import { registerApplyPatchTool } from "./tools/apply-patch-tool.ts";
|
|
4
|
+
import { clearApplyPatchRenderState, registerApplyPatchTool } from "./tools/apply-patch-tool.ts";
|
|
5
5
|
import { isCodexLikeContext, isOpenAICodexContext } from "./adapter/codex-model.ts";
|
|
6
6
|
import { createExecCommandTracker } from "./tools/exec-command-state.ts";
|
|
7
7
|
import { registerExecCommandTool } from "./tools/exec-command-tool.ts";
|
|
@@ -35,6 +35,16 @@ function getCommandArg(args: unknown): string | undefined {
|
|
|
35
35
|
return args.cmd;
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
+
function isToolCallOnlyAssistantMessage(message: unknown): boolean {
|
|
39
|
+
if (!message || typeof message !== "object" || !("role" in message) || message.role !== "assistant") {
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
if (!("content" in message) || !Array.isArray(message.content) || message.content.length === 0) {
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
return message.content.every((item) => typeof item === "object" && item !== null && "type" in item && item.type === "toolCall");
|
|
46
|
+
}
|
|
47
|
+
|
|
38
48
|
export default function codexConversion(pi: ExtensionAPI) {
|
|
39
49
|
const tracker = createExecCommandTracker();
|
|
40
50
|
const state: AdapterState = { enabled: false, promptSkills: [], webSearchNoticeShown: false };
|
|
@@ -46,12 +56,14 @@ export default function codexConversion(pi: ExtensionAPI) {
|
|
|
46
56
|
registerWebSearchTool(pi);
|
|
47
57
|
registerWebSearchSessionNoteRenderer(pi);
|
|
48
58
|
|
|
49
|
-
sessions.onSessionExit((
|
|
50
|
-
tracker.
|
|
59
|
+
sessions.onSessionExit((sessionId) => {
|
|
60
|
+
tracker.recordSessionFinished(sessionId);
|
|
51
61
|
});
|
|
52
62
|
|
|
53
63
|
pi.on("session_start", async (_event, ctx) => {
|
|
54
64
|
state.webSearchNoticeShown = false;
|
|
65
|
+
clearApplyPatchRenderState();
|
|
66
|
+
tracker.clear();
|
|
55
67
|
syncAdapter(pi, ctx, state);
|
|
56
68
|
});
|
|
57
69
|
|
|
@@ -59,8 +71,17 @@ export default function codexConversion(pi: ExtensionAPI) {
|
|
|
59
71
|
syncAdapter(pi, ctx, state);
|
|
60
72
|
});
|
|
61
73
|
|
|
74
|
+
pi.on("message_start", async (event) => {
|
|
75
|
+
if (event.message.role === "toolResult") return;
|
|
76
|
+
if (isToolCallOnlyAssistantMessage(event.message)) return;
|
|
77
|
+
tracker.resetExplorationGroup();
|
|
78
|
+
});
|
|
79
|
+
|
|
62
80
|
pi.on("tool_execution_start", async (event) => {
|
|
63
|
-
if (event.toolName !== "exec_command")
|
|
81
|
+
if (event.toolName !== "exec_command") {
|
|
82
|
+
tracker.resetExplorationGroup();
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
64
85
|
const command = getCommandArg(event.args);
|
|
65
86
|
if (!command) return;
|
|
66
87
|
tracker.recordStart(event.toolCallId, command);
|
|
@@ -72,6 +93,7 @@ export default function codexConversion(pi: ExtensionAPI) {
|
|
|
72
93
|
});
|
|
73
94
|
|
|
74
95
|
pi.on("session_shutdown", async () => {
|
|
96
|
+
clearApplyPatchRenderState();
|
|
75
97
|
sessions.shutdown();
|
|
76
98
|
});
|
|
77
99
|
|
|
@@ -61,8 +61,6 @@ function parseCommandImpl(command: string[]): ParsedShellCommand[] {
|
|
|
61
61
|
const parsed = summarizeMainTokens(tokens);
|
|
62
62
|
if (parsed.kind === "read" && cwd) {
|
|
63
63
|
commands.push({ ...parsed, path: joinPaths(cwd, parsed.path) });
|
|
64
|
-
} else if (parsed.kind === "list" && cwd && !parsed.path) {
|
|
65
|
-
commands.push({ ...parsed, path: shortDisplayPath(cwd) });
|
|
66
64
|
} else {
|
|
67
65
|
commands.push(parsed);
|
|
68
66
|
}
|
package/src/shell/parse.ts
CHANGED
|
@@ -18,12 +18,6 @@ export function parseShellPart(tokens: string[], cwd?: string): ShellAction | nu
|
|
|
18
18
|
path: joinPaths(cwd, parsed.path),
|
|
19
19
|
};
|
|
20
20
|
}
|
|
21
|
-
if (parsed.kind === "list" && cwd && !parsed.path) {
|
|
22
|
-
return {
|
|
23
|
-
...parsed,
|
|
24
|
-
path: shortDisplayPath(cwd),
|
|
25
|
-
};
|
|
26
|
-
}
|
|
27
21
|
|
|
28
22
|
return parsed;
|
|
29
23
|
}
|
package/src/shell/tokenize.ts
CHANGED
|
@@ -84,10 +84,21 @@ export function shellSplit(input: string): string[] {
|
|
|
84
84
|
|
|
85
85
|
export function shellQuote(token: string): string {
|
|
86
86
|
if (token.length === 0) return "''";
|
|
87
|
-
if (
|
|
87
|
+
if (canEmitUnquoted(token)) return token;
|
|
88
88
|
return `'${token.replace(/'/g, `'"'"'`)}'`;
|
|
89
89
|
}
|
|
90
90
|
|
|
91
|
+
function canEmitUnquoted(token: string): boolean {
|
|
92
|
+
for (const char of token) {
|
|
93
|
+
if (!isUnquotedOk(char)) return false;
|
|
94
|
+
}
|
|
95
|
+
return true;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function isUnquotedOk(char: string): boolean {
|
|
99
|
+
return /^[+\-./:@\]_0-9A-Za-z]$/.test(char);
|
|
100
|
+
}
|
|
101
|
+
|
|
91
102
|
export function joinCommandTokens(tokens: string[]): string {
|
|
92
103
|
return tokens
|
|
93
104
|
.map((token) => (token === "&&" || token === "||" || token === "|" || token === ";" ? token : shellQuote(token)))
|
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
import { isAbsolute, relative } from "node:path";
|
|
2
|
+
import { renderDiff } from "@mariozechner/pi-coding-agent";
|
|
3
|
+
import { openFileAtPath } from "../patch/paths.ts";
|
|
4
|
+
import { parsePatchActions } from "../patch/parser.ts";
|
|
5
|
+
import type { ParsedPatchAction } from "../patch/types.ts";
|
|
6
|
+
|
|
7
|
+
interface PreviewLine {
|
|
8
|
+
lineNumber: number;
|
|
9
|
+
marker: " " | "+" | "-";
|
|
10
|
+
text: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
interface FilePreview {
|
|
14
|
+
verb: "Added" | "Deleted" | "Edited";
|
|
15
|
+
path: string;
|
|
16
|
+
movePath?: string;
|
|
17
|
+
added: number;
|
|
18
|
+
removed: number;
|
|
19
|
+
lines: PreviewLine[];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function formatApplyPatchSummary(patchText: string, cwd = process.cwd()): string {
|
|
23
|
+
let actions: ParsedPatchAction[];
|
|
24
|
+
try {
|
|
25
|
+
actions = parsePatchActions({ text: patchText });
|
|
26
|
+
} catch {
|
|
27
|
+
return "apply_patch patch";
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const files = actions.map((action) => buildFilePreview(action, cwd));
|
|
31
|
+
if (files.length === 0) {
|
|
32
|
+
return "apply_patch patch";
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const totalAdded = files.reduce((sum, file) => sum + file.added, 0);
|
|
36
|
+
const totalRemoved = files.reduce((sum, file) => sum + file.removed, 0);
|
|
37
|
+
const lines: string[] = [];
|
|
38
|
+
|
|
39
|
+
if (files.length === 1) {
|
|
40
|
+
const [file] = files;
|
|
41
|
+
lines.push(`${bulletHeader(file.verb, renderPath(file.path, file.movePath, cwd))} ${renderCounts(file.added, file.removed)}`);
|
|
42
|
+
return lines.join("\n");
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
lines.push(`${bulletHeader("Edited", `${files.length} files`)} ${renderCounts(totalAdded, totalRemoved)}`);
|
|
46
|
+
for (const [index, file] of files.entries()) {
|
|
47
|
+
const prefix = index === 0 ? " └ " : " ";
|
|
48
|
+
lines.push(`${prefix}${renderPath(file.path, file.movePath, cwd)} ${renderCounts(file.added, file.removed)}`);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return lines.join("\n");
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function formatApplyPatchCall(patchText: string, cwd = process.cwd()): string {
|
|
55
|
+
let actions: ParsedPatchAction[];
|
|
56
|
+
try {
|
|
57
|
+
actions = parsePatchActions({ text: patchText });
|
|
58
|
+
} catch {
|
|
59
|
+
return "apply_patch patch";
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const files = actions.map((action) => buildFilePreview(action, cwd));
|
|
63
|
+
if (files.length === 0) {
|
|
64
|
+
return "apply_patch patch";
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const totalAdded = files.reduce((sum, file) => sum + file.added, 0);
|
|
68
|
+
const totalRemoved = files.reduce((sum, file) => sum + file.removed, 0);
|
|
69
|
+
const lines: string[] = [];
|
|
70
|
+
|
|
71
|
+
if (files.length === 1) {
|
|
72
|
+
const [file] = files;
|
|
73
|
+
lines.push(`${bulletHeader(file.verb, renderPath(file.path, file.movePath, cwd))} ${renderCounts(file.added, file.removed)}`);
|
|
74
|
+
lines.push(...file.lines.map((line) => formatPreviewLine(line, file.lines)));
|
|
75
|
+
return lines.join("\n");
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
lines.push(`${bulletHeader("Edited", `${files.length} files`)} ${renderCounts(totalAdded, totalRemoved)}`);
|
|
79
|
+
for (const [index, file] of files.entries()) {
|
|
80
|
+
if (index > 0) {
|
|
81
|
+
lines.push("");
|
|
82
|
+
}
|
|
83
|
+
lines.push(` └ ${renderPath(file.path, file.movePath, cwd)} ${renderCounts(file.added, file.removed)}`);
|
|
84
|
+
lines.push(...file.lines.map((line) => formatPreviewLine(line, file.lines)));
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return lines.join("\n");
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function renderApplyPatchCall(patchText: string, cwd = process.cwd()): string {
|
|
91
|
+
let actions: ParsedPatchAction[];
|
|
92
|
+
try {
|
|
93
|
+
actions = parsePatchActions({ text: patchText });
|
|
94
|
+
} catch {
|
|
95
|
+
return "apply_patch patch";
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const files = actions.map((action) => buildFilePreview(action, cwd));
|
|
99
|
+
if (files.length === 0) {
|
|
100
|
+
return "apply_patch patch";
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const totalAdded = files.reduce((sum, file) => sum + file.added, 0);
|
|
104
|
+
const totalRemoved = files.reduce((sum, file) => sum + file.removed, 0);
|
|
105
|
+
const lines: string[] = [];
|
|
106
|
+
|
|
107
|
+
if (files.length === 1) {
|
|
108
|
+
const [file] = files;
|
|
109
|
+
lines.push(`${bulletHeader(file.verb, renderPath(file.path, file.movePath, cwd))} ${renderCounts(file.added, file.removed)}`);
|
|
110
|
+
lines.push(...renderPreviewLines(file.lines));
|
|
111
|
+
return lines.join("\n");
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
lines.push(`${bulletHeader("Edited", `${files.length} files`)} ${renderCounts(totalAdded, totalRemoved)}`);
|
|
115
|
+
for (const [index, file] of files.entries()) {
|
|
116
|
+
if (index > 0) {
|
|
117
|
+
lines.push("");
|
|
118
|
+
}
|
|
119
|
+
lines.push(` └ ${renderPath(file.path, file.movePath, cwd)} ${renderCounts(file.added, file.removed)}`);
|
|
120
|
+
lines.push(...renderPreviewLines(file.lines));
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return lines.join("\n");
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function buildFilePreview(action: ParsedPatchAction, cwd: string): FilePreview {
|
|
127
|
+
if (action.type === "add") {
|
|
128
|
+
const lines = splitFileLines(action.newFile ?? "");
|
|
129
|
+
return {
|
|
130
|
+
verb: "Added",
|
|
131
|
+
path: action.path,
|
|
132
|
+
added: lines.length,
|
|
133
|
+
removed: 0,
|
|
134
|
+
lines: lines.map((text, index) => ({ lineNumber: index + 1, marker: "+", text })),
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (action.type === "delete") {
|
|
139
|
+
const deletedLines = readFileLines(action.path, cwd);
|
|
140
|
+
return {
|
|
141
|
+
verb: "Deleted",
|
|
142
|
+
path: action.path,
|
|
143
|
+
added: 0,
|
|
144
|
+
removed: deletedLines.length,
|
|
145
|
+
lines: deletedLines.map((text, index) => ({ lineNumber: index + 1, marker: "-", text })),
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const preview = buildUpdatePreview(action, cwd);
|
|
150
|
+
return {
|
|
151
|
+
verb: "Edited",
|
|
152
|
+
path: action.path,
|
|
153
|
+
movePath: action.movePath,
|
|
154
|
+
added: preview.added,
|
|
155
|
+
removed: preview.removed,
|
|
156
|
+
lines: preview.lines,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function buildUpdatePreview(action: ParsedPatchAction, cwd: string): { added: number; removed: number; lines: PreviewLine[] } {
|
|
161
|
+
if (!action.lines) {
|
|
162
|
+
return { added: 0, removed: 0, lines: [] };
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const originalLines = readFileLines(action.path, cwd);
|
|
166
|
+
const renderedLines: PreviewLine[] = [];
|
|
167
|
+
let added = 0;
|
|
168
|
+
let removed = 0;
|
|
169
|
+
let searchStart = 0;
|
|
170
|
+
let delta = 0;
|
|
171
|
+
let index = 0;
|
|
172
|
+
|
|
173
|
+
while (index < action.lines.length) {
|
|
174
|
+
const line = action.lines[index];
|
|
175
|
+
if (line === "*** End of File") {
|
|
176
|
+
break;
|
|
177
|
+
}
|
|
178
|
+
if (!line.startsWith("@@")) {
|
|
179
|
+
index += 1;
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
index += 1;
|
|
184
|
+
const sectionLines: string[] = [];
|
|
185
|
+
while (index < action.lines.length && !action.lines[index].startsWith("@@") && action.lines[index] !== "*** End of File") {
|
|
186
|
+
sectionLines.push(action.lines[index]);
|
|
187
|
+
index += 1;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (sectionLines.length === 0) {
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const oldSequence = sectionLines
|
|
195
|
+
.map(normalizePatchLine)
|
|
196
|
+
.filter((entry) => entry.marker === " " || entry.marker === "-")
|
|
197
|
+
.map((entry) => entry.text);
|
|
198
|
+
const sectionStart = findMatchingSequence(originalLines, oldSequence, searchStart);
|
|
199
|
+
let oldLineNumber = sectionStart + 1;
|
|
200
|
+
let newLineNumber = sectionStart + 1 + delta;
|
|
201
|
+
|
|
202
|
+
for (const rawLine of sectionLines) {
|
|
203
|
+
const entry = normalizePatchLine(rawLine);
|
|
204
|
+
if (entry.marker === "+") {
|
|
205
|
+
added += 1;
|
|
206
|
+
renderedLines.push({ lineNumber: newLineNumber, marker: "+", text: entry.text });
|
|
207
|
+
newLineNumber += 1;
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
if (entry.marker === "-") {
|
|
212
|
+
removed += 1;
|
|
213
|
+
renderedLines.push({ lineNumber: oldLineNumber, marker: "-", text: entry.text });
|
|
214
|
+
oldLineNumber += 1;
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
renderedLines.push({ lineNumber: newLineNumber, marker: " ", text: entry.text });
|
|
219
|
+
oldLineNumber += 1;
|
|
220
|
+
newLineNumber += 1;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
searchStart = sectionStart + oldSequence.length;
|
|
224
|
+
delta += sectionLines.reduce((sum, rawLine) => {
|
|
225
|
+
const marker = normalizePatchLine(rawLine).marker;
|
|
226
|
+
if (marker === "+") return sum + 1;
|
|
227
|
+
if (marker === "-") return sum - 1;
|
|
228
|
+
return sum;
|
|
229
|
+
}, 0);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
return { added, removed, lines: renderedLines };
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function formatPreviewLine(line: PreviewLine, lines: PreviewLine[]): string {
|
|
236
|
+
const numberWidth = Math.max(1, ...lines.map((entry) => String(entry.lineNumber).length));
|
|
237
|
+
return ` ${String(line.lineNumber).padStart(numberWidth, " ")} ${line.marker}${line.text}`;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function renderPreviewLines(lines: PreviewLine[]): string[] {
|
|
241
|
+
if (lines.length === 0) {
|
|
242
|
+
return [];
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const numberWidth = Math.max(1, ...lines.map((entry) => String(entry.lineNumber).length));
|
|
246
|
+
const diffText = lines
|
|
247
|
+
.map((line) => `${line.marker}${String(line.lineNumber).padStart(numberWidth, " ")} ${line.text}`)
|
|
248
|
+
.join("\n");
|
|
249
|
+
try {
|
|
250
|
+
return renderDiff(diffText)
|
|
251
|
+
.split("\n")
|
|
252
|
+
.map((line) => ` ${line}`);
|
|
253
|
+
} catch {
|
|
254
|
+
return lines.map((line) => formatPreviewLine(line, lines));
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function normalizePatchLine(rawLine: string): PreviewLine {
|
|
259
|
+
const normalized = rawLine === "" ? " " : rawLine;
|
|
260
|
+
const marker = normalized[0];
|
|
261
|
+
if (marker !== " " && marker !== "+" && marker !== "-") {
|
|
262
|
+
return { lineNumber: 0, marker: " ", text: rawLine };
|
|
263
|
+
}
|
|
264
|
+
return { lineNumber: 0, marker, text: normalized.slice(1) };
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function findMatchingSequence(lines: string[], context: string[], start: number): number {
|
|
268
|
+
if (context.length === 0) {
|
|
269
|
+
return start;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const exact = findSequence(lines, context, start, (value) => value);
|
|
273
|
+
if (exact !== -1) {
|
|
274
|
+
return exact;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const trimEnd = findSequence(lines, context, start, (value) => value.trimEnd());
|
|
278
|
+
if (trimEnd !== -1) {
|
|
279
|
+
return trimEnd;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
const trim = findSequence(lines, context, start, (value) => value.trim());
|
|
283
|
+
if (trim !== -1) {
|
|
284
|
+
return trim;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
return start;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function findSequence(lines: string[], context: string[], start: number, normalize: (value: string) => string): number {
|
|
291
|
+
for (let lineIndex = start; lineIndex <= lines.length - context.length; lineIndex += 1) {
|
|
292
|
+
let matches = true;
|
|
293
|
+
for (let contextIndex = 0; contextIndex < context.length; contextIndex += 1) {
|
|
294
|
+
if (normalize(lines[lineIndex + contextIndex]) !== normalize(context[contextIndex])) {
|
|
295
|
+
matches = false;
|
|
296
|
+
break;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
if (matches) {
|
|
300
|
+
return lineIndex;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
return -1;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function renderPath(path: string, movePath: string | undefined, cwd: string): string {
|
|
307
|
+
const from = displayPath(path, cwd);
|
|
308
|
+
if (!movePath) {
|
|
309
|
+
return from;
|
|
310
|
+
}
|
|
311
|
+
return `${from} → ${displayPath(movePath, cwd)}`;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function displayPath(path: string, cwd: string): string {
|
|
315
|
+
if (!isAbsolute(path)) {
|
|
316
|
+
return path;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
const relativePath = relative(cwd, path);
|
|
320
|
+
if (relativePath !== "" && !relativePath.startsWith("..") && !isAbsolute(relativePath)) {
|
|
321
|
+
return relativePath;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
return path;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function readFileLines(path: string, cwd: string): string[] {
|
|
328
|
+
try {
|
|
329
|
+
return splitFileLines(openFileAtPath({ cwd, path }));
|
|
330
|
+
} catch {
|
|
331
|
+
return [];
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function splitFileLines(text: string): string[] {
|
|
336
|
+
if (text.length === 0) {
|
|
337
|
+
return [];
|
|
338
|
+
}
|
|
339
|
+
const lines = text.split("\n");
|
|
340
|
+
if (lines.at(-1) === "") {
|
|
341
|
+
lines.pop();
|
|
342
|
+
}
|
|
343
|
+
return lines;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function bulletHeader(verb: string, label: string): string {
|
|
347
|
+
return `• ${verb} ${label}`;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
function renderCounts(added: number, removed: number): string {
|
|
351
|
+
return `(+${added} -${removed})`;
|
|
352
|
+
}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { Type } from "@sinclair/typebox";
|
|
2
2
|
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
3
|
-
import { Text } from "@mariozechner/pi-tui";
|
|
3
|
+
import { Container, Text } from "@mariozechner/pi-tui";
|
|
4
4
|
import { executePatch } from "../patch/core.ts";
|
|
5
5
|
import type { ExecutePatchResult } from "../patch/types.ts";
|
|
6
|
+
import { formatApplyPatchSummary, renderApplyPatchCall } from "./apply-patch-rendering.ts";
|
|
6
7
|
|
|
7
8
|
const APPLY_PATCH_PARAMETERS = Type.Object({
|
|
8
9
|
input: Type.String({
|
|
@@ -10,6 +11,21 @@ const APPLY_PATCH_PARAMETERS = Type.Object({
|
|
|
10
11
|
}),
|
|
11
12
|
});
|
|
12
13
|
|
|
14
|
+
interface ApplyPatchRenderState {
|
|
15
|
+
cwd: string;
|
|
16
|
+
collapsed: string;
|
|
17
|
+
expanded: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const applyPatchRenderStates = new Map<string, ApplyPatchRenderState>();
|
|
21
|
+
|
|
22
|
+
interface ApplyPatchRenderContextLike {
|
|
23
|
+
toolCallId?: string;
|
|
24
|
+
cwd?: string;
|
|
25
|
+
expanded?: boolean;
|
|
26
|
+
argsComplete?: boolean;
|
|
27
|
+
}
|
|
28
|
+
|
|
13
29
|
function parseApplyPatchParams(params: unknown): { patchText: string } {
|
|
14
30
|
if (!params || typeof params !== "object" || !("input" in params) || typeof params.input !== "string") {
|
|
15
31
|
throw new Error("apply_patch requires a string 'input' parameter");
|
|
@@ -23,6 +39,28 @@ function isExecutePatchResult(details: unknown): details is ExecutePatchResult {
|
|
|
23
39
|
|
|
24
40
|
export type { ExecutePatchResult } from "../patch/types.ts";
|
|
25
41
|
|
|
42
|
+
export function clearApplyPatchRenderState(): void {
|
|
43
|
+
applyPatchRenderStates.clear();
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const renderApplyPatchCallWithOptionalContext: any = (
|
|
47
|
+
args: { input?: unknown },
|
|
48
|
+
theme: { fg(role: string, text: string): string; bold(text: string): string },
|
|
49
|
+
context?: ApplyPatchRenderContextLike,
|
|
50
|
+
) => {
|
|
51
|
+
if (context?.argsComplete === false) {
|
|
52
|
+
return new Text(`${theme.fg("dim", "•")} ${theme.bold("Patching")}`, 0, 0);
|
|
53
|
+
}
|
|
54
|
+
const patchText = typeof args.input === "string" ? args.input : "";
|
|
55
|
+
if (patchText.trim().length === 0) {
|
|
56
|
+
return new Text(`${theme.fg("dim", "•")} ${theme.bold("Patching")}`, 0, 0);
|
|
57
|
+
}
|
|
58
|
+
const cached = context?.toolCallId ? applyPatchRenderStates.get(context.toolCallId) : undefined;
|
|
59
|
+
const cwd = context?.cwd ?? cached?.cwd;
|
|
60
|
+
const text = context?.expanded ? cached?.expanded ?? renderApplyPatchCall(patchText, cwd) : cached?.collapsed ?? formatApplyPatchSummary(patchText, cwd);
|
|
61
|
+
return new Text(text, 0, 0);
|
|
62
|
+
};
|
|
63
|
+
|
|
26
64
|
export function registerApplyPatchTool(pi: ExtensionAPI): void {
|
|
27
65
|
pi.registerTool({
|
|
28
66
|
name: "apply_patch",
|
|
@@ -31,12 +69,17 @@ export function registerApplyPatchTool(pi: ExtensionAPI): void {
|
|
|
31
69
|
promptSnippet: "Edit files with a patch.",
|
|
32
70
|
promptGuidelines: ["Prefer apply_patch for focused textual edits instead of rewriting whole files."],
|
|
33
71
|
parameters: APPLY_PATCH_PARAMETERS,
|
|
34
|
-
async execute(
|
|
72
|
+
async execute(toolCallId, params, signal, _onUpdate, ctx) {
|
|
35
73
|
if (signal?.aborted) {
|
|
36
74
|
throw new Error("apply_patch aborted");
|
|
37
75
|
}
|
|
38
76
|
|
|
39
77
|
const typedParams = parseApplyPatchParams(params);
|
|
78
|
+
applyPatchRenderStates.set(toolCallId, {
|
|
79
|
+
cwd: ctx.cwd,
|
|
80
|
+
collapsed: formatApplyPatchSummary(typedParams.patchText, ctx.cwd),
|
|
81
|
+
expanded: renderApplyPatchCall(typedParams.patchText, ctx.cwd),
|
|
82
|
+
});
|
|
40
83
|
const result = executePatch({ cwd: ctx.cwd, patchText: typedParams.patchText });
|
|
41
84
|
const summary = [
|
|
42
85
|
"Applied patch successfully.",
|
|
@@ -52,33 +95,17 @@ export function registerApplyPatchTool(pi: ExtensionAPI): void {
|
|
|
52
95
|
details: result,
|
|
53
96
|
};
|
|
54
97
|
},
|
|
55
|
-
renderCall
|
|
56
|
-
|
|
57
|
-
},
|
|
58
|
-
renderResult(result, { isPartial, expanded }, theme) {
|
|
98
|
+
renderCall: renderApplyPatchCallWithOptionalContext,
|
|
99
|
+
renderResult(result, { isPartial }, theme) {
|
|
59
100
|
if (isPartial) {
|
|
60
|
-
return new Text(theme.fg("
|
|
101
|
+
return new Text(`${theme.fg("dim", "•")} ${theme.bold("Patching")}`, 0, 0);
|
|
61
102
|
}
|
|
62
103
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
return new Text(theme.fg("success", "Patch applied"), 0, 0);
|
|
104
|
+
if (!isExecutePatchResult(result.details)) {
|
|
105
|
+
return new Container();
|
|
66
106
|
}
|
|
67
107
|
|
|
68
|
-
|
|
69
|
-
text += theme.fg(
|
|
70
|
-
"dim",
|
|
71
|
-
` (${details.changedFiles.length} changed, ${details.createdFiles.length} created, ${details.deletedFiles.length} deleted)`,
|
|
72
|
-
);
|
|
73
|
-
if (expanded) {
|
|
74
|
-
for (const file of details.changedFiles) {
|
|
75
|
-
text += `\n${theme.fg("dim", file)}`;
|
|
76
|
-
}
|
|
77
|
-
for (const move of details.movedFiles) {
|
|
78
|
-
text += `\n${theme.fg("accent", move)}`;
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
return new Text(text, 0, 0);
|
|
108
|
+
return new Container();
|
|
82
109
|
},
|
|
83
110
|
});
|
|
84
111
|
}
|
|
@@ -8,7 +8,11 @@ export interface RenderTheme {
|
|
|
8
8
|
|
|
9
9
|
export function renderExecCommandCall(command: string, state: ExecCommandStatus, theme: RenderTheme): string {
|
|
10
10
|
const summary = summarizeShellCommand(command);
|
|
11
|
-
return summary.maskAsExplored ? renderExplorationText(summary.actions, state, theme) : renderCommandText(command, state, theme);
|
|
11
|
+
return summary.maskAsExplored ? renderExplorationText([summary.actions], state, theme) : renderCommandText(command, state, theme);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function renderGroupedExecCommandCall(actionGroups: ShellAction[][], state: ExecCommandStatus, theme: RenderTheme): string {
|
|
15
|
+
return renderExplorationText(actionGroups, state, theme);
|
|
12
16
|
}
|
|
13
17
|
|
|
14
18
|
export function renderWriteStdinCall(
|
|
@@ -32,11 +36,11 @@ export function renderWriteStdinCall(
|
|
|
32
36
|
return text;
|
|
33
37
|
}
|
|
34
38
|
|
|
35
|
-
function renderExplorationText(
|
|
39
|
+
function renderExplorationText(actionGroups: ShellAction[][], state: ExecCommandStatus, theme: RenderTheme): string {
|
|
36
40
|
const header = state === "running" ? "Exploring" : "Explored";
|
|
37
41
|
let text = `${theme.fg("dim", "•")} ${theme.bold(header)}`;
|
|
38
42
|
|
|
39
|
-
for (const [index, line] of
|
|
43
|
+
for (const [index, line] of coalesceReadGroups(actionGroups).map(formatActionLine).entries()) {
|
|
40
44
|
const prefix = index === 0 ? " └ " : " ";
|
|
41
45
|
text += `\n${theme.fg("dim", prefix)}${theme.fg("accent", line.title)} ${theme.fg("muted", line.body)}`;
|
|
42
46
|
}
|
|
@@ -83,13 +87,56 @@ function formatActionLine(action: ShellAction): { title: string; body: string }
|
|
|
83
87
|
return { title: "Run", body: action.command };
|
|
84
88
|
}
|
|
85
89
|
|
|
86
|
-
function
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
90
|
+
function coalesceReadGroups(actionGroups: ShellAction[][]): ShellAction[] {
|
|
91
|
+
const flattened: ShellAction[] = [];
|
|
92
|
+
|
|
93
|
+
for (let index = 0; index < actionGroups.length; index += 1) {
|
|
94
|
+
const actions = actionGroups[index];
|
|
95
|
+
if (actions.every((action) => action.kind === "read")) {
|
|
96
|
+
const reads: Extract<ShellAction, { kind: "read" }>[] = [];
|
|
97
|
+
const seenPaths = new Set<string>();
|
|
98
|
+
let lastRead: Extract<ShellAction, { kind: "read" }> | undefined;
|
|
99
|
+
|
|
100
|
+
for (let readIndex = index; readIndex < actionGroups.length; readIndex += 1) {
|
|
101
|
+
const readActions = actionGroups[readIndex];
|
|
102
|
+
if (!readActions.every((action) => action.kind === "read")) {
|
|
103
|
+
break;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
for (const action of readActions) {
|
|
107
|
+
if (action.kind !== "read") continue;
|
|
108
|
+
lastRead = action;
|
|
109
|
+
if (seenPaths.has(action.path)) continue;
|
|
110
|
+
seenPaths.add(action.path);
|
|
111
|
+
reads.push(action);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
index = readIndex;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (lastRead) {
|
|
118
|
+
const duplicateNames = new Set<string>();
|
|
119
|
+
const seenNames = new Set<string>();
|
|
120
|
+
for (const read of reads) {
|
|
121
|
+
if (seenNames.has(read.name)) {
|
|
122
|
+
duplicateNames.add(read.name);
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
seenNames.add(read.name);
|
|
126
|
+
}
|
|
127
|
+
const labels = reads.map((read) => (duplicateNames.has(read.name) ? read.path : read.name));
|
|
128
|
+
flattened.push({
|
|
129
|
+
kind: "read",
|
|
130
|
+
command: labels.join(" && "),
|
|
131
|
+
name: labels.join(", "),
|
|
132
|
+
path: lastRead.path,
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
flattened.push(...actions);
|
|
91
139
|
}
|
|
92
140
|
|
|
93
|
-
|
|
94
|
-
return [{ kind: "read", command: reads.map((action) => action.command).join(" && "), name: names.join(", "), path: reads[0].path }];
|
|
141
|
+
return flattened;
|
|
95
142
|
}
|
|
@@ -1,43 +1,215 @@
|
|
|
1
|
+
import { summarizeShellCommand, type CommandSummary, type ShellAction } from "../shell/summary.ts";
|
|
2
|
+
|
|
1
3
|
export type ExecCommandStatus = "running" | "done";
|
|
2
4
|
|
|
5
|
+
export interface ExecCommandRenderInfo {
|
|
6
|
+
hidden: boolean;
|
|
7
|
+
status: ExecCommandStatus;
|
|
8
|
+
actionGroups?: ShellAction[][];
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
interface ExecEntry {
|
|
12
|
+
toolCallId: string;
|
|
13
|
+
command: string;
|
|
14
|
+
summary: CommandSummary;
|
|
15
|
+
status: ExecCommandStatus;
|
|
16
|
+
hidden: boolean;
|
|
17
|
+
groupId?: number;
|
|
18
|
+
invalidate?: () => void;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
interface ExecGroup {
|
|
22
|
+
id: number;
|
|
23
|
+
entryIds: string[];
|
|
24
|
+
visibleEntryId: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
3
27
|
export interface ExecCommandTracker {
|
|
4
28
|
getState(command: string): ExecCommandStatus;
|
|
29
|
+
getRenderInfo(toolCallId: string | undefined, command: string): ExecCommandRenderInfo;
|
|
30
|
+
registerRenderContext(toolCallId: string | undefined, invalidate: () => void): void;
|
|
5
31
|
recordStart(toolCallId: string, command: string): void;
|
|
6
|
-
recordPersistentSession(
|
|
32
|
+
recordPersistentSession(toolCallId: string, sessionId: number): void;
|
|
7
33
|
recordEnd(toolCallId: string): void;
|
|
8
|
-
|
|
34
|
+
recordSessionFinished(sessionId: number): void;
|
|
35
|
+
resetExplorationGroup(): void;
|
|
36
|
+
clear(): void;
|
|
9
37
|
}
|
|
10
38
|
|
|
11
39
|
export function createExecCommandTracker(): ExecCommandTracker {
|
|
12
40
|
const commandByToolCallId = new Map<string, string>();
|
|
13
|
-
const
|
|
14
|
-
const
|
|
41
|
+
const runningCountsByCommand = new Map<string, number>();
|
|
42
|
+
const sessionBackedToolCallIds = new Set<string>();
|
|
43
|
+
const toolCallIdBySessionId = new Map<number, string>();
|
|
44
|
+
const entriesByToolCallId = new Map<string, ExecEntry>();
|
|
45
|
+
const groupsById = new Map<number, ExecGroup>();
|
|
46
|
+
let activeExplorationGroupId: number | undefined;
|
|
47
|
+
let nextGroupId = 1;
|
|
48
|
+
|
|
49
|
+
function incrementCommand(command: string): void {
|
|
50
|
+
runningCountsByCommand.set(command, (runningCountsByCommand.get(command) ?? 0) + 1);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function decrementCommand(command: string): void {
|
|
54
|
+
const next = (runningCountsByCommand.get(command) ?? 0) - 1;
|
|
55
|
+
if (next > 0) {
|
|
56
|
+
runningCountsByCommand.set(command, next);
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
runningCountsByCommand.delete(command);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function invalidateToolCall(toolCallId: string | undefined): void {
|
|
63
|
+
if (!toolCallId) return;
|
|
64
|
+
entriesByToolCallId.get(toolCallId)?.invalidate?.();
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function findLatestEntryByCommand(command: string): ExecEntry | undefined {
|
|
68
|
+
let latest: ExecEntry | undefined;
|
|
69
|
+
for (const entry of entriesByToolCallId.values()) {
|
|
70
|
+
if (entry.command !== command) continue;
|
|
71
|
+
latest = entry;
|
|
72
|
+
}
|
|
73
|
+
return latest;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function getGroupForEntry(entry: ExecEntry | undefined): ExecGroup | undefined {
|
|
77
|
+
if (!entry?.groupId) return undefined;
|
|
78
|
+
return groupsById.get(entry.groupId);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function getVisibleEntry(group: ExecGroup | undefined): ExecEntry | undefined {
|
|
82
|
+
if (!group) return undefined;
|
|
83
|
+
return entriesByToolCallId.get(group.visibleEntryId);
|
|
84
|
+
}
|
|
15
85
|
|
|
16
86
|
return {
|
|
17
87
|
getState(command) {
|
|
18
|
-
return
|
|
88
|
+
return (runningCountsByCommand.get(command) ?? 0) > 0 ? "running" : "done";
|
|
89
|
+
},
|
|
90
|
+
getRenderInfo(toolCallId, command) {
|
|
91
|
+
if (!toolCallId) {
|
|
92
|
+
return { hidden: false, status: (runningCountsByCommand.get(command) ?? 0) > 0 ? "running" : "done" };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const entry = entriesByToolCallId.get(toolCallId);
|
|
96
|
+
if (!entry) {
|
|
97
|
+
return { hidden: false, status: (runningCountsByCommand.get(command) ?? 0) > 0 ? "running" : "done" };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (entry.hidden) {
|
|
101
|
+
return { hidden: true, status: entry.status };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const group = getGroupForEntry(entry);
|
|
105
|
+
if (!group) {
|
|
106
|
+
return {
|
|
107
|
+
hidden: false,
|
|
108
|
+
status: entry.status,
|
|
109
|
+
actionGroups: entry.summary.maskAsExplored ? [entry.summary.actions] : undefined,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const entries = group.entryIds
|
|
114
|
+
.map((groupEntryId) => entriesByToolCallId.get(groupEntryId))
|
|
115
|
+
.filter((groupEntry): groupEntry is ExecEntry => Boolean(groupEntry));
|
|
116
|
+
return {
|
|
117
|
+
hidden: false,
|
|
118
|
+
status: entries.some((groupEntry) => groupEntry.status === "running") ? "running" : "done",
|
|
119
|
+
actionGroups: entries.map((groupEntry) => groupEntry.summary.actions),
|
|
120
|
+
};
|
|
121
|
+
},
|
|
122
|
+
registerRenderContext(toolCallId, invalidate) {
|
|
123
|
+
if (!toolCallId) return;
|
|
124
|
+
const entry = entriesByToolCallId.get(toolCallId);
|
|
125
|
+
if (!entry) return;
|
|
126
|
+
entry.invalidate = invalidate;
|
|
19
127
|
},
|
|
20
128
|
recordStart(toolCallId, command) {
|
|
21
129
|
commandByToolCallId.set(toolCallId, command);
|
|
22
|
-
|
|
130
|
+
incrementCommand(command);
|
|
131
|
+
|
|
132
|
+
const summary = summarizeShellCommand(command);
|
|
133
|
+
const entry: ExecEntry = {
|
|
134
|
+
toolCallId,
|
|
135
|
+
command,
|
|
136
|
+
summary,
|
|
137
|
+
status: "running",
|
|
138
|
+
hidden: false,
|
|
139
|
+
};
|
|
140
|
+
entriesByToolCallId.set(toolCallId, entry);
|
|
141
|
+
|
|
142
|
+
if (!summary.maskAsExplored) {
|
|
143
|
+
activeExplorationGroupId = undefined;
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
let group = activeExplorationGroupId ? groupsById.get(activeExplorationGroupId) : undefined;
|
|
148
|
+
if (!group) {
|
|
149
|
+
group = { id: nextGroupId++, entryIds: [toolCallId], visibleEntryId: toolCallId };
|
|
150
|
+
groupsById.set(group.id, group);
|
|
151
|
+
activeExplorationGroupId = group.id;
|
|
152
|
+
entry.groupId = group.id;
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const previousVisibleEntry = getVisibleEntry(group);
|
|
157
|
+
if (previousVisibleEntry) {
|
|
158
|
+
previousVisibleEntry.hidden = true;
|
|
159
|
+
invalidateToolCall(previousVisibleEntry.toolCallId);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
group.entryIds.push(toolCallId);
|
|
163
|
+
group.visibleEntryId = toolCallId;
|
|
164
|
+
entry.groupId = group.id;
|
|
23
165
|
},
|
|
24
|
-
recordPersistentSession(
|
|
25
|
-
|
|
26
|
-
|
|
166
|
+
recordPersistentSession(toolCallId, sessionId) {
|
|
167
|
+
sessionBackedToolCallIds.add(toolCallId);
|
|
168
|
+
toolCallIdBySessionId.set(sessionId, toolCallId);
|
|
169
|
+
const entry = entriesByToolCallId.get(toolCallId);
|
|
170
|
+
if (!entry) return;
|
|
171
|
+
entry.status = "running";
|
|
172
|
+
const group = getGroupForEntry(entry);
|
|
173
|
+
invalidateToolCall(group?.visibleEntryId ?? entry.toolCallId);
|
|
27
174
|
},
|
|
28
175
|
recordEnd(toolCallId) {
|
|
29
176
|
const command = commandByToolCallId.get(toolCallId);
|
|
30
177
|
if (!command) return;
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
178
|
+
const entry = entriesByToolCallId.get(toolCallId);
|
|
179
|
+
if (!sessionBackedToolCallIds.has(toolCallId)) {
|
|
180
|
+
decrementCommand(command);
|
|
181
|
+
if (entry) {
|
|
182
|
+
entry.status = "done";
|
|
183
|
+
}
|
|
35
184
|
}
|
|
185
|
+
const group = getGroupForEntry(entry);
|
|
186
|
+
invalidateToolCall(group?.visibleEntryId ?? toolCallId);
|
|
36
187
|
commandByToolCallId.delete(toolCallId);
|
|
37
188
|
},
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
189
|
+
recordSessionFinished(sessionId) {
|
|
190
|
+
const toolCallId = toolCallIdBySessionId.get(sessionId);
|
|
191
|
+
if (!toolCallId) return;
|
|
192
|
+
toolCallIdBySessionId.delete(sessionId);
|
|
193
|
+
const entry = entriesByToolCallId.get(toolCallId);
|
|
194
|
+
if (!entry) return;
|
|
195
|
+
decrementCommand(entry.command);
|
|
196
|
+
entry.status = "done";
|
|
197
|
+
sessionBackedToolCallIds.delete(toolCallId);
|
|
198
|
+
const group = getGroupForEntry(entry);
|
|
199
|
+
invalidateToolCall(group?.visibleEntryId ?? entry.toolCallId);
|
|
200
|
+
},
|
|
201
|
+
resetExplorationGroup() {
|
|
202
|
+
activeExplorationGroupId = undefined;
|
|
203
|
+
},
|
|
204
|
+
clear() {
|
|
205
|
+
commandByToolCallId.clear();
|
|
206
|
+
runningCountsByCommand.clear();
|
|
207
|
+
sessionBackedToolCallIds.clear();
|
|
208
|
+
toolCallIdBySessionId.clear();
|
|
209
|
+
entriesByToolCallId.clear();
|
|
210
|
+
groupsById.clear();
|
|
211
|
+
activeExplorationGroupId = undefined;
|
|
212
|
+
nextGroupId = 1;
|
|
41
213
|
},
|
|
42
214
|
};
|
|
43
215
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
2
2
|
import { Type } from "@sinclair/typebox";
|
|
3
|
-
import { Text } from "@mariozechner/pi-tui";
|
|
4
|
-
import { renderExecCommandCall } from "./codex-rendering.ts";
|
|
3
|
+
import { Container, Text } from "@mariozechner/pi-tui";
|
|
4
|
+
import { renderExecCommandCall, renderGroupedExecCommandCall } from "./codex-rendering.ts";
|
|
5
5
|
import type { ExecCommandTracker } from "./exec-command-state.ts";
|
|
6
6
|
import type { ExecSessionManager, UnifiedExecResult } from "./exec-session-manager.ts";
|
|
7
7
|
import { formatUnifiedExecResult } from "./unified-exec-format.ts";
|
|
@@ -56,6 +56,62 @@ function isUnifiedExecResult(details: unknown): details is UnifiedExecResult {
|
|
|
56
56
|
return typeof details === "object" && details !== null;
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
+
function createEmptyResultComponent(): Container {
|
|
60
|
+
return new Container();
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
interface ExecCommandRenderContextLike {
|
|
64
|
+
toolCallId?: string;
|
|
65
|
+
invalidate?: () => void;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const renderExecCommandCallWithOptionalContext: any = (
|
|
69
|
+
args: { cmd?: unknown },
|
|
70
|
+
theme: { fg(role: string, text: string): string; bold(text: string): string },
|
|
71
|
+
context: ExecCommandRenderContextLike | undefined,
|
|
72
|
+
tracker: ExecCommandTracker,
|
|
73
|
+
) => {
|
|
74
|
+
const command = typeof args.cmd === "string" ? args.cmd : "";
|
|
75
|
+
tracker.registerRenderContext(context?.toolCallId, context?.invalidate ?? (() => {}));
|
|
76
|
+
const renderInfo = tracker.getRenderInfo(context?.toolCallId, command);
|
|
77
|
+
if (renderInfo.hidden) {
|
|
78
|
+
return new Text("", 0, 0);
|
|
79
|
+
}
|
|
80
|
+
const text = renderInfo.actionGroups
|
|
81
|
+
? renderGroupedExecCommandCall(renderInfo.actionGroups, renderInfo.status, theme)
|
|
82
|
+
: renderExecCommandCall(command, renderInfo.status, theme);
|
|
83
|
+
return new Text(text, 0, 0);
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
const renderExecCommandResultWithOptionalContext: any = (
|
|
87
|
+
result: { content: Array<{ type: string; text?: string }>; details?: unknown },
|
|
88
|
+
options: { expanded: boolean; isPartial: boolean },
|
|
89
|
+
theme: { fg(role: string, text: string): string },
|
|
90
|
+
context: ExecCommandRenderContextLike | undefined,
|
|
91
|
+
tracker: ExecCommandTracker,
|
|
92
|
+
) => {
|
|
93
|
+
if (options.isPartial || !options.expanded) {
|
|
94
|
+
return createEmptyResultComponent();
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const command = context && "args" in context && context.args && typeof (context as any).args.cmd === "string" ? (context as any).args.cmd : undefined;
|
|
98
|
+
if (tracker.getRenderInfo(context?.toolCallId, command ?? "").hidden) {
|
|
99
|
+
return createEmptyResultComponent();
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const details = isUnifiedExecResult(result.details) ? result.details : undefined;
|
|
103
|
+
const content = result.content.find((item) => item.type === "text");
|
|
104
|
+
const output = details?.output ?? (content?.type === "text" ? content.text : "");
|
|
105
|
+
let text = theme.fg("dim", output || "(no output)");
|
|
106
|
+
if (details?.session_id !== undefined) {
|
|
107
|
+
text += `\n${theme.fg("accent", `Session ${details.session_id} still running`)}`;
|
|
108
|
+
}
|
|
109
|
+
if (details?.exit_code !== undefined) {
|
|
110
|
+
text += `\n${theme.fg("muted", `Exit code: ${details.exit_code}`)}`;
|
|
111
|
+
}
|
|
112
|
+
return new Text(text, 0, 0);
|
|
113
|
+
};
|
|
114
|
+
|
|
59
115
|
export function registerExecCommandTool(pi: ExtensionAPI, tracker: ExecCommandTracker, sessions: ExecSessionManager): void {
|
|
60
116
|
pi.registerTool({
|
|
61
117
|
name: "exec_command",
|
|
@@ -68,40 +124,27 @@ export function registerExecCommandTool(pi: ExtensionAPI, tracker: ExecCommandTr
|
|
|
68
124
|
"Keep tty disabled unless the command truly needs interactive terminal behavior.",
|
|
69
125
|
],
|
|
70
126
|
parameters: EXEC_COMMAND_PARAMETERS,
|
|
71
|
-
async execute(
|
|
127
|
+
async execute(toolCallId, params, signal, _onUpdate, ctx) {
|
|
72
128
|
if (signal?.aborted) {
|
|
73
129
|
throw new Error("exec_command aborted");
|
|
74
130
|
}
|
|
75
131
|
const typedParams = parseExecCommandParams(params);
|
|
76
132
|
const result = await sessions.exec(typedParams, ctx.cwd, signal);
|
|
77
133
|
if (result.session_id !== undefined) {
|
|
78
|
-
tracker.recordPersistentSession(
|
|
134
|
+
tracker.recordPersistentSession(toolCallId, result.session_id);
|
|
79
135
|
}
|
|
80
136
|
return {
|
|
81
137
|
content: [{ type: "text", text: formatUnifiedExecResult(result, typedParams.cmd) }],
|
|
82
138
|
details: result,
|
|
83
139
|
};
|
|
84
140
|
},
|
|
85
|
-
renderCall(args, theme)
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
const details = isUnifiedExecResult(result.details) ? result.details : undefined;
|
|
95
|
-
const content = result.content.find((item) => item.type === "text");
|
|
96
|
-
const output = details?.output ?? (content?.type === "text" ? content.text : "");
|
|
97
|
-
let text = theme.fg("dim", output || "(no output)");
|
|
98
|
-
if (details?.session_id !== undefined) {
|
|
99
|
-
text += `\n${theme.fg("accent", `Session ${details.session_id} still running`)}`;
|
|
100
|
-
}
|
|
101
|
-
if (details?.exit_code !== undefined) {
|
|
102
|
-
text += `\n${theme.fg("muted", `Exit code: ${details.exit_code}`)}`;
|
|
103
|
-
}
|
|
104
|
-
return new Text(text, 0, 0);
|
|
105
|
-
},
|
|
141
|
+
renderCall: ((args: { cmd?: unknown }, theme: { fg(role: string, text: string): string; bold(text: string): string }, context?: ExecCommandRenderContextLike) =>
|
|
142
|
+
renderExecCommandCallWithOptionalContext(args, theme, context, tracker)) as any,
|
|
143
|
+
renderResult: ((
|
|
144
|
+
result: { content: Array<{ type: string; text?: string }>; details?: unknown },
|
|
145
|
+
options: { expanded: boolean; isPartial: boolean },
|
|
146
|
+
theme: { fg(role: string, text: string): string },
|
|
147
|
+
context?: ExecCommandRenderContextLike,
|
|
148
|
+
) => renderExecCommandResultWithOptionalContext(result, options, theme, context, tracker)) as any,
|
|
106
149
|
});
|
|
107
150
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { ExtensionAPI, ExtensionContext, ToolDefinition } from "@mariozechner/pi-coding-agent";
|
|
2
2
|
import { Type } from "@sinclair/typebox";
|
|
3
|
-
import { Box, Text } from "@mariozechner/pi-tui";
|
|
3
|
+
import { Box, Container, Text } from "@mariozechner/pi-tui";
|
|
4
4
|
import { isOpenAICodexModel } from "../adapter/codex-model.ts";
|
|
5
5
|
|
|
6
6
|
export const WEB_SEARCH_UNSUPPORTED_MESSAGE = "web_search is only available with the openai-codex provider";
|
|
@@ -56,6 +56,10 @@ function isWebSearchFunctionTool(tool: unknown): tool is FunctionToolPayload {
|
|
|
56
56
|
return !!tool && typeof tool === "object" && (tool as FunctionToolPayload).type === "function" && (tool as FunctionToolPayload).name === "web_search";
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
+
function createEmptyResultComponent(): Container {
|
|
60
|
+
return new Container();
|
|
61
|
+
}
|
|
62
|
+
|
|
59
63
|
export function rewriteNativeWebSearchTool(payload: unknown, model: ExtensionContext["model"]): unknown {
|
|
60
64
|
if (!supportsNativeWebSearch(model) || !payload || typeof payload !== "object") {
|
|
61
65
|
return payload;
|
|
@@ -113,7 +117,7 @@ export function createWebSearchTool(): ToolDefinition<typeof WEB_SEARCH_PARAMETE
|
|
|
113
117
|
},
|
|
114
118
|
renderResult(result, { expanded }, theme) {
|
|
115
119
|
if (!expanded) {
|
|
116
|
-
return
|
|
120
|
+
return createEmptyResultComponent();
|
|
117
121
|
}
|
|
118
122
|
const textBlock = result.content.find((item) => item.type === "text");
|
|
119
123
|
const text = textBlock?.type === "text" ? textBlock.text : "(no output)";
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
2
2
|
import { Type } from "@sinclair/typebox";
|
|
3
|
-
import { Text } from "@mariozechner/pi-tui";
|
|
3
|
+
import { Container, Text } from "@mariozechner/pi-tui";
|
|
4
4
|
import { renderWriteStdinCall } from "./codex-rendering.ts";
|
|
5
5
|
import type { ExecSessionManager, UnifiedExecResult } from "./exec-session-manager.ts";
|
|
6
6
|
import { formatUnifiedExecResult } from "./unified-exec-format.ts";
|
|
@@ -100,6 +100,10 @@ function isUnifiedExecResult(details: unknown): details is UnifiedExecResult {
|
|
|
100
100
|
return typeof details === "object" && details !== null;
|
|
101
101
|
}
|
|
102
102
|
|
|
103
|
+
function createEmptyResultComponent(): Container {
|
|
104
|
+
return new Container();
|
|
105
|
+
}
|
|
106
|
+
|
|
103
107
|
export function registerWriteStdinTool(pi: ExtensionAPI, sessions: ExecSessionManager): void {
|
|
104
108
|
pi.registerTool({
|
|
105
109
|
name: "write_stdin",
|
|
@@ -129,7 +133,7 @@ export function registerWriteStdinTool(pi: ExtensionAPI, sessions: ExecSessionMa
|
|
|
129
133
|
return new Text(renderWriteStdinCall(sessionId, input, command, theme), 0, 0);
|
|
130
134
|
},
|
|
131
135
|
renderResult(result, { expanded, isPartial }, theme) {
|
|
132
|
-
if (isPartial || !expanded) return
|
|
136
|
+
if (isPartial || !expanded) return createEmptyResultComponent();
|
|
133
137
|
const state = getResultState(result);
|
|
134
138
|
const output = renderTerminalText(state.output);
|
|
135
139
|
let text = theme.fg("dim", output || "(no output)");
|