@oh-my-pi/pi-tui 18.2.9 → 18.2.11
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/CHANGELOG.md +19 -0
- package/dist/types/render/render-utils.d.ts +2 -0
- package/dist/types/status-line/component.d.ts +2 -0
- package/dist/types/status-line/types.d.ts +2 -0
- package/dist/types/tools/eval.d.ts +15 -9
- package/dist/types/tools/task.d.ts +4 -0
- package/dist/types/tui.d.ts +8 -3
- package/package.json +9 -9
- package/src/overlays/settings-defs.ts +1 -0
- package/src/overlays/usage-dashboard.ts +98 -24
- package/src/prompt/welcome.ts +4 -1
- package/src/render/render-utils.ts +1 -1
- package/src/status-line/component.ts +24 -4
- package/src/status-line/segments.ts +8 -6
- package/src/status-line/types.ts +2 -0
- package/src/terminal-capabilities.ts +14 -5
- package/src/tools/eval-format/index.ts +1 -0
- package/src/tools/eval.ts +22 -14
- package/src/tools/task.ts +18 -0
- package/src/tui.ts +21 -10
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,25 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [18.2.11] - 2026-09-23
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- Added a Skills section to the settings.
|
|
10
|
+
- Enabled OSC 8 hyperlinks in rio.
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- Fixed scrolling startup release notes without requiring an initial Escape keypress.
|
|
15
|
+
- Improved usage quota display so names remain distinguishable and readable in narrow and multi-column dashboards.
|
|
16
|
+
|
|
17
|
+
## [18.2.10] - 2026-09-22
|
|
18
|
+
|
|
19
|
+
### Changed
|
|
20
|
+
|
|
21
|
+
- Added support for multiple concurrent TUI paint listeners to enable simultaneous session recording and streaming
|
|
22
|
+
- Coalesced status event updates for progress-based operations to reduce TUI render overhead
|
|
23
|
+
|
|
5
24
|
## [18.2.9] - 2026-09-22
|
|
6
25
|
|
|
7
26
|
### Added
|
|
@@ -96,6 +96,8 @@ export declare const TRUNCATE_LENGTHS: {
|
|
|
96
96
|
};
|
|
97
97
|
/** Human-readable key currently bound to tool-output expansion, e.g. `Ctrl+O`. */
|
|
98
98
|
export declare function expandKeyHint(): string;
|
|
99
|
+
/** Keep both ends of a single-line label without splitting wide characters. */
|
|
100
|
+
export declare function truncateMiddleToWidth(text: string, maxWidth: number): string;
|
|
99
101
|
/** Select a leading line window and count the lines hidden after it. */
|
|
100
102
|
export declare function cappedHeadLines(lines: readonly string[], max: number): {
|
|
101
103
|
lines: readonly string[];
|
|
@@ -86,6 +86,8 @@ export declare class StatusLineComponent<TSession extends StatusLineSession = St
|
|
|
86
86
|
setStreamStatus(status: {
|
|
87
87
|
viewers: number;
|
|
88
88
|
} | null): void;
|
|
89
|
+
/** Toggle the `● REC` badge shown while `/record` captures the screen. */
|
|
90
|
+
setRecording(recording: boolean): void;
|
|
89
91
|
/** Set the callback that presents detected Codex reset celebrations, or clear it with `undefined`. */
|
|
90
92
|
setCodexResetFireworksHandler(handler: ((event: CodexResetFireworksEvent) => void) | undefined): void;
|
|
91
93
|
setHookStatus(key: string, text: string | undefined): void;
|
|
@@ -54,10 +54,7 @@ interface EvalRenderCellArg {
|
|
|
54
54
|
code?: string;
|
|
55
55
|
title?: string;
|
|
56
56
|
}
|
|
57
|
-
interface EvalRenderArgs {
|
|
58
|
-
language?: string;
|
|
59
|
-
code?: string;
|
|
60
|
-
title?: string;
|
|
57
|
+
interface EvalRenderArgs extends EvalRenderCellArg {
|
|
61
58
|
cells?: EvalRenderCellArg[];
|
|
62
59
|
__partialJson?: string;
|
|
63
60
|
}
|
|
@@ -68,11 +65,20 @@ interface EvalRenderContext {
|
|
|
68
65
|
timeout?: number;
|
|
69
66
|
}
|
|
70
67
|
/**
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
|
|
68
|
+
* Coalescing key of a progress-snapshot event: `agent` and `judge_batch`
|
|
69
|
+
* events keyed by `id`, where only the newest snapshot matters. Discrete
|
|
70
|
+
* actions (everything else) have no key. Shared by {@link upsertStatusEvent}
|
|
71
|
+
* and the executors' display collectors so both coalesce identically.
|
|
72
|
+
*/
|
|
73
|
+
export declare function statusEventKey(event: {
|
|
74
|
+
op: string;
|
|
75
|
+
[key: string]: unknown;
|
|
76
|
+
}): string | undefined;
|
|
77
|
+
/**
|
|
78
|
+
* Append or replace a status event. Progress snapshots (see
|
|
79
|
+
* {@link statusEventKey}) coalesce in place, preserving first-seen order; every
|
|
80
|
+
* other op is a discrete action and simply appends. Keeps the persisted event
|
|
81
|
+
* list bounded even when a subagent or batch emits hundreds of progress ticks.
|
|
76
82
|
*/
|
|
77
83
|
export declare function upsertStatusEvent(events: EvalStatusEvent[], event: EvalStatusEvent): void;
|
|
78
84
|
/** Render eval code cells, structured display output, and progress events. */
|
|
@@ -237,6 +237,8 @@ export interface AgentProgress {
|
|
|
237
237
|
resolvedThinkingLevel?: ConfiguredThinkingLevel;
|
|
238
238
|
/** True when {@link resolvedModel} is the target of an active retry fallback (not the originally configured model). Lets observer-only UIs (collab guests, Agent Hub rows with no live session) flag the fallback and keep the provider. */
|
|
239
239
|
resolvedModelIsFallback?: boolean;
|
|
240
|
+
/** Extension routing note (e.g. model-pools) explaining why {@link resolvedModel} was chosen. */
|
|
241
|
+
resolvedModelRoute?: string;
|
|
240
242
|
/** True when a live advisor was attached to this run's session, not merely enabled in settings. */
|
|
241
243
|
advisor?: boolean;
|
|
242
244
|
/** Data extracted by registered subprocess tool handlers (keyed by tool name) */
|
|
@@ -313,6 +315,8 @@ export interface SingleResult {
|
|
|
313
315
|
resolvedThinkingLevel?: ConfiguredThinkingLevel;
|
|
314
316
|
/** True when {@link resolvedModel} is the target of an active retry fallback. Mirrors {@link AgentProgress.resolvedModelIsFallback} onto the settled result. */
|
|
315
317
|
resolvedModelIsFallback?: boolean;
|
|
318
|
+
/** Mirrors {@link AgentProgress.resolvedModelRoute} onto the settled result. */
|
|
319
|
+
resolvedModelRoute?: string;
|
|
316
320
|
/** Retains {@link AgentProgress.advisor} after the advised session is disposed. */
|
|
317
321
|
advisor?: boolean;
|
|
318
322
|
error?: string;
|
package/dist/types/tui.d.ts
CHANGED
|
@@ -30,9 +30,11 @@ export interface TuiPaint {
|
|
|
30
30
|
readonly columns: number;
|
|
31
31
|
readonly rows: number;
|
|
32
32
|
}
|
|
33
|
+
/** Observer of completed terminal paints; see {@link TUI.addPaintListener}. */
|
|
34
|
+
export type PaintListener = (paint: TuiPaint) => void;
|
|
33
35
|
export interface TUIOptions {
|
|
34
36
|
renderScheduler?: RenderScheduler;
|
|
35
|
-
onPaint?:
|
|
37
|
+
onPaint?: PaintListener;
|
|
36
38
|
}
|
|
37
39
|
/** Physical terminal dimensions supplied to a frame provider. */
|
|
38
40
|
export interface ViewportSize {
|
|
@@ -290,8 +292,11 @@ export declare class TUI extends Container {
|
|
|
290
292
|
hidden: boolean;
|
|
291
293
|
}[];
|
|
292
294
|
constructor(terminal: Terminal, showHardwareCursor?: boolean, options?: TUIOptions);
|
|
293
|
-
/**
|
|
294
|
-
|
|
295
|
+
/**
|
|
296
|
+
* Observe completed terminal paints; returns the unsubscribe. Independent
|
|
297
|
+
* observers (live stream publisher, session recorder) coexist.
|
|
298
|
+
*/
|
|
299
|
+
addPaintListener(listener: PaintListener): () => void;
|
|
295
300
|
/** Install the product-owned bounded frame provider. */
|
|
296
301
|
setFrameProvider(provider: TerminalFrameProvider | undefined): void;
|
|
297
302
|
get fullRedraws(): number;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@oh-my-pi/pi-tui",
|
|
4
|
-
"version": "18.2.
|
|
4
|
+
"version": "18.2.11",
|
|
5
5
|
"description": "Terminal User Interface library with differential rendering for efficient text-based applications",
|
|
6
6
|
"homepage": "https://omp.sh",
|
|
7
7
|
"author": "Stencil Labs, Inc.",
|
|
@@ -37,14 +37,14 @@
|
|
|
37
37
|
"fmt": "oxfmt --no-error-on-unmatched-pattern 'src/**/*.{ts,tsx}' '{test,bench,examples,scripts}/**/*.ts' '*.ts'"
|
|
38
38
|
},
|
|
39
39
|
"dependencies": {
|
|
40
|
-
"@oh-my-pi/omptype": "18.2.
|
|
41
|
-
"@oh-my-pi/pi-agent-core": "18.2.
|
|
42
|
-
"@oh-my-pi/pi-ai": "18.2.
|
|
43
|
-
"@oh-my-pi/pi-catalog": "18.2.
|
|
44
|
-
"@oh-my-pi/pi-natives": "18.2.
|
|
45
|
-
"@oh-my-pi/pi-utils": "18.2.
|
|
46
|
-
"@oh-my-pi/pi-wire": "18.2.
|
|
47
|
-
"@oh-my-pi/snapcompact": "18.2.
|
|
40
|
+
"@oh-my-pi/omptype": "18.2.11",
|
|
41
|
+
"@oh-my-pi/pi-agent-core": "18.2.11",
|
|
42
|
+
"@oh-my-pi/pi-ai": "18.2.11",
|
|
43
|
+
"@oh-my-pi/pi-catalog": "18.2.11",
|
|
44
|
+
"@oh-my-pi/pi-natives": "18.2.11",
|
|
45
|
+
"@oh-my-pi/pi-utils": "18.2.11",
|
|
46
|
+
"@oh-my-pi/pi-wire": "18.2.11",
|
|
47
|
+
"@oh-my-pi/snapcompact": "18.2.11"
|
|
48
48
|
},
|
|
49
49
|
"devDependencies": {
|
|
50
50
|
"kitty-vt-wasm": "^0.2.0"
|
|
@@ -64,6 +64,7 @@ export const TAB_GROUPS: Record<SettingTab, readonly string[]> = {
|
|
|
64
64
|
"Power",
|
|
65
65
|
"Agent",
|
|
66
66
|
"Git",
|
|
67
|
+
"Skills",
|
|
67
68
|
],
|
|
68
69
|
context: ["General", "Compaction", "Rules (TTSR)", "Experimental"],
|
|
69
70
|
memory: ["General", "Auto-Learn", "Mnemopi", "Hindsight", "Sharpshooter"],
|
|
@@ -7,7 +7,16 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import * as os from "node:os";
|
|
9
9
|
import { resolveUsedFraction, type UsageLimit, type UsageReport } from "@oh-my-pi/pi-ai";
|
|
10
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
type Component,
|
|
12
|
+
matchesKey,
|
|
13
|
+
replaceTabs,
|
|
14
|
+
routeSgrMouseInput,
|
|
15
|
+
sliceWithWidth,
|
|
16
|
+
truncateToWidth,
|
|
17
|
+
visibleWidth,
|
|
18
|
+
wrapTextWithAnsi,
|
|
19
|
+
} from "../index";
|
|
11
20
|
import { colorLuma, formatDuration, hexToRgb, rgbToHex, sanitizeText } from "@oh-my-pi/pi-utils";
|
|
12
21
|
import { formatProviderName } from "../chrome/format";
|
|
13
22
|
import {
|
|
@@ -19,6 +28,8 @@ import {
|
|
|
19
28
|
import { colorToAnsi } from "../theme/color";
|
|
20
29
|
import { ensureThemeSync, theme } from "../theme/theme";
|
|
21
30
|
import { formatAbsoluteOnlyAmount } from "../prompt/usage-amounts";
|
|
31
|
+
import { truncateMiddleToWidth } from "../render/render-utils";
|
|
32
|
+
import { sanitizeDisplayLine } from "./extensions/display-text";
|
|
22
33
|
import {
|
|
23
34
|
matchesSelectCancel,
|
|
24
35
|
matchesSelectDown,
|
|
@@ -333,6 +344,16 @@ export function formatActivityErrorDetail(error: string, homeDir = os.homedir())
|
|
|
333
344
|
const CARD_MIN_WIDTH = 32;
|
|
334
345
|
const CARD_GUTTER = 3;
|
|
335
346
|
const CARD_MAX_WINDOWS = 4;
|
|
347
|
+
const CARD_MIN_BAR_WIDTH = 12;
|
|
348
|
+
const CARD_MAX_LABEL_LINES = 2;
|
|
349
|
+
|
|
350
|
+
interface CardRowLayout {
|
|
351
|
+
labelWidth: number;
|
|
352
|
+
resetWidth: number;
|
|
353
|
+
barWidth: number;
|
|
354
|
+
stacked: boolean;
|
|
355
|
+
labelHeights: number[];
|
|
356
|
+
}
|
|
336
357
|
|
|
337
358
|
export class UsageDashboardComponent implements Component {
|
|
338
359
|
#options: UsageDashboardOptions;
|
|
@@ -423,7 +444,7 @@ export class UsageDashboardComponent implements Component {
|
|
|
423
444
|
return `${theme.fg(this.#statusColor(status), bar)}${theme.fg("dim", empty)}`;
|
|
424
445
|
}
|
|
425
446
|
|
|
426
|
-
#renderCardLines(card: ProviderCard, width: number): string[] {
|
|
447
|
+
#renderCardLines(card: ProviderCard, width: number, labels: string[][], layout: CardRowLayout): string[] {
|
|
427
448
|
const lines: string[] = [];
|
|
428
449
|
const cardStatus = card.unlimited ? "ok" : aggregateStatus(card.windows);
|
|
429
450
|
const accountsText = card.accounts > 1 ? theme.fg("dim", `${card.accounts} accts`) : "";
|
|
@@ -457,35 +478,33 @@ export class UsageDashboardComponent implements Component {
|
|
|
457
478
|
}
|
|
458
479
|
|
|
459
480
|
const hidden = card.windows.length - CARD_MAX_WINDOWS;
|
|
460
|
-
const
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
? truncateToWidth(window.windowTag, Math.max(2, Math.floor(labelWidth / 2) - 1))
|
|
473
|
-
: "";
|
|
474
|
-
const baseWidth = tagPlain ? labelWidth - visibleWidth(tagPlain) - 1 : labelWidth;
|
|
475
|
-
const basePlain = truncateToWidth(window.label, baseWidth).padEnd(baseWidth);
|
|
476
|
-
const label = tagPlain
|
|
477
|
-
? `${theme.fg("muted", basePlain)} ${theme.fg("dim", tagPlain)}`
|
|
478
|
-
: theme.fg("muted", basePlain);
|
|
481
|
+
const { labelWidth, resetWidth, barWidth, stacked, labelHeights } = layout;
|
|
482
|
+
const contentWidth = Math.max(1, width - 2);
|
|
483
|
+
for (let index = 0; index < Math.min(card.windows.length, CARD_MAX_WINDOWS); index++) {
|
|
484
|
+
const window = card.windows[index]!;
|
|
485
|
+
const labelLines = labels[index]!;
|
|
486
|
+
const label = labelLines[0] ?? "";
|
|
487
|
+
const prefix = stacked ? "" : `${label}${" ".repeat(labelWidth - visibleWidth(label))} `;
|
|
488
|
+
if (stacked) {
|
|
489
|
+
for (let line = 0; line < labelHeights[index]; line++) {
|
|
490
|
+
lines.push(` ${labelLines[line] ?? ""}`);
|
|
491
|
+
}
|
|
492
|
+
}
|
|
479
493
|
if (window.fraction === undefined) {
|
|
480
494
|
const text = theme.fg("dim", window.usedText ?? "no data");
|
|
481
|
-
|
|
495
|
+
for (const line of wrapTextWithAnsi(`${prefix}${text}`, contentWidth)) lines.push(` ${line}`);
|
|
482
496
|
continue;
|
|
483
497
|
}
|
|
484
498
|
const freePct = Math.max(0, Math.round((1 - window.fraction) * 100));
|
|
485
499
|
const pctText = theme.fg(this.#statusColor(window.status), `${freePct}%`.padStart(5));
|
|
486
500
|
const resetPlain = window.resetMs !== undefined ? formatDuration(window.resetMs) : "";
|
|
487
501
|
const resetText = resetWidth > 0 ? ` ${theme.fg("dim", resetPlain.padStart(resetWidth))}` : "";
|
|
488
|
-
|
|
502
|
+
for (const line of wrapTextWithAnsi(
|
|
503
|
+
`${prefix}${this.#miniBar(window.fraction, window.status, barWidth)}${pctText}${resetText}`,
|
|
504
|
+
contentWidth,
|
|
505
|
+
)) {
|
|
506
|
+
lines.push(` ${line}`);
|
|
507
|
+
}
|
|
489
508
|
}
|
|
490
509
|
if (hidden > 0) lines.push(` ${theme.fg("dim", `+${hidden} more`)}`);
|
|
491
510
|
return lines;
|
|
@@ -499,7 +518,62 @@ export class UsageDashboardComponent implements Component {
|
|
|
499
518
|
const cardWidth = Math.floor((innerWidth - (columns - 1) * CARD_GUTTER) / columns);
|
|
500
519
|
const lines: string[] = [];
|
|
501
520
|
for (let start = 0; start < active.length; start += columns) {
|
|
502
|
-
const
|
|
521
|
+
const cards = active.slice(start, start + columns);
|
|
522
|
+
const windows = cards.map(card => card.windows.slice(0, CARD_MAX_WINDOWS));
|
|
523
|
+
const labels = windows.map(rows =>
|
|
524
|
+
rows.map(window => {
|
|
525
|
+
const label = theme.fg("muted", sanitizeDisplayLine(window.label));
|
|
526
|
+
const tag = window.windowTag ? sanitizeDisplayLine(window.windowTag) : "";
|
|
527
|
+
return tag ? `${label} ${theme.fg("dim", tag)}` : label;
|
|
528
|
+
}),
|
|
529
|
+
);
|
|
530
|
+
// One geometry per grid row: labels, bars, and resets share columns,
|
|
531
|
+
// and every card stacks together when inline bars would be too short.
|
|
532
|
+
const labelWidth = labels.reduce(
|
|
533
|
+
(max, rows) => rows.reduce((width, label) => Math.max(width, visibleWidth(label)), max),
|
|
534
|
+
0,
|
|
535
|
+
);
|
|
536
|
+
const resetWidth = windows.reduce(
|
|
537
|
+
(max, rows) =>
|
|
538
|
+
rows.reduce(
|
|
539
|
+
(width, window) =>
|
|
540
|
+
Math.max(width, window.resetMs !== undefined ? formatDuration(window.resetMs).length : 0),
|
|
541
|
+
max,
|
|
542
|
+
),
|
|
543
|
+
0,
|
|
544
|
+
);
|
|
545
|
+
const contentWidth = Math.max(1, cardWidth - 2);
|
|
546
|
+
const suffixWidth = 5 + (resetWidth > 0 ? resetWidth + 1 : 0);
|
|
547
|
+
const inlineBarWidth = contentWidth - labelWidth - 1 - suffixWidth;
|
|
548
|
+
const stacked = inlineBarWidth < CARD_MIN_BAR_WIDTH;
|
|
549
|
+
const labelLines = labels.map(rows =>
|
|
550
|
+
rows.map(label => {
|
|
551
|
+
if (!stacked) return [label];
|
|
552
|
+
// Bound wrapping work as well as height, keeping the suffix that
|
|
553
|
+
// distinguishes model-specific quota buckets.
|
|
554
|
+
const bounded = truncateMiddleToWidth(label, contentWidth * CARD_MAX_LABEL_LINES);
|
|
555
|
+
const lines = wrapTextWithAnsi(bounded, contentWidth);
|
|
556
|
+
if (lines.length <= CARD_MAX_LABEL_LINES) return lines;
|
|
557
|
+
// Word wrapping can waste enough cells to create a third line even
|
|
558
|
+
// when the label fits. Use a grapheme-safe column split in that case.
|
|
559
|
+
const first = sliceWithWidth(bounded, 0, contentWidth, true);
|
|
560
|
+
const rest = sliceWithWidth(bounded, first.width, visibleWidth(bounded) - first.width, true);
|
|
561
|
+
return [first.text, truncateMiddleToWidth(rest.text, contentWidth)];
|
|
562
|
+
}),
|
|
563
|
+
);
|
|
564
|
+
const labelHeights = Array.from({ length: CARD_MAX_WINDOWS }, (_, index) =>
|
|
565
|
+
Math.max(0, ...labelLines.map(rows => rows[index]?.length ?? 0)),
|
|
566
|
+
);
|
|
567
|
+
const layout: CardRowLayout = {
|
|
568
|
+
labelWidth,
|
|
569
|
+
resetWidth,
|
|
570
|
+
barWidth: Math.max(1, stacked ? contentWidth - suffixWidth : inlineBarWidth),
|
|
571
|
+
stacked,
|
|
572
|
+
labelHeights,
|
|
573
|
+
};
|
|
574
|
+
const rowCards = cards.map((card, index) =>
|
|
575
|
+
this.#renderCardLines(card, cardWidth, labelLines[index]!, layout),
|
|
576
|
+
);
|
|
503
577
|
const height = Math.max(...rowCards.map(card => card.length));
|
|
504
578
|
for (let lineIdx = 0; lineIdx < height; lineIdx++) {
|
|
505
579
|
const segments = rowCards.map(card => {
|
package/src/prompt/welcome.ts
CHANGED
|
@@ -185,10 +185,13 @@ export class WelcomeComponent implements Component {
|
|
|
185
185
|
this.#requestRender();
|
|
186
186
|
this.#animTimer = setInterval(() => {
|
|
187
187
|
const elapsed = performance.now() - (this.#animStart ?? 0);
|
|
188
|
+
const requestRender = this.#requestRender;
|
|
188
189
|
if (elapsed >= INTRO_MS) {
|
|
189
190
|
this.#stopAnimation();
|
|
190
191
|
}
|
|
191
|
-
|
|
192
|
+
// Stopping clears the callback, but the settled frame must still paint
|
|
193
|
+
// so an oversized startup header can retire into native scrollback.
|
|
194
|
+
requestRender?.();
|
|
192
195
|
}, INTRO_TICK_MS);
|
|
193
196
|
}
|
|
194
197
|
|
|
@@ -211,7 +211,7 @@ export function expandKeyHint(): string {
|
|
|
211
211
|
// Text Truncation Utilities
|
|
212
212
|
// =============================================================================
|
|
213
213
|
/** Keep both ends of a single-line label without splitting wide characters. */
|
|
214
|
-
function truncateMiddleToWidth(text: string, maxWidth: number): string {
|
|
214
|
+
export function truncateMiddleToWidth(text: string, maxWidth: number): string {
|
|
215
215
|
const width = visibleWidth(text);
|
|
216
216
|
if (width <= maxWidth) return text;
|
|
217
217
|
if (maxWidth <= 1) return maxWidth === 1 ? "…" : "";
|
|
@@ -55,6 +55,18 @@ const BRAND_FADE_MS = 450;
|
|
|
55
55
|
/** Repaint cadence while the brand fade is in flight (rust omp's `FADE_FRAME`). */
|
|
56
56
|
const BRAND_FADE_FRAME_MS = 40;
|
|
57
57
|
|
|
58
|
+
/**
|
|
59
|
+
* Providers whose subscription quota is a single monthly bucket, so their
|
|
60
|
+
* `monthly`/`30d` window is the one the usage segment must show. Providers that
|
|
61
|
+
* merely report a monthly side-counter (GitHub Copilot's premium requests) stay
|
|
62
|
+
* out: their monthly row is not the session quota.
|
|
63
|
+
*/
|
|
64
|
+
const MONTHLY_SUBSCRIPTION_PROVIDERS: Record<string, true> = {
|
|
65
|
+
"alibaba-token-plan": true,
|
|
66
|
+
cursor: true,
|
|
67
|
+
"opencode-go": true,
|
|
68
|
+
};
|
|
69
|
+
|
|
58
70
|
/** A displayable limit after provider, account, model, and window filtering. */
|
|
59
71
|
interface UsageWindowCandidate {
|
|
60
72
|
id?: string;
|
|
@@ -529,6 +541,7 @@ export class StatusLineComponent<TSession extends StatusLineSession = StatusLine
|
|
|
529
541
|
#vibeWorkerTokenRate: (() => number | null) | null = null;
|
|
530
542
|
#collabStatus: CollabStatus | null = null;
|
|
531
543
|
#streamStatus: { viewers: number } | null = null;
|
|
544
|
+
#recording = false;
|
|
532
545
|
#focusedAgentId: string | undefined;
|
|
533
546
|
#activeRepoCache: ActiveRepoCache | undefined;
|
|
534
547
|
|
|
@@ -930,6 +943,13 @@ export class StatusLineComponent<TSession extends StatusLineSession = StatusLine
|
|
|
930
943
|
this.#invalidateStatusLineRenderCache();
|
|
931
944
|
}
|
|
932
945
|
|
|
946
|
+
/** Toggle the `● REC` badge shown while `/record` captures the screen. */
|
|
947
|
+
setRecording(recording: boolean): void {
|
|
948
|
+
if (this.#recording === recording) return;
|
|
949
|
+
this.#recording = recording;
|
|
950
|
+
this.#invalidateStatusLineRenderCache();
|
|
951
|
+
}
|
|
952
|
+
|
|
933
953
|
/** Set the callback that presents detected Codex reset celebrations, or clear it with `undefined`. */
|
|
934
954
|
setCodexResetFireworksHandler(handler: ((event: CodexResetFireworksEvent) => void) | undefined): void {
|
|
935
955
|
this.#onCodexResetFireworks = handler;
|
|
@@ -1834,6 +1854,8 @@ export class StatusLineComponent<TSession extends StatusLineSession = StatusLine
|
|
|
1834
1854
|
const activeModelId = normalizeUsageScopeValue(context.modelId);
|
|
1835
1855
|
const activeAntigravityCounter =
|
|
1836
1856
|
context.provider === "google-antigravity" ? getAntigravityCounterKeyForModel(context.modelId) : undefined;
|
|
1857
|
+
const monthlySubscriptionProvider =
|
|
1858
|
+
context.provider !== undefined && MONTHLY_SUBSCRIPTION_PROVIDERS[context.provider] === true;
|
|
1837
1859
|
const scopeGroups = new Map<string, UsageScopeGroup>();
|
|
1838
1860
|
for (const report of reports) {
|
|
1839
1861
|
if (!report || typeof report !== "object") continue;
|
|
@@ -1896,10 +1918,7 @@ export class StatusLineComponent<TSession extends StatusLineSession = StatusLine
|
|
|
1896
1918
|
: undefined;
|
|
1897
1919
|
const windowClass =
|
|
1898
1920
|
subscriptionWindow ??
|
|
1899
|
-
((
|
|
1900
|
-
(windowId === "monthly" || windowId === "30d")
|
|
1901
|
-
? "monthly"
|
|
1902
|
-
: undefined);
|
|
1921
|
+
(monthlySubscriptionProvider && (windowId === "monthly" || windowId === "30d") ? "monthly" : undefined);
|
|
1903
1922
|
if (!windowClass) continue;
|
|
1904
1923
|
|
|
1905
1924
|
const modelId = normalizeUsageScopeValue("modelId" in scope ? scope.modelId : undefined);
|
|
@@ -2174,6 +2193,7 @@ export class StatusLineComponent<TSession extends StatusLineSession = StatusLine
|
|
|
2174
2193
|
vim: this.#vimStatus,
|
|
2175
2194
|
collab: this.#collabStatus,
|
|
2176
2195
|
stream: this.#streamStatus,
|
|
2196
|
+
recording: this.#recording,
|
|
2177
2197
|
usageStats,
|
|
2178
2198
|
contextPercent,
|
|
2179
2199
|
contextTokens,
|
|
@@ -758,9 +758,11 @@ const collabSegment: StatusLineSegment = {
|
|
|
758
758
|
const streamSegment: StatusLineSegment = {
|
|
759
759
|
id: "stream",
|
|
760
760
|
render(ctx) {
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
761
|
+
const badges: string[] = [];
|
|
762
|
+
if (ctx.stream) badges.push(`● LIVE ${statusValue(ctx, `${ctx.stream.viewers}`)}`);
|
|
763
|
+
if (ctx.recording) badges.push("● REC");
|
|
764
|
+
if (badges.length === 0) return { content: "", visible: false };
|
|
765
|
+
return { content: theme.fg("thinkingHigh", badges.join(" ")), visible: true };
|
|
764
766
|
},
|
|
765
767
|
};
|
|
766
768
|
|
|
@@ -879,9 +881,9 @@ const usageSegment: StatusLineSegment = {
|
|
|
879
881
|
parts.push(formatQuotaWindow(ctx, "7d", u.sevenDay.percent, u.sevenDay.resetHours, "h", "round"));
|
|
880
882
|
}
|
|
881
883
|
if (u.monthly) {
|
|
882
|
-
//
|
|
883
|
-
//
|
|
884
|
-
// "1% used"; OpenCode's endpoint
|
|
884
|
+
// Monthly-subscription providers only (the normalizer gates the class).
|
|
885
|
+
// Cursor and QwenCloud floor used percents upstream (Cursor's dashboard
|
|
886
|
+
// shows 1.88 → "1% used"; OpenCode's endpoint emits floored integers).
|
|
885
887
|
parts.push(formatQuotaWindow(ctx, "mo", u.monthly.percent, u.monthly.resetHours, "h", "floor"));
|
|
886
888
|
}
|
|
887
889
|
if (u.resetCredits) {
|
package/src/status-line/types.ts
CHANGED
|
@@ -125,6 +125,8 @@ export interface SegmentContext {
|
|
|
125
125
|
} | null;
|
|
126
126
|
collab: CollabStatus | null;
|
|
127
127
|
stream: { viewers: number } | null;
|
|
128
|
+
/** A `/record` capture of this screen is running. */
|
|
129
|
+
recording: boolean;
|
|
128
130
|
// Cached values for performance (computed once per render)
|
|
129
131
|
usageStats: {
|
|
130
132
|
input: number;
|
|
@@ -678,11 +678,20 @@ const KNOWN_TERMINALS = Object.freeze({
|
|
|
678
678
|
// OSC 66 text sizing are unverified, so those stay on conservative defaults;
|
|
679
679
|
// synchronized output is left to the runtime DECRQM probe.
|
|
680
680
|
otty: new TerminalInfo("otty", ImageProtocol.Kitty, true, true, NotifyProtocol.Osc99),
|
|
681
|
-
// rio
|
|
682
|
-
//
|
|
683
|
-
//
|
|
684
|
-
//
|
|
685
|
-
|
|
681
|
+
// rio publishes its supported escape sequences at
|
|
682
|
+
// rioterm.com/docs/escape-sequence-support: OSC 8 hyperlinks are opened via
|
|
683
|
+
// the hints system (Alt+click on Windows/Linux) — live-verified on
|
|
684
|
+
// rio 0.5.28/Win11 26100 where an OSC 8 span Alt+click opened the target
|
|
685
|
+
// URL in the browser, and an OSC 52 clipboard query round-tripped. Kitty
|
|
686
|
+
// graphics direct placement plus U=1 Unicode placeholders verified by the
|
|
687
|
+
// reporter (#12205). Notifications stay on BEL: rio parses OSC 9/777 into
|
|
688
|
+
// its own notifier, but its Windows path raises an unpackaged WinRT toast
|
|
689
|
+
// with AppId "Rio" that Windows silently drops without a registered AUMID
|
|
690
|
+
// shortcut — observed as a no-op live — so flipping it would only remove
|
|
691
|
+
// the D-Bus fallback for Linux rio users. DECCARA, screen-to-scrollback,
|
|
692
|
+
// OSC 99, and OSC 66 text sizing are outside rio's supported set and keep
|
|
693
|
+
// the conservative defaults.
|
|
694
|
+
rio: new TerminalInfo("rio", ImageProtocol.Kitty, true, true),
|
|
686
695
|
});
|
|
687
696
|
|
|
688
697
|
/** Resolve terminal identity from environment markers used by common emulators. */
|
|
@@ -7,6 +7,7 @@ export * from "./python";
|
|
|
7
7
|
|
|
8
8
|
/** Formats an arbitrary eval-code prefix for display without changing the executed source. */
|
|
9
9
|
export function formatEvalCodeForDisplay(source: string, language: EvalLanguage): string {
|
|
10
|
+
if (/^\s*%/.test(source)) return source;
|
|
10
11
|
switch (language) {
|
|
11
12
|
case "js":
|
|
12
13
|
return formatJavaScriptForDisplay(source);
|
package/src/tools/eval.ts
CHANGED
|
@@ -93,10 +93,7 @@ interface EvalRenderCellArg {
|
|
|
93
93
|
title?: string;
|
|
94
94
|
}
|
|
95
95
|
|
|
96
|
-
interface EvalRenderArgs {
|
|
97
|
-
language?: string;
|
|
98
|
-
code?: string;
|
|
99
|
-
title?: string;
|
|
96
|
+
interface EvalRenderArgs extends EvalRenderCellArg {
|
|
100
97
|
cells?: EvalRenderCellArg[];
|
|
101
98
|
__partialJson?: string;
|
|
102
99
|
}
|
|
@@ -126,10 +123,9 @@ function getRenderCells(args: EvalRenderArgs | undefined): EvalRenderCell[] {
|
|
|
126
123
|
for (const cell of raw) {
|
|
127
124
|
if (!cell || typeof cell !== "object") continue;
|
|
128
125
|
const language = normalizeRenderLanguage(typeof cell.language === "string" ? cell.language : undefined);
|
|
129
|
-
const code = typeof cell.code === "string" ? cell.code : "";
|
|
130
126
|
out.push({
|
|
131
127
|
language,
|
|
132
|
-
code: formatEvalCodeForDisplay(code, language),
|
|
128
|
+
code: formatEvalCodeForDisplay(cell.code ?? "", language),
|
|
133
129
|
title: typeof cell.title === "string" ? cell.title : undefined,
|
|
134
130
|
});
|
|
135
131
|
}
|
|
@@ -139,16 +135,28 @@ function getRenderCells(args: EvalRenderArgs | undefined): EvalRenderCell[] {
|
|
|
139
135
|
type AgentEventStatus = "pending" | "running" | "completed" | "failed" | "aborted";
|
|
140
136
|
|
|
141
137
|
/**
|
|
142
|
-
*
|
|
143
|
-
*
|
|
144
|
-
*
|
|
145
|
-
*
|
|
146
|
-
* hundreds of throttled progress ticks.
|
|
138
|
+
* Coalescing key of a progress-snapshot event: `agent` and `judge_batch`
|
|
139
|
+
* events keyed by `id`, where only the newest snapshot matters. Discrete
|
|
140
|
+
* actions (everything else) have no key. Shared by {@link upsertStatusEvent}
|
|
141
|
+
* and the executors' display collectors so both coalesce identically.
|
|
147
142
|
*/
|
|
148
|
-
export function
|
|
143
|
+
export function statusEventKey(event: { op: string; [key: string]: unknown }): string | undefined {
|
|
149
144
|
if ((event.op === "agent" || event.op === "judge_batch") && typeof event.id === "string") {
|
|
150
|
-
|
|
151
|
-
|
|
145
|
+
return `${event.op}\0${event.id}`;
|
|
146
|
+
}
|
|
147
|
+
return undefined;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Append or replace a status event. Progress snapshots (see
|
|
152
|
+
* {@link statusEventKey}) coalesce in place, preserving first-seen order; every
|
|
153
|
+
* other op is a discrete action and simply appends. Keeps the persisted event
|
|
154
|
+
* list bounded even when a subagent or batch emits hundreds of progress ticks.
|
|
155
|
+
*/
|
|
156
|
+
export function upsertStatusEvent(events: EvalStatusEvent[], event: EvalStatusEvent): void {
|
|
157
|
+
const key = statusEventKey(event);
|
|
158
|
+
if (key !== undefined) {
|
|
159
|
+
const idx = events.findIndex(e => statusEventKey(e) === key);
|
|
152
160
|
if (idx >= 0) {
|
|
153
161
|
events[idx] = event;
|
|
154
162
|
return;
|
package/src/tools/task.ts
CHANGED
|
@@ -636,6 +636,18 @@ function renderDescriptionLines(description: string, prefix: string, width: numb
|
|
|
636
636
|
return wrapTextWithAnsi(description, contentWidth).map(line => `${boundedPrefix}${theme.fg("dim", line)}`);
|
|
637
637
|
}
|
|
638
638
|
|
|
639
|
+
/** `routed:` note from a `before_subagent_spawn` model replacement; empty when unrouted. */
|
|
640
|
+
function renderRouteLine(route: string | undefined, continuePrefix: string, maxWidth: number, theme: Theme): string[] {
|
|
641
|
+
if (!route) return [];
|
|
642
|
+
return [
|
|
643
|
+
truncateTaskRow(
|
|
644
|
+
`${continuePrefix}${theme.fg("dim", `routed: ${replaceTabs(sanitizeText(route))}`)}`,
|
|
645
|
+
maxWidth,
|
|
646
|
+
"",
|
|
647
|
+
),
|
|
648
|
+
];
|
|
649
|
+
}
|
|
650
|
+
|
|
639
651
|
/**
|
|
640
652
|
* Render streaming progress for a single agent.
|
|
641
653
|
*/
|
|
@@ -688,6 +700,7 @@ function renderAgentProgress(
|
|
|
688
700
|
if (fullDescription && !row.descriptionShown) {
|
|
689
701
|
lines.push(...renderDescriptionLines(fullDescription, continuePrefix, maxWidth, theme));
|
|
690
702
|
}
|
|
703
|
+
lines.push(...renderRouteLine(progress.resolvedModelRoute, continuePrefix, maxWidth, theme));
|
|
691
704
|
|
|
692
705
|
lines.push(...renderTaskSection(progress.assignment ?? progress.task, continuePrefix, expanded, theme));
|
|
693
706
|
|
|
@@ -1029,6 +1042,7 @@ function renderAgentResult(
|
|
|
1029
1042
|
if (fullDescription && !description) {
|
|
1030
1043
|
lines.push(...renderDescriptionLines(fullDescription, continuePrefix, maxWidth, theme));
|
|
1031
1044
|
}
|
|
1045
|
+
lines.push(...renderRouteLine(result.resolvedModelRoute, continuePrefix, maxWidth, theme));
|
|
1032
1046
|
|
|
1033
1047
|
lines.push(...renderTaskSection(result.assignment ?? result.task, continuePrefix, expanded, theme));
|
|
1034
1048
|
|
|
@@ -1858,6 +1872,8 @@ export interface AgentProgress {
|
|
|
1858
1872
|
resolvedThinkingLevel?: ConfiguredThinkingLevel;
|
|
1859
1873
|
/** True when {@link resolvedModel} is the target of an active retry fallback (not the originally configured model). Lets observer-only UIs (collab guests, Agent Hub rows with no live session) flag the fallback and keep the provider. */
|
|
1860
1874
|
resolvedModelIsFallback?: boolean;
|
|
1875
|
+
/** Extension routing note (e.g. model-pools) explaining why {@link resolvedModel} was chosen. */
|
|
1876
|
+
resolvedModelRoute?: string;
|
|
1861
1877
|
/** True when a live advisor was attached to this run's session, not merely enabled in settings. */
|
|
1862
1878
|
advisor?: boolean;
|
|
1863
1879
|
/** Data extracted by registered subprocess tool handlers (keyed by tool name) */
|
|
@@ -1935,6 +1951,8 @@ export interface SingleResult {
|
|
|
1935
1951
|
resolvedThinkingLevel?: ConfiguredThinkingLevel;
|
|
1936
1952
|
/** True when {@link resolvedModel} is the target of an active retry fallback. Mirrors {@link AgentProgress.resolvedModelIsFallback} onto the settled result. */
|
|
1937
1953
|
resolvedModelIsFallback?: boolean;
|
|
1954
|
+
/** Mirrors {@link AgentProgress.resolvedModelRoute} onto the settled result. */
|
|
1955
|
+
resolvedModelRoute?: string;
|
|
1938
1956
|
/** Retains {@link AgentProgress.advisor} after the advised session is disposed. */
|
|
1939
1957
|
advisor?: boolean;
|
|
1940
1958
|
error?: string;
|
package/src/tui.ts
CHANGED
|
@@ -137,9 +137,12 @@ export interface TuiPaint {
|
|
|
137
137
|
readonly rows: number;
|
|
138
138
|
}
|
|
139
139
|
|
|
140
|
+
/** Observer of completed terminal paints; see {@link TUI.addPaintListener}. */
|
|
141
|
+
export type PaintListener = (paint: TuiPaint) => void;
|
|
142
|
+
|
|
140
143
|
export interface TUIOptions {
|
|
141
144
|
renderScheduler?: RenderScheduler;
|
|
142
|
-
onPaint?:
|
|
145
|
+
onPaint?: PaintListener;
|
|
143
146
|
}
|
|
144
147
|
/** Physical terminal dimensions supplied to a frame provider. */
|
|
145
148
|
export interface ViewportSize {
|
|
@@ -789,7 +792,7 @@ export class TUI extends Container {
|
|
|
789
792
|
#debugNextWindowTop = 0;
|
|
790
793
|
#inputListeners = new Set<InputListener>();
|
|
791
794
|
#startListeners = new Set<StartListener>();
|
|
792
|
-
#
|
|
795
|
+
#paintListeners = new Set<PaintListener>();
|
|
793
796
|
|
|
794
797
|
/** Global callback for debug key (Shift+Ctrl+D). Called before input is forwarded to focused component. */
|
|
795
798
|
onDebug?: () => void;
|
|
@@ -922,7 +925,7 @@ export class TUI extends Container {
|
|
|
922
925
|
super();
|
|
923
926
|
this.terminal = terminal;
|
|
924
927
|
this.#renderScheduler = options?.renderScheduler ?? DEFAULT_RENDER_SCHEDULER;
|
|
925
|
-
|
|
928
|
+
if (options?.onPaint) this.#paintListeners.add(options.onPaint);
|
|
926
929
|
this.#showHardwareCursor = showHardwareCursor === undefined ? this.#showHardwareCursor : showHardwareCursor;
|
|
927
930
|
this.#watchdog = new LoopWatchdog();
|
|
928
931
|
}
|
|
@@ -931,9 +934,15 @@ export class TUI extends Container {
|
|
|
931
934
|
return mode === "append" || mode === "rebuild" || mode === "preserve" ? mode : "preserve";
|
|
932
935
|
}
|
|
933
936
|
|
|
934
|
-
/**
|
|
935
|
-
|
|
936
|
-
|
|
937
|
+
/**
|
|
938
|
+
* Observe completed terminal paints; returns the unsubscribe. Independent
|
|
939
|
+
* observers (live stream publisher, session recorder) coexist.
|
|
940
|
+
*/
|
|
941
|
+
addPaintListener(listener: PaintListener): () => void {
|
|
942
|
+
this.#paintListeners.add(listener);
|
|
943
|
+
return () => {
|
|
944
|
+
this.#paintListeners.delete(listener);
|
|
945
|
+
};
|
|
937
946
|
}
|
|
938
947
|
|
|
939
948
|
/** Install the product-owned bounded frame provider. */
|
|
@@ -2636,10 +2645,12 @@ export class TUI extends Container {
|
|
|
2636
2645
|
}
|
|
2637
2646
|
|
|
2638
2647
|
#notifyPaint(paint: TuiPaint): void {
|
|
2639
|
-
|
|
2640
|
-
|
|
2641
|
-
|
|
2642
|
-
|
|
2648
|
+
for (const listener of this.#paintListeners) {
|
|
2649
|
+
try {
|
|
2650
|
+
listener(paint);
|
|
2651
|
+
} catch (err) {
|
|
2652
|
+
logger.error("TUI paint listener failed", { err });
|
|
2653
|
+
}
|
|
2643
2654
|
}
|
|
2644
2655
|
}
|
|
2645
2656
|
|