dondo-donuts 0.1.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.
@@ -0,0 +1,392 @@
1
+ import { render } from 'preact';
2
+ import { useEffect, useState } from 'preact/hooks';
3
+ import packageJson from '../../package.json';
4
+ import type { LimitResult, ModelLimit } from '../types.ts';
5
+
6
+ type AccountEntry = {
7
+ active: boolean;
8
+ key: string;
9
+ updatedAt: string;
10
+ limitUpdatedAt: string;
11
+ quota: LimitResult | null;
12
+ };
13
+
14
+ type AntigravityState = {
15
+ account: string;
16
+ entries: AccountEntry[];
17
+ service: string;
18
+ vaultPath: string;
19
+ };
20
+
21
+ type CodexState = {
22
+ authPath: string;
23
+ entries: AccountEntry[];
24
+ vaultPath: string;
25
+ };
26
+
27
+ type Tab = 'antigravity' | 'codex';
28
+
29
+ const api = async <T,>(path: string, body?: unknown): Promise<T> => {
30
+ const response = await fetch(path, {
31
+ body: body ? JSON.stringify(body) : undefined,
32
+ headers: { 'Content-Type': 'application/json' },
33
+ method: body ? 'POST' : 'GET',
34
+ });
35
+ const contentType = response.headers.get('content-type') ?? '';
36
+ const json = contentType.includes('application/json') ? await response.json() : { error: await response.text() };
37
+ if (!response.ok) {
38
+ throw new Error(json.error ?? response.statusText);
39
+ }
40
+ return json as T;
41
+ };
42
+
43
+ const formatDate = (value: string) => (value ? new Date(value).toLocaleString() : '');
44
+
45
+ const ModelCard = ({ model }: { model: [string, ModelLimit] }) => {
46
+ const [name, data] = model;
47
+ const width = Math.max(0, Math.min(100, data.percentage));
48
+
49
+ return (
50
+ <div class="model">
51
+ <div>
52
+ <b>{data.displayName || name}</b>
53
+ </div>
54
+ <div class="muted small">{name}</div>
55
+ <div class="bar">
56
+ <div class="fill" style={{ width: `${width}%` }} />
57
+ </div>
58
+ <div class="small">
59
+ {data.percentage}% left{data.resetTime ? ` · resets ${formatDate(data.resetTime)}` : ''}
60
+ </div>
61
+ </div>
62
+ );
63
+ };
64
+
65
+ const AccountRow = ({
66
+ entry,
67
+ pending,
68
+ onLoad,
69
+ onRefresh,
70
+ }: {
71
+ entry: AccountEntry;
72
+ pending: boolean;
73
+ onLoad: (key: string) => void;
74
+ onRefresh: (key: string) => void;
75
+ }) => (
76
+ <article class="row">
77
+ <div class="row-head">
78
+ <div>
79
+ <div class="keyline">
80
+ <div class="key">{entry.key}</div>
81
+ {entry.active ? <span class="badge">Active</span> : null}
82
+ </div>
83
+ <div class="muted small">
84
+ Updated {formatDate(entry.updatedAt)}
85
+ {entry.limitUpdatedAt ? ` · limits ${formatDate(entry.limitUpdatedAt)}` : ''}
86
+ {entry.quota?.ok ? ` · ${entry.quota.tier}` : ''}
87
+ </div>
88
+ </div>
89
+ <div class="actions">
90
+ <button type="button" disabled={pending} onClick={() => onRefresh(entry.key)}>
91
+ Refresh
92
+ </button>
93
+ <button type="button" disabled={pending} onClick={() => onLoad(entry.key)}>
94
+ Load
95
+ </button>
96
+ </div>
97
+ </div>
98
+ {entry.quota?.ok ? (
99
+ <div class="quota">
100
+ {Object.entries(entry.quota.models).map((model) => (
101
+ <ModelCard key={model[0]} model={model} />
102
+ ))}
103
+ </div>
104
+ ) : (
105
+ <div class="err small">{entry.quota?.error ?? 'No cached limit data'}</div>
106
+ )}
107
+ </article>
108
+ );
109
+
110
+ const AntigravityPanel = ({ active }: { active: boolean }) => {
111
+ const [state, setState] = useState<AntigravityState | null>(null);
112
+ const [status, setStatus] = useState('');
113
+ const [key, setKey] = useState('');
114
+ const [loaded, setLoaded] = useState(false);
115
+ const [pendingKey, setPendingKey] = useState('');
116
+
117
+ const refresh = async (forceLimits = false) => {
118
+ setStatus(forceLimits ? 'Refreshing limits...' : 'Loading accounts...');
119
+ setState(
120
+ await api<AntigravityState>(
121
+ forceLimits ? '/api/antigravity/limits/refresh' : '/api/antigravity/state',
122
+ forceLimits ? {} : undefined,
123
+ ),
124
+ );
125
+ setLoaded(true);
126
+ setStatus('');
127
+ };
128
+
129
+ const save = async (event: Event) => {
130
+ event.preventDefault();
131
+ const trimmed = key.trim();
132
+ if (!trimmed) {
133
+ return;
134
+ }
135
+ setStatus('Saving...');
136
+ try {
137
+ await api('/api/antigravity/save', { key: trimmed });
138
+ setKey('');
139
+ await refresh(false);
140
+ setStatus(`Saved ${trimmed}`);
141
+ } catch (error) {
142
+ setStatus(error instanceof Error ? error.message : String(error));
143
+ }
144
+ };
145
+
146
+ const load = async (entryKey: string) => {
147
+ setStatus(`Loading ${entryKey}...`);
148
+ setPendingKey(entryKey);
149
+ try {
150
+ await api('/api/antigravity/load', { key: entryKey });
151
+ await refresh(false);
152
+ setStatus(`Loaded ${entryKey}`);
153
+ } catch (error) {
154
+ setStatus(error instanceof Error ? error.message : String(error));
155
+ } finally {
156
+ setPendingKey('');
157
+ }
158
+ };
159
+
160
+ const refreshOne = async (entryKey: string) => {
161
+ setStatus(`Refreshing ${entryKey}...`);
162
+ setPendingKey(entryKey);
163
+ try {
164
+ setState(await api<AntigravityState>('/api/antigravity/limits/refresh', { key: entryKey }));
165
+ setStatus(`Refreshed ${entryKey}`);
166
+ } catch (error) {
167
+ setStatus(error instanceof Error ? error.message : String(error));
168
+ } finally {
169
+ setPendingKey('');
170
+ }
171
+ };
172
+
173
+ const clear = async () => {
174
+ if (!confirm('Clear the live Antigravity keychain item and local auth state?')) {
175
+ return;
176
+ }
177
+ setStatus('Clearing...');
178
+ try {
179
+ await api('/api/antigravity/clear', {});
180
+ await refresh(false);
181
+ setStatus('Cleared live Antigravity auth state');
182
+ } catch (error) {
183
+ setStatus(error instanceof Error ? error.message : String(error));
184
+ }
185
+ };
186
+
187
+ useEffect(() => {
188
+ if (!active || loaded) {
189
+ return;
190
+ }
191
+ refresh(false).catch((error) => {
192
+ setStatus(error.message);
193
+ });
194
+ }, [active, loaded]);
195
+
196
+ return (
197
+ <div hidden={!active}>
198
+ <div class="toolbar">
199
+ <div class="muted small">{state ? `${state.service}/${state.account} · ${state.vaultPath}` : ''}</div>
200
+ <button type="button" onClick={() => refresh(true).catch((error) => setStatus(error.message))}>
201
+ Refresh limits
202
+ </button>
203
+ </div>
204
+ <section class="panel">
205
+ <form onSubmit={save}>
206
+ <input
207
+ value={key}
208
+ placeholder="Account label"
209
+ autocomplete="off"
210
+ onInput={(event) => setKey(event.currentTarget.value)}
211
+ />
212
+ <button class="primary" type="submit">
213
+ Save current
214
+ </button>
215
+ <button type="button" onClick={() => clear().catch((error) => setStatus(error.message))}>
216
+ Clear live
217
+ </button>
218
+ </form>
219
+ <div id="status" class="status muted">
220
+ {status}
221
+ </div>
222
+ </section>
223
+ <section class="list">
224
+ {state?.entries.length ? (
225
+ state.entries.map((entry) => (
226
+ <AccountRow
227
+ key={entry.key}
228
+ entry={entry}
229
+ pending={pendingKey === entry.key}
230
+ onLoad={load}
231
+ onRefresh={refreshOne}
232
+ />
233
+ ))
234
+ ) : (
235
+ <div class="muted">No saved accounts yet.</div>
236
+ )}
237
+ </section>
238
+ </div>
239
+ );
240
+ };
241
+
242
+ const CodexPanel = ({ active }: { active: boolean }) => {
243
+ const [state, setState] = useState<CodexState | null>(null);
244
+ const [status, setStatus] = useState('');
245
+ const [key, setKey] = useState('');
246
+ const [loaded, setLoaded] = useState(false);
247
+ const [pendingKey, setPendingKey] = useState('');
248
+
249
+ const refresh = async (forceLimits = false) => {
250
+ setStatus(forceLimits ? 'Refreshing limits...' : 'Loading accounts...');
251
+ setState(
252
+ await api<CodexState>(
253
+ forceLimits ? '/api/codex/limits/refresh' : '/api/codex/state',
254
+ forceLimits ? {} : undefined,
255
+ ),
256
+ );
257
+ setLoaded(true);
258
+ setStatus('');
259
+ };
260
+
261
+ const save = async (event: Event) => {
262
+ event.preventDefault();
263
+ const trimmed = key.trim();
264
+ if (!trimmed) {
265
+ return;
266
+ }
267
+ setStatus('Saving...');
268
+ try {
269
+ await api('/api/codex/save', { key: trimmed });
270
+ setKey('');
271
+ await refresh(false);
272
+ setStatus(`Saved ${trimmed}`);
273
+ } catch (error) {
274
+ setStatus(error instanceof Error ? error.message : String(error));
275
+ }
276
+ };
277
+
278
+ const load = async (entryKey: string) => {
279
+ setStatus(`Loading ${entryKey}...`);
280
+ setPendingKey(entryKey);
281
+ try {
282
+ await api('/api/codex/load', { key: entryKey });
283
+ await refresh(false);
284
+ setStatus(`Loaded ${entryKey}`);
285
+ } catch (error) {
286
+ setStatus(error instanceof Error ? error.message : String(error));
287
+ } finally {
288
+ setPendingKey('');
289
+ }
290
+ };
291
+
292
+ const refreshOne = async (entryKey: string) => {
293
+ setStatus(`Refreshing ${entryKey}...`);
294
+ setPendingKey(entryKey);
295
+ try {
296
+ setState(await api<CodexState>('/api/codex/limits/refresh', { key: entryKey }));
297
+ setStatus(`Refreshed ${entryKey}`);
298
+ } catch (error) {
299
+ setStatus(error instanceof Error ? error.message : String(error));
300
+ } finally {
301
+ setPendingKey('');
302
+ }
303
+ };
304
+
305
+ useEffect(() => {
306
+ if (!active || loaded) {
307
+ return;
308
+ }
309
+ refresh(false).catch((error) => {
310
+ setStatus(error.message);
311
+ });
312
+ }, [active, loaded]);
313
+
314
+ return (
315
+ <div hidden={!active}>
316
+ <div class="toolbar">
317
+ <div class="muted small">{state ? `${state.authPath} · ${state.vaultPath}` : ''}</div>
318
+ <button type="button" onClick={() => refresh(true).catch((error) => setStatus(error.message))}>
319
+ Refresh limits
320
+ </button>
321
+ </div>
322
+ <section class="panel">
323
+ <form onSubmit={save}>
324
+ <input
325
+ value={key}
326
+ placeholder="Account label"
327
+ autocomplete="off"
328
+ onInput={(event) => setKey(event.currentTarget.value)}
329
+ />
330
+ <button class="primary" type="submit">
331
+ Save current
332
+ </button>
333
+ </form>
334
+ <div class="status muted">{status}</div>
335
+ </section>
336
+ <section class="list">
337
+ {state?.entries.length ? (
338
+ state.entries.map((entry) => (
339
+ <AccountRow
340
+ key={entry.key}
341
+ entry={entry}
342
+ pending={pendingKey === entry.key}
343
+ onLoad={load}
344
+ onRefresh={refreshOne}
345
+ />
346
+ ))
347
+ ) : (
348
+ <div class="muted">No saved accounts yet.</div>
349
+ )}
350
+ </section>
351
+ </div>
352
+ );
353
+ };
354
+
355
+ const App = () => {
356
+ const [tab, setTab] = useState<Tab>('antigravity');
357
+
358
+ return (
359
+ <main>
360
+ <div class="top">
361
+ <div class="brand">
362
+ <img src="/icon.svg" alt="" />
363
+ <h1>Dondo</h1>
364
+ </div>
365
+ </div>
366
+ <nav class="tabs" aria-label="Platforms">
367
+ <button
368
+ type="button"
369
+ class={tab === 'antigravity' ? 'tab active' : 'tab'}
370
+ onClick={() => setTab('antigravity')}
371
+ >
372
+ Antigravity
373
+ </button>
374
+ <button type="button" class={tab === 'codex' ? 'tab active' : 'tab'} onClick={() => setTab('codex')}>
375
+ Codex
376
+ </button>
377
+ </nav>
378
+ <AntigravityPanel active={tab === 'antigravity'} />
379
+ <CodexPanel active={tab === 'codex'} />
380
+ <footer class="footer">
381
+ <a href={packageJson.homepage} target="_blank" rel="noreferrer">
382
+ GitHub
383
+ </a>
384
+ </footer>
385
+ </main>
386
+ );
387
+ };
388
+
389
+ const root = document.getElementById('app');
390
+ if (root) {
391
+ render(<App />, root);
392
+ }
package/src/ui/html.ts ADDED
@@ -0,0 +1,15 @@
1
+ export const renderHtml = () => `<!doctype html>
2
+ <html>
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
6
+ <title>Dondo</title>
7
+ <link rel="icon" href="/icon.svg" type="image/svg+xml" />
8
+ <link rel="apple-touch-icon" href="/icon.png" />
9
+ <link rel="stylesheet" href="/assets/styles.css" />
10
+ </head>
11
+ <body>
12
+ <div id="app"></div>
13
+ <script type="module" src="/assets/app.js"></script>
14
+ </body>
15
+ </html>`;
@@ -0,0 +1,213 @@
1
+ * {
2
+ box-sizing: border-box;
3
+ }
4
+
5
+ body {
6
+ margin: 0;
7
+ background: #f7f8fa;
8
+ color: #1f2937;
9
+ font:
10
+ 14px / 1.45 ui-sans-serif,
11
+ system-ui,
12
+ -apple-system,
13
+ BlinkMacSystemFont,
14
+ "Segoe UI",
15
+ sans-serif;
16
+ }
17
+
18
+ main {
19
+ max-width: 960px;
20
+ margin: 38px auto;
21
+ padding: 0 20px;
22
+ }
23
+
24
+ h1 {
25
+ margin: 0;
26
+ font-size: 24px;
27
+ }
28
+
29
+ button,
30
+ input {
31
+ font: inherit;
32
+ }
33
+
34
+ button {
35
+ cursor: pointer;
36
+ border: 1px solid #d1d5db;
37
+ border-radius: 6px;
38
+ background: #fff;
39
+ padding: 9px 12px;
40
+ }
41
+
42
+ button:hover {
43
+ filter: brightness(0.98);
44
+ }
45
+
46
+ button:disabled {
47
+ cursor: default;
48
+ opacity: 0.55;
49
+ }
50
+
51
+ input {
52
+ flex: 1;
53
+ border: 1px solid #d1d5db;
54
+ border-radius: 6px;
55
+ padding: 10px 12px;
56
+ }
57
+
58
+ form,
59
+ .actions,
60
+ .tabs {
61
+ display: flex;
62
+ gap: 10px;
63
+ }
64
+
65
+ .top {
66
+ display: flex;
67
+ align-items: center;
68
+ justify-content: space-between;
69
+ gap: 16px;
70
+ margin-bottom: 18px;
71
+ }
72
+
73
+ .brand {
74
+ display: flex;
75
+ align-items: center;
76
+ gap: 10px;
77
+ }
78
+
79
+ .brand img {
80
+ width: 32px;
81
+ height: 32px;
82
+ }
83
+
84
+ .tabs {
85
+ margin-bottom: 16px;
86
+ }
87
+
88
+ .tab {
89
+ color: #374151;
90
+ }
91
+
92
+ .tab.active,
93
+ button.primary {
94
+ border-color: #111827;
95
+ background: #111827;
96
+ color: #fff;
97
+ }
98
+
99
+ .toolbar {
100
+ display: flex;
101
+ align-items: center;
102
+ justify-content: space-between;
103
+ gap: 12px;
104
+ margin-bottom: 12px;
105
+ }
106
+
107
+ .muted {
108
+ color: #6b7280;
109
+ }
110
+
111
+ .panel,
112
+ .row {
113
+ border: 1px solid #e5e7eb;
114
+ border-radius: 8px;
115
+ background: #fff;
116
+ box-shadow: 0 1px 2px #00000008;
117
+ }
118
+
119
+ .panel {
120
+ margin-bottom: 16px;
121
+ padding: 16px;
122
+ }
123
+
124
+ .list {
125
+ display: grid;
126
+ gap: 10px;
127
+ }
128
+
129
+ .row {
130
+ padding: 14px;
131
+ }
132
+
133
+ .row-head {
134
+ display: flex;
135
+ align-items: center;
136
+ justify-content: space-between;
137
+ gap: 12px;
138
+ }
139
+
140
+ .key {
141
+ font-weight: 650;
142
+ font-size: 16px;
143
+ }
144
+
145
+ .keyline {
146
+ display: flex;
147
+ align-items: center;
148
+ gap: 8px;
149
+ }
150
+
151
+ .badge {
152
+ border-radius: 999px;
153
+ background: #e8f5ee;
154
+ color: #166534;
155
+ padding: 2px 7px;
156
+ font-size: 11px;
157
+ font-weight: 650;
158
+ }
159
+
160
+ .quota {
161
+ display: grid;
162
+ grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
163
+ gap: 8px;
164
+ margin-top: 12px;
165
+ }
166
+
167
+ .model {
168
+ border: 1px solid #eef0f3;
169
+ border-radius: 6px;
170
+ padding: 9px;
171
+ }
172
+
173
+ .bar {
174
+ overflow: hidden;
175
+ height: 6px;
176
+ margin-top: 7px;
177
+ border-radius: 999px;
178
+ background: #eef0f3;
179
+ }
180
+
181
+ .fill {
182
+ height: 100%;
183
+ background: #2563eb;
184
+ }
185
+
186
+ .err {
187
+ color: #b91c1c;
188
+ }
189
+
190
+ .status {
191
+ min-height: 20px;
192
+ margin-top: 10px;
193
+ }
194
+
195
+ .footer {
196
+ margin-top: 22px;
197
+ padding-top: 16px;
198
+ border-top: 1px solid #e5e7eb;
199
+ text-align: right;
200
+ }
201
+
202
+ .footer a {
203
+ color: #374151;
204
+ text-decoration: none;
205
+ }
206
+
207
+ .footer a:hover {
208
+ text-decoration: underline;
209
+ }
210
+
211
+ .small {
212
+ font-size: 12px;
213
+ }