tokmon 0.13.0 → 0.14.0

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 +2001 -392
  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,846 @@ 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 readdir6 } from "fs/promises";
1991
+ import { join as join12 } from "path";
1992
+ import { homedir as homedir10 } from "os";
1993
+
1994
+ // src/providers/cloud-code.ts
1995
+ var CLOUD_CODE_URLS = [
1996
+ "https://daily-cloudcode-pa.googleapis.com",
1997
+ "https://cloudcode-pa.googleapis.com"
1998
+ ];
1999
+ var LOAD_CODE_ASSIST_PATH = "/v1internal:loadCodeAssist";
2000
+ var FETCH_MODELS_PATH = "/v1internal:fetchAvailableModels";
2001
+ var RETRIEVE_QUOTA_PATH = "/v1internal:retrieveUserQuota";
2002
+ var GOOGLE_OAUTH_URL = "https://oauth2.googleapis.com/token";
2003
+ var GOOGLE_CLIENT_ID = process.env.TOKMON_GOOGLE_CLIENT_ID ?? "";
2004
+ var GOOGLE_CLIENT_SECRET = process.env.TOKMON_GOOGLE_CLIENT_SECRET ?? "";
2005
+ var OAUTH_TOKEN_KEY = "antigravityUnifiedStateSync.oauthToken";
2006
+ var OAUTH_TOKEN_SENTINEL = "oauthTokenInfoSentinelKey";
2007
+ var CC_MODEL_BLACKLIST = {
2008
+ MODEL_CHAT_20706: true,
2009
+ MODEL_CHAT_23310: true,
2010
+ MODEL_GOOGLE_GEMINI_2_5_FLASH: true,
2011
+ MODEL_GOOGLE_GEMINI_2_5_FLASH_THINKING: true,
2012
+ MODEL_GOOGLE_GEMINI_2_5_FLASH_LITE: true,
2013
+ MODEL_GOOGLE_GEMINI_2_5_PRO: true,
2014
+ MODEL_PLACEHOLDER_M19: true,
2015
+ MODEL_PLACEHOLDER_M9: true,
2016
+ MODEL_PLACEHOLDER_M12: true
2017
+ };
2018
+ function readVarint(bytes, start) {
2019
+ let value = 0;
2020
+ let shift = 0;
2021
+ let pos3 = start;
2022
+ while (pos3 < bytes.length) {
2023
+ const b = bytes[pos3++];
2024
+ value += (b & 127) * Math.pow(2, shift);
2025
+ if ((b & 128) === 0) return { value, pos: pos3 };
2026
+ shift += 7;
2027
+ }
2028
+ return null;
2029
+ }
2030
+ function readFields(bytes) {
2031
+ const fields = {};
2032
+ let pos3 = 0;
2033
+ while (pos3 < bytes.length) {
2034
+ const tag = readVarint(bytes, pos3);
2035
+ if (!tag) break;
2036
+ pos3 = tag.pos;
2037
+ const fieldNum = Math.floor(tag.value / 8);
2038
+ const wireType = tag.value % 8;
2039
+ if (wireType === 0) {
2040
+ const val = readVarint(bytes, pos3);
2041
+ if (!val) break;
2042
+ fields[fieldNum] = { type: 0, value: val.value };
2043
+ pos3 = val.pos;
2044
+ } else if (wireType === 1) {
2045
+ if (pos3 + 8 > bytes.length) break;
2046
+ pos3 += 8;
2047
+ } else if (wireType === 2) {
2048
+ const len = readVarint(bytes, pos3);
2049
+ if (!len) break;
2050
+ pos3 = len.pos;
2051
+ if (pos3 + len.value > bytes.length) break;
2052
+ fields[fieldNum] = { type: 2, data: bytes.slice(pos3, pos3 + len.value) };
2053
+ pos3 += len.value;
2054
+ } else if (wireType === 5) {
2055
+ if (pos3 + 4 > bytes.length) break;
2056
+ pos3 += 4;
2057
+ } else {
2058
+ break;
2059
+ }
2060
+ }
2061
+ return fields;
2062
+ }
2063
+ function utf8(data) {
2064
+ return Buffer.from(data).toString("utf8");
2065
+ }
2066
+ function decodeBase64(text) {
2067
+ try {
2068
+ return Buffer.from(text, "base64");
2069
+ } catch {
2070
+ return null;
2071
+ }
2072
+ }
2073
+ function unwrapKeyringBase64(raw) {
2074
+ const text = raw.trim();
2075
+ if (!text.startsWith("go-keyring-base64:")) return text;
2076
+ const decoded = decodeBase64(text.slice("go-keyring-base64:".length));
2077
+ return decoded ? utf8(decoded).trim() : text;
2078
+ }
2079
+ function unwrapOAuthSentinel(base64Text) {
2080
+ const outerBytes = decodeBase64(unwrapKeyringBase64(base64Text));
2081
+ if (!outerBytes) return null;
2082
+ const outer = readFields(outerBytes);
2083
+ if (outer[1]?.type !== 2 || !outer[1].data) return null;
2084
+ const wrapper = readFields(outer[1].data);
2085
+ const sentinel = wrapper[1]?.type === 2 && wrapper[1].data ? utf8(wrapper[1].data) : null;
2086
+ const payload = wrapper[2]?.type === 2 ? wrapper[2].data : null;
2087
+ if (sentinel !== OAUTH_TOKEN_SENTINEL || !payload) return null;
2088
+ const payloadFields = readFields(payload);
2089
+ if (payloadFields[1]?.type !== 2 || !payloadFields[1].data) return null;
2090
+ const innerText = utf8(payloadFields[1].data).trim();
2091
+ return innerText ? decodeBase64(innerText) : null;
2092
+ }
2093
+ async function readAntigravityOAuthToken(db) {
2094
+ const r = await runSqlite(db, "SELECT value FROM ItemTable WHERE key=? LIMIT 1;", [OAUTH_TOKEN_KEY]);
2095
+ if (r.status !== "ok") return { token: null, status: r.status };
2096
+ const raw = r.rows[0]?.value;
2097
+ if (typeof raw !== "string" || !raw.trim()) return { token: null, status: "ok" };
2098
+ const inner = unwrapOAuthSentinel(raw);
2099
+ if (!inner) return { token: null, status: "ok" };
2100
+ const fields = readFields(inner);
2101
+ const accessToken = fields[1]?.type === 2 && fields[1].data ? utf8(fields[1].data) : null;
2102
+ const refreshToken = fields[3]?.type === 2 && fields[3].data ? utf8(fields[3].data) : null;
2103
+ let expirySeconds = null;
2104
+ if (fields[4]?.type === 2 && fields[4].data) {
2105
+ const ts = readFields(fields[4].data);
2106
+ expirySeconds = ts[1]?.type === 0 && typeof ts[1].value === "number" ? ts[1].value : null;
2107
+ }
2108
+ if (!accessToken && !refreshToken) return { token: null, status: "ok" };
2109
+ return { token: { accessToken, refreshToken, expirySeconds }, status: "ok" };
2110
+ }
2111
+ function redact(token) {
2112
+ if (!token) return "none";
2113
+ return `...${token.slice(-4)}`;
2114
+ }
2115
+ async function refreshAccessToken(refreshToken) {
2116
+ if (!refreshToken || !GOOGLE_CLIENT_ID || !GOOGLE_CLIENT_SECRET) return null;
2117
+ const body = new URLSearchParams({
2118
+ client_id: GOOGLE_CLIENT_ID,
2119
+ client_secret: GOOGLE_CLIENT_SECRET,
2120
+ refresh_token: refreshToken,
2121
+ grant_type: "refresh_token"
2122
+ });
2123
+ try {
2124
+ const res = await fetch(GOOGLE_OAUTH_URL, {
2125
+ method: "POST",
2126
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
2127
+ body,
2128
+ signal: AbortSignal.timeout(15e3)
2129
+ });
2130
+ if (!res.ok) return null;
2131
+ const json = await readJson(res);
2132
+ return typeof json?.access_token === "string" && json.access_token.trim() ? json.access_token.trim() : null;
2133
+ } catch {
2134
+ return null;
2135
+ }
2136
+ }
2137
+ async function requestCloudCodeJson(path, token, body) {
2138
+ for (const base of CLOUD_CODE_URLS) {
2139
+ try {
2140
+ const res = await fetch(`${base}${path}`, {
2141
+ method: "POST",
2142
+ headers: {
2143
+ Accept: "application/json",
2144
+ "Content-Type": "application/json",
2145
+ Authorization: `Bearer ${token}`,
2146
+ "User-Agent": "agy"
2147
+ },
2148
+ body: JSON.stringify(body ?? {}),
2149
+ signal: AbortSignal.timeout(15e3)
2150
+ });
2151
+ if (res.status === 401 || res.status === 403) return { _authFailed: true };
2152
+ if (!res.ok) continue;
2153
+ const json = await readJson(res);
2154
+ if (json && typeof json === "object") return json;
2155
+ } catch {
2156
+ }
2157
+ }
2158
+ return null;
2159
+ }
2160
+ function readPlan(loadData) {
2161
+ const paid = typeof loadData?.paidTier?.name === "string" ? loadData.paidTier.name.trim() : "";
2162
+ if (paid) return paid;
2163
+ const current = typeof loadData?.currentTier?.name === "string" ? loadData.currentTier.name.trim() : "";
2164
+ return current || null;
2165
+ }
2166
+ function parseBuckets(data) {
2167
+ if (!Array.isArray(data?.buckets)) return [];
2168
+ return data.buckets.flatMap((bucket) => {
2169
+ const modelId = typeof bucket?.modelId === "string" ? bucket.modelId.trim() : "";
2170
+ if (!modelId) return [];
2171
+ return [{
2172
+ modelId,
2173
+ remainingFraction: typeof bucket.remainingFraction === "number" ? bucket.remainingFraction : 0,
2174
+ resetTime: typeof bucket.resetTime === "string" ? bucket.resetTime : void 0
2175
+ }];
2176
+ });
2177
+ }
2178
+ function parseModelBuckets(data) {
2179
+ const models = data?.models;
2180
+ if (!models || typeof models !== "object") return [];
2181
+ return Object.keys(models).flatMap((key) => {
2182
+ const model = models[key];
2183
+ if (!model || typeof model !== "object" || model.isInternal) return [];
2184
+ const modelId = typeof model.model === "string" && model.model.trim() ? model.model.trim() : key;
2185
+ if (CC_MODEL_BLACKLIST[modelId]) return [];
2186
+ const displayName = typeof model.displayName === "string" && model.displayName.trim() || typeof model.label === "string" && model.label.trim() || "";
2187
+ if (!displayName) return [];
2188
+ const quotaInfo = model.quotaInfo;
2189
+ return [{
2190
+ modelId: displayName,
2191
+ remainingFraction: typeof quotaInfo?.remainingFraction === "number" ? quotaInfo.remainingFraction : 0,
2192
+ resetTime: typeof quotaInfo?.resetTime === "string" ? quotaInfo.resetTime : void 0
2193
+ }];
2194
+ });
2195
+ }
2196
+ async function fetchWithAccessToken(accessToken) {
2197
+ const loadData = await requestCloudCodeJson(LOAD_CODE_ASSIST_PATH, accessToken, {});
2198
+ if (!loadData) return { ok: false, plan: null, error: "Cloud Code API error" };
2199
+ if ("_authFailed" in loadData) return { ok: false, plan: null, error: "Token expired" };
2200
+ const plan = readPlan(loadData);
2201
+ const project = typeof loadData.cloudaicompanionProject === "string" && loadData.cloudaicompanionProject.trim() ? loadData.cloudaicompanionProject.trim() : null;
2202
+ let quotaData = project ? await requestCloudCodeJson(RETRIEVE_QUOTA_PATH, accessToken, { project }) : null;
2203
+ if (!quotaData || "_authFailed" in quotaData) {
2204
+ quotaData = await requestCloudCodeJson(RETRIEVE_QUOTA_PATH, accessToken, {});
2205
+ }
2206
+ if (!quotaData) return { ok: false, plan, error: "Cloud Code quota unavailable" };
2207
+ if ("_authFailed" in quotaData) return { ok: false, plan, error: "Token expired" };
2208
+ let buckets = parseBuckets(quotaData);
2209
+ if (buckets.length === 0) {
2210
+ const modelData = await requestCloudCodeJson(FETCH_MODELS_PATH, accessToken, {});
2211
+ if (modelData && !("_authFailed" in modelData)) buckets = parseModelBuckets(modelData);
2212
+ if (modelData && "_authFailed" in modelData) return { ok: false, plan, error: "Token expired" };
2213
+ }
2214
+ if (buckets.length === 0) return { ok: false, plan, error: "No quota data" };
2215
+ return { ok: true, plan, buckets };
2216
+ }
2217
+ async function fetchCloudCodeQuota(token, expiredMessage = "Token expired") {
2218
+ const nowSec = Math.floor(Date.now() / 1e3);
2219
+ let accessToken = token.accessToken?.trim() || null;
2220
+ if (accessToken && token.expirySeconds && token.expirySeconds <= nowSec) {
2221
+ accessToken = await refreshAccessToken(token.refreshToken);
2222
+ if (!accessToken) return { ok: false, plan: null, error: expiredMessage };
2223
+ }
2224
+ if (!accessToken) {
2225
+ accessToken = await refreshAccessToken(token.refreshToken);
2226
+ if (!accessToken) return { ok: false, plan: null, error: `Missing credentials (${redact(token.accessToken)})` };
2227
+ }
2228
+ const result = await fetchWithAccessToken(accessToken);
2229
+ if (result.ok || result.error !== "Token expired") return result;
2230
+ const refreshed = await refreshAccessToken(token.refreshToken);
2231
+ if (!refreshed) return { ok: false, plan: result.plan, error: expiredMessage };
2232
+ return fetchWithAccessToken(refreshed);
2233
+ }
2234
+ function normalizeLabel(label) {
2235
+ return label.replace(/\s*\([^)]*\)\s*$/, "").trim();
2236
+ }
2237
+ function poolLabel(label) {
2238
+ const lower = normalizeLabel(label).toLowerCase();
2239
+ if (lower.includes("gemini") && lower.includes("pro")) return "Gemini Pro";
2240
+ if (lower.includes("gemini") && lower.includes("flash")) return "Gemini Flash";
2241
+ return "Claude";
2242
+ }
2243
+ function sortKey(label) {
2244
+ const lower = label.toLowerCase();
2245
+ if (lower.includes("gemini") && lower.includes("pro")) return `0a_${label}`;
2246
+ if (lower.includes("gemini")) return `0b_${label}`;
2247
+ if (lower.includes("claude") && lower.includes("opus")) return `1a_${label}`;
2248
+ if (lower.includes("claude")) return `1b_${label}`;
2249
+ return `2_${label}`;
2250
+ }
2251
+ function cloudCodeBucketsToMetrics(buckets) {
2252
+ const pooled = /* @__PURE__ */ new Map();
2253
+ for (const bucket of buckets) {
2254
+ const label = poolLabel(bucket.modelId);
2255
+ const existing = pooled.get(label);
2256
+ if (!existing || bucket.remainingFraction < existing.remainingFraction) {
2257
+ pooled.set(label, { ...bucket, modelId: label });
2258
+ }
2259
+ }
2260
+ return [...pooled.values()].sort((a, b) => sortKey(a.modelId).localeCompare(sortKey(b.modelId))).map((bucket, i) => {
2261
+ const clamped = Math.max(0, Math.min(1, bucket.remainingFraction));
2262
+ return {
2263
+ label: bucket.modelId,
2264
+ used: Math.round((1 - clamped) * 100),
2265
+ limit: 100,
2266
+ format: { kind: "percent" },
2267
+ resetsAt: bucket.resetTime ? resetIn(bucket.resetTime) : null,
2268
+ primary: i === 0
2269
+ };
2270
+ });
2271
+ }
2272
+ function cloudCodeSqliteError(status) {
2273
+ return status === "ok" ? "Not signed in \u2014 open Antigravity" : sqliteStatusMessage(status).replace(/Cursor/g, "Antigravity");
2274
+ }
2275
+
2276
+ // src/providers/antigravity/billing.ts
2277
+ async function exists(path) {
2278
+ try {
2279
+ await access7(path);
2280
+ return true;
2281
+ } catch {
2282
+ return false;
2283
+ }
2284
+ }
2285
+ async function firstExisting(paths) {
2286
+ for (const path of paths) {
2287
+ if (await exists(path)) return path;
2288
+ }
2289
+ return paths[0];
2290
+ }
2291
+ async function antigravityStateDb(homeDir) {
2292
+ const base = homeDir ?? homedir10();
2293
+ const tail = ["User", "globalStorage", "state.vscdb"];
2294
+ if (process.platform === "darwin") {
2295
+ const support = join12(base, "Library", "Application Support");
2296
+ const exact = [
2297
+ join12(support, "Antigravity IDE", ...tail),
2298
+ join12(support, "Antigravity", ...tail)
2299
+ ];
2300
+ try {
2301
+ const entries = await readdir6(support, { withFileTypes: true });
2302
+ const matches = entries.filter((e) => e.isDirectory() && e.name.includes("Antigravity")).map((e) => join12(support, e.name, ...tail));
2303
+ return firstExisting([...exact, ...matches]);
2304
+ } catch {
2305
+ return firstExisting(exact);
2306
+ }
2307
+ }
2308
+ if (process.platform === "win32") {
2309
+ const roaming = homeDir ? join12(homeDir, "AppData", "Roaming") : envDir("APPDATA") ?? join12(base, "AppData", "Roaming");
2310
+ return firstExisting([
2311
+ join12(roaming, "Antigravity IDE", ...tail),
2312
+ join12(roaming, "Antigravity", ...tail)
2313
+ ]);
2314
+ }
2315
+ const cfg = homeDir ? join12(homeDir, ".config") : envDir("XDG_CONFIG_HOME") ?? join12(base, ".config");
2316
+ return firstExisting([
2317
+ join12(cfg, "Antigravity IDE", ...tail),
2318
+ join12(cfg, "Antigravity", ...tail)
2319
+ ]);
2320
+ }
2321
+ async function detectAntigravity(homeDir) {
2322
+ return exists(await antigravityStateDb(homeDir));
2323
+ }
2324
+ async function antigravityBilling(account) {
2325
+ try {
2326
+ const db = await antigravityStateDb(account.homeDir);
2327
+ const { token, status } = await readAntigravityOAuthToken(db);
2328
+ if (!token) return { plan: null, metrics: [], error: cloudCodeSqliteError(status) };
2329
+ const quota = await fetchCloudCodeQuota(token, "Token expired \u2014 open Antigravity");
2330
+ if (!quota.ok) return { plan: quota.plan, metrics: [], error: quota.error };
2331
+ return { plan: quota.plan, metrics: cloudCodeBucketsToMetrics(quota.buckets), error: null };
2332
+ } catch {
2333
+ return { plan: null, metrics: [], error: "Antigravity billing unavailable" };
2334
+ }
2335
+ }
2336
+
2337
+ // src/providers/antigravity/index.ts
2338
+ var antigravityProvider = {
2339
+ id: "antigravity",
2340
+ name: "Antigravity",
2341
+ color: "red",
2342
+ hasUsage: false,
2343
+ hasBilling: true,
2344
+ detect: (homeDir) => detectAntigravity(homeDir),
2345
+ fetchBilling: (account) => antigravityBilling(account)
2346
+ };
2347
+
2348
+ // src/providers/gemini/billing.ts
2349
+ import { access as access8, readFile as readFile6 } from "fs/promises";
2350
+ import { join as join13 } from "path";
2351
+ import { homedir as homedir11 } from "os";
2352
+ function geminiCredsPath(homeDir) {
2353
+ return join13(homeDir ?? homedir11(), ".gemini", "oauth_creds.json");
2354
+ }
2355
+ async function detectGemini(homeDir) {
2356
+ try {
2357
+ await access8(geminiCredsPath(homeDir));
2358
+ return true;
2359
+ } catch {
2360
+ return false;
2361
+ }
2362
+ }
2363
+ async function readGeminiCreds(path) {
2364
+ try {
2365
+ return JSON.parse(await readFile6(path, "utf8"));
2366
+ } catch {
2367
+ return null;
2368
+ }
2369
+ }
2370
+ async function geminiBilling(account) {
2371
+ try {
2372
+ const creds = await readGeminiCreds(geminiCredsPath(account.homeDir));
2373
+ const accessToken = typeof creds?.access_token === "string" ? creds.access_token.trim() : "";
2374
+ const refreshToken = typeof creds?.refresh_token === "string" ? creds.refresh_token.trim() : null;
2375
+ if (!creds || !accessToken && !refreshToken) return { plan: null, metrics: [], error: "Not signed in \u2014 run gemini" };
2376
+ const quota = await fetchCloudCodeQuota({
2377
+ accessToken,
2378
+ refreshToken,
2379
+ expirySeconds: typeof creds.expiry_date === "number" ? Math.floor(creds.expiry_date / 1e3) : null
2380
+ }, "Token expired \u2014 run gemini");
2381
+ if (!quota.ok) return { plan: quota.plan, metrics: [], error: quota.error };
2382
+ return { plan: quota.plan, metrics: cloudCodeBucketsToMetrics(quota.buckets), error: null };
2383
+ } catch {
2384
+ return { plan: null, metrics: [], error: "Gemini billing unavailable" };
2385
+ }
2386
+ }
2387
+
2388
+ // src/providers/gemini/index.ts
2389
+ var geminiProvider = {
2390
+ id: "gemini",
2391
+ name: "Gemini",
2392
+ color: "greenBright",
2393
+ hasUsage: false,
2394
+ hasBilling: true,
2395
+ detect: (homeDir) => detectGemini(homeDir),
2396
+ fetchBilling: (account) => geminiBilling(account)
2397
+ };
2398
+
1417
2399
  // src/providers/detect.ts
