@cortexkit/opencode-openai-auth 0.3.4 → 0.4.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 (49) hide show
  1. package/README.md +1 -0
  2. package/dist/cli.js +1 -1
  3. package/dist/codex-http.d.ts +3 -0
  4. package/dist/config.d.ts +3 -0
  5. package/dist/core/accounts.d.ts +9 -3
  6. package/dist/core/oauth.d.ts +1 -1
  7. package/dist/core/quota-manager.d.ts +2 -2
  8. package/dist/index-0gccjqxr.js +89 -0
  9. package/dist/{index-k5zwf3j1.js → index-1anzb8jh.js} +1 -1
  10. package/dist/index-1r02s323.js +6 -0
  11. package/dist/index-2ng03cby.js +2 -0
  12. package/dist/{index-e7m7qbne.js → index-6y6ss1kc.js} +1 -1
  13. package/dist/{index-1xmasv0x.js → index-b6kp9ydc.js} +1 -1
  14. package/dist/index-fbfpefx6.js +2 -0
  15. package/dist/index-y67qn95a.js +2 -0
  16. package/dist/index.d.ts +8 -1
  17. package/dist/index.js +65 -65
  18. package/dist/model-costs.d.ts +29 -0
  19. package/dist/prompt-context.d.ts +1 -1
  20. package/dist/quota-normalize-8yzxqy78.js +1 -0
  21. package/dist/quota-normalize.d.ts +13 -4
  22. package/dist/rpc/port-file.js +1 -1
  23. package/dist/rpc/rpc-client.js +1 -1
  24. package/dist/rpc/rpc-dir.js +1 -1
  25. package/dist/sidebar-state.d.ts +47 -2
  26. package/dist/sidebar-state.js +1 -1
  27. package/dist/token-util-h46jb3eg.js +1 -0
  28. package/dist/token-yzcb1p3e.js +1 -0
  29. package/dist/tui/command-dialogs.d.ts +8 -1
  30. package/dist/tui-preferences.js +1 -1
  31. package/dist/tui.d.ts +18 -1
  32. package/dist/version.d.ts +1 -1
  33. package/dist/ws-pool.d.ts +1 -0
  34. package/package.json +6 -5
  35. package/src/core/refresh-file-lock.ts +293 -0
  36. package/src/sidebar-state.ts +393 -13
  37. package/src/tui/command-dialogs.tsx +43 -25
  38. package/src/tui-compiled/sidebar-state.ts +393 -13
  39. package/src/tui-compiled/tui/command-dialogs.tsx +25 -20
  40. package/src/tui-compiled/tui.tsx +228 -171
  41. package/src/tui.tsx +142 -64
  42. package/dist/index-5s67238f.js +0 -2
  43. package/dist/index-d9hq0tmc.js +0 -2
  44. package/dist/index-mrg2fms9.js +0 -2
  45. package/dist/index-qdh7pt5a.js +0 -3
  46. package/dist/index-y8d01tgn.js +0 -92
  47. package/dist/quota-normalize-dfdpwwm3.js +0 -1
  48. package/dist/token-01dcf92a.js +0 -1
  49. package/dist/token-util-rkqd82q9.js +0 -1
@@ -2,11 +2,79 @@ export interface QuotaWindow {
2
2
  usedPercent: number
3
3
  remainingPercent: number
4
4
  resetsAt?: string
5
+ windowMinutes?: number
5
6
  }
6
7
 
