@howaboua/pi-codex-conversion 1.0.9 → 1.0.11
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 +7 -1
- package/package.json +3 -3
- package/src/index.ts +26 -4
- 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/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 +204 -28
- package/src/tools/codex-rendering.ts +57 -10
- package/src/tools/exec-command-state.ts +188 -16
- package/src/tools/exec-command-tool.ts +69 -25
- package/src/tools/exec-session-manager.ts +60 -4
- package/src/tools/web-search-tool.ts +6 -2
- package/src/tools/write-stdin-tool.ts +10 -2
|
@@ -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
|
-
import type
|
|
5
|
+
import { ExecutePatchError, type ExecutePatchResult } from "../patch/types.ts";
|
|
6
|
+
import { formatApplyPatchSummary, formatPatchTarget, renderApplyPatchCall } from "./apply-patch-rendering.ts";
|
|
6
7
|
|
|
7
8
|
const APPLY_PATCH_PARAMETERS = Type.Object({
|
|
8
9
|
input: Type.String({
|
|
@@ -10,6 +11,38 @@ const APPLY_PATCH_PARAMETERS = Type.Object({
|
|
|
10
11
|
}),
|
|
11
12
|
});
|
|
12
13
|
|
|
14
|
+
interface ApplyPatchRenderState {
|
|
15
|
+
cwd: string;
|
|
16
|
+
patchText: string;
|
|
17
|
+
collapsed: string;
|
|
18
|
+
expanded: string;
|
|
19
|
+
status: "pending" | "partial_failure";
|
|
20
|
+
failedTarget?: string;
|
|
21
|
+
}
|
|
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
|
+
|
|
37
|
+
const applyPatchRenderStates = new Map<string, ApplyPatchRenderState>();
|
|
38
|
+
|
|
39
|
+
interface ApplyPatchRenderContextLike {
|
|
40
|
+
toolCallId?: string;
|
|
41
|
+
cwd?: string;
|
|
42
|
+
expanded?: boolean;
|
|
43
|
+
argsComplete?: boolean;
|
|
44
|
+
}
|
|
45
|
+
|
|
13
46
|
function parseApplyPatchParams(params: unknown): { patchText: string } {
|
|
14
47
|
if (!params || typeof params !== "object" || !("input" in params) || typeof params.input !== "string") {
|
|
15
48
|
throw new Error("apply_patch requires a string 'input' parameter");
|
|
@@ -17,27 +50,179 @@ function parseApplyPatchParams(params: unknown): { patchText: string } {
|
|
|
17
50
|
return { patchText: params.input };
|
|
18
51
|
}
|
|
19
52
|
|
|
20
|
-
function
|
|
21
|
-
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" = "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
|
+
const existing = applyPatchRenderStates.get(toolCallId);
|
|
78
|
+
if (!existing) {
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
applyPatchRenderStates.set(toolCallId, {
|
|
82
|
+
...existing,
|
|
83
|
+
status: "partial_failure",
|
|
84
|
+
failedTarget,
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function renderPartialFailureCall(
|
|
89
|
+
text: string,
|
|
90
|
+
theme: { fg(role: string, text: string): string },
|
|
91
|
+
failedTarget?: string,
|
|
92
|
+
): string {
|
|
93
|
+
const lines = text.split("\n");
|
|
94
|
+
if (lines.length === 0) {
|
|
95
|
+
return theme.fg("warning", "• Edit partially failed");
|
|
96
|
+
}
|
|
97
|
+
lines[0] = lines[0].replace(/^• (Added|Edited|Deleted)\b/, "• Edit partially failed");
|
|
98
|
+
const failedLineIndexes = new Set<number>();
|
|
99
|
+
if (failedTarget) {
|
|
100
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
101
|
+
const failedLine = markFailedTargetLine(lines[i], failedTarget);
|
|
102
|
+
if (failedLine) {
|
|
103
|
+
lines[i] = failedLine;
|
|
104
|
+
failedLineIndexes.add(i);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return lines
|
|
109
|
+
.map((line, index) => {
|
|
110
|
+
if (failedLineIndexes.has(index)) {
|
|
111
|
+
return theme.fg("error", line);
|
|
112
|
+
}
|
|
113
|
+
if (index === 0) {
|
|
114
|
+
return theme.fg("warning", line);
|
|
115
|
+
}
|
|
116
|
+
return line;
|
|
117
|
+
})
|
|
118
|
+
.join("\n");
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function markFailedTargetLine(line: string, failedTarget: string): string | undefined {
|
|
122
|
+
const suffixMatch = line.match(/ \(\+\d+ -\d+\)$/);
|
|
123
|
+
if (!suffixMatch) {
|
|
124
|
+
return undefined;
|
|
125
|
+
}
|
|
126
|
+
const suffix = suffixMatch[0];
|
|
127
|
+
const prefixAndTarget = line.slice(0, -suffix.length);
|
|
128
|
+
const candidatePrefixes = ["• Edit partially failed ", "• Added ", "• Edited ", "• Deleted ", " └ ", " "];
|
|
129
|
+
for (const prefix of candidatePrefixes) {
|
|
130
|
+
if (prefixAndTarget === `${prefix}${failedTarget}`) {
|
|
131
|
+
return `${prefix}${failedTarget} failed${suffix}`;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return undefined;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function summarizePatchCounts(result: ExecutePatchResult): string {
|
|
138
|
+
return [
|
|
139
|
+
`changed ${result.changedFiles.length} file${result.changedFiles.length === 1 ? "" : "s"}`,
|
|
140
|
+
`created ${result.createdFiles.length}`,
|
|
141
|
+
`deleted ${result.deletedFiles.length}`,
|
|
142
|
+
`moved ${result.movedFiles.length}`,
|
|
143
|
+
].join(", ");
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function describeFailedAction(error: ExecutePatchError, cwd: string): string | undefined {
|
|
147
|
+
if (!error.failedAction) {
|
|
148
|
+
return undefined;
|
|
149
|
+
}
|
|
150
|
+
return formatPatchTarget(error.failedAction.path, error.failedAction.type === "update" ? error.failedAction.movePath : undefined, cwd);
|
|
22
151
|
}
|
|
23
152
|
|
|
24
153
|
export type { ExecutePatchResult } from "../patch/types.ts";
|
|
25
154
|
|
|
155
|
+
export function clearApplyPatchRenderState(): void {
|
|
156
|
+
applyPatchRenderStates.clear();
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const renderApplyPatchCallWithOptionalContext: any = (
|
|
160
|
+
args: { input?: unknown },
|
|
161
|
+
theme: { fg(role: string, text: string): string; bold(text: string): string },
|
|
162
|
+
context?: ApplyPatchRenderContextLike,
|
|
163
|
+
) => {
|
|
164
|
+
if (context?.argsComplete === false) {
|
|
165
|
+
return new Text(`${theme.fg("dim", "•")} ${theme.bold("Patching")}`, 0, 0);
|
|
166
|
+
}
|
|
167
|
+
const patchText = typeof args.input === "string" ? args.input : "";
|
|
168
|
+
if (patchText.trim().length === 0) {
|
|
169
|
+
return new Text(`${theme.fg("dim", "•")} ${theme.bold("Patching")}`, 0, 0);
|
|
170
|
+
}
|
|
171
|
+
const cached = context?.toolCallId ? applyPatchRenderStates.get(context.toolCallId) : undefined;
|
|
172
|
+
const cwd = context?.cwd ?? cached?.cwd;
|
|
173
|
+
const effectivePatchText = cached?.patchText ?? patchText;
|
|
174
|
+
const baseText = context?.expanded
|
|
175
|
+
? cached?.expanded ?? renderApplyPatchCall(effectivePatchText, cwd)
|
|
176
|
+
: cached?.collapsed ?? formatApplyPatchSummary(effectivePatchText, cwd);
|
|
177
|
+
const text = cached?.status === "partial_failure" ? renderPartialFailureCall(baseText, theme, cached.failedTarget) : baseText;
|
|
178
|
+
return new Text(text, 0, 0);
|
|
179
|
+
};
|
|
180
|
+
|
|
26
181
|
export function registerApplyPatchTool(pi: ExtensionAPI): void {
|
|
27
182
|
pi.registerTool({
|
|
28
183
|
name: "apply_patch",
|
|
29
184
|
label: "apply_patch",
|
|
30
185
|
description: "Use `apply_patch` to edit files. Send the full patch in `input`.",
|
|
31
186
|
promptSnippet: "Edit files with a patch.",
|
|
32
|
-
promptGuidelines: [
|
|
187
|
+
promptGuidelines: [
|
|
188
|
+
"Prefer apply_patch for focused textual edits instead of rewriting whole files.",
|
|
189
|
+
"When one task needs coordinated edits across multiple files, send them in a single apply_patch call when one coherent patch will do.",
|
|
190
|
+
],
|
|
33
191
|
parameters: APPLY_PATCH_PARAMETERS,
|
|
34
|
-
async execute(
|
|
192
|
+
async execute(toolCallId, params, signal, _onUpdate, ctx) {
|
|
35
193
|
if (signal?.aborted) {
|
|
36
194
|
throw new Error("apply_patch aborted");
|
|
37
195
|
}
|
|
38
196
|
|
|
39
197
|
const typedParams = parseApplyPatchParams(params);
|
|
40
|
-
|
|
198
|
+
setApplyPatchRenderState(toolCallId, typedParams.patchText, ctx.cwd);
|
|
199
|
+
let result: ExecutePatchResult;
|
|
200
|
+
try {
|
|
201
|
+
result = executePatch({ cwd: ctx.cwd, patchText: typedParams.patchText });
|
|
202
|
+
} catch (error) {
|
|
203
|
+
if (error instanceof ExecutePatchError) {
|
|
204
|
+
const partial = error.hasPartialSuccess();
|
|
205
|
+
const failedTarget = describeFailedAction(error, ctx.cwd);
|
|
206
|
+
const prefix = partial
|
|
207
|
+
? `apply_patch partially failed after ${summarizePatchCounts(error.result)}`
|
|
208
|
+
: "apply_patch failed";
|
|
209
|
+
const message = failedTarget ? `${prefix} while patching ${failedTarget}: ${error.message}` : `${prefix}: ${error.message}`;
|
|
210
|
+
if (partial) {
|
|
211
|
+
markApplyPatchPartialFailure(toolCallId, failedTarget);
|
|
212
|
+
return {
|
|
213
|
+
content: [{ type: "text", text: message }],
|
|
214
|
+
details: {
|
|
215
|
+
status: "partial_failure",
|
|
216
|
+
result: error.result,
|
|
217
|
+
error: message,
|
|
218
|
+
failedTarget,
|
|
219
|
+
} satisfies ApplyPatchPartialFailureDetails,
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
throw new Error(message);
|
|
223
|
+
}
|
|
224
|
+
throw error;
|
|
225
|
+
}
|
|
41
226
|
const summary = [
|
|
42
227
|
"Applied patch successfully.",
|
|
43
228
|
`Changed files: ${result.changedFiles.length}`,
|
|
@@ -49,36 +234,27 @@ export function registerApplyPatchTool(pi: ExtensionAPI): void {
|
|
|
49
234
|
|
|
50
235
|
return {
|
|
51
236
|
content: [{ type: "text", text: summary }],
|
|
52
|
-
details:
|
|
237
|
+
details: {
|
|
238
|
+
status: "success",
|
|
239
|
+
result,
|
|
240
|
+
} satisfies ApplyPatchSuccessDetails,
|
|
53
241
|
};
|
|
54
242
|
},
|
|
55
|
-
renderCall
|
|
56
|
-
return new Text(`${theme.fg("toolTitle", theme.bold("apply_patch"))} ${theme.fg("muted", "patch")}`, 0, 0);
|
|
57
|
-
},
|
|
243
|
+
renderCall: renderApplyPatchCallWithOptionalContext,
|
|
58
244
|
renderResult(result, { isPartial, expanded }, theme) {
|
|
59
245
|
if (isPartial) {
|
|
60
|
-
return new Text(theme.fg("
|
|
246
|
+
return new Text(`${theme.fg("dim", "•")} ${theme.bold("Patching")}`, 0, 0);
|
|
61
247
|
}
|
|
62
248
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
return new Text(theme.fg("success", "Patch applied"), 0, 0);
|
|
249
|
+
if (!isApplyPatchToolDetails(result.details)) {
|
|
250
|
+
return new Container();
|
|
66
251
|
}
|
|
67
252
|
|
|
68
|
-
|
|
69
|
-
|
|
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
|
-
}
|
|
253
|
+
if (result.details.status === "partial_failure") {
|
|
254
|
+
return new Container();
|
|
80
255
|
}
|
|
81
|
-
|
|
256
|
+
|
|
257
|
+
return new Container();
|
|
82
258
|
},
|
|
83
259
|
});
|
|
84
260
|
}
|
|
@@ -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
|
}
|