tau-all-agent 0.3.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/extensions/answer.ts +4 -1
- package/extensions/btw.ts +4 -1
- package/extensions/insights.ts +4 -1
- package/extensions/sandbox/README.md +2 -0
- package/extensions/sandbox/command.ts +15 -1
- package/extensions/sandbox/config.ts +26 -15
- package/extensions/sandbox/runtime.ts +1 -1
- package/extensions/usage/README.md +1 -1
- package/extensions/usage/index.ts +227 -294
- package/package.json +1 -1
package/extensions/answer.ts
CHANGED
|
@@ -661,7 +661,10 @@ export default function (pi: ExtensionAPI) {
|
|
|
661
661
|
// Show the Q&A component
|
|
662
662
|
const answersResult = await ctx.ui.custom<string | null>(
|
|
663
663
|
(tui, theme, kb, done) => new QnAComponent(questions, tui, theme, kb, done),
|
|
664
|
-
{
|
|
664
|
+
{
|
|
665
|
+
overlay: true,
|
|
666
|
+
overlayOptions: { anchor: "bottom-center", width: "100%", maxHeight: "100%", margin: 1 },
|
|
667
|
+
},
|
|
665
668
|
);
|
|
666
669
|
|
|
667
670
|
if (answersResult === null) {
|
package/extensions/btw.ts
CHANGED
|
@@ -356,7 +356,10 @@ async function showResultDialog(ctx: ExtensionContext, result: BtwResult): Promi
|
|
|
356
356
|
await ctx.ui.custom<void>(
|
|
357
357
|
(tui, theme, keybindings, done) =>
|
|
358
358
|
new BtwResultComponent(result, tui, theme, keybindings, done),
|
|
359
|
-
{
|
|
359
|
+
{
|
|
360
|
+
overlay: true,
|
|
361
|
+
overlayOptions: { anchor: "bottom-center", width: "100%", maxHeight: "100%", margin: 1 },
|
|
362
|
+
},
|
|
360
363
|
);
|
|
361
364
|
}
|
|
362
365
|
|
package/extensions/insights.ts
CHANGED
|
@@ -598,7 +598,10 @@ async function runInsightsCommand(
|
|
|
598
598
|
await ctx.ui.custom<void>(
|
|
599
599
|
(tui, theme, keybindings, done) =>
|
|
600
600
|
new InsightsReportComponent(result, tui, theme, keybindings, done),
|
|
601
|
-
{
|
|
601
|
+
{
|
|
602
|
+
overlay: true,
|
|
603
|
+
overlayOptions: { anchor: "bottom-center", width: "100%", maxHeight: "100%", margin: 1 },
|
|
604
|
+
},
|
|
602
605
|
);
|
|
603
606
|
|
|
604
607
|
if (reportPath) {
|
|
@@ -2,4 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
Sandboxes Bash commands and their descendants on macOS and Linux, including calls through codemode. Pi itself, other tools, and MCP servers retain host permissions. Run `/sandbox` for help.
|
|
4
4
|
|
|
5
|
+
Relative filesystem rules and socket paths resolve from the owning session's working directory, including session-only `/sandbox filesystem` changes. Configuration files are not rewritten.
|
|
6
|
+
|
|
5
7
|
Terminal-status reports may expose approval titles containing commands, paths, or hosts. Disable these reports with `PI_PROGRAM_STATUS=0`.
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
cloneRuntimeConfig,
|
|
6
6
|
escapeSlashCommandArg,
|
|
7
7
|
mutateStringList,
|
|
8
|
+
resolveSandboxPath,
|
|
8
9
|
type ListOp,
|
|
9
10
|
type PromptMode,
|
|
10
11
|
type SandboxConfigPath,
|
|
@@ -585,7 +586,20 @@ export function registerSandboxCommand(pi: ExtensionAPI, runtime: SandboxRuntime
|
|
|
585
586
|
: list === "allow-write"
|
|
586
587
|
? nextConfig.filesystem.allowWrite
|
|
587
588
|
: nextConfig.filesystem.denyWrite;
|
|
588
|
-
|
|
589
|
+
// Exact runtime entries are already expanded and may contain literal dollars.
|
|
590
|
+
const resolvedPath = values.includes(targetPath)
|
|
591
|
+
? targetPath
|
|
592
|
+
: resolveSandboxPath(targetPath, `filesystem ${list}`, ctx.cwd);
|
|
593
|
+
if (resolvedPath === null) {
|
|
594
|
+
notify(
|
|
595
|
+
ctx,
|
|
596
|
+
"Invalid filesystem path: unset or empty environment variable, or control characters",
|
|
597
|
+
"warning",
|
|
598
|
+
);
|
|
599
|
+
return;
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
const changed = mutateStringList(values, op, resolvedPath);
|
|
589
603
|
if (!changed) {
|
|
590
604
|
notify(
|
|
591
605
|
ctx,
|
|
@@ -291,7 +291,7 @@ export function normalizePromptMode(value: unknown): PromptMode {
|
|
|
291
291
|
|
|
292
292
|
function expandPath(value: string, cwd?: string): string {
|
|
293
293
|
const expanded =
|
|
294
|
-
value === "~" ? homedir() : value.startsWith("~/") ?
|
|
294
|
+
value === "~" ? homedir() : value.startsWith("~/") ? homedir() + value.slice(1) : value;
|
|
295
295
|
return cwd && !expanded.startsWith("/") ? resolve(cwd, expanded) : expanded;
|
|
296
296
|
}
|
|
297
297
|
|
|
@@ -570,7 +570,7 @@ function containsControlCharacter(value: string): boolean {
|
|
|
570
570
|
return false;
|
|
571
571
|
}
|
|
572
572
|
|
|
573
|
-
function
|
|
573
|
+
export function resolveSandboxPath(value: string, field: string, cwd: string): string | null {
|
|
574
574
|
const source = value.trim();
|
|
575
575
|
if (!source) return null;
|
|
576
576
|
|
|
@@ -605,24 +605,29 @@ function expandPathConfigEntry(value: string, field: string): string | null {
|
|
|
605
605
|
return null;
|
|
606
606
|
}
|
|
607
607
|
|
|
608
|
-
return expanded;
|
|
608
|
+
return expandPath(expanded, cwd);
|
|
609
609
|
}
|
|
610
610
|
|
|
611
|
-
function expandPathConfigList(
|
|
611
|
+
function expandPathConfigList(
|
|
612
|
+
values: string[] | undefined,
|
|
613
|
+
field: string,
|
|
614
|
+
cwd: string,
|
|
615
|
+
): string[] | undefined {
|
|
612
616
|
if (!values) return undefined;
|
|
613
617
|
|
|
614
618
|
return values
|
|
615
|
-
.map((value) =>
|
|
619
|
+
.map((value) => resolveSandboxPath(value, field, cwd))
|
|
616
620
|
.filter((value): value is string => value !== null);
|
|
617
621
|
}
|
|
618
622
|
|
|
619
623
|
function expandMitmProxyPathConfig(
|
|
620
624
|
mitmProxy: SandboxRuntimeConfig["network"]["mitmProxy"],
|
|
625
|
+
cwd: string,
|
|
621
626
|
): SandboxRuntimeConfig["network"]["mitmProxy"] {
|
|
622
627
|
if (!mitmProxy) return undefined;
|
|
623
628
|
if (!isPlainObject(mitmProxy) || typeof mitmProxy.socketPath !== "string") return mitmProxy;
|
|
624
629
|
|
|
625
|
-
const socketPath =
|
|
630
|
+
const socketPath = resolveSandboxPath(mitmProxy.socketPath, "network.mitmProxy.socketPath", cwd);
|
|
626
631
|
if (!socketPath) {
|
|
627
632
|
console.error("Warning: Disabling network.mitmProxy because its socketPath did not expand.");
|
|
628
633
|
return undefined;
|
|
@@ -633,30 +638,36 @@ function expandMitmProxyPathConfig(
|
|
|
633
638
|
|
|
634
639
|
function expandNetworkPathConfig(
|
|
635
640
|
network: SandboxRuntimeConfig["network"],
|
|
641
|
+
cwd: string,
|
|
636
642
|
): SandboxRuntimeConfig["network"] {
|
|
637
643
|
return {
|
|
638
644
|
...network,
|
|
639
|
-
allowUnixSockets: expandPathConfigList(
|
|
640
|
-
|
|
645
|
+
allowUnixSockets: expandPathConfigList(
|
|
646
|
+
network.allowUnixSockets,
|
|
647
|
+
"network.allowUnixSockets",
|
|
648
|
+
cwd,
|
|
649
|
+
),
|
|
650
|
+
mitmProxy: expandMitmProxyPathConfig(network.mitmProxy, cwd),
|
|
641
651
|
};
|
|
642
652
|
}
|
|
643
653
|
|
|
644
654
|
function expandFilesystemPathConfig(
|
|
645
655
|
filesystem: SandboxRuntimeConfig["filesystem"],
|
|
656
|
+
cwd: string,
|
|
646
657
|
): SandboxRuntimeConfig["filesystem"] {
|
|
647
658
|
return {
|
|
648
659
|
...filesystem,
|
|
649
|
-
denyRead: expandPathConfigList(filesystem.denyRead, "filesystem.denyRead") ?? [],
|
|
650
|
-
allowRead: expandPathConfigList(filesystem.allowRead, "filesystem.allowRead"),
|
|
651
|
-
allowWrite: expandPathConfigList(filesystem.allowWrite, "filesystem.allowWrite") ?? [],
|
|
652
|
-
denyWrite: expandPathConfigList(filesystem.denyWrite, "filesystem.denyWrite") ?? [],
|
|
660
|
+
denyRead: expandPathConfigList(filesystem.denyRead, "filesystem.denyRead", cwd) ?? [],
|
|
661
|
+
allowRead: expandPathConfigList(filesystem.allowRead, "filesystem.allowRead", cwd),
|
|
662
|
+
allowWrite: expandPathConfigList(filesystem.allowWrite, "filesystem.allowWrite", cwd) ?? [],
|
|
663
|
+
denyWrite: expandPathConfigList(filesystem.denyWrite, "filesystem.denyWrite", cwd) ?? [],
|
|
653
664
|
};
|
|
654
665
|
}
|
|
655
666
|
|
|
656
|
-
export function toRuntimeConfig(config: SandboxConfig): SandboxRuntimeConfig {
|
|
667
|
+
export function toRuntimeConfig(config: SandboxConfig, cwd: string): SandboxRuntimeConfig {
|
|
657
668
|
const { allowGitCommonDir: _allowGitCommonDir, allowTempDirs, ...filesystem } = config.filesystem;
|
|
658
|
-
const expandedNetwork = expandNetworkPathConfig(config.network);
|
|
659
|
-
const expandedFilesystem = expandFilesystemPathConfig(filesystem);
|
|
669
|
+
const expandedNetwork = expandNetworkPathConfig(config.network, cwd);
|
|
670
|
+
const expandedFilesystem = expandFilesystemPathConfig(filesystem, cwd);
|
|
660
671
|
const allowWrite = allowTempDirs
|
|
661
672
|
? [...expandedFilesystem.allowWrite, ...getTemporaryWritePaths()]
|
|
662
673
|
: expandedFilesystem.allowWrite;
|
|
@@ -340,7 +340,7 @@ export function createSandboxRuntime(pi: ExtensionAPI): SandboxRuntime {
|
|
|
340
340
|
return null;
|
|
341
341
|
}
|
|
342
342
|
|
|
343
|
-
const runtimeConfig = toRuntimeConfig(config);
|
|
343
|
+
const runtimeConfig = toRuntimeConfig(config, ctx.cwd);
|
|
344
344
|
|
|
345
345
|
try {
|
|
346
346
|
await SandboxManager.initialize(runtimeConfig, askNetworkPermission, true);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
# Usage
|
|
2
2
|
|
|
3
|
-
Run `/usage` for historical usage and supported providers' live quotas.
|
|
3
|
+
Run `/usage` for historical usage and supported providers' live quotas. The dashboard follows Pi's active theme, including light/dark changes, and terminal color capabilities.
|
|
4
4
|
|
|
5
5
|
Live Codex quotas require `openai-codex` credentials, not `/login openai` credentials. History works without quota access.
|
|
@@ -3,10 +3,16 @@ import {
|
|
|
3
3
|
getAgentDir,
|
|
4
4
|
type ExtensionAPI,
|
|
5
5
|
type ExtensionContext,
|
|
6
|
+
type Theme,
|
|
6
7
|
} from "@earendil-works/pi-coding-agent";
|
|
7
8
|
import {
|
|
9
|
+
colorToOkhsl,
|
|
8
10
|
Key,
|
|
9
11
|
matchesKey,
|
|
12
|
+
mixColors,
|
|
13
|
+
okhslColor,
|
|
14
|
+
rgbColor,
|
|
15
|
+
type Color,
|
|
10
16
|
type Component,
|
|
11
17
|
type KeybindingsManager,
|
|
12
18
|
type TUI,
|
|
@@ -54,37 +60,6 @@ const TOD_BUCKETS: { key: TodKey; label: string; from: number; to: number }[] =
|
|
|
54
60
|
{ key: "night", label: "Night (22–23)", from: 22, to: 23 },
|
|
55
61
|
];
|
|
56
62
|
|
|
57
|
-
const DEFAULT_BG: RGB = { r: 13, g: 17, b: 23 };
|
|
58
|
-
const EMPTY_CELL_BG: RGB = { r: 22, g: 27, b: 34 };
|
|
59
|
-
const ERROR_COLOR: RGB = { r: 244, g: 67, b: 54 };
|
|
60
|
-
const MUTED_COLOR: RGB = { r: 160, g: 160, b: 160 };
|
|
61
|
-
|
|
62
|
-
const PALETTE: RGB[] = [
|
|
63
|
-
{ r: 54, g: 179, b: 166 },
|
|
64
|
-
{ r: 82, g: 146, b: 247 },
|
|
65
|
-
{ r: 171, g: 123, b: 245 },
|
|
66
|
-
{ r: 232, g: 180, b: 72 },
|
|
67
|
-
{ r: 244, g: 67, b: 54 },
|
|
68
|
-
];
|
|
69
|
-
|
|
70
|
-
const DOW_PALETTE: RGB[] = [
|
|
71
|
-
{ r: 47, g: 129, b: 247 },
|
|
72
|
-
{ r: 64, g: 196, b: 99 },
|
|
73
|
-
{ r: 163, g: 113, b: 247 },
|
|
74
|
-
{ r: 47, g: 175, b: 200 },
|
|
75
|
-
{ r: 100, g: 200, b: 150 },
|
|
76
|
-
{ r: 255, g: 159, b: 10 },
|
|
77
|
-
{ r: 244, g: 67, b: 54 },
|
|
78
|
-
];
|
|
79
|
-
|
|
80
|
-
const TOD_PALETTE: Map<TodKey, RGB> = new Map([
|
|
81
|
-
["after-midnight", { r: 100, g: 60, b: 180 }],
|
|
82
|
-
["morning", { r: 255, g: 200, b: 50 }],
|
|
83
|
-
["afternoon", { r: 64, g: 196, b: 99 }],
|
|
84
|
-
["evening", { r: 47, g: 129, b: 247 }],
|
|
85
|
-
["night", { r: 60, g: 40, b: 140 }],
|
|
86
|
-
]);
|
|
87
|
-
|
|
88
63
|
type HistoryProviderId = string;
|
|
89
64
|
type ModelKey = string;
|
|
90
65
|
type CwdKey = string;
|
|
@@ -166,13 +141,13 @@ interface RangeAgg {
|
|
|
166
141
|
|
|
167
142
|
interface ProviderPalette {
|
|
168
143
|
modelPalette: {
|
|
169
|
-
modelColors: Map<ModelKey,
|
|
170
|
-
otherColor:
|
|
144
|
+
modelColors: Map<ModelKey, Color>;
|
|
145
|
+
otherColor: Color;
|
|
171
146
|
orderedModels: ModelKey[];
|
|
172
147
|
};
|
|
173
148
|
cwdPalette: {
|
|
174
|
-
cwdColors: Map<CwdKey,
|
|
175
|
-
otherColor:
|
|
149
|
+
cwdColors: Map<CwdKey, Color>;
|
|
150
|
+
otherColor: Color;
|
|
176
151
|
orderedCwds: CwdKey[];
|
|
177
152
|
};
|
|
178
153
|
}
|
|
@@ -190,16 +165,6 @@ interface BreakdownData {
|
|
|
190
165
|
liveUsage: Map<SupportedProviderId, LiveUsageState>;
|
|
191
166
|
allRanges: Map<number, RangeAgg>;
|
|
192
167
|
providerRangesById: Map<HistoryProviderId, Map<number, RangeAgg>>;
|
|
193
|
-
allPalette: ProviderPalette;
|
|
194
|
-
providerPalettes: Map<HistoryProviderId, ProviderPalette>;
|
|
195
|
-
dowPalette: {
|
|
196
|
-
dowColors: Map<DowKey, RGB>;
|
|
197
|
-
orderedDows: DowKey[];
|
|
198
|
-
};
|
|
199
|
-
todPalette: {
|
|
200
|
-
todColors: Map<TodKey, RGB>;
|
|
201
|
-
orderedTods: TodKey[];
|
|
202
|
-
};
|
|
203
168
|
}
|
|
204
169
|
|
|
205
170
|
type BreakdownProgressPhase = "scan" | "parse" | "finalize";
|
|
@@ -229,44 +194,23 @@ function clamp01(value: number): number {
|
|
|
229
194
|
return Math.max(0, Math.min(1, value));
|
|
230
195
|
}
|
|
231
196
|
|
|
232
|
-
function
|
|
233
|
-
return a + (b - a) * t;
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
function mixRgb(a: RGB, b: RGB, t: number): RGB {
|
|
237
|
-
return {
|
|
238
|
-
r: Math.round(lerp(a.r, b.r, t)),
|
|
239
|
-
g: Math.round(lerp(a.g, b.g, t)),
|
|
240
|
-
b: Math.round(lerp(a.b, b.b, t)),
|
|
241
|
-
};
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
function weightedMix(colors: Array<{ color: RGB; weight: number }>): RGB {
|
|
197
|
+
function weightedMix(colors: Array<{ color: Color; weight: number }>, fallback: Color): Color {
|
|
245
198
|
let total = 0;
|
|
246
|
-
let
|
|
247
|
-
let g = 0;
|
|
248
|
-
let b = 0;
|
|
199
|
+
let mixed: Color | undefined;
|
|
249
200
|
for (const entry of colors) {
|
|
250
201
|
if (!Number.isFinite(entry.weight) || entry.weight <= 0) continue;
|
|
251
202
|
total += entry.weight;
|
|
252
|
-
|
|
253
|
-
g += entry.color.g * entry.weight;
|
|
254
|
-
b += entry.color.b * entry.weight;
|
|
203
|
+
mixed = mixed ? mixColors(mixed, entry.color, entry.weight / total, "srgb") : entry.color;
|
|
255
204
|
}
|
|
256
|
-
|
|
257
|
-
return { r: Math.round(r / total), g: Math.round(g / total), b: Math.round(b / total) };
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
function ansiFg(rgb: RGB, text: string): string {
|
|
261
|
-
return `\x1b[38;2;${rgb.r};${rgb.g};${rgb.b}m${text}\x1b[0m`;
|
|
205
|
+
return mixed ?? fallback;
|
|
262
206
|
}
|
|
263
207
|
|
|
264
|
-
function
|
|
265
|
-
return
|
|
208
|
+
function emptyChartColor(theme: Theme): Color {
|
|
209
|
+
return mixColors(theme.colors.toolPendingBg, theme.colors.muted, 0.25);
|
|
266
210
|
}
|
|
267
211
|
|
|
268
|
-
function
|
|
269
|
-
return
|
|
212
|
+
function truncateUsageLine(theme: Theme, text: string, width: number): string {
|
|
213
|
+
return truncateToWidth(text, width, theme.style("...", { fg: "text", bg: "toolPendingBg" }));
|
|
270
214
|
}
|
|
271
215
|
|
|
272
216
|
function formatUsd(value: number): string {
|
|
@@ -723,88 +667,29 @@ function metricValuesForKind<K>(
|
|
|
723
667
|
return { values: options.sessions, total: options.totalSessions };
|
|
724
668
|
}
|
|
725
669
|
|
|
726
|
-
function
|
|
727
|
-
const
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
function rgbToHsl(color: RGB): { h: number; s: number; l: number } {
|
|
732
|
-
const r = color.r / 255;
|
|
733
|
-
const g = color.g / 255;
|
|
734
|
-
const b = color.b / 255;
|
|
735
|
-
const max = Math.max(r, g, b);
|
|
736
|
-
const min = Math.min(r, g, b);
|
|
737
|
-
const lightness = (max + min) / 2;
|
|
738
|
-
|
|
739
|
-
if (max === min) {
|
|
740
|
-
return { h: 0, s: 0, l: lightness };
|
|
741
|
-
}
|
|
742
|
-
|
|
743
|
-
const delta = max - min;
|
|
744
|
-
const saturation = lightness > 0.5 ? delta / (2 - max - min) : delta / (max + min);
|
|
745
|
-
|
|
746
|
-
let hue = 0;
|
|
747
|
-
if (max === r) hue = (g - b) / delta + (g < b ? 6 : 0);
|
|
748
|
-
else if (max === g) hue = (b - r) / delta + 2;
|
|
749
|
-
else hue = (r - g) / delta + 4;
|
|
750
|
-
|
|
751
|
-
return { h: normalizeHue(hue * 60), s: saturation, l: lightness };
|
|
752
|
-
}
|
|
753
|
-
|
|
754
|
-
function hslToRgb(hue: number, saturation: number, lightness: number): RGB {
|
|
755
|
-
const h = normalizeHue(hue) / 360;
|
|
756
|
-
const s = clamp01(saturation);
|
|
757
|
-
const l = clamp01(lightness);
|
|
758
|
-
|
|
759
|
-
if (s === 0) {
|
|
760
|
-
const value = Math.round(l * 255);
|
|
761
|
-
return { r: value, g: value, b: value };
|
|
762
|
-
}
|
|
763
|
-
|
|
764
|
-
const hueToChannel = (p: number, q: number, t: number): number => {
|
|
765
|
-
let value = t;
|
|
766
|
-
if (value < 0) value += 1;
|
|
767
|
-
if (value > 1) value -= 1;
|
|
768
|
-
if (value < 1 / 6) return p + (q - p) * 6 * value;
|
|
769
|
-
if (value < 1 / 2) return q;
|
|
770
|
-
if (value < 2 / 3) return p + (q - p) * (2 / 3 - value) * 6;
|
|
771
|
-
return p;
|
|
772
|
-
};
|
|
773
|
-
|
|
774
|
-
const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
|
|
775
|
-
const p = 2 * l - q;
|
|
776
|
-
|
|
777
|
-
return {
|
|
778
|
-
r: Math.round(hueToChannel(p, q, h + 1 / 3) * 255),
|
|
779
|
-
g: Math.round(hueToChannel(p, q, h) * 255),
|
|
780
|
-
b: Math.round(hueToChannel(p, q, h - 1 / 3) * 255),
|
|
781
|
-
};
|
|
670
|
+
function providerColor(theme: Theme, color: RGB): Color {
|
|
671
|
+
const source = colorToOkhsl(rgbColor(color.r, color.g, color.b));
|
|
672
|
+
const reference = colorToOkhsl(theme.colors.accent);
|
|
673
|
+
// Keep the provider hue, but match the active theme's brightness and restraint.
|
|
674
|
+
return okhslColor(source.h, Math.min(source.s, reference.s), reference.l);
|
|
782
675
|
}
|
|
783
676
|
|
|
784
|
-
function
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
const hsl = rgbToHsl(color);
|
|
789
|
-
return hslToRgb(
|
|
790
|
-
hsl.h + (adjustments.hueShift ?? 0),
|
|
791
|
-
hsl.s + (adjustments.saturationShift ?? 0),
|
|
792
|
-
hsl.l + (adjustments.lightnessShift ?? 0),
|
|
793
|
-
);
|
|
794
|
-
}
|
|
795
|
-
|
|
796
|
-
function buildProviderModelPalette(color: RGB): readonly RGB[] {
|
|
677
|
+
function buildProviderModelPalette(theme: Theme, color: RGB): readonly Color[] {
|
|
678
|
+
const base = providerColor(theme, color);
|
|
679
|
+
const { h, s, l } = colorToOkhsl(base);
|
|
680
|
+
const lightnessRange = (theme.appearance === "dark" ? 1 : 0) - l;
|
|
797
681
|
return [
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
682
|
+
base,
|
|
683
|
+
okhslColor(h + 16, s * 0.85, l + lightnessRange * 0.3),
|
|
684
|
+
okhslColor(h - 12, s * 0.7, l + lightnessRange * 0.6),
|
|
801
685
|
];
|
|
802
686
|
}
|
|
803
687
|
|
|
804
688
|
function chooseModelPalette(
|
|
805
689
|
range30: RangeAgg,
|
|
690
|
+
paletteColors: readonly Color[],
|
|
691
|
+
otherColor: Color,
|
|
806
692
|
topN = 4,
|
|
807
|
-
paletteColors: readonly RGB[] = PALETTE,
|
|
808
693
|
): ProviderPalette["modelPalette"] {
|
|
809
694
|
const popularity = preferredPopularityMap({
|
|
810
695
|
cost: range30.modelCost,
|
|
@@ -818,22 +703,27 @@ function chooseModelPalette(
|
|
|
818
703
|
const orderedModels = sortMapByValueDesc(popularity)
|
|
819
704
|
.slice(0, topN)
|
|
820
705
|
.map((entry) => entry.key);
|
|
821
|
-
const modelColors = new Map<ModelKey,
|
|
706
|
+
const modelColors = new Map<ModelKey, Color>();
|
|
822
707
|
|
|
823
708
|
for (let index = 0; index < orderedModels.length; index += 1) {
|
|
824
709
|
const model = orderedModels[index];
|
|
825
|
-
const color = paletteColors[index
|
|
710
|
+
const color = paletteColors[index % paletteColors.length] ?? otherColor;
|
|
826
711
|
if (model) modelColors.set(model, color);
|
|
827
712
|
}
|
|
828
713
|
|
|
829
714
|
return {
|
|
830
715
|
modelColors,
|
|
831
|
-
otherColor
|
|
716
|
+
otherColor,
|
|
832
717
|
orderedModels,
|
|
833
718
|
};
|
|
834
719
|
}
|
|
835
720
|
|
|
836
|
-
function chooseCwdPalette(
|
|
721
|
+
function chooseCwdPalette(
|
|
722
|
+
range30: RangeAgg,
|
|
723
|
+
paletteColors: readonly Color[],
|
|
724
|
+
otherColor: Color,
|
|
725
|
+
topN = 4,
|
|
726
|
+
): ProviderPalette["cwdPalette"] {
|
|
837
727
|
const popularity = preferredPopularityMap({
|
|
838
728
|
cost: range30.cwdCost,
|
|
839
729
|
tokens: range30.cwdTokens,
|
|
@@ -846,47 +736,51 @@ function chooseCwdPalette(range30: RangeAgg, topN = 4): ProviderPalette["cwdPale
|
|
|
846
736
|
const orderedCwds = sortMapByValueDesc(popularity)
|
|
847
737
|
.slice(0, topN)
|
|
848
738
|
.map((entry) => entry.key);
|
|
849
|
-
const cwdColors = new Map<CwdKey,
|
|
739
|
+
const cwdColors = new Map<CwdKey, Color>();
|
|
850
740
|
for (let index = 0; index < orderedCwds.length; index += 1) {
|
|
851
741
|
const cwd = orderedCwds[index];
|
|
852
|
-
if (cwd) cwdColors.set(cwd,
|
|
742
|
+
if (cwd) cwdColors.set(cwd, paletteColors[index % paletteColors.length] ?? otherColor);
|
|
853
743
|
}
|
|
854
744
|
|
|
855
745
|
return {
|
|
856
746
|
cwdColors,
|
|
857
|
-
otherColor
|
|
747
|
+
otherColor,
|
|
858
748
|
orderedCwds,
|
|
859
749
|
};
|
|
860
750
|
}
|
|
861
751
|
|
|
862
|
-
function buildDowPalette():
|
|
863
|
-
const
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
752
|
+
function buildDowPalette(theme: Theme): Map<DowKey, Color> {
|
|
753
|
+
const colors = theme.colors;
|
|
754
|
+
const palette = [
|
|
755
|
+
colors.mdLink,
|
|
756
|
+
colors.success,
|
|
757
|
+
colors.accent,
|
|
758
|
+
colors.syntaxVariable,
|
|
759
|
+
mixColors(colors.success, colors.syntaxVariable, 0.5),
|
|
760
|
+
colors.warning,
|
|
761
|
+
colors.error,
|
|
762
|
+
];
|
|
763
|
+
return new Map(DOW_NAMES.map((day, index) => [day, palette[index]!]));
|
|
870
764
|
}
|
|
871
765
|
|
|
872
|
-
function buildTodPalette():
|
|
873
|
-
const
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
766
|
+
function buildTodPalette(theme: Theme): Map<TodKey, Color> {
|
|
767
|
+
const colors = theme.colors;
|
|
768
|
+
return new Map([
|
|
769
|
+
["after-midnight", colors.accent],
|
|
770
|
+
["morning", colors.warning],
|
|
771
|
+
["afternoon", colors.success],
|
|
772
|
+
["evening", colors.mdLink],
|
|
773
|
+
["night", colors.syntaxString],
|
|
774
|
+
]);
|
|
881
775
|
}
|
|
882
776
|
|
|
883
777
|
function dayMixedColor(
|
|
884
778
|
day: DayAgg,
|
|
885
|
-
colorMap: Map<string,
|
|
886
|
-
otherColor:
|
|
779
|
+
colorMap: Map<string, Color>,
|
|
780
|
+
otherColor: Color,
|
|
887
781
|
mode: MeasurementMode,
|
|
888
782
|
view: BreakdownView,
|
|
889
|
-
):
|
|
783
|
+
): Color {
|
|
890
784
|
if (view === "dow") {
|
|
891
785
|
const dowKey = DOW_NAMES[mondayIndex(day.date)] ?? "Mon";
|
|
892
786
|
return colorMap.get(dowKey) ?? otherColor;
|
|
@@ -934,7 +828,7 @@ function dayMixedColor(
|
|
|
934
828
|
: day.sessionsByModel;
|
|
935
829
|
}
|
|
936
830
|
|
|
937
|
-
const parts: Array<{ color:
|
|
831
|
+
const parts: Array<{ color: Color; weight: number }> = [];
|
|
938
832
|
let otherWeight = 0;
|
|
939
833
|
for (const [key, weight] of weights.entries()) {
|
|
940
834
|
const color = colorMap.get(key);
|
|
@@ -943,7 +837,7 @@ function dayMixedColor(
|
|
|
943
837
|
}
|
|
944
838
|
if (otherWeight > 0) parts.push({ color: otherColor, weight: otherWeight });
|
|
945
839
|
|
|
946
|
-
return weightedMix(parts);
|
|
840
|
+
return weightedMix(parts, otherColor);
|
|
947
841
|
}
|
|
948
842
|
|
|
949
843
|
interface GraphMetric {
|
|
@@ -981,9 +875,10 @@ function weeksForRange(range: RangeAgg): number {
|
|
|
981
875
|
}
|
|
982
876
|
|
|
983
877
|
function renderGraphLines(
|
|
878
|
+
theme: Theme,
|
|
984
879
|
range: RangeAgg,
|
|
985
|
-
colorMap: Map<string,
|
|
986
|
-
otherColor:
|
|
880
|
+
colorMap: Map<string, Color>,
|
|
881
|
+
otherColor: Color,
|
|
987
882
|
mode: MeasurementMode,
|
|
988
883
|
options?: { cellWidth?: number; gap?: number },
|
|
989
884
|
view: BreakdownView = "model",
|
|
@@ -1004,6 +899,8 @@ function renderGraphLines(
|
|
|
1004
899
|
const gap = Math.max(0, Math.floor(options?.gap ?? 1));
|
|
1005
900
|
const block = "█".repeat(cellWidth);
|
|
1006
901
|
const gapString = " ".repeat(gap);
|
|
902
|
+
const emptyColor = emptyChartColor(theme);
|
|
903
|
+
const emptyCell = theme.style(block, { fg: emptyColor });
|
|
1007
904
|
|
|
1008
905
|
const metric = graphMetricForRange(range, mode);
|
|
1009
906
|
const denom = metric.denom;
|
|
@@ -1037,14 +934,14 @@ function renderGraphLines(
|
|
|
1037
934
|
: (day?.sessions ?? 0);
|
|
1038
935
|
|
|
1039
936
|
if (!day || value <= 0) {
|
|
1040
|
-
line +=
|
|
937
|
+
line += emptyCell + columnGap;
|
|
1041
938
|
continue;
|
|
1042
939
|
}
|
|
1043
940
|
|
|
1044
941
|
const hue = dayMixedColor(day, colorMap, otherColor, mode, view);
|
|
1045
942
|
const intensity = 0.2 + 0.8 * clamp01(denom > 0 ? Math.log1p(value) / denom : 0);
|
|
1046
|
-
const
|
|
1047
|
-
line +=
|
|
943
|
+
const color = intensity === 1 ? hue : mixColors(emptyColor, hue, intensity);
|
|
944
|
+
line += theme.style(block, { fg: color }) + columnGap;
|
|
1048
945
|
}
|
|
1049
946
|
|
|
1050
947
|
lines.push(line);
|
|
@@ -1054,9 +951,10 @@ function renderGraphLines(
|
|
|
1054
951
|
}
|
|
1055
952
|
|
|
1056
953
|
function renderDowDistributionLines(
|
|
954
|
+
theme: Theme,
|
|
1057
955
|
range: RangeAgg,
|
|
1058
956
|
mode: MeasurementMode,
|
|
1059
|
-
dowColors: Map<DowKey,
|
|
957
|
+
dowColors: Map<DowKey, Color>,
|
|
1060
958
|
width: number,
|
|
1061
959
|
): string[] {
|
|
1062
960
|
const metric = graphMetricForRange(range, mode);
|
|
@@ -1075,6 +973,7 @@ function renderDowDistributionLines(
|
|
|
1075
973
|
const showValue = width >= dayWidth + 1 + 10 + 1 + pctWidth + 1 + valueWidth;
|
|
1076
974
|
const fixedWidth = dayWidth + 1 + 1 + pctWidth + (showValue ? 1 + valueWidth : 0);
|
|
1077
975
|
const barWidth = Math.max(1, width - fixedWidth);
|
|
976
|
+
const emptyColor = emptyChartColor(theme);
|
|
1078
977
|
|
|
1079
978
|
const lines: string[] = [];
|
|
1080
979
|
for (const dow of DOW_NAMES) {
|
|
@@ -1085,14 +984,13 @@ function renderDowDistributionLines(
|
|
|
1085
984
|
filled = Math.min(barWidth, filled);
|
|
1086
985
|
const empty = Math.max(0, barWidth - filled);
|
|
1087
986
|
|
|
1088
|
-
const color = dowColors.get(dow) ??
|
|
1089
|
-
const filledBar = filled > 0 ?
|
|
1090
|
-
const emptyBar = empty > 0 ?
|
|
987
|
+
const color = dowColors.get(dow) ?? theme.colors.muted;
|
|
988
|
+
const filledBar = filled > 0 ? theme.style("█".repeat(filled), { fg: color }) : "";
|
|
989
|
+
const emptyBar = empty > 0 ? theme.style("█".repeat(empty), { fg: emptyColor }) : "";
|
|
1091
990
|
const pct = padLeft(`${Math.round(share * 100)}%`, pctWidth);
|
|
1092
991
|
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
lines.push(line);
|
|
992
|
+
const stats = `${pct}${showValue ? ` ${padLeft(formatCount(value), valueWidth)}` : ""}`;
|
|
993
|
+
lines.push(`${padRight(dow, dayWidth)} ${filledBar}${emptyBar} ${theme.fg("text", stats)}`);
|
|
1096
994
|
}
|
|
1097
995
|
|
|
1098
996
|
return lines;
|
|
@@ -1105,6 +1003,7 @@ function formatModelLabel(modelKey: string, stripProvider: boolean): string {
|
|
|
1105
1003
|
}
|
|
1106
1004
|
|
|
1107
1005
|
function renderModelTable(
|
|
1006
|
+
theme: Theme,
|
|
1108
1007
|
range: RangeAgg,
|
|
1109
1008
|
metric: GraphMetric,
|
|
1110
1009
|
maxRows = 8,
|
|
@@ -1144,11 +1043,11 @@ function renderModelTable(
|
|
|
1144
1043
|
);
|
|
1145
1044
|
}
|
|
1146
1045
|
|
|
1147
|
-
if (sorted.length === 0) lines.push(
|
|
1046
|
+
if (sorted.length === 0) lines.push(theme.fg("muted", "(no model data found)"));
|
|
1148
1047
|
return lines;
|
|
1149
1048
|
}
|
|
1150
1049
|
|
|
1151
|
-
function renderCwdTable(range: RangeAgg, metric: GraphMetric, maxRows = 8): string[] {
|
|
1050
|
+
function renderCwdTable(theme: Theme, range: RangeAgg, metric: GraphMetric, maxRows = 8): string[] {
|
|
1152
1051
|
const { values: perCwd, total } = metricValuesForKind(metric.kind, {
|
|
1153
1052
|
sessions: range.cwdSessions,
|
|
1154
1053
|
messages: range.cwdMessages,
|
|
@@ -1186,7 +1085,7 @@ function renderCwdTable(range: RangeAgg, metric: GraphMetric, maxRows = 8): stri
|
|
|
1186
1085
|
);
|
|
1187
1086
|
}
|
|
1188
1087
|
|
|
1189
|
-
if (sorted.length === 0) lines.push(
|
|
1088
|
+
if (sorted.length === 0) lines.push(theme.fg("muted", "(no directory data found)"));
|
|
1190
1089
|
return lines;
|
|
1191
1090
|
}
|
|
1192
1091
|
|
|
@@ -1541,26 +1440,6 @@ async function computeBreakdown(
|
|
|
1541
1440
|
);
|
|
1542
1441
|
for (const file of files) addParsedFile(file);
|
|
1543
1442
|
|
|
1544
|
-
const allPaletteRange = allRanges.get(30)!;
|
|
1545
|
-
const allPalette: ProviderPalette = {
|
|
1546
|
-
modelPalette: chooseModelPalette(allPaletteRange),
|
|
1547
|
-
cwdPalette: chooseCwdPalette(allPaletteRange),
|
|
1548
|
-
};
|
|
1549
|
-
|
|
1550
|
-
const providerPalettes = new Map<HistoryProviderId, ProviderPalette>();
|
|
1551
|
-
for (const [providerId, ranges] of providerRangesById.entries()) {
|
|
1552
|
-
const range30 = ranges.get(30)!;
|
|
1553
|
-
const provider = getSupportedProvider(providerId);
|
|
1554
|
-
providerPalettes.set(providerId, {
|
|
1555
|
-
modelPalette: chooseModelPalette(
|
|
1556
|
-
range30,
|
|
1557
|
-
3,
|
|
1558
|
-
provider ? buildProviderModelPalette(provider.color) : PALETTE,
|
|
1559
|
-
),
|
|
1560
|
-
cwdPalette: chooseCwdPalette(range30),
|
|
1561
|
-
});
|
|
1562
|
-
}
|
|
1563
|
-
|
|
1564
1443
|
const liveAvailableProviders = getLiveAvailableProviders(availabilityByProvider);
|
|
1565
1444
|
|
|
1566
1445
|
return {
|
|
@@ -1568,14 +1447,11 @@ async function computeBreakdown(
|
|
|
1568
1447
|
liveUsage: buildInitialLiveUsage(SUPPORTED_PROVIDERS, availabilityByProvider),
|
|
1569
1448
|
allRanges,
|
|
1570
1449
|
providerRangesById,
|
|
1571
|
-
allPalette,
|
|
1572
|
-
providerPalettes,
|
|
1573
|
-
dowPalette: buildDowPalette(),
|
|
1574
|
-
todPalette: buildTodPalette(),
|
|
1575
1450
|
};
|
|
1576
1451
|
}
|
|
1577
1452
|
|
|
1578
1453
|
function renderUsageMeterBar(
|
|
1454
|
+
theme: Theme,
|
|
1579
1455
|
providerId: SupportedProviderId,
|
|
1580
1456
|
meter: LiveUsageMeter,
|
|
1581
1457
|
width: number,
|
|
@@ -1583,9 +1459,12 @@ function renderUsageMeterBar(
|
|
|
1583
1459
|
const barWidth = Math.max(10, Math.min(32, width));
|
|
1584
1460
|
const filled = Math.min(barWidth, Math.max(0, Math.round((barWidth * meter.usedPercent) / 100)));
|
|
1585
1461
|
const empty = Math.max(0, barWidth - filled);
|
|
1586
|
-
const
|
|
1587
|
-
const
|
|
1588
|
-
|
|
1462
|
+
const provider = getSupportedProvider(providerId);
|
|
1463
|
+
const color = provider ? providerColor(theme, provider.color) : theme.colors.muted;
|
|
1464
|
+
const bar =
|
|
1465
|
+
theme.style("█".repeat(filled), { fg: color }) +
|
|
1466
|
+
theme.style("█".repeat(empty), { fg: emptyChartColor(theme) });
|
|
1467
|
+
return truncateUsageLine(theme, bar, width);
|
|
1589
1468
|
}
|
|
1590
1469
|
|
|
1591
1470
|
function formatUsageHeader(
|
|
@@ -1601,10 +1480,14 @@ function formatUsageHeader(
|
|
|
1601
1480
|
};
|
|
1602
1481
|
}
|
|
1603
1482
|
|
|
1604
|
-
function renderUsageHeader(
|
|
1483
|
+
function renderUsageHeader(
|
|
1484
|
+
theme: Theme,
|
|
1485
|
+
plan: string | undefined,
|
|
1486
|
+
fetchedAt: Date | undefined,
|
|
1487
|
+
): string {
|
|
1605
1488
|
const header = formatUsageHeader(plan, fetchedAt);
|
|
1606
|
-
let text = bold(header.title);
|
|
1607
|
-
if (header.fetched) text += ` ${
|
|
1489
|
+
let text = theme.bold(header.title);
|
|
1490
|
+
if (header.fetched) text += ` ${theme.fg("muted", header.fetched)}`;
|
|
1608
1491
|
return text;
|
|
1609
1492
|
}
|
|
1610
1493
|
|
|
@@ -1639,26 +1522,24 @@ function resolveLiveUsageState(
|
|
|
1639
1522
|
return { kind: "ok", snapshot: liveState.snapshot };
|
|
1640
1523
|
}
|
|
1641
1524
|
|
|
1642
|
-
function formatLiveUsageItemLine(
|
|
1643
|
-
item: LiveUsageItem,
|
|
1644
|
-
options?: { dimReset?: boolean; emphasizeStatValue?: boolean },
|
|
1645
|
-
): string {
|
|
1525
|
+
function formatLiveUsageItemLine(item: LiveUsageItem, theme?: Theme): string {
|
|
1646
1526
|
if (item.kind === "meter") {
|
|
1647
1527
|
const label = item.windowLabel ? `${item.label} (${item.windowLabel})` : item.label;
|
|
1648
1528
|
let text = `${label}: ${formatUsedPercent(item.usedPercent)}`;
|
|
1649
1529
|
if (item.detail) text += ` · ${item.detail}`;
|
|
1650
1530
|
if (item.resetDescription) {
|
|
1651
|
-
const reset =
|
|
1531
|
+
const reset = theme ? theme.fg("muted", item.resetDescription) : item.resetDescription;
|
|
1652
1532
|
text += ` · ${reset}`;
|
|
1653
1533
|
}
|
|
1654
1534
|
return text;
|
|
1655
1535
|
}
|
|
1656
1536
|
|
|
1657
|
-
const value =
|
|
1537
|
+
const value = theme ? theme.bold(item.value) : item.value;
|
|
1658
1538
|
return `${item.label}: ${value}${item.detail ? ` · ${item.detail}` : ""}`;
|
|
1659
1539
|
}
|
|
1660
1540
|
|
|
1661
1541
|
function renderLiveUsageLines(
|
|
1542
|
+
theme: Theme,
|
|
1662
1543
|
tab: BreakdownTab,
|
|
1663
1544
|
liveState: LiveUsageState | undefined,
|
|
1664
1545
|
width: number,
|
|
@@ -1667,27 +1548,26 @@ function renderLiveUsageLines(
|
|
|
1667
1548
|
|
|
1668
1549
|
const resolved = resolveLiveUsageState(tab, liveState);
|
|
1669
1550
|
if (resolved.kind === "loading") {
|
|
1670
|
-
return [`${bold("Usage")} ${
|
|
1551
|
+
return [`${theme.bold("Usage")} ${theme.fg("muted", resolved.message)}`];
|
|
1671
1552
|
}
|
|
1672
1553
|
|
|
1673
1554
|
const lines: string[] = [
|
|
1674
1555
|
renderUsageHeader(
|
|
1556
|
+
theme,
|
|
1675
1557
|
resolved.kind === "ok" ? resolved.snapshot.plan : undefined,
|
|
1676
1558
|
resolved.kind === "ok" ? resolved.snapshot.fetchedAt : undefined,
|
|
1677
1559
|
),
|
|
1678
1560
|
];
|
|
1679
1561
|
if (resolved.kind === "message") {
|
|
1680
|
-
|
|
1681
|
-
resolved.tone === "error" ? ansiFg(ERROR_COLOR, resolved.message) : dim(resolved.message);
|
|
1682
|
-
lines.push(message);
|
|
1562
|
+
lines.push(theme.fg(resolved.tone, resolved.message));
|
|
1683
1563
|
return lines;
|
|
1684
1564
|
}
|
|
1685
1565
|
|
|
1686
1566
|
for (const item of resolved.snapshot.items) {
|
|
1687
1567
|
lines.push("");
|
|
1688
|
-
lines.push(formatLiveUsageItemLine(item,
|
|
1568
|
+
lines.push(formatLiveUsageItemLine(item, theme));
|
|
1689
1569
|
if (item.kind === "meter" && tab.liveProvider) {
|
|
1690
|
-
lines.push(renderUsageMeterBar(tab.liveProvider.id, item, width));
|
|
1570
|
+
lines.push(renderUsageMeterBar(theme, tab.liveProvider.id, item, width));
|
|
1691
1571
|
}
|
|
1692
1572
|
}
|
|
1693
1573
|
|
|
@@ -1754,6 +1634,7 @@ class UsageBreakdownComponent implements Component {
|
|
|
1754
1634
|
constructor(
|
|
1755
1635
|
data: BreakdownData,
|
|
1756
1636
|
tui: TUI,
|
|
1637
|
+
private readonly theme: Theme,
|
|
1757
1638
|
private readonly keybindings: KeybindingsManager,
|
|
1758
1639
|
onDone: () => void,
|
|
1759
1640
|
loadLiveUsage: (
|
|
@@ -1873,13 +1754,20 @@ class UsageBreakdownComponent implements Component {
|
|
|
1873
1754
|
|
|
1874
1755
|
render(width: number): string[] {
|
|
1875
1756
|
this.renderWidth = width;
|
|
1757
|
+
const styleLine = (line: string) => {
|
|
1758
|
+
const padding = " ".repeat(Math.max(0, width - visibleWidth(line)));
|
|
1759
|
+
return (
|
|
1760
|
+
this.theme.style(line, { fg: "text", bg: "toolPendingBg" }) +
|
|
1761
|
+
this.theme.bg("toolPendingBg", padding)
|
|
1762
|
+
);
|
|
1763
|
+
};
|
|
1876
1764
|
const height = Math.max(0, this.tui.terminal.rows - 2);
|
|
1877
1765
|
// Four chrome rows plus at least one document row. Margins are supplied by the overlay.
|
|
1878
1766
|
if (height < 5 || width < 24) {
|
|
1879
1767
|
this.viewportHeight = 0;
|
|
1880
1768
|
return ["q close · resize", "Usage needs 26×7"]
|
|
1881
1769
|
.slice(0, height)
|
|
1882
|
-
.map((line) =>
|
|
1770
|
+
.map((line) => styleLine(truncateUsageLine(this.theme, line, width)));
|
|
1883
1771
|
}
|
|
1884
1772
|
const document = this.renderDocument(width);
|
|
1885
1773
|
const body = document.slice(2);
|
|
@@ -1892,14 +1780,20 @@ class UsageBreakdownComponent implements Component {
|
|
|
1892
1780
|
return [
|
|
1893
1781
|
...document.slice(0, 2),
|
|
1894
1782
|
...visible,
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1783
|
+
truncateUsageLine(
|
|
1784
|
+
this.theme,
|
|
1785
|
+
this.theme.fg("muted", `${keys("pageUp")}/${keys("pageDown")} scroll`),
|
|
1786
|
+
width,
|
|
1787
|
+
),
|
|
1788
|
+
truncateUsageLine(
|
|
1789
|
+
this.theme,
|
|
1790
|
+
this.theme.fg(
|
|
1791
|
+
"muted",
|
|
1898
1792
|
`q close · ${this.scrollOffset + 1}–${this.scrollOffset + visible.length}/${body.length}`,
|
|
1899
1793
|
),
|
|
1900
1794
|
width,
|
|
1901
1795
|
),
|
|
1902
|
-
];
|
|
1796
|
+
].map(styleLine);
|
|
1903
1797
|
}
|
|
1904
1798
|
|
|
1905
1799
|
private async close(): Promise<void> {
|
|
@@ -1961,49 +1855,70 @@ class UsageBreakdownComponent implements Component {
|
|
|
1961
1855
|
? this.data.allRanges.get(selectedDays)!
|
|
1962
1856
|
: (this.data.providerRangesById.get(tab.id)?.get(selectedDays) ??
|
|
1963
1857
|
getEmptyRange(selectedDays));
|
|
1964
|
-
const
|
|
1858
|
+
const theme = this.theme;
|
|
1859
|
+
const colors = theme.colors;
|
|
1860
|
+
const paletteColors = [
|
|
1861
|
+
colors.accent,
|
|
1862
|
+
colors.mdLink,
|
|
1863
|
+
colors.success,
|
|
1864
|
+
colors.warning,
|
|
1865
|
+
colors.error,
|
|
1866
|
+
];
|
|
1867
|
+
const otherColor = colors.muted;
|
|
1868
|
+
const paletteRange =
|
|
1965
1869
|
tab.mode === "all"
|
|
1966
|
-
? this.data.
|
|
1967
|
-
: (this.data.
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1870
|
+
? this.data.allRanges.get(30)!
|
|
1871
|
+
: (this.data.providerRangesById.get(tab.id)?.get(30) ?? getEmptyRange(30));
|
|
1872
|
+
const palette: ProviderPalette = {
|
|
1873
|
+
modelPalette: chooseModelPalette(
|
|
1874
|
+
paletteRange,
|
|
1875
|
+
tab.liveProvider ? buildProviderModelPalette(theme, tab.liveProvider.color) : paletteColors,
|
|
1876
|
+
otherColor,
|
|
1877
|
+
tab.mode === "all" ? 4 : 3,
|
|
1878
|
+
),
|
|
1879
|
+
cwdPalette: chooseCwdPalette(paletteRange, paletteColors, otherColor),
|
|
1880
|
+
};
|
|
1881
|
+
const dowColors = buildDowPalette(theme);
|
|
1882
|
+
const todColors = buildTodPalette(theme);
|
|
1979
1883
|
const liveState = tab.liveProvider ? this.data.liveUsage.get(tab.liveProvider.id) : undefined;
|
|
1980
1884
|
const metric = graphMetricForRange(range, this.measurement);
|
|
1981
1885
|
|
|
1982
|
-
const providerBadge = bold(`[${tab.shortLabel}]`);
|
|
1886
|
+
const providerBadge = theme.bold(`[${tab.shortLabel}]`);
|
|
1983
1887
|
const rangeTab = (days: number, index: number): string =>
|
|
1984
|
-
index === this.rangeIndex
|
|
1888
|
+
index === this.rangeIndex
|
|
1889
|
+
? theme.style(`[${days}d]`, { fg: "text", bold: true })
|
|
1890
|
+
: theme.fg("muted", ` ${days}d `);
|
|
1985
1891
|
const metricTab = (mode: MeasurementMode, label: string): string =>
|
|
1986
|
-
mode === this.measurement
|
|
1892
|
+
mode === this.measurement
|
|
1893
|
+
? theme.style(`[${label}]`, { fg: "text", bold: true })
|
|
1894
|
+
: theme.fg("muted", ` ${label} `);
|
|
1987
1895
|
const viewTab = (candidate: BreakdownView, label: string): string =>
|
|
1988
|
-
candidate === this.view
|
|
1896
|
+
candidate === this.view
|
|
1897
|
+
? theme.style(`[${label}]`, { fg: "text", bold: true })
|
|
1898
|
+
: theme.fg("muted", ` ${label} `);
|
|
1989
1899
|
|
|
1990
1900
|
const header =
|
|
1991
|
-
`${bold("Usage")} ${providerBadge}${
|
|
1901
|
+
`${theme.bold("Usage")} ${providerBadge}${theme.fg("muted", ` ${this.tabIndex + 1}/${this.data.tabs.length}`)} ` +
|
|
1992
1902
|
`${rangeTab(7, 0)}${rangeTab(30, 1)}${rangeTab(90, 2)} ` +
|
|
1993
1903
|
`${metricTab("sessions", "sess")}${metricTab("messages", "msg")}${metricTab("tokens", "tok")} ` +
|
|
1994
1904
|
`${viewTab("model", "model")}${viewTab("cwd", "cwd")}${viewTab("dow", "dow")}${viewTab("tod", "tod")}`;
|
|
1995
1905
|
|
|
1996
1906
|
const lines: string[] = [];
|
|
1997
|
-
const compactHeader = `${bold("Usage")} ${providerBadge} [${selectedDays}d] [${this.measurement === "sessions" ? "sess" : this.measurement === "messages" ? "msg" : "tok"}] [${this.view}]`;
|
|
1998
|
-
lines.push(
|
|
1999
|
-
|
|
2000
|
-
|
|
1907
|
+
const compactHeader = `${theme.bold("Usage")} ${providerBadge} [${selectedDays}d] [${this.measurement === "sessions" ? "sess" : this.measurement === "messages" ? "msg" : "tok"}] [${this.view}]`;
|
|
1908
|
+
lines.push(
|
|
1909
|
+
truncateUsageLine(theme, visibleWidth(header) <= width ? header : compactHeader, width),
|
|
1910
|
+
);
|
|
1911
|
+
const navigation = theme.fg(
|
|
1912
|
+
"muted",
|
|
1913
|
+
"[/] provider · ←/→ range · ↑/↓ view · tab metric · q to close",
|
|
1914
|
+
);
|
|
1915
|
+
lines.push(truncateUsageLine(theme, navigation, width));
|
|
2001
1916
|
if (visibleWidth(compactHeader) > width) lines.push(compactHeader);
|
|
2002
1917
|
if (visibleWidth(navigation) > width) lines.push(navigation);
|
|
2003
|
-
lines.push(`${bold(tab.label)} ${
|
|
1918
|
+
lines.push(`${theme.bold(tab.label)} ${theme.fg("muted", historySemanticsText(tab))}`);
|
|
2004
1919
|
lines.push("");
|
|
2005
1920
|
|
|
2006
|
-
const liveLines = renderLiveUsageLines(tab, liveState, width);
|
|
1921
|
+
const liveLines = renderLiveUsageLines(theme, tab, liveState, width);
|
|
2007
1922
|
if (liveLines.length > 0) {
|
|
2008
1923
|
lines.push(...liveLines);
|
|
2009
1924
|
lines.push("");
|
|
@@ -2012,12 +1927,12 @@ class UsageBreakdownComponent implements Component {
|
|
|
2012
1927
|
const graphDescriptor =
|
|
2013
1928
|
this.view === "dow" ? `share of ${metric.kind} by weekday` : `${metric.kind}/day`;
|
|
2014
1929
|
lines.push(
|
|
2015
|
-
`${rangeSummary(range, selectedDays, metric.kind)}${
|
|
1930
|
+
`${rangeSummary(range, selectedDays, metric.kind)}${theme.fg("muted", ` (graph: ${graphDescriptor})`)}`,
|
|
2016
1931
|
);
|
|
2017
1932
|
lines.push("");
|
|
2018
1933
|
|
|
2019
|
-
let activeColorMap: Map<string,
|
|
2020
|
-
let activeOtherColor =
|
|
1934
|
+
let activeColorMap: Map<string, Color>;
|
|
1935
|
+
let activeOtherColor = otherColor;
|
|
2021
1936
|
const legendItems: string[] = [];
|
|
2022
1937
|
const stripProvider = tab.mode === "provider";
|
|
2023
1938
|
|
|
@@ -2027,39 +1942,46 @@ class UsageBreakdownComponent implements Component {
|
|
|
2027
1942
|
for (const model of palette.modelPalette.orderedModels) {
|
|
2028
1943
|
const color = activeColorMap.get(model);
|
|
2029
1944
|
if (color)
|
|
2030
|
-
legendItems.push(
|
|
1945
|
+
legendItems.push(
|
|
1946
|
+
`${theme.style("█", { fg: color })} ${theme.fg("text", formatModelLabel(model, stripProvider))}`,
|
|
1947
|
+
);
|
|
2031
1948
|
}
|
|
2032
|
-
legendItems.push(
|
|
1949
|
+
legendItems.push(
|
|
1950
|
+
`${theme.style("█", { fg: activeOtherColor })} ${theme.fg("text", "other")}`,
|
|
1951
|
+
);
|
|
2033
1952
|
} else if (this.view === "cwd") {
|
|
2034
1953
|
activeColorMap = palette.cwdPalette.cwdColors;
|
|
2035
1954
|
activeOtherColor = palette.cwdPalette.otherColor;
|
|
2036
1955
|
for (const cwd of palette.cwdPalette.orderedCwds) {
|
|
2037
1956
|
const color = activeColorMap.get(cwd);
|
|
2038
|
-
if (color)
|
|
1957
|
+
if (color)
|
|
1958
|
+
legendItems.push(
|
|
1959
|
+
`${theme.style("█", { fg: color })} ${theme.fg("text", abbreviatePath(cwd, 30))}`,
|
|
1960
|
+
);
|
|
2039
1961
|
}
|
|
2040
|
-
legendItems.push(
|
|
1962
|
+
legendItems.push(
|
|
1963
|
+
`${theme.style("█", { fg: activeOtherColor })} ${theme.fg("text", "other")}`,
|
|
1964
|
+
);
|
|
2041
1965
|
} else if (this.view === "dow") {
|
|
2042
|
-
activeColorMap =
|
|
2043
|
-
for (const dow of
|
|
1966
|
+
activeColorMap = dowColors;
|
|
1967
|
+
for (const dow of DOW_NAMES) {
|
|
2044
1968
|
const color = activeColorMap.get(dow);
|
|
2045
|
-
if (color) legendItems.push(`${
|
|
1969
|
+
if (color) legendItems.push(`${theme.style("█", { fg: color })} ${theme.fg("text", dow)}`);
|
|
2046
1970
|
}
|
|
2047
1971
|
} else {
|
|
2048
|
-
activeColorMap =
|
|
2049
|
-
for (const tod of
|
|
1972
|
+
activeColorMap = todColors;
|
|
1973
|
+
for (const { key: tod } of TOD_BUCKETS) {
|
|
2050
1974
|
const color = activeColorMap.get(tod);
|
|
2051
|
-
if (color)
|
|
1975
|
+
if (color)
|
|
1976
|
+
legendItems.push(
|
|
1977
|
+
`${theme.style("█", { fg: color })} ${theme.fg("text", todBucketLabel(tod))}`,
|
|
1978
|
+
);
|
|
2052
1979
|
}
|
|
2053
1980
|
}
|
|
2054
1981
|
|
|
2055
1982
|
let graphLines: string[];
|
|
2056
1983
|
if (this.view === "dow") {
|
|
2057
|
-
graphLines = renderDowDistributionLines(
|
|
2058
|
-
range,
|
|
2059
|
-
this.measurement,
|
|
2060
|
-
this.data.dowPalette.dowColors,
|
|
2061
|
-
width,
|
|
2062
|
-
);
|
|
1984
|
+
graphLines = renderDowDistributionLines(theme, range, this.measurement, dowColors, width);
|
|
2063
1985
|
} else {
|
|
2064
1986
|
const maxScale = selectedDays === 7 ? 4 : selectedDays === 30 ? 3 : 2;
|
|
2065
1987
|
const weeks = weeksForRange(range);
|
|
@@ -2069,6 +1991,7 @@ class UsageBreakdownComponent implements Component {
|
|
|
2069
1991
|
const idealCellWidth = Math.floor((graphArea + gap) / Math.max(1, weeks)) - gap;
|
|
2070
1992
|
const cellWidth = Math.min(maxScale, Math.max(1, idealCellWidth));
|
|
2071
1993
|
graphLines = renderGraphLines(
|
|
1994
|
+
theme,
|
|
2072
1995
|
range,
|
|
2073
1996
|
activeColorMap,
|
|
2074
1997
|
activeOtherColor,
|
|
@@ -2081,15 +2004,15 @@ class UsageBreakdownComponent implements Component {
|
|
|
2081
2004
|
|
|
2082
2005
|
const tableLines =
|
|
2083
2006
|
this.view === "model"
|
|
2084
|
-
? renderModelTable(range, metric, 8, { stripProvider })
|
|
2007
|
+
? renderModelTable(theme, range, metric, 8, { stripProvider })
|
|
2085
2008
|
: this.view === "cwd"
|
|
2086
|
-
? renderCwdTable(range, metric, 8)
|
|
2009
|
+
? renderCwdTable(theme, range, metric, 8)
|
|
2087
2010
|
: this.view === "dow"
|
|
2088
2011
|
? renderDowTable(range, metric)
|
|
2089
2012
|
: renderTodTable(range, metric);
|
|
2090
2013
|
|
|
2091
2014
|
if (this.view === "dow") {
|
|
2092
|
-
for (const line of graphLines) lines.push(
|
|
2015
|
+
for (const line of graphLines) lines.push(truncateUsageLine(theme, line, width));
|
|
2093
2016
|
} else {
|
|
2094
2017
|
const graphWidth = Math.max(0, ...graphLines.map((line) => visibleWidth(line)));
|
|
2095
2018
|
const separator = 2;
|
|
@@ -2103,12 +2026,15 @@ class UsageBreakdownComponent implements Component {
|
|
|
2103
2026
|
: this.view === "cwd"
|
|
2104
2027
|
? "Top directories (30d palette):"
|
|
2105
2028
|
: "Time of day:";
|
|
2106
|
-
const legendBlock = [
|
|
2029
|
+
const legendBlock = [theme.fg("muted", legendTitle), ...legendItems];
|
|
2107
2030
|
const maxLegendRows = graphLines.length;
|
|
2108
2031
|
let legendLines = legendBlock.slice(0, maxLegendRows);
|
|
2109
2032
|
if (legendBlock.length > maxLegendRows) {
|
|
2110
2033
|
const remaining = legendBlock.length - (maxLegendRows - 1);
|
|
2111
|
-
legendLines = [
|
|
2034
|
+
legendLines = [
|
|
2035
|
+
...legendBlock.slice(0, maxLegendRows - 1),
|
|
2036
|
+
theme.fg("muted", `+${remaining} more`),
|
|
2037
|
+
];
|
|
2112
2038
|
}
|
|
2113
2039
|
while (legendLines.length < graphLines.length) legendLines.push("");
|
|
2114
2040
|
|
|
@@ -2121,11 +2047,15 @@ class UsageBreakdownComponent implements Component {
|
|
|
2121
2047
|
|
|
2122
2048
|
for (let index = 0; index < graphLines.length; index += 1) {
|
|
2123
2049
|
const left = padRightAnsi(graphLines[index] ?? "", graphWidth);
|
|
2124
|
-
const right =
|
|
2125
|
-
|
|
2050
|
+
const right = truncateUsageLine(
|
|
2051
|
+
theme,
|
|
2052
|
+
legendLines[index] ?? "",
|
|
2053
|
+
Math.max(0, legendWidth),
|
|
2054
|
+
);
|
|
2055
|
+
lines.push(truncateUsageLine(theme, left + " ".repeat(separator) + right, width));
|
|
2126
2056
|
}
|
|
2127
2057
|
} else {
|
|
2128
|
-
for (const line of graphLines) lines.push(
|
|
2058
|
+
for (const line of graphLines) lines.push(truncateUsageLine(theme, line, width));
|
|
2129
2059
|
lines.push("");
|
|
2130
2060
|
const legendTitle =
|
|
2131
2061
|
this.view === "model"
|
|
@@ -2133,7 +2063,7 @@ class UsageBreakdownComponent implements Component {
|
|
|
2133
2063
|
: this.view === "cwd"
|
|
2134
2064
|
? "Top directories (30d palette):"
|
|
2135
2065
|
: "Time of day:";
|
|
2136
|
-
lines.push(
|
|
2066
|
+
lines.push(theme.fg("muted", legendTitle));
|
|
2137
2067
|
lines.push(...legendItems);
|
|
2138
2068
|
}
|
|
2139
2069
|
}
|
|
@@ -2255,11 +2185,14 @@ export default function usageBreakdownExtension(pi: ExtensionAPI) {
|
|
|
2255
2185
|
}
|
|
2256
2186
|
|
|
2257
2187
|
await ctx.ui.custom<void>(
|
|
2258
|
-
(tui,
|
|
2259
|
-
new UsageBreakdownComponent(data, tui, keybindings, done, (provider, signal) =>
|
|
2188
|
+
(tui, theme, keybindings, done) =>
|
|
2189
|
+
new UsageBreakdownComponent(data, tui, theme, keybindings, done, (provider, signal) =>
|
|
2260
2190
|
fetchLiveUsageForProvider(provider, liveUsageAvailability, ctx, signal),
|
|
2261
2191
|
),
|
|
2262
|
-
{
|
|
2192
|
+
{
|
|
2193
|
+
overlay: true,
|
|
2194
|
+
overlayOptions: { anchor: "bottom-center", width: "100%", maxHeight: "100%", margin: 1 },
|
|
2195
|
+
},
|
|
2263
2196
|
);
|
|
2264
2197
|
},
|
|
2265
2198
|
});
|
package/package.json
CHANGED