7
8
  export interface AccountQuota {
8
9
  primary?: QuotaWindow
9
10
  secondary?: QuotaWindow
11
+ resetCreditsAvailable?: number
12
+ }
13
+
14
+ export type QuotaWindowKey = 'primary' | 'secondary'
15
+
16
+ const QUOTA_WINDOW_KEYS: readonly QuotaWindowKey[] = ['primary', 'secondary']
17
+ const LEGACY_WINDOW_MINUTES: Record<QuotaWindowKey, number> = {
18
+ primary: 300,
19
+ secondary: 10_080,
20
+ }
21
+
22
+ function compactUnit(value: number): string {
23
+ return Number.isInteger(value)
24
+ ? String(value)
25
+ : String(Math.round(value * 10) / 10)
26
+ }
27
+
28
+ // Derives a short human label ("5h", "1d", "7d") from a window length in
29
+ // minutes. Snapshots written before dynamic windows carry no length, so retain
30
+ // their historical primary=5h and secondary=7d meanings.
31
+ export function formatWindowLabel(
32
+ windowMinutes: number | undefined,
33
+ fallbackKey: QuotaWindowKey,
34
+ ): string {
35
+ const minutes =
36
+ windowMinutes !== undefined &&
37
+ Number.isFinite(windowMinutes) &&
38
+ windowMinutes > 0
39
+ ? windowMinutes
40
+ : LEGACY_WINDOW_MINUTES[fallbackKey]
41
+ if (minutes < 60) return `${compactUnit(minutes)}m`
42
+ if (minutes < 1_440) return `${compactUnit(minutes / 60)}h`
43
+ return `${compactUnit(minutes / 1_440)}d`
44
+ }
45
+
46
+ export interface PresentQuotaWindow {
47
+ key: QuotaWindowKey
48
+ label: string
49
+ window: QuotaWindow
50
+ windowMs: number | null
51
+ }
52
+
53
+ // Present windows only — an absent slot means "not applicable", not
54
+ // "unknown", so it must never synthesize a placeholder row here.
55
+ export function getPresentQuotaWindows(
56
+ quota: AccountQuota | null,
57
+ ): PresentQuotaWindow[] {
58
+ if (!quota) return []
59
+ const rows: PresentQuotaWindow[] = []
60
+ for (const key of QUOTA_WINDOW_KEYS) {
61
+ const window = quota[key]
62
+ if (!window) continue
63
+ const configuredMinutes = window.windowMinutes
64
+ const windowMinutes =
65
+ configuredMinutes !== undefined &&
66
+ Number.isFinite(configuredMinutes) &&
67
+ configuredMinutes > 0
68
+ ? configuredMinutes
69
+ : LEGACY_WINDOW_MINUTES[key]
70
+ rows.push({
71
+ key,
72
+ label: formatWindowLabel(windowMinutes, key),
73
+ window,
74
+ windowMs: windowMinutes * 60_000,
75
+ })
76
+ }
77
+ return rows
10
78
  }
11
79
 
