tau-all-agent 0.2.0 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/extensions/answer.ts +115 -85
- package/extensions/branch-term/index.ts +9 -5
- package/extensions/btw.ts +70 -36
- package/extensions/insights.ts +70 -56
- package/extensions/loop.ts +14 -52
- package/extensions/memory.ts +65 -33
- package/extensions/notify.ts +5 -3
- package/extensions/review/fix.ts +10 -16
- package/extensions/review/git.ts +7 -1
- package/extensions/review/index.ts +2 -2
- package/extensions/review/models.ts +12 -2
- package/extensions/review/review.ts +3 -5
- package/extensions/review/runtime.ts +9 -8
- package/extensions/review/triage.ts +2 -4
- package/extensions/sandbox/README.md +7 -0
- package/extensions/sandbox/bash.ts +83 -85
- package/extensions/sandbox/command.ts +15 -1
- package/extensions/sandbox/config.ts +26 -15
- package/extensions/sandbox/index.ts +21 -11
- package/extensions/sandbox/permissions/network.ts +8 -3
- package/extensions/sandbox/permissions/unsandboxed.ts +1 -240
- package/extensions/sandbox/runtime.ts +54 -20
- package/extensions/subagent/README.md +58 -1
- package/extensions/subagent/config.ts +118 -0
- package/extensions/subagent/index.ts +54 -22
- package/extensions/telegram/README.md +2 -0
- package/extensions/telegram/assistant-result.d.mts +4 -1
- package/extensions/telegram/assistant-result.mjs +31 -16
- package/extensions/telegram/daemon.mjs +146 -194
- package/extensions/telegram/index.ts +11 -7
- package/extensions/tool-display-mode.ts +29 -50
- package/extensions/usage/README.md +5 -0
- package/extensions/usage/index.ts +341 -349
- package/extensions/websearch/README.md +8 -1
- package/extensions/websearch/config.ts +2 -1
- package/extensions/websearch/index.ts +18 -9
- package/extensions/websearch/providers/{openai-codex.pi.ts → openai.pi.ts} +51 -16
- package/extensions/websearch/providers/shared.ts +10 -3
- package/extensions/websearch/types.ts +2 -1
- package/extensions/worktree.ts +33 -89
- package/package.json +1 -1
- package/skills/oracle/SKILL.md +14 -8
- package/skills/oracle/scripts/oracle +43 -19
- package/skills/update-changelog/SKILL.md +8 -4
|
@@ -3,14 +3,22 @@ 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,
|
|
17
|
+
type KeybindingsManager,
|
|
11
18
|
type TUI,
|
|
12
19
|
truncateToWidth,
|
|
13
20
|
visibleWidth,
|
|
21
|
+
wrapTextWithAnsi,
|
|
14
22
|
} from "@earendil-works/pi-tui";
|
|
15
23
|
import { createHash } from "node:crypto";
|
|
16
24
|
import { createReadStream, type Dirent } from "node:fs";
|
|
@@ -52,37 +60,6 @@ const TOD_BUCKETS: { key: TodKey; label: string; from: number; to: number }[] =
|
|
|
52
60
|
{ key: "night", label: "Night (22–23)", from: 22, to: 23 },
|
|
53
61
|
];
|
|
54
62
|
|
|
55
|
-
const DEFAULT_BG: RGB = { r: 13, g: 17, b: 23 };
|
|
56
|
-
const EMPTY_CELL_BG: RGB = { r: 22, g: 27, b: 34 };
|
|
57
|
-
const ERROR_COLOR: RGB = { r: 244, g: 67, b: 54 };
|
|
58
|
-
const MUTED_COLOR: RGB = { r: 160, g: 160, b: 160 };
|
|
59
|
-
|
|
60
|
-
const PALETTE: RGB[] = [
|
|
61
|
-
{ r: 54, g: 179, b: 166 },
|
|
62
|
-
{ r: 82, g: 146, b: 247 },
|
|
63
|
-
{ r: 171, g: 123, b: 245 },
|
|
64
|
-
{ r: 232, g: 180, b: 72 },
|
|
65
|
-
{ r: 244, g: 67, b: 54 },
|
|
66
|
-
];
|
|
67
|
-
|
|
68
|
-
const DOW_PALETTE: RGB[] = [
|
|
69
|
-
{ r: 47, g: 129, b: 247 },
|
|
70
|
-
{ r: 64, g: 196, b: 99 },
|
|
71
|
-
{ r: 163, g: 113, b: 247 },
|
|
72
|
-
{ r: 47, g: 175, b: 200 },
|
|
73
|
-
{ r: 100, g: 200, b: 150 },
|
|
74
|
-
{ r: 255, g: 159, b: 10 },
|
|
75
|
-
{ r: 244, g: 67, b: 54 },
|
|
76
|
-
];
|
|
77
|
-
|
|
78
|
-
const TOD_PALETTE: Map<TodKey, RGB> = new Map([
|
|
79
|
-
["after-midnight", { r: 100, g: 60, b: 180 }],
|
|
80
|
-
["morning", { r: 255, g: 200, b: 50 }],
|
|
81
|
-
["afternoon", { r: 64, g: 196, b: 99 }],
|
|
82
|
-
["evening", { r: 47, g: 129, b: 247 }],
|
|
83
|
-
["night", { r: 60, g: 40, b: 140 }],
|
|
84
|
-
]);
|
|
85
|
-
|
|
86
63
|
type HistoryProviderId = string;
|
|
87
64
|
type ModelKey = string;
|
|
88
65
|
type CwdKey = string;
|
|
@@ -164,13 +141,13 @@ interface RangeAgg {
|
|
|
164
141
|
|
|
165
142
|
interface ProviderPalette {
|
|
166
143
|
modelPalette: {
|
|
167
|
-
modelColors: Map<ModelKey,
|
|
168
|
-
otherColor:
|
|
144
|
+
modelColors: Map<ModelKey, Color>;
|
|
145
|
+
otherColor: Color;
|
|
169
146
|
orderedModels: ModelKey[];
|
|
170
147
|
};
|
|
171
148
|
cwdPalette: {
|
|
172
|
-
cwdColors: Map<CwdKey,
|
|
173
|
-
otherColor:
|
|
149
|
+
cwdColors: Map<CwdKey, Color>;
|
|
150
|
+
otherColor: Color;
|
|
174
151
|
orderedCwds: CwdKey[];
|
|
175
152
|
};
|
|
176
153
|
}
|
|
@@ -188,16 +165,6 @@ interface BreakdownData {
|
|
|
188
165
|
liveUsage: Map<SupportedProviderId, LiveUsageState>;
|
|
189
166
|
allRanges: Map<number, RangeAgg>;
|
|
190
167
|
providerRangesById: Map<HistoryProviderId, Map<number, RangeAgg>>;
|
|
191
|
-
allPalette: ProviderPalette;
|
|
192
|
-
providerPalettes: Map<HistoryProviderId, ProviderPalette>;
|
|
193
|
-
dowPalette: {
|
|
194
|
-
dowColors: Map<DowKey, RGB>;
|
|
195
|
-
orderedDows: DowKey[];
|
|
196
|
-
};
|
|
197
|
-
todPalette: {
|
|
198
|
-
todColors: Map<TodKey, RGB>;
|
|
199
|
-
orderedTods: TodKey[];
|
|
200
|
-
};
|
|
201
168
|
}
|
|
202
169
|
|
|
203
170
|
type BreakdownProgressPhase = "scan" | "parse" | "finalize";
|
|
@@ -227,44 +194,23 @@ function clamp01(value: number): number {
|
|
|
227
194
|
return Math.max(0, Math.min(1, value));
|
|
228
195
|
}
|
|
229
196
|
|
|
230
|
-
function
|
|
231
|
-
return a + (b - a) * t;
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
function mixRgb(a: RGB, b: RGB, t: number): RGB {
|
|
235
|
-
return {
|
|
236
|
-
r: Math.round(lerp(a.r, b.r, t)),
|
|
237
|
-
g: Math.round(lerp(a.g, b.g, t)),
|
|
238
|
-
b: Math.round(lerp(a.b, b.b, t)),
|
|
239
|
-
};
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
function weightedMix(colors: Array<{ color: RGB; weight: number }>): RGB {
|
|
197
|
+
function weightedMix(colors: Array<{ color: Color; weight: number }>, fallback: Color): Color {
|
|
243
198
|
let total = 0;
|
|
244
|
-
let
|
|
245
|
-
let g = 0;
|
|
246
|
-
let b = 0;
|
|
199
|
+
let mixed: Color | undefined;
|
|
247
200
|
for (const entry of colors) {
|
|
248
201
|
if (!Number.isFinite(entry.weight) || entry.weight <= 0) continue;
|
|
249
202
|
total += entry.weight;
|
|
250
|
-
|
|
251
|
-
g += entry.color.g * entry.weight;
|
|
252
|
-
b += entry.color.b * entry.weight;
|
|
203
|
+
mixed = mixed ? mixColors(mixed, entry.color, entry.weight / total, "srgb") : entry.color;
|
|
253
204
|
}
|
|
254
|
-
|
|
255
|
-
return { r: Math.round(r / total), g: Math.round(g / total), b: Math.round(b / total) };
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
function ansiFg(rgb: RGB, text: string): string {
|
|
259
|
-
return `\x1b[38;2;${rgb.r};${rgb.g};${rgb.b}m${text}\x1b[0m`;
|
|
205
|
+
return mixed ?? fallback;
|
|
260
206
|
}
|
|
261
207
|
|
|
262
|
-
function
|
|
263
|
-
return
|
|
208
|
+
function emptyChartColor(theme: Theme): Color {
|
|
209
|
+
return mixColors(theme.colors.toolPendingBg, theme.colors.muted, 0.25);
|
|
264
210
|
}
|
|
265
211
|
|
|
266
|
-
function
|
|
267
|
-
return
|
|
212
|
+
function truncateUsageLine(theme: Theme, text: string, width: number): string {
|
|
213
|
+
return truncateToWidth(text, width, theme.style("...", { fg: "text", bg: "toolPendingBg" }));
|
|
268
214
|
}
|
|
269
215
|
|
|
270
216
|
function formatUsd(value: number): string {
|
|
@@ -721,88 +667,29 @@ function metricValuesForKind<K>(
|
|
|
721
667
|
return { values: options.sessions, total: options.totalSessions };
|
|
722
668
|
}
|
|
723
669
|
|
|
724
|
-
function
|
|
725
|
-
const
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
function rgbToHsl(color: RGB): { h: number; s: number; l: number } {
|
|
730
|
-
const r = color.r / 255;
|
|
731
|
-
const g = color.g / 255;
|
|
732
|
-
const b = color.b / 255;
|
|
733
|
-
const max = Math.max(r, g, b);
|
|
734
|
-
const min = Math.min(r, g, b);
|
|
735
|
-
const lightness = (max + min) / 2;
|
|
736
|
-
|
|
737
|
-
if (max === min) {
|
|
738
|
-
return { h: 0, s: 0, l: lightness };
|
|
739
|
-
}
|
|
740
|
-
|
|
741
|
-
const delta = max - min;
|
|
742
|
-
const saturation = lightness > 0.5 ? delta / (2 - max - min) : delta / (max + min);
|
|
743
|
-
|
|
744
|
-
let hue = 0;
|
|
745
|
-
if (max === r) hue = (g - b) / delta + (g < b ? 6 : 0);
|
|
746
|
-
else if (max === g) hue = (b - r) / delta + 2;
|
|
747
|
-
else hue = (r - g) / delta + 4;
|
|
748
|
-
|
|
749
|
-
return { h: normalizeHue(hue * 60), s: saturation, l: lightness };
|
|
750
|
-
}
|
|
751
|
-
|
|
752
|
-
function hslToRgb(hue: number, saturation: number, lightness: number): RGB {
|
|
753
|
-
const h = normalizeHue(hue) / 360;
|
|
754
|
-
const s = clamp01(saturation);
|
|
755
|
-
const l = clamp01(lightness);
|
|
756
|
-
|
|
757
|
-
if (s === 0) {
|
|
758
|
-
const value = Math.round(l * 255);
|
|
759
|
-
return { r: value, g: value, b: value };
|
|
760
|
-
}
|
|
761
|
-
|
|
762
|
-
const hueToChannel = (p: number, q: number, t: number): number => {
|
|
763
|
-
let value = t;
|
|
764
|
-
if (value < 0) value += 1;
|
|
765
|
-
if (value > 1) value -= 1;
|
|
766
|
-
if (value < 1 / 6) return p + (q - p) * 6 * value;
|
|
767
|
-
if (value < 1 / 2) return q;
|
|
768
|
-
if (value < 2 / 3) return p + (q - p) * (2 / 3 - value) * 6;
|
|
769
|
-
return p;
|
|
770
|
-
};
|
|
771
|
-
|
|
772
|
-
const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
|
|
773
|
-
const p = 2 * l - q;
|
|
774
|
-
|
|
775
|
-
return {
|
|
776
|
-
r: Math.round(hueToChannel(p, q, h + 1 / 3) * 255),
|
|
777
|
-
g: Math.round(hueToChannel(p, q, h) * 255),
|
|
778
|
-
b: Math.round(hueToChannel(p, q, h - 1 / 3) * 255),
|
|
779
|
-
};
|
|
780
|
-
}
|
|
781
|
-
|
|
782
|
-
function adjustColor(
|
|
783
|
-
color: RGB,
|
|
784
|
-
adjustments: { hueShift?: number; saturationShift?: number; lightnessShift?: number },
|
|
785
|
-
): RGB {
|
|
786
|
-
const hsl = rgbToHsl(color);
|
|
787
|
-
return hslToRgb(
|
|
788
|
-
hsl.h + (adjustments.hueShift ?? 0),
|
|
789
|
-
hsl.s + (adjustments.saturationShift ?? 0),
|
|
790
|
-
hsl.l + (adjustments.lightnessShift ?? 0),
|
|
791
|
-
);
|
|
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);
|
|
792
675
|
}
|
|
793
676
|
|
|
794
|
-
function buildProviderModelPalette(color: RGB): readonly
|
|
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;
|
|
795
681
|
return [
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
682
|
+
base,
|
|
683
|
+
okhslColor(h + 16, s * 0.85, l + lightnessRange * 0.3),
|
|
684
|
+
okhslColor(h - 12, s * 0.7, l + lightnessRange * 0.6),
|
|
799
685
|
];
|
|
800
686
|
}
|
|
801
687
|
|
|
802
688
|
function chooseModelPalette(
|
|
803
689
|
range30: RangeAgg,
|
|
690
|
+
paletteColors: readonly Color[],
|
|
691
|
+
otherColor: Color,
|
|
804
692
|
topN = 4,
|
|
805
|
-
paletteColors: readonly RGB[] = PALETTE,
|
|
806
693
|
): ProviderPalette["modelPalette"] {
|
|
807
694
|
const popularity = preferredPopularityMap({
|
|
808
695
|
cost: range30.modelCost,
|
|
@@ -816,22 +703,27 @@ function chooseModelPalette(
|
|
|
816
703
|
const orderedModels = sortMapByValueDesc(popularity)
|
|
817
704
|
.slice(0, topN)
|
|
818
705
|
.map((entry) => entry.key);
|
|
819
|
-
const modelColors = new Map<ModelKey,
|
|
706
|
+
const modelColors = new Map<ModelKey, Color>();
|
|
820
707
|
|
|
821
708
|
for (let index = 0; index < orderedModels.length; index += 1) {
|
|
822
709
|
const model = orderedModels[index];
|
|
823
|
-
const color = paletteColors[index
|
|
710
|
+
const color = paletteColors[index % paletteColors.length] ?? otherColor;
|
|
824
711
|
if (model) modelColors.set(model, color);
|
|
825
712
|
}
|
|
826
713
|
|
|
827
714
|
return {
|
|
828
715
|
modelColors,
|
|
829
|
-
otherColor
|
|
716
|
+
otherColor,
|
|
830
717
|
orderedModels,
|
|
831
718
|
};
|
|
832
719
|
}
|
|
833
720
|
|
|
834
|
-
function chooseCwdPalette(
|
|
721
|
+
function chooseCwdPalette(
|
|
722
|
+
range30: RangeAgg,
|
|
723
|
+
paletteColors: readonly Color[],
|
|
724
|
+
otherColor: Color,
|
|
725
|
+
topN = 4,
|
|
726
|
+
): ProviderPalette["cwdPalette"] {
|
|
835
727
|
const popularity = preferredPopularityMap({
|
|
836
728
|
cost: range30.cwdCost,
|
|
837
729
|
tokens: range30.cwdTokens,
|
|
@@ -844,47 +736,51 @@ function chooseCwdPalette(range30: RangeAgg, topN = 4): ProviderPalette["cwdPale
|
|
|
844
736
|
const orderedCwds = sortMapByValueDesc(popularity)
|
|
845
737
|
.slice(0, topN)
|
|
846
738
|
.map((entry) => entry.key);
|
|
847
|
-
const cwdColors = new Map<CwdKey,
|
|
739
|
+
const cwdColors = new Map<CwdKey, Color>();
|
|
848
740
|
for (let index = 0; index < orderedCwds.length; index += 1) {
|
|
849
741
|
const cwd = orderedCwds[index];
|
|
850
|
-
if (cwd) cwdColors.set(cwd,
|
|
742
|
+
if (cwd) cwdColors.set(cwd, paletteColors[index % paletteColors.length] ?? otherColor);
|
|
851
743
|
}
|
|
852
744
|
|
|
853
745
|
return {
|
|
854
746
|
cwdColors,
|
|
855
|
-
otherColor
|
|
747
|
+
otherColor,
|
|
856
748
|
orderedCwds,
|
|
857
749
|
};
|
|
858
750
|
}
|
|
859
751
|
|
|
860
|
-
function buildDowPalette():
|
|
861
|
-
const
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
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]!]));
|
|
868
764
|
}
|
|
869
765
|
|
|
870
|
-
function buildTodPalette():
|
|
871
|
-
const
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
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
|
+
]);
|
|
879
775
|
}
|
|
880
776
|
|
|
881
777
|
function dayMixedColor(
|
|
882
778
|
day: DayAgg,
|
|
883
|
-
colorMap: Map<string,
|
|
884
|
-
otherColor:
|
|
779
|
+
colorMap: Map<string, Color>,
|
|
780
|
+
otherColor: Color,
|
|
885
781
|
mode: MeasurementMode,
|
|
886
782
|
view: BreakdownView,
|
|
887
|
-
):
|
|
783
|
+
): Color {
|
|
888
784
|
if (view === "dow") {
|
|
889
785
|
const dowKey = DOW_NAMES[mondayIndex(day.date)] ?? "Mon";
|
|
890
786
|
return colorMap.get(dowKey) ?? otherColor;
|
|
@@ -932,7 +828,7 @@ function dayMixedColor(
|
|
|
932
828
|
: day.sessionsByModel;
|
|
933
829
|
}
|
|
934
830
|
|
|
935
|
-
const parts: Array<{ color:
|
|
831
|
+
const parts: Array<{ color: Color; weight: number }> = [];
|
|
936
832
|
let otherWeight = 0;
|
|
937
833
|
for (const [key, weight] of weights.entries()) {
|
|
938
834
|
const color = colorMap.get(key);
|
|
@@ -941,7 +837,7 @@ function dayMixedColor(
|
|
|
941
837
|
}
|
|
942
838
|
if (otherWeight > 0) parts.push({ color: otherColor, weight: otherWeight });
|
|
943
839
|
|
|
944
|
-
return weightedMix(parts);
|
|
840
|
+
return weightedMix(parts, otherColor);
|
|
945
841
|
}
|
|
946
842
|
|
|
947
843
|
interface GraphMetric {
|
|
@@ -979,9 +875,10 @@ function weeksForRange(range: RangeAgg): number {
|
|
|
979
875
|
}
|
|
980
876
|
|
|
981
877
|
function renderGraphLines(
|
|
878
|
+
theme: Theme,
|
|
982
879
|
range: RangeAgg,
|
|
983
|
-
colorMap: Map<string,
|
|
984
|
-
otherColor:
|
|
880
|
+
colorMap: Map<string, Color>,
|
|
881
|
+
otherColor: Color,
|
|
985
882
|
mode: MeasurementMode,
|
|
986
883
|
options?: { cellWidth?: number; gap?: number },
|
|
987
884
|
view: BreakdownView = "model",
|
|
@@ -1002,6 +899,8 @@ function renderGraphLines(
|
|
|
1002
899
|
const gap = Math.max(0, Math.floor(options?.gap ?? 1));
|
|
1003
900
|
const block = "█".repeat(cellWidth);
|
|
1004
901
|
const gapString = " ".repeat(gap);
|
|
902
|
+
const emptyColor = emptyChartColor(theme);
|
|
903
|
+
const emptyCell = theme.style(block, { fg: emptyColor });
|
|
1005
904
|
|
|
1006
905
|
const metric = graphMetricForRange(range, mode);
|
|
1007
906
|
const denom = metric.denom;
|
|
@@ -1035,14 +934,14 @@ function renderGraphLines(
|
|
|
1035
934
|
: (day?.sessions ?? 0);
|
|
1036
935
|
|
|
1037
936
|
if (!day || value <= 0) {
|
|
1038
|
-
line +=
|
|
937
|
+
line += emptyCell + columnGap;
|
|
1039
938
|
continue;
|
|
1040
939
|
}
|
|
1041
940
|
|
|
1042
941
|
const hue = dayMixedColor(day, colorMap, otherColor, mode, view);
|
|
1043
942
|
const intensity = 0.2 + 0.8 * clamp01(denom > 0 ? Math.log1p(value) / denom : 0);
|
|
1044
|
-
const
|
|
1045
|
-
line +=
|
|
943
|
+
const color = intensity === 1 ? hue : mixColors(emptyColor, hue, intensity);
|
|
944
|
+
line += theme.style(block, { fg: color }) + columnGap;
|
|
1046
945
|
}
|
|
1047
946
|
|
|
1048
947
|
lines.push(line);
|
|
@@ -1052,9 +951,10 @@ function renderGraphLines(
|
|
|
1052
951
|
}
|
|
1053
952
|
|
|
1054
953
|
function renderDowDistributionLines(
|
|
954
|
+
theme: Theme,
|
|
1055
955
|
range: RangeAgg,
|
|
1056
956
|
mode: MeasurementMode,
|
|
1057
|
-
dowColors: Map<DowKey,
|
|
957
|
+
dowColors: Map<DowKey, Color>,
|
|
1058
958
|
width: number,
|
|
1059
959
|
): string[] {
|
|
1060
960
|
const metric = graphMetricForRange(range, mode);
|
|
@@ -1073,6 +973,7 @@ function renderDowDistributionLines(
|
|
|
1073
973
|
const showValue = width >= dayWidth + 1 + 10 + 1 + pctWidth + 1 + valueWidth;
|
|
1074
974
|
const fixedWidth = dayWidth + 1 + 1 + pctWidth + (showValue ? 1 + valueWidth : 0);
|
|
1075
975
|
const barWidth = Math.max(1, width - fixedWidth);
|
|
976
|
+
const emptyColor = emptyChartColor(theme);
|
|
1076
977
|
|
|
1077
978
|
const lines: string[] = [];
|
|
1078
979
|
for (const dow of DOW_NAMES) {
|
|
@@ -1083,14 +984,13 @@ function renderDowDistributionLines(
|
|
|
1083
984
|
filled = Math.min(barWidth, filled);
|
|
1084
985
|
const empty = Math.max(0, barWidth - filled);
|
|
1085
986
|
|
|
1086
|
-
const color = dowColors.get(dow) ??
|
|
1087
|
-
const filledBar = filled > 0 ?
|
|
1088
|
-
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 }) : "";
|
|
1089
990
|
const pct = padLeft(`${Math.round(share * 100)}%`, pctWidth);
|
|
1090
991
|
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
lines.push(line);
|
|
992
|
+
const stats = `${pct}${showValue ? ` ${padLeft(formatCount(value), valueWidth)}` : ""}`;
|
|
993
|
+
lines.push(`${padRight(dow, dayWidth)} ${filledBar}${emptyBar} ${theme.fg("text", stats)}`);
|
|
1094
994
|
}
|
|
1095
995
|
|
|
1096
996
|
return lines;
|
|
@@ -1103,6 +1003,7 @@ function formatModelLabel(modelKey: string, stripProvider: boolean): string {
|
|
|
1103
1003
|
}
|
|
1104
1004
|
|
|
1105
1005
|
function renderModelTable(
|
|
1006
|
+
theme: Theme,
|
|
1106
1007
|
range: RangeAgg,
|
|
1107
1008
|
metric: GraphMetric,
|
|
1108
1009
|
maxRows = 8,
|
|
@@ -1142,11 +1043,11 @@ function renderModelTable(
|
|
|
1142
1043
|
);
|
|
1143
1044
|
}
|
|
1144
1045
|
|
|
1145
|
-
if (sorted.length === 0) lines.push(
|
|
1046
|
+
if (sorted.length === 0) lines.push(theme.fg("muted", "(no model data found)"));
|
|
1146
1047
|
return lines;
|
|
1147
1048
|
}
|
|
1148
1049
|
|
|
1149
|
-
function renderCwdTable(range: RangeAgg, metric: GraphMetric, maxRows = 8): string[] {
|
|
1050
|
+
function renderCwdTable(theme: Theme, range: RangeAgg, metric: GraphMetric, maxRows = 8): string[] {
|
|
1150
1051
|
const { values: perCwd, total } = metricValuesForKind(metric.kind, {
|
|
1151
1052
|
sessions: range.cwdSessions,
|
|
1152
1053
|
messages: range.cwdMessages,
|
|
@@ -1184,7 +1085,7 @@ function renderCwdTable(range: RangeAgg, metric: GraphMetric, maxRows = 8): stri
|
|
|
1184
1085
|
);
|
|
1185
1086
|
}
|
|
1186
1087
|
|
|
1187
|
-
if (sorted.length === 0) lines.push(
|
|
1088
|
+
if (sorted.length === 0) lines.push(theme.fg("muted", "(no directory data found)"));
|
|
1188
1089
|
return lines;
|
|
1189
1090
|
}
|
|
1190
1091
|
|
|
@@ -1539,26 +1440,6 @@ async function computeBreakdown(
|
|
|
1539
1440
|
);
|
|
1540
1441
|
for (const file of files) addParsedFile(file);
|
|
1541
1442
|
|
|
1542
|
-
const allPaletteRange = allRanges.get(30)!;
|
|
1543
|
-
const allPalette: ProviderPalette = {
|
|
1544
|
-
modelPalette: chooseModelPalette(allPaletteRange),
|
|
1545
|
-
cwdPalette: chooseCwdPalette(allPaletteRange),
|
|
1546
|
-
};
|
|
1547
|
-
|
|
1548
|
-
const providerPalettes = new Map<HistoryProviderId, ProviderPalette>();
|
|
1549
|
-
for (const [providerId, ranges] of providerRangesById.entries()) {
|
|
1550
|
-
const range30 = ranges.get(30)!;
|
|
1551
|
-
const provider = getSupportedProvider(providerId);
|
|
1552
|
-
providerPalettes.set(providerId, {
|
|
1553
|
-
modelPalette: chooseModelPalette(
|
|
1554
|
-
range30,
|
|
1555
|
-
3,
|
|
1556
|
-
provider ? buildProviderModelPalette(provider.color) : PALETTE,
|
|
1557
|
-
),
|
|
1558
|
-
cwdPalette: chooseCwdPalette(range30),
|
|
1559
|
-
});
|
|
1560
|
-
}
|
|
1561
|
-
|
|
1562
1443
|
const liveAvailableProviders = getLiveAvailableProviders(availabilityByProvider);
|
|
1563
1444
|
|
|
1564
1445
|
return {
|
|
@@ -1566,14 +1447,11 @@ async function computeBreakdown(
|
|
|
1566
1447
|
liveUsage: buildInitialLiveUsage(SUPPORTED_PROVIDERS, availabilityByProvider),
|
|
1567
1448
|
allRanges,
|
|
1568
1449
|
providerRangesById,
|
|
1569
|
-
allPalette,
|
|
1570
|
-
providerPalettes,
|
|
1571
|
-
dowPalette: buildDowPalette(),
|
|
1572
|
-
todPalette: buildTodPalette(),
|
|
1573
1450
|
};
|
|
1574
1451
|
}
|
|
1575
1452
|
|
|
1576
1453
|
function renderUsageMeterBar(
|
|
1454
|
+
theme: Theme,
|
|
1577
1455
|
providerId: SupportedProviderId,
|
|
1578
1456
|
meter: LiveUsageMeter,
|
|
1579
1457
|
width: number,
|
|
@@ -1581,9 +1459,12 @@ function renderUsageMeterBar(
|
|
|
1581
1459
|
const barWidth = Math.max(10, Math.min(32, width));
|
|
1582
1460
|
const filled = Math.min(barWidth, Math.max(0, Math.round((barWidth * meter.usedPercent) / 100)));
|
|
1583
1461
|
const empty = Math.max(0, barWidth - filled);
|
|
1584
|
-
const
|
|
1585
|
-
const
|
|
1586
|
-
|
|
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);
|
|
1587
1468
|
}
|
|
1588
1469
|
|
|
1589
1470
|
function formatUsageHeader(
|
|
@@ -1600,14 +1481,14 @@ function formatUsageHeader(
|
|
|
1600
1481
|
}
|
|
1601
1482
|
|
|
1602
1483
|
function renderUsageHeader(
|
|
1484
|
+
theme: Theme,
|
|
1603
1485
|
plan: string | undefined,
|
|
1604
1486
|
fetchedAt: Date | undefined,
|
|
1605
|
-
width: number,
|
|
1606
1487
|
): string {
|
|
1607
1488
|
const header = formatUsageHeader(plan, fetchedAt);
|
|
1608
|
-
let text = bold(header.title);
|
|
1609
|
-
if (header.fetched) text += ` ${
|
|
1610
|
-
return
|
|
1489
|
+
let text = theme.bold(header.title);
|
|
1490
|
+
if (header.fetched) text += ` ${theme.fg("muted", header.fetched)}`;
|
|
1491
|
+
return text;
|
|
1611
1492
|
}
|
|
1612
1493
|
|
|
1613
1494
|
type ResolvedLiveUsageState =
|
|
@@ -1641,26 +1522,24 @@ function resolveLiveUsageState(
|
|
|
1641
1522
|
return { kind: "ok", snapshot: liveState.snapshot };
|
|
1642
1523
|
}
|
|
1643
1524
|
|
|
1644
|
-
function formatLiveUsageItemLine(
|
|
1645
|
-
item: LiveUsageItem,
|
|
1646
|
-
options?: { dimReset?: boolean; emphasizeStatValue?: boolean },
|
|
1647
|
-
): string {
|
|
1525
|
+
function formatLiveUsageItemLine(item: LiveUsageItem, theme?: Theme): string {
|
|
1648
1526
|
if (item.kind === "meter") {
|
|
1649
1527
|
const label = item.windowLabel ? `${item.label} (${item.windowLabel})` : item.label;
|
|
1650
1528
|
let text = `${label}: ${formatUsedPercent(item.usedPercent)}`;
|
|
1651
1529
|
if (item.detail) text += ` · ${item.detail}`;
|
|
1652
1530
|
if (item.resetDescription) {
|
|
1653
|
-
const reset =
|
|
1531
|
+
const reset = theme ? theme.fg("muted", item.resetDescription) : item.resetDescription;
|
|
1654
1532
|
text += ` · ${reset}`;
|
|
1655
1533
|
}
|
|
1656
1534
|
return text;
|
|
1657
1535
|
}
|
|
1658
1536
|
|
|
1659
|
-
const value =
|
|
1537
|
+
const value = theme ? theme.bold(item.value) : item.value;
|
|
1660
1538
|
return `${item.label}: ${value}${item.detail ? ` · ${item.detail}` : ""}`;
|
|
1661
1539
|
}
|
|
1662
1540
|
|
|
1663
1541
|
function renderLiveUsageLines(
|
|
1542
|
+
theme: Theme,
|
|
1664
1543
|
tab: BreakdownTab,
|
|
1665
1544
|
liveState: LiveUsageState | undefined,
|
|
1666
1545
|
width: number,
|
|
@@ -1669,33 +1548,26 @@ function renderLiveUsageLines(
|
|
|
1669
1548
|
|
|
1670
1549
|
const resolved = resolveLiveUsageState(tab, liveState);
|
|
1671
1550
|
if (resolved.kind === "loading") {
|
|
1672
|
-
return [
|
|
1551
|
+
return [`${theme.bold("Usage")} ${theme.fg("muted", resolved.message)}`];
|
|
1673
1552
|
}
|
|
1674
1553
|
|
|
1675
1554
|
const lines: string[] = [
|
|
1676
1555
|
renderUsageHeader(
|
|
1556
|
+
theme,
|
|
1677
1557
|
resolved.kind === "ok" ? resolved.snapshot.plan : undefined,
|
|
1678
1558
|
resolved.kind === "ok" ? resolved.snapshot.fetchedAt : undefined,
|
|
1679
|
-
width,
|
|
1680
1559
|
),
|
|
1681
1560
|
];
|
|
1682
1561
|
if (resolved.kind === "message") {
|
|
1683
|
-
|
|
1684
|
-
resolved.tone === "error" ? ansiFg(ERROR_COLOR, resolved.message) : dim(resolved.message);
|
|
1685
|
-
lines.push(truncateToWidth(message, width));
|
|
1562
|
+
lines.push(theme.fg(resolved.tone, resolved.message));
|
|
1686
1563
|
return lines;
|
|
1687
1564
|
}
|
|
1688
1565
|
|
|
1689
1566
|
for (const item of resolved.snapshot.items) {
|
|
1690
1567
|
lines.push("");
|
|
1691
|
-
lines.push(
|
|
1692
|
-
truncateToWidth(
|
|
1693
|
-
formatLiveUsageItemLine(item, { dimReset: true, emphasizeStatValue: true }),
|
|
1694
|
-
width,
|
|
1695
|
-
),
|
|
1696
|
-
);
|
|
1568
|
+
lines.push(formatLiveUsageItemLine(item, theme));
|
|
1697
1569
|
if (item.kind === "meter" && tab.liveProvider) {
|
|
1698
|
-
lines.push(renderUsageMeterBar(tab.liveProvider.id, item, width));
|
|
1570
|
+
lines.push(renderUsageMeterBar(theme, tab.liveProvider.id, item, width));
|
|
1699
1571
|
}
|
|
1700
1572
|
}
|
|
1701
1573
|
|
|
@@ -1752,12 +1624,18 @@ class UsageBreakdownComponent implements Component {
|
|
|
1752
1624
|
private rangeIndex = 1;
|
|
1753
1625
|
private measurement: MeasurementMode = "tokens";
|
|
1754
1626
|
private view: BreakdownView = "model";
|
|
1627
|
+
private renderWidth?: number;
|
|
1755
1628
|
private cachedWidth?: number;
|
|
1756
1629
|
private cachedLines?: string[];
|
|
1630
|
+
private scrollOffset = 0;
|
|
1631
|
+
private viewportHeight = 0;
|
|
1632
|
+
private maxScroll = 0;
|
|
1757
1633
|
|
|
1758
1634
|
constructor(
|
|
1759
1635
|
data: BreakdownData,
|
|
1760
1636
|
tui: TUI,
|
|
1637
|
+
private readonly theme: Theme,
|
|
1638
|
+
private readonly keybindings: KeybindingsManager,
|
|
1761
1639
|
onDone: () => void,
|
|
1762
1640
|
loadLiveUsage: (
|
|
1763
1641
|
provider: UsageProviderDefinition,
|
|
@@ -1771,55 +1649,6 @@ class UsageBreakdownComponent implements Component {
|
|
|
1771
1649
|
this.ensureLiveUsageLoaded();
|
|
1772
1650
|
}
|
|
1773
1651
|
|
|
1774
|
-
private async close(): Promise<void> {
|
|
1775
|
-
if (this.closing) return;
|
|
1776
|
-
this.closing = true;
|
|
1777
|
-
for (const controller of this.liveUsageControllers.values()) {
|
|
1778
|
-
controller.abort();
|
|
1779
|
-
}
|
|
1780
|
-
await Promise.allSettled(this.liveUsageRequests.values());
|
|
1781
|
-
this.onDone();
|
|
1782
|
-
}
|
|
1783
|
-
|
|
1784
|
-
private ensureLiveUsageLoaded(): void {
|
|
1785
|
-
const tab = this.data.tabs[this.tabIndex];
|
|
1786
|
-
const provider = tab?.liveProvider;
|
|
1787
|
-
if (!provider) return;
|
|
1788
|
-
|
|
1789
|
-
const current = this.data.liveUsage.get(provider.id);
|
|
1790
|
-
if (current && current.status !== "loading") return;
|
|
1791
|
-
if (this.liveUsageRequests.has(provider.id)) return;
|
|
1792
|
-
|
|
1793
|
-
const controller = new AbortController();
|
|
1794
|
-
this.liveUsageControllers.set(provider.id, controller);
|
|
1795
|
-
this.data.liveUsage.set(provider.id, { status: "loading" });
|
|
1796
|
-
this.invalidate();
|
|
1797
|
-
this.tui.requestRender();
|
|
1798
|
-
|
|
1799
|
-
const request = this.loadLiveUsage(provider, controller.signal)
|
|
1800
|
-
.then((state) => {
|
|
1801
|
-
if (controller.signal.aborted) return;
|
|
1802
|
-
this.data.liveUsage.set(provider.id, state);
|
|
1803
|
-
})
|
|
1804
|
-
.catch((error) => {
|
|
1805
|
-
if (controller.signal.aborted) return;
|
|
1806
|
-
this.data.liveUsage.set(provider.id, {
|
|
1807
|
-
status: "error",
|
|
1808
|
-
error: error instanceof Error ? error.message : String(error),
|
|
1809
|
-
});
|
|
1810
|
-
})
|
|
1811
|
-
.finally(() => {
|
|
1812
|
-
this.liveUsageRequests.delete(provider.id);
|
|
1813
|
-
this.liveUsageControllers.delete(provider.id);
|
|
1814
|
-
if (!controller.signal.aborted) {
|
|
1815
|
-
this.invalidate();
|
|
1816
|
-
this.tui.requestRender();
|
|
1817
|
-
}
|
|
1818
|
-
});
|
|
1819
|
-
|
|
1820
|
-
this.liveUsageRequests.set(provider.id, request);
|
|
1821
|
-
}
|
|
1822
|
-
|
|
1823
1652
|
invalidate(): void {
|
|
1824
1653
|
this.cachedWidth = undefined;
|
|
1825
1654
|
this.cachedLines = undefined;
|
|
@@ -1836,6 +1665,29 @@ class UsageBreakdownComponent implements Component {
|
|
|
1836
1665
|
return;
|
|
1837
1666
|
}
|
|
1838
1667
|
|
|
1668
|
+
const width = Math.min(
|
|
1669
|
+
this.renderWidth ?? this.tui.terminal.columns - 2,
|
|
1670
|
+
this.tui.terminal.columns - 2,
|
|
1671
|
+
);
|
|
1672
|
+
if (width < 24 || this.tui.terminal.rows < 7) return;
|
|
1673
|
+
|
|
1674
|
+
const scrollActions = [
|
|
1675
|
+
["tui.altScreen.pageUp", -this.viewportHeight],
|
|
1676
|
+
["tui.altScreen.pageDown", this.viewportHeight],
|
|
1677
|
+
["tui.altScreen.halfPageUp", -Math.max(1, Math.floor(this.viewportHeight / 2))],
|
|
1678
|
+
["tui.altScreen.halfPageDown", Math.max(1, Math.floor(this.viewportHeight / 2))],
|
|
1679
|
+
["tui.altScreen.lineUp", -1],
|
|
1680
|
+
["tui.altScreen.lineDown", 1],
|
|
1681
|
+
["tui.altScreen.top", -Infinity],
|
|
1682
|
+
["tui.altScreen.bottom", Infinity],
|
|
1683
|
+
] as const;
|
|
1684
|
+
for (const [action, delta] of scrollActions) {
|
|
1685
|
+
if (!this.keybindings.matches(data, action)) continue;
|
|
1686
|
+
this.scrollOffset = Math.max(0, Math.min(this.maxScroll, this.scrollOffset + delta));
|
|
1687
|
+
this.tui.requestRender();
|
|
1688
|
+
return;
|
|
1689
|
+
}
|
|
1690
|
+
|
|
1839
1691
|
if (
|
|
1840
1692
|
matchesKey(data, Key.tab) ||
|
|
1841
1693
|
matchesKey(data, Key.shift("tab")) ||
|
|
@@ -1845,6 +1697,7 @@ class UsageBreakdownComponent implements Component {
|
|
|
1845
1697
|
const index = Math.max(0, order.indexOf(this.measurement));
|
|
1846
1698
|
const direction = matchesKey(data, Key.shift("tab")) ? -1 : 1;
|
|
1847
1699
|
this.measurement = order[(index + order.length + direction) % order.length] ?? "tokens";
|
|
1700
|
+
this.scrollOffset = 0;
|
|
1848
1701
|
this.invalidate();
|
|
1849
1702
|
this.tui.requestRender();
|
|
1850
1703
|
return;
|
|
@@ -1858,6 +1711,7 @@ class UsageBreakdownComponent implements Component {
|
|
|
1858
1711
|
) {
|
|
1859
1712
|
const direction = matchesKey(data, Key.left) || data.toLowerCase() === "h" ? -1 : 1;
|
|
1860
1713
|
this.rangeIndex = (this.rangeIndex + RANGE_DAYS.length + direction) % RANGE_DAYS.length;
|
|
1714
|
+
this.scrollOffset = 0;
|
|
1861
1715
|
this.invalidate();
|
|
1862
1716
|
this.tui.requestRender();
|
|
1863
1717
|
return;
|
|
@@ -1866,6 +1720,7 @@ class UsageBreakdownComponent implements Component {
|
|
|
1866
1720
|
if (data === "[" || data === "]" || data.toLowerCase() === "p" || data.toLowerCase() === "n") {
|
|
1867
1721
|
const direction = data === "[" || data.toLowerCase() === "p" ? -1 : 1;
|
|
1868
1722
|
this.tabIndex = (this.tabIndex + this.data.tabs.length + direction) % this.data.tabs.length;
|
|
1723
|
+
this.scrollOffset = 0;
|
|
1869
1724
|
this.ensureLiveUsageLoaded();
|
|
1870
1725
|
this.invalidate();
|
|
1871
1726
|
this.tui.requestRender();
|
|
@@ -1882,6 +1737,7 @@ class UsageBreakdownComponent implements Component {
|
|
|
1882
1737
|
const index = views.indexOf(this.view);
|
|
1883
1738
|
const direction = matchesKey(data, Key.up) || data.toLowerCase() === "k" ? -1 : 1;
|
|
1884
1739
|
this.view = views[(index + views.length + direction) % views.length] ?? "model";
|
|
1740
|
+
this.scrollOffset = 0;
|
|
1885
1741
|
this.invalidate();
|
|
1886
1742
|
this.tui.requestRender();
|
|
1887
1743
|
return;
|
|
@@ -1890,12 +1746,106 @@ class UsageBreakdownComponent implements Component {
|
|
|
1890
1746
|
const rangeByKey: Record<string, number> = { "1": 0, "2": 1, "3": 2 };
|
|
1891
1747
|
if (Object.hasOwn(rangeByKey, data)) {
|
|
1892
1748
|
this.rangeIndex = rangeByKey[data]!;
|
|
1749
|
+
this.scrollOffset = 0;
|
|
1893
1750
|
this.invalidate();
|
|
1894
1751
|
this.tui.requestRender();
|
|
1895
1752
|
}
|
|
1896
1753
|
}
|
|
1897
1754
|
|
|
1898
1755
|
render(width: number): string[] {
|
|
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
|
+
};
|
|
1764
|
+
const height = Math.max(0, this.tui.terminal.rows - 2);
|
|
1765
|
+
// Four chrome rows plus at least one document row. Margins are supplied by the overlay.
|
|
1766
|
+
if (height < 5 || width < 24) {
|
|
1767
|
+
this.viewportHeight = 0;
|
|
1768
|
+
return ["q close · resize", "Usage needs 26×7"]
|
|
1769
|
+
.slice(0, height)
|
|
1770
|
+
.map((line) => styleLine(truncateUsageLine(this.theme, line, width)));
|
|
1771
|
+
}
|
|
1772
|
+
const document = this.renderDocument(width);
|
|
1773
|
+
const body = document.slice(2);
|
|
1774
|
+
this.viewportHeight = height - 4;
|
|
1775
|
+
this.maxScroll = Math.max(0, body.length - this.viewportHeight);
|
|
1776
|
+
this.scrollOffset = Math.min(this.scrollOffset, this.maxScroll);
|
|
1777
|
+
const visible = body.slice(this.scrollOffset, this.scrollOffset + this.viewportHeight);
|
|
1778
|
+
const keys = (action: "pageUp" | "pageDown") =>
|
|
1779
|
+
this.keybindings.getKeys(`tui.altScreen.${action}`).join("/") || "unbound";
|
|
1780
|
+
return [
|
|
1781
|
+
...document.slice(0, 2),
|
|
1782
|
+
...visible,
|
|
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",
|
|
1792
|
+
`q close · ${this.scrollOffset + 1}–${this.scrollOffset + visible.length}/${body.length}`,
|
|
1793
|
+
),
|
|
1794
|
+
width,
|
|
1795
|
+
),
|
|
1796
|
+
].map(styleLine);
|
|
1797
|
+
}
|
|
1798
|
+
|
|
1799
|
+
private async close(): Promise<void> {
|
|
1800
|
+
if (this.closing) return;
|
|
1801
|
+
this.closing = true;
|
|
1802
|
+
for (const controller of this.liveUsageControllers.values()) {
|
|
1803
|
+
controller.abort();
|
|
1804
|
+
}
|
|
1805
|
+
await Promise.allSettled(this.liveUsageRequests.values());
|
|
1806
|
+
this.onDone();
|
|
1807
|
+
}
|
|
1808
|
+
|
|
1809
|
+
private ensureLiveUsageLoaded(): void {
|
|
1810
|
+
const tab = this.data.tabs[this.tabIndex];
|
|
1811
|
+
const provider = tab?.liveProvider;
|
|
1812
|
+
if (!provider) return;
|
|
1813
|
+
|
|
1814
|
+
const current = this.data.liveUsage.get(provider.id);
|
|
1815
|
+
if (current && current.status !== "loading") return;
|
|
1816
|
+
if (this.liveUsageRequests.has(provider.id)) return;
|
|
1817
|
+
|
|
1818
|
+
const controller = new AbortController();
|
|
1819
|
+
this.liveUsageControllers.set(provider.id, controller);
|
|
1820
|
+
this.data.liveUsage.set(provider.id, { status: "loading" });
|
|
1821
|
+
this.invalidate();
|
|
1822
|
+
this.tui.requestRender();
|
|
1823
|
+
|
|
1824
|
+
const request = this.loadLiveUsage(provider, controller.signal)
|
|
1825
|
+
.then((state) => {
|
|
1826
|
+
if (controller.signal.aborted) return;
|
|
1827
|
+
this.data.liveUsage.set(provider.id, state);
|
|
1828
|
+
})
|
|
1829
|
+
.catch((error) => {
|
|
1830
|
+
if (controller.signal.aborted) return;
|
|
1831
|
+
this.data.liveUsage.set(provider.id, {
|
|
1832
|
+
status: "error",
|
|
1833
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1834
|
+
});
|
|
1835
|
+
})
|
|
1836
|
+
.finally(() => {
|
|
1837
|
+
this.liveUsageRequests.delete(provider.id);
|
|
1838
|
+
this.liveUsageControllers.delete(provider.id);
|
|
1839
|
+
if (!controller.signal.aborted) {
|
|
1840
|
+
this.invalidate();
|
|
1841
|
+
this.tui.requestRender();
|
|
1842
|
+
}
|
|
1843
|
+
});
|
|
1844
|
+
|
|
1845
|
+
this.liveUsageRequests.set(provider.id, request);
|
|
1846
|
+
}
|
|
1847
|
+
|
|
1848
|
+
private renderDocument(width: number): string[] {
|
|
1899
1849
|
if (this.cachedWidth === width && this.cachedLines) return this.cachedLines;
|
|
1900
1850
|
|
|
1901
1851
|
const tab = this.data.tabs[this.tabIndex]!;
|
|
@@ -1905,64 +1855,84 @@ class UsageBreakdownComponent implements Component {
|
|
|
1905
1855
|
? this.data.allRanges.get(selectedDays)!
|
|
1906
1856
|
: (this.data.providerRangesById.get(tab.id)?.get(selectedDays) ??
|
|
1907
1857
|
getEmptyRange(selectedDays));
|
|
1908
|
-
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 =
|
|
1909
1869
|
tab.mode === "all"
|
|
1910
|
-
? this.data.
|
|
1911
|
-
: (this.data.
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
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);
|
|
1923
1883
|
const liveState = tab.liveProvider ? this.data.liveUsage.get(tab.liveProvider.id) : undefined;
|
|
1924
1884
|
const metric = graphMetricForRange(range, this.measurement);
|
|
1925
1885
|
|
|
1926
|
-
const providerBadge = bold(`[${tab.shortLabel}]`);
|
|
1886
|
+
const providerBadge = theme.bold(`[${tab.shortLabel}]`);
|
|
1927
1887
|
const rangeTab = (days: number, index: number): string =>
|
|
1928
|
-
index === this.rangeIndex
|
|
1888
|
+
index === this.rangeIndex
|
|
1889
|
+
? theme.style(`[${days}d]`, { fg: "text", bold: true })
|
|
1890
|
+
: theme.fg("muted", ` ${days}d `);
|
|
1929
1891
|
const metricTab = (mode: MeasurementMode, label: string): string =>
|
|
1930
|
-
mode === this.measurement
|
|
1892
|
+
mode === this.measurement
|
|
1893
|
+
? theme.style(`[${label}]`, { fg: "text", bold: true })
|
|
1894
|
+
: theme.fg("muted", ` ${label} `);
|
|
1931
1895
|
const viewTab = (candidate: BreakdownView, label: string): string =>
|
|
1932
|
-
candidate === this.view
|
|
1896
|
+
candidate === this.view
|
|
1897
|
+
? theme.style(`[${label}]`, { fg: "text", bold: true })
|
|
1898
|
+
: theme.fg("muted", ` ${label} `);
|
|
1933
1899
|
|
|
1934
1900
|
const header =
|
|
1935
|
-
`${bold("Usage")} ${providerBadge}${
|
|
1901
|
+
`${theme.bold("Usage")} ${providerBadge}${theme.fg("muted", ` ${this.tabIndex + 1}/${this.data.tabs.length}`)} ` +
|
|
1936
1902
|
`${rangeTab(7, 0)}${rangeTab(30, 1)}${rangeTab(90, 2)} ` +
|
|
1937
1903
|
`${metricTab("sessions", "sess")}${metricTab("messages", "msg")}${metricTab("tokens", "tok")} ` +
|
|
1938
1904
|
`${viewTab("model", "model")}${viewTab("cwd", "cwd")}${viewTab("dow", "dow")}${viewTab("tod", "tod")}`;
|
|
1939
1905
|
|
|
1940
1906
|
const lines: string[] = [];
|
|
1941
|
-
|
|
1907
|
+
const compactHeader = `${theme.bold("Usage")} ${providerBadge} [${selectedDays}d] [${this.measurement === "sessions" ? "sess" : this.measurement === "messages" ? "msg" : "tok"}] [${this.view}]`;
|
|
1942
1908
|
lines.push(
|
|
1943
|
-
|
|
1909
|
+
truncateUsageLine(theme, visibleWidth(header) <= width ? header : compactHeader, width),
|
|
1944
1910
|
);
|
|
1945
|
-
|
|
1911
|
+
const navigation = theme.fg(
|
|
1912
|
+
"muted",
|
|
1913
|
+
"[/] provider · ←/→ range · ↑/↓ view · tab metric · q to close",
|
|
1914
|
+
);
|
|
1915
|
+
lines.push(truncateUsageLine(theme, navigation, width));
|
|
1916
|
+
if (visibleWidth(compactHeader) > width) lines.push(compactHeader);
|
|
1917
|
+
if (visibleWidth(navigation) > width) lines.push(navigation);
|
|
1918
|
+
lines.push(`${theme.bold(tab.label)} ${theme.fg("muted", historySemanticsText(tab))}`);
|
|
1946
1919
|
lines.push("");
|
|
1947
1920
|
|
|
1948
|
-
const liveLines = renderLiveUsageLines(tab, liveState, width);
|
|
1921
|
+
const liveLines = renderLiveUsageLines(theme, tab, liveState, width);
|
|
1949
1922
|
if (liveLines.length > 0) {
|
|
1950
|
-
|
|
1923
|
+
lines.push(...liveLines);
|
|
1951
1924
|
lines.push("");
|
|
1952
1925
|
}
|
|
1953
1926
|
|
|
1954
1927
|
const graphDescriptor =
|
|
1955
1928
|
this.view === "dow" ? `share of ${metric.kind} by weekday` : `${metric.kind}/day`;
|
|
1956
1929
|
lines.push(
|
|
1957
|
-
|
|
1958
|
-
`${rangeSummary(range, selectedDays, metric.kind)}${dim(` (graph: ${graphDescriptor})`)}`,
|
|
1959
|
-
width,
|
|
1960
|
-
),
|
|
1930
|
+
`${rangeSummary(range, selectedDays, metric.kind)}${theme.fg("muted", ` (graph: ${graphDescriptor})`)}`,
|
|
1961
1931
|
);
|
|
1962
1932
|
lines.push("");
|
|
1963
1933
|
|
|
1964
|
-
let activeColorMap: Map<string,
|
|
1965
|
-
let activeOtherColor =
|
|
1934
|
+
let activeColorMap: Map<string, Color>;
|
|
1935
|
+
let activeOtherColor = otherColor;
|
|
1966
1936
|
const legendItems: string[] = [];
|
|
1967
1937
|
const stripProvider = tab.mode === "provider";
|
|
1968
1938
|
|
|
@@ -1972,39 +1942,46 @@ class UsageBreakdownComponent implements Component {
|
|
|
1972
1942
|
for (const model of palette.modelPalette.orderedModels) {
|
|
1973
1943
|
const color = activeColorMap.get(model);
|
|
1974
1944
|
if (color)
|
|
1975
|
-
legendItems.push(
|
|
1945
|
+
legendItems.push(
|
|
1946
|
+
`${theme.style("█", { fg: color })} ${theme.fg("text", formatModelLabel(model, stripProvider))}`,
|
|
1947
|
+
);
|
|
1976
1948
|
}
|
|
1977
|
-
legendItems.push(
|
|
1949
|
+
legendItems.push(
|
|
1950
|
+
`${theme.style("█", { fg: activeOtherColor })} ${theme.fg("text", "other")}`,
|
|
1951
|
+
);
|
|
1978
1952
|
} else if (this.view === "cwd") {
|
|
1979
1953
|
activeColorMap = palette.cwdPalette.cwdColors;
|
|
1980
1954
|
activeOtherColor = palette.cwdPalette.otherColor;
|
|
1981
1955
|
for (const cwd of palette.cwdPalette.orderedCwds) {
|
|
1982
1956
|
const color = activeColorMap.get(cwd);
|
|
1983
|
-
if (color)
|
|
1957
|
+
if (color)
|
|
1958
|
+
legendItems.push(
|
|
1959
|
+
`${theme.style("█", { fg: color })} ${theme.fg("text", abbreviatePath(cwd, 30))}`,
|
|
1960
|
+
);
|
|
1984
1961
|
}
|
|
1985
|
-
legendItems.push(
|
|
1962
|
+
legendItems.push(
|
|
1963
|
+
`${theme.style("█", { fg: activeOtherColor })} ${theme.fg("text", "other")}`,
|
|
1964
|
+
);
|
|
1986
1965
|
} else if (this.view === "dow") {
|
|
1987
|
-
activeColorMap =
|
|
1988
|
-
for (const dow of
|
|
1966
|
+
activeColorMap = dowColors;
|
|
1967
|
+
for (const dow of DOW_NAMES) {
|
|
1989
1968
|
const color = activeColorMap.get(dow);
|
|
1990
|
-
if (color) legendItems.push(`${
|
|
1969
|
+
if (color) legendItems.push(`${theme.style("█", { fg: color })} ${theme.fg("text", dow)}`);
|
|
1991
1970
|
}
|
|
1992
1971
|
} else {
|
|
1993
|
-
activeColorMap =
|
|
1994
|
-
for (const tod of
|
|
1972
|
+
activeColorMap = todColors;
|
|
1973
|
+
for (const { key: tod } of TOD_BUCKETS) {
|
|
1995
1974
|
const color = activeColorMap.get(tod);
|
|
1996
|
-
if (color)
|
|
1975
|
+
if (color)
|
|
1976
|
+
legendItems.push(
|
|
1977
|
+
`${theme.style("█", { fg: color })} ${theme.fg("text", todBucketLabel(tod))}`,
|
|
1978
|
+
);
|
|
1997
1979
|
}
|
|
1998
1980
|
}
|
|
1999
1981
|
|
|
2000
1982
|
let graphLines: string[];
|
|
2001
1983
|
if (this.view === "dow") {
|
|
2002
|
-
graphLines = renderDowDistributionLines(
|
|
2003
|
-
range,
|
|
2004
|
-
this.measurement,
|
|
2005
|
-
this.data.dowPalette.dowColors,
|
|
2006
|
-
width,
|
|
2007
|
-
);
|
|
1984
|
+
graphLines = renderDowDistributionLines(theme, range, this.measurement, dowColors, width);
|
|
2008
1985
|
} else {
|
|
2009
1986
|
const maxScale = selectedDays === 7 ? 4 : selectedDays === 30 ? 3 : 2;
|
|
2010
1987
|
const weeks = weeksForRange(range);
|
|
@@ -2014,6 +1991,7 @@ class UsageBreakdownComponent implements Component {
|
|
|
2014
1991
|
const idealCellWidth = Math.floor((graphArea + gap) / Math.max(1, weeks)) - gap;
|
|
2015
1992
|
const cellWidth = Math.min(maxScale, Math.max(1, idealCellWidth));
|
|
2016
1993
|
graphLines = renderGraphLines(
|
|
1994
|
+
theme,
|
|
2017
1995
|
range,
|
|
2018
1996
|
activeColorMap,
|
|
2019
1997
|
activeOtherColor,
|
|
@@ -2026,15 +2004,15 @@ class UsageBreakdownComponent implements Component {
|
|
|
2026
2004
|
|
|
2027
2005
|
const tableLines =
|
|
2028
2006
|
this.view === "model"
|
|
2029
|
-
? renderModelTable(range, metric, 8, { stripProvider })
|
|
2007
|
+
? renderModelTable(theme, range, metric, 8, { stripProvider })
|
|
2030
2008
|
: this.view === "cwd"
|
|
2031
|
-
? renderCwdTable(range, metric, 8)
|
|
2009
|
+
? renderCwdTable(theme, range, metric, 8)
|
|
2032
2010
|
: this.view === "dow"
|
|
2033
2011
|
? renderDowTable(range, metric)
|
|
2034
2012
|
: renderTodTable(range, metric);
|
|
2035
2013
|
|
|
2036
2014
|
if (this.view === "dow") {
|
|
2037
|
-
for (const line of graphLines) lines.push(
|
|
2015
|
+
for (const line of graphLines) lines.push(truncateUsageLine(theme, line, width));
|
|
2038
2016
|
} else {
|
|
2039
2017
|
const graphWidth = Math.max(0, ...graphLines.map((line) => visibleWidth(line)));
|
|
2040
2018
|
const separator = 2;
|
|
@@ -2048,12 +2026,15 @@ class UsageBreakdownComponent implements Component {
|
|
|
2048
2026
|
: this.view === "cwd"
|
|
2049
2027
|
? "Top directories (30d palette):"
|
|
2050
2028
|
: "Time of day:";
|
|
2051
|
-
const legendBlock = [
|
|
2029
|
+
const legendBlock = [theme.fg("muted", legendTitle), ...legendItems];
|
|
2052
2030
|
const maxLegendRows = graphLines.length;
|
|
2053
2031
|
let legendLines = legendBlock.slice(0, maxLegendRows);
|
|
2054
2032
|
if (legendBlock.length > maxLegendRows) {
|
|
2055
2033
|
const remaining = legendBlock.length - (maxLegendRows - 1);
|
|
2056
|
-
legendLines = [
|
|
2034
|
+
legendLines = [
|
|
2035
|
+
...legendBlock.slice(0, maxLegendRows - 1),
|
|
2036
|
+
theme.fg("muted", `+${remaining} more`),
|
|
2037
|
+
];
|
|
2057
2038
|
}
|
|
2058
2039
|
while (legendLines.length < graphLines.length) legendLines.push("");
|
|
2059
2040
|
|
|
@@ -2066,11 +2047,15 @@ class UsageBreakdownComponent implements Component {
|
|
|
2066
2047
|
|
|
2067
2048
|
for (let index = 0; index < graphLines.length; index += 1) {
|
|
2068
2049
|
const left = padRightAnsi(graphLines[index] ?? "", graphWidth);
|
|
2069
|
-
const right =
|
|
2070
|
-
|
|
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));
|
|
2071
2056
|
}
|
|
2072
2057
|
} else {
|
|
2073
|
-
for (const line of graphLines) lines.push(
|
|
2058
|
+
for (const line of graphLines) lines.push(truncateUsageLine(theme, line, width));
|
|
2074
2059
|
lines.push("");
|
|
2075
2060
|
const legendTitle =
|
|
2076
2061
|
this.view === "model"
|
|
@@ -2078,16 +2063,19 @@ class UsageBreakdownComponent implements Component {
|
|
|
2078
2063
|
: this.view === "cwd"
|
|
2079
2064
|
? "Top directories (30d palette):"
|
|
2080
2065
|
: "Time of day:";
|
|
2081
|
-
lines.push(
|
|
2082
|
-
|
|
2066
|
+
lines.push(theme.fg("muted", legendTitle));
|
|
2067
|
+
lines.push(...legendItems);
|
|
2083
2068
|
}
|
|
2084
2069
|
}
|
|
2085
2070
|
|
|
2086
2071
|
lines.push("");
|
|
2087
|
-
|
|
2072
|
+
lines.push(...tableLines);
|
|
2088
2073
|
|
|
2089
2074
|
this.cachedWidth = width;
|
|
2090
|
-
this.cachedLines =
|
|
2075
|
+
this.cachedLines = [
|
|
2076
|
+
...lines.slice(0, 2),
|
|
2077
|
+
...lines.slice(2).flatMap((line) => wrapTextWithAnsi(line, width)),
|
|
2078
|
+
];
|
|
2091
2079
|
return this.cachedLines;
|
|
2092
2080
|
}
|
|
2093
2081
|
}
|
|
@@ -2197,10 +2185,14 @@ export default function usageBreakdownExtension(pi: ExtensionAPI) {
|
|
|
2197
2185
|
}
|
|
2198
2186
|
|
|
2199
2187
|
await ctx.ui.custom<void>(
|
|
2200
|
-
(tui,
|
|
2201
|
-
new UsageBreakdownComponent(data, tui, done, (provider, signal) =>
|
|
2188
|
+
(tui, theme, keybindings, done) =>
|
|
2189
|
+
new UsageBreakdownComponent(data, tui, theme, keybindings, done, (provider, signal) =>
|
|
2202
2190
|
fetchLiveUsageForProvider(provider, liveUsageAvailability, ctx, signal),
|
|
2203
2191
|
),
|
|
2192
|
+
{
|
|
2193
|
+
overlay: true,
|
|
2194
|
+
overlayOptions: { anchor: "bottom-center", width: "100%", maxHeight: "100%", margin: 1 },
|
|
2195
|
+
},
|
|
2204
2196
|
);
|
|
2205
2197
|
},
|
|
2206
2198
|
});
|