tokmon 0.11.3 → 0.12.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.
Files changed (2) hide show
  1. package/dist/cli.js +801 -96
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -8,7 +8,15 @@ import { MouseProvider } from "@zenobius/ink-mouse";
8
8
  import { readFile, writeFile, mkdir } from "fs/promises";
9
9
  import { join } from "path";
10
10
  import { homedir } from "os";
11
- var DEFAULTS = { interval: 2, billingInterval: 5, clearScreen: true, timezone: null };
11
+ var DEFAULTS = {
12
+ interval: 2,
13
+ billingInterval: 5,
14
+ clearScreen: true,
15
+ timezone: null,
16
+ accounts: [],
17
+ activeAccountId: null
18
+ };
19
+ var ACCENT_COLORS = ["cyan", "magenta", "green", "yellow", "blue", "red"];
12
20
  function configDir() {
13
21
  if (process.platform === "win32") {
14
22
  return join(process.env.APPDATA ?? join(homedir(), "AppData", "Roaming"), "tokmon");
@@ -23,7 +31,12 @@ async function loadConfig() {
23
31
  try {
24
32
  const raw = await readFile(configLocation(), "utf-8");
25
33
  const parsed = JSON.parse(raw);
26
- return { ...DEFAULTS, ...parsed };
34
+ return {
35
+ ...DEFAULTS,
36
+ ...parsed,
37
+ accounts: Array.isArray(parsed.accounts) ? parsed.accounts : [],
38
+ activeAccountId: typeof parsed.activeAccountId === "string" ? parsed.activeAccountId : null
39
+ };
27
40
  } catch {
28
41
  return { ...DEFAULTS };
29
42
  }
@@ -33,6 +46,32 @@ async function saveConfig(config2) {
33
46
  await mkdir(dir, { recursive: true });
34
47
  await writeFile(configLocation(), JSON.stringify(config2, null, 2) + "\n");
35
48
  }
49
+ function slugify(value) {
50
+ return value.trim().toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "").slice(0, 48);
51
+ }
52
+ function generateAccountId(name, existing) {
53
+ const base = slugify(name) || "account";
54
+ const taken = new Set(existing.map((a) => a.id));
55
+ if (!taken.has(base)) return base;
56
+ for (let i = 2; i < 1e3; i++) {
57
+ const candidate = `${base}_${i}`;
58
+ if (!taken.has(candidate)) return candidate;
59
+ }
60
+ return `${base}_${Date.now()}`;
61
+ }
62
+ function pickAccentColor(existing) {
63
+ const used = new Set(existing.map((a) => a.color).filter(Boolean));
64
+ for (const c of ACCENT_COLORS) {
65
+ if (!used.has(c)) return c;
66
+ }
67
+ return ACCENT_COLORS[existing.length % ACCENT_COLORS.length];
68
+ }
69
+ function expandHome(p) {
70
+ if (!p) return homedir();
71
+ if (p === "~" || p === "~/") return homedir();
72
+ if (p.startsWith("~/")) return join(homedir(), p.slice(2));
73
+ return p;
74
+ }
36
75
 
37
76
  // src/app.tsx
38
77
  import { useState, useEffect, useCallback, useRef } from "react";
@@ -163,7 +202,13 @@ var PRICING = {
163
202
  };
164
203
  var FALLBACK = PRICING["claude-opus-4"];
165
204
  var fileCache = /* @__PURE__ */ new Map();
166
- function getClaudeDirs() {
205
+ function getClaudeDirs(homeDir) {
206
+ if (homeDir) {
207
+ return [
208
+ join2(homeDir, ".claude", "projects"),
209
+ join2(homeDir, ".config", "claude", "projects")
210
+ ];
211
+ }
167
212
  const home = homedir2();
168
213
  const dirs = [join2(home, ".claude", "projects")];
169
214
  if (process.env.XDG_CONFIG_HOME) {
@@ -220,10 +265,10 @@ async function parseFile(path, since) {
220
265
  }
221
266
  return entries;
222
267
  }
223
- async function loadEntries(since) {
268
+ async function loadEntries(since, homeDir) {
224
269
  const chunks = [];
225
270
  const seen = /* @__PURE__ */ new Set();
226
- for (const dir of getClaudeDirs()) {
271
+ for (const dir of getClaudeDirs(homeDir)) {
227
272
  let listing;
228
273
  try {
229
274
  listing = await readdir(dir, { recursive: true });
@@ -318,12 +363,12 @@ function groupBy(entries, keyFn) {
318
363
  }
319
364
  return rows.sort((a, b) => a.label.localeCompare(b.label));
320
365
  }
321
- async function fetchDashboard(tz) {
366
+ async function fetchDashboard(tz, homeDir) {
322
367
  const now = Date.now();
323
368
  const monthStart = startOfMonth(now, tz);
324
369
  const todayStart = startOfDay(now, tz);
325
370
  const weekStart = startOfWeek(now, tz);
326
- const entries = await loadEntries(monthStart);
371
+ const entries = await loadEntries(monthStart, homeDir);
327
372
  const todayEntries = entries.filter((e) => e.ts >= todayStart);
328
373
  let burnRate = 0;
329
374
  if (todayEntries.length > 0) {
@@ -343,9 +388,9 @@ async function fetchDashboard(tz) {
343
388
  burnRate
344
389
  };
345
390
  }
346
- async function fetchTable(tz) {
391
+ async function fetchTable(tz, homeDir) {
347
392
  const lookback = monthsAgoStart(Date.now(), 6, tz);
348
- const entries = await loadEntries(lookback);
393
+ const entries = await loadEntries(lookback, homeDir);
349
394
  return {
350
395
  daily: groupBy(entries, (e) => dayKey(e.ts, tz)),
351
396
  weekly: groupBy(entries, (e) => weekKey(e.ts, tz)),
@@ -360,13 +405,14 @@ import { join as join3 } from "path";
360
405
  import { homedir as homedir3 } from "os";
361
406
  import { promisify } from "util";
362
407
  var execFile = promisify(execFileCb);
363
- function credentialsFilePath() {
408
+ function credentialsFilePath(homeDir) {
409
+ if (homeDir) return join3(homeDir, ".claude", ".credentials.json");
364
410
  const base = process.env.CLAUDE_CONFIG_DIR ?? join3(homedir3(), ".claude");
365
411
  return join3(base, ".credentials.json");
366
412
  }
367
- async function readCredentialsFile() {
413
+ async function readCredentialsFile(homeDir) {
368
414
  try {
369
- const raw = await readFile2(credentialsFilePath(), "utf-8");
415
+ const raw = await readFile2(credentialsFilePath(homeDir), "utf-8");
370
416
  const creds = JSON.parse(raw);
371
417
  return creds?.claudeAiOauth?.accessToken ?? creds?.accessToken ?? null;
372
418
  } catch {
@@ -387,7 +433,12 @@ async function readMacKeychain() {
387
433
  return null;
388
434
  }
389
435
  }
390
- async function getAccessToken() {
436
+ async function getAccessToken(homeDir) {
437
+ if (homeDir) {
438
+ const fromFile = await readCredentialsFile(homeDir);
439
+ if (fromFile) return fromFile;
440
+ return null;
441
+ }
391
442
  if (process.platform === "darwin") {
392
443
  const token = await readMacKeychain();
393
444
  if (token) return token;
@@ -395,8 +446,8 @@ async function getAccessToken() {
395
446
  return readCredentialsFile();
396
447
  }
397
448
  var EMPTY = { session: null, weekly: null, sonnet: null, extraUsage: null, peak: null, error: null };
398
- async function fetchBilling() {
399
- const token = await getAccessToken();
449
+ async function fetchBilling(homeDir) {
450
+ const token = await getAccessToken(homeDir);
400
451
  if (!token) return { ...EMPTY, error: "No OAuth token \u2014 run claude and log in" };
401
452
  const [usageRes, peak] = await Promise.all([
402
453
  fetchUsage(token),
@@ -519,12 +570,36 @@ var VIEWS = ["Daily", "Weekly", "Monthly"];
519
570
  var SORTS = ["date \u2191", "date \u2193", "cost \u2191", "cost \u2193"];
520
571
  var SPINNER_FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
521
572
  var MONTHS = ["", "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
522
- var DEFAULT_CONFIG = { interval: 2, billingInterval: 5, clearScreen: true, timezone: null };
523
- var SETTINGS_ROWS = 4;
573
+ var DEFAULT_CONFIG = {
574
+ interval: 2,
575
+ billingInterval: 5,
576
+ clearScreen: true,
577
+ timezone: null,
578
+ accounts: [],
579
+ activeAccountId: null
580
+ };
581
+ var GENERAL_ROWS = 4;
524
582
  var IS_TTY = process.stdin.isTTY === true;
583
+ function buildSlots(config2) {
584
+ const slots = [];
585
+ if (config2.accounts.length === 0) {
586
+ slots.push({ id: null, name: "Default", homeDir: void 0, color: "green" });
587
+ return slots;
588
+ }
589
+ slots.push({ id: null, name: "All", homeDir: void 0, color: "whiteBright" });
590
+ for (const a of config2.accounts) {
591
+ slots.push({
592
+ id: a.id,
593
+ name: a.name,
594
+ homeDir: expandHome(a.homeDir),
595
+ color: a.color || "cyan"
596
+ });
597
+ }
598
+ return slots;
599
+ }
525
600
  function App({ interval: cliInterval }) {
526
- const [dashboard, setDashboard] = useState(null);
527
- const [billing, setBilling] = useState(null);
601
+ const [config2, setConfig] = useState(null);
602
+ const [stats, setStats] = useState(/* @__PURE__ */ new Map());
528
603
  const [table, setTable] = useState(null);
529
604
  const [tableLoading, setTableLoading] = useState(false);
530
605
  const [error, setError] = useState(null);
@@ -535,18 +610,28 @@ function App({ interval: cliInterval }) {
535
610
  const [expanded, setExpanded] = useState(-1);
536
611
  const [sort, setSort] = useState(1);
537
612
  const [showSettings, setShowSettings] = useState(false);
538
- const [config2, setConfig] = useState(null);
539
613
  const [settingsCursor, setSettingsCursor] = useState(0);
540
614
  const [tzEdit, setTzEdit] = useState(null);
541
615
  const [tzError, setTzError] = useState(null);
616
+ const [accountForm, setAccountForm] = useState(null);
542
617
  const tableLoadedOnce = useRef(false);
543
618
  const { stdout } = useStdout();
544
619
  const { exit } = useApp();
545
620
  const rows = stdout?.rows ?? 24;
546
621
  const cols = stdout?.columns ?? 80;
547
- const interval2 = cliInterval ?? (config2?.interval ?? 2) * 1e3;
548
622
  const cfg = config2 ?? DEFAULT_CONFIG;
623
+ const interval2 = cliInterval ?? cfg.interval * 1e3;
549
624
  const tz = resolveTimezone(cfg.timezone);
625
+ const slots = buildSlots(cfg);
626
+ const activeSlotIdx = (() => {
627
+ if (cfg.accounts.length === 0) return 0;
628
+ if (cfg.activeAccountId === null) return 0;
629
+ const i = slots.findIndex((s) => s.id === cfg.activeAccountId);
630
+ return i < 0 ? 0 : i;
631
+ })();
632
+ const activeSlot = slots[activeSlotIdx];
633
+ const slotKey = (s) => s.id ?? "__default__";
634
+ const visibleSlots = activeSlot.id === null && cfg.accounts.length > 0 ? slots.slice(1) : [activeSlot];
550
635
  useEffect(() => {
551
636
  loadConfig().then((c) => {
552
637
  if (cliInterval) c = { ...c, interval: cliInterval / 1e3 };
@@ -554,12 +639,21 @@ function App({ interval: cliInterval }) {
554
639
  });
555
640
  }, []);
556
641
  useEffect(() => {
642
+ if (!config2) return;
557
643
  let active = true;
644
+ const slotsToLoad = activeSlot.id === null && cfg.accounts.length > 0 ? slots.slice(1) : [activeSlot];
558
645
  const load = async () => {
559
646
  try {
560
- const result = await fetchDashboard(tz);
647
+ const next = /* @__PURE__ */ new Map();
648
+ await Promise.all(slotsToLoad.map(async (slot) => {
649
+ const [dashboard, billing] = await Promise.all([
650
+ fetchDashboard(tz, slot.homeDir),
651
+ fetchBilling(slot.homeDir)
652
+ ]);
653
+ next.set(slotKey(slot), { slot, dashboard, billing });
654
+ }));
561
655
  if (active) {
562
- setDashboard(result);
656
+ setStats(next);
563
657
  setError(null);
564
658
  setUpdated(/* @__PURE__ */ new Date());
565
659
  }
@@ -573,31 +667,17 @@ function App({ interval: cliInterval }) {
573
667
  active = false;
574
668
  clearInterval(id);
575
669
  };
576
- }, [interval2, tz]);
577
- const billingMs = cfg.billingInterval * 6e4;
578
- useEffect(() => {
579
- let active = true;
580
- const load = () => fetchBilling().then((b) => {
581
- if (active) setBilling(b);
582
- }).catch(() => {
583
- });
584
- load();
585
- const id = setInterval(load, billingMs);
586
- return () => {
587
- active = false;
588
- clearInterval(id);
589
- };
590
- }, [billingMs]);
670
+ }, [interval2, tz, config2, activeSlot.id]);
591
671
  useEffect(() => {
592
672
  tableLoadedOnce.current = false;
593
673
  setTable(null);
594
- }, [tz]);
674
+ }, [tz, activeSlot.id]);
595
675
  useEffect(() => {
596
676
  if (tab !== 1) return;
597
677
  if (tableLoadedOnce.current && table) return;
598
678
  let active = true;
599
679
  setTableLoading(true);
600
- fetchTable(tz).then((result) => {
680
+ fetchTable(tz, activeSlot.id === null ? void 0 : activeSlot.homeDir).then((result) => {
601
681
  if (active) {
602
682
  setTable(result);
603
683
  setTableLoading(false);
@@ -609,13 +689,13 @@ function App({ interval: cliInterval }) {
609
689
  return () => {
610
690
  active = false;
611
691
  };
612
- }, [tab, tz]);
692
+ }, [tab, tz, activeSlot.id]);
613
693
  useEffect(() => {
614
694
  if (tab !== 1 || !tableLoadedOnce.current) return;
615
695
  let active = true;
616
696
  const id = setInterval(async () => {
617
697
  try {
618
- const result = await fetchTable(tz);
698
+ const result = await fetchTable(tz, activeSlot.id === null ? void 0 : activeSlot.homeDir);
619
699
  if (active) setTable(result);
620
700
  } catch {
621
701
  }
@@ -624,7 +704,7 @@ function App({ interval: cliInterval }) {
624
704
  active = false;
625
705
  clearInterval(id);
626
706
  };
627
- }, [tab, interval2, tz]);
707
+ }, [tab, interval2, tz, activeSlot.id]);
628
708
  const resetView = useCallback(() => {
629
709
  setCursor(0);
630
710
  setExpanded(-1);
@@ -643,7 +723,156 @@ function App({ interval: cliInterval }) {
643
723
  mouse.events.off("scroll", onScroll);
644
724
  };
645
725
  }, [tab]);
726
+ function updateConfig(fn) {
727
+ setConfig((prev) => {
728
+ const next = fn(prev ?? DEFAULT_CONFIG);
729
+ saveConfig(next);
730
+ return next;
731
+ });
732
+ }
733
+ function cycleAccount(dir) {
734
+ if (slots.length <= 1) return;
735
+ const next = (activeSlotIdx + dir + slots.length) % slots.length;
736
+ const targetId = slots[next].id;
737
+ updateConfig((c) => ({ ...c, activeAccountId: targetId }));
738
+ resetView();
739
+ }
740
+ function openAddAccount() {
741
+ setAccountForm({
742
+ mode: "add",
743
+ field: "name",
744
+ name: "",
745
+ homeDir: "~",
746
+ color: pickAccentColor(cfg.accounts),
747
+ editingId: null,
748
+ error: null
749
+ });
750
+ }
751
+ function openEditAccount(acc) {
752
+ setAccountForm({
753
+ mode: "edit",
754
+ field: "name",
755
+ name: acc.name,
756
+ homeDir: acc.homeDir,
757
+ color: acc.color || "cyan",
758
+ editingId: acc.id,
759
+ error: null
760
+ });
761
+ }
762
+ function commitAccountForm() {
763
+ if (!accountForm) return;
764
+ const name = accountForm.name.trim();
765
+ const homeDir = accountForm.homeDir.trim() || "~";
766
+ const color = accountForm.color;
767
+ if (!name) {
768
+ setAccountForm({ ...accountForm, error: "Name required", field: "name" });
769
+ return;
770
+ }
771
+ updateConfig((c) => {
772
+ if (accountForm.mode === "add") {
773
+ const id = generateAccountId(name, c.accounts);
774
+ const account = { id, name, homeDir, color };
775
+ return {
776
+ ...c,
777
+ accounts: [...c.accounts, account],
778
+ activeAccountId: c.accounts.length === 0 ? id : c.activeAccountId
779
+ };
780
+ } else {
781
+ return {
782
+ ...c,
783
+ accounts: c.accounts.map(
784
+ (a) => a.id === accountForm.editingId ? { ...a, name, homeDir, color } : a
785
+ )
786
+ };
787
+ }
788
+ });
789
+ setAccountForm(null);
790
+ }
791
+ function cycleFormField(dir) {
792
+ const order = ["name", "homeDir", "color"];
793
+ setAccountForm((f) => {
794
+ if (!f) return f;
795
+ const i = order.indexOf(f.field);
796
+ const next = order[(i + dir + order.length) % order.length];
797
+ return { ...f, field: next };
798
+ });
799
+ }
800
+ function cycleColor(dir) {
801
+ setAccountForm((f) => {
802
+ if (!f) return f;
803
+ const i = COLOR_PALETTE.indexOf(f.color);
804
+ const idx = i < 0 ? 0 : i;
805
+ const next = COLOR_PALETTE[(idx + dir + COLOR_PALETTE.length) % COLOR_PALETTE.length];
806
+ return { ...f, color: next };
807
+ });
808
+ }
809
+ function deleteAccount(id) {
810
+ updateConfig((c) => ({
811
+ ...c,
812
+ accounts: c.accounts.filter((a) => a.id !== id),
813
+ activeAccountId: c.activeAccountId === id ? null : c.activeAccountId
814
+ }));
815
+ }
816
+ const accountRowsStart = GENERAL_ROWS;
817
+ const totalSettingsRows = GENERAL_ROWS + cfg.accounts.length + 1;
646
818
  useInput((input, key) => {
819
+ if (showSettings && accountForm) {
820
+ if (key.escape) {
821
+ setAccountForm(null);
822
+ return;
823
+ }
824
+ if (key.tab) {
825
+ cycleFormField(key.shift ? -1 : 1);
826
+ return;
827
+ }
828
+ if (key.upArrow) {
829
+ cycleFormField(-1);
830
+ return;
831
+ }
832
+ if (key.downArrow) {
833
+ cycleFormField(1);
834
+ return;
835
+ }
836
+ if (accountForm.field === "color") {
837
+ if (key.leftArrow) {
838
+ cycleColor(-1);
839
+ return;
840
+ }
841
+ if (key.rightArrow) {
842
+ cycleColor(1);
843
+ return;
844
+ }
845
+ if (key.return) {
846
+ commitAccountForm();
847
+ return;
848
+ }
849
+ return;
850
+ }
851
+ if (key.return) {
852
+ if (accountForm.field === "name") {
853
+ setAccountForm((f) => f && { ...f, field: "homeDir" });
854
+ } else if (accountForm.field === "homeDir") {
855
+ setAccountForm((f) => f && { ...f, field: "color" });
856
+ }
857
+ return;
858
+ }
859
+ if (key.backspace || key.delete) {
860
+ setAccountForm((f) => {
861
+ if (!f || f.field === "color") return f;
862
+ const cur = f[f.field];
863
+ return { ...f, [f.field]: cur.slice(0, -1), error: null };
864
+ });
865
+ return;
866
+ }
867
+ if (input && !key.ctrl && !key.meta) {
868
+ setAccountForm((f) => {
869
+ if (!f || f.field === "color") return f;
870
+ return { ...f, [f.field]: f[f.field] + input, error: null };
871
+ });
872
+ return;
873
+ }
874
+ return;
875
+ }
647
876
  if (showSettings && tzEdit !== null) {
648
877
  if (key.escape) {
649
878
  setTzEdit(null);
@@ -682,19 +911,31 @@ function App({ interval: cliInterval }) {
682
911
  return;
683
912
  }
684
913
  if (showSettings) {
685
- if (key.escape || input === "s") setShowSettings(false);
686
- if (key.upArrow) setSettingsCursor((c) => Math.max(0, c - 1));
687
- if (key.downArrow) setSettingsCursor((c) => Math.min(SETTINGS_ROWS - 1, c + 1));
914
+ if (key.escape || input === "s") {
915
+ setShowSettings(false);
916
+ return;
917
+ }
918
+ if (key.upArrow) {
919
+ setSettingsCursor((c) => Math.max(0, c - 1));
920
+ return;
921
+ }
922
+ if (key.downArrow) {
923
+ setSettingsCursor((c) => Math.min(totalSettingsRows - 1, c + 1));
924
+ return;
925
+ }
688
926
  if (settingsCursor === 0) {
689
927
  if (key.leftArrow) updateConfig((c) => ({ ...c, interval: Math.max(1, c.interval - 1) }));
690
928
  if (key.rightArrow) updateConfig((c) => ({ ...c, interval: c.interval + 1 }));
929
+ return;
691
930
  }
692
931
  if (settingsCursor === 1) {
693
932
  if (key.leftArrow) updateConfig((c) => ({ ...c, billingInterval: Math.max(1, c.billingInterval - 1) }));
694
933
  if (key.rightArrow) updateConfig((c) => ({ ...c, billingInterval: c.billingInterval + 1 }));
934
+ return;
695
935
  }
696
936
  if (settingsCursor === 2 && (key.leftArrow || key.rightArrow || key.return)) {
697
937
  updateConfig((c) => ({ ...c, clearScreen: !c.clearScreen }));
938
+ return;
698
939
  }
699
940
  if (settingsCursor === 3) {
700
941
  if (key.return) {
@@ -704,28 +945,57 @@ function App({ interval: cliInterval }) {
704
945
  if (key.leftArrow || key.rightArrow) {
705
946
  updateConfig((c) => ({ ...c, timezone: c.timezone === null ? systemTimezone() : null }));
706
947
  }
948
+ return;
949
+ }
950
+ const accIdx = settingsCursor - accountRowsStart;
951
+ if (accIdx >= 0 && accIdx < cfg.accounts.length) {
952
+ const acc = cfg.accounts[accIdx];
953
+ if (key.return) {
954
+ openEditAccount(acc);
955
+ return;
956
+ }
957
+ if (input === "d" || input === "x") {
958
+ deleteAccount(acc.id);
959
+ return;
960
+ }
961
+ if (input === " ") {
962
+ updateConfig((c) => ({ ...c, activeAccountId: acc.id }));
963
+ return;
964
+ }
965
+ return;
966
+ }
967
+ if (accIdx === cfg.accounts.length && key.return) {
968
+ openAddAccount();
969
+ return;
707
970
  }
708
971
  return;
709
972
  }
710
973
  if (input === "s") {
711
974
  setShowSettings(true);
975
+ setSettingsCursor(0);
712
976
  return;
713
977
  }
714
- if (key.tab) {
715
- setTab((t) => (t + 1) % TABS.length);
716
- resetView();
978
+ if (input === "a") {
979
+ cycleAccount(1);
717
980
  return;
718
981
  }
719
- if (input === "1") {
720
- setTab(0);
721
- resetView();
982
+ if (input === "A") {
983
+ cycleAccount(-1);
722
984
  return;
723
985
  }
724
- if (input === "2") {
725
- setTab(1);
986
+ if (key.tab) {
987
+ setTab((t) => (t + 1) % TABS.length);
726
988
  resetView();
727
989
  return;
728
990
  }
991
+ if (input && /^[0-9]$/.test(input) && slots.length > 1) {
992
+ const target = slots[parseInt(input, 10)];
993
+ if (target) {
994
+ updateConfig((c) => ({ ...c, activeAccountId: target.id }));
995
+ resetView();
996
+ }
997
+ return;
998
+ }
729
999
  if (tab === 1) {
730
1000
  if (input === "d") {
731
1001
  setView(0);
@@ -789,15 +1059,11 @@ function App({ interval: cliInterval }) {
789
1059
  return;
790
1060
  }
791
1061
  }, { isActive: IS_TTY });
792
- function updateConfig(fn) {
793
- setConfig((prev) => {
794
- const next = fn(prev ?? DEFAULT_CONFIG);
795
- saveConfig(next);
796
- return next;
797
- });
798
- }
799
1062
  if (error) return /* @__PURE__ */ jsx(Box, { padding: 1, children: /* @__PURE__ */ jsx(Text, { color: "red", children: error }) });
800
- if (!dashboard) return /* @__PURE__ */ jsx(Box, { padding: 1, children: /* @__PURE__ */ jsx(Text, { dimColor: true, children: "Loading..." }) });
1063
+ if (!config2) return /* @__PURE__ */ jsx(Box, { padding: 1, children: /* @__PURE__ */ jsx(Text, { dimColor: true, children: "Loading..." }) });
1064
+ const firstStats = stats.size > 0 ? [...stats.values()][0] : null;
1065
+ if (!firstStats?.dashboard) return /* @__PURE__ */ jsx(Box, { padding: 1, children: /* @__PURE__ */ jsx(Text, { dimColor: true, children: "Loading..." }) });
1066
+ const peakBilling = firstStats.billing;
801
1067
  const rawTableData = table ? [table.daily, table.weekly, table.monthly][view] : [];
802
1068
  const tableData = sortRows(rawTableData, sort);
803
1069
  return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", paddingX: 2, paddingY: 1, height: rows, children: [
@@ -809,20 +1075,47 @@ function App({ interval: cliInterval }) {
809
1075
  ] }),
810
1076
  /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
811
1077
  " \xB7 every ",
812
- cliInterval ? cliInterval / 1e3 : cfg.interval,
1078
+ cfg.interval,
813
1079
  "s"
814
1080
  ] })
815
1081
  ] }),
816
1082
  /* @__PURE__ */ jsxs(Box, { children: [
817
- billing?.peak && /* @__PURE__ */ jsxs(Fragment, { children: [
818
- /* @__PURE__ */ jsx(PeakBadge, { peak: billing.peak }),
1083
+ peakBilling?.peak && /* @__PURE__ */ jsxs(Fragment, { children: [
1084
+ /* @__PURE__ */ jsx(PeakBadge, { peak: peakBilling.peak }),
819
1085
  /* @__PURE__ */ jsx(Text, { dimColor: true, children: " \xB7 " })
820
1086
  ] }),
821
1087
  /* @__PURE__ */ jsx(Text, { dimColor: true, children: time(updated, tz) })
822
1088
  ] })
823
1089
  ] }),
824
- showSettings ? /* @__PURE__ */ jsx(SettingsView, { config: cfg, cursor: settingsCursor, tzEdit, tzError, resolvedTz: tz }) : /* @__PURE__ */ jsxs(Fragment, { children: [
825
- /* @__PURE__ */ jsxs(Box, { marginTop: 1, children: [
1090
+ showSettings ? /* @__PURE__ */ jsx(
1091
+ SettingsView,
1092
+ {
1093
+ config: cfg,
1094
+ cursor: settingsCursor,
1095
+ tzEdit,
1096
+ tzError,
1097
+ resolvedTz: tz,
1098
+ accountForm,
1099
+ onSettingsClick: (i) => setSettingsCursor(i),
1100
+ onAddAccount: openAddAccount,
1101
+ onEditAccount: openEditAccount,
1102
+ onActivateAccount: (id) => updateConfig((c) => ({ ...c, activeAccountId: id })),
1103
+ activeAccountId: cfg.activeAccountId
1104
+ }
1105
+ ) : /* @__PURE__ */ jsxs(Fragment, { children: [
1106
+ slots.length > 1 && /* @__PURE__ */ jsx(Box, { marginTop: 1, children: /* @__PURE__ */ jsx(
1107
+ AccountStrip,
1108
+ {
1109
+ slots,
1110
+ activeIdx: activeSlotIdx,
1111
+ onSelect: (i) => {
1112
+ const id = slots[i].id;
1113
+ updateConfig((c) => ({ ...c, activeAccountId: id }));
1114
+ resetView();
1115
+ }
1116
+ }
1117
+ ) }),
1118
+ /* @__PURE__ */ jsxs(Box, { marginTop: slots.length > 1 ? 0 : 1, children: [
826
1119
  /* @__PURE__ */ jsx(TabBar, { tabs: TABS, active: tab, onSelect: (i) => {
827
1120
  setTab(i);
828
1121
  resetView();
@@ -830,7 +1123,14 @@ function App({ interval: cliInterval }) {
830
1123
  /* @__PURE__ */ jsx(Text, { dimColor: true, children: " Tab/\u2190\u2192" })
831
1124
  ] }),
832
1125
  /* @__PURE__ */ jsx(Box, { height: 1 }),
833
- tab === 0 && /* @__PURE__ */ jsx(DashboardView, { data: dashboard, billing }),
1126
+ tab === 0 && /* @__PURE__ */ jsx(
1127
+ DashboardView,
1128
+ {
1129
+ slots: visibleSlots,
1130
+ stats,
1131
+ compact: visibleSlots.length > 1
1132
+ }
1133
+ ),
834
1134
  tab === 1 && /* @__PURE__ */ jsxs(Fragment, { children: [
835
1135
  /* @__PURE__ */ jsx(ViewBar, { views: VIEWS, active: view, sort: SORTS[sort], onSelect: (i) => {
836
1136
  setView(i);
@@ -843,7 +1143,7 @@ function App({ interval: cliInterval }) {
843
1143
  rows: tableData,
844
1144
  cursor,
845
1145
  expanded,
846
- maxRows: rows - 12,
1146
+ maxRows: rows - 14,
847
1147
  cols,
848
1148
  onRowClick: (idx) => {
849
1149
  if (idx === cursor) setExpanded((e) => e === idx ? -1 : idx);
@@ -853,16 +1153,18 @@ function App({ interval: cliInterval }) {
853
1153
  )
854
1154
  ] })
855
1155
  ] }),
856
- (tab === 0 || showSettings) && /* @__PURE__ */ jsx(Footer, {})
1156
+ (tab === 0 || showSettings) && /* @__PURE__ */ jsx(Footer, { hasAccounts: slots.length > 1 })
857
1157
  ] });
858
1158
  }
859
- function Footer() {
1159
+ function Footer({ hasAccounts }) {
860
1160
  return /* @__PURE__ */ jsxs(Box, { marginTop: 1, children: [
861
1161
  /* @__PURE__ */ jsx(Text, { dimColor: true, children: "by " }),
862
1162
  /* @__PURE__ */ jsx(Text, { children: "David Ilie" }),
863
1163
  /* @__PURE__ */ jsx(Text, { dimColor: true, children: " (" }),
864
1164
  /* @__PURE__ */ jsx(Text, { color: "cyan", children: "davidilie.com" }),
865
- /* @__PURE__ */ jsx(Text, { dimColor: true, children: ") \xB7 s=settings q=quit" })
1165
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: ") \xB7 s=settings " }),
1166
+ hasAccounts && /* @__PURE__ */ jsx(Text, { dimColor: true, children: "0-9=jump a/A=cycle " }),
1167
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "q=quit" })
866
1168
  ] });
867
1169
  }
868
1170
  function TabBar({ tabs, active, onSelect }) {
@@ -876,6 +1178,19 @@ function TabBar({ tabs, active, onSelect }) {
876
1178
  " "
877
1179
  ] }) }, t)) });
878
1180
  }
1181
+ function AccountStrip({ slots, activeIdx, onSelect }) {
1182
+ return /* @__PURE__ */ jsx(Box, { flexWrap: "wrap", children: slots.map((s, i) => {
1183
+ const active = i === activeIdx;
1184
+ const dot = s.id === null ? "\u2726" : "\u25CF";
1185
+ return /* @__PURE__ */ jsxs(ClickableBox, { onClick: () => onSelect(i), marginRight: 2, children: [
1186
+ /* @__PURE__ */ jsx(Text, { dimColor: !active, children: i }),
1187
+ /* @__PURE__ */ jsx(Text, { children: " " }),
1188
+ /* @__PURE__ */ jsx(Text, { color: s.color, bold: active, dimColor: !active, children: dot }),
1189
+ /* @__PURE__ */ jsx(Text, { children: " " }),
1190
+ active ? /* @__PURE__ */ jsx(Text, { bold: true, color: s.color, children: s.name }) : /* @__PURE__ */ jsx(Text, { dimColor: true, children: s.name })
1191
+ ] }, s.id ?? "__all__");
1192
+ }) });
1193
+ }
879
1194
  function ViewBar({ views, active, sort, onSelect }) {
880
1195
  return /* @__PURE__ */ jsxs(Box, { children: [
881
1196
  views.map((v, i) => /* @__PURE__ */ jsx(ClickableBox, { onClick: () => onSelect(i), marginRight: 2, children: i === active ? /* @__PURE__ */ jsxs(Text, { bold: true, color: "cyan", children: [
@@ -904,13 +1219,40 @@ function sortRows(rows, sortIdx) {
904
1219
  return sorted;
905
1220
  }
906
1221
  }
907
- function SettingsView({ config: config2, cursor, tzEdit, tzError, resolvedTz }) {
908
- const editing = tzEdit !== null;
1222
+ var COLOR_PALETTE = [
1223
+ "cyan",
1224
+ "magenta",
1225
+ "green",
1226
+ "yellow",
1227
+ "blue",
1228
+ "red",
1229
+ "cyanBright",
1230
+ "magentaBright",
1231
+ "greenBright"
1232
+ ];
1233
+ function SettingsView({
1234
+ config: config2,
1235
+ cursor,
1236
+ tzEdit,
1237
+ tzError,
1238
+ resolvedTz,
1239
+ accountForm,
1240
+ onAddAccount,
1241
+ onEditAccount,
1242
+ onActivateAccount,
1243
+ activeAccountId
1244
+ }) {
1245
+ const editingTz = tzEdit !== null;
909
1246
  const tzDisplay = config2.timezone === null ? `System (${resolvedTz})` : config2.timezone;
1247
+ const accountRowsStart = GENERAL_ROWS;
1248
+ if (accountForm) {
1249
+ return /* @__PURE__ */ jsx(AccountFormView, { form: accountForm, accounts: config2.accounts });
1250
+ }
910
1251
  return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", marginTop: 1, children: [
911
1252
  /* @__PURE__ */ jsx(Text, { bold: true, children: "Settings" }),
912
1253
  /* @__PURE__ */ jsx(Text, { dimColor: true, children: configLocation() }),
913
1254
  /* @__PURE__ */ jsx(Box, { height: 1 }),
1255
+ /* @__PURE__ */ jsx(Text, { bold: true, dimColor: true, children: "General" }),
914
1256
  /* @__PURE__ */ jsxs(Box, { children: [
915
1257
  /* @__PURE__ */ jsxs(Text, { color: cursor === 0 ? "green" : void 0, children: [
916
1258
  cursor === 0 ? "\u25B8" : " ",
@@ -963,7 +1305,7 @@ function SettingsView({ config: config2, cursor, tzEdit, tzError, resolvedTz })
963
1305
  " "
964
1306
  ] }),
965
1307
  /* @__PURE__ */ jsx(Text, { children: "Timezone " }),
966
- editing ? /* @__PURE__ */ jsxs(Fragment, { children: [
1308
+ editingTz ? /* @__PURE__ */ jsxs(Fragment, { children: [
967
1309
  /* @__PURE__ */ jsx(Text, { dimColor: true, children: "[" }),
968
1310
  /* @__PURE__ */ jsx(Text, { bold: true, color: "cyan", children: tzEdit }),
969
1311
  /* @__PURE__ */ jsx(Text, { color: "cyan", children: "_" }),
@@ -975,32 +1317,278 @@ function SettingsView({ config: config2, cursor, tzEdit, tzError, resolvedTz })
975
1317
  tzError
976
1318
  ] }),
977
1319
  /* @__PURE__ */ jsx(Box, { height: 1 }),
978
- editing ? /* @__PURE__ */ jsx(Text, { dimColor: true, children: "type IANA name (e.g. Europe/London) \xB7 empty = System \xB7 Enter save \xB7 Esc cancel" }) : /* @__PURE__ */ jsx(Text, { dimColor: true, children: "\u2191\u2193 select \u2190\u2192 adjust Enter edit tz s/Esc close" })
1320
+ /* @__PURE__ */ jsx(Text, { bold: true, dimColor: true, children: "Claude accounts" }),
1321
+ config2.accounts.length === 0 && /* @__PURE__ */ jsx(Text, { dimColor: true, children: " none \u2014 using default Claude HOME" }),
1322
+ config2.accounts.map((acc, i) => {
1323
+ const idx = accountRowsStart + i;
1324
+ const selected = cursor === idx;
1325
+ const isActive = acc.id === activeAccountId;
1326
+ return /* @__PURE__ */ jsxs(Box, { children: [
1327
+ /* @__PURE__ */ jsxs(Text, { color: selected ? "green" : void 0, children: [
1328
+ selected ? "\u25B8" : " ",
1329
+ " "
1330
+ ] }),
1331
+ /* @__PURE__ */ jsxs(Text, { color: acc.color || "cyan", children: [
1332
+ isActive ? "\u25CF" : "\u25CB",
1333
+ " "
1334
+ ] }),
1335
+ /* @__PURE__ */ jsx(Box, { width: 16, children: /* @__PURE__ */ jsx(Text, { bold: true, children: acc.name }) }),
1336
+ /* @__PURE__ */ jsx(Box, { width: 14, children: /* @__PURE__ */ jsx(Text, { dimColor: true, children: acc.id }) }),
1337
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: acc.homeDir })
1338
+ ] }, acc.id);
1339
+ }),
1340
+ /* @__PURE__ */ jsxs(Box, { children: [
1341
+ /* @__PURE__ */ jsxs(Text, { color: cursor === accountRowsStart + config2.accounts.length ? "green" : void 0, children: [
1342
+ cursor === accountRowsStart + config2.accounts.length ? "\u25B8" : " ",
1343
+ " "
1344
+ ] }),
1345
+ /* @__PURE__ */ jsx(Text, { color: "greenBright", children: "+ " }),
1346
+ /* @__PURE__ */ jsx(Text, { children: "Add account" })
1347
+ ] }),
1348
+ /* @__PURE__ */ jsx(Box, { height: 1 }),
1349
+ editingTz ? /* @__PURE__ */ jsx(Text, { dimColor: true, children: "type IANA name (e.g. Europe/London) \xB7 empty = System \xB7 Enter save \xB7 Esc cancel" }) : cursor >= accountRowsStart && cursor < accountRowsStart + config2.accounts.length ? /* @__PURE__ */ jsx(Text, { dimColor: true, children: "\u2191\u2193 select \xB7 Enter edit \xB7 space activate \xB7 d delete \xB7 s/Esc close" }) : cursor === accountRowsStart + config2.accounts.length ? /* @__PURE__ */ jsx(Text, { dimColor: true, children: "\u2191\u2193 select \xB7 Enter add account \xB7 s/Esc close" }) : /* @__PURE__ */ jsx(Text, { dimColor: true, children: "\u2191\u2193 select \u2190\u2192 adjust Enter edit s/Esc close" })
979
1350
  ] });
980
1351
  }
981
- function DashboardView({ data, billing }) {
982
- return /* @__PURE__ */ jsxs(Fragment, { children: [
1352
+ function AccountFormView({ form, accounts }) {
1353
+ const previewId = form.mode === "add" ? generateAccountId(form.name || "account", accounts) : form.editingId ?? "";
1354
+ const accent = form.color;
1355
+ const stepIndex = { name: 1, homeDir: 2, color: 3 };
1356
+ const step = stepIndex[form.field];
1357
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", marginTop: 1, children: [
1358
+ /* @__PURE__ */ jsxs(Box, { children: [
1359
+ /* @__PURE__ */ jsx(Text, { color: accent, bold: true, children: "\u258D" }),
1360
+ /* @__PURE__ */ jsxs(Text, { bold: true, children: [
1361
+ " ",
1362
+ form.mode === "add" ? "NEW ACCOUNT" : "EDIT ACCOUNT"
1363
+ ] }),
1364
+ /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
1365
+ " step ",
1366
+ step,
1367
+ " of 3"
1368
+ ] })
1369
+ ] }),
1370
+ /* @__PURE__ */ jsx(Box, { marginTop: 1, children: /* @__PURE__ */ jsx(Stepper, { active: form.field, accent }) }),
983
1371
  /* @__PURE__ */ jsxs(
984
1372
  Box,
985
1373
  {
1374
+ marginTop: 1,
986
1375
  flexDirection: "column",
1376
+ borderStyle: "round",
1377
+ borderColor: accent,
1378
+ paddingX: 2,
1379
+ paddingY: 1,
1380
+ children: [
1381
+ /* @__PURE__ */ jsx(
1382
+ FormField,
1383
+ {
1384
+ label: "Name",
1385
+ hint: "display name for this Claude account",
1386
+ value: form.name,
1387
+ focused: form.field === "name",
1388
+ accent,
1389
+ placeholder: "e.g. Work, Personal"
1390
+ }
1391
+ ),
1392
+ /* @__PURE__ */ jsx(Box, { height: 1 }),
1393
+ /* @__PURE__ */ jsx(
1394
+ FormField,
1395
+ {
1396
+ label: "Home directory",
1397
+ hint: "path containing .claude/ \xB7 ~ for default",
1398
+ value: form.homeDir,
1399
+ focused: form.field === "homeDir",
1400
+ accent,
1401
+ placeholder: "~/claude-work",
1402
+ mono: true
1403
+ }
1404
+ ),
1405
+ /* @__PURE__ */ jsx(Box, { height: 1 }),
1406
+ /* @__PURE__ */ jsx(
1407
+ ColorField,
1408
+ {
1409
+ value: form.color,
1410
+ focused: form.field === "color"
1411
+ }
1412
+ ),
1413
+ /* @__PURE__ */ jsx(Box, { height: 1 }),
1414
+ /* @__PURE__ */ jsxs(Box, { children: [
1415
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "id " }),
1416
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "\u2524 " }),
1417
+ /* @__PURE__ */ jsx(Text, { bold: true, color: accent, children: previewId || "account" }),
1418
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: " \u251C" }),
1419
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: " auto-generated from name" })
1420
+ ] })
1421
+ ]
1422
+ }
1423
+ ),
1424
+ form.error && /* @__PURE__ */ jsx(Box, { marginTop: 1, children: /* @__PURE__ */ jsxs(Text, { color: "red", children: [
1425
+ "\u26A0 ",
1426
+ form.error
1427
+ ] }) }),
1428
+ /* @__PURE__ */ jsxs(Box, { marginTop: 1, children: [
1429
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "tab/\u2191\u2193 " }),
1430
+ /* @__PURE__ */ jsx(Text, { children: "switch field" }),
1431
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: " \xB7 " }),
1432
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "enter " }),
1433
+ /* @__PURE__ */ jsx(Text, { children: form.field === "color" ? "save" : "next" }),
1434
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: " \xB7 " }),
1435
+ form.field === "color" && /* @__PURE__ */ jsxs(Fragment, { children: [
1436
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "\u2190\u2192 " }),
1437
+ /* @__PURE__ */ jsx(Text, { children: "pick color" }),
1438
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: " \xB7 " })
1439
+ ] }),
1440
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "esc " }),
1441
+ /* @__PURE__ */ jsx(Text, { children: "cancel" })
1442
+ ] })
1443
+ ] });
1444
+ }
1445
+ function Stepper({ active, accent }) {
1446
+ const steps = [
1447
+ { id: "name", label: "Name" },
1448
+ { id: "homeDir", label: "Home" },
1449
+ { id: "color", label: "Color" }
1450
+ ];
1451
+ const order = steps.map((s) => s.id);
1452
+ const activeIdx = order.indexOf(active);
1453
+ return /* @__PURE__ */ jsx(Box, { children: steps.map((s, i) => {
1454
+ const done = i < activeIdx;
1455
+ const cur = i === activeIdx;
1456
+ const dot = done ? "\u25CF" : cur ? "\u25C9" : "\u25CB";
1457
+ return /* @__PURE__ */ jsxs(Box, { children: [
1458
+ /* @__PURE__ */ jsxs(Text, { color: cur ? accent : done ? accent : void 0, dimColor: !cur && !done, children: [
1459
+ dot,
1460
+ " "
1461
+ ] }),
1462
+ /* @__PURE__ */ jsx(Text, { bold: cur, color: cur ? accent : void 0, dimColor: !cur, children: s.label }),
1463
+ i < steps.length - 1 && /* @__PURE__ */ jsx(Text, { dimColor: true, children: " \u2500 " })
1464
+ ] }, s.id);
1465
+ }) });
1466
+ }
1467
+ function FormField({
1468
+ label,
1469
+ hint,
1470
+ value,
1471
+ focused,
1472
+ accent,
1473
+ placeholder,
1474
+ mono
1475
+ }) {
1476
+ const display = value === "" ? placeholder : value;
1477
+ const isPlaceholder = value === "";
1478
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
1479
+ /* @__PURE__ */ jsx(Box, { children: /* @__PURE__ */ jsxs(Text, { color: focused ? accent : void 0, bold: focused, dimColor: !focused, children: [
1480
+ focused ? "\u25B8" : " ",
1481
+ " ",
1482
+ label
1483
+ ] }) }),
1484
+ /* @__PURE__ */ jsxs(Box, { marginTop: 0, children: [
1485
+ /* @__PURE__ */ jsxs(Text, { color: focused ? accent : void 0, children: [
1486
+ " ",
1487
+ focused ? "\u258C" : " ",
1488
+ " "
1489
+ ] }),
1490
+ /* @__PURE__ */ jsx(
1491
+ Text,
1492
+ {
1493
+ bold: focused && !isPlaceholder,
1494
+ color: focused && !isPlaceholder ? accent : void 0,
1495
+ dimColor: isPlaceholder,
1496
+ italic: mono && isPlaceholder,
1497
+ children: display
1498
+ }
1499
+ ),
1500
+ focused && /* @__PURE__ */ jsx(Text, { color: accent, children: "\u258F" })
1501
+ ] }),
1502
+ /* @__PURE__ */ jsx(Box, { children: /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
1503
+ " ",
1504
+ hint
1505
+ ] }) })
1506
+ ] });
1507
+ }
1508
+ function ColorField({ value, focused }) {
1509
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
1510
+ /* @__PURE__ */ jsx(Box, { children: /* @__PURE__ */ jsxs(Text, { color: focused ? value : void 0, bold: focused, dimColor: !focused, children: [
1511
+ focused ? "\u25B8" : " ",
1512
+ " Accent color"
1513
+ ] }) }),
1514
+ /* @__PURE__ */ jsxs(Box, { marginTop: 0, children: [
1515
+ /* @__PURE__ */ jsxs(Text, { children: [
1516
+ " ",
1517
+ focused ? "\u258C" : " ",
1518
+ " "
1519
+ ] }),
1520
+ COLOR_PALETTE.map((c, i) => {
1521
+ const selected = c === value;
1522
+ return /* @__PURE__ */ jsx(Box, { marginRight: 1, children: selected ? /* @__PURE__ */ jsx(Text, { bold: true, color: c, children: "[\u25CF]" }) : /* @__PURE__ */ jsx(Text, { color: c, dimColor: !focused, children: i === COLOR_PALETTE.length - 1 ? " \u25CF" : " \u25CF" }) }, c);
1523
+ })
1524
+ ] }),
1525
+ /* @__PURE__ */ jsx(Box, { children: /* @__PURE__ */ jsx(Text, { dimColor: true, children: " shows on dashboard, account strip, borders" }) })
1526
+ ] });
1527
+ }
1528
+ function DashboardView({ slots, stats, compact }) {
1529
+ const slotKey = (s2) => s2.id ?? "__default__";
1530
+ if (compact) {
1531
+ const accountStats = slots.map((slot2) => ({ slot: slot2, s: stats.get(slotKey(slot2)) })).filter((x) => !!x.s?.dashboard);
1532
+ return /* @__PURE__ */ jsx(ComparisonView, { accountStats });
1533
+ }
1534
+ const slot = slots[0];
1535
+ const s = stats.get(slotKey(slot));
1536
+ if (!s?.dashboard) {
1537
+ return /* @__PURE__ */ jsxs(Box, { children: [
1538
+ /* @__PURE__ */ jsxs(Text, { color: slot.color, bold: true, children: [
1539
+ "\u25CF ",
1540
+ slot.name,
1541
+ " "
1542
+ ] }),
1543
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "loading..." })
1544
+ ] });
1545
+ }
1546
+ return /* @__PURE__ */ jsx(SoloAccountCard, { slot, dashboard: s.dashboard, billing: s.billing });
1547
+ }
1548
+ function bar(value, max, width) {
1549
+ if (max <= 0) return { filled: 0, empty: width };
1550
+ const filled = Math.max(0, Math.min(width, Math.round(value / max * width)));
1551
+ return { filled, empty: width - filled };
1552
+ }
1553
+ function SoloAccountCard({ slot, dashboard, billing }) {
1554
+ const maxCost = Math.max(dashboard.today.cost, dashboard.week.cost, dashboard.month.cost, 0.01);
1555
+ const maxTokens = Math.max(dashboard.today.tokens, dashboard.week.tokens, dashboard.month.tokens, 1);
1556
+ const rows = [
1557
+ { label: "Today", s: dashboard.today },
1558
+ { label: "This Week", s: dashboard.week },
1559
+ { label: "This Month", s: dashboard.month }
1560
+ ];
1561
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
1562
+ /* @__PURE__ */ jsxs(Box, { children: [
1563
+ /* @__PURE__ */ jsxs(Text, { color: slot.color, bold: true, children: [
1564
+ "\u25CF ",
1565
+ slot.name
1566
+ ] }),
1567
+ slot.id && /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
1568
+ " ",
1569
+ slot.id
1570
+ ] })
1571
+ ] }),
1572
+ /* @__PURE__ */ jsxs(
1573
+ Box,
1574
+ {
1575
+ flexDirection: "column",
1576
+ marginTop: 1,
987
1577
  paddingLeft: 1,
988
1578
  borderStyle: "bold",
989
- borderColor: "green",
1579
+ borderColor: slot.color,
990
1580
  borderRight: false,
991
1581
  borderTop: false,
992
1582
  borderBottom: false,
993
1583
  children: [
994
- /* @__PURE__ */ jsx(Text, { bold: true, children: "Claude" }),
1584
+ /* @__PURE__ */ jsx(Text, { bold: true, children: "Usage" }),
995
1585
  /* @__PURE__ */ jsx(Box, { height: 1 }),
996
- /* @__PURE__ */ jsx(SummaryRow, { label: "Today", summary: data.today }),
997
- /* @__PURE__ */ jsx(SummaryRow, { label: "This Week", summary: data.week }),
998
- /* @__PURE__ */ jsx(SummaryRow, { label: "This Month", summary: data.month }),
999
- data.burnRate > 0 && /* @__PURE__ */ jsxs(Fragment, { children: [
1586
+ rows.map((r) => /* @__PURE__ */ jsx(DualBarRow, { label: r.label, cost: r.s.cost, tokens: r.s.tokens, maxCost, maxTokens, color: slot.color }, r.label)),
1587
+ dashboard.burnRate > 0 && /* @__PURE__ */ jsxs(Fragment, { children: [
1000
1588
  /* @__PURE__ */ jsx(Box, { height: 1 }),
1001
1589
  /* @__PURE__ */ jsxs(Box, { children: [
1002
1590
  /* @__PURE__ */ jsx(Box, { width: 14, children: /* @__PURE__ */ jsx(Text, { dimColor: true, children: "Burn rate" }) }),
1003
- /* @__PURE__ */ jsx(Box, { width: 12, justifyContent: "flex-end", children: /* @__PURE__ */ jsx(Text, { color: "red", children: currency(data.burnRate) }) }),
1591
+ /* @__PURE__ */ jsx(Box, { width: 12, justifyContent: "flex-end", children: /* @__PURE__ */ jsx(Text, { color: "red", children: currency(dashboard.burnRate) }) }),
1004
1592
  /* @__PURE__ */ jsx(Text, { dimColor: true, children: "/hr" })
1005
1593
  ] })
1006
1594
  ] })
@@ -1043,6 +1631,133 @@ function DashboardView({ data, billing }) {
1043
1631
  )
1044
1632
  ] });
1045
1633
  }
1634
+ function DualBarRow({
1635
+ label,
1636
+ cost,
1637
+ tokens: tokens2,
1638
+ maxCost,
1639
+ maxTokens,
1640
+ color
1641
+ }) {
1642
+ const W = 18;
1643
+ const c = bar(cost, maxCost, W);
1644
+ const t = bar(tokens2, maxTokens, W);
1645
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", marginBottom: 0, children: [
1646
+ /* @__PURE__ */ jsxs(Box, { children: [
1647
+ /* @__PURE__ */ jsx(Box, { width: 12, children: /* @__PURE__ */ jsx(Text, { dimColor: true, children: label }) }),
1648
+ /* @__PURE__ */ jsx(Text, { color, children: "\u2588".repeat(c.filled) }),
1649
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "\u2591".repeat(c.empty) }),
1650
+ /* @__PURE__ */ jsx(Text, { children: " " }),
1651
+ /* @__PURE__ */ jsx(Box, { width: 11, justifyContent: "flex-end", children: /* @__PURE__ */ jsx(Text, { bold: true, color: "yellow", children: currency(cost) }) })
1652
+ ] }),
1653
+ /* @__PURE__ */ jsxs(Box, { children: [
1654
+ /* @__PURE__ */ jsx(Box, { width: 12, children: /* @__PURE__ */ jsx(Text, { children: " " }) }),
1655
+ /* @__PURE__ */ jsx(Text, { color, dimColor: true, children: "\u2593".repeat(t.filled) }),
1656
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "\u2591".repeat(t.empty) }),
1657
+ /* @__PURE__ */ jsx(Text, { children: " " }),
1658
+ /* @__PURE__ */ jsx(Box, { width: 11, justifyContent: "flex-end", children: /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
1659
+ tokens(tokens2),
1660
+ " tk"
1661
+ ] }) })
1662
+ ] })
1663
+ ] });
1664
+ }
1665
+ function ComparisonView({ accountStats }) {
1666
+ if (accountStats.length === 0) {
1667
+ return /* @__PURE__ */ jsx(Text, { dimColor: true, children: "No accounts loaded yet..." });
1668
+ }
1669
+ const periods = [
1670
+ { key: "Today", pick: (d) => d.today },
1671
+ { key: "This Week", pick: (d) => d.week },
1672
+ { key: "This Month", pick: (d) => d.month }
1673
+ ];
1674
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
1675
+ periods.map((p, i) => {
1676
+ const rows = accountStats.map(({ slot, s }) => ({
1677
+ slot,
1678
+ summary: p.pick(s.dashboard)
1679
+ }));
1680
+ const maxCost = Math.max(0.01, ...rows.map((r) => r.summary.cost));
1681
+ const maxTokens = Math.max(1, ...rows.map((r) => r.summary.tokens));
1682
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", marginBottom: i < periods.length - 1 ? 1 : 0, children: [
1683
+ /* @__PURE__ */ jsx(Box, { children: /* @__PURE__ */ jsx(Text, { bold: true, dimColor: true, children: p.key.toUpperCase() }) }),
1684
+ rows.map(({ slot, summary }) => /* @__PURE__ */ jsx(
1685
+ ComparisonRow,
1686
+ {
1687
+ slot,
1688
+ cost: summary.cost,
1689
+ tokens: summary.tokens,
1690
+ maxCost,
1691
+ maxTokens
1692
+ },
1693
+ slot.id ?? "__default__"
1694
+ ))
1695
+ ] }, p.key);
1696
+ }),
1697
+ /* @__PURE__ */ jsx(Box, { height: 1 }),
1698
+ /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
1699
+ /* @__PURE__ */ jsx(Text, { bold: true, dimColor: true, children: "RATE LIMITS" }),
1700
+ accountStats.map(({ slot, s }) => /* @__PURE__ */ jsx(CompactLimitsRow, { slot, billing: s.billing }, slot.id ?? "__default__"))
1701
+ ] })
1702
+ ] });
1703
+ }
1704
+ function ComparisonRow({
1705
+ slot,
1706
+ cost,
1707
+ tokens: tokens2,
1708
+ maxCost,
1709
+ maxTokens
1710
+ }) {
1711
+ const W = 22;
1712
+ const c = bar(cost, maxCost, W);
1713
+ const t = bar(tokens2, maxTokens, W);
1714
+ return /* @__PURE__ */ jsxs(Box, { children: [
1715
+ /* @__PURE__ */ jsxs(Box, { width: 14, children: [
1716
+ /* @__PURE__ */ jsx(Text, { color: slot.color, children: "\u25CF " }),
1717
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: slot.name })
1718
+ ] }),
1719
+ /* @__PURE__ */ jsx(Text, { color: slot.color, children: "\u2588".repeat(c.filled) }),
1720
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "\u2591".repeat(c.empty) }),
1721
+ /* @__PURE__ */ jsx(Text, { children: " " }),
1722
+ /* @__PURE__ */ jsx(Box, { width: 10, justifyContent: "flex-end", children: /* @__PURE__ */ jsx(Text, { bold: true, color: "yellow", children: currency(cost) }) }),
1723
+ /* @__PURE__ */ jsx(Text, { children: " " }),
1724
+ /* @__PURE__ */ jsx(Text, { color: slot.color, dimColor: true, children: "\u2593".repeat(t.filled) }),
1725
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "\u2591".repeat(t.empty) }),
1726
+ /* @__PURE__ */ jsx(Text, { children: " " }),
1727
+ /* @__PURE__ */ jsx(Box, { width: 10, justifyContent: "flex-end", children: /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
1728
+ tokens(tokens2),
1729
+ " tk"
1730
+ ] }) })
1731
+ ] });
1732
+ }
1733
+ function CompactLimitsRow({ slot, billing }) {
1734
+ if (billing?.error) {
1735
+ return /* @__PURE__ */ jsxs(Box, { children: [
1736
+ /* @__PURE__ */ jsxs(Box, { width: 14, children: [
1737
+ /* @__PURE__ */ jsx(Text, { color: slot.color, children: "\u25CF " }),
1738
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: slot.name })
1739
+ ] }),
1740
+ /* @__PURE__ */ jsx(Text, { color: "red", children: billing.error })
1741
+ ] });
1742
+ }
1743
+ const fmtPct = (p) => p ? `${Math.round(p.utilization)}%` : "\u2014";
1744
+ const colorFor = (p) => {
1745
+ if (!p) return void 0;
1746
+ return p.utilization >= 80 ? "red" : p.utilization >= 50 ? "yellow" : "green";
1747
+ };
1748
+ return /* @__PURE__ */ jsxs(Box, { children: [
1749
+ /* @__PURE__ */ jsxs(Box, { width: 14, children: [
1750
+ /* @__PURE__ */ jsx(Text, { color: slot.color, children: "\u25CF " }),
1751
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: slot.name })
1752
+ ] }),
1753
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "S " }),
1754
+ /* @__PURE__ */ jsx(Box, { width: 6, children: /* @__PURE__ */ jsx(Text, { bold: true, color: colorFor(billing?.session), children: fmtPct(billing?.session) }) }),
1755
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "W " }),
1756
+ /* @__PURE__ */ jsx(Box, { width: 6, children: /* @__PURE__ */ jsx(Text, { bold: true, color: colorFor(billing?.weekly), children: fmtPct(billing?.weekly) }) }),
1757
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "Sonnet " }),
1758
+ /* @__PURE__ */ jsx(Box, { width: 6, children: /* @__PURE__ */ jsx(Text, { bold: true, color: colorFor(billing?.sonnet), children: fmtPct(billing?.sonnet) }) })
1759
+ ] });
1760
+ }
1046
1761
  function PeakBadge({ peak }) {
1047
1762
  const color = peak.state === "peak" ? "red" : "green";
1048
1763
  return /* @__PURE__ */ jsxs(Box, { children: [
@@ -1080,16 +1795,6 @@ function LimitBar({ label, pct, resets }) {
1080
1795
  ] })
1081
1796
  ] });
