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.
@@ -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
- { overlay: true, overlayOptions: { width: "100%", maxHeight: "100%", margin: 1 } },
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
- { overlay: true, overlayOptions: { width: "100%", maxHeight: "100%", margin: 1 } },
359
+ {
360
+ overlay: true,
361
+ overlayOptions: { anchor: "bottom-center", width: "100%", maxHeight: "100%", margin: 1 },
362
+ },
360
363
  );
361
364
  }
362
365
 
@@ -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
- { overlay: true, overlayOptions: { width: "100%", maxHeight: "100%", margin: 1 } },
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
- const changed = mutateStringList(values, op, targetPath);
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("~/") ? join(homedir(), value.slice(2)) : value;
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 expandPathConfigEntry(value: string, field: string): string | null {
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(values: string[] | undefined, field: string): string[] | undefined {
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) => expandPathConfigEntry(value, field))
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 = expandPathConfigEntry(mitmProxy.socketPath, "network.mitmProxy.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(network.allowUnixSockets, "network.allowUnixSockets"),
640
- mitmProxy: expandMitmProxyPathConfig(network.mitmProxy),
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, RGB>;
170
- otherColor: RGB;
144
+ modelColors: Map<ModelKey, Color>;
145
+ otherColor: Color;
171
146
  orderedModels: ModelKey[];
172
147
  };
