pi-my-compact-ui 0.2.2 → 0.3.0
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 +31 -3
- package/index.ts +3 -1744
- package/package.json +5 -3
- package/src/animation.ts +37 -0
- package/src/assistant-patches.ts +431 -0
- package/src/compaction.ts +75 -0
- package/src/config.ts +31 -0
- package/src/constants.ts +36 -0
- package/src/extension.ts +282 -0
- package/src/grouping.ts +327 -0
- package/src/guards.ts +58 -0
- package/src/helpers.ts +130 -0
- package/src/markdown.ts +98 -0
- package/src/modals.ts +579 -0
- package/src/settings.ts +142 -0
- package/src/state.ts +80 -0
- package/src/streaming-tools.ts +232 -0
- package/src/thinking.ts +62 -0
- package/src/tool-group.ts +449 -0
- package/src/types.ts +52 -0
package/src/modals.ts
ADDED
|
@@ -0,0 +1,579 @@
|
|
|
1
|
+
/** 工具与思考详情弹窗、交互式条目选择器。 */
|
|
2
|
+
import { matchesKey, Key, wrapTextWithAnsi, visibleWidth, truncateToWidth } from "@earendil-works/pi-tui";
|
|
3
|
+
import type { Component } from "@earendil-works/pi-tui";
|
|
4
|
+
import type { GroupEntry } from "./types.js";
|
|
5
|
+
import { toolStatus, thinkingTokenLabel, toolResultText, toolSummary } from "./helpers.js";
|
|
6
|
+
import { THINKING_SPINNER, SPINNER_MS, SPINNER, spinnerStart } from "./constants.js";
|
|
7
|
+
import { runtime } from "./state.js";
|
|
8
|
+
import { thinkingSpinnerFrame } from "./animation.js";
|
|
9
|
+
import { toolElapsed } from "./streaming-tools.js";
|
|
10
|
+
|
|
11
|
+
export class DetailModalComponent implements Component {
|
|
12
|
+
readonly entry: GroupEntry;
|
|
13
|
+
private scrollTop = 0;
|
|
14
|
+
private followingEnd = true;
|
|
15
|
+
private lastContentLineCount = 0;
|
|
16
|
+
private lastInnerHeight = 10;
|
|
17
|
+
private pollTimer: ReturnType<typeof setInterval> | null = null;
|
|
18
|
+
|
|
19
|
+
constructor(entry: GroupEntry) {
|
|
20
|
+
this.entry = entry;
|
|
21
|
+
this.startPollingIfNeeded();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
private isEntryActive(): boolean {
|
|
25
|
+
if (this.entry.kind === "thinking") {
|
|
26
|
+
return this.entry.active;
|
|
27
|
+
}
|
|
28
|
+
return toolStatus(this.entry.tool) === "pending";
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
private startPollingIfNeeded(): void {
|
|
32
|
+
if (this.isEntryActive()) {
|
|
33
|
+
const interval = this.entry.kind === "thinking" ? THINKING_SPINNER.intervalMs : SPINNER_MS;
|
|
34
|
+
this.pollTimer = setInterval(() => {
|
|
35
|
+
runtime.capturedTui?.requestRender?.();
|
|
36
|
+
if (!this.isEntryActive() && this.pollTimer) {
|
|
37
|
+
clearInterval(this.pollTimer);
|
|
38
|
+
this.pollTimer = null;
|
|
39
|
+
}
|
|
40
|
+
}, interval);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
dispose(): void {
|
|
45
|
+
if (this.pollTimer) {
|
|
46
|
+
clearInterval(this.pollTimer);
|
|
47
|
+
this.pollTimer = null;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
invalidate(): void {}
|
|
52
|
+
|
|
53
|
+
scrollBy(delta: number): void {
|
|
54
|
+
this.scrollTo(this.scrollTop + delta);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
scrollTo(newTop: number): void {
|
|
58
|
+
const maxScroll = Math.max(0, this.lastContentLineCount - this.lastInnerHeight);
|
|
59
|
+
this.scrollTop = Math.max(0, Math.min(maxScroll, newTop));
|
|
60
|
+
if (this.scrollTop >= maxScroll) {
|
|
61
|
+
this.followingEnd = true;
|
|
62
|
+
} else {
|
|
63
|
+
this.followingEnd = false;
|
|
64
|
+
}
|
|
65
|
+
runtime.capturedTui?.requestRender?.();
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
scrollToEnd(): void {
|
|
69
|
+
this.followingEnd = true;
|
|
70
|
+
this.scrollTop = Math.max(0, this.lastContentLineCount - this.lastInnerHeight);
|
|
71
|
+
runtime.capturedTui?.requestRender?.();
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
handleInput(data: string): boolean {
|
|
75
|
+
if (matchesKey(data, Key.escape) || data === "q" || data === "Q") {
|
|
76
|
+
closeDetailModal();
|
|
77
|
+
return true;
|
|
78
|
+
}
|
|
79
|
+
if (matchesKey(data, Key.up) || data === "k" || data === "K") {
|
|
80
|
+
this.scrollBy(-1);
|
|
81
|
+
return true;
|
|
82
|
+
}
|
|
83
|
+
if (matchesKey(data, Key.down) || data === "j" || data === "J") {
|
|
84
|
+
this.scrollBy(1);
|
|
85
|
+
return true;
|
|
86
|
+
}
|
|
87
|
+
if (matchesKey(data, Key.pageUp) || matchesKey(data, Key.left)) {
|
|
88
|
+
this.scrollBy(-10);
|
|
89
|
+
return true;
|
|
90
|
+
}
|
|
91
|
+
if (matchesKey(data, Key.pageDown) || matchesKey(data, Key.right) || matchesKey(data, Key.space)) {
|
|
92
|
+
this.scrollBy(10);
|
|
93
|
+
return true;
|
|
94
|
+
}
|
|
95
|
+
if (matchesKey(data, Key.home)) {
|
|
96
|
+
this.scrollTo(0);
|
|
97
|
+
return true;
|
|
98
|
+
}
|
|
99
|
+
if (matchesKey(data, Key.end)) {
|
|
100
|
+
this.scrollToEnd();
|
|
101
|
+
return true;
|
|
102
|
+
}
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
render(width: number): string[] {
|
|
107
|
+
const theme = runtime.currentTheme;
|
|
108
|
+
const fg = (color: string, t: string) => theme?.fg?.(color, t) ?? t;
|
|
109
|
+
const bold = theme?.bold ? theme.bold : (t: string) => t;
|
|
110
|
+
const borderFg = (t: string) => fg("borderAccent", fg("border", t));
|
|
111
|
+
const frame = SPINNER[Math.floor((Date.now() - spinnerStart) / SPINNER_MS) % SPINNER.length]!;
|
|
112
|
+
|
|
113
|
+
const termHeight = runtime.capturedTui?.terminal?.rows ?? process.stdout?.rows ?? 24;
|
|
114
|
+
const maxInnerHeight = Math.max(5, Math.floor(termHeight * 0.8) - 2);
|
|
115
|
+
const innerWidth = Math.max(10, width - 4);
|
|
116
|
+
|
|
117
|
+
let title = "";
|
|
118
|
+
const contentLines: string[] = [];
|
|
119
|
+
|
|
120
|
+
if (this.entry.kind === "thinking") {
|
|
121
|
+
const spin = this.entry.active ? thinkingSpinnerFrame() : undefined;
|
|
122
|
+
const icon = spin?.frame ?? "✓";
|
|
123
|
+
const tokenStr = thinkingTokenLabel(this.entry);
|
|
124
|
+
title = `${fg(spin?.color ?? "thinkingText", icon)} ${fg("toolTitle", "Thinking")} ${fg("muted", tokenStr)}`;
|
|
125
|
+
|
|
126
|
+
const rawText = this.entry.text.trim();
|
|
127
|
+
if (!rawText) {
|
|
128
|
+
contentLines.push(fg("dim", "(thinking...)"));
|
|
129
|
+
} else {
|
|
130
|
+
for (const rawLine of rawText.split("\n")) {
|
|
131
|
+
const wrapped = wrapTextWithAnsi(rawLine, innerWidth);
|
|
132
|
+
if (wrapped.length === 0) {
|
|
133
|
+
contentLines.push("");
|
|
134
|
+
} else {
|
|
135
|
+
for (const wl of wrapped) {
|
|
136
|
+
contentLines.push(fg("thinkingText", wl));
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
} else {
|
|
142
|
+
const tool = this.entry.tool;
|
|
143
|
+
const st = toolStatus(tool);
|
|
144
|
+
const icon = st === "pending" ? frame : st === "error" ? "✗" : "✓";
|
|
145
|
+
const color = st === "pending" ? "accent" : st === "error" ? "error" : "success";
|
|
146
|
+
const elapsed = `(${toolElapsed(tool)}s)`;
|
|
147
|
+
title = `${fg(color, icon)} ${fg("toolTitle", bold(tool.toolName))} ${fg("muted", elapsed)}`;
|
|
148
|
+
|
|
149
|
+
if (tool.toolName === "bash" || tool.toolName === "powershell") {
|
|
150
|
+
const cmd = tool.args?.command || "...";
|
|
151
|
+
for (const wl of wrapTextWithAnsi(`$ ${cmd}`, innerWidth)) {
|
|
152
|
+
contentLines.push(fg("accent", wl));
|
|
153
|
+
}
|
|
154
|
+
} else if (tool.args?.path) {
|
|
155
|
+
contentLines.push(`${fg("dim", "Path:")} ${tool.args.path}`);
|
|
156
|
+
} else if (tool.args) {
|
|
157
|
+
try {
|
|
158
|
+
const jsonStr = JSON.stringify(tool.args, null, 2);
|
|
159
|
+
for (const l of jsonStr.split("\n")) {
|
|
160
|
+
for (const wl of wrapTextWithAnsi(l, innerWidth)) {
|
|
161
|
+
contentLines.push(fg("dim", wl));
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
} catch {
|
|
165
|
+
contentLines.push(fg("dim", String(tool.args)));
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const resultText = toolResultText(tool);
|
|
170
|
+
contentLines.push("");
|
|
171
|
+
contentLines.push(fg("dim", "── Output ──"));
|
|
172
|
+
|
|
173
|
+
if (resultText) {
|
|
174
|
+
for (const rawLine of resultText.split("\n")) {
|
|
175
|
+
const wrapped = wrapTextWithAnsi(rawLine, innerWidth);
|
|
176
|
+
if (wrapped.length === 0) {
|
|
177
|
+
contentLines.push("");
|
|
178
|
+
} else {
|
|
179
|
+
for (const wl of wrapped) {
|
|
180
|
+
contentLines.push(fg("toolOutput", wl));
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
} else {
|
|
185
|
+
contentLines.push(fg("dim", st === "pending" ? "(running, waiting for output...)" : "(no output)"));
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const totalLines = contentLines.length;
|
|
190
|
+
const innerHeight = Math.min(Math.max(totalLines, 5), maxInnerHeight);
|
|
191
|
+
this.lastContentLineCount = totalLines;
|
|
192
|
+
this.lastInnerHeight = innerHeight;
|
|
193
|
+
const maxScroll = Math.max(0, totalLines - innerHeight);
|
|
194
|
+
|
|
195
|
+
if (this.followingEnd) {
|
|
196
|
+
this.scrollTop = maxScroll;
|
|
197
|
+
} else {
|
|
198
|
+
this.scrollTop = Math.max(0, Math.min(maxScroll, this.scrollTop));
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Top border with close button hint
|
|
202
|
+
const closeHint = "[Esc to close]";
|
|
203
|
+
const visClose = visibleWidth(closeHint);
|
|
204
|
+
const maxTitleWidth = Math.max(10, width - visClose - 12);
|
|
205
|
+
const clampedTitle = truncateToWidth(title, maxTitleWidth, "...");
|
|
206
|
+
const visTitle = visibleWidth(clampedTitle);
|
|
207
|
+
const fillerLen = Math.max(1, width - visTitle - visClose - 8);
|
|
208
|
+
const topBorder =
|
|
209
|
+
borderFg("┌─ ") +
|
|
210
|
+
clampedTitle +
|
|
211
|
+
borderFg(" " + "─".repeat(fillerLen) + " ") +
|
|
212
|
+
fg("muted", closeHint) +
|
|
213
|
+
borderFg(" ─┐");
|
|
214
|
+
|
|
215
|
+
const result: string[] = [truncateToWidth(topBorder, width)];
|
|
216
|
+
|
|
217
|
+
// Middle content lines
|
|
218
|
+
const slice = contentLines.slice(this.scrollTop, this.scrollTop + innerHeight);
|
|
219
|
+
while (slice.length < innerHeight) {
|
|
220
|
+
slice.push("");
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const showScrollbar = totalLines > innerHeight;
|
|
224
|
+
const thumbY = showScrollbar && maxScroll > 0 ? Math.round((this.scrollTop / maxScroll) * (innerHeight - 1)) : -1;
|
|
225
|
+
|
|
226
|
+
for (let i = 0; i < innerHeight; i++) {
|
|
227
|
+
const line = slice[i] ?? "";
|
|
228
|
+
const vis = visibleWidth(line);
|
|
229
|
+
const truncated = vis > innerWidth ? truncateToWidth(line, innerWidth, "...") : line;
|
|
230
|
+
const truncVis = visibleWidth(truncated);
|
|
231
|
+
const pad = Math.max(0, innerWidth - truncVis);
|
|
232
|
+
const rightChar = showScrollbar && i === thumbY ? fg("scrollbarThumb", "█") : borderFg("│");
|
|
233
|
+
result.push(borderFg("│ ") + truncated + " ".repeat(pad) + " " + rightChar);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Bottom border
|
|
237
|
+
const scrollInfo =
|
|
238
|
+
totalLines > innerHeight
|
|
239
|
+
? ` [${this.scrollTop + 1}-${Math.min(totalLines, this.scrollTop + innerHeight)}/${totalLines}] `
|
|
240
|
+
: "";
|
|
241
|
+
const visInfo = visibleWidth(scrollInfo);
|
|
242
|
+
const botFiller = Math.max(1, width - 2 - visInfo);
|
|
243
|
+
const botBorder =
|
|
244
|
+
borderFg("└" + "─".repeat(botFiller)) + (scrollInfo ? fg("dim", scrollInfo) : "") + borderFg("┘");
|
|
245
|
+
result.push(truncateToWidth(botBorder, width));
|
|
246
|
+
|
|
247
|
+
return result;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
export function openDetailModal(entry: GroupEntry): void {
|
|
252
|
+
closeDetailModal();
|
|
253
|
+
|
|
254
|
+
const modal = new DetailModalComponent(entry);
|
|
255
|
+
runtime.activeModalComponent = modal;
|
|
256
|
+
|
|
257
|
+
if (runtime.capturedTui) {
|
|
258
|
+
runtime.activeModalHandle = runtime.capturedTui.showOverlay(modal, {
|
|
259
|
+
anchor: "center",
|
|
260
|
+
width: "85%",
|
|
261
|
+
maxHeight: "80%",
|
|
262
|
+
});
|
|
263
|
+
runtime.capturedTui.requestRender();
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
if (runtime.activeUIContext?.custom) {
|
|
268
|
+
runtime.activeUIContext.custom(
|
|
269
|
+
(tui: any, _theme: any, _keybindings: any, done: () => void) => {
|
|
270
|
+
runtime.capturedTui = tui;
|
|
271
|
+
runtime.activeModalCloseFn = done;
|
|
272
|
+
return modal;
|
|
273
|
+
},
|
|
274
|
+
{
|
|
275
|
+
overlay: true,
|
|
276
|
+
overlayOptions: { anchor: "center", width: "85%", maxHeight: "80%" },
|
|
277
|
+
onHandle: (handle: any) => {
|
|
278
|
+
runtime.activeModalHandle = handle;
|
|
279
|
+
},
|
|
280
|
+
},
|
|
281
|
+
);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
export function closeDetailModal(): void {
|
|
286
|
+
if (runtime.activeModalComponent) {
|
|
287
|
+
runtime.activeModalComponent.dispose();
|
|
288
|
+
runtime.activeModalComponent = null;
|
|
289
|
+
}
|
|
290
|
+
if (runtime.activeModalHandle) {
|
|
291
|
+
const handle = runtime.activeModalHandle;
|
|
292
|
+
runtime.activeModalHandle = null;
|
|
293
|
+
handle.hide();
|
|
294
|
+
}
|
|
295
|
+
if (runtime.activeModalCloseFn) {
|
|
296
|
+
const done = runtime.activeModalCloseFn;
|
|
297
|
+
runtime.activeModalCloseFn = null;
|
|
298
|
+
done();
|
|
299
|
+
}
|
|
300
|
+
runtime.capturedTui?.requestRender?.();
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
export function getAllInspectableEntries(): GroupEntry[] {
|
|
304
|
+
const results: GroupEntry[] = [];
|
|
305
|
+
for (const g of runtime.groups) {
|
|
306
|
+
for (const entry of g.getVisibleEntries()) {
|
|
307
|
+
results.push(entry);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
return results;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
export class InspectSelectorModal implements Component {
|
|
314
|
+
private entries: GroupEntry[] = [];
|
|
315
|
+
private selectedIndex = 0;
|
|
316
|
+
private scrollTop = 0;
|
|
317
|
+
private visibleHeight = 10;
|
|
318
|
+
|
|
319
|
+
constructor() {
|
|
320
|
+
this.refreshEntries(true);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
refreshEntries(isInitial = false): void {
|
|
324
|
+
const wasAtEnd = isInitial || this.selectedIndex >= this.entries.length - 1;
|
|
325
|
+
this.entries = getAllInspectableEntries();
|
|
326
|
+
if (this.entries.length === 0) {
|
|
327
|
+
this.selectedIndex = 0;
|
|
328
|
+
this.scrollTop = 0;
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
if (wasAtEnd) {
|
|
332
|
+
// Place newest entries at the bottom; focus starts on the latest (last) entry
|
|
333
|
+
this.selectedIndex = this.entries.length - 1;
|
|
334
|
+
} else {
|
|
335
|
+
this.selectedIndex = Math.max(0, Math.min(this.selectedIndex, this.entries.length - 1));
|
|
336
|
+
}
|
|
337
|
+
this.adjustScroll();
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
private adjustScroll(): void {
|
|
341
|
+
if (this.entries.length === 0) {
|
|
342
|
+
this.scrollTop = 0;
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
if (this.selectedIndex < this.scrollTop) {
|
|
346
|
+
this.scrollTop = this.selectedIndex;
|
|
347
|
+
} else if (this.selectedIndex >= this.scrollTop + this.visibleHeight) {
|
|
348
|
+
this.scrollTop = this.selectedIndex - this.visibleHeight + 1;
|
|
349
|
+
}
|
|
350
|
+
const maxScroll = Math.max(0, this.entries.length - this.visibleHeight);
|
|
351
|
+
this.scrollTop = Math.max(0, Math.min(this.scrollTop, maxScroll));
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
handleInput(data: string): boolean {
|
|
355
|
+
if (matchesKey(data, Key.escape) || data === "q" || data === "Q") {
|
|
356
|
+
closeInspectSelectorModal();
|
|
357
|
+
return true;
|
|
358
|
+
}
|
|
359
|
+
if (matchesKey(data, Key.enter) || data === "\r" || data === "\n") {
|
|
360
|
+
const selected = this.entries[this.selectedIndex];
|
|
361
|
+
if (selected) {
|
|
362
|
+
openDetailModal(selected);
|
|
363
|
+
}
|
|
364
|
+
return true;
|
|
365
|
+
}
|
|
366
|
+
if (matchesKey(data, Key.up) || data === "k" || data === "K") {
|
|
367
|
+
if (this.entries.length > 0) {
|
|
368
|
+
this.selectedIndex = Math.max(0, this.selectedIndex - 1);
|
|
369
|
+
this.adjustScroll();
|
|
370
|
+
runtime.capturedTui?.requestRender?.();
|
|
371
|
+
}
|
|
372
|
+
return true;
|
|
373
|
+
}
|
|
374
|
+
if (matchesKey(data, Key.down) || data === "j" || data === "J") {
|
|
375
|
+
if (this.entries.length > 0) {
|
|
376
|
+
this.selectedIndex = Math.min(this.entries.length - 1, this.selectedIndex + 1);
|
|
377
|
+
this.adjustScroll();
|
|
378
|
+
runtime.capturedTui?.requestRender?.();
|
|
379
|
+
}
|
|
380
|
+
return true;
|
|
381
|
+
}
|
|
382
|
+
if (matchesKey(data, Key.pageUp)) {
|
|
383
|
+
if (this.entries.length > 0) {
|
|
384
|
+
this.selectedIndex = Math.max(0, this.selectedIndex - this.visibleHeight);
|
|
385
|
+
this.adjustScroll();
|
|
386
|
+
runtime.capturedTui?.requestRender?.();
|
|
387
|
+
}
|
|
388
|
+
return true;
|
|
389
|
+
}
|
|
390
|
+
if (matchesKey(data, Key.pageDown)) {
|
|
391
|
+
if (this.entries.length > 0) {
|
|
392
|
+
this.selectedIndex = Math.min(this.entries.length - 1, this.selectedIndex + this.visibleHeight);
|
|
393
|
+
this.adjustScroll();
|
|
394
|
+
runtime.capturedTui?.requestRender?.();
|
|
395
|
+
}
|
|
396
|
+
return true;
|
|
397
|
+
}
|
|
398
|
+
if (matchesKey(data, Key.home)) {
|
|
399
|
+
if (this.entries.length > 0) {
|
|
400
|
+
this.selectedIndex = 0;
|
|
401
|
+
this.adjustScroll();
|
|
402
|
+
runtime.capturedTui?.requestRender?.();
|
|
403
|
+
}
|
|
404
|
+
return true;
|
|
405
|
+
}
|
|
406
|
+
if (matchesKey(data, Key.end)) {
|
|
407
|
+
if (this.entries.length > 0) {
|
|
408
|
+
this.selectedIndex = this.entries.length - 1;
|
|
409
|
+
this.adjustScroll();
|
|
410
|
+
runtime.capturedTui?.requestRender?.();
|
|
411
|
+
}
|
|
412
|
+
return true;
|
|
413
|
+
}
|
|
414
|
+
return false;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
render(width: number): string[] {
|
|
418
|
+
const theme = runtime.currentTheme;
|
|
419
|
+
const fg = (color: string, t: string) => theme?.fg?.(color, t) ?? t;
|
|
420
|
+
const bold = theme?.bold ? theme.bold : (t: string) => t;
|
|
421
|
+
const borderFg = (t: string) => fg("borderAccent", fg("border", t));
|
|
422
|
+
const frame = SPINNER[Math.floor((Date.now() - spinnerStart) / SPINNER_MS) % SPINNER.length]!;
|
|
423
|
+
|
|
424
|
+
const termHeight = runtime.capturedTui?.terminal?.rows ?? process.stdout?.rows ?? 24;
|
|
425
|
+
const maxInnerHeight = Math.max(5, Math.floor(termHeight * 0.7) - 2);
|
|
426
|
+
const innerHeight = this.entries.length === 0
|
|
427
|
+
? 5
|
|
428
|
+
: Math.min(this.entries.length, maxInnerHeight);
|
|
429
|
+
this.visibleHeight = innerHeight;
|
|
430
|
+
this.adjustScroll();
|
|
431
|
+
|
|
432
|
+
const innerWidth = Math.max(10, width - 4);
|
|
433
|
+
const lines: string[] = [];
|
|
434
|
+
|
|
435
|
+
const title = ` ${fg("accent", bold("Inspect"))} ${fg("dim", "(↑/↓ select · Enter view · Esc exit)")} `;
|
|
436
|
+
const visTitle = visibleWidth(title);
|
|
437
|
+
const topFiller = Math.max(1, width - 2 - visTitle);
|
|
438
|
+
const topBorder = borderFg("┌" + "─".repeat(topFiller)) + title + borderFg("┐");
|
|
439
|
+
lines.push(truncateToWidth(topBorder, width));
|
|
440
|
+
|
|
441
|
+
if (this.entries.length === 0) {
|
|
442
|
+
const emptyMsg = " (no tool calls or thinking entries)";
|
|
443
|
+
for (let i = 0; i < innerHeight; i++) {
|
|
444
|
+
const row = i === Math.floor(innerHeight / 2) ? fg("dim", emptyMsg) : "";
|
|
445
|
+
const pad = Math.max(0, innerWidth - visibleWidth(row));
|
|
446
|
+
lines.push(borderFg("│ ") + row + " ".repeat(pad) + borderFg(" │"));
|
|
447
|
+
}
|
|
448
|
+
} else {
|
|
449
|
+
for (let i = 0; i < innerHeight; i++) {
|
|
450
|
+
const entryIdx = this.scrollTop + i;
|
|
451
|
+
const entry = this.entries[entryIdx];
|
|
452
|
+
if (!entry) {
|
|
453
|
+
lines.push(borderFg("│ ") + " ".repeat(innerWidth) + borderFg(" │"));
|
|
454
|
+
continue;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
const isSelected = entryIdx === this.selectedIndex;
|
|
458
|
+
const prefix = isSelected ? fg("accent", bold("▶ ")) : " ";
|
|
459
|
+
|
|
460
|
+
let leftContent = "";
|
|
461
|
+
let rightStr = "";
|
|
462
|
+
|
|
463
|
+
if (entry.kind === "tool") {
|
|
464
|
+
const tool = entry.tool;
|
|
465
|
+
const st = toolStatus(tool);
|
|
466
|
+
const icon = st === "pending" ? frame : st === "error" ? "✗" : "✓";
|
|
467
|
+
const iconColor = st === "pending" ? "accent" : st === "error" ? "error" : "success";
|
|
468
|
+
const s = toolSummary(tool.toolName, tool.args);
|
|
469
|
+
const elapsed = `(${toolElapsed(tool)}s)`;
|
|
470
|
+
const toolLabel = fg("toolTitle", bold(s.name));
|
|
471
|
+
const contentText = fg("dim", s.content || "...");
|
|
472
|
+
leftContent = `${prefix}${fg(iconColor, icon)} ${toolLabel} ${contentText}`;
|
|
473
|
+
rightStr = fg("muted", elapsed);
|
|
474
|
+
} else {
|
|
475
|
+
const spin = entry.active ? thinkingSpinnerFrame() : undefined;
|
|
476
|
+
const icon = spin?.frame ?? "✓";
|
|
477
|
+
const tokenLabel = thinkingTokenLabel(entry);
|
|
478
|
+
const tText = entry.text.trim().replace(/[*_#`>]+/g, "").replace(/\s+/g, " ");
|
|
479
|
+
const thinkingLabel = fg("toolTitle", "thinking");
|
|
480
|
+
const contentText = fg("thinkingText", tText || "(thinking...)");
|
|
481
|
+
leftContent = `${prefix}${fg(spin?.color ?? "thinkingText", icon)} ${thinkingLabel} ${contentText}`;
|
|
482
|
+
rightStr = fg("muted", tokenLabel);
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
const maxLeft = Math.max(1, innerWidth - visibleWidth(rightStr) - 1);
|
|
486
|
+
const ellipsis = entry.kind === "thinking" ? fg("thinkingText", "...") : fg("dim", "...");
|
|
487
|
+
const leftTrunc = truncateToWidth(leftContent, maxLeft, ellipsis);
|
|
488
|
+
const gap = Math.max(1, innerWidth - visibleWidth(leftTrunc) - visibleWidth(rightStr));
|
|
489
|
+
let rowLine = `${leftTrunc}${" ".repeat(gap)}${rightStr}`;
|
|
490
|
+
|
|
491
|
+
const rowVis = visibleWidth(rowLine);
|
|
492
|
+
if (rowVis < innerWidth) {
|
|
493
|
+
rowLine += " ".repeat(innerWidth - rowVis);
|
|
494
|
+
} else if (rowVis > innerWidth) {
|
|
495
|
+
rowLine = truncateToWidth(rowLine, innerWidth);
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
if (isSelected) {
|
|
499
|
+
if (theme?.bg) {
|
|
500
|
+
rowLine = theme.bg("selectedBg", rowLine);
|
|
501
|
+
} else if (theme?.inverse) {
|
|
502
|
+
rowLine = theme.inverse(rowLine);
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
lines.push(borderFg("│ ") + rowLine + borderFg(" │"));
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
const countInfo =
|
|
511
|
+
this.entries.length > 0
|
|
512
|
+
? ` [${this.selectedIndex + 1}/${this.entries.length}] `
|
|
513
|
+
: "";
|
|
514
|
+
const visCount = visibleWidth(countInfo);
|
|
515
|
+
const botFiller = Math.max(1, width - 2 - visCount);
|
|
516
|
+
const botBorder =
|
|
517
|
+
borderFg("└" + "─".repeat(botFiller)) + (countInfo ? fg("dim", countInfo) : "") + borderFg("┘");
|
|
518
|
+
lines.push(truncateToWidth(botBorder, width));
|
|
519
|
+
|
|
520
|
+
return lines;
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
dispose(): void {}
|
|
524
|
+
invalidate(): void {}
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
export function openInspectSelectorModal(): void {
|
|
528
|
+
if (runtime.activeModalHandle) {
|
|
529
|
+
closeDetailModal();
|
|
530
|
+
}
|
|
531
|
+
closeInspectSelectorModal();
|
|
532
|
+
|
|
533
|
+
const modal = new InspectSelectorModal();
|
|
534
|
+
runtime.activeSelectorComponent = modal;
|
|
535
|
+
|
|
536
|
+
if (runtime.capturedTui) {
|
|
537
|
+
runtime.activeSelectorHandle = runtime.capturedTui.showOverlay(modal, {
|
|
538
|
+
anchor: "center",
|
|
539
|
+
width: "85%",
|
|
540
|
+
maxHeight: "70%",
|
|
541
|
+
});
|
|
542
|
+
runtime.capturedTui.requestRender();
|
|
543
|
+
return;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
if (runtime.activeUIContext?.custom) {
|
|
547
|
+
runtime.activeUIContext.custom(
|
|
548
|
+
(tui: any, _theme: any, _keybindings: any, done: () => void) => {
|
|
549
|
+
runtime.capturedTui = tui;
|
|
550
|
+
runtime.activeSelectorCloseFn = done;
|
|
551
|
+
return modal;
|
|
552
|
+
},
|
|
553
|
+
{
|
|
554
|
+
overlay: true,
|
|
555
|
+
overlayOptions: { anchor: "center", width: "85%", maxHeight: "70%" },
|
|
556
|
+
onHandle: (handle: any) => {
|
|
557
|
+
runtime.activeSelectorHandle = handle;
|
|
558
|
+
},
|
|
559
|
+
},
|
|
560
|
+
);
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
export function closeInspectSelectorModal(): void {
|
|
565
|
+
if (runtime.activeSelectorHandle) {
|
|
566
|
+
const handle = runtime.activeSelectorHandle;
|
|
567
|
+
runtime.activeSelectorHandle = null;
|
|
568
|
+
handle.hide();
|
|
569
|
+
}
|
|
570
|
+
if (runtime.activeSelectorCloseFn) {
|
|
571
|
+
const done = runtime.activeSelectorCloseFn;
|
|
572
|
+
runtime.activeSelectorCloseFn = null;
|
|
573
|
+
done();
|
|
574
|
+
}
|
|
575
|
+
if (runtime.activeSelectorComponent) {
|
|
576
|
+
runtime.activeSelectorComponent = null;
|
|
577
|
+
}
|
|
578
|
+
runtime.capturedTui?.requestRender?.();
|
|
579
|
+
}
|