@cortexkit/opencode-anthropic-auth 1.22.0 → 2.0.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 (91) hide show
  1. package/README.md +259 -198
  2. package/dist/billing-lineage.d.ts +51 -0
  3. package/dist/cli-failure.d.ts +69 -0
  4. package/dist/cli.d.ts +16 -10
  5. package/dist/cli.js +111 -6
  6. package/dist/custody-dimensions.d.ts +19 -0
  7. package/dist/custody-mode.d.ts +46 -0
  8. package/dist/effort-history.d.ts +11 -2
  9. package/dist/fable-fallback.d.ts +1 -1
  10. package/dist/index-1jkyhgyr.js +2 -0
  11. package/dist/{index-bx5hsgf8.js → index-2arfb1y3.js} +2 -2
  12. package/dist/{index-926t2bzc.js → index-4ya27n12.js} +1 -1
  13. package/dist/{index-cgez4m0h.js → index-aaajxwzd.js} +1 -1
  14. package/dist/index-ae3fq4n6.js +42 -0
  15. package/dist/index-fds0s2j4.js +2 -0
  16. package/dist/index-nngmvczq.js +450 -0
  17. package/dist/index-pn597b6f.js +2 -0
  18. package/dist/{index-7f57ewkp.js → index-rqync0pc.js} +1 -1
  19. package/dist/index.js +1 -33
  20. package/dist/native-command.d.ts +19 -0
  21. package/dist/prime-manager-registry.d.ts +5 -1
  22. package/dist/rpc/notifications.d.ts +4 -3
  23. package/dist/rpc/port-file.js +1 -1
  24. package/dist/rpc/protocol.d.ts +15 -4
  25. package/dist/rpc/protocol.js +1 -1
  26. package/dist/rpc/rpc-client.d.ts +3 -1
  27. package/dist/rpc/rpc-client.js +1 -1
  28. package/dist/rpc/rpc-dir.js +1 -1
  29. package/dist/rpc/rpc-server.d.ts +5 -1
  30. package/dist/rpc/server-registry.d.ts +10 -0
  31. package/dist/setup/activation.d.ts +7 -0
  32. package/dist/setup/claustrum.d.ts +26 -0
  33. package/dist/setup/command-runner.d.ts +2 -0
  34. package/dist/setup/command.d.ts +20 -0
  35. package/dist/setup/command.test.d.ts +1 -0
  36. package/dist/setup/detect.d.ts +5 -0
  37. package/dist/setup/jsonc.d.ts +9 -0
  38. package/dist/setup/native-activation.d.ts +29 -0
  39. package/dist/setup/native-enrollment.d.ts +26 -0
  40. package/dist/setup/native-enrollment.test.d.ts +1 -0
  41. package/dist/setup/native-local.d.ts +20 -0
  42. package/dist/setup/native-login.d.ts +3 -0
  43. package/dist/setup/native-paths.d.ts +15 -0
  44. package/dist/setup/native-paths.test.d.ts +1 -0
  45. package/dist/setup/opencode-config.d.ts +18 -0
  46. package/dist/setup/paths.d.ts +15 -0
  47. package/dist/setup/pi.d.ts +18 -0
  48. package/dist/setup/process-fence.d.ts +20 -0
  49. package/dist/setup/process-fence.test.d.ts +1 -0
  50. package/dist/setup/types.d.ts +37 -0
  51. package/dist/sidebar-state.d.ts +3 -1
  52. package/dist/sidebar-state.js +1 -1
  53. package/dist/transform.d.ts +5 -1
  54. package/dist/tui/command-dialogs.d.ts +32 -2
  55. package/dist/tui-preferences.js +2 -2
  56. package/dist/v2/activation.d.ts +7 -0
  57. package/dist/v2/adapter.d.ts +276 -0
  58. package/dist/v2/install.d.ts +13 -0
  59. package/dist/v2/lifecycle.d.ts +8 -0
  60. package/dist/v2/menu-contract.d.ts +8 -0
  61. package/dist/v2/menu.d.ts +10 -0
  62. package/dist/v2/models.d.ts +6 -0
  63. package/dist/v2/ownership.d.ts +11 -0
  64. package/dist/v2/prime.d.ts +12 -0
  65. package/dist/v2/rpc.d.ts +50 -0
  66. package/dist/v2/server.d.ts +8 -0
  67. package/dist/v2/server.js +4 -0
  68. package/dist/v2/setup.d.ts +58 -0
  69. package/dist/v2/source.d.ts +41 -0
  70. package/dist/v2/tui.d.ts +13 -0
  71. package/dist/v2/tui.js +2 -0
  72. package/dist/v2/wire.d.ts +36 -0
  73. package/package.json +26 -6
  74. package/src/rpc/notifications.ts +37 -26
  75. package/src/rpc/protocol.ts +24 -7
  76. package/src/rpc/rpc-client.ts +18 -1
  77. package/src/rpc/rpc-server.ts +38 -8
  78. package/src/rpc/server-registry.ts +61 -0
  79. package/src/sidebar-state.ts +26 -2
  80. package/src/tui/command-dialogs.tsx +537 -68
  81. package/src/tui/entry.mjs +44 -18
  82. package/src/tui-compiled/rpc/protocol.ts +24 -7
  83. package/src/tui-compiled/rpc/rpc-client.ts +18 -1
  84. package/src/tui-compiled/sidebar-state.ts +26 -2
  85. package/src/tui-compiled/tui/command-dialogs.tsx +488 -95
  86. package/src/tui-compiled/tui.tsx +25 -7
  87. package/src/tui.tsx +38 -9
  88. package/dist/index-8aga2zwv.js +0 -4
  89. package/dist/index-d3f8g6ve.js +0 -428
  90. package/dist/index-sh51yqxs.js +0 -2
  91. package/dist/logger-jrkranbz.js +0 -1
