@timo972/cc-router 0.11.0 → 0.12.0-rc.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +33 -4
  3. package/dist/cli/cmd-accounts.js +91 -13
  4. package/dist/cli/cmd-cli-targets.js +177 -0
  5. package/dist/cli/cmd-client.js +3 -2
  6. package/dist/cli/cmd-configure.js +18 -4
  7. package/dist/cli/cmd-status.js +4 -1
  8. package/dist/cli/cmd-stop.js +18 -0
  9. package/dist/cli/index.js +10 -3
  10. package/dist/config/manager.js +19 -3
  11. package/dist/config/paths.js +1 -0
  12. package/dist/protocol/anthropic-to-openai.js +31 -19
  13. package/dist/protocol/model-ref.js +1 -1
  14. package/dist/protocol/openai-function-call.js +16 -0
  15. package/dist/protocol/openai-response-to-anthropic.js +34 -6
  16. package/dist/protocol/openai-stream-to-anthropic.js +116 -16
  17. package/dist/protocol/openai-to-anthropic.js +54 -29
  18. package/dist/providers/openai/account-state.js +4 -1
  19. package/dist/providers/openai/usage.js +14 -1
  20. package/dist/providers/xai/account-record.js +42 -0
  21. package/dist/providers/xai/device-oauth.js +116 -0
  22. package/dist/providers/xai/import-auth.js +70 -0
  23. package/dist/providers/xai/overview.js +279 -0
  24. package/dist/providers/xai/subscription-fetch.js +56 -0
  25. package/dist/proxy/allowance.js +142 -0
  26. package/dist/proxy/logger.js +4 -3
  27. package/dist/proxy/messages-cross-route.js +113 -8
  28. package/dist/proxy/models-server.js +30 -2
  29. package/dist/proxy/openai-ingress.js +7 -1
  30. package/dist/proxy/openai-routing.js +12 -7
  31. package/dist/proxy/server.js +110 -6
  32. package/dist/ui/Dashboard.js +383 -124
  33. package/dist/ui/accountsApi.js +3 -1
  34. package/dist/utils/cli-routing.js +80 -0
  35. package/dist/utils/codex-config.js +337 -15
  36. package/package.json +3 -2
@@ -4,7 +4,11 @@ import { Box, Text, useInput, useApp, useStdout, measureElement } from "ink";
4
4
  import { createAccountsApi } from "./accountsApi.js";
5
5
  import { createModelsApi } from "./modelsApi.js";
6
6
  import { getCurrentVersion } from "../utils/self-update.js";
7
+ import { readClaudeRouting, readCodexRouting, setClaudeRouting, setCodexRouting, } from "../utils/cli-routing.js";
8
+ import { mergeGrokIntoHealth, loadGrokHealthSnapshotsWithSubscription } from "../providers/xai/overview.js";
7
9
  const POLL_INTERVAL_MS = 2_000;
