@cortexkit/opencode-anthropic-auth 1.19.1 → 1.21.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.
Files changed (38) hide show
  1. package/README.md +119 -2
  2. package/dist/cache-diagnostics.d.ts +79 -0
  3. package/dist/cli.js +2 -2
  4. package/dist/index-1db17bc2.js +2 -0
  5. package/dist/{index-dex8fs0m.js → index-4nz423bt.js} +1 -1
  6. package/dist/{index-dzs7w297.js → index-4x20896x.js} +1 -1
  7. package/dist/{index-2k47hc8b.js → index-w9d41yy9.js} +1 -1
  8. package/dist/index-wezdyxst.js +2 -0
  9. package/dist/{index-075t8qec.js → index-y0khnbt6.js} +1 -1
  10. package/dist/index-yskwj5e6.js +428 -0
  11. package/dist/index.d.ts +0 -37
  12. package/dist/index.js +23 -19
  13. package/dist/lane-start.d.ts +17 -0
  14. package/dist/{logger-n49md99d.js → logger-ftk4bg3p.js} +1 -1
  15. package/dist/prime-manager-registry.d.ts +7 -0
  16. package/dist/request-policy.d.ts +25 -0
  17. package/dist/rpc/port-file.js +1 -1
  18. package/dist/rpc/protocol.d.ts +2 -1
  19. package/dist/rpc/protocol.js +1 -0
  20. package/dist/rpc/rpc-client.js +1 -1
  21. package/dist/rpc/rpc-dir.js +1 -1
  22. package/dist/server-fallback.d.ts +2 -0
  23. package/dist/sidebar-state.d.ts +26 -1
  24. package/dist/sidebar-state.js +1 -1
  25. package/dist/transform.d.ts +13 -0
  26. package/dist/tui/command-dialogs.d.ts +16 -1
  27. package/dist/tui-preferences.js +1 -1
  28. package/package.json +4 -4
  29. package/src/rpc/protocol.ts +15 -10
  30. package/src/sidebar-state.ts +154 -17
  31. package/src/tui/command-dialogs.tsx +112 -0
  32. package/src/tui-compiled/rpc/protocol.ts +15 -10
  33. package/src/tui-compiled/sidebar-state.ts +154 -17
  34. package/src/tui-compiled/tui/command-dialogs.tsx +134 -18
  35. package/src/tui-compiled/tui.tsx +43 -1
  36. package/src/tui.tsx +28 -0
  37. package/dist/index-a13w4gm9.js +0 -2
  38. package/dist/index-vwgzgh5x.js +0 -422
@@ -1,6 +1,8 @@
1
1
  /** @jsxImportSource @opentui/solid */
2
+ import type { PrimeAccountStatus } from '@cortexkit/anthropic-auth-core'
2
3
  import type { TuiPluginApi } from '@opencode-ai/plugin/tui'
3
4
  import type { OpenDialogPayload } from '../rpc/protocol.js'
5
+ import { formatPrimeCost, formatPrimeTime } from '../sidebar-state.js'
4
6
 
5
7
  type ApplyFn = (
6
8
  command: OpenDialogPayload['command'],
@@ -13,6 +15,20 @@ type KillswitchDialogConfig = {
13
15
  accounts?: Record<string, Record<string, number>>
14
16
  }
15
17
 
18
+ export const PRIME_DIALOG_OPTIONS = [
19
+ { title: 'Enable', value: 'on' },
20
+ { title: 'Disable', value: 'off' },
21
+ { title: 'Status', value: 'status' },
22
+ { title: 'Back', value: 'back' },
23
+ ]
24
+
25
+ export function handlePrimeStatusOption(
26
+ option: { value: string },
27
+ renderMain: () => void,
28
+ ): void {
29
+ if (option.value === 'back') renderMain()
30
+ }
31
+
16
32
  export function buildKillswitchThresholdSeed(
17
33
  config: KillswitchDialogConfig,
18
34
  accountIds: string[],
@@ -61,6 +77,41 @@ function showText(api: TuiPluginApi, text: string) {
61
77
  ))
62
78
  }
63
79
 