@@ -1,10 +1,277 @@
1
1
  /** @jsxImportSource @opentui/solid */
2
- import type { PrimeAccountStatus } from '@cortexkit/anthropic-auth-core'
2
+
3
+ import type {
4
+ CommandApplyRequest,
5
+ CommandApplyResult,
6
+ CommandDialogPayload,
7
+ CommandMenuModel,
8
+ KnobValue,
9
+ MenuAction,
10
+ MenuItem,
11
+ MenuSection,
12
+ } from '@cortexkit/anthropic-auth-core'
13
+ import {
14
+ custodyStatusLabel,
15
+ type PrimeAccountStatus,
16
+ } from '@cortexkit/anthropic-auth-core'
3
17
  import type { TuiPluginApi } from '@opencode-ai/plugin/tui'
4
18
  import type { AccountDialogAccount } from '../rpc/protocol'
5
19
  import type { OpenDialogPayload } from '../rpc/protocol.js'
6
20
  import { formatPrimeCost, formatPrimeTime } from '../sidebar-state.js'
7
21
 
22
+ /** Render menu sections, accounts and action inputs; send changes to the host instead of writing credentials here. */
23
+ export function openNativeMenuDialog(
24
+ api: {
25
+ readonly ui: Pick<
26
+ TuiPluginApi['ui'],
27
+ 'dialog' | 'DialogSelect' | 'DialogPrompt' | 'DialogConfirm' | 'toast'
28
+ >
29
+ },
30
+ payload: CommandDialogPayload,
31
+ apply: (request: CommandApplyRequest) => Promise<CommandApplyResult>,
32
+ isCurrentSession: () => boolean,
33
+ ): void {
34
+ let menu: CommandMenuModel = payload.menu
35
+ let inFlight = false
36
+ const DialogSelect = api.ui.DialogSelect<string>
37
+ const DialogPrompt = api.ui.DialogPrompt
38
+ const DialogConfirm = api.ui.DialogConfirm
39
+ const active = () => isCurrentSession() && !inFlight
40
+
41
+ const root = () => {
42
+ if (!active()) return
43
+ api.ui.dialog.setSize('xlarge')
44
+ api.ui.dialog.replace(() => (
45
+ <DialogSelect
46
+ title={menu.title}
47
+ options={[
48
+ ...menu.sections.map((section) => ({
49
+ title: section.title,
50
+ value: section.id,
51
+ })),
52
+ { title: 'Close', value: '__close' },
53
+ ]}
54
+ onSelect={(option) => {
55
+ if (!active()) return
56
+ const section = menu.sections.find(
57
+ (entry) => entry.id === option.value,
58
+ )
59
+ if (section) showSection(section)
60
+ else if (option.value === '__close') api.ui.dialog.clear()
61
+ }}
62
+ />
63
+ ))
64
+ }
65
+
66
+ const showSection = (section: MenuSection) => {
67
+ if (!active()) return
68
+ api.ui.dialog.replace(() => (
69
+ <box flexDirection='column' padding={1}>
70
+ <text>{section.lines.join('\n')}</text>
71
+ <DialogSelect
72
+ title={section.title}
73
+ options={[
74
+ ...section.items.map((item) => ({
75
+ title: item.label,
76
+ value: `item:${item.id}`,
77
+ description: item.detail,
78
+ })),
79
+ ...section.actions.map((action) => ({
80
+ title: action.label,
81
+ value: `action:${action.id}`,
82
+ description: action.description,
83
+ })),
84
+ { title: 'Back', value: '__back' },
85
+ ]}
86
+ onSelect={(option) => {
87
+ if (!active()) return
88
+ if (option.value === '__back') return root()
89
+ const action = section.actions.find(
90
+ (entry) => `action:${entry.id}` === option.value,
91
+ )
92
+ if (action) return collect(section, action)
93
+ const item = section.items.find(
94
+ (entry) => `item:${entry.id}` === option.value,
95
+ )
96
+ if (item) showItem(section, item)
97
+ }}
98
+ />
99
+ </box>
100
+ ))
101
+ }
102
+
103
+ const showItem = (section: MenuSection, item: MenuItem) => {
104
+ if (!active()) return
105
+ api.ui.dialog.replace(() => (
106
+ <box flexDirection='column' padding={1}>
107
+ <text>{item.detail ?? ''}</text>
108
+ <DialogSelect
109
+ title={item.label}
110
+ options={[
111
+ ...item.actions.map((action) => ({
112
+ title: action.label,
113
+ value: action.id,
114
+ description: action.description,
115
+ })),
116
+ { title: 'Back', value: '__back' },
117
+ ]}
118
+ onSelect={(option) => {
119
+ if (!active()) return
120
+ const action = item.actions.find(
121
+ (entry) => entry.id === option.value,
122
+ )
123
+ if (action) collect(section, action, item)
124
+ else if (option.value === '__back') showSection(section)
125
+ }}
126
+ />
127
+ </box>
128
+ ))
129
+ }
130
+
131
+ const collect = (
132
+ section: MenuSection,
133
+ action: MenuAction,
134
+ item?: MenuItem,
135
+ ) => {
136
+ const values: Record<string, KnobValue> = {}
137
+ const back = () => {
138
+ for (const key of Object.keys(values)) delete values[key]
139
+ if (item) showItem(section, item)
140
+ else showSection(section)
141
+ }
142
+ const submit = async () => {
143
+ if (!active()) return
144
+ inFlight = true
145
+ try {
146
+ const result = await apply({
147
+ command: menu.command,
148
+ sectionId: section.id,
149
+ actionId: action.id,
150
+ ...(item ? { itemId: item.id } : {}),
151
+ values: { ...values },
152
+ ...(action.confirm ? { confirmed: true } : {}),
153
+ })
154
+ if (!isCurrentSession()) return
155
+ menu = result.menu
156
+ api.ui.toast({ message: result.text })
157
+ } catch {
158
+ if (isCurrentSession())
159
+ api.ui.toast({ message: 'Menu apply unavailable; outcome unknown.' })
160
+ } finally {
161
+ for (const key of Object.keys(values)) delete values[key]
162
+ inFlight = false
163
+ if (isCurrentSession()) {
164
+ const refreshed = menu.sections.find(
165
+ (entry) => entry.id === section.id,
166
+ )
167
+ if (refreshed) showSection(refreshed)
168
+ else root()
169
+ }
170
+ }
171
+ }
172
+ const next = (index: number) => {
173
+ if (!active()) return
174
+ const knob = action.knobs[index]
175
+ if (!knob) {
176
+ if (!action.confirm) return void submit()
177
+ api.ui.dialog.replace(() => (
178
+ <DialogConfirm
179
+ title={action.label}
180
+ message={action.confirm?.message ?? ''}
181
+ onConfirm={() => void submit()}
182
+ onCancel={back}
183
+ />
184
+ ))
185
+ return
186
+ }
187
+ if (knob.kind === 'choice' || knob.kind === 'toggle') {
188
+ const choices =
189
+ knob.kind === 'choice'
190
+ ? knob.choices
191
+ : [
192
+ { value: 'on', label: 'On' },
193
+ { value: 'off', label: 'Off' },
194
+ ]
195
+ api.ui.dialog.replace(() => (
196
+ <DialogSelect
197
+ title={knob.label}
198
+ options={[
199
+ ...choices.map((choice) => ({
200
+ title: choice.label,
201
+ value: choice.value,
202
+ })),
203
+ { title: 'Cancel', value: '__cancel' },
204
+ ]}
205
+ onSelect={(option) => {
206
+ if (!active()) return
207
+ if (option.value === '__cancel') return back()
208
+ values[knob.id] =
209
+ knob.kind === 'toggle' ? option.value === 'on' : option.value
210
+ next(index + 1)
211
+ }}
212
+ />
213
+ ))
214
+ return
215
+ }
216
+ // DialogPrompt has no masking capability in the host API. Never seed a
217
+ // protected input or claim it is hidden; warn before collecting it.
218
+ api.ui.dialog.replace(() => (
219
+ <DialogPrompt
220
+ title={knob.label}
221
+ description={() => (
222
+ <text>
223
+ {knob.kind === 'text' && knob.masked
224
+ ? 'This host prompt displays input. Avoid screen sharing; the value is not echoed in menu results.'
225
+ : knob.required
226
+ ? 'Required input'
227
+ : 'Optional; leave empty for the default'}
228
+ </text>
229
+ )}
230
+ value={
231
+ knob.kind === 'text' && knob.masked
232
+ ? ''
233
+ : knob.value === undefined
234
+ ? ''
235
+ : String(knob.value)
236
+ }
237
+ placeholder={
238
+ knob.kind === 'text'
239
+ ? knob.placeholder
240
+ : `${knob.min ?? ''}-${knob.max ?? ''}`
241
+ }
242
+ onCancel={back}
243
+ onConfirm={(typed) => {
244
+ if (!active()) return
245
+ const text = typed.trim()
246
+ if (!text && knob.required) {
247
+ api.ui.toast({ message: `${knob.label} is required.` })
248
+ return
249
+ }
250
+ const value =
251
+ text === '' ? null : knob.kind === 'number' ? Number(text) : text
252
+ if (
253
+ knob.kind === 'number' &&
254
+ typeof value === 'number' &&
255
+ (!Number.isFinite(value) ||
256
+ (knob.min !== undefined && value < knob.min) ||
257
+ (knob.max !== undefined && value > knob.max))
258
+ ) {
259
+ api.ui.toast({
260
+ message: `${knob.label} needs a number in range.`,
261
+ })
262
+ return
263
+ }
264
+ values[knob.id] = value
265
+ next(index + 1)
266
+ }}
267
+ />
268
+ ))
269
+ }
270
+ next(0)
271
+ }
272
+ root()
273
+ }
274
+
8
275
  type ApplyFn = (
9
276
  command: OpenDialogPayload['command'],
10
277
  args: string,
@@ -16,6 +283,27 @@ type KillswitchDialogConfig = {
16
283
  accounts?: Record<string, Record<string, number>>
17
284
  }
18
285
 
286
+ type TuiAccountDialogAccount = Omit<
287
+ AccountDialogAccount,
288
+ 'vaultReauth' | 'custodyState'
289
+ > &
290
+ Partial<Pick<AccountDialogAccount, 'vaultReauth' | 'custodyState'>>
291
+
292
+ type TuiAccountDialogPayload = {
293
+ accounts: TuiAccountDialogAccount[]
294
+ claustrumDetection: string
295
+ custodyMode?: TuiCustodyMode
296
+ enrollmentStatus?: string
297
+ }
298
+
299
+ type TuiCustodyMode = 'local' | 'claustrum' | `mismatch: ${string}`
300
+
301
+ type AccountDialogOption = {
302
+ title: string
303
+ value: string
304
+ description?: string
305
+ }
306
+
19
307
  export const PRIME_DIALOG_OPTIONS = [
20
308
  { title: 'Enable', value: 'on' },
21
309
  { title: 'Disable', value: 'off' },
@@ -49,24 +337,189 @@ export function buildKillswitchThresholdSeed(
49
337
  return seedParts.join(' ')
50
338
  }
51
339
 
52
- export function buildAccountDialogOption(account: AccountDialogAccount) {
340
+ function normalizeTuiCustodyMode(value: string): TuiCustodyMode {
341
+ return value === 'local' || value === 'claustrum'
342
+ ? value
343
+ : `mismatch: ${value}`
344
+ }
345
+
346
+ export function normalizeAccountDialogPayload(
347
+ value: unknown,
348
+ ): TuiAccountDialogPayload {
349
+ const payload =
350
+ value && typeof value === 'object' ? (value as Record<string, unknown>) : {}
351
+ const rawAccounts = Array.isArray(payload.accounts) ? payload.accounts : []
352
+ const accounts = rawAccounts.flatMap((value) => {
353
+ if (!value || typeof value !== 'object') return []
354
+ const account = value as Record<string, unknown>
355
+ const quotaPercent = account.quotaPercent
356
+ if (
357
+ typeof account.id !== 'string' ||
358
+ typeof account.label !== 'string' ||
359
+ (account.role !== 'main' && account.role !== 'fallback') ||
360
+ typeof account.enabled !== 'boolean' ||
361
+ (quotaPercent !== null &&
362
+ (typeof quotaPercent !== 'number' || !Number.isFinite(quotaPercent))) ||
363
+ (account.claustrumGate !== 'on' &&
364
+ account.claustrumGate !== 'off' &&
365
+ account.claustrumGate !== 'na') ||
366
+ typeof account.vaultServed !== 'boolean'
367
+ ) {
368
+ return []
369
+ }
370
+ const normalized = {
371
+ id: account.id,
372
+ label: account.label,
373
+ role: account.role,
374
+ enabled: account.enabled,
375
+ quotaPercent,
376
+ ...(typeof account.tierLabel === 'string' && {
377
+ tierLabel: account.tierLabel,
378
+ }),
379
+ claustrumGate: account.claustrumGate,
380
+ vaultServed: account.vaultServed,
381
+ } satisfies TuiAccountDialogAccount
382
+ const custodyState = account.custodyState
383
+ if (
384
+ (custodyState === 'na' ||
385
+ custodyState === 'off' ||
386
+ custodyState === 'on-vault-served' ||
387
+ custodyState === 'on-vault-reauth' ||
388
+ custodyState === 'on-cold') &&
389
+ typeof account.vaultReauth === 'boolean'
390
+ ) {
391
+ return [
392
+ {
393
+ ...normalized,
394
+ custodyState,
395
+ vaultReauth: account.vaultReauth,
396
+ },
397
+ ]
398
+ }
399
+ return [normalized]
400
+ })
401
+ const custodyMode: TuiCustodyMode | undefined =
402
+ payload.custodyModeKnown === true && typeof payload.custodyMode === 'string'
403
+ ? normalizeTuiCustodyMode(payload.custodyMode)
404
+ : undefined
405
+ return {
406
+ accounts,
407
+ claustrumDetection:
408
+ typeof payload.claustrumDetection === 'string'
409
+ ? payload.claustrumDetection
410
+ : 'unknown',
411
+ ...(custodyMode && { custodyMode }),
412
+ ...(typeof payload.enrollmentStatus === 'string' &&
413
+ payload.enrollmentStatus.length <= 2_000 && {
414
+ enrollmentStatus: payload.enrollmentStatus,
415
+ }),
416
+ }
417
+ }
418
+
419
+ export function buildAccountDialogOption(account: TuiAccountDialogAccount) {
53
420
  const pct =
54
421
  account.quotaPercent != null
55
422
  ? ` ${Math.round(account.quotaPercent)}%`
56
423
  : ' \u2013%'
57
424
  const status = !account.enabled ? ' (disabled)' : ''
58
- const gate = ` · gate ${account.claustrumGate === 'na' ? 'n/a' : account.claustrumGate}`
59
- const vault =
60
- account.role === 'main'
61
- ? ' · vault n/a'
62
- : ` · vault ${account.vaultServed ? 'served' : 'cold'}`
425
+ const custody = account.custodyState
426
+ ? account.custodyState === 'na'
427
+ ? undefined
428
+ : custodyStatusLabel(account.custodyState).replace(
429
+ ' (OpenCode managed)',
430
+ '',
431
+ )
432
+ : undefined
63
433
  return {
64
- title: `${account.label} [${account.role}]${status}${pct}${gate}${vault}`,
434
+ title: `${account.label} [${account.role}]${status}${pct}${custody ? ` · custody ${custody}` : ''}`,
65
435
  value: account.id,
66
436
  ...(account.tierLabel && { description: account.tierLabel }),
67
437
  }
68
438
  }
69
439
 
440
+ export function buildManageAccountOptions(account: TuiAccountDialogAccount) {
441
+ const options: AccountDialogOption[] = []
442
+ const toggleLabel = account.enabled ? 'Disable' : 'Enable'
443
+ options.push({
444
+ title: toggleLabel,
445
+ value: account.enabled ? 'disable' : 'enable',
446
+ description: account.enabled
447
+ ? 'Stop using this fallback account'
448
+ : 'Allow this fallback account to be used',
449
+ })
450
+ options.push({
451
+ title: 'Move up',
452
+ value: 'move-up',
453
+ description: 'Higher priority in fallback order',
454
+ })
455
+ options.push({
456
+ title: 'Move down',
457
+ value: 'move-down',
458
+ description: 'Lower priority in fallback order',
459
+ })
460
+ options.push({
461
+ title: 'Remove\u2026',
462
+ value: 'remove',
463
+ description: 'Delete this account permanently',
464
+ })
465
+ options.push({ title: 'Back', value: 'back' })
466
+ return options
467
+ }
468
+
469
+ export function buildAccountDialogL1(value: unknown): {
470
+ header: string
471
+ options: AccountDialogOption[]
472
+ enrollmentStatus?: string
473
+ modeAction?: { command: 'claude-account'; arguments: 'local' | 'claustrum' }
474
+ } {
475
+ const payload = normalizeAccountDialogPayload(value)
476
+ const modeAction =
477
+ payload.custodyMode === 'local' || payload.custodyMode === 'claustrum'
478
+ ? {
479
+ command: 'claude-account' as const,
480
+ arguments:
481
+ payload.custodyMode === 'local'
482
+ ? ('claustrum' as const)
483
+ : ('local' as const),
484
+ }
485
+ : undefined
486
+ return {
487
+ header: payload.custodyMode
488
+ ? `Custody mode: ${payload.custodyMode}`
489
+ : 'Custody mode: unavailable from older server',
490
+ options: [
491
+ ...(modeAction
492
+ ? [
493
+ {
494
+ title:
495
+ payload.custodyMode === 'local'
496
+ ? 'Use Claustrum custody'
497
+ : 'Use local custody',
498
+ value: '__custody-mode__',
499
+ },
500
+ ]
501
+ : []),
502
+ {
503
+ title: 'Add account…',
504
+ value: '__add__',
505
+ description: 'Add an API key or OAuth fallback account',
506
+ },
507
+ ...payload.accounts.map(buildAccountDialogOption),
508
+ ],
509
+ ...(payload.enrollmentStatus && {
510
+ enrollmentStatus: payload.enrollmentStatus,
511
+ }),
512
+ ...(modeAction && { modeAction }),
513
+ }
514
+ }
515
+
516
+ export function retainAccountDialogProjection(
517
+ current: Record<string, unknown>,
518
+ incoming: Record<string, unknown>,
519
+ ): Record<string, unknown> {
520
+ return Array.isArray(incoming.accounts) ? incoming : current
521
+ }
522
+
70
523
  function showText(api: TuiPluginApi, text: string) {
71
524
  api.ui.dialog.setSize('xlarge')
72
525
  api.ui.dialog.replace(() => (
@@ -99,6 +552,8 @@ export function buildPrimeStatusRows(accounts: PrimeAccountStatus[]): string[] {
99
552
  const time = formatPrimeTime(account.lastPrimedAt)
100
553
  if (account.lastResult === 'error') {
101
554
  rows.push(`${account.label} \u00b7 primed ${time} err`)
555
+ } else if (account.lastResult === 'skipped') {
556
+ rows.push(`${account.label} \u00b7 primed ${time} skipped`)
102
557
  } else {
103
558
  rows.push(`${account.label} \u00b7 primed ${time} \u2713`)
104
559
  }
@@ -377,17 +832,17 @@ export function openCommandDialog(
377
832
  }
378
833
 
379
834
  if (payload.command === 'claude-account') {
380
- const accounts =
381
- (payload.knobs.accounts as AccountDialogAccount[] | undefined) ?? []
382
- const claustrumDetection =
383
- (payload.knobs.claustrumDetection as string | undefined) ?? 'unknown'
835
+ let accountKnobs: Record<string, unknown> = payload.knobs
836
+ const accounts = normalizeAccountDialogPayload(accountKnobs).accounts
837
+ let statusMessage: { text: string; error?: boolean } | undefined
384
838
 
385
839
  const updateAccounts = (r: {
386
840
  text: string
387
841
  knobs: Record<string, unknown>
388
842
  }) => {
389
- const updated = r.knobs.accounts as typeof accounts
390
- if (updated && updated.length > 0) {
843
+ accountKnobs = retainAccountDialogProjection(accountKnobs, r.knobs)
844
+ const updated = normalizeAccountDialogPayload(accountKnobs).accounts
845
+ if (updated.length > 0) {
391
846
  accounts.length = 0
392
847
  accounts.push(...updated)
393
848
  }
@@ -395,31 +850,61 @@ export function openCommandDialog(
395
850
 
396
851
  const buildL1 = () => {
397
852
  const DialogSelect = api.ui.DialogSelect<string>
398
- const l1Options: Array<{
399
- title: string
400
- value: string
401
- description?: string
402
- }> = [
403
- {
404
- title: 'Add account\u2026',
405
- value: '__add__',
406
- description: 'Add an API key or OAuth fallback account',
407
- },
408
- ...accounts.map(buildAccountDialogOption),
409
- ]
853
+ const l1 = buildAccountDialogL1({ ...accountKnobs, accounts })
410
854
  api.ui.dialog.setSize('xlarge')
411
855
  api.ui.dialog.replace(() => (
412
856
  <box flexDirection='column' padding={1} width='100%'>
413
- <text>{`Claustrum: ${claustrumDetection}`}</text>
857
+ <text>{`Claustrum: ${normalizeAccountDialogPayload(accountKnobs).claustrumDetection}`}</text>
858
+ <text>{l1.header}</text>
859
+ {l1.enrollmentStatus && (
860
+ <box marginTop={1}>
861
+ <text>{l1.enrollmentStatus}</text>
862
+ </box>
863
+ )}
864
+ {statusMessage && (
865
+ <box marginTop={1}>
866
+ <text fg={statusMessage.error ? '#ef4444' : '#22c55e'}>
867
+ {statusMessage.text}
868
+ </text>
869
+ </box>
870
+ )}
414
871
  <box marginTop={1}>
415
872
  <DialogSelect
416
873
  title='Claude accounts'
417
- options={l1Options}
874
+ options={l1.options}
418
875
  onSelect={(option) => {
419
876
  if (option.value === '__add__') {
877
+ statusMessage = undefined
420
878
  openAddType()
421
879
  return
422
880
  }
881
+ if (option.value === '__custody-mode__' && l1.modeAction) {
882
+ void apply(l1.modeAction.command, l1.modeAction.arguments)
883
+ .then((r) => {
884
+ const isRefusalOrError =
885
+ r.text.includes('refused') ||
886
+ r.text.includes('unavailable') ||
887
+ r.text.includes('failed') ||
888
+ r.text.includes('Error') ||
889
+ r.text.includes('error')
890
+ if (isRefusalOrError) {
891
+ statusMessage = { text: r.text, error: true }
892
+ } else {
893
+ statusMessage = { text: r.text, error: false }
894
+ api.ui.toast({ message: r.text })
895
+ }
896
+ updateAccounts(r)
897
+ buildL1()
898
+ })
899
+ .catch((err) => {
900
+ const msg =
901
+ err instanceof Error ? err.message : String(err)
902
+ statusMessage = { text: `Error: ${msg}`, error: true }
903
+ buildL1()
904
+ })
905
+ return
906
+ }
907
+ statusMessage = undefined
423
908
  const account = accounts.find((a) => a.id === option.value)
424
909
  if (!account) return
425
910
  if (account.role === 'main') {
@@ -737,37 +1222,9 @@ export function openCommandDialog(
737
1222
  const DialogConfirm = api.ui.DialogConfirm
738
1223
  api.ui.dialog.setSize('xlarge')
739
1224
 
740
- const options: Array<{
741
- title: string
742
- value: string
743
- description?: string
744
- }> = []
745
- if (!isMain) {
746
- const toggleLabel = account.enabled ? 'Disable' : 'Enable'
747
- options.push({
748
- title: toggleLabel,
749
- value: account.enabled ? 'disable' : 'enable',
750
- description: account.enabled
751
- ? 'Stop using this fallback account'
752
- : 'Allow this fallback account to be used',
753
- })
754
- options.push({
755
- title: 'Move up',
756
- value: 'move-up',
757
- description: 'Higher priority in fallback order',
758
- })
759
- options.push({
760
- title: 'Move down',
761
- value: 'move-down',
762
- description: 'Lower priority in fallback order',
763
- })
764
- options.push({
765
- title: 'Remove\u2026',
766
- value: 'remove',
767
- description: 'Delete this account permanently',
768
- })
769
- }
770
- options.push({ title: 'Back', value: 'back' })
1225
+ const options = isMain
1226
+ ? [{ title: 'Back', value: 'back' }]
1227
+ : buildManageAccountOptions(account)
771
1228
 
772
1229
  api.ui.dialog.replace(() => (
773
1230
  <DialogSelect
@@ -785,13 +1242,18 @@ export function openCommandDialog(
785
1242
  title={`Remove ${account.label}?`}
786
1243
  message={`Are you sure you want to remove the fallback account "${account.label}"?`}
787
1244
  onConfirm={() => {
788
- void apply('claude-account', `remove ${account.id}`).then(
789
- (r) => {
1245
+ void apply('claude-account', `remove ${account.id}`)
1246
+ .then((r) => {
790
1247
  api.ui.toast({ message: r.text })
791
1248
  updateAccounts(r)
792
1249
  buildL1()
793
- },
794
- )
1250
+ })
1251
+ .catch((err) => {
1252
+ const msg =
1253
+ err instanceof Error ? err.message : String(err)
1254
+ statusMessage = { text: `Error: ${msg}`, error: true }
1255
+ buildL1()
1256
+ })
795
1257
  }}
796
1258
  onCancel={() => openManage(account, isMain)}
797
1259
  />
@@ -799,18 +1261,25 @@ export function openCommandDialog(
799
1261
  return
800
1262
  }
801
1263
 
802
- void apply('claude-account', `${option.value} ${account.id}`).then(
803
- (r) => {
1264
+ const args = `${option.value} ${account.id}`
1265
+ void apply('claude-account', args)
1266
+ .then((r) => {
804
1267
  api.ui.toast({ message: r.text })
805
1268
  updateAccounts(r)
806
- const updatedList = r.knobs.accounts as typeof accounts
1269
+ const updatedList = normalizeAccountDialogPayload(
1270
+ r.knobs,
1271
+ ).accounts
807
1272
  const refreshed =
808
1273
  (updatedList && updatedList.length > 0
809
1274
  ? updatedList.find((a) => a.id === account.id)
810
1275
  : undefined) ?? account
811
1276
  openManage(refreshed, isMain)
812
- },
813
- )
1277
+ })
1278
+ .catch((err) => {
1279
+ const msg = err instanceof Error ? err.message : String(err)
1280
+ statusMessage = { text: `Error: ${msg}`, error: true }
1281
+ buildL1()
1282
+ })
814
1283
  }}
815
1284
  />
816
1285
  ))