10
+ /** Grok's plan/code-access rarely change; refresh far slower than the 2s poll. */
11
+ const GROK_SUBSCRIPTION_INTERVAL_MS = 60_000;
8
12
  /** Most activity rows the dashboard will show — the list shrinks below this
9
13
  * (down to MIN_LOG_VISIBLE) when the terminal is too short for the full
10
14
  * frame. Ink can only erase as many lines as the viewport holds, so a frame
@@ -223,6 +227,123 @@ export function isCodexLimited(codex) {
223
227
  return false;
224
228
  return (defaultBucket.primary?.utilization ?? 0) >= 1 || (defaultBucket.secondary?.utilization ?? 0) >= 1;
225
229
  }
230
+ export function isOpenAIAccount(account) {
231
+ return account.provider === "openai_subscription";
232
+ }
233
+ export function isXaiAccount(account) {
234
+ return account.provider === "xai_subscription";
235
+ }
236
+ export function isClaudeAccount(account) {
237
+ return !isOpenAIAccount(account) && !isXaiAccount(account);
238
+ }
239
+ /** Compact `rst` column: banked Codex usage-limit resets. Claude is an em dash. */
240
+ export function resetCreditsColumnLabel(account) {
241
+ if (!isOpenAIAccount(account))
242
+ return "—";
243
+ return String(account.codexRateLimits?.resetCredits?.available ?? 0);
244
+ }
245
+ export function isLimitedAccount(account) {
246
+ if (isXaiAccount(account))
247
+ return false;
248
+ if (isOpenAIAccount(account))
249
+ return isCodexLimited(account.codexRateLimits);
250
+ if (account.rateLimits?.status === "rate_limited")
251
+ return true;
252
+ if (!account.rateLimits)
253
+ return false;
254
+ const view = getGlobalCapacityView(account.rateLimits);
255
+ return view.fiveHour.utilization >= 1 || view.sevenDay.utilization >= 1;
256
+ }
257
+ /** Claude, then ChatGPT, then Grok; within each group, accounts at limit last. */
258
+ export function orderAccountsForDashboard(accounts) {
259
+ const usableThenLimited = (list) => [
260
+ ...list.filter(account => !isLimitedAccount(account)),
261
+ ...list.filter(account => isLimitedAccount(account)),
262
+ ];
263
+ return [
264
+ ...usableThenLimited(accounts.filter(isClaudeAccount)),
265
+ ...usableThenLimited(accounts.filter(isOpenAIAccount)),
266
+ ...usableThenLimited(accounts.filter(isXaiAccount)),
267
+ ];
268
+ }
269
+ /** Compact view hides healthy/inactive model rows; selection shows the full set. */
270
+ export function visibleCapacityRows(rows, selected) {
271
+ if (selected)
272
+ return rows;
273
+ return rows.filter(row => row.color === "red" || row.color === "yellow");
274
+ }
275
+ export function noteModelLimit(account) {
276
+ const limits = account.rateLimits?.usage?.modelLimits ?? [];
277
+ if (limits.length === 0)
278
+ return undefined;
279
+ const fable = limits.find(limit => limit.modelFamily === "fable" || /fable/i.test(limit.displayName));
280
+ const limit = fable ?? limits.find(entry => entry.active) ?? limits[0];
281
+ if (!limit)
282
+ return undefined;
283
+ const utilization = limit.utilization;
284
+ const color = utilization >= 1 || limit.severity === "critical"
285
+ ? "red"
286
+ : utilization >= 0.7 || limit.severity === "warning"
287
+ ? "yellow"
288
+ : "green";
289
+ return {
290
+ label: limit.displayName.replace(/^Claude\s+/i, ""),
291
+ utilization,
292
+ color,
293
+ };
294
+ }
295
+ /**
296
+ * Grok has no usage windows, so the note carries the plan instead. Prefer the
297
+ * live plan name from `/v1/user` ("GrokPro"); fall back to the coarse
298
+ * access-token `tier` when the live lookup has not landed (offline / first
299
+ * render), and to "cli" when neither is known.
300
+ */
301
+ export function grokQuotaNote(account) {
302
+ if (!isXaiAccount(account))
303
+ return undefined;
304
+ if (account.healthy === false)
305
+ return "expired";
306
+ const plan = account.xai?.subscriptionTier?.trim();
307
+ if (plan)
308
+ return plan;
309
+ const tier = account.xai?.tier;
310
+ return typeof tier === "number" && Number.isFinite(tier) ? `tier ${tier}` : "cli";
311
+ }
312
+ export function openaiQuotaGapNote(account) {
313
+ if (!isOpenAIAccount(account))
314
+ return undefined;
315
+ if (getCodexDefaultWindows(account.codexRateLimits).length > 0)
316
+ return undefined;
317
+ const plan = account.codexRateLimits?.plan?.trim();
318
+ return plan ? `${plan} · no quota` : "no quota";
319
+ }
320
+ export function isWeeklyLimited(account) {
321
+ if (isOpenAIAccount(account)) {
322
+ return getCodexDefaultWindows(account.codexRateLimits).some(window => window.kind === "weekly" && window.utilization >= 1);
323
+ }
324
+ if (!account.rateLimits)
325
+ return false;
326
+ return getGlobalCapacityView(account.rateLimits).sevenDay.utilization >= 1;
327
+ }
328
+ export function earliestWeeklyReset(accounts) {
329
+ let earliest;
330
+ for (const account of accounts) {
331
+ if (isOpenAIAccount(account)) {
332
+ for (const window of getCodexDefaultWindows(account.codexRateLimits)) {
333
+ if (window.kind !== "weekly" || window.resetAt <= 0)
334
+ continue;
335
+ if (earliest === undefined || window.resetAt < earliest)
336
+ earliest = window.resetAt;
337
+ }
338
+ }
339
+ else if (account.rateLimits) {
340
+ const resetAt = getGlobalCapacityView(account.rateLimits).sevenDay.resetAt;
341
+ if (resetAt > 0 && (earliest === undefined || resetAt < earliest))
342
+ earliest = resetAt;
343
+ }
344
+ }
345
+ return earliest;
346
+ }
226
347
  /**
227
348
  * First visible row of a scrolling list window that follows its selection.
228
349
  *
@@ -417,6 +538,27 @@ export function Dashboard({ port, baseUrl, authToken, onIntent }) {
417
538
  if (!data && (input === "q" || key.escape))
418
539
  exit();
419
540
  });
541
+ // Live Grok plan name / code-access, refreshed on a slow cadence and merged
542
+ // synchronously into each 2s health poll — so the plan shows without a
543
+ // /v1/user round-trip every tick. Undefined until the first fetch lands, at
544
+ // which point `mergeGrokIntoHealth` falls back to its access-token tier.
545
+ const grokSnapshotsRef = useRef(undefined);
546
+ useEffect(() => {
547
+ let cancelled = false;
548
+ const refresh = async () => {
549
+ try {
550
+ const snapshots = await loadGrokHealthSnapshotsWithSubscription();
551
+ if (!cancelled)
552
+ grokSnapshotsRef.current = snapshots;
553
+ }
554
+ catch {
555
+ // Keep the last known snapshots; the poll degrades to the tier fallback.
556
+ }
557
+ };
558
+ refresh();
559
+ const timer = setInterval(refresh, GROK_SUBSCRIPTION_INTERVAL_MS);
560
+ return () => { cancelled = true; clearInterval(timer); };
561
+ }, []);
420
562
  useEffect(() => {
421
563
  let cancelled = false;
422
564
  const healthUrl = `${resolvedBase}/cc-router/health`;
@@ -432,7 +574,7 @@ export function Dashboard({ port, baseUrl, authToken, onIntent }) {
432
574
  if (cancelled)
433
575
  return;
434
576
  if (res.ok) {
435
- setData(await res.json());
577
+ setData(mergeGrokIntoHealth(await res.json(), grokSnapshotsRef.current));
436
578
  setConnectError(null);
437
579
  setLastUpdate(Date.now());
438
580
  setRetryCount(0);
@@ -466,13 +608,36 @@ function ErrorScreen({ error, port, retries }) {
466
608
  }
467
609
  // ─── Live dashboard ───────────────────────────────────────────────────────────
468
610
  function LiveDashboard({ data, port, baseUrl, lastUpdate, api, modelsApi, onIntent, }) {
611
+ const [cliRouting, setCliRouting] = useState(() => ({
612
+ claude: readClaudeRouting(),
613
+ codex: readCodexRouting(),
614
+ }));
615
+ const refreshCliRouting = useCallback(() => {
616
+ setCliRouting({
617
+ claude: readClaudeRouting(),
618
+ codex: readCodexRouting(),
619
+ });
620
+ }, []);
621
+ useEffect(() => {
622
+ const timer = setInterval(refreshCliRouting, POLL_INTERVAL_MS);
623
+ return () => clearInterval(timer);
624
+ }, [refreshCliRouting]);
469
625
  const { exit } = useApp();
470
- const healthyCount = data.accounts.filter(a => a.healthy).length;
471
- const updatedAgo = Math.round((Date.now() - lastUpdate) / 1000);
626
+ const orderedAccounts = orderAccountsForDashboard(data.accounts);
627
+ const healthyCount = orderedAccounts.filter(a => a.healthy).length;
628
+ // Fleet-wide weekly-cap rollup surfaced in the always-visible ACCOUNTS title,
629
+ // so the "N 7d full" signal is legible at a glance even when a provider group
630
+ // is scrolled out of the account window (or TOTALS is hidden in compact view).
631
+ const weeklyFullCount = orderedAccounts.filter(isWeeklyLimited).length;
472
632
  const logs = data.recentLogs;
473
633
  // ── Focus / mode ──────────────────────────────────────────────────────────
474
634
  const [focus, setFocus] = useState("logs");
475
635
  const [mode, setMode] = useState("view");
636
+ // Compact ("zen") view: hides TOTALS + RECENT ACTIVITY so the account list
637
+ // gets the whole vertical budget — the fix for a short terminal starving a
638
+ // long fleet (e.g. showing 1 of 11 accounts). Toggled with [z], view-only,
639
+ // never touches the default layout's carefully-tuned fit.
640
+ const [compact, setCompact] = useState(false);
476
641
  // Selected log by timestamp (existing)
477
642
  const [selectedTs, setSelectedTs] = useState(null);
478
643
  const selectedLogIndex = selectedTs !== null
@@ -497,7 +662,8 @@ function LiveDashboard({ data, port, baseUrl, lastUpdate, api, modelsApi, onInte
497
662
  const [logVisible, setLogVisible] = useState(MIN_LOG_VISIBLE);
498
663
  const [accountsVisible, setAccountsVisible] = useState(Number.MAX_SAFE_INTEGER);
499
664
  const [modelsVisible, setModelsVisible] = useState(MODEL_VISIBLE_ROWS);
500
- const shownAccounts = Math.max(1, Math.min(accountsVisible, data.accounts.length));
665
+ const [selectedAccountId, setSelectedAccountId] = useState(null);
666
+ const shownAccounts = Math.max(1, Math.min(accountsVisible, orderedAccounts.length || 1));
501
667
  const contentRef = useRef(null);
502
668
  const accountRowsRef = useRef(null);
503
669
  const logRowsRef = useRef(null);
@@ -523,7 +689,7 @@ function LiveDashboard({ data, port, baseUrl, lastUpdate, api, modelsApi, onInte
523
689
  // denial, re-enabling the grow/shrink oscillation. Geometry drift from
524
690
  // scrolling (like every other content mutation the key cannot see) is
525
691
  // covered by the denial TTL instead.
526
- const fitKey = `${terminalRows}:${terminalColumns}:${data.accounts.length}:${modelsStatus?.models.length ?? 0}:${logs[0]?.ts ?? 0}`;
692
+ const fitKey = `${terminalRows}:${terminalColumns}:${orderedAccounts.length}:${modelsStatus?.models.length ?? 0}:${logs[0]?.ts ?? 0}:${compact}`;
527
693
  if (fitKeyRef.current !== fitKey) {
528
694
  fitKeyRef.current = fitKey;
529
695
  fitMemoryRef.current = { attempts: {}, denials: {} };
@@ -550,12 +716,14 @@ function LiveDashboard({ data, port, baseUrl, lastUpdate, api, modelsApi, onInte
550
716
  // mechanics — each list got its own oscillation bug while the paths were
551
717
  // separate (tall accounts, wrapped activity details, wrapped model ids).
552
718
  const lists = [
719
+ // In compact view the activity list is not rendered, so it must not
720
+ // compete for rows — dropping it here hands the whole budget to accounts.
721
+ ...(compact ? [] : [{
722
+ key: "logs", current: logVisible, min: MIN_LOG_VISIBLE, max: LOG_VISIBLE,
723
+ avgRow: shownLogs > 0 ? Math.max(1, logsH / shownLogs) : 1,
724
+ }]),
553
725
  {
554
- key: "logs", current: logVisible, min: MIN_LOG_VISIBLE, max: LOG_VISIBLE,
555
- avgRow: shownLogs > 0 ? Math.max(1, logsH / shownLogs) : 1,
556
- },
557
- {
558
- key: "accounts", current: shownAccounts, min: 1, max: data.accounts.length, growOne: true,
726
+ key: "accounts", current: shownAccounts, min: 1, max: Math.max(1, orderedAccounts.length), growOne: true,
559
727
  avgRow: shownAccounts > 0 ? Math.max(1, accountsH / shownAccounts) : 2,
560
728
  },
561
729
  ...(modelsPanelOpen ? [{
@@ -577,18 +745,16 @@ function LiveDashboard({ data, port, baseUrl, lastUpdate, api, modelsApi, onInte
577
745
  // selected row out of the stored window, and it must stay visible anyway.
578
746
  const [logScrollTop, setLogScrollTop] = useState(0);
579
747
  const logWindowTop = followScrollWindow(logScrollTop, selectedLogIndex, logs.length, logVisible);
580
- // Selected account by id
581
- const [selectedAccountId, setSelectedAccountId] = useState(null);
582
748
  const selectedAccountIndex = selectedAccountId !== null
583
- ? Math.max(0, data.accounts.findIndex(a => a.id === selectedAccountId))
749
+ ? Math.max(0, orderedAccounts.findIndex(a => a.id === selectedAccountId))
584
750
  : 0;
585
- const selectedAccount = data.accounts[selectedAccountIndex] ?? null;
751
+ const selectedAccount = orderedAccounts[selectedAccountIndex] ?? null;
586
752
  // Same follow-scroll for the accounts window: when the fitting controller
587
753
  // shrinks the list below the fleet size, the selected account must stay on
588
754
  // screen — account actions (caps, toggle, delete confirmation) target the
589
755
  // selection, and acting on an invisible account is how a wrong one dies.
590
756
  const [accountScrollTop, setAccountScrollTop] = useState(0);
591
- const accountWindowTop = followScrollWindow(accountScrollTop, selectedAccountIndex, data.accounts.length, shownAccounts);
757
+ const accountWindowTop = followScrollWindow(accountScrollTop, selectedAccountIndex, orderedAccounts.length, shownAccounts);
592
758
  const [modelsStatus, setModelsStatus] = useState(null);
593
759
  const [selectedModelId, setSelectedModelId] = useState(null);
594
760
  const modelRows = modelsStatus?.models ?? [];
@@ -635,6 +801,10 @@ function LiveDashboard({ data, port, baseUrl, lastUpdate, api, modelsApi, onInte
635
801
  const doToggleEnabled = useCallback(async () => {
636
802
  if (!selectedAccount)
637
803
  return;
804
+ if (isXaiAccount(selectedAccount)) {
805
+ showBanner("Grok is read-only here — use grok login / grok logout", "yellow");
806
+ return;
807
+ }
638
808
  const newValue = !(selectedAccount.enabled !== false);
639
809
  try {
640
810
  await api.patch(selectedAccount.id, { enabled: newValue });
@@ -647,8 +817,12 @@ function LiveDashboard({ data, port, baseUrl, lastUpdate, api, modelsApi, onInte
647
817
  const doToggleProvider = useCallback(async (provider) => {
648
818
  const providerStatus = provider === "anthropic_subscription"
649
819
  ? data.operational?.providers.anthropic
650
- : data.operational?.providers.openai;
651
- const label = provider === "anthropic_subscription" ? "Claude" : "OpenAI";
820
+ : provider === "openai_subscription"
821
+ ? data.operational?.providers.openai
822
+ : data.operational?.providers.xai;
823
+ const label = provider === "anthropic_subscription" ? "Claude"
824
+ : provider === "openai_subscription" ? "OpenAI"
825
+ : "Grok";
652
826
  if (!providerStatus?.configured || providerStatus.accounts === 0) {
653
827
  showBanner(`${label} accounts are not configured`, "yellow");
654
828
  return;
@@ -698,6 +872,19 @@ function LiveDashboard({ data, port, baseUrl, lastUpdate, api, modelsApi, onInte
698
872
  showBanner(`Models error: ${errMsg(err)}`, "red");
699
873
  }
700
874
  }, [modelsApi, showBanner]);
875
+ const doToggleCli = useCallback((target) => {
876
+ const current = target === "claude" ? cliRouting.claude.enabled : cliRouting.codex.enabled;
877
+ const result = target === "claude"
878
+ ? setClaudeRouting(!current)
879
+ : setCodexRouting(!current);
880
+ refreshCliRouting();
881
+ const label = target === "claude" ? "Claude CLI" : "Codex CLI";
882
+ if (!result.changed) {
883
+ showBanner(`${label} already ${result.enabled ? "on" : "off"}`, "gray");
884
+ return;
885
+ }
886
+ showBanner(`${label} → ${result.enabled ? "proxy" : "native"} (restart the CLI to pick up)`, result.enabled ? "green" : "yellow");
887
+ }, [cliRouting, refreshCliRouting, showBanner]);
701
888
  const doSetSelectedModel = useCallback(async (provider) => {
702
889
  if (!selectedModel)
703
890
  return;
@@ -784,6 +971,11 @@ function LiveDashboard({ data, port, baseUrl, lastUpdate, api, modelsApi, onInte
784
971
  return;
785
972
  }
786
973
  if (key.tab) {
974
+ // Compact view hides the activity list, so skip "logs" in the cycle.
975
+ if (compact) {
976
+ setFocus(f => f === "models" ? "accounts" : "models");
977
+ return;
978
+ }
787
979
  setFocus(f => f === "logs" ? "accounts" : f === "accounts" ? "models" : "logs");
788
980
  return;
789
981
  }
@@ -803,13 +995,13 @@ function LiveDashboard({ data, port, baseUrl, lastUpdate, api, modelsApi, onInte
803
995
  if (focus === "accounts") {
804
996
  if (key.upArrow) {
805
997
  const next = Math.max(0, selectedAccountIndex - 1);
806
- setSelectedAccountId(data.accounts[next]?.id ?? null);
807
- setAccountScrollTop(followScrollWindow(accountWindowTop, next, data.accounts.length, shownAccounts));
998
+ setSelectedAccountId(orderedAccounts[next]?.id ?? null);
999
+ setAccountScrollTop(followScrollWindow(accountWindowTop, next, orderedAccounts.length, shownAccounts));
808
1000
  }
809
1001
  if (key.downArrow) {
810
- const next = Math.min(data.accounts.length - 1, selectedAccountIndex + 1);
811
- setSelectedAccountId(data.accounts[next]?.id ?? null);
812
- setAccountScrollTop(followScrollWindow(accountWindowTop, next, data.accounts.length, shownAccounts));
1002
+ const next = Math.min(orderedAccounts.length - 1, selectedAccountIndex + 1);
1003
+ setSelectedAccountId(orderedAccounts[next]?.id ?? null);
1004
+ setAccountScrollTop(followScrollWindow(accountWindowTop, next, orderedAccounts.length, shownAccounts));
813
1005
  }
814
1006
  // Account actions (only when focus = accounts)
815
1007
  if (input === "e") {
@@ -824,12 +1016,24 @@ function LiveDashboard({ data, port, baseUrl, lastUpdate, api, modelsApi, onInte
824
1016
  void doToggleProvider("openai_subscription");
825
1017
  return;
826
1018
  }
1019
+ if (input === "g") {
1020
+ void doToggleProvider("xai_subscription");
1021
+ return;
1022
+ }
827
1023
  if (input === "w") {
1024
+ if (selectedAccount && isXaiAccount(selectedAccount)) {
1025
+ showBanner("Grok has no 7d cap in cc-router", "yellow");
1026
+ return;
1027
+ }
828
1028
  setMode("editWeekly");
829
1029
  setEditBuffer("");
830
1030
  return;
831
1031
  }
832
1032
  if (input === "s") {
1033
+ if (selectedAccount && isXaiAccount(selectedAccount)) {
1034
+ showBanner("Grok has no 5h cap in cc-router", "yellow");
1035
+ return;
1036
+ }
833
1037
  setMode("editSession");
834
1038
  setEditBuffer("");
835
1039
  return;
@@ -840,6 +1044,10 @@ function LiveDashboard({ data, port, baseUrl, lastUpdate, api, modelsApi, onInte
840
1044
  // the CLI for something the dashboard can do was left over from before
841
1045
  // that existed.
842
1046
  if (input === "d") {
1047
+ if (selectedAccount && isXaiAccount(selectedAccount)) {
1048
+ showBanner("Grok accounts live in ~/.grok — run grok logout", "yellow");
1049
+ return;
1050
+ }
843
1051
  setMode("confirmDelete");
844
1052
  return;
845
1053
  }
@@ -866,10 +1074,30 @@ function LiveDashboard({ data, port, baseUrl, lastUpdate, api, modelsApi, onInte
866
1074
  return;
867
1075
  }
868
1076
  }
1077
+ // CLI routing toggles — [c] is model-default only while MODELS is focused.
1078
+ if (input === "c" && focus !== "models") {
1079
+ doToggleCli("claude");
1080
+ return;
1081
+ }
1082
+ if (input === "x") {
1083
+ doToggleCli("codex");
1084
+ return;
1085
+ }
869
1086
  if (input === "m") {
870
1087
  void doLoadModels();
871
1088
  return;
872
1089
  }
1090
+ // z = compact view — hide TOTALS + RECENT ACTIVITY so the account list
1091
+ // gets the freed rows. On small terminals this is the difference between
1092
+ // "showing 1–1" and the full fleet. Move focus off the (now hidden) logs.
1093
+ if (input === "z") {
1094
+ const next = !compact;
1095
+ setCompact(next);
1096
+ if (next && focus === "logs")
1097
+ setFocus("accounts");
1098
+ showBanner(next ? "Compact on — activity & totals hidden" : "Compact off", "cyan");
1099
+ return;
1100
+ }
873
1101
  // n = add account — works regardless of focus.
874
1102
  // Requires an onIntent handler because the outer loop runs the OAuth
875
1103
  // flow after Ink unmounts; if none is wired, this key is a no-op.
@@ -883,23 +1111,26 @@ function LiveDashboard({ data, port, baseUrl, lastUpdate, api, modelsApi, onInte
883
1111
  });
884
1112
  const selectedLog = logs[selectedLogIndex] ?? null;
885
1113
  const visibleLogs = logs.slice(logWindowTop, logWindowTop + logVisible);
886
- return (_jsx(Box, { flexDirection: "column", height: frameBound, overflowY: "hidden", children: _jsxs(Box, { flexDirection: "column", flexShrink: 0, ref: contentRef, children: [_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { children: [_jsx(Text, { bold: true, color: "cyan", children: " CC-Router " }), _jsx(Text, { color: "gray", children: "\u00B7 " }), _jsx(Text, { color: "green", children: data.mode }), _jsxs(Text, { color: "gray", children: [" \u2192 ", data.target, " \u00B7 "] }), _jsxs(Text, { children: ["up ", formatUptime(data.uptime)] }), _jsxs(Text, { color: "gray", children: [" \u00B7 updated ", updatedAgo, "s ago \u00B7 [q] quit"] })] }), mode === "editWeekly" && selectedAccount && (_jsxs(Box, { paddingLeft: 2, children: [_jsx(Text, { color: "cyan", children: "Set 7d cap for " }), _jsx(Text, { color: "white", bold: true, children: selectedAccount.id }), _jsx(Text, { color: "cyan", children: " (0\u2013100%): " }), _jsx(Text, { color: "white", bold: true, children: editBuffer }), _jsx(Text, { color: "gray", children: "\u2588 [Enter] save [Esc] cancel" })] })), mode === "editSession" && selectedAccount && (_jsxs(Box, { paddingLeft: 2, children: [_jsx(Text, { color: "cyan", children: "Set 5h cap for " }), _jsx(Text, { color: "white", bold: true, children: selectedAccount.id }), _jsx(Text, { color: "cyan", children: " (0\u2013100%): " }), _jsx(Text, { color: "white", bold: true, children: editBuffer }), _jsx(Text, { color: "gray", children: "\u2588 [Enter] save [Esc] cancel" })] })), mode === "confirmDelete" && selectedAccount && (_jsx(Box, { paddingLeft: 2, children: _jsxs(Text, { color: "red", bold: true, children: ["Delete \"", selectedAccount.id, "\"? [y] yes [n/Esc] cancel"] }) })), data.version !== DASHBOARD_VERSION && (_jsxs(Box, { children: [_jsx(Text, { bold: true, color: "yellow", children: " \u26A0 VERSION MISMATCH " }), _jsxs(Text, { color: "yellow", children: [data.version !== undefined
1114
+ return (_jsx(Box, { flexDirection: "column", height: frameBound, overflowY: "hidden", children: _jsxs(Box, { flexDirection: "column", flexShrink: 0, ref: contentRef, children: [_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { children: [_jsx(Text, { bold: true, color: "cyan", children: " CC-Router " }), _jsx(Text, { color: "gray", children: "\u00B7 " }), _jsx(Text, { color: "green", children: data.mode }), _jsx(Text, { color: "gray", children: " \u00B7 " }), _jsxs(Text, { children: ["up ", formatUptime(data.uptime)] }), _jsx(Text, { color: "gray", children: " \u00B7 " }), _jsx(Text, { color: "cyan", children: data.totalRequests }), _jsx(Text, { color: "gray", children: " req " }), _jsx(Text, { color: data.totalErrors > 0 ? "red" : "green", children: data.totalErrors }), _jsx(Text, { color: "gray", children: " err" }), _jsx(CacheHealthBadge, { read: data.totalCacheReadTokens, created: data.totalCacheCreationTokens, input: data.totalInputTokens }), _jsx(Text, { color: "gray", children: " \u00B7 [q] quit" })] }), mode === "editWeekly" && selectedAccount && (_jsxs(Box, { paddingLeft: 2, children: [_jsx(Text, { color: "cyan", children: "Set 7d cap for " }), _jsx(Text, { color: "white", bold: true, children: selectedAccount.id }), _jsx(Text, { color: "cyan", children: " (0\u2013100%): " }), _jsx(Text, { color: "white", bold: true, children: editBuffer }), _jsx(Text, { color: "gray", children: "\u2588 [Enter] save [Esc] cancel" })] })), mode === "editSession" && selectedAccount && (_jsxs(Box, { paddingLeft: 2, children: [_jsx(Text, { color: "cyan", children: "Set 5h cap for " }), _jsx(Text, { color: "white", bold: true, children: selectedAccount.id }), _jsx(Text, { color: "cyan", children: " (0\u2013100%): " }), _jsx(Text, { color: "white", bold: true, children: editBuffer }), _jsx(Text, { color: "gray", children: "\u2588 [Enter] save [Esc] cancel" })] })), mode === "confirmDelete" && selectedAccount && (_jsx(Box, { paddingLeft: 2, children: _jsxs(Text, { color: "red", bold: true, children: ["Delete \"", selectedAccount.id, "\"? [y] yes [n/Esc] cancel"] }) })), data.version !== DASHBOARD_VERSION && (_jsxs(Box, { children: [_jsx(Text, { bold: true, color: "yellow", children: " \u26A0 VERSION MISMATCH " }), _jsxs(Text, { color: "yellow", children: [data.version !== undefined
887
1115
  ? `daemon v${data.version}`
888
1116
  : "daemon version unreported (older build)", ` · dashboard v${DASHBOARD_VERSION}`] }), LOCAL_TARGET_RE.test(baseUrl) ? (_jsxs(_Fragment, { children: [_jsx(Text, { color: "gray", children: " \u2014 restart: " }), _jsx(Text, { color: "cyan", children: "cc-router stop --keep-config && cc-router start" })] })) : (
889
1117
  // A remote router can only be restarted where it runs; printing a
890
1118
  // local restart command here would never clear the banner.
891
- _jsxs(Text, { color: "gray", children: [" \u2014 update and restart the daemon on ", baseUrl] }))] })), _jsx(Box, { marginTop: 1 }), data.operational && (_jsxs(_Fragment, { children: [_jsx(OperationsPanel, { operational: data.operational, baseUrl: baseUrl, focus: focus }), _jsx(Box, { marginTop: 1 })] })), (focus === "models" || modelsStatus) && (_jsxs(_Fragment, { children: [_jsx(ModelsPanel, { status: modelsStatus, selectedIndex: selectedModelIndex, focused: focus === "models", visibleRows: modelsVisible, rowsRef: modelRowsRef }), _jsx(Box, { marginTop: 1 })] })), _jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { children: [_jsxs(Text, { bold: true, children: [" ACCOUNTS ", _jsxs(Text, { color: healthyCount === data.accounts.length ? "green" : "yellow", children: [healthyCount, "/", data.accounts.length, " healthy"] })] }), shownAccounts < data.accounts.length && (_jsxs(Text, { color: "gray", children: [" · showing ", accountWindowTop + 1, "\u2013", accountWindowTop + shownAccounts] })), _jsx(Text, { color: "gray", children: " " }), _jsx(Text, { color: focus === "accounts" ? "white" : "gray", children: "[Tab] focus [e] toggle [a] Claude all [o] OpenAI all [w] 7d cap [s] 5h cap [n] add [d] delete" })] }), _jsx(Box, { marginTop: 1, flexDirection: "column", ref: accountRowsRef, children: data.accounts.slice(accountWindowTop, accountWindowTop + shownAccounts).map((a, i) => (_jsx(AccountRow, { account: a, selected: focus === "accounts" && accountWindowTop + i === selectedAccountIndex }, a.id))) })] }), banner && (_jsx(Box, { marginTop: 1, paddingLeft: 2, children: _jsxs(Text, { color: banner.color, children: [" ", banner.text] }) })), _jsx(Box, { marginTop: 1 }), _jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { children: [_jsx(Text, { bold: true, children: " TOTALS " }), _jsx(Text, { children: "requests " }), _jsx(Text, { color: "cyan", children: data.totalRequests }), _jsx(Text, { color: "gray", children: " \u00B7 " }), _jsx(Text, { children: "errors " }), _jsx(Text, { color: data.totalErrors > 0 ? "red" : "green", children: data.totalErrors }), _jsx(Text, { color: "gray", children: " \u00B7 " }), _jsx(Text, { children: "refreshes " }), _jsx(Text, { color: "yellow", children: data.totalRefreshes }), _jsx(CacheHealthBadge, { read: data.totalCacheReadTokens, created: data.totalCacheCreationTokens, input: data.totalInputTokens })] }), _jsx(TokenSummary, { cacheRead: data.totalCacheReadTokens, cacheCreated: data.totalCacheCreationTokens, uncached: data.totalInputTokens, output: data.totalOutputTokens ?? 0 })] }), _jsx(Box, { marginTop: 1 }), _jsx(Text, { bold: true, children: " RECENT ACTIVITY" }), _jsx(Box, { marginTop: 1 })] }), _jsx(Box, { flexDirection: "column", ref: logRowsRef, children: visibleLogs.length === 0
1119
+ _jsxs(Text, { color: "gray", children: [" \u2014 update and restart the daemon on ", baseUrl] }))] })), _jsx(Box, { marginTop: 1 }), data.operational && (_jsxs(_Fragment, { children: [_jsx(OperationsPanel, { operational: data.operational, baseUrl: baseUrl, focus: focus, cliRouting: cliRouting }), _jsx(Box, { marginTop: 1 })] })), (focus === "models" || modelsStatus) && (_jsxs(_Fragment, { children: [_jsx(ModelsPanel, { status: modelsStatus, selectedIndex: selectedModelIndex, focused: focus === "models", visibleRows: modelsVisible, rowsRef: modelRowsRef }), _jsx(Box, { marginTop: 1 })] })), _jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { children: [_jsxs(Text, { bold: true, children: [" ACCOUNTS ", _jsxs(Text, { color: healthyCount === orderedAccounts.length ? "green" : "yellow", children: [healthyCount, "/", orderedAccounts.length, " healthy"] }), weeklyFullCount > 0 && _jsx(Text, { color: "red", children: ` · ${weeklyFullCount} 7d full` })] }), shownAccounts < orderedAccounts.length && (_jsxs(Text, { color: "gray", children: [" · showing ", accountWindowTop + 1, "\u2013", accountWindowTop + shownAccounts] })), compact && _jsx(Text, { color: "cyan", children: " · compact" })] }), _jsx(Box, { marginTop: 1, flexDirection: "column", ref: accountRowsRef, children: _jsx(AccountGroups, { visible: orderedAccounts.slice(accountWindowTop, accountWindowTop + shownAccounts), fleet: orderedAccounts, windowTop: accountWindowTop, selectedIndex: selectedAccountIndex, focused: focus === "accounts" }) })] }), banner && (_jsx(Box, { marginTop: 1, paddingLeft: 2, children: _jsxs(Text, { color: banner.color, children: [" ", banner.text] }) })), !compact && (_jsxs(_Fragment, { children: [_jsx(Box, { marginTop: 1 }), _jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { children: [_jsx(Text, { bold: true, children: " TOTALS " }), _jsx(Text, { children: "requests " }), _jsx(Text, { color: "cyan", children: data.totalRequests }), _jsx(Text, { color: "gray", children: " \u00B7 " }), _jsx(Text, { children: "errors " }), _jsx(Text, { color: data.totalErrors > 0 ? "red" : "green", children: data.totalErrors }), _jsx(Text, { color: "gray", children: " \u00B7 " }), _jsx(Text, { children: "refreshes " }), _jsx(Text, { color: "yellow", children: data.totalRefreshes }), _jsx(CacheHealthBadge, { read: data.totalCacheReadTokens, created: data.totalCacheCreationTokens, input: data.totalInputTokens })] }), _jsx(TokenSummary, { cacheRead: data.totalCacheReadTokens, cacheCreated: data.totalCacheCreationTokens, uncached: data.totalInputTokens, output: data.totalOutputTokens ?? 0 })] }), _jsx(Box, { marginTop: 1 }), _jsx(Text, { bold: true, children: " RECENT ACTIVITY" }), _jsx(Box, { marginTop: 1 })] }))] }), !compact && (_jsx(Box, { flexDirection: "column", ref: logRowsRef, children: visibleLogs.length === 0
892
1120
  ? _jsx(Text, { color: "gray", children: " No activity yet" })
893
- : visibleLogs.map((log, i) => (_jsx(LogRow, { log: log, selected: focus === "logs" && logWindowTop + i === selectedLogIndex }, `${log.ts}-${i}`))) }), focus === "logs" && selectedLog && (_jsxs(Box, { flexDirection: "column", children: [_jsx(Box, { marginTop: 1 }), _jsx(DetailPanel, { log: selectedLog })] }))] }) }));
1121
+ : visibleLogs.map((log, i) => (_jsx(LogRow, { log: log, selected: focus === "logs" && logWindowTop + i === selectedLogIndex }, `${log.ts}-${i}`))) })), !compact && focus === "logs" && selectedLog && (_jsxs(Box, { flexDirection: "column", children: [_jsx(Box, { marginTop: 1 }), _jsx(DetailPanel, { log: selectedLog })] })), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: "gray", children: focus === "accounts"
1122
+ ? " [Tab] [e] toggle [a]/[o]/[g] provider [n] add [d] delete [w] 7d [s] 5h [z] compact [q]"
1123
+ : focus === "models"
1124
+ ? " [Tab] [m/r] refresh [c]/[o] default [Esc] logs [z] compact [q]"
1125
+ : " [Tab] [m] models [z] compact [q] quit" }) })] }) }));
894
1126
  }
895
- function OperationsPanel({ operational, baseUrl, focus }) {
1127
+ function OperationsPanel({ operational, baseUrl, focus, cliRouting }) {
896
1128
  const authLabel = operational.auth.required ? "protected" : "open";
897
1129
  const authColor = operational.auth.required ? "green" : "yellow";
898
1130
  const claudeReady = operational.capabilities.anthropicMessages;
899
1131
  const openAIReady = operational.capabilities.openAIResponses;
900
1132
  const crossReady = operational.capabilities.crossProviderMessages;
901
- const modelsReady = operational.capabilities.dynamicModels;
902
- return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { children: [_jsx(Text, { bold: true, children: " OPERATIONS " }), _jsx(Text, { color: "gray", children: "base " }), _jsx(Text, { color: "cyan", children: baseUrl }), _jsx(Text, { color: "gray", children: " \u00B7 auth " }), _jsx(Text, { color: authColor, children: authLabel }), _jsx(Text, { color: "gray", children: " \u00B7 models " }), _jsx(Text, { color: modelsReady ? "green" : "red", children: modelsReady ? "dynamic" : "off" })] }), _jsxs(Box, { paddingLeft: 2, children: [_jsx(ProviderBadge, { label: "Claude", status: operational.providers.anthropic, ready: claudeReady }), _jsx(Text, { color: "gray", children: " " }), _jsx(ProviderBadge, { label: "OpenAI", status: operational.providers.openai, ready: openAIReady }), _jsx(Text, { color: "gray", children: " \u00B7 cross-route " }), _jsx(Text, { color: crossReady ? "green" : "gray", children: crossReady ? "ready" : "needs OpenAI" })] }), _jsxs(Box, { paddingLeft: 2, children: [_jsx(Text, { color: "gray", children: "endpoints " }), _jsx(Text, { color: "white", children: operational.endpoints.messages }), _jsx(Text, { color: "gray", children: " " }), _jsx(Text, { color: "white", children: operational.endpoints.responses }), _jsx(Text, { color: "gray", children: " " }), _jsx(Text, { color: "white", children: operational.endpoints.models }), _jsx(Text, { color: "gray", children: " " }), _jsx(Text, { color: "white", children: operational.endpoints.accounts })] }), _jsxs(Box, { paddingLeft: 2, children: [_jsx(Text, { color: "gray", children: "routing " }), _jsxs(Text, { color: "white", children: ["claude=", operational.routing.anthropicDefaultModel ?? "default"] }), _jsxs(Text, { color: "gray", children: [" aliases[", operational.routing.anthropicAliases.join(",") || "-", "]"] }), _jsx(Text, { color: "gray", children: " " }), _jsxs(Text, { color: "white", children: ["openai=", operational.routing.openAIDefaultModel ?? "default"] }), _jsxs(Text, { color: "gray", children: [" aliases[", operational.routing.openAIAliases.join(",") || "-", "]"] })] }), _jsxs(Box, { paddingLeft: 2, children: [_jsx(Text, { color: "gray", children: "models " }), _jsx(Text, { color: focus === "models" ? "white" : "cyan", children: "[m] list/select" }), _jsx(Text, { color: "gray", children: " change " }), _jsx(Text, { color: focus === "models" ? "white" : "cyan", children: "[c] Claude [o] OpenAI" })] })] }));
1133
+ return (_jsxs(Box, { children: [_jsx(Text, { bold: true, children: " OPERATIONS " }), _jsx(Text, { color: "cyan", children: baseUrl.replace(/^https?:\/\//, "") }), _jsx(Text, { color: "gray", children: " \u00B7 auth " }), _jsx(Text, { color: authColor, children: authLabel }), _jsx(Text, { color: "gray", children: " \u00B7 " }), _jsx(ProviderBadge, { label: "Claude", status: operational.providers.anthropic, ready: claudeReady }), _jsx(Text, { color: "gray", children: " " }), _jsx(ProviderBadge, { label: "ChatGPT", status: operational.providers.openai, ready: openAIReady }), _jsx(Text, { color: "gray", children: " " }), _jsx(ProviderBadge, { label: "Grok", status: operational.providers.xai ?? { configured: false, accounts: 0, healthy: 0, enabled: 0 }, ready: (operational.providers.xai?.healthy ?? 0) > 0 }), crossReady && _jsx(Text, { color: "gray", children: " \u00B7 cross-route" }), _jsx(Text, { color: "gray", children: " \u00B7 cli " }), _jsx(Text, { color: cliRouting.claude.enabled ? "green" : "gray", children: "C" }), _jsx(Text, { color: "gray", children: "/" }), _jsx(Text, { color: cliRouting.codex.enabled ? "green" : "gray", children: "X" }), _jsx(Text, { color: focus === "models" ? "gray" : "cyan", children: " [c]/[x]" }), _jsx(Text, { color: "gray", children: " \u00B7 " }), _jsx(Text, { color: focus === "models" ? "white" : "cyan", children: "[m]" })] }));
903
1134
  }
904
1135
  function ModelsPanel({ status, selectedIndex, focused, visibleRows = MODEL_VISIBLE_ROWS, rowsRef, }) {
905
1136
  const models = status?.models ?? [];
@@ -935,66 +1166,134 @@ function ModelRow({ model, selected, currentClaude, currentOpenAI, }) {
935
1166
  function ProviderBadge({ label, status, ready, }) {
936
1167
  const color = !status.configured ? "gray" : ready ? "green" : "yellow";
937
1168
  const text = status.configured
938
- ? `${label} ${status.healthy}/${status.accounts} healthy`
939
- : `${label} not configured`;
1169
+ ? `${label} ${status.healthy}/${status.accounts}`
1170
+ : `${label} off`;
940
1171
  return _jsx(Text, { color: color, children: text });
941
1172
  }
942
- // ─── Account row (two-line: status + utilization bars) ───────────────────────
1173
+ function AccountGroups({ visible, fleet, windowTop, selectedIndex, focused, }) {
1174
+ const claudeVisible = visible.filter(isClaudeAccount);
1175
+ const chatgptVisible = visible.filter(isOpenAIAccount);
1176
+ const grokVisible = visible.filter(isXaiAccount);
1177
+ const claudeFleet = fleet.filter(isClaudeAccount);
1178
+ const chatgptFleet = fleet.filter(isOpenAIAccount);
1179
+ const grokFleet = fleet.filter(isXaiAccount);
1180
+ if (visible.length === 0) {
1181
+ return _jsx(Text, { color: "gray", children: " No accounts" });
1182
+ }
1183
+ const renderRows = (accounts, offsetInVisible) => accounts.map((account, i) => (_jsx(AccountRow, { account: account, selected: focused && windowTop + offsetInVisible + i === selectedIndex }, account.id)));
1184
+ // Render a group whenever its provider has ANY account — even when the scroll
1185
+ // window currently shows none of its rows. A fully-clipped group still emits
1186
+ // its header + count so a tail provider (e.g. a single Grok account behind
1187
+ // many Claude/ChatGPT ones on a short terminal) never vanishes without a
1188
+ // trace. `offset` advances by VISIBLE rows only, so selection indexing is
1189
+ // unaffected by header-only groups.
1190
+ const groups = [];
1191
+ let offset = 0;
1192
+ if (claudeFleet.length > 0) {
1193
+ groups.push({ key: "CLAUDE", visible: claudeVisible, fleet: claudeFleet, offset });
1194
+ offset += claudeVisible.length;
1195
+ }
1196
+ if (chatgptFleet.length > 0) {
1197
+ groups.push({ key: "CHATGPT", visible: chatgptVisible, fleet: chatgptFleet, offset });
1198
+ offset += chatgptVisible.length;
1199
+ }
1200
+ if (grokFleet.length > 0) {
1201
+ groups.push({ key: "GROK", visible: grokVisible, fleet: grokFleet, offset });
1202
+ }
1203
+ return (_jsx(Box, { flexDirection: "column", children: groups.map((group, index) => (_jsxs(Box, { flexDirection: "column", marginTop: index === 0 ? 0 : 1, children: [_jsx(GroupHeader, { label: group.key, accounts: group.fleet }), group.visible.length > 0 && _jsx(ColumnLegend, {}), renderRows(group.visible, group.offset)] }, group.key))) }));
1204
+ }
1205
+ function GroupHeader({ label, accounts }) {
1206
+ const healthy = accounts.filter(account => account.healthy).length;
1207
+ const weeklyFull = accounts.filter(isWeeklyLimited);
1208
+ const color = healthy === accounts.length && weeklyFull.length === 0 ? "green" : "yellow";
1209
+ const fableHint = label === "CLAUDE" ? exhaustedModelHint(accounts) : undefined;
1210
+ // Grok has no usage windows, so surface its plan ("GrokPro") in the header —
1211
+ // this is the one signal a clipped-out Grok row would otherwise hide.
1212
+ const grokPlan = label === "GROK" && accounts[0] ? grokQuotaNote(accounts[0]) : undefined;
1213
+ return (_jsxs(Box, { children: [_jsxs(Text, { bold: true, color: "gray", children: [" ", label, " "] }), _jsxs(Text, { color: color, children: [healthy, "/", accounts.length, " ok"] }), weeklyFull.length > 0 && _jsx(Text, { color: "red", children: ` · ${weeklyFull.length} 7d full` }), fableHint && _jsx(Text, { color: "red", children: ` · ${fableHint}` }), grokPlan && _jsx(Text, { color: "gray", children: ` · ${grokPlan}` })] }));
1214
+ }
1215
+ const COL = {
1216
+ name: 22,
1217
+ req: 5,
1218
+ sess: 6,
1219
+ pct: 4,
1220
+ note: 22,
1221
+ reset: 8,
1222
+ rst: 5,
1223
+ };
1224
+ function ColumnLegend() {
1225
+ return (_jsx(Text, { color: "gray", children: ` ${"".padEnd(COL.name)} ${"req".padStart(COL.req)}${"s·n".padStart(COL.sess)} ${"5h".padStart(COL.pct)} ${"7d".padStart(COL.pct)} ${"note".padEnd(COL.note)} ${"↻5h".padEnd(COL.reset)} ${"↻7d".padEnd(COL.reset)} ${"rst".padStart(COL.rst)}` }));
1226
+ }
1227
+ function exhaustedModelHint(accounts) {
1228
+ const hits = [];
1229
+ for (const account of accounts) {
1230
+ for (const row of getAccountCapacityRows(account)) {
1231
+ if ((row.utilization ?? 0) < 1 || row.color !== "red")
1232
+ continue;
1233
+ hits.push(`${row.label} full`);
1234
+ }
1235
+ }
1236
+ return hits[0];
1237
+ }
1238
+ // ─── Account row (one aligned line, no bars) ─────────────────────────────────
943
1239
  function AccountRow({ account: a, selected }) {
944
1240
  const rl = a.rateLimits ?? EMPTY_RL;
945
1241
  const usage = rl.usage;
946
1242
  const globalCapacity = getGlobalCapacityView(rl);
947
- const isOpenAI = a.provider === "openai_subscription";
948
- const codex = a.codexRateLimits;
949
- const codexDefaultWindows = getCodexDefaultWindows(codex);
950
- const capacityRows = isOpenAI
951
- ? getCodexCapacityRows(a.codexRateLimits, a.globalCooldownUntilMs)
952
- : getAccountCapacityRows(a);
953
- const isLimited = isOpenAI ? isCodexLimited(a.codexRateLimits) : rl.status === "rate_limited";
1243
+ const isOpenAI = isOpenAIAccount(a);
1244
+ const isXai = isXaiAccount(a);
1245
+ const isLimited = isLimitedAccount(a);
954
1246
  const isDisabled = a.enabled === false;
1247
+ const modelNote = isOpenAI || isXai ? undefined : noteModelLimit(a);
1248
+ const extraCap = usage?.extraUsage;
1249
+ const extraOff = extraCap !== undefined && extraCap.usable !== true
1250
+ && (extraCap.spendLimitReached
1251
+ || (modelNote !== undefined && modelNote.utilization >= 1 && extraCap.enabled === false));
1252
+ const gapNote = openaiQuotaGapNote(a);
1253
+ const grokNote = grokQuotaNote(a);
1254
+ const note = [
1255
+ modelNote
1256
+ ? `${modelNote.label} ${Math.round(modelNote.utilization * 100)}%`
1257
+ : "",
1258
+ extraOff ? "extra off" : "",
1259
+ gapNote ?? "",
1260
+ grokNote ?? "",
1261
+ ].filter(Boolean).join(" · ");
1262
+ const codexWindows = getCodexDefaultWindows(a.codexRateLimits);
1263
+ const sessionWindow = codexWindows.find(window => window.kind === "session");
1264
+ const weeklyWindow = codexWindows.find(window => window.kind === "weekly");
1265
+ const hasClaudeQuota = !isOpenAI && !isXai && (rl.lastUpdated > 0 || usage);
1266
+ const fiveHour = isOpenAI
1267
+ ? sessionWindow
1268
+ : hasClaudeQuota ? globalCapacity.fiveHour : undefined;
1269
+ const sevenDay = isOpenAI
1270
+ ? weeklyWindow
1271
+ : hasClaudeQuota ? globalCapacity.sevenDay : undefined;
1272
+ const creditsLabel = resetCreditsColumnLabel(a);
955
1273
  const dot = isDisabled ? "⊘" : isLimited ? "⊘" : a.busy ? "◌" : a.healthy ? "●" : "●";
956
1274
  const dotColor = isDisabled ? "gray" : isLimited ? "red" : a.busy ? "yellow" : a.healthy ? "green" : "red";
957
- const statusLabel = isDisabled ? "OFF " : isLimited ? "LIMITED" : a.busy ? "busy " : a.healthy ? "ok " : "ERROR ";
958
- const statusColor = isDisabled ? "gray" : isLimited ? "red" : a.busy ? "yellow" : a.healthy ? "green" : "red";
959
- const expiryLabel = a.expiresInMs > 0 ? formatMs(a.expiresInMs) : "EXPIRED";
960
- const expiryColor = a.expiresInMs < 10 * 60 * 1000 ? "red"
961
- : a.expiresInMs < 30 * 60 * 1000 ? "yellow"
962
- : "white";
963
- const providerTag = isOpenAI
964
- ? ` [OpenAI${codex?.plan ? ` ${codex.plan}` : ""}]`
965
- : rl.plan ? ` [${rl.plan}]` : "";
966
- // User-defined caps hint
967
- const s5 = a.sessionLimitPercent ?? 100;
968
- const w7 = a.weeklyLimitPercent ?? 100;
969
- const hasCaps = s5 < 100 || w7 < 100;
970
- const capsHint = hasCaps
971
- ? ` cap${s5 < 100 ? ` 5h≤${s5}%` : ""}${w7 < 100 ? ` 7d≤${w7}%` : ""}`
972
- : "";
973
- const pointer = selected ? "▶" : " ";
974
- const nameColor = isDisabled ? "gray" : undefined;
975
- const creditsLabel = codex?.credits
976
- ? codex.credits.unlimited
977
- ? "∞"
978
- : codex.credits.balance !== undefined
979
- ? codex.credits.balance
980
- : codex.credits.hasCredits ? "yes" : "no"
981
- : undefined;
982
- return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { children: [_jsx(Text, { color: selected ? "cyan" : undefined, children: pointer }), _jsxs(Text, { color: dotColor, children: [" ", dot, " "] }), _jsx(Text, { color: nameColor, dimColor: isDisabled, children: a.id.slice(0, 20).padEnd(21) }), _jsx(Text, { color: statusColor, children: statusLabel }), providerTag && _jsx(Text, { color: isOpenAI ? "cyan" : "magenta", children: providerTag.padEnd(10) }), !providerTag && _jsx(Text, { children: "".padEnd(10) }), _jsx(Text, { color: "gray", children: " req " }), _jsx(Text, { color: "white", children: String(a.requestCount).padStart(5) }), _jsx(Text, { color: "gray", children: " err " }), _jsx(Text, { color: a.errorCount > 0 ? "red" : "gray", children: String(a.errorCount).padStart(3) }), _jsx(Text, { color: "gray", children: " tok " }), _jsx(Text, { color: expiryColor, children: expiryLabel.padEnd(8) }), _jsx(Text, { color: "gray", children: " last " }), _jsx(Text, { color: "gray", children: formatAgo(a.lastUsedMs) }), _jsxs(Text, { color: "gray", children: [" ", a.activeSessions ?? 0, " active / ", a.inFlightRequests ?? 0, " streams"] }), capsHint && _jsx(Text, { color: "yellow", children: capsHint }), a.credentialsPendingWrite && (
983
- // The account still works — its rotated token is live in memory — but a
984
- // restart before the pending write lands would need a re-login.
985
- _jsx(Text, { color: "yellow", children: " creds unsaved" }))] }), (rl.lastUpdated > 0 || usage) && (_jsxs(Box, { paddingLeft: 4, children: [_jsx(UtilBar, { label: "5h", util: globalCapacity.fiveHour.utilization, resetTs: globalCapacity.fiveHour.resetAt, isActive: rl.claim === "five_hour", cap: s5 }), _jsx(Text, { children: " " }), _jsx(UtilBar, { label: "7d all-model", util: globalCapacity.sevenDay.utilization, resetTs: globalCapacity.sevenDay.resetAt, isActive: rl.claim === "seven_day", cap: w7 }), usage && _jsx(Text, { color: globalCapacity.usageFetchStatus === "fresh" ? "gray" : "yellow", children: ` usage ${globalCapacity.usageFetchStatus} ${usage.fetchedAt > 0 ? formatAgo(usage.fetchedAt) : ""}` })] })), isOpenAI && codexDefaultWindows.length > 0 && (_jsxs(Box, { paddingLeft: 4, children: [codexDefaultWindows.map((window, index) => (_jsxs(React.Fragment, { children: [index > 0 && _jsx(Text, { children: " " }), _jsx(UtilBar, { label: window.label, util: window.utilization, resetTs: window.resetAt, isActive: false, cap: window.kind === "weekly" ? w7 : s5 })] }, window.label))), creditsLabel !== undefined && _jsx(Text, { color: "gray", children: ` credits ${creditsLabel}` })] })), capacityRows.map((row, index) => (_jsxs(Box, { paddingLeft: 4, children: [_jsxs(Text, { color: row.color, children: [" ", row.label] }), _jsxs(Text, { color: "gray", children: [" \u00B7 ", row.state] }), row.utilization !== undefined && _jsx(Text, { color: row.color, children: ` ${Math.round(row.utilization * 100)}%` }), row.resetAt !== undefined && row.resetAt > 0 && (_jsxs(Text, { color: "gray", children: [" ", `↻${formatResetIn(row.resetAt)}`] }))] }, `${row.label}-${index}`)))] }));
1275
+ const sessions = a.activeSessions ?? 0;
1276
+ const inflight = a.inFlightRequests ?? 0;
1277
+ const load = sessions <= 0 && inflight <= 0
1278
+ ? ""
1279
+ : inflight > 0
1280
+ ? `${sessions}·${inflight}`
1281
+ : String(sessions);
1282
+ return (_jsxs(Box, { children: [_jsx(Text, { color: selected ? "cyan" : undefined, children: selected ? "▶" : " " }), _jsx(Text, { color: dotColor, children: dot }), _jsx(Text, { color: selected ? "white" : isDisabled ? "gray" : undefined, dimColor: isDisabled, children: ` ${a.id.slice(0, COL.name).padEnd(COL.name)}` }), _jsx(Text, { color: "white", children: String(a.requestCount).padStart(COL.req) }), _jsx(Text, { color: "gray", children: load.padStart(COL.sess) }), _jsx(Text, { children: " " }), _jsx(QuotaCell, { util: fiveHour?.utilization }), _jsx(Text, { children: " " }), _jsx(QuotaCell, { util: sevenDay?.utilization }), _jsx(Text, { color: extraOff || gapNote ? "yellow" : modelNote?.color ?? "gray", children: ` ${note.padEnd(COL.note)}` }), _jsx(Text, { children: " " }), _jsx(ResetCell, { resetTs: fiveHour?.resetAt }), _jsx(Text, { children: " " }), _jsx(ResetCell, { resetTs: sevenDay?.resetAt }), _jsx(Text, { children: " " }), _jsx(Text, { color: "gray", children: creditsLabel.padStart(COL.rst) }), a.credentialsPendingWrite && _jsx(Text, { color: "yellow", children: " unsaved" })] }));
986
1283
  }
987
- // ─── Utilization bar ─────────────────────────────────────────────────────────
988
- function UtilBar({ label, util, resetTs, isActive, cap }) {
1284
+ function QuotaCell({ util }) {
1285
+ if (util === undefined) {
1286
+ return _jsx(Text, { color: "gray", children: "—".padStart(COL.pct) });
1287
+ }
989
1288
  const pct = Math.round(util * 100);
990
- const BAR_W = 12;
991
- const filled = Math.round(util * BAR_W);
992
- const capPos = Math.round((cap / 100) * BAR_W);
993
- const bar = "█".repeat(Math.min(filled, BAR_W)) + "░".repeat(Math.max(BAR_W - filled, 0));
994
- const color = pct >= cap ? "red" : pct >= 90 ? "red" : pct >= 70 ? "yellow" : "green";
995
- const resetLabel = resetTs > 0 ? formatResetIn(resetTs) : "";
996
- const capLabel = cap < 100 ? ` cap ${cap}%` : "";
997
- return (_jsxs(Box, { children: [_jsxs(Text, { color: isActive ? "white" : "gray", bold: isActive, children: [label, " "] }), _jsx(Text, { color: color, children: bar }), _jsxs(Text, { color: color, children: [String(pct).padStart(4), "%"] }), capLabel && _jsx(Text, { color: "yellow", children: capLabel }), resetLabel && _jsxs(Text, { color: "gray", children: [" \u21BB", resetLabel] })] }));
1289
+ const color = pct >= 90 ? "red" : pct >= 70 ? "yellow" : "green";
1290
+ return _jsx(Text, { color: color, children: `${pct}%`.padStart(COL.pct) });
1291
+ }
1292
+ function ResetCell({ resetTs }) {
1293
+ if (!resetTs || resetTs <= 0) {
1294
+ return _jsx(Text, { color: "gray", children: "—".padEnd(COL.reset) });
1295
+ }
1296
+ return _jsx(Text, { color: "gray", children: `↻${formatResetIn(resetTs)}`.padEnd(COL.reset) });
998
1297
  }
999
1298
  function formatResetIn(unixSeconds) {
1000
1299
  const diff = unixSeconds - Date.now() / 1000;
@@ -1047,28 +1346,21 @@ function LogRow({ log, selected }) {
1047
1346
  }
1048
1347
  // ─── Detail panel ─────────────────────────────────────────────────────────────
1049
1348
  function DetailPanel({ log }) {
1050
- const time = new Date(log.ts).toLocaleString("en-GB", {
1051
- hour12: false,
1052
- year: "numeric", month: "2-digit", day: "2-digit",
1053
- hour: "2-digit", minute: "2-digit", second: "2-digit",
1054
- });
1349
+ const time = new Date(log.ts).toLocaleTimeString("en-GB", { hour12: false });
1055
1350
  const isError = log.type === "error";
1056
1351
  const isWarn = log.type === "warn";
1057
- const statusLabel = log.statusCode === undefined ? "—"
1352
+ const statusLabel = log.statusCode === undefined ? ""
1058
1353
  : log.statusCode === 0 ? "connection error"
1059
- : `${log.statusCode} ${httpStatusText(log.statusCode)}`;
1354
+ : String(log.statusCode);
1060
1355
  const statusColor = log.statusCode === undefined ? "gray"
1061
1356
  : log.statusCode === 0 ? "red"
1062
1357
  : log.statusCode >= 500 ? "red"
1063
1358
  : log.statusCode >= 400 ? "yellow"
1064
1359
  : "green";
1065
- return (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: "gray", paddingX: 1, children: [_jsx(Text, { bold: true, color: isError ? "red" : isWarn ? "yellow" : "cyan", children: " DETAILS " }), _jsxs(Box, { marginTop: 1, flexDirection: "column", gap: 0, children: [_jsxs(Box, { gap: 2, children: [_jsx(Field, { label: "Time", value: time }), _jsx(Field, { label: "Account", value: log.accountId })] }), _jsxs(Box, { gap: 2, children: [_jsx(Field, { label: "Method", value: log.method ?? "—" }), _jsx(Field, { label: "Path", value: log.path ?? "—" })] }), _jsxs(Box, { gap: 2, children: [_jsx(FieldColored, { label: "Status", value: statusLabel, color: statusColor }), _jsx(Field, { label: "Duration", value: log.durationMs !== undefined ? `${log.durationMs}ms` : "—" }), _jsx(Field, { label: "Type", value: log.type }), _jsx(Field, { label: "Source", value: sourceFullLabel(log.source) })] }), log.details && (_jsx(Box, { children: _jsx(Field, { label: "Details", value: log.details }) })), log.cacheReadTokens !== undefined && (_jsx(Box, { gap: 2, children: _jsx(CacheBreakdown, { read: log.cacheReadTokens, created: log.cacheCreationTokens ?? 0, input: log.inputTokens ?? 0, output: log.outputTokens ?? 0 }) }))] })] }));
1066
- }
1067
- function Field({ label, value }) {
1068
- return (_jsxs(Box, { children: [_jsxs(Text, { color: "gray", children: [label, ": "] }), _jsx(Text, { color: "white", children: value })] }));
1069
- }
1070
- function FieldColored({ label, value, color }) {
1071
- return (_jsxs(Box, { children: [_jsxs(Text, { color: "gray", children: [label, ": "] }), _jsx(Text, { color: color, children: value })] }));
1360
+ const inputTok = (log.cacheReadTokens ?? 0) + (log.cacheCreationTokens ?? 0) + (log.inputTokens ?? 0);
1361
+ const outputTok = log.outputTokens ?? 0;
1362
+ const hitPct = inputTok > 0 ? Math.round(((log.cacheReadTokens ?? 0) / inputTok) * 100) : null;
1363
+ return (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: "gray", paddingX: 1, children: [_jsx(Text, { bold: true, color: isError ? "red" : isWarn ? "yellow" : "cyan", children: " DETAILS " }), _jsxs(Box, { children: [_jsxs(Text, { color: "gray", children: [time, " ", log.accountId] }), log.method && log.path && _jsx(Text, { color: "white", children: ` ${log.method} ${log.path}` }), statusLabel !== "" && _jsx(Text, { color: statusColor, children: ` ${statusLabel}` }), log.durationMs !== undefined && _jsx(Text, { color: "gray", children: ` ${log.durationMs}ms` }), _jsx(Text, { color: "gray", children: ` ${sourceFullLabel(log.source)}` }), log.details && _jsx(Text, { color: "gray", children: ` ${log.details}` })] }), (inputTok > 0 || outputTok > 0) && (_jsxs(Text, { color: "gray", children: [`${fmtTok(inputTok)} in · ${fmtTok(outputTok)} out`, hitPct !== null ? ` · cache ${hitPct}%` : ""] }))] }));
1072
1364
  }
1073
1365
  // ─── Cache health badge (aggregated) ─────────────────────────────────────────
1074
1366
  function CacheHealthBadge({ read, created, input }) {
@@ -1080,13 +1372,6 @@ function CacheHealthBadge({ read, created, input }) {
1080
1372
  const label = hitPct >= 70 ? "healthy" : hitPct >= 30 ? "fair" : "poor";
1081
1373
  return (_jsxs(_Fragment, { children: [_jsx(Text, { color: "gray", children: " \u00B7 " }), _jsx(Text, { children: "cache " }), _jsxs(Text, { color: color, children: [hitPct, "% hit "] }), _jsxs(Text, { color: "gray", children: ["(", label, ")"] })] }));
1082
1374
  }
1083
- // ─── Cache breakdown (per-request detail) ────────────────────────────────────
1084
- function CacheBreakdown({ read, created, input, output }) {
1085
- const totalInput = read + created + input;
1086
- const hitPct = totalInput > 0 ? (read / totalInput) * 100 : 0;
1087
- const color = totalInput === 0 ? "gray" : hitPct >= 70 ? "green" : hitPct >= 30 ? "yellow" : "red";
1088
- return (_jsxs(_Fragment, { children: [_jsx(FieldColored, { label: "Cache hit", value: totalInput > 0 ? `${fmtTok(read)} tok (${hitPct.toFixed(1)}%)` : "—", color: color }), _jsx(Field, { label: "Cache created", value: fmtTok(created) + " tok" }), _jsx(Field, { label: "Uncached", value: fmtTok(input) + " tok" }), _jsx(Field, { label: "Total input", value: fmtTok(totalInput) + " tok" }), _jsx(Field, { label: "Output", value: fmtTok(output) + " tok" }), _jsx(Field, { label: "Total", value: fmtTok(totalInput + output) + " tok" })] }));
1089
- }
1090
1375
  // ─── Token summary (aggregated totals) ──────────────────────────────────────
1091
1376
  function TokenSummary({ cacheRead, cacheCreated, uncached, output }) {
1092
1377
  const totalInput = cacheRead + cacheCreated + uncached;
@@ -1114,17 +1399,6 @@ function sourceFullLabel(source) {
1114
1399
  return "API";
1115
1400
  return "—";
1116
1401
  }
1117
- // ─── HTTP status text ─────────────────────────────────────────────────────────
1118
- function httpStatusText(code) {
1119
- const map = {
1120
- 200: "OK", 201: "Created", 204: "No Content",
1121
- 400: "Bad Request", 401: "Unauthorized", 403: "Forbidden",
1122
- 404: "Not Found", 429: "Too Many Requests",
1123
- 500: "Internal Server Error", 502: "Bad Gateway",
1124
- 503: "Service Unavailable", 529: "Overloaded",
1125
- };
1126
- return map[code] ?? "";
1127
- }
1128
1402
  // ─── Formatters ───────────────────────────────────────────────────────────────
1129
1403
  function formatUptime(seconds) {
1130
1404
  const h = Math.floor(seconds / 3_600);
@@ -1136,18 +1410,3 @@ function formatUptime(seconds) {
1136
1410
  return `${m}m ${s}s`;
1137
1411
  return `${s}s`;
1138
1412
  }
1139
- function formatMs(ms) {
1140
- const totalMin = Math.round(ms / 60_000);
1141
- if (totalMin >= 60)
1142
- return `${Math.floor(totalMin / 60)}h ${totalMin % 60}m`;
1143
- return `${totalMin}m`;
1144
- }
1145
- function formatAgo(ts) {
1146
- if (!ts)
1147
- return "never";
1148
- const s = Math.round((Date.now() - ts) / 1_000);
1149
- if (s < 60)
1150
- return `${s}s ago`;
1151
- const m = Math.floor(s / 60);
1152
- return `${m}m ago`;
1153
- }