1418
2400
  import { accessSync, constants } from "fs";
1419
- import { join as join9, delimiter, isAbsolute as isAbsolute3 } from "path";
1420
- import { homedir as homedir7 } from "os";
2401
+ import { join as join14, delimiter, isAbsolute as isAbsolute3 } from "path";
2402
+ import { homedir as homedir12 } from "os";
1421
2403
  function searchDirs() {
1422
- const home = homedir7();
2404
+ const home = homedir12();
1423
2405
  const fromEnv = (process.env.PATH ?? "").split(delimiter).filter(Boolean);
1424
2406
  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")
2407
+ process.env.APPDATA && join14(process.env.APPDATA, "npm"),
2408
+ process.env.LOCALAPPDATA && join14(process.env.LOCALAPPDATA, "pnpm"),
2409
+ join14(home, "scoop", "shims")
1428
2410
  ] : [
1429
2411
  "/opt/homebrew/bin",
1430
2412
  "/usr/local/bin",
1431
2413
  "/usr/bin",
1432
2414
  "/bin",
1433
2415
  "/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")
2416
+ join14(home, ".local", "bin"),
2417
+ join14(home, "bin"),
2418
+ join14(home, ".npm-global", "bin"),
2419
+ join14(home, ".bun", "bin"),
2420
+ join14(home, ".local", "share", "pnpm")
1439
2421
  ];
1440
2422
  return [.../* @__PURE__ */ new Set([...fromEnv, ...extra.filter((d) => !!d)])];
1441
2423
  }
@@ -1452,7 +2434,7 @@ function onPath(names) {
1452
2434
  for (const dir of searchDirs()) {
1453
2435
  for (const n of names) {
1454
2436
  for (const e of exts) {
1455
- if (isExec(join9(dir, n + e))) return true;
2437
+ if (isExec(join14(dir, n + e))) return true;
1456
2438
  }
1457
2439
  }
1458
2440
  }
@@ -1462,7 +2444,7 @@ function anyExists(paths) {
1462
2444
  return paths.some((p) => !!p && isExec(p));
1463
2445
  }
1464
2446
  function installSignals(id) {
1465
- const home = homedir7();
2447
+ const home = homedir12();
1466
2448
  const pf = process.env.ProgramFiles;
1467
2449
  const pf86 = process.env["ProgramFiles(x86)"];
1468
2450
  const lad = process.env.LOCALAPPDATA;
@@ -1470,8 +2452,8 @@ function installSignals(id) {
1470
2452
  case "claude":
1471
2453
  return onPath(["claude"]) || anyExists([
1472
2454
  "/Applications/Claude.app",
1473
- join9(home, "Applications", "Claude.app"),
1474
- lad && join9(lad, "Programs", "claude", "Claude.exe")
2455
+ join14(home, "Applications", "Claude.app"),
2456
+ lad && join14(lad, "Programs", "claude", "Claude.exe")
1475
2457
  ]);
1476
2458
  case "codex": {
1477
2459
  const bin = process.env.CODEX_BIN;
@@ -1481,25 +2463,44 @@ function installSignals(id) {
1481
2463
  case "cursor":
1482
2464
  return onPath(["cursor", "cursor-agent"]) || anyExists([
1483
2465
  "/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"),
2466
+ join14(home, "Applications", "Cursor.app"),
2467
+ lad && join14(lad, "Programs", "cursor", "Cursor.exe"),
2468
+ pf && join14(pf, "Cursor", "Cursor.exe"),
2469
+ pf86 && join14(pf86, "Cursor", "Cursor.exe"),
1488
2470
  "/opt/Cursor/cursor",
1489
2471
  "/usr/share/cursor/cursor",
1490
2472
  "/usr/bin/cursor"
1491
2473
  ]);
2474
+ case "pi":
2475
+ return onPath(["pi"]);
2476
+ case "opencode":
2477
+ return onPath(["opencode"]);
2478
+ case "copilot":
2479
+ return onPath(["gh"]);
2480
+ case "antigravity":
2481
+ return onPath(["antigravity"]) || anyExists([
2482
+ "/Applications/Antigravity.app",
2483
+ join14(home, "Applications", "Antigravity.app"),
2484
+ lad && join14(lad, "Programs", "Antigravity", "Antigravity.exe")
2485
+ ]);
2486
+ case "gemini":
2487
+ return onPath(["gemini"]);
1492
2488
  default:
1493
2489
  return false;
1494
2490
  }
1495
2491
  }
1496
2492
 
1497
2493
  // src/providers/index.ts
1498
- var PROVIDER_ORDER = ["claude", "codex", "cursor"];
2494
+ var PROVIDER_ORDER = ["claude", "codex", "cursor", "copilot", "pi", "opencode", "antigravity", "gemini"];
1499
2495
  var PROVIDERS = {
1500
2496
  claude: claudeProvider,
1501
2497
  codex: codexProvider,
1502
- cursor: cursorProvider
2498
+ cursor: cursorProvider,
2499
+ pi: piProvider,
2500
+ opencode: opencodeProvider,
2501
+ copilot: copilotProvider,
2502
+ antigravity: antigravityProvider,
2503
+ gemini: geminiProvider
1503
2504
  };
1504
2505
  var ALL_PROVIDERS = PROVIDER_ORDER.map((id) => PROVIDERS[id]);
1505
2506
  async function detectProviders() {
@@ -1548,14 +2549,46 @@ function accountsByProvider(accounts) {
1548
2549
  return groups;
1549
2550
  }
1550
2551
 
2552
+ // src/snapshot.ts
2553
+ import { readFile as readFile7, writeFile as writeFile3, mkdir as mkdir3, rename as rename3 } from "fs/promises";
2554
+ import { join as join15 } from "path";
2555
+ function snapshotFile() {
2556
+ return join15(cacheDir(), "dashboard-snapshot.json");
2557
+ }
2558
+ async function loadSnapshot() {
2559
+ try {
2560
+ const obj = JSON.parse(await readFile7(snapshotFile(), "utf-8"));
2561
+ return obj && typeof obj === "object" ? obj : {};
2562
+ } catch {
2563
+ return {};
2564
+ }
2565
+ }
2566
+ var saveQueue2 = Promise.resolve();
2567
+ function saveSnapshot(stats) {
2568
+ const obj = {};
2569
+ for (const [id, s] of stats) {
2570
+ if (s.dashboard || s.billing) obj[id] = { dashboard: s.dashboard ?? null, billing: s.billing ?? null };
2571
+ }
2572
+ saveQueue2 = saveQueue2.then(async () => {
2573
+ try {
2574
+ const dir = cacheDir();
2575
+ await mkdir3(dir, { recursive: true });
2576
+ const tmp = join15(dir, `dashboard-snapshot.json.${process.pid}.tmp`);
2577
+ await writeFile3(tmp, JSON.stringify(obj));
2578
+ await rename3(tmp, snapshotFile());
2579
+ } catch {
2580
+ }
2581
+ });
2582
+ }
2583
+
1551
2584
  // src/ui/shared.tsx
1552
2585
  import { useState, useEffect, useRef } from "react";
1553
2586
  import { Box, Text } from "ink";
1554
2587
  import { useOnMouseClick } from "@zenobius/ink-mouse";
1555
2588
  import { jsx, jsxs } from "react/jsx-runtime";
1556
- var SPINNER_FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
1557
2589
  function truncateName(s, n) {
1558
- return s.length > n ? s.slice(0, n - 1) + "\u2026" : s;
2590
+ const ell = glyphs().ellipsis;
2591
+ return s.length > n ? s.slice(0, n - ell.length) + ell : s;
1559
2592
  }
1560
2593
  function ClickableBox({ onClick, children, ...props }) {
1561
2594
  const ref = useRef(null);
@@ -1565,21 +2598,22 @@ function ClickableBox({ onClick, children, ...props }) {
1565
2598
  return /* @__PURE__ */ jsx(Box, { ref, ...props, children });
1566
2599
  }
1567
2600
  function Spinner({ label }) {
2601
+ const frames = glyphs().spinner;
1568
2602
  const [i, setI] = useState(0);
1569
2603
  useEffect(() => {
1570
- const id = setInterval(() => setI((n) => (n + 1) % SPINNER_FRAMES.length), 80);
2604
+ const id = setInterval(() => setI((n) => (n + 1) % frames.length), 80);
1571
2605
  return () => clearInterval(id);
1572
2606
  }, []);
1573
2607
  return /* @__PURE__ */ jsxs(Box, { children: [
1574
2608
  /* @__PURE__ */ jsxs(Text, { color: "green", children: [
1575
- SPINNER_FRAMES[i],
2609
+ frames[i],
1576
2610
  " "
1577
2611
  ] }),
1578
2612
  /* @__PURE__ */ jsx(Text, { dimColor: true, children: label })
1579
2613
  ] });
1580
2614
  }
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: [
2615
+ function TabBar({ tabs, active: active2, onSelect }) {
2616
+ 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
2617
  " ",
1584
2618
  t,
1585
2619
  " "
@@ -1592,7 +2626,10 @@ function TabBar({ tabs, active, onSelect }) {
1592
2626
  function PeakBadge({ peak }) {
1593
2627
  const color = peak.state === "peak" ? "red" : "green";
1594
2628
  return /* @__PURE__ */ jsxs(Box, { children: [
1595
- /* @__PURE__ */ jsx(Text, { color, children: "\u25CF " }),
2629
+ /* @__PURE__ */ jsxs(Text, { color, children: [
2630
+ glyphs().dot,
2631
+ " "
2632
+ ] }),
1596
2633
  /* @__PURE__ */ jsx(Text, { bold: true, color, children: peak.label }),
1597
2634
  peak.minutesUntilChange !== null && peak.minutesUntilChange > 0 && /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
1598
2635
  " (",
@@ -1608,24 +2645,24 @@ function fmtMinutes(mins) {
1608
2645
  return m === 0 ? `${h}h` : `${h}h ${m}m`;
1609
2646
  }
1610
2647
  function currencySymbol(cur) {
1611
- return cur === "EUR" ? "\u20AC" : cur === "GBP" ? "\xA3" : "$";
2648
+ return cur === "EUR" ? glyphs().eur : cur === "GBP" ? glyphs().gbp : "$";
1612
2649
  }
1613
- var SPARK = ["\u2581", "\u2582", "\u2583", "\u2584", "\u2585", "\u2586", "\u2587", "\u2588"];
1614
2650
  function sparkline(values) {
1615
2651
  if (values.length === 0) return "";
2652
+ const spark = glyphs().spark;
1616
2653
  const max = Math.max(...values);
1617
- if (max <= 0) return SPARK[0].repeat(values.length);
2654
+ if (max <= 0) return spark[0].repeat(values.length);
1618
2655
  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];
2656
+ if (v <= 0) return spark[0];
2657
+ const idx = Math.min(spark.length - 1, 1 + Math.round(v / max * (spark.length - 2)));
2658
+ return spark[idx];
1622
2659
  }).join("");
1623
2660
  }
1624
2661
  function Bar({ pct: pct2, color, width = 24 }) {
1625
2662
  const filled = Math.max(0, Math.min(width, Math.round(pct2 / 100 * width)));
1626
2663
  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) })
2664
+ /* @__PURE__ */ jsx(Text, { color, children: glyphs().barFull.repeat(filled) }),
2665
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: glyphs().barEmpty.repeat(width - filled) })
1629
2666
  ] });
1630
2667
  }
