@cortexkit/opencode-anthropic-auth 1.10.2 → 1.11.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.
- package/dist/cli.d.ts +41 -0
- package/dist/cli.js +6 -6
- package/dist/index-7f07xr7k.js +2 -0
- package/dist/{index-p8j7mmfc.js → index-7hq52c3b.js} +1 -1
- package/dist/index-c1phnbjn.js +417 -0
- package/dist/index-srn1rawx.js +2 -0
- package/dist/index-vhcx8tx6.js +1 -0
- package/dist/index-zfddfgk2.js +4 -0
- package/dist/index.js +11 -10
- package/dist/logger-q4dnhr8x.js +1 -0
- package/dist/rpc/port-file.d.ts +1 -1
- package/dist/rpc/port-file.js +1 -1
- package/dist/rpc/protocol.d.ts +2 -1
- package/dist/rpc/rpc-client.d.ts +1 -1
- package/dist/rpc/rpc-client.js +1 -1
- package/dist/rpc/rpc-dir.js +1 -1
- package/dist/sidebar-state.d.ts +4 -1
- package/dist/sidebar-state.js +1 -1
- package/dist/tui/command-dialogs.d.ts +1 -0
- package/dist/tui-preferences.js +2 -2
- package/package.json +4 -4
- package/src/rpc/port-file.ts +7 -1
- package/src/rpc/protocol.ts +3 -0
- package/src/rpc/rpc-client.ts +10 -3
- package/src/rpc/rpc-dir.ts +2 -2
- package/src/rpc/rpc-server.ts +5 -3
- package/src/sidebar-state.ts +99 -11
- package/src/tui/command-dialogs.tsx +478 -1
- package/src/tui-preferences.ts +57 -18
- package/src/tui.tsx +25 -14
- package/dist/index-3ynbghx3.js +0 -1
- package/dist/index-9yjbmhf1.js +0 -2
- package/dist/index-fvzta6xw.js +0 -410
- package/dist/index-jb14nj5c.js +0 -2
|
@@ -5,7 +5,7 @@ import type { OpenDialogPayload } from '../rpc/protocol.js'
|
|
|
5
5
|
type ApplyFn = (
|
|
6
6
|
command: OpenDialogPayload['command'],
|
|
7
7
|
args: string,
|
|
8
|
-
) => Promise<{ text: string }>
|
|
8
|
+
) => Promise<{ text: string; knobs: Record<string, unknown> }>
|
|
9
9
|
|
|
10
10
|
function showText(api: TuiPluginApi, text: string) {
|
|
11
11
|
api.ui.dialog.setSize('xlarge')
|
|
@@ -224,6 +224,483 @@ export function openCommandDialog(
|
|
|
224
224
|
return
|
|
225
225
|
}
|
|
226
226
|
|
|
227
|
+
if (payload.command === 'claude-account') {
|
|
228
|
+
const accounts =
|
|
229
|
+
(payload.knobs.accounts as Array<{
|
|
230
|
+
id: string
|
|
231
|
+
label: string
|
|
232
|
+
role: string
|
|
233
|
+
enabled: boolean
|
|
234
|
+
quotaPercent: number | null
|
|
235
|
+
}>) ?? []
|
|
236
|
+
|
|
237
|
+
const updateAccounts = (r: {
|
|
238
|
+
text: string
|
|
239
|
+
knobs: Record<string, unknown>
|
|
240
|
+
}) => {
|
|
241
|
+
const updated = r.knobs.accounts as typeof accounts
|
|
242
|
+
if (updated && updated.length > 0) {
|
|
243
|
+
accounts.length = 0
|
|
244
|
+
accounts.push(...updated)
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const buildL1 = () => {
|
|
249
|
+
const DialogSelect = api.ui.DialogSelect<string>
|
|
250
|
+
const l1Options: Array<{
|
|
251
|
+
title: string
|
|
252
|
+
value: string
|
|
253
|
+
description?: string
|
|
254
|
+
}> = [
|
|
255
|
+
{
|
|
256
|
+
title: 'Add account\u2026',
|
|
257
|
+
value: '__add__',
|
|
258
|
+
description: 'Add an API key or OAuth fallback account',
|
|
259
|
+
},
|
|
260
|
+
...accounts.map((a) => {
|
|
261
|
+
const pct =
|
|
262
|
+
a.quotaPercent != null
|
|
263
|
+
? ` ${Math.round(a.quotaPercent)}%`
|
|
264
|
+
: ' \u2013%'
|
|
265
|
+
const status = !a.enabled ? ' (disabled)' : ''
|
|
266
|
+
return {
|
|
267
|
+
title: `${a.label} [${a.role}]${status}${pct}`,
|
|
268
|
+
value: a.id,
|
|
269
|
+
}
|
|
270
|
+
}),
|
|
271
|
+
]
|
|
272
|
+
api.ui.dialog.setSize('xlarge')
|
|
273
|
+
api.ui.dialog.replace(() => (
|
|
274
|
+
<DialogSelect
|
|
275
|
+
title='Claude accounts'
|
|
276
|
+
options={l1Options}
|
|
277
|
+
onSelect={(option) => {
|
|
278
|
+
if (option.value === '__add__') {
|
|
279
|
+
openAddType()
|
|
280
|
+
return
|
|
281
|
+
}
|
|
282
|
+
const account = accounts.find((a) => a.id === option.value)
|
|
283
|
+
if (!account) return
|
|
284
|
+
if (account.role === 'main') {
|
|
285
|
+
const pct =
|
|
286
|
+
account.quotaPercent != null
|
|
287
|
+
? ` ${Math.round(account.quotaPercent)}%`
|
|
288
|
+
: ' \u2013%'
|
|
289
|
+
showText(
|
|
290
|
+
api,
|
|
291
|
+
`${account.label}\nRole: main (read-only)\nQuota:${pct}`,
|
|
292
|
+
)
|
|
293
|
+
return
|
|
294
|
+
}
|
|
295
|
+
openManage(account, false)
|
|
296
|
+
}}
|
|
297
|
+
/>
|
|
298
|
+
))
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// -- Add type selection (OAuth vs API key) ------------------------------
|
|
302
|
+
const openAddType = () => {
|
|
303
|
+
const DialogSelect = api.ui.DialogSelect<string>
|
|
304
|
+
api.ui.dialog.setSize('xlarge')
|
|
305
|
+
api.ui.dialog.replace(() => (
|
|
306
|
+
<DialogSelect
|
|
307
|
+
title='Add account'
|
|
308
|
+
options={[
|
|
309
|
+
{
|
|
310
|
+
title: 'OAuth (login)',
|
|
311
|
+
value: 'oauth',
|
|
312
|
+
description:
|
|
313
|
+
'Sign in to Claude via browser — works with Pro, Max, and Team plans',
|
|
314
|
+
},
|
|
315
|
+
{
|
|
316
|
+
title: 'API key',
|
|
317
|
+
value: 'apikey',
|
|
318
|
+
description:
|
|
319
|
+
'Provide an API key for an Anthropic-compatible endpoint',
|
|
320
|
+
},
|
|
321
|
+
{ title: 'Back', value: 'back' },
|
|
322
|
+
]}
|
|
323
|
+
onSelect={(option) => {
|
|
324
|
+
if (option.value === 'back') {
|
|
325
|
+
buildL1()
|
|
326
|
+
return
|
|
327
|
+
}
|
|
328
|
+
if (option.value === 'apikey') {
|
|
329
|
+
openAddApiKey()
|
|
330
|
+
return
|
|
331
|
+
}
|
|
332
|
+
openAddOAuthStart()
|
|
333
|
+
}}
|
|
334
|
+
/>
|
|
335
|
+
))
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// -- Add API key (multi-step: key → baseURL → authHeader → label) ------
|
|
339
|
+
const openAddApiKey = () => {
|
|
340
|
+
const collected: {
|
|
341
|
+
apiKey?: string
|
|
342
|
+
baseURL?: string
|
|
343
|
+
authHeader?: string
|
|
344
|
+
label?: string
|
|
345
|
+
} = {}
|
|
346
|
+
|
|
347
|
+
const openApiKeyPrompt = () => {
|
|
348
|
+
const DialogPrompt = api.ui.DialogPrompt
|
|
349
|
+
api.ui.dialog.setSize('xlarge')
|
|
350
|
+
api.ui.dialog.replace(() => (
|
|
351
|
+
<DialogPrompt
|
|
352
|
+
title='Add API key account \u2014 API key'
|
|
353
|
+
description={() => <text>Paste your API key (required).</text>}
|
|
354
|
+
placeholder='sk-ant-...'
|
|
355
|
+
value=''
|
|
356
|
+
onConfirm={(value: string) => {
|
|
357
|
+
const trimmed = value.trim()
|
|
358
|
+
if (!trimmed) {
|
|
359
|
+
openAddType()
|
|
360
|
+
return
|
|
361
|
+
}
|
|
362
|
+
collected.apiKey = trimmed
|
|
363
|
+
openBaseURLPrompt()
|
|
364
|
+
}}
|
|
365
|
+
onCancel={() => openAddType()}
|
|
366
|
+
/>
|
|
367
|
+
))
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
const openBaseURLPrompt = () => {
|
|
371
|
+
const DialogPrompt = api.ui.DialogPrompt
|
|
372
|
+
api.ui.dialog.setSize('xlarge')
|
|
373
|
+
api.ui.dialog.replace(() => (
|
|
374
|
+
<DialogPrompt
|
|
375
|
+
title='Add API key account \u2014 base URL'
|
|
376
|
+
description={() => (
|
|
377
|
+
<text>
|
|
378
|
+
Anthropic-compatible API base URL. Default:
|
|
379
|
+
https://api.kie.ai/claude
|
|
380
|
+
</text>
|
|
381
|
+
)}
|
|
382
|
+
placeholder='https://api.kie.ai/claude'
|
|
383
|
+
value=''
|
|
384
|
+
onConfirm={(value: string) => {
|
|
385
|
+
const trimmed = value.trim()
|
|
386
|
+
collected.baseURL = trimmed || 'https://api.kie.ai/claude'
|
|
387
|
+
openAuthHeaderSelect()
|
|
388
|
+
}}
|
|
389
|
+
onCancel={() => openApiKeyPrompt()}
|
|
390
|
+
/>
|
|
391
|
+
))
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
const openAuthHeaderSelect = () => {
|
|
395
|
+
const DialogSelect = api.ui.DialogSelect<string>
|
|
396
|
+
api.ui.dialog.setSize('xlarge')
|
|
397
|
+
api.ui.dialog.replace(() => (
|
|
398
|
+
<DialogSelect
|
|
399
|
+
title='Add API key account \u2014 auth header'
|
|
400
|
+
options={[
|
|
401
|
+
{
|
|
402
|
+
title: 'Authorization: Bearer (default)',
|
|
403
|
+
value: 'authorization-bearer',
|
|
404
|
+
description: 'Standard bearer token authentication',
|
|
405
|
+
},
|
|
406
|
+
{
|
|
407
|
+
title: 'X-API-Key',
|
|
408
|
+
value: 'x-api-key',
|
|
409
|
+
description: 'Custom header-based API key',
|
|
410
|
+
},
|
|
411
|
+
]}
|
|
412
|
+
onSelect={(option) => {
|
|
413
|
+
collected.authHeader = option.value as
|
|
414
|
+
| 'authorization-bearer'
|
|
415
|
+
| 'x-api-key'
|
|
416
|
+
openLabelPrompt()
|
|
417
|
+
}}
|
|
418
|
+
/>
|
|
419
|
+
))
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
const openLabelPrompt = () => {
|
|
423
|
+
const DialogPrompt = api.ui.DialogPrompt
|
|
424
|
+
api.ui.dialog.setSize('xlarge')
|
|
425
|
+
api.ui.dialog.replace(() => (
|
|
426
|
+
<DialogPrompt
|
|
427
|
+
title='Add API key account \u2014 label'
|
|
428
|
+
description={() => (
|
|
429
|
+
<text>A short name for this account (optional).</text>
|
|
430
|
+
)}
|
|
431
|
+
placeholder='e.g. Work API'
|
|
432
|
+
value=''
|
|
433
|
+
onConfirm={(value: string) => {
|
|
434
|
+
const trimmed = value.trim()
|
|
435
|
+
collected.label = trimmed || undefined
|
|
436
|
+
const apiKey = collected.apiKey
|
|
437
|
+
if (!apiKey) return
|
|
438
|
+
let args = `add-apikey ${apiKey}`
|
|
439
|
+
if (
|
|
440
|
+
collected.baseURL &&
|
|
441
|
+
collected.baseURL !== 'https://api.kie.ai/claude'
|
|
442
|
+
) {
|
|
443
|
+
args += ` --base-url ${collected.baseURL}`
|
|
444
|
+
}
|
|
445
|
+
if (
|
|
446
|
+
collected.authHeader &&
|
|
447
|
+
collected.authHeader !== 'authorization-bearer'
|
|
448
|
+
) {
|
|
449
|
+
args += ` --auth-header ${collected.authHeader}`
|
|
450
|
+
}
|
|
451
|
+
if (collected.label) {
|
|
452
|
+
args += ` --label ${collected.label}`
|
|
453
|
+
}
|
|
454
|
+
void apply('claude-account', args).then((r) => {
|
|
455
|
+
api.ui.toast({ message: r.text })
|
|
456
|
+
updateAccounts(r)
|
|
457
|
+
buildL1()
|
|
458
|
+
})
|
|
459
|
+
}}
|
|
460
|
+
onCancel={() => openAuthHeaderSelect()}
|
|
461
|
+
/>
|
|
462
|
+
))
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
openApiKeyPrompt()
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
// -- Add OAuth (OSC-52 copy + code entry) ------------------------------
|
|
469
|
+
const openAddOAuthStart = () => {
|
|
470
|
+
void apply('claude-account', 'add-oauth-start').then((r) => {
|
|
471
|
+
const oauthUrl = r.knobs.oauthUrl as string | undefined
|
|
472
|
+
updateAccounts(r)
|
|
473
|
+
if (oauthUrl) {
|
|
474
|
+
openOAuthUrlScreen(oauthUrl)
|
|
475
|
+
} else {
|
|
476
|
+
api.ui.toast({ message: r.text })
|
|
477
|
+
buildL1()
|
|
478
|
+
}
|
|
479
|
+
})
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
const openOAuthUrlScreen = (oauthUrl: string) => {
|
|
483
|
+
const DialogSelect = api.ui.DialogSelect<string>
|
|
484
|
+
api.ui.dialog.setSize('xlarge')
|
|
485
|
+
api.ui.dialog.replace(() => (
|
|
486
|
+
<DialogSelect
|
|
487
|
+
title='OAuth sign-in'
|
|
488
|
+
options={[
|
|
489
|
+
{
|
|
490
|
+
title: 'Copy URL to clipboard',
|
|
491
|
+
value: 'copy',
|
|
492
|
+
description: oauthUrl,
|
|
493
|
+
},
|
|
494
|
+
{
|
|
495
|
+
title: 'Enter sign-in code',
|
|
496
|
+
value: 'code',
|
|
497
|
+
description:
|
|
498
|
+
'Open the URL in your browser, sign in, then paste the callback URL or code',
|
|
499
|
+
},
|
|
500
|
+
{ title: 'Cancel', value: 'cancel' },
|
|
501
|
+
]}
|
|
502
|
+
onSelect={(option) => {
|
|
503
|
+
if (option.value === 'cancel') {
|
|
504
|
+
buildL1()
|
|
505
|
+
return
|
|
506
|
+
}
|
|
507
|
+
if (option.value === 'copy') {
|
|
508
|
+
const ok = api.renderer.copyToClipboardOSC52(oauthUrl)
|
|
509
|
+
if (ok) {
|
|
510
|
+
api.ui.toast({ message: 'URL copied to clipboard' })
|
|
511
|
+
} else {
|
|
512
|
+
api.ui.toast({
|
|
513
|
+
message:
|
|
514
|
+
'Copy unavailable \u2014 select the URL text above to copy',
|
|
515
|
+
})
|
|
516
|
+
}
|
|
517
|
+
openOAuthUrlScreen(oauthUrl)
|
|
518
|
+
return
|
|
519
|
+
}
|
|
520
|
+
openOAuthCodePrompt(oauthUrl)
|
|
521
|
+
}}
|
|
522
|
+
/>
|
|
523
|
+
))
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
const openOAuthCodePrompt = (oauthUrl: string) => {
|
|
527
|
+
const DialogPrompt = api.ui.DialogPrompt
|
|
528
|
+
api.ui.dialog.setSize('xlarge')
|
|
529
|
+
api.ui.dialog.replace(() => (
|
|
530
|
+
<DialogPrompt
|
|
531
|
+
title='OAuth sign-in \u2014 enter code'
|
|
532
|
+
description={() => (
|
|
533
|
+
<text>
|
|
534
|
+
After signing in you will be redirected. Paste the full callback
|
|
535
|
+
URL or authorization code below.
|
|
536
|
+
</text>
|
|
537
|
+
)}
|
|
538
|
+
placeholder='Paste callback URL or code here'
|
|
539
|
+
value=''
|
|
540
|
+
onConfirm={(value: string) => {
|
|
541
|
+
const trimmed = value.trim()
|
|
542
|
+
if (!trimmed) {
|
|
543
|
+
buildL1()
|
|
544
|
+
return
|
|
545
|
+
}
|
|
546
|
+
openOAuthLabelPrompt(trimmed, oauthUrl)
|
|
547
|
+
}}
|
|
548
|
+
// Step BACK to the sign-in URL screen, not L1: the OAuth session
|
|
549
|
+
// (PKCE verifier/state) is already minted. Returning to L1 would let a
|
|
550
|
+
// retry re-run add-oauth-start and re-mint it, invalidating the URL the
|
|
551
|
+
// user is mid-sign-in with. Same session → same URL is preserved.
|
|
552
|
+
onCancel={() => openOAuthUrlScreen(oauthUrl)}
|
|
553
|
+
/>
|
|
554
|
+
))
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
const openOAuthLabelPrompt = (code: string, oauthUrl: string) => {
|
|
558
|
+
const DialogPrompt = api.ui.DialogPrompt
|
|
559
|
+
api.ui.dialog.setSize('xlarge')
|
|
560
|
+
api.ui.dialog.replace(() => (
|
|
561
|
+
<DialogPrompt
|
|
562
|
+
title='OAuth sign-in \u2014 label'
|
|
563
|
+
description={() => (
|
|
564
|
+
<text>A short name for this account (optional).</text>
|
|
565
|
+
)}
|
|
566
|
+
placeholder='e.g. work'
|
|
567
|
+
value=''
|
|
568
|
+
onConfirm={(value: string) => {
|
|
569
|
+
const label = value.trim()
|
|
570
|
+
const args = label
|
|
571
|
+
? `add-oauth-finish ${code} --label ${label}`
|
|
572
|
+
: `add-oauth-finish ${code}`
|
|
573
|
+
void apply('claude-account', args).then((r) => {
|
|
574
|
+
api.ui.toast({ message: r.text })
|
|
575
|
+
updateAccounts(r)
|
|
576
|
+
buildL1()
|
|
577
|
+
})
|
|
578
|
+
}}
|
|
579
|
+
// Step BACK to the code prompt, not L1: the user already obtained an
|
|
580
|
+
// auth code to reach this step. Returning to L1 would let a retry
|
|
581
|
+
// re-run add-oauth-start, minting a new PKCE verifier/state that
|
|
582
|
+
// invalidates the code they already have and forces a full re-auth.
|
|
583
|
+
onCancel={() => openOAuthCodePrompt(oauthUrl)}
|
|
584
|
+
/>
|
|
585
|
+
))
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
// -- Manage existing account -------------------------------------------
|
|
589
|
+
const openManage = (
|
|
590
|
+
account: (typeof accounts)[number],
|
|
591
|
+
isMain: boolean,
|
|
592
|
+
) => {
|
|
593
|
+
const DialogSelect = api.ui.DialogSelect<string>
|
|
594
|
+
const DialogConfirm = api.ui.DialogConfirm
|
|
595
|
+
api.ui.dialog.setSize('xlarge')
|
|
596
|
+
|
|
597
|
+
const options: Array<{
|
|
598
|
+
title: string
|
|
599
|
+
value: string
|
|
600
|
+
description?: string
|
|
601
|
+
}> = []
|
|
602
|
+
if (!isMain) {
|
|
603
|
+
const toggleLabel = account.enabled ? 'Disable' : 'Enable'
|
|
604
|
+
options.push({
|
|
605
|
+
title: toggleLabel,
|
|
606
|
+
value: account.enabled ? 'disable' : 'enable',
|
|
607
|
+
description: account.enabled
|
|
608
|
+
? 'Stop using this fallback account'
|
|
609
|
+
: 'Allow this fallback account to be used',
|
|
610
|
+
})
|
|
611
|
+
options.push({
|
|
612
|
+
title: 'Move up',
|
|
613
|
+
value: 'move-up',
|
|
614
|
+
description: 'Higher priority in fallback order',
|
|
615
|
+
})
|
|
616
|
+
options.push({
|
|
617
|
+
title: 'Move down',
|
|
618
|
+
value: 'move-down',
|
|
619
|
+
description: 'Lower priority in fallback order',
|
|
620
|
+
})
|
|
621
|
+
options.push({
|
|
622
|
+
title: 'Remove\u2026',
|
|
623
|
+
value: 'remove',
|
|
624
|
+
description: 'Delete this account permanently',
|
|
625
|
+
})
|
|
626
|
+
}
|
|
627
|
+
options.push({ title: 'Back', value: 'back' })
|
|
628
|
+
|
|
629
|
+
api.ui.dialog.replace(() => (
|
|
630
|
+
<DialogSelect
|
|
631
|
+
title={`Manage ${account.label}`}
|
|
632
|
+
options={options}
|
|
633
|
+
onSelect={(option) => {
|
|
634
|
+
if (option.value === 'back') {
|
|
635
|
+
buildL1()
|
|
636
|
+
return
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
if (option.value === 'remove') {
|
|
640
|
+
api.ui.dialog.replace(() => (
|
|
641
|
+
<DialogConfirm
|
|
642
|
+
title={`Remove ${account.label}?`}
|
|
643
|
+
message={`Are you sure you want to remove the fallback account "${account.label}"?`}
|
|
644
|
+
onConfirm={() => {
|
|
645
|
+
void apply('claude-account', `remove ${account.id}`).then(
|
|
646
|
+
(r) => {
|
|
647
|
+
api.ui.toast({ message: r.text })
|
|
648
|
+
updateAccounts(r)
|
|
649
|
+
buildL1()
|
|
650
|
+
},
|
|
651
|
+
)
|
|
652
|
+
}}
|
|
653
|
+
onCancel={() => openManage(account, isMain)}
|
|
654
|
+
/>
|
|
655
|
+
))
|
|
656
|
+
return
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
void apply('claude-account', `${option.value} ${account.id}`).then(
|
|
660
|
+
(r) => {
|
|
661
|
+
api.ui.toast({ message: r.text })
|
|
662
|
+
updateAccounts(r)
|
|
663
|
+
const updatedList = r.knobs.accounts as typeof accounts
|
|
664
|
+
const refreshed =
|
|
665
|
+
(updatedList && updatedList.length > 0
|
|
666
|
+
? updatedList.find((a) => a.id === account.id)
|
|
667
|
+
: undefined) ?? account
|
|
668
|
+
openManage(refreshed, isMain)
|
|
669
|
+
},
|
|
670
|
+
)
|
|
671
|
+
}}
|
|
672
|
+
/>
|
|
673
|
+
))
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
buildL1()
|
|
677
|
+
return
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
if (payload.command === 'claude-logging') {
|
|
681
|
+
const current = (payload.knobs.level as string) ?? 'info'
|
|
682
|
+
const levels = ['error', 'warn', 'info', 'debug', 'trace']
|
|
683
|
+
const DialogSelect = api.ui.DialogSelect<string>
|
|
684
|
+
api.ui.dialog.setSize('xlarge')
|
|
685
|
+
api.ui.dialog.replace(() => (
|
|
686
|
+
<DialogSelect
|
|
687
|
+
title='Claude log level'
|
|
688
|
+
current={current}
|
|
689
|
+
options={levels.map((level) => ({
|
|
690
|
+
title: level === current ? `\u2022 ${level}` : level,
|
|
691
|
+
value: level,
|
|
692
|
+
}))}
|
|
693
|
+
onSelect={(option) => {
|
|
694
|
+
void apply('claude-logging', String(option.value)).then((r) => {
|
|
695
|
+
api.ui.toast({ message: r.text })
|
|
696
|
+
api.ui.dialog.clear()
|
|
697
|
+
})
|
|
698
|
+
}}
|
|
699
|
+
/>
|
|
700
|
+
))
|
|
701
|
+
return
|
|
702
|
+
}
|
|
703
|
+
|
|
227
704
|
// fallback for quota (display-only)
|
|
228
705
|
showText(api, payload.text)
|
|
229
706
|
}
|
package/src/tui-preferences.ts
CHANGED
|
@@ -2,6 +2,10 @@ import { watch } from 'node:fs'
|
|
|
2
2
|
import { mkdir, readFile, rename, writeFile } from 'node:fs/promises'
|
|
3
3
|
import { homedir } from 'node:os'
|
|
4
4
|
import { basename, dirname, join } from 'node:path'
|
|
5
|
+
import {
|
|
6
|
+
clearTimeout as nativeClearTimeout,
|
|
7
|
+
setTimeout as nativeSetTimeout,
|
|
8
|
+
} from 'node:timers'
|
|
5
9
|
import { applyEdits, modify, type ParseError, parse } from 'jsonc-parser'
|
|
6
10
|
|
|
7
11
|
export const TUI_PREFS_FILE_ENV = 'OPENCODE_TUI_PREFERENCES_FILE'
|
|
@@ -268,6 +272,7 @@ export function queueTuiPreferenceUpdate(
|
|
|
268
272
|
}
|
|
269
273
|
|
|
270
274
|
const WATCH_DEBOUNCE_MS = 150
|
|
275
|
+
const WATCH_POLL_MS = 100
|
|
271
276
|
|
|
272
277
|
// Watches the directory rather than the file: editors and our own atomic
|
|
273
278
|
// writes replace the file via rename, which kills file-level watchers.
|
|
@@ -290,16 +295,37 @@ const WATCH_DEBOUNCE_MS = 150
|
|
|
290
295
|
export function watchTuiPreferences(onChange: () => void): () => void {
|
|
291
296
|
const file = getTuiPreferencesFile()
|
|
292
297
|
const name = basename(file)
|
|
293
|
-
let timer: ReturnType<typeof
|
|
298
|
+
let timer: ReturnType<typeof nativeSetTimeout> | null = null
|
|
294
299
|
let lastSeen: string | null = null
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
300
|
+
let observedEvent = false
|
|
301
|
+
let disposed = false
|
|
302
|
+
|
|
303
|
+
const checkForChange = async () => {
|
|
304
|
+
const text = await readFile(file, 'utf8').catch(() => null)
|
|
305
|
+
if (disposed || text === null) return
|
|
306
|
+
if (text === lastSeen) return
|
|
307
|
+
lastSeen = text
|
|
308
|
+
onChange()
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
const scheduleCheck = () => {
|
|
312
|
+
observedEvent = true
|
|
313
|
+
if (timer) nativeClearTimeout(timer)
|
|
314
|
+
timer = nativeSetTimeout(() => {
|
|
315
|
+
timer = null
|
|
316
|
+
void checkForChange()
|
|
317
|
+
}, WATCH_DEBOUNCE_MS)
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// Seed asynchronously. If a real file event arrives before the read resolves,
|
|
321
|
+
// do not let the seed overwrite that pending change; the debounced re-read
|
|
322
|
+
// will compare against the previous value and fire.
|
|
298
323
|
void readFile(file, 'utf8')
|
|
299
324
|
.then((text) => {
|
|
300
|
-
if (lastSeen === null) lastSeen = text
|
|
325
|
+
if (!observedEvent && lastSeen === null) lastSeen = text
|
|
301
326
|
})
|
|
302
327
|
.catch(() => {})
|
|
328
|
+
|
|
303
329
|
try {
|
|
304
330
|
const watcher = watch(dirname(file), (_event, filename) => {
|
|
305
331
|
// Exact match against the preferences file name, plus the temp file
|
|
@@ -308,21 +334,34 @@ export function watchTuiPreferences(onChange: () => void): () => void {
|
|
|
308
334
|
filename === name ||
|
|
309
335
|
(filename?.startsWith(`${name}.`) && filename.endsWith('.tmp'))
|
|
310
336
|
if (filename != null && !isOurs) return
|
|
311
|
-
|
|
312
|
-
timer = setTimeout(() => {
|
|
313
|
-
timer = null
|
|
314
|
-
void readFile(file, 'utf8')
|
|
315
|
-
.catch(() => null)
|
|
316
|
-
.then((text) => {
|
|
317
|
-
if (text === null) return
|
|
318
|
-
if (text === lastSeen) return
|
|
319
|
-
lastSeen = text
|
|
320
|
-
onChange()
|
|
321
|
-
})
|
|
322
|
-
}, WATCH_DEBOUNCE_MS)
|
|
337
|
+
scheduleCheck()
|
|
323
338
|
})
|
|
339
|
+
|
|
340
|
+
// Bun's fs.watch can miss atomic temp-file rename updates on some
|
|
341
|
+
// backends, so keep a low-cost polling loop as the correctness path.
|
|
342
|
+
// Use recursive timers instead of fs.watchFile because watchFile can keep
|
|
343
|
+
// Bun test processes alive long after unwatchFile().
|
|
344
|
+
let pollTimer: ReturnType<typeof nativeSetTimeout> | null = null
|
|
345
|
+
let pollInFlight = false
|
|
346
|
+
const schedulePoll = () => {
|
|
347
|
+
if (disposed || pollTimer) return
|
|
348
|
+
pollTimer = nativeSetTimeout(() => {
|
|
349
|
+
pollTimer = null
|
|
350
|
+
if (disposed || pollInFlight) return
|
|
351
|
+
pollInFlight = true
|
|
352
|
+
void checkForChange().finally(() => {
|
|
353
|
+
pollInFlight = false
|
|
354
|
+
schedulePoll()
|
|
355
|
+
})
|
|
356
|
+
}, WATCH_POLL_MS)
|
|
357
|
+
pollTimer.unref?.()
|
|
358
|
+
}
|
|
359
|
+
schedulePoll()
|
|
360
|
+
|
|
324
361
|
return () => {
|
|
325
|
-
|
|
362
|
+
disposed = true
|
|
363
|
+
if (timer) nativeClearTimeout(timer)
|
|
364
|
+
if (pollTimer) nativeClearTimeout(pollTimer)
|
|
326
365
|
watcher.close()
|
|
327
366
|
}
|
|
328
367
|
} catch {
|