@cortexkit/opencode-openai-auth 0.3.2 → 0.3.4

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 (41) hide show
  1. package/dist/cli.js +1 -1
  2. package/dist/commands.d.ts +3 -1
  3. package/dist/core/accounts.d.ts +6 -0
  4. package/dist/core/cachekeep.d.ts +16 -0
  5. package/dist/core/oauth.d.ts +1 -1
  6. package/dist/core/quota-manager.d.ts +33 -1
  7. package/dist/{index-gwmhhrrc.js → index-1xmasv0x.js} +1 -1
  8. package/dist/{index-899gpxdd.js → index-5s67238f.js} +1 -1
  9. package/dist/{index-53j7bdev.js → index-e7m7qbne.js} +1 -1
  10. package/dist/{index-7w467pz8.js → index-k5zwf3j1.js} +1 -1
  11. package/dist/{index-qp2nja20.js → index-mrg2fms9.js} +2 -2
  12. package/dist/{index-jn8xd8fy.js → index-qdh7pt5a.js} +1 -1
  13. package/dist/index-y8d01tgn.js +92 -0
  14. package/dist/index.js +68 -62
  15. package/dist/{quota-normalize-dxftsgep.js → quota-normalize-dfdpwwm3.js} +1 -1
  16. package/dist/rpc/port-file.js +1 -1
  17. package/dist/rpc/rpc-client.js +1 -1
  18. package/dist/rpc/rpc-dir.js +1 -1
  19. package/dist/sidebar-state.js +1 -1
  20. package/dist/{token-28n5g707.js → token-01dcf92a.js} +1 -1
  21. package/dist/token-util-rkqd82q9.js +1 -0
  22. package/dist/tui-preferences.js +1 -1
  23. package/dist/version.d.ts +1 -1
  24. package/dist/ws-pool.d.ts +11 -0
  25. package/dist/ws.d.ts +1 -1
  26. package/package.json +6 -4
  27. package/src/tui/command-dialogs.tsx +77 -0
  28. package/src/tui/entry.mjs +37 -0
  29. package/src/tui-compiled/logger.ts +161 -0
  30. package/src/tui-compiled/rpc/port-file.ts +70 -0
  31. package/src/tui-compiled/rpc/protocol.ts +31 -0
  32. package/src/tui-compiled/rpc/rpc-client.ts +78 -0
  33. package/src/tui-compiled/rpc/rpc-dir.ts +22 -0
  34. package/src/tui-compiled/sidebar-state.ts +316 -0
  35. package/src/tui-compiled/tui/command-dialogs.tsx +640 -0
  36. package/src/tui-compiled/tui-preferences.ts +328 -0
  37. package/src/tui-compiled/tui.tsx +1041 -0
  38. package/src/tui-compiled/util/open-url.ts +30 -0
  39. package/src/tui.tsx +16 -13
  40. package/dist/index-z21wp7ac.js +0 -92
  41. package/dist/token-util-gmr2b13z.js +0 -1