1631
2668
  function metricValueText(m) {
@@ -1647,20 +2684,76 @@ import { Box as Box2, Text as Text2 } from "ink";
1647
2684
  import { Fragment, jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
1648
2685
  var GAP = 2;
1649
2686
  var MIN_CARD = 56;
1650
- function DashboardView({ groups, stats, cols, focusId, layout }) {
2687
+ var MIN_CARD_DENSE = 44;
2688
+ var CARD_H = { full: 14, compact: 12, mini: 8 };
2689
+ var VARIANT_ORDER = ["full", "compact", "mini"];
2690
+ var INDICATOR_ROWS = 1;
2691
+ var MAX_SINGLE_CARD = Math.round(MIN_CARD * 1.6);
2692
+ function chooseLayout(content, budget, n, single, cols) {
2693
+ if (n <= 0) return { ncols: 1, variant: "mini", cardsPerPage: 1, pageCount: 1 };
2694
+ const gridHeight = (rows, H2) => rows * H2 + Math.max(0, rows - 1);
2695
+ const colCap = single ? 1 : cols >= 3 * MIN_CARD_DENSE + 2 * GAP ? 3 : cols >= 2 * MIN_CARD + GAP ? 2 : 1;
2696
+ const maxCols = Math.max(1, Math.min(colCap, n));
2697
+ const cardWidthAt = (nc) => nc <= 1 ? content : Math.floor((content - GAP * (nc - 1)) / nc);
2698
+ const minWidthAt = (nc) => nc >= 3 ? MIN_CARD_DENSE : MIN_CARD;
2699
+ for (const variant of VARIANT_ORDER) {
2700
+ for (let nc = maxCols; nc >= 1; nc--) {
2701
+ if (nc > 1 && cardWidthAt(nc) < minWidthAt(nc)) continue;
2702
+ const rows = Math.ceil(n / nc);
2703
+ if (gridHeight(rows, CARD_H[variant]) <= budget) {
2704
+ return { ncols: nc, variant, cardsPerPage: n, pageCount: 1 };
2705
+ }
2706
+ }
2707
+ }
2708
+ let ncols = 1;
2709
+ for (let nc = maxCols; nc >= 1; nc--) {
2710
+ if (nc === 1 || cardWidthAt(nc) >= minWidthAt(nc)) {
2711
+ ncols = nc;
2712
+ break;
2713
+ }
2714
+ }
2715
+ const H = CARD_H.mini;
2716
+ const fitBudget = budget - INDICATOR_ROWS;
2717
+ const rowsThatFit = Math.max(1, Math.floor((fitBudget + 1) / (H + 1)));
2718
+ const cardsPerPage = Math.max(1, rowsThatFit * ncols);
2719
+ const pageCount = Math.max(1, Math.ceil(n / cardsPerPage));
2720
+ return { ncols, variant: "mini", cardsPerPage, pageCount };
2721
+ }
2722
+ function DashboardView({ groups, stats, cols, budget, focusId, layout, page = 0 }) {
1651
2723
  if (groups.length === 0) {
1652
- return /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "No providers enabled \u2014 press s to pick providers." });
2724
+ return /* @__PURE__ */ jsxs2(Text2, { dimColor: true, children: [
2725
+ "No providers enabled ",
2726
+ glyphs().emDash,
2727
+ " press s to pick providers."
2728
+ ] });
1653
2729
  }
1654
2730
  let shown = groups;
1655
2731
  if (layout === "single" && focusId === null) shown = groups.slice(0, 1);
1656
- const content = Math.max(MIN_CARD, cols - 4);
1657
2732
  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)) });
2733
+ const content = Math.max(MIN_CARD, cols - 4);
2734
+ const { ncols, variant, cardsPerPage, pageCount } = chooseLayout(content, budget, shown.length, single, cols);
2735
+ let cardW = ncols <= 1 ? content : Math.floor((content - GAP * (ncols - 1)) / ncols);
2736
+ if (ncols === 1 && cardW > MAX_SINGLE_CARD) cardW = MAX_SINGLE_CARD;
2737
+ const pg = pageCount > 1 ? (page % pageCount + pageCount) % pageCount : 0;
2738
+ const visible = pageCount > 1 ? shown.slice(pg * cardsPerPage, pg * cardsPerPage + cardsPerPage) : shown;
2739
+ return /* @__PURE__ */ jsxs2(Box2, { height: budget, flexDirection: "column", overflow: "hidden", children: [
2740
+ /* @__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)) }),
2741
+ pageCount > 1 && /* @__PURE__ */ jsxs2(Text2, { dimColor: true, children: [
2742
+ " ",
2743
+ glyphs().middot,
2744
+ " page ",
2745
+ pg + 1,
2746
+ "/",
2747
+ pageCount,
2748
+ " ",
2749
+ glyphs().middot,
2750
+ " scroll ",
2751
+ glyphs().arrowU,
2752
+ glyphs().arrowD
2753
+ ] })
2754
+ ] });
1662
2755
  }
1663
- function ProviderCard({ provider, accounts, stats, width }) {
2756
+ function ProviderCard({ provider, accounts, stats, width, variant }) {
1664
2757
  const meta = PROVIDERS[provider];
1665
2758
  const items = accounts.map((a) => ({ account: a, s: stats.get(a.id) }));
1666
2759
  const dashboards = items.map((i) => i.s?.dashboard).filter((d) => !!d);
@@ -1670,9 +2763,14 @@ function ProviderCard({ provider, accounts, stats, width }) {
1670
2763
  const inner = width - 4;
1671
2764
  const barW = Math.max(10, Math.min(46, inner - 20));
1672
2765
  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: [
2766
+ const showBars = variant !== "mini";
2767
+ const showSpark = variant === "full";
2768
+ return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", width, borderStyle: glyphs().border, borderColor: meta.color, paddingX: 1, children: [
1674
2769
  /* @__PURE__ */ jsxs2(Box2, { children: [
1675
- /* @__PURE__ */ jsx2(Text2, { color: meta.color, children: "\u25CF " }),
2770
+ /* @__PURE__ */ jsxs2(Text2, { color: meta.color, children: [
2771
+ glyphs().dot,
2772
+ " "
2773
+ ] }),
1676
2774
  /* @__PURE__ */ jsx2(Text2, { bold: true, color: meta.color, children: meta.name }),
1677
2775
  /* @__PURE__ */ jsx2(Box2, { flexGrow: 1 }),
1678
2776
  plan && /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: plan })
@@ -1685,17 +2783,21 @@ function ProviderCard({ provider, accounts, stats, width }) {
1685
2783
  /* @__PURE__ */ jsx2(KpiLine, { agg })
1686
2784
  ] }) : /* @__PURE__ */ jsxs2(Fragment, { children: [
1687
2785
  /* @__PURE__ */ jsx2(Box2, { height: 1 }),
1688
- /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "Fetching usage\u2026" })
2786
+ /* @__PURE__ */ jsxs2(Text2, { dimColor: true, children: [
2787
+ "Fetching usage",
2788
+ glyphs().ellipsis
2789
+ ] })
1689
2790
  ] })),
1690
- meta.hasBilling && /* @__PURE__ */ jsxs2(Fragment, { children: [
2791
+ meta.hasBilling && showBars && /* @__PURE__ */ jsxs2(Fragment, { children: [
1691
2792
  meta.hasUsage && /* @__PURE__ */ jsx2(Rule, { inner }),
1692
2793
  /* @__PURE__ */ jsx2(LimitsBlock, { items, barW })
1693
2794
  ] }),
1694
- hasSpark && /* @__PURE__ */ jsxs2(Fragment, { children: [
2795
+ meta.hasBilling && !showBars && !meta.hasUsage && /* @__PURE__ */ jsx2(CompactBilling, { items }),
2796
+ hasSpark && showSpark && /* @__PURE__ */ jsxs2(Fragment, { children: [
1695
2797
  /* @__PURE__ */ jsx2(Rule, { inner }),
1696
2798
  /* @__PURE__ */ jsx2(SparkFooter, { series: agg.series, month: agg.month.cost, color: meta.color })
1697
2799
  ] }),
1698
- !meta.hasUsage && activity && /* @__PURE__ */ jsxs2(Fragment, { children: [
2800
+ !meta.hasUsage && activity && showSpark && /* @__PURE__ */ jsxs2(Fragment, { children: [
1699
2801
  /* @__PURE__ */ jsx2(Rule, { inner }),
1700
2802
  /* @__PURE__ */ jsxs2(Box2, { children: [
1701
2803
  /* @__PURE__ */ jsx2(Box2, { width: 4, children: /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "14d" }) }),
@@ -1703,14 +2805,29 @@ function ProviderCard({ provider, accounts, stats, width }) {
1703
2805
  /* @__PURE__ */ jsx2(Box2, { flexGrow: 1, justifyContent: "flex-end", children: /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: activity.summary }) })
1704
2806
  ] })
1705
2807
  ] }),
1706
- !meta.hasUsage && !activity && /* @__PURE__ */ jsxs2(Fragment, { children: [
2808
+ !meta.hasUsage && !activity && variant === "full" && /* @__PURE__ */ jsxs2(Fragment, { children: [
1707
2809
  /* @__PURE__ */ jsx2(Box2, { flexGrow: 1 }),
1708
- /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "Billing only \u2014 no local history" })
2810
+ /* @__PURE__ */ jsxs2(Text2, { dimColor: true, children: [
2811
+ "Billing only ",
2812
+ glyphs().emDash,
2813
+ " no local history"
2814
+ ] })
1709
2815
  ] })
1710
2816
  ] });
1711
2817
  }
2818
+ function CompactBilling({ items }) {
2819
+ const billing = items.map((i) => i.s?.billing).find(Boolean);
2820
+ if (!billing) return /* @__PURE__ */ jsxs2(Text2, { dimColor: true, children: [
2821
+ "Fetching",
2822
+ glyphs().ellipsis
2823
+ ] });
2824
+ if (billing.error) return /* @__PURE__ */ jsx2(Text2, { color: "red", children: billing.error });
2825
+ const m = billing.metrics[0];
2826
+ if (!m) return /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "No data" });
2827
+ return /* @__PURE__ */ jsx2(Text2, { bold: true, color: "yellow", children: metricValueText(m) });
2828
+ }
1712
2829
  function Rule({ inner }) {
1713
- return /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "\u2500".repeat(Math.max(0, inner)) });
2830
+ return /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: glyphs().rule.repeat(Math.max(0, inner)) });
1714
2831
  }
