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