173
148
  cwdPalette: {
174
- cwdColors: Map<CwdKey, RGB>;
175
- otherColor: RGB;
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 lerp(a: number, b: number, t: number): number {
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 r = 0;
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
- r += entry.color.r * entry.weight;
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
- if (total <= 0) return EMPTY_CELL_BG;
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 dim(text: string): string {
265
- return `\x1b[2m${text}\x1b[0m`;
208
+ function emptyChartColor(theme: Theme): Color {
209
+ return mixColors(theme.colors.toolPendingBg, theme.colors.muted, 0.25);
266
210
  }
267
211
 
268
- function bold(text: string): string {
269
- return `\x1b[1m${text}\x1b[0m`;
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 normalizeHue(value: number): number {
727
- const normalized = value % 360;
728
- return normalized < 0 ? normalized + 360 : normalized;
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 adjustColor(
785
- color: RGB,
786
- adjustments: { hueShift?: number; saturationShift?: number; lightnessShift?: number },
787
- ): RGB {
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
- color,
799
- adjustColor(color, { hueShift: 16, saturationShift: 0.04, lightnessShift: 0.08 }),
800
- adjustColor(color, { hueShift: -12, saturationShift: 0.02, lightnessShift: -0.06 }),
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, RGB>();
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] ?? PALETTE[index % PALETTE.length] ?? MUTED_COLOR;
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: MUTED_COLOR,
716
+ otherColor,
832
717
  orderedModels,
833
718
  };
834
719
  }
835
720
 
836
- function chooseCwdPalette(range30: RangeAgg, topN = 4): ProviderPalette["cwdPalette"] {
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, RGB>();
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, PALETTE[index % PALETTE.length]!);
742
+ if (cwd) cwdColors.set(cwd, paletteColors[index % paletteColors.length] ?? otherColor);
853
743
  }
854
744
 
855
745
  return {
856
746
  cwdColors,
857
- otherColor: MUTED_COLOR,
747
+ otherColor,
858
748
  orderedCwds,
859
749
  };
860
750
  }
861
751
 
862
- function buildDowPalette(): BreakdownData["dowPalette"] {
863
- const dowColors = new Map<DowKey, RGB>();
864
- for (let index = 0; index < DOW_NAMES.length; index += 1) {
865
- const dow = DOW_NAMES[index];
866
- const color = DOW_PALETTE[index];
867
- if (dow && color) dowColors.set(dow, color);
868
- }
869
- return { dowColors, orderedDows: [...DOW_NAMES] };
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(): BreakdownData["todPalette"] {
873
- const todColors = new Map<TodKey, RGB>();
874
- const orderedTods: TodKey[] = [];
875
- for (const bucket of TOD_BUCKETS) {
876
- const color = TOD_PALETTE.get(bucket.key);
877
- if (color) todColors.set(bucket.key, color);
878
- orderedTods.push(bucket.key);
879
- }
880
- return { todColors, orderedTods };
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, RGB>,
886
- otherColor: RGB,
779
+ colorMap: Map<string, Color>,
780
+ otherColor: Color,
887
781
  mode: MeasurementMode,
888
782
  view: BreakdownView,
889
- ): RGB {
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: RGB; weight: number }> = [];
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, RGB>,
986
- otherColor: RGB,
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 += ansiFg(EMPTY_CELL_BG, block) + columnGap;
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 rgb = mixRgb(DEFAULT_BG, hue, intensity);
1047
- line += ansiFg(rgb, block) + columnGap;
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, RGB>,
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) ?? MUTED_COLOR;
1089
- const filledBar = filled > 0 ? ansiFg(color, "█".repeat(filled)) : "";
1090
- const emptyBar = empty > 0 ? ansiFg(EMPTY_CELL_BG, "█".repeat(empty)) : "";
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
- let line = `${padRight(dow, dayWidth)} ${filledBar}${emptyBar} ${pct}`;
1094
- if (showValue) line += ` ${padLeft(formatCount(value), valueWidth)}`;
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(dim("(no model data found)"));
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(dim("(no directory data found)"));
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 color = getSupportedProvider(providerId)?.color ?? MUTED_COLOR;
1587
- const bar = `${ansiFg(color, "█".repeat(filled))}${ansiFg(EMPTY_CELL_BG, "█".repeat(empty))}`;
1588
- return truncateToWidth(bar, width);
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(plan: string | undefined, fetchedAt: Date | undefined): string {
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 += ` ${dim(header.fetched)}`;
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 = options?.dimReset ? dim(item.resetDescription) : item.resetDescription;
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 = options?.emphasizeStatValue ? bold(item.value) : item.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")} ${dim(resolved.message)}`];
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
- const message =
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, { dimReset: true, emphasizeStatValue: true }));
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) => truncateToWidth(line, width));
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
- truncateToWidth(dim(`${keys("pageUp")}/${keys("pageDown")} scroll`), width),
1896
- truncateToWidth(
1897
- dim(
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 palette =
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.allPalette
1967
- : (this.data.providerPalettes.get(tab.id) ?? {
1968
- modelPalette: {
1969
- modelColors: new Map<ModelKey, RGB>(),
1970
- otherColor: MUTED_COLOR,
1971
- orderedModels: [],
1972
- },
1973
- cwdPalette: {
1974
- cwdColors: new Map<CwdKey, RGB>(),
1975
- otherColor: MUTED_COLOR,
1976
- orderedCwds: [],
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 ? bold(`[${days}d]`) : dim(` ${days}d `);
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 ? bold(`[${label}]`) : dim(` ${label} `);
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 ? bold(`[${label}]`) : dim(` ${label} `);
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}${dim(` ${this.tabIndex + 1}/${this.data.tabs.length}`)} ` +
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(truncateToWidth(visibleWidth(header) <= width ? header : compactHeader, width));
1999
- const navigation = dim("[/] provider · ←/→ range · ↑/↓ view · tab metric · q to close");
2000
- lines.push(truncateToWidth(navigation, width));
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)} ${dim(historySemanticsText(tab))}`);
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)}${dim(` (graph: ${graphDescriptor})`)}`,
1930
+ `${rangeSummary(range, selectedDays, metric.kind)}${theme.fg("muted", ` (graph: ${graphDescriptor})`)}`,
2016
1931
  );
2017
1932
  lines.push("");
2018
1933
 
2019
- let activeColorMap: Map<string, RGB>;
2020
- let activeOtherColor = MUTED_COLOR;
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(`${ansiFg(color, "█")} ${formatModelLabel(model, stripProvider)}`);
1945
+ legendItems.push(
1946
+ `${theme.style("█", { fg: color })} ${theme.fg("text", formatModelLabel(model, stripProvider))}`,
1947
+ );
2031
1948
  }
2032
- legendItems.push(`${ansiFg(activeOtherColor, "█")} other`);
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) legendItems.push(`${ansiFg(color, "█")} ${abbreviatePath(cwd, 30)}`);
1957
+ if (color)
1958
+ legendItems.push(
1959
+ `${theme.style("█", { fg: color })} ${theme.fg("text", abbreviatePath(cwd, 30))}`,
1960
+ );
2039
1961
  }
2040
- legendItems.push(`${ansiFg(activeOtherColor, "█")} other`);
1962
+ legendItems.push(
1963
+ `${theme.style("█", { fg: activeOtherColor })} ${theme.fg("text", "other")}`,
1964
+ );
2041
1965
  } else if (this.view === "dow") {
2042
- activeColorMap = this.data.dowPalette.dowColors;
2043
- for (const dow of this.data.dowPalette.orderedDows) {
1966
+ activeColorMap = dowColors;
1967
+ for (const dow of DOW_NAMES) {
2044
1968
  const color = activeColorMap.get(dow);
2045
- if (color) legendItems.push(`${ansiFg(color, "█")} ${dow}`);
1969
+ if (color) legendItems.push(`${theme.style("█", { fg: color })} ${theme.fg("text", dow)}`);
2046
1970
  }
2047
1971
  } else {
2048
- activeColorMap = this.data.todPalette.todColors;
2049
- for (const tod of this.data.todPalette.orderedTods) {
1972
+ activeColorMap = todColors;
1973
+ for (const { key: tod } of TOD_BUCKETS) {
2050
1974
  const color = activeColorMap.get(tod);
2051
- if (color) legendItems.push(`${ansiFg(color, "█")} ${todBucketLabel(tod)}`);
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(truncateToWidth(line, width));
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 = [dim(legendTitle), ...legendItems];
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 = [...legendBlock.slice(0, maxLegendRows - 1), dim(`+${remaining} more`)];
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 = truncateToWidth(legendLines[index] ?? "", Math.max(0, legendWidth));
2125
- lines.push(truncateToWidth(left + " ".repeat(separator) + right, width));
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(truncateToWidth(line, width));
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(dim(legendTitle));
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, _theme, keybindings, done) =>
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
- { overlay: true, overlayOptions: { width: "100%", maxHeight: "100%", margin: 1 } },
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tau-all-agent",
3
- "version": "0.3.0",
3
+ "version": "0.3.1",
4
4
  "description": "Full Tau package for Pi with coding workflows, web search, sandboxing, memory, Telegram, Home Assistant, OpenSCAD, skills, and a theme",
5
5
  "keywords": [
6
6
  "agent-tools",