@cortexkit/opencode-openai-auth 0.1.1 → 0.1.3

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 (64) hide show
  1. package/README.md +27 -0
  2. package/dist/cli.d.ts +2 -0
  3. package/dist/cli.js +14 -0
  4. package/dist/commands.d.ts +49 -0
  5. package/dist/core/accounts.d.ts +227 -0
  6. package/dist/core/atomic-write.d.ts +1 -0
  7. package/dist/core/backoff.d.ts +53 -0
  8. package/dist/core/cachekeep.d.ts +95 -0
  9. package/dist/core/oauth.d.ts +126 -0
  10. package/dist/core/provider.d.ts +59 -0
  11. package/dist/core/quota-manager.d.ts +136 -0
  12. package/dist/core/refresh-all-quota.d.ts +52 -0
  13. package/dist/core/refresh-file-lock.d.ts +15 -0
  14. package/dist/index-0s5tftne.js +3 -0
  15. package/dist/index-6qjg8qfw.js +2 -0
  16. package/dist/index-dz7tcrxb.js +2 -0
  17. package/dist/index-eks9xm6s.js +1 -0
  18. package/dist/index-hrg6bdgb.js +2 -0
  19. package/dist/index-jshhys8t.js +2 -0
  20. package/dist/index-mrh6k1zf.js +92 -0
  21. package/dist/index-sgfft5d3.js +2 -0
  22. package/dist/index.d.ts +3 -19
  23. package/dist/index.js +118 -124
  24. package/dist/logger.d.ts +11 -1
  25. package/dist/quota-normalize-qh5n0p7v.js +1 -0
  26. package/dist/quota-normalize.d.ts +33 -0
  27. package/dist/response-stream-error.d.ts +12 -1
  28. package/dist/rpc/notifications.d.ts +5 -0
  29. package/dist/rpc/port-file.d.ts +12 -0
  30. package/dist/rpc/port-file.js +1 -0
  31. package/dist/rpc/protocol.d.ts +20 -0
  32. package/dist/rpc/protocol.js +0 -0
  33. package/dist/rpc/rpc-client.d.ts +7 -0
  34. package/dist/rpc/rpc-client.js +1 -0
  35. package/dist/rpc/rpc-dir.d.ts +3 -0
  36. package/dist/rpc/rpc-dir.js +1 -0
  37. package/dist/rpc/rpc-server.d.ts +13 -0
  38. package/dist/sidebar-state.d.ts +83 -0
  39. package/dist/sidebar-state.js +1 -0
  40. package/dist/token-util-sqwtzxz0.js +1 -0
  41. package/dist/token-zzzp7w2y.js +1 -0
  42. package/dist/tui/command-dialogs.d.ts +14 -0
  43. package/dist/tui-preferences.d.ts +40 -0
  44. package/dist/tui-preferences.js +6 -0
  45. package/dist/tui.d.ts +9 -0
  46. package/dist/util/open-url.d.ts +6 -0
  47. package/dist/version.d.ts +1 -1
  48. package/dist/ws-pool.d.ts +8 -0
  49. package/dist/ws.d.ts +4 -0
  50. package/package.json +25 -4
  51. package/src/rpc/notifications.ts +54 -0
  52. package/src/rpc/port-file.ts +70 -0
  53. package/src/rpc/protocol.ts +31 -0
  54. package/src/rpc/rpc-client.ts +78 -0
  55. package/src/rpc/rpc-dir.ts +22 -0
  56. package/src/rpc/rpc-server.ts +129 -0
  57. package/src/sidebar-state.ts +312 -0
  58. package/src/tui/command-dialogs.tsx +742 -0
  59. package/src/tui-preferences.ts +328 -0
  60. package/src/tui.tsx +837 -0
  61. package/dist/index-6z8w05g9.js +0 -2
  62. package/dist/index-xqe8xm96.js +0 -2
  63. package/dist/token-util-fs3z8kkp.js +0 -1
  64. package/dist/token-yyjm093c.js +0 -1