12
80
  export interface SidebarAccountState {
@@ -15,8 +83,17 @@ export interface SidebarAccountState {
15
83
  quota: AccountQuota | null
16
84
  killed: boolean
17
85
  enabled: boolean
86
+ resetCredits?: number
18
87
  }
19
88
 
89
+ export interface ActiveRoutingEntry {
90
+ activeId: string
91
+ route: string
92
+ updatedAt: number
93
+ }
94
+
95
+ export type ActiveRoutingMap = Record<string, ActiveRoutingEntry>
96
+
20
97
  export interface SidebarState {
21
98
  main: {
22
99
  quota: AccountQuota | null
@@ -25,10 +102,14 @@ export interface SidebarState {
25
102
  quotaBackoffUntil?: number
26
103
  refreshBackedOff?: boolean
27
104
  refreshBackoffUntil?: number
105
+ resetCredits?: number
28
106
  }
29
107
  fallbacks: SidebarAccountState[]
108
+ /** @deprecated Compatibility field for readers that do not consume activeRouting. */
30
109
  activeId: string | undefined
110
+ /** Machine-global routing mode and compatibility value for older readers. */
31
111
  route: string
112
+ activeRouting?: ActiveRoutingMap
32
113
  planType?: string
33
114
  credits?: number
34
115
  lastUpdated: number
@@ -38,6 +119,7 @@ import { randomUUID } from 'node:crypto'
38
119
  import { mkdir, readFile, rename, rm, writeFile } from 'node:fs/promises'
39
120
  import { tmpdir } from 'node:os'
40
121
  import { dirname, join } from 'node:path'
122
+ import { acquireRefreshFileLock } from './core/refresh-file-lock'
41
123
  import { createLogger } from './logger'
42
124
 
43
125
  const logSb = createLogger('sidebar')
@@ -46,6 +128,50 @@ const STATE_FILE_ENV = 'OPENCODE_OPENAI_AUTH_SIDEBAR_STATE_FILE'
46
128
  const DEFAULT_STATE_DIR = join(tmpdir(), 'opencode-openai-auth')
47
129
  const DEFAULT_STATE_FILE = join(DEFAULT_STATE_DIR, 'sidebar-state.json')
48
130
 
131
+ function normalizeResetCredits(value: unknown): number | undefined {
132
+ return typeof value === 'number' && Number.isFinite(value) && value >= 0
133
+ ? value
134
+ : undefined
135
+ }
136
+
137
+ function resetCreditsField(value: unknown): { resetCredits?: number } {
138
+ const credits = normalizeResetCredits(value)
139
+ return credits !== undefined ? { resetCredits: credits } : {}
140
+ }
141
+
142
+ function normalizeActiveRouting(value: unknown): ActiveRoutingMap | undefined {
143
+ if (value === null || typeof value !== 'object' || Array.isArray(value)) {
144
+ return undefined
145
+ }
146
+
147
+ const normalized: ActiveRoutingMap = {}
148
+ for (const [sessionId, rawEntry] of Object.entries(value)) {
149
+ if (
150
+ rawEntry === null ||
151
+ typeof rawEntry !== 'object' ||
152
+ Array.isArray(rawEntry)
153
+ ) {
154
+ continue
155
+ }
156
+ const entry = rawEntry as Record<string, unknown>
157
+ if (
158
+ typeof entry.activeId !== 'string' ||
159
+ typeof entry.route !== 'string' ||
160
+ typeof entry.updatedAt !== 'number' ||
161
+ !Number.isFinite(entry.updatedAt)
162
+ ) {
163
+ continue
164
+ }
165
+ normalized[sessionId] = {
166
+ activeId: entry.activeId,
167
+ route: entry.route,
168
+ updatedAt: entry.updatedAt,
169
+ }
170
+ }
171
+
172
+ return Object.keys(normalized).length > 0 ? normalized : undefined
173
+ }
174
+
49
175
  export function getSidebarStateFile(): string {
50
176
  return process.env[STATE_FILE_ENV] || DEFAULT_STATE_FILE
51
177
  }
@@ -99,6 +225,7 @@ export function normalizeSidebarState(raw: unknown): SidebarState {
99
225
  ...(typeof m.refreshBackoffUntil === 'number'
100
226
  ? { refreshBackoffUntil: m.refreshBackoffUntil }
101
227
  : {}),
228
+ ...resetCreditsField(m.resetCredits),
102
229
  }
103
230
  } else {
104
231
  main = { quota: null, killed: false }
@@ -123,6 +250,7 @@ export function normalizeSidebarState(raw: unknown): SidebarState {
123
250
  quota: ('quota' in e ? e.quota : null) as AccountQuota | null,
124
251
  killed: typeof e.killed === 'boolean' ? e.killed : false,
125
252
  enabled: typeof e.enabled === 'boolean' ? e.enabled : true,
253
+ ...resetCreditsField(e.resetCredits),
126
254
  }))
127
255
  : []
128
256
 
@@ -139,13 +267,14 @@ export function normalizeSidebarState(raw: unknown): SidebarState {
139
267
  // Optional top-level fields
140
268
  const planType = typeof r.planType === 'string' ? r.planType : undefined
141
269
  const credits = typeof r.credits === 'number' ? r.credits : undefined
142
-
270
+ const activeRouting = normalizeActiveRouting(r.activeRouting)
143
271
  return {
144
272
  main,
145
273
  fallbacks,
146
274
  activeId,
147
275
  route,
148
276
  lastUpdated,
277
+ ...(activeRouting !== undefined ? { activeRouting } : {}),
149
278
  ...(planType !== undefined ? { planType } : {}),
150
279
  ...(credits !== undefined ? { credits } : {}),
151
280
  }
@@ -160,9 +289,90 @@ export async function getSidebarState(): Promise<SidebarState> {
160
289
  }
161
290
  }
162
291
 
292
+ export const ACTIVE_ROUTING_MAX_AGE_MS = 60 * 60 * 1000
293
+ export const ACTIVE_ROUTING_MAX_ENTRIES = 128
294
+
295
+ export type SidebarRoutingAccount = {
296
+ id: string
297
+ enabled?: boolean
298
+ }
299
+
300
+ export function isUsableRoutingEntry(
301
+ entry: ActiveRoutingEntry,
302
+ accounts: readonly SidebarRoutingAccount[],
303
+ now = Date.now(),
304
+ ): boolean {
305
+ const fresh =
306
+ entry.updatedAt >= now - ACTIVE_ROUTING_MAX_AGE_MS && entry.updatedAt <= now
307
+ if (!fresh) return false
308
+ return (
309
+ entry.activeId === 'main' ||
310
+ accounts.some(
311
+ (account) => account.enabled !== false && account.id === entry.activeId,
312
+ )
313
+ )
314
+ }
315
+
316
+ export function resolveSessionSidebarRouting(
317
+ state: SidebarState,
318
+ sessionId?: string,
319
+ now = Date.now(),
320
+ ): { activeId: string; route: string } {
321
+ if (!sessionId) {
322
+ return { activeId: state.activeId ?? 'main', route: state.route }
323
+ }
324
+ const own = sessionId ? state.activeRouting?.[sessionId] : undefined
325
+ if (own && isUsableRoutingEntry(own, state.fallbacks, now)) {
326
+ return { activeId: own.activeId, route: own.route }
327
+ }
328
+
329
+ const fallback = state.fallbacks.find(
330
+ (account) => account.enabled && !account.killed,
331
+ )
332
+ return {
333
+ activeId:
334
+ state.route === 'fallback-first' && fallback ? fallback.id : 'main',
335
+ route: state.route,
336
+ }
337
+ }
338
+
339
+ export function pruneActiveRouting(
340
+ activeRouting: ActiveRoutingMap | undefined,
341
+ accounts: readonly SidebarRoutingAccount[],
342
+ now = Date.now(),
343
+ removedSessionId?: string,
344
+ ): ActiveRoutingMap | undefined {
345
+ if (!activeRouting) return undefined
346
+ const kept = Object.entries(activeRouting).filter(
347
+ ([sessionId, entry]) =>
348
+ sessionId !== removedSessionId &&
349
+ isUsableRoutingEntry(entry, accounts, now),
350
+ )
351
+ const bounded =
352
+ kept.length <= ACTIVE_ROUTING_MAX_ENTRIES
353
+ ? kept
354
+ : kept
355
+ .sort((left, right) => right[1].updatedAt - left[1].updatedAt)
356
+ .slice(0, ACTIVE_ROUTING_MAX_ENTRIES)
357
+ return bounded.length > 0 ? Object.fromEntries(bounded) : undefined
358
+ }
359
+
163
360
  // Serialization chain: concurrent calls are queued so a stale background
164
361
  // write cannot land after a newer one and corrupt the file.
165
362
  let sidebarWriteChain: Promise<void> = Promise.resolve()
363
+ const MAX_MERGE_ATTEMPTS = 3
364
+ const SIDEBAR_WRITE_LOCK_TTL_MS = 10_000
365
+ const SIDEBAR_WRITE_LOCK_WAIT_MS = 15_000
366
+
367
+ interface SidebarMergeHooks {
368
+ beforeRecheck?: () => void | Promise<void>
369
+ }
370
+
371
+ function enqueueSidebarWrite(operation: () => Promise<void>): Promise<void> {
372
+ const result = sidebarWriteChain.then(operation)
373
+ sidebarWriteChain = result.catch(() => {})
374
+ return result
375
+ }
166
376
 
167
377
  /**
168
378
  * Write sidebar state to disk, serialized through a promise chain so
@@ -179,10 +389,177 @@ export function setSidebarState(
179
389
  state: SidebarState,
180
390
  file = getSidebarStateFile(),
181
391
  ): Promise<void> {
182
- sidebarWriteChain = sidebarWriteChain
183
- .then(() => doWriteSidebarState(state, file))
184
- .catch(() => {})
185
- return sidebarWriteChain
392
+ return enqueueSidebarWrite(() => doWriteSidebarState(state, file))
393
+ }
394
+
395
+ async function readSidebarState(file: string): Promise<SidebarState> {
396
+ try {
397
+ return parseSidebarState(await readRawSidebar(file))
398
+ } catch {
399
+ return DEFAULT_SIDEBAR_STATE
400
+ }
401
+ }
402
+
403
+ async function readRawSidebar(file: string): Promise<string> {
404
+ try {
405
+ return await readFile(file, 'utf8')
406
+ } catch (error) {
407
+ if ((error as { code?: string }).code === 'ENOENT') return ''
408
+ throw error
409
+ }
410
+ }
411
+
412
+ function parseSidebarState(raw: string): SidebarState {
413
+ if (raw === '') return DEFAULT_SIDEBAR_STATE
414
+ try {
415
+ return normalizeSidebarState(JSON.parse(raw))
416
+ } catch {
417
+ return DEFAULT_SIDEBAR_STATE
418
+ }
419
+ }
420
+
421
+ async function acquireSidebarWriteLock(file: string) {
422
+ await mkdir(dirname(file), { recursive: true })
423
+ const deadline = Date.now() + SIDEBAR_WRITE_LOCK_WAIT_MS
424
+ while (Date.now() <= deadline) {
425
+ const lock = await acquireRefreshFileLock({
426
+ name: 'sidebar-write',
427
+ path: file,
428
+ ttlMs: SIDEBAR_WRITE_LOCK_TTL_MS,
429
+ renew: true,
430
+ })
431
+ if (lock) return lock
432
+ await new Promise((resolve) => setTimeout(resolve, 10))
433
+ }
434
+ throw new Error('Timed out waiting for the sidebar state write lock')
435
+ }
436
+
437
+ async function writeMergedSidebarState(
438
+ file: string,
439
+ merge: (latest: SidebarState) => SidebarState,
440
+ hooks?: SidebarMergeHooks,
441
+ ): Promise<void> {
442
+ const lock = await acquireSidebarWriteLock(file)
443
+ try {
444
+ // The file lock serializes current writers. Rechecking also preserves data
445
+ // from older plugin processes that do not participate in this lock.
446
+ for (let attempt = 0; attempt < MAX_MERGE_ATTEMPTS; attempt += 1) {
447
+ const rawBefore = await readRawSidebar(file)
448
+ const next = merge(parseSidebarState(rawBefore))
449
+ if (attempt === 0) await hooks?.beforeRecheck?.()
450
+ const rawRecheck = await readRawSidebar(file)
451
+ if (rawRecheck !== rawBefore) continue
452
+ await doWriteSidebarState(next, file)
453
+ return
454
+ }
455
+
456
+ const latest = await readSidebarState(file)
457
+ await doWriteSidebarState(merge(latest), file)
458
+ } finally {
459
+ await lock.release()
460
+ }
461
+ }
462
+
463
+ export type SidebarMachineState = Pick<
464
+ SidebarState,
465
+ 'main' | 'fallbacks' | 'planType' | 'credits' | 'lastUpdated'
466
+ > & { route: string }
467
+
468
+ export function setSidebarMachineState(
469
+ machineState: SidebarMachineState,
470
+ file = getSidebarStateFile(),
471
+ hooks?: SidebarMergeHooks,
472
+ ): Promise<void> {
473
+ return enqueueSidebarWrite(async () => {
474
+ await writeMergedSidebarState(
475
+ file,
476
+ (latest) => ({
477
+ ...latest,
478
+ ...machineState,
479
+ activeId: latest.activeId,
480
+ activeRouting: latest.activeRouting,
481
+ lastUpdated: Math.max(Date.now(), latest.lastUpdated + 1),
482
+ }),
483
+ hooks,
484
+ )
485
+ })
486
+ }
487
+
488
+ export function upsertSidebarActiveRouting(
489
+ input: { sessionId: string } & ActiveRoutingEntry,
490
+ accounts: readonly SidebarRoutingAccount[],
491
+ file = getSidebarStateFile(),
492
+ hooks?: SidebarMergeHooks,
493
+ ): Promise<void> {
494
+ return enqueueSidebarWrite(async () => {
495
+ await writeMergedSidebarState(
496
+ file,
497
+ (latest) => {
498
+ const activeRouting = pruneActiveRouting(
499
+ {
500
+ ...latest.activeRouting,
501
+ [input.sessionId]: {
502
+ activeId: input.activeId,
503
+ route: input.route,
504
+ updatedAt: input.updatedAt,
505
+ },
506
+ },
507
+ accounts,
508
+ Date.now(),
509
+ )
510
+ return {
511
+ ...latest,
512
+ activeId: input.activeId,
513
+ route: input.route,
514
+ activeRouting,
515
+ lastUpdated: Math.max(Date.now(), latest.lastUpdated + 1),
516
+ }
517
+ },
518
+ hooks,
519
+ )
520
+ })
521
+ }
522
+
523
+ export function setSidebarLegacyRouting(
524
+ input: ActiveRoutingEntry,
525
+ file = getSidebarStateFile(),
526
+ ): Promise<void> {
527
+ return enqueueSidebarWrite(async () => {
528
+ await writeMergedSidebarState(file, (latest) => ({
529
+ ...latest,
530
+ activeId: input.activeId,
531
+ route: input.route,
532
+ lastUpdated: Math.max(Date.now(), latest.lastUpdated + 1),
533
+ }))
534
+ })
535
+ }
536
+
537
+ export function removeSidebarActiveRouting(
538
+ sessionId: string,
539
+ accounts: readonly SidebarRoutingAccount[],
540
+ file = getSidebarStateFile(),
541
+ hooks?: SidebarMergeHooks,
542
+ ): Promise<void> {
543
+ return enqueueSidebarWrite(async () => {
544
+ await writeMergedSidebarState(
545
+ file,
546
+ (latest) => {
547
+ const now = Date.now()
548
+ const activeRouting = pruneActiveRouting(
549
+ latest.activeRouting,
550
+ accounts,
551
+ now,
552
+ sessionId,
553
+ )
554
+ return {
555
+ ...latest,
556
+ activeRouting,
557
+ lastUpdated: Math.max(Date.now(), latest.lastUpdated + 1),
558
+ }
559
+ },
560
+ hooks,
561
+ )
562
+ })
186
563
  }
187
564
 
188
565
  async function doWriteSidebarState(
@@ -200,6 +577,7 @@ async function doWriteSidebarState(
200
577
  pid: process.pid,
201
578
  error: e instanceof Error ? e.message : String(e),
202
579
  })
580
+ throw e
203
581
  }
204
582
  }
205
583
 
@@ -250,20 +628,22 @@ export function getCollapsedQuotaSummary(quota: AccountQuota | null): {
250
628
  } {
251
629
  const primaryUsedPercent = quota?.primary?.usedPercent ?? null
252
630
  const secondaryUsedPercent = quota?.secondary?.usedPercent ?? null
253
- if (primaryUsedPercent == null && secondaryUsedPercent == null) {
254
- return { primaryUsedPercent, secondaryUsedPercent, text: null }
255
- }
256
-
631
+ const rows = getPresentQuotaWindows(quota)
257
632
  return {
258
633
  primaryUsedPercent,
259
634
  secondaryUsedPercent,
260
- text: `5h: ${primaryUsedPercent == null ? '\u2014' : `${Math.round(primaryUsedPercent)}%`} 7d: ${secondaryUsedPercent == null ? '\u2014' : `${Math.round(secondaryUsedPercent)}%`}`,
635
+ text:
636
+ rows.length === 0
637
+ ? null
638
+ : rows
639
+ .map(
640
+ ({ label, window }) =>
641
+ `${label}: ${Math.round(window.usedPercent)}%`,
642
+ )
643
+ .join(' '),
261
644
  }
262
645
  }
263
646
 
264
- export const FIVE_HOUR_MS = 5 * 60 * 60 * 1000
265
- export const SEVEN_DAY_MS = 7 * 24 * 60 * 60 * 1000
266
-
267
647
  const PACING_MIN_ELAPSED_MS = 5 * 60 * 1000
268
648
  const PACING_MIN_ELAPSED_FRACTION = 0.01
269
649
  const ON_PACE_DELTA = 1
@@ -7,7 +7,7 @@ import { setProp as _$setProp } from "opentui:runtime-module:%40opentui%2Fsolid"
7
7
  import { createElement as _$createElement } from "opentui:runtime-module:%40opentui%2Fsolid";
8
8
  /** @jsxImportSource @opentui/solid */
9
9
 
10
- import { getSidebarState } from "../sidebar-state.js";
10
+ import { getCollapsedQuotaSummary, getSidebarState, resolveSessionSidebarRouting } from "../sidebar-state.js";
11
11
  import { openUrl } from "../util/open-url";
12
12
  export function buildCachekeepDialogOptions(payload) {
13
13
  const enabled = payload.knobs.enabled === true;
@@ -101,7 +101,7 @@ function showSetCachekeepWindowPrompt(api, apply, state, render) {
101
101
  onCancel: () => render(state)
102
102
  }));
