@oh-my-pi/pi-coding-agent 15.7.5 → 15.7.6
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 +30 -1
- package/dist/types/config/settings-schema.d.ts +9 -0
- package/dist/types/eval/__tests__/heartbeat.test.d.ts +1 -0
- package/dist/types/eval/heartbeat.d.ts +45 -0
- package/dist/types/extensibility/extensions/loader.d.ts +2 -2
- package/dist/types/extensibility/extensions/runner.d.ts +2 -1
- package/dist/types/extensibility/extensions/types.d.ts +24 -5
- package/dist/types/lsp/diagnostics-ledger.d.ts +10 -0
- package/dist/types/lsp/index.d.ts +2 -0
- package/dist/types/lsp/utils.d.ts +4 -0
- package/dist/types/modes/acp/acp-agent.d.ts +1 -1
- package/dist/types/modes/components/assistant-message.d.ts +3 -1
- package/dist/types/modes/components/custom-editor.d.ts +0 -1
- package/dist/types/modes/components/hook-selector.d.ts +7 -1
- package/dist/types/modes/controllers/extension-ui-controller.d.ts +2 -2
- package/dist/types/modes/interactive-mode.d.ts +2 -2
- package/dist/types/modes/rpc/rpc-mode.d.ts +1 -1
- package/dist/types/modes/theme/theme.d.ts +1 -1
- package/dist/types/modes/types.d.ts +2 -2
- package/dist/types/session/agent-session.d.ts +8 -1
- package/dist/types/tools/ask.d.ts +8 -6
- package/dist/types/tools/eval-backends.d.ts +12 -0
- package/dist/types/tools/eval-render.d.ts +52 -0
- package/dist/types/tools/eval.d.ts +2 -35
- package/dist/types/tools/index.d.ts +4 -11
- package/dist/types/tui/output-block.d.ts +4 -3
- package/examples/extensions/README.md +1 -0
- package/examples/extensions/thinking-note.ts +13 -0
- package/package.json +9 -9
- package/src/config/model-registry.ts +33 -4
- package/src/config/settings-schema.ts +10 -0
- package/src/discovery/claude.ts +41 -22
- package/src/edit/index.ts +23 -3
- package/src/eval/__tests__/agent-bridge.test.ts +90 -0
- package/src/eval/__tests__/heartbeat.test.ts +84 -0
- package/src/eval/__tests__/llm-bridge.test.ts +30 -0
- package/src/eval/agent-bridge.ts +44 -38
- package/src/eval/heartbeat.ts +74 -0
- package/src/eval/js/executor.ts +13 -9
- package/src/eval/llm-bridge.ts +20 -14
- package/src/eval/py/executor.ts +14 -18
- package/src/exec/bash-executor.ts +31 -5
- package/src/extensibility/extensions/loader.ts +16 -18
- package/src/extensibility/extensions/runner.ts +22 -17
- package/src/extensibility/extensions/types.ts +39 -5
- package/src/internal-urls/docs-index.generated.ts +3 -3
- package/src/lsp/diagnostics-ledger.ts +51 -0
- package/src/lsp/index.ts +9 -22
- package/src/lsp/utils.ts +21 -0
- package/src/modes/acp/acp-agent.ts +8 -4
- package/src/modes/components/assistant-message.ts +28 -1
- package/src/modes/components/custom-editor.ts +9 -7
- package/src/modes/components/hook-selector.ts +159 -32
- package/src/modes/components/tool-execution.ts +20 -4
- package/src/modes/controllers/event-controller.ts +5 -2
- package/src/modes/controllers/extension-ui-controller.ts +3 -2
- package/src/modes/controllers/input-controller.ts +0 -15
- package/src/modes/interactive-mode.ts +2 -1
- package/src/modes/rpc/rpc-mode.ts +17 -6
- package/src/modes/theme/theme-schema.json +30 -0
- package/src/modes/theme/theme.ts +39 -2
- package/src/modes/types.ts +2 -1
- package/src/modes/utils/ui-helpers.ts +5 -2
- package/src/prompts/tools/ask.md +2 -1
- package/src/prompts/tools/eval.md +1 -1
- package/src/session/agent-session.ts +65 -11
- package/src/tools/ask.ts +74 -32
- package/src/tools/eval-backends.ts +38 -0
- package/src/tools/eval-render.ts +750 -0
- package/src/tools/eval.ts +27 -754
- package/src/tools/index.ts +7 -37
- package/src/tools/renderers.ts +1 -1
- package/src/tools/write.ts +9 -1
- package/src/tui/output-block.ts +5 -4
|
@@ -0,0 +1,750 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TUI rendering for the eval tool.
|
|
3
|
+
*
|
|
4
|
+
* Split out from `eval.ts` so the renderer can be imported by `renderers.ts`
|
|
5
|
+
* without dragging the eval *runtime* (JS/Python backends -> agent bridge ->
|
|
6
|
+
* task executor -> sdk -> extension loader -> root barrel) into the renderer
|
|
7
|
+
* module graph. That transitive chain re-enters `renderers.ts` while `eval.ts`
|
|
8
|
+
* is still initializing, which previously crashed module load with a TDZ
|
|
9
|
+
* `Cannot access 'evalToolRenderer' before initialization`.
|
|
10
|
+
*/
|
|
11
|
+
import type { Component } from "@oh-my-pi/pi-tui";
|
|
12
|
+
import { Markdown, Text } from "@oh-my-pi/pi-tui";
|
|
13
|
+
import { formatNumber } from "@oh-my-pi/pi-utils";
|
|
14
|
+
import { settings } from "../config/settings";
|
|
15
|
+
import type { EvalCellResult, EvalLanguage, EvalStatusEvent, EvalToolDetails } from "../eval/types";
|
|
16
|
+
import type { RenderResultOptions } from "../extensibility/custom-tools/types";
|
|
17
|
+
import { formatContextUsage } from "../modes/components/status-line/context-thresholds";
|
|
18
|
+
import { truncateToVisualLines } from "../modes/components/visual-truncate";
|
|
19
|
+
import { shimmerEnabled } from "../modes/theme/shimmer";
|
|
20
|
+
import { getMarkdownTheme, type Theme } from "../modes/theme/theme";
|
|
21
|
+
import { borderShimmerTick, renderCodeCell } from "../tui";
|
|
22
|
+
import {
|
|
23
|
+
JSON_TREE_MAX_DEPTH_COLLAPSED,
|
|
24
|
+
JSON_TREE_MAX_DEPTH_EXPANDED,
|
|
25
|
+
JSON_TREE_MAX_LINES_COLLAPSED,
|
|
26
|
+
JSON_TREE_MAX_LINES_EXPANDED,
|
|
27
|
+
JSON_TREE_SCALAR_LEN_COLLAPSED,
|
|
28
|
+
JSON_TREE_SCALAR_LEN_EXPANDED,
|
|
29
|
+
renderJsonTreeLines,
|
|
30
|
+
} from "./json-tree";
|
|
31
|
+
import { formatStyledTruncationWarning, stripOutputNotice } from "./output-meta";
|
|
32
|
+
import {
|
|
33
|
+
formatBadge,
|
|
34
|
+
formatDuration,
|
|
35
|
+
formatStatusIcon,
|
|
36
|
+
formatTitle,
|
|
37
|
+
replaceTabs,
|
|
38
|
+
shortenPath,
|
|
39
|
+
truncateToWidth,
|
|
40
|
+
wrapBrackets,
|
|
41
|
+
} from "./render-utils";
|
|
42
|
+
|
|
43
|
+
export const EVAL_DEFAULT_PREVIEW_LINES = 10;
|
|
44
|
+
|
|
45
|
+
function languageForHighlighter(language: EvalLanguage | undefined): "python" | "javascript" {
|
|
46
|
+
return language === "js" ? "javascript" : "python";
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
interface EvalRenderCellArg {
|
|
50
|
+
language?: string;
|
|
51
|
+
code?: string;
|
|
52
|
+
title?: string;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
interface EvalRenderArgs {
|
|
56
|
+
cells?: EvalRenderCellArg[];
|
|
57
|
+
__partialJson?: string;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
interface EvalRenderContext {
|
|
61
|
+
output?: string;
|
|
62
|
+
expanded?: boolean;
|
|
63
|
+
previewLines?: number;
|
|
64
|
+
timeout?: number;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
interface EvalRenderCell {
|
|
68
|
+
language: EvalLanguage;
|
|
69
|
+
code: string;
|
|
70
|
+
title?: string;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function normalizeRenderLanguage(value: string | undefined): EvalLanguage {
|
|
74
|
+
return value === "js" ? "js" : "python";
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function getRenderCells(args: EvalRenderArgs | undefined): EvalRenderCell[] {
|
|
78
|
+
const raw = args?.cells;
|
|
79
|
+
if (!Array.isArray(raw)) return [];
|
|
80
|
+
const out: EvalRenderCell[] = [];
|
|
81
|
+
for (const cell of raw) {
|
|
82
|
+
if (!cell || typeof cell !== "object") continue;
|
|
83
|
+
const code = typeof cell.code === "string" ? cell.code : "";
|
|
84
|
+
out.push({
|
|
85
|
+
language: normalizeRenderLanguage(typeof cell.language === "string" ? cell.language : undefined),
|
|
86
|
+
code,
|
|
87
|
+
title: typeof cell.title === "string" ? cell.title : undefined,
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
return out;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
type AgentEventStatus = "pending" | "running" | "completed" | "failed" | "aborted";
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Append or replace a status event. `agent` events are progress snapshots keyed
|
|
97
|
+
* by `id`, so they coalesce in place (preserving first-seen order); every other
|
|
98
|
+
* op is a discrete action and simply appends. Keeps the persisted event list
|
|
99
|
+
* bounded even when a subagent emits hundreds of throttled progress ticks.
|
|
100
|
+
*/
|
|
101
|
+
export function upsertStatusEvent(events: EvalStatusEvent[], event: EvalStatusEvent): void {
|
|
102
|
+
if (event.op === "agent" && typeof event.id === "string") {
|
|
103
|
+
const id = event.id;
|
|
104
|
+
const idx = events.findIndex(e => e.op === "agent" && e.id === id);
|
|
105
|
+
if (idx >= 0) {
|
|
106
|
+
events[idx] = event;
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
events.push(event);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function eventString(value: unknown): string | undefined {
|
|
114
|
+
return typeof value === "string" && value.length > 0 ? value : undefined;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function eventNumber(value: unknown): number {
|
|
118
|
+
return typeof value === "number" && Number.isFinite(value) ? value : 0;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function agentEventStatus(value: unknown): AgentEventStatus {
|
|
122
|
+
switch (value) {
|
|
123
|
+
case "pending":
|
|
124
|
+
case "running":
|
|
125
|
+
case "completed":
|
|
126
|
+
case "failed":
|
|
127
|
+
case "aborted":
|
|
128
|
+
return value;
|
|
129
|
+
default:
|
|
130
|
+
return "running";
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** Append the toolCount · context · cost · model stat run, mirroring the task tool. */
|
|
135
|
+
function formatAgentStats(event: EvalStatusEvent, theme: Theme): string {
|
|
136
|
+
let line = "";
|
|
137
|
+
const toolCount = eventNumber(event.toolCount);
|
|
138
|
+
if (toolCount > 0) {
|
|
139
|
+
line += `${theme.sep.dot}${theme.fg("dim", `${formatNumber(toolCount)} ${theme.icon.extensionTool}`)}`;
|
|
140
|
+
}
|
|
141
|
+
const contextTokens = eventNumber(event.contextTokens);
|
|
142
|
+
if (contextTokens > 0) {
|
|
143
|
+
const contextWindow = eventNumber(event.contextWindow);
|
|
144
|
+
const ctx =
|
|
145
|
+
contextWindow > 0
|
|
146
|
+
? formatContextUsage((contextTokens / contextWindow) * 100, contextWindow)
|
|
147
|
+
: formatNumber(contextTokens);
|
|
148
|
+
line += `${theme.sep.dot}${theme.fg("dim", ctx)}`;
|
|
149
|
+
}
|
|
150
|
+
const cost = eventNumber(event.cost);
|
|
151
|
+
if (cost > 0) {
|
|
152
|
+
line += `${theme.sep.dot}${theme.fg("statusLineCost", `$${cost.toFixed(2)}`)}`;
|
|
153
|
+
}
|
|
154
|
+
const model = eventString(event.model);
|
|
155
|
+
if (model && settings.get("task.showResolvedModelBadge")) {
|
|
156
|
+
line += `${theme.sep.dot}${theme.fg("dim", truncateToWidth(replaceTabs(model), 30))}`;
|
|
157
|
+
}
|
|
158
|
+
return line;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Render coalesced `agent()` progress as a Task-tool-style tree, one entry per
|
|
163
|
+
* subagent: a status line (icon · id · stats) plus, while running, the current
|
|
164
|
+
* tool/intent. Drawn below the cell box so progress streams live.
|
|
165
|
+
*/
|
|
166
|
+
function renderAgentProgressEvents(events: EvalStatusEvent[], theme: Theme, spinnerFrame?: number): string[] {
|
|
167
|
+
const lines: string[] = [];
|
|
168
|
+
for (let i = 0; i < events.length; i++) {
|
|
169
|
+
const event = events[i];
|
|
170
|
+
const isLast = i === events.length - 1;
|
|
171
|
+
const prefix = theme.fg("dim", isLast ? theme.tree.last : theme.tree.branch);
|
|
172
|
+
const cont = isLast ? " " : `${theme.fg("dim", theme.tree.vertical)} `;
|
|
173
|
+
|
|
174
|
+
const status = agentEventStatus(event.status);
|
|
175
|
+
const iconStatus =
|
|
176
|
+
status === "completed"
|
|
177
|
+
? "success"
|
|
178
|
+
: status === "failed"
|
|
179
|
+
? "error"
|
|
180
|
+
: status === "aborted"
|
|
181
|
+
? "aborted"
|
|
182
|
+
: status === "pending"
|
|
183
|
+
? "pending"
|
|
184
|
+
: "running";
|
|
185
|
+
const iconColor =
|
|
186
|
+
status === "completed" ? "success" : status === "failed" || status === "aborted" ? "error" : "accent";
|
|
187
|
+
const icon = formatStatusIcon(iconStatus, theme, status === "running" ? spinnerFrame : undefined);
|
|
188
|
+
|
|
189
|
+
const id = eventString(event.id) ?? "agent";
|
|
190
|
+
let line = `${prefix} ${theme.fg(iconColor, icon)} ${theme.fg("accent", theme.bold(id))}`;
|
|
191
|
+
|
|
192
|
+
if (status === "failed" || status === "aborted") {
|
|
193
|
+
line += ` ${formatBadge(status, iconColor, theme)}`;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const currentTool = eventString(event.currentTool);
|
|
197
|
+
const lastIntent = eventString(event.lastIntent);
|
|
198
|
+
if (status === "running" && !currentTool && !lastIntent) {
|
|
199
|
+
const preview = eventString(event.taskPreview);
|
|
200
|
+
if (preview) line += ` ${theme.fg("muted", truncateToWidth(replaceTabs(preview), 48))}`;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
line += formatAgentStats(event, theme);
|
|
204
|
+
if (status === "completed" || status === "failed" || status === "aborted") {
|
|
205
|
+
const durationMs = eventNumber(event.durationMs);
|
|
206
|
+
if (durationMs > 0) line += `${theme.sep.dot}${theme.fg("dim", formatDuration(durationMs))}`;
|
|
207
|
+
}
|
|
208
|
+
lines.push(line);
|
|
209
|
+
|
|
210
|
+
if (status === "running") {
|
|
211
|
+
if (currentTool) {
|
|
212
|
+
let toolLine = `${cont}${theme.tree.hook} ${theme.fg("muted", currentTool)}`;
|
|
213
|
+
const detail = lastIntent ?? eventString(event.currentToolArgs);
|
|
214
|
+
if (detail) toolLine += `: ${theme.fg("dim", truncateToWidth(replaceTabs(detail), 48))}`;
|
|
215
|
+
lines.push(toolLine);
|
|
216
|
+
} else if (lastIntent) {
|
|
217
|
+
lines.push(`${cont}${theme.tree.hook} ${theme.fg("dim", truncateToWidth(replaceTabs(lastIntent), 48))}`);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
return lines;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/** Format a status event as a single line for display. */
|
|
225
|
+
function formatStatusEvent(event: EvalStatusEvent, theme: Theme): string {
|
|
226
|
+
const { op, ...data } = event;
|
|
227
|
+
|
|
228
|
+
type AvailableIcon = "icon.file" | "icon.folder" | "icon.git" | "icon.package";
|
|
229
|
+
const opIcons: Record<string, AvailableIcon> = {
|
|
230
|
+
read: "icon.file",
|
|
231
|
+
write: "icon.file",
|
|
232
|
+
append: "icon.file",
|
|
233
|
+
cat: "icon.file",
|
|
234
|
+
touch: "icon.file",
|
|
235
|
+
ls: "icon.folder",
|
|
236
|
+
cd: "icon.folder",
|
|
237
|
+
pwd: "icon.folder",
|
|
238
|
+
mkdir: "icon.folder",
|
|
239
|
+
tree: "icon.folder",
|
|
240
|
+
git_status: "icon.git",
|
|
241
|
+
git_diff: "icon.git",
|
|
242
|
+
git_log: "icon.git",
|
|
243
|
+
git_show: "icon.git",
|
|
244
|
+
git_branch: "icon.git",
|
|
245
|
+
git_file_at: "icon.git",
|
|
246
|
+
git_has_changes: "icon.git",
|
|
247
|
+
run: "icon.package",
|
|
248
|
+
sh: "icon.package",
|
|
249
|
+
env: "icon.package",
|
|
250
|
+
batch: "icon.package",
|
|
251
|
+
llm: "icon.package",
|
|
252
|
+
log: "icon.package",
|
|
253
|
+
phase: "icon.package",
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
const iconKey = opIcons[op] ?? "icon.file";
|
|
257
|
+
const icon = theme.styledSymbol(iconKey, "muted");
|
|
258
|
+
|
|
259
|
+
const parts: string[] = [];
|
|
260
|
+
|
|
261
|
+
if (data.error) {
|
|
262
|
+
return `${icon} ${theme.fg("warning", op)}: ${theme.fg("dim", String(data.error))}`;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
switch (op) {
|
|
266
|
+
case "read":
|
|
267
|
+
parts.push(`${data.chars ?? data.bytes ?? 0} chars`);
|
|
268
|
+
if (data.path) parts.push(`from ${shortenPath(String(data.path))}`);
|
|
269
|
+
break;
|
|
270
|
+
case "write":
|
|
271
|
+
case "append":
|
|
272
|
+
parts.push(`${data.chars ?? data.bytes ?? 0} chars`);
|
|
273
|
+
if (data.path) parts.push(`to ${shortenPath(String(data.path))}`);
|
|
274
|
+
break;
|
|
275
|
+
case "cat":
|
|
276
|
+
parts.push(`${data.files} file${(data.files as number) !== 1 ? "s" : ""}`);
|
|
277
|
+
parts.push(`${data.chars} chars`);
|
|
278
|
+
break;
|
|
279
|
+
case "ls":
|
|
280
|
+
parts.push(`${data.count} entr${(data.count as number) !== 1 ? "ies" : "y"}`);
|
|
281
|
+
break;
|
|
282
|
+
case "env":
|
|
283
|
+
if (data.action === "set") {
|
|
284
|
+
parts.push(`set ${data.key}=${truncateToWidth(String(data.value ?? ""), 30)}`);
|
|
285
|
+
} else if (data.action === "get") {
|
|
286
|
+
parts.push(`${data.key}=${truncateToWidth(String(data.value ?? ""), 30)}`);
|
|
287
|
+
} else {
|
|
288
|
+
parts.push(`${data.count} variable${(data.count as number) !== 1 ? "s" : ""}`);
|
|
289
|
+
}
|
|
290
|
+
break;
|
|
291
|
+
case "git_status":
|
|
292
|
+
if (data.clean) {
|
|
293
|
+
parts.push("clean");
|
|
294
|
+
} else {
|
|
295
|
+
const statusParts: string[] = [];
|
|
296
|
+
if (data.staged) statusParts.push(`${data.staged} staged`);
|
|
297
|
+
if (data.modified) statusParts.push(`${data.modified} modified`);
|
|
298
|
+
if (data.untracked) statusParts.push(`${data.untracked} untracked`);
|
|
299
|
+
parts.push(statusParts.join(", ") || "unknown");
|
|
300
|
+
}
|
|
301
|
+
if (data.branch) parts.push(`on ${data.branch}`);
|
|
302
|
+
break;
|
|
303
|
+
case "git_log":
|
|
304
|
+
parts.push(`${data.commits} commit${(data.commits as number) !== 1 ? "s" : ""}`);
|
|
305
|
+
break;
|
|
306
|
+
case "git_diff":
|
|
307
|
+
parts.push(`${data.lines} line${(data.lines as number) !== 1 ? "s" : ""}`);
|
|
308
|
+
if (data.staged) parts.push("(staged)");
|
|
309
|
+
break;
|
|
310
|
+
case "diff":
|
|
311
|
+
if (data.identical) {
|
|
312
|
+
parts.push("files identical");
|
|
313
|
+
} else {
|
|
314
|
+
parts.push("files differ");
|
|
315
|
+
}
|
|
316
|
+
break;
|
|
317
|
+
case "batch":
|
|
318
|
+
parts.push(`${data.files} file${(data.files as number) !== 1 ? "s" : ""} processed`);
|
|
319
|
+
break;
|
|
320
|
+
case "llm":
|
|
321
|
+
if (data.model) parts.push(String(data.model));
|
|
322
|
+
if (data.tier && data.tier !== data.model) parts.push(`(${data.tier})`);
|
|
323
|
+
parts.push(`${data.chars ?? 0} chars`);
|
|
324
|
+
break;
|
|
325
|
+
case "wc":
|
|
326
|
+
parts.push(`${data.lines}L ${data.words}W ${data.chars}C`);
|
|
327
|
+
break;
|
|
328
|
+
case "cd":
|
|
329
|
+
case "pwd":
|
|
330
|
+
case "mkdir":
|
|
331
|
+
case "touch":
|
|
332
|
+
if (data.path) parts.push(shortenPath(String(data.path)));
|
|
333
|
+
break;
|
|
334
|
+
case "log":
|
|
335
|
+
parts.push(String(data.message ?? ""));
|
|
336
|
+
break;
|
|
337
|
+
case "phase":
|
|
338
|
+
parts.push(String(data.title ?? ""));
|
|
339
|
+
break;
|
|
340
|
+
default:
|
|
341
|
+
if (data.count !== undefined) {
|
|
342
|
+
parts.push(String(data.count));
|
|
343
|
+
}
|
|
344
|
+
if (data.path) {
|
|
345
|
+
parts.push(shortenPath(String(data.path)));
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
const desc = parts.length > 0 ? parts.join(" · ") : "";
|
|
350
|
+
return `${icon} ${theme.fg("muted", op)}${desc ? ` ${theme.fg("dim", desc)}` : ""}`;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/** Format status event with expanded detail lines. */
|
|
354
|
+
function formatStatusEventExpanded(event: EvalStatusEvent, theme: Theme): string[] {
|
|
355
|
+
const lines: string[] = [];
|
|
356
|
+
const { op, ...data } = event;
|
|
357
|
+
|
|
358
|
+
lines.push(formatStatusEvent(event, theme));
|
|
359
|
+
|
|
360
|
+
const addItems = (items: unknown[], formatter: (item: unknown) => string, max = 5) => {
|
|
361
|
+
const arr = Array.isArray(items) ? items : [];
|
|
362
|
+
for (let i = 0; i < Math.min(arr.length, max); i++) {
|
|
363
|
+
lines.push(` ${theme.fg("dim", formatter(arr[i]))}`);
|
|
364
|
+
}
|
|
365
|
+
if (arr.length > max) {
|
|
366
|
+
lines.push(` ${theme.fg("dim", `… ${arr.length - max} more`)}`);
|
|
367
|
+
}
|
|
368
|
+
};
|
|
369
|
+
|
|
370
|
+
const addPreview = (preview: string, maxLines = 3) => {
|
|
371
|
+
const previewLines = String(preview).split("\n").slice(0, maxLines);
|
|
372
|
+
for (const line of previewLines) {
|
|
373
|
+
lines.push(` ${theme.fg("toolOutput", truncateToWidth(replaceTabs(line), 80))}`);
|
|
374
|
+
}
|
|
375
|
+
const totalLines = String(preview).split("\n").length;
|
|
376
|
+
if (totalLines > maxLines) {
|
|
377
|
+
lines.push(` ${theme.fg("dim", `… ${totalLines - maxLines} more lines`)}`);
|
|
378
|
+
}
|
|
379
|
+
};
|
|
380
|
+
|
|
381
|
+
switch (op) {
|
|
382
|
+
case "ls":
|
|
383
|
+
if (data.items) addItems(data.items as unknown[], m => String(m));
|
|
384
|
+
break;
|
|
385
|
+
case "env":
|
|
386
|
+
if (data.keys) addItems(data.keys as unknown[], k => String(k), 10);
|
|
387
|
+
break;
|
|
388
|
+
case "git_log":
|
|
389
|
+
if (data.entries) {
|
|
390
|
+
addItems(data.entries as unknown[], e => {
|
|
391
|
+
const entry = e as { sha: string; subject: string };
|
|
392
|
+
return `${entry.sha} ${truncateToWidth(entry.subject, 50)}`;
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
break;
|
|
396
|
+
case "git_status":
|
|
397
|
+
if (data.files) addItems(data.files as unknown[], f => String(f));
|
|
398
|
+
break;
|
|
399
|
+
case "git_branch":
|
|
400
|
+
if (data.branches) addItems(data.branches as unknown[], b => String(b));
|
|
401
|
+
break;
|
|
402
|
+
case "read":
|
|
403
|
+
case "cat":
|
|
404
|
+
case "head":
|
|
405
|
+
case "tail":
|
|
406
|
+
case "tree":
|
|
407
|
+
case "diff":
|
|
408
|
+
case "git_diff":
|
|
409
|
+
case "sh":
|
|
410
|
+
if (data.preview) addPreview(String(data.preview));
|
|
411
|
+
break;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
return lines;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/** Render status events as tree lines. */
|
|
418
|
+
function renderStatusEvents(events: EvalStatusEvent[], theme: Theme, expanded: boolean): string[] {
|
|
419
|
+
if (events.length === 0) return [];
|
|
420
|
+
|
|
421
|
+
const maxCollapsed = 3;
|
|
422
|
+
const maxExpanded = 10;
|
|
423
|
+
const displayCount = expanded ? Math.min(events.length, maxExpanded) : Math.min(events.length, maxCollapsed);
|
|
424
|
+
|
|
425
|
+
const lines: string[] = [];
|
|
426
|
+
for (let i = 0; i < displayCount; i++) {
|
|
427
|
+
const isLast = i === displayCount - 1 && (expanded || events.length <= maxCollapsed);
|
|
428
|
+
const branch = isLast ? theme.tree.last : theme.tree.branch;
|
|
429
|
+
|
|
430
|
+
if (expanded) {
|
|
431
|
+
const eventLines = formatStatusEventExpanded(events[i], theme);
|
|
432
|
+
lines.push(`${theme.fg("dim", branch)} ${eventLines[0]}`);
|
|
433
|
+
const continueBranch = isLast ? " " : `${theme.tree.vertical} `;
|
|
434
|
+
for (let j = 1; j < eventLines.length; j++) {
|
|
435
|
+
lines.push(`${theme.fg("dim", continueBranch)}${eventLines[j]}`);
|
|
436
|
+
}
|
|
437
|
+
} else {
|
|
438
|
+
lines.push(`${theme.fg("dim", branch)} ${formatStatusEvent(events[i], theme)}`);
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
if (!expanded && events.length > maxCollapsed) {
|
|
443
|
+
lines.push(`${theme.fg("dim", theme.tree.last)} ${theme.fg("dim", `… ${events.length - maxCollapsed} more`)}`);
|
|
444
|
+
} else if (expanded && events.length > maxExpanded) {
|
|
445
|
+
lines.push(`${theme.fg("dim", theme.tree.last)} ${theme.fg("dim", `… ${events.length - maxExpanded} more`)}`);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
return lines;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
function formatCellOutputLines(
|
|
452
|
+
cell: EvalCellResult,
|
|
453
|
+
expanded: boolean,
|
|
454
|
+
previewLines: number,
|
|
455
|
+
theme: Theme,
|
|
456
|
+
width: number,
|
|
457
|
+
): { lines: string[]; hiddenCount: number } {
|
|
458
|
+
if (!cell.output) {
|
|
459
|
+
return { lines: [], hiddenCount: 0 };
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
if (cell.hasMarkdown && cell.status !== "error") {
|
|
463
|
+
const md = new Markdown(cell.output, 0, 0, getMarkdownTheme());
|
|
464
|
+
const allLines = md.render(width);
|
|
465
|
+
const displayLines = expanded ? allLines : allLines.slice(-previewLines);
|
|
466
|
+
const hiddenCount = allLines.length - displayLines.length;
|
|
467
|
+
return { lines: displayLines, hiddenCount };
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
const rawLines = cell.output.split("\n");
|
|
471
|
+
const displayLines = expanded ? rawLines : rawLines.slice(-previewLines);
|
|
472
|
+
const hiddenCount = rawLines.length - displayLines.length;
|
|
473
|
+
const outputLines = displayLines.map(line => {
|
|
474
|
+
const cleaned = replaceTabs(line);
|
|
475
|
+
return cell.status === "error" ? theme.fg("error", cleaned) : theme.fg("toolOutput", cleaned);
|
|
476
|
+
});
|
|
477
|
+
|
|
478
|
+
return { lines: outputLines, hiddenCount };
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
export const evalToolRenderer = {
|
|
482
|
+
renderCall(args: EvalRenderArgs, options: RenderResultOptions, uiTheme: Theme): Component {
|
|
483
|
+
const cells = getRenderCells(args);
|
|
484
|
+
|
|
485
|
+
if (cells.length === 0) {
|
|
486
|
+
const promptSym = uiTheme.fg("accent", ">>>");
|
|
487
|
+
const text = formatTitle(`${promptSym} …`, uiTheme);
|
|
488
|
+
return new Text(text, 0, 0);
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
let cached: { key: string; width: number; result: string[] } | undefined;
|
|
492
|
+
|
|
493
|
+
return {
|
|
494
|
+
render: (width: number): string[] => {
|
|
495
|
+
const animate = options.isPartial && shimmerEnabled();
|
|
496
|
+
const key = `${animate ? borderShimmerTick() : 0}|${cells.map(c => `${c.language}:${c.title ?? ""}:${c.code.length}`).join("|")}`;
|
|
497
|
+
if (cached && cached.key === key && cached.width === width) {
|
|
498
|
+
return cached.result;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
const lines: string[] = [];
|
|
502
|
+
for (let i = 0; i < cells.length; i++) {
|
|
503
|
+
const cell = cells[i];
|
|
504
|
+
const cellLines = renderCodeCell(
|
|
505
|
+
{
|
|
506
|
+
code: cell.code,
|
|
507
|
+
language: languageForHighlighter(cell.language),
|
|
508
|
+
index: i,
|
|
509
|
+
total: cells.length,
|
|
510
|
+
title: cell.title,
|
|
511
|
+
status: "pending",
|
|
512
|
+
width,
|
|
513
|
+
codeMaxLines: EVAL_DEFAULT_PREVIEW_LINES,
|
|
514
|
+
expanded: true,
|
|
515
|
+
animate,
|
|
516
|
+
},
|
|
517
|
+
uiTheme,
|
|
518
|
+
);
|
|
519
|
+
lines.push(...cellLines);
|
|
520
|
+
if (i < cells.length - 1) {
|
|
521
|
+
lines.push("");
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
cached = { key, width, result: lines };
|
|
525
|
+
return lines;
|
|
526
|
+
},
|
|
527
|
+
invalidate: () => {
|
|
528
|
+
cached = undefined;
|
|
529
|
+
},
|
|
530
|
+
};
|
|
531
|
+
},
|
|
532
|
+
|
|
533
|
+
renderResult(
|
|
534
|
+
result: { content: Array<{ type: string; text?: string }>; details?: EvalToolDetails },
|
|
535
|
+
options: RenderResultOptions & { renderContext?: EvalRenderContext },
|
|
536
|
+
uiTheme: Theme,
|
|
537
|
+
_args?: EvalRenderArgs,
|
|
538
|
+
): Component {
|
|
539
|
+
const details = result.details;
|
|
540
|
+
|
|
541
|
+
const rawOutput =
|
|
542
|
+
options.renderContext?.output ?? (result.content?.find(c => c.type === "text")?.text ?? "").trimEnd();
|
|
543
|
+
// Strip the LLM-facing notice (appended by wrappedExecute) before display;
|
|
544
|
+
// the styled `warningLine` below carries the same text in ⟨…⟩ form.
|
|
545
|
+
const output = stripOutputNotice(rawOutput, details?.meta).trimEnd();
|
|
546
|
+
|
|
547
|
+
const jsonOutputs = details?.jsonOutputs ?? [];
|
|
548
|
+
const treeExpanded = options.renderContext?.expanded ?? options.expanded;
|
|
549
|
+
const treeDepth = treeExpanded ? JSON_TREE_MAX_DEPTH_EXPANDED : JSON_TREE_MAX_DEPTH_COLLAPSED;
|
|
550
|
+
const treeLineCap = treeExpanded ? JSON_TREE_MAX_LINES_EXPANDED : JSON_TREE_MAX_LINES_COLLAPSED;
|
|
551
|
+
const treeScalarLen = treeExpanded ? JSON_TREE_SCALAR_LEN_EXPANDED : JSON_TREE_SCALAR_LEN_COLLAPSED;
|
|
552
|
+
const labelOutputs = jsonOutputs.length > 1;
|
|
553
|
+
const jsonLines = jsonOutputs.flatMap((value, index) => {
|
|
554
|
+
const tree = renderJsonTreeLines(value, uiTheme, treeDepth, treeLineCap, treeScalarLen);
|
|
555
|
+
const body = tree.truncated ? [...tree.lines, uiTheme.fg("dim", "…")] : tree.lines;
|
|
556
|
+
return labelOutputs ? [uiTheme.fg("dim", `display[${index + 1}]`), ...body] : body;
|
|
557
|
+
});
|
|
558
|
+
|
|
559
|
+
const timeoutSeconds = options.renderContext?.timeout;
|
|
560
|
+
const timeoutLine =
|
|
561
|
+
typeof timeoutSeconds === "number"
|
|
562
|
+
? uiTheme.fg("dim", wrapBrackets(`Timeout: ${timeoutSeconds}s`, uiTheme))
|
|
563
|
+
: undefined;
|
|
564
|
+
let warningLine: string | undefined;
|
|
565
|
+
if (details?.meta?.truncation) {
|
|
566
|
+
warningLine = formatStyledTruncationWarning(details.meta, uiTheme) ?? undefined;
|
|
567
|
+
}
|
|
568
|
+
const noticeLine = details?.notice ? uiTheme.fg("dim", wrapBrackets(details.notice, uiTheme)) : undefined;
|
|
569
|
+
|
|
570
|
+
const cellResults = details?.cells;
|
|
571
|
+
if (cellResults && cellResults.length > 0) {
|
|
572
|
+
let cached: { key: string; width: number; result: string[] } | undefined;
|
|
573
|
+
|
|
574
|
+
return {
|
|
575
|
+
render: (width: number): string[] => {
|
|
576
|
+
const expanded = options.renderContext?.expanded ?? options.expanded;
|
|
577
|
+
const previewLines = options.renderContext?.previewLines ?? EVAL_DEFAULT_PREVIEW_LINES;
|
|
578
|
+
const animate = options.isPartial && shimmerEnabled();
|
|
579
|
+
const key = `${expanded}|${previewLines}|${options.spinnerFrame}|${animate ? borderShimmerTick() : 0}`;
|
|
580
|
+
if (cached && cached.key === key && cached.width === width) {
|
|
581
|
+
return cached.result;
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
const lines: string[] = [];
|
|
585
|
+
for (let i = 0; i < cellResults.length; i++) {
|
|
586
|
+
const cell = cellResults[i];
|
|
587
|
+
const allEvents = cell.statusEvents ?? [];
|
|
588
|
+
const agentEvents = allEvents.filter(e => e.op === "agent");
|
|
589
|
+
const otherEvents = agentEvents.length > 0 ? allEvents.filter(e => e.op !== "agent") : allEvents;
|
|
590
|
+
const statusLines = renderStatusEvents(otherEvents, uiTheme, expanded);
|
|
591
|
+
const outputContent = formatCellOutputLines(cell, expanded, previewLines, uiTheme, width);
|
|
592
|
+
const outputLines = [...outputContent.lines];
|
|
593
|
+
if (!expanded && outputContent.hiddenCount > 0) {
|
|
594
|
+
outputLines.push(
|
|
595
|
+
uiTheme.fg("dim", `… ${outputContent.hiddenCount} more lines (ctrl+o to expand)`),
|
|
596
|
+
);
|
|
597
|
+
}
|
|
598
|
+
if (statusLines.length > 0) {
|
|
599
|
+
if (outputLines.length > 0) {
|
|
600
|
+
outputLines.push(uiTheme.fg("dim", "Status"));
|
|
601
|
+
}
|
|
602
|
+
outputLines.push(...statusLines);
|
|
603
|
+
}
|
|
604
|
+
const cellLines = renderCodeCell(
|
|
605
|
+
{
|
|
606
|
+
code: cell.code,
|
|
607
|
+
language: languageForHighlighter(cell.language ?? details?.language),
|
|
608
|
+
index: i,
|
|
609
|
+
total: cellResults.length,
|
|
610
|
+
title: cell.title,
|
|
611
|
+
status: cell.status,
|
|
612
|
+
spinnerFrame: options.spinnerFrame,
|
|
613
|
+
duration: cell.durationMs,
|
|
614
|
+
output: outputLines.length > 0 ? outputLines.join("\n") : undefined,
|
|
615
|
+
outputMaxLines: outputLines.length,
|
|
616
|
+
codeMaxLines: expanded ? Number.POSITIVE_INFINITY : EVAL_DEFAULT_PREVIEW_LINES,
|
|
617
|
+
expanded,
|
|
618
|
+
width,
|
|
619
|
+
animate,
|
|
620
|
+
},
|
|
621
|
+
uiTheme,
|
|
622
|
+
);
|
|
623
|
+
lines.push(...cellLines);
|
|
624
|
+
if (agentEvents.length > 0) {
|
|
625
|
+
lines.push(...renderAgentProgressEvents(agentEvents, uiTheme, options.spinnerFrame));
|
|
626
|
+
}
|
|
627
|
+
if (i < cellResults.length - 1) {
|
|
628
|
+
lines.push("");
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
if (jsonLines.length > 0) {
|
|
632
|
+
if (lines.length > 0) {
|
|
633
|
+
lines.push("");
|
|
634
|
+
}
|
|
635
|
+
lines.push(...jsonLines);
|
|
636
|
+
}
|
|
637
|
+
if (timeoutLine) {
|
|
638
|
+
lines.push(timeoutLine);
|
|
639
|
+
}
|
|
640
|
+
if (noticeLine) {
|
|
641
|
+
lines.push(noticeLine);
|
|
642
|
+
}
|
|
643
|
+
if (warningLine) {
|
|
644
|
+
lines.push(warningLine);
|
|
645
|
+
}
|
|
646
|
+
cached = { key, width, result: lines };
|
|
647
|
+
return lines;
|
|
648
|
+
},
|
|
649
|
+
invalidate: () => {
|
|
650
|
+
cached = undefined;
|
|
651
|
+
},
|
|
652
|
+
};
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
const displayOutput = output;
|
|
656
|
+
const combinedOutput = [displayOutput, ...jsonLines].filter(Boolean).join("\n");
|
|
657
|
+
|
|
658
|
+
const statusEvents = details?.statusEvents ?? [];
|
|
659
|
+
const statusLines = renderStatusEvents(
|
|
660
|
+
statusEvents,
|
|
661
|
+
uiTheme,
|
|
662
|
+
options.renderContext?.expanded ?? options.expanded,
|
|
663
|
+
);
|
|
664
|
+
|
|
665
|
+
if (!combinedOutput && statusLines.length === 0) {
|
|
666
|
+
const lines = [timeoutLine, noticeLine, warningLine].filter(Boolean) as string[];
|
|
667
|
+
return new Text(lines.join("\n"), 0, 0);
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
if (!combinedOutput && statusLines.length > 0) {
|
|
671
|
+
const lines = [uiTheme.fg("dim", "Status"), ...statusLines, timeoutLine, noticeLine, warningLine].filter(
|
|
672
|
+
Boolean,
|
|
673
|
+
) as string[];
|
|
674
|
+
return new Text(lines.join("\n"), 0, 0);
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
if (options.renderContext?.expanded ?? options.expanded) {
|
|
678
|
+
const styledOutput = combinedOutput
|
|
679
|
+
.split("\n")
|
|
680
|
+
.map(line => uiTheme.fg("toolOutput", line))
|
|
681
|
+
.join("\n");
|
|
682
|
+
const lines = [
|
|
683
|
+
styledOutput,
|
|
684
|
+
...(statusLines.length > 0 ? [uiTheme.fg("dim", "Status"), ...statusLines] : []),
|
|
685
|
+
timeoutLine,
|
|
686
|
+
noticeLine,
|
|
687
|
+
warningLine,
|
|
688
|
+
].filter(Boolean) as string[];
|
|
689
|
+
return new Text(lines.join("\n"), 0, 0);
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
const styledOutput = combinedOutput
|
|
693
|
+
.split("\n")
|
|
694
|
+
.map(line => uiTheme.fg("toolOutput", line))
|
|
695
|
+
.join("\n");
|
|
696
|
+
const textContent = `\n${styledOutput}`;
|
|
697
|
+
|
|
698
|
+
let cachedWidth: number | undefined;
|
|
699
|
+
let cachedLines: string[] | undefined;
|
|
700
|
+
let cachedSkipped: number | undefined;
|
|
701
|
+
let cachedPreviewLines: number | undefined;
|
|
702
|
+
|
|
703
|
+
return {
|
|
704
|
+
render: (width: number): string[] => {
|
|
705
|
+
const previewLines = options.renderContext?.previewLines ?? EVAL_DEFAULT_PREVIEW_LINES;
|
|
706
|
+
if (cachedLines === undefined || cachedWidth !== width || cachedPreviewLines !== previewLines) {
|
|
707
|
+
const result = truncateToVisualLines(textContent, previewLines, width);
|
|
708
|
+
cachedLines = result.visualLines;
|
|
709
|
+
cachedSkipped = result.skippedCount;
|
|
710
|
+
cachedWidth = width;
|
|
711
|
+
cachedPreviewLines = previewLines;
|
|
712
|
+
}
|
|
713
|
+
const outputLines: string[] = [];
|
|
714
|
+
if (cachedSkipped && cachedSkipped > 0) {
|
|
715
|
+
outputLines.push("");
|
|
716
|
+
const skippedLine = uiTheme.fg(
|
|
717
|
+
"dim",
|
|
718
|
+
`… (${cachedSkipped} earlier lines, showing ${cachedLines.length} of ${cachedSkipped + cachedLines.length}) (ctrl+o to expand)`,
|
|
719
|
+
);
|
|
720
|
+
outputLines.push(truncateToWidth(skippedLine, width));
|
|
721
|
+
}
|
|
722
|
+
outputLines.push(...cachedLines);
|
|
723
|
+
if (statusLines.length > 0) {
|
|
724
|
+
outputLines.push(truncateToWidth(uiTheme.fg("dim", "Status"), width));
|
|
725
|
+
for (const statusLine of statusLines) {
|
|
726
|
+
outputLines.push(truncateToWidth(statusLine, width));
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
if (timeoutLine) {
|
|
730
|
+
outputLines.push(truncateToWidth(timeoutLine, width));
|
|
731
|
+
}
|
|
732
|
+
if (noticeLine) {
|
|
733
|
+
outputLines.push(truncateToWidth(noticeLine, width));
|
|
734
|
+
}
|
|
735
|
+
if (warningLine) {
|
|
736
|
+
outputLines.push(truncateToWidth(warningLine, width));
|
|
737
|
+
}
|
|
738
|
+
return outputLines;
|
|
739
|
+
},
|
|
740
|
+
invalidate: () => {
|
|
741
|
+
cachedWidth = undefined;
|
|
742
|
+
cachedLines = undefined;
|
|
743
|
+
cachedSkipped = undefined;
|
|
744
|
+
cachedPreviewLines = undefined;
|
|
745
|
+
},
|
|
746
|
+
};
|
|
747
|
+
},
|
|
748
|
+
mergeCallAndResult: true,
|
|
749
|
+
inline: true,
|
|
750
|
+
};
|