xtrm-tools 0.5.45 → 0.5.47
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/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +14 -0
- package/README.md +24 -5
- package/cli/dist/index.cjs +9812 -9983
- package/cli/dist/index.cjs.map +1 -1
- package/cli/package.json +1 -1
- package/config/instructions/agents-top.md +2 -4
- package/config/instructions/claude-top.md +2 -4
- package/config/pi/extensions/beads/index.ts +18 -78
- package/config/pi/extensions/custom-footer/index.ts +2 -3
- package/config/pi/extensions/xtrm-ui/format.ts +93 -0
- package/config/pi/extensions/xtrm-ui/index.ts +1044 -0
- package/config/pi/extensions/xtrm-ui/package.json +10 -0
- package/config/pi/extensions/xtrm-ui/themes/pidex-dark.json +85 -0
- package/config/pi/extensions/xtrm-ui/themes/pidex-light.json +85 -0
- package/config/pi/install-schema.json +0 -1
- package/hooks/beads-claim-sync.mjs +15 -96
- package/hooks/beads-gate-messages.mjs +2 -4
- package/hooks/beads-gate-utils.mjs +0 -18
- package/hooks/statusline.mjs +5 -3
- package/package.json +1 -1
- package/plugins/xtrm-tools/.claude-plugin/plugin.json +1 -1
- package/plugins/xtrm-tools/hooks/beads-claim-sync.mjs +15 -96
- package/plugins/xtrm-tools/hooks/beads-gate-messages.mjs +2 -4
- package/plugins/xtrm-tools/hooks/beads-gate-utils.mjs +0 -18
- package/plugins/xtrm-tools/hooks/statusline.mjs +5 -3
- package/plugins/xtrm-tools/skills/planning/SKILL.md +75 -20
- package/plugins/xtrm-tools/skills/using-xtrm/SKILL.md +1 -1
- package/plugins/xtrm-tools/skills/xt-debugging/SKILL.md +149 -0
- package/plugins/xtrm-tools/skills/xt-end/SKILL.md +28 -0
- package/skills/planning/SKILL.md +75 -20
- package/skills/using-xtrm/SKILL.md +1 -1
- package/skills/xt-debugging/SKILL.md +149 -0
- package/skills/xt-end/SKILL.md +28 -0
- package/plugins/xtrm-tools/skills/gitnexus-debugging/SKILL.md +0 -85
- package/skills/gitnexus-debugging/SKILL.md +0 -85
|
@@ -0,0 +1,1044 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* XTRM UI Extension
|
|
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.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import type {
|
|
14
|
+
BashToolDetails,
|
|
15
|
+
EditToolDetails,
|
|
16
|
+
ExtensionAPI,
|
|
17
|
+
ExtensionContext,
|
|
18
|
+
FindToolDetails,
|
|
19
|
+
GrepToolDetails,
|
|
20
|
+
LsToolDetails,
|
|
21
|
+
ReadToolDetails,
|
|
22
|
+
ToolResultEvent,
|
|
23
|
+
} from "@mariozechner/pi-coding-agent";
|
|
24
|
+
import {
|
|
25
|
+
CustomEditor,
|
|
26
|
+
createBashTool,
|
|
27
|
+
createEditTool,
|
|
28
|
+
createFindTool,
|
|
29
|
+
createGrepTool,
|
|
30
|
+
createLsTool,
|
|
31
|
+
createReadTool,
|
|
32
|
+
createWriteTool,
|
|
33
|
+
} from "@mariozechner/pi-coding-agent";
|
|
34
|
+
import { Box, Text, truncateToWidth, visibleWidth } from "@mariozechner/pi-tui";
|
|
35
|
+
import { basename } from "node:path";
|
|
36
|
+
import {
|
|
37
|
+
cleanOutputLines,
|
|
38
|
+
countPrefixedItems,
|
|
39
|
+
diffStats,
|
|
40
|
+
formatDuration,
|
|
41
|
+
formatLineLabel,
|
|
42
|
+
joinMeta,
|
|
43
|
+
lineCount,
|
|
44
|
+
previewLines,
|
|
45
|
+
renderToolSummary,
|
|
46
|
+
shortenCommand,
|
|
47
|
+
shortenPath,
|
|
48
|
+
} from "./format";
|
|
49
|
+
|
|
50
|
+
// ============================================================================
|
|
51
|
+
// Types
|
|
52
|
+
// ============================================================================
|
|
53
|
+
|
|
54
|
+
export type XtrmThemeName = "pidex-dark" | "pidex-light";
|
|
55
|
+
export type XtrmDensity = "compact" | "comfortable";
|
|
56
|
+
|
|
57
|
+
export interface XtrmUiPrefs {
|
|
58
|
+
themeName: XtrmThemeName;
|
|
59
|
+
density: XtrmDensity;
|
|
60
|
+
showHeader: boolean;
|
|
61
|
+
compactTools: boolean;
|
|
62
|
+
showFooter: boolean; // Our key addition - when false, skip setFooter()
|
|
63
|
+
forceTheme: boolean; // When false, skip setTheme (allow external theme override)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// ============================================================================
|
|
67
|
+
// Defaults
|
|
68
|
+
// ============================================================================
|
|
69
|
+
|
|
70
|
+
export const XTRM_UI_PREFS_ENTRY = "xtrm-ui-prefs";
|
|
71
|
+
|
|
72
|
+
export const DEFAULT_PREFS: XtrmUiPrefs = {
|
|
73
|
+
themeName: "pidex-light",
|
|
74
|
+
density: "compact",
|
|
75
|
+
showHeader: true,
|
|
76
|
+
compactTools: true,
|
|
77
|
+
showFooter: false, // XTRM: disable pi-dex footer, use custom-footer
|
|
78
|
+
forceTheme: true,
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
// ============================================================================
|
|
82
|
+
// Preferences
|
|
83
|
+
// ============================================================================
|
|
84
|
+
|
|
85
|
+
type MaybeCustomEntry = {
|
|
86
|
+
type?: string;
|
|
87
|
+
customType?: string;
|
|
88
|
+
data?: unknown;
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
function normalizePrefs(input: unknown): XtrmUiPrefs {
|
|
92
|
+
if (!input || typeof input !== "object") return { ...DEFAULT_PREFS };
|
|
93
|
+
const source = input as Partial<XtrmUiPrefs>;
|
|
94
|
+
return {
|
|
95
|
+
themeName: source.themeName === "pidex-dark" ? "pidex-dark" : "pidex-light",
|
|
96
|
+
density: source.density === "comfortable" ? "comfortable" : "compact",
|
|
97
|
+
showHeader: source.showHeader ?? DEFAULT_PREFS.showHeader,
|
|
98
|
+
compactTools: source.compactTools ?? DEFAULT_PREFS.compactTools,
|
|
99
|
+
showFooter: source.showFooter ?? DEFAULT_PREFS.showFooter,
|
|
100
|
+
forceTheme: source.forceTheme ?? DEFAULT_PREFS.forceTheme,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function loadPrefs(entries: ReadonlyArray<MaybeCustomEntry>): XtrmUiPrefs {
|
|
105
|
+
for (let i = entries.length - 1; i >= 0; i--) {
|
|
106
|
+
const entry = entries[i];
|
|
107
|
+
if (entry?.type === "custom" && entry.customType === XTRM_UI_PREFS_ENTRY) {
|
|
108
|
+
return normalizePrefs(entry.data);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return { ...DEFAULT_PREFS };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function persistPrefs(pi: ExtensionAPI, prefs: XtrmUiPrefs): void {
|
|
115
|
+
pi.appendEntry(XTRM_UI_PREFS_ENTRY, prefs);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// ============================================================================
|
|
119
|
+
// Chrome Application
|
|
120
|
+
// ============================================================================
|
|
121
|
+
|
|
122
|
+
function fitVisible(text: string, width: number): string {
|
|
123
|
+
const truncated = truncateToWidth(text, width);
|
|
124
|
+
return truncated + " ".repeat(Math.max(0, width - visibleWidth(truncated)));
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function formatThinking(level: string): string {
|
|
128
|
+
return level === "off" ? "standard" : level;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function applyXtrmChrome(
|
|
132
|
+
ctx: ExtensionContext,
|
|
133
|
+
prefs: XtrmUiPrefs,
|
|
134
|
+
getThinkingLevel: () => string
|
|
135
|
+
): void {
|
|
136
|
+
// Theme
|
|
137
|
+
if (prefs.forceTheme) ctx.ui.setTheme(prefs.themeName);
|
|
138
|
+
|
|
139
|
+
// Tool expansion
|
|
140
|
+
ctx.ui.setToolsExpanded(!prefs.compactTools);
|
|
141
|
+
|
|
142
|
+
// Editor — density-aware input padding
|
|
143
|
+
ctx.ui.setEditorComponent((tui, theme, keybindings) => {
|
|
144
|
+
const editor = new XtrmEditor(tui, theme, keybindings);
|
|
145
|
+
editor.setPrefs(prefs);
|
|
146
|
+
return editor;
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
// Header (optional)
|
|
150
|
+
if (prefs.showHeader) {
|
|
151
|
+
ctx.ui.setHeader((_tui, theme) => ({
|
|
152
|
+
invalidate() {},
|
|
153
|
+
render(width: number): string[] {
|
|
154
|
+
const boxWidth = width >= 54 ? 50 : Math.max(24, width);
|
|
155
|
+
const model = ctx.model?.id ?? "no-model";
|
|
156
|
+
const thinking = getThinkingLevel();
|
|
157
|
+
const border = (text: string) => theme.fg("borderAccent", text);
|
|
158
|
+
const leftPad = "";
|
|
159
|
+
|
|
160
|
+
const top = leftPad + border(`╭${"─".repeat(Math.max(0, boxWidth - 2))}╮`);
|
|
161
|
+
const line1 =
|
|
162
|
+
leftPad +
|
|
163
|
+
border("│") +
|
|
164
|
+
fitVisible(
|
|
165
|
+
` ${theme.fg("dim", ">_")} ${theme.bold("XTRM")} ${theme.fg("dim", `(v1.0.0)`)}`,
|
|
166
|
+
boxWidth - 2
|
|
167
|
+
) +
|
|
168
|
+
border("│");
|
|
169
|
+
const gap = leftPad + border("│") + fitVisible("", boxWidth - 2) + border("│");
|
|
170
|
+
const line2 =
|
|
171
|
+
leftPad +
|
|
172
|
+
border("│") +
|
|
173
|
+
fitVisible(
|
|
174
|
+
` ${theme.fg("dim", "model:".padEnd(11))}${model} ${thinking}${theme.fg("accent", " /model")}${theme.fg("dim", " to change")}`,
|
|
175
|
+
boxWidth - 2
|
|
176
|
+
) +
|
|
177
|
+
border("│");
|
|
178
|
+
const line3 =
|
|
179
|
+
leftPad +
|
|
180
|
+
border("│") +
|
|
181
|
+
fitVisible(
|
|
182
|
+
` ${theme.fg("dim", "directory:".padEnd(11))}${basename(ctx.cwd)}`,
|
|
183
|
+
boxWidth - 2
|
|
184
|
+
) +
|
|
185
|
+
border("│");
|
|
186
|
+
const bottom = leftPad + border(`╰${"─".repeat(Math.max(0, boxWidth - 2))}╯`);
|
|
187
|
+
|
|
188
|
+
return [top, line1, gap, line2, line3, bottom];
|
|
189
|
+
},
|
|
190
|
+
}));
|
|
191
|
+
} else {
|
|
192
|
+
ctx.ui.setHeader(undefined);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Footer - ONLY if showFooter is true (default false for XTRM)
|
|
196
|
+
// This is the key difference from pi-dex - we let custom-footer handle it
|
|
197
|
+
if (prefs.showFooter) {
|
|
198
|
+
ctx.ui.setFooter((tui, theme, footerData) => {
|
|
199
|
+
const unsubscribe = footerData.onBranchChange(() => tui.requestRender());
|
|
200
|
+
return {
|
|
201
|
+
dispose: unsubscribe,
|
|
202
|
+
invalidate() {},
|
|
203
|
+
render(width: number): string[] {
|
|
204
|
+
const modelId = ctx.model?.id ?? "no-model";
|
|
205
|
+
const thinking = getThinkingLevel();
|
|
206
|
+
const contextUsage = ctx.getContextUsage();
|
|
207
|
+
const leftPct = contextUsage?.percent != null ? `${100 - Math.round(contextUsage.percent)}% left` : undefined;
|
|
208
|
+
const line = theme.fg(
|
|
209
|
+
"dim",
|
|
210
|
+
[`${modelId} ${thinking}`, leftPct, basename(ctx.cwd)]
|
|
211
|
+
.filter(Boolean)
|
|
212
|
+
.join(" · ")
|
|
213
|
+
);
|
|
214
|
+
return [truncateToWidth(line, width)];
|
|
215
|
+
},
|
|
216
|
+
};
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
// If showFooter is false, we do NOT call setFooter - custom-footer will handle it
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// ============================================================================
|
|
223
|
+
// Tool Render Helpers
|
|
224
|
+
// ============================================================================
|
|
225
|
+
|
|
226
|
+
function renderOutputPreview(theme: any, lines: string[], maxLines: number): string {
|
|
227
|
+
const subset = lines.slice(0, maxLines);
|
|
228
|
+
let text = subset.map((line) => theme.fg("toolOutput", ` ${line}`)).join("\n");
|
|
229
|
+
if (lines.length > maxLines) text += `\n${theme.fg("muted", ` … +${lines.length - maxLines} more`)}`;
|
|
230
|
+
return text;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function renderVerticalPreview(theme: any, lines: string[], maxLines: number): string {
|
|
234
|
+
const subset = lines.slice(0, maxLines);
|
|
235
|
+
let text = subset.map((line) => `${theme.fg("muted", "│")} ${theme.fg("toolOutput", line)}`).join("\n");
|
|
236
|
+
if (lines.length > maxLines) text += `\n${theme.fg("muted", "│")} ${theme.fg("muted", `… +${lines.length - maxLines} more lines`)}`;
|
|
237
|
+
return text;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function renderDiffPreview(theme: any, diff: string, maxLines: number): string {
|
|
241
|
+
const lines = diff.split("\n").slice(0, maxLines);
|
|
242
|
+
let out = "";
|
|
243
|
+
for (const line of lines) {
|
|
244
|
+
const styled =
|
|
245
|
+
line.startsWith("+") && !line.startsWith("+++") ? theme.fg("toolDiffAdded", ` ${line}`)
|
|
246
|
+
: line.startsWith("-") && !line.startsWith("---") ? theme.fg("toolDiffRemoved", ` ${line}`)
|
|
247
|
+
: theme.fg("toolDiffContext", ` ${line}`);
|
|
248
|
+
out += (out ? "\n" : "") + styled;
|
|
249
|
+
}
|
|
250
|
+
if (diff.split("\n").length > maxLines) out += `\n${theme.fg("muted", ` … +${diff.split("\n").length - maxLines} more`)}`;
|
|
251
|
+
return out;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function lineRange(offset?: number, limit?: number): string | undefined {
|
|
255
|
+
if (offset == null && limit == null) return undefined;
|
|
256
|
+
const start = offset ?? 1;
|
|
257
|
+
if (limit == null) return `${start}`;
|
|
258
|
+
return `${start}-${start + limit - 1}`;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function summarizeCount(text: string): number {
|
|
262
|
+
return text.split("\n").filter((line) => line.trim().length > 0).length;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// ============================================================================
|
|
266
|
+
// Editor (task p38n.3)
|
|
267
|
+
// ============================================================================
|
|
268
|
+
|
|
269
|
+
class XtrmEditor extends CustomEditor {
|
|
270
|
+
constructor(...args: ConstructorParameters<typeof CustomEditor>) {
|
|
271
|
+
super(...args);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
setPrefs(prefs: XtrmUiPrefs): void {
|
|
275
|
+
this.setPaddingX(prefs.density === "comfortable" ? 2 : 1);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
render(width: number): string[] {
|
|
279
|
+
return super.render(width);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// ============================================================================
|
|
284
|
+
// Commands
|
|
285
|
+
// ============================================================================
|
|
286
|
+
|
|
287
|
+
function sendInfoMessage(pi: ExtensionAPI, title: string, content: string): void {
|
|
288
|
+
pi.sendMessage({
|
|
289
|
+
customType: "xtrm-ui-info",
|
|
290
|
+
content,
|
|
291
|
+
display: true,
|
|
292
|
+
details: { title },
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function parseThemeArg(arg: string): XtrmThemeName | undefined {
|
|
297
|
+
const normalized = arg.trim().toLowerCase();
|
|
298
|
+
if (normalized === "dark" || normalized === "pidex-dark") return "pidex-dark";
|
|
299
|
+
if (normalized === "light" || normalized === "pidex-light") return "pidex-light";
|
|
300
|
+
return undefined;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function parseDensityArg(arg: string): XtrmDensity | undefined {
|
|
304
|
+
const normalized = arg.trim().toLowerCase();
|
|
305
|
+
if (normalized === "compact") return "compact";
|
|
306
|
+
if (normalized === "comfortable" || normalized === "normal") return "comfortable";
|
|
307
|
+
return undefined;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function registerCommands(pi: ExtensionAPI, getPrefs: () => XtrmUiPrefs, setPrefs: (p: XtrmUiPrefs) => void, getThinkingLevel: () => string) {
|
|
311
|
+
pi.registerMessageRenderer("xtrm-ui-info", (message, _options, theme) => {
|
|
312
|
+
const title = (message.details as { title?: string } | undefined)?.title ?? "XTRM UI";
|
|
313
|
+
const box = new Box(1, 1, (text) => theme.bg("customMessageBg", text));
|
|
314
|
+
box.addChild(new Text(theme.fg("customMessageLabel", theme.bold(title)), 0, 0));
|
|
315
|
+
box.addChild(new Text(theme.fg("customMessageText", String(message.content ?? "")), 0, 0));
|
|
316
|
+
return box;
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
pi.registerCommand("xtrm-ui", {
|
|
320
|
+
description: "Show XTRM UI status and active preferences",
|
|
321
|
+
handler: async (_args, ctx) => {
|
|
322
|
+
const prefs = getPrefs();
|
|
323
|
+
const contextUsage = ctx.getContextUsage();
|
|
324
|
+
const lines = [
|
|
325
|
+
`Theme: ${prefs.themeName}`,
|
|
326
|
+
`Force theme: ${prefs.forceTheme ? "on" : "off"}`,
|
|
327
|
+
`Density: ${prefs.density}`,
|
|
328
|
+
`Compact tools: ${prefs.compactTools ? "on" : "off"}`,
|
|
329
|
+
`Show header: ${prefs.showHeader ? "yes" : "no"}`,
|
|
330
|
+
`Show footer: ${prefs.showFooter ? "yes" : "no"} (custom-footer handles this)`,
|
|
331
|
+
`Model: ${ctx.model?.id ?? "none"}`,
|
|
332
|
+
`Context: ${contextUsage?.tokens ?? "unknown"}/${contextUsage?.contextWindow ?? "unknown"}`,
|
|
333
|
+
];
|
|
334
|
+
sendInfoMessage(pi, "XTRM UI status", lines.join("\\n"));
|
|
335
|
+
},
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
pi.registerCommand("xtrm-ui-theme", {
|
|
339
|
+
description: "Switch XTRM UI theme: dark|light",
|
|
340
|
+
getArgumentCompletions: (prefix) => {
|
|
341
|
+
const values = ["dark", "light"].filter((item) => item.startsWith(prefix));
|
|
342
|
+
return values.length > 0 ? values.map((value) => ({ value, label: value })) : null;
|
|
343
|
+
},
|
|
344
|
+
handler: async (args, ctx) => {
|
|
345
|
+
const themeName = parseThemeArg(args);
|
|
346
|
+
if (!themeName) {
|
|
347
|
+
ctx.ui.notify("Usage: /xtrm-ui-theme dark|light", "warning");
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
const prefs = { ...getPrefs(), themeName };
|
|
351
|
+
setPrefs(prefs);
|
|
352
|
+
persistPrefs(pi, prefs);
|
|
353
|
+
applyXtrmChrome(ctx, prefs, getThinkingLevel);
|
|
354
|
+
ctx.ui.notify(`XTRM UI theme set to ${themeName}`, "info");
|
|
355
|
+
},
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
pi.registerCommand("xtrm-ui-density", {
|
|
359
|
+
description: "Switch XTRM UI density: compact|comfortable",
|
|
360
|
+
getArgumentCompletions: (prefix) => {
|
|
361
|
+
const values = ["compact", "comfortable"].filter((item) => item.startsWith(prefix));
|
|
362
|
+
return values.length > 0 ? values.map((value) => ({ value, label: value })) : null;
|
|
363
|
+
},
|
|
364
|
+
handler: async (args, ctx) => {
|
|
365
|
+
const density = parseDensityArg(args);
|
|
366
|
+
if (!density) {
|
|
367
|
+
ctx.ui.notify("Usage: /xtrm-ui-density compact|comfortable", "warning");
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
const prefs = { ...getPrefs(), density };
|
|
371
|
+
setPrefs(prefs);
|
|
372
|
+
persistPrefs(pi, prefs);
|
|
373
|
+
applyXtrmChrome(ctx, prefs, getThinkingLevel);
|
|
374
|
+
ctx.ui.notify(`XTRM UI density set to ${density}`, "info");
|
|
375
|
+
},
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
pi.registerCommand("xtrm-ui-header", {
|
|
379
|
+
description: "Toggle XTRM UI header: on|off",
|
|
380
|
+
getArgumentCompletions: (prefix) => {
|
|
381
|
+
const values = ["on", "off"].filter((item) => item.startsWith(prefix));
|
|
382
|
+
return values.length > 0 ? values.map((value) => ({ value, label: value })) : null;
|
|
383
|
+
},
|
|
384
|
+
handler: async (args, ctx) => {
|
|
385
|
+
const showHeader = args.trim().toLowerCase() === "on";
|
|
386
|
+
const prefs = { ...getPrefs(), showHeader };
|
|
387
|
+
setPrefs(prefs);
|
|
388
|
+
persistPrefs(pi, prefs);
|
|
389
|
+
applyXtrmChrome(ctx, prefs, getThinkingLevel);
|
|
390
|
+
ctx.ui.notify(`XTRM UI header ${showHeader ? "enabled" : "disabled"}`, "info");
|
|
391
|
+
},
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
pi.registerCommand("xtrm-ui-forcetheme", {
|
|
395
|
+
description: "Control whether xtrm-ui overrides the active theme: on|off",
|
|
396
|
+
getArgumentCompletions: (prefix) => {
|
|
397
|
+
const values = ["on", "off"].filter((item) => item.startsWith(prefix));
|
|
398
|
+
return values.length > 0 ? values.map((value) => ({ value, label: value })) : null;
|
|
399
|
+
},
|
|
400
|
+
handler: async (args, ctx) => {
|
|
401
|
+
const normalized = args.trim().toLowerCase();
|
|
402
|
+
if (normalized !== "on" && normalized !== "off") {
|
|
403
|
+
ctx.ui.notify("Usage: /xtrm-ui-forcetheme on|off", "warning");
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
406
|
+
const forceTheme = normalized === "on";
|
|
407
|
+
const prefs = { ...getPrefs(), forceTheme };
|
|
408
|
+
setPrefs(prefs);
|
|
409
|
+
persistPrefs(pi, prefs);
|
|
410
|
+
applyXtrmChrome(ctx, prefs, getThinkingLevel);
|
|
411
|
+
ctx.ui.notify(`XTRM UI force theme ${forceTheme ? "enabled" : "disabled"}`, "info");
|
|
412
|
+
},
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
pi.registerCommand("xtrm-ui-reset", {
|
|
416
|
+
description: "Restore XTRM UI defaults",
|
|
417
|
+
handler: async (_args, ctx) => {
|
|
418
|
+
const prefs = { ...DEFAULT_PREFS };
|
|
419
|
+
setPrefs(prefs);
|
|
420
|
+
persistPrefs(pi, prefs);
|
|
421
|
+
applyXtrmChrome(ctx, prefs, getThinkingLevel);
|
|
422
|
+
ctx.ui.notify("XTRM UI reset to defaults", "info");
|
|
423
|
+
},
|
|
424
|
+
});
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// ============================================================================
|
|
428
|
+
// Tool Renderers (ported from pi-dex tooling.ts)
|
|
429
|
+
// ============================================================================
|
|
430
|
+
|
|
431
|
+
type BuiltInTools = ReturnType<typeof createBuiltInTools>;
|
|
432
|
+
|
|
433
|
+
type XtrmMeta<TArgs = Record<string, unknown>> = {
|
|
434
|
+
tool: string;
|
|
435
|
+
args: TArgs;
|
|
436
|
+
durationMs: number;
|
|
437
|
+
};
|
|
438
|
+
|
|
439
|
+
type DetailsWithXtrmMeta<TDetails, TArgs = Record<string, unknown>> = TDetails & {
|
|
440
|
+
xtrmMeta?: XtrmMeta<TArgs>;
|
|
441
|
+
};
|
|
442
|
+
|
|
443
|
+
const toolCache = new Map<string, BuiltInTools>();
|
|
444
|
+
|
|
445
|
+
function createBuiltInTools(cwd: string) {
|
|
446
|
+
return {
|
|
447
|
+
bash: createBashTool(cwd),
|
|
448
|
+
read: createReadTool(cwd),
|
|
449
|
+
edit: createEditTool(cwd),
|
|
450
|
+
write: createWriteTool(cwd),
|
|
451
|
+
find: createFindTool(cwd),
|
|
452
|
+
grep: createGrepTool(cwd),
|
|
453
|
+
ls: createLsTool(cwd),
|
|
454
|
+
};
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
function getTools(cwd: string): BuiltInTools {
|
|
458
|
+
let tools = toolCache.get(cwd);
|
|
459
|
+
if (!tools) {
|
|
460
|
+
tools = createBuiltInTools(cwd);
|
|
461
|
+
toolCache.set(cwd, tools);
|
|
462
|
+
}
|
|
463
|
+
return tools;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
function withXtrmMeta<TDetails extends object, TArgs extends Record<string, unknown>>(
|
|
467
|
+
details: TDetails | undefined,
|
|
468
|
+
tool: string,
|
|
469
|
+
args: TArgs,
|
|
470
|
+
durationMs: number,
|
|
471
|
+
): DetailsWithXtrmMeta<TDetails, TArgs> {
|
|
472
|
+
return { ...(details ?? ({} as TDetails)), xtrmMeta: { tool, args, durationMs } };
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
function getXtrmMeta<TDetails extends object, TArgs extends Record<string, unknown>>(
|
|
476
|
+
details: TDetails | undefined,
|
|
477
|
+
): XtrmMeta<TArgs> | undefined {
|
|
478
|
+
if (!details || typeof details !== "object") return undefined;
|
|
479
|
+
return (details as DetailsWithXtrmMeta<TDetails, TArgs>).xtrmMeta;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
function getTextContent(result: { content: Array<{ type: string; text?: string }> }): string {
|
|
483
|
+
const item = result.content.find((content) => content.type === "text");
|
|
484
|
+
return item?.text ?? "";
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
function renderPendingCall(toolName: string, args: Record<string, unknown>, theme: any): Text {
|
|
488
|
+
return new Text(renderToolSummary(theme, "pending", toolName, summarizeToolSubject(toolName, args), undefined), 0, 0);
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
function stableToolSignature(toolName: string, args: Record<string, unknown>): string {
|
|
492
|
+
return `${toolName}:${JSON.stringify(args)}`;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
function summarizeToolSubject(toolName: string, args: Record<string, unknown>): string | undefined {
|
|
496
|
+
switch (toolName) {
|
|
497
|
+
case "bash": return shortenCommand(String(args.command ?? ""), 52);
|
|
498
|
+
case "read": {
|
|
499
|
+
const path = shortenPath(String(args.path ?? ""), 42);
|
|
500
|
+
const range = lineRange(args.offset as number | undefined, args.limit as number | undefined);
|
|
501
|
+
return range ? `${path}:${range}` : path;
|
|
502
|
+
}
|
|
503
|
+
case "edit":
|
|
504
|
+
case "write": return shortenPath(String(args.path ?? ""), 42);
|
|
505
|
+
case "find":
|
|
506
|
+
case "grep": return String(args.pattern ?? "");
|
|
507
|
+
case "ls": return shortenPath(String(args.path ?? "."), 42);
|
|
508
|
+
default: return undefined;
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
const SERENA_COMPACT_TOOLS = new Set([
|
|
513
|
+
"find_symbol",
|
|
514
|
+
"find_referencing_symbols",
|
|
515
|
+
"insert_after_symbol",
|
|
516
|
+
"replace_symbol_body",
|
|
517
|
+
"read_file",
|
|
518
|
+
"get_symbols_overview",
|
|
519
|
+
"insert_before_symbol",
|
|
520
|
+
"rename_symbol",
|
|
521
|
+
"restart_language_server",
|
|
522
|
+
"jet_brains_get_symbols_overview",
|
|
523
|
+
"jet_brains_find_symbol",
|
|
524
|
+
"jet_brains_find_referencing_symbols",
|
|
525
|
+
"jet_brains_type_hierarchy",
|
|
526
|
+
"search_for_pattern",
|
|
527
|
+
"list_dir",
|
|
528
|
+
"find_file",
|
|
529
|
+
"create_text_file",
|
|
530
|
+
"replace_content",
|
|
531
|
+
"delete_lines",
|
|
532
|
+
"replace_lines",
|
|
533
|
+
"insert_at_line",
|
|
534
|
+
"execute_shell_command",
|
|
535
|
+
"get_current_config",
|
|
536
|
+
"activate_project",
|
|
537
|
+
"remove_project",
|
|
538
|
+
"switch_modes",
|
|
539
|
+
"open_dashboard",
|
|
540
|
+
"check_onboarding_performed",
|
|
541
|
+
"onboarding",
|
|
542
|
+
"initial_instructions",
|
|
543
|
+
"prepare_for_new_conversation",
|
|
544
|
+
"summarize_changes",
|
|
545
|
+
"think_about_collected_information",
|
|
546
|
+
"think_about_task_adherence",
|
|
547
|
+
"think_about_whether_you_are_done",
|
|
548
|
+
"read_memory",
|
|
549
|
+
"write_memory",
|
|
550
|
+
"list_memories",
|
|
551
|
+
"delete_memory",
|
|
552
|
+
"rename_memory",
|
|
553
|
+
"edit_memory",
|
|
554
|
+
"serena_mcp_reset",
|
|
555
|
+
]);
|
|
556
|
+
|
|
557
|
+
function parseJson(text: string): unknown | undefined {
|
|
558
|
+
try {
|
|
559
|
+
return JSON.parse(text);
|
|
560
|
+
} catch {
|
|
561
|
+
return undefined;
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
function asRecord(value: unknown): Record<string, unknown> | undefined {
|
|
566
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value as Record<string, unknown> : undefined;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
function countSearchMatches(payload: unknown): number | undefined {
|
|
570
|
+
const record = asRecord(payload);
|
|
571
|
+
if (!record) return undefined;
|
|
572
|
+
let total = 0;
|
|
573
|
+
for (const value of Object.values(record)) {
|
|
574
|
+
if (Array.isArray(value)) total += value.length;
|
|
575
|
+
}
|
|
576
|
+
return total > 0 ? total : undefined;
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
function countOverviewSymbols(payload: unknown): number {
|
|
580
|
+
if (Array.isArray(payload)) {
|
|
581
|
+
const nested = payload.reduce<number>((total, value) => total + countOverviewSymbols(value), 0);
|
|
582
|
+
return nested || payload.length;
|
|
583
|
+
}
|
|
584
|
+
const record = asRecord(payload);
|
|
585
|
+
if (!record) return 0;
|
|
586
|
+
return Object.values(record).reduce<number>((total, value) => total + countOverviewSymbols(value), 0);
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
function countLines(text: string): number {
|
|
590
|
+
if (!text) return 0;
|
|
591
|
+
return text.split("\n").length;
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
function countJsonItems(payload: unknown): number | undefined {
|
|
595
|
+
if (Array.isArray(payload)) return payload.length;
|
|
596
|
+
const record = asRecord(payload);
|
|
597
|
+
if (!record) return undefined;
|
|
598
|
+
|
|
599
|
+
let total = 0;
|
|
600
|
+
for (const value of Object.values(record)) {
|
|
601
|
+
if (Array.isArray(value)) total += value.length;
|
|
602
|
+
}
|
|
603
|
+
return total > 0 ? total : undefined;
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
function summarizeSerenaSubject(toolName: string, input: Record<string, unknown>): string | undefined {
|
|
607
|
+
switch (toolName) {
|
|
608
|
+
case "find_symbol":
|
|
609
|
+
case "find_referencing_symbols":
|
|
610
|
+
case "replace_symbol_body":
|
|
611
|
+
case "insert_after_symbol":
|
|
612
|
+
case "insert_before_symbol":
|
|
613
|
+
case "rename_symbol":
|
|
614
|
+
case "jet_brains_find_symbol":
|
|
615
|
+
case "jet_brains_find_referencing_symbols":
|
|
616
|
+
case "jet_brains_type_hierarchy":
|
|
617
|
+
return String(input.name_path_pattern ?? input.name_path ?? "symbol");
|
|
618
|
+
case "get_symbols_overview":
|
|
619
|
+
case "jet_brains_get_symbols_overview":
|
|
620
|
+
case "read_file":
|
|
621
|
+
case "create_text_file":
|
|
622
|
+
case "replace_content":
|
|
623
|
+
case "replace_lines":
|
|
624
|
+
case "delete_lines":
|
|
625
|
+
case "insert_at_line":
|
|
626
|
+
case "list_dir":
|
|
627
|
+
case "find_file":
|
|
628
|
+
return shortenPath(String(input.relative_path ?? input.path ?? "."), 42);
|
|
629
|
+
case "search_for_pattern":
|
|
630
|
+
return shortenCommand(String(input.substring_pattern ?? ""), 52);
|
|
631
|
+
case "read_memory":
|
|
632
|
+
case "write_memory":
|
|
633
|
+
case "delete_memory":
|
|
634
|
+
case "rename_memory":
|
|
635
|
+
case "edit_memory":
|
|
636
|
+
return String(input.memory_name ?? input.old_name ?? "memory");
|
|
637
|
+
case "activate_project":
|
|
638
|
+
case "remove_project":
|
|
639
|
+
return String(input.project ?? input.project_name ?? "project");
|
|
640
|
+
case "switch_modes": {
|
|
641
|
+
const modes = input.modes;
|
|
642
|
+
if (Array.isArray(modes)) return modes.map((mode) => String(mode)).join(",");
|
|
643
|
+
return "modes";
|
|
644
|
+
}
|
|
645
|
+
case "execute_shell_command":
|
|
646
|
+
return shortenCommand(String(input.command ?? ""), 52);
|
|
647
|
+
default:
|
|
648
|
+
return undefined;
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
function summarizeSerenaToolResult(
|
|
653
|
+
toolName: string,
|
|
654
|
+
input: Record<string, unknown>,
|
|
655
|
+
text: string,
|
|
656
|
+
durationMs: number | undefined,
|
|
657
|
+
): string {
|
|
658
|
+
const payload = parseJson(text);
|
|
659
|
+
const duration = formatDuration(durationMs);
|
|
660
|
+
const subject = summarizeSerenaSubject(toolName, input);
|
|
661
|
+
const meta = (...parts: Array<string | undefined>) => {
|
|
662
|
+
const joined = joinMeta(parts);
|
|
663
|
+
return joined ? ` · ${joined}` : "";
|
|
664
|
+
};
|
|
665
|
+
|
|
666
|
+
switch (toolName) {
|
|
667
|
+
case "find_symbol":
|
|
668
|
+
case "find_referencing_symbols":
|
|
669
|
+
case "jet_brains_find_symbol":
|
|
670
|
+
case "jet_brains_find_referencing_symbols": {
|
|
671
|
+
const count = countJsonItems(payload) ?? (text.match(/"name_path"\s*:/g)?.length ?? 0);
|
|
672
|
+
return `• serena ${toolName} ${subject ?? "symbol"}${meta(formatLineLabel(count, "result"), duration)}`;
|
|
673
|
+
}
|
|
674
|
+
case "get_symbols_overview":
|
|
675
|
+
case "jet_brains_get_symbols_overview":
|
|
676
|
+
case "jet_brains_type_hierarchy": {
|
|
677
|
+
const count = Math.max(countOverviewSymbols(payload), text.match(/"name_path"\s*:/g)?.length ?? 0);
|
|
678
|
+
return `• serena ${toolName} ${subject ?? "file"}${meta(formatLineLabel(count, "symbol"), duration)}`;
|
|
679
|
+
}
|
|
680
|
+
case "search_for_pattern": {
|
|
681
|
+
const count = countSearchMatches(payload) ?? (text.match(/^\s*>\s*\d+:/gm)?.length ?? 0);
|
|
682
|
+
return `• serena search ${subject ?? "pattern"}${meta(formatLineLabel(count, "match"), duration)}`;
|
|
683
|
+
}
|
|
684
|
+
case "read_file": {
|
|
685
|
+
return `• serena read ${subject ?? "file"}${meta(formatLineLabel(countLines(text), "line"), duration)}`;
|
|
686
|
+
}
|
|
687
|
+
case "list_dir": {
|
|
688
|
+
const count = countJsonItems(payload) ?? countLines(text);
|
|
689
|
+
return `• serena list_dir ${subject ?? "."}${meta(formatLineLabel(count, "entry"), duration)}`;
|
|
690
|
+
}
|
|
691
|
+
case "find_file": {
|
|
692
|
+
const count = countJsonItems(payload) ?? countLines(text);
|
|
693
|
+
return `• serena find_file ${String(input.file_mask ?? "")}${meta(formatLineLabel(count, "match"), duration)}`;
|
|
694
|
+
}
|
|
695
|
+
case "replace_symbol_body":
|
|
696
|
+
case "insert_after_symbol":
|
|
697
|
+
case "insert_before_symbol":
|
|
698
|
+
case "rename_symbol":
|
|
699
|
+
case "create_text_file":
|
|
700
|
+
case "replace_content":
|
|
701
|
+
case "replace_lines":
|
|
702
|
+
case "delete_lines":
|
|
703
|
+
case "insert_at_line":
|
|
704
|
+
case "write_memory":
|
|
705
|
+
case "delete_memory":
|
|
706
|
+
case "rename_memory":
|
|
707
|
+
case "edit_memory":
|
|
708
|
+
case "activate_project":
|
|
709
|
+
case "remove_project":
|
|
710
|
+
case "switch_modes":
|
|
711
|
+
case "restart_language_server":
|
|
712
|
+
case "onboarding":
|
|
713
|
+
case "serena_mcp_reset":
|
|
714
|
+
return `• serena ${toolName}${subject ? ` ${subject}` : ""}${meta(duration)}`;
|
|
715
|
+
case "execute_shell_command": {
|
|
716
|
+
const count = countLines(text);
|
|
717
|
+
return `• serena shell ${subject ?? "command"}${meta(formatLineLabel(count, "line"), duration)}`;
|
|
718
|
+
}
|
|
719
|
+
default: {
|
|
720
|
+
const count = countJsonItems(payload) ?? countLines(text);
|
|
721
|
+
return `• serena ${toolName}${subject ? ` ${subject}` : ""}${meta(formatLineLabel(count, "item"), duration)}`;
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
function registerXtrmUiTools(pi: ExtensionAPI): void {
|
|
727
|
+
const activeToolCalls = new Map<string, string>();
|
|
728
|
+
const activeSignatureCounts = new Map<string, number>();
|
|
729
|
+
const toolCallStartTimes = new Map<string, number>();
|
|
730
|
+
|
|
731
|
+
const trackToolCallStart = (toolCallId: string, toolName: string, args: Record<string, unknown>) => {
|
|
732
|
+
const signature = stableToolSignature(toolName, args);
|
|
733
|
+
activeToolCalls.set(toolCallId, signature);
|
|
734
|
+
activeSignatureCounts.set(signature, (activeSignatureCounts.get(signature) ?? 0) + 1);
|
|
735
|
+
toolCallStartTimes.set(toolCallId, Date.now());
|
|
736
|
+
};
|
|
737
|
+
|
|
738
|
+
const trackToolCallEnd = (toolCallId: string) => {
|
|
739
|
+
const signature = activeToolCalls.get(toolCallId);
|
|
740
|
+
if (!signature) return;
|
|
741
|
+
activeToolCalls.delete(toolCallId);
|
|
742
|
+
const next = (activeSignatureCounts.get(signature) ?? 1) - 1;
|
|
743
|
+
if (next <= 0) activeSignatureCounts.delete(signature);
|
|
744
|
+
else activeSignatureCounts.set(signature, next);
|
|
745
|
+
toolCallStartTimes.delete(toolCallId);
|
|
746
|
+
};
|
|
747
|
+
|
|
748
|
+
const isToolCallActive = (toolName: string, args: Record<string, unknown>) =>
|
|
749
|
+
activeSignatureCounts.has(stableToolSignature(toolName, args));
|
|
750
|
+
|
|
751
|
+
const renderPendingCallIfActive = (toolName: string, args: Record<string, unknown>, theme: any) =>
|
|
752
|
+
isToolCallActive(toolName, args) ? renderPendingCall(toolName, args, theme) : undefined;
|
|
753
|
+
|
|
754
|
+
pi.on("tool_call", async (event) => {
|
|
755
|
+
trackToolCallStart(event.toolCallId, event.toolName, event.input as Record<string, unknown>);
|
|
756
|
+
});
|
|
757
|
+
|
|
758
|
+
pi.on("tool_execution_end", async (event) => {
|
|
759
|
+
trackToolCallEnd(event.toolCallId);
|
|
760
|
+
});
|
|
761
|
+
|
|
762
|
+
pi.on("tool_result", async (event: ToolResultEvent, ctx) => {
|
|
763
|
+
if (!SERENA_COMPACT_TOOLS.has(event.toolName)) return undefined;
|
|
764
|
+
if (ctx.ui.getToolsExpanded()) return undefined;
|
|
765
|
+
if (event.isError) return undefined;
|
|
766
|
+
|
|
767
|
+
const text = getTextContent({ content: event.content as Array<{ type: string; text?: string }> });
|
|
768
|
+
if (!text.trim()) return undefined;
|
|
769
|
+
|
|
770
|
+
const startedAt = toolCallStartTimes.get(event.toolCallId);
|
|
771
|
+
const durationMs = startedAt != null ? Date.now() - startedAt : undefined;
|
|
772
|
+
const compactText = summarizeSerenaToolResult(event.toolName, event.input, text, durationMs);
|
|
773
|
+
|
|
774
|
+
return {
|
|
775
|
+
content: [{ type: "text", text: compactText }],
|
|
776
|
+
details: event.details,
|
|
777
|
+
};
|
|
778
|
+
});
|
|
779
|
+
|
|
780
|
+
pi.registerTool({
|
|
781
|
+
name: "bash",
|
|
782
|
+
label: "bash",
|
|
783
|
+
description: getTools(process.cwd()).bash.description,
|
|
784
|
+
parameters: getTools(process.cwd()).bash.parameters,
|
|
785
|
+
async execute(toolCallId, params, signal, onUpdate, ctx) {
|
|
786
|
+
const started = Date.now();
|
|
787
|
+
const result = await getTools(ctx.cwd).bash.execute(toolCallId, params, signal, onUpdate);
|
|
788
|
+
return { ...result, details: withXtrmMeta(result.details as BashToolDetails | undefined, "bash", params as Record<string, unknown>, Date.now() - started) };
|
|
789
|
+
},
|
|
790
|
+
renderCall: (args, theme) => renderPendingCallIfActive("bash", args as Record<string, unknown>, theme),
|
|
791
|
+
renderResult(result, { expanded, isPartial }, theme) {
|
|
792
|
+
const details = (result.details ?? {}) as DetailsWithXtrmMeta<BashToolDetails, Record<string, unknown>>;
|
|
793
|
+
const meta = getXtrmMeta<BashToolDetails, Record<string, unknown>>(details);
|
|
794
|
+
const command = shortenCommand(String(meta?.args.command ?? ""));
|
|
795
|
+
if (isPartial) {
|
|
796
|
+
return new Text(`${theme.fg("accent", "•")} ${theme.fg("toolTitle", "Running ")}${theme.fg("accent", command)}${theme.fg("toolTitle", " in bash")}`, 0, 0);
|
|
797
|
+
}
|
|
798
|
+
const output = getTextContent(result as any);
|
|
799
|
+
const outputLines = cleanOutputLines(output);
|
|
800
|
+
const exitMatch = output.match(/exit code:\s*(-?\d+)/i);
|
|
801
|
+
const exitCode = exitMatch ? Number.parseInt(exitMatch[1] ?? "0", 10) : 0;
|
|
802
|
+
const bullet = exitCode === 0 ? theme.fg("success", "•") : theme.fg("error", "•");
|
|
803
|
+
const summary = joinMeta([formatLineLabel(outputLines.length, "line"), formatDuration(meta?.durationMs), details.truncation?.truncated ? "truncated" : undefined]);
|
|
804
|
+
let text = `${bullet} ${theme.fg("toolTitle", "Ran ")}${theme.fg("accent", command)}`;
|
|
805
|
+
if (summary) text += theme.fg("dim", ` · ${summary}`);
|
|
806
|
+
if (expanded && outputLines.length > 0) text += `\n${renderVerticalPreview(theme, outputLines, 10)}`;
|
|
807
|
+
return new Text(text, 0, 0);
|
|
808
|
+
},
|
|
809
|
+
});
|
|
810
|
+
|
|
811
|
+
pi.registerTool({
|
|
812
|
+
name: "read",
|
|
813
|
+
label: "read",
|
|
814
|
+
description: getTools(process.cwd()).read.description,
|
|
815
|
+
parameters: getTools(process.cwd()).read.parameters,
|
|
816
|
+
async execute(toolCallId, params, signal, onUpdate, ctx) {
|
|
817
|
+
const started = Date.now();
|
|
818
|
+
const result = await getTools(ctx.cwd).read.execute(toolCallId, params, signal, onUpdate);
|
|
819
|
+
return { ...result, details: withXtrmMeta(result.details as ReadToolDetails | undefined, "read", params as Record<string, unknown>, Date.now() - started) };
|
|
820
|
+
},
|
|
821
|
+
renderCall: (args, theme) => renderPendingCallIfActive("read", args as Record<string, unknown>, theme),
|
|
822
|
+
renderResult(result, { expanded, isPartial }, theme) {
|
|
823
|
+
if (isPartial) return new Text(renderToolSummary(theme, "pending", "read", "loading", undefined), 0, 0);
|
|
824
|
+
const details = (result.details ?? {}) as DetailsWithXtrmMeta<ReadToolDetails, Record<string, unknown>>;
|
|
825
|
+
const meta = getXtrmMeta<ReadToolDetails, Record<string, unknown>>(details);
|
|
826
|
+
const subjectBase = shortenPath(String(meta?.args.path ?? ""));
|
|
827
|
+
const range = lineRange(meta?.args.offset as number | undefined, meta?.args.limit as number | undefined);
|
|
828
|
+
const subject = range ? `${subjectBase}:${range}` : subjectBase;
|
|
829
|
+
const first = result.content[0];
|
|
830
|
+
if (first?.type === "image") {
|
|
831
|
+
return new Text(renderToolSummary(theme, "success", "read", subject, joinMeta(["image", formatDuration(meta?.durationMs)])), 0, 0);
|
|
832
|
+
}
|
|
833
|
+
const textContent = getTextContent(result as any);
|
|
834
|
+
const lines = textContent.split("\n");
|
|
835
|
+
let text = renderToolSummary(theme, "success", "read", subject, joinMeta([formatLineLabel(lines.length, "line"), formatDuration(meta?.durationMs), details.truncation?.truncated ? `from ${details.truncation.totalLines}` : undefined]));
|
|
836
|
+
if (expanded && textContent.length > 0) text += `\n${renderOutputPreview(theme, previewLines(textContent, 14), 14)}`;
|
|
837
|
+
return new Text(text, 0, 0);
|
|
838
|
+
},
|
|
839
|
+
});
|
|
840
|
+
|
|
841
|
+
pi.registerTool({
|
|
842
|
+
name: "edit",
|
|
843
|
+
label: "edit",
|
|
844
|
+
description: getTools(process.cwd()).edit.description,
|
|
845
|
+
parameters: getTools(process.cwd()).edit.parameters,
|
|
846
|
+
async execute(toolCallId, params, signal, onUpdate, ctx) {
|
|
847
|
+
const started = Date.now();
|
|
848
|
+
const result = await getTools(ctx.cwd).edit.execute(toolCallId, params, signal, onUpdate);
|
|
849
|
+
return { ...result, details: withXtrmMeta(result.details as EditToolDetails | undefined, "edit", params as Record<string, unknown>, Date.now() - started) };
|
|
850
|
+
},
|
|
851
|
+
renderCall: (args, theme) => renderPendingCallIfActive("edit", args as Record<string, unknown>, theme),
|
|
852
|
+
renderResult(result, { expanded, isPartial }, theme) {
|
|
853
|
+
if (isPartial) return new Text(renderToolSummary(theme, "pending", "edit", "applying", undefined), 0, 0);
|
|
854
|
+
const details = (result.details ?? {}) as DetailsWithXtrmMeta<EditToolDetails, Record<string, unknown>>;
|
|
855
|
+
const meta = getXtrmMeta<EditToolDetails, Record<string, unknown>>(details);
|
|
856
|
+
const textContent = getTextContent(result as any);
|
|
857
|
+
if (/^error/i.test(textContent.trim())) {
|
|
858
|
+
return new Text(renderToolSummary(theme, "error", "edit", shortenPath(String(meta?.args.path ?? "")), textContent.split("\n")[0]), 0, 0);
|
|
859
|
+
}
|
|
860
|
+
const stats = details.diff ? diffStats(details.diff) : { additions: 0, removals: 0 };
|
|
861
|
+
let text = renderToolSummary(theme, "success", "edit", shortenPath(String(meta?.args.path ?? "")), joinMeta([`+${stats.additions}`, `-${stats.removals}`, formatDuration(meta?.durationMs)]));
|
|
862
|
+
if (expanded && details.diff) text += `\n${renderDiffPreview(theme, details.diff, 18)}`;
|
|
863
|
+
return new Text(text, 0, 0);
|
|
864
|
+
},
|
|
865
|
+
});
|
|
866
|
+
|
|
867
|
+
pi.registerTool({
|
|
868
|
+
name: "write",
|
|
869
|
+
label: "write",
|
|
870
|
+
description: getTools(process.cwd()).write.description,
|
|
871
|
+
parameters: getTools(process.cwd()).write.parameters,
|
|
872
|
+
async execute(toolCallId, params, signal, onUpdate, ctx) {
|
|
873
|
+
const started = Date.now();
|
|
874
|
+
const result = await getTools(ctx.cwd).write.execute(toolCallId, params, signal, onUpdate);
|
|
875
|
+
return { ...result, details: withXtrmMeta(result.details as Record<string, never> | undefined, "write", params as Record<string, unknown>, Date.now() - started) };
|
|
876
|
+
},
|
|
877
|
+
renderCall: (args, theme) => renderPendingCallIfActive("write", args as Record<string, unknown>, theme),
|
|
878
|
+
renderResult(result, { isPartial }, theme) {
|
|
879
|
+
if (isPartial) return new Text(renderToolSummary(theme, "pending", "write", "writing", undefined), 0, 0);
|
|
880
|
+
const details = (result.details ?? {}) as DetailsWithXtrmMeta<Record<string, never>, Record<string, unknown>>;
|
|
881
|
+
const meta = getXtrmMeta<Record<string, never>, Record<string, unknown>>(details);
|
|
882
|
+
const textContent = getTextContent(result as any);
|
|
883
|
+
if (/^error/i.test(textContent.trim())) {
|
|
884
|
+
return new Text(renderToolSummary(theme, "error", "write", shortenPath(String(meta?.args.path ?? "")), textContent.split("\n")[0]), 0, 0);
|
|
885
|
+
}
|
|
886
|
+
return new Text(renderToolSummary(theme, "success", "write", shortenPath(String(meta?.args.path ?? "")), joinMeta([formatLineLabel(lineCount(String(meta?.args.content ?? "")), "line"), formatDuration(meta?.durationMs)])), 0, 0);
|
|
887
|
+
},
|
|
888
|
+
});
|
|
889
|
+
|
|
890
|
+
pi.registerTool({
|
|
891
|
+
name: "find",
|
|
892
|
+
label: "find",
|
|
893
|
+
description: getTools(process.cwd()).find.description,
|
|
894
|
+
parameters: getTools(process.cwd()).find.parameters,
|
|
895
|
+
async execute(toolCallId, params, signal, onUpdate, ctx) {
|
|
896
|
+
const started = Date.now();
|
|
897
|
+
const result = await getTools(ctx.cwd).find.execute(toolCallId, params, signal, onUpdate);
|
|
898
|
+
return { ...result, details: withXtrmMeta(result.details as FindToolDetails | undefined, "find", params as Record<string, unknown>, Date.now() - started) };
|
|
899
|
+
},
|
|
900
|
+
renderCall: (args, theme) => renderPendingCallIfActive("find", args as Record<string, unknown>, theme),
|
|
901
|
+
renderResult(result, { expanded, isPartial }, theme) {
|
|
902
|
+
if (isPartial) return new Text(renderToolSummary(theme, "pending", "find", "searching", undefined), 0, 0);
|
|
903
|
+
const details = (result.details ?? {}) as DetailsWithXtrmMeta<FindToolDetails, Record<string, unknown>>;
|
|
904
|
+
const meta = getXtrmMeta<FindToolDetails, Record<string, unknown>>(details);
|
|
905
|
+
const textContent = getTextContent(result as any);
|
|
906
|
+
const count = summarizeCount(textContent);
|
|
907
|
+
let text = renderToolSummary(theme, "success", "find", String(meta?.args.pattern ?? ""), joinMeta([formatLineLabel(count, "match"), formatDuration(meta?.durationMs), details.resultLimitReached ? "limit reached" : undefined]));
|
|
908
|
+
if (expanded && count > 0) text += `\n${renderOutputPreview(theme, previewLines(textContent, 10), 10)}`;
|
|
909
|
+
return new Text(text, 0, 0);
|
|
910
|
+
},
|
|
911
|
+
});
|
|
912
|
+
|
|
913
|
+
pi.registerTool({
|
|
914
|
+
name: "grep",
|
|
915
|
+
label: "grep",
|
|
916
|
+
description: getTools(process.cwd()).grep.description,
|
|
917
|
+
parameters: getTools(process.cwd()).grep.parameters,
|
|
918
|
+
async execute(toolCallId, params, signal, onUpdate, ctx) {
|
|
919
|
+
const started = Date.now();
|
|
920
|
+
const result = await getTools(ctx.cwd).grep.execute(toolCallId, params, signal, onUpdate);
|
|
921
|
+
return { ...result, details: withXtrmMeta(result.details as GrepToolDetails | undefined, "grep", params as Record<string, unknown>, Date.now() - started) };
|
|
922
|
+
},
|
|
923
|
+
renderCall: (args, theme) => renderPendingCallIfActive("grep", args as Record<string, unknown>, theme),
|
|
924
|
+
renderResult(result, { expanded, isPartial }, theme) {
|
|
925
|
+
if (isPartial) return new Text(renderToolSummary(theme, "pending", "grep", "searching", undefined), 0, 0);
|
|
926
|
+
const details = (result.details ?? {}) as DetailsWithXtrmMeta<GrepToolDetails, Record<string, unknown>>;
|
|
927
|
+
const meta = getXtrmMeta<GrepToolDetails, Record<string, unknown>>(details);
|
|
928
|
+
const textContent = getTextContent(result as any);
|
|
929
|
+
const count = countPrefixedItems(textContent, ["-- "]) || summarizeCount(textContent);
|
|
930
|
+
let text = renderToolSummary(theme, "success", "grep", String(meta?.args.pattern ?? ""), joinMeta([formatLineLabel(count, "match"), formatDuration(meta?.durationMs), details.matchLimitReached ? "limit reached" : undefined]));
|
|
931
|
+
if (expanded && textContent.length > 0) text += `\n${renderOutputPreview(theme, previewLines(textContent, 12), 12)}`;
|
|
932
|
+
return new Text(text, 0, 0);
|
|
933
|
+
},
|
|
934
|
+
});
|
|
935
|
+
|
|
936
|
+
pi.registerTool({
|
|
937
|
+
name: "ls",
|
|
938
|
+
label: "ls",
|
|
939
|
+
description: getTools(process.cwd()).ls.description,
|
|
940
|
+
parameters: getTools(process.cwd()).ls.parameters,
|
|
941
|
+
async execute(toolCallId, params, signal, onUpdate, ctx) {
|
|
942
|
+
const started = Date.now();
|
|
943
|
+
const result = await getTools(ctx.cwd).ls.execute(toolCallId, params, signal, onUpdate);
|
|
944
|
+
return { ...result, details: withXtrmMeta(result.details as LsToolDetails | undefined, "ls", params as Record<string, unknown>, Date.now() - started) };
|
|
945
|
+
},
|
|
946
|
+
renderCall: (args, theme) => renderPendingCallIfActive("ls", args as Record<string, unknown>, theme),
|
|
947
|
+
renderResult(result, { expanded, isPartial }, theme) {
|
|
948
|
+
if (isPartial) return new Text(renderToolSummary(theme, "pending", "ls", "listing", undefined), 0, 0);
|
|
949
|
+
const details = (result.details ?? {}) as DetailsWithXtrmMeta<LsToolDetails, Record<string, unknown>>;
|
|
950
|
+
const meta = getXtrmMeta<LsToolDetails, Record<string, unknown>>(details);
|
|
951
|
+
const textContent = getTextContent(result as any);
|
|
952
|
+
const count = summarizeCount(textContent);
|
|
953
|
+
let text = renderToolSummary(theme, "success", "ls", shortenPath(String(meta?.args.path ?? ".")), joinMeta([formatLineLabel(count, "entry"), formatDuration(meta?.durationMs), details.entryLimitReached ? "limit reached" : undefined]));
|
|
954
|
+
if (expanded && count > 0) text += `\n${renderOutputPreview(theme, previewLines(textContent, 12), 12)}`;
|
|
955
|
+
return new Text(text, 0, 0);
|
|
956
|
+
},
|
|
957
|
+
});
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
// ============================================================================
|
|
961
|
+
// Main Extension
|
|
962
|
+
// ============================================================================
|
|
963
|
+
|
|
964
|
+
function isXtrmTheme(name: string | undefined): boolean {
|
|
965
|
+
return name === "pidex-dark" || name === "pidex-light";
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
export default function xtrmUiExtension(pi: ExtensionAPI): void {
|
|
969
|
+
let prefs: XtrmUiPrefs = { ...DEFAULT_PREFS };
|
|
970
|
+
let previousThemeName: string | null = null;
|
|
971
|
+
|
|
972
|
+
const getPrefs = () => prefs;
|
|
973
|
+
const setPrefs = (p: XtrmUiPrefs) => { prefs = p; };
|
|
974
|
+
const getThinkingLevel = () => formatThinking(pi.getThinkingLevel());
|
|
975
|
+
|
|
976
|
+
registerXtrmUiTools(pi);
|
|
977
|
+
registerCommands(pi, getPrefs, setPrefs, getThinkingLevel);
|
|
978
|
+
|
|
979
|
+
const refresh = (ctx: ExtensionContext) => {
|
|
980
|
+
applyXtrmChrome(ctx, prefs, getThinkingLevel);
|
|
981
|
+
};
|
|
982
|
+
|
|
983
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
984
|
+
prefs = loadPrefs(ctx.sessionManager.getEntries() as Array<MaybeCustomEntry>);
|
|
985
|
+
if (!previousThemeName && !isXtrmTheme(ctx.ui.theme.name)) {
|
|
986
|
+
previousThemeName = ctx.ui.theme.name ?? null;
|
|
987
|
+
}
|
|
988
|
+
refresh(ctx);
|
|
989
|
+
});
|
|
990
|
+
|
|
991
|
+
pi.on("session_switch", async (_event, ctx) => {
|
|
992
|
+
if (!previousThemeName && !isXtrmTheme(ctx.ui.theme.name)) {
|
|
993
|
+
previousThemeName = ctx.ui.theme.name ?? null;
|
|
994
|
+
}
|
|
995
|
+
refresh(ctx);
|
|
996
|
+
});
|
|
997
|
+
|
|
998
|
+
pi.on("session_fork", async (_event, ctx) => {
|
|
999
|
+
if (!previousThemeName && !isXtrmTheme(ctx.ui.theme.name)) {
|
|
1000
|
+
previousThemeName = ctx.ui.theme.name ?? null;
|
|
1001
|
+
}
|
|
1002
|
+
refresh(ctx);
|
|
1003
|
+
});
|
|
1004
|
+
|
|
1005
|
+
pi.on("model_select", async (_event, ctx) => {
|
|
1006
|
+
refresh(ctx);
|
|
1007
|
+
});
|
|
1008
|
+
|
|
1009
|
+
pi.on("session_shutdown", async (_event, ctx) => {
|
|
1010
|
+
if (previousThemeName) {
|
|
1011
|
+
ctx.ui.setTheme(previousThemeName);
|
|
1012
|
+
}
|
|
1013
|
+
});
|
|
1014
|
+
|
|
1015
|
+
pi.on("input", async (event) => {
|
|
1016
|
+
if (event.source === "extension") return { action: "continue" as const };
|
|
1017
|
+
if (!event.text.trim()) return { action: "continue" as const };
|
|
1018
|
+
if (event.text.startsWith("/") || event.text.startsWith("!")) return { action: "continue" as const };
|
|
1019
|
+
if (event.text.startsWith("› ")) return { action: "continue" as const };
|
|
1020
|
+
return event.images
|
|
1021
|
+
? { action: "transform" as const, text: `› ${event.text}`, images: event.images }
|
|
1022
|
+
: { action: "transform" as const, text: `› ${event.text}` };
|
|
1023
|
+
});
|
|
1024
|
+
|
|
1025
|
+
pi.on("context", async (event) => {
|
|
1026
|
+
const messages = event.messages.map((message) => {
|
|
1027
|
+
if (message.role === "user" && typeof message.content === "string" && message.content.startsWith("› ")) {
|
|
1028
|
+
return { ...message, content: message.content.slice(2) };
|
|
1029
|
+
}
|
|
1030
|
+
if (message.role === "user" && Array.isArray(message.content)) {
|
|
1031
|
+
return {
|
|
1032
|
+
...message,
|
|
1033
|
+
content: message.content.map((item, index) =>
|
|
1034
|
+
index === 0 && item.type === "text" && item.text.startsWith("› ")
|
|
1035
|
+
? { ...item, text: item.text.slice(2) }
|
|
1036
|
+
: item
|
|
1037
|
+
),
|
|
1038
|
+
};
|
|
1039
|
+
}
|
|
1040
|
+
return message;
|
|
1041
|
+
});
|
|
1042
|
+
return { messages };
|
|
1043
|
+
});
|
|
1044
|
+
}
|