1715
2832
  function SummaryRow({ label, s }) {
1716
2833
  const cachedPct = s.tokens > 0 ? Math.round(s.cacheRead / s.tokens * 100) : 0;
@@ -1755,10 +2872,16 @@ function LimitsBlock({ items, barW }) {
1755
2872
  const billing = s?.billing;
1756
2873
  return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", marginTop: showName && idx > 0 ? 1 : 0, children: [
1757
2874
  showName && /* @__PURE__ */ jsxs2(Box2, { children: [
1758
- /* @__PURE__ */ jsx2(Text2, { color: account.color, children: "\u25CF " }),
2875
+ /* @__PURE__ */ jsxs2(Text2, { color: account.color, children: [
2876
+ glyphs().dot,
2877
+ " "
2878
+ ] }),
1759
2879
  /* @__PURE__ */ jsx2(Text2, { bold: true, children: truncateName(account.name, 22) })
1760
2880
  ] }),
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}`))
2881
+ !billing ? /* @__PURE__ */ jsxs2(Text2, { dimColor: true, children: [
2882
+ "Fetching",
2883
+ glyphs().ellipsis
2884
+ ] }) : 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
2885
  ] }, account.id);
1763
2886
  }) });
1764
2887
  }
@@ -1812,12 +2935,12 @@ function aggregate(list) {
1812
2935
  import { Box as Box3, Text as Text3 } from "ink";
1813
2936
  import { Fragment as Fragment2, jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
1814
2937
  var MONTHS = ["", "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
1815
- function TableProviderBar({ providers, active, onSelect }) {
2938
+ function TableProviderBar({ providers, active: active2, onSelect }) {
1816
2939
  return /* @__PURE__ */ jsxs3(Box3, { children: [
1817
2940
  /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "provider " }),
1818
2941
  providers.map((p) => {
1819
2942
  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: [
2943
+ return /* @__PURE__ */ jsx3(ClickableBox, { onClick: () => onSelect(p), marginRight: 1, children: p === active2 ? /* @__PURE__ */ jsxs3(Text3, { bold: true, color: meta.color, inverse: true, children: [
1821
2944
  " ",
1822
2945
  meta.name,
1823
2946
  " "
@@ -1842,15 +2965,23 @@ function ControlBar({ views, period, sort, search, searching, showPeriod }) {
1842
2965
  ] }),
1843
2966
  /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "sort " }),
1844
2967
  /* @__PURE__ */ jsx3(Text3, { bold: true, color: "magenta", children: sort }),
1845
- /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: " o cycle \xB7 " }),
2968
+ /* @__PURE__ */ jsxs3(Text3, { dimColor: true, children: [
2969
+ " o cycle ",
2970
+ glyphs().middot,
2971
+ " "
2972
+ ] }),
1846
2973
  searching ? /* @__PURE__ */ jsxs3(Fragment2, { children: [
1847
2974
  /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "/" }),
1848
2975
  /* @__PURE__ */ jsx3(Text3, { bold: true, color: "cyan", children: search }),
1849
- /* @__PURE__ */ jsx3(Text3, { color: "cyan", children: "\u258F" })
2976
+ /* @__PURE__ */ jsx3(Text3, { color: "cyan", children: glyphs().vbar })
1850
2977
  ] }) : search ? /* @__PURE__ */ jsxs3(Fragment2, { children: [
1851
2978
  /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "filter " }),
1852
2979
  /* @__PURE__ */ jsx3(Text3, { bold: true, color: "green", children: search }),
1853
- /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: " (/ edit \xB7 esc clear)" })
2980
+ /* @__PURE__ */ jsxs3(Text3, { dimColor: true, children: [
2981
+ " (/ edit ",
2982
+ glyphs().middot,
2983
+ " esc clear)"
2984
+ ] })
1854
2985
  ] }) : /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "/ filter" })
1855
2986
  ] });
1856
2987
  }
@@ -1887,14 +3018,14 @@ function TokenTable({ rows, cursor, expanded, maxRows, cols, onRowClick }) {
1887
3018
  W.total > 0 && /* @__PURE__ */ jsx3(Text3, { bold: true, children: col("Total", W.total) }),
1888
3019
  /* @__PURE__ */ jsx3(Text3, { bold: true, children: col("Cost", W.cost) })
1889
3020
  ] }),
1890
- /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "\u2500".repeat(lineW + 2) }),
3021
+ /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: glyphs().rule.repeat(lineW + 2) }),
1891
3022
  visible.map((r, vi) => {
1892
3023
  const idx = scrollStart + vi;
1893
3024
  const selected = idx === clampedCursor;
1894
3025
  return /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", children: [
1895
3026
  /* @__PURE__ */ jsx3(ClickableBox, { onClick: () => onRowClick(idx), children: /* @__PURE__ */ jsxs3(Text3, { inverse: selected, children: [
1896
3027
  /* @__PURE__ */ jsxs3(Text3, { color: selected ? void 0 : "cyan", children: [
1897
- selected ? "\u25B8 " : " ",
3028
+ selected ? `${glyphs().caretR} ` : " ",
1898
3029
  col(fmtLabel(r.label), W.label, "left")
1899
3030
  ] }),
1900
3031
  /* @__PURE__ */ jsx3(Text3, { dimColor: !selected, children: col(r.models.join(", "), W.models, "left") }),
@@ -1908,7 +3039,7 @@ function TokenTable({ rows, cursor, expanded, maxRows, cols, onRowClick }) {
1908
3039
  idx === expanded && /* @__PURE__ */ jsx3(RowDetail, { row: r, indent: W.label + 2 })
1909
3040
  ] }, r.label);
1910
3041
  }),
1911
- /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "\u2500".repeat(lineW + 2) }),
3042
+ /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: glyphs().rule.repeat(lineW + 2) }),
1912
3043
  /* @__PURE__ */ jsxs3(Text3, { children: [
1913
3044
  /* @__PURE__ */ jsxs3(Text3, { bold: true, color: "greenBright", children: [
1914
3045
  " ",
@@ -1924,7 +3055,17 @@ function TokenTable({ rows, cursor, expanded, maxRows, cols, onRowClick }) {
1924
3055
  ] }),
1925
3056
  /* @__PURE__ */ jsx3(Box3, { height: 1 }),
1926
3057
  /* @__PURE__ */ jsxs3(Text3, { dimColor: true, children: [
1927
- "\u2191\u2193 navigate \xB7 Enter detail \xB7 o sort \xB7 g/G top/bottom \xB7 ",
3058
+ glyphs().arrowU,
3059
+ glyphs().arrowD,
3060
+ " navigate ",
3061
+ glyphs().middot,
3062
+ " Enter detail ",
3063
+ glyphs().middot,
3064
+ " o sort ",
3065
+ glyphs().middot,
3066
+ " g/G top/bottom ",
3067
+ glyphs().middot,
3068
+ " ",
1928
3069
  clampedCursor + 1,
1929
3070
  "/",
1930
3071
  rows.length
@@ -1934,7 +3075,7 @@ function TokenTable({ rows, cursor, expanded, maxRows, cols, onRowClick }) {
1934
3075
  function RowDetail({ row, indent }) {
1935
3076
  return /* @__PURE__ */ jsx3(Box3, { flexDirection: "column", paddingLeft: indent, children: row.breakdown.map((m, i) => /* @__PURE__ */ jsxs3(Text3, { children: [
1936
3077
  /* @__PURE__ */ jsxs3(Text3, { dimColor: true, children: [
1937
- i === row.breakdown.length - 1 ? "\u2514\u2500" : "\u251C\u2500",
3078
+ i === row.breakdown.length - 1 ? glyphs().treeEnd : glyphs().treeMid,
1938
3079
  " "
1939
3080
  ] }),
1940
3081
  /* @__PURE__ */ jsx3(Text3, { bold: true, children: col(m.name, 16, "left") }),
@@ -1975,14 +3116,14 @@ function CursorSpendTable({ rows, cursor, maxRows, onRowClick }) {
1975
3116
  /* @__PURE__ */ jsx3(Text3, { bold: true, children: col("Amount", W.amount) }),
1976
3117
  /* @__PURE__ */ jsx3(Text3, { bold: true, children: col("Share", W.share) })
1977
3118
  ] }),
1978
- /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "\u2500".repeat(W.model + W.cost + W.amount + W.share + 2) }),
3119
+ /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: glyphs().rule.repeat(W.model + W.cost + W.amount + W.share + 2) }),
1979
3120
  visible.map((r, vi) => {
1980
3121
  const idx = scrollStart + vi;
1981
3122
  const selected = idx === clamped;
1982
3123
  const share = total > 0 ? r.usd / total * 100 : 0;
1983
3124
  return /* @__PURE__ */ jsx3(ClickableBox, { onClick: () => onRowClick(idx), children: /* @__PURE__ */ jsxs3(Text3, { inverse: selected, children: [
1984
3125
  /* @__PURE__ */ jsxs3(Text3, { color: selected ? void 0 : "magenta", children: [
1985
- selected ? "\u25B8 " : " ",
3126
+ selected ? `${glyphs().caretR} ` : " ",
1986
3127
  col(r.name, W.model, "left")
1987
3128
  ] }),
1988
3129
  /* @__PURE__ */ jsx3(Text3, { bold: true, color: selected ? void 0 : "yellow", children: col(currency(r.usd), W.cost) }),
@@ -1990,7 +3131,7 @@ function CursorSpendTable({ rows, cursor, maxRows, onRowClick }) {
1990
3131
  /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: col(share.toFixed(1) + "%", W.share) })
1991
3132
  ] }) }, r.name);
1992
3133
  }),
1993
- /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "\u2500".repeat(W.model + W.cost + W.amount + W.share + 2) }),
3134
+ /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: glyphs().rule.repeat(W.model + W.cost + W.amount + W.share + 2) }),
1994
3135
  /* @__PURE__ */ jsxs3(Text3, { children: [
1995
3136
  /* @__PURE__ */ jsxs3(Text3, { bold: true, color: "greenBright", children: [
1996
3137
  " ",
@@ -2002,7 +3143,11 @@ function CursorSpendTable({ rows, cursor, maxRows, onRowClick }) {
2002
3143
  ] }),
2003
3144
  /* @__PURE__ */ jsx3(Box3, { height: 1 }),
2004
3145
  /* @__PURE__ */ jsxs3(Text3, { dimColor: true, children: [
2005
- "local spend by model (composerData) \xB7 est. API-equivalent \xB7 ",
3146
+ "local spend by model (composerData) ",
3147
+ glyphs().middot,
3148
+ " est. API-equivalent ",
3149
+ glyphs().middot,
3150
+ " ",
2006
3151
  clamped + 1,
2007
3152
  "/",
2008
3153
  rows.length
@@ -2019,43 +3164,174 @@ function fmtLabel(label) {
2019
3164
  // src/ui/onboarding.tsx
2020
3165
  import { Box as Box4, Text as Text4 } from "ink";
2021
3166
  import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
2022
- function Onboarding({ items, cursor, onToggle, onConfirm }) {
3167
+ 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
3168
  const anyEnabled = items.some((it) => it.enabled);
2024
3169
  const startIdx = items.length;
2025
3170
  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." }),
3171
+ /* @__PURE__ */ jsx4(Box4, { children: /* @__PURE__ */ jsx4(Text4, { bold: true, color: "greenBright", children: heading }) }),
3172
+ /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: subheading }),
2028
3173
  /* @__PURE__ */ jsx4(Box4, { height: 1 }),
2029
3174
  items.map((it, i) => {
2030
3175
  const selected = cursor === i;
2031
- const box = it.enabled ? "[\u2713]" : "[ ]";
3176
+ const box = it.enabled ? `[${glyphs().check}]` : "[ ]";
2032
3177
  return /* @__PURE__ */ jsxs4(ClickableBox, { onClick: () => onToggle(i), children: [
2033
3178
  /* @__PURE__ */ jsxs4(Text4, { color: selected ? "green" : void 0, children: [
2034
- selected ? "\u25B8" : " ",
3179
+ selected ? glyphs().caretR : " ",
2035
3180
  " "
2036
3181
  ] }),
2037
3182
  /* @__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 }) }),
3183
+ /* @__PURE__ */ jsxs4(Text4, { color: it.color, children: [
3184
+ " ",
3185
+ glyphs().dot,
3186
+ " "
3187
+ ] }),
3188
+ /* @__PURE__ */ jsx4(Box4, { width: 13, flexShrink: 0, children: /* @__PURE__ */ jsx4(Text4, { bold: selected, dimColor: !it.detected && !it.enabled, wrap: "truncate", children: it.name }) }),
2040
3189
  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
3190
  ] }, it.id);
2042
3191
  }),
2043
3192
  /* @__PURE__ */ jsx4(Box4, { height: 1 }),
2044
3193
  /* @__PURE__ */ jsxs4(ClickableBox, { onClick: onConfirm, children: [
2045
3194
  /* @__PURE__ */ jsxs4(Text4, { color: cursor === startIdx ? "green" : void 0, children: [
2046
- cursor === startIdx ? "\u25B8" : " ",
3195
+ cursor === startIdx ? glyphs().caretR : " ",
2047
3196
  " "
2048
3197
  ] }),
2049
- /* @__PURE__ */ jsx4(Text4, { bold: true, color: anyEnabled ? "greenBright" : void 0, dimColor: !anyEnabled, children: anyEnabled ? "\u25B6 Start tokmon" : "\u25B6 Start (nothing selected)" })
3198
+ /* @__PURE__ */ jsx4(Text4, { bold: true, color: anyEnabled ? "greenBright" : void 0, dimColor: !anyEnabled, children: anyEnabled ? `${glyphs().play} Start tokmon` : `${glyphs().play} Start (nothing selected)` })
2050
3199
  ] }),
2051
3200
  /* @__PURE__ */ jsx4(Box4, { height: 1 }),
2052
- /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: "\u2191\u2193 move \xB7 space toggle \xB7 enter start \xB7 q quit" })
3201
+ /* @__PURE__ */ jsxs4(Text4, { dimColor: true, children: [
3202
+ glyphs().arrowU,
3203
+ glyphs().arrowD,
3204
+ " move ",
3205
+ glyphs().middot,
3206
+ " space toggle ",
3207
+ glyphs().middot,
3208
+ " enter start ",
3209
+ glyphs().middot,
3210
+ " q quit"
3211
+ ] })
2053
3212
  ] });
2054
3213
  }
2055
3214
 
2056
- // src/ui/settings.tsx
3215
+ // src/ui/loading.tsx
3216
+ import { useState as useState2, useEffect as useEffect2 } from "react";
2057
3217
  import { Box as Box5, Text as Text5 } from "ink";
2058
- import { Fragment as Fragment3, jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
3218
+ import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
3219
+ function accountReady(s, providerId) {
3220
+ if (!s) return false;
3221
+ const p = PROVIDERS[providerId];
3222
+ if (p.hasBilling && s.billing?.error) return true;
3223
+ if (p.hasUsage && !s.dashboard) return false;
3224
+ if (p.hasBilling && !s.billing) return false;
3225
+ return true;
3226
+ }
3227
+ function groupTodayCost(items) {
3228
+ return items.reduce((sum, s) => {
3229
+ const d = s?.dashboard;
3230
+ return sum + (d?.today.cost ?? 0);
3231
+ }, 0);
3232
+ }
3233
+ function headlineFor(group, items) {
3234
+ const meta = PROVIDERS[group.provider];
3235
+ if (meta.hasUsage) return `${currency(groupTodayCost(items))} today`;
3236
+ const billing = items.map((s) => s?.billing).find(Boolean);
3237
+ if (!billing) return "no data";
3238
+ if (billing.error) return billing.error;
3239
+ const m = billing.metrics[0];
3240
+ if (m) return metricValueText(m);
3241
+ return billing.plan ?? "no data";
3242
+ }
3243
+ var STAGGER_FRAMES = 2;
3244
+ function LoadingView({ groups, stats, cols, rows }) {
3245
+ const sp = glyphs().spinner;
3246
+ const [frame, setFrame] = useState2(0);
3247
+ useEffect2(() => {
3248
+ const id = setInterval(() => setFrame((f) => f + 1), 80);
3249
+ return () => clearInterval(id);
3250
+ }, []);
3251
+ const nameW = Math.min(13, groups.reduce((w, g) => Math.max(w, PROVIDERS[g.provider].name.length), 0));
3252
+ const readyCount = groups.filter((g) => g.accounts.every((a) => accountReady(stats.get(a.id), g.provider))).length;
3253
+ const maxRows = Math.max(1, rows - 7);
3254
+ const visible = groups.slice(0, maxRows);
3255
+ const hidden = groups.length - visible.length;
3256
+ return /* @__PURE__ */ jsxs5(Box5, { flexDirection: "column", children: [
3257
+ /* @__PURE__ */ jsxs5(Text5, { bold: true, color: "greenBright", children: [
3258
+ glyphs().dotSel,
3259
+ " tokmon"
3260
+ ] }),
3261
+ /* @__PURE__ */ jsxs5(Box5, { marginTop: 1, children: [
3262
+ /* @__PURE__ */ jsxs5(Text5, { dimColor: true, children: [
3263
+ "Detecting installed tools",
3264
+ glyphs().ellipsis
3265
+ ] }),
3266
+ /* @__PURE__ */ jsxs5(Text5, { dimColor: true, children: [
3267
+ " ",
3268
+ groups.length,
3269
+ " found"
3270
+ ] })
3271
+ ] }),
3272
+ /* @__PURE__ */ jsxs5(Box5, { marginTop: 1, flexDirection: "column", children: [
3273
+ visible.map((g, i) => {
3274
+ const meta = PROVIDERS[g.provider];
3275
+ const items = g.accounts.map((a) => stats.get(a.id));
3276
+ const ready = g.accounts.every((a) => accountReady(stats.get(a.id), g.provider));
3277
+ const errored = items.some((s) => !!s?.billing?.error);
3278
+ const revealed = frame >= i * STAGGER_FRAMES;
3279
+ const name = truncateName(meta.name, nameW);
3280
+ const namePad = " ".repeat(Math.max(0, nameW - name.length));
3281
+ if (!revealed) {
3282
+ return /* @__PURE__ */ jsxs5(Box5, { children: [
3283
+ /* @__PURE__ */ jsxs5(Text5, { dimColor: true, children: [
3284
+ glyphs().dot,
3285
+ " "
3286
+ ] }),
3287
+ /* @__PURE__ */ jsxs5(Text5, { dimColor: true, children: [
3288
+ name,
3289
+ namePad
3290
+ ] })
3291
+ ] }, g.provider);
3292
+ }
3293
+ return /* @__PURE__ */ jsxs5(Box5, { children: [
3294
+ /* @__PURE__ */ jsxs5(Text5, { color: meta.color, children: [
3295
+ glyphs().dot,
3296
+ " "
3297
+ ] }),
3298
+ /* @__PURE__ */ jsx5(Text5, { bold: true, color: meta.color, children: name }),
3299
+ /* @__PURE__ */ jsx5(Text5, { children: namePad }),
3300
+ /* @__PURE__ */ jsx5(Text5, { children: " " }),
3301
+ errored ? /* @__PURE__ */ jsxs5(Text5, { color: "red", children: [
3302
+ glyphs().warn,
3303
+ " "
3304
+ ] }) : ready ? /* @__PURE__ */ jsxs5(Text5, { color: "green", children: [
3305
+ glyphs().check,
3306
+ " "
3307
+ ] }) : /* @__PURE__ */ jsxs5(Text5, { color: "green", children: [
3308
+ sp[frame % sp.length],
3309
+ " "
3310
+ ] }),
3311
+ errored ? /* @__PURE__ */ jsx5(Text5, { color: "red", children: headlineFor(g, items) }) : ready ? /* @__PURE__ */ jsx5(Text5, { children: headlineFor(g, items) }) : /* @__PURE__ */ jsxs5(Text5, { dimColor: true, children: [
3312
+ "loading",
3313
+ glyphs().ellipsis
3314
+ ] })
3315
+ ] }, g.provider);
3316
+ }),
3317
+ hidden > 0 && /* @__PURE__ */ jsxs5(Text5, { dimColor: true, children: [
3318
+ "+",
3319
+ hidden,
3320
+ " more"
3321
+ ] })
3322
+ ] }),
3323
+ /* @__PURE__ */ jsx5(Box5, { marginTop: 1, children: /* @__PURE__ */ jsxs5(Text5, { dimColor: true, children: [
3324
+ "loading ",
3325
+ readyCount,
3326
+ " / ",
3327
+ groups.length
3328
+ ] }) })
3329
+ ] });
3330
+ }
3331
+
3332
+ // src/ui/settings.tsx
3333
+ import { Box as Box6, Text as Text6 } from "ink";
3334
+ import { Fragment as Fragment3, jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
2059
3335
  var GENERAL_ROWS = 6;
2060
3336
  var PROVIDER_ROWS_START = GENERAL_ROWS;
2061
3337
  var ACCOUNT_ROWS_START = GENERAL_ROWS + PROVIDER_ORDER.length;
@@ -2080,134 +3356,191 @@ function SettingsView({
2080
3356
  accountForm,
2081
3357
  activeAccountId
2082
3358
  }) {
2083
- if (accountForm) return /* @__PURE__ */ jsx5(AccountFormView, { form: accountForm, accounts: config2.accounts });
3359
+ if (accountForm) return /* @__PURE__ */ jsx6(AccountFormView, { form: accountForm, accounts: config2.accounts });
2084
3360
  const editingTz = tzEdit !== null;
2085
3361
  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",
3362
+ return /* @__PURE__ */ jsxs6(Box6, { flexDirection: "column", marginTop: 1, children: [
3363
+ /* @__PURE__ */ jsx6(Text6, { bold: true, children: "Settings" }),
3364
+ /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: configLocation() }),
3365
+ /* @__PURE__ */ jsx6(Box6, { height: 1 }),
3366
+ /* @__PURE__ */ jsx6(Text6, { bold: true, dimColor: true, children: "General" }),
3367
+ /* @__PURE__ */ jsxs6(Row, { cursor, idx: 0, label: "Refresh interval", children: [
3368
+ /* @__PURE__ */ jsxs6(Text6, { dimColor: true, children: [
3369
+ glyphs().caretL,
2094
3370
  " "
2095
3371
  ] }),
2096
- /* @__PURE__ */ jsxs5(Text5, { bold: true, color: "yellow", children: [
3372
+ /* @__PURE__ */ jsxs6(Text6, { bold: true, color: "yellow", children: [
2097
3373
  config2.interval,
2098
3374
  "s"
2099
3375
  ] }),
2100
- /* @__PURE__ */ jsxs5(Text5, { dimColor: true, children: [
3376
+ /* @__PURE__ */ jsxs6(Text6, { dimColor: true, children: [
2101
3377
  " ",
2102
- "\u25B8"
3378
+ glyphs().caretR
2103
3379
  ] })
2104
3380
  ] }),
2105
- /* @__PURE__ */ jsxs5(Row, { cursor, idx: 1, label: "Billing poll", children: [
2106
- /* @__PURE__ */ jsxs5(Text5, { dimColor: true, children: [
2107
- "\u25C2",
3381
+ /* @__PURE__ */ jsxs6(Row, { cursor, idx: 1, label: "Billing poll", children: [
3382
+ /* @__PURE__ */ jsxs6(Text6, { dimColor: true, children: [
3383
+ glyphs().caretL,
2108
3384
  " "
2109
3385
  ] }),
2110
- /* @__PURE__ */ jsxs5(Text5, { bold: true, color: "yellow", children: [
3386
+ /* @__PURE__ */ jsxs6(Text6, { bold: true, color: "yellow", children: [
2111
3387
  config2.billingInterval,
2112
3388
  "m"
2113
3389
  ] }),
2114
- /* @__PURE__ */ jsxs5(Text5, { dimColor: true, children: [
3390
+ /* @__PURE__ */ jsxs6(Text6, { dimColor: true, children: [
2115
3391
  " ",
2116
- "\u25B8"
3392
+ glyphs().caretR
2117
3393
  ] })
2118
3394
  ] }),
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: [
3395
+ /* @__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" }) }),
3396
+ /* @__PURE__ */ jsx6(Row, { cursor, idx: 3, label: "Timezone", children: editingTz ? /* @__PURE__ */ jsxs6(Fragment3, { children: [
3397
+ /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: "[" }),
3398
+ /* @__PURE__ */ jsx6(Text6, { bold: true, color: "cyan", children: tzEdit }),
3399
+ /* @__PURE__ */ jsx6(Text6, { color: "cyan", children: "_" }),
3400
+ /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: "]" })
3401
+ ] }) : /* @__PURE__ */ jsx6(Text6, { bold: true, color: "yellow", children: tzDisplay }) }),
3402
+ cursor === 3 && tzError && /* @__PURE__ */ jsxs6(Text6, { color: "red", children: [
2127
3403
  " ",
2128
3404
  tzError
2129
3405
  ] }),
2130
- /* @__PURE__ */ jsxs5(Row, { cursor, idx: 4, label: "Dashboard", children: [
2131
- /* @__PURE__ */ jsxs5(Text5, { dimColor: true, children: [
2132
- "\u25C2",
3406
+ /* @__PURE__ */ jsxs6(Row, { cursor, idx: 4, label: "Dashboard", children: [
3407
+ /* @__PURE__ */ jsxs6(Text6, { dimColor: true, children: [
3408
+ glyphs().caretL,
2133
3409
  " "
2134
3410
  ] }),
2135
- /* @__PURE__ */ jsx5(Text5, { bold: true, color: "yellow", children: config2.dashboardLayout === "grid" ? "grid (all)" : "single (cycle)" }),
2136
- /* @__PURE__ */ jsxs5(Text5, { dimColor: true, children: [
3411
+ /* @__PURE__ */ jsx6(Text6, { bold: true, color: "yellow", children: config2.dashboardLayout === "grid" ? "grid (all)" : "single (cycle)" }),
3412
+ /* @__PURE__ */ jsxs6(Text6, { dimColor: true, children: [
2137
3413
  " ",
2138
- "\u25B8"
3414
+ glyphs().caretR
2139
3415
  ] })
2140
3416
  ] }),
2141
- /* @__PURE__ */ jsxs5(Row, { cursor, idx: 5, label: "Default focus", children: [
2142
- /* @__PURE__ */ jsxs5(Text5, { dimColor: true, children: [
2143
- "\u25C2",
3417
+ /* @__PURE__ */ jsxs6(Row, { cursor, idx: 5, label: "Default focus", children: [
3418
+ /* @__PURE__ */ jsxs6(Text6, { dimColor: true, children: [
3419
+ glyphs().caretL,
2144
3420
  " "
2145
3421
  ] }),
2146
- /* @__PURE__ */ jsx5(Text5, { bold: true, color: "yellow", children: config2.defaultFocus === "all" ? "All" : "Last account" }),
2147
- /* @__PURE__ */ jsxs5(Text5, { dimColor: true, children: [
3422
+ /* @__PURE__ */ jsx6(Text6, { bold: true, color: "yellow", children: config2.defaultFocus === "all" ? "All" : "Last account" }),
3423
+ /* @__PURE__ */ jsxs6(Text6, { dimColor: true, children: [
2148
3424
  " ",
2149
- "\u25B8"
3425
+ glyphs().caretR
2150
3426
  ] })
2151
3427
  ] }),
2152
- /* @__PURE__ */ jsx5(Box5, { height: 1 }),
2153
- /* @__PURE__ */ jsx5(Text5, { bold: true, dimColor: true, children: "Providers" }),
3428
+ /* @__PURE__ */ jsx6(Box6, { height: 1 }),
3429
+ /* @__PURE__ */ jsx6(Text6, { bold: true, dimColor: true, children: "Providers" }),
2154
3430
  PROVIDER_ORDER.map((pid, i) => {
2155
3431
  const idx = PROVIDER_ROWS_START + i;
2156
3432
  const selected = cursor === idx;
2157
3433
  const enabled = !config2.disabledProviders.includes(pid);
2158
3434
  const p = PROVIDERS[pid];
2159
- return /* @__PURE__ */ jsxs5(Box5, { children: [
2160
- /* @__PURE__ */ jsxs5(Text5, { color: selected ? "green" : void 0, children: [
2161
- selected ? "\u25B8" : " ",
3435
+ return /* @__PURE__ */ jsxs6(Box6, { children: [
3436
+ /* @__PURE__ */ jsxs6(Text6, { color: selected ? "green" : void 0, children: [
3437
+ selected ? glyphs().caretR : " ",
2162
3438
  " "
2163
3439
  ] }),
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" })
3440
+ /* @__PURE__ */ jsx6(Text6, { bold: enabled, color: enabled ? p.color : void 0, dimColor: !enabled, children: enabled ? `[${glyphs().check}]` : "[ ]" }),
3441
+ /* @__PURE__ */ jsxs6(Text6, { color: p.color, children: [
3442
+ " ",
3443
+ glyphs().dot,
3444
+ " "
3445
+ ] }),
3446
+ /* @__PURE__ */ jsx6(Box6, { width: 9, children: /* @__PURE__ */ jsx6(Text6, { bold: selected, children: p.name }) }),
3447
+ /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: enabled ? "tracking" : "off" })
2168
3448
  ] }, pid);
2169
3449
  }),
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" }),
3450
+ /* @__PURE__ */ jsx6(Box6, { height: 1 }),
3451
+ /* @__PURE__ */ jsx6(Text6, { bold: true, dimColor: true, children: "Accounts" }),
3452
+ config2.accounts.length === 0 && /* @__PURE__ */ jsxs6(Text6, { dimColor: true, children: [
3453
+ " none configured ",
3454
+ glyphs().emDash,
3455
+ " enabled providers track automatically"
3456
+ ] }),
2173
3457
  config2.accounts.map((acc, i) => {
2174
3458
  const idx = ACCOUNT_ROWS_START + i;
2175
3459
  const selected = cursor === idx;
2176
3460
  const isActive = acc.id === activeAccountId;
2177
3461
  const provider = PROVIDERS[acc.providerId];
2178
- return /* @__PURE__ */ jsxs5(Box5, { children: [
2179
- /* @__PURE__ */ jsxs5(Text5, { color: selected ? "green" : void 0, children: [
2180
- selected ? "\u25B8" : " ",
3462
+ return /* @__PURE__ */ jsxs6(Box6, { children: [
3463
+ /* @__PURE__ */ jsxs6(Text6, { color: selected ? "green" : void 0, children: [
3464
+ selected ? glyphs().caretR : " ",
2181
3465
  " "
2182
3466
  ] }),
2183
- /* @__PURE__ */ jsxs5(Text5, { color: acc.color || provider.color, children: [
2184
- isActive ? "\u25CF" : "\u25CB",
3467
+ /* @__PURE__ */ jsxs6(Text6, { color: acc.color || provider.color, children: [
3468
+ isActive ? glyphs().dot : glyphs().radioOff,
2185
3469
  " "
2186
3470
  ] }),
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) })
3471
+ /* @__PURE__ */ jsx6(Box6, { width: 16, children: /* @__PURE__ */ jsx6(Text6, { bold: true, children: truncateName(acc.name, 15) }) }),
3472
+ /* @__PURE__ */ jsx6(Box6, { width: 9, children: /* @__PURE__ */ jsx6(Text6, { color: provider.color, children: provider.name }) }),
3473
+ /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: truncateName(acc.homeDir, 24) })
2190
3474
  ] }, acc.id);
2191
3475
  }),
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" : " ",
3476
+ /* @__PURE__ */ jsxs6(Box6, { children: [
3477
+ /* @__PURE__ */ jsxs6(Text6, { color: cursor === ACCOUNT_ROWS_START + config2.accounts.length ? "green" : void 0, children: [
3478
+ cursor === ACCOUNT_ROWS_START + config2.accounts.length ? glyphs().caretR : " ",
2195
3479
  " "
2196
3480
  ] }),
2197
- /* @__PURE__ */ jsx5(Text5, { color: "greenBright", children: "+ " }),
2198
- /* @__PURE__ */ jsx5(Text5, { children: "Add account" })
3481
+ /* @__PURE__ */ jsx6(Text6, { color: "greenBright", children: "+ " }),
3482
+ /* @__PURE__ */ jsx6(Text6, { children: "Add account" })
2199
3483
  ] }),
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" })
3484
+ /* @__PURE__ */ jsx6(Box6, { height: 1 }),
3485
+ editingTz ? /* @__PURE__ */ jsxs6(Text6, { dimColor: true, children: [
3486
+ "type IANA name (e.g. Europe/London) ",
3487
+ glyphs().middot,
3488
+ " empty = System ",
3489
+ glyphs().middot,
3490
+ " Enter save ",
3491
+ glyphs().middot,
3492
+ " Esc cancel"
3493
+ ] }) : cursor >= PROVIDER_ROWS_START && cursor < ACCOUNT_ROWS_START ? /* @__PURE__ */ jsxs6(Text6, { dimColor: true, children: [
3494
+ glyphs().arrowU,
3495
+ glyphs().arrowD,
3496
+ " select ",
3497
+ glyphs().middot,
3498
+ " space toggle provider ",
3499
+ glyphs().middot,
3500
+ " s/Esc close"
3501
+ ] }) : cursor >= ACCOUNT_ROWS_START && cursor < ACCOUNT_ROWS_START + config2.accounts.length ? /* @__PURE__ */ jsxs6(Text6, { dimColor: true, children: [
3502
+ glyphs().arrowU,
3503
+ glyphs().arrowD,
3504
+ " select ",
3505
+ glyphs().middot,
3506
+ " ",
3507
+ glyphs().shift,
3508
+ glyphs().arrowU,
3509
+ glyphs().arrowD,
3510
+ " reorder ",
3511
+ glyphs().middot,
3512
+ " Enter edit ",
3513
+ glyphs().middot,
3514
+ " space activate ",
3515
+ glyphs().middot,
3516
+ " d delete ",
3517
+ glyphs().middot,
3518
+ " s/Esc close"
3519
+ ] }) : cursor === ACCOUNT_ROWS_START + config2.accounts.length ? /* @__PURE__ */ jsxs6(Text6, { dimColor: true, children: [
3520
+ glyphs().arrowU,
3521
+ glyphs().arrowD,
3522
+ " select ",
3523
+ glyphs().middot,
3524
+ " Enter add account ",
3525
+ glyphs().middot,
3526
+ " s/Esc close"
3527
+ ] }) : /* @__PURE__ */ jsxs6(Text6, { dimColor: true, children: [
3528
+ glyphs().arrowU,
3529
+ glyphs().arrowD,
3530
+ " select ",
3531
+ glyphs().arrowL,
3532
+ glyphs().arrowR,
3533
+ " adjust Enter edit s/Esc close"
3534
+ ] })
2202
3535
  ] });
2203
3536
  }
2204
3537
  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" : " ",
3538
+ return /* @__PURE__ */ jsxs6(Box6, { children: [
3539
+ /* @__PURE__ */ jsxs6(Text6, { color: cursor === idx ? "green" : void 0, children: [
3540
+ cursor === idx ? glyphs().caretR : " ",
2208
3541
  " "
2209
3542
  ] }),
2210
- /* @__PURE__ */ jsx5(Box5, { width: 20, children: /* @__PURE__ */ jsx5(Text5, { children: label }) }),
3543
+ /* @__PURE__ */ jsx6(Box6, { width: 20, children: /* @__PURE__ */ jsx6(Text6, { children: label }) }),
2211
3544
  children
2212
3545
  ] });
2213
3546
  }
@@ -2216,24 +3549,24 @@ function AccountFormView({ form, accounts }) {
2216
3549
  const accent = form.color;
2217
3550
  const stepIndex = { provider: 1, name: 2, homeDir: 3, color: 4 };
2218
3551
  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: [
3552
+ return /* @__PURE__ */ jsxs6(Box6, { flexDirection: "column", marginTop: 1, children: [
3553
+ /* @__PURE__ */ jsxs6(Box6, { children: [
3554
+ /* @__PURE__ */ jsx6(Text6, { color: accent, bold: true, children: glyphs().vbar }),
3555
+ /* @__PURE__ */ jsxs6(Text6, { bold: true, children: [
2223
3556
  " ",
2224
3557
  form.mode === "add" ? "NEW ACCOUNT" : "EDIT ACCOUNT"
2225
3558
  ] }),
2226
- /* @__PURE__ */ jsxs5(Text5, { dimColor: true, children: [
3559
+ /* @__PURE__ */ jsxs6(Text6, { dimColor: true, children: [
2227
3560
  " step ",
2228
3561
  step,
2229
3562
  " of 4"
2230
3563
  ] })
2231
3564
  ] }),
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(
3565
+ /* @__PURE__ */ jsx6(Box6, { marginTop: 1, children: /* @__PURE__ */ jsx6(Stepper, { active: form.field, accent }) }),
3566
+ /* @__PURE__ */ jsxs6(Box6, { marginTop: 1, flexDirection: "column", borderStyle: glyphs().border, borderColor: accent, paddingX: 2, paddingY: 1, children: [
3567
+ /* @__PURE__ */ jsx6(ProviderField, { value: form.providerId, focused: form.field === "provider" }),
3568
+ /* @__PURE__ */ jsx6(Box6, { height: 1 }),
3569
+ /* @__PURE__ */ jsx6(
2237
3570
  FormField,
2238
3571
  {
2239
3572
  label: "Name",
@@ -2244,12 +3577,12 @@ function AccountFormView({ form, accounts }) {
2244
3577
  placeholder: "e.g. Work, Personal"
2245
3578
  }
2246
3579
  ),
2247
- /* @__PURE__ */ jsx5(Box5, { height: 1 }),
2248
- /* @__PURE__ */ jsx5(
3580
+ /* @__PURE__ */ jsx6(Box6, { height: 1 }),
3581
+ /* @__PURE__ */ jsx6(
2249
3582
  FormField,
2250
3583
  {
2251
3584
  label: "Home directory",
2252
- hint: "path containing the tool's data dir \xB7 ~ for default",
3585
+ hint: `path containing the tool's data dir ${glyphs().middot} ~ for default`,
2253
3586
  value: form.homeDir,
2254
3587
  focused: form.field === "homeDir",
2255
3588
  accent,
@@ -2257,100 +3590,134 @@ function AccountFormView({ form, accounts }) {
2257
3590
  mono: true
2258
3591
  }
2259
3592
  ),
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" })
3593
+ /* @__PURE__ */ jsx6(Box6, { height: 1 }),
3594
+ /* @__PURE__ */ jsx6(ColorField, { value: form.color, focused: form.field === "color" }),
3595
+ /* @__PURE__ */ jsx6(Box6, { height: 1 }),
3596
+ /* @__PURE__ */ jsxs6(Box6, { children: [
3597
+ /* @__PURE__ */ jsxs6(Text6, { dimColor: true, children: [
3598
+ "id ",
3599
+ glyphs().boxMark,
3600
+ " "
3601
+ ] }),
3602
+ /* @__PURE__ */ jsx6(Text6, { bold: true, color: accent, children: previewId || "account" }),
3603
+ /* @__PURE__ */ jsxs6(Text6, { dimColor: true, children: [
3604
+ " ",
3605
+ glyphs().boxMark,
3606
+ " auto-generated from name"
3607
+ ] })
2267
3608
  ] })
