@cortexkit/opencode-anthropic-auth 1.16.0 → 1.18.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/README.md +3 -3
- package/dist/cli.js +1 -1
- package/dist/fable-fallback.d.ts +3 -2
- package/dist/{index-epg96jpt.js → index-1ets4bf8.js} +1 -1
- package/dist/{index-dbczmr1v.js → index-9aeag0wm.js} +1 -1
- package/dist/index-h0pmqm96.js +422 -0
- package/dist/{index-5xsc5gwa.js → index-k5enth8h.js} +1 -1
- package/dist/{index-z9dywmf2.js → index-kckdabb4.js} +1 -1
- package/dist/index-xjvcfyt6.js +2 -0
- package/dist/index.js +12 -12
- package/dist/{logger-ta3qm0a2.js → logger-whpn7rjx.js} +1 -1
- package/dist/rpc/port-file.js +1 -1
- package/dist/rpc/rpc-client.js +1 -1
- package/dist/rpc/rpc-dir.js +1 -1
- package/dist/sidebar-state.d.ts +64 -2
- package/dist/sidebar-state.js +1 -1
- package/dist/transform.d.ts +1 -0
- package/dist/tui/command-dialogs.d.ts +12 -0
- package/dist/tui-preferences.js +1 -1
- package/dist/tui.d.ts +2 -0
- package/package.json +2 -2
- package/src/sidebar-state.ts +407 -5
- package/src/tui/command-dialogs.tsx +22 -11
- package/src/tui-compiled/sidebar-state.ts +407 -5
- package/src/tui-compiled/tui/command-dialogs.tsx +12 -8
- package/src/tui-compiled/tui.tsx +147 -104
- package/src/tui.tsx +34 -2
- package/dist/index-0z4mmpxy.js +0 -2
- package/dist/index-sgwz35qx.js +0 -422
package/src/sidebar-state.ts
CHANGED
|
@@ -15,6 +15,15 @@ export interface AccountQuota {
|
|
|
15
15
|
five_hour?: QuotaWindow
|
|
16
16
|
seven_day?: QuotaWindow
|
|
17
17
|
scoped?: ScopedQuotaWindow[]
|
|
18
|
+
extraUsage?: {
|
|
19
|
+
used: { amountMinor: number; currency: string; exponent: number }
|
|
20
|
+
limit: { amountMinor: number; currency: string; exponent: number }
|
|
21
|
+
utilizationPercent?: number
|
|
22
|
+
severity?: string
|
|
23
|
+
exhausted: boolean
|
|
24
|
+
}
|
|
25
|
+
bindingWindow?: string
|
|
26
|
+
fallbackAdvised?: boolean
|
|
18
27
|
}
|
|
19
28
|
|
|
20
29
|
export interface SidebarAccountState {
|
|
@@ -25,6 +34,7 @@ export interface SidebarAccountState {
|
|
|
25
34
|
// True when the account's refresh token is permanently dead (400
|
|
26
35
|
// invalid_grant) and it needs a re-login — distinct from a transient backoff.
|
|
27
36
|
needsReauth: boolean
|
|
37
|
+
tierLabel?: string
|
|
28
38
|
}
|
|
29
39
|
|
|
30
40
|
export interface FableRecoverySidebarState {
|
|
@@ -32,11 +42,18 @@ export interface FableRecoverySidebarState {
|
|
|
32
42
|
mode: 'opus' | 'fable'
|
|
33
43
|
remaining: number
|
|
34
44
|
changedAt: number
|
|
45
|
+
/**
|
|
46
|
+
* The model id the user originally requested (e.g. 'claude-fable-5' or
|
|
47
|
+
* 'claude-opus-5'). Optional for backward compatibility with pre-Opus-5
|
|
48
|
+
* state files; older recoveries default to Fable 5 in the summary.
|
|
49
|
+
*/
|
|
50
|
+
requestedModelId?: string
|
|
35
51
|
}
|
|
36
52
|
|
|
37
53
|
export interface SidebarState {
|
|
38
54
|
main: {
|
|
39
55
|
quota: AccountQuota | null
|
|
56
|
+
tierLabel?: string
|
|
40
57
|
quotaBackedOff?: boolean
|
|
41
58
|
quotaBackoffUntil?: number
|
|
42
59
|
refreshBackedOff?: boolean
|
|
@@ -56,9 +73,20 @@ export interface SidebarState {
|
|
|
56
73
|
lastUpdated: number
|
|
57
74
|
}
|
|
58
75
|
|
|
59
|
-
import {
|
|
76
|
+
import { randomUUID } from 'node:crypto'
|
|
77
|
+
import {
|
|
78
|
+
mkdir,
|
|
79
|
+
readFile,
|
|
80
|
+
rename,
|
|
81
|
+
rm,
|
|
82
|
+
rmdir,
|
|
83
|
+
stat,
|
|
84
|
+
unlink,
|
|
85
|
+
writeFile,
|
|
86
|
+
} from 'node:fs/promises'
|
|
60
87
|
import { tmpdir } from 'node:os'
|
|
61
88
|
import { dirname, join } from 'node:path'
|
|
89
|
+
import { logger } from '@cortexkit/anthropic-auth-core'
|
|
62
90
|
|
|
63
91
|
const STATE_FILE_ENV = 'OPENCODE_ANTHROPIC_AUTH_SIDEBAR_STATE_FILE'
|
|
64
92
|
const DEFAULT_STATE_DIR = join(tmpdir(), 'opencode-anthropic-auth')
|
|
@@ -136,9 +164,53 @@ function normalizeAccountQuota(value: unknown): AccountQuota | null {
|
|
|
136
164
|
quota.scoped = scoped
|
|
137
165
|
}
|
|
138
166
|
|
|
167
|
+
if (isRecord(value.extraUsage)) {
|
|
168
|
+
const used = normalizeQuotaMoney(value.extraUsage.used)
|
|
169
|
+
const limit = normalizeQuotaMoney(value.extraUsage.limit)
|
|
170
|
+
if (used && limit && typeof value.extraUsage.exhausted === 'boolean') {
|
|
171
|
+
quota.extraUsage = {
|
|
172
|
+
used,
|
|
173
|
+
limit,
|
|
174
|
+
...(typeof value.extraUsage.utilizationPercent === 'number' &&
|
|
175
|
+
Number.isFinite(value.extraUsage.utilizationPercent) && {
|
|
176
|
+
utilizationPercent: value.extraUsage.utilizationPercent,
|
|
177
|
+
}),
|
|
178
|
+
...(typeof value.extraUsage.severity === 'string' && {
|
|
179
|
+
severity: value.extraUsage.severity,
|
|
180
|
+
}),
|
|
181
|
+
exhausted: value.extraUsage.exhausted,
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
if (typeof value.bindingWindow === 'string' && value.bindingWindow.trim()) {
|
|
186
|
+
quota.bindingWindow = value.bindingWindow.trim()
|
|
187
|
+
}
|
|
188
|
+
if (typeof value.fallbackAdvised === 'boolean') {
|
|
189
|
+
quota.fallbackAdvised = value.fallbackAdvised
|
|
190
|
+
}
|
|
191
|
+
|
|
139
192
|
return Object.keys(quota).length ? quota : null
|
|
140
193
|
}
|
|
141
194
|
|
|
195
|
+
function normalizeQuotaMoney(value: unknown) {
|
|
196
|
+
if (!isRecord(value)) return undefined
|
|
197
|
+
if (
|
|
198
|
+
!Number.isInteger(value.amountMinor) ||
|
|
199
|
+
typeof value.currency !== 'string' ||
|
|
200
|
+
!/^[A-Za-z]{3}$/.test(value.currency.trim()) ||
|
|
201
|
+
!Number.isInteger(value.exponent) ||
|
|
202
|
+
(value.exponent as number) < 0 ||
|
|
203
|
+
(value.exponent as number) > 20
|
|
204
|
+
) {
|
|
205
|
+
return undefined
|
|
206
|
+
}
|
|
207
|
+
return {
|
|
208
|
+
amountMinor: value.amountMinor as number,
|
|
209
|
+
currency: value.currency.trim(),
|
|
210
|
+
exponent: value.exponent as number,
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
142
214
|
export function normalizeSidebarState(raw: unknown): SidebarState {
|
|
143
215
|
if (!isRecord(raw)) return { ...DEFAULT_SIDEBAR_STATE }
|
|
144
216
|
|
|
@@ -146,6 +218,9 @@ export function normalizeSidebarState(raw: unknown): SidebarState {
|
|
|
146
218
|
if (isRecord(raw.main)) {
|
|
147
219
|
const m = raw.main
|
|
148
220
|
main.quota = normalizeAccountQuota(m.quota)
|
|
221
|
+
if (typeof m.tierLabel === 'string' && m.tierLabel.trim()) {
|
|
222
|
+
main.tierLabel = m.tierLabel.trim()
|
|
223
|
+
}
|
|
149
224
|
if (typeof m.quotaBackedOff === 'boolean')
|
|
150
225
|
main.quotaBackedOff = m.quotaBackedOff
|
|
151
226
|
if (typeof m.quotaBackoffUntil === 'number')
|
|
@@ -167,6 +242,10 @@ export function normalizeSidebarState(raw: unknown): SidebarState {
|
|
|
167
242
|
enabled: typeof entry.enabled === 'boolean' ? entry.enabled : false,
|
|
168
243
|
needsReauth:
|
|
169
244
|
typeof entry.needsReauth === 'boolean' ? entry.needsReauth : false,
|
|
245
|
+
tierLabel:
|
|
246
|
+
typeof entry.tierLabel === 'string' && entry.tierLabel.trim()
|
|
247
|
+
? entry.tierLabel.trim()
|
|
248
|
+
: undefined,
|
|
170
249
|
}))
|
|
171
250
|
: []
|
|
172
251
|
|
|
@@ -214,6 +293,10 @@ export function normalizeSidebarState(raw: unknown): SidebarState {
|
|
|
214
293
|
mode: recovery.mode,
|
|
215
294
|
remaining: Math.max(0, Math.floor(recovery.remaining)),
|
|
216
295
|
changedAt: recovery.changedAt,
|
|
296
|
+
requestedModelId:
|
|
297
|
+
typeof recovery.requestedModelId === 'string'
|
|
298
|
+
? recovery.requestedModelId
|
|
299
|
+
: undefined,
|
|
217
300
|
},
|
|
218
301
|
]
|
|
219
302
|
})
|
|
@@ -238,23 +321,327 @@ export function normalizeSidebarState(raw: unknown): SidebarState {
|
|
|
238
321
|
|
|
239
322
|
let writeChain: Promise<void> = Promise.resolve()
|
|
240
323
|
|
|
241
|
-
|
|
324
|
+
// Wait through ordinary contention while staying below the 2s stale-eviction window.
|
|
325
|
+
const SIDEBAR_LOCK_BUDGET_MS = 1_000
|
|
326
|
+
const SIDEBAR_LOCK_STALE_MS = 2_000
|
|
327
|
+
const SIDEBAR_LOCK_RETRY_MIN_MS = 5
|
|
328
|
+
const SIDEBAR_LOCK_RETRY_MAX_MS = 15
|
|
329
|
+
|
|
330
|
+
interface SidebarStateWriteTestHooks {
|
|
331
|
+
afterMergeRead?: (stateFile: string) => void | Promise<void>
|
|
332
|
+
beforeRename?: (stateFile: string, tempFile: string) => void | Promise<void>
|
|
333
|
+
afterRename?: (stateFile: string) => void | Promise<void>
|
|
334
|
+
afterStaleLockStat?: (lockDir: string) => void | Promise<void>
|
|
335
|
+
onStaleLockClaimed?: (lockDir: string) => void | Promise<void>
|
|
336
|
+
onLockAcquired?: (lockDir: string) => void | Promise<void>
|
|
337
|
+
beforeReleaseDirectoryRemoval?: (lockDir: string) => void | Promise<void>
|
|
338
|
+
lockBudgetMs?: number
|
|
339
|
+
lockRetryMinMs?: number
|
|
340
|
+
lockRetryMaxMs?: number
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
let sidebarStateWriteTestHooks: SidebarStateWriteTestHooks | null = null
|
|
344
|
+
|
|
345
|
+
export function __setSidebarStateWriteTestHooks(
|
|
346
|
+
hooks: SidebarStateWriteTestHooks | null,
|
|
347
|
+
): void {
|
|
348
|
+
sidebarStateWriteTestHooks = hooks
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// Boot-routing test hooks live here rather than in index.ts: the plugin entry
|
|
352
|
+
// module must export nothing beyond the plugin factory — opencode's plugin
|
|
353
|
+
// loader treats extra entry-point exports as plugin candidates and fails to
|
|
354
|
+
// load the plugin (same incident class as the removed
|
|
355
|
+
// __setBootProfileHydrationForTest export).
|
|
356
|
+
export interface InitialSidebarRoutingTestHooks {
|
|
357
|
+
beforeSidebarRead?: () => void | Promise<void>
|
|
358
|
+
beforeStorageLoad?: () => void | Promise<void>
|
|
359
|
+
afterStorageLoad?: () => void | Promise<void>
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
let initialSidebarRoutingTestHooks: InitialSidebarRoutingTestHooks | null = null
|
|
363
|
+
|
|
364
|
+
export function __setInitialSidebarRoutingTestHooks(
|
|
365
|
+
hooks: InitialSidebarRoutingTestHooks | null,
|
|
366
|
+
): void {
|
|
367
|
+
initialSidebarRoutingTestHooks = hooks
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
export function getInitialSidebarRoutingTestHooks(): InitialSidebarRoutingTestHooks | null {
|
|
371
|
+
return initialSidebarRoutingTestHooks
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
function sleep(ms: number): Promise<void> {
|
|
375
|
+
return new Promise((resolve) => setTimeout(resolve, ms))
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
async function acquireSidebarStateLock(stateFile: string): Promise<{
|
|
379
|
+
release: () => Promise<void>
|
|
380
|
+
ownsLock: () => Promise<boolean>
|
|
381
|
+
} | null> {
|
|
382
|
+
const lockDir = `${stateFile}.lock`
|
|
383
|
+
const budgetMs =
|
|
384
|
+
sidebarStateWriteTestHooks?.lockBudgetMs ?? SIDEBAR_LOCK_BUDGET_MS
|
|
385
|
+
const retryMinMs =
|
|
386
|
+
sidebarStateWriteTestHooks?.lockRetryMinMs ?? SIDEBAR_LOCK_RETRY_MIN_MS
|
|
387
|
+
const retryMaxMs =
|
|
388
|
+
sidebarStateWriteTestHooks?.lockRetryMaxMs ?? SIDEBAR_LOCK_RETRY_MAX_MS
|
|
389
|
+
const deadline = Date.now() + budgetMs
|
|
390
|
+
|
|
391
|
+
while (true) {
|
|
392
|
+
try {
|
|
393
|
+
await mkdir(lockDir)
|
|
394
|
+
const ownerId = randomUUID()
|
|
395
|
+
const ownerFile = join(lockDir, ownerId)
|
|
396
|
+
await writeFile(ownerFile, '', {
|
|
397
|
+
encoding: 'utf8',
|
|
398
|
+
mode: 0o600,
|
|
399
|
+
flag: 'wx',
|
|
400
|
+
})
|
|
401
|
+
await sidebarStateWriteTestHooks?.onLockAcquired?.(lockDir)
|
|
402
|
+
return {
|
|
403
|
+
ownsLock: async () => {
|
|
404
|
+
try {
|
|
405
|
+
await stat(ownerFile)
|
|
406
|
+
return true
|
|
407
|
+
} catch {
|
|
408
|
+
return false
|
|
409
|
+
}
|
|
410
|
+
},
|
|
411
|
+
release: async () => {
|
|
412
|
+
// Unlinking this acquisition's unique file makes the filesystem reject
|
|
413
|
+
// a stale release after eviction handed the path to a successor.
|
|
414
|
+
try {
|
|
415
|
+
await unlink(ownerFile)
|
|
416
|
+
} catch {
|
|
417
|
+
return
|
|
418
|
+
}
|
|
419
|
+
await sidebarStateWriteTestHooks?.beforeReleaseDirectoryRemoval?.(
|
|
420
|
+
lockDir,
|
|
421
|
+
)
|
|
422
|
+
await rmdir(lockDir).catch(() => {})
|
|
423
|
+
},
|
|
424
|
+
}
|
|
425
|
+
} catch (error) {
|
|
426
|
+
if ((error as NodeJS.ErrnoException).code !== 'EEXIST') {
|
|
427
|
+
logger.trace('sidebar', 'state lock unavailable; write skipped', {
|
|
428
|
+
stateFile,
|
|
429
|
+
error: error instanceof Error ? error.message : String(error),
|
|
430
|
+
})
|
|
431
|
+
return null
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
try {
|
|
436
|
+
const lockStat = await stat(lockDir)
|
|
437
|
+
if (Date.now() - lockStat.mtimeMs > SIDEBAR_LOCK_STALE_MS) {
|
|
438
|
+
await sidebarStateWriteTestHooks?.afterStaleLockStat?.(lockDir)
|
|
439
|
+
const evictedLockDir = `${lockDir}.evict-${process.pid}-${randomUUID()}`
|
|
440
|
+
try {
|
|
441
|
+
await rename(lockDir, evictedLockDir)
|
|
442
|
+
} catch (error) {
|
|
443
|
+
if ((error as NodeJS.ErrnoException).code === 'ENOENT') continue
|
|
444
|
+
throw error
|
|
445
|
+
}
|
|
446
|
+
await sidebarStateWriteTestHooks?.onStaleLockClaimed?.(lockDir)
|
|
447
|
+
await rm(evictedLockDir, { recursive: true, force: true }).catch(
|
|
448
|
+
() => {},
|
|
449
|
+
)
|
|
450
|
+
continue
|
|
451
|
+
}
|
|
452
|
+
} catch (error) {
|
|
453
|
+
if ((error as NodeJS.ErrnoException).code === 'ENOENT') continue
|
|
454
|
+
logger.trace('sidebar', 'state lock inspection failed; write skipped', {
|
|
455
|
+
stateFile,
|
|
456
|
+
error: error instanceof Error ? error.message : String(error),
|
|
457
|
+
})
|
|
458
|
+
return null
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
const remainingMs = deadline - Date.now()
|
|
462
|
+
if (remainingMs <= 0) {
|
|
463
|
+
logger.warn('sidebar', 'lock budget exhausted, write skipped', {
|
|
464
|
+
stateFile,
|
|
465
|
+
budgetMs,
|
|
466
|
+
})
|
|
467
|
+
return null
|
|
468
|
+
}
|
|
469
|
+
const jitterMs =
|
|
470
|
+
retryMinMs + Math.floor(Math.random() * (retryMaxMs - retryMinMs + 1))
|
|
471
|
+
await sleep(Math.min(jitterMs, remainingMs))
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
export async function __acquireSidebarStateLockForTest(
|
|
476
|
+
stateFile: string,
|
|
477
|
+
): Promise<(() => Promise<void>) | null> {
|
|
478
|
+
return (await acquireSidebarStateLock(stateFile))?.release ?? null
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
async function writeSidebarStateAtomic(
|
|
482
|
+
stateFile: string,
|
|
483
|
+
state: SidebarState,
|
|
484
|
+
ownsLock: () => Promise<boolean>,
|
|
485
|
+
): Promise<'written' | 'lock-lost-before-rename' | 'lock-lost-after-rename'> {
|
|
486
|
+
const tempFile = `${stateFile}.${process.pid}.${randomUUID()}.tmp`
|
|
487
|
+
await writeFile(tempFile, JSON.stringify(state), {
|
|
488
|
+
encoding: 'utf8',
|
|
489
|
+
mode: 0o600,
|
|
490
|
+
})
|
|
242
491
|
try {
|
|
243
|
-
|
|
492
|
+
if (!(await ownsLock())) {
|
|
493
|
+
logger.trace('sidebar', 'state lock lost before rename; write aborted', {
|
|
494
|
+
stateFile,
|
|
495
|
+
})
|
|
496
|
+
await rm(tempFile, { force: true }).catch(() => {})
|
|
497
|
+
return 'lock-lost-before-rename'
|
|
498
|
+
}
|
|
499
|
+
await sidebarStateWriteTestHooks?.beforeRename?.(stateFile, tempFile)
|
|
500
|
+
// No production await separates this ownership fence from rename. A process
|
|
501
|
+
// freeze between the adjacent syscalls remains possible, so rename is also
|
|
502
|
+
// fenced from the other side below.
|
|
503
|
+
if (!(await ownsLock())) {
|
|
504
|
+
logger.trace('sidebar', 'state lock lost before rename; write aborted', {
|
|
505
|
+
stateFile,
|
|
506
|
+
})
|
|
507
|
+
await rm(tempFile, { force: true }).catch(() => {})
|
|
508
|
+
return 'lock-lost-before-rename'
|
|
509
|
+
}
|
|
510
|
+
await rename(tempFile, stateFile)
|
|
511
|
+
await sidebarStateWriteTestHooks?.afterRename?.(stateFile)
|
|
512
|
+
if (!(await ownsLock())) return 'lock-lost-after-rename'
|
|
513
|
+
return 'written'
|
|
514
|
+
} catch (error) {
|
|
515
|
+
await rm(tempFile, { force: true }).catch(() => {})
|
|
516
|
+
throw error
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
export async function getSidebarState(
|
|
521
|
+
stateFile = getSidebarStateFile(),
|
|
522
|
+
): Promise<SidebarState> {
|
|
523
|
+
try {
|
|
524
|
+
const raw = await readFile(stateFile, 'utf8')
|
|
244
525
|
return normalizeSidebarState(JSON.parse(raw))
|
|
245
526
|
} catch {
|
|
246
527
|
return DEFAULT_SIDEBAR_STATE
|
|
247
528
|
}
|
|
248
529
|
}
|
|
249
530
|
|
|
531
|
+
export interface SidebarStateWriteOptions {
|
|
532
|
+
routingAuthoritative?: boolean
|
|
533
|
+
resolvePreservedRouting?: (current: SidebarState) =>
|
|
534
|
+
| (Pick<SidebarState, 'activeId' | 'route'> & {
|
|
535
|
+
state?: SidebarState
|
|
536
|
+
})
|
|
537
|
+
| undefined
|
|
538
|
+
| Promise<
|
|
539
|
+
| (Pick<SidebarState, 'activeId' | 'route'> & {
|
|
540
|
+
state?: SidebarState
|
|
541
|
+
})
|
|
542
|
+
| undefined
|
|
543
|
+
>
|
|
544
|
+
onRoutingResolved?: (
|
|
545
|
+
routing: Pick<SidebarState, 'activeId' | 'route'>,
|
|
546
|
+
) => void
|
|
547
|
+
}
|
|
548
|
+
|
|
250
549
|
export async function setSidebarState(
|
|
251
550
|
state: SidebarState,
|
|
252
551
|
stateFile = getSidebarStateFile(),
|
|
552
|
+
options: SidebarStateWriteOptions = {},
|
|
253
553
|
): Promise<void> {
|
|
254
554
|
writeChain = writeChain
|
|
255
555
|
.then(async () => {
|
|
256
556
|
await mkdir(dirname(stateFile), { recursive: true })
|
|
257
|
-
|
|
557
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
558
|
+
const repairingPostRenameLoss = attempt === 1
|
|
559
|
+
const lock = await acquireSidebarStateLock(stateFile)
|
|
560
|
+
// Sidebar frames are display-only and refresh within seconds; dropping one
|
|
561
|
+
// is safer than clobbering routing state that may stay authoritative until
|
|
562
|
+
// another process handles its next request.
|
|
563
|
+
if (!lock) {
|
|
564
|
+
if (repairingPostRenameLoss) {
|
|
565
|
+
logger.warn(
|
|
566
|
+
'sidebar',
|
|
567
|
+
'post-rename repair lock unavailable; write skipped',
|
|
568
|
+
{ stateFile },
|
|
569
|
+
)
|
|
570
|
+
}
|
|
571
|
+
return
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
let stateToWrite = state
|
|
575
|
+
let result:
|
|
576
|
+
| 'written'
|
|
577
|
+
| 'lock-lost-before-rename'
|
|
578
|
+
| 'lock-lost-after-rename'
|
|
579
|
+
try {
|
|
580
|
+
if (options.routingAuthoritative === false) {
|
|
581
|
+
const current = await getSidebarState(stateFile)
|
|
582
|
+
const preservedRouting =
|
|
583
|
+
await options.resolvePreservedRouting?.(current)
|
|
584
|
+
if (preservedRouting) {
|
|
585
|
+
const preservedState = preservedRouting.state ?? state
|
|
586
|
+
stateToWrite = {
|
|
587
|
+
...preservedState,
|
|
588
|
+
activeId: preservedRouting.activeId,
|
|
589
|
+
route: preservedRouting.route,
|
|
590
|
+
lastUpdated: Math.max(
|
|
591
|
+
preservedState.lastUpdated,
|
|
592
|
+
current.lastUpdated,
|
|
593
|
+
),
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
await sidebarStateWriteTestHooks?.afterMergeRead?.(stateFile)
|
|
597
|
+
} else if (repairingPostRenameLoss) {
|
|
598
|
+
const current = await getSidebarState(stateFile)
|
|
599
|
+
// A successor owns its fresh account/quota and recovery snapshots;
|
|
600
|
+
// this routing frame may only republish its decision and shared UI state.
|
|
601
|
+
stateToWrite = {
|
|
602
|
+
...current,
|
|
603
|
+
activeId: state.activeId,
|
|
604
|
+
route: state.route,
|
|
605
|
+
relay: state.relay,
|
|
606
|
+
fastMode: state.fastMode,
|
|
607
|
+
cacheKeep: state.cacheKeep,
|
|
608
|
+
lastUpdated: Math.max(current.lastUpdated, state.lastUpdated),
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
result = await writeSidebarStateAtomic(
|
|
612
|
+
stateFile,
|
|
613
|
+
stateToWrite,
|
|
614
|
+
lock.ownsLock,
|
|
615
|
+
)
|
|
616
|
+
} finally {
|
|
617
|
+
await lock.release()
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
if (result === 'lock-lost-after-rename') {
|
|
621
|
+
if (repairingPostRenameLoss) {
|
|
622
|
+
logger.warn(
|
|
623
|
+
'sidebar',
|
|
624
|
+
'post-rename repair lost lock; write skipped',
|
|
625
|
+
{ stateFile },
|
|
626
|
+
)
|
|
627
|
+
return
|
|
628
|
+
}
|
|
629
|
+
logger.warn(
|
|
630
|
+
'sidebar',
|
|
631
|
+
'state lock lost after rename; repairing write',
|
|
632
|
+
{
|
|
633
|
+
stateFile,
|
|
634
|
+
},
|
|
635
|
+
)
|
|
636
|
+
continue
|
|
637
|
+
}
|
|
638
|
+
if (result === 'lock-lost-before-rename') return
|
|
639
|
+
options.onRoutingResolved?.({
|
|
640
|
+
activeId: stateToWrite.activeId,
|
|
641
|
+
route: stateToWrite.route,
|
|
642
|
+
})
|
|
643
|
+
return
|
|
644
|
+
}
|
|
258
645
|
})
|
|
259
646
|
.catch(() => {
|
|
260
647
|
// Best-effort — sidebar is non-critical
|
|
@@ -300,6 +687,19 @@ export function formatScopedQuotaLabel(title: string) {
|
|
|
300
687
|
return /^fable$/i.test(label) ? 'Fa' : label
|
|
301
688
|
}
|
|
302
689
|
|
|
690
|
+
/**
|
|
691
|
+
* Pretty label for a recoverable-refusal source model (Fable 5 or Opus 5).
|
|
692
|
+
* Falls back to the raw id for unrecognized ids so the sidebar never goes
|
|
693
|
+
* blank on a future model that has not yet been cataloged here.
|
|
694
|
+
*/
|
|
695
|
+
export function formatFallbackModelLabel(modelId: string | undefined): string {
|
|
696
|
+
if (modelId === 'claude-fable-5' || modelId?.startsWith('claude-fable-5-'))
|
|
697
|
+
return 'Fable 5'
|
|
698
|
+
if (modelId === 'claude-opus-5' || modelId?.startsWith('claude-opus-5-'))
|
|
699
|
+
return 'Opus 5'
|
|
700
|
+
return modelId ?? 'Fable 5'
|
|
701
|
+
}
|
|
702
|
+
|
|
303
703
|
export function getFableRecoverySummary(
|
|
304
704
|
state: SidebarState,
|
|
305
705
|
sessionId: string,
|
|
@@ -308,7 +708,9 @@ export function getFableRecoverySummary(
|
|
|
308
708
|
(candidate) => candidate.sessionId === sessionId,
|
|
309
709
|
)
|
|
310
710
|
if (!recovery) return undefined
|
|
311
|
-
if (recovery.mode === 'fable')
|
|
711
|
+
if (recovery.mode === 'fable') {
|
|
712
|
+
return `${formatFallbackModelLabel(recovery.requestedModelId)} · restored`
|
|
713
|
+
}
|
|
312
714
|
return `Opus 4.8 · ${recovery.remaining} left`
|
|
313
715
|
}
|
|
314
716
|
|
|
@@ -32,6 +32,26 @@ export function buildKillswitchThresholdSeed(
|
|
|
32
32
|
return seedParts.join(' ')
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
+
export function buildAccountDialogOption(account: {
|
|
36
|
+
id: string
|
|
37
|
+
label: string
|
|
38
|
+
role: string
|
|
39
|
+
enabled: boolean
|
|
40
|
+
quotaPercent: number | null
|
|
41
|
+
tierLabel?: string
|
|
42
|
+
}) {
|
|
43
|
+
const pct =
|
|
44
|
+
account.quotaPercent != null
|
|
45
|
+
? ` ${Math.round(account.quotaPercent)}%`
|
|
46
|
+
: ' \u2013%'
|
|
47
|
+
const status = !account.enabled ? ' (disabled)' : ''
|
|
48
|
+
return {
|
|
49
|
+
title: `${account.label} [${account.role}]${status}${pct}`,
|
|
50
|
+
value: account.id,
|
|
51
|
+
...(account.tierLabel && { description: account.tierLabel }),
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
35
55
|
function showText(api: TuiPluginApi, text: string) {
|
|
36
56
|
api.ui.dialog.setSize('xlarge')
|
|
37
57
|
api.ui.dialog.replace(() => (
|
|
@@ -253,6 +273,7 @@ export function openCommandDialog(
|
|
|
253
273
|
role: string
|
|
254
274
|
enabled: boolean
|
|
255
275
|
quotaPercent: number | null
|
|
276
|
+
tierLabel?: string
|
|
256
277
|
}>) ?? []
|
|
257
278
|
|
|
258
279
|
const updateAccounts = (r: {
|
|
@@ -278,17 +299,7 @@ export function openCommandDialog(
|
|
|
278
299
|
value: '__add__',
|
|
279
300
|
description: 'Add an API key or OAuth fallback account',
|
|
280
301
|
},
|
|
281
|
-
...accounts.map(
|
|
282
|
-
const pct =
|
|
283
|
-
a.quotaPercent != null
|
|
284
|
-
? ` ${Math.round(a.quotaPercent)}%`
|
|
285
|
-
: ' \u2013%'
|
|
286
|
-
const status = !a.enabled ? ' (disabled)' : ''
|
|
287
|
-
return {
|
|
288
|
-
title: `${a.label} [${a.role}]${status}${pct}`,
|
|
289
|
-
value: a.id,
|
|
290
|
-
}
|
|
291
|
-
}),
|
|
302
|
+
...accounts.map(buildAccountDialogOption),
|
|
292
303
|
]
|
|
293
304
|
api.ui.dialog.setSize('xlarge')
|
|
294
305
|
api.ui.dialog.replace(() => (
|