@oh-my-pi/pi-tui 18.4.0 → 18.4.2
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 +42 -0
- package/dist/types/chat/image-loading.d.ts +24 -0
- package/dist/types/chat/transcript-entry.d.ts +9 -0
- package/dist/types/chrome/transcript-container.d.ts +24 -3
- package/dist/types/overlays/agents-hub.d.ts +7 -4
- package/dist/types/overlays/model-browser.d.ts +15 -0
- package/dist/types/overlays/rewind-selector.d.ts +4 -3
- package/dist/types/overlays/usage-dashboard.d.ts +9 -2
- package/dist/types/prompt/composer-attachments.d.ts +7 -0
- package/dist/types/prompt/composer-cache.d.ts +34 -16
- package/dist/types/prompt/composer.d.ts +15 -18
- package/dist/types/prompt/model-mention-autocomplete.d.ts +4 -1
- package/dist/types/prompt/welcome.d.ts +5 -2
- package/dist/types/render/width-aware-text.d.ts +6 -0
- package/dist/types/status-line/component.d.ts +13 -4
- package/dist/types/status-line/metrics.d.ts +0 -1
- package/dist/types/status-line/startup.d.ts +38 -0
- package/dist/types/status-line/types.d.ts +0 -2
- package/dist/types/terminal-capabilities.d.ts +10 -6
- package/dist/types/terminal.d.ts +7 -0
- package/dist/types/theme/color.d.ts +1 -0
- package/dist/types/theme/session-color.d.ts +3 -3
- package/dist/types/theme/shimmer.d.ts +1 -1
- package/dist/types/theme/theme-class.d.ts +1 -1
- package/dist/types/tools/output-meta.d.ts +2 -1
- package/dist/types/tools/vibe.d.ts +4 -0
- package/dist/types/tui.d.ts +3 -2
- package/package.json +9 -9
- package/src/chat/assistant-message.ts +27 -21
- package/src/chat/image-loading.ts +81 -0
- package/src/chat/tool-execution.ts +53 -17
- package/src/chat/transcript-entry.ts +16 -0
- package/src/chrome/transcript-container.ts +282 -86
- package/src/components/editor.ts +28 -6
- package/src/components/image.ts +25 -9
- package/src/components/loader.ts +37 -17
- package/src/overlays/agent-hub.ts +16 -6
- package/src/overlays/agents-hub.ts +7 -18
- package/src/overlays/copy-selector.ts +8 -31
- package/src/overlays/model-browser.ts +106 -25
- package/src/overlays/model-hub.ts +49 -16
- package/src/overlays/rewind-selector.ts +60 -13
- package/src/overlays/usage-dashboard.ts +47 -6
- package/src/prompt/composer-attachments.ts +54 -7
- package/src/prompt/composer-cache.ts +217 -196
- package/src/prompt/composer.ts +45 -72
- package/src/prompt/custom-editor.ts +24 -6
- package/src/prompt/model-mention-autocomplete.ts +22 -10
- package/src/prompt/welcome.ts +31 -28
- package/src/render/width-aware-text.ts +9 -0
- package/src/status-line/component.ts +286 -182
- package/src/status-line/metrics.ts +3 -21
- package/src/status-line/segments.ts +68 -79
- package/src/status-line/startup.ts +181 -0
- package/src/status-line/types.ts +0 -2
- package/src/terminal-capabilities.ts +12 -6
- package/src/terminal.ts +75 -20
- package/src/theme/color.ts +15 -0
- package/src/theme/session-color.ts +26 -4
- package/src/theme/shimmer.ts +55 -76
- package/src/theme/theme-class.ts +29 -12
- package/src/tools/output-meta.ts +11 -3
- package/src/tools/vibe.ts +15 -2
- package/src/tui.ts +208 -154
- package/src/utils.ts +15 -0
|
@@ -7,7 +7,6 @@ export interface BillingSummaryOptions {
|
|
|
7
7
|
readonly usingSubscription: boolean;
|
|
8
8
|
readonly premiumRequests: number;
|
|
9
9
|
readonly fractionDigits: number;
|
|
10
|
-
readonly startupPlaceholder?: boolean;
|
|
11
10
|
readonly pricingPeriod?: "peak" | "off-peak";
|
|
12
11
|
readonly advisor?: {
|
|
13
12
|
readonly cost: number;
|
|
@@ -30,24 +29,13 @@ function formatSpend(amount: number, usingSubscription: boolean, fractionDigits:
|
|
|
30
29
|
return `S${formatted}`;
|
|
31
30
|
}
|
|
32
31
|
|
|
33
|
-
function formatSpendPlaceholder(usingSubscription: boolean, uiTheme: Theme): string {
|
|
34
|
-
if (!usingSubscription) return "$…";
|
|
35
|
-
if (uiTheme.getSymbolPreset() === "nerd" && uiTheme.icon.subscription) {
|
|
36
|
-
return `${uiTheme.icon.subscription} …`;
|
|
37
|
-
}
|
|
38
|
-
return "S…";
|
|
39
|
-
}
|
|
40
|
-
|
|
41
32
|
function formatAdvisorSpend(
|
|
42
33
|
amount: number,
|
|
43
34
|
usingSubscription: boolean,
|
|
44
35
|
fractionDigits: number,
|
|
45
|
-
placeholder: boolean,
|
|
46
36
|
uiTheme: Theme,
|
|
47
37
|
): string {
|
|
48
|
-
const spend =
|
|
49
|
-
? formatSpendPlaceholder(usingSubscription, uiTheme)
|
|
50
|
-
: formatSpend(amount, usingSubscription, fractionDigits, uiTheme);
|
|
38
|
+
const spend = formatSpend(amount, usingSubscription, fractionDigits, uiTheme);
|
|
51
39
|
const icon = uiTheme.icon.advisor;
|
|
52
40
|
return icon && icon !== "(adv)" ? `${icon} ${spend}` : `${spend} (adv)`;
|
|
53
41
|
}
|
|
@@ -63,21 +51,16 @@ export function formatBillingSummary(options: BillingSummaryOptions, uiTheme: Th
|
|
|
63
51
|
return undefined;
|
|
64
52
|
}
|
|
65
53
|
|
|
66
|
-
const placeholder = options.startupPlaceholder === true;
|
|
67
54
|
const parts: string[] = [];
|
|
68
55
|
if (options.cost || options.pricingPeriod) {
|
|
69
|
-
parts.push(
|
|
70
|
-
placeholder
|
|
71
|
-
? formatSpendPlaceholder(options.usingSubscription, uiTheme)
|
|
72
|
-
: formatSpend(options.cost, options.usingSubscription, options.fractionDigits, uiTheme),
|
|
73
|
-
);
|
|
56
|
+
parts.push(formatSpend(options.cost, options.usingSubscription, options.fractionDigits, uiTheme));
|
|
74
57
|
} else if (options.usingSubscription) {
|
|
75
58
|
parts.push(
|
|
76
59
|
uiTheme.getSymbolPreset() === "nerd" && uiTheme.icon.subscription ? uiTheme.icon.subscription : "(sub)",
|
|
77
60
|
);
|
|
78
61
|
}
|
|
79
62
|
if (options.pricingPeriod) parts.push(options.pricingPeriod === "peak" ? "↑" : "↓");
|
|
80
|
-
if (premiumRequests) parts.push(`★ ${
|
|
63
|
+
if (premiumRequests) parts.push(`★ ${formatNumber(premiumRequests)}`);
|
|
81
64
|
if (advisorCost && options.advisor) {
|
|
82
65
|
const prefix = parts.length > 0 ? "+ " : "";
|
|
83
66
|
parts.push(
|
|
@@ -85,7 +68,6 @@ export function formatBillingSummary(options: BillingSummaryOptions, uiTheme: Th
|
|
|
85
68
|
advisorCost,
|
|
86
69
|
options.advisor.usingSubscription,
|
|
87
70
|
options.fractionDigits,
|
|
88
|
-
placeholder,
|
|
89
71
|
uiTheme,
|
|
90
72
|
)}`,
|
|
91
73
|
);
|
|
@@ -9,7 +9,6 @@ import {
|
|
|
9
9
|
getProjectDir,
|
|
10
10
|
normalizePathForComparison,
|
|
11
11
|
relativePathWithinNormalizedRoot,
|
|
12
|
-
relativePathWithinRoot,
|
|
13
12
|
} from "@oh-my-pi/pi-utils";
|
|
14
13
|
import { type SymbolKey, type Theme, type ThemeColor, theme } from "../theme";
|
|
15
14
|
import { shortenPath, TRUNCATE_LENGTHS, truncateToWidth } from "../render/render-utils";
|
|
@@ -28,15 +27,10 @@ export type { SegmentContext } from "./types";
|
|
|
28
27
|
// Helpers
|
|
29
28
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
30
29
|
|
|
31
|
-
const STARTUP_PLACEHOLDER = "…";
|
|
32
|
-
|
|
33
30
|
function withIcon(icon: string, text: string): string {
|
|
34
31
|
return icon ? `${icon} ${text}` : text;
|
|
35
32
|
}
|
|
36
33
|
|
|
37
|
-
function statusValue(ctx: SegmentContext, value: string): string {
|
|
38
|
-
return ctx.startupPlaceholder ? STARTUP_PLACEHOLDER : value;
|
|
39
|
-
}
|
|
40
34
|
/**
|
|
41
35
|
* Hash-derived accent ANSI for the session title (or preview stand-in title).
|
|
42
36
|
* Undefined when `statusLine.sessionAccent` is off or the session is unnamed,
|
|
@@ -75,14 +69,6 @@ function leadingGlyph(display: string): string {
|
|
|
75
69
|
return space === -1 ? display : display.slice(0, space);
|
|
76
70
|
}
|
|
77
71
|
|
|
78
|
-
function stripDisplayRoot(pwd: string): string {
|
|
79
|
-
for (const root of [path.join(os.homedir(), "Projects"), "/work"]) {
|
|
80
|
-
const relative = relativePathWithinRoot(root, pwd);
|
|
81
|
-
if (relative) return relative;
|
|
82
|
-
}
|
|
83
|
-
return pwd;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
72
|
/**
|
|
87
73
|
* Single-field usage counter (`token_in`, `token_out`, `cache_read`,
|
|
88
74
|
* `cache_write`). Hidden on zero, startup-placeholder aware, icon omitted when
|
|
@@ -101,7 +87,7 @@ function singleStatSegment(
|
|
|
101
87
|
if (!value) return { content: "", visible: false };
|
|
102
88
|
const content = formatMetric({
|
|
103
89
|
leading: theme.icon[iconKey] || undefined,
|
|
104
|
-
value:
|
|
90
|
+
value: formatNumber(value),
|
|
105
91
|
});
|
|
106
92
|
return { content: theme.fg(color, content ?? ""), visible: true };
|
|
107
93
|
},
|
|
@@ -126,28 +112,50 @@ const NORMALIZED_SCRATCH_ROOTS: readonly string[] = (() => {
|
|
|
126
112
|
return [...new Set(Array.from(roots, normalizePathForComparison))];
|
|
127
113
|
})();
|
|
128
114
|
|
|
129
|
-
interface
|
|
115
|
+
interface ProjectDirDisplay {
|
|
116
|
+
projectDir: string;
|
|
117
|
+
homeDir: string;
|
|
118
|
+
displayRoots: readonly string[] | undefined;
|
|
130
119
|
scratch: boolean;
|
|
131
|
-
|
|
120
|
+
displayPath: string;
|
|
132
121
|
}
|
|
133
122
|
|
|
134
|
-
|
|
123
|
+
let projectDirDisplay: ProjectDirDisplay | undefined;
|
|
135
124
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
125
|
+
/**
|
|
126
|
+
* Retain only the active directory: switching cwd re-resolves its aliases,
|
|
127
|
+
* while repeated paints reuse both scratch classification and root stripping.
|
|
128
|
+
* Display roots remain normalized while the home directory is unchanged.
|
|
129
|
+
*/
|
|
130
|
+
function getProjectDirDisplay(projectDir: string): ProjectDirDisplay {
|
|
131
|
+
const cached = projectDirDisplay;
|
|
132
|
+
if (cached?.projectDir === projectDir) return cached;
|
|
139
133
|
|
|
134
|
+
const homeDir = os.homedir();
|
|
135
|
+
let displayRoots = cached?.homeDir === homeDir ? cached.displayRoots : undefined;
|
|
140
136
|
const normalizedProjectDir = normalizePathForComparison(projectDir);
|
|
141
|
-
let
|
|
137
|
+
let scratch = false;
|
|
138
|
+
let displayPath = projectDir;
|
|
142
139
|
for (const normalizedRoot of NORMALIZED_SCRATCH_ROOTS) {
|
|
143
140
|
const relative = relativePathWithinNormalizedRoot(normalizedRoot, normalizedProjectDir);
|
|
144
141
|
if (relative !== null) {
|
|
145
|
-
|
|
142
|
+
scratch = true;
|
|
143
|
+
displayPath = relative || projectDir;
|
|
146
144
|
break;
|
|
147
145
|
}
|
|
148
146
|
}
|
|
149
|
-
|
|
150
|
-
|
|
147
|
+
if (!scratch) {
|
|
148
|
+
displayRoots ??= [path.join(homeDir, "Projects"), "/work"].map(normalizePathForComparison);
|
|
149
|
+
for (const root of displayRoots) {
|
|
150
|
+
const relative = relativePathWithinNormalizedRoot(root, normalizedProjectDir);
|
|
151
|
+
if (relative) {
|
|
152
|
+
displayPath = relative;
|
|
153
|
+
break;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
projectDirDisplay = { projectDir, homeDir, displayRoots, scratch, displayPath };
|
|
158
|
+
return projectDirDisplay;
|
|
151
159
|
}
|
|
152
160
|
|
|
153
161
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
@@ -160,7 +168,7 @@ const piSegment: StatusLineSegment = {
|
|
|
160
168
|
if (ctx.focusedAgentId) {
|
|
161
169
|
const icon = theme.icon.ghost ? `${theme.icon.ghost} ` : "";
|
|
162
170
|
return {
|
|
163
|
-
content: theme.fg("warning", `${icon}${
|
|
171
|
+
content: theme.fg("warning", `${icon}${ctx.focusedAgentId}`),
|
|
164
172
|
visible: true,
|
|
165
173
|
};
|
|
166
174
|
}
|
|
@@ -173,7 +181,7 @@ const piSegment: StatusLineSegment = {
|
|
|
173
181
|
// trailing space here would double the gap at the first separator (#11103).
|
|
174
182
|
const content =
|
|
175
183
|
ctx.turnElapsedMs != null
|
|
176
|
-
? `${brandSpinnerFrame(ctx.now?.getTime())} ${
|
|
184
|
+
? `${brandSpinnerFrame(ctx.now?.getTime())} ${brandTimer(ctx.turnElapsedMs)}`
|
|
177
185
|
: theme.icon.omp
|
|
178
186
|
? theme.icon.omp
|
|
179
187
|
: "";
|
|
@@ -220,7 +228,6 @@ const modelSegment: StatusLineSegment = {
|
|
|
220
228
|
if (modelName.startsWith("Claude ")) {
|
|
221
229
|
modelName = modelName.slice(7);
|
|
222
230
|
}
|
|
223
|
-
modelName = statusValue(ctx, modelName);
|
|
224
231
|
|
|
225
232
|
// Resolve the current thinking-level display ("◉ xhigh", "⟳ auto", …)
|
|
226
233
|
// when the model supports thinking and the segment isn't hiding it.
|
|
@@ -242,10 +249,6 @@ const modelSegment: StatusLineSegment = {
|
|
|
242
249
|
}
|
|
243
250
|
}
|
|
244
251
|
|
|
245
|
-
if (ctx.startupPlaceholder && thinkingDisplay) {
|
|
246
|
-
thinkingDisplay = withIcon(leadingGlyph(thinkingDisplay), STARTUP_PLACEHOLDER);
|
|
247
|
-
}
|
|
248
|
-
|
|
249
252
|
// Compact mode swaps the model icon for the thinking-level glyph and drops
|
|
250
253
|
// the " · <level>" tail, keeping the level visible as a single icon.
|
|
251
254
|
const compact = ctx.compactThinkingLevel && thinkingDisplay !== "";
|
|
@@ -337,7 +340,7 @@ function renderGoalMode(ctx: SegmentContext, mode: { enabled: boolean; paused: b
|
|
|
337
340
|
const parts: string[] = [withIcon(icon, "Goal")];
|
|
338
341
|
const showBudget = ctx.goalStatusInFooter === true;
|
|
339
342
|
if (showBudget && goal) {
|
|
340
|
-
parts.push(
|
|
343
|
+
parts.push(formatGoalBudget(goal.tokensUsed, goal.tokenBudget));
|
|
341
344
|
}
|
|
342
345
|
return {
|
|
343
346
|
content: color === "accent" ? accentFg(ctx, color, parts.join(" ")) : theme.fg(color, parts.join(" ")),
|
|
@@ -397,11 +400,13 @@ const modeSegment: StatusLineSegment = {
|
|
|
397
400
|
if (loop) {
|
|
398
401
|
const icon = loop.state === "paused" ? theme.icon.pause || theme.icon.loop : theme.icon.loop;
|
|
399
402
|
const color: ThemeColor = loop.state === "paused" ? "warning" : "customMessageLabel";
|
|
400
|
-
const
|
|
403
|
+
const stateLabel = loop.state === "waiting" ? "next prompt repeats" : loop.state;
|
|
404
|
+
const label = `Loop${loop.state === "waiting" ? ":" : ""} ${stateLabel}`;
|
|
405
|
+
const parts = [withIcon(icon, label)];
|
|
401
406
|
const limit = formatLoopLimit(loop.limit, ctx.now?.getTime());
|
|
402
|
-
if (limit) parts.push(
|
|
407
|
+
if (limit) parts.push(limit);
|
|
403
408
|
if (loop.condition) {
|
|
404
|
-
parts.push(
|
|
409
|
+
parts.push(summarizeLoopCondition(loop.condition, TRUNCATE_LENGTHS.SHORT));
|
|
405
410
|
}
|
|
406
411
|
return { content: theme.fg(color, parts.join(" ")), visible: true };
|
|
407
412
|
}
|
|
@@ -423,24 +428,14 @@ const pathSegment: StatusLineSegment = {
|
|
|
423
428
|
if (stripPrefix && ctx.worktree) {
|
|
424
429
|
const { projectName, worktreeName } = ctx.worktree;
|
|
425
430
|
const label = ctx.git.branch === worktreeName ? projectName : `${projectName}/${worktreeName}`;
|
|
426
|
-
const text =
|
|
427
|
-
? STARTUP_PLACEHOLDER
|
|
428
|
-
: fileHyperlink(getProjectDir(), clampPathLength(label, opts.maxLength ?? 40));
|
|
431
|
+
const text = fileHyperlink(getProjectDir(), clampPathLength(label, opts.maxLength ?? 40));
|
|
429
432
|
const content = withIcon(theme.icon.worktree, text);
|
|
430
433
|
return { content: theme.fg("statusLinePath", content), visible: true };
|
|
431
434
|
}
|
|
432
435
|
|
|
433
436
|
const projectDir = ctx.activeRepo?.cwd ?? getProjectDir();
|
|
434
|
-
const { scratch,
|
|
435
|
-
let pwd = projectDir;
|
|
436
|
-
|
|
437
|
-
if (stripPrefix) {
|
|
438
|
-
if (scratch) {
|
|
439
|
-
if (relative) pwd = relative;
|
|
440
|
-
} else {
|
|
441
|
-
pwd = stripDisplayRoot(pwd);
|
|
442
|
-
}
|
|
443
|
-
}
|
|
437
|
+
const { scratch, displayPath } = getProjectDirDisplay(projectDir);
|
|
438
|
+
let pwd = stripPrefix ? displayPath : projectDir;
|
|
444
439
|
const repoSuffix = ctx.activeRepo ? ` ↳ ${ctx.activeRepo.relativeRepoRoot}` : "";
|
|
445
440
|
if (opts.abbreviate !== false) {
|
|
446
441
|
pwd = shortenPath(pwd);
|
|
@@ -450,7 +445,7 @@ const pathSegment: StatusLineSegment = {
|
|
|
450
445
|
|
|
451
446
|
const showScratchIcon = scratch && stripPrefix;
|
|
452
447
|
const icon = showScratchIcon ? theme.icon.scratchFolder : theme.icon.folder;
|
|
453
|
-
const text =
|
|
448
|
+
const text = `${fileHyperlink(projectDir, pwd)}${repoSuffix}`;
|
|
454
449
|
const content = withIcon(icon, text);
|
|
455
450
|
return { content: theme.fg("statusLinePath", content), visible: true };
|
|
456
451
|
},
|
|
@@ -469,20 +464,20 @@ const gitSegment: StatusLineSegment = {
|
|
|
469
464
|
const showBranch = opts.showBranch !== false;
|
|
470
465
|
let content = "";
|
|
471
466
|
if (showBranch && branch) {
|
|
472
|
-
content = withIcon(theme.icon.branch,
|
|
467
|
+
content = withIcon(theme.icon.branch, branch);
|
|
473
468
|
}
|
|
474
469
|
|
|
475
470
|
// Add status indicators
|
|
476
471
|
if (gitStatus) {
|
|
477
472
|
const indicators: string[] = [];
|
|
478
473
|
if (opts.showUnstaged !== false && gitStatus.unstaged > 0) {
|
|
479
|
-
indicators.push(theme.fg("statusLineDirty", `*${
|
|
474
|
+
indicators.push(theme.fg("statusLineDirty", `*${gitStatus.unstaged}`));
|
|
480
475
|
}
|
|
481
476
|
if (opts.showStaged !== false && gitStatus.staged > 0) {
|
|
482
|
-
indicators.push(theme.fg("statusLineStaged", `+${
|
|
477
|
+
indicators.push(theme.fg("statusLineStaged", `+${gitStatus.staged}`));
|
|
483
478
|
}
|
|
484
479
|
if (opts.showUntracked !== false && gitStatus.untracked > 0) {
|
|
485
|
-
indicators.push(theme.fg("statusLineUntracked", `?${
|
|
480
|
+
indicators.push(theme.fg("statusLineUntracked", `?${gitStatus.untracked}`));
|
|
486
481
|
}
|
|
487
482
|
if (indicators.length > 0) {
|
|
488
483
|
const indicatorText = indicators.join(" ");
|
|
@@ -507,9 +502,8 @@ const prSegment: StatusLineSegment = {
|
|
|
507
502
|
const { pr } = ctx.git;
|
|
508
503
|
if (!pr) return { content: "", visible: false };
|
|
509
504
|
|
|
510
|
-
const label = withIcon(theme.icon.pr, `#${
|
|
511
|
-
const content =
|
|
512
|
-
!ctx.startupPlaceholder && TERMINAL.hyperlinks ? `\x1b]8;;${pr.url}\x07${label}\x1b]8;;\x07` : label;
|
|
505
|
+
const label = withIcon(theme.icon.pr, `#${pr.number}`);
|
|
506
|
+
const content = TERMINAL.hyperlinks ? `\x1b]8;;${pr.url}\x07${label}\x1b]8;;\x07` : label;
|
|
513
507
|
return { content: accentFg(ctx, "accent", content), visible: true };
|
|
514
508
|
},
|
|
515
509
|
};
|
|
@@ -520,7 +514,7 @@ const subagentsSegment: StatusLineSegment = {
|
|
|
520
514
|
if (ctx.subagentCount === 0) {
|
|
521
515
|
return { content: "", visible: false };
|
|
522
516
|
}
|
|
523
|
-
const content = withIcon(theme.icon.agents,
|
|
517
|
+
const content = withIcon(theme.icon.agents, `${ctx.subagentCount}`);
|
|
524
518
|
return { content: theme.fg("statusLineSubagents", content), visible: true };
|
|
525
519
|
},
|
|
526
520
|
};
|
|
@@ -542,7 +536,7 @@ const tokenTotalSegment: StatusLineSegment = {
|
|
|
542
536
|
|
|
543
537
|
const content = formatMetric({
|
|
544
538
|
leading: theme.icon.tokens || undefined,
|
|
545
|
-
value:
|
|
539
|
+
value: formatNumber(total),
|
|
546
540
|
});
|
|
547
541
|
return { content: theme.fg("statusLineSpend", content ?? ""), visible: true };
|
|
548
542
|
},
|
|
@@ -556,7 +550,7 @@ const tokenRateSegment: StatusLineSegment = {
|
|
|
556
550
|
|
|
557
551
|
const content = formatMetric({
|
|
558
552
|
leading: theme.icon.throughput || undefined,
|
|
559
|
-
value: `${
|
|
553
|
+
value: `${tokensPerSecond.toFixed(1)} tok/s`,
|
|
560
554
|
});
|
|
561
555
|
return { content: theme.fg("statusLineOutput", content ?? ""), visible: true };
|
|
562
556
|
},
|
|
@@ -583,7 +577,6 @@ const costSegment: StatusLineSegment = {
|
|
|
583
577
|
usingSubscription,
|
|
584
578
|
premiumRequests,
|
|
585
579
|
fractionDigits: 2,
|
|
586
|
-
startupPlaceholder: ctx.startupPlaceholder,
|
|
587
580
|
pricingPeriod,
|
|
588
581
|
advisor: advisorCost
|
|
589
582
|
? {
|
|
@@ -623,9 +616,10 @@ const contextPctSegment: StatusLineSegment = {
|
|
|
623
616
|
: theme.fg(color, theme.icon.auto)
|
|
624
617
|
}`;
|
|
625
618
|
}
|
|
619
|
+
// A known window with unknown usage (startup prepaint) shows the window alone.
|
|
626
620
|
const text = theme.fg(
|
|
627
621
|
color,
|
|
628
|
-
|
|
622
|
+
pct === null && window > 0 ? formatNumber(window) : formatContextUsage(pct, window, ctx.contextTokens),
|
|
629
623
|
);
|
|
630
624
|
const content = withIcon(theme.icon.context, `${text}${autoIcon}`);
|
|
631
625
|
|
|
@@ -639,7 +633,7 @@ const contextTotalSegment: StatusLineSegment = {
|
|
|
639
633
|
const window = ctx.contextWindow;
|
|
640
634
|
if (!window) return { content: "", visible: false };
|
|
641
635
|
return {
|
|
642
|
-
content: theme.fg("statusLineContext", withIcon(theme.icon.context,
|
|
636
|
+
content: theme.fg("statusLineContext", withIcon(theme.icon.context, formatNumber(window))),
|
|
643
637
|
visible: true,
|
|
644
638
|
};
|
|
645
639
|
},
|
|
@@ -657,7 +651,7 @@ const timeSpentSegment: StatusLineSegment = {
|
|
|
657
651
|
id: "time_spent",
|
|
658
652
|
render(ctx) {
|
|
659
653
|
if (ctx.activeMs < 1000) return { content: "", visible: false };
|
|
660
|
-
return { content: withIcon(theme.icon.time,
|
|
654
|
+
return { content: withIcon(theme.icon.time, formatDuration(ctx.activeMs)), visible: true };
|
|
661
655
|
},
|
|
662
656
|
};
|
|
663
657
|
|
|
@@ -681,7 +675,7 @@ const timeSegment: StatusLineSegment = {
|
|
|
681
675
|
}
|
|
682
676
|
timeStr += suffix;
|
|
683
677
|
|
|
684
|
-
return { content: withIcon(theme.icon.time,
|
|
678
|
+
return { content: withIcon(theme.icon.time, timeStr), visible: true };
|
|
685
679
|
},
|
|
686
680
|
};
|
|
687
681
|
|
|
@@ -690,7 +684,7 @@ const sessionSegment: StatusLineSegment = {
|
|
|
690
684
|
render(ctx) {
|
|
691
685
|
const sessionManager = ctx.session.sessionManager;
|
|
692
686
|
const sessionId = sessionManager?.getSessionId?.();
|
|
693
|
-
const display =
|
|
687
|
+
const display = sessionId?.slice(0, 8) || "new";
|
|
694
688
|
|
|
695
689
|
return { content: withIcon(theme.icon.session, display), visible: true };
|
|
696
690
|
},
|
|
@@ -699,7 +693,7 @@ const sessionSegment: StatusLineSegment = {
|
|
|
699
693
|
const hostnameSegment: StatusLineSegment = {
|
|
700
694
|
id: "hostname",
|
|
701
695
|
render(ctx) {
|
|
702
|
-
const name =
|
|
696
|
+
const name = ctx.hostname ?? os.hostname().split(".")[0];
|
|
703
697
|
const content = withIcon(theme.icon.host, name);
|
|
704
698
|
const ansi = sessionAccentAnsi(ctx);
|
|
705
699
|
return { content: ansi ? `${ansi}${content}\x1b[39m` : content, visible: true };
|
|
@@ -729,7 +723,7 @@ const cacheHitSegment: StatusLineSegment = {
|
|
|
729
723
|
const total = cacheRead + cacheWrite + input;
|
|
730
724
|
|
|
731
725
|
const rate = (cacheRead / total) * 100;
|
|
732
|
-
const rateStr =
|
|
726
|
+
const rateStr = rate.toFixed(2);
|
|
733
727
|
|
|
734
728
|
const parts: string[] = [theme.icon.cache];
|
|
735
729
|
parts.push(theme.fg("statusLineSpend", `${rateStr}%`));
|
|
@@ -744,7 +738,7 @@ const sessionNameSegment: StatusLineSegment = {
|
|
|
744
738
|
const name = sessionManager?.getSessionName() || ctx.previewTitle;
|
|
745
739
|
if (!name) return { content: "", visible: false };
|
|
746
740
|
|
|
747
|
-
const content =
|
|
741
|
+
const content = sanitizeStatusText(name);
|
|
748
742
|
return { content: accentFg(ctx, "accent", content), visible: true };
|
|
749
743
|
},
|
|
750
744
|
};
|
|
@@ -753,7 +747,7 @@ const collabSegment: StatusLineSegment = {
|
|
|
753
747
|
id: "collab",
|
|
754
748
|
render(ctx) {
|
|
755
749
|
if (!ctx.collab) return { content: "", visible: false };
|
|
756
|
-
const participants =
|
|
750
|
+
const participants = `${ctx.collab.participantCount}`;
|
|
757
751
|
const label = ctx.collab.role === "host" ? `⇄ collab:${participants}` : `⇄ collab guest:${participants}`;
|
|
758
752
|
return { content: accentFg(ctx, "accent", label), visible: true };
|
|
759
753
|
},
|
|
@@ -763,7 +757,7 @@ const streamSegment: StatusLineSegment = {
|
|
|
763
757
|
id: "stream",
|
|
764
758
|
render(ctx) {
|
|
765
759
|
const badges: string[] = [];
|
|
766
|
-
if (ctx.stream) badges.push(`● LIVE ${
|
|
760
|
+
if (ctx.stream) badges.push(`● LIVE ${ctx.stream.viewers}`);
|
|
767
761
|
if (ctx.recording) badges.push("● REC");
|
|
768
762
|
if (badges.length === 0) return { content: "", visible: false };
|
|
769
763
|
return { content: theme.fg("thinkingHigh", badges.join(" ")), visible: true };
|
|
@@ -838,11 +832,8 @@ function formatQuotaWindow(
|
|
|
838
832
|
integer: "round" | "floor",
|
|
839
833
|
): string {
|
|
840
834
|
const whole = integer === "floor" ? Math.floor(percent) : Math.round(percent);
|
|
841
|
-
const pctText = theme.fg(pickUsageColor(percent), `${
|
|
842
|
-
const resetText =
|
|
843
|
-
reset !== undefined
|
|
844
|
-
? theme.fg("muted", ctx.startupPlaceholder ? " (…)" : ` (${formatUsageReset(reset, resetUnit)})`)
|
|
845
|
-
: "";
|
|
835
|
+
const pctText = theme.fg(pickUsageColor(percent), `${whole}%`);
|
|
836
|
+
const resetText = reset !== undefined ? theme.fg("muted", ` (${formatUsageReset(reset, resetUnit)})`) : "";
|
|
846
837
|
return `${label} ${pctText}${resetText}`;
|
|
847
838
|
}
|
|
848
839
|
|
|
@@ -870,9 +861,7 @@ const usageSegment: StatusLineSegment = {
|
|
|
870
861
|
}
|
|
871
862
|
const parts: string[] = [];
|
|
872
863
|
if (u.tier) {
|
|
873
|
-
const tier =
|
|
874
|
-
? STARTUP_PLACEHOLDER
|
|
875
|
-
: truncateToWidth(sanitizeStatusText(u.tier), TRUNCATE_LENGTHS.SHORT);
|
|
864
|
+
const tier = truncateToWidth(sanitizeStatusText(u.tier), TRUNCATE_LENGTHS.SHORT);
|
|
876
865
|
if (tier) parts.push(accentFg(ctx, "accent", tier));
|
|
877
866
|
}
|
|
878
867
|
if (u.fiveHour) {
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Status line for the speculative first frame, before any session exists.
|
|
3
|
+
*
|
|
4
|
+
* InteractiveMode persists {@link StatusLineStartupData} (settings plus the last
|
|
5
|
+
* session's model and thinking state); the next launch renders it through the
|
|
6
|
+
* real {@link StatusLineComponent} as a fresh, unnamed session at the live
|
|
7
|
+
* terminal width. Path and git branch are read live; context usage is reported
|
|
8
|
+
* unknown so the gauge shows the window without a percent.
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```ts ignore
|
|
12
|
+
* const statusLine = createStartupStatusLine(cached.statusLine);
|
|
13
|
+
* statusLine.attachToEditor(editor, getComposerStyle(shape));
|
|
14
|
+
* ```
|
|
15
|
+
*/
|
|
16
|
+
import type { ThinkingLevel } from "@oh-my-pi/pi-agent-core";
|
|
17
|
+
import type { Model } from "@oh-my-pi/pi-catalog/types";
|
|
18
|
+
import { isRecord } from "@oh-my-pi/pi-utils/type-guards";
|
|
19
|
+
import { parseThinkingLevel } from "../thinking";
|
|
20
|
+
import { StatusLineComponent } from "./component";
|
|
21
|
+
import type { CompactionBoundaries } from "./context-usage";
|
|
22
|
+
import type { StatusLineHost, StatusLineSession } from "./host";
|
|
23
|
+
import {
|
|
24
|
+
CONTEXT_LINE_MODE_VALUES,
|
|
25
|
+
STATUS_LINE_PRESET_VALUES,
|
|
26
|
+
STATUS_LINE_SEGMENT_IDS,
|
|
27
|
+
STATUS_LINE_SEPARATOR_VALUES,
|
|
28
|
+
} from "./schema";
|
|
29
|
+
import type { StatusLineSettings } from "./types";
|
|
30
|
+
|
|
31
|
+
/** Session-independent inputs that reproduce a fresh session's status bar on the next launch. */
|
|
32
|
+
export interface StatusLineStartupData {
|
|
33
|
+
readonly settings: StatusLineSettings;
|
|
34
|
+
readonly gitEnabled: boolean;
|
|
35
|
+
/** Model of the last session; the next fresh session usually starts on it too. */
|
|
36
|
+
readonly model?: Model;
|
|
37
|
+
readonly thinkingLevel?: ThinkingLevel;
|
|
38
|
+
readonly autoThinking: boolean;
|
|
39
|
+
readonly fastMode: boolean;
|
|
40
|
+
/** Whether `model` bills through a subscription (drives the cost segment's prefix). */
|
|
41
|
+
readonly usingSubscription: boolean;
|
|
42
|
+
readonly autoCompactEnabled: boolean;
|
|
43
|
+
readonly compactionBoundaries: CompactionBoundaries | null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const NO_MESSAGES: readonly never[] = [];
|
|
47
|
+
const NO_USAGE = {
|
|
48
|
+
input: 0,
|
|
49
|
+
output: 0,
|
|
50
|
+
cacheRead: 0,
|
|
51
|
+
cacheWrite: 0,
|
|
52
|
+
totalTokens: 0,
|
|
53
|
+
orchestrationInput: 0,
|
|
54
|
+
orchestrationOutput: 0,
|
|
55
|
+
orchestrationCacheRead: 0,
|
|
56
|
+
premiumRequests: 0,
|
|
57
|
+
cost: 0,
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
/** Build the prepaint status line; the caller disposes it once the session-bound bar mounts. */
|
|
61
|
+
export function createStartupStatusLine(data: StatusLineStartupData): StatusLineComponent {
|
|
62
|
+
const session: StatusLineSession = {
|
|
63
|
+
state: { model: data.model, thinkingLevel: data.thinkingLevel, messages: NO_MESSAGES },
|
|
64
|
+
model: data.model,
|
|
65
|
+
messages: NO_MESSAGES,
|
|
66
|
+
isStreaming: false,
|
|
67
|
+
isAutoThinking: data.autoThinking,
|
|
68
|
+
sessionManager: {
|
|
69
|
+
getSessionName: () => undefined,
|
|
70
|
+
getSessionId: () => "",
|
|
71
|
+
getUsageStatistics: () => NO_USAGE,
|
|
72
|
+
},
|
|
73
|
+
modelRegistry: { isUsingOAuth: () => data.usingSubscription },
|
|
74
|
+
getContextUsage: () =>
|
|
75
|
+
data.model?.contextWindow ? { tokens: 0, contextWindow: data.model.contextWindow, percent: null } : undefined,
|
|
76
|
+
autoResolvedThinkingLevel: () => undefined,
|
|
77
|
+
isFastModeActive: () => data.fastMode,
|
|
78
|
+
getAsyncJobSnapshot: () => null,
|
|
79
|
+
getGoalModeState: () => undefined,
|
|
80
|
+
};
|
|
81
|
+
const host: StatusLineHost = {
|
|
82
|
+
getSettings: () => data.settings,
|
|
83
|
+
gitEnabled: () => data.gitEnabled,
|
|
84
|
+
codexResetFireworksEnabled: () => false,
|
|
85
|
+
getSettingsRevision: () => 0,
|
|
86
|
+
getSessionSettingsIdentity: () => undefined,
|
|
87
|
+
getSessionSettingsRevision: () => 0,
|
|
88
|
+
goalStatusInFooter: () => false,
|
|
89
|
+
activeAccount: () => undefined,
|
|
90
|
+
canFetchUsageReports: () => false,
|
|
91
|
+
fetchUsageReports: async () => null,
|
|
92
|
+
resolveActiveRepo: () => null,
|
|
93
|
+
lookupPullRequest: async () => ({ stdout: "", exitCode: 1 }),
|
|
94
|
+
calculateTokensPerSecond: () => null,
|
|
95
|
+
limitMatchesActiveAccount: () => false,
|
|
96
|
+
computeCompactionBoundaries: () => data.compactionBoundaries,
|
|
97
|
+
};
|
|
98
|
+
const statusLine = new StatusLineComponent(session, host);
|
|
99
|
+
statusLine.setAutoCompactEnabled(data.autoCompactEnabled);
|
|
100
|
+
return statusLine;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function isOneOf<T extends string>(values: readonly T[], value: unknown): value is T {
|
|
104
|
+
return typeof value === "string" && values.some(candidate => candidate === value);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function isOptionalBoolean(value: unknown): boolean {
|
|
108
|
+
return value === undefined || typeof value === "boolean";
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function isSegmentList(value: unknown): boolean {
|
|
112
|
+
return value === undefined || (Array.isArray(value) && value.every(id => isOneOf(STATUS_LINE_SEGMENT_IDS, id)));
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** Shallow check of persisted settings; nested segment options are trusted as written by this version. */
|
|
116
|
+
function isStatusLineSettings(value: unknown): value is StatusLineSettings {
|
|
117
|
+
return (
|
|
118
|
+
isRecord(value) &&
|
|
119
|
+
(value.preset === undefined || isOneOf(STATUS_LINE_PRESET_VALUES, value.preset)) &&
|
|
120
|
+
isSegmentList(value.leftSegments) &&
|
|
121
|
+
isSegmentList(value.rightSegments) &&
|
|
122
|
+
(value.separator === undefined || isOneOf(STATUS_LINE_SEPARATOR_VALUES, value.separator)) &&
|
|
123
|
+
(value.segmentOptions === undefined || isRecord(value.segmentOptions)) &&
|
|
124
|
+
(value.contextLine === undefined || isOneOf(CONTEXT_LINE_MODE_VALUES, value.contextLine)) &&
|
|
125
|
+
isOptionalBoolean(value.showHookStatus) &&
|
|
126
|
+
isOptionalBoolean(value.sessionAccent) &&
|
|
127
|
+
isOptionalBoolean(value.transparent) &&
|
|
128
|
+
isOptionalBoolean(value.compactThinkingLevel)
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** Check the model fields the status line reads; the rest is trusted as serialized by this version. */
|
|
133
|
+
function isPersistedModel(value: unknown): value is Model {
|
|
134
|
+
return (
|
|
135
|
+
isRecord(value) &&
|
|
136
|
+
typeof value.id === "string" &&
|
|
137
|
+
typeof value.name === "string" &&
|
|
138
|
+
typeof value.provider === "string" &&
|
|
139
|
+
typeof value.api === "string" &&
|
|
140
|
+
typeof value.contextWindow === "number"
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function readCompactionBoundaries(value: unknown): CompactionBoundaries | null | undefined {
|
|
145
|
+
if (value === null) return null;
|
|
146
|
+
if (!isRecord(value) || typeof value.thresholdPercent !== "number") return undefined;
|
|
147
|
+
const speculationPercent = value.speculationPercent;
|
|
148
|
+
if (speculationPercent !== null && typeof speculationPercent !== "number") return undefined;
|
|
149
|
+
return { thresholdPercent: value.thresholdPercent, speculationPercent };
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** Validate persisted {@link StatusLineStartupData}; `undefined` for anything malformed. */
|
|
153
|
+
export function readStatusLineStartupData(value: unknown): StatusLineStartupData | undefined {
|
|
154
|
+
if (!isRecord(value) || !isStatusLineSettings(value.settings)) return undefined;
|
|
155
|
+
const model = value.model;
|
|
156
|
+
if (model !== undefined && !isPersistedModel(model)) return undefined;
|
|
157
|
+
const thinkingLevel = typeof value.thinkingLevel === "string" ? parseThinkingLevel(value.thinkingLevel) : undefined;
|
|
158
|
+
if (value.thinkingLevel !== undefined && thinkingLevel === undefined) return undefined;
|
|
159
|
+
const compactionBoundaries = readCompactionBoundaries(value.compactionBoundaries);
|
|
160
|
+
if (
|
|
161
|
+
typeof value.gitEnabled !== "boolean" ||
|
|
162
|
+
typeof value.autoThinking !== "boolean" ||
|
|
163
|
+
typeof value.fastMode !== "boolean" ||
|
|
164
|
+
typeof value.usingSubscription !== "boolean" ||
|
|
165
|
+
typeof value.autoCompactEnabled !== "boolean" ||
|
|
166
|
+
compactionBoundaries === undefined
|
|
167
|
+
) {
|
|
168
|
+
return undefined;
|
|
169
|
+
}
|
|
170
|
+
return {
|
|
171
|
+
settings: value.settings,
|
|
172
|
+
gitEnabled: value.gitEnabled,
|
|
173
|
+
model,
|
|
174
|
+
thinkingLevel,
|
|
175
|
+
autoThinking: value.autoThinking,
|
|
176
|
+
fastMode: value.fastMode,
|
|
177
|
+
usingSubscription: value.usingSubscription,
|
|
178
|
+
autoCompactEnabled: value.autoCompactEnabled,
|
|
179
|
+
compactionBoundaries,
|
|
180
|
+
};
|
|
181
|
+
}
|
package/src/status-line/types.ts
CHANGED
|
@@ -84,8 +84,6 @@ export interface SegmentContext {
|
|
|
84
84
|
sessionAccent?: boolean;
|
|
85
85
|
/** Stand-in session title for previews; `session_name` renders it when the session is unnamed. */
|
|
86
86
|
previewTitle?: string;
|
|
87
|
-
/** Replace dynamic values with ellipses while preserving each segment's icon, color, and static text. */
|
|
88
|
-
startupPlaceholder?: boolean;
|
|
89
87
|
activeRepo: ActiveRepoContext | null;
|
|
90
88
|
width: number;
|
|
91
89
|
options: StatusLineSegmentOptions;
|