1082
1797
  }
1083
- function SummaryRow({ label, summary }) {
1084
- return /* @__PURE__ */ jsxs(Box, { children: [
1085
- /* @__PURE__ */ jsx(Box, { width: 14, children: /* @__PURE__ */ jsx(Text, { dimColor: true, children: label }) }),
1086
- /* @__PURE__ */ jsx(Box, { width: 12, justifyContent: "flex-end", children: /* @__PURE__ */ jsx(Text, { bold: true, color: "yellow", children: currency(summary.cost) }) }),
1087
- /* @__PURE__ */ jsx(Box, { width: 18, justifyContent: "flex-end", children: /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
1088
- tokens(summary.tokens),
1089
- " tokens"
1090
- ] }) })
1091
- ] });
1092
- }
1093
1798
  function TableView({ rows: allRows, cursor, expanded, maxRows, cols, onRowClick }) {
1094
1799
  const wide = cols > 90;
1095
1800
  const base = wide ? { label: 12, input: 10, output: 10, cc: 14, cr: 12, total: 11, cost: 13 } : { label: 8, input: 7, output: 7, cc: 7, cr: 8, total: 0, cost: 11 };
@@ -1167,7 +1872,7 @@ function TableView({ rows: allRows, cursor, expanded, maxRows, cols, onRowClick
1167
1872
  allRows.length
1168
1873
  ] }),
1169
1874
  /* @__PURE__ */ jsx(Box, { height: 1 }),
1170
- /* @__PURE__ */ jsx(Footer, {})
1875
+ /* @__PURE__ */ jsx(Footer, { hasAccounts: false })
1171
1876
  ] });
1172
1877
  }
1173
1878
  function RowDetail({ row, indent }) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tokmon",
3
- "version": "0.11.3",
3
+ "version": "0.12.1",
4
4
  "description": "Terminal dashboard for Claude Code usage and costs",
5
5
  "type": "module",
6
6
  "bin": {