tokmon 0.13.0 → 0.14.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 (3) hide show
  1. package/README.md +31 -13
  2. package/dist/cli.js +2141 -403
  3. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/cli.tsx
4
+ import { EventEmitter } from "events";
4
5
  import { render } from "ink";
5
6
  import { MouseProvider } from "@zenobius/ink-mouse";
6
7
 
@@ -22,8 +23,11 @@ var DEFAULTS = {
22
23
  disabledProviders: [],
23
24
  onboarded: false,
24
25
  dashboardLayout: "grid",
25
- defaultFocus: "all"
26
+ defaultFocus: "all",
27
+ ascii: "auto",
28
+ knownProviders: []
26
29
  };
30
+ var LEGACY_KNOWN = ["claude", "codex", "cursor"];
27
31
  var ACCENT_COLORS = ["cyan", "magenta", "green", "yellow", "blue", "red"];
28
32
  function configDir() {
29
33
  if (process.platform === "win32") {
@@ -43,7 +47,7 @@ function cacheDir() {
43
47
  }
44
48
  return join(envDir("XDG_CACHE_HOME") ?? join(homedir(), ".cache"), "tokmon");
45
49
  }
46
- var PROVIDER_IDS = ["claude", "codex", "cursor"];
50
+ var PROVIDER_IDS = ["claude", "codex", "cursor", "pi", "opencode", "copilot", "antigravity", "gemini"];
47
51
  function clampNum(v, fallback, min) {
48
52
  return typeof v === "number" && Number.isFinite(v) && v >= min ? v : fallback;
49
53
  }
@@ -84,7 +88,9 @@ async function loadConfig() {
84
88
  // provider picker once, so existing users can opt into Codex/Cursor.
85
89
  onboarded: parsed.onboarded === true,
86
90
  dashboardLayout: parsed.dashboardLayout === "single" ? "single" : "grid",
87
- defaultFocus: parsed.defaultFocus === "last" ? "last" : "all"
91
+ defaultFocus: parsed.defaultFocus === "last" ? "last" : "all",
92
+ ascii: parsed.ascii === "on" ? "on" : parsed.ascii === "off" ? "off" : "auto",
93
+ knownProviders: Array.isArray(parsed.knownProviders) ? parsed.knownProviders.filter((p) => PROVIDER_IDS.includes(p)) : parsed.onboarded === true ? [...LEGACY_KNOWN] : []
88
94
  };
89
95
  } catch {
90
96
  return { ...DEFAULTS };
@@ -505,9 +511,105 @@ function mergeTables(list) {
505
511
  };
506
512
  }
507
513
 
514
+ // src/glyphs.ts
515
+ var GLYPHS_UNICODE = {
516
+ spark: ["\u2581", "\u2582", "\u2583", "\u2584", "\u2585", "\u2586", "\u2587", "\u2588"],
517
+ barFull: "\u2501",
518
+ barEmpty: "\u2500",
519
+ rule: "\u2500",
520
+ spinner: ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"],
521
+ dot: "\u25CF",
522
+ dotSel: "\u25C9",
523
+ radioOff: "\u25CB",
524
+ dotAll: "\u2726",
525
+ caretR: "\u25B8",
526
+ caretL: "\u25C2",
527
+ play: "\u25B6",
528
+ arrowU: "\u2191",
529
+ arrowD: "\u2193",
530
+ arrowL: "\u2190",
531
+ arrowR: "\u2192",
532
+ shift: "\u21E7",
533
+ vbar: "\u258C",
534
+ treeMid: "\u251C\u2500",
535
+ treeEnd: "\u2514\u2500",
536
+ boxMark: "\u2502",
537
+ check: "\u2713",
538
+ warn: "\u26A0",
539
+ ellipsis: "\u2026",
540
+ middot: "\xB7",
541
+ emDash: "\u2014",
542
+ eur: "\u20AC",
543
+ gbp: "\xA3",
544
+ border: "round"
545
+ };
546
+ var GLYPHS_ASCII = {
547
+ spark: [".", ":", "-", "=", "+", "*", "#", "@"],
548
+ barFull: "#",
549
+ barEmpty: "-",
550
+ rule: "-",
551
+ spinner: ["|", "/", "-", "\\"],
552
+ dot: "*",
553
+ dotSel: "*",
554
+ radioOff: "o",
555
+ dotAll: "+",
556
+ caretR: ">",
557
+ caretL: "<",
558
+ play: ">",
559
+ arrowU: "^",
560
+ arrowD: "v",
561
+ arrowL: "<",
562
+ arrowR: ">",
563
+ shift: "^",
564
+ vbar: "|",
565
+ treeMid: "+-",
566
+ treeEnd: "`-",
567
+ boxMark: "|",
568
+ check: "x",
569
+ warn: "!",
570
+ ellipsis: "...",
571
+ middot: "-",
572
+ emDash: "-",
573
+ eur: "EUR",
574
+ gbp: "GBP",
575
+ border: "classic"
576
+ };
577
+ function detectUnicode(env, isTTY, platform) {
578
+ if (!isTTY) return false;
579
+ if (env.TERM === "dumb") return false;
580
+ if (platform === "win32") {
581
+ return Boolean(env.WT_SESSION || env.ConEmuANSI === "ON" || env.TERM_PROGRAM === "vscode" || /xterm/i.test(env.TERM ?? ""));
582
+ }
583
+ const loc = env.LC_ALL || env.LC_CTYPE || env.LANG || "";
584
+ if (loc && /\.(iso|latin|ascii|cp\d|koi|gbk|big5)/i.test(loc)) return false;
585
+ if (/^(C|POSIX)$/i.test(loc)) return false;
586
+ return true;
587
+ }
588
+ function resolveGlyphs(opts) {
589
+ let ascii;
590
+ if (opts.flag === "on") ascii = true;
591
+ else if (opts.flag === "off") ascii = false;
592
+ else {
593
+ const e = (opts.env.TOKMON_ASCII ?? "").toLowerCase();
594
+ if (/^(1|true|on|yes)$/.test(e)) ascii = true;
595
+ else if (/^(0|false|off|no)$/.test(e)) ascii = false;
596
+ else if (opts.config === "on") ascii = true;
597
+ else if (opts.config === "off") ascii = false;
598
+ else ascii = !detectUnicode(opts.env, opts.isTTY, opts.platform);
599
+ }
600
+ return ascii ? GLYPHS_ASCII : GLYPHS_UNICODE;
601
+ }
602
+ var active = GLYPHS_UNICODE;
603
+ function setGlyphs(set) {
604
+ active = set;
605
+ }
606
+ function glyphs() {
607
+ return active;
608
+ }
609
+
508
610
  // src/app.tsx
509
- import { useState as useState2, useEffect as useEffect2, useCallback, useRef as useRef2, useMemo } from "react";
510
- import { Box as Box6, Text as Text6, useInput, useStdout, useApp } from "ink";
611
+ import { useState as useState3, useEffect as useEffect3, useCallback, useRef as useRef2, useMemo } from "react";
612
+ import { Box as Box7, Text as Text7, useInput, useStdout, useApp } from "ink";
511
613
  import { useMouse } from "@zenobius/ink-mouse";
512
614
 
513
615
  // src/http.ts
@@ -746,12 +848,26 @@ function resetIn(iso) {
746
848
 
747
849
  // src/providers/claude/billing.ts
748
850
  var execFile = promisify(execFileCb);
851
+ function parseAuth(raw) {
852
+ try {
853
+ const creds = JSON.parse(raw);
854
+ const o = creds?.claudeAiOauth ?? creds;
855
+ const token = o?.accessToken;
856
+ if (typeof token !== "string" || !token) return null;
857
+ return {
858
+ token,
859
+ subscriptionType: typeof o.subscriptionType === "string" ? o.subscriptionType : void 0,
860
+ rateLimitTier: typeof o.rateLimitTier === "string" ? o.rateLimitTier : void 0
861
+ };
862
+ } catch {
863
+ return null;
864
+ }
865
+ }
749
866
  async function readCredentialsFile(homeDir) {
750
867
  for (const dir of claudeConfigDirs(homeDir)) {
751
868
  try {
752
- const creds = JSON.parse(await readFile3(join4(dir, ".credentials.json"), "utf-8"));
753
- const token = creds?.claudeAiOauth?.accessToken ?? creds?.accessToken;
754
- if (token) return token;
869
+ const auth = parseAuth(await readFile3(join4(dir, ".credentials.json"), "utf-8"));
870
+ if (auth) return auth;
755
871
  } catch {
756
872
  }
757
873
  }
@@ -765,38 +881,45 @@ async function readMacKeychain() {
765
881
  "Claude Code-credentials",
766
882
  "-w"
767
883
  ], { timeout: 5e3 });
768
- const creds = JSON.parse(stdout.trim());
769
- return creds?.claudeAiOauth?.accessToken ?? creds?.accessToken ?? null;
884
+ return parseAuth(stdout.trim());
770
885
  } catch {
771
886
  return null;
772
887
  }
773
888
  }
774
- async function getAccessToken(homeDir) {
889
+ async function getAuth(homeDir) {
775
890
  const isDefault = !homeDir || homeDir === homedir3();
776
891
  if (isDefault && process.platform === "darwin") {
777
- const token = await readMacKeychain();
778
- if (token) return token;
892
+ const auth = await readMacKeychain();
893
+ if (auth) return auth;
779
894
  }
780
895
  return readCredentialsFile(homeDir);
781
896
  }
897
+ function planLabel(auth) {
898
+ const sub = auth.subscriptionType;
899
+ if (!sub) return null;
900
+ const base = sub.charAt(0).toUpperCase() + sub.slice(1);
901
+ const tier = (auth.rateLimitTier ?? "").match(/(\d+)x/);
902
+ return tier ? `${base} ${tier[1]}x` : base;
903
+ }
782
904
  var pct = (used, resets, primary) => ({ label: "", used, limit: 100, format: { kind: "percent" }, resetsAt: resets ?? null, primary });
783
905
  async function claudeBilling(account) {
784
- const token = await getAccessToken(account.homeDir);
785
- if (!token) return { plan: null, metrics: [], error: "No OAuth token \u2014 run claude and log in" };
906
+ const auth = await getAuth(account.homeDir);
907
+ if (!auth) return { plan: null, metrics: [], error: "No OAuth token \u2014 run claude and log in" };
908
+ const plan = planLabel(auth);
786
909
  try {
787
910
  const res = await fetch("https://api.anthropic.com/api/oauth/usage", {
788
911
  headers: {
789
- "Authorization": `Bearer ${token}`,
912
+ "Authorization": `Bearer ${auth.token}`,
790
913
  "anthropic-beta": "oauth-2025-04-20",
791
914
  "User-Agent": "tokmon"
792
915
  },
793
916
  signal: AbortSignal.timeout(1e4)
794
917
  });
795
- if (res.status === 429) return { plan: null, metrics: [], error: "Rate limited \u2014 retrying next poll" };
796
- if (res.status === 401) return { plan: null, metrics: [], error: "Token expired \u2014 restart Claude Code" };
797
- if (!res.ok) return { plan: null, metrics: [], error: `API ${res.status}` };
918
+ if (res.status === 429) return { plan, metrics: [], error: "Rate limited \u2014 retrying next poll" };
919
+ if (res.status === 401) return { plan, metrics: [], error: "Token expired \u2014 restart Claude Code" };
920
+ if (!res.ok) return { plan, metrics: [], error: `API ${res.status}` };
798
921
  const data = await readJson(res);
799
- if (!data) return { plan: null, metrics: [], error: "Unexpected API response" };
922
+ if (!data) return { plan, metrics: [], error: "Unexpected API response" };
800
923
  const metrics = [];
801
924
  if (data.five_hour) {
802
925
  metrics.push({ ...pct(data.five_hour.utilization, resetIn(data.five_hour.resets_at), true), label: "5h" });
@@ -815,9 +938,9 @@ async function claudeBilling(account) {
815
938
  format: { kind: "dollars", currency: data.extra_usage.currency ?? "USD" }
816
939
  });
817
940
  }
818
- return { plan: null, metrics, error: null };
941
+ return { plan, metrics, error: null };
819
942
  } catch {
820
- return { plan: null, metrics: [], error: "Network error" };
943
+ return { plan, metrics: [], error: "Network error" };
821
944
  }
822
945
  }
823
946
 
@@ -1017,7 +1140,7 @@ async function readKeychainAuth() {
1017
1140
  return null;
1018
1141
  }
1019
1142
  }
