@jaggerxtrm/pi-extensions 0.7.16 → 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/README.md +7 -0
- package/extensions/README.md +12 -0
- package/extensions/sp-terminal-overlay/index.ts +495 -0
- package/extensions/sp-terminal-overlay/package.json +15 -0
- package/extensions/xtrm-ui/format.ts +7 -2
- package/extensions/xtrm-ui/index.ts +511 -10
- package/package.json +1 -1
- package/src/extensions/sp-terminal-overlay.ts +3 -0
- package/src/registry.ts +2 -0
package/README.md
CHANGED
|
@@ -41,3 +41,10 @@ Pi discovers this package through:
|
|
|
41
41
|
- `pi.extensions: ["./src/index.ts"]`
|
|
42
42
|
|
|
43
43
|
After install, keep `.pi/settings.json` package wiring pointed at `npm:@jaggerxtrm/pi-extensions`.
|
|
44
|
+
|
|
45
|
+
## Managed extensions
|
|
46
|
+
|
|
47
|
+
Notable bundled extensions include:
|
|
48
|
+
|
|
49
|
+
- `xtrm-ui` — XTRM Pi chrome, native tool summaries, selectable external tool chrome (`/xtrm-ui chrome background|box`).
|
|
50
|
+
- `sp-terminal-overlay` — `/sp-feed`, `/sp-ps`, and `/xtrm-terminal` streaming overlays for specialist monitoring.
|
package/extensions/README.md
CHANGED
|
@@ -3,3 +3,15 @@
|
|
|
3
3
|
This directory is the canonical source for managed Pi extension entrypoints.
|
|
4
4
|
|
|
5
5
|
Runtime delivery is package-based via `npm:@jaggerxtrm/pi-extensions`.
|
|
6
|
+
|
|
7
|
+
## sp-terminal-overlay
|
|
8
|
+
|
|
9
|
+
Streaming terminal-style overlay for specialist/process monitoring commands.
|
|
10
|
+
|
|
11
|
+
Commands:
|
|
12
|
+
|
|
13
|
+
- `/sp-feed [args]` — opens `sp feed -f [args]` in an overlay.
|
|
14
|
+
- `/sp-ps [args]` / `/xtrm-ps [args]` — opens `sp ps [args]` (defaults to `sp ps --follow`) in an overlay.
|
|
15
|
+
- `/xtrm-terminal <command>` — opens an arbitrary shell command in an overlay.
|
|
16
|
+
|
|
17
|
+
Keys: `Esc`/`q` close, `r` restart, arrows/page keys scroll.
|
|
@@ -0,0 +1,495 @@
|
|
|
1
|
+
import { spawn, type ChildProcessWithoutNullStreams } from "node:child_process";
|
|
2
|
+
import type { ExtensionAPI, ExtensionCommandContext, Theme } from "@mariozechner/pi-coding-agent";
|
|
3
|
+
import { matchesKey, truncateToWidth, visibleWidth } from "@mariozechner/pi-tui";
|
|
4
|
+
|
|
5
|
+
const MAX_BUFFER_LINES = 2000;
|
|
6
|
+
const DEFAULT_VISIBLE_LINES = 24;
|
|
7
|
+
const RENDER_THROTTLE_MS = 100;
|
|
8
|
+
const ANSI_SGR_PATTERN = /^\x1b\[[0-9;]*m$/u;
|
|
9
|
+
const DISALLOWED_SGR_CODES = new Set([5, 6, 8]);
|
|
10
|
+
|
|
11
|
+
function padVisible(text: string, width: number): string {
|
|
12
|
+
return text + " ".repeat(Math.max(0, width - visibleWidth(text)));
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function resetAnsi(text: string): string {
|
|
16
|
+
return text.includes("\x1b") ? `${text}\x1b[0m` : text;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function shellQuote(value: string): string {
|
|
20
|
+
return `'${value.replace(/'/g, `'"'"'`)}'`;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function resolveSpFeedCommand(args: string): string {
|
|
24
|
+
const trimmed = args.trim();
|
|
25
|
+
return trimmed ? `sp feed -f ${trimmed}` : "sp feed -f";
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function resolveSpPsCommand(args: string): string {
|
|
29
|
+
const trimmed = args.trim();
|
|
30
|
+
return trimmed ? `sp ps ${trimmed}` : "sp ps --follow";
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function openTerminalOverlay(ctx: ExtensionCommandContext, title: string, command: string): Promise<void> {
|
|
34
|
+
return ctx.ui.custom<void>(
|
|
35
|
+
(tui, theme, _keybindings, done) => {
|
|
36
|
+
const terminal = new StreamingTerminalOverlay({
|
|
37
|
+
title,
|
|
38
|
+
command,
|
|
39
|
+
cwd: process.cwd(),
|
|
40
|
+
theme,
|
|
41
|
+
requestRender: () => {
|
|
42
|
+
tui.requestRender();
|
|
43
|
+
},
|
|
44
|
+
close: () => {
|
|
45
|
+
terminal.dispose();
|
|
46
|
+
done(undefined);
|
|
47
|
+
},
|
|
48
|
+
});
|
|
49
|
+
return terminal;
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
overlay: true,
|
|
53
|
+
overlayOptions: {
|
|
54
|
+
anchor: "center",
|
|
55
|
+
width: "80%",
|
|
56
|
+
minWidth: 72,
|
|
57
|
+
maxHeight: "80%",
|
|
58
|
+
margin: 1,
|
|
59
|
+
},
|
|
60
|
+
} as Parameters<ExtensionCommandContext["ui"]["custom"]>[1],
|
|
61
|
+
).then(() => undefined);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
type StreamingTerminalOverlayOptions = {
|
|
65
|
+
title: string;
|
|
66
|
+
command: string;
|
|
67
|
+
cwd: string;
|
|
68
|
+
theme: Theme;
|
|
69
|
+
requestRender: () => void;
|
|
70
|
+
close: () => void;
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
class StreamingTerminalOverlay {
|
|
74
|
+
private child: ChildProcessWithoutNullStreams | undefined;
|
|
75
|
+
private lines: string[] = [];
|
|
76
|
+
private currentLine = "";
|
|
77
|
+
private screenLines: string[] = [];
|
|
78
|
+
private cursorRow = 0;
|
|
79
|
+
private cursorCol = 0;
|
|
80
|
+
private terminalMode = false;
|
|
81
|
+
private scrollOffset = 0;
|
|
82
|
+
private status = "starting";
|
|
83
|
+
private closed = false;
|
|
84
|
+
private renderTimer: ReturnType<typeof setTimeout> | undefined;
|
|
85
|
+
private lastRenderAt = 0;
|
|
86
|
+
|
|
87
|
+
constructor(private readonly options: StreamingTerminalOverlayOptions) {
|
|
88
|
+
this.start();
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
handleInput(data: string): void {
|
|
92
|
+
if (matchesKey(data, "escape") || matchesKey(data, "q") || matchesKey(data, "ctrl+c")) {
|
|
93
|
+
this.options.close();
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
if (matchesKey(data, "r")) {
|
|
97
|
+
this.restart();
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
if (matchesKey(data, "up")) {
|
|
101
|
+
this.scrollOffset = Math.min(this.scrollOffset + 1, Math.max(0, this.renderSourceLines().length - 1));
|
|
102
|
+
this.options.requestRender();
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
if (matchesKey(data, "down")) {
|
|
106
|
+
this.scrollOffset = Math.max(0, this.scrollOffset - 1);
|
|
107
|
+
this.options.requestRender();
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
if (matchesKey(data, "pageup")) {
|
|
111
|
+
this.scrollOffset = Math.min(this.scrollOffset + DEFAULT_VISIBLE_LINES, Math.max(0, this.renderSourceLines().length - 1));
|
|
112
|
+
this.options.requestRender();
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
if (matchesKey(data, "pagedown")) {
|
|
116
|
+
this.scrollOffset = Math.max(0, this.scrollOffset - DEFAULT_VISIBLE_LINES);
|
|
117
|
+
this.options.requestRender();
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
if (matchesKey(data, "home")) {
|
|
121
|
+
this.scrollOffset = Math.max(0, this.renderSourceLines().length - 1);
|
|
122
|
+
this.options.requestRender();
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
if (matchesKey(data, "end")) {
|
|
126
|
+
this.scrollOffset = 0;
|
|
127
|
+
this.options.requestRender();
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
render(width: number): string[] {
|
|
132
|
+
const theme = this.options.theme;
|
|
133
|
+
const overlayWidth = Math.max(40, width);
|
|
134
|
+
const innerWidth = overlayWidth - 2;
|
|
135
|
+
const contentWidth = Math.max(1, innerWidth - 2);
|
|
136
|
+
const allLines = this.renderSourceLines();
|
|
137
|
+
const visibleCount = DEFAULT_VISIBLE_LINES;
|
|
138
|
+
const end = Math.max(0, allLines.length - this.scrollOffset);
|
|
139
|
+
const start = Math.max(0, end - visibleCount);
|
|
140
|
+
const visible = allLines.slice(start, end);
|
|
141
|
+
const hiddenAbove = start;
|
|
142
|
+
const hiddenBelow = Math.max(0, allLines.length - end);
|
|
143
|
+
|
|
144
|
+
const border = (text: string) => theme.fg("border", text);
|
|
145
|
+
const title = ` ${theme.fg("accent", theme.bold(this.options.title))} ${theme.fg("dim", this.status)} `;
|
|
146
|
+
const top = border("╭") + truncateToWidth(title, Math.max(0, innerWidth), "") + border("─".repeat(Math.max(0, innerWidth - visibleWidth(title)))) + border("╮");
|
|
147
|
+
const row = (content: string) => {
|
|
148
|
+
const truncated = resetAnsi(truncateToWidth(content, contentWidth));
|
|
149
|
+
return `${border("│")} ${padVisible(truncated, contentWidth)} ${border("│")}`;
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
const output = [top];
|
|
153
|
+
output.push(row(theme.fg("dim", `$ ${this.options.command}`)));
|
|
154
|
+
output.push(row(theme.fg("dim", "Esc/q close • r restart • ↑↓ scroll • PgUp/PgDn page")));
|
|
155
|
+
output.push(row(""));
|
|
156
|
+
const bodyRows: string[] = [];
|
|
157
|
+
if (hiddenAbove > 0) bodyRows.push(theme.fg("dim", `… ${hiddenAbove} lines above`));
|
|
158
|
+
bodyRows.push(...visible);
|
|
159
|
+
if (visible.length === 0) bodyRows.push(theme.fg("dim", "waiting for output…"));
|
|
160
|
+
if (hiddenBelow > 0) bodyRows.push(theme.fg("dim", `… ${hiddenBelow} lines below`));
|
|
161
|
+
|
|
162
|
+
for (let index = 0; index < visibleCount; index++) {
|
|
163
|
+
output.push(row(bodyRows[index] ?? ""));
|
|
164
|
+
}
|
|
165
|
+
output.push(border("╰" + "─".repeat(innerWidth) + "╯"));
|
|
166
|
+
return output;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
invalidate(): void {}
|
|
170
|
+
|
|
171
|
+
dispose(): void {
|
|
172
|
+
this.closed = true;
|
|
173
|
+
if (this.renderTimer) clearTimeout(this.renderTimer);
|
|
174
|
+
this.renderTimer = undefined;
|
|
175
|
+
this.stop();
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
private start(): void {
|
|
179
|
+
this.stop();
|
|
180
|
+
this.status = "running";
|
|
181
|
+
this.lines = [];
|
|
182
|
+
this.currentLine = "";
|
|
183
|
+
this.screenLines = [];
|
|
184
|
+
this.cursorRow = 0;
|
|
185
|
+
this.cursorCol = 0;
|
|
186
|
+
this.terminalMode = false;
|
|
187
|
+
this.scrollOffset = 0;
|
|
188
|
+
|
|
189
|
+
const shell = process.env.SHELL || "/bin/sh";
|
|
190
|
+
this.child = spawn(shell, ["-lc", this.options.command], {
|
|
191
|
+
cwd: this.options.cwd,
|
|
192
|
+
env: {
|
|
193
|
+
...process.env,
|
|
194
|
+
FORCE_COLOR: process.env.FORCE_COLOR ?? "1",
|
|
195
|
+
TERM: process.env.TERM ?? "xterm-256color",
|
|
196
|
+
COLUMNS: process.env.COLUMNS ?? "120",
|
|
197
|
+
LINES: process.env.LINES ?? "40",
|
|
198
|
+
},
|
|
199
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
this.child.stdout.on("data", (chunk) => this.append(String(chunk)));
|
|
203
|
+
this.child.stderr.on("data", (chunk) => this.append(String(chunk)));
|
|
204
|
+
this.child.on("error", (error) => {
|
|
205
|
+
this.status = "error";
|
|
206
|
+
this.append(`\n[error] ${error.message}\n`);
|
|
207
|
+
});
|
|
208
|
+
this.child.on("close", (code, signal) => {
|
|
209
|
+
this.flushCurrentLine();
|
|
210
|
+
this.status = signal ? `stopped (${signal})` : `exited ${code ?? "unknown"}`;
|
|
211
|
+
this.requestRenderSoon(true);
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
this.requestRenderSoon(true);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
private restart(): void {
|
|
218
|
+
this.start();
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
private stop(): void {
|
|
222
|
+
const child = this.child;
|
|
223
|
+
this.child = undefined;
|
|
224
|
+
if (child && !child.killed) {
|
|
225
|
+
child.kill("SIGTERM");
|
|
226
|
+
setTimeout(() => {
|
|
227
|
+
if (!child.killed) child.kill("SIGKILL");
|
|
228
|
+
}, 750).unref?.();
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
private requestRenderSoon(immediate = false): void {
|
|
233
|
+
if (this.closed) return;
|
|
234
|
+
if (immediate) {
|
|
235
|
+
if (this.renderTimer) clearTimeout(this.renderTimer);
|
|
236
|
+
this.renderTimer = undefined;
|
|
237
|
+
this.lastRenderAt = Date.now();
|
|
238
|
+
this.options.requestRender();
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const now = Date.now();
|
|
243
|
+
const elapsed = now - this.lastRenderAt;
|
|
244
|
+
if (elapsed >= RENDER_THROTTLE_MS) {
|
|
245
|
+
this.lastRenderAt = now;
|
|
246
|
+
this.options.requestRender();
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
if (this.renderTimer) return;
|
|
251
|
+
this.renderTimer = setTimeout(() => {
|
|
252
|
+
this.renderTimer = undefined;
|
|
253
|
+
this.lastRenderAt = Date.now();
|
|
254
|
+
this.options.requestRender();
|
|
255
|
+
}, RENDER_THROTTLE_MS - elapsed);
|
|
256
|
+
this.renderTimer.unref?.();
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
private renderSourceLines(): string[] {
|
|
260
|
+
if (this.terminalMode) {
|
|
261
|
+
return this.screenLines.map((line) => line.replace(/\s+$/u, ""));
|
|
262
|
+
}
|
|
263
|
+
return this.currentLine ? [...this.lines, this.currentLine] : this.lines;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
private append(text: string): void {
|
|
267
|
+
if (this.closed) return;
|
|
268
|
+
for (let index = 0; index < text.length; index++) {
|
|
269
|
+
const char = text[index]!;
|
|
270
|
+
if (char === "\x1b") {
|
|
271
|
+
const sgrMatch = text.slice(index).match(/^\x1b\[[0-9;]*m/u);
|
|
272
|
+
if (sgrMatch?.[0]) {
|
|
273
|
+
this.appendSafeSgr(sgrMatch[0]);
|
|
274
|
+
index += sgrMatch[0].length - 1;
|
|
275
|
+
continue;
|
|
276
|
+
}
|
|
277
|
+
const nextIndex = this.consumeEscape(text, index);
|
|
278
|
+
if (nextIndex !== index) {
|
|
279
|
+
index = nextIndex;
|
|
280
|
+
continue;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
this.appendChar(char);
|
|
284
|
+
}
|
|
285
|
+
this.trimBuffer();
|
|
286
|
+
this.requestRenderSoon();
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
private appendSafeSgr(sequence: string): void {
|
|
290
|
+
if (!ANSI_SGR_PATTERN.test(sequence)) return;
|
|
291
|
+
const params = sequence.slice(2, -1).split(";").filter(Boolean).map((part) => Number.parseInt(part, 10));
|
|
292
|
+
if (params.some((param) => Number.isNaN(param) || DISALLOWED_SGR_CODES.has(param))) return;
|
|
293
|
+
|
|
294
|
+
// Cursor-addressed dashboards need cell-aware mutation. Keeping raw SGR inside
|
|
295
|
+
// screenLines makes cursorCol slicing unsafe, so preserve colors only for
|
|
296
|
+
// append-only feed output.
|
|
297
|
+
if (this.terminalMode) return;
|
|
298
|
+
this.currentLine += sequence;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
private appendChar(char: string): void {
|
|
302
|
+
if (char === "\r") {
|
|
303
|
+
if (this.terminalMode) this.cursorCol = 0;
|
|
304
|
+
else this.currentLine = "";
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
if (char === "\n") {
|
|
308
|
+
if (this.terminalMode) {
|
|
309
|
+
this.cursorRow++;
|
|
310
|
+
this.cursorCol = 0;
|
|
311
|
+
this.ensureScreenLine(this.cursorRow);
|
|
312
|
+
} else {
|
|
313
|
+
this.flushCurrentLine();
|
|
314
|
+
}
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
if (char === "\b" || char === "\x7f") {
|
|
318
|
+
if (this.terminalMode) this.cursorCol = Math.max(0, this.cursorCol - 1);
|
|
319
|
+
else this.currentLine = this.currentLine.slice(0, -1);
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
if (char < " " && char !== "\t") return;
|
|
323
|
+
|
|
324
|
+
if (this.terminalMode) {
|
|
325
|
+
this.writeScreenChar(char === "\t" ? " " : char);
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
this.currentLine += char;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
private consumeEscape(text: string, start: number): number {
|
|
332
|
+
const introducer = text[start + 1];
|
|
333
|
+
if (introducer === "[") {
|
|
334
|
+
let end = start + 2;
|
|
335
|
+
while (end < text.length && !/[A-Za-z~]/.test(text[end]!)) end++;
|
|
336
|
+
if (end >= text.length) return start;
|
|
337
|
+
this.handleCsi(text.slice(start + 2, end), text[end]!);
|
|
338
|
+
return end;
|
|
339
|
+
}
|
|
340
|
+
if (introducer === "]") {
|
|
341
|
+
let end = start + 2;
|
|
342
|
+
while (end < text.length) {
|
|
343
|
+
if (text[end] === "\x07") return end;
|
|
344
|
+
if (text[end] === "\x1b" && text[end + 1] === "\\") return end + 1;
|
|
345
|
+
end++;
|
|
346
|
+
}
|
|
347
|
+
return start;
|
|
348
|
+
}
|
|
349
|
+
if (introducer) return start + 1;
|
|
350
|
+
return start;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
private handleCsi(params: string, final: string): void {
|
|
354
|
+
const numbers = params
|
|
355
|
+
.replace(/[?>!]/g, "")
|
|
356
|
+
.split(";")
|
|
357
|
+
.map((part) => Number.parseInt(part || "0", 10));
|
|
358
|
+
const first = numbers[0] ?? 0;
|
|
359
|
+
|
|
360
|
+
if (final === "m") return;
|
|
361
|
+
if (final === "H" || final === "f") {
|
|
362
|
+
this.enterTerminalMode();
|
|
363
|
+
this.cursorRow = Math.max(0, (numbers[0] || 1) - 1);
|
|
364
|
+
this.cursorCol = Math.max(0, (numbers[1] || 1) - 1);
|
|
365
|
+
this.ensureScreenLine(this.cursorRow);
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
if (final === "J") {
|
|
369
|
+
this.enterTerminalMode();
|
|
370
|
+
if (first === 2 || first === 3) {
|
|
371
|
+
this.screenLines = [];
|
|
372
|
+
this.cursorRow = 0;
|
|
373
|
+
this.cursorCol = 0;
|
|
374
|
+
this.ensureScreenLine(0);
|
|
375
|
+
} else if (first === 0) {
|
|
376
|
+
this.screenLines = this.screenLines.slice(0, this.cursorRow + 1);
|
|
377
|
+
this.clearScreenLineFromCursor();
|
|
378
|
+
}
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
381
|
+
if (final === "K") {
|
|
382
|
+
this.enterTerminalMode();
|
|
383
|
+
if (first === 2) this.screenLines[this.cursorRow] = "";
|
|
384
|
+
else this.clearScreenLineFromCursor();
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
if (final === "A") {
|
|
388
|
+
this.enterTerminalMode();
|
|
389
|
+
this.cursorRow = Math.max(0, this.cursorRow - Math.max(1, first));
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
392
|
+
if (final === "B") {
|
|
393
|
+
this.enterTerminalMode();
|
|
394
|
+
this.cursorRow += Math.max(1, first);
|
|
395
|
+
this.ensureScreenLine(this.cursorRow);
|
|
396
|
+
return;
|
|
397
|
+
}
|
|
398
|
+
if (final === "C") {
|
|
399
|
+
this.enterTerminalMode();
|
|
400
|
+
this.cursorCol += Math.max(1, first);
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
if (final === "D") {
|
|
404
|
+
this.enterTerminalMode();
|
|
405
|
+
this.cursorCol = Math.max(0, this.cursorCol - Math.max(1, first));
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
private enterTerminalMode(): void {
|
|
410
|
+
if (this.terminalMode) return;
|
|
411
|
+
this.terminalMode = true;
|
|
412
|
+
this.screenLines = this.currentLine ? [...this.lines, this.currentLine] : [...this.lines];
|
|
413
|
+
if (this.screenLines.length === 0) this.screenLines.push("");
|
|
414
|
+
this.cursorRow = Math.max(0, this.screenLines.length - 1);
|
|
415
|
+
this.cursorCol = visibleWidth(this.screenLines[this.cursorRow] ?? "");
|
|
416
|
+
this.currentLine = "";
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
private ensureScreenLine(row: number): void {
|
|
420
|
+
while (this.screenLines.length <= row) this.screenLines.push("");
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
private writeScreenChar(char: string): void {
|
|
424
|
+
this.ensureScreenLine(this.cursorRow);
|
|
425
|
+
const line = this.screenLines[this.cursorRow] ?? "";
|
|
426
|
+
const padded = line.length < this.cursorCol ? line + " ".repeat(this.cursorCol - line.length) : line;
|
|
427
|
+
this.screenLines[this.cursorRow] = padded.slice(0, this.cursorCol) + char + padded.slice(this.cursorCol + 1);
|
|
428
|
+
this.cursorCol++;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
private clearScreenLineFromCursor(): void {
|
|
432
|
+
this.ensureScreenLine(this.cursorRow);
|
|
433
|
+
this.screenLines[this.cursorRow] = (this.screenLines[this.cursorRow] ?? "").slice(0, this.cursorCol);
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
private flushCurrentLine(): void {
|
|
437
|
+
if (this.terminalMode) return;
|
|
438
|
+
this.lines.push(this.currentLine);
|
|
439
|
+
this.currentLine = "";
|
|
440
|
+
this.trimBuffer();
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
private trimBuffer(): void {
|
|
444
|
+
if (this.lines.length > MAX_BUFFER_LINES) this.lines.splice(0, this.lines.length - MAX_BUFFER_LINES);
|
|
445
|
+
if (this.screenLines.length > MAX_BUFFER_LINES) this.screenLines.splice(0, this.screenLines.length - MAX_BUFFER_LINES);
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
export default function spTerminalOverlayExtension(pi: ExtensionAPI): void {
|
|
450
|
+
pi.registerCommand("sp-feed", {
|
|
451
|
+
description: "Open a streaming overlay for `sp feed -f`",
|
|
452
|
+
handler: async (args, ctx) => {
|
|
453
|
+
await openTerminalOverlay(ctx, "sp feed", resolveSpFeedCommand(args));
|
|
454
|
+
},
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
pi.registerCommand("sp-ps", {
|
|
458
|
+
description: "Open a streaming overlay for `sp ps --follow`",
|
|
459
|
+
handler: async (args, ctx) => {
|
|
460
|
+
await openTerminalOverlay(ctx, "sp ps", resolveSpPsCommand(args));
|
|
461
|
+
},
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
pi.registerCommand("xtrm-ps", {
|
|
465
|
+
description: "Alias for /sp-ps",
|
|
466
|
+
handler: async (args, ctx) => {
|
|
467
|
+
await openTerminalOverlay(ctx, "sp ps", resolveSpPsCommand(args));
|
|
468
|
+
},
|
|
469
|
+
});
|
|
470
|
+
|
|
471
|
+
pi.registerCommand("xtrm-terminal", {
|
|
472
|
+
description: "Open a streaming terminal overlay for an arbitrary shell command",
|
|
473
|
+
handler: async (args, ctx) => {
|
|
474
|
+
const command = args.trim();
|
|
475
|
+
if (!command) {
|
|
476
|
+
ctx.ui.notify("Usage: /xtrm-terminal <command>", "warning");
|
|
477
|
+
return;
|
|
478
|
+
}
|
|
479
|
+
await openTerminalOverlay(ctx, command, command);
|
|
480
|
+
},
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
pi.registerCommand("xtrm-terminal-file", {
|
|
484
|
+
description: "Open a streaming overlay for a command with one shell-quoted file/path argument",
|
|
485
|
+
handler: async (args, ctx) => {
|
|
486
|
+
const [commandName, ...rest] = args.trim().split(/\s+/u);
|
|
487
|
+
const fileArg = rest.join(" ");
|
|
488
|
+
if (!commandName || !fileArg) {
|
|
489
|
+
ctx.ui.notify("Usage: /xtrm-terminal-file <command> <path>", "warning");
|
|
490
|
+
return;
|
|
491
|
+
}
|
|
492
|
+
await openTerminalOverlay(ctx, commandName, `${commandName} ${shellQuote(fileArg)}`);
|
|
493
|
+
},
|
|
494
|
+
});
|
|
495
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@xtrm/pi-sp-terminal-overlay",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "XTRM Pi overlay for streaming sp feed and terminal command output",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"exports": {
|
|
7
|
+
".": "./index.ts"
|
|
8
|
+
},
|
|
9
|
+
"license": "MIT",
|
|
10
|
+
"pi": {
|
|
11
|
+
"extensions": [
|
|
12
|
+
"./index.ts"
|
|
13
|
+
]
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -258,6 +258,9 @@ export function formatLineLabel(count: number, noun: string): string {
|
|
|
258
258
|
return `${count} ${noun}${count === 1 ? "" : "s"}`;
|
|
259
259
|
}
|
|
260
260
|
|
|
261
|
+
const TOOL_SUMMARY_SUBJECT_MAX = 34;
|
|
262
|
+
const TOOL_SUMMARY_META_MAX = 34;
|
|
263
|
+
|
|
261
264
|
export function renderToolSummary(
|
|
262
265
|
theme: { fg(color: string, text: string): string; bold(text: string): string },
|
|
263
266
|
status: "pending" | "success" | "error" | "muted",
|
|
@@ -270,9 +273,11 @@ export function renderToolSummary(
|
|
|
270
273
|
: status === "error" ? "error"
|
|
271
274
|
: status === "success" ? "success"
|
|
272
275
|
: "muted";
|
|
276
|
+
const compactSubject = subject ? shortenCommand(subject, TOOL_SUMMARY_SUBJECT_MAX) : undefined;
|
|
277
|
+
const compactMeta = meta ? shortenCommand(meta, TOOL_SUMMARY_META_MAX) : undefined;
|
|
273
278
|
let text = `${theme.fg(color, "•")} ${theme.fg("toolTitle", theme.bold(label))}`;
|
|
274
|
-
if (
|
|
275
|
-
if (
|
|
279
|
+
if (compactSubject) text += ` ${theme.fg("accent", compactSubject)}`;
|
|
280
|
+
if (compactMeta) text += theme.fg("muted", ` · ${compactMeta}`);
|
|
276
281
|
return text;
|
|
277
282
|
}
|
|
278
283
|
|
|
@@ -32,8 +32,9 @@ 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";
|
|
36
|
-
import { basename, join } from "node:path";
|
|
35
|
+
import { existsSync, readFileSync, realpathSync, writeFileSync } from "node:fs";
|
|
36
|
+
import { basename, dirname, join } from "node:path";
|
|
37
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
37
38
|
import {
|
|
38
39
|
cleanOutputLines,
|
|
39
40
|
countPrefixedItems,
|
|
@@ -56,6 +57,7 @@ import {
|
|
|
56
57
|
|
|
57
58
|
export type XtrmThemeName = "pidex-dark" | "pidex-light" | "pidex-dark-flattools" | "pidex-light-flattools";
|
|
58
59
|
export type XtrmDensity = "compact" | "comfortable";
|
|
60
|
+
export type XtrmExternalToolChrome = "background" | "box";
|
|
59
61
|
|
|
60
62
|
export interface XtrmUiPrefs {
|
|
61
63
|
themeName: XtrmThemeName;
|
|
@@ -66,6 +68,8 @@ export interface XtrmUiPrefs {
|
|
|
66
68
|
forceTheme: boolean; // When false, skip setTheme (allow external theme override)
|
|
67
69
|
toolRowBg: boolean; // Subtle background behind tool text rows (no padding)
|
|
68
70
|
compactExternalToolResults: boolean; // Compact extension tool results (disables full expand output)
|
|
71
|
+
externalToolChrome: XtrmExternalToolChrome; // Visual treatment for non-native tool rows
|
|
72
|
+
hideThinkingPlaceholder: boolean; // When false, hidden thinking blocks render no placeholder text
|
|
69
73
|
}
|
|
70
74
|
|
|
71
75
|
// ============================================================================
|
|
@@ -83,8 +87,17 @@ export const DEFAULT_PREFS: XtrmUiPrefs = {
|
|
|
83
87
|
forceTheme: true,
|
|
84
88
|
toolRowBg: false,
|
|
85
89
|
compactExternalToolResults: true,
|
|
90
|
+
externalToolChrome: "background",
|
|
91
|
+
hideThinkingPlaceholder: false,
|
|
86
92
|
};
|
|
87
93
|
|
|
94
|
+
let activeExternalToolChrome: XtrmExternalToolChrome = DEFAULT_PREFS.externalToolChrome;
|
|
95
|
+
|
|
96
|
+
function setActiveExternalToolChrome(chrome: XtrmExternalToolChrome): void {
|
|
97
|
+
activeExternalToolChrome = chrome;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
|
|
88
101
|
// ============================================================================
|
|
89
102
|
// Preferences
|
|
90
103
|
// ============================================================================
|
|
@@ -108,6 +121,8 @@ function normalizePrefs(input: unknown): XtrmUiPrefs {
|
|
|
108
121
|
toolRowBg: source.toolRowBg ?? DEFAULT_PREFS.toolRowBg,
|
|
109
122
|
compactExternalToolResults:
|
|
110
123
|
source.compactExternalToolResults ?? DEFAULT_PREFS.compactExternalToolResults,
|
|
124
|
+
externalToolChrome: source.externalToolChrome === "box" ? "box" : "background",
|
|
125
|
+
hideThinkingPlaceholder: source.hideThinkingPlaceholder ?? DEFAULT_PREFS.hideThinkingPlaceholder,
|
|
111
126
|
};
|
|
112
127
|
}
|
|
113
128
|
|
|
@@ -125,6 +140,339 @@ function persistPrefs(pi: ExtensionAPI, prefs: XtrmUiPrefs): void {
|
|
|
125
140
|
pi.appendEntry(XTRM_UI_PREFS_ENTRY, prefs);
|
|
126
141
|
}
|
|
127
142
|
|
|
143
|
+
|
|
144
|
+
// ============================================================================
|
|
145
|
+
// Thinking Chrome
|
|
146
|
+
// ============================================================================
|
|
147
|
+
|
|
148
|
+
type AssistantMessageComponentCtor = {
|
|
149
|
+
prototype: {
|
|
150
|
+
updateContent?: (message: AssistantMessageLike) => void;
|
|
151
|
+
setExpanded?: (expanded: boolean) => void;
|
|
152
|
+
};
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
type AssistantContentBlock = { type?: string; thinking?: string };
|
|
156
|
+
type AssistantMessageLike = { content?: AssistantContentBlock[] };
|
|
157
|
+
type PatchableAssistantMessage = {
|
|
158
|
+
hideThinkingBlock?: boolean;
|
|
159
|
+
hiddenThinkingLabel?: string;
|
|
160
|
+
lastMessage?: AssistantMessageLike;
|
|
161
|
+
__xtrmThinkingExpanded?: boolean;
|
|
162
|
+
updateContent?: (message: AssistantMessageLike) => void;
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
const PATCHED_ASSISTANT_MESSAGE = "__xtrmUiSilentHiddenThinking";
|
|
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
|
+
|
|
198
|
+
async function installSilentHiddenThinkingPatch(): Promise<void> {
|
|
199
|
+
const entryPath = resolvePiCodingAgentEntryPath();
|
|
200
|
+
const componentPath = join(dirname(entryPath), "modes", "interactive", "components", "assistant-message.js");
|
|
201
|
+
const mod = await import(pathToFileURL(componentPath).href) as {
|
|
202
|
+
AssistantMessageComponent?: AssistantMessageComponentCtor;
|
|
203
|
+
};
|
|
204
|
+
const proto = mod.AssistantMessageComponent?.prototype as
|
|
205
|
+
| (AssistantMessageComponentCtor["prototype"] & { [PATCHED_ASSISTANT_MESSAGE]?: boolean })
|
|
206
|
+
| undefined;
|
|
207
|
+
if (!proto?.updateContent || proto[PATCHED_ASSISTANT_MESSAGE]) return;
|
|
208
|
+
|
|
209
|
+
const updateContent = proto.updateContent;
|
|
210
|
+
proto.updateContent = function patchedUpdateContent(this: PatchableAssistantMessage, message: AssistantMessageLike) {
|
|
211
|
+
if (this.hiddenThinkingLabel === "" && Array.isArray(message.content)) {
|
|
212
|
+
if (!this.__xtrmThinkingExpanded) {
|
|
213
|
+
updateContent.call(this, {
|
|
214
|
+
...message,
|
|
215
|
+
content: message.content.filter((block) => block.type !== "thinking" || !block.thinking?.trim()),
|
|
216
|
+
});
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const previousHideThinking = this.hideThinkingBlock;
|
|
221
|
+
this.hideThinkingBlock = false;
|
|
222
|
+
updateContent.call(this, message);
|
|
223
|
+
this.hideThinkingBlock = previousHideThinking;
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
updateContent.call(this, message);
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
proto.setExpanded = function setExpanded(this: PatchableAssistantMessage, expanded: boolean) {
|
|
230
|
+
this.__xtrmThinkingExpanded = expanded;
|
|
231
|
+
if (this.lastMessage) this.updateContent?.(this.lastMessage);
|
|
232
|
+
};
|
|
233
|
+
proto[PATCHED_ASSISTANT_MESSAGE] = true;
|
|
234
|
+
}
|
|
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
|
+
|
|
470
|
+
function applyThinkingChrome(ctx: ExtensionContext, prefs: XtrmUiPrefs): void {
|
|
471
|
+
(ctx.ui as { setHiddenThinkingLabel?: (label?: string) => void }).setHiddenThinkingLabel?.(
|
|
472
|
+
prefs.hideThinkingPlaceholder ? undefined : "",
|
|
473
|
+
);
|
|
474
|
+
}
|
|
475
|
+
|
|
128
476
|
// ============================================================================
|
|
129
477
|
// Chrome Application
|
|
130
478
|
// ============================================================================
|
|
@@ -324,6 +672,13 @@ function parseDensityArg(arg: string): XtrmDensity | undefined {
|
|
|
324
672
|
return undefined;
|
|
325
673
|
}
|
|
326
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
|
+
|
|
327
682
|
function registerCommands(pi: ExtensionAPI, getPrefs: () => XtrmUiPrefs, setPrefs: (p: XtrmUiPrefs) => void, getThinkingLevel: () => string) {
|
|
328
683
|
pi.registerMessageRenderer("xtrm-ui-info", (message, _options, theme) => {
|
|
329
684
|
const title = (message.details as { title?: string } | undefined)?.title ?? "XTRM UI";
|
|
@@ -335,7 +690,26 @@ function registerCommands(pi: ExtensionAPI, getPrefs: () => XtrmUiPrefs, setPref
|
|
|
335
690
|
|
|
336
691
|
pi.registerCommand("xtrm-ui", {
|
|
337
692
|
description: "Show XTRM UI status and active preferences",
|
|
338
|
-
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
|
+
|
|
339
713
|
const prefs = getPrefs();
|
|
340
714
|
const contextUsage = ctx.getContextUsage();
|
|
341
715
|
const lines = [
|
|
@@ -347,6 +721,7 @@ function registerCommands(pi: ExtensionAPI, getPrefs: () => XtrmUiPrefs, setPref
|
|
|
347
721
|
`Show footer: ${prefs.showFooter ? "yes" : "no"} (custom-footer handles this)`,
|
|
348
722
|
`Tool row background: ${prefs.toolRowBg ? "on" : "off"}`,
|
|
349
723
|
`Compact external tool results: ${prefs.compactExternalToolResults ? "on" : "off"}`,
|
|
724
|
+
`External tool chrome: ${prefs.externalToolChrome}`,
|
|
350
725
|
`Model: ${ctx.model?.id ?? "none"}`,
|
|
351
726
|
`Context: ${contextUsage?.tokens ?? "unknown"}/${contextUsage?.contextWindow ?? "unknown"}`,
|
|
352
727
|
];
|
|
@@ -477,6 +852,25 @@ function registerCommands(pi: ExtensionAPI, getPrefs: () => XtrmUiPrefs, setPref
|
|
|
477
852
|
},
|
|
478
853
|
});
|
|
479
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
|
+
|
|
480
874
|
pi.registerCommand("xtrm-ui-reset", {
|
|
481
875
|
description: "Restore XTRM UI defaults",
|
|
482
876
|
handler: async (_args, ctx) => {
|
|
@@ -884,6 +1278,101 @@ function summarizeGenericToolResult(
|
|
|
884
1278
|
return `• ${normalized}${subject ? ` ${subject}` : ""}${joined ? ` · ${joined}` : ""}`;
|
|
885
1279
|
}
|
|
886
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
|
+
|
|
887
1376
|
const XTRM_BUILTIN_TOOLS = new Set(["bash", "read", "edit", "write", "find", "grep", "ls"]);
|
|
888
1377
|
|
|
889
1378
|
function registerXtrmUiTools(pi: ExtensionAPI, getPrefs: () => XtrmUiPrefs): void {
|
|
@@ -933,6 +1422,7 @@ function registerXtrmUiTools(pi: ExtensionAPI, getPrefs: () => XtrmUiPrefs): voi
|
|
|
933
1422
|
pi.on("tool_result", async (event: ToolResultEvent, _ctx) => {
|
|
934
1423
|
if (event.isError) return undefined;
|
|
935
1424
|
if (XTRM_BUILTIN_TOOLS.has(event.toolName)) return undefined;
|
|
1425
|
+
if (!getPrefs().compactExternalToolResults) return undefined;
|
|
936
1426
|
|
|
937
1427
|
const text = getTextContent({ content: event.content as Array<{ type: string; text?: string }> });
|
|
938
1428
|
const startedAt = toolCallStartTimes.get(event.toolCallId);
|
|
@@ -946,13 +1436,17 @@ function registerXtrmUiTools(pi: ExtensionAPI, getPrefs: () => XtrmUiPrefs): voi
|
|
|
946
1436
|
? (event.input as Record<string, unknown>)
|
|
947
1437
|
: {};
|
|
948
1438
|
|
|
949
|
-
const compactText =
|
|
950
|
-
|
|
951
|
-
|
|
1439
|
+
const compactText = summarizeExternalToolResult(
|
|
1440
|
+
event.toolName,
|
|
1441
|
+
safeInput,
|
|
1442
|
+
sourceText,
|
|
1443
|
+
event.details,
|
|
1444
|
+
durationMs,
|
|
1445
|
+
);
|
|
952
1446
|
|
|
953
1447
|
return {
|
|
954
1448
|
content: [{ type: "text", text: formatHierarchyText(compactText) }],
|
|
955
|
-
details: event.details,
|
|
1449
|
+
details: withXtrmToolDetails(event.details, sourceText, event.toolName),
|
|
956
1450
|
};
|
|
957
1451
|
});
|
|
958
1452
|
|
|
@@ -971,7 +1465,7 @@ function registerXtrmUiTools(pi: ExtensionAPI, getPrefs: () => XtrmUiPrefs): voi
|
|
|
971
1465
|
renderResult(result, { expanded, isPartial }, theme) {
|
|
972
1466
|
const details = (result.details ?? {}) as DetailsWithXtrmMeta<BashToolDetails, Record<string, unknown>>;
|
|
973
1467
|
const meta = getXtrmMeta<BashToolDetails, Record<string, unknown>>(details);
|
|
974
|
-
const command = shortenCommand(String(meta?.args.command ?? ""));
|
|
1468
|
+
const command = shortenCommand(String(meta?.args.command ?? ""), 38);
|
|
975
1469
|
if (isPartial) {
|
|
976
1470
|
return toolRowText(theme, `${theme.fg("accent", "•")} ${theme.fg("toolTitle", "Running ")}${theme.fg("accent", command)}${theme.fg("toolTitle", " in bash")}`);
|
|
977
1471
|
}
|
|
@@ -1190,12 +1684,18 @@ function isXtrmTheme(name: string | undefined): boolean {
|
|
|
1190
1684
|
}
|
|
1191
1685
|
|
|
1192
1686
|
export default function xtrmUiExtension(pi: ExtensionAPI): void {
|
|
1687
|
+
void installSilentHiddenThinkingPatch().catch(() => undefined);
|
|
1688
|
+
void installExternalToolFramePatch().catch(() => undefined);
|
|
1689
|
+
|
|
1193
1690
|
let prefs: XtrmUiPrefs = { ...DEFAULT_PREFS };
|
|
1194
1691
|
let previousThemeName: string | null = null;
|
|
1195
1692
|
const extensionThemeDir = join(__dirname, "../../themes/xtrm-ui");
|
|
1196
1693
|
|
|
1197
1694
|
const getPrefs = () => prefs;
|
|
1198
|
-
const setPrefs = (p: XtrmUiPrefs) => {
|
|
1695
|
+
const setPrefs = (p: XtrmUiPrefs) => {
|
|
1696
|
+
prefs = p;
|
|
1697
|
+
setActiveExternalToolChrome(p.externalToolChrome);
|
|
1698
|
+
};
|
|
1199
1699
|
const getThinkingLevel = () => formatThinking(pi.getThinkingLevel());
|
|
1200
1700
|
|
|
1201
1701
|
registerXtrmUiTools(pi, getPrefs);
|
|
@@ -1203,6 +1703,7 @@ export default function xtrmUiExtension(pi: ExtensionAPI): void {
|
|
|
1203
1703
|
|
|
1204
1704
|
const refresh = (ctx: ExtensionContext) => {
|
|
1205
1705
|
applyXtrmChrome(ctx, prefs, getThinkingLevel);
|
|
1706
|
+
applyThinkingChrome(ctx, prefs);
|
|
1206
1707
|
};
|
|
1207
1708
|
|
|
1208
1709
|
pi.on("resources_discover", async () => ({
|
|
@@ -1210,7 +1711,7 @@ export default function xtrmUiExtension(pi: ExtensionAPI): void {
|
|
|
1210
1711
|
}));
|
|
1211
1712
|
|
|
1212
1713
|
pi.on("session_start", async (_event, ctx) => {
|
|
1213
|
-
|
|
1714
|
+
setPrefs(loadPrefs(ctx.sessionManager.getEntries() as Array<MaybeCustomEntry>));
|
|
1214
1715
|
if (!previousThemeName && !isXtrmTheme(ctx.ui.theme.name)) {
|
|
1215
1716
|
previousThemeName = ctx.ui.theme.name ?? null;
|
|
1216
1717
|
}
|
package/package.json
CHANGED
package/src/registry.ts
CHANGED
|
@@ -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
|
];
|