103
103
  }
104
- export function openCommandDialog(api, payload, apply) {
104
+ export function openCommandDialog(api, payload, apply, sessionId) {
105
105
  if (payload.command === 'openai-routing') {
106
106
  const current = payload.knobs.mode ?? 'main-first';
107
107
  const DialogSelect = api.ui.DialogSelect;
@@ -203,7 +203,7 @@ export function openCommandDialog(api, payload, apply) {
203
203
  }, {
204
204
  title: 'Edit thresholds\u2026',
205
205
  value: 'edit',
206
- description: 'Set per-account 5h,1w cutoffs'
206
+ description: 'Set per-account primary,secondary cutoffs'
207
207
  }],
208
208
  onSelect: option => {
209
209
  if (option.value === 'edit') {
@@ -293,7 +293,7 @@ export function openCommandDialog(api, payload, apply) {
293
293
  return;
294
294
  }
295
295
  if (payload.command === 'openai-account') {
296
- openAccountDialog(api, apply);
296
+ openAccountDialog(api, apply, sessionId);
297
297
  return;
298
298
  }
299
299
 
@@ -303,12 +303,13 @@ export function openCommandDialog(api, payload, apply) {
303
303
 
304
304
  // -- Accounts dialog ---------------------------------------------------------
305
305
 
306
- function formatQuota5h7d(quota) {
307
- if (!quota) return 'no quota data';
308
- const parts = [];
309
- if (quota.primary) parts.push(`5h: ${Math.round(quota.primary.usedPercent)}%`);
310
- if (quota.secondary) parts.push(`7d: ${Math.round(quota.secondary.usedPercent)}%`);
311
- return parts.length > 0 ? parts.join(' ') : 'no quota data';
306
+ export function formatQuotaWindows(quota) {
307
+ const windowSummary = getCollapsedQuotaSummary(quota ?? null).text;
308
+ if (windowSummary) return windowSummary;
309
+ if (quota?.resetCreditsAvailable !== undefined) {
310
+ return `resets: ${quota.resetCreditsAvailable}`;
311
+ }
312
+ return 'no quota data';
312
313
  }
313
314
  function osc52Copy(api, text) {
314
315
  try {
@@ -321,7 +322,19 @@ function osc52Copy(api, text) {
321
322
  }
322
323
  return false;
323
324
  }
324
- function openAccountDialog(api, apply) {
325
+ export function buildAccountDialogRows(state, sessionId) {
326
+ const activeId = resolveSessionSidebarRouting(state, sessionId).activeId;
327
+ return [{
328
+ title: `main${activeId === 'main' ? ' \u2022 active' : ''}`,
329
+ value: 'main',
330
+ description: formatQuotaWindows(state.main.quota)
331
+ }, ...state.fallbacks.map(fb => ({
332
+ title: `${fb.label ?? fb.id}${activeId === fb.id ? ' \u2022 active' : ''}${!fb.enabled ? ' (disabled)' : ''}`,
333
+ value: fb.id,
334
+ description: formatQuotaWindows(fb.quota)
335
+ }))];
336
+ }
337
+ function openAccountDialog(api, apply, sessionId) {
325
338
  const DialogConfirm = api.ui.DialogConfirm;
326
339
  function showL1() {
327
340
  void getSidebarState().then(state => {
@@ -330,15 +343,7 @@ function openAccountDialog(api, apply) {
330
343
  api.ui.dialog.replace(() => _$createComponent(DialogSelectInner, {
331
344
  title: "OpenAI Accounts",
332
345
  get options() {
333
- return [{
334
- title: `main${state.activeId === 'main' || !state.activeId ? ' \u2022 active' : ''}`,
335
- value: 'main',
336
- description: formatQuota5h7d(state.main.quota)
337
- }, ...state.fallbacks.map(fb => ({
338
- title: `${fb.label ?? fb.id}${state.activeId === fb.id ? ' \u2022 active' : ''}${!fb.enabled ? ' (disabled)' : ''}`,
339
- value: fb.id,
340
- description: formatQuota5h7d(fb.quota)
341
- })), {
346
+ return [...buildAccountDialogRows(state, sessionId), {
342
347
  title: 'Add account\u2026',
343
348
  value: '__add__',
344
349
  description: 'Sign in to a new OpenAI account'