1020
- async function getAuth(homeDir) {
1143
+ async function getAuth2(homeDir) {
1021
1144
  for (const home of codexHomes(homeDir)) {
1022
1145
  const auth = await readAuthFile(home);
1023
1146
  if (auth) return auth;
@@ -1025,7 +1148,7 @@ async function getAuth(homeDir) {
1025
1148
  if (process.platform === "darwin") return readKeychainAuth();
1026
1149
  return null;
1027
1150
  }
1028
- function planLabel(planType) {
1151
+ function planLabel2(planType) {
1029
1152
  if (typeof planType !== "string" || !planType.trim()) return null;
1030
1153
  const p = planType.trim().toLowerCase();
1031
1154
  if (p === "prolite") return "Pro 5x";
@@ -1077,7 +1200,7 @@ async function liveBilling(auth) {
1077
1200
  metrics.push({ label: "Credits", used: balance * CREDIT_USD_RATE, limit: null, format: { kind: "dollars" } });
1078
1201
  }
1079
1202
  if (metrics.length === 0) return null;
1080
- return { plan: planLabel(data.plan_type), metrics, error: null };
1203
+ return { plan: planLabel2(data.plan_type), metrics, error: null };
1081
1204
  } catch {
1082
1205
  return null;
1083
1206
  }
@@ -1134,10 +1257,10 @@ async function snapshotBilling(homeDir) {
1134
1257
  metrics.push({ label: "Credits", used: balance * CREDIT_USD_RATE, limit: null, format: { kind: "dollars" } });
1135
1258
  }
1136
1259
  if (metrics.length === 0) return null;
1137
- return { plan: planLabel(last.plan_type), metrics, error: null };
1260
+ return { plan: planLabel2(last.plan_type), metrics, error: null };
1138
1261
  }
1139
1262
  async function codexBilling(account) {
1140
- const auth = await getAuth(account.homeDir);
1263
+ const auth = await getAuth2(account.homeDir);
1141
1264
  if (auth) {
1142
1265
  const live = await liveBilling(auth);
1143
1266
  if (live) return live;
@@ -1177,29 +1300,75 @@ import { homedir as homedir5 } from "os";
1177
1300
  import { execFile as execFileCb3 } from "child_process";
1178
1301
  import { promisify as promisify3 } from "util";
1179
1302
  var execFile3 = promisify3(execFileCb3);
1180
- async function runSqlite(db, sql, extraArgs = []) {
1303
+ var nativeDb;
1304
+ async function getNativeDb() {
1305
+ if (nativeDb !== void 0) return nativeDb;
1306
+ try {
1307
+ nativeDb = (await import("sqlite")).DatabaseSync;
1308
+ } catch {
1309
+ nativeDb = null;
1310
+ }
1311
+ return nativeDb;
1312
+ }
1313
+ function classify(msg) {
1314
+ if (/unable to open|no such file|cannot open|ENOENT/i.test(msg)) return "missing";
1315
+ if (/database is (locked|busy)|readonly/i.test(msg)) return "locked";
1316
+ if (/no such (function|table|column)|unknown option/i.test(msg)) return "old";
1317
+ return "error";
1318
+ }
1319
+ async function runSqlite(db, sql, params = []) {
1320
+ const DB = await getNativeDb();
1321
+ if (DB) {
1322
+ let handle;
1323
+ try {
1324
+ handle = new DB(db, { readOnly: true, timeout: 1500 });
1325
+ const rows = handle.prepare(sql).all(...params);
1326
+ return { status: "ok", rows };
1327
+ } catch {
1328
+ } finally {
1329
+ try {
1330
+ handle?.close();
1331
+ } catch {
1332
+ }
1333
+ }
1334
+ }
1335
+ return runSqliteCli(db, sql, params);
1336
+ }
1337
+ function inlineParams(sql, params) {
1338
+ let i = 0;
1339
+ return sql.replace(/\?/g, () => {
1340
+ const p = params[i++];
1341
+ return typeof p === "number" ? String(p) : `'${String(p).replace(/'/g, "''")}'`;
1342
+ });
1343
+ }
1344
+ async function runSqliteCli(db, sql, params) {
1181
1345
  try {
1182
1346
  const { stdout } = await execFile3(
1183
1347
  "sqlite3",
1184
- ["-readonly", "-cmd", "PRAGMA busy_timeout=1500;", ...extraArgs, db, sql],
1348
+ // -json yields row objects; `.timeout` sets the busy handler silently
1349
+ // (a `-cmd 'PRAGMA busy_timeout=…'` would print its result row first).
1350
+ ["-readonly", "-json", "-cmd", ".timeout 1500", db, inlineParams(sql, params)],
1185
1351
  { timeout: 1e4, maxBuffer: 8 << 20 }
1186
1352
  );
1187
- return { status: "ok", stdout };
1353
+ const text = stdout.trim();
1354
+ if (!text) return { status: "ok", rows: [] };
1355
+ try {
1356
+ return { status: "ok", rows: JSON.parse(text) };
1357
+ } catch {
1358
+ return { status: "error", rows: [] };
1359
+ }
1188
1360
  } catch (e) {
1189
1361
  const err = e;
1190
- if (err?.code === "ENOENT") return { status: "missing", stdout: "" };
1191
- const msg = String(err?.stderr ?? err?.message ?? "");
1192
- if (/database is (locked|busy)/i.test(msg)) return { status: "locked", stdout: "" };
1193
- if (/no such function|unknown option|no such table/i.test(msg)) return { status: "old", stdout: "" };
1194
- return { status: "error", stdout: "" };
1362
+ if (err?.code === "ENOENT") return { status: "missing", rows: [] };
1363
+ return { status: classify(String(err?.stderr ?? err?.message ?? "")), rows: [] };
1195
1364
  }
1196
1365
  }
1197
1366
  function sqliteStatusMessage(status) {
1198
1367
  switch (status) {
1199
1368
  case "missing":
1200
- return "sqlite3 CLI not found \u2014 install it to read Cursor";
1369
+ return "Cursor data not found \u2014 open Cursor";
1201
1370
  case "old":
1202
- return "sqlite3 too old (needs JSON support)";
1371
+ return "Cursor DB unreadable";
1203
1372
  case "locked":
1204
1373
  return "Cursor DB busy \u2014 retrying next poll";
1205
1374
  default:
@@ -1222,17 +1391,14 @@ async function cursorActivity(homeDir) {
1222
1391
  const now = Date.now();
1223
1392
  const res = await runSqlite(
1224
1393
  db,
1225
- `SELECT date(createdAt/1000,'unixepoch','localtime') d, count(*) c FROM ai_code_hashes WHERE source!='human' AND createdAt >= ${Math.floor(now - 30 * DAY_MS2)} GROUP BY d;`
1394
+ `SELECT date(createdAt/1000,'unixepoch','localtime') AS d, count(*) AS c FROM ai_code_hashes WHERE source!='human' AND createdAt >= ${Math.floor(now - 30 * DAY_MS2)} GROUP BY d;`
1226
1395
  );
1227
1396
  if (res.status !== "ok") return null;
1228
- const daily = res.stdout.trim();
1229
1397
  const byDay = /* @__PURE__ */ new Map();
1230
1398
  let month = 0;
1231
- for (const line of daily.split("\n")) {
1232
- if (!line) continue;
1233
- const [d, c] = line.split("|");
1234
- const n = Number(c) || 0;
1235
- byDay.set(d, n);
1399
+ for (const row of res.rows) {
1400
+ const n = Number(row.c) || 0;
1401
+ byDay.set(String(row.d), n);
1236
1402
  month += n;
1237
1403
  }
1238
1404
  const series = [];
@@ -1247,17 +1413,15 @@ async function cursorActivity(homeDir) {
1247
1413
  // src/providers/cursor/composer.ts
1248
1414
  async function cursorModelSpend(homeDir) {
1249
1415
  const db = cursorStateDb(homeDir);
1250
- const sql = "SELECT mk.key, sum(json_extract(mk.value,'$.costInCents')), sum(json_extract(mk.value,'$.amount')) FROM cursorDiskKV c, json_each(c.value,'$.usageData') mk WHERE c.key LIKE 'composerData:%' AND json_valid(c.value) AND json_type(c.value,'$.usageData')='object' GROUP BY mk.key ORDER BY 2 DESC;";
1251
- const res = await runSqlite(db, sql, ["-separator", " "]);
1416
+ const sql = "SELECT mk.key AS name, sum(json_extract(mk.value,'$.costInCents')) AS cents, sum(json_extract(mk.value,'$.amount')) AS amt FROM cursorDiskKV c, json_each(c.value,'$.usageData') mk WHERE c.key LIKE 'composerData:%' AND json_valid(c.value) AND json_type(c.value,'$.usageData')='object' GROUP BY mk.key ORDER BY cents DESC;";
1417
+ const res = await runSqlite(db, sql);
1252
1418
  if (res.status !== "ok") return null;
1253
1419
  const models = [];
1254
1420
  let total = 0;
1255
- for (const line of res.stdout.trim().split("\n")) {
1256
- if (!line) continue;
1257
- const [name, cents, amt] = line.split(" ");
1258
- const usd = (Number(cents) || 0) / 100;
1421
+ for (const row of res.rows) {
1422
+ const usd = (Number(row.cents) || 0) / 100;
1259
1423
  if (usd <= 0) continue;
1260
- models.push({ name, usd, requests: Number(amt) || 0 });
1424
+ models.push({ name: String(row.name ?? ""), usd, requests: Number(row.amt) || 0 });
1261
1425
  total += usd;
1262
1426
  }
1263
1427
  if (total <= 0) return null;
@@ -1290,9 +1454,9 @@ async function detectCursor(homeDir) {
1290
1454
  }
1291
1455
  }
1292
1456
  async function readState(db, key) {
1293
- const safe = key.replace(/'/g, "''");
1294
- const r = await runSqlite(db, `SELECT value FROM ItemTable WHERE key='${safe}' LIMIT 1;`);
1295
- return { value: r.status === "ok" ? r.stdout.trim() || null : null, status: r.status };
1457
+ const r = await runSqlite(db, "SELECT value FROM ItemTable WHERE key=? LIMIT 1;", [key]);
1458
+ const raw = r.status === "ok" ? r.rows[0]?.value : void 0;
1459
+ return { value: typeof raw === "string" && raw.trim() ? raw.trim() : null, status: r.status };
1296
1460
  }
1297
1461
  async function connectPost(url, token) {
1298
1462
  try {
@@ -1414,28 +1578,930 @@ var cursorProvider = {
1414
1578
  fetchBilling: (account) => cursorBilling(account)
1415
1579
  };
1416
1580
 
1581
+ // src/providers/pi/usage.ts
1582
+ import { readdir as readdir4, stat as fsStat4, access as access4 } from "fs/promises";
1583
+ import { createReadStream as createReadStream4 } from "fs";
1584
+ import { createInterface as createInterface4 } from "readline";
1585
+ import { join as join9 } from "path";
1586
+ import { homedir as homedir7 } from "os";
1587
+ function piSessionsDir(homeDir) {
1588
+ return join9(homeDir ?? homedir7(), ".pi", "agent", "sessions");
1589
+ }
1590
+ async function detectPi(homeDir) {
1591
+ try {
1592
+ await access4(piSessionsDir(homeDir));
1593
+ return true;
1594
+ } catch {
1595
+ return false;
1596
+ }
1597
+ }
1598
+ function pos(v) {
1599
+ return typeof v === "number" && Number.isFinite(v) && v > 0 ? v : 0;
1600
+ }
1601
+ async function parseFile3(path) {
1602
+ const entries = [];
1603
+ const rl = createInterface4({ input: createReadStream4(path), crlfDelay: Infinity });
1604
+ for await (const rawLine of rl) {
1605
+ if (!rawLine.includes('"usage"')) continue;
1606
+ try {
1607
+ const line = rawLine.charCodeAt(0) === 65279 ? rawLine.slice(1) : rawLine;
1608
+ const obj = JSON.parse(line);
1609
+ if (obj?.type !== "message") continue;
1610
+ const msg = obj.message;
1611
+ if (msg?.role !== "assistant" || !msg?.usage) continue;
1612
+ const u = msg.usage;
1613
+ const ts = new Date(obj.timestamp ?? msg.timestamp ?? 0).getTime();
1614
+ if (!Number.isFinite(ts)) continue;
1615
+ const input = safeNum(u.input);
1616
+ const output = safeNum(u.output);
1617
+ const cacheRead = safeNum(u.cacheRead);
1618
+ const cacheCreate = safeNum(u.cacheWrite);
1619
+ if (input + output + cacheRead + cacheCreate === 0) continue;
1620
+ const c = u.cost ?? {};
1621
+ const costInput = pos(c.input);
1622
+ const cacheSavings = input > 0 && cacheRead > 0 ? Math.max(0, cacheRead * (costInput / input) - pos(c.cacheRead)) : 0;
1623
+ entries.push({
1624
+ ts,
1625
+ model: typeof msg.model === "string" && msg.model ? msg.model : "unknown",
1626
+ cost: pos(c.total),
1627
+ input,
1628
+ output,
1629
+ cacheCreate,
1630
+ cacheRead,
1631
+ cacheSavings
1632
+ });
1633
+ } catch {
1634
+ }
1635
+ }
1636
+ return entries;
1637
+ }
1638
+ async function loadEntries3(since, homeDir) {
1639
+ const dir = piSessionsDir(homeDir);
1640
+ const files = [];
1641
+ const seenIno = /* @__PURE__ */ new Set();
1642
+ let listing;
1643
+ try {
1644
+ listing = await readdir4(dir, { recursive: true });
1645
+ } catch {
1646
+ return [];
1647
+ }
1648
+ for (const f of listing) {
1649
+ if (!f.endsWith(".jsonl")) continue;
1650
+ const path = join9(dir, f);
1651
+ try {
1652
+ const s = await fsStat4(path);
1653
+ if (s.mtimeMs < since) continue;
1654
+ if (s.ino && process.platform !== "win32") {
1655
+ const idn = `${s.dev}:${s.ino}`;
1656
+ if (seenIno.has(idn)) continue;
1657
+ seenIno.add(idn);
1658
+ }
1659
+ files.push({ path, mtimeMs: s.mtimeMs, size: s.size });
1660
+ } catch {
1661
+ }
1662
+ }
1663
+ return loadCachedEntries(files, parseFile3, since);
1664
+ }
1665
+ async function piDashboard(tz, homeDir) {
1666
+ const now = Date.now();
1667
+ const since = Math.min(startOfMonth(now, tz), startOfWeek(now, tz), now - SPARK_DAYS * 864e5);
1668
+ return summarize(await loadEntries3(since, homeDir), tz);
1669
+ }
1670
+ async function piTable(tz, homeDir) {
1671
+ return tabulate(await loadEntries3(monthsAgoStart(Date.now(), 6, tz), homeDir), tz);
1672
+ }
1673
+
1674
+ // src/providers/pi/index.ts
1675
+ var piProvider = {
1676
+ id: "pi",
1677
+ name: "Pi",
1678
+ color: "blue",
1679
+ hasUsage: true,
1680
+ hasBilling: false,
1681
+ detect: (homeDir) => detectPi(homeDir),
1682
+ fetchSummary: (account, tz) => piDashboard(tz, account.homeDir),
1683
+ fetchTable: (account, tz) => piTable(tz, account.homeDir)
1684
+ };
1685
+
1686
+ // src/providers/opencode/usage.ts
1687
+ import { access as access5 } from "fs/promises";
1688
+ import { join as join10 } from "path";
1689
+ import { homedir as homedir8 } from "os";
1690
+ function opencodeDbPaths(homeDir) {
1691
+ const base = homeDir ?? homedir8();
1692
+ const paths = [];
1693
+ if (!homeDir && process.env.XDG_DATA_HOME) paths.push(join10(process.env.XDG_DATA_HOME, "opencode", "opencode.db"));
1694
+ paths.push(join10(base, ".local", "share", "opencode", "opencode.db"));
1695
+ if (process.platform === "darwin") paths.push(join10(base, "Library", "Application Support", "opencode", "opencode.db"));
1696
+ if (process.platform === "win32") {
1697
+ const lad = homeDir ? join10(homeDir, "AppData", "Local") : process.env.LOCALAPPDATA;
1698
+ if (lad) paths.push(join10(lad, "opencode", "opencode.db"));
1699
+ }
1700
+ return [...new Set(paths)];
1701
+ }
1702
+ async function findDb(homeDir) {
1703
+ for (const p of opencodeDbPaths(homeDir)) {
1704
+ try {
1705
+ await access5(p);
1706
+ return p;
1707
+ } catch {
1708
+ }
1709
+ }
1710
+ return null;
1711
+ }
1712
+ async function detectOpencode(homeDir) {
1713
+ return await findDb(homeDir) !== null;
1714
+ }
1715
+ var pos2 = (v) => typeof v === "number" && Number.isFinite(v) && v > 0 ? v : 0;
1716
+ async function loadEntries4(since, homeDir) {
1717
+ const db = await findDb(homeDir);
1718
+ if (!db) return [];
1719
+ const sql = "SELECT time_created AS ts, json_extract(data,'$.modelID') AS model, json_extract(data,'$.cost') AS cost, json_extract(data,'$.tokens.input') AS input, json_extract(data,'$.tokens.output') AS output, json_extract(data,'$.tokens.reasoning') AS reasoning, json_extract(data,'$.tokens.cache.read') AS cacheRead, json_extract(data,'$.tokens.cache.write') AS cacheWrite FROM message WHERE json_valid(data) AND json_extract(data,'$.role')='assistant' AND json_type(data,'$.tokens')='object' AND time_created >= ?;";
1720
+ const res = await runSqlite(db, sql, [Math.floor(since)]);
1721
+ if (res.status !== "ok") return [];
1722
+ const entries = [];
1723
+ for (const row of res.rows) {
1724
+ const ts = pos2(row.ts);
1725
+ if (!ts) continue;
1726
+ const input = pos2(row.input);
1727
+ const output = pos2(row.output);
1728
+ const cacheRead = pos2(row.cacheRead);
1729
+ const cacheCreate = pos2(row.cacheWrite);
1730
+ if (input + output + cacheRead + cacheCreate === 0) continue;
1731
+ entries.push({
1732
+ ts,
1733
+ model: typeof row.model === "string" && row.model ? row.model : "unknown",
1734
+ cost: pos2(row.cost),
1735
+ input,
1736
+ output,
1737
+ cacheCreate,
1738
+ cacheRead,
1739
+ cacheSavings: 0
1740
+ // opencode stores only a total cost, no per-component split to derive savings
1741
+ });
1742
+ }
1743
+ return entries;
1744
+ }
1745
+ async function opencodeDashboard(tz, homeDir) {
1746
+ const now = Date.now();
1747
+ const since = Math.min(startOfMonth(now, tz), startOfWeek(now, tz), now - SPARK_DAYS * 864e5);
1748
+ return summarize(await loadEntries4(since, homeDir), tz);
1749
+ }
1750
+ async function opencodeTable(tz, homeDir) {
1751
+ return tabulate(await loadEntries4(monthsAgoStart(Date.now(), 6, tz), homeDir), tz);
1752
+ }
1753
+
1754
+ // src/providers/opencode/index.ts
1755
+ var opencodeProvider = {
1756
+ id: "opencode",
1757
+ name: "opencode",
1758
+ color: "yellow",
1759
+ hasUsage: true,
1760
+ hasBilling: false,
1761
+ detect: (homeDir) => detectOpencode(homeDir),
1762
+ fetchSummary: (account, tz) => opencodeDashboard(tz, account.homeDir),
1763
+ fetchTable: (account, tz) => opencodeTable(tz, account.homeDir)
1764
+ };
1765
+
1766
+ // src/providers/copilot/billing.ts
1767
+ import { execFile as execFileCb4 } from "child_process";
1768
+ import { access as access6, readFile as readFile5, readdir as readdir5 } from "fs/promises";
1769
+ import { join as join11 } from "path";
1770
+ import { homedir as homedir9 } from "os";
1771
+ import { promisify as promisify4 } from "util";
1772
+ var execFile4 = promisify4(execFileCb4);
1773
+ var USAGE_URL3 = "https://api.github.com/copilot_internal/user";
1774
+ var GH_KEYCHAIN_SERVICE = "gh:github.com";
1775
+ var GO_KEYRING_PREFIX = "go-keyring-base64:";
1776
+ function ghConfigDir(homeDir) {
1777
+ if (!homeDir) {
1778
+ const explicit = process.env.GH_CONFIG_DIR;
1779
+ if (explicit && explicit.trim()) return explicit.trim();
1780
+ if (process.platform === "win32") {
1781
+ return join11(envDir("APPDATA") ?? join11(homedir9(), "AppData", "Roaming"), "GitHub CLI");
1782
+ }
1783
+ const xdg = envDir("XDG_CONFIG_HOME");
1784
+ return xdg ? join11(xdg, "gh") : join11(homedir9(), ".config", "gh");
1785
+ }
1786
+ return process.platform === "win32" ? join11(homeDir, "AppData", "Roaming", "GitHub CLI") : join11(homeDir, ".config", "gh");
1787
+ }
1788
+ function ghHostsPath(homeDir) {
1789
+ return join11(ghConfigDir(homeDir), "hosts.yml");
1790
+ }
1791
+ async function detectCopilot(homeDir) {
1792
+ try {
1793
+ await access6(ghHostsPath(homeDir));
1794
+ return true;
1795
+ } catch {
1796
+ }
1797
+ try {
1798
+ await execFile4("gh", ["--version"], { timeout: 3e3 });
1799
+ return true;
1800
+ } catch {
1801
+ return false;
1802
+ }
1803
+ }
1804
+ function unquoteYamlValue(value) {
1805
+ const trimmed = value.trim();
1806
+ if (trimmed.startsWith('"') && trimmed.endsWith('"') || trimmed.startsWith("'") && trimmed.endsWith("'")) {
1807
+ return trimmed.slice(1, -1);
1808
+ }
1809
+ return trimmed;
1810
+ }
1811
+ function tokenFromHostsYaml(raw) {
1812
+ const lines = raw.split(/\r?\n/);
1813
+ let inGithub = false;
1814
+ let githubIndent = -1;
1815
+ for (const line of lines) {
1816
+ const match = line.match(/^(\s*)([^:#][^:]*):\s*(.*)$/);
1817
+ if (!match) continue;
1818
+ const indent = match[1].length;
1819
+ const key = match[2].trim();
1820
+ const value = match[3].trim();
1821
+ if (indent === 0) {
1822
+ inGithub = key === "github.com";
1823
+ githubIndent = inGithub ? indent : -1;
1824
+ continue;
1825
+ }
1826
+ if (inGithub && indent > githubIndent && key === "oauth_token" && value) {
1827
+ return unquoteYamlValue(value);
1828
+ }
1829
+ }
1830
+ return null;
1831
+ }
1832
+ async function loadTokenFromHosts(homeDir) {
1833
+ try {
1834
+ const token = tokenFromHostsYaml(await readFile5(ghHostsPath(homeDir), "utf-8"));
1835
+ return token ? { token, source: "gh-hosts" } : null;
1836
+ } catch {
1837
+ return null;
1838
+ }
1839
+ }
1840
+ async function readMacKeychainService(service) {
1841
+ if (process.platform !== "darwin") return null;
1842
+ try {
1843
+ const { stdout } = await execFile4("security", [
1844
+ "find-generic-password",
1845
+ "-s",
1846
+ service,
1847
+ "-w"
1848
+ ], { timeout: 5e3 });
1849
+ const raw = stdout.trim();
1850
+ if (!raw) return null;
1851
+ if (raw.startsWith(GO_KEYRING_PREFIX)) {
1852
+ return Buffer.from(raw.slice(GO_KEYRING_PREFIX.length), "base64").toString("utf-8");
1853
+ }
1854
+ return raw;
1855
+ } catch {
1856
+ return null;
1857
+ }
1858
+ }
1859
+ async function loadTokenFromGhKeychain() {
1860
+ const token = await readMacKeychainService(GH_KEYCHAIN_SERVICE);
1861
+ return token ? { token, source: "gh-keychain" } : null;
1862
+ }
1863
+ function vscodeUserDir(homeDir) {
1864
+ const home = homeDir ?? homedir9();
1865
+ if (process.platform === "darwin") return join11(home, "Library", "Application Support", "Code", "User");
1866
+ if (process.platform === "win32") return join11(home, "AppData", "Roaming", "Code", "User");
1867
+ return join11(home, ".config", "Code", "User");
1868
+ }
1869
+ function tokenFromText(raw) {
1870
+ const patterns = [
1871
+ /github\.com[^A-Za-z0-9_]+oauth_token[^A-Za-z0-9_]+([A-Za-z0-9_]{20,})/i,
1872
+ /github\.com[^A-Za-z0-9_]+(gh[opusr]_[A-Za-z0-9_]{20,})/i,
1873
+ /\b(gh[opusr]_[A-Za-z0-9_]{20,})\b/
1874
+ ];
1875
+ for (const pattern of patterns) {
1876
+ const token = raw.match(pattern)?.[1];
1877
+ if (token) return token;
1878
+ }
1879
+ return null;
1880
+ }
1881
+ async function loadTokenFromVsCode(homeDir) {
1882
+ const userDir = vscodeUserDir(homeDir);
1883
+ const candidates = [
1884
+ join11(userDir, "globalStorage", "github.copilot-chat", "auth.json"),
1885
+ join11(userDir, "globalStorage", "github.copilot", "auth.json"),
1886
+ join11(userDir, "globalStorage", "state.vscdb")
1887
+ ];
1888
+ try {
1889
+ for (const dirent of await readdir5(join11(userDir, "globalStorage"), { withFileTypes: true })) {
1890
+ if (dirent.isDirectory() && dirent.name.toLowerCase().includes("github")) {
1891
+ candidates.push(join11(userDir, "globalStorage", dirent.name, "auth.json"));
1892
+ }
1893
+ }
1894
+ } catch {
1895
+ }
1896
+ for (const path of candidates) {
1897
+ try {
1898
+ const token = tokenFromText(await readFile5(path, "utf-8"));
1899
+ if (token) return { token, source: "vscode" };
1900
+ } catch {
1901
+ }
1902
+ }
1903
+ return null;
1904
+ }
1905
+ async function loadToken(homeDir) {
1906
+ return await loadTokenFromHosts(homeDir) || await loadTokenFromGhKeychain() || await loadTokenFromVsCode(homeDir);
1907
+ }
1908
+ function redactToken(token) {
1909
+ return token.length <= 4 ? "****" : `****${token.slice(-4)}`;
1910
+ }
1911
+ function resetDate(value) {
1912
+ return typeof value === "string" && value.trim() ? resetIn(value) : null;
1913
+ }
1914
+ function percentMetric2(label, snapshot, reset, primary) {
1915
+ if (!snapshot || typeof snapshot.percent_remaining !== "number") return null;
1916
+ const used = Math.min(100, Math.max(0, 100 - snapshot.percent_remaining));
1917
+ return { label, used, limit: 100, format: { kind: "percent" }, resetsAt: reset, primary };
1918
+ }
1919
+ function countMetric(label, remaining, total, reset) {
1920
+ if (typeof remaining !== "number" || typeof total !== "number" || total <= 0) return null;
1921
+ return {
1922
+ label,
1923
+ used: Math.max(0, total - remaining),
1924
+ limit: total,
1925
+ format: { kind: "count" },
1926
+ resetsAt: reset
1927
+ };
1928
+ }
1929
+ async function fetchUsage(token) {
1930
+ try {
1931
+ const res = await fetch(USAGE_URL3, {
1932
+ headers: {
1933
+ "Authorization": `token ${token}`,
1934
+ "Accept": "application/json",
1935
+ "Editor-Version": "vscode/1.96.2",
1936
+ "Editor-Plugin-Version": "copilot-chat/0.26.7",
1937
+ "User-Agent": "GitHubCopilotChat/0.26.7",
1938
+ "X-Github-Api-Version": "2025-04-01"
1939
+ },
1940
+ signal: AbortSignal.timeout(1e4)
1941
+ });
1942
+ if (!res.ok) return { data: null, status: res.status };
1943
+ return { data: await readJson(res), status: res.status };
1944
+ } catch {
1945
+ return { data: null, status: null };
1946
+ }
1947
+ }
1948
+ async function copilotBilling(account) {
1949
+ const cred = await loadToken(account.homeDir);
1950
+ if (!cred) return { plan: null, metrics: [], error: "Not logged in \u2014 run gh auth login" };
1951
+ const { data, status } = await fetchUsage(cred.token);
1952
+ if (!data) {
1953
+ if (status === 401 || status === 403) {
1954
+ return { plan: null, metrics: [], error: `Token invalid (${redactToken(cred.token)}) \u2014 run gh auth login` };
1955
+ }
1956
+ if (status) return { plan: null, metrics: [], error: `Copilot API ${status}` };
1957
+ return { plan: null, metrics: [], error: "Network error" };
1958
+ }
1959
+ const plan = typeof data.copilot_plan === "string" && data.copilot_plan.trim() ? data.copilot_plan : null;
1960
+ const metrics = [];
1961
+ const quotaReset = resetDate(data.quota_reset_date);
1962
+ const snapshots = data.quota_snapshots;
1963
+ const premium = percentMetric2("Premium", snapshots?.premium_interactions, quotaReset, true);
1964
+ if (premium) metrics.push(premium);
1965
+ const chat = percentMetric2("Chat", snapshots?.chat, quotaReset);
1966
+ if (chat) metrics.push(chat);
1967
+ if (data.limited_user_quotas && data.monthly_quotas) {
1968
+ const reset = resetDate(data.limited_user_reset_date);
1969
+ const limitedChat = countMetric("Chat", data.limited_user_quotas.chat, data.monthly_quotas.chat, reset);
1970
+ if (limitedChat) metrics.push(limitedChat);
1971
+ const completions = countMetric("Completions", data.limited_user_quotas.completions, data.monthly_quotas.completions, reset);
1972
+ if (completions) metrics.push(completions);
1973
+ }
1974
+ if (metrics.length === 0) return { plan, metrics: [], error: "No usage data" };
1975
+ return { plan, metrics, error: null };
1976
+ }
1977
+
1978
+ // src/providers/copilot/index.ts
1979
+ var copilotProvider = {
1980
+ id: "copilot",
1981
+ name: "Copilot",
1982
+ color: "white",
1983
+ hasUsage: false,
1984
+ hasBilling: true,
1985
+ detect: (homeDir) => detectCopilot(homeDir),
1986
+ fetchBilling: (account) => copilotBilling(account)
1987
+ };
1988
+
1989
+ // src/providers/antigravity/billing.ts
1990
+ import { access as access7, readdir as readdir7 } from "fs/promises";
1991
+ import { join as join13 } from "path";
1992
+ import { homedir as homedir11 } from "os";
1993
+
1994
+ // src/providers/cloud-code.ts
1995
+ import { readFile as readFile6, readdir as readdir6, realpath, stat } from "fs/promises";
1996
+ import { spawnSync } from "child_process";
1997
+ import { homedir as homedir10 } from "os";
1998
+ import { dirname, join as join12 } from "path";
1999
+ var CLOUD_CODE_URLS = [
2000
+ "https://daily-cloudcode-pa.googleapis.com",
2001
+ "https://cloudcode-pa.googleapis.com"
2002
+ ];
2003
+ var LOAD_CODE_ASSIST_PATH = "/v1internal:loadCodeAssist";
2004
+ var FETCH_MODELS_PATH = "/v1internal:fetchAvailableModels";
2005
+ var RETRIEVE_QUOTA_PATH = "/v1internal:retrieveUserQuota";
2006
+ var GOOGLE_OAUTH_URL = "https://oauth2.googleapis.com/token";
2007
+ var GOOGLE_OAUTH_CLIENT_REGEX = /OAUTH_CLIENT_ID\s*=\s*["']([0-9]{6,}-[a-z0-9]+\.apps\.googleusercontent\.com)["']\s*;?\s*(?:var|const|let)?\s*OAUTH_CLIENT_SECRET\s*=\s*["'](GOCSPX-[A-Za-z0-9_-]+)["']/s;
2008
+ var MAX_BUNDLE_READ = 32 * 1024 * 1024;
2009
+ var cachedClient;
2010
+ var OAUTH_TOKEN_KEY = "antigravityUnifiedStateSync.oauthToken";
2011
+ var OAUTH_TOKEN_SENTINEL = "oauthTokenInfoSentinelKey";
2012
+ var CC_MODEL_BLACKLIST = {
2013
+ MODEL_CHAT_20706: true,
2014
+ MODEL_CHAT_23310: true,
2015
+ MODEL_GOOGLE_GEMINI_2_5_FLASH: true,
2016
+ MODEL_GOOGLE_GEMINI_2_5_FLASH_THINKING: true,
2017
+ MODEL_GOOGLE_GEMINI_2_5_FLASH_LITE: true,
2018
+ MODEL_GOOGLE_GEMINI_2_5_PRO: true,
2019
+ MODEL_PLACEHOLDER_M19: true,
2020
+ MODEL_PLACEHOLDER_M9: true,
2021
+ MODEL_PLACEHOLDER_M12: true
2022
+ };
2023
+ function readVarint(bytes, start) {
2024
+ let value = 0;
2025
+ let shift = 0;
2026
+ let pos3 = start;
2027
+ while (pos3 < bytes.length) {
2028
+ const b = bytes[pos3++];
2029
+ value += (b & 127) * Math.pow(2, shift);
2030
+ if ((b & 128) === 0) return { value, pos: pos3 };
2031
+ shift += 7;
2032
+ }
2033
+ return null;
2034
+ }
2035
+ function readFields(bytes) {
2036
+ const fields = {};
2037
+ let pos3 = 0;
2038
+ while (pos3 < bytes.length) {
2039
+ const tag = readVarint(bytes, pos3);
2040
+ if (!tag) break;
2041
+ pos3 = tag.pos;
2042
+ const fieldNum = Math.floor(tag.value / 8);
2043
+ const wireType = tag.value % 8;
2044
+ if (wireType === 0) {
2045
+ const val = readVarint(bytes, pos3);
2046
+ if (!val) break;
2047
+ fields[fieldNum] = { type: 0, value: val.value };
2048
+ pos3 = val.pos;
2049
+ } else if (wireType === 1) {
2050
+ if (pos3 + 8 > bytes.length) break;
2051
+ pos3 += 8;
2052
+ } else if (wireType === 2) {
2053
+ const len = readVarint(bytes, pos3);
2054
+ if (!len) break;
2055
+ pos3 = len.pos;
2056
+ if (pos3 + len.value > bytes.length) break;
2057
+ fields[fieldNum] = { type: 2, data: bytes.slice(pos3, pos3 + len.value) };
2058
+ pos3 += len.value;
2059
+ } else if (wireType === 5) {
2060
+ if (pos3 + 4 > bytes.length) break;
2061
+ pos3 += 4;
2062
+ } else {
2063
+ break;
2064
+ }
2065
+ }
2066
+ return fields;
2067
+ }
2068
+ function utf8(data) {
2069
+ return Buffer.from(data).toString("utf8");
2070
+ }
2071
+ function decodeBase64(text) {
2072
+ try {
2073
+ return Buffer.from(text, "base64");
2074
+ } catch {
2075
+ return null;
2076
+ }
2077
+ }
2078
+ function unwrapKeyringBase64(raw) {
2079
+ const text = raw.trim();
2080
+ if (!text.startsWith("go-keyring-base64:")) return text;
2081
+ const decoded = decodeBase64(text.slice("go-keyring-base64:".length));
2082
+ return decoded ? utf8(decoded).trim() : text;
2083
+ }
2084
+ function unwrapOAuthSentinel(base64Text) {
2085
+ const outerBytes = decodeBase64(unwrapKeyringBase64(base64Text));
2086
+ if (!outerBytes) return null;
2087
+ const outer = readFields(outerBytes);
2088
+ if (outer[1]?.type !== 2 || !outer[1].data) return null;
2089
+ const wrapper = readFields(outer[1].data);
2090
+ const sentinel = wrapper[1]?.type === 2 && wrapper[1].data ? utf8(wrapper[1].data) : null;
2091
+ const payload = wrapper[2]?.type === 2 ? wrapper[2].data : null;
2092
+ if (sentinel !== OAUTH_TOKEN_SENTINEL || !payload) return null;
2093
+ const payloadFields = readFields(payload);
2094
+ if (payloadFields[1]?.type !== 2 || !payloadFields[1].data) return null;
2095
+ const innerText = utf8(payloadFields[1].data).trim();
2096
+ return innerText ? decodeBase64(innerText) : null;
2097
+ }
2098
+ async function readAntigravityOAuthToken(db) {
2099
+ const r = await runSqlite(db, "SELECT value FROM ItemTable WHERE key=? LIMIT 1;", [OAUTH_TOKEN_KEY]);
2100
+ if (r.status !== "ok") return { token: null, status: r.status };
2101
+ const raw = r.rows[0]?.value;
2102
+ if (typeof raw !== "string" || !raw.trim()) return { token: null, status: "ok" };
2103
+ const inner = unwrapOAuthSentinel(raw);
2104
+ if (!inner) return { token: null, status: "ok" };
2105
+ const fields = readFields(inner);
2106
+ const accessToken = fields[1]?.type === 2 && fields[1].data ? utf8(fields[1].data) : null;
2107
+ const refreshToken = fields[3]?.type === 2 && fields[3].data ? utf8(fields[3].data) : null;
2108
+ let expirySeconds = null;
2109
+ if (fields[4]?.type === 2 && fields[4].data) {
2110
+ const ts = readFields(fields[4].data);
2111
+ expirySeconds = ts[1]?.type === 0 && typeof ts[1].value === "number" ? ts[1].value : null;
2112
+ }
2113
+ if (!accessToken && !refreshToken) return { token: null, status: "ok" };
2114
+ return { token: { accessToken, refreshToken, expirySeconds }, status: "ok" };
2115
+ }
2116
+ function redact(token) {
2117
+ if (!token) return "none";
2118
+ return `...${token.slice(-4)}`;
2119
+ }
2120
+ function geminiBundleCandidates() {
2121
+ const candidates = [];
2122
+ const addBundle = (nodeModulesRoot) => {
2123
+ if (!nodeModulesRoot) return;
2124
+ candidates.push(join12(nodeModulesRoot, "@google", "gemini-cli", "bundle"));
2125
+ };
2126
+ try {
2127
+ const which = spawnSync("command", ["-v", "gemini"], { encoding: "utf8", timeout: 5e3 });
2128
+ const resolved = typeof which.stdout === "string" ? which.stdout.trim().split("\n")[0]?.trim() : "";
2129
+ if (resolved) candidates.push(resolved);
2130
+ } catch {
2131
+ }
2132
+ const home = homedir10();
2133
+ addBundle("/opt/homebrew/lib/node_modules");
2134
+ addBundle("/usr/local/lib/node_modules");
2135
+ addBundle(join12(home, ".local", "share", "node_modules"));
2136
+ addBundle(join12(home, ".bun", "install", "global", "node_modules"));
2137
+ try {
2138
+ const prefix = spawnSync("npm", ["config", "get", "prefix"], { encoding: "utf8", timeout: 5e3 });
2139
+ const root = typeof prefix.stdout === "string" ? prefix.stdout.trim() : "";
2140
+ if (root && root !== "undefined") addBundle(join12(root, "lib", "node_modules"));
2141
+ } catch {
2142
+ }
2143
+ return [...new Set(candidates.filter(Boolean))];
2144
+ }
2145
+ async function resolveBundleDir(candidate) {
2146
+ try {
2147
+ if (candidate.endsWith(`${join12("@google", "gemini-cli", "bundle")}`)) {
2148
+ return candidate;
2149
+ }
2150
+ const real = await realpath(candidate);
2151
+ return dirname(real);
2152
+ } catch {
2153
+ return null;
2154
+ }
2155
+ }
2156
+ async function scanBundleDir(dir) {
2157
+ let entries;
2158
+ try {
2159
+ entries = await readdir6(dir);
2160
+ } catch {
2161
+ return null;
2162
+ }
2163
+ const targets = entries.filter((name) => name === "gemini.js" || name.startsWith("chunk-") && name.endsWith(".js"));
2164
+ for (const name of targets) {
2165
+ const filePath = join12(dir, name);
2166
+ try {
2167
+ const info = await stat(filePath);
2168
+ if (!info.isFile() || info.size > MAX_BUNDLE_READ) continue;
2169
+ const contents = await readFile6(filePath, "utf8");
2170
+ if (!contents.includes("OAUTH_CLIENT_SECRET")) continue;
2171
+ const match = GOOGLE_OAUTH_CLIENT_REGEX.exec(contents);
2172
+ if (match) return { clientId: match[1], clientSecret: match[2] };
2173
+ } catch {
2174
+ }
2175
+ }
2176
+ return null;
2177
+ }
2178
+ async function discoverGoogleOAuthClient() {
2179
+ try {
2180
+ for (const candidate of geminiBundleCandidates()) {
2181
+ const dir = await resolveBundleDir(candidate);
2182
+ if (!dir) continue;
2183
+ const found = await scanBundleDir(dir);
2184
+ if (found) return found;
2185
+ }
2186
+ } catch {
2187
+ }
2188
+ return null;
2189
+ }
2190
+ async function resolveGoogleClient() {
2191
+ const envId = process.env.TOKMON_GOOGLE_CLIENT_ID?.trim();
2192
+ const envSecret = process.env.TOKMON_GOOGLE_CLIENT_SECRET?.trim();
2193
+ if (envId && envSecret) return { clientId: envId, clientSecret: envSecret };
2194
+ if (cachedClient === void 0) cachedClient = await discoverGoogleOAuthClient();
2195
+ return cachedClient;
2196
+ }
2197
+ async function refreshAccessToken(refreshToken) {
2198
+ if (!refreshToken) return null;
2199
+ const client = await resolveGoogleClient();
2200
+ if (!client) return null;
2201
+ const body = new URLSearchParams({
2202
+ client_id: client.clientId,
2203
+ client_secret: client.clientSecret,
2204
+ refresh_token: refreshToken,
2205
+ grant_type: "refresh_token"
2206
+ });
2207
+ try {
2208
+ const res = await fetch(GOOGLE_OAUTH_URL, {
2209
+ method: "POST",
2210
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
2211
+ body,
2212
+ signal: AbortSignal.timeout(15e3)
2213
+ });
2214
+ if (!res.ok) return null;
2215
+ const json = await readJson(res);
2216
+ return typeof json?.access_token === "string" && json.access_token.trim() ? json.access_token.trim() : null;
2217
+ } catch {
2218
+ return null;
2219
+ }
2220
+ }
2221
+ async function requestCloudCodeJson(path, token, body) {
2222
+ for (const base of CLOUD_CODE_URLS) {
2223
+ try {
2224
+ const res = await fetch(`${base}${path}`, {
2225
+ method: "POST",
2226
+ headers: {
2227
+ Accept: "application/json",
2228
+ "Content-Type": "application/json",
2229
+ Authorization: `Bearer ${token}`,
2230
+ "User-Agent": "agy"
2231
+ },
2232
+ body: JSON.stringify(body ?? {}),
2233
+ signal: AbortSignal.timeout(15e3)
2234
+ });
2235
+ if (res.status === 401 || res.status === 403) return { _authFailed: true };
2236
+ if (!res.ok) continue;
2237
+ const json = await readJson(res);
2238
+ if (json && typeof json === "object") return json;
2239
+ } catch {
2240
+ }
2241
+ }
2242
+ return null;
2243
+ }
2244
+ function readPlan(loadData) {
2245
+ const paid = typeof loadData?.paidTier?.name === "string" ? loadData.paidTier.name.trim() : "";
2246
+ if (paid) return paid;
2247
+ const current = typeof loadData?.currentTier?.name === "string" ? loadData.currentTier.name.trim() : "";
2248
+ return current || null;
2249
+ }
2250
+ function parseBuckets(data) {
2251
+ if (!Array.isArray(data?.buckets)) return [];
2252
+ return data.buckets.flatMap((bucket) => {
2253
+ const modelId = typeof bucket?.modelId === "string" ? bucket.modelId.trim() : "";
2254
+ if (!modelId) return [];
2255
+ return [{
2256
+ modelId,
2257
+ remainingFraction: typeof bucket.remainingFraction === "number" ? bucket.remainingFraction : 0,
2258
+ resetTime: typeof bucket.resetTime === "string" ? bucket.resetTime : void 0
2259
+ }];
2260
+ });
2261
+ }
2262
+ function parseModelBuckets(data) {
2263
+ const models = data?.models;
2264
+ if (!models || typeof models !== "object") return [];
2265
+ return Object.keys(models).flatMap((key) => {
2266
+ const model = models[key];
2267
+ if (!model || typeof model !== "object" || model.isInternal) return [];
2268
+ const modelId = typeof model.model === "string" && model.model.trim() ? model.model.trim() : key;
2269
+ if (CC_MODEL_BLACKLIST[modelId]) return [];
2270
+ const displayName = typeof model.displayName === "string" && model.displayName.trim() || typeof model.label === "string" && model.label.trim() || "";
2271
+ if (!displayName) return [];
2272
+ const quotaInfo = model.quotaInfo;
2273
+ return [{
2274
+ modelId: displayName,
2275
+ remainingFraction: typeof quotaInfo?.remainingFraction === "number" ? quotaInfo.remainingFraction : 0,
2276
+ resetTime: typeof quotaInfo?.resetTime === "string" ? quotaInfo.resetTime : void 0
2277
+ }];
2278
+ });
2279
+ }
2280
+ async function fetchWithAccessToken(accessToken) {
2281
+ const loadData = await requestCloudCodeJson(LOAD_CODE_ASSIST_PATH, accessToken, {});
2282
+ if (!loadData) return { ok: false, plan: null, error: "Cloud Code API error" };
2283
+ if ("_authFailed" in loadData) return { ok: false, plan: null, error: "Token expired" };
2284
+ const plan = readPlan(loadData);
2285
+ const project = typeof loadData.cloudaicompanionProject === "string" && loadData.cloudaicompanionProject.trim() ? loadData.cloudaicompanionProject.trim() : null;
2286
+ let quotaData = project ? await requestCloudCodeJson(RETRIEVE_QUOTA_PATH, accessToken, { project }) : null;
2287
+ if (!quotaData || "_authFailed" in quotaData) {
2288
+ quotaData = await requestCloudCodeJson(RETRIEVE_QUOTA_PATH, accessToken, {});
2289
+ }
2290
+ if (!quotaData) return { ok: false, plan, error: "Cloud Code quota unavailable" };
2291
+ if ("_authFailed" in quotaData) return { ok: false, plan, error: "Token expired" };
2292
+ let buckets = parseBuckets(quotaData);
2293
+ if (buckets.length === 0) {
2294
+ const modelData = await requestCloudCodeJson(FETCH_MODELS_PATH, accessToken, {});
2295
+ if (modelData && !("_authFailed" in modelData)) buckets = parseModelBuckets(modelData);
2296
+ if (modelData && "_authFailed" in modelData) return { ok: false, plan, error: "Token expired" };
2297
+ }
2298
+ if (buckets.length === 0) return { ok: false, plan, error: "No quota data" };
2299
+ return { ok: true, plan, buckets };
2300
+ }
2301
+ async function fetchCloudCodeQuota(token, expiredMessage = "Token expired") {
2302
+ const nowSec = Math.floor(Date.now() / 1e3);
2303
+ let accessToken = token.accessToken?.trim() || null;
2304
+ if (accessToken && token.expirySeconds && token.expirySeconds <= nowSec) {
2305
+ accessToken = await refreshAccessToken(token.refreshToken);
2306
+ if (!accessToken) return { ok: false, plan: null, error: expiredMessage };
2307
+ }
2308
+ if (!accessToken) {
2309
+ accessToken = await refreshAccessToken(token.refreshToken);
2310
+ if (!accessToken) return { ok: false, plan: null, error: `Missing credentials (${redact(token.accessToken)})` };
2311
+ }
2312
+ const result = await fetchWithAccessToken(accessToken);
2313
+ if (result.ok || result.error !== "Token expired") return result;
2314
+ const refreshed = await refreshAccessToken(token.refreshToken);
2315
+ if (!refreshed) return { ok: false, plan: result.plan, error: expiredMessage };
2316
+ return fetchWithAccessToken(refreshed);
2317
+ }
2318
+ function normalizeLabel(label) {
2319
+ return label.replace(/\s*\([^)]*\)\s*$/, "").trim();
2320
+ }
2321
+ function poolLabel(label) {
2322
+ const lower = normalizeLabel(label).toLowerCase();
2323
+ if (lower.includes("gemini") && lower.includes("pro")) return "Gemini Pro";
2324
+ if (lower.includes("gemini") && lower.includes("flash")) return "Gemini Flash";
2325
+ return "Claude";
2326
+ }
2327
+ function sortKey(label) {
2328
+ const lower = label.toLowerCase();
2329
+ if (lower.includes("gemini") && lower.includes("pro")) return `0a_${label}`;
2330
+ if (lower.includes("gemini")) return `0b_${label}`;
2331
+ if (lower.includes("claude") && lower.includes("opus")) return `1a_${label}`;
2332
+ if (lower.includes("claude")) return `1b_${label}`;
2333
+ return `2_${label}`;
2334
+ }
2335
+ function cloudCodeBucketsToMetrics(buckets) {
2336
+ const pooled = /* @__PURE__ */ new Map();
2337
+ for (const bucket of buckets) {
2338
+ const label = poolLabel(bucket.modelId);
2339
+ const existing = pooled.get(label);
2340
+ if (!existing || bucket.remainingFraction < existing.remainingFraction) {
2341
+ pooled.set(label, { ...bucket, modelId: label });
2342
+ }
2343
+ }
2344
+ return [...pooled.values()].sort((a, b) => sortKey(a.modelId).localeCompare(sortKey(b.modelId))).map((bucket, i) => {
2345
+ const clamped = Math.max(0, Math.min(1, bucket.remainingFraction));
2346
+ return {
2347
+ label: bucket.modelId,
2348
+ used: Math.round((1 - clamped) * 100),
2349
+ limit: 100,
2350
+ format: { kind: "percent" },
2351
+ resetsAt: bucket.resetTime ? resetIn(bucket.resetTime) : null,
2352
+ primary: i === 0
2353
+ };
2354
+ });
2355
+ }
2356
+ function cloudCodeSqliteError(status) {
2357
+ return status === "ok" ? "Not signed in \u2014 open Antigravity" : sqliteStatusMessage(status).replace(/Cursor/g, "Antigravity");
2358
+ }
2359
+
2360
+ // src/providers/antigravity/billing.ts
2361
+ async function exists(path) {
2362
+ try {
2363
+ await access7(path);
2364
+ return true;
2365
+ } catch {
2366
+ return false;
2367
+ }
2368
+ }
2369
+ async function firstExisting(paths) {
2370
+ for (const path of paths) {
2371
+ if (await exists(path)) return path;
2372
+ }
2373
+ return paths[0];
2374
+ }
2375
+ async function antigravityStateDb(homeDir) {
2376
+ const base = homeDir ?? homedir11();
2377
+ const tail = ["User", "globalStorage", "state.vscdb"];
2378
+ if (process.platform === "darwin") {
2379
+ const support = join13(base, "Library", "Application Support");
2380
+ const exact = [
2381
+ join13(support, "Antigravity IDE", ...tail),
2382
+ join13(support, "Antigravity", ...tail)
2383
+ ];
2384
+ try {
2385
+ const entries = await readdir7(support, { withFileTypes: true });
2386
+ const matches = entries.filter((e) => e.isDirectory() && e.name.includes("Antigravity")).map((e) => join13(support, e.name, ...tail));
2387
+ return firstExisting([...exact, ...matches]);
2388
+ } catch {
2389
+ return firstExisting(exact);
2390
+ }
2391
+ }
2392
+ if (process.platform === "win32") {
2393
+ const roaming = homeDir ? join13(homeDir, "AppData", "Roaming") : envDir("APPDATA") ?? join13(base, "AppData", "Roaming");
2394
+ return firstExisting([
2395
+ join13(roaming, "Antigravity IDE", ...tail),
2396
+ join13(roaming, "Antigravity", ...tail)
2397
+ ]);
2398
+ }
2399
+ const cfg = homeDir ? join13(homeDir, ".config") : envDir("XDG_CONFIG_HOME") ?? join13(base, ".config");
2400
+ return firstExisting([
2401
+ join13(cfg, "Antigravity IDE", ...tail),
2402
+ join13(cfg, "Antigravity", ...tail)
2403
+ ]);
2404
+ }
2405
+ async function detectAntigravity(homeDir) {
2406
+ return exists(await antigravityStateDb(homeDir));
2407
+ }
2408
+ async function antigravityBilling(account) {
2409
+ try {
2410
+ const db = await antigravityStateDb(account.homeDir);
2411
+ const { token, status } = await readAntigravityOAuthToken(db);
2412
+ if (!token) return { plan: null, metrics: [], error: cloudCodeSqliteError(status) };
2413
+ const quota = await fetchCloudCodeQuota(token, "Token expired \u2014 open Antigravity");
2414
+ if (!quota.ok) return { plan: quota.plan, metrics: [], error: quota.error };
2415
+ return { plan: quota.plan, metrics: cloudCodeBucketsToMetrics(quota.buckets), error: null };
2416
+ } catch {
2417
+ return { plan: null, metrics: [], error: "Antigravity billing unavailable" };
2418
+ }
2419
+ }
2420
+
2421
+ // src/providers/antigravity/index.ts
2422
+ var antigravityProvider = {
2423
+ id: "antigravity",
2424
+ name: "Antigravity",
2425
+ color: "red",
2426
+ hasUsage: false,
2427
+ hasBilling: true,
2428
+ detect: (homeDir) => detectAntigravity(homeDir),
2429
+ fetchBilling: (account) => antigravityBilling(account)
2430
+ };
2431
+
2432
+ // src/providers/gemini/billing.ts
2433
+ import { access as access8, readFile as readFile7 } from "fs/promises";
2434
+ import { join as join14 } from "path";
2435
+ import { homedir as homedir12 } from "os";
2436
+ function geminiCredsPath(homeDir) {
2437
+ return join14(homeDir ?? homedir12(), ".gemini", "oauth_creds.json");
2438
+ }
2439
+ async function detectGemini(homeDir) {
2440
+ try {
2441
+ await access8(geminiCredsPath(homeDir));
2442
+ return true;
2443
+ } catch {
2444
+ return false;
2445
+ }
2446
+ }
2447
+ async function readGeminiCreds(path) {
2448
+ try {
2449
+ return JSON.parse(await readFile7(path, "utf8"));
2450
+ } catch {
2451
+ return null;
2452
+ }
2453
+ }
2454
+ async function geminiBilling(account) {
2455
+ try {
2456
+ const creds = await readGeminiCreds(geminiCredsPath(account.homeDir));
2457
+ const accessToken = typeof creds?.access_token === "string" ? creds.access_token.trim() : "";
2458
+ const refreshToken = typeof creds?.refresh_token === "string" ? creds.refresh_token.trim() : null;
2459
+ if (!creds || !accessToken && !refreshToken) return { plan: null, metrics: [], error: "Not signed in \u2014 run gemini" };
2460
+ const quota = await fetchCloudCodeQuota({
2461
+ accessToken,
2462
+ refreshToken,
2463
+ expirySeconds: typeof creds.expiry_date === "number" ? Math.floor(creds.expiry_date / 1e3) : null
2464
+ }, "Token expired \u2014 run gemini");
2465
+ if (!quota.ok) return { plan: quota.plan, metrics: [], error: quota.error };
2466
+ return { plan: quota.plan, metrics: cloudCodeBucketsToMetrics(quota.buckets), error: null };
2467
+ } catch {
2468
+ return { plan: null, metrics: [], error: "Gemini billing unavailable" };
2469
+ }
2470
+ }
2471
+
2472
+ // src/providers/gemini/index.ts
2473
+ var geminiProvider = {
2474
+ id: "gemini",
2475
+ name: "Gemini",
2476
+ color: "greenBright",
2477
+ hasUsage: false,
2478
+ hasBilling: true,
2479
+ detect: (homeDir) => detectGemini(homeDir),
2480
+ fetchBilling: (account) => geminiBilling(account)
2481
+ };
2482
+
1417
2483
  // src/providers/detect.ts
1418
2484
  import { accessSync, constants } from "fs";
1419
- import { join as join9, delimiter, isAbsolute as isAbsolute3 } from "path";
1420
- import { homedir as homedir7 } from "os";
2485
+ import { join as join15, delimiter, isAbsolute as isAbsolute3 } from "path";
2486
+ import { homedir as homedir13 } from "os";
1421
2487
  function searchDirs() {
1422
- const home = homedir7();
2488
+ const home = homedir13();
1423
2489
  const fromEnv = (process.env.PATH ?? "").split(delimiter).filter(Boolean);
1424
2490
  const extra = process.platform === "win32" ? [
1425
- process.env.APPDATA && join9(process.env.APPDATA, "npm"),
1426
- process.env.LOCALAPPDATA && join9(process.env.LOCALAPPDATA, "pnpm"),
1427
- join9(home, "scoop", "shims")
2491
+ process.env.APPDATA && join15(process.env.APPDATA, "npm"),
2492
+ process.env.LOCALAPPDATA && join15(process.env.LOCALAPPDATA, "pnpm"),
2493
+ join15(home, "scoop", "shims")
1428
2494
  ] : [
1429
2495
  "/opt/homebrew/bin",
1430
2496
  "/usr/local/bin",
1431
2497
  "/usr/bin",
1432
2498
  "/bin",
1433
2499
  "/opt/local/bin",
1434
- join9(home, ".local", "bin"),
1435
- join9(home, "bin"),
1436
- join9(home, ".npm-global", "bin"),
1437
- join9(home, ".bun", "bin"),
1438
- join9(home, ".local", "share", "pnpm")
2500
+ join15(home, ".local", "bin"),
2501
+ join15(home, "bin"),
2502
+ join15(home, ".npm-global", "bin"),
2503
+ join15(home, ".bun", "bin"),
2504
+ join15(home, ".local", "share", "pnpm")
1439
2505
  ];
1440
2506
  return [.../* @__PURE__ */ new Set([...fromEnv, ...extra.filter((d) => !!d)])];
1441
2507
  }
@@ -1452,7 +2518,7 @@ function onPath(names) {
1452
2518
  for (const dir of searchDirs()) {
1453
2519
  for (const n of names) {
1454
2520
  for (const e of exts) {
1455
- if (isExec(join9(dir, n + e))) return true;
2521
+ if (isExec(join15(dir, n + e))) return true;
1456
2522
  }
1457
2523
  }
1458
2524
  }
@@ -1462,7 +2528,7 @@ function anyExists(paths) {
1462
2528
  return paths.some((p) => !!p && isExec(p));
1463
2529
  }
1464
2530
  function installSignals(id) {
1465
- const home = homedir7();
2531
+ const home = homedir13();
1466
2532
  const pf = process.env.ProgramFiles;
1467
2533
  const pf86 = process.env["ProgramFiles(x86)"];
1468
2534
  const lad = process.env.LOCALAPPDATA;
@@ -1470,8 +2536,8 @@ function installSignals(id) {
1470
2536
  case "claude":
1471
2537
  return onPath(["claude"]) || anyExists([
1472
2538
  "/Applications/Claude.app",
1473
- join9(home, "Applications", "Claude.app"),
1474
- lad && join9(lad, "Programs", "claude", "Claude.exe")
2539
+ join15(home, "Applications", "Claude.app"),
2540
+ lad && join15(lad, "Programs", "claude", "Claude.exe")
1475
2541
  ]);
1476
2542
  case "codex": {
1477
2543
  const bin = process.env.CODEX_BIN;
@@ -1481,25 +2547,44 @@ function installSignals(id) {
1481
2547
  case "cursor":
1482
2548
  return onPath(["cursor", "cursor-agent"]) || anyExists([
1483
2549
  "/Applications/Cursor.app",
1484
- join9(home, "Applications", "Cursor.app"),
1485
- lad && join9(lad, "Programs", "cursor", "Cursor.exe"),
1486
- pf && join9(pf, "Cursor", "Cursor.exe"),
1487
- pf86 && join9(pf86, "Cursor", "Cursor.exe"),
2550
+ join15(home, "Applications", "Cursor.app"),
2551
+ lad && join15(lad, "Programs", "cursor", "Cursor.exe"),
2552
+ pf && join15(pf, "Cursor", "Cursor.exe"),
2553
+ pf86 && join15(pf86, "Cursor", "Cursor.exe"),
1488
2554
  "/opt/Cursor/cursor",
1489
2555
  "/usr/share/cursor/cursor",
1490
2556
  "/usr/bin/cursor"
1491
2557
  ]);
2558
+ case "pi":
2559
+ return onPath(["pi"]);
2560
+ case "opencode":
2561
+ return onPath(["opencode"]);
2562
+ case "copilot":
2563
+ return onPath(["gh"]);
2564
+ case "antigravity":
2565
+ return onPath(["antigravity"]) || anyExists([
2566
+ "/Applications/Antigravity.app",
2567
+ join15(home, "Applications", "Antigravity.app"),
2568
+ lad && join15(lad, "Programs", "Antigravity", "Antigravity.exe")
2569
+ ]);
2570
+ case "gemini":
2571
+ return onPath(["gemini"]);
1492
2572
  default:
1493
2573
  return false;
1494
2574
  }
1495
2575
  }
1496
2576
 
1497
2577
  // src/providers/index.ts
1498
- var PROVIDER_ORDER = ["claude", "codex", "cursor"];
2578
+ var PROVIDER_ORDER = ["claude", "codex", "cursor", "copilot", "pi", "opencode", "antigravity", "gemini"];
1499
2579
  var PROVIDERS = {
1500
2580
  claude: claudeProvider,
1501
2581
  codex: codexProvider,
1502
- cursor: cursorProvider
2582
+ cursor: cursorProvider,
2583
+ pi: piProvider,
2584
+ opencode: opencodeProvider,
2585
+ copilot: copilotProvider,
2586
+ antigravity: antigravityProvider,
2587
+ gemini: geminiProvider
1503
2588
  };
1504
2589
  var ALL_PROVIDERS = PROVIDER_ORDER.map((id) => PROVIDERS[id]);
1505
2590
  async function detectProviders() {
@@ -1548,14 +2633,46 @@ function accountsByProvider(accounts) {
1548
2633
  return groups;
1549
2634
  }
1550
2635
 
2636
+ // src/snapshot.ts
2637
+ import { readFile as readFile8, writeFile as writeFile3, mkdir as mkdir3, rename as rename3 } from "fs/promises";
2638
+ import { join as join16 } from "path";
2639
+ function snapshotFile() {
2640
+ return join16(cacheDir(), "dashboard-snapshot.json");
2641
+ }
2642
+ async function loadSnapshot() {
2643
+ try {
2644
+ const obj = JSON.parse(await readFile8(snapshotFile(), "utf-8"));
2645
+ return obj && typeof obj === "object" ? obj : {};
2646
+ } catch {
2647
+ return {};
2648
+ }
2649
+ }
2650
+ var saveQueue2 = Promise.resolve();
2651
+ function saveSnapshot(stats) {
2652
+ const obj = {};
2653
+ for (const [id, s] of stats) {
2654
+ if (s.dashboard || s.billing) obj[id] = { dashboard: s.dashboard ?? null, billing: s.billing ?? null };
2655
+ }
2656
+ saveQueue2 = saveQueue2.then(async () => {
2657
+ try {
2658
+ const dir = cacheDir();
2659
+ await mkdir3(dir, { recursive: true });
2660
+ const tmp = join16(dir, `dashboard-snapshot.json.${process.pid}.tmp`);
2661
+ await writeFile3(tmp, JSON.stringify(obj));
2662
+ await rename3(tmp, snapshotFile());
2663
+ } catch {
2664
+ }
2665
+ });
2666
+ }
2667
+
1551
2668
  // src/ui/shared.tsx
1552
2669
  import { useState, useEffect, useRef } from "react";
1553
2670
  import { Box, Text } from "ink";
1554
2671
  import { useOnMouseClick } from "@zenobius/ink-mouse";
1555
2672
  import { jsx, jsxs } from "react/jsx-runtime";
1556
- var SPINNER_FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
1557
2673
  function truncateName(s, n) {
1558
- return s.length > n ? s.slice(0, n - 1) + "\u2026" : s;
2674
+ const ell = glyphs().ellipsis;
2675
+ return s.length > n ? s.slice(0, n - ell.length) + ell : s;
1559
2676
  }
1560
2677
  function ClickableBox({ onClick, children, ...props }) {
1561
2678
  const ref = useRef(null);
@@ -1565,21 +2682,22 @@ function ClickableBox({ onClick, children, ...props }) {
1565
2682
  return /* @__PURE__ */ jsx(Box, { ref, ...props, children });
1566
2683
  }
1567
2684
  function Spinner({ label }) {
2685
+ const frames = glyphs().spinner;
1568
2686
  const [i, setI] = useState(0);
1569
2687
  useEffect(() => {
1570
- const id = setInterval(() => setI((n) => (n + 1) % SPINNER_FRAMES.length), 80);
2688
+ const id = setInterval(() => setI((n) => (n + 1) % frames.length), 80);
1571
2689
  return () => clearInterval(id);
1572
2690
  }, []);
1573
2691
  return /* @__PURE__ */ jsxs(Box, { children: [
1574
2692
  /* @__PURE__ */ jsxs(Text, { color: "green", children: [
1575
- SPINNER_FRAMES[i],
2693
+ frames[i],
1576
2694
  " "
1577
2695
  ] }),
1578
2696
  /* @__PURE__ */ jsx(Text, { dimColor: true, children: label })
1579
2697
  ] });
1580
2698
  }
1581
- function TabBar({ tabs, active, onSelect }) {
1582
- return /* @__PURE__ */ jsx(Box, { children: tabs.map((t, i) => /* @__PURE__ */ jsx(ClickableBox, { onClick: () => onSelect(i), marginRight: 1, children: i === active ? /* @__PURE__ */ jsxs(Text, { bold: true, inverse: true, children: [
2699
+ function TabBar({ tabs, active: active2, onSelect }) {
2700
+ return /* @__PURE__ */ jsx(Box, { children: tabs.map((t, i) => /* @__PURE__ */ jsx(ClickableBox, { onClick: () => onSelect(i), marginRight: 1, children: i === active2 ? /* @__PURE__ */ jsxs(Text, { bold: true, inverse: true, children: [
1583
2701
  " ",
1584
2702
  t,
1585
2703
  " "
@@ -1592,7 +2710,10 @@ function TabBar({ tabs, active, onSelect }) {
1592
2710
  function PeakBadge({ peak }) {
1593
2711
  const color = peak.state === "peak" ? "red" : "green";
1594
2712
  return /* @__PURE__ */ jsxs(Box, { children: [
1595
- /* @__PURE__ */ jsx(Text, { color, children: "\u25CF " }),
2713
+ /* @__PURE__ */ jsxs(Text, { color, children: [
2714
+ glyphs().dot,
2715
+ " "
2716
+ ] }),
1596
2717
  /* @__PURE__ */ jsx(Text, { bold: true, color, children: peak.label }),
1597
2718
  peak.minutesUntilChange !== null && peak.minutesUntilChange > 0 && /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
1598
2719
  " (",
@@ -1608,24 +2729,24 @@ function fmtMinutes(mins) {
1608
2729
  return m === 0 ? `${h}h` : `${h}h ${m}m`;
1609
2730
  }
1610
2731
  function currencySymbol(cur) {
1611
- return cur === "EUR" ? "\u20AC" : cur === "GBP" ? "\xA3" : "$";
2732
+ return cur === "EUR" ? glyphs().eur : cur === "GBP" ? glyphs().gbp : "$";
1612
2733
  }
1613
- var SPARK = ["\u2581", "\u2582", "\u2583", "\u2584", "\u2585", "\u2586", "\u2587", "\u2588"];
1614
2734
  function sparkline(values) {
1615
2735
  if (values.length === 0) return "";
2736
+ const spark = glyphs().spark;
1616
2737
  const max = Math.max(...values);
1617
- if (max <= 0) return SPARK[0].repeat(values.length);
2738
+ if (max <= 0) return spark[0].repeat(values.length);
1618
2739
  return values.map((v) => {
1619
- if (v <= 0) return SPARK[0];
1620
- const idx = Math.min(SPARK.length - 1, 1 + Math.round(v / max * (SPARK.length - 2)));
1621
- return SPARK[idx];
2740
+ if (v <= 0) return spark[0];
2741
+ const idx = Math.min(spark.length - 1, 1 + Math.round(v / max * (spark.length - 2)));
2742
+ return spark[idx];
1622
2743
  }).join("");
1623
2744
  }
1624
2745
  function Bar({ pct: pct2, color, width = 24 }) {
1625
2746
  const filled = Math.max(0, Math.min(width, Math.round(pct2 / 100 * width)));
1626
2747
  return /* @__PURE__ */ jsxs(Text, { children: [
1627
- /* @__PURE__ */ jsx(Text, { color, children: "\u2501".repeat(filled) }),
1628
- /* @__PURE__ */ jsx(Text, { dimColor: true, children: "\u2500".repeat(width - filled) })
2748
+ /* @__PURE__ */ jsx(Text, { color, children: glyphs().barFull.repeat(filled) }),
2749
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: glyphs().barEmpty.repeat(width - filled) })
1629
2750
  ] });
1630
2751
  }
1631
2752
  function metricValueText(m) {
@@ -1647,20 +2768,76 @@ import { Box as Box2, Text as Text2 } from "ink";
1647
2768
  import { Fragment, jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
1648
2769
  var GAP = 2;
1649
2770
  var MIN_CARD = 56;
1650
- function DashboardView({ groups, stats, cols, focusId, layout }) {
2771
+ var MIN_CARD_DENSE = 44;
2772
+ var CARD_H = { full: 14, compact: 12, mini: 8 };
2773
+ var VARIANT_ORDER = ["full", "compact", "mini"];
2774
+ var INDICATOR_ROWS = 1;
2775
+ var MAX_SINGLE_CARD = Math.round(MIN_CARD * 1.6);
2776
+ function chooseLayout(content, budget, n, single, cols) {
2777
+ if (n <= 0) return { ncols: 1, variant: "mini", cardsPerPage: 1, pageCount: 1 };
2778
+ const gridHeight = (rows, H2) => rows * H2 + Math.max(0, rows - 1);
2779
+ const colCap = single ? 1 : cols >= 3 * MIN_CARD_DENSE + 2 * GAP ? 3 : cols >= 2 * MIN_CARD + GAP ? 2 : 1;
2780
+ const maxCols = Math.max(1, Math.min(colCap, n));
2781
+ const cardWidthAt = (nc) => nc <= 1 ? content : Math.floor((content - GAP * (nc - 1)) / nc);
2782
+ const minWidthAt = (nc) => nc >= 3 ? MIN_CARD_DENSE : MIN_CARD;
2783
+ for (const variant of VARIANT_ORDER) {
2784
+ for (let nc = maxCols; nc >= 1; nc--) {
2785
+ if (nc > 1 && cardWidthAt(nc) < minWidthAt(nc)) continue;
2786
+ const rows = Math.ceil(n / nc);
2787
+ if (gridHeight(rows, CARD_H[variant]) <= budget) {
2788
+ return { ncols: nc, variant, cardsPerPage: n, pageCount: 1 };
2789
+ }
2790
+ }
2791
+ }
2792
+ let ncols = 1;
2793
+ for (let nc = maxCols; nc >= 1; nc--) {
2794
+ if (nc === 1 || cardWidthAt(nc) >= minWidthAt(nc)) {
2795
+ ncols = nc;
2796
+ break;
2797
+ }
2798
+ }
2799
+ const H = CARD_H.mini;
2800
+ const fitBudget = budget - INDICATOR_ROWS;
2801
+ const rowsThatFit = Math.max(1, Math.floor((fitBudget + 1) / (H + 1)));
2802
+ const cardsPerPage = Math.max(1, rowsThatFit * ncols);
2803
+ const pageCount = Math.max(1, Math.ceil(n / cardsPerPage));
2804
+ return { ncols, variant: "mini", cardsPerPage, pageCount };
2805
+ }
2806
+ function DashboardView({ groups, stats, cols, budget, focusId, layout, page = 0 }) {
1651
2807
  if (groups.length === 0) {
1652
- return /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "No providers enabled \u2014 press s to pick providers." });
2808
+ return /* @__PURE__ */ jsxs2(Text2, { dimColor: true, children: [
2809
+ "No providers enabled ",
2810
+ glyphs().emDash,
2811
+ " press s to pick providers."
2812
+ ] });
1653
2813
  }
1654
2814
  let shown = groups;
1655
2815
  if (layout === "single" && focusId === null) shown = groups.slice(0, 1);
1656
- const content = Math.max(MIN_CARD, cols - 4);
1657
2816
  const single = focusId !== null || layout === "single";
1658
- const auto = Math.max(1, Math.min(2, Math.floor((content + GAP) / (MIN_CARD + GAP))));
1659
- const ncols = single ? 1 : Math.min(auto, shown.length);
1660
- const cardW = ncols <= 1 ? content : Math.floor((content - GAP * (ncols - 1)) / ncols);
1661
- return /* @__PURE__ */ jsx2(Box2, { width: content, flexWrap: "wrap", columnGap: GAP, rowGap: 1, children: shown.map((g) => /* @__PURE__ */ jsx2(ProviderCard, { provider: g.provider, accounts: g.accounts, stats, width: cardW }, g.provider)) });
2817
+ const content = Math.max(MIN_CARD, cols - 4);
2818
+ const { ncols, variant, cardsPerPage, pageCount } = chooseLayout(content, budget, shown.length, single, cols);
2819
+ let cardW = ncols <= 1 ? content : Math.floor((content - GAP * (ncols - 1)) / ncols);
2820
+ if (ncols === 1 && cardW > MAX_SINGLE_CARD) cardW = MAX_SINGLE_CARD;
2821
+ const pg = pageCount > 1 ? (page % pageCount + pageCount) % pageCount : 0;
2822
+ const visible = pageCount > 1 ? shown.slice(pg * cardsPerPage, pg * cardsPerPage + cardsPerPage) : shown;
2823
+ return /* @__PURE__ */ jsxs2(Box2, { height: budget, flexDirection: "column", overflow: "hidden", children: [
2824
+ /* @__PURE__ */ jsx2(Box2, { width: content, flexWrap: "wrap", columnGap: GAP, rowGap: 1, children: visible.map((g) => /* @__PURE__ */ jsx2(Box2, { flexShrink: 0, children: /* @__PURE__ */ jsx2(ProviderCard, { provider: g.provider, accounts: g.accounts, stats, width: cardW, variant }) }, g.provider)) }),
2825
+ pageCount > 1 && /* @__PURE__ */ jsxs2(Text2, { dimColor: true, children: [
2826
+ " ",
2827
+ glyphs().middot,
2828
+ " page ",
2829
+ pg + 1,
2830
+ "/",
2831
+ pageCount,
2832
+ " ",
2833
+ glyphs().middot,
2834
+ " scroll ",
2835
+ glyphs().arrowU,
2836
+ glyphs().arrowD
2837
+ ] })
2838
+ ] });
1662
2839
  }
1663
- function ProviderCard({ provider, accounts, stats, width }) {
2840
+ function ProviderCard({ provider, accounts, stats, width, variant }) {
1664
2841
  const meta = PROVIDERS[provider];
1665
2842
  const items = accounts.map((a) => ({ account: a, s: stats.get(a.id) }));
1666
2843
  const dashboards = items.map((i) => i.s?.dashboard).filter((d) => !!d);
@@ -1670,9 +2847,14 @@ function ProviderCard({ provider, accounts, stats, width }) {
1670
2847
  const inner = width - 4;
1671
2848
  const barW = Math.max(10, Math.min(46, inner - 20));
1672
2849
  const hasSpark = !!agg && agg.series.some((v) => v > 0);
1673
- return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", width, borderStyle: "round", borderColor: meta.color, paddingX: 1, children: [
2850
+ const showBars = variant !== "mini";
2851
+ const showSpark = variant === "full";
2852
+ return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", width, borderStyle: glyphs().border, borderColor: meta.color, paddingX: 1, children: [
1674
2853
  /* @__PURE__ */ jsxs2(Box2, { children: [
1675
- /* @__PURE__ */ jsx2(Text2, { color: meta.color, children: "\u25CF " }),
2854
+ /* @__PURE__ */ jsxs2(Text2, { color: meta.color, children: [
2855
+ glyphs().dot,
2856
+ " "
2857
+ ] }),
1676
2858
  /* @__PURE__ */ jsx2(Text2, { bold: true, color: meta.color, children: meta.name }),
1677
2859
  /* @__PURE__ */ jsx2(Box2, { flexGrow: 1 }),
1678
2860
  plan && /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: plan })
@@ -1685,17 +2867,21 @@ function ProviderCard({ provider, accounts, stats, width }) {
1685
2867
  /* @__PURE__ */ jsx2(KpiLine, { agg })
1686
2868
  ] }) : /* @__PURE__ */ jsxs2(Fragment, { children: [
1687
2869
  /* @__PURE__ */ jsx2(Box2, { height: 1 }),
1688
- /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "Fetching usage\u2026" })
2870
+ /* @__PURE__ */ jsxs2(Text2, { dimColor: true, children: [
2871
+ "Fetching usage",
2872
+ glyphs().ellipsis
2873
+ ] })
1689
2874
  ] })),
1690
- meta.hasBilling && /* @__PURE__ */ jsxs2(Fragment, { children: [
2875
+ meta.hasBilling && showBars && /* @__PURE__ */ jsxs2(Fragment, { children: [
1691
2876
  meta.hasUsage && /* @__PURE__ */ jsx2(Rule, { inner }),
1692
2877
  /* @__PURE__ */ jsx2(LimitsBlock, { items, barW })
1693
2878
  ] }),
1694
- hasSpark && /* @__PURE__ */ jsxs2(Fragment, { children: [
2879
+ meta.hasBilling && !showBars && !meta.hasUsage && /* @__PURE__ */ jsx2(CompactBilling, { items }),
2880
+ hasSpark && showSpark && /* @__PURE__ */ jsxs2(Fragment, { children: [
1695
2881
  /* @__PURE__ */ jsx2(Rule, { inner }),
1696
2882
  /* @__PURE__ */ jsx2(SparkFooter, { series: agg.series, month: agg.month.cost, color: meta.color })
1697
2883
  ] }),
1698
- !meta.hasUsage && activity && /* @__PURE__ */ jsxs2(Fragment, { children: [
2884
+ !meta.hasUsage && activity && showSpark && /* @__PURE__ */ jsxs2(Fragment, { children: [
1699
2885
  /* @__PURE__ */ jsx2(Rule, { inner }),
1700
2886
  /* @__PURE__ */ jsxs2(Box2, { children: [
1701
2887
  /* @__PURE__ */ jsx2(Box2, { width: 4, children: /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "14d" }) }),
@@ -1703,14 +2889,29 @@ function ProviderCard({ provider, accounts, stats, width }) {
1703
2889
  /* @__PURE__ */ jsx2(Box2, { flexGrow: 1, justifyContent: "flex-end", children: /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: activity.summary }) })
1704
2890
  ] })
1705
2891
  ] }),
1706
- !meta.hasUsage && !activity && /* @__PURE__ */ jsxs2(Fragment, { children: [
2892
+ !meta.hasUsage && !activity && variant === "full" && /* @__PURE__ */ jsxs2(Fragment, { children: [
1707
2893
  /* @__PURE__ */ jsx2(Box2, { flexGrow: 1 }),
1708
- /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "Billing only \u2014 no local history" })
2894
+ /* @__PURE__ */ jsxs2(Text2, { dimColor: true, children: [
2895
+ "Billing only ",
2896
+ glyphs().emDash,
2897
+ " no local history"
2898
+ ] })
1709
2899
  ] })
1710
2900
  ] });
1711
2901
  }
2902
+ function CompactBilling({ items }) {
2903
+ const billing = items.map((i) => i.s?.billing).find(Boolean);
2904
+ if (!billing) return /* @__PURE__ */ jsxs2(Text2, { dimColor: true, children: [
2905
+ "Fetching",
2906
+ glyphs().ellipsis
2907
+ ] });
2908
+ if (billing.error) return /* @__PURE__ */ jsx2(Text2, { color: "red", children: billing.error });
2909
+ const m = billing.metrics[0];
2910
+ if (!m) return /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "No data" });
2911
+ return /* @__PURE__ */ jsx2(Text2, { bold: true, color: "yellow", children: metricValueText(m) });
2912
+ }
1712
2913
  function Rule({ inner }) {
1713
- return /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "\u2500".repeat(Math.max(0, inner)) });
2914
+ return /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: glyphs().rule.repeat(Math.max(0, inner)) });
1714
2915
  }
1715
2916
  function SummaryRow({ label, s }) {
1716
2917
  const cachedPct = s.tokens > 0 ? Math.round(s.cacheRead / s.tokens * 100) : 0;
@@ -1755,10 +2956,16 @@ function LimitsBlock({ items, barW }) {
1755
2956
  const billing = s?.billing;
1756
2957
  return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", marginTop: showName && idx > 0 ? 1 : 0, children: [
1757
2958
  showName && /* @__PURE__ */ jsxs2(Box2, { children: [
1758
- /* @__PURE__ */ jsx2(Text2, { color: account.color, children: "\u25CF " }),
2959
+ /* @__PURE__ */ jsxs2(Text2, { color: account.color, children: [
2960
+ glyphs().dot,
2961
+ " "
2962
+ ] }),
1759
2963
  /* @__PURE__ */ jsx2(Text2, { bold: true, children: truncateName(account.name, 22) })
1760
2964
  ] }),
1761
- !billing ? /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "Fetching\u2026" }) : billing.error ? /* @__PURE__ */ jsx2(Text2, { color: "red", children: billing.error }) : billing.metrics.length === 0 ? /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "No data" }) : billing.metrics.map((m, i) => /* @__PURE__ */ jsx2(MetricRow, { m, color: account.color, barW }, `${m.label}${i}`))
2965
+ !billing ? /* @__PURE__ */ jsxs2(Text2, { dimColor: true, children: [
2966
+ "Fetching",
2967
+ glyphs().ellipsis
2968
+ ] }) : billing.error ? /* @__PURE__ */ jsx2(Text2, { color: "red", children: billing.error }) : billing.metrics.length === 0 ? /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "No data" }) : billing.metrics.map((m, i) => /* @__PURE__ */ jsx2(MetricRow, { m, color: account.color, barW }, `${m.label}${i}`))
1762
2969
  ] }, account.id);
1763
2970
  }) });
1764
2971
  }
@@ -1807,17 +3014,41 @@ function aggregate(list) {
1807
3014
  }
1808
3015
  return z;
1809
3016
  }
3017
+ function TotalsRow({ groups, stats, cols }) {
3018
+ const zero = () => ({ cost: 0, tokens: 0, cacheRead: 0, cacheSavings: 0 });
3019
+ const t = zero(), w = zero(), m = zero();
3020
+ for (const g of groups) {
3021
+ if (!PROVIDERS[g.provider].hasUsage) continue;
3022
+ for (const a of g.accounts) {
3023
+ const d = stats.get(a.id)?.dashboard;
3024
+ if (!d) continue;
3025
+ t.cost += d.today.cost;
3026
+ t.tokens += d.today.tokens;
3027
+ w.cost += d.week.cost;
3028
+ w.tokens += d.week.tokens;
3029
+ m.cost += d.month.cost;
3030
+ m.tokens += d.month.tokens;
3031
+ }
3032
+ }
3033
+ const inner = cols - 4;
3034
+ const dot = glyphs().middot;
3035
+ const full = `${glyphs().dotAll} Today ${currency(t.cost)} (${tokens(t.tokens)} tok) ${dot} Week ${currency(w.cost)} (${tokens(w.tokens)} tok) ${dot} Month ${currency(m.cost)} (${tokens(m.tokens)} tok)`;
3036
+ const noTok = `${glyphs().dotAll} Today ${currency(t.cost)} ${dot} Week ${currency(w.cost)} ${dot} Month ${currency(m.cost)}`;
3037
+ const tight = `${glyphs().dotAll} ${currency(t.cost)} ${dot} ${currency(w.cost)} ${dot} ${currency(m.cost)}`;
3038
+ const text = full.length <= inner ? full : noTok.length <= inner ? noTok : tight;
3039
+ return /* @__PURE__ */ jsx2(Box2, { marginTop: 1, children: /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: text }) });
3040
+ }
1810
3041
 
1811
3042
  // src/ui/table.tsx
1812
3043
  import { Box as Box3, Text as Text3 } from "ink";
1813
3044
  import { Fragment as Fragment2, jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
1814
3045
  var MONTHS = ["", "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
1815
- function TableProviderBar({ providers, active, onSelect }) {
3046
+ function TableProviderBar({ providers, active: active2, onSelect }) {
1816
3047
  return /* @__PURE__ */ jsxs3(Box3, { children: [
1817
3048
  /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "provider " }),
1818
3049
  providers.map((p) => {
1819
3050
  const meta = PROVIDERS[p];
1820
- return /* @__PURE__ */ jsx3(ClickableBox, { onClick: () => onSelect(p), marginRight: 1, children: p === active ? /* @__PURE__ */ jsxs3(Text3, { bold: true, color: meta.color, inverse: true, children: [
3051
+ return /* @__PURE__ */ jsx3(ClickableBox, { onClick: () => onSelect(p), marginRight: 1, children: p === active2 ? /* @__PURE__ */ jsxs3(Text3, { bold: true, color: meta.color, inverse: true, children: [
1821
3052
  " ",
1822
3053
  meta.name,
1823
3054
  " "
@@ -1842,15 +3073,23 @@ function ControlBar({ views, period, sort, search, searching, showPeriod }) {
1842
3073
  ] }),
1843
3074
  /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "sort " }),
1844
3075
  /* @__PURE__ */ jsx3(Text3, { bold: true, color: "magenta", children: sort }),
1845
- /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: " o cycle \xB7 " }),
3076
+ /* @__PURE__ */ jsxs3(Text3, { dimColor: true, children: [
3077
+ " o cycle ",
3078
+ glyphs().middot,
3079
+ " "
3080
+ ] }),
1846
3081
  searching ? /* @__PURE__ */ jsxs3(Fragment2, { children: [
1847
3082
  /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "/" }),
1848
3083
  /* @__PURE__ */ jsx3(Text3, { bold: true, color: "cyan", children: search }),
1849
- /* @__PURE__ */ jsx3(Text3, { color: "cyan", children: "\u258F" })
3084
+ /* @__PURE__ */ jsx3(Text3, { color: "cyan", children: glyphs().vbar })
1850
3085
  ] }) : search ? /* @__PURE__ */ jsxs3(Fragment2, { children: [
1851
3086
  /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "filter " }),
1852
3087
  /* @__PURE__ */ jsx3(Text3, { bold: true, color: "green", children: search }),
1853
- /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: " (/ edit \xB7 esc clear)" })
3088
+ /* @__PURE__ */ jsxs3(Text3, { dimColor: true, children: [
3089
+ " (/ edit ",
3090
+ glyphs().middot,
3091
+ " esc clear)"
3092
+ ] })
1854
3093
  ] }) : /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "/ filter" })
1855
3094
  ] });
1856
3095
  }
@@ -1887,14 +3126,14 @@ function TokenTable({ rows, cursor, expanded, maxRows, cols, onRowClick }) {
1887
3126
  W.total > 0 && /* @__PURE__ */ jsx3(Text3, { bold: true, children: col("Total", W.total) }),
1888
3127
  /* @__PURE__ */ jsx3(Text3, { bold: true, children: col("Cost", W.cost) })
1889
3128
  ] }),
1890
- /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "\u2500".repeat(lineW + 2) }),
3129
+ /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: glyphs().rule.repeat(lineW + 2) }),
1891
3130
  visible.map((r, vi) => {
1892
3131
  const idx = scrollStart + vi;
1893
3132
  const selected = idx === clampedCursor;
1894
3133
  return /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", children: [
1895
3134
  /* @__PURE__ */ jsx3(ClickableBox, { onClick: () => onRowClick(idx), children: /* @__PURE__ */ jsxs3(Text3, { inverse: selected, children: [
1896
3135
  /* @__PURE__ */ jsxs3(Text3, { color: selected ? void 0 : "cyan", children: [
1897
- selected ? "\u25B8 " : " ",
3136
+ selected ? `${glyphs().caretR} ` : " ",
1898
3137
  col(fmtLabel(r.label), W.label, "left")
1899
3138
  ] }),
1900
3139
  /* @__PURE__ */ jsx3(Text3, { dimColor: !selected, children: col(r.models.join(", "), W.models, "left") }),
@@ -1908,7 +3147,7 @@ function TokenTable({ rows, cursor, expanded, maxRows, cols, onRowClick }) {
1908
3147
  idx === expanded && /* @__PURE__ */ jsx3(RowDetail, { row: r, indent: W.label + 2 })
1909
3148
  ] }, r.label);
1910
3149
  }),
1911
- /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "\u2500".repeat(lineW + 2) }),
3150
+ /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: glyphs().rule.repeat(lineW + 2) }),
1912
3151
  /* @__PURE__ */ jsxs3(Text3, { children: [
1913
3152
  /* @__PURE__ */ jsxs3(Text3, { bold: true, color: "greenBright", children: [
1914
3153
  " ",
@@ -1924,7 +3163,17 @@ function TokenTable({ rows, cursor, expanded, maxRows, cols, onRowClick }) {
1924
3163
  ] }),
1925
3164
  /* @__PURE__ */ jsx3(Box3, { height: 1 }),
1926
3165
  /* @__PURE__ */ jsxs3(Text3, { dimColor: true, children: [
1927
- "\u2191\u2193 navigate \xB7 Enter detail \xB7 o sort \xB7 g/G top/bottom \xB7 ",
3166
+ glyphs().arrowU,
3167
+ glyphs().arrowD,
3168
+ " navigate ",
3169
+ glyphs().middot,
3170
+ " Enter detail ",
3171
+ glyphs().middot,
3172
+ " o sort ",
3173
+ glyphs().middot,
3174
+ " g/G top/bottom ",
3175
+ glyphs().middot,
3176
+ " ",
1928
3177
  clampedCursor + 1,
1929
3178
  "/",
1930
3179
  rows.length
@@ -1934,7 +3183,7 @@ function TokenTable({ rows, cursor, expanded, maxRows, cols, onRowClick }) {
1934
3183
  function RowDetail({ row, indent }) {
1935
3184
  return /* @__PURE__ */ jsx3(Box3, { flexDirection: "column", paddingLeft: indent, children: row.breakdown.map((m, i) => /* @__PURE__ */ jsxs3(Text3, { children: [
1936
3185
  /* @__PURE__ */ jsxs3(Text3, { dimColor: true, children: [
1937
- i === row.breakdown.length - 1 ? "\u2514\u2500" : "\u251C\u2500",
3186
+ i === row.breakdown.length - 1 ? glyphs().treeEnd : glyphs().treeMid,
1938
3187
  " "
1939
3188
  ] }),
1940
3189
  /* @__PURE__ */ jsx3(Text3, { bold: true, children: col(m.name, 16, "left") }),
@@ -1975,14 +3224,14 @@ function CursorSpendTable({ rows, cursor, maxRows, onRowClick }) {
1975
3224
  /* @__PURE__ */ jsx3(Text3, { bold: true, children: col("Amount", W.amount) }),
1976
3225
  /* @__PURE__ */ jsx3(Text3, { bold: true, children: col("Share", W.share) })
1977
3226
  ] }),
1978
- /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "\u2500".repeat(W.model + W.cost + W.amount + W.share + 2) }),
3227
+ /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: glyphs().rule.repeat(W.model + W.cost + W.amount + W.share + 2) }),
1979
3228
  visible.map((r, vi) => {
1980
3229
  const idx = scrollStart + vi;
1981
3230
  const selected = idx === clamped;
1982
3231
  const share = total > 0 ? r.usd / total * 100 : 0;
1983
3232
  return /* @__PURE__ */ jsx3(ClickableBox, { onClick: () => onRowClick(idx), children: /* @__PURE__ */ jsxs3(Text3, { inverse: selected, children: [
1984
3233
  /* @__PURE__ */ jsxs3(Text3, { color: selected ? void 0 : "magenta", children: [
1985
- selected ? "\u25B8 " : " ",
3234
+ selected ? `${glyphs().caretR} ` : " ",
1986
3235
  col(r.name, W.model, "left")
1987
3236
  ] }),
1988
3237
  /* @__PURE__ */ jsx3(Text3, { bold: true, color: selected ? void 0 : "yellow", children: col(currency(r.usd), W.cost) }),
@@ -1990,7 +3239,7 @@ function CursorSpendTable({ rows, cursor, maxRows, onRowClick }) {
1990
3239
  /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: col(share.toFixed(1) + "%", W.share) })
1991
3240
  ] }) }, r.name);
1992
3241
  }),
1993
- /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "\u2500".repeat(W.model + W.cost + W.amount + W.share + 2) }),
3242
+ /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: glyphs().rule.repeat(W.model + W.cost + W.amount + W.share + 2) }),
1994
3243
  /* @__PURE__ */ jsxs3(Text3, { children: [
1995
3244
  /* @__PURE__ */ jsxs3(Text3, { bold: true, color: "greenBright", children: [
1996
3245
  " ",
@@ -2002,7 +3251,11 @@ function CursorSpendTable({ rows, cursor, maxRows, onRowClick }) {
2002
3251
  ] }),
2003
3252
  /* @__PURE__ */ jsx3(Box3, { height: 1 }),
2004
3253
  /* @__PURE__ */ jsxs3(Text3, { dimColor: true, children: [
2005
- "local spend by model (composerData) \xB7 est. API-equivalent \xB7 ",
3254
+ "local spend by model (composerData) ",
3255
+ glyphs().middot,
3256
+ " est. API-equivalent ",
3257
+ glyphs().middot,
3258
+ " ",
2006
3259
  clamped + 1,
2007
3260
  "/",
2008
3261
  rows.length
@@ -2019,43 +3272,174 @@ function fmtLabel(label) {
2019
3272
  // src/ui/onboarding.tsx
2020
3273
  import { Box as Box4, Text as Text4 } from "ink";
2021
3274
  import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
2022
- function Onboarding({ items, cursor, onToggle, onConfirm }) {
3275
+ function Onboarding({ items, cursor, onToggle, onConfirm, heading = "Welcome to tokmon", subheading = "Pick the tools you want to track. You can change this anytime in settings." }) {
2023
3276
  const anyEnabled = items.some((it) => it.enabled);
2024
3277
  const startIdx = items.length;
2025
3278
  return /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", marginTop: 1, children: [
2026
- /* @__PURE__ */ jsx4(Box4, { children: /* @__PURE__ */ jsx4(Text4, { bold: true, color: "greenBright", children: "Welcome to tokmon" }) }),
2027
- /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: "Pick the tools you want to track. You can change this anytime in settings." }),
3279
+ /* @__PURE__ */ jsx4(Box4, { children: /* @__PURE__ */ jsx4(Text4, { bold: true, color: "greenBright", children: heading }) }),
3280
+ /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: subheading }),
2028
3281
  /* @__PURE__ */ jsx4(Box4, { height: 1 }),
2029
3282
  items.map((it, i) => {
2030
3283
  const selected = cursor === i;
2031
- const box = it.enabled ? "[\u2713]" : "[ ]";
3284
+ const box = it.enabled ? `[${glyphs().check}]` : "[ ]";
2032
3285
  return /* @__PURE__ */ jsxs4(ClickableBox, { onClick: () => onToggle(i), children: [
2033
3286
  /* @__PURE__ */ jsxs4(Text4, { color: selected ? "green" : void 0, children: [
2034
- selected ? "\u25B8" : " ",
3287
+ selected ? glyphs().caretR : " ",
2035
3288
  " "
2036
3289
  ] }),
2037
3290
  /* @__PURE__ */ jsx4(Text4, { bold: it.enabled, color: it.enabled ? it.color : void 0, dimColor: !it.enabled, children: box }),
2038
- /* @__PURE__ */ jsx4(Text4, { color: it.color, children: " \u25CF " }),
2039
- /* @__PURE__ */ jsx4(Box4, { width: 10, children: /* @__PURE__ */ jsx4(Text4, { bold: selected, dimColor: !it.detected && !it.enabled, children: it.name }) }),
3291
+ /* @__PURE__ */ jsxs4(Text4, { color: it.color, children: [
3292
+ " ",
3293
+ glyphs().dot,
3294
+ " "
3295
+ ] }),
3296
+ /* @__PURE__ */ jsx4(Box4, { width: 13, flexShrink: 0, children: /* @__PURE__ */ jsx4(Text4, { bold: selected, dimColor: !it.detected && !it.enabled, wrap: "truncate", children: it.name }) }),
2040
3297
  it.detected ? /* @__PURE__ */ jsx4(Text4, { color: "green", dimColor: true, children: "installed" }) : it.enabled ? /* @__PURE__ */ jsx4(Text4, { color: "yellow", dimColor: true, children: "manual" }) : /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: "not found" })
2041
3298
  ] }, it.id);
2042
3299
  }),
2043
3300
  /* @__PURE__ */ jsx4(Box4, { height: 1 }),
2044
3301
  /* @__PURE__ */ jsxs4(ClickableBox, { onClick: onConfirm, children: [
2045
3302
  /* @__PURE__ */ jsxs4(Text4, { color: cursor === startIdx ? "green" : void 0, children: [
2046
- cursor === startIdx ? "\u25B8" : " ",
3303
+ cursor === startIdx ? glyphs().caretR : " ",
2047
3304
  " "
2048
3305
  ] }),
2049
- /* @__PURE__ */ jsx4(Text4, { bold: true, color: anyEnabled ? "greenBright" : void 0, dimColor: !anyEnabled, children: anyEnabled ? "\u25B6 Start tokmon" : "\u25B6 Start (nothing selected)" })
3306
+ /* @__PURE__ */ jsx4(Text4, { bold: true, color: anyEnabled ? "greenBright" : void 0, dimColor: !anyEnabled, children: anyEnabled ? `${glyphs().play} Start tokmon` : `${glyphs().play} Start (nothing selected)` })
2050
3307
  ] }),
2051
3308
  /* @__PURE__ */ jsx4(Box4, { height: 1 }),
2052
- /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: "\u2191\u2193 move \xB7 space toggle \xB7 enter start \xB7 q quit" })
3309
+ /* @__PURE__ */ jsxs4(Text4, { dimColor: true, children: [
3310
+ glyphs().arrowU,
3311
+ glyphs().arrowD,
3312
+ " move ",
3313
+ glyphs().middot,
3314
+ " space toggle ",
3315
+ glyphs().middot,
3316
+ " enter start ",
3317
+ glyphs().middot,
3318
+ " q quit"
3319
+ ] })
2053
3320
  ] });
2054
3321
  }
2055
3322
 
2056
- // src/ui/settings.tsx
3323
+ // src/ui/loading.tsx
3324
+ import { useState as useState2, useEffect as useEffect2 } from "react";
2057
3325
  import { Box as Box5, Text as Text5 } from "ink";
2058
- import { Fragment as Fragment3, jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
3326
+ import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
3327
+ function accountReady(s, providerId) {
3328
+ if (!s) return false;
3329
+ const p = PROVIDERS[providerId];
3330
+ if (p.hasBilling && s.billing?.error) return true;
3331
+ if (p.hasUsage && !s.dashboard) return false;
3332
+ if (p.hasBilling && !s.billing) return false;
3333
+ return true;
3334
+ }
3335
+ function groupTodayCost(items) {
3336
+ return items.reduce((sum, s) => {
3337
+ const d = s?.dashboard;
3338
+ return sum + (d?.today.cost ?? 0);
3339
+ }, 0);
3340
+ }
3341
+ function headlineFor(group, items) {
3342
+ const meta = PROVIDERS[group.provider];
3343
+ if (meta.hasUsage) return `${currency(groupTodayCost(items))} today`;
3344
+ const billing = items.map((s) => s?.billing).find(Boolean);
3345
+ if (!billing) return "no data";
3346
+ if (billing.error) return billing.error;
3347
+ const m = billing.metrics[0];
3348
+ if (m) return metricValueText(m);
3349
+ return billing.plan ?? "no data";
3350
+ }
3351
+ var STAGGER_FRAMES = 2;
3352
+ function LoadingView({ groups, stats, cols, rows }) {
3353
+ const sp = glyphs().spinner;
3354
+ const [frame, setFrame] = useState2(0);
3355
+ useEffect2(() => {
3356
+ const id = setInterval(() => setFrame((f) => f + 1), 80);
3357
+ return () => clearInterval(id);
3358
+ }, []);
3359
+ const nameW = Math.min(13, groups.reduce((w, g) => Math.max(w, PROVIDERS[g.provider].name.length), 0));
3360
+ const readyCount = groups.filter((g) => g.accounts.every((a) => accountReady(stats.get(a.id), g.provider))).length;
3361
+ const maxRows = Math.max(1, rows - 7);
3362
+ const visible = groups.slice(0, maxRows);
3363
+ const hidden = groups.length - visible.length;
3364
+ return /* @__PURE__ */ jsxs5(Box5, { flexDirection: "column", children: [
3365
+ /* @__PURE__ */ jsxs5(Text5, { bold: true, color: "greenBright", children: [
3366
+ glyphs().dotSel,
3367
+ " tokmon"
3368
+ ] }),
3369
+ /* @__PURE__ */ jsxs5(Box5, { marginTop: 1, children: [
3370
+ /* @__PURE__ */ jsxs5(Text5, { dimColor: true, children: [
3371
+ "Detecting installed tools",
3372
+ glyphs().ellipsis
3373
+ ] }),
3374
+ /* @__PURE__ */ jsxs5(Text5, { dimColor: true, children: [
3375
+ " ",
3376
+ groups.length,
3377
+ " found"
3378
+ ] })
3379
+ ] }),
3380
+ /* @__PURE__ */ jsxs5(Box5, { marginTop: 1, flexDirection: "column", children: [
3381
+ visible.map((g, i) => {
3382
+ const meta = PROVIDERS[g.provider];
3383
+ const items = g.accounts.map((a) => stats.get(a.id));
3384
+ const ready = g.accounts.every((a) => accountReady(stats.get(a.id), g.provider));
3385
+ const errored = items.some((s) => !!s?.billing?.error);
3386
+ const revealed = frame >= i * STAGGER_FRAMES;
3387
+ const name = truncateName(meta.name, nameW);
3388
+ const namePad = " ".repeat(Math.max(0, nameW - name.length));
3389
+ if (!revealed) {
3390
+ return /* @__PURE__ */ jsxs5(Box5, { children: [
3391
+ /* @__PURE__ */ jsxs5(Text5, { dimColor: true, children: [
3392
+ glyphs().dot,
3393
+ " "
3394
+ ] }),
3395
+ /* @__PURE__ */ jsxs5(Text5, { dimColor: true, children: [
3396
+ name,
3397
+ namePad
3398
+ ] })
3399
+ ] }, g.provider);
3400
+ }
3401
+ return /* @__PURE__ */ jsxs5(Box5, { children: [
3402
+ /* @__PURE__ */ jsxs5(Text5, { color: meta.color, children: [
3403
+ glyphs().dot,
3404
+ " "
3405
+ ] }),
3406
+ /* @__PURE__ */ jsx5(Text5, { bold: true, color: meta.color, children: name }),
3407
+ /* @__PURE__ */ jsx5(Text5, { children: namePad }),
3408
+ /* @__PURE__ */ jsx5(Text5, { children: " " }),
3409
+ errored ? /* @__PURE__ */ jsxs5(Text5, { color: "red", children: [
3410
+ glyphs().warn,
3411
+ " "
3412
+ ] }) : ready ? /* @__PURE__ */ jsxs5(Text5, { color: "green", children: [
3413
+ glyphs().check,
3414
+ " "
3415
+ ] }) : /* @__PURE__ */ jsxs5(Text5, { color: "green", children: [
3416
+ sp[frame % sp.length],
3417
+ " "
3418
+ ] }),
3419
+ errored ? /* @__PURE__ */ jsx5(Text5, { color: "red", children: headlineFor(g, items) }) : ready ? /* @__PURE__ */ jsx5(Text5, { children: headlineFor(g, items) }) : /* @__PURE__ */ jsxs5(Text5, { dimColor: true, children: [
3420
+ "loading",
3421
+ glyphs().ellipsis
3422
+ ] })
3423
+ ] }, g.provider);
3424
+ }),
3425
+ hidden > 0 && /* @__PURE__ */ jsxs5(Text5, { dimColor: true, children: [
3426
+ "+",
3427
+ hidden,
3428
+ " more"
3429
+ ] })
3430
+ ] }),
3431
+ /* @__PURE__ */ jsx5(Box5, { marginTop: 1, children: /* @__PURE__ */ jsxs5(Text5, { dimColor: true, children: [
3432
+ "loading ",
3433
+ readyCount,
3434
+ " / ",
3435
+ groups.length
3436
+ ] }) })
3437
+ ] });
3438
+ }
3439
+
3440
+ // src/ui/settings.tsx
3441
+ import { Box as Box6, Text as Text6 } from "ink";
3442
+ import { Fragment as Fragment3, jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
2059
3443
  var GENERAL_ROWS = 6;
2060
3444
  var PROVIDER_ROWS_START = GENERAL_ROWS;
2061
3445
  var ACCOUNT_ROWS_START = GENERAL_ROWS + PROVIDER_ORDER.length;
@@ -2080,134 +3464,191 @@ function SettingsView({
2080
3464
  accountForm,
2081
3465
  activeAccountId
2082
3466
  }) {
2083
- if (accountForm) return /* @__PURE__ */ jsx5(AccountFormView, { form: accountForm, accounts: config2.accounts });
3467
+ if (accountForm) return /* @__PURE__ */ jsx6(AccountFormView, { form: accountForm, accounts: config2.accounts });
2084
3468
  const editingTz = tzEdit !== null;
2085
3469
  const tzDisplay = config2.timezone === null ? `System (${resolvedTz})` : config2.timezone;
2086
- return /* @__PURE__ */ jsxs5(Box5, { flexDirection: "column", marginTop: 1, children: [
2087
- /* @__PURE__ */ jsx5(Text5, { bold: true, children: "Settings" }),
2088
- /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: configLocation() }),
2089
- /* @__PURE__ */ jsx5(Box5, { height: 1 }),
2090
- /* @__PURE__ */ jsx5(Text5, { bold: true, dimColor: true, children: "General" }),
2091
- /* @__PURE__ */ jsxs5(Row, { cursor, idx: 0, label: "Refresh interval", children: [
2092
- /* @__PURE__ */ jsxs5(Text5, { dimColor: true, children: [
2093
- "\u25C2",
3470
+ return /* @__PURE__ */ jsxs6(Box6, { flexDirection: "column", marginTop: 1, children: [
3471
+ /* @__PURE__ */ jsx6(Text6, { bold: true, children: "Settings" }),
3472
+ /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: configLocation() }),
3473
+ /* @__PURE__ */ jsx6(Box6, { height: 1 }),
3474
+ /* @__PURE__ */ jsx6(Text6, { bold: true, dimColor: true, children: "General" }),
3475
+ /* @__PURE__ */ jsxs6(Row, { cursor, idx: 0, label: "Refresh interval", children: [
3476
+ /* @__PURE__ */ jsxs6(Text6, { dimColor: true, children: [
3477
+ glyphs().caretL,
2094
3478
  " "
2095
3479
  ] }),
2096
- /* @__PURE__ */ jsxs5(Text5, { bold: true, color: "yellow", children: [
3480
+ /* @__PURE__ */ jsxs6(Text6, { bold: true, color: "yellow", children: [
2097
3481
  config2.interval,
2098
3482
  "s"
2099
3483
  ] }),
2100
- /* @__PURE__ */ jsxs5(Text5, { dimColor: true, children: [
3484
+ /* @__PURE__ */ jsxs6(Text6, { dimColor: true, children: [
2101
3485
  " ",
2102
- "\u25B8"
3486
+ glyphs().caretR
2103
3487
  ] })
2104
3488
  ] }),
2105
- /* @__PURE__ */ jsxs5(Row, { cursor, idx: 1, label: "Billing poll", children: [
2106
- /* @__PURE__ */ jsxs5(Text5, { dimColor: true, children: [
2107
- "\u25C2",
3489
+ /* @__PURE__ */ jsxs6(Row, { cursor, idx: 1, label: "Billing poll", children: [
3490
+ /* @__PURE__ */ jsxs6(Text6, { dimColor: true, children: [
3491
+ glyphs().caretL,
2108
3492
  " "
2109
3493
  ] }),
2110
- /* @__PURE__ */ jsxs5(Text5, { bold: true, color: "yellow", children: [
3494
+ /* @__PURE__ */ jsxs6(Text6, { bold: true, color: "yellow", children: [
2111
3495
  config2.billingInterval,
2112
3496
  "m"
2113
3497
  ] }),
2114
- /* @__PURE__ */ jsxs5(Text5, { dimColor: true, children: [
3498
+ /* @__PURE__ */ jsxs6(Text6, { dimColor: true, children: [
2115
3499
  " ",
2116
- "\u25B8"
3500
+ glyphs().caretR
2117
3501
  ] })
2118
3502
  ] }),
2119
- /* @__PURE__ */ jsx5(Row, { cursor, idx: 2, label: "Clear screen", children: /* @__PURE__ */ jsx5(Text5, { bold: true, color: config2.clearScreen ? "green" : "red", children: config2.clearScreen ? "on" : "off" }) }),
2120
- /* @__PURE__ */ jsx5(Row, { cursor, idx: 3, label: "Timezone", children: editingTz ? /* @__PURE__ */ jsxs5(Fragment3, { children: [
2121
- /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: "[" }),
2122
- /* @__PURE__ */ jsx5(Text5, { bold: true, color: "cyan", children: tzEdit }),
2123
- /* @__PURE__ */ jsx5(Text5, { color: "cyan", children: "_" }),
2124
- /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: "]" })
2125
- ] }) : /* @__PURE__ */ jsx5(Text5, { bold: true, color: "yellow", children: tzDisplay }) }),
2126
- cursor === 3 && tzError && /* @__PURE__ */ jsxs5(Text5, { color: "red", children: [
3503
+ /* @__PURE__ */ jsx6(Row, { cursor, idx: 2, label: "Clear screen", children: /* @__PURE__ */ jsx6(Text6, { bold: true, color: config2.clearScreen ? "green" : "red", children: config2.clearScreen ? "on" : "off" }) }),
3504
+ /* @__PURE__ */ jsx6(Row, { cursor, idx: 3, label: "Timezone", children: editingTz ? /* @__PURE__ */ jsxs6(Fragment3, { children: [
3505
+ /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: "[" }),
3506
+ /* @__PURE__ */ jsx6(Text6, { bold: true, color: "cyan", children: tzEdit }),
3507
+ /* @__PURE__ */ jsx6(Text6, { color: "cyan", children: "_" }),
3508
+ /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: "]" })
3509
+ ] }) : /* @__PURE__ */ jsx6(Text6, { bold: true, color: "yellow", children: tzDisplay }) }),
3510
+ cursor === 3 && tzError && /* @__PURE__ */ jsxs6(Text6, { color: "red", children: [
2127
3511
  " ",
2128
3512
  tzError
2129
3513
  ] }),
2130
- /* @__PURE__ */ jsxs5(Row, { cursor, idx: 4, label: "Dashboard", children: [
2131
- /* @__PURE__ */ jsxs5(Text5, { dimColor: true, children: [
2132
- "\u25C2",
3514
+ /* @__PURE__ */ jsxs6(Row, { cursor, idx: 4, label: "Dashboard", children: [
3515
+ /* @__PURE__ */ jsxs6(Text6, { dimColor: true, children: [
3516
+ glyphs().caretL,
2133
3517
  " "
2134
3518
  ] }),
2135
- /* @__PURE__ */ jsx5(Text5, { bold: true, color: "yellow", children: config2.dashboardLayout === "grid" ? "grid (all)" : "single (cycle)" }),
2136
- /* @__PURE__ */ jsxs5(Text5, { dimColor: true, children: [
3519
+ /* @__PURE__ */ jsx6(Text6, { bold: true, color: "yellow", children: config2.dashboardLayout === "grid" ? "grid (all)" : "single (cycle)" }),
3520
+ /* @__PURE__ */ jsxs6(Text6, { dimColor: true, children: [
2137
3521
  " ",
2138
- "\u25B8"
3522
+ glyphs().caretR
2139
3523
  ] })
2140
3524
  ] }),
2141
- /* @__PURE__ */ jsxs5(Row, { cursor, idx: 5, label: "Default focus", children: [
2142
- /* @__PURE__ */ jsxs5(Text5, { dimColor: true, children: [
2143
- "\u25C2",
3525
+ /* @__PURE__ */ jsxs6(Row, { cursor, idx: 5, label: "Default focus", children: [
3526
+ /* @__PURE__ */ jsxs6(Text6, { dimColor: true, children: [
3527
+ glyphs().caretL,
2144
3528
  " "
2145
3529
  ] }),
2146
- /* @__PURE__ */ jsx5(Text5, { bold: true, color: "yellow", children: config2.defaultFocus === "all" ? "All" : "Last account" }),
2147
- /* @__PURE__ */ jsxs5(Text5, { dimColor: true, children: [
3530
+ /* @__PURE__ */ jsx6(Text6, { bold: true, color: "yellow", children: config2.defaultFocus === "all" ? "All" : "Last account" }),
3531
+ /* @__PURE__ */ jsxs6(Text6, { dimColor: true, children: [
2148
3532
  " ",
2149
- "\u25B8"
3533
+ glyphs().caretR
2150
3534
  ] })
2151
3535
  ] }),
2152
- /* @__PURE__ */ jsx5(Box5, { height: 1 }),
2153
- /* @__PURE__ */ jsx5(Text5, { bold: true, dimColor: true, children: "Providers" }),
3536
+ /* @__PURE__ */ jsx6(Box6, { height: 1 }),
3537
+ /* @__PURE__ */ jsx6(Text6, { bold: true, dimColor: true, children: "Providers" }),
2154
3538
  PROVIDER_ORDER.map((pid, i) => {
2155
3539
  const idx = PROVIDER_ROWS_START + i;
2156
3540
  const selected = cursor === idx;
2157
3541
  const enabled = !config2.disabledProviders.includes(pid);
2158
3542
  const p = PROVIDERS[pid];
2159
- return /* @__PURE__ */ jsxs5(Box5, { children: [
2160
- /* @__PURE__ */ jsxs5(Text5, { color: selected ? "green" : void 0, children: [
2161
- selected ? "\u25B8" : " ",
3543
+ return /* @__PURE__ */ jsxs6(Box6, { children: [
3544
+ /* @__PURE__ */ jsxs6(Text6, { color: selected ? "green" : void 0, children: [
3545
+ selected ? glyphs().caretR : " ",
3546
+ " "
3547
+ ] }),
3548
+ /* @__PURE__ */ jsx6(Text6, { bold: enabled, color: enabled ? p.color : void 0, dimColor: !enabled, children: enabled ? `[${glyphs().check}]` : "[ ]" }),
3549
+ /* @__PURE__ */ jsxs6(Text6, { color: p.color, children: [
3550
+ " ",
3551
+ glyphs().dot,
2162
3552
  " "
2163
3553
  ] }),
2164
- /* @__PURE__ */ jsx5(Text5, { bold: enabled, color: enabled ? p.color : void 0, dimColor: !enabled, children: enabled ? "[\u2713]" : "[ ]" }),
2165
- /* @__PURE__ */ jsx5(Text5, { color: p.color, children: " \u25CF " }),
2166
- /* @__PURE__ */ jsx5(Box5, { width: 9, children: /* @__PURE__ */ jsx5(Text5, { bold: selected, children: p.name }) }),
2167
- /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: enabled ? "tracking" : "off" })
3554
+ /* @__PURE__ */ jsx6(Box6, { width: 9, children: /* @__PURE__ */ jsx6(Text6, { bold: selected, children: p.name }) }),
3555
+ /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: enabled ? "tracking" : "off" })
2168
3556
  ] }, pid);
2169
3557
  }),
2170
- /* @__PURE__ */ jsx5(Box5, { height: 1 }),
2171
- /* @__PURE__ */ jsx5(Text5, { bold: true, dimColor: true, children: "Accounts" }),
2172
- config2.accounts.length === 0 && /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: " none configured \u2014 enabled providers track automatically" }),
3558
+ /* @__PURE__ */ jsx6(Box6, { height: 1 }),
3559
+ /* @__PURE__ */ jsx6(Text6, { bold: true, dimColor: true, children: "Accounts" }),
3560
+ config2.accounts.length === 0 && /* @__PURE__ */ jsxs6(Text6, { dimColor: true, children: [
3561
+ " none configured ",
3562
+ glyphs().emDash,
3563
+ " enabled providers track automatically"
3564
+ ] }),
2173
3565
  config2.accounts.map((acc, i) => {
2174
3566
  const idx = ACCOUNT_ROWS_START + i;
2175
3567
  const selected = cursor === idx;
2176
3568
  const isActive = acc.id === activeAccountId;
2177
3569
  const provider = PROVIDERS[acc.providerId];
2178
- return /* @__PURE__ */ jsxs5(Box5, { children: [
2179
- /* @__PURE__ */ jsxs5(Text5, { color: selected ? "green" : void 0, children: [
2180
- selected ? "\u25B8" : " ",
3570
+ return /* @__PURE__ */ jsxs6(Box6, { children: [
3571
+ /* @__PURE__ */ jsxs6(Text6, { color: selected ? "green" : void 0, children: [
3572
+ selected ? glyphs().caretR : " ",
2181
3573
  " "
2182
3574
  ] }),
2183
- /* @__PURE__ */ jsxs5(Text5, { color: acc.color || provider.color, children: [
2184
- isActive ? "\u25CF" : "\u25CB",
3575
+ /* @__PURE__ */ jsxs6(Text6, { color: acc.color || provider.color, children: [
3576
+ isActive ? glyphs().dot : glyphs().radioOff,
2185
3577
  " "
2186
3578
  ] }),
2187
- /* @__PURE__ */ jsx5(Box5, { width: 16, children: /* @__PURE__ */ jsx5(Text5, { bold: true, children: truncateName(acc.name, 15) }) }),
2188
- /* @__PURE__ */ jsx5(Box5, { width: 9, children: /* @__PURE__ */ jsx5(Text5, { color: provider.color, children: provider.name }) }),
2189
- /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: truncateName(acc.homeDir, 24) })
3579
+ /* @__PURE__ */ jsx6(Box6, { width: 16, children: /* @__PURE__ */ jsx6(Text6, { bold: true, children: truncateName(acc.name, 15) }) }),
3580
+ /* @__PURE__ */ jsx6(Box6, { width: 9, children: /* @__PURE__ */ jsx6(Text6, { color: provider.color, children: provider.name }) }),
3581
+ /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: truncateName(acc.homeDir, 24) })
2190
3582
  ] }, acc.id);
2191
3583
  }),
2192
- /* @__PURE__ */ jsxs5(Box5, { children: [
2193
- /* @__PURE__ */ jsxs5(Text5, { color: cursor === ACCOUNT_ROWS_START + config2.accounts.length ? "green" : void 0, children: [
2194
- cursor === ACCOUNT_ROWS_START + config2.accounts.length ? "\u25B8" : " ",
3584
+ /* @__PURE__ */ jsxs6(Box6, { children: [
3585
+ /* @__PURE__ */ jsxs6(Text6, { color: cursor === ACCOUNT_ROWS_START + config2.accounts.length ? "green" : void 0, children: [
3586
+ cursor === ACCOUNT_ROWS_START + config2.accounts.length ? glyphs().caretR : " ",
2195
3587
  " "
2196
3588
  ] }),
2197
- /* @__PURE__ */ jsx5(Text5, { color: "greenBright", children: "+ " }),
2198
- /* @__PURE__ */ jsx5(Text5, { children: "Add account" })
3589
+ /* @__PURE__ */ jsx6(Text6, { color: "greenBright", children: "+ " }),
3590
+ /* @__PURE__ */ jsx6(Text6, { children: "Add account" })
2199
3591
  ] }),
2200
- /* @__PURE__ */ jsx5(Box5, { height: 1 }),
2201
- editingTz ? /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: "type IANA name (e.g. Europe/London) \xB7 empty = System \xB7 Enter save \xB7 Esc cancel" }) : cursor >= PROVIDER_ROWS_START && cursor < ACCOUNT_ROWS_START ? /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: "\u2191\u2193 select \xB7 space toggle provider \xB7 s/Esc close" }) : cursor >= ACCOUNT_ROWS_START && cursor < ACCOUNT_ROWS_START + config2.accounts.length ? /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: "\u2191\u2193 select \xB7 \u21E7\u2191\u2193 reorder \xB7 Enter edit \xB7 space activate \xB7 d delete \xB7 s/Esc close" }) : cursor === ACCOUNT_ROWS_START + config2.accounts.length ? /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: "\u2191\u2193 select \xB7 Enter add account \xB7 s/Esc close" }) : /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: "\u2191\u2193 select \u2190\u2192 adjust Enter edit s/Esc close" })
3592
+ /* @__PURE__ */ jsx6(Box6, { height: 1 }),
3593
+ editingTz ? /* @__PURE__ */ jsxs6(Text6, { dimColor: true, children: [
3594
+ "type IANA name (e.g. Europe/London) ",
3595
+ glyphs().middot,
3596
+ " empty = System ",
3597
+ glyphs().middot,
3598
+ " Enter save ",
3599
+ glyphs().middot,
3600
+ " Esc cancel"
3601
+ ] }) : cursor >= PROVIDER_ROWS_START && cursor < ACCOUNT_ROWS_START ? /* @__PURE__ */ jsxs6(Text6, { dimColor: true, children: [
3602
+ glyphs().arrowU,
3603
+ glyphs().arrowD,
3604
+ " select ",
3605
+ glyphs().middot,
3606
+ " space toggle provider ",
3607
+ glyphs().middot,
3608
+ " s/Esc close"
3609
+ ] }) : cursor >= ACCOUNT_ROWS_START && cursor < ACCOUNT_ROWS_START + config2.accounts.length ? /* @__PURE__ */ jsxs6(Text6, { dimColor: true, children: [
3610
+ glyphs().arrowU,
3611
+ glyphs().arrowD,
3612
+ " select ",
3613
+ glyphs().middot,
3614
+ " ",
3615
+ glyphs().shift,
3616
+ glyphs().arrowU,
3617
+ glyphs().arrowD,
3618
+ " reorder ",
3619
+ glyphs().middot,
3620
+ " Enter edit ",
3621
+ glyphs().middot,
3622
+ " space activate ",
3623
+ glyphs().middot,
3624
+ " d delete ",
3625
+ glyphs().middot,
3626
+ " s/Esc close"
3627
+ ] }) : cursor === ACCOUNT_ROWS_START + config2.accounts.length ? /* @__PURE__ */ jsxs6(Text6, { dimColor: true, children: [
3628
+ glyphs().arrowU,
3629
+ glyphs().arrowD,
3630
+ " select ",
3631
+ glyphs().middot,
3632
+ " Enter add account ",
3633
+ glyphs().middot,
3634
+ " s/Esc close"
3635
+ ] }) : /* @__PURE__ */ jsxs6(Text6, { dimColor: true, children: [
3636
+ glyphs().arrowU,
3637
+ glyphs().arrowD,
3638
+ " select ",
3639
+ glyphs().arrowL,
3640
+ glyphs().arrowR,
3641
+ " adjust Enter edit s/Esc close"
3642
+ ] })
2202
3643
  ] });
2203
3644
  }
2204
3645
  function Row({ cursor, idx, label, children }) {
2205
- return /* @__PURE__ */ jsxs5(Box5, { children: [
2206
- /* @__PURE__ */ jsxs5(Text5, { color: cursor === idx ? "green" : void 0, children: [
2207
- cursor === idx ? "\u25B8" : " ",
3646
+ return /* @__PURE__ */ jsxs6(Box6, { children: [
3647
+ /* @__PURE__ */ jsxs6(Text6, { color: cursor === idx ? "green" : void 0, children: [
3648
+ cursor === idx ? glyphs().caretR : " ",
2208
3649
  " "
2209
3650
  ] }),
2210
- /* @__PURE__ */ jsx5(Box5, { width: 20, children: /* @__PURE__ */ jsx5(Text5, { children: label }) }),
3651
+ /* @__PURE__ */ jsx6(Box6, { width: 20, children: /* @__PURE__ */ jsx6(Text6, { children: label }) }),
2211
3652
  children
2212
3653
  ] });
2213
3654
  }
@@ -2216,24 +3657,24 @@ function AccountFormView({ form, accounts }) {
2216
3657
  const accent = form.color;
2217
3658
  const stepIndex = { provider: 1, name: 2, homeDir: 3, color: 4 };
2218
3659
  const step = stepIndex[form.field];
2219
- return /* @__PURE__ */ jsxs5(Box5, { flexDirection: "column", marginTop: 1, children: [
2220
- /* @__PURE__ */ jsxs5(Box5, { children: [
2221
- /* @__PURE__ */ jsx5(Text5, { color: accent, bold: true, children: "\u258D" }),
2222
- /* @__PURE__ */ jsxs5(Text5, { bold: true, children: [
3660
+ return /* @__PURE__ */ jsxs6(Box6, { flexDirection: "column", marginTop: 1, children: [
3661
+ /* @__PURE__ */ jsxs6(Box6, { children: [
3662
+ /* @__PURE__ */ jsx6(Text6, { color: accent, bold: true, children: glyphs().vbar }),
3663
+ /* @__PURE__ */ jsxs6(Text6, { bold: true, children: [
2223
3664
  " ",
2224
3665
  form.mode === "add" ? "NEW ACCOUNT" : "EDIT ACCOUNT"
2225
3666
  ] }),
2226
- /* @__PURE__ */ jsxs5(Text5, { dimColor: true, children: [
3667
+ /* @__PURE__ */ jsxs6(Text6, { dimColor: true, children: [
2227
3668
  " step ",
2228
3669
  step,
2229
3670
  " of 4"
2230
3671
  ] })
2231
3672
  ] }),
2232
- /* @__PURE__ */ jsx5(Box5, { marginTop: 1, children: /* @__PURE__ */ jsx5(Stepper, { active: form.field, accent }) }),
2233
- /* @__PURE__ */ jsxs5(Box5, { marginTop: 1, flexDirection: "column", borderStyle: "round", borderColor: accent, paddingX: 2, paddingY: 1, children: [
2234
- /* @__PURE__ */ jsx5(ProviderField, { value: form.providerId, focused: form.field === "provider" }),
2235
- /* @__PURE__ */ jsx5(Box5, { height: 1 }),
2236
- /* @__PURE__ */ jsx5(
3673
+ /* @__PURE__ */ jsx6(Box6, { marginTop: 1, children: /* @__PURE__ */ jsx6(Stepper, { active: form.field, accent }) }),
3674
+ /* @__PURE__ */ jsxs6(Box6, { marginTop: 1, flexDirection: "column", borderStyle: glyphs().border, borderColor: accent, paddingX: 2, paddingY: 1, children: [
3675
+ /* @__PURE__ */ jsx6(ProviderField, { value: form.providerId, focused: form.field === "provider" }),
3676
+ /* @__PURE__ */ jsx6(Box6, { height: 1 }),
3677
+ /* @__PURE__ */ jsx6(
2237
3678
  FormField,
2238
3679
  {
2239
3680
  label: "Name",
@@ -2244,12 +3685,12 @@ function AccountFormView({ form, accounts }) {
2244
3685
  placeholder: "e.g. Work, Personal"
2245
3686
  }
2246
3687
  ),
2247
- /* @__PURE__ */ jsx5(Box5, { height: 1 }),
2248
- /* @__PURE__ */ jsx5(
3688
+ /* @__PURE__ */ jsx6(Box6, { height: 1 }),
3689
+ /* @__PURE__ */ jsx6(
2249
3690
  FormField,
2250
3691
  {
2251
3692
  label: "Home directory",
2252
- hint: "path containing the tool's data dir \xB7 ~ for default",
3693
+ hint: `path containing the tool's data dir ${glyphs().middot} ~ for default`,
2253
3694
  value: form.homeDir,
2254
3695
  focused: form.field === "homeDir",
2255
3696
  accent,
@@ -2257,100 +3698,134 @@ function AccountFormView({ form, accounts }) {
2257
3698
  mono: true
2258
3699
  }
2259
3700
  ),
2260
- /* @__PURE__ */ jsx5(Box5, { height: 1 }),
2261
- /* @__PURE__ */ jsx5(ColorField, { value: form.color, focused: form.field === "color" }),
2262
- /* @__PURE__ */ jsx5(Box5, { height: 1 }),
2263
- /* @__PURE__ */ jsxs5(Box5, { children: [
2264
- /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: "id \u2524 " }),
2265
- /* @__PURE__ */ jsx5(Text5, { bold: true, color: accent, children: previewId || "account" }),
2266
- /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: " \u251C auto-generated from name" })
3701
+ /* @__PURE__ */ jsx6(Box6, { height: 1 }),
3702
+ /* @__PURE__ */ jsx6(ColorField, { value: form.color, focused: form.field === "color" }),
3703
+ /* @__PURE__ */ jsx6(Box6, { height: 1 }),
3704
+ /* @__PURE__ */ jsxs6(Box6, { children: [
3705
+ /* @__PURE__ */ jsxs6(Text6, { dimColor: true, children: [
3706
+ "id ",
3707
+ glyphs().boxMark,
3708
+ " "
3709
+ ] }),
3710
+ /* @__PURE__ */ jsx6(Text6, { bold: true, color: accent, children: previewId || "account" }),
3711
+ /* @__PURE__ */ jsxs6(Text6, { dimColor: true, children: [
3712
+ " ",
3713
+ glyphs().boxMark,
3714
+ " auto-generated from name"
3715
+ ] })
2267
3716
  ] })
2268
3717
  ] }),
2269
- form.error && /* @__PURE__ */ jsx5(Box5, { marginTop: 1, children: /* @__PURE__ */ jsxs5(Text5, { color: "red", children: [
2270
- "\u26A0 ",
3718
+ form.error && /* @__PURE__ */ jsx6(Box6, { marginTop: 1, children: /* @__PURE__ */ jsxs6(Text6, { color: "red", children: [
3719
+ glyphs().warn,
3720
+ " ",
2271
3721
  form.error
2272
3722
  ] }) }),
2273
- /* @__PURE__ */ jsxs5(Box5, { marginTop: 1, children: [
2274
- /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: "tab/\u2191\u2193 " }),
2275
- /* @__PURE__ */ jsx5(Text5, { children: "switch field" }),
2276
- /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: " \xB7 " }),
2277
- /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: "enter " }),
2278
- /* @__PURE__ */ jsx5(Text5, { children: form.field === "color" ? "save" : "next" }),
2279
- /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: " \xB7 " }),
2280
- (form.field === "color" || form.field === "provider") && /* @__PURE__ */ jsxs5(Fragment3, { children: [
2281
- /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: "\u2190\u2192 " }),
2282
- /* @__PURE__ */ jsx5(Text5, { children: form.field === "provider" ? "pick provider" : "pick color" }),
2283
- /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: " \xB7 " })
3723
+ /* @__PURE__ */ jsxs6(Box6, { marginTop: 1, children: [
3724
+ /* @__PURE__ */ jsxs6(Text6, { dimColor: true, children: [
3725
+ "tab/",
3726
+ glyphs().arrowU,
3727
+ glyphs().arrowD,
3728
+ " "
3729
+ ] }),
3730
+ /* @__PURE__ */ jsx6(Text6, { children: "switch field" }),
3731
+ /* @__PURE__ */ jsxs6(Text6, { dimColor: true, children: [
3732
+ " ",
3733
+ glyphs().middot,
3734
+ " "
2284
3735
  ] }),
2285
- /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: "esc " }),
2286
- /* @__PURE__ */ jsx5(Text5, { children: "cancel" })
3736
+ /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: "enter " }),
3737
+ /* @__PURE__ */ jsx6(Text6, { children: form.field === "color" ? "save" : "next" }),
3738
+ /* @__PURE__ */ jsxs6(Text6, { dimColor: true, children: [
3739
+ " ",
3740
+ glyphs().middot,
3741
+ " "
3742
+ ] }),
3743
+ (form.field === "color" || form.field === "provider") && /* @__PURE__ */ jsxs6(Fragment3, { children: [
3744
+ /* @__PURE__ */ jsxs6(Text6, { dimColor: true, children: [
3745
+ glyphs().arrowL,
3746
+ glyphs().arrowR,
3747
+ " "
3748
+ ] }),
3749
+ /* @__PURE__ */ jsx6(Text6, { children: form.field === "provider" ? "pick provider" : "pick color" }),
3750
+ /* @__PURE__ */ jsxs6(Text6, { dimColor: true, children: [
3751
+ " ",
3752
+ glyphs().middot,
3753
+ " "
3754
+ ] })
3755
+ ] }),
3756
+ /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: "esc " }),
3757
+ /* @__PURE__ */ jsx6(Text6, { children: "cancel" })
2287
3758
  ] })
2288
3759
  ] });
2289
3760
  }
2290
- function Stepper({ active, accent }) {
3761
+ function Stepper({ active: active2, accent }) {
2291
3762
  const steps = [
2292
3763
  { id: "provider", label: "Provider" },
2293
3764
  { id: "name", label: "Name" },
2294
3765
  { id: "homeDir", label: "Home" },
2295
3766
  { id: "color", label: "Color" }
2296
3767
  ];
2297
- const activeIdx = steps.findIndex((s) => s.id === active);
2298
- return /* @__PURE__ */ jsx5(Box5, { children: steps.map((s, i) => {
3768
+ const activeIdx = steps.findIndex((s) => s.id === active2);
3769
+ return /* @__PURE__ */ jsx6(Box6, { children: steps.map((s, i) => {
2299
3770
  const done = i < activeIdx;
2300
3771
  const cur = i === activeIdx;
2301
- const dot = done ? "\u25CF" : cur ? "\u25C9" : "\u25CB";
2302
- return /* @__PURE__ */ jsxs5(Box5, { children: [
2303
- /* @__PURE__ */ jsxs5(Text5, { color: cur || done ? accent : void 0, dimColor: !cur && !done, children: [
3772
+ const dot = done ? glyphs().dot : cur ? glyphs().dotSel : glyphs().radioOff;
3773
+ return /* @__PURE__ */ jsxs6(Box6, { children: [
3774
+ /* @__PURE__ */ jsxs6(Text6, { color: cur || done ? accent : void 0, dimColor: !cur && !done, children: [
2304
3775
  dot,
2305
3776
  " "
2306
3777
  ] }),
2307
- /* @__PURE__ */ jsx5(Text5, { bold: cur, color: cur ? accent : void 0, dimColor: !cur, children: s.label }),
2308
- i < steps.length - 1 && /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: " \u2500 " })
3778
+ /* @__PURE__ */ jsx6(Text6, { bold: cur, color: cur ? accent : void 0, dimColor: !cur, children: s.label }),
3779
+ i < steps.length - 1 && /* @__PURE__ */ jsxs6(Text6, { dimColor: true, children: [
3780
+ " ",
3781
+ glyphs().rule,
3782
+ " "
3783
+ ] })
2309
3784
  ] }, s.id);
2310
3785
  }) });
2311
3786
  }
2312
3787
  function ProviderField({ value, focused }) {
2313
- return /* @__PURE__ */ jsxs5(Box5, { flexDirection: "column", children: [
2314
- /* @__PURE__ */ jsx5(Box5, { children: /* @__PURE__ */ jsxs5(Text5, { color: focused ? PROVIDERS[value].color : void 0, bold: focused, dimColor: !focused, children: [
2315
- focused ? "\u25B8" : " ",
3788
+ return /* @__PURE__ */ jsxs6(Box6, { flexDirection: "column", children: [
3789
+ /* @__PURE__ */ jsx6(Box6, { children: /* @__PURE__ */ jsxs6(Text6, { color: focused ? PROVIDERS[value].color : void 0, bold: focused, dimColor: !focused, children: [
3790
+ focused ? glyphs().caretR : " ",
2316
3791
  " Provider"
2317
3792
  ] }) }),
2318
- /* @__PURE__ */ jsxs5(Box5, { children: [
2319
- /* @__PURE__ */ jsxs5(Text5, { children: [
3793
+ /* @__PURE__ */ jsxs6(Box6, { children: [
3794
+ /* @__PURE__ */ jsxs6(Text6, { children: [
2320
3795
  " ",
2321
- focused ? "\u258C" : " ",
3796
+ focused ? glyphs().vbar : " ",
2322
3797
  " "
2323
3798
  ] }),
2324
3799
  PROVIDER_ORDER.map((pid) => {
2325
3800
  const selected = pid === value;
2326
3801
  const p = PROVIDERS[pid];
2327
- return /* @__PURE__ */ jsx5(Box5, { marginRight: 2, children: selected ? /* @__PURE__ */ jsxs5(Text5, { bold: true, color: p.color, children: [
3802
+ return /* @__PURE__ */ jsx6(Box6, { marginRight: 2, children: selected ? /* @__PURE__ */ jsxs6(Text6, { bold: true, color: p.color, children: [
2328
3803
  "[",
2329
3804
  p.name,
2330
3805
  "]"
2331
- ] }) : /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: p.name }) }, pid);
3806
+ ] }) : /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: p.name }) }, pid);
2332
3807
  })
2333
3808
  ] }),
2334
- /* @__PURE__ */ jsx5(Box5, { children: /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: " which tool this account tracks" }) })
3809
+ /* @__PURE__ */ jsx6(Box6, { children: /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: " which tool this account tracks" }) })
2335
3810
  ] });
2336
3811
  }
2337
3812
  function FormField({ label, hint, value, focused, accent, placeholder, mono }) {
2338
3813
  const isPlaceholder = value === "";
2339
3814
  const display = isPlaceholder ? placeholder : value;
2340
- return /* @__PURE__ */ jsxs5(Box5, { flexDirection: "column", children: [
2341
- /* @__PURE__ */ jsx5(Box5, { children: /* @__PURE__ */ jsxs5(Text5, { color: focused ? accent : void 0, bold: focused, dimColor: !focused, children: [
2342
- focused ? "\u25B8" : " ",
3815
+ return /* @__PURE__ */ jsxs6(Box6, { flexDirection: "column", children: [
3816
+ /* @__PURE__ */ jsx6(Box6, { children: /* @__PURE__ */ jsxs6(Text6, { color: focused ? accent : void 0, bold: focused, dimColor: !focused, children: [
3817
+ focused ? glyphs().caretR : " ",
2343
3818
  " ",
2344
3819
  label
2345
3820
  ] }) }),
2346
- /* @__PURE__ */ jsxs5(Box5, { children: [
2347
- /* @__PURE__ */ jsxs5(Text5, { color: focused ? accent : void 0, children: [
3821
+ /* @__PURE__ */ jsxs6(Box6, { children: [
3822
+ /* @__PURE__ */ jsxs6(Text6, { color: focused ? accent : void 0, children: [
2348
3823
  " ",
2349
- focused ? "\u258C" : " ",
3824
+ focused ? glyphs().vbar : " ",
2350
3825
  " "
2351
3826
  ] }),
2352
- /* @__PURE__ */ jsx5(
2353
- Text5,
3827
+ /* @__PURE__ */ jsx6(
3828
+ Text6,
2354
3829
  {
2355
3830
  bold: focused && !isPlaceholder,
2356
3831
  color: focused && !isPlaceholder ? accent : void 0,
@@ -2359,39 +3834,59 @@ function FormField({ label, hint, value, focused, accent, placeholder, mono }) {
2359
3834
  children: display
2360
3835
  }
2361
3836
  ),
2362
- focused && /* @__PURE__ */ jsx5(Text5, { color: accent, children: "\u258F" })
3837
+ focused && /* @__PURE__ */ jsx6(Text6, { color: accent, children: glyphs().vbar })
2363
3838
  ] }),
2364
- /* @__PURE__ */ jsx5(Box5, { children: /* @__PURE__ */ jsxs5(Text5, { dimColor: true, children: [
3839
+ /* @__PURE__ */ jsx6(Box6, { children: /* @__PURE__ */ jsxs6(Text6, { dimColor: true, children: [
2365
3840
  " ",
2366
3841
  hint
2367
3842
  ] }) })
2368
3843
  ] });
2369
3844
  }
2370
3845
  function ColorField({ value, focused }) {
2371
- return /* @__PURE__ */ jsxs5(Box5, { flexDirection: "column", children: [
2372
- /* @__PURE__ */ jsx5(Box5, { children: /* @__PURE__ */ jsxs5(Text5, { color: focused ? value : void 0, bold: focused, dimColor: !focused, children: [
2373
- focused ? "\u25B8" : " ",
3846
+ return /* @__PURE__ */ jsxs6(Box6, { flexDirection: "column", children: [
3847
+ /* @__PURE__ */ jsx6(Box6, { children: /* @__PURE__ */ jsxs6(Text6, { color: focused ? value : void 0, bold: focused, dimColor: !focused, children: [
3848
+ focused ? glyphs().caretR : " ",
2374
3849
  " Accent color"
2375
3850
  ] }) }),
2376
- /* @__PURE__ */ jsxs5(Box5, { children: [
2377
- /* @__PURE__ */ jsxs5(Text5, { children: [
3851
+ /* @__PURE__ */ jsxs6(Box6, { children: [
3852
+ /* @__PURE__ */ jsxs6(Text6, { children: [
2378
3853
  " ",
2379
- focused ? "\u258C" : " ",
3854
+ focused ? glyphs().vbar : " ",
2380
3855
  " "
2381
3856
  ] }),
2382
- COLOR_PALETTE.map((c) => /* @__PURE__ */ jsx5(Box5, { marginRight: 1, children: c === value ? /* @__PURE__ */ jsx5(Text5, { bold: true, color: c, children: "[\u25CF]" }) : /* @__PURE__ */ jsx5(Text5, { color: c, dimColor: !focused, children: " \u25CF" }) }, c))
3857
+ COLOR_PALETTE.map((c) => /* @__PURE__ */ jsx6(Box6, { marginRight: 1, children: c === value ? /* @__PURE__ */ jsxs6(Text6, { bold: true, color: c, children: [
3858
+ "[",
3859
+ glyphs().dot,
3860
+ "]"
3861
+ ] }) : /* @__PURE__ */ jsxs6(Text6, { color: c, dimColor: !focused, children: [
3862
+ " ",
3863
+ glyphs().dot
3864
+ ] }) }, c))
2383
3865
  ] }),
2384
- /* @__PURE__ */ jsx5(Box5, { children: /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: " shows on dashboard, account strip, borders" }) })
3866
+ /* @__PURE__ */ jsx6(Box6, { children: /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: " shows on dashboard, account strip, borders" }) })
2385
3867
  ] });
2386
3868
  }
2387
3869
 
2388
3870
  // src/app.tsx
2389
- import { Fragment as Fragment4, jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
3871
+ import { Fragment as Fragment4, jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
2390
3872
  var TABS = ["Dashboard", "Table"];
2391
3873
  var VIEWS = ["Daily", "Weekly", "Monthly"];
2392
- var SORTS = ["date \u2191", "date \u2193", "cost \u2191", "cost \u2193"];
2393
- var CURSOR_SORTS = ["cost \u2193", "amount \u2193", "model"];
3874
+ var SORTS = [
3875
+ { label: "date", dir: "up" },
3876
+ { label: "date", dir: "down" },
3877
+ { label: "cost", dir: "up" },
3878
+ { label: "cost", dir: "down" }
3879
+ ];
3880
+ var CURSOR_SORTS = [
3881
+ { label: "cost", dir: "down" },
3882
+ { label: "amount", dir: "down" },
3883
+ { label: "model", dir: null }
3884
+ ];
2394
3885
  var IS_TTY = process.stdin.isTTY === true;
3886
+ var DEBOUNCE_MS = 300;
3887
+ var LOADER_GRACE_MS = 600;
3888
+ var LOADER_MAX_MS = 8e3;
3889
+ var LOADER_MIN_VISIBLE_MS = 700;
2395
3890
  var DEFAULT_CONFIG = {
2396
3891
  interval: 2,
2397
3892
  billingInterval: 5,
@@ -2402,33 +3897,46 @@ var DEFAULT_CONFIG = {
2402
3897
  disabledProviders: [],
2403
3898
  onboarded: false,
2404
3899
  dashboardLayout: "grid",
2405
- defaultFocus: "all"
3900
+ defaultFocus: "all",
3901
+ ascii: "auto",
3902
+ knownProviders: []
2406
3903
  };
2407
- function App({ interval: cliInterval }) {
2408
- const [config2, setConfig] = useState2(null);
2409
- const [detected, setDetected] = useState2([]);
2410
- const [stats, setStats] = useState2(/* @__PURE__ */ new Map());
2411
- const [peak, setPeak] = useState2(null);
2412
- const [table, setTable] = useState2(null);
2413
- const [tableLoading, setTableLoading] = useState2(false);
2414
- const [error, setError] = useState2(null);
2415
- const [updated, setUpdated] = useState2(/* @__PURE__ */ new Date());
2416
- const [tab, setTab] = useState2(0);
2417
- const [view, setView] = useState2(0);
2418
- const [cursor, setCursor] = useState2(0);
2419
- const [expanded, setExpanded] = useState2(-1);
2420
- const [sort, setSort] = useState2(1);
2421
- const [tableProvider, setTableProvider] = useState2(null);
2422
- const [search, setSearch] = useState2("");
2423
- const [searchMode, setSearchMode] = useState2(false);
2424
- const [cursorRows, setCursorRows] = useState2(null);
2425
- const [showSettings, setShowSettings] = useState2(false);
2426
- const [settingsCursor, setSettingsCursor] = useState2(0);
2427
- const [tzEdit, setTzEdit] = useState2(null);
2428
- const [tzError, setTzError] = useState2(null);
2429
- const [accountForm, setAccountForm] = useState2(null);
2430
- const [onboardSel, setOnboardSel] = useState2(null);
2431
- const [onboardCursor, setOnboardCursor] = useState2(0);
3904
+ function applyStartup(c, cliInterval) {
3905
+ if (cliInterval) c = { ...c, interval: cliInterval / 1e3 };
3906
+ if (c.defaultFocus === "all") c = { ...c, activeAccountId: null };
3907
+ return c;
3908
+ }
3909
+ function App({ interval: cliInterval, initialConfig }) {
3910
+ const [config2, setConfig] = useState3(() => initialConfig ? applyStartup(initialConfig, cliInterval) : null);
3911
+ const [detected, setDetected] = useState3([]);
3912
+ const [stats, setStats] = useState3(/* @__PURE__ */ new Map());
3913
+ const [peak, setPeak] = useState3(null);
3914
+ const [table, setTable] = useState3(null);
3915
+ const [tableLoading, setTableLoading] = useState3(false);
3916
+ const [error, setError] = useState3(null);
3917
+ const [updated, setUpdated] = useState3(/* @__PURE__ */ new Date());
3918
+ const [tab, setTab] = useState3(0);
3919
+ const [view, setView] = useState3(0);
3920
+ const [cursor, setCursor] = useState3(0);
3921
+ const [expanded, setExpanded] = useState3(-1);
3922
+ const [sort, setSort] = useState3(1);
3923
+ const [tableProvider, setTableProvider] = useState3(null);
3924
+ const [search, setSearch] = useState3("");
3925
+ const [searchMode, setSearchMode] = useState3(false);
3926
+ const [cursorRows, setCursorRows] = useState3(null);
3927
+ const [showSettings, setShowSettings] = useState3(false);
3928
+ const [settingsCursor, setSettingsCursor] = useState3(0);
3929
+ const [tzEdit, setTzEdit] = useState3(null);
3930
+ const [tzError, setTzError] = useState3(null);
3931
+ const [accountForm, setAccountForm] = useState3(null);
3932
+ const [onboardSel, setOnboardSel] = useState3(null);
3933
+ const [onboardCursor, setOnboardCursor] = useState3(0);
3934
+ const [dashPage, setDashPage] = useState3(0);
3935
+ const [debouncePassed, setDebouncePassed] = useState3(false);
3936
+ const [graceHold, setGraceHold] = useState3(false);
3937
+ const [loaderShownAt, setLoaderShownAt] = useState3(null);
3938
+ const loaderDone = useRef2(false);
3939
+ const prevShowPicker = useRef2(false);
2432
3940
  const { stdout } = useStdout();
2433
3941
  const { exit } = useApp();
2434
3942
  const rows = stdout?.rows ?? 24;
@@ -2442,6 +3950,8 @@ function App({ interval: cliInterval }) {
2442
3950
  const accountsRef = useRef2([]);
2443
3951
  accountsRef.current = accounts;
2444
3952
  const rowCountRef = useRef2(0);
3953
+ const dashPageCountRef = useRef2(1);
3954
+ const seededRef = useRef2(false);
2445
3955
  const accountsKey = accounts.map((a) => `${a.id}:${a.homeDir ?? ""}`).join("|");
2446
3956
  const slots = accounts.length > 1 ? [{ id: null, name: "All", color: "whiteBright" }, ...accounts.map((a) => ({ id: a.id, name: a.name, color: a.color }))] : accounts.map((a) => ({ id: a.id, name: a.name, color: a.color }));
2447
3957
  const activeSlotIdx = (() => {
@@ -2452,31 +3962,117 @@ function App({ interval: cliInterval }) {
2452
3962
  const focusId = slots[activeSlotIdx]?.id ?? null;
2453
3963
  const visibleAccounts = focusId === null ? accounts : accounts.filter((a) => a.id === focusId);
2454
3964
  const groups = accountsByProvider(visibleAccounts);
3965
+ const TOO_SMALL = cols < 40 || rows < 12;
3966
+ const allGroups = accountsByProvider(accounts);
3967
+ const allReady = accounts.length > 0 && accounts.every((a) => accountReady(stats.get(a.id), a.providerId));
3968
+ const hasStrip = slots.length > 1;
3969
+ const stripChipW = (s) => 2 + 2 + truncateName(s.name, 16).length + 2;
3970
+ const stripChars = slots.reduce((sum, s) => sum + stripChipW(s), 0);
3971
+ const stripLines = hasStrip ? Math.max(1, Math.ceil(stripChars / Math.max(
3972
+ 1,
3973
+ cols - 4 - 7
3974
+ /*"focus "*/
3975
+ ))) : 0;
3976
+ const headerRows = cols < 70 ? 2 : 1;
3977
+ const CHROME = 2 + headerRows + 3 + (hasStrip ? 1 + stripLines : 0) + 2 + 2;
3978
+ const gridBudget = Math.max(1, rows - CHROME);
3979
+ const dashLayout = chooseLayout(
3980
+ Math.max(56, cols - 4),
3981
+ gridBudget,
3982
+ groups.length,
3983
+ focusId !== null || cfg.dashboardLayout === "single",
3984
+ cols
3985
+ );
3986
+ const dashPageCount = dashLayout.pageCount;
3987
+ const dashPaginated = dashPageCount > 1;
3988
+ dashPageCountRef.current = dashPageCount;
2455
3989
  const tableProvs = accountsByProvider(accounts).map((g) => g.provider);
2456
3990
  const effTableProvider = tableProvider && tableProvs.includes(tableProvider) ? tableProvider : tableProvs[0] ?? null;
2457
3991
  const tableIsCursor = !!effTableProvider && !PROVIDERS[effTableProvider].hasUsage;
2458
3992
  const tableAccounts = effTableProvider ? accounts.filter((a) => a.providerId === effTableProvider) : [];
2459
3993
  const SORTS_FOR = tableIsCursor ? CURSOR_SORTS : SORTS;
2460
3994
  const needsOnboarding = configReady && !cfg.onboarded;
3995
+ const newProviders = configReady && cfg.onboarded ? PROVIDER_ORDER.filter((p) => !cfg.knownProviders.includes(p) && detected.includes(p)) : [];
3996
+ const showPicker = needsOnboarding || newProviders.length > 0;
3997
+ const minVisibleHold = loaderShownAt !== null && Date.now() - loaderShownAt < LOADER_MIN_VISIBLE_MS;
3998
+ const showLoader = configReady && !showPicker && !showSettings && !TOO_SMALL && accounts.length > 0 && (!allReady || graceHold || minVisibleHold) && (debouncePassed || loaderShownAt !== null) && !loaderDone.current;
3999
+ const pickerProviders = needsOnboarding ? PROVIDER_ORDER : newProviders;
2461
4000
  const onboardEnabled = onboardSel ?? detected;
2462
- const onboardItems = PROVIDER_ORDER.map((pid) => ({
4001
+ const onboardItems = pickerProviders.map((pid) => ({
2463
4002
  id: pid,
2464
4003
  name: PROVIDERS[pid].name,
2465
4004
  color: PROVIDERS[pid].color,
2466
4005
  detected: detected.includes(pid),
2467
4006
  enabled: onboardEnabled.includes(pid)
2468
4007
  }));
2469
- useEffect2(() => {
2470
- loadConfig().then((c) => {
2471
- if (cliInterval) c = { ...c, interval: cliInterval / 1e3 };
2472
- if (c.defaultFocus === "all") c = { ...c, activeAccountId: null };
2473
- setConfig(c);
2474
- });
4008
+ useEffect3(() => {
4009
+ const wasPicker = prevShowPicker.current;
4010
+ prevShowPicker.current = showPicker;
4011
+ if (wasPicker && !showPicker) {
4012
+ loaderDone.current = false;
4013
+ setDebouncePassed(false);
4014
+ setGraceHold(false);
4015
+ setLoaderShownAt(null);
4016
+ }
4017
+ }, [showPicker]);
4018
+ useEffect3(() => {
4019
+ if (showLoader && loaderShownAt === null) setLoaderShownAt(Date.now());
4020
+ }, [showLoader, loaderShownAt]);
4021
+ useEffect3(() => {
4022
+ if (!initialConfig) loadConfig().then((c) => setConfig(applyStartup(c, cliInterval)));
2475
4023
  detectProviders().then(setDetected);
2476
4024
  }, []);
2477
- useEffect2(() => {
2478
- if (!configReady) return;
2479
- let active = true;
4025
+ useEffect3(() => {
4026
+ if (seededRef.current || !configReady || showPicker || accounts.length === 0) return;
4027
+ seededRef.current = true;
4028
+ loadSnapshot().then((snap) => {
4029
+ setStats((prev) => {
4030
+ if (prev.size > 0) return prev;
4031
+ const next = new Map(prev);
4032
+ for (const acc of accountsRef.current) {
4033
+ const s = snap[acc.id];
4034
+ if (s && (s.dashboard || s.billing)) next.set(acc.id, { account: acc, dashboard: s.dashboard ?? null, billing: s.billing ?? null });
4035
+ }
4036
+ return next;
4037
+ });
4038
+ });
4039
+ }, [configReady, showPicker, accountsKey]);
4040
+ useEffect3(() => {
4041
+ if (stats.size === 0) return;
4042
+ const t = setTimeout(() => saveSnapshot(stats), 500);
4043
+ return () => clearTimeout(t);
4044
+ }, [stats]);
4045
+ useEffect3(() => {
4046
+ if (!configReady || showPicker || accounts.length === 0) return;
4047
+ if (allReady || loaderDone.current) return;
4048
+ const debounce = setTimeout(() => setDebouncePassed(true), DEBOUNCE_MS);
4049
+ const deadline = setTimeout(() => {
4050
+ loaderDone.current = true;
4051
+ setDebouncePassed(false);
4052
+ }, LOADER_MAX_MS);
4053
+ return () => {
4054
+ clearTimeout(debounce);
4055
+ clearTimeout(deadline);
4056
+ };
4057
+ }, [configReady, showPicker, accountsKey]);
4058
+ useEffect3(() => {
4059
+ if (!allReady || loaderDone.current) return;
4060
+ if (loaderShownAt === null) {
4061
+ loaderDone.current = true;
4062
+ return;
4063
+ }
4064
+ setGraceHold(true);
4065
+ const minRemaining = Math.max(0, LOADER_MIN_VISIBLE_MS - (Date.now() - loaderShownAt));
4066
+ const hold = Math.max(LOADER_GRACE_MS, minRemaining);
4067
+ const t = setTimeout(() => {
4068
+ loaderDone.current = true;
4069
+ setGraceHold(false);
4070
+ }, hold);
4071
+ return () => clearTimeout(t);
4072
+ }, [allReady]);
4073
+ useEffect3(() => {
4074
+ if (!configReady || showPicker) return;
4075
+ let active2 = true;
2480
4076
  let timer;
2481
4077
  const load = async () => {
2482
4078
  try {
@@ -2485,27 +4081,27 @@ function App({ interval: cliInterval }) {
2485
4081
  if (!provider.hasUsage || !provider.fetchSummary) return;
2486
4082
  try {
2487
4083
  const dashboard = await provider.fetchSummary(acc, tz);
2488
- if (active) setStats((prev) => upsert(prev, acc, { dashboard }));
4084
+ if (active2) setStats((prev) => upsert(prev, acc, { dashboard }));
2489
4085
  } catch {
2490
4086
  }
2491
4087
  }));
2492
- if (active) {
4088
+ if (active2) {
2493
4089
  setError(null);
2494
4090
  setUpdated(/* @__PURE__ */ new Date());
2495
4091
  }
2496
4092
  } finally {
2497
- if (active) timer = setTimeout(load, interval2);
4093
+ if (active2) timer = setTimeout(load, interval2);
2498
4094
  }
2499
4095
  };
2500
4096
  load();
2501
4097
  return () => {
2502
- active = false;
4098
+ active2 = false;
2503
4099
  clearTimeout(timer);
2504
4100
  };
2505
- }, [interval2, tz, configReady, accountsKey]);
2506
- useEffect2(() => {
2507
- if (!configReady) return;
2508
- let active = true;
4101
+ }, [interval2, tz, configReady, showPicker, accountsKey]);
4102
+ useEffect3(() => {
4103
+ if (!configReady || showPicker) return;
4104
+ let active2 = true;
2509
4105
  let timer;
2510
4106
  const load = async () => {
2511
4107
  try {
@@ -2515,42 +4111,42 @@ function App({ interval: cliInterval }) {
2515
4111
  if (!provider.hasBilling || !provider.fetchBilling) return;
2516
4112
  try {
2517
4113
  const billing = await provider.fetchBilling(acc);
2518
- if (active) setStats((prev) => upsert(prev, acc, { billing }));
4114
+ if (active2) setStats((prev) => upsert(prev, acc, { billing }));
2519
4115
  } catch {
2520
4116
  }
2521
4117
  }));
2522
4118
  const p = await peakP;
2523
- if (active && p) setPeak(p);
4119
+ if (active2 && p) setPeak(p);
2524
4120
  } finally {
2525
- if (active) timer = setTimeout(load, billingMs);
4121
+ if (active2) timer = setTimeout(load, billingMs);
2526
4122
  }
2527
4123
  };
2528
4124
  load();
2529
4125
  return () => {
2530
- active = false;
4126
+ active2 = false;
2531
4127
  clearTimeout(timer);
2532
4128
  };
2533
- }, [billingMs, configReady, accountsKey]);
4129
+ }, [billingMs, configReady, showPicker, accountsKey]);
2534
4130
  const tableKey = `${effTableProvider}|${tableAccounts.map((a) => `${a.id}:${a.homeDir ?? ""}`).join(",")}|${tz}`;
2535
- useEffect2(() => {
4131
+ useEffect3(() => {
2536
4132
  setTable(null);
2537
4133
  setCursorRows(null);
2538
4134
  setCursor(0);
2539
4135
  setExpanded(-1);
2540
4136
  setSort(tableIsCursor ? 0 : 1);
2541
4137
  }, [tableKey]);
2542
- useEffect2(() => {
4138
+ useEffect3(() => {
2543
4139
  if (tab !== 1 || !effTableProvider) return;
2544
- let active = true;
4140
+ let active2 = true;
2545
4141
  let timer;
2546
4142
  const fetchOnce = async () => {
2547
4143
  try {
2548
4144
  if (tableIsCursor) {
2549
4145
  const s = await cursorModelSpend(tableAccounts[0]?.homeDir);
2550
- if (active) setCursorRows(s?.models ?? []);
4146
+ if (active2) setCursorRows(s?.models ?? []);
2551
4147
  } else {
2552
4148
  const r = await fetchScopeTable(tableAccounts, tz);
2553
- if (active) setTable(r);
4149
+ if (active2) setTable(r);
2554
4150
  }
2555
4151
  } catch {
2556
4152
  }
@@ -2558,35 +4154,43 @@ function App({ interval: cliInterval }) {
2558
4154
  const run = async () => {
2559
4155
  setTableLoading(true);
2560
4156
  await fetchOnce();
2561
- if (!active) return;
4157
+ if (!active2) return;
2562
4158
  setTableLoading(false);
2563
4159
  const loop = async () => {
2564
4160
  await fetchOnce();
2565
- if (active) timer = setTimeout(loop, Math.max(interval2, 1e4));
4161
+ if (active2) timer = setTimeout(loop, Math.max(interval2, 1e4));
2566
4162
  };
2567
4163
  timer = setTimeout(loop, Math.max(interval2, 1e4));
2568
4164
  };
2569
4165
  run();
2570
4166
  return () => {
2571
- active = false;
4167
+ active2 = false;
2572
4168
  clearTimeout(timer);
2573
4169
  };
2574
4170
  }, [tab, tableKey, interval2]);
2575
- useEffect2(() => {
4171
+ useEffect3(() => {
2576
4172
  setCursor(0);
2577
4173
  setExpanded(-1);
2578
4174
  }, [search]);
4175
+ useEffect3(() => {
4176
+ setDashPage((p) => Math.min(p, dashPageCount - 1));
4177
+ }, [dashPageCount]);
2579
4178
  const resetView = useCallback(() => {
2580
4179
  setCursor(0);
2581
4180
  setExpanded(-1);
2582
4181
  }, []);
2583
4182
  const clampRow = (n) => Math.max(0, Math.min(rowCountRef.current - 1, n));
2584
4183
  const mouse = useMouse();
2585
- useEffect2(() => {
4184
+ useEffect3(() => {
2586
4185
  if (!IS_TTY) return;
2587
4186
  mouse.enable();
2588
4187
  const onScroll = (_pos, dir) => {
2589
- if (tab === 1) setCursor((c) => dir === "scrollup" ? Math.max(0, c - 3) : c + 3);
4188
+ const up = dir === "scrollup";
4189
+ if (tab === 1) {
4190
+ setCursor((c) => up ? Math.max(0, c - 3) : c + 3);
4191
+ } else if (tab === 0 && dashPageCountRef.current > 1) {
4192
+ setDashPage((p) => up ? Math.max(0, p - 1) : Math.min(dashPageCountRef.current - 1, p + 1));
4193
+ }
2590
4194
  };
2591
4195
  mouse.events.on("scroll", onScroll);
2592
4196
  return () => {
@@ -2601,8 +4205,8 @@ function App({ interval: cliInterval }) {
2601
4205
  });
2602
4206
  }
2603
4207
  function toggleOnboard(i) {
2604
- if (i < 0 || i >= PROVIDER_ORDER.length) return;
2605
- const pid = PROVIDER_ORDER[i];
4208
+ if (i < 0 || i >= pickerProviders.length) return;
4209
+ const pid = pickerProviders[i];
2606
4210
  setOnboardSel((prev) => {
2607
4211
  const base = prev ?? detected;
2608
4212
  return base.includes(pid) ? base.filter((p) => p !== pid) : [...base, pid];
@@ -2611,16 +4215,31 @@ function App({ interval: cliInterval }) {
2611
4215
  function toggleProvider(pid) {
2612
4216
  updateConfig((c) => ({
2613
4217
  ...c,
4218
+ // Toggling in settings is also an explicit decision → mark it known.
4219
+ knownProviders: c.knownProviders.includes(pid) ? c.knownProviders : [...c.knownProviders, pid],
2614
4220
  disabledProviders: c.disabledProviders.includes(pid) ? c.disabledProviders.filter((p) => p !== pid) : [...c.disabledProviders, pid]
2615
4221
  }));
2616
4222
  }
2617
4223
  function confirmOnboarding() {
2618
4224
  const enabled = onboardEnabled;
2619
- updateConfig((c) => ({
2620
- ...c,
2621
- disabledProviders: PROVIDER_ORDER.filter((p) => !enabled.includes(p)),
2622
- onboarded: true
2623
- }));
4225
+ updateConfig((c) => {
4226
+ if (!c.onboarded) {
4227
+ return {
4228
+ ...c,
4229
+ disabledProviders: PROVIDER_ORDER.filter((p) => !enabled.includes(p)),
4230
+ knownProviders: [...PROVIDER_ORDER],
4231
+ onboarded: true
4232
+ };
4233
+ }
4234
+ const newlyDisabled = pickerProviders.filter((p) => !enabled.includes(p));
4235
+ return {
4236
+ ...c,
4237
+ disabledProviders: [.../* @__PURE__ */ new Set([...c.disabledProviders, ...newlyDisabled])],
4238
+ knownProviders: [.../* @__PURE__ */ new Set([...c.knownProviders, ...pickerProviders])]
4239
+ };
4240
+ });
4241
+ setOnboardSel(null);
4242
+ setOnboardCursor(0);
2624
4243
  }
2625
4244
  function cycleAccount(dir) {
2626
4245
  if (slots.length <= 1) return;
@@ -2724,12 +4343,12 @@ function App({ interval: cliInterval }) {
2724
4343
  }
2725
4344
  const totalSettingsRows = ACCOUNT_ROWS_START + cfg.accounts.length + 1;
2726
4345
  useInput((input, key) => {
2727
- if (needsOnboarding) {
4346
+ if (showPicker) {
2728
4347
  if (input === "q") {
2729
4348
  exit();
2730
4349
  return;
2731
4350
  }
2732
- const startIdx = PROVIDER_ORDER.length;
4351
+ const startIdx = pickerProviders.length;
2733
4352
  if (key.upArrow) {
2734
4353
  setOnboardCursor((c) => Math.max(0, c - 1));
2735
4354
  return;
@@ -2968,6 +4587,16 @@ function App({ interval: cliInterval }) {
2968
4587
  }
2969
4588
  return;
2970
4589
  }
4590
+ if (tab === 0 && dashPaginated) {
4591
+ if (input === "]" || key.downArrow || key.pageDown) {
4592
+ setDashPage((p) => Math.min(dashPageCount - 1, p + 1));
4593
+ return;
4594
+ }
4595
+ if (input === "[" || key.upArrow || key.pageUp) {
4596
+ setDashPage((p) => Math.max(0, p - 1));
4597
+ return;
4598
+ }
4599
+ }
2971
4600
  if (tab === 1) {
2972
4601
  if (input === "p") {
2973
4602
  cycleTableProvider(1);
@@ -3046,36 +4675,58 @@ function App({ interval: cliInterval }) {
3046
4675
  return;
3047
4676
  }
3048
4677
  }, { isActive: IS_TTY });
3049
- if (error) return /* @__PURE__ */ jsx6(Box6, { padding: 1, children: /* @__PURE__ */ jsx6(Text6, { color: "red", children: error }) });
3050
- if (!config2) return /* @__PURE__ */ jsx6(Box6, { padding: 1, children: /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: "Loading..." }) });
3051
- if (needsOnboarding) {
3052
- return /* @__PURE__ */ jsx6(Box6, { flexDirection: "column", paddingX: 2, paddingY: 1, height: rows, children: /* @__PURE__ */ jsx6(Onboarding, { items: onboardItems, cursor: onboardCursor, onToggle: toggleOnboard, onConfirm: confirmOnboarding }) });
4678
+ if (error) return /* @__PURE__ */ jsx7(Box7, { padding: 1, children: /* @__PURE__ */ jsx7(Text7, { color: "red", children: error }) });
4679
+ if (!config2) return /* @__PURE__ */ jsx7(Box7, { padding: 1, children: /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: "Loading..." }) });
4680
+ if (showPicker) {
4681
+ return /* @__PURE__ */ jsx7(Box7, { flexDirection: "column", paddingX: 2, paddingY: 1, height: rows, children: /* @__PURE__ */ jsx7(
4682
+ Onboarding,
4683
+ {
4684
+ items: onboardItems,
4685
+ cursor: onboardCursor,
4686
+ onToggle: toggleOnboard,
4687
+ onConfirm: confirmOnboarding,
4688
+ heading: needsOnboarding ? "Welcome to tokmon" : "New providers detected",
4689
+ subheading: needsOnboarding ? "Pick the tools you want to track. You can change this anytime in settings." : "tokmon found these installed since you last set up. Pick which to track."
4690
+ }
4691
+ ) });
4692
+ }
4693
+ if (showLoader) {
4694
+ return /* @__PURE__ */ jsx7(Box7, { flexDirection: "column", paddingX: 2, paddingY: 1, height: rows, overflow: "hidden", children: /* @__PURE__ */ jsx7(LoadingView, { groups: allGroups, stats, cols, rows }) });
4695
+ }
4696
+ if (TOO_SMALL && !showSettings) {
4697
+ return /* @__PURE__ */ jsx7(TinyFallback, { groups, stats, rows, cols });
3053
4698
  }
3054
4699
  const tokenRows = sortRows(filterTokenRows(table ? [table.daily, table.weekly, table.monthly][view] : [], search), sort);
3055
4700
  const cursorTableRows = sortCursorRows(filterCursorRows(cursorRows ?? [], search), sort);
3056
4701
  rowCountRef.current = tableIsCursor ? cursorTableRows.length : tokenRows.length;
3057
- return /* @__PURE__ */ jsxs6(Box6, { flexDirection: "column", paddingX: 2, paddingY: 1, height: rows, children: [
3058
- /* @__PURE__ */ jsxs6(Box6, { justifyContent: "space-between", children: [
3059
- /* @__PURE__ */ jsxs6(Box6, { children: [
3060
- /* @__PURE__ */ jsxs6(Text6, { bold: true, color: "greenBright", children: [
3061
- "\u25C9",
4702
+ return /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", paddingX: 2, paddingY: 1, height: rows, overflow: "hidden", children: [
4703
+ /* @__PURE__ */ jsxs7(Box7, { justifyContent: "space-between", children: [
4704
+ /* @__PURE__ */ jsxs7(Box7, { children: [
4705
+ /* @__PURE__ */ jsxs7(Text7, { bold: true, color: "greenBright", children: [
4706
+ glyphs().dotSel,
3062
4707
  " tokmon"
3063
4708
  ] }),
3064
- /* @__PURE__ */ jsxs6(Text6, { dimColor: true, children: [
3065
- " \xB7 every ",
4709
+ /* @__PURE__ */ jsxs7(Text7, { dimColor: true, children: [
4710
+ " ",
4711
+ glyphs().middot,
4712
+ " every ",
3066
4713
  cfg.interval,
3067
4714
  "s"
3068
4715
  ] })
3069
4716
  ] }),
3070
- /* @__PURE__ */ jsxs6(Box6, { children: [
3071
- peak && /* @__PURE__ */ jsxs6(Fragment4, { children: [
3072
- /* @__PURE__ */ jsx6(PeakBadge, { peak }),
3073
- /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: " \xB7 " })
4717
+ /* @__PURE__ */ jsxs7(Box7, { children: [
4718
+ peak && /* @__PURE__ */ jsxs7(Fragment4, { children: [
4719
+ /* @__PURE__ */ jsx7(PeakBadge, { peak }),
4720
+ /* @__PURE__ */ jsxs7(Text7, { dimColor: true, children: [
4721
+ " ",
4722
+ glyphs().middot,
4723
+ " "
4724
+ ] })
3074
4725
  ] }),
3075
- /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: time(updated, tz) })
4726
+ /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: time(updated, tz) })
3076
4727
  ] })
3077
4728
  ] }),
3078
- showSettings ? /* @__PURE__ */ jsx6(
4729
+ showSettings ? /* @__PURE__ */ jsx7(
3079
4730
  SettingsView,
3080
4731
  {
3081
4732
  config: cfg,
@@ -3086,19 +4737,23 @@ function App({ interval: cliInterval }) {
3086
4737
  accountForm,
3087
4738
  activeAccountId: cfg.activeAccountId
3088
4739
  }
3089
- ) : /* @__PURE__ */ jsxs6(Fragment4, { children: [
3090
- /* @__PURE__ */ jsxs6(Box6, { marginTop: 1, marginBottom: 1, children: [
3091
- /* @__PURE__ */ jsx6(TabBar, { tabs: TABS, active: tab, onSelect: (i) => {
4740
+ ) : /* @__PURE__ */ jsxs7(Fragment4, { children: [
4741
+ /* @__PURE__ */ jsxs7(Box7, { marginTop: 1, marginBottom: 1, children: [
4742
+ /* @__PURE__ */ jsx7(TabBar, { tabs: TABS, active: tab, onSelect: (i) => {
3092
4743
  setTab(i);
3093
4744
  resetView();
3094
4745
  } }),
3095
- /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: " Tab/\u2190\u2192" })
4746
+ /* @__PURE__ */ jsxs7(Text7, { dimColor: true, children: [
4747
+ " Tab/",
4748
+ glyphs().arrowL,
4749
+ glyphs().arrowR
4750
+ ] })
3096
4751
  ] }),
3097
- tab === 0 && /* @__PURE__ */ jsxs6(Fragment4, { children: [
3098
- /* @__PURE__ */ jsx6(DashboardView, { groups, stats, cols, focusId, layout: cfg.dashboardLayout }),
3099
- slots.length > 1 && /* @__PURE__ */ jsxs6(Box6, { marginTop: 1, children: [
3100
- /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: "focus " }),
3101
- /* @__PURE__ */ jsx6(
4752
+ tab === 0 && /* @__PURE__ */ jsxs7(Fragment4, { children: [
4753
+ /* @__PURE__ */ jsx7(DashboardView, { groups, stats, cols, budget: gridBudget, focusId, layout: cfg.dashboardLayout, page: dashPage }),
4754
+ slots.length > 1 && /* @__PURE__ */ jsxs7(Box7, { marginTop: 1, children: [
4755
+ /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: "focus " }),
4756
+ /* @__PURE__ */ jsx7(
3102
4757
  AccountStrip,
3103
4758
  {
3104
4759
  slots,
@@ -3109,30 +4764,35 @@ function App({ interval: cliInterval }) {
3109
4764
  }
3110
4765
  }
3111
4766
  )
3112
- ] })
4767
+ ] }),
4768
+ /* @__PURE__ */ jsx7(TotalsRow, { groups, stats, cols })
3113
4769
  ] }),
3114
- tab === 1 && /* @__PURE__ */ jsxs6(Fragment4, { children: [
3115
- tableProvs.length > 0 && /* @__PURE__ */ jsx6(TableProviderBar, { providers: tableProvs, active: effTableProvider, onSelect: (p) => {
4770
+ tab === 1 && /* @__PURE__ */ jsxs7(Fragment4, { children: [
4771
+ tableProvs.length > 0 && /* @__PURE__ */ jsx7(TableProviderBar, { providers: tableProvs, active: effTableProvider, onSelect: (p) => {
3116
4772
  setTableProvider(p);
3117
4773
  setCursor(0);
3118
4774
  setExpanded(-1);
3119
4775
  setSearch("");
3120
4776
  setSearchMode(false);
3121
4777
  } }),
3122
- /* @__PURE__ */ jsx6(Box6, { height: 1 }),
3123
- /* @__PURE__ */ jsx6(
4778
+ /* @__PURE__ */ jsx7(Box7, { height: 1 }),
4779
+ /* @__PURE__ */ jsx7(
3124
4780
  ControlBar,
3125
4781
  {
3126
4782
  views: VIEWS,
3127
4783
  period: view,
3128
- sort: SORTS_FOR[sort % SORTS_FOR.length],
4784
+ sort: sortLabel(SORTS_FOR[sort % SORTS_FOR.length]),
3129
4785
  search,
3130
4786
  searching: searchMode,
3131
4787
  showPeriod: !tableIsCursor
3132
4788
  }
3133
4789
  ),
3134
- /* @__PURE__ */ jsx6(Box6, { height: 1 }),
3135
- !effTableProvider ? /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: "No providers enabled \u2014 press s to pick providers." }) : tableLoading && !table && !cursorRows ? /* @__PURE__ */ jsx6(Spinner, { label: "Loading history" }) : tableIsCursor ? /* @__PURE__ */ jsx6(
4790
+ /* @__PURE__ */ jsx7(Box7, { height: 1 }),
4791
+ !effTableProvider ? /* @__PURE__ */ jsxs7(Text7, { dimColor: true, children: [
4792
+ "No providers enabled ",
4793
+ glyphs().emDash,
4794
+ " press s to pick providers."
4795
+ ] }) : tableLoading && !table && !cursorRows ? /* @__PURE__ */ jsx7(Spinner, { label: "Loading history" }) : tableIsCursor ? /* @__PURE__ */ jsx7(
3136
4796
  CursorSpendTable,
3137
4797
  {
3138
4798
  rows: cursorTableRows,
@@ -3140,7 +4800,7 @@ function App({ interval: cliInterval }) {
3140
4800
  maxRows: Math.max(1, rows - 16),
3141
4801
  onRowClick: (idx) => setCursor(idx)
3142
4802
  }
3143
- ) : /* @__PURE__ */ jsx6(
4803
+ ) : /* @__PURE__ */ jsx7(
3144
4804
  TokenTable,
3145
4805
  {
3146
4806
  rows: tokenRows,
@@ -3156,7 +4816,7 @@ function App({ interval: cliInterval }) {
3156
4816
  )
3157
4817
  ] })
3158
4818
  ] }),
3159
- (tab === 0 || showSettings) && /* @__PURE__ */ jsx6(Footer, { hasAccounts: slots.length > 1 })
4819
+ (tab === 0 || showSettings) && /* @__PURE__ */ jsx7(Footer, { hasAccounts: slots.length > 1, paginated: tab === 0 && dashPaginated, cols })
3160
4820
  ] });
3161
4821
  }
3162
4822
  function upsert(prev, account, patch) {
@@ -3180,6 +4840,11 @@ async function fetchScopeTable(scope, tz) {
3180
4840
  if (valid.length === 1) return valid[0];
3181
4841
  return mergeTables(valid);
3182
4842
  }
4843
+ function sortLabel(entry) {
4844
+ if (entry.dir === "up") return `${entry.label} ${glyphs().arrowU}`;
4845
+ if (entry.dir === "down") return `${entry.label} ${glyphs().arrowD}`;
4846
+ return entry.label;
4847
+ }
3183
4848
  function sortRows(rows, sortIdx) {
3184
4849
  const sorted = [...rows];
3185
4850
  switch (sortIdx % SORTS.length) {
@@ -3217,47 +4882,113 @@ function sortCursorRows(rows, sortIdx) {
3217
4882
  }
3218
4883
  }
3219
4884
  function AccountStrip({ slots, activeIdx, onSelect }) {
3220
- return /* @__PURE__ */ jsx6(Box6, { flexWrap: "wrap", children: slots.map((s, i) => {
3221
- const active = i === activeIdx;
3222
- const dot = s.id === null ? "\u2726" : "\u25CF";
4885
+ return /* @__PURE__ */ jsx7(Box7, { flexWrap: "wrap", children: slots.map((s, i) => {
4886
+ const active2 = i === activeIdx;
4887
+ const dot = s.id === null ? glyphs().dotAll : glyphs().dot;
3223
4888
  const label = truncateName(s.name, 16);
3224
- return /* @__PURE__ */ jsxs6(ClickableBox, { onClick: () => onSelect(i), marginRight: 2, children: [
3225
- /* @__PURE__ */ jsx6(Text6, { dimColor: !active, children: i }),
3226
- /* @__PURE__ */ jsx6(Text6, { children: " " }),
3227
- /* @__PURE__ */ jsx6(Text6, { color: s.color, bold: active, dimColor: !active, children: dot }),
3228
- /* @__PURE__ */ jsx6(Text6, { children: " " }),
3229
- active ? /* @__PURE__ */ jsx6(Text6, { bold: true, color: s.color, children: label }) : /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: label })
4889
+ return /* @__PURE__ */ jsxs7(ClickableBox, { onClick: () => onSelect(i), marginRight: 2, children: [
4890
+ /* @__PURE__ */ jsx7(Text7, { dimColor: !active2, children: i }),
4891
+ /* @__PURE__ */ jsx7(Text7, { children: " " }),
4892
+ /* @__PURE__ */ jsx7(Text7, { color: s.color, bold: active2, dimColor: !active2, children: dot }),
4893
+ /* @__PURE__ */ jsx7(Text7, { children: " " }),
4894
+ active2 ? /* @__PURE__ */ jsx7(Text7, { bold: true, color: s.color, children: label }) : /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: label })
3230
4895
  ] }, s.id ?? "__all__");
3231
4896
  }) });
3232
4897
  }
3233
- function Footer({ hasAccounts }) {
3234
- return /* @__PURE__ */ jsxs6(Box6, { marginTop: 1, children: [
3235
- /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: "by " }),
3236
- /* @__PURE__ */ jsx6(Text6, { children: "David Ilie" }),
3237
- /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: " (" }),
3238
- /* @__PURE__ */ jsx6(Text6, { color: "cyan", children: "davidilie.com" }),
3239
- /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: ") \xB7 s=settings " }),
3240
- hasAccounts && /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: "0-9=jump a/A=cycle " }),
3241
- /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: "q=quit" })
4898
+ function Footer({ hasAccounts, paginated, cols }) {
4899
+ const inner = cols - 4;
4900
+ const BASE2 = "by David Ilie (davidilie.com) \xB7 s=settings q=quit".length;
4901
+ const JUMP = "0-9=jump a/A=cycle ".length;
4902
+ const PAGE = "scroll=page ".length;
4903
+ const showJump = hasAccounts && inner >= BASE2 + JUMP + (paginated ? PAGE : 0);
4904
+ const showPage = paginated && inner >= BASE2 + (showJump ? JUMP : 0) + PAGE;
4905
+ return /* @__PURE__ */ jsxs7(Box7, { marginTop: 1, flexWrap: "nowrap", children: [
4906
+ /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: "by " }),
4907
+ /* @__PURE__ */ jsx7(Text7, { children: "David Ilie" }),
4908
+ /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: " (" }),
4909
+ /* @__PURE__ */ jsx7(Text7, { color: "cyan", children: "davidilie.com" }),
4910
+ /* @__PURE__ */ jsxs7(Text7, { dimColor: true, children: [
4911
+ ") ",
4912
+ glyphs().middot,
4913
+ " s=settings "
4914
+ ] }),
4915
+ showJump && /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: "0-9=jump a/A=cycle " }),
4916
+ showPage && /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: "scroll=page " }),
4917
+ /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: "q=quit" })
4918
+ ] });
4919
+ }
4920
+ function TinyFallback({ groups, stats, rows, cols }) {
4921
+ const maxLines = Math.max(1, rows - 4);
4922
+ const visible = groups.slice(0, maxLines);
4923
+ const hidden = groups.length - visible.length;
4924
+ const w = Math.max(8, cols - 2);
4925
+ return /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", paddingX: 1, height: rows, overflow: "hidden", children: [
4926
+ /* @__PURE__ */ jsxs7(Text7, { bold: true, color: "greenBright", children: [
4927
+ glyphs().dotSel,
4928
+ " tokmon"
4929
+ ] }),
4930
+ groups.length === 0 ? /* @__PURE__ */ jsxs7(Text7, { dimColor: true, children: [
4931
+ "No providers ",
4932
+ glyphs().emDash,
4933
+ " s=settings"
4934
+ ] }) : visible.map((g) => /* @__PURE__ */ jsx7(TinyRow, { provider: g.provider, accounts: g.accounts, stats, width: w }, g.provider)),
4935
+ hidden > 0 && /* @__PURE__ */ jsxs7(Text7, { dimColor: true, children: [
4936
+ "+",
4937
+ hidden,
4938
+ " more (enlarge terminal)"
4939
+ ] }),
4940
+ /* @__PURE__ */ jsx7(Box7, { flexGrow: 1 }),
4941
+ /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: "s=settings q=quit" })
4942
+ ] });
4943
+ }
4944
+ function TinyRow({ provider, accounts, stats, width }) {
4945
+ const meta = PROVIDERS[provider];
4946
+ const dashboards = accounts.map((a) => stats.get(a.id)?.dashboard).filter(Boolean);
4947
+ const billings = accounts.map((a) => stats.get(a.id)?.billing).filter(Boolean);
4948
+ const todayCost = dashboards.reduce((sum, d) => sum + (d?.today.cost ?? 0), 0);
4949
+ const pctMetric = billings.flatMap((b) => b?.metrics ?? []).find((m) => m.format.kind === "percent");
4950
+ const detail = meta.hasUsage ? `${currency(todayCost)} today` : pctMetric ? `${Math.round(pctMetric.used)}%` : "billing";
4951
+ const name = truncateName(meta.name, Math.max(4, width - 18));
4952
+ return /* @__PURE__ */ jsxs7(Box7, { width, children: [
4953
+ /* @__PURE__ */ jsxs7(Text7, { color: meta.color, children: [
4954
+ glyphs().dot,
4955
+ " "
4956
+ ] }),
4957
+ /* @__PURE__ */ jsx7(Text7, { bold: true, color: meta.color, children: name }),
4958
+ /* @__PURE__ */ jsx7(Box7, { flexGrow: 1 }),
4959
+ /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: detail })
3242
4960
  ] });
3243
4961
  }
3244
4962
 
3245
4963
  // src/cli.tsx
3246
- import { jsx as jsx7 } from "react/jsx-runtime";
4964
+ import { jsx as jsx8 } from "react/jsx-runtime";
3247
4965
  process.on("unhandledRejection", () => {
3248
4966
  });
4967
+ EventEmitter.defaultMaxListeners = 100;
4968
+ var emitWarning = process.emitWarning.bind(process);
4969
+ process.emitWarning = ((warning, ...rest) => {
4970
+ const msg = typeof warning === "string" ? warning : warning?.message;
4971
+ if (typeof msg === "string" && /SQLite is an experimental feature/i.test(msg)) return;
4972
+ return emitWarning(warning, ...rest);
4973
+ });
3249
4974
  var args = process.argv.slice(2);
3250
4975
  var interval;
4976
+ var asciiFlag = null;
3251
4977
  for (let i = 0; i < args.length; i++) {
3252
4978
  if ((args[i] === "--interval" || args[i] === "-i") && args[i + 1]) {
3253
4979
  interval = Math.max(500, Number(args[i + 1]) * 1e3);
3254
4980
  i++;
3255
4981
  }
4982
+ if (args[i] === "--ascii") asciiFlag = "on";
4983
+ if (args[i] === "--no-ascii") asciiFlag = "off";
3256
4984
  if (args[i] === "--help" || args[i] === "-h") {
3257
- console.log("tokmon - Terminal dashboard for Claude, Codex, and Cursor usage\n");
4985
+ console.log("tokmon - Terminal usage dashboard for your AI coding tools\n");
4986
+ console.log(" Claude \xB7 Codex \xB7 Cursor \xB7 Copilot \xB7 opencode \xB7 pi \xB7 Antigravity \xB7 Gemini\n");
3258
4987
  console.log("Usage: tokmon [options]\n");
3259
4988
  console.log("Options:");
3260
4989
  console.log(" -i, --interval <seconds> Refresh interval (default: from config or 2)");
4990
+ console.log(" --ascii Force ASCII glyphs (also: TOKMON_ASCII=1)");
4991
+ console.log(" --no-ascii Force Unicode glyphs");
3261
4992
  console.log(" -h, --help Show this help\n");
3262
4993
  console.log("Keybindings:");
3263
4994
  console.log(" Tab Switch Dashboard / Table");
@@ -3274,6 +5005,13 @@ var config = await loadConfig();
3274
5005
  if (config.clearScreen && process.stdout.isTTY) {
3275
5006
  process.stdout.write("\x1B[2J\x1B[H");
3276
5007
  }
3277
- var { waitUntilExit } = render(/* @__PURE__ */ jsx7(MouseProvider, { children: /* @__PURE__ */ jsx7(App, { interval }) }));
5008
+ setGlyphs(resolveGlyphs({
5009
+ flag: asciiFlag,
5010
+ env: process.env,
5011
+ config: config.ascii,
5012
+ isTTY: !!process.stdout.isTTY,
5013
+ platform: process.platform
5014
+ }));
5015
+ var { waitUntilExit } = render(/* @__PURE__ */ jsx8(MouseProvider, { children: /* @__PURE__ */ jsx8(App, { interval, initialConfig: config }) }));
3278
5016
  await waitUntilExit();
3279
5017
  await flushDisk();