@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
@@ -1,7 +1,13 @@
1
1
  /** @jsxImportSource @opentui/solid */
2
2
  import type { TuiPluginApi } from '@opencode-ai/plugin/tui'
3
3
  import type { OpenDialogPayload } from '../rpc/protocol.js'
4
- import { getSidebarState } from '../sidebar-state.js'
4
+ import {
5
+ type AccountQuota,
6
+ getCollapsedQuotaSummary,
7
+ getSidebarState,
8
+ resolveSessionSidebarRouting,
9
+ type SidebarState,
10
+ } from '../sidebar-state.js'
5
11
  import { openUrl } from '../util/open-url'
6
12
 
7
13
  type ApplyFn = (
@@ -139,6 +145,7 @@ export function openCommandDialog(
139
145
  api: TuiPluginApi,
140
146
  payload: OpenDialogPayload,
141
147
  apply: ApplyFn,
148
+ sessionId?: string,
142
149
  ) {
143
150
  if (payload.command === 'openai-routing') {
144
151
  const current = (payload.knobs.mode as string) ?? 'main-first'
@@ -248,7 +255,7 @@ export function openCommandDialog(
248
255
  {
249
256
  title: 'Edit thresholds\u2026',
250
257
  value: 'edit',
251
- description: 'Set per-account 5h,1w cutoffs',
258
+ description: 'Set per-account primary,secondary cutoffs',
252
259
  },
253
260
  ]}
254
261
  onSelect={(option) => {
@@ -345,7 +352,7 @@ export function openCommandDialog(
345
352
  }
346
353
 
347
354
  if (payload.command === 'openai-account') {
348
- openAccountDialog(api, apply)
355
+ openAccountDialog(api, apply, sessionId)
349
356
  return
350
357
  }
351
358
 
@@ -355,18 +362,15 @@ export function openCommandDialog(
355
362
 
356
363
  // -- Accounts dialog ---------------------------------------------------------
357
364
 
358
- function formatQuota5h7d(
359
- quota:
360
- | { primary?: { usedPercent: number }; secondary?: { usedPercent: number } }
361
- | null
362
- | undefined,
365
+ export function formatQuotaWindows(
366
+ quota: AccountQuota | null | undefined,
363
367
  ): string {
364
- if (!quota) return 'no quota data'
365
- const parts: string[] = []
366
- if (quota.primary) parts.push(`5h: ${Math.round(quota.primary.usedPercent)}%`)
367
- if (quota.secondary)
368
- parts.push(`7d: ${Math.round(quota.secondary.usedPercent)}%`)
369
- return parts.length > 0 ? parts.join(' ') : 'no quota data'
368
+ const windowSummary = getCollapsedQuotaSummary(quota ?? null).text
369
+ if (windowSummary) return windowSummary
370
+ if (quota?.resetCreditsAvailable !== undefined) {
371
+ return `resets: ${quota.resetCreditsAvailable}`
372
+ }
373
+ return 'no quota data'
370
374
  }
371
375
 
372
376
  function osc52Copy(api: TuiPluginApi, text: string): boolean {
@@ -386,7 +390,30 @@ function osc52Copy(api: TuiPluginApi, text: string): boolean {
386
390
  return false
387
391
  }
388
392
 
389
- function openAccountDialog(api: TuiPluginApi, apply: ApplyFn) {
393
+ export function buildAccountDialogRows(
394
+ state: SidebarState,
395
+ sessionId?: string,
396
+ ) {
397
+ const activeId = resolveSessionSidebarRouting(state, sessionId).activeId
398
+ return [
399
+ {
400
+ title: `main${activeId === 'main' ? ' \u2022 active' : ''}`,
401
+ value: 'main',
402
+ description: formatQuotaWindows(state.main.quota),
403
+ },
404
+ ...state.fallbacks.map((fb) => ({
405
+ title: `${fb.label ?? fb.id}${activeId === fb.id ? ' \u2022 active' : ''}${!fb.enabled ? ' (disabled)' : ''}`,
406
+ value: fb.id,
407
+ description: formatQuotaWindows(fb.quota),
408
+ })),
409
+ ]
410
+ }
411
+
412
+ function openAccountDialog(
413
+ api: TuiPluginApi,
414
+ apply: ApplyFn,
415
+ sessionId?: string,
416
+ ) {
390
417
  const DialogConfirm = api.ui.DialogConfirm
391
418
 
392
419
  function showL1() {
@@ -397,16 +424,7 @@ function openAccountDialog(api: TuiPluginApi, apply: ApplyFn) {
397
424
  <DialogSelectInner
398
425
  title='OpenAI Accounts'
399
426
  options={[
400
- {
401
- title: `main${state.activeId === 'main' || !state.activeId ? ' \u2022 active' : ''}`,
402
- value: 'main',
403
- description: formatQuota5h7d(state.main.quota),
404
- },
405
- ...state.fallbacks.map((fb) => ({
406
- title: `${fb.label ?? fb.id}${state.activeId === fb.id ? ' \u2022 active' : ''}${!fb.enabled ? ' (disabled)' : ''}`,
407
- value: fb.id,
408
- description: formatQuota5h7d(fb.quota),
409
- })),
427
+ ...buildAccountDialogRows(state, sessionId),
410
428
  {
411
429
  title: 'Add account\u2026',
412
430
  value: '__add__',