@tt-a1i/openpi 0.2.0 → 0.3.1
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 +28 -6
- package/SETUP.md +1 -1
- package/extensions/capabilities/index.ts +4 -1
- package/extensions/plan-mode/bash-policy.ts +219 -42
- package/extensions/plan-mode/index.ts +44 -19
- package/extensions/setup/index.ts +3 -3
- package/extensions/shared/editor-layers.ts +150 -0
- package/extensions/shared/setup-config.ts +4 -15
- package/extensions/subagents/index.ts +174 -124
- package/extensions/subagents/src/prompt.ts +6 -6
- package/extensions/subagents/src/result-delivery.ts +50 -5
- package/extensions/subagents/src/ui/takeover.ts +231 -133
- package/extensions/subagents/src/ui/transcript.ts +252 -37
- package/extensions/subagents/src/ui/wait-result.ts +6 -19
- package/extensions/suggestions/index.ts +27 -18
- package/extensions/tasks/index.ts +24 -6
- package/extensions/ui-customization/footer.ts +59 -10
- package/extensions/workflows/index.ts +28 -20
- package/package.json +1 -1
- package/skills/subagents/SKILL.md +1 -1
|
@@ -17,7 +17,12 @@ import { sanitizeTerminalText } from "../../../shared/terminal-text.ts";
|
|
|
17
17
|
import { formatElapsed, type SubagentSnapshot } from "../domain.ts";
|
|
18
18
|
import { formatContextUtilization } from "../format.ts";
|
|
19
19
|
import type { SubagentReadModel } from "../manager.ts";
|
|
20
|
-
import {
|
|
20
|
+
import {
|
|
21
|
+
SPINNER_INTERVAL_MS,
|
|
22
|
+
TranscriptRenderer,
|
|
23
|
+
buildTranscriptLines,
|
|
24
|
+
spinnerFrame,
|
|
25
|
+
} from "./transcript.ts";
|
|
21
26
|
|
|
22
27
|
export function sanitizeSubagentDisplayLine(value: string) {
|
|
23
28
|
return sanitizeTerminalText(value).replace(/\s+/g, " ").trim();
|
|
@@ -30,26 +35,55 @@ function configuredKeys(
|
|
|
30
35
|
return keybindings.getKeys(binding).join("/") || "unbound";
|
|
31
36
|
}
|
|
32
37
|
|
|
33
|
-
|
|
38
|
+
/**
|
|
39
|
+
* One spinner definition for the whole subagent UI: the dashboard glyph, the
|
|
40
|
+
* takeover header, and the transcript's live tools must animate in step, so the
|
|
41
|
+
* frames and their cadence live in `transcript.ts` and are imported here.
|
|
42
|
+
*/
|
|
43
|
+
function statusGlyph(
|
|
44
|
+
snap: SubagentSnapshot,
|
|
45
|
+
theme: Theme,
|
|
46
|
+
now = Date.now(),
|
|
47
|
+
): string {
|
|
34
48
|
switch (snap.status) {
|
|
35
49
|
case "running":
|
|
36
|
-
return theme.fg("warning",
|
|
50
|
+
return theme.fg("warning", spinnerFrame(now));
|
|
37
51
|
case "done":
|
|
38
|
-
return theme.fg("success", "
|
|
52
|
+
return theme.fg("success", "✓");
|
|
39
53
|
case "error":
|
|
40
|
-
return theme.fg("error", "
|
|
54
|
+
return theme.fg("error", "✗");
|
|
41
55
|
}
|
|
42
56
|
}
|
|
43
57
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
58
|
+
/**
|
|
59
|
+
* Tool names come from the child's own tool-call events, so they are as
|
|
60
|
+
* untrusted as their arguments and must be sanitized before reaching the
|
|
61
|
+
* terminal: `truncateToWidth` only ignores escape sequences while measuring.
|
|
62
|
+
*/
|
|
63
|
+
function runningActivity(snap: SubagentSnapshot) {
|
|
64
|
+
if (snap.status !== "running") return "";
|
|
65
|
+
const liveTool = snap.liveTools.at(-1);
|
|
66
|
+
if (liveTool) {
|
|
67
|
+
const name = sanitizeSubagentDisplayLine(liveTool.name);
|
|
68
|
+
const args = truncateToWidth(
|
|
69
|
+
sanitizeSubagentDisplayLine(liveTool.argsPreview ?? ""),
|
|
70
|
+
48,
|
|
71
|
+
);
|
|
72
|
+
return args ? `${name} · ${args}` : name;
|
|
52
73
|
}
|
|
74
|
+
for (const item of [...snap.transcript].reverse()) {
|
|
75
|
+
if (item.kind === "toolResult") {
|
|
76
|
+
return sanitizeSubagentDisplayLine(item.name);
|
|
77
|
+
}
|
|
78
|
+
if (item.kind !== "assistant") continue;
|
|
79
|
+
const tool = [...item.parts]
|
|
80
|
+
.reverse()
|
|
81
|
+
.find((part) => part.type === "toolCall");
|
|
82
|
+
if (tool?.type === "toolCall") {
|
|
83
|
+
return sanitizeSubagentDisplayLine(tool.name);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return "";
|
|
53
87
|
}
|
|
54
88
|
|
|
55
89
|
// --- Entry points --------------------------------------------------------------
|
|
@@ -126,7 +160,7 @@ export function reconcileDashboardSelection(
|
|
|
126
160
|
selection.id = subs[selection.index]?.id;
|
|
127
161
|
}
|
|
128
162
|
|
|
129
|
-
class SubagentDashboard implements Component {
|
|
163
|
+
export class SubagentDashboard implements Component {
|
|
130
164
|
private tui: TUI;
|
|
131
165
|
private theme: Theme;
|
|
132
166
|
private keybindings: KeybindingsManager;
|
|
@@ -135,7 +169,7 @@ class SubagentDashboard implements Component {
|
|
|
135
169
|
private done: (value: string | null) => void;
|
|
136
170
|
|
|
137
171
|
private closed = false;
|
|
138
|
-
private ticker
|
|
172
|
+
private ticker?: ReturnType<typeof setInterval>;
|
|
139
173
|
private unsubChange: () => void;
|
|
140
174
|
|
|
141
175
|
constructor(
|
|
@@ -152,19 +186,29 @@ class SubagentDashboard implements Component {
|
|
|
152
186
|
this.view = view;
|
|
153
187
|
this.selection = selection;
|
|
154
188
|
this.done = done;
|
|
155
|
-
|
|
156
|
-
this.
|
|
157
|
-
|
|
189
|
+
this.refreshTicker();
|
|
190
|
+
this.unsubChange = view.subscribe(() => {
|
|
191
|
+
this.refreshTicker();
|
|
192
|
+
this.tui.requestRender();
|
|
193
|
+
});
|
|
158
194
|
}
|
|
159
195
|
|
|
160
196
|
private subs(): ReadonlyArray<SubagentSnapshot> {
|
|
161
197
|
return this.view.list();
|
|
162
198
|
}
|
|
163
199
|
|
|
200
|
+
private refreshTicker() {
|
|
201
|
+
const interval = this.subs().some((snap) => snap.status === "running")
|
|
202
|
+
? SPINNER_INTERVAL_MS
|
|
203
|
+
: 1000;
|
|
204
|
+
if (this.ticker) clearInterval(this.ticker);
|
|
205
|
+
this.ticker = setInterval(() => this.tui.requestRender(), interval);
|
|
206
|
+
}
|
|
207
|
+
|
|
164
208
|
private cleanup() {
|
|
165
209
|
if (this.closed) return false;
|
|
166
210
|
this.closed = true;
|
|
167
|
-
clearInterval(this.ticker);
|
|
211
|
+
if (this.ticker) clearInterval(this.ticker);
|
|
168
212
|
this.unsubChange();
|
|
169
213
|
return true;
|
|
170
214
|
}
|
|
@@ -237,45 +281,36 @@ class SubagentDashboard implements Component {
|
|
|
237
281
|
const subs = this.subs();
|
|
238
282
|
reconcileDashboardSelection(this.selection, subs);
|
|
239
283
|
|
|
284
|
+
// One timestamp per frame so every row's spinner shows the same frame.
|
|
285
|
+
const now = Date.now();
|
|
240
286
|
const rows = this.tui.terminal.rows || 30;
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
const
|
|
245
|
-
const innerWidth = width - 2;
|
|
287
|
+
const maxBodyHeight = Math.max(1, rows - 5);
|
|
288
|
+
const bodyHeight =
|
|
289
|
+
subs.length > maxBodyHeight ? maxBodyHeight : Math.max(1, subs.length);
|
|
290
|
+
const innerWidth = Math.max(0, width - 2);
|
|
246
291
|
|
|
247
292
|
const lines: string[] = [];
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
const
|
|
251
|
-
const
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
lines.push(
|
|
260
|
-
truncateToWidth(
|
|
261
|
-
` ${headerLeft}${" ".repeat(headerPad)}${headerRight} `,
|
|
262
|
-
width,
|
|
263
|
-
),
|
|
264
|
-
);
|
|
265
|
-
|
|
266
|
-
// Top border with panel title
|
|
267
|
-
const settled = subs.filter((s) => s.status !== "running").length;
|
|
293
|
+
const running = subs.filter((snap) => snap.status === "running").length;
|
|
294
|
+
const done = subs.filter((snap) => snap.status === "done").length;
|
|
295
|
+
const failed = subs.filter((snap) => snap.status === "error").length;
|
|
296
|
+
const summary =
|
|
297
|
+
[
|
|
298
|
+
running > 0 ? `${running} running` : "",
|
|
299
|
+
done > 0 ? `${done} done` : "",
|
|
300
|
+
failed > 0 ? `${failed} failed` : "",
|
|
301
|
+
]
|
|
302
|
+
.filter(Boolean)
|
|
303
|
+
.join(" · ") || "no agents";
|
|
268
304
|
lines.push(
|
|
269
305
|
theme.fg("border", "╭") +
|
|
270
|
-
this.borderSegment(innerWidth, `
|
|
306
|
+
this.borderSegment(innerWidth, `Subagents · ${summary}`) +
|
|
271
307
|
theme.fg("border", "╮"),
|
|
272
308
|
);
|
|
273
309
|
|
|
274
|
-
// Rows
|
|
275
310
|
const divider = theme.fg("border", "│");
|
|
276
|
-
const rowLines = this.renderRows(subs, innerWidth, bodyHeight);
|
|
277
|
-
for (
|
|
278
|
-
lines.push(divider + this.pad(
|
|
311
|
+
const rowLines = this.renderRows(subs, innerWidth, bodyHeight, now);
|
|
312
|
+
for (const row of rowLines) {
|
|
313
|
+
lines.push(divider + this.pad(row, innerWidth) + divider);
|
|
279
314
|
}
|
|
280
315
|
|
|
281
316
|
// Bottom border
|
|
@@ -303,37 +338,40 @@ class SubagentDashboard implements Component {
|
|
|
303
338
|
subs: ReadonlyArray<SubagentSnapshot>,
|
|
304
339
|
width: number,
|
|
305
340
|
height: number,
|
|
341
|
+
now: number,
|
|
306
342
|
): string[] {
|
|
307
343
|
const theme = this.theme;
|
|
308
344
|
const out: string[] = [];
|
|
309
345
|
|
|
310
|
-
|
|
346
|
+
const needsMore = subs.length > height && height > 1;
|
|
347
|
+
const visibleHeight = needsMore ? height - 1 : height;
|
|
348
|
+
|
|
349
|
+
// Scroll window around selection. The more indicator gets its own row, so
|
|
350
|
+
// it never replaces a selectable subagent.
|
|
311
351
|
let start = 0;
|
|
312
|
-
if (subs.length >
|
|
352
|
+
if (subs.length > visibleHeight) {
|
|
313
353
|
start = Math.min(
|
|
314
|
-
Math.max(0, this.selection.index - Math.floor(
|
|
315
|
-
subs.length -
|
|
354
|
+
Math.max(0, this.selection.index - Math.floor(visibleHeight / 2)),
|
|
355
|
+
Math.max(0, subs.length - visibleHeight),
|
|
316
356
|
);
|
|
317
357
|
}
|
|
318
|
-
const visible = subs.slice(start, start +
|
|
358
|
+
const visible = subs.slice(start, start + visibleHeight);
|
|
319
359
|
|
|
320
360
|
for (let i = 0; i < visible.length; i++) {
|
|
321
361
|
const snap = visible[i];
|
|
322
362
|
const index = start + i;
|
|
323
363
|
const isSelected = index === this.selection.index;
|
|
324
364
|
|
|
325
|
-
// Left: marker, status square, title
|
|
326
365
|
const marker = isSelected ? theme.fg("accent", "❯") : " ";
|
|
327
366
|
const safeTitle = sanitizeSubagentDisplayLine(snap.title) || snap.id;
|
|
328
367
|
const title = isSelected
|
|
329
368
|
? theme.fg("accent", safeTitle)
|
|
330
369
|
: theme.fg("text", safeTitle);
|
|
331
|
-
const
|
|
370
|
+
const activity = runningActivity(snap);
|
|
371
|
+
const prefix = ` ${marker} ${statusGlyph(snap, theme, now)} `;
|
|
332
372
|
|
|
333
|
-
// Right: backend · model · context utilization · elapsed · status
|
|
334
373
|
const utilization = formatContextUtilization(snap.usage);
|
|
335
|
-
const
|
|
336
|
-
const rightParts = [
|
|
374
|
+
const metadata = [
|
|
337
375
|
theme.fg("muted", snap.backend),
|
|
338
376
|
theme.fg(
|
|
339
377
|
"muted",
|
|
@@ -341,24 +379,45 @@ class SubagentDashboard implements Component {
|
|
|
341
379
|
),
|
|
342
380
|
...(utilization ? [theme.fg("muted", utilization)] : []),
|
|
343
381
|
theme.fg("muted", formatElapsed(snap)),
|
|
344
|
-
statusWord(snap, theme),
|
|
345
382
|
];
|
|
346
|
-
const
|
|
347
|
-
|
|
383
|
+
const dot = theme.fg("dim", " · ");
|
|
384
|
+
// Preserve elapsed and the activity-bearing left side longest. Shed the
|
|
385
|
+
// least useful metadata as a segment instead of clipping a joined tail.
|
|
386
|
+
while (
|
|
387
|
+
metadata.length > 1 &&
|
|
388
|
+
visibleWidth(metadata.join(dot)) > Math.max(0, width - 16)
|
|
389
|
+
) {
|
|
390
|
+
metadata.shift();
|
|
391
|
+
}
|
|
392
|
+
const right = metadata.join(dot);
|
|
348
393
|
const rightWidth = visibleWidth(right);
|
|
349
|
-
const leftMax = Math.max(0, width - rightWidth - 2);
|
|
350
|
-
const
|
|
351
|
-
const
|
|
352
|
-
|
|
394
|
+
const leftMax = Math.max(0, width - rightWidth - (right ? 2 : 0));
|
|
395
|
+
const activityLabel = activity ? theme.fg("muted", ` · ${activity}`) : "";
|
|
396
|
+
const left =
|
|
397
|
+
prefix +
|
|
398
|
+
truncateToWidth(
|
|
399
|
+
title,
|
|
400
|
+
Math.max(
|
|
401
|
+
0,
|
|
402
|
+
leftMax - visibleWidth(prefix) - visibleWidth(activityLabel),
|
|
403
|
+
),
|
|
404
|
+
) +
|
|
405
|
+
truncateToWidth(
|
|
406
|
+
activityLabel,
|
|
407
|
+
Math.max(0, leftMax - visibleWidth(prefix)),
|
|
408
|
+
);
|
|
409
|
+
const gap = right
|
|
410
|
+
? Math.max(1, width - visibleWidth(left) - rightWidth)
|
|
411
|
+
: 0;
|
|
412
|
+
out.push(truncateToWidth(left + " ".repeat(gap) + right, width));
|
|
353
413
|
}
|
|
354
414
|
|
|
355
|
-
if (
|
|
356
|
-
out
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
width,
|
|
415
|
+
if (needsMore) {
|
|
416
|
+
out.push(
|
|
417
|
+
truncateToWidth(
|
|
418
|
+
theme.fg("dim", ` … ${subs.length - visible.length} more`),
|
|
419
|
+
width,
|
|
420
|
+
),
|
|
362
421
|
);
|
|
363
422
|
}
|
|
364
423
|
return out;
|
|
@@ -371,7 +430,7 @@ class SubagentDashboard implements Component {
|
|
|
371
430
|
|
|
372
431
|
const TRANSCRIPT_SCROLL_STEP = 6;
|
|
373
432
|
|
|
374
|
-
class TakeoverView implements Component, Focusable {
|
|
433
|
+
export class TakeoverView implements Component, Focusable {
|
|
375
434
|
private tui: TUI;
|
|
376
435
|
private theme: Theme;
|
|
377
436
|
private keybindings: KeybindingsManager;
|
|
@@ -386,7 +445,7 @@ class TakeoverView implements Component, Focusable {
|
|
|
386
445
|
private scrollOffset = 0;
|
|
387
446
|
private unsubscribe: () => void;
|
|
388
447
|
private renderTimer?: ReturnType<typeof setTimeout>;
|
|
389
|
-
private ticker
|
|
448
|
+
private ticker?: ReturnType<typeof setInterval>;
|
|
390
449
|
private closed = false;
|
|
391
450
|
|
|
392
451
|
private _focused = false;
|
|
@@ -414,9 +473,11 @@ class TakeoverView implements Component, Focusable {
|
|
|
414
473
|
this.view = view;
|
|
415
474
|
this.done = done;
|
|
416
475
|
this.options = options;
|
|
417
|
-
this.unsubscribe = view.subscribeTo(id, () =>
|
|
418
|
-
|
|
419
|
-
|
|
476
|
+
this.unsubscribe = view.subscribeTo(id, () => {
|
|
477
|
+
this.refreshTicker();
|
|
478
|
+
this.scheduleRender();
|
|
479
|
+
});
|
|
480
|
+
this.refreshTicker();
|
|
420
481
|
this.input.onSubmit = (value: string) => {
|
|
421
482
|
const text = value.trim();
|
|
422
483
|
if (!text) return;
|
|
@@ -431,6 +492,13 @@ class TakeoverView implements Component, Focusable {
|
|
|
431
492
|
return this.view.get(this.id);
|
|
432
493
|
}
|
|
433
494
|
|
|
495
|
+
private refreshTicker() {
|
|
496
|
+
const interval =
|
|
497
|
+
this.snap()?.status === "running" ? SPINNER_INTERVAL_MS : 1000;
|
|
498
|
+
if (this.ticker) clearInterval(this.ticker);
|
|
499
|
+
this.ticker = setInterval(() => this.tui.requestRender(), interval);
|
|
500
|
+
}
|
|
501
|
+
|
|
434
502
|
private scheduleRender() {
|
|
435
503
|
if (this.renderTimer) return;
|
|
436
504
|
// Streaming can emit an event per token. Limit terminal repaints so this
|
|
@@ -445,7 +513,7 @@ class TakeoverView implements Component, Focusable {
|
|
|
445
513
|
if (this.closed) return false;
|
|
446
514
|
this.closed = true;
|
|
447
515
|
this.unsubscribe();
|
|
448
|
-
clearInterval(this.ticker);
|
|
516
|
+
if (this.ticker) clearInterval(this.ticker);
|
|
449
517
|
if (this.renderTimer) clearTimeout(this.renderTimer);
|
|
450
518
|
this.renderTimer = undefined;
|
|
451
519
|
return true;
|
|
@@ -504,59 +572,95 @@ class TakeoverView implements Component, Focusable {
|
|
|
504
572
|
|
|
505
573
|
private viewportHeight(): number {
|
|
506
574
|
const rows = this.tui.terminal.rows || 30;
|
|
507
|
-
//
|
|
508
|
-
//
|
|
509
|
-
return Math.max(
|
|
575
|
+
// Top rule, transcript rule, input, key hints, and bottom rule are five
|
|
576
|
+
// chrome rows. The overlay leaves Pi's final footer row visible.
|
|
577
|
+
return Math.max(1, rows - 6);
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
private rule(width: number, left = "", right = "") {
|
|
581
|
+
const fill = "─";
|
|
582
|
+
const available = Math.max(1, width);
|
|
583
|
+
const leftWidth = visibleWidth(left);
|
|
584
|
+
const rightWidth = visibleWidth(right);
|
|
585
|
+
if (!right || leftWidth + rightWidth + 2 > available) {
|
|
586
|
+
return truncateToWidth(
|
|
587
|
+
left + fill.repeat(Math.max(0, available - leftWidth)),
|
|
588
|
+
available,
|
|
589
|
+
);
|
|
590
|
+
}
|
|
591
|
+
return (
|
|
592
|
+
left +
|
|
593
|
+
fill.repeat(Math.max(1, available - leftWidth - rightWidth)) +
|
|
594
|
+
right
|
|
595
|
+
);
|
|
510
596
|
}
|
|
511
597
|
|
|
512
598
|
render(width: number): string[] {
|
|
513
599
|
const theme = this.theme;
|
|
514
|
-
const
|
|
600
|
+
const now = Date.now();
|
|
515
601
|
const lines: string[] = [];
|
|
516
602
|
const snap = this.snap();
|
|
517
603
|
|
|
518
604
|
if (!snap) {
|
|
519
|
-
|
|
520
|
-
lines.push(
|
|
521
|
-
|
|
605
|
+
const border = theme.fg("borderAccent", "─".repeat(Math.max(1, width)));
|
|
606
|
+
lines.push(
|
|
607
|
+
border,
|
|
608
|
+
theme.fg("dim", `${this.id} is no longer tracked`),
|
|
609
|
+
border,
|
|
610
|
+
);
|
|
522
611
|
return lines;
|
|
523
612
|
}
|
|
524
613
|
|
|
525
|
-
|
|
614
|
+
const title = sanitizeSubagentDisplayLine(snap.title) || snap.id;
|
|
615
|
+
const headerLeft =
|
|
616
|
+
theme.fg("borderAccent", "─ ") +
|
|
617
|
+
statusGlyph(snap, theme, now) +
|
|
618
|
+
" " +
|
|
619
|
+
theme.fg("accent", theme.bold(title)) +
|
|
620
|
+
theme.fg("borderAccent", " ");
|
|
526
621
|
const utilization = formatContextUtilization(snap.usage);
|
|
527
|
-
const
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
theme.bold(sanitizeSubagentDisplayLine(snap.title) || snap.id),
|
|
532
|
-
) +
|
|
533
|
-
theme.fg("muted", ` · ${snap.status} · ${formatElapsed(snap)}`) +
|
|
534
|
-
(this.options?.badge
|
|
535
|
-
? theme.fg(
|
|
536
|
-
"muted",
|
|
537
|
-
` · ${sanitizeSubagentDisplayLine(this.options.badge)}`,
|
|
538
|
-
)
|
|
539
|
-
: "") +
|
|
622
|
+
const metadata = [
|
|
623
|
+
...(this.options?.badge
|
|
624
|
+
? [theme.fg("muted", sanitizeSubagentDisplayLine(this.options.badge))]
|
|
625
|
+
: []),
|
|
540
626
|
theme.fg(
|
|
541
|
-
"
|
|
542
|
-
|
|
543
|
-
)
|
|
544
|
-
(utilization ? theme.fg("
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
627
|
+
"muted",
|
|
628
|
+
sanitizeSubagentDisplayLine(snap.meta.modelLabel ?? "?") || "?",
|
|
629
|
+
),
|
|
630
|
+
...(utilization ? [theme.fg("muted", utilization)] : []),
|
|
631
|
+
theme.fg("muted", formatElapsed(snap)),
|
|
632
|
+
];
|
|
633
|
+
const dot = theme.fg("dim", " · ");
|
|
634
|
+
while (
|
|
635
|
+
metadata.length > 1 &&
|
|
636
|
+
visibleWidth(headerLeft) + visibleWidth(metadata.join(dot)) + 2 > width
|
|
637
|
+
) {
|
|
638
|
+
metadata.shift();
|
|
639
|
+
}
|
|
640
|
+
lines.push(
|
|
641
|
+
this.rule(
|
|
642
|
+
width,
|
|
643
|
+
truncateToWidth(
|
|
644
|
+
headerLeft,
|
|
645
|
+
Math.max(1, width - visibleWidth(metadata.join(dot)) - 2),
|
|
646
|
+
),
|
|
647
|
+
metadata.join(dot),
|
|
648
|
+
),
|
|
649
|
+
);
|
|
650
|
+
|
|
651
|
+
// Fixed-height transcript viewport. Errors consume a row, but scroll state
|
|
652
|
+
// is represented by the following rule so its height never changes.
|
|
653
|
+
// `now` is shared with the header glyph so both spinners show one frame.
|
|
550
654
|
const transcript = buildTranscriptLines(
|
|
551
655
|
snap,
|
|
552
656
|
width,
|
|
553
657
|
theme,
|
|
554
658
|
this.transcriptRenderer,
|
|
659
|
+
{ now },
|
|
555
660
|
);
|
|
556
661
|
const viewport = this.viewportHeight();
|
|
557
662
|
const errorRows = snap.errorText ? 1 : 0;
|
|
558
|
-
const
|
|
559
|
-
const transcriptCapacity = Math.max(1, viewport - errorRows - scrollRows);
|
|
663
|
+
const transcriptCapacity = Math.max(1, viewport - errorRows);
|
|
560
664
|
const maxOffset = Math.max(0, transcript.length - transcriptCapacity);
|
|
561
665
|
if (this.scrollOffset > maxOffset) this.scrollOffset = maxOffset;
|
|
562
666
|
|
|
@@ -572,39 +676,33 @@ class TakeoverView implements Component, Focusable {
|
|
|
572
676
|
),
|
|
573
677
|
);
|
|
574
678
|
}
|
|
575
|
-
|
|
576
|
-
const capacity = Math.max(
|
|
577
|
-
1,
|
|
578
|
-
viewport - body.length - (this.scrollOffset > 0 ? 1 : 0),
|
|
579
|
-
);
|
|
580
679
|
const end = transcript.length - this.scrollOffset;
|
|
581
|
-
const visible = transcript.slice(
|
|
582
|
-
|
|
680
|
+
const visible = transcript.slice(
|
|
681
|
+
Math.max(0, end - Math.max(1, viewport - body.length)),
|
|
682
|
+
end,
|
|
683
|
+
);
|
|
684
|
+
if (visible.length === 0) body.push(theme.fg("dim", "waiting for output…"));
|
|
583
685
|
else body.push(...visible);
|
|
584
|
-
|
|
585
|
-
if (this.scrollOffset > 0) {
|
|
586
|
-
body.push(
|
|
587
|
-
truncateToWidth(
|
|
588
|
-
theme.fg("dim", `... ${this.scrollOffset} lines below · ↓/pgdn`),
|
|
589
|
-
width,
|
|
590
|
-
),
|
|
591
|
-
);
|
|
592
|
-
}
|
|
593
686
|
while (body.length < viewport) body.push("");
|
|
594
687
|
lines.push(...body.slice(0, viewport));
|
|
595
688
|
|
|
596
|
-
lines.push(
|
|
689
|
+
lines.push(
|
|
690
|
+
this.rule(
|
|
691
|
+
width,
|
|
692
|
+
theme.fg("borderAccent", "─"),
|
|
693
|
+
this.scrollOffset > 0 ? theme.fg("dim", `↓ ${this.scrollOffset}`) : "",
|
|
694
|
+
),
|
|
695
|
+
);
|
|
597
696
|
lines.push(...this.input.render(width));
|
|
697
|
+
const hints = `${configuredKeys(this.keybindings, "tui.input.submit")} send · ${configuredKeys(this.keybindings, "app.interrupt")} back · ${configuredKeys(this.keybindings, "app.clear")} abort run · ${configuredKeys(this.keybindings, "tui.editor.cursorUp")}/${configuredKeys(this.keybindings, "tui.editor.cursorDown")} scroll · ${configuredKeys(this.keybindings, "tui.editor.pageUp")}/${configuredKeys(this.keybindings, "tui.editor.pageDown")} page`;
|
|
698
|
+
const compactHints = `${configuredKeys(this.keybindings, "tui.input.submit")} send · ${configuredKeys(this.keybindings, "app.interrupt")} back · ${configuredKeys(this.keybindings, "app.clear")} abort run · ${configuredKeys(this.keybindings, "tui.editor.cursorUp")}/${configuredKeys(this.keybindings, "tui.editor.cursorDown")} scroll`;
|
|
598
699
|
lines.push(
|
|
599
700
|
truncateToWidth(
|
|
600
|
-
theme.fg(
|
|
601
|
-
"dim",
|
|
602
|
-
`${configuredKeys(this.keybindings, "tui.input.submit")} send · ${configuredKeys(this.keybindings, "app.interrupt")} back · ${configuredKeys(this.keybindings, "app.clear")} abort run · ${configuredKeys(this.keybindings, "tui.editor.cursorUp")}/${configuredKeys(this.keybindings, "tui.editor.cursorDown")} scroll · ${configuredKeys(this.keybindings, "tui.editor.pageUp")}/${configuredKeys(this.keybindings, "tui.editor.pageDown")} page`,
|
|
603
|
-
),
|
|
701
|
+
theme.fg("dim", visibleWidth(hints) <= width ? hints : compactHints),
|
|
604
702
|
width,
|
|
605
703
|
),
|
|
606
704
|
);
|
|
607
|
-
lines.push(
|
|
705
|
+
lines.push(this.rule(width, theme.fg("borderAccent", "─")));
|
|
608
706
|
return lines;
|
|
609
707
|
}
|
|
610
708
|
|