@oh-my-pi/pi-coding-agent 6.9.0 → 7.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +173 -51
- package/examples/sdk/04-skills.ts +1 -1
- package/package.json +6 -5
- package/src/cli/stats-cli.ts +191 -0
- package/src/core/agent-session.ts +214 -4
- package/src/core/auth-storage.ts +524 -202
- package/src/core/bash-executor.ts +1 -1
- package/src/core/extensions/index.ts +2 -0
- package/src/core/extensions/runner.ts +31 -0
- package/src/core/extensions/types.ts +24 -0
- package/src/core/messages.ts +48 -0
- package/src/core/model-registry.ts +7 -0
- package/src/core/python-executor.ts +29 -8
- package/src/core/python-gateway-coordinator.ts +55 -1
- package/src/core/python-prelude.py +201 -8
- package/src/core/session-manager.ts +10 -1
- package/src/core/tools/bash.ts +5 -7
- package/src/core/tools/find.ts +18 -5
- package/src/core/tools/index.ts +1 -1
- package/src/core/tools/lsp/index.ts +13 -2
- package/src/core/tools/patch/applicator.ts +115 -17
- package/src/core/tools/patch/index.ts +1 -1
- package/src/core/tools/patch/normalize.ts +185 -10
- package/src/core/tools/python.ts +445 -86
- package/src/core/tools/read.ts +4 -4
- package/src/core/tools/task/executor.ts +2 -6
- package/src/core/tools/task/index.ts +30 -12
- package/src/core/tools/task/render.ts +163 -30
- package/src/core/tools/task/template.ts +37 -0
- package/src/core/tools/task/types.ts +6 -2
- package/src/core/tools/task/worker.ts +1 -1
- package/src/index.ts +2 -0
- package/src/main.ts +12 -0
- package/src/modes/interactive/components/python-execution.ts +180 -0
- package/src/modes/interactive/components/welcome.ts +1 -0
- package/src/modes/interactive/controllers/command-controller.ts +395 -0
- package/src/modes/interactive/controllers/input-controller.ts +83 -8
- package/src/modes/interactive/interactive-mode.ts +16 -1
- package/src/modes/interactive/theme/dark.json +2 -9
- package/src/modes/interactive/theme/defaults/alabaster.json +2 -8
- package/src/modes/interactive/theme/defaults/amethyst.json +2 -9
- package/src/modes/interactive/theme/defaults/anthracite.json +2 -9
- package/src/modes/interactive/theme/defaults/basalt.json +89 -88
- package/src/modes/interactive/theme/defaults/birch.json +2 -8
- package/src/modes/interactive/theme/defaults/dark-abyss.json +2 -8
- package/src/modes/interactive/theme/defaults/dark-arctic.json +2 -9
- package/src/modes/interactive/theme/defaults/dark-aurora.json +3 -2
- package/src/modes/interactive/theme/defaults/dark-catppuccin.json +2 -1
- package/src/modes/interactive/theme/defaults/dark-cavern.json +2 -8
- package/src/modes/interactive/theme/defaults/dark-copper.json +3 -2
- package/src/modes/interactive/theme/defaults/dark-cosmos.json +2 -8
- package/src/modes/interactive/theme/defaults/dark-cyberpunk.json +2 -9
- package/src/modes/interactive/theme/defaults/dark-dracula.json +2 -9
- package/src/modes/interactive/theme/defaults/dark-eclipse.json +2 -8
- package/src/modes/interactive/theme/defaults/dark-ember.json +3 -2
- package/src/modes/interactive/theme/defaults/dark-equinox.json +2 -8
- package/src/modes/interactive/theme/defaults/dark-forest.json +2 -9
- package/src/modes/interactive/theme/defaults/dark-github.json +2 -9
- package/src/modes/interactive/theme/defaults/dark-gruvbox.json +2 -9
- package/src/modes/interactive/theme/defaults/dark-lavender.json +3 -2
- package/src/modes/interactive/theme/defaults/dark-lunar.json +2 -8
- package/src/modes/interactive/theme/defaults/dark-midnight.json +3 -2
- package/src/modes/interactive/theme/defaults/dark-monochrome.json +2 -9
- package/src/modes/interactive/theme/defaults/dark-monokai.json +2 -9
- package/src/modes/interactive/theme/defaults/dark-nebula.json +2 -8
- package/src/modes/interactive/theme/defaults/dark-nord.json +2 -9
- package/src/modes/interactive/theme/defaults/dark-ocean.json +2 -9
- package/src/modes/interactive/theme/defaults/dark-one.json +2 -9
- package/src/modes/interactive/theme/defaults/dark-rainforest.json +2 -8
- package/src/modes/interactive/theme/defaults/dark-reef.json +2 -8
- package/src/modes/interactive/theme/defaults/dark-retro.json +2 -9
- package/src/modes/interactive/theme/defaults/dark-rose-pine.json +2 -1
- package/src/modes/interactive/theme/defaults/dark-sakura.json +3 -2
- package/src/modes/interactive/theme/defaults/dark-slate.json +3 -2
- package/src/modes/interactive/theme/defaults/dark-solarized.json +2 -1
- package/src/modes/interactive/theme/defaults/dark-solstice.json +2 -8
- package/src/modes/interactive/theme/defaults/dark-starfall.json +2 -8
- package/src/modes/interactive/theme/defaults/dark-sunset.json +2 -9
- package/src/modes/interactive/theme/defaults/dark-swamp.json +2 -8
- package/src/modes/interactive/theme/defaults/dark-synthwave.json +2 -1
- package/src/modes/interactive/theme/defaults/dark-taiga.json +2 -8
- package/src/modes/interactive/theme/defaults/dark-terminal.json +3 -2
- package/src/modes/interactive/theme/defaults/dark-tokyo-night.json +2 -9
- package/src/modes/interactive/theme/defaults/dark-tundra.json +2 -8
- package/src/modes/interactive/theme/defaults/dark-twilight.json +2 -8
- package/src/modes/interactive/theme/defaults/dark-volcanic.json +2 -8
- package/src/modes/interactive/theme/defaults/graphite.json +2 -9
- package/src/modes/interactive/theme/defaults/light-arctic.json +2 -1
- package/src/modes/interactive/theme/defaults/light-aurora-day.json +2 -8
- package/src/modes/interactive/theme/defaults/light-canyon.json +2 -8
- package/src/modes/interactive/theme/defaults/light-catppuccin.json +2 -1
- package/src/modes/interactive/theme/defaults/light-cirrus.json +2 -8
- package/src/modes/interactive/theme/defaults/light-coral.json +3 -2
- package/src/modes/interactive/theme/defaults/light-cyberpunk.json +2 -9
- package/src/modes/interactive/theme/defaults/light-dawn.json +2 -8
- package/src/modes/interactive/theme/defaults/light-dunes.json +2 -8
- package/src/modes/interactive/theme/defaults/light-eucalyptus.json +3 -2
- package/src/modes/interactive/theme/defaults/light-forest.json +2 -9
- package/src/modes/interactive/theme/defaults/light-frost.json +3 -2
- package/src/modes/interactive/theme/defaults/light-github.json +2 -1
- package/src/modes/interactive/theme/defaults/light-glacier.json +2 -8
- package/src/modes/interactive/theme/defaults/light-gruvbox.json +2 -9
- package/src/modes/interactive/theme/defaults/light-haze.json +2 -8
- package/src/modes/interactive/theme/defaults/light-honeycomb.json +3 -2
- package/src/modes/interactive/theme/defaults/light-lagoon.json +2 -8
- package/src/modes/interactive/theme/defaults/light-lavender.json +3 -2
- package/src/modes/interactive/theme/defaults/light-meadow.json +2 -8
- package/src/modes/interactive/theme/defaults/light-mint.json +3 -2
- package/src/modes/interactive/theme/defaults/light-monochrome.json +2 -1
- package/src/modes/interactive/theme/defaults/light-ocean.json +2 -9
- package/src/modes/interactive/theme/defaults/light-one.json +2 -8
- package/src/modes/interactive/theme/defaults/light-opal.json +2 -8
- package/src/modes/interactive/theme/defaults/light-orchard.json +2 -8
- package/src/modes/interactive/theme/defaults/light-paper.json +3 -2
- package/src/modes/interactive/theme/defaults/light-prism.json +2 -8
- package/src/modes/interactive/theme/defaults/light-retro.json +2 -9
- package/src/modes/interactive/theme/defaults/light-sand.json +3 -2
- package/src/modes/interactive/theme/defaults/light-savanna.json +2 -8
- package/src/modes/interactive/theme/defaults/light-solarized.json +2 -1
- package/src/modes/interactive/theme/defaults/light-soleil.json +2 -8
- package/src/modes/interactive/theme/defaults/light-sunset.json +2 -9
- package/src/modes/interactive/theme/defaults/light-synthwave.json +2 -9
- package/src/modes/interactive/theme/defaults/light-tokyo-night.json +2 -9
- package/src/modes/interactive/theme/defaults/light-wetland.json +2 -8
- package/src/modes/interactive/theme/defaults/light-zenith.json +2 -8
- package/src/modes/interactive/theme/defaults/limestone.json +2 -8
- package/src/modes/interactive/theme/defaults/mahogany.json +2 -9
- package/src/modes/interactive/theme/defaults/marble.json +2 -8
- package/src/modes/interactive/theme/defaults/obsidian.json +89 -88
- package/src/modes/interactive/theme/defaults/onyx.json +89 -88
- package/src/modes/interactive/theme/defaults/pearl.json +2 -8
- package/src/modes/interactive/theme/defaults/porcelain.json +89 -88
- package/src/modes/interactive/theme/defaults/quartz.json +2 -8
- package/src/modes/interactive/theme/defaults/sandstone.json +2 -8
- package/src/modes/interactive/theme/defaults/titanium.json +88 -87
- package/src/modes/interactive/theme/light.json +2 -8
- package/src/modes/interactive/theme/theme-schema.json +5 -0
- package/src/modes/interactive/theme/theme.ts +7 -0
- package/src/modes/interactive/types.ts +7 -1
- package/src/modes/interactive/utils/ui-helpers.ts +20 -0
- package/src/prompts/system/system-prompt.md +88 -78
- package/src/prompts/tools/python.md +39 -2
- package/src/prompts/tools/task.md +8 -13
package/src/core/tools/python.ts
CHANGED
|
@@ -2,10 +2,10 @@ import { relative, resolve, sep } from "node:path";
|
|
|
2
2
|
import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
|
|
3
3
|
import type { ImageContent } from "@oh-my-pi/pi-ai";
|
|
4
4
|
import type { Component } from "@oh-my-pi/pi-tui";
|
|
5
|
-
import { Text, truncateToWidth } from "@oh-my-pi/pi-tui";
|
|
5
|
+
import { Text, truncateToWidth, visibleWidth } from "@oh-my-pi/pi-tui";
|
|
6
6
|
import { type Static, Type } from "@sinclair/typebox";
|
|
7
7
|
import { truncateToVisualLines } from "../../modes/interactive/components/visual-truncate";
|
|
8
|
-
import type
|
|
8
|
+
import { highlightCode, type Theme } from "../../modes/interactive/theme/theme";
|
|
9
9
|
import pythonDescription from "../../prompts/tools/python.md" with { type: "text" };
|
|
10
10
|
import type { RenderResultOptions } from "../custom-tools/types";
|
|
11
11
|
import { renderPromptTemplate } from "../prompt-templates";
|
|
@@ -39,15 +39,26 @@ function groupPreludeHelpers(helpers: PreludeHelper[]): PreludeCategory[] {
|
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
export const pythonSchema = Type.Object({
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
42
|
+
cells: Type.Array(
|
|
43
|
+
Type.Object({
|
|
44
|
+
code: Type.String({
|
|
45
|
+
description:
|
|
46
|
+
"Python code for this cell. Keep it focused (imports, helper, test, use). No narrative text—put explanations in the assistant message or in the cell title.",
|
|
47
|
+
}),
|
|
48
|
+
title: Type.Optional(
|
|
49
|
+
Type.String({ description: "Short label for the cell (e.g., 'imports', 'parse helper')." }),
|
|
50
|
+
),
|
|
51
|
+
}),
|
|
52
|
+
{
|
|
53
|
+
description:
|
|
54
|
+
"Python cells to execute sequentially. Each cell runs in the same kernel—imports and variables persist. Keep cells small: one logical step each (import, define, test, use). If a cell fails, fix only that cell; earlier cells' state remains.",
|
|
55
|
+
},
|
|
46
56
|
),
|
|
57
|
+
timeoutMs: Type.Optional(Type.Number({ description: "Timeout in milliseconds (default: 30000)" })),
|
|
58
|
+
cwd: Type.Optional(Type.String({ description: "Working directory for the command (default: current directory)" })),
|
|
47
59
|
reset: Type.Optional(Type.Boolean({ description: "Restart the kernel before executing this code" })),
|
|
48
60
|
});
|
|
49
|
-
|
|
50
|
-
export type PythonToolParams = { code: string; timeout?: number; workdir?: string; reset?: boolean };
|
|
61
|
+
export type PythonToolParams = Static<typeof pythonSchema>;
|
|
51
62
|
|
|
52
63
|
export type PythonToolResult = {
|
|
53
64
|
content: Array<{ type: "text"; text: string }>;
|
|
@@ -56,7 +67,19 @@ export type PythonToolResult = {
|
|
|
56
67
|
|
|
57
68
|
export type PythonProxyExecutor = (params: PythonToolParams, signal?: AbortSignal) => Promise<PythonToolResult>;
|
|
58
69
|
|
|
70
|
+
export interface PythonCellResult {
|
|
71
|
+
index: number;
|
|
72
|
+
title?: string;
|
|
73
|
+
code: string;
|
|
74
|
+
output: string;
|
|
75
|
+
status: "pending" | "running" | "complete" | "error";
|
|
76
|
+
durationMs?: number;
|
|
77
|
+
exitCode?: number;
|
|
78
|
+
statusEvents?: PythonStatusEvent[];
|
|
79
|
+
}
|
|
80
|
+
|
|
59
81
|
export interface PythonToolDetails {
|
|
82
|
+
cells?: PythonCellResult[];
|
|
60
83
|
truncation?: TruncationResult;
|
|
61
84
|
fullOutputPath?: string;
|
|
62
85
|
fullOutput?: string;
|
|
@@ -151,7 +174,7 @@ export class PythonTool implements AgentTool<typeof pythonSchema> {
|
|
|
151
174
|
throw new Error("Python tool requires a session when not using proxy executor");
|
|
152
175
|
}
|
|
153
176
|
|
|
154
|
-
const {
|
|
177
|
+
const { cells, timeoutMs = 30000, cwd, reset } = params;
|
|
155
178
|
const controller = new AbortController();
|
|
156
179
|
const onAbort = () => controller.abort();
|
|
157
180
|
signal?.addEventListener("abort", onAbort, { once: true });
|
|
@@ -161,7 +184,7 @@ export class PythonTool implements AgentTool<typeof pythonSchema> {
|
|
|
161
184
|
throw new Error("Aborted");
|
|
162
185
|
}
|
|
163
186
|
|
|
164
|
-
const commandCwd =
|
|
187
|
+
const commandCwd = cwd ? resolveToCwd(cwd, this.session.cwd) : this.session.cwd;
|
|
165
188
|
let cwdStat: Awaited<ReturnType<Bun.BunFile["stat"]>>;
|
|
166
189
|
try {
|
|
167
190
|
cwdStat = await Bun.file(commandCwd).stat();
|
|
@@ -177,89 +200,185 @@ export class PythonTool implements AgentTool<typeof pythonSchema> {
|
|
|
177
200
|
let tailBytes = 0;
|
|
178
201
|
const jsonOutputs: unknown[] = [];
|
|
179
202
|
const images: ImageContent[] = [];
|
|
203
|
+
const statusEvents: PythonStatusEvent[] = [];
|
|
204
|
+
|
|
205
|
+
const cellResults: PythonCellResult[] = cells.map((cell, index) => ({
|
|
206
|
+
index,
|
|
207
|
+
title: cell.title,
|
|
208
|
+
code: cell.code,
|
|
209
|
+
output: "",
|
|
210
|
+
status: "pending",
|
|
211
|
+
}));
|
|
212
|
+
const cellOutputs: string[] = [];
|
|
213
|
+
let lastFullOutputPath: string | undefined;
|
|
214
|
+
|
|
215
|
+
const appendTail = (text: string) => {
|
|
216
|
+
if (!text) return;
|
|
217
|
+
const chunkBytes = Buffer.byteLength(text, "utf-8");
|
|
218
|
+
tailChunks.push({ text, bytes: chunkBytes });
|
|
219
|
+
tailBytes += chunkBytes;
|
|
220
|
+
while (tailBytes > maxTailBytes && tailChunks.length > 1) {
|
|
221
|
+
const removed = tailChunks.shift();
|
|
222
|
+
if (removed) {
|
|
223
|
+
tailBytes -= removed.bytes;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
const buildUpdateDetails = (truncation?: TruncationResult): PythonToolDetails => {
|
|
229
|
+
const details: PythonToolDetails = {
|
|
230
|
+
cells: cellResults.map((cell) => ({
|
|
231
|
+
...cell,
|
|
232
|
+
statusEvents: cell.statusEvents ? [...cell.statusEvents] : undefined,
|
|
233
|
+
})),
|
|
234
|
+
};
|
|
235
|
+
if (truncation) {
|
|
236
|
+
details.truncation = truncation;
|
|
237
|
+
}
|
|
238
|
+
if (lastFullOutputPath) {
|
|
239
|
+
details.fullOutputPath = lastFullOutputPath;
|
|
240
|
+
}
|
|
241
|
+
if (jsonOutputs.length > 0) {
|
|
242
|
+
details.jsonOutputs = jsonOutputs;
|
|
243
|
+
}
|
|
244
|
+
if (images.length > 0) {
|
|
245
|
+
details.images = images;
|
|
246
|
+
}
|
|
247
|
+
if (statusEvents.length > 0) {
|
|
248
|
+
details.statusEvents = statusEvents;
|
|
249
|
+
}
|
|
250
|
+
return details;
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
const pushUpdate = () => {
|
|
254
|
+
if (!onUpdate) return;
|
|
255
|
+
const tailText = tailChunks.map((entry) => entry.text).join("");
|
|
256
|
+
const truncation = truncateTail(tailText);
|
|
257
|
+
onUpdate({
|
|
258
|
+
content: [{ type: "text", text: truncation.content || "" }],
|
|
259
|
+
details: buildUpdateDetails(truncation.truncated ? truncation : undefined),
|
|
260
|
+
});
|
|
261
|
+
};
|
|
180
262
|
|
|
181
263
|
const sessionFile = this.session.getSessionFile?.() ?? undefined;
|
|
182
|
-
const sessionId = sessionFile ? `session:${sessionFile}:
|
|
183
|
-
const
|
|
264
|
+
const sessionId = sessionFile ? `session:${sessionFile}:cwd:${commandCwd}` : `cwd:${commandCwd}`;
|
|
265
|
+
const baseExecutorOptions: Omit<PythonExecutorOptions, "reset"> = {
|
|
184
266
|
cwd: commandCwd,
|
|
185
267
|
timeoutMs,
|
|
186
268
|
signal: controller.signal,
|
|
187
269
|
sessionId,
|
|
188
270
|
kernelMode: this.session.settings?.getPythonKernelMode?.() ?? "session",
|
|
189
271
|
useSharedGateway: this.session.settings?.getPythonSharedGateway?.() ?? true,
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
272
|
+
sessionFile: sessionFile ?? undefined,
|
|
273
|
+
};
|
|
274
|
+
|
|
275
|
+
for (let i = 0; i < cells.length; i++) {
|
|
276
|
+
const cell = cells[i];
|
|
277
|
+
const isFirstCell = i === 0;
|
|
278
|
+
const cellResult = cellResults[i];
|
|
279
|
+
cellResult.status = "running";
|
|
280
|
+
cellResult.output = "";
|
|
281
|
+
cellResult.statusEvents = undefined;
|
|
282
|
+
cellResult.exitCode = undefined;
|
|
283
|
+
cellResult.durationMs = undefined;
|
|
284
|
+
pushUpdate();
|
|
285
|
+
|
|
286
|
+
const executorOptions: PythonExecutorOptions = {
|
|
287
|
+
...baseExecutorOptions,
|
|
288
|
+
reset: isFirstCell ? reset : false,
|
|
289
|
+
};
|
|
290
|
+
|
|
291
|
+
const startTime = Date.now();
|
|
292
|
+
const result = await executePython(cell.code, executorOptions);
|
|
293
|
+
const durationMs = Date.now() - startTime;
|
|
294
|
+
|
|
295
|
+
const cellStatusEvents: PythonStatusEvent[] = [];
|
|
296
|
+
for (const output of result.displayOutputs) {
|
|
297
|
+
if (output.type === "json") {
|
|
298
|
+
jsonOutputs.push(output.data);
|
|
200
299
|
}
|
|
201
|
-
if (
|
|
202
|
-
|
|
203
|
-
const truncation = truncateTail(tailText);
|
|
204
|
-
onUpdate({
|
|
205
|
-
content: [{ type: "text", text: truncation.content || "" }],
|
|
206
|
-
details: truncation.truncated ? { truncation } : undefined,
|
|
207
|
-
});
|
|
300
|
+
if (output.type === "image") {
|
|
301
|
+
images.push({ type: "image", data: output.data, mimeType: output.mimeType });
|
|
208
302
|
}
|
|
209
|
-
|
|
210
|
-
|
|
303
|
+
if (output.type === "status") {
|
|
304
|
+
statusEvents.push(output.event);
|
|
305
|
+
cellStatusEvents.push(output.event);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
211
308
|
|
|
212
|
-
|
|
309
|
+
if (result.fullOutputPath) {
|
|
310
|
+
lastFullOutputPath = result.fullOutputPath;
|
|
311
|
+
}
|
|
213
312
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
313
|
+
const cellOutput = result.output.trim();
|
|
314
|
+
cellResult.output = cellOutput;
|
|
315
|
+
cellResult.exitCode = result.exitCode;
|
|
316
|
+
cellResult.durationMs = durationMs;
|
|
317
|
+
cellResult.statusEvents = cellStatusEvents.length > 0 ? cellStatusEvents : undefined;
|
|
318
|
+
|
|
319
|
+
let combinedCellOutput = "";
|
|
320
|
+
if (cells.length > 1) {
|
|
321
|
+
const cellHeader = `[${i + 1}/${cells.length}]`;
|
|
322
|
+
const cellTitle = cell.title ? ` ${cell.title}` : "";
|
|
323
|
+
if (cellOutput) {
|
|
324
|
+
combinedCellOutput = `${cellHeader}${cellTitle}\n${cellOutput}`;
|
|
325
|
+
} else {
|
|
326
|
+
combinedCellOutput = `${cellHeader}${cellTitle} (ok)`;
|
|
327
|
+
}
|
|
328
|
+
cellOutputs.push(combinedCellOutput);
|
|
329
|
+
} else if (cellOutput) {
|
|
330
|
+
combinedCellOutput = cellOutput;
|
|
331
|
+
cellOutputs.push(combinedCellOutput);
|
|
218
332
|
}
|
|
219
|
-
|
|
220
|
-
|
|
333
|
+
|
|
334
|
+
if (combinedCellOutput) {
|
|
335
|
+
const prefix = cellOutputs.length > 1 ? "\n\n" : "";
|
|
336
|
+
appendTail(`${prefix}${combinedCellOutput}`);
|
|
221
337
|
}
|
|
222
|
-
|
|
223
|
-
|
|
338
|
+
|
|
339
|
+
if (result.cancelled) {
|
|
340
|
+
cellResult.status = "error";
|
|
341
|
+
pushUpdate();
|
|
342
|
+
const errorMsg = result.output || "Command aborted";
|
|
343
|
+
throw new Error(cells.length > 1 ? `Cell ${i + 1} aborted: ${errorMsg}` : errorMsg);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
if (result.exitCode !== 0 && result.exitCode !== undefined) {
|
|
347
|
+
cellResult.status = "error";
|
|
348
|
+
pushUpdate();
|
|
349
|
+
const combinedOutput = cellOutputs.join("\n\n");
|
|
350
|
+
throw new Error(
|
|
351
|
+
cells.length > 1
|
|
352
|
+
? `${combinedOutput}\n\nCell ${i + 1} failed (exit code ${result.exitCode}). Earlier cells succeeded—their state persists. Fix only cell ${i + 1}.`
|
|
353
|
+
: `${combinedOutput}\n\nCommand exited with code ${result.exitCode}`,
|
|
354
|
+
);
|
|
224
355
|
}
|
|
225
|
-
}
|
|
226
356
|
|
|
227
|
-
|
|
228
|
-
|
|
357
|
+
cellResult.status = "complete";
|
|
358
|
+
pushUpdate();
|
|
229
359
|
}
|
|
230
360
|
|
|
231
|
-
const
|
|
361
|
+
const combinedOutput = cellOutputs.join("\n\n");
|
|
362
|
+
const truncation = truncateTail(combinedOutput);
|
|
232
363
|
let outputText =
|
|
233
364
|
truncation.content || (jsonOutputs.length > 0 || images.length > 0 ? "(no text output)" : "(no output)");
|
|
234
|
-
|
|
365
|
+
|
|
366
|
+
const details: PythonToolDetails = {
|
|
367
|
+
cells: cellResults,
|
|
368
|
+
fullOutputPath: lastFullOutputPath,
|
|
369
|
+
jsonOutputs: jsonOutputs.length > 0 ? jsonOutputs : undefined,
|
|
370
|
+
images: images.length > 0 ? images : undefined,
|
|
371
|
+
statusEvents: statusEvents.length > 0 ? statusEvents : undefined,
|
|
372
|
+
};
|
|
235
373
|
|
|
236
374
|
if (truncation.truncated) {
|
|
237
|
-
details =
|
|
238
|
-
truncation,
|
|
239
|
-
fullOutputPath: result.fullOutputPath,
|
|
240
|
-
jsonOutputs: jsonOutputs,
|
|
241
|
-
images,
|
|
242
|
-
statusEvents: statusEvents.length > 0 ? statusEvents : undefined,
|
|
243
|
-
};
|
|
375
|
+
details.truncation = truncation;
|
|
244
376
|
outputText += formatTailTruncationNotice(truncation, {
|
|
245
|
-
fullOutputPath:
|
|
246
|
-
originalContent:
|
|
377
|
+
fullOutputPath: lastFullOutputPath,
|
|
378
|
+
originalContent: combinedOutput,
|
|
247
379
|
});
|
|
248
380
|
}
|
|
249
381
|
|
|
250
|
-
if (!details && (jsonOutputs.length > 0 || images.length > 0 || statusEvents.length > 0)) {
|
|
251
|
-
details = {
|
|
252
|
-
jsonOutputs: jsonOutputs.length > 0 ? jsonOutputs : undefined,
|
|
253
|
-
images: images.length > 0 ? images : undefined,
|
|
254
|
-
statusEvents: statusEvents.length > 0 ? statusEvents : undefined,
|
|
255
|
-
};
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
if (result.exitCode !== 0 && result.exitCode !== undefined) {
|
|
259
|
-
outputText += `\n\nCommand exited with code ${result.exitCode}`;
|
|
260
|
-
throw new Error(outputText);
|
|
261
|
-
}
|
|
262
|
-
|
|
263
382
|
return { content: [{ type: "text", text: outputText }], details };
|
|
264
383
|
} finally {
|
|
265
384
|
signal?.removeEventListener("abort", onAbort);
|
|
@@ -268,9 +387,9 @@ export class PythonTool implements AgentTool<typeof pythonSchema> {
|
|
|
268
387
|
}
|
|
269
388
|
|
|
270
389
|
interface PythonRenderArgs {
|
|
271
|
-
code?: string
|
|
390
|
+
cells?: Array<{ code: string; title?: string }>;
|
|
272
391
|
timeout?: number;
|
|
273
|
-
|
|
392
|
+
cwd?: string;
|
|
274
393
|
}
|
|
275
394
|
|
|
276
395
|
interface PythonRenderContext {
|
|
@@ -600,13 +719,175 @@ function renderStatusEvents(events: PythonStatusEvent[], theme: Theme, expanded:
|
|
|
600
719
|
return lines;
|
|
601
720
|
}
|
|
602
721
|
|
|
722
|
+
function applyCellBackground(line: string, width: number, bgFn?: (text: string) => string): string {
|
|
723
|
+
if (!bgFn) return line;
|
|
724
|
+
if (width <= 0) return bgFn(line);
|
|
725
|
+
const paddingNeeded = Math.max(0, width - visibleWidth(line));
|
|
726
|
+
const padded = line + " ".repeat(paddingNeeded);
|
|
727
|
+
return bgFn(padded);
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
function highlightPythonCode(code?: string): string[] {
|
|
731
|
+
return highlightCode(code ?? "", "python");
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
function formatCellStatus(cell: PythonCellResult, ui: ToolUIKit, spinnerFrame?: number): string | undefined {
|
|
735
|
+
switch (cell.status) {
|
|
736
|
+
case "pending":
|
|
737
|
+
return `${ui.statusIcon("pending")} ${ui.theme.fg("muted", "pending")}`;
|
|
738
|
+
case "running":
|
|
739
|
+
return `${ui.statusIcon("running", spinnerFrame)} ${ui.theme.fg("muted", "running")}`;
|
|
740
|
+
case "complete":
|
|
741
|
+
return ui.statusIcon("success");
|
|
742
|
+
case "error":
|
|
743
|
+
return ui.statusIcon("error");
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
function formatCellHeader(
|
|
748
|
+
cell: PythonCellResult,
|
|
749
|
+
index: number,
|
|
750
|
+
total: number,
|
|
751
|
+
ui: ToolUIKit,
|
|
752
|
+
spinnerFrame?: number,
|
|
753
|
+
workdirLabel?: string,
|
|
754
|
+
): string {
|
|
755
|
+
const indexLabel = ui.theme.fg("accent", `[${index + 1}/${total}]`);
|
|
756
|
+
const title = cell.title ? ` ${cell.title}` : "";
|
|
757
|
+
const metaParts: string[] = [];
|
|
758
|
+
if (workdirLabel) {
|
|
759
|
+
metaParts.push(ui.theme.fg("dim", workdirLabel));
|
|
760
|
+
}
|
|
761
|
+
if (cell.durationMs !== undefined) {
|
|
762
|
+
metaParts.push(ui.theme.fg("dim", `(${ui.formatDuration(cell.durationMs)})`));
|
|
763
|
+
}
|
|
764
|
+
const statusLabel = formatCellStatus(cell, ui, spinnerFrame);
|
|
765
|
+
if (statusLabel) {
|
|
766
|
+
metaParts.push(statusLabel);
|
|
767
|
+
}
|
|
768
|
+
const meta = metaParts.length > 0 ? ` ${metaParts.join(ui.theme.fg("dim", ui.theme.sep.dot))}` : "";
|
|
769
|
+
return `${indexLabel}${title}${meta}`;
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
function formatCellOutputLines(
|
|
773
|
+
cell: PythonCellResult,
|
|
774
|
+
expanded: boolean,
|
|
775
|
+
previewLines: number,
|
|
776
|
+
theme: Theme,
|
|
777
|
+
): { lines: string[]; hiddenCount: number } {
|
|
778
|
+
const rawLines = cell.output ? cell.output.split("\n") : [];
|
|
779
|
+
const displayLines = expanded ? rawLines : rawLines.slice(-previewLines);
|
|
780
|
+
const hiddenCount = rawLines.length - displayLines.length;
|
|
781
|
+
const outputLines = displayLines.map((line) => theme.fg("toolOutput", line));
|
|
782
|
+
|
|
783
|
+
if (outputLines.length === 0) {
|
|
784
|
+
return { lines: [], hiddenCount: 0 };
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
return { lines: outputLines, hiddenCount };
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
function renderCellBlock(
|
|
791
|
+
cell: PythonCellResult,
|
|
792
|
+
index: number,
|
|
793
|
+
total: number,
|
|
794
|
+
ui: ToolUIKit,
|
|
795
|
+
options: {
|
|
796
|
+
expanded: boolean;
|
|
797
|
+
previewLines: number;
|
|
798
|
+
spinnerFrame?: number;
|
|
799
|
+
showOutput: boolean;
|
|
800
|
+
workdirLabel?: string;
|
|
801
|
+
width: number;
|
|
802
|
+
bgFn?: (text: string) => string;
|
|
803
|
+
},
|
|
804
|
+
): string[] {
|
|
805
|
+
const { expanded, previewLines, spinnerFrame, showOutput, workdirLabel, width, bgFn } = options;
|
|
806
|
+
const h = ui.theme.boxSharp.horizontal;
|
|
807
|
+
const v = ui.theme.boxSharp.vertical;
|
|
808
|
+
const cap = h.repeat(3);
|
|
809
|
+
const border = (text: string) => ui.theme.fg("dim", text);
|
|
810
|
+
const lineWidth = Math.max(0, width);
|
|
811
|
+
|
|
812
|
+
const buildBarLine = (leftChar: string, label?: string): string => {
|
|
813
|
+
const left = border(`${leftChar}${cap}`);
|
|
814
|
+
if (lineWidth <= 0) return left;
|
|
815
|
+
const rawLabel = label ? ` ${label} ` : " ";
|
|
816
|
+
const maxLabelWidth = Math.max(0, lineWidth - visibleWidth(left));
|
|
817
|
+
const trimmedLabel = truncateToWidth(rawLabel, maxLabelWidth, ui.theme.format.ellipsis);
|
|
818
|
+
const fillCount = Math.max(0, lineWidth - visibleWidth(left + trimmedLabel));
|
|
819
|
+
return `${left}${trimmedLabel}${border(h.repeat(fillCount))}`;
|
|
820
|
+
};
|
|
821
|
+
|
|
822
|
+
const lines: string[] = [];
|
|
823
|
+
lines.push(
|
|
824
|
+
applyCellBackground(
|
|
825
|
+
buildBarLine(ui.theme.boxSharp.topLeft, formatCellHeader(cell, index, total, ui, spinnerFrame, workdirLabel)),
|
|
826
|
+
lineWidth,
|
|
827
|
+
bgFn,
|
|
828
|
+
),
|
|
829
|
+
);
|
|
830
|
+
|
|
831
|
+
const codePrefix = border(`${v} `);
|
|
832
|
+
const codeWidth = Math.max(0, lineWidth - visibleWidth(codePrefix));
|
|
833
|
+
const codeLines = highlightPythonCode(cell.code);
|
|
834
|
+
for (const line of codeLines) {
|
|
835
|
+
const text = truncateToWidth(line, codeWidth, ui.theme.format.ellipsis);
|
|
836
|
+
lines.push(applyCellBackground(`${codePrefix}${text}`, lineWidth, bgFn));
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
const statusLines = renderStatusEvents(cell.statusEvents ?? [], ui.theme, expanded);
|
|
840
|
+
const outputContent = formatCellOutputLines(cell, expanded, previewLines, ui.theme);
|
|
841
|
+
const hasOutput = outputContent.lines.length > 0;
|
|
842
|
+
const hasStatus = statusLines.length > 0;
|
|
843
|
+
const showOutputSection = showOutput && (hasOutput || hasStatus);
|
|
844
|
+
|
|
845
|
+
if (showOutputSection) {
|
|
846
|
+
lines.push(
|
|
847
|
+
applyCellBackground(
|
|
848
|
+
buildBarLine(ui.theme.boxSharp.teeRight, ui.theme.fg("toolTitle", "Output")),
|
|
849
|
+
lineWidth,
|
|
850
|
+
bgFn,
|
|
851
|
+
),
|
|
852
|
+
);
|
|
853
|
+
|
|
854
|
+
for (const line of outputContent.lines) {
|
|
855
|
+
const text = truncateToWidth(line, codeWidth, ui.theme.format.ellipsis);
|
|
856
|
+
lines.push(applyCellBackground(`${codePrefix}${text}`, lineWidth, bgFn));
|
|
857
|
+
}
|
|
858
|
+
if (!expanded && outputContent.hiddenCount > 0) {
|
|
859
|
+
const hint = ui.theme.fg(
|
|
860
|
+
"dim",
|
|
861
|
+
`${ui.theme.format.ellipsis} ${outputContent.hiddenCount} more lines (ctrl+o to expand)`,
|
|
862
|
+
);
|
|
863
|
+
lines.push(
|
|
864
|
+
applyCellBackground(
|
|
865
|
+
`${codePrefix}${truncateToWidth(hint, codeWidth, ui.theme.format.ellipsis)}`,
|
|
866
|
+
lineWidth,
|
|
867
|
+
bgFn,
|
|
868
|
+
),
|
|
869
|
+
);
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
for (const line of statusLines) {
|
|
873
|
+
const text = truncateToWidth(line, codeWidth, ui.theme.format.ellipsis);
|
|
874
|
+
lines.push(applyCellBackground(`${codePrefix}${text}`, lineWidth, bgFn));
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
const bottomLeft = border(`${ui.theme.boxSharp.bottomLeft}${cap}`);
|
|
879
|
+
const bottomFillCount = Math.max(0, lineWidth - visibleWidth(bottomLeft));
|
|
880
|
+
const bottomLine = `${bottomLeft}${border(h.repeat(bottomFillCount))}`;
|
|
881
|
+
lines.push(applyCellBackground(bottomLine, lineWidth, bgFn));
|
|
882
|
+
return lines;
|
|
883
|
+
}
|
|
884
|
+
|
|
603
885
|
export const pythonToolRenderer = {
|
|
604
886
|
renderCall(args: PythonRenderArgs, uiTheme: Theme): Component {
|
|
605
887
|
const ui = new ToolUIKit(uiTheme);
|
|
606
|
-
const
|
|
607
|
-
const prompt = uiTheme.fg("accent", ">>>");
|
|
888
|
+
const cells = args.cells ?? [];
|
|
608
889
|
const cwd = process.cwd();
|
|
609
|
-
let displayWorkdir = args.
|
|
890
|
+
let displayWorkdir = args.cwd;
|
|
610
891
|
|
|
611
892
|
if (displayWorkdir) {
|
|
612
893
|
const resolvedCwd = resolve(cwd);
|
|
@@ -622,11 +903,44 @@ export const pythonToolRenderer = {
|
|
|
622
903
|
}
|
|
623
904
|
}
|
|
624
905
|
|
|
625
|
-
const
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
906
|
+
const workdirLabel = displayWorkdir ? `cd ${displayWorkdir}` : undefined;
|
|
907
|
+
if (cells.length === 0) {
|
|
908
|
+
const prompt = uiTheme.fg("accent", ">>>");
|
|
909
|
+
const prefix = workdirLabel ? `${uiTheme.fg("dim", `${workdirLabel} && `)}` : "";
|
|
910
|
+
const text = ui.title(`${prompt} ${prefix}${uiTheme.format.ellipsis}`);
|
|
911
|
+
return new Text(text, 0, 0);
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
return {
|
|
915
|
+
render: (width: number): string[] => {
|
|
916
|
+
const lines: string[] = [];
|
|
917
|
+
for (let i = 0; i < cells.length; i++) {
|
|
918
|
+
const cell = cells[i];
|
|
919
|
+
const cellResult: PythonCellResult = {
|
|
920
|
+
index: i,
|
|
921
|
+
title: cell.title,
|
|
922
|
+
code: cell.code,
|
|
923
|
+
output: "",
|
|
924
|
+
status: "pending",
|
|
925
|
+
};
|
|
926
|
+
lines.push(
|
|
927
|
+
...renderCellBlock(cellResult, i, cells.length, ui, {
|
|
928
|
+
expanded: true,
|
|
929
|
+
previewLines: PYTHON_DEFAULT_PREVIEW_LINES,
|
|
930
|
+
showOutput: false,
|
|
931
|
+
workdirLabel: i === 0 ? workdirLabel : undefined,
|
|
932
|
+
width,
|
|
933
|
+
bgFn: (text: string) => uiTheme.bg("toolPendingBg", text),
|
|
934
|
+
}),
|
|
935
|
+
);
|
|
936
|
+
if (i < cells.length - 1) {
|
|
937
|
+
lines.push("");
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
return lines;
|
|
941
|
+
},
|
|
942
|
+
invalidate: () => {},
|
|
943
|
+
};
|
|
630
944
|
},
|
|
631
945
|
|
|
632
946
|
renderResult(
|
|
@@ -642,7 +956,6 @@ export const pythonToolRenderer = {
|
|
|
642
956
|
const previewLines = renderContext?.previewLines ?? PYTHON_DEFAULT_PREVIEW_LINES;
|
|
643
957
|
const output = renderContext?.output ?? (result.content?.find((c) => c.type === "text")?.text ?? "").trim();
|
|
644
958
|
const fullOutput = details?.fullOutput;
|
|
645
|
-
const displayOutput = expanded ? (fullOutput ?? output) : output;
|
|
646
959
|
const showingFullOutput = expanded && fullOutput !== undefined;
|
|
647
960
|
|
|
648
961
|
const jsonOutputs = details?.jsonOutputs ?? [];
|
|
@@ -652,12 +965,6 @@ export const pythonToolRenderer = {
|
|
|
652
965
|
return [header, ...treeLines];
|
|
653
966
|
});
|
|
654
967
|
|
|
655
|
-
// Render status events
|
|
656
|
-
const statusEvents = details?.statusEvents ?? [];
|
|
657
|
-
const statusLines = renderStatusEvents(statusEvents, uiTheme, expanded);
|
|
658
|
-
|
|
659
|
-
const combinedOutput = [displayOutput, ...jsonLines].filter(Boolean).join("\n");
|
|
660
|
-
|
|
661
968
|
const truncation = details?.truncation;
|
|
662
969
|
const fullOutputPath = details?.fullOutputPath;
|
|
663
970
|
const timeoutSeconds = renderContext?.timeout;
|
|
@@ -685,12 +992,63 @@ export const pythonToolRenderer = {
|
|
|
685
992
|
}
|
|
686
993
|
}
|
|
687
994
|
|
|
995
|
+
const cellResults = details?.cells;
|
|
996
|
+
if (cellResults && cellResults.length > 0) {
|
|
997
|
+
return {
|
|
998
|
+
render: (width: number): string[] => {
|
|
999
|
+
const lines: string[] = [];
|
|
1000
|
+
for (let i = 0; i < cellResults.length; i++) {
|
|
1001
|
+
const cell = cellResults[i];
|
|
1002
|
+
const showOutput = cell.status !== "pending";
|
|
1003
|
+
const bgColor =
|
|
1004
|
+
cell.status === "error"
|
|
1005
|
+
? "toolErrorBg"
|
|
1006
|
+
: cell.status === "complete"
|
|
1007
|
+
? "toolSuccessBg"
|
|
1008
|
+
: "toolPendingBg";
|
|
1009
|
+
lines.push(
|
|
1010
|
+
...renderCellBlock(cell, i, cellResults.length, ui, {
|
|
1011
|
+
expanded,
|
|
1012
|
+
previewLines,
|
|
1013
|
+
spinnerFrame: options.spinnerFrame,
|
|
1014
|
+
showOutput,
|
|
1015
|
+
width,
|
|
1016
|
+
bgFn: (text: string) => uiTheme.bg(bgColor, text),
|
|
1017
|
+
}),
|
|
1018
|
+
);
|
|
1019
|
+
if (i < cellResults.length - 1) {
|
|
1020
|
+
lines.push("");
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
if (jsonLines.length > 0) {
|
|
1024
|
+
if (lines.length > 0) {
|
|
1025
|
+
lines.push("");
|
|
1026
|
+
}
|
|
1027
|
+
lines.push(...jsonLines);
|
|
1028
|
+
}
|
|
1029
|
+
if (timeoutLine) {
|
|
1030
|
+
lines.push(timeoutLine);
|
|
1031
|
+
}
|
|
1032
|
+
if (warningLine) {
|
|
1033
|
+
lines.push(warningLine);
|
|
1034
|
+
}
|
|
1035
|
+
return lines;
|
|
1036
|
+
},
|
|
1037
|
+
invalidate: () => {},
|
|
1038
|
+
};
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
const displayOutput = expanded ? (fullOutput ?? output) : output;
|
|
1042
|
+
const combinedOutput = [displayOutput, ...jsonLines].filter(Boolean).join("\n");
|
|
1043
|
+
|
|
1044
|
+
const statusEvents = details?.statusEvents ?? [];
|
|
1045
|
+
const statusLines = renderStatusEvents(statusEvents, uiTheme, expanded);
|
|
1046
|
+
|
|
688
1047
|
if (!combinedOutput && statusLines.length === 0) {
|
|
689
1048
|
const lines = [timeoutLine, warningLine].filter(Boolean) as string[];
|
|
690
1049
|
return new Text(lines.join("\n"), 0, 0);
|
|
691
1050
|
}
|
|
692
1051
|
|
|
693
|
-
// If only status events (no text output), show them directly
|
|
694
1052
|
if (!combinedOutput && statusLines.length > 0) {
|
|
695
1053
|
const lines = [...statusLines, timeoutLine, warningLine].filter(Boolean) as string[];
|
|
696
1054
|
return new Text(lines.join("\n"), 0, 0);
|
|
@@ -733,7 +1091,6 @@ export const pythonToolRenderer = {
|
|
|
733
1091
|
outputLines.push(truncateToWidth(skippedLine, width, uiTheme.fg("dim", uiTheme.format.ellipsis)));
|
|
734
1092
|
}
|
|
735
1093
|
outputLines.push(...cachedLines);
|
|
736
|
-
// Add status events below the output
|
|
737
1094
|
for (const statusLine of statusLines) {
|
|
738
1095
|
outputLines.push(truncateToWidth(statusLine, width, uiTheme.fg("dim", uiTheme.format.ellipsis)));
|
|
739
1096
|
}
|
|
@@ -752,4 +1109,6 @@ export const pythonToolRenderer = {
|
|
|
752
1109
|
},
|
|
753
1110
|
};
|
|
754
1111
|
},
|
|
1112
|
+
mergeCallAndResult: true,
|
|
1113
|
+
inline: true,
|
|
755
1114
|
};
|
package/src/core/tools/read.ts
CHANGED
|
@@ -223,11 +223,11 @@ async function listCandidateFiles(
|
|
|
223
223
|
const { stdout, stderr, exitCode } = await runFd(fdPath, args, signal);
|
|
224
224
|
const output = stdout.trim();
|
|
225
225
|
|
|
226
|
-
if (exitCode !== 0 && !output) {
|
|
227
|
-
return { files: [], truncated: false, error: stderr.trim() || `fd exited with code ${exitCode ?? -1}` };
|
|
228
|
-
}
|
|
229
|
-
|
|
230
226
|
if (!output) {
|
|
227
|
+
// fd exit codes: 0 = found, 1 = no matches, other = error
|
|
228
|
+
if (exitCode !== 0 && exitCode !== 1) {
|
|
229
|
+
return { files: [], truncated: false, error: stderr.trim() || `fd failed (exit ${exitCode})` };
|
|
230
|
+
}
|
|
231
231
|
return { files: [], truncated: false };
|
|
232
232
|
}
|
|
233
233
|
|