@cortexkit/opencode-anthropic-auth 1.15.1 → 1.17.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.
@@ -32,6 +32,26 @@ export function buildKillswitchThresholdSeed(
32
32
  return seedParts.join(' ')
33
33
  }
34
34
 
35
+ export function buildAccountDialogOption(account: {
36
+ id: string
37
+ label: string
38
+ role: string
39
+ enabled: boolean
40
+ quotaPercent: number | null
41
+ tierLabel?: string
42
+ }) {
43
+ const pct =
44
+ account.quotaPercent != null
45
+ ? ` ${Math.round(account.quotaPercent)}%`
46
+ : ' \u2013%'
47
+ const status = !account.enabled ? ' (disabled)' : ''
48
+ return {
49
+ title: `${account.label} [${account.role}]${status}${pct}`,
50
+ value: account.id,
51
+ ...(account.tierLabel && { description: account.tierLabel }),
52
+ }
53
+ }
54
+
35
55
  function showText(api: TuiPluginApi, text: string) {
36
56
  api.ui.dialog.setSize('xlarge')
37
57
  api.ui.dialog.replace(() => (
@@ -65,6 +85,17 @@ export function openCommandDialog(
65
85
  value: 'fallback-first',
66
86
  description: 'Prefer fallback accounts, preserve main',
67
87
  },
88
+ {
89
+ title: 'Sticky balanced',
90
+ value: 'sticky-balanced',
91
+ description:
92
+ 'Balance new sessions by quota and keep each account sticky',
93
+ },
94
+ {
95
+ title: 'Reset this session',
96
+ value: 'reset',
97
+ description: 'Reassign this session on its next request',
98
+ },
68
99
  ]}
69
100
  onSelect={(option) => {
70
101
  void apply('claude-routing', String(option.value)).then((r) => {
@@ -158,9 +189,9 @@ export function openCommandDialog(
158
189
  api.ui.dialog.setSize('xlarge')
159
190
  api.ui.dialog.replace(() => (
160
191
  <DialogPrompt
161
- title='Claude cachekeep window'
192
+ title='Claude cachekeep schedule'
162
193
  description={() => <text>{payload.text}</text>}
163
- placeholder="HH-HH (e.g. 08-20) or 'off'"
194
+ placeholder="'always', HH-HH (e.g. 08-20), or 'off'"
164
195
  value={seed}
165
196
  onConfirm={(value: string) => {
166
197
  void apply('claude-cachekeep', value.trim()).then((r) => {
@@ -242,6 +273,7 @@ export function openCommandDialog(
242
273
  role: string
243
274
  enabled: boolean
244
275
  quotaPercent: number | null
276
+ tierLabel?: string
245
277
  }>) ?? []
246
278
 
247
279
  const updateAccounts = (r: {
@@ -267,17 +299,7 @@ export function openCommandDialog(
267
299
  value: '__add__',
268
300
  description: 'Add an API key or OAuth fallback account',
269
301
  },
270
- ...accounts.map((a) => {
271
- const pct =
272
- a.quotaPercent != null
273
- ? ` ${Math.round(a.quotaPercent)}%`
274
- : ' \u2013%'
275
- const status = !a.enabled ? ' (disabled)' : ''
276
- return {
277
- title: `${a.label} [${a.role}]${status}${pct}`,
278
- value: a.id,
279
- }
280
- }),
302
+ ...accounts.map(buildAccountDialogOption),
281
303
  ]
282
304
  api.ui.dialog.setSize('xlarge')
283
305
  api.ui.dialog.replace(() => (
@@ -15,6 +15,15 @@ export interface AccountQuota {
15
15
  five_hour?: QuotaWindow
16
16
  seven_day?: QuotaWindow
17
17
  scoped?: ScopedQuotaWindow[]
18
+ extraUsage?: {
19
+ used: { amountMinor: number; currency: string; exponent: number }
20
+ limit: { amountMinor: number; currency: string; exponent: number }
21
+ utilizationPercent?: number
22
+ severity?: string
23
+ exhausted: boolean
24
+ }
25
+ bindingWindow?: string
26
+ fallbackAdvised?: boolean
18
27
  }
19
28
 
20
29
  export interface SidebarAccountState {
@@ -25,6 +34,7 @@ export interface SidebarAccountState {
25
34
  // True when the account's refresh token is permanently dead (400
26
35
  // invalid_grant) and it needs a re-login — distinct from a transient backoff.
27
36
  needsReauth: boolean
37
+ tierLabel?: string
28
38
  }
29
39
 
30
40
  export interface FableRecoverySidebarState {
@@ -37,6 +47,7 @@ export interface FableRecoverySidebarState {
37
47
  export interface SidebarState {
38
48
  main: {
39
49
  quota: AccountQuota | null
50
+ tierLabel?: string
40
51
  quotaBackedOff?: boolean
41
52
  quotaBackoffUntil?: number
42
53
  refreshBackedOff?: boolean
@@ -56,9 +67,20 @@ export interface SidebarState {
56
67
  lastUpdated: number
57
68
  }
58
69
 
59
- import { mkdir, readFile, writeFile } from 'node:fs/promises'
70
+ import { randomUUID } from 'node:crypto'
71
+ import {
72
+ mkdir,
73
+ readFile,
74
+ rename,
75
+ rm,
76
+ rmdir,
77
+ stat,
78
+ unlink,
79
+ writeFile,
80
+ } from 'node:fs/promises'
60
81
  import { tmpdir } from 'node:os'
61
82
  import { dirname, join } from 'node:path'
83
+ import { logger } from '@cortexkit/anthropic-auth-core'
62
84
 
63
85
  const STATE_FILE_ENV = 'OPENCODE_ANTHROPIC_AUTH_SIDEBAR_STATE_FILE'
64
86
  const DEFAULT_STATE_DIR = join(tmpdir(), 'opencode-anthropic-auth')
@@ -136,9 +158,53 @@ function normalizeAccountQuota(value: unknown): AccountQuota | null {
136
158
  quota.scoped = scoped
137
159
  }
138
160
 
161
+ if (isRecord(value.extraUsage)) {
162
+ const used = normalizeQuotaMoney(value.extraUsage.used)
163
+ const limit = normalizeQuotaMoney(value.extraUsage.limit)
164
+ if (used && limit && typeof value.extraUsage.exhausted === 'boolean') {
165
+ quota.extraUsage = {
166
+ used,
167
+ limit,
168
+ ...(typeof value.extraUsage.utilizationPercent === 'number' &&
169
+ Number.isFinite(value.extraUsage.utilizationPercent) && {
170
+ utilizationPercent: value.extraUsage.utilizationPercent,
171
+ }),
172
+ ...(typeof value.extraUsage.severity === 'string' && {
173
+ severity: value.extraUsage.severity,
174
+ }),
175
+ exhausted: value.extraUsage.exhausted,
176
+ }
177
+ }
178
+ }
179
+ if (typeof value.bindingWindow === 'string' && value.bindingWindow.trim()) {
180
+ quota.bindingWindow = value.bindingWindow.trim()
181
+ }
182
+ if (typeof value.fallbackAdvised === 'boolean') {
183
+ quota.fallbackAdvised = value.fallbackAdvised
184
+ }
185
+
139
186
  return Object.keys(quota).length ? quota : null
140
187
  }
141
188
 
189
+ function normalizeQuotaMoney(value: unknown) {
190
+ if (!isRecord(value)) return undefined
191
+ if (
192
+ !Number.isInteger(value.amountMinor) ||
193
+ typeof value.currency !== 'string' ||
194
+ !/^[A-Za-z]{3}$/.test(value.currency.trim()) ||
195
+ !Number.isInteger(value.exponent) ||
196
+ (value.exponent as number) < 0 ||
197
+ (value.exponent as number) > 20
198
+ ) {
199
+ return undefined
200
+ }
201
+ return {
202
+ amountMinor: value.amountMinor as number,
203
+ currency: value.currency.trim(),
204
+ exponent: value.exponent as number,
205
+ }
206
+ }
207
+
142
208
  export function normalizeSidebarState(raw: unknown): SidebarState {
143
209
  if (!isRecord(raw)) return { ...DEFAULT_SIDEBAR_STATE }
144
210
 
@@ -146,6 +212,9 @@ export function normalizeSidebarState(raw: unknown): SidebarState {
146
212
  if (isRecord(raw.main)) {
147
213
  const m = raw.main
148
214
  main.quota = normalizeAccountQuota(m.quota)
215
+ if (typeof m.tierLabel === 'string' && m.tierLabel.trim()) {
216
+ main.tierLabel = m.tierLabel.trim()
217
+ }
149
218
  if (typeof m.quotaBackedOff === 'boolean')
150
219
  main.quotaBackedOff = m.quotaBackedOff
151
220
  if (typeof m.quotaBackoffUntil === 'number')
@@ -167,6 +236,10 @@ export function normalizeSidebarState(raw: unknown): SidebarState {
167
236
  enabled: typeof entry.enabled === 'boolean' ? entry.enabled : false,
168
237
  needsReauth:
169
238
  typeof entry.needsReauth === 'boolean' ? entry.needsReauth : false,
239
+ tierLabel:
240
+ typeof entry.tierLabel === 'string' && entry.tierLabel.trim()
241
+ ? entry.tierLabel.trim()
242
+ : undefined,
170
243
  }))
171
244
  : []
172
245
 
@@ -238,23 +311,327 @@ export function normalizeSidebarState(raw: unknown): SidebarState {
238
311
 
239
312
  let writeChain: Promise<void> = Promise.resolve()
240
313
 
241
- export async function getSidebarState(): Promise<SidebarState> {
314
+ // Wait through ordinary contention while staying below the 2s stale-eviction window.
315
+ const SIDEBAR_LOCK_BUDGET_MS = 1_000
316
+ const SIDEBAR_LOCK_STALE_MS = 2_000
317
+ const SIDEBAR_LOCK_RETRY_MIN_MS = 5
318
+ const SIDEBAR_LOCK_RETRY_MAX_MS = 15
319
+
320
+ interface SidebarStateWriteTestHooks {
321
+ afterMergeRead?: (stateFile: string) => void | Promise<void>
322
+ beforeRename?: (stateFile: string, tempFile: string) => void | Promise<void>
323
+ afterRename?: (stateFile: string) => void | Promise<void>
324
+ afterStaleLockStat?: (lockDir: string) => void | Promise<void>
325
+ onStaleLockClaimed?: (lockDir: string) => void | Promise<void>
326
+ onLockAcquired?: (lockDir: string) => void | Promise<void>
327
+ beforeReleaseDirectoryRemoval?: (lockDir: string) => void | Promise<void>
328
+ lockBudgetMs?: number
329
+ lockRetryMinMs?: number
330
+ lockRetryMaxMs?: number
331
+ }
332
+
333
+ let sidebarStateWriteTestHooks: SidebarStateWriteTestHooks | null = null
334
+
335
+ export function __setSidebarStateWriteTestHooks(
336
+ hooks: SidebarStateWriteTestHooks | null,
337
+ ): void {
338
+ sidebarStateWriteTestHooks = hooks
339
+ }
340
+
341
+ // Boot-routing test hooks live here rather than in index.ts: the plugin entry
342
+ // module must export nothing beyond the plugin factory — opencode's plugin
343
+ // loader treats extra entry-point exports as plugin candidates and fails to
344
+ // load the plugin (same incident class as the removed
345
+ // __setBootProfileHydrationForTest export).
346
+ export interface InitialSidebarRoutingTestHooks {
347
+ beforeSidebarRead?: () => void | Promise<void>
348
+ beforeStorageLoad?: () => void | Promise<void>
349
+ afterStorageLoad?: () => void | Promise<void>
350
+ }
351
+
352
+ let initialSidebarRoutingTestHooks: InitialSidebarRoutingTestHooks | null = null
353
+
354
+ export function __setInitialSidebarRoutingTestHooks(
355
+ hooks: InitialSidebarRoutingTestHooks | null,
356
+ ): void {
357
+ initialSidebarRoutingTestHooks = hooks
358
+ }
359
+
360
+ export function getInitialSidebarRoutingTestHooks(): InitialSidebarRoutingTestHooks | null {
361
+ return initialSidebarRoutingTestHooks
362
+ }
363
+
364
+ function sleep(ms: number): Promise<void> {
365
+ return new Promise((resolve) => setTimeout(resolve, ms))
366
+ }
367
+
368
+ async function acquireSidebarStateLock(stateFile: string): Promise<{
369
+ release: () => Promise<void>
370
+ ownsLock: () => Promise<boolean>
371
+ } | null> {
372
+ const lockDir = `${stateFile}.lock`
373
+ const budgetMs =
374
+ sidebarStateWriteTestHooks?.lockBudgetMs ?? SIDEBAR_LOCK_BUDGET_MS
375
+ const retryMinMs =
376
+ sidebarStateWriteTestHooks?.lockRetryMinMs ?? SIDEBAR_LOCK_RETRY_MIN_MS
377
+ const retryMaxMs =
378
+ sidebarStateWriteTestHooks?.lockRetryMaxMs ?? SIDEBAR_LOCK_RETRY_MAX_MS
379
+ const deadline = Date.now() + budgetMs
380
+
381
+ while (true) {
382
+ try {
383
+ await mkdir(lockDir)
384
+ const ownerId = randomUUID()
385
+ const ownerFile = join(lockDir, ownerId)
386
+ await writeFile(ownerFile, '', {
387
+ encoding: 'utf8',
388
+ mode: 0o600,
389
+ flag: 'wx',
390
+ })
391
+ await sidebarStateWriteTestHooks?.onLockAcquired?.(lockDir)
392
+ return {
393
+ ownsLock: async () => {
394
+ try {
395
+ await stat(ownerFile)
396
+ return true
397
+ } catch {
398
+ return false
399
+ }
400
+ },
401
+ release: async () => {
402
+ // Unlinking this acquisition's unique file makes the filesystem reject
403
+ // a stale release after eviction handed the path to a successor.
404
+ try {
405
+ await unlink(ownerFile)
406
+ } catch {
407
+ return
408
+ }
409
+ await sidebarStateWriteTestHooks?.beforeReleaseDirectoryRemoval?.(
410
+ lockDir,
411
+ )
412
+ await rmdir(lockDir).catch(() => {})
413
+ },
414
+ }
415
+ } catch (error) {
416
+ if ((error as NodeJS.ErrnoException).code !== 'EEXIST') {
417
+ logger.trace('sidebar', 'state lock unavailable; write skipped', {
418
+ stateFile,
419
+ error: error instanceof Error ? error.message : String(error),
420
+ })
421
+ return null
422
+ }
423
+ }
424
+
425
+ try {
426
+ const lockStat = await stat(lockDir)
427
+ if (Date.now() - lockStat.mtimeMs > SIDEBAR_LOCK_STALE_MS) {
428
+ await sidebarStateWriteTestHooks?.afterStaleLockStat?.(lockDir)
429
+ const evictedLockDir = `${lockDir}.evict-${process.pid}-${randomUUID()}`
430
+ try {
431
+ await rename(lockDir, evictedLockDir)
432
+ } catch (error) {
433
+ if ((error as NodeJS.ErrnoException).code === 'ENOENT') continue
434
+ throw error
435
+ }
436
+ await sidebarStateWriteTestHooks?.onStaleLockClaimed?.(lockDir)
437
+ await rm(evictedLockDir, { recursive: true, force: true }).catch(
438
+ () => {},
439
+ )
440
+ continue
441
+ }
442
+ } catch (error) {
443
+ if ((error as NodeJS.ErrnoException).code === 'ENOENT') continue
444
+ logger.trace('sidebar', 'state lock inspection failed; write skipped', {
445
+ stateFile,
446
+ error: error instanceof Error ? error.message : String(error),
447
+ })
448
+ return null
449
+ }
450
+
451
+ const remainingMs = deadline - Date.now()
452
+ if (remainingMs <= 0) {
453
+ logger.warn('sidebar', 'lock budget exhausted, write skipped', {
454
+ stateFile,
455
+ budgetMs,
456
+ })
457
+ return null
458
+ }
459
+ const jitterMs =
460
+ retryMinMs + Math.floor(Math.random() * (retryMaxMs - retryMinMs + 1))
461
+ await sleep(Math.min(jitterMs, remainingMs))
462
+ }
463
+ }
464
+
465
+ export async function __acquireSidebarStateLockForTest(
466
+ stateFile: string,
467
+ ): Promise<(() => Promise<void>) | null> {
468
+ return (await acquireSidebarStateLock(stateFile))?.release ?? null
469
+ }
470
+
471
+ async function writeSidebarStateAtomic(
472
+ stateFile: string,
473
+ state: SidebarState,
474
+ ownsLock: () => Promise<boolean>,
475
+ ): Promise<'written' | 'lock-lost-before-rename' | 'lock-lost-after-rename'> {
476
+ const tempFile = `${stateFile}.${process.pid}.${randomUUID()}.tmp`
477
+ await writeFile(tempFile, JSON.stringify(state), {
478
+ encoding: 'utf8',
479
+ mode: 0o600,
480
+ })
242
481
  try {
243
- const raw = await readFile(getSidebarStateFile(), 'utf8')
482
+ if (!(await ownsLock())) {
483
+ logger.trace('sidebar', 'state lock lost before rename; write aborted', {
484
+ stateFile,
485
+ })
486
+ await rm(tempFile, { force: true }).catch(() => {})
487
+ return 'lock-lost-before-rename'
488
+ }
489
+ await sidebarStateWriteTestHooks?.beforeRename?.(stateFile, tempFile)
490
+ // No production await separates this ownership fence from rename. A process
491
+ // freeze between the adjacent syscalls remains possible, so rename is also
492
+ // fenced from the other side below.
493
+ if (!(await ownsLock())) {
494
+ logger.trace('sidebar', 'state lock lost before rename; write aborted', {
495
+ stateFile,
496
+ })
497
+ await rm(tempFile, { force: true }).catch(() => {})
498
+ return 'lock-lost-before-rename'
499
+ }
500
+ await rename(tempFile, stateFile)
501
+ await sidebarStateWriteTestHooks?.afterRename?.(stateFile)
502
+ if (!(await ownsLock())) return 'lock-lost-after-rename'
503
+ return 'written'
504
+ } catch (error) {
505
+ await rm(tempFile, { force: true }).catch(() => {})
506
+ throw error
507
+ }
508
+ }
509
+
510
+ export async function getSidebarState(
511
+ stateFile = getSidebarStateFile(),
512
+ ): Promise<SidebarState> {
513
+ try {
514
+ const raw = await readFile(stateFile, 'utf8')
244
515
  return normalizeSidebarState(JSON.parse(raw))
245
516
  } catch {
246
517
  return DEFAULT_SIDEBAR_STATE
247
518
  }
248
519
  }
249
520
 
521
+ export interface SidebarStateWriteOptions {
522
+ routingAuthoritative?: boolean
523
+ resolvePreservedRouting?: (current: SidebarState) =>
524
+ | (Pick<SidebarState, 'activeId' | 'route'> & {
525
+ state?: SidebarState
526
+ })
527
+ | undefined
528
+ | Promise<
529
+ | (Pick<SidebarState, 'activeId' | 'route'> & {
530
+ state?: SidebarState
531
+ })
532
+ | undefined
533
+ >
534
+ onRoutingResolved?: (
535
+ routing: Pick<SidebarState, 'activeId' | 'route'>,
536
+ ) => void
537
+ }
538
+
250
539
  export async function setSidebarState(
251
540
  state: SidebarState,
252
541
  stateFile = getSidebarStateFile(),
542
+ options: SidebarStateWriteOptions = {},
253
543
  ): Promise<void> {
254
544
  writeChain = writeChain
255
545
  .then(async () => {
256
546
  await mkdir(dirname(stateFile), { recursive: true })
257
- await writeFile(stateFile, JSON.stringify(state), 'utf8')
547
+ for (let attempt = 0; attempt < 2; attempt++) {
548
+ const repairingPostRenameLoss = attempt === 1
549
+ const lock = await acquireSidebarStateLock(stateFile)
550
+ // Sidebar frames are display-only and refresh within seconds; dropping one
551
+ // is safer than clobbering routing state that may stay authoritative until
552
+ // another process handles its next request.
553
+ if (!lock) {
554
+ if (repairingPostRenameLoss) {
555
+ logger.warn(
556
+ 'sidebar',
557
+ 'post-rename repair lock unavailable; write skipped',
558
+ { stateFile },
559
+ )
560
+ }
561
+ return
562
+ }
563
+
564
+ let stateToWrite = state
565
+ let result:
566
+ | 'written'
567
+ | 'lock-lost-before-rename'
568
+ | 'lock-lost-after-rename'
569
+ try {
570
+ if (options.routingAuthoritative === false) {
571
+ const current = await getSidebarState(stateFile)
572
+ const preservedRouting =
573
+ await options.resolvePreservedRouting?.(current)
574
+ if (preservedRouting) {
575
+ const preservedState = preservedRouting.state ?? state
576
+ stateToWrite = {
577
+ ...preservedState,
578
+ activeId: preservedRouting.activeId,
579
+ route: preservedRouting.route,
580
+ lastUpdated: Math.max(
581
+ preservedState.lastUpdated,
582
+ current.lastUpdated,
583
+ ),
584
+ }
585
+ }
586
+ await sidebarStateWriteTestHooks?.afterMergeRead?.(stateFile)
587
+ } else if (repairingPostRenameLoss) {
588
+ const current = await getSidebarState(stateFile)
589
+ // A successor owns its fresh account/quota and recovery snapshots;
590
+ // this routing frame may only republish its decision and shared UI state.
591
+ stateToWrite = {
592
+ ...current,
593
+ activeId: state.activeId,
594
+ route: state.route,
595
+ relay: state.relay,
596
+ fastMode: state.fastMode,
597
+ cacheKeep: state.cacheKeep,
598
+ lastUpdated: Math.max(current.lastUpdated, state.lastUpdated),
599
+ }
600
+ }
601
+ result = await writeSidebarStateAtomic(
602
+ stateFile,
603
+ stateToWrite,
604
+ lock.ownsLock,
605
+ )
606
+ } finally {
607
+ await lock.release()
608
+ }
609
+
610
+ if (result === 'lock-lost-after-rename') {
611
+ if (repairingPostRenameLoss) {
612
+ logger.warn(
613
+ 'sidebar',
614
+ 'post-rename repair lost lock; write skipped',
615
+ { stateFile },
616
+ )
617
+ return
618
+ }
619
+ logger.warn(
620
+ 'sidebar',
621
+ 'state lock lost after rename; repairing write',
622
+ {
623
+ stateFile,
624
+ },
625
+ )
626
+ continue
627
+ }
628
+ if (result === 'lock-lost-before-rename') return
629
+ options.onRoutingResolved?.({
630
+ activeId: stateToWrite.activeId,
631
+ route: stateToWrite.route,
632
+ })
633
+ return
634
+ }
258
635
  })
259
636
  .catch(() => {
260
637
  // Best-effort — sidebar is non-critical
@@ -25,6 +25,17 @@ export function buildKillswitchThresholdSeed(config, accountIds) {
25
25
  }
26
26
  return seedParts.join(' ');
27
27
  }
28
+ export function buildAccountDialogOption(account) {
29
+ const pct = account.quotaPercent != null ? ` ${Math.round(account.quotaPercent)}%` : ' \u2013%';
30
+ const status = !account.enabled ? ' (disabled)' : '';
31
+ return {
32
+ title: `${account.label} [${account.role}]${status}${pct}`,
33
+ value: account.id,
34
+ ...(account.tierLabel && {
35
+ description: account.tierLabel
36
+ })
37
+ };
38
+ }
28
39
  function showText(api, text) {
29
40
  api.ui.dialog.setSize('xlarge');
30
41
  api.ui.dialog.replace(() => (() => {
@@ -54,6 +65,14 @@ export function openCommandDialog(api, payload, apply) {
54
65
  title: 'Fallback first',
55
66
  value: 'fallback-first',
56
67
  description: 'Prefer fallback accounts, preserve main'
68
+ }, {
69
+ title: 'Sticky balanced',
70
+ value: 'sticky-balanced',
71
+ description: 'Balance new sessions by quota and keep each account sticky'
72
+ }, {
73
+ title: 'Reset this session',
74
+ value: 'reset',
75
+ description: 'Reassign this session on its next request'
57
76
  }],
58
77
  onSelect: option => {
59
78
  void apply('claude-routing', String(option.value)).then(r => {
@@ -140,13 +159,13 @@ export function openCommandDialog(api, payload, apply) {
140
159
  const DialogPrompt = api.ui.DialogPrompt;
141
160
  api.ui.dialog.setSize('xlarge');
142
161
  api.ui.dialog.replace(() => _$createComponent(DialogPrompt, {
143
- title: "Claude cachekeep window",
162
+ title: "Claude cachekeep schedule",
144
163
  description: () => (() => {
145
164
  var _el$3 = _$createElement("text");
146
165
  _$insert(_el$3, () => payload.text);
147
166
  return _el$3;
148
167
  })(),
149
- placeholder: "HH-HH (e.g. 08-20) or 'off'",
168
+ placeholder: "'always', HH-HH (e.g. 08-20), or 'off'",
150
169
  value: seed,
151
170
  onConfirm: value => {
152
171
  void apply('claude-cachekeep', value.trim()).then(r => {
@@ -231,14 +250,7 @@ export function openCommandDialog(api, payload, apply) {
231
250
  title: 'Add account\u2026',
232
251
  value: '__add__',
233
252
  description: 'Add an API key or OAuth fallback account'
234
- }, ...accounts.map(a => {
235
- const pct = a.quotaPercent != null ? ` ${Math.round(a.quotaPercent)}%` : ' \u2013%';
236
- const status = !a.enabled ? ' (disabled)' : '';
237
- return {
238
- title: `${a.label} [${a.role}]${status}${pct}`,
239
- value: a.id
240
- };
241
- })];
253
+ }, ...accounts.map(buildAccountDialogOption)];
242
254
  api.ui.dialog.setSize('xlarge');
243
255
  api.ui.dialog.replace(() => _$createComponent(DialogSelect, {
244
256
  title: "Claude accounts",