1import {
2 collectLimitAccounts,
3 collectLimitPools,
4 type LimitAccount,
5 type LimitPresentations,
6} from "@t3tools/shared/usageLimits";
7
8export interface SubscriptionUsageSnapshot {
9 url?: string;
10 checkedAt: number;
11 providers: Array<{
12 name: string;
13 detail: string;
14 windows: Array<{ kind?: string; label: string; remaining: number; reset: string }>;
15 expiresAt: number;
16 totalWindows: number;
17 }>;
18}
19
20// Snapshots expire after 15 minutes; background refresh needs a
21// separate authenticated transport while the mobile app is suspended.
22const SNAPSHOT_MAX_AGE = 15 * 60_000;
23export const WIDGET_REFRESH_INTERVAL = 5 * 60_000;
24
25/** Bound probes across config updates, reconnects, and foreground transitions. */
26export function createWidgetRefresher<Id>(refresh: (id: Id) => Promise<unknown>) {
27 const attempted = new Map<Id, number>();
28 const pending = new Set<Id>();
29 return async (connected: readonly Id[], now: number) => {
30 await Promise.allSettled(
31 connected.map(async (id) => {
32 if (pending.has(id) || now - (attempted.get(id) ?? -Infinity) < WIDGET_REFRESH_INTERVAL)
33 return;
34 attempted.set(id, now);
35 pending.add(id);
36 try {
37 await refresh(id);
38 } finally {
39 pending.delete(id);
40 }
41 }),
42 );
43 };
44}
45
46function subscriptionUsageProps(
47 accounts: readonly LimitAccount[],
48 now: number,
49 configuredDrivers: ReadonlySet<string>,
50 maxWindowsPerProvider: number,
51): SubscriptionUsageSnapshot {
52 const pools = collectLimitPools(accounts, now);
53 const checked = accounts
54 .filter((account) => account.driver === "codex" || account.driver === "claudeAgent")
55 .map((account) => Date.parse(account.limits.checkedAt));
56 return {
57 checkedAt: checked.length > 0 && checked.every(Number.isFinite) ? Math.min(...checked) : 0,
58 providers: (["codex", "claudeAgent"] as const)
59 .filter(
60 (driver) =>
61 configuredDrivers.has(driver) || accounts.some((account) => account.driver === driver),
62 )
63 .map((driver) => {
64 const pool = pools.find((candidate) => candidate.driver === driver);
65 const name = driver === "codex" ? "Codex" : "Claude";
66 if (!pool)
67 return {
68 name,
69 detail: "No limits available",
70 windows: [],
71 expiresAt: 0,
72 totalWindows: 0,
73 };
74 const checkedAt = Math.min(...pool.accounts.map((a) => Date.parse(a.limits.checkedAt)));
75 const expiresAt = Math.min(
76 checkedAt + SNAPSHOT_MAX_AGE,
77 ...pool.windows.flatMap((window) => window.resets.map((reset) => reset.at)),
78 );
79 const fresh = Number.isFinite(expiresAt) && expiresAt > now;
80 const sortedWindows = [...pool.windows].sort(
81 (a, b) => a.remainingPercent - b.remainingPercent,
82 );
83 // Keep a session and weekly limit when scoped limits fill the storage budget.
84 const selectedWindows = [
85 ...new Set([
86 sortedWindows.find((window) => window.kind === "session"),
87 sortedWindows.find((window) => window.kind === "weekly"),
88 ...sortedWindows,
89 ]),
90 ]
91 .filter((window) => window !== undefined)
92 .slice(0, maxWindowsPerProvider)
93 .sort((a, b) => a.remainingPercent - b.remainingPercent);
94 return {
95 name,
96 detail: !fresh
97 ? "Open The Knife to refresh"
98 : pool.accounts.length > 1
99 ? `${pool.accounts.length} accounts · pooled`
100 : "Subscription remaining",
101 expiresAt: fresh ? expiresAt : 0,
102 totalWindows: fresh ? pool.windows.length : 0,
103 windows: fresh
104 ? selectedWindows.map((window) => ({
105 kind: window.kind,
106 label: window.label,
107 remaining: Math.round(window.remainingPercent),
108 reset: window.resets[0]
109 ? `Next reset ${new Date(window.resets[0].at).toLocaleString(undefined, {
110 month: "short",
111 day: "numeric",
112 hour: "numeric",
113 minute: "2-digit",
114 })}`
115 : "Reset time unavailable",
116 }))
117 : [],
118 };
119 }),
120 };
121}
122
123/** Deduplicate accounts before pooling, and only publish display data to the OS. */
124export function buildSubscriptionUsageSnapshot(
125 presentations: LimitPresentations,
126 url: string,
127 maxWindowsPerProvider = 6,
128): SubscriptionUsageSnapshot {
129 // Freshness is evaluated at publication/render time, not on unrelated config emissions.
130 const configuredDrivers = new Set(
131 [...presentations.values()].flatMap((presentation) =>
132 (presentation.serverConfig?.providers ?? [])
133 // Servers report default-enabled drivers even when their CLI is missing.
134 .filter(
135 (provider) =>
136 provider.enabled &&
137 provider.installed &&
138 provider.usageLimits?.unavailable?.reason !== "unsupported",
139 )
140 .map((provider) => provider.driver),
141 ),
142 );
143 return {
144 ...subscriptionUsageProps(
145 collectLimitAccounts(presentations),
146 0,
147 configuredDrivers,
148 maxWindowsPerProvider,
149 ),
150 url,
151 };
152}
153
154export function subscriptionUsageTimeline(snapshot: SubscriptionUsageSnapshot, now: number) {
155 const deadlines = [...new Set(snapshot.providers.map((p) => p.expiresAt))]
156 .filter((deadline) => deadline > now)
157 .sort((a, b) => a - b);
158 return [now, ...deadlines].map((date) => ({
159 date: new Date(date),
160 props: {
161 ...snapshot,
162 providers: snapshot.providers.map((provider) =>
163 provider.windows.length > 0 && provider.expiresAt <= date
164 ? { ...provider, detail: "Open The Knife to refresh", windows: [], totalWindows: 0 }
165 : provider,
166 ),
167 },
168 }));
169}