@oh-my-pi/pi-coding-agent 5.4.2 → 5.6.7
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 +103 -0
- package/docs/python-repl.md +77 -0
- package/examples/hooks/snake.ts +7 -7
- package/package.json +5 -5
- package/src/bun-imports.d.ts +6 -0
- package/src/cli/args.ts +7 -0
- package/src/cli/setup-cli.ts +231 -0
- package/src/cli.ts +2 -0
- package/src/core/agent-session.ts +118 -15
- package/src/core/bash-executor.ts +3 -84
- package/src/core/compaction/compaction.ts +10 -5
- package/src/core/extensions/index.ts +2 -0
- package/src/core/extensions/loader.ts +13 -1
- package/src/core/extensions/runner.ts +50 -2
- package/src/core/extensions/types.ts +67 -2
- package/src/core/keybindings.ts +51 -1
- package/src/core/prompt-templates.ts +15 -0
- package/src/core/python-executor-display.test.ts +42 -0
- package/src/core/python-executor-lifecycle.test.ts +99 -0
- package/src/core/python-executor-mapping.test.ts +41 -0
- package/src/core/python-executor-per-call.test.ts +49 -0
- package/src/core/python-executor-session.test.ts +103 -0
- package/src/core/python-executor-streaming.test.ts +77 -0
- package/src/core/python-executor-timeout.test.ts +35 -0
- package/src/core/python-executor.lifecycle.test.ts +139 -0
- package/src/core/python-executor.result.test.ts +49 -0
- package/src/core/python-executor.test.ts +180 -0
- package/src/core/python-executor.ts +313 -0
- package/src/core/python-gateway-coordinator.ts +832 -0
- package/src/core/python-kernel-display.test.ts +54 -0
- package/src/core/python-kernel-env.test.ts +138 -0
- package/src/core/python-kernel-session.test.ts +87 -0
- package/src/core/python-kernel-ws.test.ts +104 -0
- package/src/core/python-kernel.lifecycle.test.ts +249 -0
- package/src/core/python-kernel.test.ts +549 -0
- package/src/core/python-kernel.ts +1178 -0
- package/src/core/python-prelude.py +889 -0
- package/src/core/python-prelude.test.ts +140 -0
- package/src/core/python-prelude.ts +3 -0
- package/src/core/sdk.ts +24 -6
- package/src/core/session-manager.ts +174 -82
- package/src/core/settings-manager-python.test.ts +23 -0
- package/src/core/settings-manager.ts +202 -0
- package/src/core/streaming-output.test.ts +26 -0
- package/src/core/streaming-output.ts +100 -0
- package/src/core/system-prompt.python.test.ts +17 -0
- package/src/core/system-prompt.ts +3 -1
- package/src/core/timings.ts +1 -1
- package/src/core/tools/bash.ts +13 -2
- package/src/core/tools/edit-diff.ts +9 -1
- package/src/core/tools/index.test.ts +50 -23
- package/src/core/tools/index.ts +83 -1
- package/src/core/tools/python-execution.test.ts +68 -0
- package/src/core/tools/python-fallback.test.ts +72 -0
- package/src/core/tools/python-renderer.test.ts +36 -0
- package/src/core/tools/python-tool-mode.test.ts +43 -0
- package/src/core/tools/python.test.ts +121 -0
- package/src/core/tools/python.ts +760 -0
- package/src/core/tools/renderers.ts +2 -0
- package/src/core/tools/schema-validation.test.ts +1 -0
- package/src/core/tools/task/executor.ts +146 -3
- package/src/core/tools/task/worker-protocol.ts +32 -2
- package/src/core/tools/task/worker.ts +182 -15
- package/src/index.ts +6 -0
- package/src/main.ts +136 -40
- package/src/modes/interactive/components/custom-editor.ts +16 -31
- package/src/modes/interactive/components/extensions/extension-dashboard.ts +5 -16
- package/src/modes/interactive/components/extensions/extension-list.ts +5 -13
- package/src/modes/interactive/components/history-search.ts +5 -8
- package/src/modes/interactive/components/hook-editor.ts +3 -4
- package/src/modes/interactive/components/hook-input.ts +3 -3
- package/src/modes/interactive/components/hook-selector.ts +5 -15
- package/src/modes/interactive/components/index.ts +1 -0
- package/src/modes/interactive/components/keybinding-hints.ts +66 -0
- package/src/modes/interactive/components/model-selector.ts +53 -66
- package/src/modes/interactive/components/oauth-selector.ts +5 -5
- package/src/modes/interactive/components/session-selector.ts +29 -23
- package/src/modes/interactive/components/settings-defs.ts +404 -196
- package/src/modes/interactive/components/settings-selector.ts +14 -10
- package/src/modes/interactive/components/status-line-segment-editor.ts +7 -7
- package/src/modes/interactive/components/tool-execution.ts +8 -0
- package/src/modes/interactive/components/tree-selector.ts +29 -23
- package/src/modes/interactive/components/user-message-selector.ts +6 -17
- package/src/modes/interactive/controllers/command-controller.ts +86 -37
- package/src/modes/interactive/controllers/event-controller.ts +8 -0
- package/src/modes/interactive/controllers/extension-ui-controller.ts +51 -0
- package/src/modes/interactive/controllers/input-controller.ts +42 -6
- package/src/modes/interactive/interactive-mode.ts +56 -30
- package/src/modes/interactive/theme/theme-schema.json +2 -2
- package/src/modes/interactive/types.ts +6 -1
- package/src/modes/interactive/utils/ui-helpers.ts +2 -1
- package/src/modes/print-mode.ts +23 -0
- package/src/modes/rpc/rpc-mode.ts +21 -0
- package/src/prompts/agents/reviewer.md +1 -1
- package/src/prompts/system/system-prompt.md +32 -1
- package/src/prompts/tools/python.md +91 -0
- package/src/prompts/tools/task.md +5 -1
|
@@ -0,0 +1,760 @@
|
|
|
1
|
+
import { relative, resolve, sep } from "node:path";
|
|
2
|
+
import type { AgentTool, AgentToolContext } from "@oh-my-pi/pi-agent-core";
|
|
3
|
+
import type { ImageContent } from "@oh-my-pi/pi-ai";
|
|
4
|
+
import type { Component } from "@oh-my-pi/pi-tui";
|
|
5
|
+
import { Text, truncateToWidth } from "@oh-my-pi/pi-tui";
|
|
6
|
+
import { Type } from "@sinclair/typebox";
|
|
7
|
+
import { truncateToVisualLines } from "../../modes/interactive/components/visual-truncate";
|
|
8
|
+
import type { Theme } from "../../modes/interactive/theme/theme";
|
|
9
|
+
import pythonDescription from "../../prompts/tools/python.md" with { type: "text" };
|
|
10
|
+
import type { RenderResultOptions } from "../custom-tools/types";
|
|
11
|
+
import { renderPromptTemplate } from "../prompt-templates";
|
|
12
|
+
import { executePython, getPreludeDocs, type PythonExecutorOptions } from "../python-executor";
|
|
13
|
+
import type { PreludeHelper, PythonStatusEvent } from "../python-kernel";
|
|
14
|
+
import type { ToolSession } from "./index";
|
|
15
|
+
import { resolveToCwd } from "./path-utils";
|
|
16
|
+
import { createToolUIKit, getTreeBranch, getTreeContinuePrefix, shortenPath, truncate } from "./render-utils";
|
|
17
|
+
import { DEFAULT_MAX_BYTES, formatSize, type TruncationResult, truncateTail } from "./truncate";
|
|
18
|
+
|
|
19
|
+
export const PYTHON_DEFAULT_PREVIEW_LINES = 10;
|
|
20
|
+
|
|
21
|
+
type PreludeCategory = {
|
|
22
|
+
name: string;
|
|
23
|
+
functions: PreludeHelper[];
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
function groupPreludeHelpers(helpers: PreludeHelper[]): PreludeCategory[] {
|
|
27
|
+
const categories: PreludeCategory[] = [];
|
|
28
|
+
const byName = new Map<string, PreludeHelper[]>();
|
|
29
|
+
for (const helper of helpers) {
|
|
30
|
+
let bucket = byName.get(helper.category);
|
|
31
|
+
if (!bucket) {
|
|
32
|
+
bucket = [];
|
|
33
|
+
byName.set(helper.category, bucket);
|
|
34
|
+
categories.push({ name: helper.category, functions: bucket });
|
|
35
|
+
}
|
|
36
|
+
bucket.push(helper);
|
|
37
|
+
}
|
|
38
|
+
return categories;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export const pythonSchema = Type.Object({
|
|
42
|
+
code: Type.String({ description: "Python code to execute" }),
|
|
43
|
+
timeout: Type.Optional(Type.Number({ description: "Timeout in seconds (optional, no default timeout)" })),
|
|
44
|
+
workdir: Type.Optional(
|
|
45
|
+
Type.String({ description: "Working directory for the command (default: current directory)" }),
|
|
46
|
+
),
|
|
47
|
+
reset: Type.Optional(Type.Boolean({ description: "Restart the kernel before executing this code" })),
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
export type PythonToolParams = { code: string; timeout?: number; workdir?: string; reset?: boolean };
|
|
51
|
+
|
|
52
|
+
export type PythonToolResult = {
|
|
53
|
+
content: Array<{ type: "text"; text: string }>;
|
|
54
|
+
details: PythonToolDetails | undefined;
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
export type PythonProxyExecutor = (params: PythonToolParams, signal?: AbortSignal) => Promise<PythonToolResult>;
|
|
58
|
+
|
|
59
|
+
export interface PythonToolDetails {
|
|
60
|
+
truncation?: TruncationResult;
|
|
61
|
+
fullOutputPath?: string;
|
|
62
|
+
fullOutput?: string;
|
|
63
|
+
jsonOutputs?: unknown[];
|
|
64
|
+
images?: ImageContent[];
|
|
65
|
+
/** Structured status events from prelude helpers */
|
|
66
|
+
statusEvents?: PythonStatusEvent[];
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function formatJsonScalar(value: unknown): string {
|
|
70
|
+
if (value === null) return "null";
|
|
71
|
+
if (value === undefined) return "undefined";
|
|
72
|
+
if (typeof value === "string") return JSON.stringify(value);
|
|
73
|
+
if (typeof value === "number" || typeof value === "boolean" || typeof value === "bigint") return String(value);
|
|
74
|
+
if (typeof value === "function") return "[function]";
|
|
75
|
+
return "[object]";
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function renderJsonTree(value: unknown, theme: Theme, expanded: boolean, maxDepth = expanded ? 6 : 2): string[] {
|
|
79
|
+
const maxItems = expanded ? 20 : 5;
|
|
80
|
+
|
|
81
|
+
const renderNode = (node: unknown, prefix: string, depth: number, isLast: boolean, label?: string): string[] => {
|
|
82
|
+
const branch = getTreeBranch(isLast, theme);
|
|
83
|
+
const displayLabel = label ? `${label}: ` : "";
|
|
84
|
+
|
|
85
|
+
if (depth >= maxDepth || node === null || typeof node !== "object") {
|
|
86
|
+
return [`${prefix}${branch} ${displayLabel}${formatJsonScalar(node)}`];
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const isArray = Array.isArray(node);
|
|
90
|
+
const entries = isArray
|
|
91
|
+
? node.map((val, index) => [String(index), val] as const)
|
|
92
|
+
: Object.entries(node as object);
|
|
93
|
+
const header = `${prefix}${branch} ${displayLabel}${isArray ? `Array(${entries.length})` : `Object(${entries.length})`}`;
|
|
94
|
+
const lines = [header];
|
|
95
|
+
|
|
96
|
+
const childPrefix = prefix + getTreeContinuePrefix(isLast, theme);
|
|
97
|
+
const visible = entries.slice(0, maxItems);
|
|
98
|
+
for (let i = 0; i < visible.length; i++) {
|
|
99
|
+
const [key, val] = visible[i];
|
|
100
|
+
const childLast = i === visible.length - 1 && (expanded || entries.length <= maxItems);
|
|
101
|
+
lines.push(...renderNode(val, childPrefix, depth + 1, childLast, isArray ? `[${key}]` : key));
|
|
102
|
+
}
|
|
103
|
+
if (!expanded && entries.length > maxItems) {
|
|
104
|
+
const moreBranch = theme.tree.last;
|
|
105
|
+
lines.push(`${childPrefix}${moreBranch} ${entries.length - maxItems} more item(s)`);
|
|
106
|
+
}
|
|
107
|
+
return lines;
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
return renderNode(value, "", 0, true);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function getPythonToolDescription(): string {
|
|
114
|
+
const helpers = getPreludeDocs();
|
|
115
|
+
const categories = groupPreludeHelpers(helpers);
|
|
116
|
+
return renderPromptTemplate(pythonDescription, { categories });
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
interface CreatePythonToolOptions {
|
|
120
|
+
proxyExecutor?: PythonProxyExecutor;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function createPythonTool(
|
|
124
|
+
session: ToolSession | null,
|
|
125
|
+
options?: CreatePythonToolOptions,
|
|
126
|
+
): AgentTool<typeof pythonSchema> {
|
|
127
|
+
const { proxyExecutor } = options ?? {};
|
|
128
|
+
|
|
129
|
+
return {
|
|
130
|
+
name: "python",
|
|
131
|
+
label: "Python",
|
|
132
|
+
description: getPythonToolDescription(),
|
|
133
|
+
parameters: pythonSchema,
|
|
134
|
+
execute: async (
|
|
135
|
+
_toolCallId: string,
|
|
136
|
+
params: PythonToolParams,
|
|
137
|
+
signal?: AbortSignal,
|
|
138
|
+
onUpdate?,
|
|
139
|
+
_ctx?: AgentToolContext,
|
|
140
|
+
) => {
|
|
141
|
+
if (proxyExecutor) {
|
|
142
|
+
return proxyExecutor(params, signal);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (!session) {
|
|
146
|
+
throw new Error("Python tool requires a session when not using proxy executor");
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const { code, timeout, workdir, reset } = params;
|
|
150
|
+
const controller = new AbortController();
|
|
151
|
+
const onAbort = () => controller.abort();
|
|
152
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
153
|
+
|
|
154
|
+
try {
|
|
155
|
+
if (signal?.aborted) {
|
|
156
|
+
throw new Error("Aborted");
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const commandCwd = workdir ? resolveToCwd(workdir, session.cwd) : session.cwd;
|
|
160
|
+
let cwdStat: Awaited<ReturnType<Bun.BunFile["stat"]>>;
|
|
161
|
+
try {
|
|
162
|
+
cwdStat = await Bun.file(commandCwd).stat();
|
|
163
|
+
} catch {
|
|
164
|
+
throw new Error(`Working directory does not exist: ${commandCwd}`);
|
|
165
|
+
}
|
|
166
|
+
if (!cwdStat.isDirectory()) {
|
|
167
|
+
throw new Error(`Working directory is not a directory: ${commandCwd}`);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const maxTailBytes = DEFAULT_MAX_BYTES * 2;
|
|
171
|
+
const tailChunks: Array<{ text: string; bytes: number }> = [];
|
|
172
|
+
let tailBytes = 0;
|
|
173
|
+
const jsonOutputs: unknown[] = [];
|
|
174
|
+
const images: ImageContent[] = [];
|
|
175
|
+
|
|
176
|
+
const sessionFile = session.getSessionFile?.() ?? undefined;
|
|
177
|
+
const sessionId = sessionFile ? `session:${sessionFile}:workdir:${commandCwd}` : `cwd:${commandCwd}`;
|
|
178
|
+
const executorOptions: PythonExecutorOptions = {
|
|
179
|
+
cwd: commandCwd,
|
|
180
|
+
timeout: timeout ? timeout * 1000 : undefined,
|
|
181
|
+
signal: controller.signal,
|
|
182
|
+
sessionId,
|
|
183
|
+
kernelMode: session.settings?.getPythonKernelMode?.() ?? "session",
|
|
184
|
+
useSharedGateway: session.settings?.getPythonSharedGateway?.() ?? true,
|
|
185
|
+
reset,
|
|
186
|
+
onChunk: (chunk) => {
|
|
187
|
+
const chunkBytes = Buffer.byteLength(chunk, "utf-8");
|
|
188
|
+
tailChunks.push({ text: chunk, bytes: chunkBytes });
|
|
189
|
+
tailBytes += chunkBytes;
|
|
190
|
+
while (tailBytes > maxTailBytes && tailChunks.length > 1) {
|
|
191
|
+
const removed = tailChunks.shift();
|
|
192
|
+
if (removed) {
|
|
193
|
+
tailBytes -= removed.bytes;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
if (onUpdate) {
|
|
197
|
+
const tailText = tailChunks.map((entry) => entry.text).join("");
|
|
198
|
+
const truncation = truncateTail(tailText);
|
|
199
|
+
onUpdate({
|
|
200
|
+
content: [{ type: "text", text: truncation.content || "" }],
|
|
201
|
+
details: truncation.truncated ? { truncation } : undefined,
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
},
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
const result = await executePython(code, executorOptions);
|
|
208
|
+
|
|
209
|
+
const statusEvents: PythonStatusEvent[] = [];
|
|
210
|
+
for (const output of result.displayOutputs) {
|
|
211
|
+
if (output.type === "json") {
|
|
212
|
+
jsonOutputs.push(output.data);
|
|
213
|
+
}
|
|
214
|
+
if (output.type === "image") {
|
|
215
|
+
images.push({ type: "image", data: output.data, mimeType: output.mimeType });
|
|
216
|
+
}
|
|
217
|
+
if (output.type === "status") {
|
|
218
|
+
statusEvents.push(output.event);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
if (result.cancelled) {
|
|
223
|
+
throw new Error(result.output || "Command aborted");
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const truncation = truncateTail(result.output);
|
|
227
|
+
let outputText =
|
|
228
|
+
truncation.content || (jsonOutputs.length > 0 || images.length > 0 ? "(no text output)" : "(no output)");
|
|
229
|
+
let details: PythonToolDetails | undefined;
|
|
230
|
+
|
|
231
|
+
if (truncation.truncated) {
|
|
232
|
+
const fullOutputSuffix = result.fullOutputPath ? ` Full output: ${result.fullOutputPath}` : "";
|
|
233
|
+
details = {
|
|
234
|
+
truncation,
|
|
235
|
+
fullOutputPath: result.fullOutputPath,
|
|
236
|
+
jsonOutputs: jsonOutputs,
|
|
237
|
+
images,
|
|
238
|
+
statusEvents: statusEvents.length > 0 ? statusEvents : undefined,
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
const startLine = truncation.totalLines - truncation.outputLines + 1;
|
|
242
|
+
const endLine = truncation.totalLines;
|
|
243
|
+
|
|
244
|
+
if (truncation.lastLinePartial) {
|
|
245
|
+
const lastLineSize = formatSize(Buffer.byteLength(result.output.split("\n").pop() || "", "utf-8"));
|
|
246
|
+
outputText += `\n\n[Showing last ${formatSize(truncation.outputBytes)} of line ${endLine} (line is ${lastLineSize})${fullOutputSuffix}]`;
|
|
247
|
+
} else if (truncation.truncatedBy === "lines") {
|
|
248
|
+
outputText += `\n\n[Showing lines ${startLine}-${endLine} of ${truncation.totalLines}${fullOutputSuffix}]`;
|
|
249
|
+
} else {
|
|
250
|
+
outputText += `\n\n[Showing lines ${startLine}-${endLine} of ${truncation.totalLines} (${formatSize(DEFAULT_MAX_BYTES)} limit)${fullOutputSuffix}]`;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
if (!details && (jsonOutputs.length > 0 || images.length > 0 || statusEvents.length > 0)) {
|
|
255
|
+
details = {
|
|
256
|
+
jsonOutputs: jsonOutputs.length > 0 ? jsonOutputs : undefined,
|
|
257
|
+
images: images.length > 0 ? images : undefined,
|
|
258
|
+
statusEvents: statusEvents.length > 0 ? statusEvents : undefined,
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
if (result.exitCode !== 0 && result.exitCode !== undefined) {
|
|
263
|
+
outputText += `\n\nCommand exited with code ${result.exitCode}`;
|
|
264
|
+
throw new Error(outputText);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
return { content: [{ type: "text", text: outputText }], details };
|
|
268
|
+
} finally {
|
|
269
|
+
signal?.removeEventListener("abort", onAbort);
|
|
270
|
+
}
|
|
271
|
+
},
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
interface PythonRenderArgs {
|
|
276
|
+
code?: string;
|
|
277
|
+
timeout?: number;
|
|
278
|
+
workdir?: string;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
interface PythonRenderContext {
|
|
282
|
+
output?: string;
|
|
283
|
+
expanded?: boolean;
|
|
284
|
+
previewLines?: number;
|
|
285
|
+
timeout?: number;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/** Format a status event as a single line for display. */
|
|
289
|
+
function formatStatusEvent(event: PythonStatusEvent, theme: Theme): string {
|
|
290
|
+
const { op, ...data } = event;
|
|
291
|
+
|
|
292
|
+
// Map operations to available theme icons
|
|
293
|
+
type AvailableIcon = "icon.file" | "icon.folder" | "icon.git" | "icon.package";
|
|
294
|
+
const opIcons: Record<string, AvailableIcon> = {
|
|
295
|
+
// File I/O
|
|
296
|
+
read: "icon.file",
|
|
297
|
+
write: "icon.file",
|
|
298
|
+
append: "icon.file",
|
|
299
|
+
cat: "icon.file",
|
|
300
|
+
touch: "icon.file",
|
|
301
|
+
lines: "icon.file",
|
|
302
|
+
// Navigation/Directory
|
|
303
|
+
ls: "icon.folder",
|
|
304
|
+
cd: "icon.folder",
|
|
305
|
+
pwd: "icon.folder",
|
|
306
|
+
mkdir: "icon.folder",
|
|
307
|
+
tree: "icon.folder",
|
|
308
|
+
stat: "icon.folder",
|
|
309
|
+
// Search (use file icon since no search icon)
|
|
310
|
+
find: "icon.file",
|
|
311
|
+
grep: "icon.file",
|
|
312
|
+
rgrep: "icon.file",
|
|
313
|
+
glob: "icon.file",
|
|
314
|
+
// Edit operations (use file icon)
|
|
315
|
+
replace: "icon.file",
|
|
316
|
+
sed: "icon.file",
|
|
317
|
+
rsed: "icon.file",
|
|
318
|
+
delete_lines: "icon.file",
|
|
319
|
+
delete_matching: "icon.file",
|
|
320
|
+
insert_at: "icon.file",
|
|
321
|
+
// Git
|
|
322
|
+
git_status: "icon.git",
|
|
323
|
+
git_diff: "icon.git",
|
|
324
|
+
git_log: "icon.git",
|
|
325
|
+
git_show: "icon.git",
|
|
326
|
+
git_branch: "icon.git",
|
|
327
|
+
git_file_at: "icon.git",
|
|
328
|
+
git_has_changes: "icon.git",
|
|
329
|
+
// Shell/batch (use package icon)
|
|
330
|
+
run: "icon.package",
|
|
331
|
+
sh: "icon.package",
|
|
332
|
+
env: "icon.package",
|
|
333
|
+
batch: "icon.package",
|
|
334
|
+
};
|
|
335
|
+
|
|
336
|
+
const iconKey = opIcons[op] ?? "icon.file";
|
|
337
|
+
const icon = theme.styledSymbol(iconKey, "muted");
|
|
338
|
+
|
|
339
|
+
// Format the status message based on operation type
|
|
340
|
+
const parts: string[] = [];
|
|
341
|
+
|
|
342
|
+
// Error handling
|
|
343
|
+
if (data.error) {
|
|
344
|
+
return `${icon} ${theme.fg("warning", op)}: ${theme.fg("dim", String(data.error))}`;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// Build description based on common fields
|
|
348
|
+
switch (op) {
|
|
349
|
+
case "read":
|
|
350
|
+
parts.push(`${data.chars} chars`);
|
|
351
|
+
if (data.path) parts.push(`from ${shortenPath(String(data.path))}`);
|
|
352
|
+
break;
|
|
353
|
+
case "write":
|
|
354
|
+
case "append":
|
|
355
|
+
parts.push(`${data.chars} chars`);
|
|
356
|
+
if (data.path) parts.push(`to ${shortenPath(String(data.path))}`);
|
|
357
|
+
break;
|
|
358
|
+
case "cat":
|
|
359
|
+
parts.push(`${data.files} file${(data.files as number) !== 1 ? "s" : ""}`);
|
|
360
|
+
parts.push(`${data.chars} chars`);
|
|
361
|
+
break;
|
|
362
|
+
case "find":
|
|
363
|
+
case "glob":
|
|
364
|
+
parts.push(`${data.count} match${(data.count as number) !== 1 ? "es" : ""}`);
|
|
365
|
+
if (data.pattern) parts.push(`for "${truncate(String(data.pattern), 20, theme.format.ellipsis)}"`);
|
|
366
|
+
break;
|
|
367
|
+
case "grep":
|
|
368
|
+
parts.push(`${data.count} match${(data.count as number) !== 1 ? "es" : ""}`);
|
|
369
|
+
if (data.path) parts.push(`in ${shortenPath(String(data.path))}`);
|
|
370
|
+
break;
|
|
371
|
+
case "rgrep":
|
|
372
|
+
parts.push(`${data.count} match${(data.count as number) !== 1 ? "es" : ""}`);
|
|
373
|
+
if (data.pattern) parts.push(`for "${truncate(String(data.pattern), 20, theme.format.ellipsis)}"`);
|
|
374
|
+
break;
|
|
375
|
+
case "ls":
|
|
376
|
+
parts.push(`${data.count} entr${(data.count as number) !== 1 ? "ies" : "y"}`);
|
|
377
|
+
break;
|
|
378
|
+
case "env":
|
|
379
|
+
if (data.action === "set") {
|
|
380
|
+
parts.push(`set ${data.key}=${truncate(String(data.value ?? ""), 30, theme.format.ellipsis)}`);
|
|
381
|
+
} else if (data.action === "get") {
|
|
382
|
+
parts.push(`${data.key}=${truncate(String(data.value ?? ""), 30, theme.format.ellipsis)}`);
|
|
383
|
+
} else {
|
|
384
|
+
parts.push(`${data.count} variable${(data.count as number) !== 1 ? "s" : ""}`);
|
|
385
|
+
}
|
|
386
|
+
break;
|
|
387
|
+
case "stat":
|
|
388
|
+
if (data.is_dir) {
|
|
389
|
+
parts.push("directory");
|
|
390
|
+
} else {
|
|
391
|
+
parts.push(`${data.size} bytes`);
|
|
392
|
+
}
|
|
393
|
+
if (data.path) parts.push(shortenPath(String(data.path)));
|
|
394
|
+
break;
|
|
395
|
+
case "replace":
|
|
396
|
+
case "sed":
|
|
397
|
+
parts.push(`${data.count} replacement${(data.count as number) !== 1 ? "s" : ""}`);
|
|
398
|
+
if (data.path) parts.push(`in ${shortenPath(String(data.path))}`);
|
|
399
|
+
break;
|
|
400
|
+
case "rsed":
|
|
401
|
+
parts.push(`${data.count} replacement${(data.count as number) !== 1 ? "s" : ""}`);
|
|
402
|
+
if (data.files) parts.push(`in ${data.files} file${(data.files as number) !== 1 ? "s" : ""}`);
|
|
403
|
+
break;
|
|
404
|
+
case "git_status":
|
|
405
|
+
if (data.clean) {
|
|
406
|
+
parts.push("clean");
|
|
407
|
+
} else {
|
|
408
|
+
const statusParts: string[] = [];
|
|
409
|
+
if (data.staged) statusParts.push(`${data.staged} staged`);
|
|
410
|
+
if (data.modified) statusParts.push(`${data.modified} modified`);
|
|
411
|
+
if (data.untracked) statusParts.push(`${data.untracked} untracked`);
|
|
412
|
+
parts.push(statusParts.join(", ") || "unknown");
|
|
413
|
+
}
|
|
414
|
+
if (data.branch) parts.push(`on ${data.branch}`);
|
|
415
|
+
break;
|
|
416
|
+
case "git_log":
|
|
417
|
+
parts.push(`${data.commits} commit${(data.commits as number) !== 1 ? "s" : ""}`);
|
|
418
|
+
break;
|
|
419
|
+
case "git_diff":
|
|
420
|
+
parts.push(`${data.lines} line${(data.lines as number) !== 1 ? "s" : ""}`);
|
|
421
|
+
if (data.staged) parts.push("(staged)");
|
|
422
|
+
break;
|
|
423
|
+
case "diff":
|
|
424
|
+
if (data.identical) {
|
|
425
|
+
parts.push("files identical");
|
|
426
|
+
} else {
|
|
427
|
+
parts.push("files differ");
|
|
428
|
+
}
|
|
429
|
+
break;
|
|
430
|
+
case "batch":
|
|
431
|
+
parts.push(`${data.files} file${(data.files as number) !== 1 ? "s" : ""} processed`);
|
|
432
|
+
break;
|
|
433
|
+
case "wc":
|
|
434
|
+
parts.push(`${data.lines}L ${data.words}W ${data.chars}C`);
|
|
435
|
+
break;
|
|
436
|
+
case "lines":
|
|
437
|
+
parts.push(`${data.count} line${(data.count as number) !== 1 ? "s" : ""}`);
|
|
438
|
+
if (data.start && data.end) parts.push(`(${data.start}-${data.end})`);
|
|
439
|
+
break;
|
|
440
|
+
case "delete_lines":
|
|
441
|
+
case "delete_matching":
|
|
442
|
+
parts.push(`${data.count} line${(data.count as number) !== 1 ? "s" : ""} deleted`);
|
|
443
|
+
break;
|
|
444
|
+
case "insert_at":
|
|
445
|
+
parts.push(`${data.lines_inserted} line${(data.lines_inserted as number) !== 1 ? "s" : ""} inserted`);
|
|
446
|
+
break;
|
|
447
|
+
case "cd":
|
|
448
|
+
case "pwd":
|
|
449
|
+
case "mkdir":
|
|
450
|
+
case "touch":
|
|
451
|
+
if (data.path) parts.push(shortenPath(String(data.path)));
|
|
452
|
+
break;
|
|
453
|
+
case "rm":
|
|
454
|
+
case "mv":
|
|
455
|
+
case "cp":
|
|
456
|
+
if (data.src) parts.push(`${shortenPath(String(data.src))} → ${shortenPath(String(data.dst))}`);
|
|
457
|
+
else if (data.path) parts.push(shortenPath(String(data.path)));
|
|
458
|
+
break;
|
|
459
|
+
default:
|
|
460
|
+
// Generic formatting for other operations
|
|
461
|
+
if (data.count !== undefined) {
|
|
462
|
+
parts.push(String(data.count));
|
|
463
|
+
}
|
|
464
|
+
if (data.path) {
|
|
465
|
+
parts.push(shortenPath(String(data.path)));
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
const desc = parts.length > 0 ? parts.join(" · ") : "";
|
|
470
|
+
return `${icon} ${theme.fg("muted", op)}${desc ? ` ${theme.fg("dim", desc)}` : ""}`;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
/** Format status event with expanded detail lines. */
|
|
474
|
+
function formatStatusEventExpanded(event: PythonStatusEvent, theme: Theme): string[] {
|
|
475
|
+
const lines: string[] = [];
|
|
476
|
+
const { op, ...data } = event;
|
|
477
|
+
|
|
478
|
+
// Main status line
|
|
479
|
+
lines.push(formatStatusEvent(event, theme));
|
|
480
|
+
|
|
481
|
+
// Add detail lines for operations with list data
|
|
482
|
+
const addItems = (items: unknown[], formatter: (item: unknown) => string, max = 5) => {
|
|
483
|
+
const arr = Array.isArray(items) ? items : [];
|
|
484
|
+
for (let i = 0; i < Math.min(arr.length, max); i++) {
|
|
485
|
+
lines.push(` ${theme.fg("dim", formatter(arr[i]))}`);
|
|
486
|
+
}
|
|
487
|
+
if (arr.length > max) {
|
|
488
|
+
lines.push(` ${theme.fg("dim", `${theme.format.ellipsis} ${arr.length - max} more`)}`);
|
|
489
|
+
}
|
|
490
|
+
};
|
|
491
|
+
|
|
492
|
+
// Add preview lines (truncated content)
|
|
493
|
+
const addPreview = (preview: string, maxLines = 3) => {
|
|
494
|
+
const previewLines = String(preview).split("\n").slice(0, maxLines);
|
|
495
|
+
for (const line of previewLines) {
|
|
496
|
+
lines.push(` ${theme.fg("toolOutput", truncate(line, 80, theme.format.ellipsis))}`);
|
|
497
|
+
}
|
|
498
|
+
const totalLines = String(preview).split("\n").length;
|
|
499
|
+
if (totalLines > maxLines) {
|
|
500
|
+
lines.push(` ${theme.fg("dim", `${theme.format.ellipsis} ${totalLines - maxLines} more lines`)}`);
|
|
501
|
+
}
|
|
502
|
+
};
|
|
503
|
+
|
|
504
|
+
switch (op) {
|
|
505
|
+
case "find":
|
|
506
|
+
case "glob":
|
|
507
|
+
if (data.matches) addItems(data.matches as unknown[], (m) => String(m));
|
|
508
|
+
break;
|
|
509
|
+
case "ls":
|
|
510
|
+
if (data.items) addItems(data.items as unknown[], (m) => String(m));
|
|
511
|
+
break;
|
|
512
|
+
case "grep":
|
|
513
|
+
if (data.hits) {
|
|
514
|
+
addItems(data.hits as unknown[], (h) => {
|
|
515
|
+
const hit = h as { line: number; text: string };
|
|
516
|
+
return `${hit.line}: ${truncate(hit.text, 60, theme.format.ellipsis)}`;
|
|
517
|
+
});
|
|
518
|
+
}
|
|
519
|
+
break;
|
|
520
|
+
case "rgrep":
|
|
521
|
+
if (data.hits) {
|
|
522
|
+
addItems(data.hits as unknown[], (h) => {
|
|
523
|
+
const hit = h as { file: string; line: number; text: string };
|
|
524
|
+
return `${shortenPath(hit.file)}:${hit.line}: ${truncate(hit.text, 50, theme.format.ellipsis)}`;
|
|
525
|
+
});
|
|
526
|
+
}
|
|
527
|
+
break;
|
|
528
|
+
case "rsed":
|
|
529
|
+
if (data.changed) {
|
|
530
|
+
addItems(data.changed as unknown[], (c) => {
|
|
531
|
+
const change = c as { file: string; count: number };
|
|
532
|
+
return `${shortenPath(change.file)}: ${change.count} replacement${change.count !== 1 ? "s" : ""}`;
|
|
533
|
+
});
|
|
534
|
+
}
|
|
535
|
+
break;
|
|
536
|
+
case "env":
|
|
537
|
+
if (data.keys) addItems(data.keys as unknown[], (k) => String(k), 10);
|
|
538
|
+
break;
|
|
539
|
+
case "git_log":
|
|
540
|
+
if (data.entries) {
|
|
541
|
+
addItems(data.entries as unknown[], (e) => {
|
|
542
|
+
const entry = e as { sha: string; subject: string };
|
|
543
|
+
return `${entry.sha} ${truncate(entry.subject, 50, theme.format.ellipsis)}`;
|
|
544
|
+
});
|
|
545
|
+
}
|
|
546
|
+
break;
|
|
547
|
+
case "git_status":
|
|
548
|
+
if (data.files) addItems(data.files as unknown[], (f) => String(f));
|
|
549
|
+
break;
|
|
550
|
+
case "git_branch":
|
|
551
|
+
if (data.branches) addItems(data.branches as unknown[], (b) => String(b));
|
|
552
|
+
break;
|
|
553
|
+
case "read":
|
|
554
|
+
case "cat":
|
|
555
|
+
case "head":
|
|
556
|
+
case "tail":
|
|
557
|
+
case "tree":
|
|
558
|
+
case "diff":
|
|
559
|
+
case "lines":
|
|
560
|
+
case "git_diff":
|
|
561
|
+
case "sh":
|
|
562
|
+
if (data.preview) addPreview(String(data.preview));
|
|
563
|
+
break;
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
return lines;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
/** Render status events as tree lines. */
|
|
570
|
+
function renderStatusEvents(events: PythonStatusEvent[], theme: Theme, expanded: boolean): string[] {
|
|
571
|
+
if (events.length === 0) return [];
|
|
572
|
+
|
|
573
|
+
const maxCollapsed = 3;
|
|
574
|
+
const maxExpanded = 10;
|
|
575
|
+
const displayCount = expanded ? Math.min(events.length, maxExpanded) : Math.min(events.length, maxCollapsed);
|
|
576
|
+
|
|
577
|
+
const lines: string[] = [];
|
|
578
|
+
for (let i = 0; i < displayCount; i++) {
|
|
579
|
+
const isLast = i === displayCount - 1 && (expanded || events.length <= maxCollapsed);
|
|
580
|
+
const branch = isLast ? theme.tree.last : theme.tree.branch;
|
|
581
|
+
|
|
582
|
+
if (expanded) {
|
|
583
|
+
// Show expanded details for each event
|
|
584
|
+
const eventLines = formatStatusEventExpanded(events[i], theme);
|
|
585
|
+
lines.push(`${theme.fg("dim", branch)} ${eventLines[0]}`);
|
|
586
|
+
const continueBranch = isLast ? " " : `${theme.tree.vertical} `;
|
|
587
|
+
for (let j = 1; j < eventLines.length; j++) {
|
|
588
|
+
lines.push(`${theme.fg("dim", continueBranch)}${eventLines[j]}`);
|
|
589
|
+
}
|
|
590
|
+
} else {
|
|
591
|
+
lines.push(`${theme.fg("dim", branch)} ${formatStatusEvent(events[i], theme)}`);
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
if (!expanded && events.length > maxCollapsed) {
|
|
596
|
+
lines.push(
|
|
597
|
+
`${theme.fg("dim", theme.tree.last)} ${theme.fg("dim", `${theme.format.ellipsis} ${events.length - maxCollapsed} more`)}`,
|
|
598
|
+
);
|
|
599
|
+
} else if (expanded && events.length > maxExpanded) {
|
|
600
|
+
lines.push(
|
|
601
|
+
`${theme.fg("dim", theme.tree.last)} ${theme.fg("dim", `${theme.format.ellipsis} ${events.length - maxExpanded} more`)}`,
|
|
602
|
+
);
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
return lines;
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
export const pythonToolRenderer = {
|
|
609
|
+
renderCall(args: PythonRenderArgs, uiTheme: Theme): Component {
|
|
610
|
+
const ui = createToolUIKit(uiTheme);
|
|
611
|
+
const code = args.code || uiTheme.format.ellipsis;
|
|
612
|
+
const prompt = uiTheme.fg("accent", ">>>");
|
|
613
|
+
const cwd = process.cwd();
|
|
614
|
+
let displayWorkdir = args.workdir;
|
|
615
|
+
|
|
616
|
+
if (displayWorkdir) {
|
|
617
|
+
const resolvedCwd = resolve(cwd);
|
|
618
|
+
const resolvedWorkdir = resolve(displayWorkdir);
|
|
619
|
+
if (resolvedWorkdir === resolvedCwd) {
|
|
620
|
+
displayWorkdir = undefined;
|
|
621
|
+
} else {
|
|
622
|
+
const relativePath = relative(resolvedCwd, resolvedWorkdir);
|
|
623
|
+
const isWithinCwd = relativePath && !relativePath.startsWith("..") && !relativePath.startsWith(`..${sep}`);
|
|
624
|
+
if (isWithinCwd) {
|
|
625
|
+
displayWorkdir = relativePath;
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
const cmdText = displayWorkdir
|
|
631
|
+
? `${prompt} ${uiTheme.fg("dim", `cd ${displayWorkdir} &&`)} ${code}`
|
|
632
|
+
: `${prompt} ${code}`;
|
|
633
|
+
const text = ui.title(cmdText);
|
|
634
|
+
return new Text(text, 0, 0);
|
|
635
|
+
},
|
|
636
|
+
|
|
637
|
+
renderResult(
|
|
638
|
+
result: { content: Array<{ type: string; text?: string }>; details?: PythonToolDetails },
|
|
639
|
+
options: RenderResultOptions & { renderContext?: PythonRenderContext },
|
|
640
|
+
uiTheme: Theme,
|
|
641
|
+
): Component {
|
|
642
|
+
const ui = createToolUIKit(uiTheme);
|
|
643
|
+
const { renderContext } = options;
|
|
644
|
+
const details = result.details;
|
|
645
|
+
|
|
646
|
+
const expanded = renderContext?.expanded ?? options.expanded;
|
|
647
|
+
const previewLines = renderContext?.previewLines ?? PYTHON_DEFAULT_PREVIEW_LINES;
|
|
648
|
+
const output = renderContext?.output ?? (result.content?.find((c) => c.type === "text")?.text ?? "").trim();
|
|
649
|
+
const fullOutput = details?.fullOutput;
|
|
650
|
+
const displayOutput = expanded ? (fullOutput ?? output) : output;
|
|
651
|
+
const showingFullOutput = expanded && fullOutput !== undefined;
|
|
652
|
+
|
|
653
|
+
const jsonOutputs = details?.jsonOutputs ?? [];
|
|
654
|
+
const jsonLines = jsonOutputs.flatMap((value, index) => {
|
|
655
|
+
const header = `JSON output ${index + 1}`;
|
|
656
|
+
const treeLines = renderJsonTree(value, uiTheme, expanded);
|
|
657
|
+
return [header, ...treeLines];
|
|
658
|
+
});
|
|
659
|
+
|
|
660
|
+
// Render status events
|
|
661
|
+
const statusEvents = details?.statusEvents ?? [];
|
|
662
|
+
const statusLines = renderStatusEvents(statusEvents, uiTheme, expanded);
|
|
663
|
+
|
|
664
|
+
const combinedOutput = [displayOutput, ...jsonLines].filter(Boolean).join("\n");
|
|
665
|
+
|
|
666
|
+
const truncation = details?.truncation;
|
|
667
|
+
const fullOutputPath = details?.fullOutputPath;
|
|
668
|
+
const timeoutSeconds = renderContext?.timeout;
|
|
669
|
+
const timeoutLine =
|
|
670
|
+
typeof timeoutSeconds === "number"
|
|
671
|
+
? uiTheme.fg("dim", ui.wrapBrackets(`Timeout: ${timeoutSeconds}s`))
|
|
672
|
+
: undefined;
|
|
673
|
+
let warningLine: string | undefined;
|
|
674
|
+
if (fullOutputPath || (truncation?.truncated && !showingFullOutput)) {
|
|
675
|
+
const warnings: string[] = [];
|
|
676
|
+
if (fullOutputPath) {
|
|
677
|
+
warnings.push(`Full output: ${fullOutputPath}`);
|
|
678
|
+
}
|
|
679
|
+
if (truncation?.truncated && !showingFullOutput) {
|
|
680
|
+
if (truncation.truncatedBy === "lines") {
|
|
681
|
+
warnings.push(`Truncated: showing ${truncation.outputLines} of ${truncation.totalLines} lines`);
|
|
682
|
+
} else {
|
|
683
|
+
warnings.push(
|
|
684
|
+
`Truncated: ${truncation.outputLines} lines shown (${ui.formatBytes(truncation.maxBytes ?? DEFAULT_MAX_BYTES)} limit)`,
|
|
685
|
+
);
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
if (warnings.length > 0) {
|
|
689
|
+
warningLine = uiTheme.fg("warning", ui.wrapBrackets(warnings.join(". ")));
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
if (!combinedOutput && statusLines.length === 0) {
|
|
694
|
+
const lines = [timeoutLine, warningLine].filter(Boolean) as string[];
|
|
695
|
+
return new Text(lines.join("\n"), 0, 0);
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
// If only status events (no text output), show them directly
|
|
699
|
+
if (!combinedOutput && statusLines.length > 0) {
|
|
700
|
+
const lines = [...statusLines, timeoutLine, warningLine].filter(Boolean) as string[];
|
|
701
|
+
return new Text(lines.join("\n"), 0, 0);
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
if (expanded) {
|
|
705
|
+
const styledOutput = combinedOutput
|
|
706
|
+
.split("\n")
|
|
707
|
+
.map((line) => uiTheme.fg("toolOutput", line))
|
|
708
|
+
.join("\n");
|
|
709
|
+
const lines = [styledOutput, ...statusLines, timeoutLine, warningLine].filter(Boolean) as string[];
|
|
710
|
+
return new Text(lines.join("\n"), 0, 0);
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
const styledOutput = combinedOutput
|
|
714
|
+
.split("\n")
|
|
715
|
+
.map((line) => uiTheme.fg("toolOutput", line))
|
|
716
|
+
.join("\n");
|
|
717
|
+
const textContent = `\n${styledOutput}`;
|
|
718
|
+
|
|
719
|
+
let cachedWidth: number | undefined;
|
|
720
|
+
let cachedLines: string[] | undefined;
|
|
721
|
+
let cachedSkipped: number | undefined;
|
|
722
|
+
|
|
723
|
+
return {
|
|
724
|
+
render: (width: number): string[] => {
|
|
725
|
+
if (cachedLines === undefined || cachedWidth !== width) {
|
|
726
|
+
const result = truncateToVisualLines(textContent, previewLines, width);
|
|
727
|
+
cachedLines = result.visualLines;
|
|
728
|
+
cachedSkipped = result.skippedCount;
|
|
729
|
+
cachedWidth = width;
|
|
730
|
+
}
|
|
731
|
+
const outputLines: string[] = [];
|
|
732
|
+
if (cachedSkipped && cachedSkipped > 0) {
|
|
733
|
+
outputLines.push("");
|
|
734
|
+
const skippedLine = uiTheme.fg(
|
|
735
|
+
"dim",
|
|
736
|
+
`${uiTheme.format.ellipsis} (${cachedSkipped} earlier lines, showing ${cachedLines.length} of ${cachedSkipped + cachedLines.length}) (ctrl+o to expand)`,
|
|
737
|
+
);
|
|
738
|
+
outputLines.push(truncateToWidth(skippedLine, width, uiTheme.fg("dim", uiTheme.format.ellipsis)));
|
|
739
|
+
}
|
|
740
|
+
outputLines.push(...cachedLines);
|
|
741
|
+
// Add status events below the output
|
|
742
|
+
for (const statusLine of statusLines) {
|
|
743
|
+
outputLines.push(truncateToWidth(statusLine, width, uiTheme.fg("dim", uiTheme.format.ellipsis)));
|
|
744
|
+
}
|
|
745
|
+
if (timeoutLine) {
|
|
746
|
+
outputLines.push(truncateToWidth(timeoutLine, width, uiTheme.fg("dim", uiTheme.format.ellipsis)));
|
|
747
|
+
}
|
|
748
|
+
if (warningLine) {
|
|
749
|
+
outputLines.push(truncateToWidth(warningLine, width, uiTheme.fg("warning", uiTheme.format.ellipsis)));
|
|
750
|
+
}
|
|
751
|
+
return outputLines;
|
|
752
|
+
},
|
|
753
|
+
invalidate: () => {
|
|
754
|
+
cachedWidth = undefined;
|
|
755
|
+
cachedLines = undefined;
|
|
756
|
+
cachedSkipped = undefined;
|
|
757
|
+
},
|
|
758
|
+
};
|
|
759
|
+
},
|
|
760
|
+
};
|