2268
3609
  ] }),
2269
- form.error && /* @__PURE__ */ jsx5(Box5, { marginTop: 1, children: /* @__PURE__ */ jsxs5(Text5, { color: "red", children: [
2270
- "\u26A0 ",
3610
+ form.error && /* @__PURE__ */ jsx6(Box6, { marginTop: 1, children: /* @__PURE__ */ jsxs6(Text6, { color: "red", children: [
3611
+ glyphs().warn,
3612
+ " ",
2271
3613
  form.error
2272
3614
  ] }) }),
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 " })
3615
+ /* @__PURE__ */ jsxs6(Box6, { marginTop: 1, children: [
3616
+ /* @__PURE__ */ jsxs6(Text6, { dimColor: true, children: [
3617
+ "tab/",
3618
+ glyphs().arrowU,
3619
+ glyphs().arrowD,
3620
+ " "
2284
3621
  ] }),
2285
- /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: "esc " }),
2286
- /* @__PURE__ */ jsx5(Text5, { children: "cancel" })
3622
+ /* @__PURE__ */ jsx6(Text6, { children: "switch field" }),
3623
+ /* @__PURE__ */ jsxs6(Text6, { dimColor: true, children: [
3624
+ " ",
3625
+ glyphs().middot,
3626
+ " "
3627
+ ] }),
3628
+ /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: "enter " }),
3629
+ /* @__PURE__ */ jsx6(Text6, { children: form.field === "color" ? "save" : "next" }),
3630
+ /* @__PURE__ */ jsxs6(Text6, { dimColor: true, children: [
3631
+ " ",
3632
+ glyphs().middot,
3633
+ " "
3634
+ ] }),
3635
+ (form.field === "color" || form.field === "provider") && /* @__PURE__ */ jsxs6(Fragment3, { children: [
3636
+ /* @__PURE__ */ jsxs6(Text6, { dimColor: true, children: [
3637
+ glyphs().arrowL,
3638
+ glyphs().arrowR,
3639
+ " "
3640
+ ] }),
3641
+ /* @__PURE__ */ jsx6(Text6, { children: form.field === "provider" ? "pick provider" : "pick color" }),
3642
+ /* @__PURE__ */ jsxs6(Text6, { dimColor: true, children: [
3643
+ " ",
3644
+ glyphs().middot,
3645
+ " "
3646
+ ] })
3647
+ ] }),
3648
+ /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: "esc " }),
3649
+ /* @__PURE__ */ jsx6(Text6, { children: "cancel" })
2287
3650
  ] })