80
+ /**
81
+ * Format the per-account status lines shown in the Claude prime Status
82
+ * view. Pure: consumed by the dialog's Status pane; the sidebar uses a
83
+ * different formatter (`formatPrimeSidebarValue`) for its one-line
84
+ * expanded row.
85
+ */
86
+ export function buildPrimeStatusRows(accounts: PrimeAccountStatus[]): string[] {
87
+ const rows: string[] = []
88
+ for (const account of accounts) {
89
+ if (account.usage?.count) {
90
+ const cost = account.estimatedCostUsd ?? 0
91
+ rows.push(
92
+ `${account.label}: ${account.usage.count} ${account.usage.count === 1 ? 'prime' : 'primes'} \u2248 $${formatPrimeCost(cost)}`,
93
+ )
94
+ }
95
+ if (account.nextDueAt && account.nextDueAt > Date.now()) {
96
+ rows.push(
97
+ `${account.label} \u00b7 next prime ${formatPrimeTime(account.nextDueAt)}`,
98
+ )
99
+ } else if (account.lastPrimedAt) {
100
+ const time = formatPrimeTime(account.lastPrimedAt)
101
+ if (account.lastResult === 'error') {
102
+ rows.push(`${account.label} \u00b7 primed ${time} err`)
103
+ } else {
104
+ rows.push(`${account.label} \u00b7 primed ${time} \u2713`)
105
+ }
106
+ } else if (account.usage?.count) {
107
+ // already shown above
108
+ } else {
109
+ rows.push(`${account.label} \u2014 window active`)
110
+ }
111
+ }
112
+ return rows
113
+ }
114
+
64
115
  export function openCommandDialog(
65
116
  api: TuiPluginApi,
66
117
  payload: OpenDialogPayload,
@@ -205,6 +256,67 @@ export function openCommandDialog(
205
256
  return
206
257
  }
207
258
 
259
+ if (payload.command === 'claude-prime') {
260
+ // Prime modal spec: always four options — Enable / Disable / Status /
261
+ // Back — regardless of current state (the contextual toggle
262
+ // approach is forbidden). Status uses a DialogSelect replace with
263
+ // a single Back action (killswitch dialog-replace pattern), so the
264
+ // user has a working affordance to return to the main menu.
265
+ const DialogSelect = api.ui.DialogSelect<string>
266
+ const openStatusView = () => {
267
+ const accounts =
268
+ (payload.knobs.accounts as PrimeAccountStatus[] | undefined) ?? []
269
+ const lines = buildPrimeStatusRows(accounts)
270
+ const statusText = `Claude prime status:\n\n${lines.join('\n')}`
271
+ api.ui.dialog.setSize('xlarge')
272
+ api.ui.dialog.replace(() => (
273
+ <box flexDirection='column' padding={1} width='100%'>
274
+ <text>{statusText}</text>
275
+ <box marginTop={1}>
276
+ <DialogSelect
277
+ title='Claude prime — status'
278
+ current='back'
279
+ options={[{ title: 'Back', value: 'back' }]}
280
+ onSelect={(option) => handlePrimeStatusOption(option, renderMain)}
281
+ />
282
+ </box>
283
+ </box>
284
+ ))
285
+ }
286
+ const renderMain = () => {
287
+ const enabled = payload.knobs.enabled === true
288
+ api.ui.dialog.setSize('xlarge')
289
+ api.ui.dialog.replace(() => (
290
+ <DialogSelect
291
+ title='Claude prime'
292
+ current={enabled ? 'on' : 'off'}
293
+ options={PRIME_DIALOG_OPTIONS}
294
+ onSelect={(option) => {
295
+ if (option.value === 'back') {
296
+ api.ui.dialog.clear()
297
+ return
298
+ }
299
+ if (option.value === 'status') {
300
+ openStatusView()
301
+ return
302
+ }
303
+ void apply('claude-prime', String(option.value)).then((r) => {
304
+ api.ui.toast({ message: r.text })
305
+ payload = {
306
+ command: 'claude-prime',
307
+ text: r.text,
308
+ knobs: r.knobs,
309
+ }
310
+ renderMain()
311
+ })
312
+ }}
313
+ />
314
+ ))
315
+ }
316
+ renderMain()
317
+ return
318
+ }
319
+
208
320
  if (payload.command === 'claude-killswitch') {
209
321
  const config = (payload.knobs.config ?? {}) as KillswitchDialogConfig
210
322
  const accountIds = (payload.knobs.accountIds as string[]) ?? []
@@ -1,13 +1,18 @@
1
- export type CommandModalName =
2
- | 'claude-account'
3
- | 'claude-cache'
4
- | 'claude-cachekeep'
5
- | 'claude-quota'
6
- | 'claude-dump'
7
- | 'claude-fast'
8
- | 'claude-routing'
9
- | 'claude-killswitch'
10
- | 'claude-logging'
1
+ export const COMMAND_MODAL_NAMES = [
2
+ 'claude-account',
3
+ 'claude-cache',
4
+ 'claude-cachekeep',
5
+ 'claude-prime',
6
+ 'claude-start',
7
+ 'claude-quota',
8
+ 'claude-dump',
9
+ 'claude-fast',
10
+ 'claude-routing',
11
+ 'claude-killswitch',
12
+ 'claude-logging',
13
+ ] as const
14
+
15
+ export type CommandModalName = (typeof COMMAND_MODAL_NAMES)[number]
11
16
 
12
17
  export interface OpenDialogPayload {
13
18
  command: CommandModalName
@@ -52,6 +52,16 @@ export interface FableRecoverySidebarState {
52
52
  targetModelId?: string
53
53
  }
54
54
 
55
+ export interface PrimeSidebarAccountState {
56
+ id: string
57
+ label: string
58
+ nextDueAt?: number | null
59
+ lastPrimedAt?: number | null
60
+ lastResult?: 'ok' | 'error'
61
+ usage?: PrimeUsageCounters
62
+ estimatedCostUsd?: number
63
+ }
64
+
55
65
  export interface SidebarState {
56
66
  main: {
57
67
  quota: AccountQuota | null
@@ -71,6 +81,15 @@ export interface SidebarState {
71
81
  window?: string
72
82
  trackedSessions?: number
73
83
  }
84
+ /**
85
+ * Prime opt-in flag plus per-account status. Absent on the wire when
86
+ * the feature is disabled — `normalizeSidebarState` validates every
87
+ * account independently and drops any entry it cannot prove.
88
+ */
89
+ prime?: {
90
+ enabled: boolean
91
+ accounts: PrimeSidebarAccountState[]
92
+ }
74
93
  fableRecoveries?: FableRecoverySidebarState[]
75
94
  lastUpdated: number
76
95
  }
@@ -88,6 +107,7 @@ import {
88
107
  } from 'node:fs/promises'
89
108
  import { tmpdir } from 'node:os'
90
109
  import { dirname, join } from 'node:path'
110
+ import type { PrimeUsageCounters } from '@cortexkit/anthropic-auth-core'
91
111
  import { logger } from '@cortexkit/anthropic-auth-core'
92
112
 
93
113
  const STATE_FILE_ENV = 'OPENCODE_ANTHROPIC_AUTH_SIDEBAR_STATE_FILE'
@@ -111,6 +131,77 @@ function isRecord(v: unknown): v is Record<string, unknown> {
111
131
  return v !== null && typeof v === 'object' && !Array.isArray(v)
112
132
  }
113
133
 
134
+ function isFiniteNumber(value: unknown): value is number {
135
+ return typeof value === 'number' && Number.isFinite(value)
136
+ }
137
+
138
+ function normalizePrimeUsage(value: unknown): PrimeUsageCounters | undefined {
139
+ if (!isRecord(value)) return undefined
140
+ const count = Number(value.count)
141
+ const inputTokens = Number(value.inputTokens)
142
+ const outputTokens = Number(value.outputTokens)
143
+ const since = Number(value.since)
144
+ if (
145
+ ![count, inputTokens, outputTokens, since].every(Number.isFinite) ||
146
+ count < 0 ||
147
+ inputTokens < 0 ||
148
+ outputTokens < 0 ||
149
+ since < 0
150
+ ) {
151
+ return undefined
152
+ }
153
+ return {
154
+ count: Math.floor(count),
155
+ inputTokens: Math.floor(inputTokens),
156
+ outputTokens: Math.floor(outputTokens),
157
+ since: Math.floor(since),
158
+ }
159
+ }
160
+
161
+ function normalizePrimeAccount(
162
+ value: unknown,
163
+ ): PrimeSidebarAccountState | undefined {
164
+ if (!isRecord(value)) return undefined
165
+ if (typeof value.id !== 'string' || !value.id.trim()) return undefined
166
+ if (typeof value.label !== 'string' || !value.label.trim()) return undefined
167
+ const account: PrimeSidebarAccountState = {
168
+ id: value.id.trim(),
169
+ label: value.label.trim(),
170
+ }
171
+ if (value.nextDueAt === null) {
172
+ account.nextDueAt = null
173
+ } else if (isFiniteNumber(value.nextDueAt)) {
174
+ account.nextDueAt = value.nextDueAt
175
+ }
176
+ if (value.lastPrimedAt === null) {
177
+ account.lastPrimedAt = null
178
+ } else if (isFiniteNumber(value.lastPrimedAt)) {
179
+ account.lastPrimedAt = value.lastPrimedAt
180
+ }
181
+ if (value.lastResult === 'ok' || value.lastResult === 'error') {
182
+ account.lastResult = value.lastResult
183
+ }
184
+ const usage = normalizePrimeUsage(value.usage)
185
+ if (usage) account.usage = usage
186
+ if (isFiniteNumber(value.estimatedCostUsd)) {
187
+ account.estimatedCostUsd = value.estimatedCostUsd
188
+ }
189
+ return account
190
+ }
191
+
192
+ function normalizePrimeSection(
193
+ value: unknown,
194
+ ): SidebarState['prime'] | undefined {
195
+ if (!isRecord(value)) return undefined
196
+ if (typeof value.enabled !== 'boolean') return undefined
197
+ const accounts = Array.isArray(value.accounts)
198
+ ? value.accounts
199
+ .map(normalizePrimeAccount)
200
+ .filter((a): a is PrimeSidebarAccountState => a != null)
201
+ : []
202
+ return { enabled: value.enabled, accounts }
203
+ }
204
+
114
205
  function normalizeQuotaWindow(value: unknown): QuotaWindow | undefined {
115
206
  if (!isRecord(value)) return undefined
116
207
  const usedPercent = Number(value.usedPercent)
@@ -322,6 +413,7 @@ export function normalizeSidebarState(raw: unknown): SidebarState {
322
413
  ? raw.fastMode
323
414
  : DEFAULT_SIDEBAR_STATE.fastMode,
324
415
  cacheKeep,
416
+ prime: normalizePrimeSection(raw.prime),
325
417
  fableRecoveries: fableRecoveries.length > 0 ? fableRecoveries : undefined,
326
418
  lastUpdated: typeof raw.lastUpdated === 'number' ? raw.lastUpdated : 0,
327
419
  }
@@ -348,12 +440,20 @@ interface SidebarStateWriteTestHooks {
348
440
  lockRetryMaxMs?: number
349
441
  }
350
442
 
351
- let sidebarStateWriteTestHooks: SidebarStateWriteTestHooks | null = null
443
+ const sidebarStateWriteTestHooks = new Map<string, SidebarStateWriteTestHooks>()
352
444
 
353
445
  export function __setSidebarStateWriteTestHooks(
354
446
  hooks: SidebarStateWriteTestHooks | null,
447
+ stateFile = getSidebarStateFile(),
355
448
  ): void {
356
- sidebarStateWriteTestHooks = hooks
449
+ if (hooks) sidebarStateWriteTestHooks.set(stateFile, hooks)
450
+ else sidebarStateWriteTestHooks.delete(stateFile)
451
+ }
452
+
453
+ function sidebarStateWriteHooksFor(
454
+ stateFile: string,
455
+ ): SidebarStateWriteTestHooks | undefined {
456
+ return sidebarStateWriteTestHooks.get(stateFile)
357
457
  }
358
458
 
359
459
  // Boot-routing test hooks live here rather than in index.ts: the plugin entry
@@ -388,12 +488,10 @@ async function acquireSidebarStateLock(stateFile: string): Promise<{
388
488
  ownsLock: () => Promise<boolean>
389
489
  } | null> {
390
490
  const lockDir = `${stateFile}.lock`
391
- const budgetMs =
392
- sidebarStateWriteTestHooks?.lockBudgetMs ?? SIDEBAR_LOCK_BUDGET_MS
393
- const retryMinMs =
394
- sidebarStateWriteTestHooks?.lockRetryMinMs ?? SIDEBAR_LOCK_RETRY_MIN_MS
395
- const retryMaxMs =
396
- sidebarStateWriteTestHooks?.lockRetryMaxMs ?? SIDEBAR_LOCK_RETRY_MAX_MS
491
+ const testHooks = sidebarStateWriteHooksFor(stateFile)
492
+ const budgetMs = testHooks?.lockBudgetMs ?? SIDEBAR_LOCK_BUDGET_MS
493
+ const retryMinMs = testHooks?.lockRetryMinMs ?? SIDEBAR_LOCK_RETRY_MIN_MS
494
+ const retryMaxMs = testHooks?.lockRetryMaxMs ?? SIDEBAR_LOCK_RETRY_MAX_MS
397
495
  const deadline = Date.now() + budgetMs
398
496
 
399
497
  while (true) {
@@ -406,7 +504,7 @@ async function acquireSidebarStateLock(stateFile: string): Promise<{
406
504
  mode: 0o600,
407
505
  flag: 'wx',
408
506
  })
409
- await sidebarStateWriteTestHooks?.onLockAcquired?.(lockDir)
507
+ await testHooks?.onLockAcquired?.(lockDir)
410
508
  return {
411
509
  ownsLock: async () => {
412
510
  try {
@@ -424,9 +522,7 @@ async function acquireSidebarStateLock(stateFile: string): Promise<{
424
522
  } catch {
425
523
  return
426
524
  }
427
- await sidebarStateWriteTestHooks?.beforeReleaseDirectoryRemoval?.(
428
- lockDir,
429
- )
525
+ await testHooks?.beforeReleaseDirectoryRemoval?.(lockDir)
430
526
  await rmdir(lockDir).catch(() => {})
431
527
  },
432
528
  }
@@ -443,7 +539,7 @@ async function acquireSidebarStateLock(stateFile: string): Promise<{
443
539
  try {
444
540
  const lockStat = await stat(lockDir)
445
541
  if (Date.now() - lockStat.mtimeMs > SIDEBAR_LOCK_STALE_MS) {
446
- await sidebarStateWriteTestHooks?.afterStaleLockStat?.(lockDir)
542
+ await testHooks?.afterStaleLockStat?.(lockDir)
447
543
  const evictedLockDir = `${lockDir}.evict-${process.pid}-${randomUUID()}`
448
544
  try {
449
545
  await rename(lockDir, evictedLockDir)
@@ -451,7 +547,7 @@ async function acquireSidebarStateLock(stateFile: string): Promise<{
451
547
  if ((error as NodeJS.ErrnoException).code === 'ENOENT') continue
452
548
  throw error
453
549
  }
454
- await sidebarStateWriteTestHooks?.onStaleLockClaimed?.(lockDir)
550
+ await testHooks?.onStaleLockClaimed?.(lockDir)
455
551
  await rm(evictedLockDir, { recursive: true, force: true }).catch(
456
552
  () => {},
457
553
  )
@@ -491,6 +587,7 @@ async function writeSidebarStateAtomic(
491
587
  state: SidebarState,
492
588
  ownsLock: () => Promise<boolean>,
493
589
  ): Promise<'written' | 'lock-lost-before-rename' | 'lock-lost-after-rename'> {
590
+ const testHooks = sidebarStateWriteHooksFor(stateFile)
494
591
  const tempFile = `${stateFile}.${process.pid}.${randomUUID()}.tmp`
495
592
  await writeFile(tempFile, JSON.stringify(state), {
496
593
  encoding: 'utf8',
@@ -504,7 +601,7 @@ async function writeSidebarStateAtomic(
504
601
  await rm(tempFile, { force: true }).catch(() => {})
505
602
  return 'lock-lost-before-rename'
506
603
  }
507
- await sidebarStateWriteTestHooks?.beforeRename?.(stateFile, tempFile)
604
+ await testHooks?.beforeRename?.(stateFile, tempFile)
508
605
  // No production await separates this ownership fence from rename. A process
509
606
  // freeze between the adjacent syscalls remains possible, so rename is also
510
607
  // fenced from the other side below.
@@ -516,7 +613,7 @@ async function writeSidebarStateAtomic(
516
613
  return 'lock-lost-before-rename'
517
614
  }
518
615
  await rename(tempFile, stateFile)
519
- await sidebarStateWriteTestHooks?.afterRename?.(stateFile)
616
+ await testHooks?.afterRename?.(stateFile)
520
617
  if (!(await ownsLock())) return 'lock-lost-after-rename'
521
618
  return 'written'
522
619
  } catch (error) {
@@ -601,7 +698,9 @@ export async function setSidebarState(
601
698
  ),
602
699
  }
603
700
  }
604
- await sidebarStateWriteTestHooks?.afterMergeRead?.(stateFile)
701
+ await sidebarStateWriteHooksFor(stateFile)?.afterMergeRead?.(
702
+ stateFile,
703
+ )
605
704
  } else if (repairingPostRenameLoss) {
606
705
  const current = await getSidebarState(stateFile)
607
706
  // A successor owns its fresh account/quota and recovery snapshots;
@@ -710,6 +809,44 @@ export function formatFallbackModelLabel(modelId: string | undefined): string {
710
809
  return modelId ?? 'Fable 5'
711
810
  }
712
811
 
812
+ export function formatPrimeTime(epochMs: number): string {
813
+ return new Date(epochMs).toLocaleTimeString([], {
814
+ hour: '2-digit',
815
+ minute: '2-digit',
816
+ })
817
+ }
818
+
819
+ export function formatPrimeCost(value: number): string {
820
+ if (value === 0) return '0'
821
+ if (value < 0.0001) return value.toExponential(2)
822
+ return value.toFixed(Math.min(6, Math.max(0, 4)))
823
+ }
824
+
825
+ export function formatPrimeAccountValue(account: PrimeSidebarAccountState): {
826
+ text: string
827
+ hasError: boolean
828
+ } {
829
+ if (account.lastResult === 'error') {
830
+ return { text: 'err', hasError: true }
831
+ }
832
+ if (account.nextDueAt && account.nextDueAt > Date.now()) {
833
+ return { text: formatPrimeTime(account.nextDueAt), hasError: false }
834
+ }
835
+ if (account.lastPrimedAt) {
836
+ return {
837
+ text: `primed ${formatPrimeTime(account.lastPrimedAt)} \u2713`,
838
+ hasError: false,
839
+ }
840
+ }
841
+ if (account.usage?.count) {
842
+ return {
843
+ text: `\u2713 ${account.usage.count} \u2248 $${formatPrimeCost(account.estimatedCostUsd ?? 0)}`,
844
+ hasError: false,
845
+ }
846
+ }
847
+ return { text: '\u2014', hasError: false }
848
+ }
849
+
713
850
  export function getFableRecoverySummary(
714
851
  state: SidebarState,
715
852
  sessionId: string,
@@ -6,6 +6,23 @@ import { setProp as _$setProp } from "opentui:runtime-module:%40opentui%2Fsolid"
6
6
  import { createElement as _$createElement } from "opentui:runtime-module:%40opentui%2Fsolid";
7
7
  /** @jsxImportSource @opentui/solid */
8
8
 
9
+ import { formatPrimeCost, formatPrimeTime } from "../sidebar-state.js";
10
+ export const PRIME_DIALOG_OPTIONS = [{
11
+ title: 'Enable',
12
+ value: 'on'
13
+ }, {
14
+ title: 'Disable',
15
+ value: 'off'
16
+ }, {
17
+ title: 'Status',
18
+ value: 'status'
19
+ }, {
20
+ title: 'Back',
21
+ value: 'back'
22
+ }];
23
+ export function handlePrimeStatusOption(option, renderMain) {
24
+ if (option.value === 'back') renderMain();
25
+ }
9
26
  export function buildKillswitchThresholdSeed(config, accountIds) {
10
27
  const readT = t => {
11
28
  const fh = t?.five_hour ?? t?.['5h'] ?? 5;
@@ -49,6 +66,37 @@ function showText(api, text) {
49
66
  return _el$;
50
67
  })());
51
68
  }
69
+
70
+ /**
71
+ * Format the per-account status lines shown in the Claude prime Status
72
+ * view. Pure: consumed by the dialog's Status pane; the sidebar uses a
73
+ * different formatter (`formatPrimeSidebarValue`) for its one-line
74
+ * expanded row.
75
+ */
76
+ export function buildPrimeStatusRows(accounts) {
77
+ const rows = [];
78
+ for (const account of accounts) {
79
+ if (account.usage?.count) {
80
+ const cost = account.estimatedCostUsd ?? 0;
81
+ rows.push(`${account.label}: ${account.usage.count} ${account.usage.count === 1 ? 'prime' : 'primes'} \u2248 $${formatPrimeCost(cost)}`);
82
+ }
83
+ if (account.nextDueAt && account.nextDueAt > Date.now()) {
84
+ rows.push(`${account.label} \u00b7 next prime ${formatPrimeTime(account.nextDueAt)}`);
85
+ } else if (account.lastPrimedAt) {
86
+ const time = formatPrimeTime(account.lastPrimedAt);
87
+ if (account.lastResult === 'error') {
88
+ rows.push(`${account.label} \u00b7 primed ${time} err`);
89
+ } else {
90
+ rows.push(`${account.label} \u00b7 primed ${time} \u2713`);
91
+ }
92
+ } else if (account.usage?.count) {
93
+ // already shown above
94
+ } else {
95
+ rows.push(`${account.label} \u2014 window active`);
96
+ }
97
+ }
98
+ return rows;
99
+ }
52
100
  export function openCommandDialog(api, payload, apply) {
53
101
  if (payload.command === 'claude-routing') {
54
102
  const current = payload.knobs.mode ?? 'main-first';
@@ -179,6 +227,74 @@ export function openCommandDialog(api, payload, apply) {
179
227
  }));
180
228
  return;
181
229
  }
230
+ if (payload.command === 'claude-prime') {
231
+ // Prime modal spec: always four options — Enable / Disable / Status /
232
+ // Back — regardless of current state (the contextual toggle
233
+ // approach is forbidden). Status uses a DialogSelect replace with
234
+ // a single Back action (killswitch dialog-replace pattern), so the
235
+ // user has a working affordance to return to the main menu.
236
+ const DialogSelect = api.ui.DialogSelect;
237
+ const openStatusView = () => {
238
+ const accounts = payload.knobs.accounts ?? [];
239
+ const lines = buildPrimeStatusRows(accounts);
240
+ const statusText = `Claude prime status:\n\n${lines.join('\n')}`;
241
+ api.ui.dialog.setSize('xlarge');
242
+ api.ui.dialog.replace(() => (() => {
243
+ var _el$4 = _$createElement("box"),
244
+ _el$5 = _$createElement("text"),
245
+ _el$6 = _$createElement("box");
246
+ _$insertNode(_el$4, _el$5);
247
+ _$insertNode(_el$4, _el$6);
248
+ _$setProp(_el$4, "flexDirection", 'column');
249
+ _$setProp(_el$4, "padding", 1);
250
+ _$setProp(_el$4, "width", '100%');
251
+ _$insert(_el$5, statusText);
252
+ _$setProp(_el$6, "marginTop", 1);
253
+ _$insert(_el$6, _$createComponent(DialogSelect, {
254
+ title: "Claude prime \u2014 status",
255
+ current: "back",
256
+ options: [{
257
+ title: 'Back',
258
+ value: 'back'
259
+ }],
260
+ onSelect: option => handlePrimeStatusOption(option, renderMain)
261
+ }));
262
+ return _el$4;
263
+ })());
264
+ };
265
+ const renderMain = () => {
266
+ const enabled = payload.knobs.enabled === true;
267
+ api.ui.dialog.setSize('xlarge');
268
+ api.ui.dialog.replace(() => _$createComponent(DialogSelect, {
269
+ title: "Claude prime",
270
+ current: enabled ? 'on' : 'off',
271
+ options: PRIME_DIALOG_OPTIONS,
272
+ onSelect: option => {
273
+ if (option.value === 'back') {
274
+ api.ui.dialog.clear();
275
+ return;
276
+ }
277
+ if (option.value === 'status') {
278
+ openStatusView();
279
+ return;
280
+ }
281
+ void apply('claude-prime', String(option.value)).then(r => {
282
+ api.ui.toast({
283
+ message: r.text
284
+ });
285
+ payload = {
286
+ command: 'claude-prime',
287
+ text: r.text,
288
+ knobs: r.knobs
289
+ };
290
+ renderMain();
291
+ });
292
+ }
293
+ }));
294
+ };
295
+ renderMain();
296
+ return;
297
+ }
182
298
  if (payload.command === 'claude-killswitch') {
183
299
  const config = payload.knobs.config ?? {};
184
300
  const accountIds = payload.knobs.accountIds ?? [];
@@ -190,9 +306,9 @@ export function openCommandDialog(api, payload, apply) {
190
306
  api.ui.dialog.replace(() => _$createComponent(DialogPrompt, {
191
307
  title: "Killswitch thresholds",
192
308
  description: () => (() => {
193
- var _el$4 = _$createElement("text");
194
- _$insert(_el$4, () => payload.text);
195
- return _el$4;
309
+ var _el$7 = _$createElement("text");
310
+ _$insert(_el$7, () => payload.text);
311
+ return _el$7;
196
312
  })(),
197
313
  placeholder: "main:5,10,0 work-alt:5,10,0",
198
314
  value: seed,
@@ -313,9 +429,9 @@ export function openCommandDialog(api, payload, apply) {
313
429
  api.ui.dialog.replace(() => _$createComponent(DialogPrompt, {
314
430
  title: "Add API key account \\u2014 API key",
315
431
  description: () => (() => {
316
- var _el$5 = _$createElement("text");
317
- _$insertNode(_el$5, _$createTextNode(`Paste your API key (required).`));
318
- return _el$5;
432
+ var _el$8 = _$createElement("text");
433
+ _$insertNode(_el$8, _$createTextNode(`Paste your API key (required).`));
434
+ return _el$8;
319
435
  })(),
320
436
  placeholder: "sk-ant-...",
321
437
  value: "",
@@ -337,9 +453,9 @@ export function openCommandDialog(api, payload, apply) {
337
453
  api.ui.dialog.replace(() => _$createComponent(DialogPrompt, {
338
454
  title: "Add API key account \\u2014 base URL",
339
455
  description: () => (() => {
340
- var _el$7 = _$createElement("text");
341
- _$insertNode(_el$7, _$createTextNode(`Anthropic-compatible API base URL. Default: https://api.kie.ai/claude`));
342
- return _el$7;
456
+ var _el$0 = _$createElement("text");
457
+ _$insertNode(_el$0, _$createTextNode(`Anthropic-compatible API base URL. Default: https://api.kie.ai/claude`));
458
+ return _el$0;
343
459
  })(),
344
460
  placeholder: "https://api.kie.ai/claude",
345
461
  value: "",
@@ -377,9 +493,9 @@ export function openCommandDialog(api, payload, apply) {
377
493
  api.ui.dialog.replace(() => _$createComponent(DialogPrompt, {
378
494
  title: "Add API key account \\u2014 label",
379
495
  description: () => (() => {
380
- var _el$9 = _$createElement("text");
381
- _$insertNode(_el$9, _$createTextNode(`A short name for this account (optional).`));
382
- return _el$9;
496
+ var _el$10 = _$createElement("text");
497
+ _$insertNode(_el$10, _$createTextNode(`A short name for this account (optional).`));
498
+ return _el$10;
383
499
  })(),
384
500
  placeholder: "e.g. Work API",
385
501
  value: "",
@@ -473,9 +589,9 @@ export function openCommandDialog(api, payload, apply) {
473
589
  api.ui.dialog.replace(() => _$createComponent(DialogPrompt, {
474
590
  title: "OAuth sign-in \\u2014 enter code",
475
591
  description: () => (() => {
476
- var _el$1 = _$createElement("text");
477
- _$insertNode(_el$1, _$createTextNode(`After signing in you will be redirected. Paste the full callback URL or authorization code below.`));
478
- return _el$1;
592
+ var _el$12 = _$createElement("text");
593
+ _$insertNode(_el$12, _$createTextNode(`After signing in you will be redirected. Paste the full callback URL or authorization code below.`));
594
+ return _el$12;
479
595
  })(),
480
596
  placeholder: "Paste callback URL or code here",
481
597
  value: "",
@@ -496,9 +612,9 @@ export function openCommandDialog(api, payload, apply) {
496
612
  api.ui.dialog.replace(() => _$createComponent(DialogPrompt, {
497
613
  title: "OAuth sign-in \\u2014 label",
498
614
  description: () => (() => {
499
- var _el$11 = _$createElement("text");
500
- _$insertNode(_el$11, _$createTextNode(`A short name for this account (optional).`));
501
- return _el$11;
615
+ var _el$14 = _$createElement("text");
616
+ _$insertNode(_el$14, _$createTextNode(`A short name for this account (optional).`));
617
+ return _el$14;
502
618
  })(),
503
619
  placeholder: "e.g. work",
504
620
  value: "",