@@ -0,0 +1,742 @@
1
+ /** @jsxImportSource @opentui/solid */
2
+ import type { TuiPluginApi } from '@opencode-ai/plugin/tui'
3
+ import type { OpenDialogPayload } from '../rpc/protocol.js'
4
+ import { getSidebarState } from '../sidebar-state.js'
5
+ import { openUrl } from '../util/open-url'
6
+
7
+ type ApplyFn = (
8
+ command: OpenDialogPayload['command'],
9
+ args: string,
10
+ ) => Promise<{ text: string; knobs: Record<string, unknown> }>
11
+
12
+ export function buildCachekeepDialogOptions(payload: OpenDialogPayload) {
13
+ const enabled = payload.knobs.enabled === true
14
+ const subagents = payload.knobs.subagents === true
15
+ const running = payload.knobs.running === true
16
+ const tracked = Number(payload.knobs.tracked ?? 0)
17
+ const generatedAt = Number(payload.knobs.generatedAt ?? Date.now())
18
+ const lastWarmAt = Number(payload.knobs.lastWarmAt ?? 0)
19
+ const maxIdleWarmMs = Number(payload.knobs.maxIdleWarmMs ?? 60 * 60 * 1000)
20
+ const maxSubagentIdleMs = Number(
21
+ payload.knobs.maxSubagentIdleMs ?? 30 * 60 * 1000,
22
+ )
23
+ const lastWarm = lastWarmAt
24
+ ? `${Math.ceil((generatedAt - lastWarmAt) / 1000)}s ago`
25
+ : 'none yet'
26
+ const idleWindow = Math.round(maxIdleWarmMs / 60_000)
27
+ const subIdleWindow = Math.round(maxSubagentIdleMs / 60_000)
28
+ const statusParts = [
29
+ enabled ? '● Enabled' : '○ Disabled',
30
+ `timer ${running ? 'armed' : 'idle'}`,
31
+ `${tracked} tracked`,
32
+ `last warm ${lastWarm}`,
33
+ `${idleWindow}m idle cap`,
34
+ `subagent idle ${subIdleWindow}m`,
35
+ ].filter((part) => part.length > 0)
36
+
37
+ return [
38
+ {
39
+ title: statusParts.join(' | '),
40
+ value: 'status',
41
+ description: 'Current cachekeep state',
42
+ },
43
+ {
44
+ title: enabled ? 'Turn off' : 'Turn on',
45
+ value: enabled ? 'off' : 'on',
46
+ description: enabled
47
+ ? 'Stop capturing and warming prompt-cache prefixes'
48
+ : 'Persistently enable capture; the timer self-arms on request capture',
49
+ },
50
+ {
51
+ title: subagents ? 'Subagent warming: on' : 'Subagent warming: off',
52
+ value: subagents ? 'subagents off' : 'subagents on',
53
+ description: subagents
54
+ ? `Warm subagent sessions (${subIdleWindow}m idle cap). Disable to skip subagents.`
55
+ : `Skip subagent sessions. Enable to warm them too (${subIdleWindow}m idle cap).`,
56
+ },
57
+ {
58
+ title: 'Refresh status',
59
+ value: 'refresh',
60
+ description: 'Re-read cachekeep status',
61
+ },
62
+ {
63
+ title: 'Close',
64
+ value: 'close',
65
+ description: 'Close this dialog',
66
+ },
67
+ ]
68
+ }
69
+
70
+ function showText(api: TuiPluginApi, text: string) {
71
+ api.ui.dialog.setSize('xlarge')
72
+ api.ui.dialog.replace(() => (
73
+ <box flexDirection='column' padding={1} width='100%'>
74
+ <text>{text}</text>
75
+ </box>
76
+ ))
77
+ }
78
+
79
+ export function openCommandDialog(
80
+ api: TuiPluginApi,
81
+ payload: OpenDialogPayload,
82
+ apply: ApplyFn,
83
+ ) {
84
+ if (payload.command === 'openai-routing') {
85
+ const current = (payload.knobs.mode as string) ?? 'main-first'
86
+ const DialogSelect = api.ui.DialogSelect<string>
87
+ api.ui.dialog.setSize('xlarge')
88
+ api.ui.dialog.replace(() => (
89
+ <DialogSelect
90
+ title='OpenAI routing'
91
+ current={current}
92
+ options={[
93
+ {
94
+ title: 'Main first',
95
+ value: 'main-first',
96
+ description: 'Use the main account until exhausted',
97
+ },
98
+ {
99
+ title: 'Fallback first',
100
+ value: 'fallback-first',
101
+ description: 'Prefer fallback accounts, preserve main',
102
+ },
103
+ ]}
104
+ onSelect={(option) => {
105
+ void apply('openai-routing', String(option.value)).then((r) => {
106
+ api.ui.toast({ message: r.text })
107
+ api.ui.dialog.clear()
108
+ })
109
+ }}
110
+ />
111
+ ))
112
+ return
113
+ }
114
+
115
+ if (payload.command === 'openai-dump') {
116
+ const enabled = payload.knobs.enabled === true
117
+ const DialogConfirm = api.ui.DialogConfirm
118
+ api.ui.dialog.setSize('xlarge')
119
+ api.ui.dialog.replace(() => (
120
+ <DialogConfirm
121
+ title='OpenAI request dump'
122
+ message={`${payload.text}\n\n${enabled ? 'Disable' : 'Enable'} request dump?`}
123
+ onConfirm={() => {
124
+ void apply('openai-dump', enabled ? 'off' : 'on').then((r) => {
125
+ api.ui.toast({ message: r.text })
126
+ api.ui.dialog.clear()
127
+ })
128
+ }}
129
+ onCancel={() => api.ui.dialog.clear()}
130
+ />
131
+ ))
132
+ return
133
+ }
134
+
135
+ if (payload.command === 'openai-killswitch') {
136
+ const config = (payload.knobs.config ?? {}) as {
137
+ enabled?: boolean
138
+ main?: Record<string, number>
139
+ accounts?: Record<string, Record<string, number>>
140
+ }
141
+ const accountIds = (payload.knobs.accountIds as string[]) ?? []
142
+ const enabled = config.enabled === true
143
+ const readT = (t: Record<string, number> | undefined) => {
144
+ const fh = t?.primary ?? t?.['5h'] ?? 5
145
+ const sd = t?.secondary ?? t?.['1w'] ?? 10
146
+ return { fh, sd }
147
+ }
148
+ const mainT = readT(config.main)
149
+ const seedParts = [`main:${mainT.fh},${mainT.sd}`]
150
+ for (const id of accountIds) {
151
+ const t = readT(config.accounts?.[id] ?? config.main)
152
+ seedParts.push(`${id}:${t.fh},${t.sd}`)
153
+ }
154
+ const seed = seedParts.join(' ')
155
+
156
+ const openEdit = () => {
157
+ const DialogPrompt = api.ui.DialogPrompt
158
+ api.ui.dialog.setSize('xlarge')
159
+ api.ui.dialog.replace(() => (
160
+ <DialogPrompt
161
+ title='Killswitch thresholds'
162
+ description={() => <text>{payload.text}</text>}
163
+ placeholder='main:5,10 work-alt:5,10'
164
+ value={seed}
165
+ onConfirm={(value: string) => {
166
+ void apply('openai-killswitch', `set ${value.trim()}`).then((r) => {
167
+ api.ui.toast({ message: r.text })
168
+ api.ui.dialog.clear()
169
+ })
170
+ }}
171
+ onCancel={() => api.ui.dialog.clear()}
172
+ />
173
+ ))
174
+ }
175
+
176
+ const DialogSelect = api.ui.DialogSelect<string>
177
+ api.ui.dialog.setSize('xlarge')
178
+ api.ui.dialog.replace(() => (
179
+ <DialogSelect
180
+ title='OpenAI killswitch'
181
+ options={[
182
+ {
183
+ title: enabled ? 'Disable killswitch' : 'Enable killswitch',
184
+ value: enabled ? 'off' : 'on',
185
+ description: enabled
186
+ ? 'Stop hard-blocking on low quota'
187
+ : 'Hard-block requests when quota drops below thresholds',
188
+ },
189
+ {
190
+ title: 'Edit thresholds\u2026',
191
+ value: 'edit',
192
+ description: 'Set per-account 5h,1w cutoffs',
193
+ },
194
+ ]}
195
+ onSelect={(option) => {
196
+ if (option.value === 'edit') {
197
+ openEdit()
198
+ return
199
+ }
200
+ void apply('openai-killswitch', String(option.value)).then((r) => {
201
+ api.ui.toast({ message: r.text })
202
+ api.ui.dialog.clear()
203
+ })
204
+ }}
205
+ />
206
+ ))
207
+ return
208
+ }
209
+
210
+ if (payload.command === 'openai-logging') {
211
+ const current = (payload.knobs.level as string) ?? 'info'
212
+ const levels = ['error', 'warn', 'info', 'debug', 'trace']
213
+ const DialogSelect = api.ui.DialogSelect<string>
214
+ api.ui.dialog.setSize('xlarge')
215
+ api.ui.dialog.replace(() => (
216
+ <DialogSelect
217
+ title='OpenAI logging'
218
+ current={current}
219
+ options={levels.map((level) => ({
220
+ title: level,
221
+ value: level,
222
+ description:
223
+ level === current
224
+ ? 'currently active'
225
+ : `Set log level to ${level}`,
226
+ }))}
227
+ onSelect={(option) => {
228
+ void apply('openai-logging', String(option.value)).then((r) => {
229
+ api.ui.toast({ message: r.text })
230
+ api.ui.dialog.clear()
231
+ })
232
+ }}
233
+ />
234
+ ))
235
+ return
236
+ }
237
+
238
+ if (payload.command === 'openai-cachekeep') {
239
+ const render = (state: OpenDialogPayload) => {
240
+ const options = buildCachekeepDialogOptions(state)
241
+ const DialogSelect = api.ui.DialogSelect<string>
242
+ api.ui.dialog.setSize('xlarge')
243
+ api.ui.dialog.replace(() => (
244
+ <DialogSelect
245
+ title='OpenAI cachekeep'
246
+ current='status'
247
+ options={options}
248
+ onSelect={(option) => {
249
+ if (option.value === 'close') {
250
+ api.ui.dialog.clear()
251
+ return
252
+ }
253
+ const args =
254
+ option.value === 'refresh' ? 'status' : String(option.value)
255
+ void apply('openai-cachekeep', args).then((r) => {
256
+ render({
257
+ command: 'openai-cachekeep',
258
+ text: r.text,
259
+ knobs: r.knobs,
260
+ })
261
+ })
262
+ }}
263
+ />
264
+ ))
265
+ }
266
+ render(payload)
267
+ return
268
+ }
269
+
270
+ if (payload.command === 'openai-account') {
271
+ openAccountDialog(api, apply)
272
+ return
273
+ }
274
+
275
+ // fallback for quota (display-only)
276
+ showText(api, payload.text)
277
+ }
278
+
279
+ // -- Accounts dialog ---------------------------------------------------------
280
+
281
+ function formatQuota5h7d(
282
+ quota:
283
+ | { primary?: { usedPercent: number }; secondary?: { usedPercent: number } }
284
+ | null
285
+ | undefined,
286
+ ): string {
287
+ if (!quota) return 'no quota data'
288
+ const parts: string[] = []
289
+ if (quota.primary) parts.push(`5h: ${Math.round(quota.primary.usedPercent)}%`)
290
+ if (quota.secondary)
291
+ parts.push(`7d: ${Math.round(quota.secondary.usedPercent)}%`)
292
+ return parts.length > 0 ? parts.join(' ') : 'no quota data'
293
+ }
294
+
295
+ function osc52Copy(api: TuiPluginApi, text: string): boolean {
296
+ try {
297
+ const renderer = api.renderer as unknown as
298
+ | {
299
+ copyToClipboardOSC52?: (t: string) => boolean
300
+ }
301
+ | null
302
+ | undefined
303
+ if (renderer && typeof renderer.copyToClipboardOSC52 === 'function') {
304
+ return renderer.copyToClipboardOSC52(text)
305
+ }
306
+ } catch {
307
+ // best-effort
308
+ }
309
+ return false
310
+ }
311
+
312
+ function openAccountDialog(api: TuiPluginApi, apply: ApplyFn) {
313
+ const DialogConfirm = api.ui.DialogConfirm
314
+
315
+ function showL1() {
316
+ void getSidebarState().then((state) => {
317
+ const DialogSelectInner = api.ui.DialogSelect<string>
318
+ api.ui.dialog.setSize('xlarge')
319
+ api.ui.dialog.replace(() => (
320
+ <DialogSelectInner
321
+ title='OpenAI Accounts'
322
+ options={[
323
+ {
324
+ title: `main${state.activeId === 'main' || !state.activeId ? ' \u2022 active' : ''}`,
325
+ value: 'main',
326
+ description: formatQuota5h7d(state.main.quota),
327
+ },
328
+ ...state.fallbacks.map((fb) => ({
329
+ title: `${fb.label ?? fb.id}${state.activeId === fb.id ? ' \u2022 active' : ''}${!fb.enabled ? ' (disabled)' : ''}`,
330
+ value: fb.id,
331
+ description: formatQuota5h7d(fb.quota),
332
+ })),
333
+ {
334
+ title: 'Add account\u2026',
335
+ value: '__add__',
336
+ description: 'Sign in to a new OpenAI account',
337
+ },
338
+ ]}
339
+ onSelect={(option) => {
340
+ if (option.value === '__add__') {
341
+ showAddFlow()
342
+ return
343
+ }
344
+ if (option.value === 'main') {
345
+ showL2Main()
346
+ return
347
+ }
348
+ showL2Fallback(option.value)
349
+ }}
350
+ />
351
+ ))
352
+ })
353
+ }
354
+
355
+ function showL2Main() {
356
+ void getSidebarState().then((state) => {
357
+ const DialogSelectInner = api.ui.DialogSelect<string>
358
+ const isActive = state.activeId === 'main' || !state.activeId
359
+ const options: Array<{
360
+ title: string
361
+ value: string
362
+ description: string
363
+ }> = []
364
+ if (!isActive) {
365
+ options.push({
366
+ title: 'Switch to main account',
367
+ value: 'switch',
368
+ description: 'Make main the active account',
369
+ })
370
+ }
371
+ options.push({
372
+ title: 'Back',
373
+ value: 'back',
374
+ description: 'Return to account list',
375
+ })
376
+ api.ui.dialog.setSize('xlarge')
377
+ api.ui.dialog.replace(() => (
378
+ <DialogSelectInner
379
+ title='Main account'
380
+ options={options}
381
+ onSelect={(option) => {
382
+ if (option.value === 'switch') {
383
+ void apply('openai-account', 'switch main').then((r) => {
384
+ api.ui.toast({ message: r.text })
385
+ showL1()
386
+ })
387
+ return
388
+ }
389
+ showL1()
390
+ }}
391
+ />
392
+ ))
393
+ })
394
+ }
395
+
396
+ function showL2Fallback(id: string) {
397
+ void getSidebarState().then((state) => {
398
+ const DialogSelectInner = api.ui.DialogSelect<string>
399
+ const fbIndex = state.fallbacks.findIndex((f) => f.id === id)
400
+ const fb = state.fallbacks[fbIndex]
401
+ if (!fb) {
402
+ showL1()
403
+ return
404
+ }
405
+ const isActive = state.activeId === id
406
+ const options: Array<{
407
+ title: string
408
+ value: string
409
+ description: string
410
+ }> = []
411
+
412
+ if (!isActive) {
413
+ options.push({
414
+ title: 'Switch to this account',
415
+ value: 'switch',
416
+ description: 'Make this the active account',
417
+ })
418
+ }
419
+
420
+ options.push({
421
+ title: 'Remove',
422
+ value: 'remove',
423
+ description: `Remove ${fb.label ?? fb.id}`,
424
+ })
425
+
426
+ if (fbIndex > 0) {
427
+ const neighbor = state.fallbacks[fbIndex - 1]
428
+ if (neighbor) {
429
+ options.push({
430
+ title: 'Move up',
431
+ value: 'move_up',
432
+ description: `Swap with ${neighbor.label ?? neighbor.id}`,
433
+ })
434
+ }
435
+ }
436
+
437
+ if (fbIndex < state.fallbacks.length - 1) {
438
+ const neighbor = state.fallbacks[fbIndex + 1]
439
+ if (neighbor) {
440
+ options.push({
441
+ title: 'Move down',
442
+ value: 'move_down',
443
+ description: `Swap with ${neighbor.label ?? neighbor.id}`,
444
+ })
445
+ }
446
+ }
447
+
448
+ options.push({
449
+ title: 'Back',
450
+ value: 'back',
451
+ description: 'Return to account list',
452
+ })
453
+
454
+ api.ui.dialog.setSize('xlarge')
455
+ api.ui.dialog.replace(() => (
456
+ <DialogSelectInner
457
+ title={fb.label ?? fb.id}
458
+ options={options}
459
+ onSelect={(option) => {
460
+ if (option.value === 'switch') {
461
+ void apply('openai-account', `switch ${id}`).then((r) => {
462
+ api.ui.toast({ message: r.text })
463
+ showL1()
464
+ })
465
+ return
466
+ }
467
+ if (option.value === 'remove') {
468
+ api.ui.dialog.setSize('xlarge')
469
+ api.ui.dialog.replace(() => (
470
+ <DialogConfirm
471
+ title='Remove account'
472
+ message={`Remove ${fb.label ?? fb.id}?`}
473
+ onConfirm={() => {
474
+ void apply('openai-account', `remove ${id}`).then((r) => {
475
+ api.ui.toast({ message: r.text })
476
+ showL1()
477
+ })
478
+ }}
479
+ onCancel={() => showL2Fallback(id)}
480
+ />
481
+ ))
482
+ return
483
+ }
484
+ if (option.value === 'move_up') {
485
+ const neighbor = state.fallbacks[fbIndex - 1]
486
+ if (neighbor) {
487
+ void apply('openai-account', `order ${id} ${neighbor.id}`).then(
488
+ (r) => {
489
+ api.ui.toast({ message: r.text })
490
+ showL1()
491
+ },
492
+ )
493
+ }
494
+ return
495
+ }
496
+ if (option.value === 'move_down') {
497
+ const neighbor = state.fallbacks[fbIndex + 1]
498
+ if (neighbor) {
499
+ void apply('openai-account', `order ${id} ${neighbor.id}`).then(
500
+ (r) => {
501
+ api.ui.toast({ message: r.text })
502
+ showL1()
503
+ },
504
+ )
505
+ }
506
+ return
507
+ }
508
+ showL1()
509
+ }}
510
+ />
511
+ ))
512
+ })
513
+ }
514
+
515
+ // -- Add flow ---------------------------------------------------------------
516
+
517
+ function showAddFlow() {
518
+ const DialogSelectInner = api.ui.DialogSelect<string>
519
+ api.ui.dialog.setSize('xlarge')
520
+ api.ui.dialog.replace(() => (
521
+ <DialogSelectInner
522
+ title='Add account'
523
+ options={[
524
+ {
525
+ title: 'Browser sign-in (local)',
526
+ value: 'browser',
527
+ description: 'Opens a browser window for OAuth on this machine',
528
+ },
529
+ {
530
+ title: 'Device code (remote / no browser)',
531
+ value: 'device',
532
+ description: 'Enter a code on another device to authorize',
533
+ },
534
+ {
535
+ title: 'Back',
536
+ value: 'back',
537
+ description: 'Return to account list',
538
+ },
539
+ ]}
540
+ onSelect={(option) => {
541
+ if (option.value === 'browser') {
542
+ showLabelPrompt('browser')
543
+ return
544
+ }
545
+ if (option.value === 'device') {
546
+ showLabelPrompt('device')
547
+ return
548
+ }
549
+ showL1()
550
+ }}
551
+ />
552
+ ))
553
+ }
554
+
555
+ function showLabelPrompt(mode: 'browser' | 'device') {
556
+ const DialogPromptInner = api.ui.DialogPrompt
557
+ api.ui.dialog.setSize('xlarge')
558
+ api.ui.dialog.replace(() => (
559
+ <DialogPromptInner
560
+ title='Label (optional)'
561
+ description={() => (
562
+ <text>
563
+ Give this account a name (e.g. "work", "personal"). Leave empty to
564
+ auto-detect.
565
+ </text>
566
+ )}
567
+ placeholder='e.g. work'
568
+ value=''
569
+ onConfirm={(label: string) => {
570
+ if (mode === 'browser') {
571
+ startBrowserAdd(label.trim() || undefined)
572
+ } else {
573
+ startDeviceAdd(label.trim() || undefined)
574
+ }
575
+ }}
576
+ onCancel={() => showAddFlow()}
577
+ />
578
+ ))
579
+ }
580
+
581
+ function startBrowserAdd(label: string | undefined) {
582
+ const args = label ? `add ${label}` : 'add'
583
+ void apply('openai-account', args).then((r) => {
584
+ const url = r.knobs.url as string | undefined
585
+ const instructions = r.knobs.instructions as string | undefined
586
+
587
+ if (!url) {
588
+ api.ui.toast({ message: 'Failed to get auth URL', variant: 'warning' })
589
+ showL1()
590
+ return
591
+ }
592
+
593
+ // Auto-open the browser (best-effort)
594
+ try {
595
+ openUrl(url)
596
+ } catch {
597
+ // best-effort
598
+ }
599
+
600
+ showBrowserAuthScreen(url, instructions)
601
+ })
602
+ }
603
+
604
+ function showBrowserAuthScreen(
605
+ url: string,
606
+ _instructions: string | undefined,
607
+ ) {
608
+ const DialogSelectInner = api.ui.DialogSelect<string>
609
+ api.ui.dialog.setSize('xlarge')
610
+ api.ui.dialog.replace(() => (
611
+ <DialogSelectInner
612
+ title='Browser sign-in'
613
+ options={[
614
+ {
615
+ title: 'Copy auth URL',
616
+ value: 'copy_url',
617
+ description: url,
618
+ },
619
+ {
620
+ title: 'Open in browser',
621
+ value: 'open',
622
+ description: 'Try to open the URL in your browser',
623
+ },
624
+ {
625
+ title: "Done / I've authorized",
626
+ value: 'done',
627
+ description: 'Return to account list',
628
+ },
629
+ {
630
+ title: 'Cancel',
631
+ value: 'cancel',
632
+ description: 'Return to account list without adding',
633
+ },
634
+ ]}
635
+ skipFilter={true}
636
+ onSelect={(option) => {
637
+ if (option.value === 'copy_url') {
638
+ const ok = osc52Copy(api, url)
639
+ api.ui.toast({
640
+ message: ok
641
+ ? 'Auth URL copied'
642
+ : 'Copy unsupported — select the URL text manually',
643
+ variant: ok ? 'success' : 'warning',
644
+ })
645
+ return
646
+ }
647
+ if (option.value === 'open') {
648
+ try {
649
+ openUrl(url)
650
+ } catch {
651
+ // best-effort
652
+ }
653
+ return
654
+ }
655
+ showL1()
656
+ }}
657
+ />
658
+ ))
659
+ }
660
+
661
+ function startDeviceAdd(label: string | undefined) {
662
+ const args = label ? `add --headless ${label}` : 'add --headless'
663
+ void apply('openai-account', args).then((r) => {
664
+ const verificationUrl = r.knobs.verificationUrl as string | undefined
665
+ const userCode = r.knobs.userCode as string | undefined
666
+ const instructions = r.knobs.instructions as string | undefined
667
+
668
+ if (!verificationUrl) {
669
+ api.ui.toast({
670
+ message: 'Failed to get device code',
671
+ variant: 'warning',
672
+ })
673
+ showL1()
674
+ return
675
+ }
676
+
677
+ showDeviceCodeScreen(verificationUrl, userCode, instructions)
678
+ })
679
+ }
680
+
681
+ function showDeviceCodeScreen(
682
+ verificationUrl: string,
683
+ userCode: string | undefined,
684
+ _instructions: string | undefined,
685
+ ) {
686
+ const DialogSelectInner = api.ui.DialogSelect<string>
687
+ api.ui.dialog.setSize('xlarge')
688
+ api.ui.dialog.replace(() => (
689
+ <DialogSelectInner
690
+ title='Device code'
691
+ options={[
692
+ {
693
+ title: userCode ? `Copy code: ${userCode}` : 'Copy code',
694
+ value: 'copy_code',
695
+ description: 'Copy the user code to clipboard',
696
+ },
697
+ {
698
+ title: 'Copy URL',
699
+ value: 'copy_url',
700
+ description: verificationUrl,
701
+ },
702
+ {
703
+ title: "Done / I've authorized",
704
+ value: 'done',
705
+ description: 'Return to account list',
706
+ },
707
+ {
708
+ title: 'Cancel',
709
+ value: 'cancel',
710
+ description: 'Return to account list without adding',
711
+ },
712
+ ]}
713
+ skipFilter={true}
714
+ onSelect={(option) => {
715
+ if (option.value === 'copy_code' && userCode) {
716
+ const ok = osc52Copy(api, userCode)
717
+ api.ui.toast({
718
+ message: ok
719
+ ? 'Code copied'
720
+ : 'Copy unsupported — enter the code manually',
721
+ variant: ok ? 'success' : 'warning',
722
+ })
723
+ return
724
+ }
725
+ if (option.value === 'copy_url') {
726
+ const ok = osc52Copy(api, verificationUrl)
727
+ api.ui.toast({
728
+ message: ok
729
+ ? 'URL copied'
730
+ : 'Copy unsupported — enter the URL manually',
731
+ variant: ok ? 'success' : 'warning',
732
+ })
733
+ return
734
+ }
735
+ showL1()
736
+ }}
737
+ />
738
+ ))
739
+ }
740
+
741
+ void showL1()
742
+ }