2288
3651
  ] });
2289
3652
  }
2290
- function Stepper({ active, accent }) {
3653
+ function Stepper({ active: active2, accent }) {
2291
3654
  const steps = [
2292
3655
  { id: "provider", label: "Provider" },
2293
3656
  { id: "name", label: "Name" },
2294
3657
  { id: "homeDir", label: "Home" },
2295
3658
  { id: "color", label: "Color" }
2296
3659
  ];
2297
- const activeIdx = steps.findIndex((s) => s.id === active);
2298
- return /* @__PURE__ */ jsx5(Box5, { children: steps.map((s, i) => {
3660
+ const activeIdx = steps.findIndex((s) => s.id === active2);
3661
+ return /* @__PURE__ */ jsx6(Box6, { children: steps.map((s, i) => {
2299
3662
  const done = i < activeIdx;
2300
3663
  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: [
3664
+ const dot = done ? glyphs().dot : cur ? glyphs().dotSel : glyphs().radioOff;
3665
+ return /* @__PURE__ */ jsxs6(Box6, { children: [
3666
+ /* @__PURE__ */ jsxs6(Text6, { color: cur || done ? accent : void 0, dimColor: !cur && !done, children: [
2304
3667
  dot,
2305
3668
  " "
2306
3669
  ] }),
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 " })
3670
+ /* @__PURE__ */ jsx6(Text6, { bold: cur, color: cur ? accent : void 0, dimColor: !cur, children: s.label }),
3671
+ i < steps.length - 1 && /* @__PURE__ */ jsxs6(Text6, { dimColor: true, children: [
3672
+ " ",
3673
+ glyphs().rule,
3674
+ " "
3675
+ ] })
2309
3676
  ] }, s.id);
2310
3677
  }) });
2311
3678
  }
2312
3679
  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" : " ",
3680
+ return /* @__PURE__ */ jsxs6(Box6, { flexDirection: "column", children: [
3681
+ /* @__PURE__ */ jsx6(Box6, { children: /* @__PURE__ */ jsxs6(Text6, { color: focused ? PROVIDERS[value].color : void 0, bold: focused, dimColor: !focused, children: [
3682
+ focused ? glyphs().caretR : " ",
2316
3683
  " Provider"
2317
3684
  ] }) }),
2318
- /* @__PURE__ */ jsxs5(Box5, { children: [
2319
- /* @__PURE__ */ jsxs5(Text5, { children: [
3685
+ /* @__PURE__ */ jsxs6(Box6, { children: [
3686
+ /* @__PURE__ */ jsxs6(Text6, { children: [
2320
3687
  " ",
2321
- focused ? "\u258C" : " ",
3688
+ focused ? glyphs().vbar : " ",
2322
3689
  " "
2323
3690
  ] }),
2324
3691
  PROVIDER_ORDER.map((pid) => {
2325
3692
  const selected = pid === value;
2326
3693
  const p = PROVIDERS[pid];
2327
- return /* @__PURE__ */ jsx5(Box5, { marginRight: 2, children: selected ? /* @__PURE__ */ jsxs5(Text5, { bold: true, color: p.color, children: [
3694
+ return /* @__PURE__ */ jsx6(Box6, { marginRight: 2, children: selected ? /* @__PURE__ */ jsxs6(Text6, { bold: true, color: p.color, children: [
2328
3695
  "[",
2329
3696
  p.name,
2330
3697
  "]"
2331
- ] }) : /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: p.name }) }, pid);
3698
+ ] }) : /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: p.name }) }, pid);
2332
3699
  })
2333
3700
  ] }),
2334
- /* @__PURE__ */ jsx5(Box5, { children: /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: " which tool this account tracks" }) })
3701
+ /* @__PURE__ */ jsx6(Box6, { children: /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: " which tool this account tracks" }) })
2335
3702
  ] });
2336
3703
  }
2337
3704
  function FormField({ label, hint, value, focused, accent, placeholder, mono }) {
2338
3705
  const isPlaceholder = value === "";
2339
3706
  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" : " ",
3707
+ return /* @__PURE__ */ jsxs6(Box6, { flexDirection: "column", children: [
3708
+ /* @__PURE__ */ jsx6(Box6, { children: /* @__PURE__ */ jsxs6(Text6, { color: focused ? accent : void 0, bold: focused, dimColor: !focused, children: [
3709
+ focused ? glyphs().caretR : " ",
2343
3710
  " ",
2344
3711
  label
2345
3712
  ] }) }),
2346
- /* @__PURE__ */ jsxs5(Box5, { children: [
2347
- /* @__PURE__ */ jsxs5(Text5, { color: focused ? accent : void 0, children: [
3713
+ /* @__PURE__ */ jsxs6(Box6, { children: [
3714
+ /* @__PURE__ */ jsxs6(Text6, { color: focused ? accent : void 0, children: [
2348
3715
  " ",
2349
- focused ? "\u258C" : " ",
3716
+ focused ? glyphs().vbar : " ",
2350
3717
  " "
2351
3718
  ] }),
2352
- /* @__PURE__ */ jsx5(
2353
- Text5,
3719
+ /* @__PURE__ */ jsx6(
3720
+ Text6,
2354
3721
  {
2355
3722
  bold: focused && !isPlaceholder,
2356
3723
  color: focused && !isPlaceholder ? accent : void 0,
@@ -2359,39 +3726,58 @@ function FormField({ label, hint, value, focused, accent, placeholder, mono }) {
2359
3726
  children: display
2360
3727
  }
2361
3728
  ),
2362
- focused && /* @__PURE__ */ jsx5(Text5, { color: accent, children: "\u258F" })
3729
+ focused && /* @__PURE__ */ jsx6(Text6, { color: accent, children: glyphs().vbar })
2363
3730
  ] }),
2364
- /* @__PURE__ */ jsx5(Box5, { children: /* @__PURE__ */ jsxs5(Text5, { dimColor: true, children: [
3731
+ /* @__PURE__ */ jsx6(Box6, { children: /* @__PURE__ */ jsxs6(Text6, { dimColor: true, children: [
2365
3732
  " ",
2366
3733
  hint
2367
3734
  ] }) })
2368
3735
  ] });
2369
3736
  }
2370
3737
  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" : " ",
3738
+ return /* @__PURE__ */ jsxs6(Box6, { flexDirection: "column", children: [
3739
+ /* @__PURE__ */ jsx6(Box6, { children: /* @__PURE__ */ jsxs6(Text6, { color: focused ? value : void 0, bold: focused, dimColor: !focused, children: [
3740
+ focused ? glyphs().caretR : " ",
2374
3741
  " Accent color"
2375
3742
  ] }) }),
2376
- /* @__PURE__ */ jsxs5(Box5, { children: [
2377
- /* @__PURE__ */ jsxs5(Text5, { children: [
3743
+ /* @__PURE__ */ jsxs6(Box6, { children: [
3744
+ /* @__PURE__ */ jsxs6(Text6, { children: [
2378
3745
  " ",
2379
- focused ? "\u258C" : " ",
3746
+ focused ? glyphs().vbar : " ",
2380
3747
  " "
2381
3748
  ] }),
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))
3749
+ COLOR_PALETTE.map((c) => /* @__PURE__ */ jsx6(Box6, { marginRight: 1, children: c === value ? /* @__PURE__ */ jsxs6(Text6, { bold: true, color: c, children: [
3750
+ "[",
3751
+ glyphs().dot,
3752
+ "]"
3753
+ ] }) : /* @__PURE__ */ jsxs6(Text6, { color: c, dimColor: !focused, children: [
3754
+ " ",
3755
+ glyphs().dot
3756
+ ] }) }, c))
2383
3757
  ] }),
2384
- /* @__PURE__ */ jsx5(Box5, { children: /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: " shows on dashboard, account strip, borders" }) })
3758
+ /* @__PURE__ */ jsx6(Box6, { children: /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: " shows on dashboard, account strip, borders" }) })
2385
3759
  ] });
2386
3760
  }
2387
3761
 
2388
3762
  // src/app.tsx
2389
- import { Fragment as Fragment4, jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
3763
+ import { Fragment as Fragment4, jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
2390
3764
  var TABS = ["Dashboard", "Table"];
2391
3765
  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"];
3766
+ var SORTS = [
3767
+ { label: "date", dir: "up" },
3768
+ { label: "date", dir: "down" },
3769
+ { label: "cost", dir: "up" },
3770
+ { label: "cost", dir: "down" }
3771
+ ];
3772
+ var CURSOR_SORTS = [
3773
+ { label: "cost", dir: "down" },
3774
+ { label: "amount", dir: "down" },
3775
+ { label: "model", dir: null }
3776
+ ];
2394
3777
  var IS_TTY = process.stdin.isTTY === true;
3778
+ var DEBOUNCE_MS = 300;
3779
+ var LOADER_GRACE_MS = 600;
3780
+ var LOADER_MAX_MS = 8e3;
2395
3781
  var DEFAULT_CONFIG = {
2396
3782
  interval: 2,
2397
3783
  billingInterval: 5,
@@ -2402,33 +3788,39 @@ var DEFAULT_CONFIG = {
2402
3788
  disabledProviders: [],
2403
3789
  onboarded: false,
2404
3790
  dashboardLayout: "grid",
2405
- defaultFocus: "all"
3791
+ defaultFocus: "all",
3792
+ ascii: "auto",
3793
+ knownProviders: []
2406
3794
  };
2407
3795
  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);
3796
+ const [config2, setConfig] = useState3(null);
3797
+ const [detected, setDetected] = useState3([]);
3798
+ const [stats, setStats] = useState3(/* @__PURE__ */ new Map());
3799
+ const [peak, setPeak] = useState3(null);
3800
+ const [table, setTable] = useState3(null);
3801
+ const [tableLoading, setTableLoading] = useState3(false);
3802
+ const [error, setError] = useState3(null);
3803
+ const [updated, setUpdated] = useState3(/* @__PURE__ */ new Date());
3804
+ const [tab, setTab] = useState3(0);
3805
+ const [view, setView] = useState3(0);
3806
+ const [cursor, setCursor] = useState3(0);
3807
+ const [expanded, setExpanded] = useState3(-1);
3808
+ const [sort, setSort] = useState3(1);
3809
+ const [tableProvider, setTableProvider] = useState3(null);
3810
+ const [search, setSearch] = useState3("");
3811
+ const [searchMode, setSearchMode] = useState3(false);
3812
+ const [cursorRows, setCursorRows] = useState3(null);
3813
+ const [showSettings, setShowSettings] = useState3(false);
3814
+ const [settingsCursor, setSettingsCursor] = useState3(0);
3815
+ const [tzEdit, setTzEdit] = useState3(null);
3816
+ const [tzError, setTzError] = useState3(null);
3817
+ const [accountForm, setAccountForm] = useState3(null);
3818
+ const [onboardSel, setOnboardSel] = useState3(null);
3819
+ const [onboardCursor, setOnboardCursor] = useState3(0);
3820
+ const [dashPage, setDashPage] = useState3(0);
3821
+ const [debouncePassed, setDebouncePassed] = useState3(false);
3822
+ const [graceHold, setGraceHold] = useState3(false);
3823
+ const loaderDone = useRef2(false);
2432
3824
  const { stdout } = useStdout();
2433
3825
  const { exit } = useApp();
2434
3826
  const rows = stdout?.rows ?? 24;
@@ -2442,6 +3834,8 @@ function App({ interval: cliInterval }) {
2442
3834
  const accountsRef = useRef2([]);
2443
3835
  accountsRef.current = accounts;
2444
3836
  const rowCountRef = useRef2(0);
3837
+ const dashPageCountRef = useRef2(1);
3838
+ const seededRef = useRef2(false);
2445
3839
  const accountsKey = accounts.map((a) => `${a.id}:${a.homeDir ?? ""}`).join("|");
2446
3840
  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
3841
  const activeSlotIdx = (() => {
@@ -2452,21 +3846,49 @@ function App({ interval: cliInterval }) {
2452
3846
  const focusId = slots[activeSlotIdx]?.id ?? null;
2453
3847
  const visibleAccounts = focusId === null ? accounts : accounts.filter((a) => a.id === focusId);
2454
3848
  const groups = accountsByProvider(visibleAccounts);
3849
+ const TOO_SMALL = cols < 40 || rows < 12;
3850
+ const allGroups = accountsByProvider(accounts);
3851
+ const allReady = accounts.length > 0 && accounts.every((a) => accountReady(stats.get(a.id), a.providerId));
3852
+ const hasStrip = slots.length > 1;
3853
+ const stripChipW = (s) => 2 + 2 + truncateName(s.name, 16).length + 2;
3854
+ const stripChars = slots.reduce((sum, s) => sum + stripChipW(s), 0);
3855
+ const stripLines = hasStrip ? Math.max(1, Math.ceil(stripChars / Math.max(
3856
+ 1,
3857
+ cols - 4 - 7
3858
+ /*"focus "*/
3859
+ ))) : 0;
3860
+ const headerRows = cols < 70 ? 2 : 1;
3861
+ const CHROME = 2 + headerRows + 3 + (hasStrip ? 1 + stripLines : 0) + 2;
3862
+ const gridBudget = Math.max(1, rows - CHROME);
3863
+ const dashLayout = chooseLayout(
3864
+ Math.max(56, cols - 4),
3865
+ gridBudget,
3866
+ groups.length,
3867
+ focusId !== null || cfg.dashboardLayout === "single",
3868
+ cols
3869
+ );
3870
+ const dashPageCount = dashLayout.pageCount;
3871
+ const dashPaginated = dashPageCount > 1;
3872
+ dashPageCountRef.current = dashPageCount;
2455
3873
  const tableProvs = accountsByProvider(accounts).map((g) => g.provider);
2456
3874
  const effTableProvider = tableProvider && tableProvs.includes(tableProvider) ? tableProvider : tableProvs[0] ?? null;
2457
3875
  const tableIsCursor = !!effTableProvider && !PROVIDERS[effTableProvider].hasUsage;
2458
3876
  const tableAccounts = effTableProvider ? accounts.filter((a) => a.providerId === effTableProvider) : [];
2459
3877
  const SORTS_FOR = tableIsCursor ? CURSOR_SORTS : SORTS;
2460
3878
  const needsOnboarding = configReady && !cfg.onboarded;
3879
+ const newProviders = configReady && cfg.onboarded ? PROVIDER_ORDER.filter((p) => !cfg.knownProviders.includes(p) && detected.includes(p)) : [];
3880
+ const showPicker = needsOnboarding || newProviders.length > 0;
3881
+ const showLoader = configReady && !showPicker && !showSettings && !TOO_SMALL && accounts.length > 0 && (!allReady || graceHold) && debouncePassed && !loaderDone.current;
3882
+ const pickerProviders = needsOnboarding ? PROVIDER_ORDER : newProviders;
2461
3883
  const onboardEnabled = onboardSel ?? detected;
2462
- const onboardItems = PROVIDER_ORDER.map((pid) => ({
3884
+ const onboardItems = pickerProviders.map((pid) => ({
2463
3885
  id: pid,
2464
3886
  name: PROVIDERS[pid].name,
2465
3887
  color: PROVIDERS[pid].color,
2466
3888
  detected: detected.includes(pid),
2467
3889
  enabled: onboardEnabled.includes(pid)
2468
3890
  }));
2469
- useEffect2(() => {
3891
+ useEffect3(() => {
2470
3892
  loadConfig().then((c) => {
2471
3893
  if (cliInterval) c = { ...c, interval: cliInterval / 1e3 };
2472
3894
  if (c.defaultFocus === "all") c = { ...c, activeAccountId: null };
@@ -2474,9 +3896,55 @@ function App({ interval: cliInterval }) {
2474
3896
  });
2475
3897
  detectProviders().then(setDetected);
2476
3898
  }, []);
2477
- useEffect2(() => {
3899
+ useEffect3(() => {
3900
+ if (seededRef.current || !configReady || accounts.length === 0) return;
3901
+ seededRef.current = true;
3902
+ loadSnapshot().then((snap) => {
3903
+ setStats((prev) => {
3904
+ if (prev.size > 0) return prev;
3905
+ const next = new Map(prev);
3906
+ for (const acc of accountsRef.current) {
3907
+ const s = snap[acc.id];
3908
+ if (s && (s.dashboard || s.billing)) next.set(acc.id, { account: acc, dashboard: s.dashboard ?? null, billing: s.billing ?? null });
3909
+ }
3910
+ return next;
3911
+ });
3912
+ });
3913
+ }, [configReady, accountsKey]);
3914
+ useEffect3(() => {
3915
+ if (stats.size === 0) return;
3916
+ const t = setTimeout(() => saveSnapshot(stats), 500);
3917
+ return () => clearTimeout(t);
3918
+ }, [stats]);
3919
+ useEffect3(() => {
3920
+ if (!configReady || showPicker || accounts.length === 0) return;
3921
+ if (allReady || loaderDone.current) return;
3922
+ const debounce = setTimeout(() => setDebouncePassed(true), DEBOUNCE_MS);
3923
+ const deadline = setTimeout(() => {
3924
+ loaderDone.current = true;
3925
+ setDebouncePassed(false);
3926
+ }, LOADER_MAX_MS);
3927
+ return () => {
3928
+ clearTimeout(debounce);
3929
+ clearTimeout(deadline);
3930
+ };
3931
+ }, [configReady, showPicker, accountsKey]);
3932
+ useEffect3(() => {
3933
+ if (!allReady || loaderDone.current) return;
3934
+ if (!debouncePassed) {
3935
+ loaderDone.current = true;
3936
+ return;
3937
+ }
3938
+ setGraceHold(true);
3939
+ const t = setTimeout(() => {
3940
+ loaderDone.current = true;
3941
+ setGraceHold(false);
3942
+ }, LOADER_GRACE_MS);
3943
+ return () => clearTimeout(t);
3944
+ }, [allReady]);
3945
+ useEffect3(() => {
2478
3946
  if (!configReady) return;
2479
- let active = true;
3947
+ let active2 = true;
2480
3948
  let timer;
2481
3949
  const load = async () => {
2482
3950
  try {
@@ -2485,27 +3953,27 @@ function App({ interval: cliInterval }) {
2485
3953
  if (!provider.hasUsage || !provider.fetchSummary) return;
2486
3954
  try {
2487
3955
  const dashboard = await provider.fetchSummary(acc, tz);
2488
- if (active) setStats((prev) => upsert(prev, acc, { dashboard }));
3956
+ if (active2) setStats((prev) => upsert(prev, acc, { dashboard }));
2489
3957
  } catch {
2490
3958
  }
2491
3959
  }));
2492
- if (active) {
3960
+ if (active2) {
2493
3961
  setError(null);
2494
3962
  setUpdated(/* @__PURE__ */ new Date());
2495
3963
  }
2496
3964
  } finally {
2497
- if (active) timer = setTimeout(load, interval2);
3965
+ if (active2) timer = setTimeout(load, interval2);
2498
3966
  }
2499
3967
  };
2500
3968
  load();
2501
3969
  return () => {
2502
- active = false;
3970
+ active2 = false;
2503
3971
  clearTimeout(timer);
2504
3972
  };
2505
3973
  }, [interval2, tz, configReady, accountsKey]);
2506
- useEffect2(() => {
3974
+ useEffect3(() => {
2507
3975
  if (!configReady) return;
2508
- let active = true;
3976
+ let active2 = true;
2509
3977
  let timer;
2510
3978
  const load = async () => {
2511
3979
  try {
@@ -2515,42 +3983,42 @@ function App({ interval: cliInterval }) {
2515
3983
  if (!provider.hasBilling || !provider.fetchBilling) return;
2516
3984
  try {
2517
3985
  const billing = await provider.fetchBilling(acc);
2518
- if (active) setStats((prev) => upsert(prev, acc, { billing }));
3986
+ if (active2) setStats((prev) => upsert(prev, acc, { billing }));
2519
3987
  } catch {
2520
3988
  }
2521
3989
  }));
2522
3990
  const p = await peakP;
2523
- if (active && p) setPeak(p);
3991
+ if (active2 && p) setPeak(p);
2524
3992
  } finally {
2525
- if (active) timer = setTimeout(load, billingMs);
3993
+ if (active2) timer = setTimeout(load, billingMs);
2526
3994
  }
2527
3995
  };
2528
3996
  load();
2529
3997
  return () => {
2530
- active = false;
3998
+ active2 = false;
2531
3999
  clearTimeout(timer);
2532
4000
  };
2533
4001
  }, [billingMs, configReady, accountsKey]);
2534
4002
  const tableKey = `${effTableProvider}|${tableAccounts.map((a) => `${a.id}:${a.homeDir ?? ""}`).join(",")}|${tz}`;
2535
- useEffect2(() => {
4003
+ useEffect3(() => {
2536
4004
  setTable(null);
2537
4005
  setCursorRows(null);
2538
4006
  setCursor(0);
2539
4007
  setExpanded(-1);
2540
4008
  setSort(tableIsCursor ? 0 : 1);
2541
4009
  }, [tableKey]);
2542
- useEffect2(() => {
4010
+ useEffect3(() => {
2543
4011
  if (tab !== 1 || !effTableProvider) return;
2544
- let active = true;
4012
+ let active2 = true;
2545
4013
  let timer;
2546
4014
  const fetchOnce = async () => {
2547
4015
  try {
2548
4016
  if (tableIsCursor) {
2549
4017
  const s = await cursorModelSpend(tableAccounts[0]?.homeDir);
2550
- if (active) setCursorRows(s?.models ?? []);
4018
+ if (active2) setCursorRows(s?.models ?? []);
2551
4019
  } else {
2552
4020
  const r = await fetchScopeTable(tableAccounts, tz);
2553
- if (active) setTable(r);
4021
+ if (active2) setTable(r);
2554
4022
  }
2555
4023
  } catch {
2556
4024
  }
@@ -2558,35 +4026,43 @@ function App({ interval: cliInterval }) {
2558
4026
  const run = async () => {
2559
4027
  setTableLoading(true);
2560
4028
  await fetchOnce();
2561
- if (!active) return;
4029
+ if (!active2) return;
2562
4030
  setTableLoading(false);
2563
4031
  const loop = async () => {
2564
4032
  await fetchOnce();
2565
- if (active) timer = setTimeout(loop, Math.max(interval2, 1e4));
4033
+ if (active2) timer = setTimeout(loop, Math.max(interval2, 1e4));
2566
4034
  };
2567
4035
  timer = setTimeout(loop, Math.max(interval2, 1e4));
2568
4036
  };
2569
4037
  run();
2570
4038
  return () => {
2571
- active = false;
4039
+ active2 = false;
2572
4040
  clearTimeout(timer);
2573
4041
  };
2574
4042
  }, [tab, tableKey, interval2]);
2575
- useEffect2(() => {
4043
+ useEffect3(() => {
2576
4044
  setCursor(0);
2577
4045
  setExpanded(-1);
2578
4046
  }, [search]);
4047
+ useEffect3(() => {
4048
+ setDashPage((p) => Math.min(p, dashPageCount - 1));
4049
+ }, [dashPageCount]);
2579
4050
  const resetView = useCallback(() => {
2580
4051
  setCursor(0);
2581
4052
  setExpanded(-1);
2582
4053
  }, []);
2583
4054
  const clampRow = (n) => Math.max(0, Math.min(rowCountRef.current - 1, n));
2584
4055
  const mouse = useMouse();
2585
- useEffect2(() => {
4056
+ useEffect3(() => {
2586
4057
  if (!IS_TTY) return;
2587
4058
  mouse.enable();
2588
4059
  const onScroll = (_pos, dir) => {
2589
- if (tab === 1) setCursor((c) => dir === "scrollup" ? Math.max(0, c - 3) : c + 3);
4060
+ const up = dir === "scrollup";
4061
+ if (tab === 1) {
4062
+ setCursor((c) => up ? Math.max(0, c - 3) : c + 3);
4063
+ } else if (tab === 0 && dashPageCountRef.current > 1) {
4064
+ setDashPage((p) => up ? Math.max(0, p - 1) : Math.min(dashPageCountRef.current - 1, p + 1));
4065
+ }
2590
4066
  };
2591
4067
  mouse.events.on("scroll", onScroll);
2592
4068
  return () => {
@@ -2601,8 +4077,8 @@ function App({ interval: cliInterval }) {
2601
4077
  });
2602
4078
  }
2603
4079
  function toggleOnboard(i) {
2604
- if (i < 0 || i >= PROVIDER_ORDER.length) return;
2605
- const pid = PROVIDER_ORDER[i];
4080
+ if (i < 0 || i >= pickerProviders.length) return;
4081
+ const pid = pickerProviders[i];
2606
4082
  setOnboardSel((prev) => {
2607
4083
  const base = prev ?? detected;
2608
4084
  return base.includes(pid) ? base.filter((p) => p !== pid) : [...base, pid];
@@ -2611,16 +4087,31 @@ function App({ interval: cliInterval }) {
2611
4087
  function toggleProvider(pid) {
2612
4088
  updateConfig((c) => ({
2613
4089
  ...c,
4090
+ // Toggling in settings is also an explicit decision → mark it known.
4091
+ knownProviders: c.knownProviders.includes(pid) ? c.knownProviders : [...c.knownProviders, pid],
2614
4092
  disabledProviders: c.disabledProviders.includes(pid) ? c.disabledProviders.filter((p) => p !== pid) : [...c.disabledProviders, pid]
2615
4093
  }));
2616
4094
  }
2617
4095
  function confirmOnboarding() {
2618
4096
  const enabled = onboardEnabled;
2619
- updateConfig((c) => ({
2620
- ...c,
2621
- disabledProviders: PROVIDER_ORDER.filter((p) => !enabled.includes(p)),
2622
- onboarded: true
2623
- }));
4097
+ updateConfig((c) => {
4098
+ if (!c.onboarded) {
4099
+ return {
4100
+ ...c,
4101
+ disabledProviders: PROVIDER_ORDER.filter((p) => !enabled.includes(p)),
4102
+ knownProviders: [...PROVIDER_ORDER],
4103
+ onboarded: true
4104
+ };
4105
+ }
4106
+ const newlyDisabled = pickerProviders.filter((p) => !enabled.includes(p));
4107
+ return {
4108
+ ...c,
4109
+ disabledProviders: [.../* @__PURE__ */ new Set([...c.disabledProviders, ...newlyDisabled])],
4110
+ knownProviders: [.../* @__PURE__ */ new Set([...c.knownProviders, ...pickerProviders])]
4111
+ };
4112
+ });
4113
+ setOnboardSel(null);
4114
+ setOnboardCursor(0);
2624
4115
  }
2625
4116
  function cycleAccount(dir) {
2626
4117
  if (slots.length <= 1) return;
@@ -2724,12 +4215,12 @@ function App({ interval: cliInterval }) {
2724
4215
  }
2725
4216
  const totalSettingsRows = ACCOUNT_ROWS_START + cfg.accounts.length + 1;
2726
4217
  useInput((input, key) => {
2727
- if (needsOnboarding) {
4218
+ if (showPicker) {
2728
4219
  if (input === "q") {
2729
4220
  exit();
2730
4221
  return;
2731
4222
  }
2732
- const startIdx = PROVIDER_ORDER.length;
4223
+ const startIdx = pickerProviders.length;
2733
4224
  if (key.upArrow) {
2734
4225
  setOnboardCursor((c) => Math.max(0, c - 1));
2735
4226
  return;
@@ -2968,6 +4459,16 @@ function App({ interval: cliInterval }) {
2968
4459
  }
2969
4460
  return;
2970
4461
  }
4462
+ if (tab === 0 && dashPaginated) {
4463
+ if (input === "]" || key.downArrow || key.pageDown) {
4464
+ setDashPage((p) => Math.min(dashPageCount - 1, p + 1));
4465
+ return;
4466
+ }
4467
+ if (input === "[" || key.upArrow || key.pageUp) {
4468
+ setDashPage((p) => Math.max(0, p - 1));
4469
+ return;
4470
+ }
4471
+ }
2971
4472
  if (tab === 1) {
2972
4473
  if (input === "p") {
2973
4474
  cycleTableProvider(1);
@@ -3046,36 +4547,58 @@ function App({ interval: cliInterval }) {
3046
4547
  return;
3047
4548
  }
3048
4549
  }, { 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 }) });
4550
+ if (error) return /* @__PURE__ */ jsx7(Box7, { padding: 1, children: /* @__PURE__ */ jsx7(Text7, { color: "red", children: error }) });
4551
+ if (!config2) return /* @__PURE__ */ jsx7(Box7, { padding: 1, children: /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: "Loading..." }) });
4552
+ if (showPicker) {
4553
+ return /* @__PURE__ */ jsx7(Box7, { flexDirection: "column", paddingX: 2, paddingY: 1, height: rows, children: /* @__PURE__ */ jsx7(
4554
+ Onboarding,
4555
+ {
4556
+ items: onboardItems,
4557
+ cursor: onboardCursor,
4558
+ onToggle: toggleOnboard,
4559
+ onConfirm: confirmOnboarding,
4560
+ heading: needsOnboarding ? "Welcome to tokmon" : "New providers detected",
4561
+ 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."
4562
+ }
4563
+ ) });
4564
+ }
4565
+ if (showLoader) {
4566
+ return /* @__PURE__ */ jsx7(Box7, { flexDirection: "column", paddingX: 2, paddingY: 1, height: rows, overflow: "hidden", children: /* @__PURE__ */ jsx7(LoadingView, { groups: allGroups, stats, cols, rows }) });
4567
+ }
4568
+ if (TOO_SMALL && !showSettings) {
4569
+ return /* @__PURE__ */ jsx7(TinyFallback, { groups, stats, rows, cols });
3053
4570
  }
3054
4571
  const tokenRows = sortRows(filterTokenRows(table ? [table.daily, table.weekly, table.monthly][view] : [], search), sort);
3055
4572
  const cursorTableRows = sortCursorRows(filterCursorRows(cursorRows ?? [], search), sort);
3056
4573
  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",
4574
+ return /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", paddingX: 2, paddingY: 1, height: rows, overflow: "hidden", children: [
4575
+ /* @__PURE__ */ jsxs7(Box7, { justifyContent: "space-between", children: [
4576
+ /* @__PURE__ */ jsxs7(Box7, { children: [
4577
+ /* @__PURE__ */ jsxs7(Text7, { bold: true, color: "greenBright", children: [
4578
+ glyphs().dotSel,
3062
4579
  " tokmon"
3063
4580
  ] }),
3064
- /* @__PURE__ */ jsxs6(Text6, { dimColor: true, children: [
3065
- " \xB7 every ",
4581
+ /* @__PURE__ */ jsxs7(Text7, { dimColor: true, children: [
4582
+ " ",
4583
+ glyphs().middot,
4584
+ " every ",
3066
4585
  cfg.interval,
3067
4586
  "s"
3068
4587
  ] })
3069
4588
  ] }),
3070
- /* @__PURE__ */ jsxs6(Box6, { children: [
3071
- peak && /* @__PURE__ */ jsxs6(Fragment4, { children: [
3072
- /* @__PURE__ */ jsx6(PeakBadge, { peak }),
3073
- /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: " \xB7 " })
4589
+ /* @__PURE__ */ jsxs7(Box7, { children: [
4590
+ peak && /* @__PURE__ */ jsxs7(Fragment4, { children: [
4591
+ /* @__PURE__ */ jsx7(PeakBadge, { peak }),
4592
+ /* @__PURE__ */ jsxs7(Text7, { dimColor: true, children: [
4593
+ " ",
4594
+ glyphs().middot,
4595
+ " "
4596
+ ] })
3074
4597
  ] }),
3075
- /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: time(updated, tz) })
4598
+ /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: time(updated, tz) })
3076
4599
  ] })
3077
4600
  ] }),
3078
- showSettings ? /* @__PURE__ */ jsx6(
4601
+ showSettings ? /* @__PURE__ */ jsx7(
3079
4602
  SettingsView,
3080
4603
  {
3081
4604
  config: cfg,
@@ -3086,19 +4609,23 @@ function App({ interval: cliInterval }) {
3086
4609
  accountForm,
3087
4610
  activeAccountId: cfg.activeAccountId
3088
4611
  }
3089
- ) : /* @__PURE__ */ jsxs6(Fragment4, { children: [
3090
- /* @__PURE__ */ jsxs6(Box6, { marginTop: 1, marginBottom: 1, children: [
3091
- /* @__PURE__ */ jsx6(TabBar, { tabs: TABS, active: tab, onSelect: (i) => {
4612
+ ) : /* @__PURE__ */ jsxs7(Fragment4, { children: [
4613
+ /* @__PURE__ */ jsxs7(Box7, { marginTop: 1, marginBottom: 1, children: [
4614
+ /* @__PURE__ */ jsx7(TabBar, { tabs: TABS, active: tab, onSelect: (i) => {
3092
4615
  setTab(i);
3093
4616
  resetView();
3094
4617
  } }),
3095
- /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: " Tab/\u2190\u2192" })
4618
+ /* @__PURE__ */ jsxs7(Text7, { dimColor: true, children: [
4619
+ " Tab/",
4620
+ glyphs().arrowL,
4621
+ glyphs().arrowR
4622
+ ] })
3096
4623
  ] }),
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(
4624
+ tab === 0 && /* @__PURE__ */ jsxs7(Fragment4, { children: [
4625
+ /* @__PURE__ */ jsx7(DashboardView, { groups, stats, cols, budget: gridBudget, focusId, layout: cfg.dashboardLayout, page: dashPage }),
4626
+ slots.length > 1 && /* @__PURE__ */ jsxs7(Box7, { marginTop: 1, children: [
4627
+ /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: "focus " }),
4628
+ /* @__PURE__ */ jsx7(
3102
4629
  AccountStrip,
3103
4630
  {
3104
4631
  slots,
@@ -3111,28 +4638,32 @@ function App({ interval: cliInterval }) {
3111
4638
  )
3112
4639
  ] })
3113
4640
  ] }),
3114
- tab === 1 && /* @__PURE__ */ jsxs6(Fragment4, { children: [
3115
- tableProvs.length > 0 && /* @__PURE__ */ jsx6(TableProviderBar, { providers: tableProvs, active: effTableProvider, onSelect: (p) => {
4641
+ tab === 1 && /* @__PURE__ */ jsxs7(Fragment4, { children: [
4642
+ tableProvs.length > 0 && /* @__PURE__ */ jsx7(TableProviderBar, { providers: tableProvs, active: effTableProvider, onSelect: (p) => {
3116
4643
  setTableProvider(p);
3117
4644
  setCursor(0);
3118
4645
  setExpanded(-1);
3119
4646
  setSearch("");
3120
4647
  setSearchMode(false);
3121
4648
  } }),
3122
- /* @__PURE__ */ jsx6(Box6, { height: 1 }),
3123
- /* @__PURE__ */ jsx6(
4649
+ /* @__PURE__ */ jsx7(Box7, { height: 1 }),
4650
+ /* @__PURE__ */ jsx7(
3124
4651
  ControlBar,
3125
4652
  {
3126
4653
  views: VIEWS,
3127
4654
  period: view,
3128
- sort: SORTS_FOR[sort % SORTS_FOR.length],
4655
+ sort: sortLabel(SORTS_FOR[sort % SORTS_FOR.length]),
3129
4656
  search,
3130
4657
  searching: searchMode,
3131
4658
  showPeriod: !tableIsCursor
3132
4659
  }
3133
4660
  ),
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(
4661
+ /* @__PURE__ */ jsx7(Box7, { height: 1 }),
4662
+ !effTableProvider ? /* @__PURE__ */ jsxs7(Text7, { dimColor: true, children: [
4663
+ "No providers enabled ",
4664
+ glyphs().emDash,
4665
+ " press s to pick providers."
4666
+ ] }) : tableLoading && !table && !cursorRows ? /* @__PURE__ */ jsx7(Spinner, { label: "Loading history" }) : tableIsCursor ? /* @__PURE__ */ jsx7(
3136
4667
  CursorSpendTable,
3137
4668
  {
3138
4669
  rows: cursorTableRows,
@@ -3140,7 +4671,7 @@ function App({ interval: cliInterval }) {
3140
4671
  maxRows: Math.max(1, rows - 16),
3141
4672
  onRowClick: (idx) => setCursor(idx)
3142
4673
  }
3143
- ) : /* @__PURE__ */ jsx6(
4674
+ ) : /* @__PURE__ */ jsx7(
3144
4675
  TokenTable,
3145
4676
  {
3146
4677
  rows: tokenRows,
@@ -3156,7 +4687,7 @@ function App({ interval: cliInterval }) {
3156
4687
  )
3157
4688
  ] })
3158
4689
  ] }),
3159
- (tab === 0 || showSettings) && /* @__PURE__ */ jsx6(Footer, { hasAccounts: slots.length > 1 })
4690
+ (tab === 0 || showSettings) && /* @__PURE__ */ jsx7(Footer, { hasAccounts: slots.length > 1, paginated: tab === 0 && dashPaginated, cols })
3160
4691
  ] });
3161
4692
  }
3162
4693
  function upsert(prev, account, patch) {
@@ -3180,6 +4711,11 @@ async function fetchScopeTable(scope, tz) {
3180
4711
  if (valid.length === 1) return valid[0];
3181
4712
  return mergeTables(valid);
3182
4713
  }
4714
+ function sortLabel(entry) {
4715
+ if (entry.dir === "up") return `${entry.label} ${glyphs().arrowU}`;
4716
+ if (entry.dir === "down") return `${entry.label} ${glyphs().arrowD}`;
4717
+ return entry.label;
4718
+ }
3183
4719
  function sortRows(rows, sortIdx) {
3184
4720
  const sorted = [...rows];
3185
4721
  switch (sortIdx % SORTS.length) {
@@ -3217,47 +4753,113 @@ function sortCursorRows(rows, sortIdx) {
3217
4753
  }
3218
4754
  }
3219
4755
  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";
4756
+ return /* @__PURE__ */ jsx7(Box7, { flexWrap: "wrap", children: slots.map((s, i) => {
4757
+ const active2 = i === activeIdx;
4758
+ const dot = s.id === null ? glyphs().dotAll : glyphs().dot;
3223
4759
  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 })
4760
+ return /* @__PURE__ */ jsxs7(ClickableBox, { onClick: () => onSelect(i), marginRight: 2, children: [
4761
+ /* @__PURE__ */ jsx7(Text7, { dimColor: !active2, children: i }),
4762
+ /* @__PURE__ */ jsx7(Text7, { children: " " }),
4763
+ /* @__PURE__ */ jsx7(Text7, { color: s.color, bold: active2, dimColor: !active2, children: dot }),
4764
+ /* @__PURE__ */ jsx7(Text7, { children: " " }),
4765
+ active2 ? /* @__PURE__ */ jsx7(Text7, { bold: true, color: s.color, children: label }) : /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: label })
3230
4766
  ] }, s.id ?? "__all__");
3231
4767
  }) });
3232
4768
  }
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" })
4769
+ function Footer({ hasAccounts, paginated, cols }) {
4770
+ const inner = cols - 4;
4771
+ const BASE2 = "by David Ilie (davidilie.com) \xB7 s=settings q=quit".length;
4772
+ const JUMP = "0-9=jump a/A=cycle ".length;
4773
+ const PAGE = "scroll=page ".length;
4774
+ const showJump = hasAccounts && inner >= BASE2 + JUMP + (paginated ? PAGE : 0);
4775
+ const showPage = paginated && inner >= BASE2 + (showJump ? JUMP : 0) + PAGE;
4776
+ return /* @__PURE__ */ jsxs7(Box7, { marginTop: 1, flexWrap: "nowrap", children: [
4777
+ /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: "by " }),
4778
+ /* @__PURE__ */ jsx7(Text7, { children: "David Ilie" }),
4779
+ /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: " (" }),
4780
+ /* @__PURE__ */ jsx7(Text7, { color: "cyan", children: "davidilie.com" }),
4781
+ /* @__PURE__ */ jsxs7(Text7, { dimColor: true, children: [
4782
+ ") ",
4783
+ glyphs().middot,
4784
+ " s=settings "
4785
+ ] }),
4786
+ showJump && /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: "0-9=jump a/A=cycle " }),
4787
+ showPage && /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: "scroll=page " }),
4788
+ /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: "q=quit" })
4789
+ ] });
4790
+ }
4791
+ function TinyFallback({ groups, stats, rows, cols }) {
4792
+ const maxLines = Math.max(1, rows - 4);
4793
+ const visible = groups.slice(0, maxLines);
4794
+ const hidden = groups.length - visible.length;
4795
+ const w = Math.max(8, cols - 2);
4796
+ return /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", paddingX: 1, height: rows, overflow: "hidden", children: [
4797
+ /* @__PURE__ */ jsxs7(Text7, { bold: true, color: "greenBright", children: [
4798
+ glyphs().dotSel,
4799
+ " tokmon"
4800
+ ] }),
4801
+ groups.length === 0 ? /* @__PURE__ */ jsxs7(Text7, { dimColor: true, children: [
4802
+ "No providers ",
4803
+ glyphs().emDash,
4804
+ " s=settings"
4805
+ ] }) : visible.map((g) => /* @__PURE__ */ jsx7(TinyRow, { provider: g.provider, accounts: g.accounts, stats, width: w }, g.provider)),
4806
+ hidden > 0 && /* @__PURE__ */ jsxs7(Text7, { dimColor: true, children: [
4807
+ "+",
4808
+ hidden,
4809
+ " more (enlarge terminal)"
4810
+ ] }),
4811
+ /* @__PURE__ */ jsx7(Box7, { flexGrow: 1 }),
4812
+ /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: "s=settings q=quit" })
4813
+ ] });
4814
+ }
4815
+ function TinyRow({ provider, accounts, stats, width }) {
4816
+ const meta = PROVIDERS[provider];
4817
+ const dashboards = accounts.map((a) => stats.get(a.id)?.dashboard).filter(Boolean);
4818
+ const billings = accounts.map((a) => stats.get(a.id)?.billing).filter(Boolean);
4819
+ const todayCost = dashboards.reduce((sum, d) => sum + (d?.today.cost ?? 0), 0);
4820
+ const pctMetric = billings.flatMap((b) => b?.metrics ?? []).find((m) => m.format.kind === "percent");
4821
+ const detail = meta.hasUsage ? `${currency(todayCost)} today` : pctMetric ? `${Math.round(pctMetric.used)}%` : "billing";
4822
+ const name = truncateName(meta.name, Math.max(4, width - 18));
4823
+ return /* @__PURE__ */ jsxs7(Box7, { width, children: [
4824
+ /* @__PURE__ */ jsxs7(Text7, { color: meta.color, children: [
4825
+ glyphs().dot,
4826
+ " "
4827
+ ] }),
4828
+ /* @__PURE__ */ jsx7(Text7, { bold: true, color: meta.color, children: name }),
4829
+ /* @__PURE__ */ jsx7(Box7, { flexGrow: 1 }),
4830
+ /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: detail })
3242
4831
  ] });
3243
4832
  }
3244
4833
 
3245
4834
  // src/cli.tsx
3246
- import { jsx as jsx7 } from "react/jsx-runtime";
4835
+ import { jsx as jsx8 } from "react/jsx-runtime";
3247
4836
  process.on("unhandledRejection", () => {
3248
4837
  });
4838
+ EventEmitter.defaultMaxListeners = 100;
4839
+ var emitWarning = process.emitWarning.bind(process);
4840
+ process.emitWarning = ((warning, ...rest) => {
4841
+ const msg = typeof warning === "string" ? warning : warning?.message;
4842
+ if (typeof msg === "string" && /SQLite is an experimental feature/i.test(msg)) return;
4843
+ return emitWarning(warning, ...rest);
4844
+ });
3249
4845
  var args = process.argv.slice(2);
3250
4846
  var interval;
4847
+ var asciiFlag = null;
3251
4848
  for (let i = 0; i < args.length; i++) {
3252
4849
  if ((args[i] === "--interval" || args[i] === "-i") && args[i + 1]) {
3253
4850
  interval = Math.max(500, Number(args[i + 1]) * 1e3);
3254
4851
  i++;
3255
4852
  }
4853
+ if (args[i] === "--ascii") asciiFlag = "on";
4854
+ if (args[i] === "--no-ascii") asciiFlag = "off";
3256
4855
  if (args[i] === "--help" || args[i] === "-h") {
3257
- console.log("tokmon - Terminal dashboard for Claude, Codex, and Cursor usage\n");
4856
+ console.log("tokmon - Terminal usage dashboard for your AI coding tools\n");
4857
+ console.log(" Claude \xB7 Codex \xB7 Cursor \xB7 Copilot \xB7 opencode \xB7 pi \xB7 Antigravity \xB7 Gemini\n");
3258
4858
  console.log("Usage: tokmon [options]\n");
3259
4859
  console.log("Options:");
3260
4860
  console.log(" -i, --interval <seconds> Refresh interval (default: from config or 2)");
4861
+ console.log(" --ascii Force ASCII glyphs (also: TOKMON_ASCII=1)");
4862
+ console.log(" --no-ascii Force Unicode glyphs");
3261
4863
  console.log(" -h, --help Show this help\n");
3262
4864
  console.log("Keybindings:");
3263
4865
  console.log(" Tab Switch Dashboard / Table");
@@ -3274,6 +4876,13 @@ var config = await loadConfig();
3274
4876
  if (config.clearScreen && process.stdout.isTTY) {
3275
4877
  process.stdout.write("\x1B[2J\x1B[H");
3276
4878
  }
3277
- var { waitUntilExit } = render(/* @__PURE__ */ jsx7(MouseProvider, { children: /* @__PURE__ */ jsx7(App, { interval }) }));
4879
+ setGlyphs(resolveGlyphs({
4880
+ flag: asciiFlag,
4881
+ env: process.env,
4882
+ config: config.ascii,
4883
+ isTTY: !!process.stdout.isTTY,
4884
+ platform: process.platform
4885
+ }));
4886
+ var { waitUntilExit } = render(/* @__PURE__ */ jsx8(MouseProvider, { children: /* @__PURE__ */ jsx8(App, { interval }) }));
3278
4887
  await waitUntilExit();
3279
4888
  await flushDisk();