@oh-my-pi/pi-coding-agent 14.5.12 → 14.5.13
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 +32 -0
- package/package.json +18 -10
- package/src/cli/jupyter-cli.ts +1 -1
- package/src/config/model-equivalence.ts +49 -16
- package/src/config/model-registry.ts +100 -25
- package/src/config/model-resolver.ts +29 -15
- package/src/config/settings-schema.ts +20 -6
- package/src/config/settings.ts +9 -8
- package/src/config.ts +9 -0
- package/src/eval/backend.ts +43 -0
- package/src/eval/eval.lark +43 -0
- package/src/eval/index.ts +5 -0
- package/src/eval/js/context-manager.ts +717 -0
- package/src/eval/js/executor.ts +131 -0
- package/src/eval/js/index.ts +46 -0
- package/src/eval/js/prelude.ts +2 -0
- package/src/eval/js/prelude.txt +84 -0
- package/src/eval/js/tool-bridge.ts +124 -0
- package/src/eval/parse.ts +337 -0
- package/src/{ipy → eval/py}/executor.ts +2 -180
- package/src/{ipy → eval/py}/gateway-coordinator.ts +2 -2
- package/src/eval/py/index.ts +58 -0
- package/src/{ipy → eval/py}/kernel.ts +5 -41
- package/src/{ipy → eval/py}/prelude.py +39 -227
- package/src/eval/types.ts +48 -0
- package/src/export/html/template.generated.ts +1 -1
- package/src/export/html/template.js +8 -10
- package/src/extensibility/extensions/types.ts +2 -3
- package/src/internal-urls/docs-index.generated.ts +5 -5
- package/src/lsp/client.ts +9 -0
- package/src/lsp/index.ts +395 -0
- package/src/lsp/types.ts +15 -4
- package/src/main.ts +25 -14
- package/src/mcp/oauth-flow.ts +1 -1
- package/src/memories/index.ts +1 -1
- package/src/modes/acp/acp-event-mapper.ts +1 -1
- package/src/modes/components/{python-execution.ts → eval-execution.ts} +11 -4
- package/src/modes/components/login-dialog.ts +1 -1
- package/src/modes/components/oauth-selector.ts +2 -1
- package/src/modes/components/tool-execution.ts +3 -4
- package/src/modes/controllers/command-controller.ts +28 -8
- package/src/modes/controllers/input-controller.ts +4 -4
- package/src/modes/controllers/selector-controller.ts +2 -1
- package/src/modes/interactive-mode.ts +4 -5
- package/src/modes/types.ts +3 -3
- package/src/modes/utils/ui-helpers.ts +2 -2
- package/src/prompts/system/system-prompt.md +3 -3
- package/src/prompts/tools/eval.md +92 -0
- package/src/prompts/tools/lsp.md +7 -3
- package/src/sdk.ts +45 -31
- package/src/session/agent-session.ts +42 -42
- package/src/session/messages.ts +1 -1
- package/src/slash-commands/builtin-registry.ts +1 -1
- package/src/system-prompt.ts +34 -66
- package/src/task/executor.ts +5 -9
- package/src/tools/browser/launch.ts +22 -0
- package/src/tools/browser/registry.ts +25 -244
- package/src/tools/browser/render.ts +1 -1
- package/src/tools/browser/tab-protocol.ts +101 -0
- package/src/tools/browser/tab-supervisor.ts +429 -0
- package/src/tools/browser/tab-worker-entry.ts +21 -0
- package/src/tools/browser/tab-worker.ts +1006 -0
- package/src/tools/browser.ts +12 -29
- package/src/tools/checkpoint.ts +2 -2
- package/src/tools/{python.ts → eval.ts} +324 -315
- package/src/tools/exit-plan-mode.ts +1 -1
- package/src/tools/index.ts +62 -100
- package/src/tools/read.ts +0 -6
- package/src/tools/recipe/runners/pkg.ts +34 -32
- package/src/tools/renderers.ts +2 -2
- package/src/tools/resolve.ts +7 -2
- package/src/tools/todo-write.ts +0 -1
- package/src/tools/tool-timeouts.ts +2 -2
- package/src/utils/markit.ts +15 -7
- package/src/utils/tools-manager.ts +5 -5
- package/src/web/search/index.ts +5 -5
- package/src/web/search/provider.ts +121 -39
- package/src/web/search/providers/gemini.ts +2 -2
- package/src/web/search/render.ts +2 -2
- package/src/ipy/modules.ts +0 -144
- package/src/prompts/tools/python.md +0 -57
- package/src/tools/browser/vm.ts +0 -792
- /package/src/{ipy → eval/py}/cancellation.ts +0 -0
- /package/src/{ipy → eval/py}/prelude.ts +0 -0
- /package/src/{ipy → eval/py}/runtime.ts +0 -0
|
@@ -1,89 +1,42 @@
|
|
|
1
|
-
import * as path from "node:path";
|
|
2
1
|
import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
|
|
3
2
|
import type { ImageContent } from "@oh-my-pi/pi-ai";
|
|
4
3
|
import type { Component } from "@oh-my-pi/pi-tui";
|
|
5
4
|
import { Markdown, Text } from "@oh-my-pi/pi-tui";
|
|
6
|
-
import {
|
|
5
|
+
import { prompt } from "@oh-my-pi/pi-utils";
|
|
7
6
|
import { type Static, Type } from "@sinclair/typebox";
|
|
7
|
+
import { jsBackend, parseEvalInput, pythonBackend } from "../eval";
|
|
8
|
+
import type { ExecutorBackend } from "../eval/backend";
|
|
9
|
+
import evalGrammar from "../eval/eval.lark" with { type: "text" };
|
|
10
|
+
import type { ParsedEvalCell } from "../eval/parse";
|
|
11
|
+
import type { EvalCellResult, EvalLanguage, EvalStatusEvent, EvalToolDetails } from "../eval/types";
|
|
8
12
|
import type { RenderResultOptions } from "../extensibility/custom-tools/types";
|
|
9
|
-
import { executePython, getPreludeDocs, type PythonExecutorOptions, warmPythonEnvironment } from "../ipy/executor";
|
|
10
|
-
import type { PreludeHelper, PythonStatusEvent } from "../ipy/kernel";
|
|
11
13
|
import { truncateToVisualLines } from "../modes/components/visual-truncate";
|
|
12
14
|
import { getMarkdownTheme, type Theme } from "../modes/theme/theme";
|
|
13
|
-
import
|
|
15
|
+
import evalDescription from "../prompts/tools/eval.md" with { type: "text" };
|
|
14
16
|
import { DEFAULT_MAX_BYTES, OutputSink, type OutputSummary, TailBuffer } from "../session/streaming-output";
|
|
15
17
|
import { getTreeBranch, getTreeContinuePrefix, renderCodeCell } from "../tui";
|
|
16
|
-
import type
|
|
17
|
-
import { formatStyledTruncationWarning
|
|
18
|
+
import { resolveEvalBackends, type ToolSession } from ".";
|
|
19
|
+
import { formatStyledTruncationWarning } from "./output-meta";
|
|
18
20
|
import { formatTitle, replaceTabs, shortenPath, truncateToWidth, wrapBrackets } from "./render-utils";
|
|
19
21
|
import { ToolAbortError, ToolError } from "./tool-errors";
|
|
20
22
|
import { toolResult } from "./tool-result";
|
|
21
23
|
import { clampTimeout } from "./tool-timeouts";
|
|
22
24
|
|
|
23
|
-
export const
|
|
25
|
+
export const EVAL_DEFAULT_PREVIEW_LINES = 10;
|
|
24
26
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
function groupPreludeHelpers(helpers: PreludeHelper[]): PreludeCategory[] {
|
|
31
|
-
const categories: PreludeCategory[] = [];
|
|
32
|
-
const byName = new Map<string, PreludeHelper[]>();
|
|
33
|
-
for (const helper of helpers) {
|
|
34
|
-
let bucket = byName.get(helper.category);
|
|
35
|
-
if (!bucket) {
|
|
36
|
-
bucket = [];
|
|
37
|
-
byName.set(helper.category, bucket);
|
|
38
|
-
categories.push({ name: helper.category, functions: bucket });
|
|
39
|
-
}
|
|
40
|
-
bucket.push(helper);
|
|
41
|
-
}
|
|
42
|
-
return categories;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
export const pythonSchema = Type.Object({
|
|
46
|
-
cells: Type.Array(
|
|
47
|
-
Type.Object({
|
|
48
|
-
code: Type.String({ description: "python code", examples: ["print('hello')", "import json"] }),
|
|
49
|
-
title: Type.String({ description: "cell label", examples: ["imports", "helper"] }),
|
|
50
|
-
}),
|
|
51
|
-
{ description: "cells to execute" },
|
|
52
|
-
),
|
|
53
|
-
timeout: Type.Optional(Type.Number({ description: "timeout in seconds", default: 30 })),
|
|
54
|
-
reset: Type.Optional(Type.Boolean({ description: "restart kernel" })),
|
|
27
|
+
export const evalSchema = Type.Object({
|
|
28
|
+
input: Type.String({
|
|
29
|
+
description: "atom-style eval input containing CELL sections, fenced code, and optional RESET directive",
|
|
30
|
+
}),
|
|
55
31
|
});
|
|
56
|
-
export type
|
|
32
|
+
export type EvalToolParams = Static<typeof evalSchema>;
|
|
57
33
|
|
|
58
|
-
export type
|
|
34
|
+
export type EvalToolResult = {
|
|
59
35
|
content: Array<{ type: "text"; text: string }>;
|
|
60
|
-
details:
|
|
36
|
+
details: EvalToolDetails | undefined;
|
|
61
37
|
};
|
|
62
38
|
|
|
63
|
-
export type
|
|
64
|
-
|
|
65
|
-
export interface PythonCellResult {
|
|
66
|
-
index: number;
|
|
67
|
-
title?: string;
|
|
68
|
-
code: string;
|
|
69
|
-
output: string;
|
|
70
|
-
status: "pending" | "running" | "complete" | "error";
|
|
71
|
-
durationMs?: number;
|
|
72
|
-
exitCode?: number;
|
|
73
|
-
statusEvents?: PythonStatusEvent[];
|
|
74
|
-
hasMarkdown?: boolean;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
export interface PythonToolDetails {
|
|
78
|
-
cells?: PythonCellResult[];
|
|
79
|
-
jsonOutputs?: unknown[];
|
|
80
|
-
images?: ImageContent[];
|
|
81
|
-
/** Structured status events from prelude helpers */
|
|
82
|
-
statusEvents?: PythonStatusEvent[];
|
|
83
|
-
isError?: boolean;
|
|
84
|
-
/** Structured output metadata for notices */
|
|
85
|
-
meta?: OutputMeta;
|
|
86
|
-
}
|
|
39
|
+
export type EvalProxyExecutor = (params: EvalToolParams, signal?: AbortSignal) => Promise<EvalToolResult>;
|
|
87
40
|
|
|
88
41
|
function formatJsonScalar(value: unknown): string {
|
|
89
42
|
if (value === null) return "null";
|
|
@@ -129,61 +82,189 @@ function renderJsonTree(value: unknown, theme: Theme, expanded: boolean, maxDept
|
|
|
129
82
|
return renderNode(value, "", 0, true);
|
|
130
83
|
}
|
|
131
84
|
|
|
132
|
-
export
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
85
|
+
export interface EvalToolDescriptionOptions {
|
|
86
|
+
py?: boolean;
|
|
87
|
+
js?: boolean;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function getEvalToolDescription(options: EvalToolDescriptionOptions = {}): string {
|
|
91
|
+
const py = options.py ?? true;
|
|
92
|
+
const js = options.js ?? true;
|
|
93
|
+
return prompt.render(evalDescription, { py, js });
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export interface EvalToolOptions {
|
|
97
|
+
proxyExecutor?: EvalProxyExecutor;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
interface ResolvedBackend {
|
|
101
|
+
backend: ExecutorBackend;
|
|
102
|
+
fallback: boolean;
|
|
103
|
+
notice?: string;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
interface ResolvedEvalCell {
|
|
107
|
+
index: number;
|
|
108
|
+
title?: string;
|
|
109
|
+
code: string;
|
|
110
|
+
timeoutMs: number;
|
|
111
|
+
reset: boolean;
|
|
112
|
+
resolved: ResolvedBackend;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function uniqueEvalLanguages(cells: ResolvedEvalCell[]): EvalLanguage[] {
|
|
116
|
+
return [...new Set(cells.map(cell => cell.resolved.backend.id))];
|
|
136
117
|
}
|
|
137
118
|
|
|
138
|
-
|
|
139
|
-
|
|
119
|
+
function detailsNotice(cells: ResolvedEvalCell[]): string | undefined {
|
|
120
|
+
const notices = [
|
|
121
|
+
...new Set(cells.map(cell => cell.resolved.notice).filter((notice): notice is string => Boolean(notice))),
|
|
122
|
+
];
|
|
123
|
+
return notices.length > 0 ? notices.join(" ") : undefined;
|
|
140
124
|
}
|
|
141
125
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
126
|
+
function languageForHighlighter(language: EvalLanguage | undefined): "python" | "javascript" {
|
|
127
|
+
return language === "js" ? "javascript" : "python";
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function timeoutSecondsFromMs(timeoutMs: number): number {
|
|
131
|
+
return clampTimeout("eval", timeoutMs / 1000);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Best-effort language sniff for cells with no explicit `language`.
|
|
136
|
+
*
|
|
137
|
+
* Order:
|
|
138
|
+
* 1. Shebang on first line (`#!/usr/bin/env python`, `#!/usr/bin/env node`, etc.)
|
|
139
|
+
* 2. Strong syntactic markers unique to one language. We bias false negatives over
|
|
140
|
+
* false positives — anything ambiguous returns `undefined` and the caller falls
|
|
141
|
+
* back to the default-backend rules.
|
|
142
|
+
*/
|
|
143
|
+
function sniffLanguage(code: string): EvalLanguage | undefined {
|
|
144
|
+
const stripped = code.replace(/^\s+/, "");
|
|
145
|
+
if (stripped.startsWith("#!")) {
|
|
146
|
+
const firstLine = stripped.split("\n", 1)[0]!.toLowerCase();
|
|
147
|
+
if (/(\bpython\d?\b|\bipython\b)/.test(firstLine)) return "python";
|
|
148
|
+
if (/(\bnode\b|\bbun\b|\bdeno\b|\bjavascript\b|\bjs\b)/.test(firstLine)) return "js";
|
|
149
|
+
}
|
|
150
|
+
const jsMarkers =
|
|
151
|
+
/(^|\n)\s*(const|let|var|async\s+function|function\s*\*?\s*[\w$]*\s*\(|import\s+[^\n]+\sfrom\s|export\s+(default|const|let|function|class|async)|require\s*\(|console\.\w+\s*\(|=>|;\s*$)/m;
|
|
152
|
+
const pyMarkers =
|
|
153
|
+
/(^|\n)\s*(def\s+\w+\s*\(|from\s+[\w.]+\s+import|import\s+\w+(\s+as\s+\w+)?\s*$|class\s+\w+\s*[(:]|print\s*\(|elif\s+[^\n]*:|with\s+[^\n]+:\s*$|@[\w.]+\s*$)/m;
|
|
154
|
+
const hasJs = jsMarkers.test(code);
|
|
155
|
+
const hasPy = pyMarkers.test(code);
|
|
156
|
+
if (hasJs && !hasPy) return "js";
|
|
157
|
+
if (hasPy && !hasJs) return "python";
|
|
158
|
+
return undefined;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
async function resolveBackend(
|
|
162
|
+
session: ToolSession,
|
|
163
|
+
requested: EvalLanguage | undefined,
|
|
164
|
+
code: string,
|
|
165
|
+
): Promise<ResolvedBackend> {
|
|
166
|
+
const allowPy = (session.settings.get("eval.py") as boolean | undefined) ?? true;
|
|
167
|
+
const allowJs = (session.settings.get("eval.js") as boolean | undefined) ?? true;
|
|
168
|
+
|
|
169
|
+
if (requested === "python") {
|
|
170
|
+
if (!allowPy) throw new ToolError("Python backend is disabled (eval.py = false).");
|
|
171
|
+
if (!(await pythonBackend.isAvailable(session))) {
|
|
172
|
+
throw new ToolError(
|
|
173
|
+
'Python backend is unavailable in this session. Pass language: "js" or install the python kernel.',
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
return { backend: pythonBackend, fallback: false };
|
|
177
|
+
}
|
|
178
|
+
if (requested === "js") {
|
|
179
|
+
if (!allowJs) throw new ToolError("JavaScript backend is disabled (eval.js = false).");
|
|
180
|
+
return { backend: jsBackend, fallback: false };
|
|
181
|
+
}
|
|
182
|
+
// Auto-detect.
|
|
183
|
+
const sniffed = sniffLanguage(code);
|
|
184
|
+
if (sniffed === "python" && allowPy && (await pythonBackend.isAvailable(session))) {
|
|
185
|
+
return { backend: pythonBackend, fallback: false };
|
|
186
|
+
}
|
|
187
|
+
if (sniffed === "js" && allowJs) {
|
|
188
|
+
return { backend: jsBackend, fallback: false };
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Sniffer returned undefined or the preferred backend was disabled. Prefer
|
|
192
|
+
// python when its kernel is up, else fall back to js.
|
|
193
|
+
if (allowPy && (await pythonBackend.isAvailable(session))) {
|
|
194
|
+
const notice =
|
|
195
|
+
sniffed === "js" ? "JavaScript markers detected but eval.js is disabled; using Python." : undefined;
|
|
196
|
+
return { backend: pythonBackend, fallback: false, notice };
|
|
197
|
+
}
|
|
198
|
+
if (allowJs) {
|
|
199
|
+
const notice =
|
|
200
|
+
sniffed === "python"
|
|
201
|
+
? "Python markers detected but the python kernel is unavailable; using JavaScript."
|
|
202
|
+
: undefined;
|
|
203
|
+
return { backend: jsBackend, fallback: true, notice };
|
|
204
|
+
}
|
|
205
|
+
throw new ToolError("No eval backend is available; enable eval.py or eval.js.");
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
export class EvalTool implements AgentTool<typeof evalSchema> {
|
|
209
|
+
readonly name = "eval";
|
|
210
|
+
readonly label = "Eval";
|
|
145
211
|
get description(): string {
|
|
146
|
-
return
|
|
212
|
+
if (!this.session) return getEvalToolDescription();
|
|
213
|
+
const backends = resolveEvalBackends(this.session);
|
|
214
|
+
return getEvalToolDescription({ py: backends.python, js: backends.js });
|
|
147
215
|
}
|
|
148
|
-
readonly parameters =
|
|
216
|
+
readonly parameters = evalSchema;
|
|
149
217
|
readonly concurrency = "exclusive";
|
|
150
218
|
readonly strict = true;
|
|
151
219
|
|
|
152
|
-
|
|
220
|
+
get customFormat(): { syntax: "lark"; definition: string } {
|
|
221
|
+
return { syntax: "lark", definition: evalGrammar };
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
readonly #proxyExecutor?: EvalProxyExecutor;
|
|
153
225
|
|
|
154
226
|
constructor(
|
|
155
227
|
private readonly session: ToolSession | null,
|
|
156
|
-
options?:
|
|
228
|
+
options?: EvalToolOptions,
|
|
157
229
|
) {
|
|
158
230
|
this.#proxyExecutor = options?.proxyExecutor;
|
|
159
231
|
}
|
|
160
232
|
|
|
161
233
|
async execute(
|
|
162
234
|
_toolCallId: string,
|
|
163
|
-
params: Static<typeof
|
|
235
|
+
params: Static<typeof evalSchema>,
|
|
164
236
|
signal?: AbortSignal,
|
|
165
237
|
onUpdate?: AgentToolUpdateCallback,
|
|
166
238
|
_ctx?: AgentToolContext,
|
|
167
|
-
): Promise<AgentToolResult<
|
|
239
|
+
): Promise<AgentToolResult<EvalToolDetails | undefined>> {
|
|
168
240
|
if (this.#proxyExecutor) {
|
|
169
241
|
return this.#proxyExecutor(params, signal);
|
|
170
242
|
}
|
|
171
243
|
|
|
172
244
|
if (!this.session) {
|
|
173
|
-
throw new ToolError("
|
|
245
|
+
throw new ToolError("Eval tool requires a session when not using proxy executor");
|
|
174
246
|
}
|
|
175
247
|
const session = this.session;
|
|
176
248
|
|
|
177
|
-
const
|
|
178
|
-
|
|
179
|
-
const
|
|
180
|
-
const
|
|
181
|
-
|
|
182
|
-
|
|
249
|
+
const parsedInput = parseEvalInput(params.input);
|
|
250
|
+
let previousRuntimeLanguage: EvalLanguage | undefined;
|
|
251
|
+
const cells: ResolvedEvalCell[] = [];
|
|
252
|
+
for (const cell of parsedInput.cells) {
|
|
253
|
+
const requested = cell.languageOrigin === "fence" ? cell.language : (previousRuntimeLanguage ?? undefined);
|
|
254
|
+
const resolved = await resolveBackend(session, requested, cell.code);
|
|
255
|
+
previousRuntimeLanguage = resolved.backend.id;
|
|
256
|
+
cells.push({
|
|
257
|
+
index: cell.index,
|
|
258
|
+
title: cell.title,
|
|
259
|
+
code: cell.code,
|
|
260
|
+
timeoutMs: cell.timeoutMs,
|
|
261
|
+
reset: cell.reset,
|
|
262
|
+
resolved,
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
const languages = uniqueEvalLanguages(cells);
|
|
266
|
+
const notice = detailsNotice(cells);
|
|
183
267
|
const sessionAbortController = new AbortController();
|
|
184
|
-
const combinedSignal = signal
|
|
185
|
-
? AbortSignal.any([signal, timeoutSignal, sessionAbortController.signal])
|
|
186
|
-
: AbortSignal.any([timeoutSignal, sessionAbortController.signal]);
|
|
187
268
|
let outputSink: OutputSink | undefined;
|
|
188
269
|
let outputSummary: OutputSummary | undefined;
|
|
189
270
|
let outputDumped = false;
|
|
@@ -194,22 +275,23 @@ export class PythonTool implements AgentTool<typeof pythonSchema> {
|
|
|
194
275
|
return outputSummary;
|
|
195
276
|
};
|
|
196
277
|
|
|
197
|
-
const execution = (async (): Promise<AgentToolResult<
|
|
278
|
+
const execution = (async (): Promise<AgentToolResult<EvalToolDetails | undefined>> => {
|
|
198
279
|
try {
|
|
199
280
|
if (signal?.aborted) {
|
|
200
281
|
throw new ToolAbortError();
|
|
201
282
|
}
|
|
202
|
-
session.
|
|
283
|
+
session.assertEvalExecutionAllowed?.();
|
|
203
284
|
|
|
204
285
|
const tailBuffer = new TailBuffer(DEFAULT_MAX_BYTES * 2);
|
|
205
286
|
const jsonOutputs: unknown[] = [];
|
|
206
287
|
const images: ImageContent[] = [];
|
|
207
|
-
const statusEvents:
|
|
288
|
+
const statusEvents: EvalStatusEvent[] = [];
|
|
208
289
|
|
|
209
|
-
const cellResults:
|
|
210
|
-
index,
|
|
290
|
+
const cellResults: EvalCellResult[] = cells.map(cell => ({
|
|
291
|
+
index: cell.index,
|
|
211
292
|
title: cell.title,
|
|
212
293
|
code: cell.code,
|
|
294
|
+
language: cell.resolved.backend.id,
|
|
213
295
|
output: "",
|
|
214
296
|
status: "pending",
|
|
215
297
|
}));
|
|
@@ -219,8 +301,10 @@ export class PythonTool implements AgentTool<typeof pythonSchema> {
|
|
|
219
301
|
tailBuffer.append(text);
|
|
220
302
|
};
|
|
221
303
|
|
|
222
|
-
const buildUpdateDetails = ():
|
|
223
|
-
const details:
|
|
304
|
+
const buildUpdateDetails = (): EvalToolDetails => {
|
|
305
|
+
const details: EvalToolDetails = {
|
|
306
|
+
language: languages[0],
|
|
307
|
+
languages,
|
|
224
308
|
cells: cellResults.map(cell => ({
|
|
225
309
|
...cell,
|
|
226
310
|
statusEvents: cell.statusEvents ? [...cell.statusEvents] : undefined,
|
|
@@ -235,6 +319,9 @@ export class PythonTool implements AgentTool<typeof pythonSchema> {
|
|
|
235
319
|
if (statusEvents.length > 0) {
|
|
236
320
|
details.statusEvents = statusEvents;
|
|
237
321
|
}
|
|
322
|
+
if (notice) {
|
|
323
|
+
details.notice = notice;
|
|
324
|
+
}
|
|
238
325
|
return details;
|
|
239
326
|
};
|
|
240
327
|
|
|
@@ -248,9 +335,9 @@ export class PythonTool implements AgentTool<typeof pythonSchema> {
|
|
|
248
335
|
};
|
|
249
336
|
|
|
250
337
|
const sessionFile = session.getSessionFile?.() ?? undefined;
|
|
251
|
-
const kernelOwnerId = session.
|
|
252
|
-
const { path: artifactPath, id: artifactId } = (await session.allocateOutputArtifact?.("
|
|
253
|
-
session.
|
|
338
|
+
const kernelOwnerId = session.getEvalKernelOwnerId?.() ?? undefined;
|
|
339
|
+
const { path: artifactPath, id: artifactId } = (await session.allocateOutputArtifact?.("eval")) ?? {};
|
|
340
|
+
session.assertEvalExecutionAllowed?.();
|
|
254
341
|
outputSink = new OutputSink({
|
|
255
342
|
artifactPath,
|
|
256
343
|
artifactId,
|
|
@@ -261,36 +348,16 @@ export class PythonTool implements AgentTool<typeof pythonSchema> {
|
|
|
261
348
|
});
|
|
262
349
|
const sessionId = sessionFile ? `session:${sessionFile}:cwd:${session.cwd}` : `cwd:${session.cwd}`;
|
|
263
350
|
|
|
264
|
-
if (getPreludeDocs().length === 0) {
|
|
265
|
-
const warmup = await warmPythonEnvironment(
|
|
266
|
-
session.cwd,
|
|
267
|
-
sessionId,
|
|
268
|
-
session.settings.get("python.sharedGateway"),
|
|
269
|
-
sessionFile ?? undefined,
|
|
270
|
-
kernelOwnerId,
|
|
271
|
-
combinedSignal,
|
|
272
|
-
);
|
|
273
|
-
if (!warmup.ok) {
|
|
274
|
-
if (combinedSignal.aborted) throw new ToolAbortError();
|
|
275
|
-
throw new ToolError(warmup.reason ?? "Python prelude helpers unavailable");
|
|
276
|
-
}
|
|
277
|
-
session.assertPythonExecutionAllowed?.();
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
const baseExecutorOptions = {
|
|
281
|
-
cwd: session.cwd,
|
|
282
|
-
deadlineMs,
|
|
283
|
-
signal: combinedSignal,
|
|
284
|
-
sessionId,
|
|
285
|
-
kernelMode: session.settings.get("python.kernelMode"),
|
|
286
|
-
useSharedGateway: session.settings.get("python.sharedGateway"),
|
|
287
|
-
sessionFile: sessionFile ?? undefined,
|
|
288
|
-
kernelOwnerId,
|
|
289
|
-
};
|
|
290
|
-
|
|
291
351
|
for (let i = 0; i < cells.length; i++) {
|
|
292
352
|
const cell = cells[i];
|
|
293
|
-
const
|
|
353
|
+
const backend = cell.resolved.backend;
|
|
354
|
+
const timeoutSec = timeoutSecondsFromMs(cell.timeoutMs);
|
|
355
|
+
const deadlineMs = Date.now() + timeoutSec * 1000;
|
|
356
|
+
const timeoutSignal = AbortSignal.timeout(Math.max(0, deadlineMs - Date.now()));
|
|
357
|
+
const combinedSignal = signal
|
|
358
|
+
? AbortSignal.any([signal, timeoutSignal, sessionAbortController.signal])
|
|
359
|
+
: AbortSignal.any([timeoutSignal, sessionAbortController.signal]);
|
|
360
|
+
|
|
294
361
|
const cellResult = cellResults[i];
|
|
295
362
|
cellResult.status = "running";
|
|
296
363
|
cellResult.output = "";
|
|
@@ -299,19 +366,25 @@ export class PythonTool implements AgentTool<typeof pythonSchema> {
|
|
|
299
366
|
cellResult.durationMs = undefined;
|
|
300
367
|
pushUpdate();
|
|
301
368
|
|
|
302
|
-
const
|
|
303
|
-
|
|
304
|
-
|
|
369
|
+
const startTime = Date.now();
|
|
370
|
+
const result = await backend.execute(cell.code, {
|
|
371
|
+
cwd: session.cwd,
|
|
372
|
+
sessionId,
|
|
373
|
+
sessionFile: sessionFile ?? undefined,
|
|
374
|
+
kernelOwnerId,
|
|
375
|
+
signal: combinedSignal,
|
|
376
|
+
session,
|
|
377
|
+
deadlineMs,
|
|
378
|
+
reset: cell.reset,
|
|
379
|
+
artifactPath,
|
|
380
|
+
artifactId,
|
|
305
381
|
onChunk: chunk => {
|
|
306
382
|
outputSink!.push(chunk);
|
|
307
383
|
},
|
|
308
|
-
};
|
|
309
|
-
|
|
310
|
-
const startTime = Date.now();
|
|
311
|
-
const result = await executePython(cell.code, executorOptions);
|
|
384
|
+
});
|
|
312
385
|
const durationMs = Date.now() - startTime;
|
|
313
386
|
|
|
314
|
-
const cellStatusEvents:
|
|
387
|
+
const cellStatusEvents: EvalStatusEvent[] = [];
|
|
315
388
|
let cellHasMarkdown = false;
|
|
316
389
|
for (const output of result.displayOutputs) {
|
|
317
390
|
if (output.type === "json") {
|
|
@@ -366,35 +439,17 @@ export class PythonTool implements AgentTool<typeof pythonSchema> {
|
|
|
366
439
|
? `${combinedOutput}\n\nCell ${i + 1} aborted: ${errorMsg}`
|
|
367
440
|
: combinedOutput || errorMsg;
|
|
368
441
|
|
|
369
|
-
const
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
totalBytes: 0,
|
|
374
|
-
outputLines: 0,
|
|
375
|
-
outputBytes: 0,
|
|
376
|
-
};
|
|
377
|
-
const outputLines = combinedOutput.length > 0 ? combinedOutput.split("\n").length : 0;
|
|
378
|
-
const outputBytes = Buffer.byteLength(combinedOutput, "utf-8");
|
|
379
|
-
const missingLines = Math.max(0, rawSummary.totalLines - rawSummary.outputLines);
|
|
380
|
-
const missingBytes = Math.max(0, rawSummary.totalBytes - rawSummary.outputBytes);
|
|
381
|
-
const summaryForMeta: OutputSummary = {
|
|
382
|
-
output: combinedOutput,
|
|
383
|
-
truncated: rawSummary.truncated,
|
|
384
|
-
totalLines: outputLines + missingLines,
|
|
385
|
-
totalBytes: outputBytes + missingBytes,
|
|
386
|
-
outputLines,
|
|
387
|
-
outputBytes,
|
|
388
|
-
artifactId: rawSummary.artifactId,
|
|
389
|
-
};
|
|
390
|
-
|
|
391
|
-
const details: PythonToolDetails = {
|
|
442
|
+
const summaryForMeta = await summarizeFinal(combinedOutput, finalizeOutput);
|
|
443
|
+
const details: EvalToolDetails = {
|
|
444
|
+
language: languages[0],
|
|
445
|
+
languages,
|
|
392
446
|
cells: cellResults,
|
|
393
447
|
jsonOutputs: jsonOutputs.length > 0 ? jsonOutputs : undefined,
|
|
394
448
|
images: images.length > 0 ? images : undefined,
|
|
395
449
|
statusEvents: statusEvents.length > 0 ? statusEvents : undefined,
|
|
396
450
|
isError: true,
|
|
397
451
|
};
|
|
452
|
+
if (notice) details.notice = notice;
|
|
398
453
|
|
|
399
454
|
return toolResult(details)
|
|
400
455
|
.text(outputText)
|
|
@@ -413,35 +468,17 @@ export class PythonTool implements AgentTool<typeof pythonSchema> {
|
|
|
413
468
|
? `${combinedOutput}\n\nCommand exited with code ${result.exitCode}`
|
|
414
469
|
: `Command exited with code ${result.exitCode}`;
|
|
415
470
|
|
|
416
|
-
const
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
totalBytes: 0,
|
|
421
|
-
outputLines: 0,
|
|
422
|
-
outputBytes: 0,
|
|
423
|
-
};
|
|
424
|
-
const outputLines = combinedOutput.length > 0 ? combinedOutput.split("\n").length : 0;
|
|
425
|
-
const outputBytes = Buffer.byteLength(combinedOutput, "utf-8");
|
|
426
|
-
const missingLines = Math.max(0, rawSummary.totalLines - rawSummary.outputLines);
|
|
427
|
-
const missingBytes = Math.max(0, rawSummary.totalBytes - rawSummary.outputBytes);
|
|
428
|
-
const summaryForMeta: OutputSummary = {
|
|
429
|
-
output: combinedOutput,
|
|
430
|
-
truncated: rawSummary.truncated,
|
|
431
|
-
totalLines: outputLines + missingLines,
|
|
432
|
-
totalBytes: outputBytes + missingBytes,
|
|
433
|
-
outputLines,
|
|
434
|
-
outputBytes,
|
|
435
|
-
artifactId: rawSummary.artifactId,
|
|
436
|
-
};
|
|
437
|
-
|
|
438
|
-
const details: PythonToolDetails = {
|
|
471
|
+
const summaryForMeta = await summarizeFinal(combinedOutput, finalizeOutput);
|
|
472
|
+
const details: EvalToolDetails = {
|
|
473
|
+
language: languages[0],
|
|
474
|
+
languages,
|
|
439
475
|
cells: cellResults,
|
|
440
476
|
jsonOutputs: jsonOutputs.length > 0 ? jsonOutputs : undefined,
|
|
441
477
|
images: images.length > 0 ? images : undefined,
|
|
442
478
|
statusEvents: statusEvents.length > 0 ? statusEvents : undefined,
|
|
443
479
|
isError: true,
|
|
444
480
|
};
|
|
481
|
+
if (notice) details.notice = notice;
|
|
445
482
|
|
|
446
483
|
return toolResult(details)
|
|
447
484
|
.text(outputText)
|
|
@@ -456,40 +493,22 @@ export class PythonTool implements AgentTool<typeof pythonSchema> {
|
|
|
456
493
|
const combinedOutput = cellOutputs.join("\n\n");
|
|
457
494
|
const outputText =
|
|
458
495
|
combinedOutput || (jsonOutputs.length > 0 || images.length > 0 ? "(no text output)" : "(no output)");
|
|
459
|
-
const
|
|
460
|
-
output: "",
|
|
461
|
-
truncated: false,
|
|
462
|
-
totalLines: 0,
|
|
463
|
-
totalBytes: 0,
|
|
464
|
-
outputLines: 0,
|
|
465
|
-
outputBytes: 0,
|
|
466
|
-
};
|
|
467
|
-
const outputLines = combinedOutput.length > 0 ? combinedOutput.split("\n").length : 0;
|
|
468
|
-
const outputBytes = Buffer.byteLength(combinedOutput, "utf-8");
|
|
469
|
-
const missingLines = Math.max(0, rawSummary.totalLines - rawSummary.outputLines);
|
|
470
|
-
const missingBytes = Math.max(0, rawSummary.totalBytes - rawSummary.outputBytes);
|
|
471
|
-
const summaryForMeta: OutputSummary = {
|
|
472
|
-
output: combinedOutput,
|
|
473
|
-
truncated: rawSummary.truncated,
|
|
474
|
-
totalLines: outputLines + missingLines,
|
|
475
|
-
totalBytes: outputBytes + missingBytes,
|
|
476
|
-
outputLines,
|
|
477
|
-
outputBytes,
|
|
478
|
-
artifactId: rawSummary.artifactId,
|
|
479
|
-
};
|
|
496
|
+
const summaryForMeta = await summarizeFinal(combinedOutput, finalizeOutput);
|
|
480
497
|
|
|
481
|
-
const details:
|
|
498
|
+
const details: EvalToolDetails = {
|
|
499
|
+
language: languages[0],
|
|
500
|
+
languages,
|
|
482
501
|
cells: cellResults,
|
|
483
502
|
jsonOutputs: jsonOutputs.length > 0 ? jsonOutputs : undefined,
|
|
484
503
|
images: images.length > 0 ? images : undefined,
|
|
485
504
|
statusEvents: statusEvents.length > 0 ? statusEvents : undefined,
|
|
486
505
|
};
|
|
506
|
+
if (notice) details.notice = notice;
|
|
487
507
|
|
|
488
|
-
|
|
508
|
+
return toolResult(details)
|
|
489
509
|
.text(outputText)
|
|
490
|
-
.truncationFromSummary(summaryForMeta, { direction: "tail" })
|
|
491
|
-
|
|
492
|
-
return resultBuilder.done();
|
|
510
|
+
.truncationFromSummary(summaryForMeta, { direction: "tail" })
|
|
511
|
+
.done();
|
|
493
512
|
} finally {
|
|
494
513
|
if (!outputDumped) {
|
|
495
514
|
try {
|
|
@@ -498,57 +517,95 @@ export class PythonTool implements AgentTool<typeof pythonSchema> {
|
|
|
498
517
|
}
|
|
499
518
|
}
|
|
500
519
|
})();
|
|
501
|
-
|
|
520
|
+
|
|
521
|
+
return await (session.trackEvalExecution?.(execution, sessionAbortController) ?? execution);
|
|
502
522
|
}
|
|
503
523
|
}
|
|
504
524
|
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
525
|
+
async function summarizeFinal(
|
|
526
|
+
combinedOutput: string,
|
|
527
|
+
finalizeOutput: () => Promise<OutputSummary | undefined>,
|
|
528
|
+
): Promise<OutputSummary> {
|
|
529
|
+
const rawSummary = (await finalizeOutput()) ?? {
|
|
530
|
+
output: "",
|
|
531
|
+
truncated: false,
|
|
532
|
+
totalLines: 0,
|
|
533
|
+
totalBytes: 0,
|
|
534
|
+
outputLines: 0,
|
|
535
|
+
outputBytes: 0,
|
|
536
|
+
};
|
|
537
|
+
const outputLines = combinedOutput.length > 0 ? combinedOutput.split("\n").length : 0;
|
|
538
|
+
const outputBytes = Buffer.byteLength(combinedOutput, "utf-8");
|
|
539
|
+
const missingLines = Math.max(0, rawSummary.totalLines - rawSummary.outputLines);
|
|
540
|
+
const missingBytes = Math.max(0, rawSummary.totalBytes - rawSummary.outputBytes);
|
|
541
|
+
return {
|
|
542
|
+
output: combinedOutput,
|
|
543
|
+
truncated: rawSummary.truncated,
|
|
544
|
+
totalLines: outputLines + missingLines,
|
|
545
|
+
totalBytes: outputBytes + missingBytes,
|
|
546
|
+
outputLines,
|
|
547
|
+
outputBytes,
|
|
548
|
+
artifactId: rawSummary.artifactId,
|
|
549
|
+
};
|
|
509
550
|
}
|
|
510
551
|
|
|
511
|
-
interface
|
|
552
|
+
interface EvalRenderArgs {
|
|
553
|
+
input?: string;
|
|
554
|
+
__partialJson?: string;
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
interface EvalRenderContext {
|
|
512
558
|
output?: string;
|
|
513
559
|
expanded?: boolean;
|
|
514
560
|
previewLines?: number;
|
|
515
561
|
timeout?: number;
|
|
516
562
|
}
|
|
517
563
|
|
|
564
|
+
function decodePartialJsonStringFragment(fragment: string): string {
|
|
565
|
+
let text = fragment.replace(/\\u[0-9a-fA-F]{0,3}$/, "");
|
|
566
|
+
const trailingBackslashes = text.match(/\\+$/)?.[0].length ?? 0;
|
|
567
|
+
if (trailingBackslashes % 2 === 1) text = text.slice(0, -1);
|
|
568
|
+
try {
|
|
569
|
+
return JSON.parse(`"${text}"`) as string;
|
|
570
|
+
} catch {
|
|
571
|
+
return text;
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
function extractPartialJsonString(partialJson: string | undefined, key: string): string | undefined {
|
|
576
|
+
if (!partialJson) return undefined;
|
|
577
|
+
const pattern = new RegExp(`"${key}"\\s*:\\s*"((?:\\\\.|[^"\\\\])*)`, "u");
|
|
578
|
+
const match = pattern.exec(partialJson);
|
|
579
|
+
if (!match) return undefined;
|
|
580
|
+
return decodePartialJsonStringFragment(match[1]);
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
function getRenderInput(args: EvalRenderArgs | undefined): string | undefined {
|
|
584
|
+
return args?.input ?? extractPartialJsonString(args?.__partialJson, "input");
|
|
585
|
+
}
|
|
586
|
+
|
|
518
587
|
/** Format a status event as a single line for display. */
|
|
519
|
-
function formatStatusEvent(event:
|
|
588
|
+
function formatStatusEvent(event: EvalStatusEvent, theme: Theme): string {
|
|
520
589
|
const { op, ...data } = event;
|
|
521
590
|
|
|
522
|
-
// Map operations to available theme icons
|
|
523
591
|
type AvailableIcon = "icon.file" | "icon.folder" | "icon.git" | "icon.package";
|
|
524
592
|
const opIcons: Record<string, AvailableIcon> = {
|
|
525
|
-
// File I/O
|
|
526
593
|
read: "icon.file",
|
|
527
594
|
write: "icon.file",
|
|
528
595
|
append: "icon.file",
|
|
529
596
|
cat: "icon.file",
|
|
530
597
|
touch: "icon.file",
|
|
531
|
-
lines: "icon.file",
|
|
532
|
-
// Navigation/Directory
|
|
533
598
|
ls: "icon.folder",
|
|
534
599
|
cd: "icon.folder",
|
|
535
600
|
pwd: "icon.folder",
|
|
536
601
|
mkdir: "icon.folder",
|
|
537
602
|
tree: "icon.folder",
|
|
538
603
|
stat: "icon.folder",
|
|
539
|
-
// Search (use file icon since no search icon)
|
|
540
604
|
find: "icon.file",
|
|
541
605
|
grep: "icon.file",
|
|
542
606
|
rgrep: "icon.file",
|
|
543
607
|
glob: "icon.file",
|
|
544
|
-
// Edit operations (use file icon)
|
|
545
|
-
replace: "icon.file",
|
|
546
608
|
sed: "icon.file",
|
|
547
|
-
rsed: "icon.file",
|
|
548
|
-
delete_lines: "icon.file",
|
|
549
|
-
delete_matching: "icon.file",
|
|
550
|
-
insert_at: "icon.file",
|
|
551
|
-
// Git
|
|
552
609
|
git_status: "icon.git",
|
|
553
610
|
git_diff: "icon.git",
|
|
554
611
|
git_log: "icon.git",
|
|
@@ -556,7 +613,6 @@ function formatStatusEvent(event: PythonStatusEvent, theme: Theme): string {
|
|
|
556
613
|
git_branch: "icon.git",
|
|
557
614
|
git_file_at: "icon.git",
|
|
558
615
|
git_has_changes: "icon.git",
|
|
559
|
-
// Shell/batch (use package icon)
|
|
560
616
|
run: "icon.package",
|
|
561
617
|
sh: "icon.package",
|
|
562
618
|
env: "icon.package",
|
|
@@ -566,23 +622,20 @@ function formatStatusEvent(event: PythonStatusEvent, theme: Theme): string {
|
|
|
566
622
|
const iconKey = opIcons[op] ?? "icon.file";
|
|
567
623
|
const icon = theme.styledSymbol(iconKey, "muted");
|
|
568
624
|
|
|
569
|
-
// Format the status message based on operation type
|
|
570
625
|
const parts: string[] = [];
|
|
571
626
|
|
|
572
|
-
// Error handling
|
|
573
627
|
if (data.error) {
|
|
574
628
|
return `${icon} ${theme.fg("warning", op)}: ${theme.fg("dim", String(data.error))}`;
|
|
575
629
|
}
|
|
576
630
|
|
|
577
|
-
// Build description based on common fields
|
|
578
631
|
switch (op) {
|
|
579
632
|
case "read":
|
|
580
|
-
parts.push(`${data.chars} chars`);
|
|
633
|
+
parts.push(`${data.chars ?? data.bytes ?? 0} chars`);
|
|
581
634
|
if (data.path) parts.push(`from ${shortenPath(String(data.path))}`);
|
|
582
635
|
break;
|
|
583
636
|
case "write":
|
|
584
637
|
case "append":
|
|
585
|
-
parts.push(`${data.chars} chars`);
|
|
638
|
+
parts.push(`${data.chars ?? data.bytes ?? 0} chars`);
|
|
586
639
|
if (data.path) parts.push(`to ${shortenPath(String(data.path))}`);
|
|
587
640
|
break;
|
|
588
641
|
case "cat":
|
|
@@ -622,15 +675,10 @@ function formatStatusEvent(event: PythonStatusEvent, theme: Theme): string {
|
|
|
622
675
|
}
|
|
623
676
|
if (data.path) parts.push(shortenPath(String(data.path)));
|
|
624
677
|
break;
|
|
625
|
-
case "replace":
|
|
626
678
|
case "sed":
|
|
627
679
|
parts.push(`${data.count} replacement${(data.count as number) !== 1 ? "s" : ""}`);
|
|
628
680
|
if (data.path) parts.push(`in ${shortenPath(String(data.path))}`);
|
|
629
681
|
break;
|
|
630
|
-
case "rsed":
|
|
631
|
-
parts.push(`${data.count} replacement${(data.count as number) !== 1 ? "s" : ""}`);
|
|
632
|
-
if (data.files) parts.push(`in ${data.files} file${(data.files as number) !== 1 ? "s" : ""}`);
|
|
633
|
-
break;
|
|
634
682
|
case "git_status":
|
|
635
683
|
if (data.clean) {
|
|
636
684
|
parts.push("clean");
|
|
@@ -663,31 +711,13 @@ function formatStatusEvent(event: PythonStatusEvent, theme: Theme): string {
|
|
|
663
711
|
case "wc":
|
|
664
712
|
parts.push(`${data.lines}L ${data.words}W ${data.chars}C`);
|
|
665
713
|
break;
|
|
666
|
-
case "lines":
|
|
667
|
-
parts.push(`${data.count} line${(data.count as number) !== 1 ? "s" : ""}`);
|
|
668
|
-
if (data.start && data.end) parts.push(`(${data.start}-${data.end})`);
|
|
669
|
-
break;
|
|
670
|
-
case "delete_lines":
|
|
671
|
-
case "delete_matching":
|
|
672
|
-
parts.push(`${data.count} line${(data.count as number) !== 1 ? "s" : ""} deleted`);
|
|
673
|
-
break;
|
|
674
|
-
case "insert_at":
|
|
675
|
-
parts.push(`${data.lines_inserted} line${(data.lines_inserted as number) !== 1 ? "s" : ""} inserted`);
|
|
676
|
-
break;
|
|
677
714
|
case "cd":
|
|
678
715
|
case "pwd":
|
|
679
716
|
case "mkdir":
|
|
680
717
|
case "touch":
|
|
681
718
|
if (data.path) parts.push(shortenPath(String(data.path)));
|
|
682
719
|
break;
|
|
683
|
-
case "rm":
|
|
684
|
-
case "mv":
|
|
685
|
-
case "cp":
|
|
686
|
-
if (data.src) parts.push(`${shortenPath(String(data.src))} → ${shortenPath(String(data.dst))}`);
|
|
687
|
-
else if (data.path) parts.push(shortenPath(String(data.path)));
|
|
688
|
-
break;
|
|
689
720
|
default:
|
|
690
|
-
// Generic formatting for other operations
|
|
691
721
|
if (data.count !== undefined) {
|
|
692
722
|
parts.push(String(data.count));
|
|
693
723
|
}
|
|
@@ -701,14 +731,12 @@ function formatStatusEvent(event: PythonStatusEvent, theme: Theme): string {
|
|
|
701
731
|
}
|
|
702
732
|
|
|
703
733
|
/** Format status event with expanded detail lines. */
|
|
704
|
-
function formatStatusEventExpanded(event:
|
|
734
|
+
function formatStatusEventExpanded(event: EvalStatusEvent, theme: Theme): string[] {
|
|
705
735
|
const lines: string[] = [];
|
|
706
736
|
const { op, ...data } = event;
|
|
707
737
|
|
|
708
|
-
// Main status line
|
|
709
738
|
lines.push(formatStatusEvent(event, theme));
|
|
710
739
|
|
|
711
|
-
// Add detail lines for operations with list data
|
|
712
740
|
const addItems = (items: unknown[], formatter: (item: unknown) => string, max = 5) => {
|
|
713
741
|
const arr = Array.isArray(items) ? items : [];
|
|
714
742
|
for (let i = 0; i < Math.min(arr.length, max); i++) {
|
|
@@ -719,7 +747,6 @@ function formatStatusEventExpanded(event: PythonStatusEvent, theme: Theme): stri
|
|
|
719
747
|
}
|
|
720
748
|
};
|
|
721
749
|
|
|
722
|
-
// Add preview lines (truncated content)
|
|
723
750
|
const addPreview = (preview: string, maxLines = 3) => {
|
|
724
751
|
const previewLines = String(preview).split("\n").slice(0, maxLines);
|
|
725
752
|
for (const line of previewLines) {
|
|
@@ -755,14 +782,6 @@ function formatStatusEventExpanded(event: PythonStatusEvent, theme: Theme): stri
|
|
|
755
782
|
});
|
|
756
783
|
}
|
|
757
784
|
break;
|
|
758
|
-
case "rsed":
|
|
759
|
-
if (data.changed) {
|
|
760
|
-
addItems(data.changed as unknown[], c => {
|
|
761
|
-
const change = c as { file: string; count: number };
|
|
762
|
-
return `${shortenPath(change.file)}: ${change.count} replacement${change.count !== 1 ? "s" : ""}`;
|
|
763
|
-
});
|
|
764
|
-
}
|
|
765
|
-
break;
|
|
766
785
|
case "env":
|
|
767
786
|
if (data.keys) addItems(data.keys as unknown[], k => String(k), 10);
|
|
768
787
|
break;
|
|
@@ -786,7 +805,6 @@ function formatStatusEventExpanded(event: PythonStatusEvent, theme: Theme): stri
|
|
|
786
805
|
case "tail":
|
|
787
806
|
case "tree":
|
|
788
807
|
case "diff":
|
|
789
|
-
case "lines":
|
|
790
808
|
case "git_diff":
|
|
791
809
|
case "sh":
|
|
792
810
|
if (data.preview) addPreview(String(data.preview));
|
|
@@ -797,7 +815,7 @@ function formatStatusEventExpanded(event: PythonStatusEvent, theme: Theme): stri
|
|
|
797
815
|
}
|
|
798
816
|
|
|
799
817
|
/** Render status events as tree lines. */
|
|
800
|
-
function renderStatusEvents(events:
|
|
818
|
+
function renderStatusEvents(events: EvalStatusEvent[], theme: Theme, expanded: boolean): string[] {
|
|
801
819
|
if (events.length === 0) return [];
|
|
802
820
|
|
|
803
821
|
const maxCollapsed = 3;
|
|
@@ -810,7 +828,6 @@ function renderStatusEvents(events: PythonStatusEvent[], theme: Theme, expanded:
|
|
|
810
828
|
const branch = isLast ? theme.tree.last : theme.tree.branch;
|
|
811
829
|
|
|
812
830
|
if (expanded) {
|
|
813
|
-
// Show expanded details for each event
|
|
814
831
|
const eventLines = formatStatusEventExpanded(events[i], theme);
|
|
815
832
|
lines.push(`${theme.fg("dim", branch)} ${eventLines[0]}`);
|
|
816
833
|
const continueBranch = isLast ? " " : `${theme.tree.vertical} `;
|
|
@@ -832,7 +849,7 @@ function renderStatusEvents(events: PythonStatusEvent[], theme: Theme, expanded:
|
|
|
832
849
|
}
|
|
833
850
|
|
|
834
851
|
function formatCellOutputLines(
|
|
835
|
-
cell:
|
|
852
|
+
cell: EvalCellResult,
|
|
836
853
|
expanded: boolean,
|
|
837
854
|
previewLines: number,
|
|
838
855
|
theme: Theme,
|
|
@@ -861,60 +878,46 @@ function formatCellOutputLines(
|
|
|
861
878
|
return { lines: outputLines, hiddenCount };
|
|
862
879
|
}
|
|
863
880
|
|
|
864
|
-
export const
|
|
865
|
-
renderCall(args:
|
|
866
|
-
const
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
if (resolvedWorkdir === resolvedCwd) {
|
|
874
|
-
displayWorkdir = undefined;
|
|
875
|
-
} else {
|
|
876
|
-
const relativePath = path.relative(resolvedCwd, resolvedWorkdir);
|
|
877
|
-
const isWithinCwd =
|
|
878
|
-
relativePath && !relativePath.startsWith("..") && !relativePath.startsWith(`..${path.sep}`);
|
|
879
|
-
if (isWithinCwd) {
|
|
880
|
-
displayWorkdir = relativePath;
|
|
881
|
-
}
|
|
881
|
+
export const evalToolRenderer = {
|
|
882
|
+
renderCall(args: EvalRenderArgs, _options: RenderResultOptions, uiTheme: Theme): Component {
|
|
883
|
+
const input = getRenderInput(args);
|
|
884
|
+
let cells: ParsedEvalCell[] = [];
|
|
885
|
+
if (input) {
|
|
886
|
+
try {
|
|
887
|
+
cells = parseEvalInput(input).cells;
|
|
888
|
+
} catch {
|
|
889
|
+
cells = [];
|
|
882
890
|
}
|
|
883
891
|
}
|
|
884
892
|
|
|
885
|
-
const workdirLabel = displayWorkdir ? `cd ${displayWorkdir}` : undefined;
|
|
886
893
|
if (cells.length === 0) {
|
|
887
|
-
const
|
|
888
|
-
const
|
|
889
|
-
const text = formatTitle(`${prompt} ${prefix}…`, uiTheme);
|
|
894
|
+
const promptSym = uiTheme.fg("accent", ">>>");
|
|
895
|
+
const text = formatTitle(`${promptSym} …`, uiTheme);
|
|
890
896
|
return new Text(text, 0, 0);
|
|
891
897
|
}
|
|
892
898
|
|
|
893
|
-
|
|
894
|
-
let cached: { width: number; result: string[] } | undefined;
|
|
899
|
+
let cached: { key: string; width: number; result: string[] } | undefined;
|
|
895
900
|
|
|
896
901
|
return {
|
|
897
902
|
render: (width: number): string[] => {
|
|
898
|
-
|
|
903
|
+
const key = `${input?.length ?? 0}`;
|
|
904
|
+
if (cached && cached.key === key && cached.width === width) {
|
|
899
905
|
return cached.result;
|
|
900
906
|
}
|
|
901
907
|
|
|
902
908
|
const lines: string[] = [];
|
|
903
909
|
for (let i = 0; i < cells.length; i++) {
|
|
904
910
|
const cell = cells[i];
|
|
905
|
-
const cellTitle = cell.title;
|
|
906
|
-
const combinedTitle =
|
|
907
|
-
cellTitle && workdirLabel ? `${workdirLabel} · ${cellTitle}` : (cellTitle ?? workdirLabel);
|
|
908
911
|
const cellLines = renderCodeCell(
|
|
909
912
|
{
|
|
910
913
|
code: cell.code,
|
|
911
|
-
language:
|
|
914
|
+
language: languageForHighlighter(cell.language),
|
|
912
915
|
index: i,
|
|
913
916
|
total: cells.length,
|
|
914
|
-
title:
|
|
917
|
+
title: cell.title,
|
|
915
918
|
status: "pending",
|
|
916
919
|
width,
|
|
917
|
-
codeMaxLines:
|
|
920
|
+
codeMaxLines: EVAL_DEFAULT_PREVIEW_LINES,
|
|
918
921
|
expanded: true,
|
|
919
922
|
},
|
|
920
923
|
uiTheme,
|
|
@@ -924,7 +927,7 @@ export const pythonToolRenderer = {
|
|
|
924
927
|
lines.push("");
|
|
925
928
|
}
|
|
926
929
|
}
|
|
927
|
-
cached = { width, result: lines };
|
|
930
|
+
cached = { key, width, result: lines };
|
|
928
931
|
return lines;
|
|
929
932
|
},
|
|
930
933
|
invalidate: () => {
|
|
@@ -934,9 +937,10 @@ export const pythonToolRenderer = {
|
|
|
934
937
|
},
|
|
935
938
|
|
|
936
939
|
renderResult(
|
|
937
|
-
result: { content: Array<{ type: string; text?: string }>; details?:
|
|
938
|
-
options: RenderResultOptions & { renderContext?:
|
|
940
|
+
result: { content: Array<{ type: string; text?: string }>; details?: EvalToolDetails },
|
|
941
|
+
options: RenderResultOptions & { renderContext?: EvalRenderContext },
|
|
939
942
|
uiTheme: Theme,
|
|
943
|
+
_args?: EvalRenderArgs,
|
|
940
944
|
): Component {
|
|
941
945
|
const details = result.details;
|
|
942
946
|
|
|
@@ -959,17 +963,16 @@ export const pythonToolRenderer = {
|
|
|
959
963
|
if (details?.meta?.truncation) {
|
|
960
964
|
warningLine = formatStyledTruncationWarning(details.meta, uiTheme) ?? undefined;
|
|
961
965
|
}
|
|
966
|
+
const noticeLine = details?.notice ? uiTheme.fg("dim", wrapBrackets(details.notice, uiTheme)) : undefined;
|
|
962
967
|
|
|
963
968
|
const cellResults = details?.cells;
|
|
964
969
|
if (cellResults && cellResults.length > 0) {
|
|
965
|
-
// Cache state following Box pattern
|
|
966
970
|
let cached: { key: string; width: number; result: string[] } | undefined;
|
|
967
971
|
|
|
968
972
|
return {
|
|
969
973
|
render: (width: number): string[] => {
|
|
970
|
-
// Read mutable state at render time
|
|
971
974
|
const expanded = options.renderContext?.expanded ?? options.expanded;
|
|
972
|
-
const previewLines = options.renderContext?.previewLines ??
|
|
975
|
+
const previewLines = options.renderContext?.previewLines ?? EVAL_DEFAULT_PREVIEW_LINES;
|
|
973
976
|
const key = `${expanded}|${previewLines}|${options.spinnerFrame}`;
|
|
974
977
|
if (cached && cached.key === key && cached.width === width) {
|
|
975
978
|
return cached.result;
|
|
@@ -995,7 +998,7 @@ export const pythonToolRenderer = {
|
|
|
995
998
|
const cellLines = renderCodeCell(
|
|
996
999
|
{
|
|
997
1000
|
code: cell.code,
|
|
998
|
-
language:
|
|
1001
|
+
language: languageForHighlighter(cell.language ?? details?.language),
|
|
999
1002
|
index: i,
|
|
1000
1003
|
total: cellResults.length,
|
|
1001
1004
|
title: cell.title,
|
|
@@ -1004,7 +1007,7 @@ export const pythonToolRenderer = {
|
|
|
1004
1007
|
duration: cell.durationMs,
|
|
1005
1008
|
output: outputLines.length > 0 ? outputLines.join("\n") : undefined,
|
|
1006
1009
|
outputMaxLines: outputLines.length,
|
|
1007
|
-
codeMaxLines: expanded ? Number.POSITIVE_INFINITY :
|
|
1010
|
+
codeMaxLines: expanded ? Number.POSITIVE_INFINITY : EVAL_DEFAULT_PREVIEW_LINES,
|
|
1008
1011
|
expanded,
|
|
1009
1012
|
width,
|
|
1010
1013
|
},
|
|
@@ -1024,6 +1027,9 @@ export const pythonToolRenderer = {
|
|
|
1024
1027
|
if (timeoutLine) {
|
|
1025
1028
|
lines.push(timeoutLine);
|
|
1026
1029
|
}
|
|
1030
|
+
if (noticeLine) {
|
|
1031
|
+
lines.push(noticeLine);
|
|
1032
|
+
}
|
|
1027
1033
|
if (warningLine) {
|
|
1028
1034
|
lines.push(warningLine);
|
|
1029
1035
|
}
|
|
@@ -1047,12 +1053,12 @@ export const pythonToolRenderer = {
|
|
|
1047
1053
|
);
|
|
1048
1054
|
|
|
1049
1055
|
if (!combinedOutput && statusLines.length === 0) {
|
|
1050
|
-
const lines = [timeoutLine, warningLine].filter(Boolean) as string[];
|
|
1056
|
+
const lines = [timeoutLine, noticeLine, warningLine].filter(Boolean) as string[];
|
|
1051
1057
|
return new Text(lines.join("\n"), 0, 0);
|
|
1052
1058
|
}
|
|
1053
1059
|
|
|
1054
1060
|
if (!combinedOutput && statusLines.length > 0) {
|
|
1055
|
-
const lines = [uiTheme.fg("dim", "Status"), ...statusLines, timeoutLine, warningLine].filter(
|
|
1061
|
+
const lines = [uiTheme.fg("dim", "Status"), ...statusLines, timeoutLine, noticeLine, warningLine].filter(
|
|
1056
1062
|
Boolean,
|
|
1057
1063
|
) as string[];
|
|
1058
1064
|
return new Text(lines.join("\n"), 0, 0);
|
|
@@ -1067,6 +1073,7 @@ export const pythonToolRenderer = {
|
|
|
1067
1073
|
styledOutput,
|
|
1068
1074
|
...(statusLines.length > 0 ? [uiTheme.fg("dim", "Status"), ...statusLines] : []),
|
|
1069
1075
|
timeoutLine,
|
|
1076
|
+
noticeLine,
|
|
1070
1077
|
warningLine,
|
|
1071
1078
|
].filter(Boolean) as string[];
|
|
1072
1079
|
return new Text(lines.join("\n"), 0, 0);
|
|
@@ -1085,8 +1092,7 @@ export const pythonToolRenderer = {
|
|
|
1085
1092
|
|
|
1086
1093
|
return {
|
|
1087
1094
|
render: (width: number): string[] => {
|
|
1088
|
-
|
|
1089
|
-
const previewLines = options.renderContext?.previewLines ?? PYTHON_DEFAULT_PREVIEW_LINES;
|
|
1095
|
+
const previewLines = options.renderContext?.previewLines ?? EVAL_DEFAULT_PREVIEW_LINES;
|
|
1090
1096
|
if (cachedLines === undefined || cachedWidth !== width || cachedPreviewLines !== previewLines) {
|
|
1091
1097
|
const result = truncateToVisualLines(textContent, previewLines, width);
|
|
1092
1098
|
cachedLines = result.visualLines;
|
|
@@ -1113,6 +1119,9 @@ export const pythonToolRenderer = {
|
|
|
1113
1119
|
if (timeoutLine) {
|
|
1114
1120
|
outputLines.push(truncateToWidth(timeoutLine, width));
|
|
1115
1121
|
}
|
|
1122
|
+
if (noticeLine) {
|
|
1123
|
+
outputLines.push(truncateToWidth(noticeLine, width));
|
|
1124
|
+
}
|
|
1116
1125
|
if (warningLine) {
|
|
1117
1126
|
outputLines.push(truncateToWidth(warningLine, width));
|
|
1118
1127
|
}
|