xtrm-tools 0.7.19 → 0.7.20
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/.xtrm/registry.json +330 -330
- package/.xtrm/skills/default/using-specialists-v3/SKILL.md +2 -1
- package/.xtrm/skills/default/using-xtrm/SKILL.md +4 -0
- package/CHANGELOG.md +9 -0
- package/README.md +40 -28
- package/cli/dist/index.cjs +2 -0
- package/cli/dist/index.cjs.map +1 -1
- package/cli/package.json +1 -1
- package/package.json +1 -1
- package/packages/pi-extensions/README.md +7 -0
- package/packages/pi-extensions/extensions/README.md +12 -0
- package/packages/pi-extensions/extensions/sp-terminal-overlay/index.ts +495 -0
- package/packages/pi-extensions/extensions/sp-terminal-overlay/package.json +15 -0
- package/packages/pi-extensions/extensions/xtrm-ui/format.ts +7 -2
- package/packages/pi-extensions/extensions/xtrm-ui/index.ts +436 -10
- package/packages/pi-extensions/package.json +1 -1
- package/packages/pi-extensions/src/extensions/sp-terminal-overlay.ts +3 -0
- package/packages/pi-extensions/src/registry.ts +2 -0
|
@@ -32,7 +32,7 @@ import {
|
|
|
32
32
|
createWriteTool,
|
|
33
33
|
} from "@mariozechner/pi-coding-agent";
|
|
34
34
|
import { Box, Text, truncateToWidth, visibleWidth } from "@mariozechner/pi-tui";
|
|
35
|
-
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
35
|
+
import { existsSync, readFileSync, realpathSync, writeFileSync } from "node:fs";
|
|
36
36
|
import { basename, dirname, join } from "node:path";
|
|
37
37
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
38
38
|
import {
|
|
@@ -57,6 +57,7 @@ import {
|
|
|
57
57
|
|
|
58
58
|
export type XtrmThemeName = "pidex-dark" | "pidex-light" | "pidex-dark-flattools" | "pidex-light-flattools";
|
|
59
59
|
export type XtrmDensity = "compact" | "comfortable";
|
|
60
|
+
export type XtrmExternalToolChrome = "background" | "box";
|
|
60
61
|
|
|
61
62
|
export interface XtrmUiPrefs {
|
|
62
63
|
themeName: XtrmThemeName;
|
|
@@ -67,6 +68,7 @@ export interface XtrmUiPrefs {
|
|
|
67
68
|
forceTheme: boolean; // When false, skip setTheme (allow external theme override)
|
|
68
69
|
toolRowBg: boolean; // Subtle background behind tool text rows (no padding)
|
|
69
70
|
compactExternalToolResults: boolean; // Compact extension tool results (disables full expand output)
|
|
71
|
+
externalToolChrome: XtrmExternalToolChrome; // Visual treatment for non-native tool rows
|
|
70
72
|
hideThinkingPlaceholder: boolean; // When false, hidden thinking blocks render no placeholder text
|
|
71
73
|
}
|
|
72
74
|
|
|
@@ -85,9 +87,17 @@ export const DEFAULT_PREFS: XtrmUiPrefs = {
|
|
|
85
87
|
forceTheme: true,
|
|
86
88
|
toolRowBg: false,
|
|
87
89
|
compactExternalToolResults: true,
|
|
90
|
+
externalToolChrome: "background",
|
|
88
91
|
hideThinkingPlaceholder: false,
|
|
89
92
|
};
|
|
90
93
|
|
|
94
|
+
let activeExternalToolChrome: XtrmExternalToolChrome = DEFAULT_PREFS.externalToolChrome;
|
|
95
|
+
|
|
96
|
+
function setActiveExternalToolChrome(chrome: XtrmExternalToolChrome): void {
|
|
97
|
+
activeExternalToolChrome = chrome;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
|
|
91
101
|
// ============================================================================
|
|
92
102
|
// Preferences
|
|
93
103
|
// ============================================================================
|
|
@@ -111,6 +121,7 @@ function normalizePrefs(input: unknown): XtrmUiPrefs {
|
|
|
111
121
|
toolRowBg: source.toolRowBg ?? DEFAULT_PREFS.toolRowBg,
|
|
112
122
|
compactExternalToolResults:
|
|
113
123
|
source.compactExternalToolResults ?? DEFAULT_PREFS.compactExternalToolResults,
|
|
124
|
+
externalToolChrome: source.externalToolChrome === "box" ? "box" : "background",
|
|
114
125
|
hideThinkingPlaceholder: source.hideThinkingPlaceholder ?? DEFAULT_PREFS.hideThinkingPlaceholder,
|
|
115
126
|
};
|
|
116
127
|
}
|
|
@@ -153,8 +164,39 @@ type PatchableAssistantMessage = {
|
|
|
153
164
|
|
|
154
165
|
const PATCHED_ASSISTANT_MESSAGE = "__xtrmUiSilentHiddenThinking";
|
|
155
166
|
|
|
167
|
+
function maybeFileUrlToPath(value: string): string {
|
|
168
|
+
return value.startsWith("file:") ? fileURLToPath(value) : value;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function resolvePiCodingAgentEntryPath(): string {
|
|
172
|
+
const candidates: string[] = [];
|
|
173
|
+
|
|
174
|
+
const argvPath = process.argv[1];
|
|
175
|
+
if (argvPath && existsSync(argvPath)) {
|
|
176
|
+
const realArgvPath = realpathSync(argvPath);
|
|
177
|
+
if (realArgvPath.endsWith("/dist/cli.js")) {
|
|
178
|
+
candidates.push(join(dirname(realArgvPath), "index.js"));
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
candidates.push(
|
|
183
|
+
join(dirname(process.execPath), "..", "lib", "node_modules", "@earendil-works", "pi-coding-agent", "dist", "index.js"),
|
|
184
|
+
join(dirname(process.execPath), "..", "lib", "node_modules", "@mariozechner", "pi-coding-agent", "dist", "index.js"),
|
|
185
|
+
);
|
|
186
|
+
|
|
187
|
+
for (const packageName of ["@earendil-works/pi-coding-agent", "@mariozechner/pi-coding-agent"]) {
|
|
188
|
+
try {
|
|
189
|
+
candidates.push(maybeFileUrlToPath(import.meta.resolve(packageName)));
|
|
190
|
+
} catch {}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const entryPath = candidates.find((candidate) => existsSync(candidate));
|
|
194
|
+
if (!entryPath) throw new Error("Could not resolve pi-coding-agent entry path");
|
|
195
|
+
return entryPath;
|
|
196
|
+
}
|
|
197
|
+
|
|
156
198
|
async function installSilentHiddenThinkingPatch(): Promise<void> {
|
|
157
|
-
const entryPath =
|
|
199
|
+
const entryPath = resolvePiCodingAgentEntryPath();
|
|
158
200
|
const componentPath = join(dirname(entryPath), "modes", "interactive", "components", "assistant-message.js");
|
|
159
201
|
const mod = await import(pathToFileURL(componentPath).href) as {
|
|
160
202
|
AssistantMessageComponent?: AssistantMessageComponentCtor;
|
|
@@ -191,6 +233,240 @@ async function installSilentHiddenThinkingPatch(): Promise<void> {
|
|
|
191
233
|
proto[PATCHED_ASSISTANT_MESSAGE] = true;
|
|
192
234
|
}
|
|
193
235
|
|
|
236
|
+
type ToolExecutionComponentCtor = {
|
|
237
|
+
prototype: {
|
|
238
|
+
getRenderShell?: () => "default" | "self";
|
|
239
|
+
hasRendererDefinition?: () => boolean;
|
|
240
|
+
render?: (width: number) => string[];
|
|
241
|
+
};
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
type PatchableToolExecutionComponent = {
|
|
245
|
+
toolName?: string;
|
|
246
|
+
args?: unknown;
|
|
247
|
+
result?: { content?: Array<{ type: string; text?: string }>; details?: unknown; isError?: boolean };
|
|
248
|
+
expanded?: boolean;
|
|
249
|
+
hasRendererDefinition?: () => boolean;
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
type ExternalToolFrameKind = "serena" | "gitnexus" | "structured" | "process" | "external";
|
|
253
|
+
|
|
254
|
+
const PATCHED_EXTERNAL_TOOL_FRAME = "__xtrmUiExternalToolFrame";
|
|
255
|
+
const EXTERNAL_TOOL_FRAME_PATCH_VERSION = 10;
|
|
256
|
+
const ANSI_PATTERN = /\x1b\[[0-9;?]*[ -/]*[@-~]/g;
|
|
257
|
+
|
|
258
|
+
function stripAnsi(text: string): string {
|
|
259
|
+
return text.replace(ANSI_PATTERN, "");
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function isBlankRenderedLine(line: string): boolean {
|
|
263
|
+
return stripAnsi(line).trim().length === 0;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function externalToolFrameKind(toolName: string | undefined): ExternalToolFrameKind | undefined {
|
|
267
|
+
if (!toolName || XTRM_BUILTIN_TOOLS.has(toolName)) return undefined;
|
|
268
|
+
if (toolName === "structured_return") return "structured";
|
|
269
|
+
if (toolName === "process") return "process";
|
|
270
|
+
if (toolName.startsWith("gitnexus_")) return "gitnexus";
|
|
271
|
+
if (SERENA_COMPACT_TOOLS.has(toolName)) return "serena";
|
|
272
|
+
return "external";
|
|
273
|
+
}
|
|
274
|
+
|
|
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
|
+
function getToolArgs(component: PatchableToolExecutionComponent): Record<string, unknown> {
|
|
286
|
+
return component.args && typeof component.args === "object" && !Array.isArray(component.args)
|
|
287
|
+
? component.args as Record<string, unknown>
|
|
288
|
+
: {};
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function summarizeExternalToolPending(toolName: string | undefined, input: Record<string, unknown>): string {
|
|
292
|
+
const name = toolName ?? "tool";
|
|
293
|
+
if (name === "structured_return") {
|
|
294
|
+
return `• structured_return ${shortenCommand(String(input.command ?? "running"), 38)}`;
|
|
295
|
+
}
|
|
296
|
+
if (name === "process") {
|
|
297
|
+
return `• process ${String(input.action ?? "running")}`;
|
|
298
|
+
}
|
|
299
|
+
if (name.startsWith("gitnexus_")) {
|
|
300
|
+
const subject = summarizeSerenaSubject(name, input) ?? summarizeToolSubject(name, input);
|
|
301
|
+
return `• ${normalizeToolLabel(name)}${subject ? ` ${subject}` : ""}`;
|
|
302
|
+
}
|
|
303
|
+
if (SERENA_COMPACT_TOOLS.has(name)) {
|
|
304
|
+
const subject = summarizeSerenaSubject(name, input);
|
|
305
|
+
return `• serena ${name}${subject ? ` ${subject}` : ""}`;
|
|
306
|
+
}
|
|
307
|
+
const subject = summarizeToolSubject(name, input) ?? summarizeSerenaSubject(name, input);
|
|
308
|
+
return `• ${normalizeToolLabel(name)}${subject ? ` ${subject}` : ""}`;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
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
|
+
const text = component.result?.content?.find((content) => content.type === "text")?.text;
|
|
316
|
+
if (text) return text.split("\n");
|
|
317
|
+
|
|
318
|
+
return [summarizeExternalToolPending(component.toolName, getToolArgs(component))];
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function trimRenderedToolLines(lines: string[]): string[] {
|
|
322
|
+
let start = 0;
|
|
323
|
+
let end = lines.length;
|
|
324
|
+
while (start < end && isBlankRenderedLine(lines[start] ?? "")) start++;
|
|
325
|
+
while (end > start && isBlankRenderedLine(lines[end - 1] ?? "")) end--;
|
|
326
|
+
return lines.slice(start, end).map((line) => line.replace(/\s+$/u, ""));
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function externalToolBgRgb(kind: ExternalToolFrameKind): [number, number, number] {
|
|
330
|
+
const bgColors: Record<ExternalToolFrameKind, [number, number, number]> = {
|
|
331
|
+
serena: [13, 34, 49],
|
|
332
|
+
gitnexus: [31, 23, 55],
|
|
333
|
+
structured: [35, 23, 55],
|
|
334
|
+
process: [10, 42, 52],
|
|
335
|
+
external: [27, 33, 43],
|
|
336
|
+
};
|
|
337
|
+
return bgColors[kind];
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function externalToolBgColor(kind: ExternalToolFrameKind, text: string): string {
|
|
341
|
+
const [r, g, b] = externalToolBgRgb(kind);
|
|
342
|
+
return `\x1b[48;2;${r};${g};${b}m${text}\x1b[49m`;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function externalToolBadgeColor(kind: ExternalToolFrameKind, text: string): string {
|
|
346
|
+
const bgColors: Record<ExternalToolFrameKind, [number, number, number]> = {
|
|
347
|
+
serena: [26, 96, 132],
|
|
348
|
+
gitnexus: [82, 58, 150],
|
|
349
|
+
structured: [105, 61, 150],
|
|
350
|
+
process: [17, 118, 145],
|
|
351
|
+
external: [74, 88, 112],
|
|
352
|
+
};
|
|
353
|
+
const [badgeR, badgeG, badgeB] = bgColors[kind];
|
|
354
|
+
const [rowR, rowG, rowB] = externalToolBgRgb(kind);
|
|
355
|
+
return `\x1b[1m\x1b[48;2;${badgeR};${badgeG};${badgeB}m${text}\x1b[22m\x1b[48;2;${rowR};${rowG};${rowB}m`;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
function highlightExternalToolBadge(kind: ExternalToolFrameKind, line: string): string {
|
|
359
|
+
const match = line.match(/^(•\s+(?:serena\s+\S+|gitnexus(?:_\S+)?|structured_return|process|\S+))/u);
|
|
360
|
+
if (!match?.[1]) return line;
|
|
361
|
+
return externalToolBadgeColor(kind, match[1]) + line.slice(match[1].length);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
function externalToolBorderColor(kind: ExternalToolFrameKind, text: string): string {
|
|
365
|
+
const colors: Record<ExternalToolFrameKind, [number, number, number]> = {
|
|
366
|
+
serena: [150, 210, 255],
|
|
367
|
+
gitnexus: [185, 168, 255],
|
|
368
|
+
structured: [205, 166, 255],
|
|
369
|
+
process: [145, 231, 255],
|
|
370
|
+
external: [168, 181, 199],
|
|
371
|
+
};
|
|
372
|
+
const [r, g, b] = colors[kind];
|
|
373
|
+
return `[2m[38;2;${r};${g};${b}m${text}[39m[22m`;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function collapsedExternalToolLines(contentLines: string[], expanded: boolean): string[] {
|
|
377
|
+
return expanded ? contentLines : [contentLines.join(" · ")];
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
function renderExternalToolBackgroundLines(
|
|
381
|
+
contentLines: string[],
|
|
382
|
+
width: number,
|
|
383
|
+
kind: ExternalToolFrameKind,
|
|
384
|
+
expanded: boolean,
|
|
385
|
+
): string[] {
|
|
386
|
+
const availableWidth = Math.max(8, width);
|
|
387
|
+
const renderWidth = availableWidth;
|
|
388
|
+
const visibleLines = collapsedExternalToolLines(contentLines, expanded);
|
|
389
|
+
|
|
390
|
+
return visibleLines.map((rawLine) => {
|
|
391
|
+
const line = truncateToWidth(rawLine, Math.max(1, renderWidth - 2));
|
|
392
|
+
const highlighted = highlightExternalToolBadge(kind, line);
|
|
393
|
+
return externalToolBgColor(kind, ` ${padVisible(highlighted, Math.max(1, renderWidth - 2))} `);
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
function renderExternalToolBoxLines(
|
|
398
|
+
contentLines: string[],
|
|
399
|
+
width: number,
|
|
400
|
+
kind: ExternalToolFrameKind,
|
|
401
|
+
expanded: boolean,
|
|
402
|
+
): string[] {
|
|
403
|
+
const availableWidth = Math.max(8, width - 4);
|
|
404
|
+
const maxContentWidth = expanded ? availableWidth : Math.min(availableWidth, 34);
|
|
405
|
+
const visibleLines = collapsedExternalToolLines(contentLines, expanded);
|
|
406
|
+
const contentWidth = Math.max(
|
|
407
|
+
1,
|
|
408
|
+
Math.min(maxContentWidth, ...visibleLines.map((line) => visibleWidth(line))),
|
|
409
|
+
);
|
|
410
|
+
const innerWidth = contentWidth + 2;
|
|
411
|
+
|
|
412
|
+
const framed = [externalToolBorderColor(kind, `╭${"─".repeat(innerWidth)}╮`)];
|
|
413
|
+
for (const rawLine of visibleLines) {
|
|
414
|
+
const line = truncateToWidth(rawLine, contentWidth);
|
|
415
|
+
framed.push(`${externalToolBorderColor(kind, "│")} ${padVisible(line, contentWidth)} ${externalToolBorderColor(kind, "│")}`);
|
|
416
|
+
}
|
|
417
|
+
framed.push(externalToolBorderColor(kind, `╰${"─".repeat(innerWidth)}╯`));
|
|
418
|
+
return framed;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
function renderExternalToolLines(
|
|
422
|
+
lines: string[],
|
|
423
|
+
width: number,
|
|
424
|
+
kind: ExternalToolFrameKind,
|
|
425
|
+
expanded = false,
|
|
426
|
+
): string[] {
|
|
427
|
+
const contentLines = trimRenderedToolLines(lines).filter((line) => !isBlankRenderedLine(line));
|
|
428
|
+
if (contentLines.length === 0) return [];
|
|
429
|
+
|
|
430
|
+
return activeExternalToolChrome === "box"
|
|
431
|
+
? renderExternalToolBoxLines(contentLines, width, kind, expanded)
|
|
432
|
+
: renderExternalToolBackgroundLines(contentLines, width, kind, expanded);
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
async function installExternalToolFramePatch(): Promise<void> {
|
|
436
|
+
const entryPath = resolvePiCodingAgentEntryPath();
|
|
437
|
+
const componentPath = join(dirname(entryPath), "modes", "interactive", "components", "tool-execution.js");
|
|
438
|
+
const mod = await import(pathToFileURL(componentPath).href) as {
|
|
439
|
+
ToolExecutionComponent?: ToolExecutionComponentCtor;
|
|
440
|
+
};
|
|
441
|
+
const proto = mod.ToolExecutionComponent?.prototype as
|
|
442
|
+
| (ToolExecutionComponentCtor["prototype"] & { [PATCHED_EXTERNAL_TOOL_FRAME]?: boolean })
|
|
443
|
+
| undefined;
|
|
444
|
+
if (!proto?.render || proto[PATCHED_EXTERNAL_TOOL_FRAME] === EXTERNAL_TOOL_FRAME_PATCH_VERSION) return;
|
|
445
|
+
|
|
446
|
+
const getRenderShell = proto.getRenderShell;
|
|
447
|
+
const render = proto.render;
|
|
448
|
+
|
|
449
|
+
proto.getRenderShell = function patchedGetRenderShell(this: PatchableToolExecutionComponent) {
|
|
450
|
+
const kind = externalToolFrameKind(this.toolName);
|
|
451
|
+
if (kind) return "self";
|
|
452
|
+
return getRenderShell?.call(this) ?? "default";
|
|
453
|
+
};
|
|
454
|
+
|
|
455
|
+
proto.render = function patchedRender(this: PatchableToolExecutionComponent, width: number) {
|
|
456
|
+
const rendered = render.call(this, width);
|
|
457
|
+
const kind = externalToolFrameKind(this.toolName);
|
|
458
|
+
if (!kind || rendered.length === 0) return rendered;
|
|
459
|
+
|
|
460
|
+
const firstContentIndex = rendered.findIndex((line) => !isBlankRenderedLine(line));
|
|
461
|
+
const leading = firstContentIndex > 0 ? rendered.slice(0, firstContentIndex) : [];
|
|
462
|
+
const content = extractResultTextLines(this) ?? rendered;
|
|
463
|
+
const styled = renderExternalToolLines(content, width, kind, Boolean(this.expanded));
|
|
464
|
+
return styled.length > 0 ? [...leading, ...styled] : rendered;
|
|
465
|
+
};
|
|
466
|
+
|
|
467
|
+
proto[PATCHED_EXTERNAL_TOOL_FRAME] = EXTERNAL_TOOL_FRAME_PATCH_VERSION;
|
|
468
|
+
}
|
|
469
|
+
|
|
194
470
|
function applyThinkingChrome(ctx: ExtensionContext, prefs: XtrmUiPrefs): void {
|
|
195
471
|
(ctx.ui as { setHiddenThinkingLabel?: (label?: string) => void }).setHiddenThinkingLabel?.(
|
|
196
472
|
prefs.hideThinkingPlaceholder ? undefined : "",
|
|
@@ -396,6 +672,13 @@ function parseDensityArg(arg: string): XtrmDensity | undefined {
|
|
|
396
672
|
return undefined;
|
|
397
673
|
}
|
|
398
674
|
|
|
675
|
+
function parseExternalToolChromeArg(arg: string): XtrmExternalToolChrome | undefined {
|
|
676
|
+
const normalized = arg.trim().toLowerCase();
|
|
677
|
+
if (normalized === "background" || normalized === "bg" || normalized === "row") return "background";
|
|
678
|
+
if (normalized === "box" || normalized === "frame" || normalized === "border") return "box";
|
|
679
|
+
return undefined;
|
|
680
|
+
}
|
|
681
|
+
|
|
399
682
|
function registerCommands(pi: ExtensionAPI, getPrefs: () => XtrmUiPrefs, setPrefs: (p: XtrmUiPrefs) => void, getThinkingLevel: () => string) {
|
|
400
683
|
pi.registerMessageRenderer("xtrm-ui-info", (message, _options, theme) => {
|
|
401
684
|
const title = (message.details as { title?: string } | undefined)?.title ?? "XTRM UI";
|
|
@@ -407,7 +690,26 @@ function registerCommands(pi: ExtensionAPI, getPrefs: () => XtrmUiPrefs, setPref
|
|
|
407
690
|
|
|
408
691
|
pi.registerCommand("xtrm-ui", {
|
|
409
692
|
description: "Show XTRM UI status and active preferences",
|
|
410
|
-
handler: async (
|
|
693
|
+
handler: async (args, ctx) => {
|
|
694
|
+
const trimmedArgs = args.trim();
|
|
695
|
+
if (trimmedArgs) {
|
|
696
|
+
const [subcommand, ...rest] = trimmedArgs.split(/\s+/u);
|
|
697
|
+
if (subcommand === "chrome" || subcommand === "external-chrome" || subcommand === "tool-chrome") {
|
|
698
|
+
const externalToolChrome = parseExternalToolChromeArg(rest.join(" "));
|
|
699
|
+
if (!externalToolChrome) {
|
|
700
|
+
ctx.ui.notify("Usage: /xtrm-ui chrome background|box", "warning");
|
|
701
|
+
return;
|
|
702
|
+
}
|
|
703
|
+
const prefs = { ...getPrefs(), externalToolChrome };
|
|
704
|
+
setPrefs(prefs);
|
|
705
|
+
persistPrefs(pi, prefs);
|
|
706
|
+
ctx.ui.notify(`External tool chrome set to ${externalToolChrome}.`, "info");
|
|
707
|
+
return;
|
|
708
|
+
}
|
|
709
|
+
ctx.ui.notify("Usage: /xtrm-ui [chrome background|box]", "warning");
|
|
710
|
+
return;
|
|
711
|
+
}
|
|
712
|
+
|
|
411
713
|
const prefs = getPrefs();
|
|
412
714
|
const contextUsage = ctx.getContextUsage();
|
|
413
715
|
const lines = [
|
|
@@ -419,6 +721,7 @@ function registerCommands(pi: ExtensionAPI, getPrefs: () => XtrmUiPrefs, setPref
|
|
|
419
721
|
`Show footer: ${prefs.showFooter ? "yes" : "no"} (custom-footer handles this)`,
|
|
420
722
|
`Tool row background: ${prefs.toolRowBg ? "on" : "off"}`,
|
|
421
723
|
`Compact external tool results: ${prefs.compactExternalToolResults ? "on" : "off"}`,
|
|
724
|
+
`External tool chrome: ${prefs.externalToolChrome}`,
|
|
422
725
|
`Model: ${ctx.model?.id ?? "none"}`,
|
|
423
726
|
`Context: ${contextUsage?.tokens ?? "unknown"}/${contextUsage?.contextWindow ?? "unknown"}`,
|
|
424
727
|
];
|
|
@@ -549,6 +852,25 @@ function registerCommands(pi: ExtensionAPI, getPrefs: () => XtrmUiPrefs, setPref
|
|
|
549
852
|
},
|
|
550
853
|
});
|
|
551
854
|
|
|
855
|
+
pi.registerCommand("xtrm-ui-external-chrome", {
|
|
856
|
+
description: "Choose non-native tool chrome: background|box",
|
|
857
|
+
getArgumentCompletions: (prefix) => {
|
|
858
|
+
const values = ["background", "box"].filter((item) => item.startsWith(prefix));
|
|
859
|
+
return values.length > 0 ? values.map((value) => ({ value, label: value })) : null;
|
|
860
|
+
},
|
|
861
|
+
handler: async (args, ctx) => {
|
|
862
|
+
const externalToolChrome = parseExternalToolChromeArg(args);
|
|
863
|
+
if (!externalToolChrome) {
|
|
864
|
+
ctx.ui.notify("Usage: /xtrm-ui-external-chrome background|box", "warning");
|
|
865
|
+
return;
|
|
866
|
+
}
|
|
867
|
+
const prefs = { ...getPrefs(), externalToolChrome };
|
|
868
|
+
setPrefs(prefs);
|
|
869
|
+
persistPrefs(pi, prefs);
|
|
870
|
+
ctx.ui.notify(`External tool chrome set to ${externalToolChrome}.`, "info");
|
|
871
|
+
},
|
|
872
|
+
});
|
|
873
|
+
|
|
552
874
|
pi.registerCommand("xtrm-ui-reset", {
|
|
553
875
|
description: "Restore XTRM UI defaults",
|
|
554
876
|
handler: async (_args, ctx) => {
|
|
@@ -956,6 +1278,101 @@ function summarizeGenericToolResult(
|
|
|
956
1278
|
return `• ${normalized}${subject ? ` ${subject}` : ""}${joined ? ` · ${joined}` : ""}`;
|
|
957
1279
|
}
|
|
958
1280
|
|
|
1281
|
+
function summarizeStructuredReturnToolResult(
|
|
1282
|
+
input: Record<string, unknown>,
|
|
1283
|
+
text: string,
|
|
1284
|
+
details: unknown,
|
|
1285
|
+
durationMs: number | undefined,
|
|
1286
|
+
): string {
|
|
1287
|
+
const record = asRecord(details);
|
|
1288
|
+
const command = shortenCommand(String(input.command ?? text.split("→")[0] ?? "command"), 52);
|
|
1289
|
+
const resultText = text.includes("→") ? text.split("→").slice(1).join("→").trim() : text.trim();
|
|
1290
|
+
const resultLines = resultText.split("\n").map((line) => line.trim()).filter(Boolean);
|
|
1291
|
+
const summary = resultLines.find((line) => !line.startsWith("cwd:"));
|
|
1292
|
+
const parser = typeof record?.parser === "string" ? record.parser : undefined;
|
|
1293
|
+
const exitCode = typeof record?.exitCode === "number" ? `exit ${record.exitCode}` : undefined;
|
|
1294
|
+
const duration = formatDuration(durationMs);
|
|
1295
|
+
const meta = joinMeta([summary ? shortenCommand(summary, 72) : undefined, parser, exitCode, duration]);
|
|
1296
|
+
return `• structured_return ${command}${meta ? ` · ${meta}` : ""}`;
|
|
1297
|
+
}
|
|
1298
|
+
|
|
1299
|
+
function summarizeProcessToolResult(
|
|
1300
|
+
input: Record<string, unknown>,
|
|
1301
|
+
text: string,
|
|
1302
|
+
details: unknown,
|
|
1303
|
+
durationMs: number | undefined,
|
|
1304
|
+
): string {
|
|
1305
|
+
const record = asRecord(details);
|
|
1306
|
+
const action = String(record?.action ?? input.action ?? "action");
|
|
1307
|
+
const duration = formatDuration(durationMs);
|
|
1308
|
+
const meta = (...parts: Array<string | undefined>) => {
|
|
1309
|
+
const joined = joinMeta([...parts, duration]);
|
|
1310
|
+
return joined ? ` · ${joined}` : "";
|
|
1311
|
+
};
|
|
1312
|
+
|
|
1313
|
+
if (action === "start") {
|
|
1314
|
+
const proc = asRecord(record?.process);
|
|
1315
|
+
const name = String(proc?.name ?? input.name ?? "process");
|
|
1316
|
+
const id = proc?.id ? String(proc.id) : undefined;
|
|
1317
|
+
const pid = proc?.pid != null ? `pid ${String(proc.pid)}` : undefined;
|
|
1318
|
+
return `• process start "${name}"${meta(id, pid)}`;
|
|
1319
|
+
}
|
|
1320
|
+
|
|
1321
|
+
if (action === "list") {
|
|
1322
|
+
const processes = Array.isArray(record?.processes) ? record.processes : [];
|
|
1323
|
+
const running = processes.filter((item) => {
|
|
1324
|
+
const proc = asRecord(item);
|
|
1325
|
+
return proc?.status === "running" || proc?.status === "terminating";
|
|
1326
|
+
}).length;
|
|
1327
|
+
return `• process list${meta(`${processes.length} ${processes.length === 1 ? "process" : "processes"}`, `${running} running`)}`;
|
|
1328
|
+
}
|
|
1329
|
+
|
|
1330
|
+
if (action === "output") {
|
|
1331
|
+
const output = asRecord(record?.output);
|
|
1332
|
+
const stdout = Array.isArray(output?.stdout) ? output.stdout.length : undefined;
|
|
1333
|
+
const stderr = Array.isArray(output?.stderr) ? output.stderr.length : undefined;
|
|
1334
|
+
return `• process output ${String(input.id ?? "process")}${meta(
|
|
1335
|
+
stdout != null ? `${stdout} stdout` : undefined,
|
|
1336
|
+
stderr != null ? `${stderr} stderr` : undefined,
|
|
1337
|
+
)}`;
|
|
1338
|
+
}
|
|
1339
|
+
|
|
1340
|
+
if (action === "logs") {
|
|
1341
|
+
return `• process logs ${String(input.id ?? "process")}${meta("log paths")}`;
|
|
1342
|
+
}
|
|
1343
|
+
|
|
1344
|
+
const message = typeof record?.message === "string" ? record.message : text.split("\n")[0];
|
|
1345
|
+
return `• process ${action}${message ? ` · ${shortenCommand(message, 38)}` : ""}${duration ? ` · ${duration}` : ""}`;
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1348
|
+
function summarizeExternalToolResult(
|
|
1349
|
+
toolName: string,
|
|
1350
|
+
input: Record<string, unknown>,
|
|
1351
|
+
text: string,
|
|
1352
|
+
details: unknown,
|
|
1353
|
+
durationMs: number | undefined,
|
|
1354
|
+
): string {
|
|
1355
|
+
if (SERENA_COMPACT_TOOLS.has(toolName)) {
|
|
1356
|
+
return summarizeSerenaToolResult(toolName, input, text, durationMs);
|
|
1357
|
+
}
|
|
1358
|
+
if (toolName === "structured_return") {
|
|
1359
|
+
return summarizeStructuredReturnToolResult(input, text, details, durationMs);
|
|
1360
|
+
}
|
|
1361
|
+
if (toolName === "process") {
|
|
1362
|
+
return summarizeProcessToolResult(input, text, details, durationMs);
|
|
1363
|
+
}
|
|
1364
|
+
return summarizeGenericToolResult(toolName, input, text, durationMs);
|
|
1365
|
+
}
|
|
1366
|
+
|
|
1367
|
+
function withXtrmToolDetails(details: unknown, sourceText: string, toolName: string): unknown {
|
|
1368
|
+
const record = asRecord(details);
|
|
1369
|
+
return {
|
|
1370
|
+
...(record ?? {}),
|
|
1371
|
+
xtrmOriginalText: sourceText,
|
|
1372
|
+
xtrmToolFrame: externalToolFrameKind(toolName),
|
|
1373
|
+
};
|
|
1374
|
+
}
|
|
1375
|
+
|
|
959
1376
|
const XTRM_BUILTIN_TOOLS = new Set(["bash", "read", "edit", "write", "find", "grep", "ls"]);
|
|
960
1377
|
|
|
961
1378
|
function registerXtrmUiTools(pi: ExtensionAPI, getPrefs: () => XtrmUiPrefs): void {
|
|
@@ -1005,6 +1422,7 @@ function registerXtrmUiTools(pi: ExtensionAPI, getPrefs: () => XtrmUiPrefs): voi
|
|
|
1005
1422
|
pi.on("tool_result", async (event: ToolResultEvent, _ctx) => {
|
|
1006
1423
|
if (event.isError) return undefined;
|
|
1007
1424
|
if (XTRM_BUILTIN_TOOLS.has(event.toolName)) return undefined;
|
|
1425
|
+
if (!getPrefs().compactExternalToolResults) return undefined;
|
|
1008
1426
|
|
|
1009
1427
|
const text = getTextContent({ content: event.content as Array<{ type: string; text?: string }> });
|
|
1010
1428
|
const startedAt = toolCallStartTimes.get(event.toolCallId);
|
|
@@ -1018,13 +1436,17 @@ function registerXtrmUiTools(pi: ExtensionAPI, getPrefs: () => XtrmUiPrefs): voi
|
|
|
1018
1436
|
? (event.input as Record<string, unknown>)
|
|
1019
1437
|
: {};
|
|
1020
1438
|
|
|
1021
|
-
const compactText =
|
|
1022
|
-
|
|
1023
|
-
|
|
1439
|
+
const compactText = summarizeExternalToolResult(
|
|
1440
|
+
event.toolName,
|
|
1441
|
+
safeInput,
|
|
1442
|
+
sourceText,
|
|
1443
|
+
event.details,
|
|
1444
|
+
durationMs,
|
|
1445
|
+
);
|
|
1024
1446
|
|
|
1025
1447
|
return {
|
|
1026
1448
|
content: [{ type: "text", text: formatHierarchyText(compactText) }],
|
|
1027
|
-
details: event.details,
|
|
1449
|
+
details: withXtrmToolDetails(event.details, sourceText, event.toolName),
|
|
1028
1450
|
};
|
|
1029
1451
|
});
|
|
1030
1452
|
|
|
@@ -1043,7 +1465,7 @@ function registerXtrmUiTools(pi: ExtensionAPI, getPrefs: () => XtrmUiPrefs): voi
|
|
|
1043
1465
|
renderResult(result, { expanded, isPartial }, theme) {
|
|
1044
1466
|
const details = (result.details ?? {}) as DetailsWithXtrmMeta<BashToolDetails, Record<string, unknown>>;
|
|
1045
1467
|
const meta = getXtrmMeta<BashToolDetails, Record<string, unknown>>(details);
|
|
1046
|
-
const command = shortenCommand(String(meta?.args.command ?? ""));
|
|
1468
|
+
const command = shortenCommand(String(meta?.args.command ?? ""), 38);
|
|
1047
1469
|
if (isPartial) {
|
|
1048
1470
|
return toolRowText(theme, `${theme.fg("accent", "•")} ${theme.fg("toolTitle", "Running ")}${theme.fg("accent", command)}${theme.fg("toolTitle", " in bash")}`);
|
|
1049
1471
|
}
|
|
@@ -1263,13 +1685,17 @@ function isXtrmTheme(name: string | undefined): boolean {
|
|
|
1263
1685
|
|
|
1264
1686
|
export default function xtrmUiExtension(pi: ExtensionAPI): void {
|
|
1265
1687
|
void installSilentHiddenThinkingPatch().catch(() => undefined);
|
|
1688
|
+
void installExternalToolFramePatch().catch(() => undefined);
|
|
1266
1689
|
|
|
1267
1690
|
let prefs: XtrmUiPrefs = { ...DEFAULT_PREFS };
|
|
1268
1691
|
let previousThemeName: string | null = null;
|
|
1269
1692
|
const extensionThemeDir = join(__dirname, "../../themes/xtrm-ui");
|
|
1270
1693
|
|
|
1271
1694
|
const getPrefs = () => prefs;
|
|
1272
|
-
const setPrefs = (p: XtrmUiPrefs) => {
|
|
1695
|
+
const setPrefs = (p: XtrmUiPrefs) => {
|
|
1696
|
+
prefs = p;
|
|
1697
|
+
setActiveExternalToolChrome(p.externalToolChrome);
|
|
1698
|
+
};
|
|
1273
1699
|
const getThinkingLevel = () => formatThinking(pi.getThinkingLevel());
|
|
1274
1700
|
|
|
1275
1701
|
registerXtrmUiTools(pi, getPrefs);
|
|
@@ -1285,7 +1711,7 @@ export default function xtrmUiExtension(pi: ExtensionAPI): void {
|
|
|
1285
1711
|
}));
|
|
1286
1712
|
|
|
1287
1713
|
pi.on("session_start", async (_event, ctx) => {
|
|
1288
|
-
|
|
1714
|
+
setPrefs(loadPrefs(ctx.sessionManager.getEntries() as Array<MaybeCustomEntry>));
|
|
1289
1715
|
if (!previousThemeName && !isXtrmTheme(ctx.ui.theme.name)) {
|
|
1290
1716
|
previousThemeName = ctx.ui.theme.name ?? null;
|
|
1291
1717
|
}
|
|
@@ -12,6 +12,7 @@ import piSerenaCompactExtension from "./extensions/pi-serena-compact.ts";
|
|
|
12
12
|
import qualityGatesExtension from "./extensions/quality-gates.ts";
|
|
13
13
|
import serviceSkillsExtension from "./extensions/service-skills.ts";
|
|
14
14
|
import sessionFlowExtension from "./extensions/session-flow.ts";
|
|
15
|
+
import spTerminalOverlayExtension from "./extensions/sp-terminal-overlay.ts";
|
|
15
16
|
import xtrmLoaderExtension from "./extensions/xtrm-loader.ts";
|
|
16
17
|
import xtrmUiExtension from "./extensions/xtrm-ui.ts";
|
|
17
18
|
|
|
@@ -33,6 +34,7 @@ export const managedPiExtensions: readonly ManagedPiExtension[] = [
|
|
|
33
34
|
{ id: "quality-gates", register: qualityGatesExtension },
|
|
34
35
|
{ id: "service-skills", register: serviceSkillsExtension },
|
|
35
36
|
{ id: "session-flow", register: sessionFlowExtension },
|
|
37
|
+
{ id: "sp-terminal-overlay", register: spTerminalOverlayExtension },
|
|
36
38
|
{ id: "xtrm-loader", register: xtrmLoaderExtension },
|
|
37
39
|
{ id: "xtrm-ui", register: xtrmUiExtension },
|
|
38
40
|
];
|