@@ -0,0 +1,1041 @@
1
+ import { memo as _$memo } from "opentui:runtime-module:%40opentui%2Fsolid";
2
+ import { createTextNode as _$createTextNode } from "opentui:runtime-module:%40opentui%2Fsolid";
3
+ import { createComponent as _$createComponent } from "opentui:runtime-module:%40opentui%2Fsolid";
4
+ import { effect as _$effect } from "opentui:runtime-module:%40opentui%2Fsolid";
5
+ import { insertNode as _$insertNode } from "opentui:runtime-module:%40opentui%2Fsolid";
6
+ import { insert as _$insert } from "opentui:runtime-module:%40opentui%2Fsolid";
7
+ import { setProp as _$setProp } from "opentui:runtime-module:%40opentui%2Fsolid";
8
+ import { createElement as _$createElement } from "opentui:runtime-module:%40opentui%2Fsolid";
9
+ /** @jsxImportSource @opentui/solid */
10
+
11
+ import { readFileSync } from "node:fs";
12
+ import { dirname, join } from "node:path";
13
+ import { fileURLToPath } from "node:url";
14
+ import { createEffect, createSignal, For, onCleanup, Show } from "opentui:runtime-module:solid-js";
15
+ import { createLogger } from "./logger.js";
16
+ import { createRpcClient } from "./rpc/rpc-client.js";
17
+ import { getRpcDir } from "./rpc/rpc-dir.js";
18
+ import { computeQuotaPacing, DEFAULT_SIDEBAR_STATE, FIVE_HOUR_MS, getCollapsedQuotaSummary, getSidebarState, resolveActiveAccount, SEVEN_DAY_MS } from "./sidebar-state.js";
19
+ import { openCommandDialog } from "./tui/command-dialogs.js";
20
+ import { computeEffectiveOrder, DEFAULT_SLOT_ORDER, PLUGIN_KEY, queueTuiPreferenceUpdate, readTuiPreferencesFile, resolveOpenaiAuthPrefs, watchTuiPreferences } from "./tui-preferences.js";
21
+ const RPC_POLL_MS = 500;
22
+ let rpcPollStarted = false;
23
+ const log = createLogger('rpc-tui');
24
+ const ID = 'cortexkit.openai-auth';
25
+
26
+ // Read package metadata from either the raw src/ entry or its generated
27
+ // src/tui-compiled/ counterpart. Avoid a JSON import because package.json sits
28
+ // outside the declaration build's rootDir.
29
+ const PLUGIN_VERSION = (() => {
30
+ const here = dirname(fileURLToPath(import.meta.url));
31
+ for (const packageFile of [join(here, '..', 'package.json'), join(here, '..', '..', 'package.json')]) {
32
+ try {
33
+ const raw = readFileSync(packageFile, 'utf8');
34
+ const version = JSON.parse(raw).version;
35
+ if (version) return version;
36
+ } catch {
37
+ // Try the path for the other TUI entry layout.
38
+ }
39
+ }
40
+ return '';
41
+ })();
42
+
43
+ // biome-ignore lint/suspicious/noExplicitAny: opentui border prop not typed in plugin tui surface
44
+ const SINGLE_BORDER = {
45
+ type: 'single'
46
+ };
47
+ // Tone -> theme token. Theme tokens only — never hardcoded colors. Fallbacks
48
+ // resolve to other theme tokens so the sidebar always tracks the active theme.
49
+ function toneColor(theme, tone) {
50
+ switch (tone) {
51
+ case 'ok':
52
+ return theme.success ?? theme.accent;
53
+ case 'warn':
54
+ return theme.warning ?? theme.accent;
55
+ case 'err':
56
+ return theme.error ?? theme.accent;
57
+ case 'muted':
58
+ return theme.textMuted ?? theme.text;
59
+ case 'accent':
60
+ return theme.accent ?? theme.text;
61
+ default:
62
+ return theme.text;
63
+ }
64
+ }
65
+ function usageTone(usedPct, appearance) {
66
+ if (usedPct < appearance.warnThreshold) return 'ok';
67
+ if (usedPct < appearance.errorThreshold) return 'warn';
68
+ return 'err';
69
+ }
70
+ const PACE_RESERVE_CHAR = '\u2592';
71
+ const PACE_DEFICIT_CHAR = '\u2593';
72
+
73
+ // Variant C pacing bar: normal fill up to min(used, pace), then a pace
74
+ // segment covering the gap — headroom being banked (reserve, ok tone) or the
75
+ // overshoot itself (deficit, err tone) — then empty cells. Without pacing the
76
+ // bar renders as a single fill+empty pair, identical to the pre-pacing look.
77
+ // Empty text nodes still occupy a phantom cell in the opentui flex row, so
78
+ // zero-length segments are dropped from every return path (plain and pacing).
79
+ function quotaBarSegments(usedPct, appearance, pacing) {
80
+ const width = appearance.barWidth;
81
+ const cells = pct => Math.max(0, Math.min(Math.round(pct / 100 * width), width));
82
+ const usedCells = cells(usedPct);
83
+ const fillTone = usageTone(usedPct, appearance);
84
+ const plain = [{
85
+ text: appearance.barFilledChar.repeat(usedCells),
86
+ tone: fillTone
87
+ }, {
88
+ text: appearance.barEmptyChar.repeat(width - usedCells),
89
+ tone: fillTone
90
+ }];
91
+ if (!pacing) return plain.filter(segment => segment.text.length > 0);
92
+ const paceCells = cells(pacing.pacePercent);
93
+ const lo = Math.min(usedCells, paceCells);
94
+ const hi = Math.max(usedCells, paceCells);
95
+ if (hi === lo) return plain.filter(segment => segment.text.length > 0);
96
+ const overspent = usedCells > paceCells;
97
+ return [{
98
+ text: appearance.barFilledChar.repeat(lo),
99
+ tone: fillTone
100
+ }, {
101
+ text: (overspent ? PACE_DEFICIT_CHAR : PACE_RESERVE_CHAR).repeat(hi - lo),
102
+ tone: overspent ? 'err' : 'ok'
103
+ }, {
104
+ text: appearance.barEmptyChar.repeat(width - hi),
105
+ tone: fillTone
106
+ }].filter(segment => segment.text.length > 0);
107
+ }
108
+ export function formatResetIn(resetsAt) {
109
+ if (!resetsAt) return '';
110
+ const ms = new Date(resetsAt).getTime() - Date.now();
111
+ if (Number.isNaN(ms)) return '';
112
+ if (ms <= 0) return 'now';
113
+ const mins = Math.floor(ms / 60_000);
114
+ if (mins < 60) return `${mins}m`;
115
+ const hrs = Math.floor(mins / 60);
116
+ if (hrs < 24) {
117
+ const rm = mins % 60;
118
+ return rm > 0 ? `${hrs}h${rm}m` : `${hrs}h`;
119
+ }
120
+ const days = Math.floor(hrs / 24);
121
+ const rh = hrs % 24;
122
+ return rh > 0 ? `${days}d${rh}h` : `${days}d`;
123
+ }
124
+ function formatUntil(until) {
125
+ if (!until) return '';
126
+ const ms = until - Date.now();
127
+ if (ms <= 0) return 'now';
128
+ const mins = Math.ceil(ms / 60_000);
129
+ if (mins < 60) return `${mins}m`;
130
+ const hrs = Math.floor(mins / 60);
131
+ const rm = mins % 60;
132
+ return rm > 0 ? `${hrs}h${rm}m` : `${hrs}h`;
133
+ }
134
+
135
+ // --- Reusable components (aft-style) ---------------------------------------
136
+
137
+ function SectionHeader(props) {
138
+ return (() => {
139
+ var _el$ = _$createElement("box"),
140
+ _el$2 = _$createElement("text"),
141
+ _el$3 = _$createElement("b");
142
+ _$insertNode(_el$, _el$2);
143
+ _$setProp(_el$, "width", '100%');
144
+ _$insertNode(_el$2, _el$3);
145
+ _$insert(_el$3, () => props.title);
146
+ _$effect(_p$ => {
147
+ var _v$ = props.marginTop ?? 1,
148
+ _v$2 = props.theme.text;
149
+ _v$ !== _p$.e && (_p$.e = _$setProp(_el$, "marginTop", _v$, _p$.e));
150
+ _v$2 !== _p$.t && (_p$.t = _$setProp(_el$2, "fg", _v$2, _p$.t));
151
+ return _p$;
152
+ }, {
153
+ e: undefined,
154
+ t: undefined
155
+ });
156
+ return _el$;
157
+ })();
158
+ }
159
+ function StatRow(props) {
160
+ return (() => {
161
+ var _el$4 = _$createElement("box"),
162
+ _el$5 = _$createElement("text"),
163
+ _el$6 = _$createElement("text"),
164
+ _el$7 = _$createElement("b");
165
+ _$insertNode(_el$4, _el$5);
166
+ _$insertNode(_el$4, _el$6);
167
+ _$setProp(_el$4, "width", '100%');
168
+ _$setProp(_el$4, "flexDirection", 'row');
169
+ _$setProp(_el$4, "justifyContent", 'space-between');
170
+ _$insert(_el$5, () => props.label);
171
+ _$insertNode(_el$6, _el$7);
172
+ _$insert(_el$7, () => props.value);
173
+ _$effect(_p$ => {
174
+ var _v$3 = props.theme.textMuted,
175
+ _v$4 = toneColor(props.theme, props.tone ?? 'text');
176
+ _v$3 !== _p$.e && (_p$.e = _$setProp(_el$5, "fg", _v$3, _p$.e));
177
+ _v$4 !== _p$.t && (_p$.t = _$setProp(_el$6, "fg", _v$4, _p$.t));
178
+ return _p$;
179
+ }, {
180
+ e: undefined,
181
+ t: undefined
182
+ });
183
+ return _el$4;
184
+ })();
185
+ }
186
+
187
+ // Compact row for the collapsed view: muted label left, caller-provided value
188
+ // right. Mirrors StatRow's layout so columns line up.
189
+ function CollapsedRow(props) {
190
+ return (() => {
191
+ var _el$8 = _$createElement("box"),
192
+ _el$9 = _$createElement("text");
193
+ _$insertNode(_el$8, _el$9);
194
+ _$setProp(_el$8, "width", '100%');
195
+ _$setProp(_el$8, "flexDirection", 'row');
196
+ _$setProp(_el$8, "justifyContent", 'space-between');
197
+ _$insert(_el$9, () => props.label);
198
+ _$insert(_el$8, () => props.children, null);
199
+ _$effect(_$p => _$setProp(_el$9, "fg", props.theme.textMuted, _$p));
200
+ return _el$8;
201
+ })();
202
+ }
203
+
204
+ // Quota window row: muted label left, tone-colored bar + percentage right,
205
+ // with an optional muted reset suffix. When pacing data is present, the bar
206
+ // gains a pace segment and an off-pace window adds a muted subline with the
207
+ // reserve/deficit delta and the projected runout.
208
+ function QuotaRow(props) {
209
+ const used = () => props.window?.usedPercent ?? 0;
210
+ const reset = () => formatResetIn(props.window?.resetsAt);
211
+ const paceLine = () => {
212
+ const pacing = props.pacing;
213
+ if (!pacing || pacing.state === 'on-pace') return null;
214
+ const pct = Math.round(Math.abs(pacing.deltaPercent));
215
+ if (pacing.state === 'reserve') return `reserve ${pct}% \u00b7 lasts`;
216
+ return pacing.runsOutAt ? `deficit ${pct}% \u00b7 out in ${formatResetIn(pacing.runsOutAt)}` : `deficit ${pct}% \u00b7 lasts`;
217
+ };
218
+ return _$createComponent(Show, {
219
+ get when() {
220
+ return props.window;
221
+ },
222
+ get fallback() {
223
+ return (() => {
224
+ var _el$17 = _$createElement("box"),
225
+ _el$18 = _$createElement("text"),
226
+ _el$19 = _$createElement("text");
227
+ _$insertNode(_el$17, _el$18);
228
+ _$insertNode(_el$17, _el$19);
229
+ _$setProp(_el$17, "width", '100%');
230
+ _$setProp(_el$17, "flexDirection", 'row');
231
+ _$insert(_el$18, () => props.label.padEnd(3));
232
+ _$insertNode(_el$19, _$createTextNode(`—`));
233
+ _$effect(_p$ => {
234
+ var _v$9 = props.theme.textMuted,
235
+ _v$0 = props.theme.textMuted;
236
+ _v$9 !== _p$.e && (_p$.e = _$setProp(_el$18, "fg", _v$9, _p$.e));
237
+ _v$0 !== _p$.t && (_p$.t = _$setProp(_el$19, "fg", _v$0, _p$.t));
238
+ return _p$;
239
+ }, {
240
+ e: undefined,
241
+ t: undefined
242
+ });
243
+ return _el$17;
244
+ })();
245
+ },
246
+ get children() {
247
+ return [(() => {
248
+ var _el$0 = _$createElement("box"),
249
+ _el$1 = _$createElement("box"),
250
+ _el$10 = _$createElement("text"),
251
+ _el$11 = _$createElement("text");
252
+ _$insertNode(_el$0, _el$1);
253
+ _$setProp(_el$0, "width", '100%');
254
+ _$setProp(_el$0, "flexDirection", 'row');
255
+ _$setProp(_el$0, "justifyContent", 'space-between');
256
+ _$insertNode(_el$1, _el$10);
257
+ _$insertNode(_el$1, _el$11);
258
+ _$setProp(_el$1, "flexDirection", 'row');
259
+ _$insert(_el$10, () => props.label.padEnd(3));
260
+ _$insert(_el$1, _$createComponent(For, {
261
+ get each() {
262
+ return quotaBarSegments(used(), props.appearance, props.pacing);
263
+ },
264
+ children: segment => (() => {
265
+ var _el$21 = _$createElement("text");
266
+ _$insert(_el$21, () => segment.text);
267
+ _$effect(_$p => _$setProp(_el$21, "fg", toneColor(props.theme, segment.tone), _$p));
268
+ return _el$21;
269
+ })()
270
+ }), _el$11);
271
+ _$insert(_el$11, () => ` ${String(Math.round(used())).padStart(3)}%`);
272
+ _$insert(_el$0, _$createComponent(Show, {
273
+ get when() {
274
+ return reset();
275
+ },
276
+ get children() {
277
+ var _el$12 = _$createElement("text");
278
+ _$insert(_el$12, reset);
279
+ _$effect(_$p => _$setProp(_el$12, "fg", props.theme.textMuted, _$p));
280
+ return _el$12;
281
+ }
282
+ }), null);
283
+ _$effect(_p$ => {
284
+ var _v$5 = props.theme.textMuted,
285
+ _v$6 = toneColor(props.theme, usageTone(used(), props.appearance));
286
+ _v$5 !== _p$.e && (_p$.e = _$setProp(_el$10, "fg", _v$5, _p$.e));
287
+ _v$6 !== _p$.t && (_p$.t = _$setProp(_el$11, "fg", _v$6, _p$.t));
288
+ return _p$;
289
+ }, {
290
+ e: undefined,
291
+ t: undefined
292
+ });
293
+ return _el$0;
294
+ })(), _$createComponent(Show, {
295
+ get when() {
296
+ return paceLine();
297
+ },
298
+ get children() {
299
+ var _el$13 = _$createElement("box"),
300
+ _el$14 = _$createElement("text"),
301
+ _el$16 = _$createElement("text");
302
+ _$insertNode(_el$13, _el$14);
303
+ _$insertNode(_el$13, _el$16);
304
+ _$setProp(_el$13, "width", '100%');
305
+ _$setProp(_el$13, "flexDirection", 'row');
306
+ _$insertNode(_el$14, _$createTextNode(` `));
307
+ _$insert(_el$16, paceLine);
308
+ _$effect(_p$ => {
309
+ var _v$7 = props.theme.textMuted,
310
+ _v$8 = toneColor(props.theme, props.pacing?.state === 'deficit' ? 'warn' : 'muted');
311
+ _v$7 !== _p$.e && (_p$.e = _$setProp(_el$14, "fg", _v$7, _p$.e));
312
+ _v$8 !== _p$.t && (_p$.t = _$setProp(_el$16, "fg", _v$8, _p$.t));
313
+ return _p$;
314
+ }, {
315
+ e: undefined,
316
+ t: undefined
317
+ });
318
+ return _el$13;
319
+ }
320
+ })];
321
+ }
322
+ });
323
+ }
324
+
325
+ // Account block: header row (name + status word) then per-window quota rows.
326
+ // Pacing is computed here — per window, against the wall clock at render time
327
+ // (re-evaluated on every state poll) — and disabled by passing false.
328
+ function AccountBlock(props) {
329
+ const statusWord = () => props.killed ? 'blocked' : props.active ? 'active' : 'idle';
330
+ const statusTone = () => props.killed ? 'err' : props.active ? 'ok' : 'muted';
331
+ const pacingFor = (window, windowMs) => props.pacingEnabled && window ? computeQuotaPacing(window, windowMs, Date.now()) : null;
332
+ return (() => {
333
+ var _el$22 = _$createElement("box"),
334
+ _el$23 = _$createElement("box"),
335
+ _el$24 = _$createElement("text"),
336
+ _el$25 = _$createElement("b"),
337
+ _el$26 = _$createElement("text"),
338
+ _el$27 = _$createElement("b");
339
+ _$insertNode(_el$22, _el$23);
340
+ _$setProp(_el$22, "width", '100%');
341
+ _$setProp(_el$22, "flexDirection", 'column');
342
+ _$insertNode(_el$23, _el$24);
343
+ _$insertNode(_el$23, _el$26);
344
+ _$setProp(_el$23, "width", '100%');
345
+ _$setProp(_el$23, "flexDirection", 'row');
346
+ _$setProp(_el$23, "justifyContent", 'space-between');
347
+ _$insertNode(_el$24, _el$25);
348
+ _$insert(_el$25, () => props.name);
349
+ _$insertNode(_el$26, _el$27);
350
+ _$insert(_el$27, statusWord);
351
+ _$insert(_el$22, _$createComponent(Show, {
352
+ get when() {
353
+ return props.quota;
354
+ },
355
+ get fallback() {
356
+ return (() => {
357
+ var _el$28 = _$createElement("text");
358
+ _$insertNode(_el$28, _$createTextNode(`checking…`));
359
+ _$effect(_$p => _$setProp(_el$28, "fg", props.theme.textMuted, _$p));
360
+ return _el$28;
361
+ })();
362
+ },
363
+ get children() {
364
+ return [_$createComponent(QuotaRow, {
365
+ get theme() {
366
+ return props.theme;
367
+ },
368
+ get appearance() {
369
+ return props.appearance;
370
+ },
371
+ label: "5h",
372
+ get window() {
373
+ return props.quota?.primary;
374
+ },
375
+ get pacing() {
376
+ return pacingFor(props.quota?.primary, FIVE_HOUR_MS);
377
+ }
378
+ }), _$createComponent(QuotaRow, {
379
+ get theme() {
380
+ return props.theme;
381
+ },
382
+ get appearance() {
383
+ return props.appearance;
384
+ },
385
+ label: "7d",
386
+ get window() {
387
+ return props.quota?.secondary;
388
+ },
389
+ get pacing() {
390
+ return pacingFor(props.quota?.secondary, SEVEN_DAY_MS);
391
+ }
392
+ })];
393
+ }
394
+ }), null);
395
+ _$effect(_p$ => {
396
+ var _v$1 = props.marginTop ?? 0,
397
+ _v$10 = props.theme.text,
398
+ _v$11 = toneColor(props.theme, statusTone());
399
+ _v$1 !== _p$.e && (_p$.e = _$setProp(_el$22, "marginTop", _v$1, _p$.e));
400
+ _v$10 !== _p$.t && (_p$.t = _$setProp(_el$24, "fg", _v$10, _p$.t));
401
+ _v$11 !== _p$.a && (_p$.a = _$setProp(_el$26, "fg", _v$11, _p$.a));
402
+ return _p$;
403
+ }, {
404
+ e: undefined,
405
+ t: undefined,
406
+ a: undefined
407
+ });
408
+ return _el$22;
409
+ })();
410
+ }
411
+
412
+ // --- Quota dialog content ---------------------------------------------------
413
+
414
+ // Renders the same rich visualization as the sidebar (account blocks with
415
+ // quota bars + pacing) so the modal matches the sidebar look.
416
+ function QuotaDialogContent(props) {
417
+ const prefs = props.controller.prefs;
418
+ const [state, setState] = createSignal(DEFAULT_SIDEBAR_STATE);
419
+ let lastUpdated = 0;
420
+ async function refresh() {
421
+ const next = await readStateFromFile();
422
+ if (next.lastUpdated !== lastUpdated) {
423
+ lastUpdated = next.lastUpdated;
424
+ setState(next);
425
+ }
426
+ }
427
+ createEffect(() => {
428
+ const timer = setInterval(refresh, prefs().pollMs);
429
+ onCleanup(() => clearInterval(timer));
430
+ });
431
+ setTimeout(refresh, 0);
432
+ const theme = () => props.api.theme.current;
433
+ const enabledFallbacks = () => (state().fallbacks ?? []).filter(f => f.enabled);
434
+ return (() => {
435
+ var _el$30 = _$createElement("box"),
436
+ _el$31 = _$createElement("box"),
437
+ _el$32 = _$createElement("box"),
438
+ _el$33 = _$createElement("text"),
439
+ _el$34 = _$createElement("b");
440
+ _$insertNode(_el$30, _el$31);
441
+ _$setProp(_el$30, "flexDirection", 'column');
442
+ _$setProp(_el$30, "padding", 2);
443
+ _$setProp(_el$30, "width", '100%');
444
+ _$setProp(_el$30, "alignItems", 'center');
445
+ _$insertNode(_el$31, _el$32);
446
+ _$setProp(_el$31, "flexDirection", 'column');
447
+ _$setProp(_el$31, "width", 58);
448
+ _$insertNode(_el$32, _el$33);
449
+ _$setProp(_el$32, "width", '100%');
450
+ _$setProp(_el$32, "justifyContent", 'center');
451
+ _$setProp(_el$32, "marginBottom", 1);
452
+ _$insertNode(_el$33, _el$34);
453
+ _$insertNode(_el$34, _$createTextNode(`Codex Quota`));
454
+ _$insert(_el$31, _$createComponent(AccountBlock, {
455
+ get theme() {
456
+ return theme();
457
+ },
458
+ get appearance() {
459
+ return prefs().appearance;
460
+ },
461
+ name: "main",
462
+ get quota() {
463
+ return state().main?.quota ?? null;
464
+ },
465
+ get killed() {
466
+ return state().main?.killed ?? false;
467
+ },
468
+ get active() {
469
+ return state().activeId === 'main';
470
+ },
471
+ get pacingEnabled() {
472
+ return prefs().sections.pacing;
473
+ }
474
+ }), null);
475
+ _$insert(_el$31, _$createComponent(Show, {
476
+ get when() {
477
+ return prefs().sections.fallbackAccounts;
478
+ },
479
+ get children() {
480
+ return _$createComponent(For, {
481
+ get each() {
482
+ return enabledFallbacks();
483
+ },
484
+ children: fb => _$createComponent(AccountBlock, {
485
+ get theme() {
486
+ return theme();
487
+ },
488
+ get appearance() {
489
+ return prefs().appearance;
490
+ },
491
+ get name() {
492
+ return fb.label ?? fb.id;
493
+ },
494
+ get quota() {
495
+ return fb.quota;
496
+ },
497
+ get killed() {
498
+ return fb.killed;
499
+ },
500
+ get active() {
501
+ return state().activeId === fb.id;
502
+ },
503
+ get pacingEnabled() {
504
+ return prefs().sections.pacing;
505
+ },
506
+ marginTop: 1
507
+ })
508
+ });
509
+ }
510
+ }), null);
511
+ _$effect(_$p => _$setProp(_el$33, "fg", theme().text, _$p));
512
+ return _el$30;
513
+ })();
514
+ }
515
+
516
+ // --- State plumbing ---------------------------------------------------------
517
+
518
+ export async function readStateFromFile() {
519
+ return getSidebarState();
520
+ }
521
+ // The TUI may unmount and remount sidebar_content when the user switches
522
+ // views (e.g. main -> subagent -> main). A remount re-runs the component
523
+ // body, so any signal created inside the component would reset to its
524
+ // seed. The controller lives in the plugin closure (process lifetime)
525
+ // and owns the durable prefs/collapse signals plus the single shared
526
+ // watcher subscription, so collapse and live pref reloads survive the
527
+ // remount. No effects or memos here — those need a Solid owner, so the
528
+ // poll-interval createEffect stays inside the component.
529
+ function createSidebarController(initialPrefs) {
530
+ const [prefs, setPrefs] = createSignal(initialPrefs);
531
+ const seedCollapsed = initialPrefs.rememberCollapsed && initialPrefs.collapsed != null ? initialPrefs.collapsed : initialPrefs.startCollapsed;
532
+ const [collapsed, setCollapsed] = createSignal(seedCollapsed);
533
+ let lastPersistedCollapsed = initialPrefs.collapsed;
534
+ let lastApplied = JSON.stringify(initialPrefs);
535
+
536
+ // The watcher lives for the plugin/process lifetime — it is intentionally
537
+ // never disposed. Collapse guard mirrors the race-fix in toggleCollapsed:
538
+ // lastPersistedCollapsed is advanced only once our own write lands, so
539
+ // watcher echoes of the previous persisted value are rejected by the
540
+ // `!==` check and cannot revert a user click.
541
+ watchTuiPreferences(() => {
542
+ void (async () => {
543
+ const next = resolveOpenaiAuthPrefs(await readTuiPreferencesFile());
544
+ const serialized = JSON.stringify(next);
545
+ if (serialized === lastApplied) return;
546
+ lastApplied = serialized;
547
+ setPrefs(next);
548
+ if (next.rememberCollapsed && next.collapsed != null && next.collapsed !== lastPersistedCollapsed) {
549
+ lastPersistedCollapsed = next.collapsed;
550
+ setCollapsed(next.collapsed);
551
+ }
552
+ })();
553
+ });
554
+ function toggleCollapsed() {
555
+ const next = !collapsed();
556
+ setCollapsed(next);
557
+ if (prefs().rememberCollapsed) {
558
+ void queueTuiPreferenceUpdate(PLUGIN_KEY, ['collapsed'], next).then(() => {
559
+ lastPersistedCollapsed = next;
560
+ });
561
+ }
562
+ }
563
+ return {
564
+ prefs,
565
+ collapsed,
566
+ toggleCollapsed
567
+ };
568
+ }
569
+ function QuotaSidebar(props) {
570
+ const prefs = props.controller.prefs;
571
+ const collapsed = props.controller.collapsed;
572
+ const [state, setState] = createSignal(DEFAULT_SIDEBAR_STATE);
573
+ let lastUpdated = 0;
574
+ let debounce = null;
575
+ async function refresh() {
576
+ const next = await readStateFromFile();
577
+ if (next.lastUpdated !== lastUpdated) {
578
+ lastUpdated = next.lastUpdated;
579
+ setState(next);
580
+ }
581
+ }
582
+ function scheduleRefresh() {
583
+ if (debounce) clearTimeout(debounce);
584
+ debounce = setTimeout(() => {
585
+ debounce = null;
586
+ void refresh();
587
+ }, prefs().refreshDebounceMs);
588
+ }
589
+
590
+ // Background poller (server and TUI run as separate module instances, so we
591
+ // sync through the state file) plus event-driven refreshes for low latency.
592
+ // The interval rebuilds whenever pollMs changes.
593
+ createEffect(() => {
594
+ const timer = setInterval(refresh, prefs().pollMs);
595
+ onCleanup(() => clearInterval(timer));
596
+ });
597
+ const unsubs = [props.api.event.on('session.updated', scheduleRefresh), props.api.event.on('message.updated', scheduleRefresh)];
598
+ setTimeout(refresh, 300);
599
+ onCleanup(() => {
600
+ if (debounce) clearTimeout(debounce);
601
+ for (const u of unsubs) u();
602
+ });
603
+ const theme = () => props.api.theme.current;
604
+ const enabledFallbacks = () => (state().fallbacks ?? []).filter(f => f.enabled);
605
+ const hasData = () => state().main?.quota != null || enabledFallbacks().length > 0;
606
+ const headerLabel = () => {
607
+ const name = prefs().header.label;
608
+ return !hasData() ? name : collapsed() ? `\u25b6 ${name}` : `\u25bc ${name}`;
609
+ };
610
+ const activeAccount = () => resolveActiveAccount(state());
611
+ const activeQuotaSummary = () => getCollapsedQuotaSummary(activeAccount().quota);
612
+ const activePacingDeficit = () => {
613
+ if (!prefs().sections.pacing) return false;
614
+ const quota = activeAccount().quota;
615
+ if (!quota) return false;
616
+ const now = Date.now();
617
+ const windows = [[quota.primary, FIVE_HOUR_MS], [quota.secondary, SEVEN_DAY_MS]];
618
+ return windows.some(([w, ms]) => w != null && computeQuotaPacing(w, ms, now)?.state === 'deficit');
619
+ };
620
+ const activeQuotaTone = () => {
621
+ const summary = activeQuotaSummary();
622
+ const values = [summary.primaryUsedPercent, summary.secondaryUsedPercent].filter(value => value != null);
623
+ // Pacing deficit is an advisory projection, not actual quota exhaustion,
624
+ // so it can only BUMP the usage tone up to warn at most — never soften a
625
+ // real warn/err usage reading and never paint a true red.
626
+ const base = values.length > 0 ? usageTone(Math.max(...values), prefs().appearance) : 'muted';
627
+ if (!activePacingDeficit()) return base;
628
+ return base === 'ok' || base === 'muted' ? 'warn' : base;
629
+ };
630
+ const killedNames = () => [state().main?.killed ? 'main' : '', ...enabledFallbacks().filter(f => f.killed).map(f => f.label ?? f.id)].filter(Boolean);
631
+ const quotaBackedOff = () => state().main?.quotaBackedOff === true;
632
+ const refreshBackedOff = () => state().main?.refreshBackedOff === true;
633
+ const degraded = () => killedNames().length > 0 || quotaBackedOff() || refreshBackedOff();
634
+ return (() => {
635
+ var _el$36 = _$createElement("box"),
636
+ _el$37 = _$createElement("box"),
637
+ _el$38 = _$createElement("box"),
638
+ _el$39 = _$createElement("text"),
639
+ _el$40 = _$createElement("b");
640
+ _$insertNode(_el$36, _el$37);
641
+ _$setProp(_el$36, "width", '100%');
642
+ _$setProp(_el$36, "flexDirection", 'column');
643
+ _$setProp(_el$36, "border", SINGLE_BORDER);
644
+ _$setProp(_el$36, "paddingTop", 1);
645
+ _$setProp(_el$36, "paddingBottom", 1);
646
+ _$setProp(_el$36, "paddingLeft", 1);
647
+ _$setProp(_el$36, "paddingRight", 1);
648
+ _$insertNode(_el$37, _el$38);
649
+ _$setProp(_el$37, "width", '100%');
650
+ _$setProp(_el$37, "flexDirection", 'row');
651
+ _$setProp(_el$37, "justifyContent", 'space-between');
652
+ _$setProp(_el$37, "alignItems", 'center');
653
+ _$setProp(_el$37, "onMouseDown", () => {
654
+ if (hasData()) props.controller.toggleCollapsed();
655
+ });
656
+ _$insertNode(_el$38, _el$39);
657
+ _$setProp(_el$38, "paddingLeft", 1);
658
+ _$setProp(_el$38, "paddingRight", 1);
659
+ _$insertNode(_el$39, _el$40);
660
+ _$insert(_el$40, headerLabel);
661
+ _$insert(_el$37, _$createComponent(Show, {
662
+ get when() {
663
+ return degraded();
664
+ },
665
+ get fallback() {
666
+ return _$createComponent(Show, {
667
+ get when() {
668
+ return prefs().header.showVersion && PLUGIN_VERSION !== '';
669
+ },
670
+ get children() {
671
+ var _el$49 = _$createElement("text");
672
+ _$insert(_el$49, `v${PLUGIN_VERSION}`);
673
+ _$effect(_$p => _$setProp(_el$49, "fg", theme().textMuted, _$p));
674
+ return _el$49;
675
+ }
676
+ });
677
+ },
678
+ get children() {
679
+ var _el$41 = _$createElement("box"),
680
+ _el$42 = _$createElement("text"),
681
+ _el$43 = _$createElement("b");
682
+ _$insertNode(_el$41, _el$42);
683
+ _$setProp(_el$41, "paddingLeft", 1);
684
+ _$setProp(_el$41, "paddingRight", 1);
685
+ _$insertNode(_el$42, _el$43);
686
+ _$insertNode(_el$43, _$createTextNode(`LIMITED`));
687
+ _$effect(_p$ => {
688
+ var _v$12 = theme().warning,
689
+ _v$13 = theme().background;
690
+ _v$12 !== _p$.e && (_p$.e = _$setProp(_el$41, "backgroundColor", _v$12, _p$.e));
691
+ _v$13 !== _p$.t && (_p$.t = _$setProp(_el$42, "fg", _v$13, _p$.t));
692
+ return _p$;
693
+ }, {
694
+ e: undefined,
695
+ t: undefined
696
+ });
697
+ return _el$41;
698
+ }
699
+ }), null);
700
+ _$insert(_el$36, _$createComponent(Show, {
701
+ get when() {
702
+ return _$memo(() => !!collapsed())() && hasData();
703
+ },
704
+ get children() {
705
+ return _$createComponent(CollapsedRow, {
706
+ get theme() {
707
+ return theme();
708
+ },
709
+ get label() {
710
+ return activeAccount().name;
711
+ },
712
+ get children() {
713
+ return _$createComponent(Show, {
714
+ get when() {
715
+ return activeQuotaSummary().text != null;
716
+ },
717
+ get fallback() {
718
+ return (() => {
719
+ var _el$50 = _$createElement("text");
720
+ _$insertNode(_el$50, _$createTextNode(`—`));
721
+ _$effect(_$p => _$setProp(_el$50, "fg", theme().textMuted, _$p));
722
+ return _el$50;
723
+ })();
724
+ },
725
+ get children() {
726
+ var _el$45 = _$createElement("box"),
727
+ _el$46 = _$createElement("text"),
728
+ _el$47 = _$createElement("b"),
729
+ _el$48 = _$createElement("text");
730
+ _$insertNode(_el$45, _el$46);
731
+ _$insertNode(_el$45, _el$48);
732
+ _$setProp(_el$45, "flexDirection", 'row');
733
+ _$insertNode(_el$46, _el$47);
734
+ _$insert(_el$47, () => activeQuotaSummary().text);
735
+ _$insert(_el$48, () => activeAccount().killed ? ' \u2298' : ' \u25cf');
736
+ _$effect(_p$ => {
737
+ var _v$14 = toneColor(theme(), activeQuotaTone()),
738
+ _v$15 = toneColor(theme(), activeAccount().killed ? 'err' : activeQuotaTone());
739
+ _v$14 !== _p$.e && (_p$.e = _$setProp(_el$46, "fg", _v$14, _p$.e));
740
+ _v$15 !== _p$.t && (_p$.t = _$setProp(_el$48, "fg", _v$15, _p$.t));
741
+ return _p$;
742
+ }, {
743
+ e: undefined,
744
+ t: undefined
745
+ });
746
+ return _el$45;
747
+ }
748
+ });
749
+ }
750
+ });
751
+ }
752
+ }), null);
753
+ _$insert(_el$36, _$createComponent(Show, {
754
+ get when() {
755
+ return !collapsed() || !hasData();
756
+ },
757
+ get children() {
758
+ return [_$createComponent(Show, {
759
+ get when() {
760
+ return hasData();
761
+ },
762
+ get fallback() {
763
+ return (() => {
764
+ var _el$52 = _$createElement("box"),
765
+ _el$53 = _$createElement("text");
766
+ _$insertNode(_el$52, _el$53);
767
+ _$setProp(_el$52, "marginTop", 1);
768
+ _$setProp(_el$52, "width", '100%');
769
+ _$insertNode(_el$53, _$createTextNode(`Waiting for quota…`));
770
+ _$effect(_$p => _$setProp(_el$53, "fg", theme().textMuted, _$p));
771
+ return _el$52;
772
+ })();
773
+ },
774
+ get children() {
775
+ return _$createComponent(Show, {
776
+ get when() {
777
+ return prefs().sections.quota;
778
+ },
779
+ get children() {
780
+ return [_$createComponent(SectionHeader, {
781
+ get theme() {
782
+ return theme();
783
+ },
784
+ title: "Quota"
785
+ }), _$createComponent(AccountBlock, {
786
+ get theme() {
787
+ return theme();
788
+ },
789
+ get appearance() {
790
+ return prefs().appearance;
791
+ },
792
+ name: "main",
793
+ get quota() {
794
+ return state().main?.quota ?? null;
795
+ },
796
+ get killed() {
797
+ return state().main?.killed ?? false;
798
+ },
799
+ get active() {
800
+ return state().activeId === 'main';
801
+ },
802
+ get pacingEnabled() {
803
+ return prefs().sections.pacing;
804
+ }
805
+ }), _$createComponent(Show, {
806
+ get when() {
807
+ return prefs().sections.fallbackAccounts;
808
+ },
809
+ get children() {
810
+ return _$createComponent(For, {
811
+ get each() {
812
+ return enabledFallbacks();
813
+ },
814
+ children: fb => _$createComponent(AccountBlock, {
815
+ get theme() {
816
+ return theme();
817
+ },
818
+ get appearance() {
819
+ return prefs().appearance;
820
+ },
821
+ get name() {
822
+ return fb.label ?? fb.id;
823
+ },
824
+ get quota() {
825
+ return fb.quota;
826
+ },
827
+ get killed() {
828
+ return fb.killed;
829
+ },
830
+ get active() {
831
+ return state().activeId === fb.id;
832
+ },
833
+ get pacingEnabled() {
834
+ return prefs().sections.pacing;
835
+ },
836
+ marginTop: 1
837
+ })
838
+ });
839
+ }
840
+ })];
841
+ }
842
+ });
843
+ }
844
+ }), _$createComponent(Show, {
845
+ get when() {
846
+ return prefs().sections.routing;
847
+ },
848
+ get children() {
849
+ return [_$createComponent(SectionHeader, {
850
+ get theme() {
851
+ return theme();
852
+ },
853
+ title: "Routing"
854
+ }), _$createComponent(StatRow, {
855
+ get theme() {
856
+ return theme();
857
+ },
858
+ label: "Route",
859
+ get value() {
860
+ return state().route;
861
+ },
862
+ tone: "accent"
863
+ })];
864
+ }
865
+ }), _$createComponent(Show, {
866
+ get when() {
867
+ return state().planType || state().credits != null;
868
+ },
869
+ get children() {
870
+ return [_$createComponent(Show, {
871
+ get when() {
872
+ return state().planType;
873
+ },
874
+ get children() {
875
+ return _$createComponent(StatRow, {
876
+ get theme() {
877
+ return theme();
878
+ },
879
+ label: "Plan",
880
+ get value() {
881
+ return state().planType ?? '\u2014';
882
+ },
883
+ tone: "text"
884
+ });
885
+ }
886
+ }), _$createComponent(Show, {
887
+ get when() {
888
+ return state().credits != null;
889
+ },
890
+ get children() {
891
+ return _$createComponent(StatRow, {
892
+ get theme() {
893
+ return theme();
894
+ },
895
+ label: "Credits",
896
+ get value() {
897
+ return String(state().credits);
898
+ },
899
+ tone: "text"
900
+ });
901
+ }
902
+ })];
903
+ }
904
+ }), _$createComponent(Show, {
905
+ get when() {
906
+ return _$memo(() => !!degraded())() && prefs().sections.health;
907
+ },
908
+ get children() {
909
+ return [_$createComponent(SectionHeader, {
910
+ get theme() {
911
+ return theme();
912
+ },
913
+ title: "Health"
914
+ }), _$createComponent(Show, {
915
+ get when() {
916
+ return quotaBackedOff();
917
+ },
918
+ get children() {
919
+ return _$createComponent(StatRow, {
920
+ get theme() {
921
+ return theme();
922
+ },
923
+ label: "Quota API",
924
+ get value() {
925
+ return `backoff ${formatUntil(state().main?.quotaBackoffUntil)}`;
926
+ },
927
+ tone: "warn"
928
+ });
929
+ }
930
+ }), _$createComponent(Show, {
931
+ get when() {
932
+ return refreshBackedOff();
933
+ },
934
+ get children() {
935
+ return _$createComponent(StatRow, {
936
+ get theme() {
937
+ return theme();
938
+ },
939
+ label: "Token refresh",
940
+ get value() {
941
+ return `backoff ${formatUntil(state().main?.refreshBackoffUntil)}`;
942
+ },
943
+ tone: "warn"
944
+ });
945
+ }
946
+ })];
947
+ }
948
+ }), _$createComponent(Show, {
949
+ get when() {
950
+ return killedNames().length > 0;
951
+ },
952
+ get children() {
953
+ return _$createComponent(StatRow, {
954
+ get theme() {
955
+ return theme();
956
+ },
957
+ label: "Killswitch",
958
+ get value() {
959
+ return `${killedNames().join(', ')} blocked`;
960
+ },
961
+ tone: "err"
962
+ });
963
+ }
964
+ })];
965
+ }
966
+ }), null);
967
+ _$effect(_p$ => {
968
+ var _v$16 = theme().borderActive,
969
+ _v$17 = theme().accent,
970
+ _v$18 = theme().background;
971
+ _v$16 !== _p$.e && (_p$.e = _$setProp(_el$36, "borderColor", _v$16, _p$.e));
972
+ _v$17 !== _p$.t && (_p$.t = _$setProp(_el$38, "backgroundColor", _v$17, _p$.t));
973
+ _v$18 !== _p$.a && (_p$.a = _$setProp(_el$39, "fg", _v$18, _p$.a));
974
+ return _p$;
975
+ }, {
976
+ e: undefined,
977
+ t: undefined,
978
+ a: undefined
979
+ });
980
+ return _el$36;
981
+ })();
982
+ }
983
+ const tui = async api => {
984
+ const root = await readTuiPreferencesFile();
985
+ const controller = createSidebarController(resolveOpenaiAuthPrefs(root));
986
+ api.slots.register({
987
+ order: computeEffectiveOrder(root, PLUGIN_KEY, DEFAULT_SLOT_ORDER),
988
+ slots: {
989
+ sidebar_content(_ctx, _props) {
990
+ return _$createComponent(QuotaSidebar, {
991
+ api: api,
992
+ controller: controller
993
+ });
994
+ }
995
+ }
996
+ });
997
+ if (!rpcPollStarted) {
998
+ rpcPollStarted = true;
999
+ const myPid = process.pid;
1000
+ const rpcClient = createRpcClient(getRpcDir(api.state.path.directory ?? ''), myPid, entry => {
1001
+ log.debug('rpc tui pid', {
1002
+ myPid,
1003
+ matchedPortFilePid: entry?.pid ?? null,
1004
+ rpcPort: entry?.port ?? null
1005
+ });
1006
+ });
1007
+ let lastNotificationId = 0;
1008
+ let rpcInFlight = false;
1009
+ setInterval(() => {
1010
+ if (rpcInFlight) return;
1011
+ const current = api.route.current;
1012
+ const resolved = typeof current === 'function' ? current() : current;
1013
+ const sessionId = resolved?.params?.sessionID;
1014
+ rpcInFlight = true;
1015
+ void rpcClient.pending(lastNotificationId, sessionId).then(messages => {
1016
+ for (const message of [...messages].sort((a, b) => a.id - b.id)) {
1017
+ lastNotificationId = Math.max(lastNotificationId, message.id);
1018
+ if (message.payload.command === 'openai-quota') {
1019
+ api.ui.dialog.setSize('xlarge');
1020
+ api.ui.dialog.replace(() => _$createComponent(QuotaDialogContent, {
1021
+ api: api,
1022
+ controller: controller
1023
+ }));
1024
+ continue;
1025
+ }
1026
+ openCommandDialog(api, message.payload, (command, args) => rpcClient.apply({
1027
+ command,
1028
+ arguments: args
1029
+ }));
1030
+ }
1031
+ }).catch(() => {}).finally(() => {
1032
+ rpcInFlight = false;
1033
+ });
1034
+ }, RPC_POLL_MS);
1035
+ }
1036
+ };
1037
+ const plugin = {
1038
+ id: ID,
1039
+ tui
1040
+ };
1041
+ export default plugin;