@jaggerxtrm/pi-extensions 0.9.3 → 0.9.5
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/extensions/beads/index.ts +40 -3
- package/extensions/custom-footer/index.ts +99 -65
- package/extensions/xtprompt/index.test.ts +496 -0
- package/extensions/xtprompt/index.ts +421 -238
- package/extensions/xtprompt/package.json +1 -1
- package/extensions/xtrm-loader/index.ts +2 -2
- package/extensions/xtrm-ui/format.ts +6 -8
- package/extensions/xtrm-ui/index.ts +376 -833
- package/extensions/xtrm-ui/package.json +2 -2
- package/package.json +1 -1
- package/src/registry.ts +14 -8
- package/{extensions/xtrm-ui/themes/pidex-dark-flattools.json → themes/xtrm-ui/xtrm-dark-flattools.json} +1 -1
- package/{extensions/xtrm-ui/themes/pidex-dark.json → themes/xtrm-ui/xtrm-dark.json} +1 -1
- package/themes/xtrm-ui/{pidex-light-flattools.json → xtrm-light-flattools.json} +1 -1
- package/themes/xtrm-ui/{pidex-light.json → xtrm-light.json} +1 -1
- package/extensions/xtrm-ui/themes/pidex-light-flattools.json +0 -79
- package/extensions/xtrm-ui/themes/pidex-light.json +0 -85
- package/themes/xtrm-ui/pidex-dark-flattools.json +0 -79
- package/themes/xtrm-ui/pidex-dark.json +0 -85
|
@@ -1,13 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* XTRM
|
|
3
|
-
*
|
|
4
|
-
* Wraps pi-dex functionality with XTRM-specific preferences:
|
|
5
|
-
* - Uses pi-dex themes and header
|
|
6
|
-
* - Disables pi-dex footer (let custom-footer handle it)
|
|
7
|
-
* - Provides /xtrm-ui commands for theme/density switching
|
|
8
|
-
*
|
|
9
|
-
* This eliminates the race condition between pi-dex's footer and
|
|
10
|
-
* XTRM's custom-footer extension.
|
|
2
|
+
* XTRM-owned Pi chrome, themes, and native/external tool rendering.
|
|
3
|
+
* custom-footer remains the sole footer owner.
|
|
11
4
|
*/
|
|
12
5
|
|
|
13
6
|
import type {
|
|
@@ -19,7 +12,6 @@ import type {
|
|
|
19
12
|
GrepToolDetails,
|
|
20
13
|
LsToolDetails,
|
|
21
14
|
ReadToolDetails,
|
|
22
|
-
ToolResultEvent,
|
|
23
15
|
} from "@earendil-works/pi-coding-agent";
|
|
24
16
|
import {
|
|
25
17
|
CustomEditor,
|
|
@@ -32,7 +24,7 @@ import {
|
|
|
32
24
|
createWriteTool,
|
|
33
25
|
} from "@earendil-works/pi-coding-agent";
|
|
34
26
|
import { Box, Text, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
35
|
-
import { existsSync, readFileSync, realpathSync
|
|
27
|
+
import { existsSync, readFileSync, realpathSync } from "node:fs";
|
|
36
28
|
import { basename, dirname, join } from "node:path";
|
|
37
29
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
38
30
|
import {
|
|
@@ -58,21 +50,16 @@ import {
|
|
|
58
50
|
// Types
|
|
59
51
|
// ============================================================================
|
|
60
52
|
|
|
61
|
-
export type XtrmThemeName = "
|
|
53
|
+
export type XtrmThemeName = "xtrm-dark" | "xtrm-light";
|
|
54
|
+
export type XtrmResolvedThemeName = XtrmThemeName | "xtrm-dark-flattools" | "xtrm-light-flattools";
|
|
62
55
|
export type XtrmDensity = "compact" | "comfortable";
|
|
63
|
-
export type XtrmExternalToolChrome = "background" | "box";
|
|
64
56
|
|
|
65
57
|
export interface XtrmUiPrefs {
|
|
66
58
|
themeName: XtrmThemeName;
|
|
67
59
|
density: XtrmDensity;
|
|
68
60
|
showHeader: boolean;
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
forceTheme: boolean; // When false, skip setTheme (allow external theme override)
|
|
72
|
-
toolRowBg: boolean; // Subtle background behind tool text rows (no padding)
|
|
73
|
-
compactExternalToolResults: boolean; // Compact extension tool results (disables full expand output)
|
|
74
|
-
externalToolChrome: XtrmExternalToolChrome; // Visual treatment for non-native tool rows
|
|
75
|
-
hideThinkingPlaceholder: boolean; // When false, hidden thinking blocks render no placeholder text
|
|
61
|
+
forceTheme: boolean;
|
|
62
|
+
toolRowBg: boolean;
|
|
76
63
|
}
|
|
77
64
|
|
|
78
65
|
// ============================================================================
|
|
@@ -82,24 +69,13 @@ export interface XtrmUiPrefs {
|
|
|
82
69
|
export const XTRM_UI_PREFS_ENTRY = "xtrm-ui-prefs";
|
|
83
70
|
|
|
84
71
|
export const DEFAULT_PREFS: XtrmUiPrefs = {
|
|
85
|
-
themeName: "
|
|
72
|
+
themeName: "xtrm-dark",
|
|
86
73
|
density: "compact",
|
|
87
74
|
showHeader: true,
|
|
88
|
-
compactTools: true,
|
|
89
|
-
showFooter: false, // XTRM: disable pi-dex footer, use custom-footer
|
|
90
75
|
forceTheme: true,
|
|
91
76
|
toolRowBg: false,
|
|
92
|
-
compactExternalToolResults: true,
|
|
93
|
-
externalToolChrome: "background",
|
|
94
|
-
hideThinkingPlaceholder: false,
|
|
95
77
|
};
|
|
96
78
|
|
|
97
|
-
let activeExternalToolChrome: XtrmExternalToolChrome = DEFAULT_PREFS.externalToolChrome;
|
|
98
|
-
|
|
99
|
-
function setActiveExternalToolChrome(chrome: XtrmExternalToolChrome): void {
|
|
100
|
-
activeExternalToolChrome = chrome;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
79
|
|
|
104
80
|
// ============================================================================
|
|
105
81
|
// Preferences
|
|
@@ -113,19 +89,18 @@ type MaybeCustomEntry = {
|
|
|
113
89
|
|
|
114
90
|
function normalizePrefs(input: unknown): XtrmUiPrefs {
|
|
115
91
|
if (!input || typeof input !== "object") return { ...DEFAULT_PREFS };
|
|
116
|
-
const source = input as Partial<XtrmUiPrefs
|
|
92
|
+
const source = input as Partial<XtrmUiPrefs> & { themeName?: unknown };
|
|
93
|
+
const themeName = source.themeName;
|
|
94
|
+
const lightTheme = themeName === "xtrm-light"
|
|
95
|
+
|| themeName === "xtrm-light-flattools"
|
|
96
|
+
|| themeName === "pidex-light"
|
|
97
|
+
|| themeName === "pidex-light-flattools";
|
|
117
98
|
return {
|
|
118
|
-
themeName:
|
|
99
|
+
themeName: lightTheme ? "xtrm-light" : "xtrm-dark",
|
|
119
100
|
density: source.density === "comfortable" ? "comfortable" : "compact",
|
|
120
101
|
showHeader: source.showHeader ?? DEFAULT_PREFS.showHeader,
|
|
121
|
-
compactTools: source.compactTools ?? DEFAULT_PREFS.compactTools,
|
|
122
|
-
showFooter: source.showFooter ?? DEFAULT_PREFS.showFooter,
|
|
123
102
|
forceTheme: source.forceTheme ?? DEFAULT_PREFS.forceTheme,
|
|
124
103
|
toolRowBg: source.toolRowBg ?? DEFAULT_PREFS.toolRowBg,
|
|
125
|
-
compactExternalToolResults:
|
|
126
|
-
source.compactExternalToolResults ?? DEFAULT_PREFS.compactExternalToolResults,
|
|
127
|
-
externalToolChrome: source.externalToolChrome === "box" ? "box" : "background",
|
|
128
|
-
hideThinkingPlaceholder: source.hideThinkingPlaceholder ?? DEFAULT_PREFS.hideThinkingPlaceholder,
|
|
129
104
|
};
|
|
130
105
|
}
|
|
131
106
|
|
|
@@ -247,12 +222,14 @@ type PatchableToolExecutionComponent = {
|
|
|
247
222
|
result?: { content?: Array<{ type: string; text?: string }>; details?: unknown; isError?: boolean };
|
|
248
223
|
expanded?: boolean;
|
|
249
224
|
hasRendererDefinition?: () => boolean;
|
|
225
|
+
__xtrmExternalStartedAt?: number;
|
|
226
|
+
__xtrmExternalDurationMs?: number;
|
|
250
227
|
};
|
|
251
228
|
|
|
252
229
|
type ExternalToolFrameKind = "serena" | "gitnexus" | "structured" | "process" | "external";
|
|
253
230
|
|
|
254
231
|
const PATCHED_EXTERNAL_TOOL_FRAME = "__xtrmUiExternalToolFrame";
|
|
255
|
-
const EXTERNAL_TOOL_FRAME_PATCH_VERSION =
|
|
232
|
+
const EXTERNAL_TOOL_FRAME_PATCH_VERSION = 17;
|
|
256
233
|
const ANSI_PATTERN = /\x1b\[[0-9;?]*[ -/]*[@-~]/g;
|
|
257
234
|
|
|
258
235
|
function stripAnsi(text: string): string {
|
|
@@ -272,16 +249,6 @@ function externalToolFrameKind(toolName: string | undefined): ExternalToolFrameK
|
|
|
272
249
|
return "external";
|
|
273
250
|
}
|
|
274
251
|
|
|
275
|
-
function padVisible(text: string, width: number): string {
|
|
276
|
-
const visible = visibleWidth(text);
|
|
277
|
-
return text + " ".repeat(Math.max(0, width - visible));
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
function getXtrmOriginalText(details: unknown): string | undefined {
|
|
281
|
-
const record = asRecord(details);
|
|
282
|
-
return typeof record?.xtrmOriginalText === "string" ? record.xtrmOriginalText : undefined;
|
|
283
|
-
}
|
|
284
|
-
|
|
285
252
|
function getToolArgs(component: PatchableToolExecutionComponent): Record<string, unknown> {
|
|
286
253
|
return component.args && typeof component.args === "object" && !Array.isArray(component.args)
|
|
287
254
|
? component.args as Record<string, unknown>
|
|
@@ -309,13 +276,10 @@ function summarizeExternalToolPending(toolName: string | undefined, input: Recor
|
|
|
309
276
|
}
|
|
310
277
|
|
|
311
278
|
function extractResultTextLines(component: PatchableToolExecutionComponent): string[] | undefined {
|
|
312
|
-
const originalText = component.expanded ? getXtrmOriginalText(component.result?.details) : undefined;
|
|
313
|
-
if (originalText) return originalText.split("\n");
|
|
314
|
-
|
|
315
279
|
const text = component.result?.content?.find((content) => content.type === "text")?.text;
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
280
|
+
return text
|
|
281
|
+
? text.split("\n")
|
|
282
|
+
: [summarizeExternalToolPending(component.toolName, getToolArgs(component))];
|
|
319
283
|
}
|
|
320
284
|
|
|
321
285
|
function trimRenderedToolLines(lines: string[]): string[] {
|
|
@@ -338,66 +302,83 @@ function externalToolBadgeColor(kind: ExternalToolFrameKind, text: string): stri
|
|
|
338
302
|
return `\x1b[38;2;3;8;12m\x1b[48;2;${badgeR};${badgeG};${badgeB}m${text}\x1b[39m\x1b[49m`;
|
|
339
303
|
}
|
|
340
304
|
|
|
341
|
-
function highlightExternalToolBadge(kind: ExternalToolFrameKind, line: string): string {
|
|
342
|
-
const
|
|
343
|
-
if (
|
|
344
|
-
|
|
345
|
-
}
|
|
305
|
+
export function highlightExternalToolBadge(kind: ExternalToolFrameKind, line: string): string {
|
|
306
|
+
const marked = line.match(/^([•›]\s+)(\S+)/u);
|
|
307
|
+
if (marked?.[1] && marked[2]) {
|
|
308
|
+
return marked[1] + externalToolBadgeColor(kind, marked[2]) + line.slice(marked[1].length + marked[2].length);
|
|
309
|
+
}
|
|
346
310
|
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
serena: [150, 210, 255],
|
|
350
|
-
gitnexus: [185, 168, 255],
|
|
351
|
-
structured: [205, 166, 255],
|
|
352
|
-
process: [145, 231, 255],
|
|
353
|
-
external: [168, 181, 199],
|
|
354
|
-
};
|
|
355
|
-
const [r, g, b] = colors[kind];
|
|
356
|
-
return `[2m[38;2;${r};${g};${b}m${text}[39m[22m`;
|
|
311
|
+
const provider = line.match(/^(\[[A-Za-z][A-Za-z0-9 _-]{0,31}\])/u)?.[1];
|
|
312
|
+
return provider ? externalToolBadgeColor(kind, provider) + line.slice(provider.length) : line;
|
|
357
313
|
}
|
|
358
314
|
|
|
359
|
-
function collapsedExternalToolLines(contentLines: string[], expanded: boolean): string[] {
|
|
360
|
-
|
|
315
|
+
export function collapsedExternalToolLines(contentLines: string[], expanded: boolean): string[] {
|
|
316
|
+
if (expanded || contentLines.length <= 6) return contentLines;
|
|
317
|
+
return [
|
|
318
|
+
...contentLines.slice(0, 6),
|
|
319
|
+
`... (${contentLines.length - 6} more lines, ctrl+o to expand)`,
|
|
320
|
+
];
|
|
361
321
|
}
|
|
362
322
|
|
|
363
|
-
function renderExternalToolBackgroundLines(
|
|
323
|
+
export function renderExternalToolBackgroundLines(
|
|
364
324
|
contentLines: string[],
|
|
365
325
|
width: number,
|
|
366
326
|
kind: ExternalToolFrameKind,
|
|
367
327
|
expanded: boolean,
|
|
328
|
+
toolName?: string,
|
|
329
|
+
durationMs?: number,
|
|
368
330
|
): string[] {
|
|
369
|
-
|
|
370
|
-
const
|
|
371
|
-
|
|
331
|
+
let displayLines = contentLines;
|
|
332
|
+
const raw = contentLines.length === 1 ? contentLines[0]?.trim() : undefined;
|
|
333
|
+
if (raw?.startsWith("{") || raw?.startsWith("[")) {
|
|
334
|
+
try {
|
|
335
|
+
displayLines = JSON.stringify(JSON.parse(raw), null, 2).split("\n");
|
|
336
|
+
} catch {
|
|
337
|
+
// Keep non-JSON output unchanged.
|
|
338
|
+
}
|
|
339
|
+
}
|
|
372
340
|
|
|
373
|
-
|
|
374
|
-
|
|
341
|
+
const firstLine = displayLines[0] ?? "";
|
|
342
|
+
const action = kind === "gitnexus" && toolName?.startsWith("gitnexus_")
|
|
343
|
+
? toolName.slice("gitnexus_".length)
|
|
344
|
+
: kind === "serena" ? toolName : undefined;
|
|
345
|
+
const providerHeader = /^\[[A-Za-z][A-Za-z0-9 _-]{0,31}\]/u.test(firstLine);
|
|
346
|
+
|
|
347
|
+
if (providerHeader && action && !firstLine.endsWith(` ${action}`)) {
|
|
348
|
+
displayLines = [`${firstLine} ${action}`, ...displayLines.slice(1)];
|
|
349
|
+
} else if (!/^(?:[•›]\s+|\[[A-Za-z][A-Za-z0-9 _-]{0,31}\])/u.test(firstLine)) {
|
|
350
|
+
const labels: Record<ExternalToolFrameKind, string> = {
|
|
351
|
+
serena: "Serena",
|
|
352
|
+
gitnexus: "GitNexus",
|
|
353
|
+
structured: "structured_return",
|
|
354
|
+
process: "process",
|
|
355
|
+
external: "external",
|
|
356
|
+
};
|
|
357
|
+
const label = kind === "external" && toolName ? normalizeToolLabel(toolName) : labels[kind];
|
|
358
|
+
displayLines = [`[${label}]${action ? ` ${action}` : ""}`, ...displayLines];
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
const header = displayLines[0] ?? "";
|
|
362
|
+
const payloadLines = displayLines.slice(1);
|
|
363
|
+
const visiblePayload = expanded ? payloadLines : payloadLines.slice(0, 6);
|
|
364
|
+
const shown = visiblePayload.length;
|
|
365
|
+
const total = payloadLines.length;
|
|
366
|
+
const lineSummary = !expanded && shown < total
|
|
367
|
+
? `showing ${shown}/${total} lines (ctrl+o expand)`
|
|
368
|
+
: total > 0 ? formatLineLabel(total, "line") : undefined;
|
|
369
|
+
const footerMeta = joinMeta([
|
|
370
|
+
lineSummary,
|
|
371
|
+
formatDuration(durationMs),
|
|
372
|
+
formatPayloadSize(contentLines.join("\n")),
|
|
373
|
+
]);
|
|
374
|
+
const renderWidth = Math.max(8, width);
|
|
375
|
+
const body = [header, ...visiblePayload].map((rawLine) => {
|
|
376
|
+
const line = truncateToWidth(rawLine, renderWidth);
|
|
375
377
|
return highlightExternalToolBadge(kind, line);
|
|
376
378
|
});
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
contentLines: string[],
|
|
381
|
-
width: number,
|
|
382
|
-
kind: ExternalToolFrameKind,
|
|
383
|
-
expanded: boolean,
|
|
384
|
-
): string[] {
|
|
385
|
-
const availableWidth = Math.max(8, width - 4);
|
|
386
|
-
const maxContentWidth = expanded ? availableWidth : Math.min(availableWidth, 34);
|
|
387
|
-
const visibleLines = collapsedExternalToolLines(contentLines, expanded);
|
|
388
|
-
const contentWidth = Math.max(
|
|
389
|
-
1,
|
|
390
|
-
Math.min(maxContentWidth, ...visibleLines.map((line) => visibleWidth(line))),
|
|
391
|
-
);
|
|
392
|
-
const innerWidth = contentWidth + 2;
|
|
393
|
-
|
|
394
|
-
const framed = [externalToolBorderColor(kind, `╭${"─".repeat(innerWidth)}╮`)];
|
|
395
|
-
for (const rawLine of visibleLines) {
|
|
396
|
-
const line = truncateToWidth(rawLine, contentWidth);
|
|
397
|
-
framed.push(`${externalToolBorderColor(kind, "│")} ${padVisible(line, contentWidth)} ${externalToolBorderColor(kind, "│")}`);
|
|
398
|
-
}
|
|
399
|
-
framed.push(externalToolBorderColor(kind, `╰${"─".repeat(innerWidth)}╯`));
|
|
400
|
-
return framed;
|
|
379
|
+
return footerMeta
|
|
380
|
+
? [...body, `\x1b[2m${truncateToWidth(`└─ ${footerMeta}`, renderWidth)}\x1b[22m`]
|
|
381
|
+
: body;
|
|
401
382
|
}
|
|
402
383
|
|
|
403
384
|
function renderExternalToolLines(
|
|
@@ -405,13 +386,13 @@ function renderExternalToolLines(
|
|
|
405
386
|
width: number,
|
|
406
387
|
kind: ExternalToolFrameKind,
|
|
407
388
|
expanded = false,
|
|
389
|
+
toolName?: string,
|
|
390
|
+
durationMs?: number,
|
|
408
391
|
): string[] {
|
|
409
392
|
const contentLines = trimRenderedToolLines(lines).filter((line) => !isBlankRenderedLine(line));
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
? renderExternalToolBoxLines(contentLines, width, kind, expanded)
|
|
414
|
-
: renderExternalToolBackgroundLines(contentLines, width, kind, expanded);
|
|
393
|
+
return contentLines.length > 0
|
|
394
|
+
? renderExternalToolBackgroundLines(contentLines, width, kind, expanded, toolName, durationMs)
|
|
395
|
+
: [];
|
|
415
396
|
}
|
|
416
397
|
|
|
417
398
|
async function installExternalToolFramePatch(): Promise<void> {
|
|
@@ -421,7 +402,7 @@ async function installExternalToolFramePatch(): Promise<void> {
|
|
|
421
402
|
ToolExecutionComponent?: ToolExecutionComponentCtor;
|
|
422
403
|
};
|
|
423
404
|
const proto = mod.ToolExecutionComponent?.prototype as
|
|
424
|
-
| (ToolExecutionComponentCtor["prototype"] & { [PATCHED_EXTERNAL_TOOL_FRAME]?:
|
|
405
|
+
| (ToolExecutionComponentCtor["prototype"] & { [PATCHED_EXTERNAL_TOOL_FRAME]?: number })
|
|
425
406
|
| undefined;
|
|
426
407
|
if (!proto?.render || proto[PATCHED_EXTERNAL_TOOL_FRAME] === EXTERNAL_TOOL_FRAME_PATCH_VERSION) return;
|
|
427
408
|
|
|
@@ -435,24 +416,33 @@ async function installExternalToolFramePatch(): Promise<void> {
|
|
|
435
416
|
};
|
|
436
417
|
|
|
437
418
|
proto.render = function patchedRender(this: PatchableToolExecutionComponent, width: number) {
|
|
438
|
-
const rendered = render.call(this, width);
|
|
439
419
|
const kind = externalToolFrameKind(this.toolName);
|
|
420
|
+
if (kind) this.__xtrmExternalStartedAt ??= Date.now();
|
|
421
|
+
const rendered = render.call(this, width);
|
|
440
422
|
if (!kind || rendered.length === 0) return rendered;
|
|
441
423
|
|
|
424
|
+
if (this.result && this.__xtrmExternalDurationMs == null) {
|
|
425
|
+
this.__xtrmExternalDurationMs = Date.now() - (this.__xtrmExternalStartedAt ?? Date.now());
|
|
426
|
+
}
|
|
442
427
|
const firstContentIndex = rendered.findIndex((line) => !isBlankRenderedLine(line));
|
|
443
428
|
const leading = firstContentIndex > 0 ? rendered.slice(0, firstContentIndex) : [];
|
|
444
429
|
const content = extractResultTextLines(this) ?? rendered;
|
|
445
|
-
const styled = renderExternalToolLines(
|
|
430
|
+
const styled = renderExternalToolLines(
|
|
431
|
+
content,
|
|
432
|
+
width,
|
|
433
|
+
kind,
|
|
434
|
+
Boolean(this.expanded),
|
|
435
|
+
this.toolName,
|
|
436
|
+
this.__xtrmExternalDurationMs,
|
|
437
|
+
);
|
|
446
438
|
return styled.length > 0 ? [...leading, ...styled] : rendered;
|
|
447
439
|
};
|
|
448
440
|
|
|
449
441
|
proto[PATCHED_EXTERNAL_TOOL_FRAME] = EXTERNAL_TOOL_FRAME_PATCH_VERSION;
|
|
450
442
|
}
|
|
451
443
|
|
|
452
|
-
function applyThinkingChrome(ctx: ExtensionContext
|
|
453
|
-
(ctx.ui as { setHiddenThinkingLabel?: (label?: string) => void }).setHiddenThinkingLabel?.(
|
|
454
|
-
prefs.hideThinkingPlaceholder ? undefined : "",
|
|
455
|
-
);
|
|
444
|
+
function applyThinkingChrome(ctx: ExtensionContext): void {
|
|
445
|
+
(ctx.ui as { setHiddenThinkingLabel?: (label?: string) => void }).setHiddenThinkingLabel?.("");
|
|
456
446
|
}
|
|
457
447
|
|
|
458
448
|
// ============================================================================
|
|
@@ -464,10 +454,9 @@ function fitVisible(text: string, width: number): string {
|
|
|
464
454
|
return truncated + " ".repeat(Math.max(0, width - visibleWidth(truncated)));
|
|
465
455
|
}
|
|
466
456
|
|
|
467
|
-
function resolveThemeForPrefs(prefs: XtrmUiPrefs):
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
return base as XtrmThemeName;
|
|
457
|
+
function resolveThemeForPrefs(prefs: XtrmUiPrefs): XtrmResolvedThemeName {
|
|
458
|
+
if (prefs.toolRowBg) return prefs.themeName;
|
|
459
|
+
return prefs.themeName === "xtrm-light" ? "xtrm-light-flattools" : "xtrm-dark-flattools";
|
|
471
460
|
}
|
|
472
461
|
|
|
473
462
|
function formatThinking(level: string): string {
|
|
@@ -477,108 +466,62 @@ function formatThinking(level: string): string {
|
|
|
477
466
|
function applyXtrmChrome(
|
|
478
467
|
ctx: ExtensionContext,
|
|
479
468
|
prefs: XtrmUiPrefs,
|
|
480
|
-
getThinkingLevel: () => string
|
|
469
|
+
getThinkingLevel: () => string,
|
|
481
470
|
): void {
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
// Tool expansion
|
|
486
|
-
ctx.ui.setToolsExpanded(!prefs.compactTools);
|
|
471
|
+
if (prefs.forceTheme) {
|
|
472
|
+
ctx.ui.setTheme(resolveThemeForPrefs(prefs));
|
|
473
|
+
}
|
|
487
474
|
|
|
488
|
-
|
|
475
|
+
ctx.ui.setToolsExpanded(false);
|
|
489
476
|
ctx.ui.setEditorComponent((tui, theme, keybindings) => {
|
|
490
477
|
const editor = new XtrmEditor(tui, theme, keybindings);
|
|
491
478
|
editor.setPrefs(prefs);
|
|
492
479
|
return editor;
|
|
493
480
|
});
|
|
494
481
|
|
|
495
|
-
|
|
496
|
-
if (prefs.showHeader) {
|
|
497
|
-
ctx.ui.setHeader((_tui, theme) => ({
|
|
498
|
-
invalidate() {},
|
|
499
|
-
render(width: number): string[] {
|
|
500
|
-
const boxWidth = width >= 54 ? 50 : Math.max(24, width);
|
|
501
|
-
const model = ctx.model?.id ?? "no-model";
|
|
502
|
-
const thinking = getThinkingLevel();
|
|
503
|
-
const border = (text: string) => theme.fg("borderAccent", text);
|
|
504
|
-
const leftPad = "";
|
|
505
|
-
|
|
506
|
-
const top = leftPad + border(`╭${"─".repeat(Math.max(0, boxWidth - 2))}╮`);
|
|
507
|
-
const line1 =
|
|
508
|
-
leftPad +
|
|
509
|
-
border("│") +
|
|
510
|
-
fitVisible(
|
|
511
|
-
` ${theme.fg("dim", ">_")} ${theme.bold("XTRM")} ${theme.fg("dim", `(v1.0.0)`)}`,
|
|
512
|
-
boxWidth - 2
|
|
513
|
-
) +
|
|
514
|
-
border("│");
|
|
515
|
-
const gap = leftPad + border("│") + fitVisible("", boxWidth - 2) + border("│");
|
|
516
|
-
const line2 =
|
|
517
|
-
leftPad +
|
|
518
|
-
border("│") +
|
|
519
|
-
fitVisible(
|
|
520
|
-
` ${theme.fg("dim", "model:".padEnd(11))}${model} ${thinking}${theme.fg("accent", " /model")}${theme.fg("dim", " to change")}`,
|
|
521
|
-
boxWidth - 2
|
|
522
|
-
) +
|
|
523
|
-
border("│");
|
|
524
|
-
const line3 =
|
|
525
|
-
leftPad +
|
|
526
|
-
border("│") +
|
|
527
|
-
fitVisible(
|
|
528
|
-
` ${theme.fg("dim", "directory:".padEnd(11))}${basename(ctx.cwd)}`,
|
|
529
|
-
boxWidth - 2
|
|
530
|
-
) +
|
|
531
|
-
border("│");
|
|
532
|
-
const bottom = leftPad + border(`╰${"─".repeat(Math.max(0, boxWidth - 2))}╯`);
|
|
533
|
-
|
|
534
|
-
return [top, line1, gap, line2, line3, bottom];
|
|
535
|
-
},
|
|
536
|
-
}));
|
|
537
|
-
} else {
|
|
482
|
+
if (!prefs.showHeader) {
|
|
538
483
|
ctx.ui.setHeader(undefined);
|
|
484
|
+
return;
|
|
539
485
|
}
|
|
540
486
|
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
const
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
487
|
+
ctx.ui.setHeader((_tui, theme) => ({
|
|
488
|
+
invalidate() {},
|
|
489
|
+
render(width: number): string[] {
|
|
490
|
+
const boxWidth = width >= 54 ? 50 : Math.max(24, width);
|
|
491
|
+
const model = ctx.model?.id ?? "no-model";
|
|
492
|
+
const thinking = getThinkingLevel();
|
|
493
|
+
const border = (text: string) => theme.fg("borderAccent", text);
|
|
494
|
+
const top = border(`╭${"─".repeat(Math.max(0, boxWidth - 2))}╮`);
|
|
495
|
+
const line1 =
|
|
496
|
+
border("│") +
|
|
497
|
+
fitVisible(
|
|
498
|
+
` ${theme.fg("dim", ">_")} ${theme.bold("XTRM")} ${theme.fg("dim", "(v1.0.0)")}`,
|
|
499
|
+
boxWidth - 2,
|
|
500
|
+
) +
|
|
501
|
+
border("│");
|
|
502
|
+
const gap = border("│") + fitVisible("", boxWidth - 2) + border("│");
|
|
503
|
+
const line2 =
|
|
504
|
+
border("│") +
|
|
505
|
+
fitVisible(
|
|
506
|
+
` ${theme.fg("dim", "model:".padEnd(11))}${model} ${thinking}${theme.fg("accent", " /model")}${theme.fg("dim", " to change")}`,
|
|
507
|
+
boxWidth - 2,
|
|
508
|
+
) +
|
|
509
|
+
border("│");
|
|
510
|
+
const line3 =
|
|
511
|
+
border("│") +
|
|
512
|
+
fitVisible(
|
|
513
|
+
` ${theme.fg("dim", "directory:".padEnd(11))}${basename(ctx.cwd)}`,
|
|
514
|
+
boxWidth - 2,
|
|
515
|
+
) +
|
|
516
|
+
border("│");
|
|
517
|
+
const bottom = border(`╰${"─".repeat(Math.max(0, boxWidth - 2))}╯`);
|
|
518
|
+
|
|
519
|
+
return [top, line1, gap, line2, line3, bottom];
|
|
520
|
+
},
|
|
521
|
+
}));
|
|
566
522
|
}
|
|
567
523
|
|
|
568
524
|
|
|
569
|
-
function writeExternalCompactFlag(enabled: boolean): void {
|
|
570
|
-
try {
|
|
571
|
-
const settingsPath = join(process.env.HOME ?? "", ".pi", "agent", "settings.json");
|
|
572
|
-
if (!settingsPath) return;
|
|
573
|
-
let settings: Record<string, unknown> = {};
|
|
574
|
-
if (existsSync(settingsPath)) {
|
|
575
|
-
try { settings = JSON.parse(readFileSync(settingsPath, "utf8")); } catch {}
|
|
576
|
-
}
|
|
577
|
-
settings.xtrmExternalCompact = enabled;
|
|
578
|
-
writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n", "utf8");
|
|
579
|
-
} catch {}
|
|
580
|
-
}
|
|
581
|
-
|
|
582
525
|
// ============================================================================
|
|
583
526
|
// Tool Render Helpers
|
|
584
527
|
// ============================================================================
|
|
@@ -642,8 +585,8 @@ function sendInfoMessage(pi: ExtensionAPI, title: string, content: string): void
|
|
|
642
585
|
|
|
643
586
|
function parseThemeArg(arg: string): XtrmThemeName | undefined {
|
|
644
587
|
const normalized = arg.trim().toLowerCase();
|
|
645
|
-
if (normalized === "dark"
|
|
646
|
-
if (normalized === "light"
|
|
588
|
+
if (normalized === "dark") return "xtrm-dark";
|
|
589
|
+
if (normalized === "light") return "xtrm-light";
|
|
647
590
|
return undefined;
|
|
648
591
|
}
|
|
649
592
|
|
|
@@ -654,14 +597,19 @@ function parseDensityArg(arg: string): XtrmDensity | undefined {
|
|
|
654
597
|
return undefined;
|
|
655
598
|
}
|
|
656
599
|
|
|
657
|
-
function
|
|
600
|
+
function parseToggleArg(arg: string): boolean | undefined {
|
|
658
601
|
const normalized = arg.trim().toLowerCase();
|
|
659
|
-
if (normalized === "
|
|
660
|
-
if (normalized === "
|
|
602
|
+
if (normalized === "on") return true;
|
|
603
|
+
if (normalized === "off") return false;
|
|
661
604
|
return undefined;
|
|
662
605
|
}
|
|
663
606
|
|
|
664
|
-
function registerCommands(
|
|
607
|
+
function registerCommands(
|
|
608
|
+
pi: ExtensionAPI,
|
|
609
|
+
getPrefs: () => XtrmUiPrefs,
|
|
610
|
+
setPrefs: (prefs: XtrmUiPrefs) => void,
|
|
611
|
+
getThinkingLevel: () => string,
|
|
612
|
+
): void {
|
|
665
613
|
pi.registerMessageRenderer("xtrm-ui-info", (message, _options, theme) => {
|
|
666
614
|
const title = (message.details as { title?: string } | undefined)?.title ?? "XTRM UI";
|
|
667
615
|
const box = new Box(1, 1, (text) => theme.bg("customMessageBg", text));
|
|
@@ -673,41 +621,22 @@ function registerCommands(pi: ExtensionAPI, getPrefs: () => XtrmUiPrefs, setPref
|
|
|
673
621
|
pi.registerCommand("xtrm-ui", {
|
|
674
622
|
description: "Show XTRM UI status and active preferences",
|
|
675
623
|
handler: async (args, ctx) => {
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
const [subcommand, ...rest] = trimmedArgs.split(/\s+/u);
|
|
679
|
-
if (subcommand === "chrome" || subcommand === "external-chrome" || subcommand === "tool-chrome") {
|
|
680
|
-
const externalToolChrome = parseExternalToolChromeArg(rest.join(" "));
|
|
681
|
-
if (!externalToolChrome) {
|
|
682
|
-
ctx.ui.notify("Usage: /xtrm-ui chrome background|box", "warning");
|
|
683
|
-
return;
|
|
684
|
-
}
|
|
685
|
-
const prefs = { ...getPrefs(), externalToolChrome };
|
|
686
|
-
setPrefs(prefs);
|
|
687
|
-
persistPrefs(pi, prefs);
|
|
688
|
-
ctx.ui.notify(`External tool chrome set to ${externalToolChrome}.`, "info");
|
|
689
|
-
return;
|
|
690
|
-
}
|
|
691
|
-
ctx.ui.notify("Usage: /xtrm-ui [chrome background|box]", "warning");
|
|
624
|
+
if (args.trim()) {
|
|
625
|
+
ctx.ui.notify("Usage: /xtrm-ui", "warning");
|
|
692
626
|
return;
|
|
693
627
|
}
|
|
694
628
|
|
|
695
629
|
const prefs = getPrefs();
|
|
696
630
|
const contextUsage = ctx.getContextUsage();
|
|
697
|
-
|
|
631
|
+
sendInfoMessage(pi, "XTRM UI status", [
|
|
698
632
|
`Theme: ${prefs.themeName}`,
|
|
699
|
-
`Force theme: ${prefs.forceTheme ? "on" : "off"}`,
|
|
700
633
|
`Density: ${prefs.density}`,
|
|
701
|
-
`Compact tools: ${prefs.compactTools ? "on" : "off"}`,
|
|
702
634
|
`Show header: ${prefs.showHeader ? "yes" : "no"}`,
|
|
703
|
-
`
|
|
635
|
+
`Force theme: ${prefs.forceTheme ? "on" : "off"}`,
|
|
704
636
|
`Tool row background: ${prefs.toolRowBg ? "on" : "off"}`,
|
|
705
|
-
`Compact external tool results: ${prefs.compactExternalToolResults ? "on" : "off"}`,
|
|
706
|
-
`External tool chrome: ${prefs.externalToolChrome}`,
|
|
707
637
|
`Model: ${ctx.model?.id ?? "none"}`,
|
|
708
638
|
`Context: ${contextUsage?.tokens ?? "unknown"}/${contextUsage?.contextWindow ?? "unknown"}`,
|
|
709
|
-
];
|
|
710
|
-
sendInfoMessage(pi, "XTRM UI status", lines.join("\\n"));
|
|
639
|
+
].join("\n"));
|
|
711
640
|
},
|
|
712
641
|
});
|
|
713
642
|
|
|
@@ -732,7 +661,7 @@ function registerCommands(pi: ExtensionAPI, getPrefs: () => XtrmUiPrefs, setPref
|
|
|
732
661
|
});
|
|
733
662
|
|
|
734
663
|
pi.registerCommand("xtrm-ui-density", {
|
|
735
|
-
description: "Switch
|
|
664
|
+
description: "Switch editor density: compact|comfortable",
|
|
736
665
|
getArgumentCompletions: (prefix) => {
|
|
737
666
|
const values = ["compact", "comfortable"].filter((item) => item.startsWith(prefix));
|
|
738
667
|
return values.length > 0 ? values.map((value) => ({ value, label: value })) : null;
|
|
@@ -743,10 +672,9 @@ function registerCommands(pi: ExtensionAPI, getPrefs: () => XtrmUiPrefs, setPref
|
|
|
743
672
|
ctx.ui.notify("Usage: /xtrm-ui-density compact|comfortable", "warning");
|
|
744
673
|
return;
|
|
745
674
|
}
|
|
746
|
-
const prefs = { ...getPrefs(), density
|
|
675
|
+
const prefs = { ...getPrefs(), density };
|
|
747
676
|
setPrefs(prefs);
|
|
748
677
|
persistPrefs(pi, prefs);
|
|
749
|
-
writeExternalCompactFlag(prefs.compactExternalToolResults);
|
|
750
678
|
applyXtrmChrome(ctx, prefs, getThinkingLevel);
|
|
751
679
|
ctx.ui.notify(`XTRM UI density set to ${density}`, "info");
|
|
752
680
|
},
|
|
@@ -759,7 +687,11 @@ function registerCommands(pi: ExtensionAPI, getPrefs: () => XtrmUiPrefs, setPref
|
|
|
759
687
|
return values.length > 0 ? values.map((value) => ({ value, label: value })) : null;
|
|
760
688
|
},
|
|
761
689
|
handler: async (args, ctx) => {
|
|
762
|
-
const showHeader = args
|
|
690
|
+
const showHeader = parseToggleArg(args);
|
|
691
|
+
if (showHeader === undefined) {
|
|
692
|
+
ctx.ui.notify("Usage: /xtrm-ui-header on|off", "warning");
|
|
693
|
+
return;
|
|
694
|
+
}
|
|
763
695
|
const prefs = { ...getPrefs(), showHeader };
|
|
764
696
|
setPrefs(prefs);
|
|
765
697
|
persistPrefs(pi, prefs);
|
|
@@ -775,12 +707,11 @@ function registerCommands(pi: ExtensionAPI, getPrefs: () => XtrmUiPrefs, setPref
|
|
|
775
707
|
return values.length > 0 ? values.map((value) => ({ value, label: value })) : null;
|
|
776
708
|
},
|
|
777
709
|
handler: async (args, ctx) => {
|
|
778
|
-
const
|
|
779
|
-
if (
|
|
710
|
+
const forceTheme = parseToggleArg(args);
|
|
711
|
+
if (forceTheme === undefined) {
|
|
780
712
|
ctx.ui.notify("Usage: /xtrm-ui-forcetheme on|off", "warning");
|
|
781
713
|
return;
|
|
782
714
|
}
|
|
783
|
-
const forceTheme = normalized === "on";
|
|
784
715
|
const prefs = { ...getPrefs(), forceTheme };
|
|
785
716
|
setPrefs(prefs);
|
|
786
717
|
persistPrefs(pi, prefs);
|
|
@@ -796,12 +727,11 @@ function registerCommands(pi: ExtensionAPI, getPrefs: () => XtrmUiPrefs, setPref
|
|
|
796
727
|
return values.length > 0 ? values.map((value) => ({ value, label: value })) : null;
|
|
797
728
|
},
|
|
798
729
|
handler: async (args, ctx) => {
|
|
799
|
-
const
|
|
800
|
-
if (
|
|
730
|
+
const toolRowBg = parseToggleArg(args);
|
|
731
|
+
if (toolRowBg === undefined) {
|
|
801
732
|
ctx.ui.notify("Usage: /xtrm-ui-rowbg on|off", "warning");
|
|
802
733
|
return;
|
|
803
734
|
}
|
|
804
|
-
const toolRowBg = normalized === "on";
|
|
805
735
|
const prefs = { ...getPrefs(), toolRowBg };
|
|
806
736
|
setPrefs(prefs);
|
|
807
737
|
persistPrefs(pi, prefs);
|
|
@@ -810,49 +740,6 @@ function registerCommands(pi: ExtensionAPI, getPrefs: () => XtrmUiPrefs, setPref
|
|
|
810
740
|
},
|
|
811
741
|
});
|
|
812
742
|
|
|
813
|
-
pi.registerCommand("xtrm-ui-compact-tools", {
|
|
814
|
-
description: "Compact extension tool results: on|off (off keeps full Ctrl+O expand output)",
|
|
815
|
-
getArgumentCompletions: (prefix) => {
|
|
816
|
-
const values = ["on", "off"].filter((item) => item.startsWith(prefix));
|
|
817
|
-
return values.length > 0 ? values.map((value) => ({ value, label: value })) : null;
|
|
818
|
-
},
|
|
819
|
-
handler: async (args, ctx) => {
|
|
820
|
-
const normalized = args.trim().toLowerCase();
|
|
821
|
-
if (normalized !== "on" && normalized !== "off") {
|
|
822
|
-
ctx.ui.notify("Usage: /xtrm-ui-compact-tools on|off", "warning");
|
|
823
|
-
return;
|
|
824
|
-
}
|
|
825
|
-
const compactExternalToolResults = normalized === "on";
|
|
826
|
-
const prefs = { ...getPrefs(), compactExternalToolResults };
|
|
827
|
-
setPrefs(prefs);
|
|
828
|
-
persistPrefs(pi, prefs);
|
|
829
|
-
writeExternalCompactFlag(compactExternalToolResults);
|
|
830
|
-
ctx.ui.notify(
|
|
831
|
-
`Compact external tool results ${compactExternalToolResults ? "enabled" : "disabled"}.`,
|
|
832
|
-
"info",
|
|
833
|
-
);
|
|
834
|
-
},
|
|
835
|
-
});
|
|
836
|
-
|
|
837
|
-
pi.registerCommand("xtrm-ui-external-chrome", {
|
|
838
|
-
description: "Choose non-native tool chrome: background|box",
|
|
839
|
-
getArgumentCompletions: (prefix) => {
|
|
840
|
-
const values = ["background", "box"].filter((item) => item.startsWith(prefix));
|
|
841
|
-
return values.length > 0 ? values.map((value) => ({ value, label: value })) : null;
|
|
842
|
-
},
|
|
843
|
-
handler: async (args, ctx) => {
|
|
844
|
-
const externalToolChrome = parseExternalToolChromeArg(args);
|
|
845
|
-
if (!externalToolChrome) {
|
|
846
|
-
ctx.ui.notify("Usage: /xtrm-ui-external-chrome background|box", "warning");
|
|
847
|
-
return;
|
|
848
|
-
}
|
|
849
|
-
const prefs = { ...getPrefs(), externalToolChrome };
|
|
850
|
-
setPrefs(prefs);
|
|
851
|
-
persistPrefs(pi, prefs);
|
|
852
|
-
ctx.ui.notify(`External tool chrome set to ${externalToolChrome}.`, "info");
|
|
853
|
-
},
|
|
854
|
-
});
|
|
855
|
-
|
|
856
743
|
pi.registerCommand("xtrm-ui-reset", {
|
|
857
744
|
description: "Restore XTRM UI defaults",
|
|
858
745
|
handler: async (_args, ctx) => {
|
|
@@ -866,25 +753,25 @@ function registerCommands(pi: ExtensionAPI, getPrefs: () => XtrmUiPrefs, setPref
|
|
|
866
753
|
}
|
|
867
754
|
|
|
868
755
|
// ============================================================================
|
|
869
|
-
// Tool Renderers
|
|
756
|
+
// XTRM Tool Renderers
|
|
870
757
|
// ============================================================================
|
|
871
758
|
|
|
872
759
|
type BuiltInTools = ReturnType<typeof createBuiltInTools>;
|
|
873
760
|
|
|
874
|
-
type XtrmMeta<TArgs = Record<string, unknown>> = {
|
|
875
|
-
tool: string;
|
|
876
|
-
args: TArgs;
|
|
877
|
-
durationMs: number;
|
|
878
|
-
};
|
|
879
|
-
|
|
880
761
|
type XtrmWritePreview =
|
|
881
762
|
| { kind: "created"; lineCount: number }
|
|
882
763
|
| { kind: "updated"; diff: string; additions: number; removals: number }
|
|
883
764
|
| { kind: "unchanged" };
|
|
884
765
|
|
|
885
|
-
type
|
|
886
|
-
|
|
887
|
-
|
|
766
|
+
type XtrmToolRenderState = {
|
|
767
|
+
startedAt?: number;
|
|
768
|
+
writePreview?: XtrmWritePreview;
|
|
769
|
+
};
|
|
770
|
+
|
|
771
|
+
type XtrmToolRenderContext = {
|
|
772
|
+
executionStarted: boolean;
|
|
773
|
+
isPartial: boolean;
|
|
774
|
+
state: XtrmToolRenderState;
|
|
888
775
|
};
|
|
889
776
|
|
|
890
777
|
const toolCache = new Map<string, BuiltInTools>();
|
|
@@ -910,22 +797,6 @@ function getTools(cwd: string): BuiltInTools {
|
|
|
910
797
|
return tools;
|
|
911
798
|
}
|
|
912
799
|
|
|
913
|
-
function withXtrmMeta<TDetails extends object, TArgs extends Record<string, unknown>>(
|
|
914
|
-
details: TDetails | undefined,
|
|
915
|
-
tool: string,
|
|
916
|
-
args: TArgs,
|
|
917
|
-
durationMs: number,
|
|
918
|
-
): DetailsWithXtrmMeta<TDetails, TArgs> {
|
|
919
|
-
return { ...(details ?? ({} as TDetails)), xtrmMeta: { tool, args, durationMs } };
|
|
920
|
-
}
|
|
921
|
-
|
|
922
|
-
function getXtrmMeta<TDetails extends object, TArgs extends Record<string, unknown>>(
|
|
923
|
-
details: TDetails | undefined,
|
|
924
|
-
): XtrmMeta<TArgs> | undefined {
|
|
925
|
-
if (!details || typeof details !== "object") return undefined;
|
|
926
|
-
return (details as DetailsWithXtrmMeta<TDetails, TArgs>).xtrmMeta;
|
|
927
|
-
}
|
|
928
|
-
|
|
929
800
|
function getTextContent(result: { content: Array<{ type: string; text?: string }> }): string {
|
|
930
801
|
const item = result.content.find((content) => content.type === "text");
|
|
931
802
|
return item?.text ?? "";
|
|
@@ -955,12 +826,17 @@ function createWritePreview(path: string, nextContent: string): XtrmWritePreview
|
|
|
955
826
|
};
|
|
956
827
|
}
|
|
957
828
|
|
|
958
|
-
function
|
|
959
|
-
|
|
829
|
+
function appendToolFooter(theme: any, text: string, parts: Array<string | undefined>): string {
|
|
830
|
+
const meta = joinMeta(parts);
|
|
831
|
+
return meta ? `${text}\n${theme.fg("dim", `└─ ${meta}`)}` : text;
|
|
960
832
|
}
|
|
961
833
|
|
|
962
|
-
function
|
|
963
|
-
|
|
834
|
+
function renderPendingCall(toolName: string, args: Record<string, unknown>, theme: any): Text {
|
|
835
|
+
if (toolName === "bash") {
|
|
836
|
+
const command = shortenCommand(String(args.command ?? ""), 80);
|
|
837
|
+
return new Text(`${theme.fg("accent", TOOL_ROW_MARKER)} ${theme.fg("accent", "$")} ${theme.fg("accent", command)}`, 0, 0);
|
|
838
|
+
}
|
|
839
|
+
return new Text(renderToolSummary(theme, "pending", toolName, summarizeToolSubject(toolName, args), undefined), 0, 0);
|
|
964
840
|
}
|
|
965
841
|
|
|
966
842
|
function summarizeToolSubject(toolName: string, args: Record<string, unknown>): string | undefined {
|
|
@@ -1025,55 +901,10 @@ const SERENA_COMPACT_TOOLS = new Set([
|
|
|
1025
901
|
"serena_mcp_reset",
|
|
1026
902
|
]);
|
|
1027
903
|
|
|
1028
|
-
function parseJson(text: string): unknown | undefined {
|
|
1029
|
-
try {
|
|
1030
|
-
return JSON.parse(text);
|
|
1031
|
-
} catch {
|
|
1032
|
-
return undefined;
|
|
1033
|
-
}
|
|
1034
|
-
}
|
|
1035
|
-
|
|
1036
904
|
function asRecord(value: unknown): Record<string, unknown> | undefined {
|
|
1037
905
|
return value && typeof value === "object" && !Array.isArray(value) ? value as Record<string, unknown> : undefined;
|
|
1038
906
|
}
|
|
1039
907
|
|
|
1040
|
-
function countSearchMatches(payload: unknown): number | undefined {
|
|
1041
|
-
const record = asRecord(payload);
|
|
1042
|
-
if (!record) return undefined;
|
|
1043
|
-
let total = 0;
|
|
1044
|
-
for (const value of Object.values(record)) {
|
|
1045
|
-
if (Array.isArray(value)) total += value.length;
|
|
1046
|
-
}
|
|
1047
|
-
return total > 0 ? total : undefined;
|
|
1048
|
-
}
|
|
1049
|
-
|
|
1050
|
-
function countOverviewSymbols(payload: unknown): number {
|
|
1051
|
-
if (Array.isArray(payload)) {
|
|
1052
|
-
const nested = payload.reduce<number>((total, value) => total + countOverviewSymbols(value), 0);
|
|
1053
|
-
return nested || payload.length;
|
|
1054
|
-
}
|
|
1055
|
-
const record = asRecord(payload);
|
|
1056
|
-
if (!record) return 0;
|
|
1057
|
-
return Object.values(record).reduce<number>((total, value) => total + countOverviewSymbols(value), 0);
|
|
1058
|
-
}
|
|
1059
|
-
|
|
1060
|
-
function countLines(text: string): number {
|
|
1061
|
-
if (!text) return 0;
|
|
1062
|
-
return text.split("\n").length;
|
|
1063
|
-
}
|
|
1064
|
-
|
|
1065
|
-
function countJsonItems(payload: unknown): number | undefined {
|
|
1066
|
-
if (Array.isArray(payload)) return payload.length;
|
|
1067
|
-
const record = asRecord(payload);
|
|
1068
|
-
if (!record) return undefined;
|
|
1069
|
-
|
|
1070
|
-
let total = 0;
|
|
1071
|
-
for (const value of Object.values(record)) {
|
|
1072
|
-
if (Array.isArray(value)) total += value.length;
|
|
1073
|
-
}
|
|
1074
|
-
return total > 0 ? total : undefined;
|
|
1075
|
-
}
|
|
1076
|
-
|
|
1077
908
|
function summarizeSerenaSubject(toolName: string, input: Record<string, unknown>): string | undefined {
|
|
1078
909
|
switch (toolName) {
|
|
1079
910
|
case "find_symbol":
|
|
@@ -1120,104 +951,8 @@ function summarizeSerenaSubject(toolName: string, input: Record<string, unknown>
|
|
|
1120
951
|
}
|
|
1121
952
|
}
|
|
1122
953
|
|
|
1123
|
-
function summarizeSerenaToolResult(
|
|
1124
|
-
toolName: string,
|
|
1125
|
-
input: Record<string, unknown>,
|
|
1126
|
-
text: string,
|
|
1127
|
-
durationMs: number | undefined,
|
|
1128
|
-
): string {
|
|
1129
|
-
const payload = parseJson(text);
|
|
1130
|
-
const duration = formatDuration(durationMs);
|
|
1131
|
-
const payloadSize = formatPayloadSize(text);
|
|
1132
|
-
const subject = summarizeSerenaSubject(toolName, input);
|
|
1133
|
-
const meta = (...parts: Array<string | undefined>) => {
|
|
1134
|
-
const joined = joinCompactMeta([duration, payloadSize, ...parts]);
|
|
1135
|
-
return joined ? ` · ${joined}` : "";
|
|
1136
|
-
};
|
|
1137
|
-
|
|
1138
|
-
switch (toolName) {
|
|
1139
|
-
case "find_symbol":
|
|
1140
|
-
case "find_referencing_symbols":
|
|
1141
|
-
case "jet_brains_find_symbol":
|
|
1142
|
-
case "jet_brains_find_referencing_symbols": {
|
|
1143
|
-
const count = countJsonItems(payload) ?? (text.match(/"name_path"\s*:/g)?.length ?? 0);
|
|
1144
|
-
return `${TOOL_ROW_MARKER} serena ${toolName} ${subject ?? "symbol"}${meta(formatLineLabel(count, "result"))}`;
|
|
1145
|
-
}
|
|
1146
|
-
case "get_symbols_overview":
|
|
1147
|
-
case "jet_brains_get_symbols_overview":
|
|
1148
|
-
case "jet_brains_type_hierarchy": {
|
|
1149
|
-
const count = Math.max(countOverviewSymbols(payload), text.match(/"name_path"\s*:/g)?.length ?? 0);
|
|
1150
|
-
return `${TOOL_ROW_MARKER} serena ${toolName} ${subject ?? "file"}${meta(formatLineLabel(count, "symbol"))}`;
|
|
1151
|
-
}
|
|
1152
|
-
case "search_for_pattern": {
|
|
1153
|
-
const count = countSearchMatches(payload) ?? (text.match(/^\s*>\s*\d+:/gm)?.length ?? 0);
|
|
1154
|
-
return `${TOOL_ROW_MARKER} serena search ${subject ?? "pattern"}${meta(formatLineLabel(count, "match"))}`;
|
|
1155
|
-
}
|
|
1156
|
-
case "read_file": {
|
|
1157
|
-
return `${TOOL_ROW_MARKER} serena read ${subject ?? "file"}${meta(formatLineLabel(countLines(text), "line"))}`;
|
|
1158
|
-
}
|
|
1159
|
-
case "list_dir": {
|
|
1160
|
-
const count = countJsonItems(payload) ?? countLines(text);
|
|
1161
|
-
return `${TOOL_ROW_MARKER} serena list_dir ${subject ?? "."}${meta(formatLineLabel(count, "entry"))}`;
|
|
1162
|
-
}
|
|
1163
|
-
case "find_file": {
|
|
1164
|
-
const count = countJsonItems(payload) ?? countLines(text);
|
|
1165
|
-
return `${TOOL_ROW_MARKER} serena find_file ${String(input.file_mask ?? "")}${meta(formatLineLabel(count, "match"))}`;
|
|
1166
|
-
}
|
|
1167
|
-
case "replace_symbol_body":
|
|
1168
|
-
case "insert_after_symbol":
|
|
1169
|
-
case "insert_before_symbol":
|
|
1170
|
-
case "rename_symbol":
|
|
1171
|
-
case "create_text_file":
|
|
1172
|
-
case "replace_content":
|
|
1173
|
-
case "replace_lines":
|
|
1174
|
-
case "delete_lines":
|
|
1175
|
-
case "insert_at_line":
|
|
1176
|
-
case "write_memory":
|
|
1177
|
-
case "delete_memory":
|
|
1178
|
-
case "rename_memory":
|
|
1179
|
-
case "edit_memory":
|
|
1180
|
-
case "activate_project":
|
|
1181
|
-
case "remove_project":
|
|
1182
|
-
case "switch_modes":
|
|
1183
|
-
case "restart_language_server":
|
|
1184
|
-
case "onboarding":
|
|
1185
|
-
case "serena_mcp_reset":
|
|
1186
|
-
return `${TOOL_ROW_MARKER} serena ${toolName}${subject ? ` ${subject}` : ""}${meta()}`;
|
|
1187
|
-
case "execute_shell_command": {
|
|
1188
|
-
const count = countLines(text);
|
|
1189
|
-
return `${TOOL_ROW_MARKER} serena shell ${subject ?? "command"}${meta(formatLineLabel(count, "line"))}`;
|
|
1190
|
-
}
|
|
1191
|
-
default: {
|
|
1192
|
-
const count = countJsonItems(payload) ?? countLines(text);
|
|
1193
|
-
return `${TOOL_ROW_MARKER} serena ${toolName}${subject ? ` ${subject}` : ""}${meta(formatLineLabel(count, "item"))}`;
|
|
1194
|
-
}
|
|
1195
|
-
}
|
|
1196
|
-
}
|
|
1197
954
|
|
|
1198
955
|
|
|
1199
|
-
function formatHierarchyText(text: string): string {
|
|
1200
|
-
const lines = text.split("\n");
|
|
1201
|
-
const out: string[] = [];
|
|
1202
|
-
for (const raw of lines) {
|
|
1203
|
-
const line = raw.trimEnd();
|
|
1204
|
-
if (!line.trim()) {
|
|
1205
|
-
out.push("");
|
|
1206
|
-
continue;
|
|
1207
|
-
}
|
|
1208
|
-
if (line.startsWith("● ")) {
|
|
1209
|
-
out.push(line);
|
|
1210
|
-
continue;
|
|
1211
|
-
}
|
|
1212
|
-
if (/^(Read|Searched|Listed|Update\(|File must be read first|Now )/.test(line.trimStart())) {
|
|
1213
|
-
out.push(` └─ ${line.trimStart()}`);
|
|
1214
|
-
continue;
|
|
1215
|
-
}
|
|
1216
|
-
out.push(line);
|
|
1217
|
-
}
|
|
1218
|
-
return out.join("\n");
|
|
1219
|
-
}
|
|
1220
|
-
|
|
1221
956
|
function normalizeToolLabel(toolName: string): string {
|
|
1222
957
|
const gitnexusMap: Record<string, string> = {
|
|
1223
958
|
gitnexus_query: "gitnexus query",
|
|
@@ -1245,155 +980,13 @@ function normalizeToolLabel(toolName: string): string {
|
|
|
1245
980
|
return toolName;
|
|
1246
981
|
}
|
|
1247
982
|
|
|
1248
|
-
function summarizeGenericToolResult(
|
|
1249
|
-
toolName: string,
|
|
1250
|
-
input: Record<string, unknown>,
|
|
1251
|
-
text: string,
|
|
1252
|
-
durationMs: number | undefined,
|
|
1253
|
-
): string {
|
|
1254
|
-
const payload = parseJson(text);
|
|
1255
|
-
const duration = formatDuration(durationMs);
|
|
1256
|
-
const payloadSize = formatPayloadSize(text);
|
|
1257
|
-
const subject = summarizeToolSubject(toolName, input) ?? summarizeSerenaSubject(toolName, input);
|
|
1258
|
-
const count = countJsonItems(payload) ?? countLines(text);
|
|
1259
|
-
const label = formatLineLabel(count, "line");
|
|
1260
|
-
const joined = joinCompactMeta([duration, payloadSize, label]);
|
|
1261
|
-
const normalized = normalizeToolLabel(toolName);
|
|
1262
|
-
return `${TOOL_ROW_MARKER} ${normalized}${subject ? ` ${subject}` : ""}${joined ? ` · ${joined}` : ""}`;
|
|
1263
|
-
}
|
|
1264
|
-
|
|
1265
|
-
function summarizeStructuredReturnToolResult(
|
|
1266
|
-
input: Record<string, unknown>,
|
|
1267
|
-
text: string,
|
|
1268
|
-
details: unknown,
|
|
1269
|
-
durationMs: number | undefined,
|
|
1270
|
-
): string {
|
|
1271
|
-
const record = asRecord(details);
|
|
1272
|
-
const command = shortenCommand(String(input.command ?? text.split("→")[0] ?? "command"), 52);
|
|
1273
|
-
const resultText = text.includes("→") ? text.split("→").slice(1).join("→").trim() : text.trim();
|
|
1274
|
-
const resultLines = resultText.split("\n").map((line) => line.trim()).filter(Boolean);
|
|
1275
|
-
const summary = resultLines.find((line) => !line.startsWith("cwd:"));
|
|
1276
|
-
const parser = typeof record?.parser === "string" ? record.parser : undefined;
|
|
1277
|
-
const exitCode = typeof record?.exitCode === "number" ? `exit ${record.exitCode}` : undefined;
|
|
1278
|
-
const resultMeta = joinCompactMeta([formatDuration(durationMs), formatPayloadSize(text)]);
|
|
1279
|
-
const meta = joinMeta([summary ? shortenCommand(summary, 72) : undefined, parser, exitCode, resultMeta]);
|
|
1280
|
-
return `${TOOL_ROW_MARKER} structured_return ${command}${meta ? ` · ${meta}` : ""}`;
|
|
1281
|
-
}
|
|
1282
|
-
|
|
1283
|
-
function summarizeProcessToolResult(
|
|
1284
|
-
input: Record<string, unknown>,
|
|
1285
|
-
text: string,
|
|
1286
|
-
details: unknown,
|
|
1287
|
-
durationMs: number | undefined,
|
|
1288
|
-
): string {
|
|
1289
|
-
const record = asRecord(details);
|
|
1290
|
-
const action = String(record?.action ?? input.action ?? "action");
|
|
1291
|
-
const duration = formatDuration(durationMs);
|
|
1292
|
-
const payloadSize = formatPayloadSize(text);
|
|
1293
|
-
const meta = (...parts: Array<string | undefined>) => {
|
|
1294
|
-
const joined = joinCompactMeta([duration, payloadSize, ...parts]);
|
|
1295
|
-
return joined ? ` · ${joined}` : "";
|
|
1296
|
-
};
|
|
1297
|
-
|
|
1298
|
-
if (action === "start") {
|
|
1299
|
-
const proc = asRecord(record?.process);
|
|
1300
|
-
const name = String(proc?.name ?? input.name ?? "process");
|
|
1301
|
-
const id = proc?.id ? String(proc.id) : undefined;
|
|
1302
|
-
const pid = proc?.pid != null ? `pid ${String(proc.pid)}` : undefined;
|
|
1303
|
-
return `${TOOL_ROW_MARKER} process start "${name}"${meta(id, pid)}`;
|
|
1304
|
-
}
|
|
1305
|
-
|
|
1306
|
-
if (action === "list") {
|
|
1307
|
-
const processes = Array.isArray(record?.processes) ? record.processes : [];
|
|
1308
|
-
const running = processes.filter((item) => {
|
|
1309
|
-
const proc = asRecord(item);
|
|
1310
|
-
return proc?.status === "running" || proc?.status === "terminating";
|
|
1311
|
-
}).length;
|
|
1312
|
-
return `${TOOL_ROW_MARKER} process list${meta(`${processes.length} ${processes.length === 1 ? "process" : "processes"}`, `${running} running`)}`;
|
|
1313
|
-
}
|
|
1314
|
-
|
|
1315
|
-
if (action === "output") {
|
|
1316
|
-
const output = asRecord(record?.output);
|
|
1317
|
-
const stdout = Array.isArray(output?.stdout) ? output.stdout.length : undefined;
|
|
1318
|
-
const stderr = Array.isArray(output?.stderr) ? output.stderr.length : undefined;
|
|
1319
|
-
return `${TOOL_ROW_MARKER} process output ${String(input.id ?? "process")}${meta(
|
|
1320
|
-
stdout != null ? `${stdout} stdout` : undefined,
|
|
1321
|
-
stderr != null ? `${stderr} stderr` : undefined,
|
|
1322
|
-
)}`;
|
|
1323
|
-
}
|
|
1324
|
-
|
|
1325
|
-
if (action === "logs") {
|
|
1326
|
-
return `${TOOL_ROW_MARKER} process logs ${String(input.id ?? "process")}${meta("log paths")}`;
|
|
1327
|
-
}
|
|
1328
|
-
|
|
1329
|
-
const message = typeof record?.message === "string" ? record.message : text.split("\n")[0];
|
|
1330
|
-
return `${TOOL_ROW_MARKER} process ${action}${message ? ` · ${shortenCommand(message, 38)}` : ""}${meta()}`;
|
|
1331
|
-
}
|
|
1332
|
-
|
|
1333
|
-
function summarizeExternalToolResult(
|
|
1334
|
-
toolName: string,
|
|
1335
|
-
input: Record<string, unknown>,
|
|
1336
|
-
text: string,
|
|
1337
|
-
details: unknown,
|
|
1338
|
-
durationMs: number | undefined,
|
|
1339
|
-
): string {
|
|
1340
|
-
if (SERENA_COMPACT_TOOLS.has(toolName)) {
|
|
1341
|
-
return summarizeSerenaToolResult(toolName, input, text, durationMs);
|
|
1342
|
-
}
|
|
1343
|
-
if (toolName === "structured_return") {
|
|
1344
|
-
return summarizeStructuredReturnToolResult(input, text, details, durationMs);
|
|
1345
|
-
}
|
|
1346
|
-
if (toolName === "process") {
|
|
1347
|
-
return summarizeProcessToolResult(input, text, details, durationMs);
|
|
1348
|
-
}
|
|
1349
|
-
return summarizeGenericToolResult(toolName, input, text, durationMs);
|
|
1350
|
-
}
|
|
1351
983
|
|
|
1352
|
-
function withXtrmToolDetails(details: unknown, sourceText: string, toolName: string): unknown {
|
|
1353
|
-
const record = asRecord(details);
|
|
1354
|
-
return {
|
|
1355
|
-
...(record ?? {}),
|
|
1356
|
-
xtrmOriginalText: sourceText,
|
|
1357
|
-
xtrmToolFrame: externalToolFrameKind(toolName),
|
|
1358
|
-
};
|
|
1359
|
-
}
|
|
1360
984
|
|
|
1361
985
|
const XTRM_BUILTIN_TOOLS = new Set(["bash", "read", "edit", "write", "find", "grep", "ls"]);
|
|
1362
986
|
|
|
1363
|
-
// Tools whose RESULT PAYLOAD the model needs verbatim in context. In pi, a tool_result
|
|
1364
|
-
// hook's return value REPLACES the model-facing content (not just the TUI render), so
|
|
1365
|
-
// compacting these would replace the model-facing payload with "· N lines" / "· N results"
|
|
1366
|
-
// for every pi surface that loads xtrm-ui — interactive sessions (where the human can
|
|
1367
|
-
// expand the TUI row but the model cannot) and any headless pi run that doesn't pass
|
|
1368
|
-
// --no-extensions. Specialists are unaffected: they run with --no-extensions and never
|
|
1369
|
-
// load xtrm-ui (selectively re-attach only quality-gates / service-skills / pi-gitnexus /
|
|
1370
|
-
// pi-serena-tools). Mutation/no-payload tools are still summarized below, preserving
|
|
1371
|
-
// most of the token savings.
|
|
1372
|
-
const PAYLOAD_TOOLS = new Set<string>([
|
|
1373
|
-
// Serena reads / inspection
|
|
1374
|
-
"read_file",
|
|
1375
|
-
"find_symbol",
|
|
1376
|
-
"find_referencing_symbols",
|
|
1377
|
-
"get_symbols_overview",
|
|
1378
|
-
"search_for_pattern",
|
|
1379
|
-
"find_file",
|
|
1380
|
-
"list_dir",
|
|
1381
|
-
"read_memory",
|
|
1382
|
-
// JetBrains backend equivalents
|
|
1383
|
-
"jet_brains_find_symbol",
|
|
1384
|
-
"jet_brains_find_referencing_symbols",
|
|
1385
|
-
"jet_brains_get_symbols_overview",
|
|
1386
|
-
"jet_brains_type_hierarchy",
|
|
1387
|
-
// GitNexus reads (same blindness risk)
|
|
1388
|
-
"gitnexus_query",
|
|
1389
|
-
"gitnexus_context",
|
|
1390
|
-
"gitnexus_impact",
|
|
1391
|
-
"gitnexus_detect_changes",
|
|
1392
|
-
]);
|
|
1393
987
|
|
|
1394
988
|
function registerXtrmUiTools(pi: ExtensionAPI, getPrefs: () => XtrmUiPrefs): void {
|
|
1395
|
-
const
|
|
1396
|
-
|
|
989
|
+
const tools = getTools(process.cwd());
|
|
1397
990
|
const toolRowText = (theme: any, text: string) =>
|
|
1398
991
|
new Text(
|
|
1399
992
|
text,
|
|
@@ -1401,110 +994,53 @@ function registerXtrmUiTools(pi: ExtensionAPI, getPrefs: () => XtrmUiPrefs): voi
|
|
|
1401
994
|
0,
|
|
1402
995
|
getPrefs().toolRowBg ? (line: string) => theme.bg("selectedBg", line) : undefined,
|
|
1403
996
|
);
|
|
1404
|
-
const
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
const trackToolCallEnd = (toolCallId: string) => {
|
|
1415
|
-
const signature = activeToolCalls.get(toolCallId);
|
|
1416
|
-
if (!signature) return;
|
|
1417
|
-
activeToolCalls.delete(toolCallId);
|
|
1418
|
-
const next = (activeSignatureCounts.get(signature) ?? 1) - 1;
|
|
1419
|
-
if (next <= 0) activeSignatureCounts.delete(signature);
|
|
1420
|
-
else activeSignatureCounts.set(signature, next);
|
|
1421
|
-
toolCallStartTimes.delete(toolCallId);
|
|
997
|
+
const renderCall = (
|
|
998
|
+
toolName: string,
|
|
999
|
+
args: Record<string, unknown>,
|
|
1000
|
+
theme: any,
|
|
1001
|
+
context: XtrmToolRenderContext,
|
|
1002
|
+
) => {
|
|
1003
|
+
context.state.startedAt ??= Date.now();
|
|
1004
|
+
return context.isPartial && !context.executionStarted
|
|
1005
|
+
? renderPendingCall(toolName, args, theme)
|
|
1006
|
+
: toolRowText(theme, "");
|
|
1422
1007
|
};
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
activeSignatureCounts.has(stableToolSignature(toolName, args));
|
|
1426
|
-
|
|
1427
|
-
const renderPendingCallIfActive = (toolName: string, args: Record<string, unknown>, theme: any) =>
|
|
1428
|
-
isToolCallActive(toolName, args) ? renderPendingCall(toolName, args, theme) : toolRowText(theme, "");
|
|
1429
|
-
|
|
1430
|
-
pi.on("tool_call", async (event) => {
|
|
1431
|
-
trackToolCallStart(event.toolCallId, event.toolName, event.input as Record<string, unknown>);
|
|
1432
|
-
});
|
|
1433
|
-
|
|
1434
|
-
pi.on("tool_execution_end", async (event) => {
|
|
1435
|
-
trackToolCallEnd(event.toolCallId);
|
|
1436
|
-
});
|
|
1437
|
-
|
|
1438
|
-
pi.on("tool_result", async (event: ToolResultEvent, _ctx) => {
|
|
1439
|
-
if (event.isError) return undefined;
|
|
1440
|
-
if (XTRM_BUILTIN_TOOLS.has(event.toolName)) {
|
|
1441
|
-
trackToolCallEnd(event.toolCallId);
|
|
1442
|
-
return undefined;
|
|
1443
|
-
}
|
|
1444
|
-
// Never compact read/inspect tools: the hook return replaces model-facing content,
|
|
1445
|
-
// so summarizing these would blind headless agents/specialists (no row to expand).
|
|
1446
|
-
if (PAYLOAD_TOOLS.has(event.toolName)) return undefined;
|
|
1447
|
-
if (!getPrefs().compactExternalToolResults) return undefined;
|
|
1448
|
-
|
|
1449
|
-
const text = getTextContent({ content: event.content as Array<{ type: string; text?: string }> });
|
|
1450
|
-
const startedAt = toolCallStartTimes.get(event.toolCallId);
|
|
1451
|
-
const durationMs = startedAt != null ? Date.now() - startedAt : undefined;
|
|
1452
|
-
|
|
1453
|
-
const fallbackText = "[non-text result]";
|
|
1454
|
-
const sourceText = text.trim() ? text : fallbackText;
|
|
1455
|
-
|
|
1456
|
-
const safeInput =
|
|
1457
|
-
event.input && typeof event.input === "object" && !Array.isArray(event.input)
|
|
1458
|
-
? (event.input as Record<string, unknown>)
|
|
1459
|
-
: {};
|
|
1460
|
-
|
|
1461
|
-
const compactText = summarizeExternalToolResult(
|
|
1462
|
-
event.toolName,
|
|
1463
|
-
safeInput,
|
|
1464
|
-
sourceText,
|
|
1465
|
-
event.details,
|
|
1466
|
-
durationMs,
|
|
1467
|
-
);
|
|
1468
|
-
|
|
1469
|
-
return {
|
|
1470
|
-
content: [{ type: "text", text: formatHierarchyText(compactText) }],
|
|
1471
|
-
details: withXtrmToolDetails(event.details, sourceText, event.toolName),
|
|
1472
|
-
};
|
|
1473
|
-
});
|
|
1008
|
+
const renderDuration = (context: XtrmToolRenderContext) =>
|
|
1009
|
+
formatDuration(context.state.startedAt == null ? undefined : Date.now() - context.state.startedAt);
|
|
1474
1010
|
|
|
1475
1011
|
pi.registerTool({
|
|
1476
1012
|
name: "bash",
|
|
1477
1013
|
label: "bash",
|
|
1478
|
-
description:
|
|
1479
|
-
parameters:
|
|
1014
|
+
description: tools.bash.description,
|
|
1015
|
+
parameters: tools.bash.parameters,
|
|
1016
|
+
execute: tools.bash.execute,
|
|
1480
1017
|
renderShell: "self",
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
renderResult(result, { expanded, isPartial }, theme) {
|
|
1488
|
-
const details = (result.details ?? {}) as DetailsWithXtrmMeta<BashToolDetails, Record<string, unknown>>;
|
|
1489
|
-
const meta = getXtrmMeta<BashToolDetails, Record<string, unknown>>(details);
|
|
1490
|
-
const command = shortenCommand(String(meta?.args.command ?? ""), 80);
|
|
1018
|
+
renderCall: (args, theme, context) =>
|
|
1019
|
+
renderCall("bash", args as Record<string, unknown>, theme, context),
|
|
1020
|
+
renderResult(result, { expanded, isPartial }, theme, context) {
|
|
1021
|
+
const details = (result.details ?? {}) as BashToolDetails;
|
|
1022
|
+
const args = context.args as Record<string, unknown>;
|
|
1023
|
+
const command = String(args.command ?? "");
|
|
1491
1024
|
if (isPartial) {
|
|
1492
|
-
return toolRowText(theme, `${theme.fg("accent", TOOL_ROW_MARKER)} ${theme.fg("
|
|
1025
|
+
return toolRowText(theme, `${theme.fg("accent", TOOL_ROW_MARKER)} ${theme.fg("accent", "$")} ${theme.fg("accent", command)}`);
|
|
1493
1026
|
}
|
|
1494
1027
|
const output = getTextContent(result as any);
|
|
1495
1028
|
const outputLines = cleanOutputLines(output);
|
|
1496
|
-
const
|
|
1497
|
-
|
|
1498
|
-
const
|
|
1499
|
-
|
|
1500
|
-
|
|
1029
|
+
const statusColor = context.isError ? "error" : "success";
|
|
1030
|
+
let text = `${theme.fg(statusColor, TOOL_ROW_MARKER)} ${theme.fg(statusColor, "$")} ${theme.fg(statusColor, command)}`;
|
|
1031
|
+
const visibleLines = expanded ? outputLines : outputLines.slice(-4);
|
|
1032
|
+
if (visibleLines.length > 0) {
|
|
1033
|
+
text += "\n" + visibleLines.map((line) => ` ${theme.fg("toolOutput", line)}`).join("\n");
|
|
1034
|
+
}
|
|
1035
|
+
const lineSummary = !expanded && visibleLines.length < outputLines.length
|
|
1036
|
+
? `showing ${visibleLines.length}/${outputLines.length} lines (ctrl+o expand)`
|
|
1037
|
+
: formatLineLabel(outputLines.length, "line");
|
|
1038
|
+
text = appendToolFooter(theme, text, [
|
|
1039
|
+
lineSummary,
|
|
1040
|
+
renderDuration(context),
|
|
1501
1041
|
formatPayloadSize(output),
|
|
1502
|
-
formatLineLabel(outputLines.length, "line"),
|
|
1503
1042
|
details.truncation?.truncated ? "truncated" : undefined,
|
|
1504
1043
|
]);
|
|
1505
|
-
let text = `${bullet} ${theme.fg("toolTitle", "bash:")}${theme.fg("accent", command)}`;
|
|
1506
|
-
if (summary) text += theme.fg("dim", ` · ${summary}`);
|
|
1507
|
-
if (expanded && outputLines.length > 0) text += `\n${renderVerticalPreview(theme, outputLines, 10)}`;
|
|
1508
1044
|
return toolRowText(theme, text);
|
|
1509
1045
|
},
|
|
1510
1046
|
});
|
|
@@ -1512,30 +1048,45 @@ function registerXtrmUiTools(pi: ExtensionAPI, getPrefs: () => XtrmUiPrefs): voi
|
|
|
1512
1048
|
pi.registerTool({
|
|
1513
1049
|
name: "read",
|
|
1514
1050
|
label: "read",
|
|
1515
|
-
description:
|
|
1516
|
-
parameters:
|
|
1051
|
+
description: tools.read.description,
|
|
1052
|
+
parameters: tools.read.parameters,
|
|
1053
|
+
execute: tools.read.execute,
|
|
1517
1054
|
renderShell: "self",
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
return { ...result, details: withXtrmMeta(result.details as ReadToolDetails | undefined, "read", params as Record<string, unknown>, Date.now() - started) };
|
|
1522
|
-
},
|
|
1523
|
-
renderCall: (args, theme) => renderPendingCallIfActive("read", args as Record<string, unknown>, theme),
|
|
1524
|
-
renderResult(result, { expanded, isPartial }, theme) {
|
|
1055
|
+
renderCall: (args, theme, context) =>
|
|
1056
|
+
renderCall("read", args as Record<string, unknown>, theme, context),
|
|
1057
|
+
renderResult(result, { expanded, isPartial }, theme, context) {
|
|
1525
1058
|
if (isPartial) return toolRowText(theme, renderToolSummary(theme, "pending", "read", "loading", undefined));
|
|
1526
|
-
const details = (result.details ?? {}) as
|
|
1527
|
-
const
|
|
1528
|
-
const subjectBase = shortenPath(String(
|
|
1529
|
-
const range = lineRange(
|
|
1059
|
+
const details = (result.details ?? {}) as ReadToolDetails;
|
|
1060
|
+
const args = context.args as Record<string, unknown>;
|
|
1061
|
+
const subjectBase = shortenPath(String(args.path ?? ""));
|
|
1062
|
+
const range = lineRange(args.offset as number | undefined, args.limit as number | undefined);
|
|
1530
1063
|
const subject = range ? `${subjectBase}:${range}` : subjectBase;
|
|
1531
1064
|
const first = result.content[0];
|
|
1532
1065
|
if (first?.type === "image") {
|
|
1533
|
-
|
|
1066
|
+
const text = appendToolFooter(
|
|
1067
|
+
theme,
|
|
1068
|
+
renderToolSummary(theme, "success", "read", subject, undefined),
|
|
1069
|
+
["image", renderDuration(context)],
|
|
1070
|
+
);
|
|
1071
|
+
return toolRowText(theme, text);
|
|
1534
1072
|
}
|
|
1535
1073
|
const textContent = getTextContent(result as any);
|
|
1536
1074
|
const lines = textContent.split("\n");
|
|
1537
|
-
|
|
1538
|
-
|
|
1075
|
+
const totalLines = lines.length;
|
|
1076
|
+
const showContent = expanded || totalLines <= 6;
|
|
1077
|
+
let text = renderToolSummary(theme, context.isError ? "error" : "success", "read", subject, undefined);
|
|
1078
|
+
if (showContent && totalLines > 0) {
|
|
1079
|
+
text += "\n" + lines.map((line) => ` ${theme.fg("toolOutput", line)}`).join("\n");
|
|
1080
|
+
}
|
|
1081
|
+
const lineSummary = !showContent && totalLines > 0
|
|
1082
|
+
? `${formatLineLabel(totalLines, "line")} (ctrl+o expand)`
|
|
1083
|
+
: formatLineLabel(totalLines, "line");
|
|
1084
|
+
text = appendToolFooter(theme, text, [
|
|
1085
|
+
lineSummary,
|
|
1086
|
+
renderDuration(context),
|
|
1087
|
+
formatPayloadSize(textContent),
|
|
1088
|
+
details.truncation?.truncated ? `from ${details.truncation.totalLines}` : undefined,
|
|
1089
|
+
]);
|
|
1539
1090
|
return toolRowText(theme, text);
|
|
1540
1091
|
},
|
|
1541
1092
|
});
|
|
@@ -1543,26 +1094,30 @@ function registerXtrmUiTools(pi: ExtensionAPI, getPrefs: () => XtrmUiPrefs): voi
|
|
|
1543
1094
|
pi.registerTool({
|
|
1544
1095
|
name: "edit",
|
|
1545
1096
|
label: "edit",
|
|
1546
|
-
description:
|
|
1547
|
-
parameters:
|
|
1097
|
+
description: tools.edit.description,
|
|
1098
|
+
parameters: tools.edit.parameters,
|
|
1099
|
+
execute: tools.edit.execute,
|
|
1548
1100
|
renderShell: "self",
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
return { ...result, details: withXtrmMeta(result.details as EditToolDetails | undefined, "edit", params as Record<string, unknown>, Date.now() - started) };
|
|
1553
|
-
},
|
|
1554
|
-
renderCall: (args, theme) => renderPendingCallIfActive("edit", args as Record<string, unknown>, theme),
|
|
1555
|
-
renderResult(result, { expanded, isPartial }, theme) {
|
|
1101
|
+
renderCall: (args, theme, context) =>
|
|
1102
|
+
renderCall("edit", args as Record<string, unknown>, theme, context),
|
|
1103
|
+
renderResult(result, { isPartial }, theme, context) {
|
|
1556
1104
|
if (isPartial) return toolRowText(theme, renderToolSummary(theme, "pending", "edit", "applying", undefined));
|
|
1557
|
-
const details = (result.details ?? {}) as
|
|
1558
|
-
const
|
|
1105
|
+
const details = (result.details ?? {}) as EditToolDetails;
|
|
1106
|
+
const args = context.args as Record<string, unknown>;
|
|
1107
|
+
const path = String(args.path ?? "");
|
|
1559
1108
|
const textContent = getTextContent(result as any);
|
|
1560
|
-
if (
|
|
1561
|
-
|
|
1109
|
+
if (context.isError) {
|
|
1110
|
+
const text = appendToolFooter(
|
|
1111
|
+
theme,
|
|
1112
|
+
renderToolSummary(theme, "error", "edit", path, textContent.split("\n")[0]),
|
|
1113
|
+
[renderDuration(context)],
|
|
1114
|
+
);
|
|
1115
|
+
return toolRowText(theme, text);
|
|
1562
1116
|
}
|
|
1563
1117
|
const stats = details.diff ? diffStats(details.diff) : { additions: 0, removals: 0 };
|
|
1564
|
-
let text = renderToolSummary(theme, "success", "edit",
|
|
1565
|
-
if (
|
|
1118
|
+
let text = renderToolSummary(theme, "success", "edit", path, undefined);
|
|
1119
|
+
if (details.diff) text += `\n${renderRichDiffPreview(theme, details.diff, 18)}`;
|
|
1120
|
+
text = appendToolFooter(theme, text, [`+${stats.additions}`, `-${stats.removals}`, renderDuration(context)]);
|
|
1566
1121
|
return toolRowText(theme, text);
|
|
1567
1122
|
},
|
|
1568
1123
|
});
|
|
@@ -1570,85 +1125,88 @@ function registerXtrmUiTools(pi: ExtensionAPI, getPrefs: () => XtrmUiPrefs): voi
|
|
|
1570
1125
|
pi.registerTool({
|
|
1571
1126
|
name: "write",
|
|
1572
1127
|
label: "write",
|
|
1573
|
-
description:
|
|
1574
|
-
parameters:
|
|
1128
|
+
description: tools.write.description,
|
|
1129
|
+
parameters: tools.write.parameters,
|
|
1130
|
+
execute: tools.write.execute,
|
|
1575
1131
|
renderShell: "self",
|
|
1576
|
-
|
|
1577
|
-
const
|
|
1578
|
-
const
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
const details = withXtrmMeta(result.details as Record<string, never> | undefined, "write", args, Date.now() - started);
|
|
1584
|
-
return { ...result, details: { ...details, xtrmWritePreview: preview } };
|
|
1132
|
+
renderCall(args, theme, context) {
|
|
1133
|
+
const input = args as Record<string, unknown>;
|
|
1134
|
+
const state = context.state as XtrmToolRenderState;
|
|
1135
|
+
if (context.argsComplete && !state.writePreview) {
|
|
1136
|
+
state.writePreview = createWritePreview(String(input.path ?? ""), String(input.content ?? ""));
|
|
1137
|
+
}
|
|
1138
|
+
return renderCall("write", input, theme, context);
|
|
1585
1139
|
},
|
|
1586
|
-
|
|
1587
|
-
renderResult(result, { expanded, isPartial }, theme) {
|
|
1140
|
+
renderResult(result, { expanded, isPartial }, theme, context) {
|
|
1588
1141
|
if (isPartial) return toolRowText(theme, renderToolSummary(theme, "pending", "write", "writing", undefined));
|
|
1589
|
-
const
|
|
1590
|
-
const
|
|
1142
|
+
const args = context.args as Record<string, unknown>;
|
|
1143
|
+
const path = String(args.path ?? "");
|
|
1144
|
+
const content = String(args.content ?? "");
|
|
1591
1145
|
const textContent = getTextContent(result as any);
|
|
1592
|
-
if (
|
|
1593
|
-
|
|
1146
|
+
if (context.isError) {
|
|
1147
|
+
const text = appendToolFooter(
|
|
1148
|
+
theme,
|
|
1149
|
+
renderToolSummary(theme, "error", "write", path, textContent.split("\n")[0]),
|
|
1150
|
+
[renderDuration(context)],
|
|
1151
|
+
);
|
|
1152
|
+
return toolRowText(theme, text);
|
|
1594
1153
|
}
|
|
1595
1154
|
|
|
1596
|
-
const
|
|
1597
|
-
const preview = details.xtrmWritePreview;
|
|
1598
|
-
|
|
1155
|
+
const preview = (context.state as XtrmToolRenderState).writePreview;
|
|
1599
1156
|
if (preview?.kind === "unchanged") {
|
|
1600
|
-
|
|
1601
|
-
}
|
|
1602
|
-
|
|
1603
|
-
if (preview?.kind === "updated") {
|
|
1604
|
-
let text = renderToolSummary(
|
|
1157
|
+
const text = appendToolFooter(
|
|
1605
1158
|
theme,
|
|
1606
|
-
"success",
|
|
1607
|
-
"
|
|
1608
|
-
subject,
|
|
1609
|
-
joinMeta([`+${preview.additions}`, `-${preview.removals}`, formatDuration(meta?.durationMs)]),
|
|
1159
|
+
renderToolSummary(theme, "success", "write", path, undefined),
|
|
1160
|
+
["no changes", renderDuration(context)],
|
|
1610
1161
|
);
|
|
1611
|
-
|
|
1162
|
+
return toolRowText(theme, text);
|
|
1163
|
+
}
|
|
1164
|
+
if (preview?.kind === "updated") {
|
|
1165
|
+
let text = renderToolSummary(theme, "success", "write", path, undefined);
|
|
1166
|
+
if (preview.diff) text += `\n${renderRichDiffPreview(theme, preview.diff, 18)}`;
|
|
1167
|
+
text = appendToolFooter(theme, text, [`+${preview.additions}`, `-${preview.removals}`, renderDuration(context)]);
|
|
1612
1168
|
return toolRowText(theme, text);
|
|
1613
1169
|
}
|
|
1614
1170
|
|
|
1615
|
-
const lines = preview?.kind === "created"
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
),
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
);
|
|
1171
|
+
const lines = preview?.kind === "created" ? preview.lineCount : lineCount(content);
|
|
1172
|
+
let text = renderToolSummary(theme, "success", "write", path, undefined);
|
|
1173
|
+
const contentLines = content.split("\n");
|
|
1174
|
+
const showContent = content && (expanded || contentLines.length <= 6);
|
|
1175
|
+
if (showContent) {
|
|
1176
|
+
text += "\n" + contentLines.map((line) => ` ${theme.fg("toolOutput", line)}`).join("\n");
|
|
1177
|
+
}
|
|
1178
|
+
text = appendToolFooter(theme, text, [
|
|
1179
|
+
!showContent && lines > 0 ? `${formatLineLabel(lines, "line")} (ctrl+o expand)` : formatLineLabel(lines, "line"),
|
|
1180
|
+
renderDuration(context),
|
|
1181
|
+
formatPayloadSize(content),
|
|
1182
|
+
]);
|
|
1183
|
+
return toolRowText(theme, text);
|
|
1629
1184
|
},
|
|
1630
1185
|
});
|
|
1631
1186
|
|
|
1632
1187
|
pi.registerTool({
|
|
1633
1188
|
name: "find",
|
|
1634
1189
|
label: "find",
|
|
1635
|
-
description:
|
|
1636
|
-
parameters:
|
|
1190
|
+
description: tools.find.description,
|
|
1191
|
+
parameters: tools.find.parameters,
|
|
1192
|
+
execute: tools.find.execute,
|
|
1637
1193
|
renderShell: "self",
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
return { ...result, details: withXtrmMeta(result.details as FindToolDetails | undefined, "find", params as Record<string, unknown>, Date.now() - started) };
|
|
1642
|
-
},
|
|
1643
|
-
renderCall: (args, theme) => renderPendingCallIfActive("find", args as Record<string, unknown>, theme),
|
|
1644
|
-
renderResult(result, { expanded, isPartial }, theme) {
|
|
1194
|
+
renderCall: (args, theme, context) =>
|
|
1195
|
+
renderCall("find", args as Record<string, unknown>, theme, context),
|
|
1196
|
+
renderResult(result, { expanded, isPartial }, theme, context) {
|
|
1645
1197
|
if (isPartial) return toolRowText(theme, renderToolSummary(theme, "pending", "find", "searching", undefined));
|
|
1646
|
-
const details = (result.details ?? {}) as
|
|
1647
|
-
const
|
|
1198
|
+
const details = (result.details ?? {}) as FindToolDetails;
|
|
1199
|
+
const args = context.args as Record<string, unknown>;
|
|
1648
1200
|
const textContent = getTextContent(result as any);
|
|
1649
1201
|
const count = summarizeCount(textContent);
|
|
1650
|
-
let text = renderToolSummary(theme, "success", "find", String(
|
|
1202
|
+
let text = renderToolSummary(theme, context.isError ? "error" : "success", "find", String(args.pattern ?? ""), undefined);
|
|
1651
1203
|
if (expanded && count > 0) text += `\n${renderOutputPreview(theme, previewLines(textContent, 10), 10)}`;
|
|
1204
|
+
text = appendToolFooter(theme, text, [
|
|
1205
|
+
!expanded && count > 0 ? `${formatLineLabel(count, "match")} (ctrl+o expand)` : formatLineLabel(count, "match"),
|
|
1206
|
+
renderDuration(context),
|
|
1207
|
+
formatPayloadSize(textContent),
|
|
1208
|
+
details.resultLimitReached ? "limit reached" : undefined,
|
|
1209
|
+
]);
|
|
1652
1210
|
return toolRowText(theme, text);
|
|
1653
1211
|
},
|
|
1654
1212
|
});
|
|
@@ -1656,23 +1214,26 @@ function registerXtrmUiTools(pi: ExtensionAPI, getPrefs: () => XtrmUiPrefs): voi
|
|
|
1656
1214
|
pi.registerTool({
|
|
1657
1215
|
name: "grep",
|
|
1658
1216
|
label: "grep",
|
|
1659
|
-
description:
|
|
1660
|
-
parameters:
|
|
1217
|
+
description: tools.grep.description,
|
|
1218
|
+
parameters: tools.grep.parameters,
|
|
1219
|
+
execute: tools.grep.execute,
|
|
1661
1220
|
renderShell: "self",
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
return { ...result, details: withXtrmMeta(result.details as GrepToolDetails | undefined, "grep", params as Record<string, unknown>, Date.now() - started) };
|
|
1666
|
-
},
|
|
1667
|
-
renderCall: (args, theme) => renderPendingCallIfActive("grep", args as Record<string, unknown>, theme),
|
|
1668
|
-
renderResult(result, { expanded, isPartial }, theme) {
|
|
1221
|
+
renderCall: (args, theme, context) =>
|
|
1222
|
+
renderCall("grep", args as Record<string, unknown>, theme, context),
|
|
1223
|
+
renderResult(result, { expanded, isPartial }, theme, context) {
|
|
1669
1224
|
if (isPartial) return toolRowText(theme, renderToolSummary(theme, "pending", "grep", "searching", undefined));
|
|
1670
|
-
const details = (result.details ?? {}) as
|
|
1671
|
-
const
|
|
1225
|
+
const details = (result.details ?? {}) as GrepToolDetails;
|
|
1226
|
+
const args = context.args as Record<string, unknown>;
|
|
1672
1227
|
const textContent = getTextContent(result as any);
|
|
1673
1228
|
const count = countPrefixedItems(textContent, ["-- "]) || summarizeCount(textContent);
|
|
1674
|
-
let text = renderToolSummary(theme, "success", "grep", String(
|
|
1229
|
+
let text = renderToolSummary(theme, context.isError ? "error" : "success", "grep", String(args.pattern ?? ""), undefined);
|
|
1675
1230
|
if (expanded && textContent.length > 0) text += `\n${renderOutputPreview(theme, previewLines(textContent, 12), 12)}`;
|
|
1231
|
+
text = appendToolFooter(theme, text, [
|
|
1232
|
+
!expanded && count > 0 ? `${formatLineLabel(count, "match")} (ctrl+o expand)` : formatLineLabel(count, "match"),
|
|
1233
|
+
renderDuration(context),
|
|
1234
|
+
formatPayloadSize(textContent),
|
|
1235
|
+
details.matchLimitReached ? "limit reached" : undefined,
|
|
1236
|
+
]);
|
|
1676
1237
|
return toolRowText(theme, text);
|
|
1677
1238
|
},
|
|
1678
1239
|
});
|
|
@@ -1680,23 +1241,26 @@ function registerXtrmUiTools(pi: ExtensionAPI, getPrefs: () => XtrmUiPrefs): voi
|
|
|
1680
1241
|
pi.registerTool({
|
|
1681
1242
|
name: "ls",
|
|
1682
1243
|
label: "ls",
|
|
1683
|
-
description:
|
|
1684
|
-
parameters:
|
|
1244
|
+
description: tools.ls.description,
|
|
1245
|
+
parameters: tools.ls.parameters,
|
|
1246
|
+
execute: tools.ls.execute,
|
|
1685
1247
|
renderShell: "self",
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
return { ...result, details: withXtrmMeta(result.details as LsToolDetails | undefined, "ls", params as Record<string, unknown>, Date.now() - started) };
|
|
1690
|
-
},
|
|
1691
|
-
renderCall: (args, theme) => renderPendingCallIfActive("ls", args as Record<string, unknown>, theme),
|
|
1692
|
-
renderResult(result, { expanded, isPartial }, theme) {
|
|
1248
|
+
renderCall: (args, theme, context) =>
|
|
1249
|
+
renderCall("ls", args as Record<string, unknown>, theme, context),
|
|
1250
|
+
renderResult(result, { expanded, isPartial }, theme, context) {
|
|
1693
1251
|
if (isPartial) return toolRowText(theme, renderToolSummary(theme, "pending", "ls", "listing", undefined));
|
|
1694
|
-
const details = (result.details ?? {}) as
|
|
1695
|
-
const
|
|
1252
|
+
const details = (result.details ?? {}) as LsToolDetails;
|
|
1253
|
+
const args = context.args as Record<string, unknown>;
|
|
1696
1254
|
const textContent = getTextContent(result as any);
|
|
1697
1255
|
const count = summarizeCount(textContent);
|
|
1698
|
-
let text = renderToolSummary(theme, "success", "ls", shortenPath(String(
|
|
1256
|
+
let text = renderToolSummary(theme, context.isError ? "error" : "success", "ls", shortenPath(String(args.path ?? ".")), undefined);
|
|
1699
1257
|
if (expanded && count > 0) text += `\n${renderOutputPreview(theme, previewLines(textContent, 12), 12)}`;
|
|
1258
|
+
text = appendToolFooter(theme, text, [
|
|
1259
|
+
!expanded && count > 0 ? `${formatLineLabel(count, "entry")} (ctrl+o expand)` : formatLineLabel(count, "entry"),
|
|
1260
|
+
renderDuration(context),
|
|
1261
|
+
formatPayloadSize(textContent),
|
|
1262
|
+
details.entryLimitReached ? "limit reached" : undefined,
|
|
1263
|
+
]);
|
|
1700
1264
|
return toolRowText(theme, text);
|
|
1701
1265
|
},
|
|
1702
1266
|
});
|
|
@@ -1706,22 +1270,16 @@ function registerXtrmUiTools(pi: ExtensionAPI, getPrefs: () => XtrmUiPrefs): voi
|
|
|
1706
1270
|
// Main Extension
|
|
1707
1271
|
// ============================================================================
|
|
1708
1272
|
|
|
1709
|
-
function isXtrmTheme(name: string | undefined): boolean {
|
|
1710
|
-
return name === "pidex-dark" || name === "pidex-light";
|
|
1711
|
-
}
|
|
1712
|
-
|
|
1713
1273
|
export default function xtrmUiExtension(pi: ExtensionAPI): void {
|
|
1714
1274
|
void installSilentHiddenThinkingPatch().catch(() => undefined);
|
|
1715
1275
|
void installExternalToolFramePatch().catch(() => undefined);
|
|
1716
1276
|
|
|
1717
1277
|
let prefs: XtrmUiPrefs = { ...DEFAULT_PREFS };
|
|
1718
|
-
let previousThemeName: string | null = null;
|
|
1719
1278
|
const extensionThemeDir = join(__dirname, "../../themes/xtrm-ui");
|
|
1720
1279
|
|
|
1721
1280
|
const getPrefs = () => prefs;
|
|
1722
|
-
const setPrefs = (
|
|
1723
|
-
prefs =
|
|
1724
|
-
setActiveExternalToolChrome(p.externalToolChrome);
|
|
1281
|
+
const setPrefs = (nextPrefs: XtrmUiPrefs) => {
|
|
1282
|
+
prefs = nextPrefs;
|
|
1725
1283
|
};
|
|
1726
1284
|
const getThinkingLevel = () => formatThinking(pi.getThinkingLevel());
|
|
1727
1285
|
|
|
@@ -1730,7 +1288,7 @@ export default function xtrmUiExtension(pi: ExtensionAPI): void {
|
|
|
1730
1288
|
|
|
1731
1289
|
const refresh = (ctx: ExtensionContext) => {
|
|
1732
1290
|
applyXtrmChrome(ctx, prefs, getThinkingLevel);
|
|
1733
|
-
applyThinkingChrome(ctx
|
|
1291
|
+
applyThinkingChrome(ctx);
|
|
1734
1292
|
};
|
|
1735
1293
|
|
|
1736
1294
|
pi.on("resources_discover", async () => ({
|
|
@@ -1739,27 +1297,14 @@ export default function xtrmUiExtension(pi: ExtensionAPI): void {
|
|
|
1739
1297
|
|
|
1740
1298
|
pi.on("session_start", async (_event, ctx) => {
|
|
1741
1299
|
setPrefs(loadPrefs(ctx.sessionManager.getEntries() as Array<MaybeCustomEntry>));
|
|
1742
|
-
if (!previousThemeName && !isXtrmTheme(ctx.ui.theme.name)) {
|
|
1743
|
-
previousThemeName = ctx.ui.theme.name ?? null;
|
|
1744
|
-
}
|
|
1745
1300
|
refresh(ctx);
|
|
1746
|
-
|
|
1747
|
-
setTimeout(() => {
|
|
1748
|
-
ctx.ui.setTheme(resolveThemeForPrefs(prefs));
|
|
1749
|
-
}, 0);
|
|
1750
1301
|
});
|
|
1751
1302
|
|
|
1752
1303
|
pi.on("session_switch", async (_event, ctx) => {
|
|
1753
|
-
if (!previousThemeName && !isXtrmTheme(ctx.ui.theme.name)) {
|
|
1754
|
-
previousThemeName = ctx.ui.theme.name ?? null;
|
|
1755
|
-
}
|
|
1756
1304
|
refresh(ctx);
|
|
1757
1305
|
});
|
|
1758
1306
|
|
|
1759
1307
|
pi.on("session_fork", async (_event, ctx) => {
|
|
1760
|
-
if (!previousThemeName && !isXtrmTheme(ctx.ui.theme.name)) {
|
|
1761
|
-
previousThemeName = ctx.ui.theme.name ?? null;
|
|
1762
|
-
}
|
|
1763
1308
|
refresh(ctx);
|
|
1764
1309
|
});
|
|
1765
1310
|
|
|
@@ -1767,10 +1312,8 @@ export default function xtrmUiExtension(pi: ExtensionAPI): void {
|
|
|
1767
1312
|
refresh(ctx);
|
|
1768
1313
|
});
|
|
1769
1314
|
|
|
1770
|
-
pi.on("session_shutdown", async (
|
|
1771
|
-
|
|
1772
|
-
ctx.ui.setTheme(previousThemeName);
|
|
1773
|
-
}
|
|
1315
|
+
pi.on("session_shutdown", async () => {
|
|
1316
|
+
// No-op: no theme restoration on shutdown
|
|
1774
1317
|
});
|
|
1775
1318
|
|
|
1776
1319
|
pi.on("input", async (event) => {
|