@cortexkit/opencode-openai-auth 0.3.3 → 0.4.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 +1 -0
- package/dist/cli.js +1 -1
- package/dist/codex-http.d.ts +3 -0
- package/dist/config.d.ts +3 -0
- package/dist/core/accounts.d.ts +9 -3
- package/dist/core/oauth.d.ts +1 -1
- package/dist/core/quota-manager.d.ts +2 -2
- package/dist/{index-k5zwf3j1.js → index-1anzb8jh.js} +1 -1
- package/dist/index-1r02s323.js +6 -0
- package/dist/index-2ng03cby.js +2 -0
- package/dist/{index-e7m7qbne.js → index-6y6ss1kc.js} +1 -1
- package/dist/{index-1xmasv0x.js → index-b6kp9ydc.js} +1 -1
- package/dist/index-cz1bkmjz.js +2 -0
- package/dist/index-dzg88nw2.js +89 -0
- package/dist/index-y67qn95a.js +2 -0
- package/dist/index.d.ts +8 -1
- package/dist/index.js +65 -65
- package/dist/model-costs.d.ts +29 -0
- package/dist/prompt-context.d.ts +1 -1
- package/dist/quota-normalize-8yzxqy78.js +1 -0
- package/dist/quota-normalize.d.ts +13 -4
- 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 +47 -2
- package/dist/sidebar-state.js +1 -1
- package/dist/token-util-h46jb3eg.js +1 -0
- package/dist/token-yzcb1p3e.js +1 -0
- package/dist/tui/command-dialogs.d.ts +8 -1
- package/dist/tui-preferences.js +1 -1
- package/dist/tui.d.ts +18 -1
- package/dist/version.d.ts +1 -1
- package/dist/ws-pool.d.ts +1 -0
- package/package.json +11 -8
- package/src/core/refresh-file-lock.ts +293 -0
- package/src/sidebar-state.ts +392 -13
- package/src/tui/command-dialogs.tsx +43 -25
- package/src/tui/entry.mjs +37 -0
- package/src/tui-compiled/logger.ts +161 -0
- package/src/tui-compiled/rpc/port-file.ts +70 -0
- package/src/tui-compiled/rpc/protocol.ts +31 -0
- package/src/tui-compiled/rpc/rpc-client.ts +78 -0
- package/src/tui-compiled/rpc/rpc-dir.ts +22 -0
- package/src/tui-compiled/sidebar-state.ts +695 -0
- package/src/tui-compiled/tui/command-dialogs.tsx +645 -0
- package/src/tui-compiled/tui-preferences.ts +328 -0
- package/src/tui-compiled/tui.tsx +1098 -0
- package/src/tui-compiled/util/open-url.ts +30 -0
- package/src/tui.tsx +158 -77
- package/dist/index-5s67238f.js +0 -2
- package/dist/index-d9hq0tmc.js +0 -2
- package/dist/index-mrg2fms9.js +0 -2
- package/dist/index-phrx93zy.js +0 -92
- package/dist/index-qdh7pt5a.js +0 -3
- package/dist/quota-normalize-dfdpwwm3.js +0 -1
- package/dist/token-01dcf92a.js +0 -1
- package/dist/token-util-rkqd82q9.js +0 -1
package/src/sidebar-state.ts
CHANGED
|
@@ -2,11 +2,79 @@ export interface QuotaWindow {
|
|
|
2
2
|
usedPercent: number
|
|
3
3
|
remainingPercent: number
|
|
4
4
|
resetsAt?: string
|
|
5
|
+
windowMinutes?: number
|
|
5
6
|
}
|
|
6
7
|
|
|
7
8
|
export interface AccountQuota {
|
|
8
9
|
primary?: QuotaWindow
|
|
9
10
|
secondary?: QuotaWindow
|
|
11
|
+
resetCreditsAvailable?: number
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export type QuotaWindowKey = 'primary' | 'secondary'
|
|
15
|
+
|
|
16
|
+
const QUOTA_WINDOW_KEYS: readonly QuotaWindowKey[] = ['primary', 'secondary']
|
|
17
|
+
const LEGACY_WINDOW_MINUTES: Record<QuotaWindowKey, number> = {
|
|
18
|
+
primary: 300,
|
|
19
|
+
secondary: 10_080,
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function compactUnit(value: number): string {
|
|
23
|
+
return Number.isInteger(value)
|
|
24
|
+
? String(value)
|
|
25
|
+
: String(Math.round(value * 10) / 10)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Derives a short human label ("5h", "1d", "7d") from a window length in
|
|
29
|
+
// minutes. Snapshots written before dynamic windows carry no length, so retain
|
|
30
|
+
// their historical primary=5h and secondary=7d meanings.
|
|
31
|
+
export function formatWindowLabel(
|
|
32
|
+
windowMinutes: number | undefined,
|
|
33
|
+
fallbackKey: QuotaWindowKey,
|
|
34
|
+
): string {
|
|
35
|
+
const minutes =
|
|
36
|
+
windowMinutes !== undefined &&
|
|
37
|
+
Number.isFinite(windowMinutes) &&
|
|
38
|
+
windowMinutes > 0
|
|
39
|
+
? windowMinutes
|
|
40
|
+
: LEGACY_WINDOW_MINUTES[fallbackKey]
|
|
41
|
+
if (minutes < 60) return `${compactUnit(minutes)}m`
|
|
42
|
+
if (minutes < 1_440) return `${compactUnit(minutes / 60)}h`
|
|
43
|
+
return `${compactUnit(minutes / 1_440)}d`
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface PresentQuotaWindow {
|
|
47
|
+
key: QuotaWindowKey
|
|
48
|
+
label: string
|
|
49
|
+
window: QuotaWindow
|
|
50
|
+
windowMs: number | null
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Present windows only — an absent slot means "not applicable", not
|
|
54
|
+
// "unknown", so it must never synthesize a placeholder row here.
|
|
55
|
+
export function getPresentQuotaWindows(
|
|
56
|
+
quota: AccountQuota | null,
|
|
57
|
+
): PresentQuotaWindow[] {
|
|
58
|
+
if (!quota) return []
|
|
59
|
+
const rows: PresentQuotaWindow[] = []
|
|
60
|
+
for (const key of QUOTA_WINDOW_KEYS) {
|
|
61
|
+
const window = quota[key]
|
|
62
|
+
if (!window) continue
|
|
63
|
+
const configuredMinutes = window.windowMinutes
|
|
64
|
+
const windowMinutes =
|
|
65
|
+
configuredMinutes !== undefined &&
|
|
66
|
+
Number.isFinite(configuredMinutes) &&
|
|
67
|
+
configuredMinutes > 0
|
|
68
|
+
? configuredMinutes
|
|
69
|
+
: LEGACY_WINDOW_MINUTES[key]
|
|
70
|
+
rows.push({
|
|
71
|
+
key,
|
|
72
|
+
label: formatWindowLabel(windowMinutes, key),
|
|
73
|
+
window,
|
|
74
|
+
windowMs: windowMinutes * 60_000,
|
|
75
|
+
})
|
|
76
|
+
}
|
|
77
|
+
return rows
|
|
10
78
|
}
|
|
11
79
|
|
|
12
80
|
export interface SidebarAccountState {
|
|
@@ -15,8 +83,17 @@ export interface SidebarAccountState {
|
|
|
15
83
|
quota: AccountQuota | null
|
|
16
84
|
killed: boolean
|
|
17
85
|
enabled: boolean
|
|
86
|
+
resetCredits?: number
|
|
18
87
|
}
|
|
19
88
|
|
|
89
|
+
export interface ActiveRoutingEntry {
|
|
90
|
+
activeId: string
|
|
91
|
+
route: string
|
|
92
|
+
updatedAt: number
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export type ActiveRoutingMap = Record<string, ActiveRoutingEntry>
|
|
96
|
+
|
|
20
97
|
export interface SidebarState {
|
|
21
98
|
main: {
|
|
22
99
|
quota: AccountQuota | null
|
|
@@ -25,10 +102,14 @@ export interface SidebarState {
|
|
|
25
102
|
quotaBackoffUntil?: number
|
|
26
103
|
refreshBackedOff?: boolean
|
|
27
104
|
refreshBackoffUntil?: number
|
|
105
|
+
resetCredits?: number
|
|
28
106
|
}
|
|
29
107
|
fallbacks: SidebarAccountState[]
|
|
108
|
+
/** @deprecated Compatibility field for readers that do not consume activeRouting. */
|
|
30
109
|
activeId: string | undefined
|
|
110
|
+
/** Machine-global routing mode and compatibility value for older readers. */
|
|
31
111
|
route: string
|
|
112
|
+
activeRouting?: ActiveRoutingMap
|
|
32
113
|
planType?: string
|
|
33
114
|
credits?: number
|
|
34
115
|
lastUpdated: number
|
|
@@ -38,6 +119,7 @@ import { randomUUID } from 'node:crypto'
|
|
|
38
119
|
import { mkdir, readFile, rename, rm, writeFile } from 'node:fs/promises'
|
|
39
120
|
import { tmpdir } from 'node:os'
|
|
40
121
|
import { dirname, join } from 'node:path'
|
|
122
|
+
import { acquireRefreshFileLock } from './core/refresh-file-lock'
|
|
41
123
|
import { createLogger } from './logger'
|
|
42
124
|
|
|
43
125
|
const logSb = createLogger('sidebar')
|
|
@@ -46,6 +128,50 @@ const STATE_FILE_ENV = 'OPENCODE_OPENAI_AUTH_SIDEBAR_STATE_FILE'
|
|
|
46
128
|
const DEFAULT_STATE_DIR = join(tmpdir(), 'opencode-openai-auth')
|
|
47
129
|
const DEFAULT_STATE_FILE = join(DEFAULT_STATE_DIR, 'sidebar-state.json')
|
|
48
130
|
|
|
131
|
+
function normalizeResetCredits(value: unknown): number | undefined {
|
|
132
|
+
return typeof value === 'number' && Number.isFinite(value) && value >= 0
|
|
133
|
+
? value
|
|
134
|
+
: undefined
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function resetCreditsField(value: unknown): { resetCredits?: number } {
|
|
138
|
+
const credits = normalizeResetCredits(value)
|
|
139
|
+
return credits !== undefined ? { resetCredits: credits } : {}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function normalizeActiveRouting(value: unknown): ActiveRoutingMap | undefined {
|
|
143
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value)) {
|
|
144
|
+
return undefined
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const normalized: ActiveRoutingMap = {}
|
|
148
|
+
for (const [sessionId, rawEntry] of Object.entries(value)) {
|
|
149
|
+
if (
|
|
150
|
+
rawEntry === null ||
|
|
151
|
+
typeof rawEntry !== 'object' ||
|
|
152
|
+
Array.isArray(rawEntry)
|
|
153
|
+
) {
|
|
154
|
+
continue
|
|
155
|
+
}
|
|
156
|
+
const entry = rawEntry as Record<string, unknown>
|
|
157
|
+
if (
|
|
158
|
+
typeof entry.activeId !== 'string' ||
|
|
159
|
+
typeof entry.route !== 'string' ||
|
|
160
|
+
typeof entry.updatedAt !== 'number' ||
|
|
161
|
+
!Number.isFinite(entry.updatedAt)
|
|
162
|
+
) {
|
|
163
|
+
continue
|
|
164
|
+
}
|
|
165
|
+
normalized[sessionId] = {
|
|
166
|
+
activeId: entry.activeId,
|
|
167
|
+
route: entry.route,
|
|
168
|
+
updatedAt: entry.updatedAt,
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return Object.keys(normalized).length > 0 ? normalized : undefined
|
|
173
|
+
}
|
|
174
|
+
|
|
49
175
|
export function getSidebarStateFile(): string {
|
|
50
176
|
return process.env[STATE_FILE_ENV] || DEFAULT_STATE_FILE
|
|
51
177
|
}
|
|
@@ -99,6 +225,7 @@ export function normalizeSidebarState(raw: unknown): SidebarState {
|
|
|
99
225
|
...(typeof m.refreshBackoffUntil === 'number'
|
|
100
226
|
? { refreshBackoffUntil: m.refreshBackoffUntil }
|
|
101
227
|
: {}),
|
|
228
|
+
...resetCreditsField(m.resetCredits),
|
|
102
229
|
}
|
|
103
230
|
} else {
|
|
104
231
|
main = { quota: null, killed: false }
|
|
@@ -123,6 +250,7 @@ export function normalizeSidebarState(raw: unknown): SidebarState {
|
|
|
123
250
|
quota: ('quota' in e ? e.quota : null) as AccountQuota | null,
|
|
124
251
|
killed: typeof e.killed === 'boolean' ? e.killed : false,
|
|
125
252
|
enabled: typeof e.enabled === 'boolean' ? e.enabled : true,
|
|
253
|
+
...resetCreditsField(e.resetCredits),
|
|
126
254
|
}))
|
|
127
255
|
: []
|
|
128
256
|
|
|
@@ -139,13 +267,14 @@ export function normalizeSidebarState(raw: unknown): SidebarState {
|
|
|
139
267
|
// Optional top-level fields
|
|
140
268
|
const planType = typeof r.planType === 'string' ? r.planType : undefined
|
|
141
269
|
const credits = typeof r.credits === 'number' ? r.credits : undefined
|
|
142
|
-
|
|
270
|
+
const activeRouting = normalizeActiveRouting(r.activeRouting)
|
|
143
271
|
return {
|
|
144
272
|
main,
|
|
145
273
|
fallbacks,
|
|
146
274
|
activeId,
|
|
147
275
|
route,
|
|
148
276
|
lastUpdated,
|
|
277
|
+
...(activeRouting !== undefined ? { activeRouting } : {}),
|
|
149
278
|
...(planType !== undefined ? { planType } : {}),
|
|
150
279
|
...(credits !== undefined ? { credits } : {}),
|
|
151
280
|
}
|
|
@@ -160,9 +289,90 @@ export async function getSidebarState(): Promise<SidebarState> {
|
|
|
160
289
|
}
|
|
161
290
|
}
|
|
162
291
|
|
|
292
|
+
export const ACTIVE_ROUTING_MAX_AGE_MS = 60 * 60 * 1000
|
|
293
|
+
export const ACTIVE_ROUTING_MAX_ENTRIES = 128
|
|
294
|
+
|
|
295
|
+
export type SidebarRoutingAccount = {
|
|
296
|
+
id: string
|
|
297
|
+
enabled?: boolean
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
export function isUsableRoutingEntry(
|
|
301
|
+
entry: ActiveRoutingEntry,
|
|
302
|
+
accounts: readonly SidebarRoutingAccount[],
|
|
303
|
+
now = Date.now(),
|
|
304
|
+
): boolean {
|
|
305
|
+
const fresh =
|
|
306
|
+
entry.updatedAt >= now - ACTIVE_ROUTING_MAX_AGE_MS && entry.updatedAt <= now
|
|
307
|
+
if (!fresh) return false
|
|
308
|
+
return (
|
|
309
|
+
entry.activeId === 'main' ||
|
|
310
|
+
accounts.some(
|
|
311
|
+
(account) => account.enabled !== false && account.id === entry.activeId,
|
|
312
|
+
)
|
|
313
|
+
)
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
export function resolveSessionSidebarRouting(
|
|
317
|
+
state: SidebarState,
|
|
318
|
+
sessionId?: string,
|
|
319
|
+
now = Date.now(),
|
|
320
|
+
): { activeId: string; route: string } {
|
|
321
|
+
if (!sessionId) {
|
|
322
|
+
return { activeId: state.activeId ?? 'main', route: state.route }
|
|
323
|
+
}
|
|
324
|
+
const own = sessionId ? state.activeRouting?.[sessionId] : undefined
|
|
325
|
+
if (own && isUsableRoutingEntry(own, state.fallbacks, now)) {
|
|
326
|
+
return { activeId: own.activeId, route: own.route }
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
const fallback = state.fallbacks.find(
|
|
330
|
+
(account) => account.enabled && !account.killed,
|
|
331
|
+
)
|
|
332
|
+
return {
|
|
333
|
+
activeId:
|
|
334
|
+
state.route === 'fallback-first' && fallback ? fallback.id : 'main',
|
|
335
|
+
route: state.route,
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
export function pruneActiveRouting(
|
|
340
|
+
activeRouting: ActiveRoutingMap | undefined,
|
|
341
|
+
accounts: readonly SidebarRoutingAccount[],
|
|
342
|
+
now = Date.now(),
|
|
343
|
+
removedSessionId?: string,
|
|
344
|
+
): ActiveRoutingMap | undefined {
|
|
345
|
+
if (!activeRouting) return undefined
|
|
346
|
+
const kept = Object.entries(activeRouting).filter(
|
|
347
|
+
([sessionId, entry]) =>
|
|
348
|
+
sessionId !== removedSessionId &&
|
|
349
|
+
isUsableRoutingEntry(entry, accounts, now),
|
|
350
|
+
)
|
|
351
|
+
const bounded =
|
|
352
|
+
kept.length <= ACTIVE_ROUTING_MAX_ENTRIES
|
|
353
|
+
? kept
|
|
354
|
+
: kept
|
|
355
|
+
.sort((left, right) => right[1].updatedAt - left[1].updatedAt)
|
|
356
|
+
.slice(0, ACTIVE_ROUTING_MAX_ENTRIES)
|
|
357
|
+
return bounded.length > 0 ? Object.fromEntries(bounded) : undefined
|
|
358
|
+
}
|
|
359
|
+
|
|
163
360
|
// Serialization chain: concurrent calls are queued so a stale background
|
|
164
361
|
// write cannot land after a newer one and corrupt the file.
|
|
165
362
|
let sidebarWriteChain: Promise<void> = Promise.resolve()
|
|
363
|
+
const MAX_MERGE_ATTEMPTS = 3
|
|
364
|
+
const SIDEBAR_WRITE_LOCK_TTL_MS = 10_000
|
|
365
|
+
const SIDEBAR_WRITE_LOCK_WAIT_MS = 15_000
|
|
366
|
+
|
|
367
|
+
interface SidebarMergeHooks {
|
|
368
|
+
beforeRecheck?: () => void | Promise<void>
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
function enqueueSidebarWrite(operation: () => Promise<void>): Promise<void> {
|
|
372
|
+
const result = sidebarWriteChain.then(operation)
|
|
373
|
+
sidebarWriteChain = result.catch(() => {})
|
|
374
|
+
return result
|
|
375
|
+
}
|
|
166
376
|
|
|
167
377
|
/**
|
|
168
378
|
* Write sidebar state to disk, serialized through a promise chain so
|
|
@@ -179,10 +389,176 @@ export function setSidebarState(
|
|
|
179
389
|
state: SidebarState,
|
|
180
390
|
file = getSidebarStateFile(),
|
|
181
391
|
): Promise<void> {
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
392
|
+
return enqueueSidebarWrite(() => doWriteSidebarState(state, file))
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
async function readSidebarState(file: string): Promise<SidebarState> {
|
|
396
|
+
try {
|
|
397
|
+
return parseSidebarState(await readRawSidebar(file))
|
|
398
|
+
} catch {
|
|
399
|
+
return DEFAULT_SIDEBAR_STATE
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
async function readRawSidebar(file: string): Promise<string> {
|
|
404
|
+
try {
|
|
405
|
+
return await readFile(file, 'utf8')
|
|
406
|
+
} catch (error) {
|
|
407
|
+
if ((error as { code?: string }).code === 'ENOENT') return ''
|
|
408
|
+
throw error
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
function parseSidebarState(raw: string): SidebarState {
|
|
413
|
+
if (raw === '') return DEFAULT_SIDEBAR_STATE
|
|
414
|
+
try {
|
|
415
|
+
return normalizeSidebarState(JSON.parse(raw))
|
|
416
|
+
} catch {
|
|
417
|
+
return DEFAULT_SIDEBAR_STATE
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
async function acquireSidebarWriteLock(file: string) {
|
|
422
|
+
const deadline = Date.now() + SIDEBAR_WRITE_LOCK_WAIT_MS
|
|
423
|
+
while (Date.now() <= deadline) {
|
|
424
|
+
const lock = await acquireRefreshFileLock({
|
|
425
|
+
name: 'sidebar-write',
|
|
426
|
+
path: file,
|
|
427
|
+
ttlMs: SIDEBAR_WRITE_LOCK_TTL_MS,
|
|
428
|
+
renew: true,
|
|
429
|
+
})
|
|
430
|
+
if (lock) return lock
|
|
431
|
+
await new Promise((resolve) => setTimeout(resolve, 10))
|
|
432
|
+
}
|
|
433
|
+
throw new Error('Timed out waiting for the sidebar state write lock')
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
async function writeMergedSidebarState(
|
|
437
|
+
file: string,
|
|
438
|
+
merge: (latest: SidebarState) => SidebarState,
|
|
439
|
+
hooks?: SidebarMergeHooks,
|
|
440
|
+
): Promise<void> {
|
|
441
|
+
const lock = await acquireSidebarWriteLock(file)
|
|
442
|
+
try {
|
|
443
|
+
// The file lock serializes current writers. Rechecking also preserves data
|
|
444
|
+
// from older plugin processes that do not participate in this lock.
|
|
445
|
+
for (let attempt = 0; attempt < MAX_MERGE_ATTEMPTS; attempt += 1) {
|
|
446
|
+
const rawBefore = await readRawSidebar(file)
|
|
447
|
+
const next = merge(parseSidebarState(rawBefore))
|
|
448
|
+
if (attempt === 0) await hooks?.beforeRecheck?.()
|
|
449
|
+
const rawRecheck = await readRawSidebar(file)
|
|
450
|
+
if (rawRecheck !== rawBefore) continue
|
|
451
|
+
await doWriteSidebarState(next, file)
|
|
452
|
+
return
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
const latest = await readSidebarState(file)
|
|
456
|
+
await doWriteSidebarState(merge(latest), file)
|
|
457
|
+
} finally {
|
|
458
|
+
await lock.release()
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
export type SidebarMachineState = Pick<
|
|
463
|
+
SidebarState,
|
|
464
|
+
'main' | 'fallbacks' | 'planType' | 'credits' | 'lastUpdated'
|
|
465
|
+
> & { route: string }
|
|
466
|
+
|
|
467
|
+
export function setSidebarMachineState(
|
|
468
|
+
machineState: SidebarMachineState,
|
|
469
|
+
file = getSidebarStateFile(),
|
|
470
|
+
hooks?: SidebarMergeHooks,
|
|
471
|
+
): Promise<void> {
|
|
472
|
+
return enqueueSidebarWrite(async () => {
|
|
473
|
+
await writeMergedSidebarState(
|
|
474
|
+
file,
|
|
475
|
+
(latest) => ({
|
|
476
|
+
...latest,
|
|
477
|
+
...machineState,
|
|
478
|
+
activeId: latest.activeId,
|
|
479
|
+
activeRouting: latest.activeRouting,
|
|
480
|
+
lastUpdated: Math.max(Date.now(), latest.lastUpdated + 1),
|
|
481
|
+
}),
|
|
482
|
+
hooks,
|
|
483
|
+
)
|
|
484
|
+
})
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
export function upsertSidebarActiveRouting(
|
|
488
|
+
input: { sessionId: string } & ActiveRoutingEntry,
|
|
489
|
+
accounts: readonly SidebarRoutingAccount[],
|
|
490
|
+
file = getSidebarStateFile(),
|
|
491
|
+
hooks?: SidebarMergeHooks,
|
|
492
|
+
): Promise<void> {
|
|
493
|
+
return enqueueSidebarWrite(async () => {
|
|
494
|
+
await writeMergedSidebarState(
|
|
495
|
+
file,
|
|
496
|
+
(latest) => {
|
|
497
|
+
const activeRouting = pruneActiveRouting(
|
|
498
|
+
{
|
|
499
|
+
...latest.activeRouting,
|
|
500
|
+
[input.sessionId]: {
|
|
501
|
+
activeId: input.activeId,
|
|
502
|
+
route: input.route,
|
|
503
|
+
updatedAt: input.updatedAt,
|
|
504
|
+
},
|
|
505
|
+
},
|
|
506
|
+
accounts,
|
|
507
|
+
Date.now(),
|
|
508
|
+
)
|
|
509
|
+
return {
|
|
510
|
+
...latest,
|
|
511
|
+
activeId: input.activeId,
|
|
512
|
+
route: input.route,
|
|
513
|
+
activeRouting,
|
|
514
|
+
lastUpdated: Math.max(Date.now(), latest.lastUpdated + 1),
|
|
515
|
+
}
|
|
516
|
+
},
|
|
517
|
+
hooks,
|
|
518
|
+
)
|
|
519
|
+
})
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
export function setSidebarLegacyRouting(
|
|
523
|
+
input: ActiveRoutingEntry,
|
|
524
|
+
file = getSidebarStateFile(),
|
|
525
|
+
): Promise<void> {
|
|
526
|
+
return enqueueSidebarWrite(async () => {
|
|
527
|
+
await writeMergedSidebarState(file, (latest) => ({
|
|
528
|
+
...latest,
|
|
529
|
+
activeId: input.activeId,
|
|
530
|
+
route: input.route,
|
|
531
|
+
lastUpdated: Math.max(Date.now(), latest.lastUpdated + 1),
|
|
532
|
+
}))
|
|
533
|
+
})
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
export function removeSidebarActiveRouting(
|
|
537
|
+
sessionId: string,
|
|
538
|
+
accounts: readonly SidebarRoutingAccount[],
|
|
539
|
+
file = getSidebarStateFile(),
|
|
540
|
+
hooks?: SidebarMergeHooks,
|
|
541
|
+
): Promise<void> {
|
|
542
|
+
return enqueueSidebarWrite(async () => {
|
|
543
|
+
await writeMergedSidebarState(
|
|
544
|
+
file,
|
|
545
|
+
(latest) => {
|
|
546
|
+
const now = Date.now()
|
|
547
|
+
const activeRouting = pruneActiveRouting(
|
|
548
|
+
latest.activeRouting,
|
|
549
|
+
accounts,
|
|
550
|
+
now,
|
|
551
|
+
sessionId,
|
|
552
|
+
)
|
|
553
|
+
return {
|
|
554
|
+
...latest,
|
|
555
|
+
activeRouting,
|
|
556
|
+
lastUpdated: Math.max(Date.now(), latest.lastUpdated + 1),
|
|
557
|
+
}
|
|
558
|
+
},
|
|
559
|
+
hooks,
|
|
560
|
+
)
|
|
561
|
+
})
|
|
186
562
|
}
|
|
187
563
|
|
|
188
564
|
async function doWriteSidebarState(
|
|
@@ -200,6 +576,7 @@ async function doWriteSidebarState(
|
|
|
200
576
|
pid: process.pid,
|
|
201
577
|
error: e instanceof Error ? e.message : String(e),
|
|
202
578
|
})
|
|
579
|
+
throw e
|
|
203
580
|
}
|
|
204
581
|
}
|
|
205
582
|
|
|
@@ -250,20 +627,22 @@ export function getCollapsedQuotaSummary(quota: AccountQuota | null): {
|
|
|
250
627
|
} {
|
|
251
628
|
const primaryUsedPercent = quota?.primary?.usedPercent ?? null
|
|
252
629
|
const secondaryUsedPercent = quota?.secondary?.usedPercent ?? null
|
|
253
|
-
|
|
254
|
-
return { primaryUsedPercent, secondaryUsedPercent, text: null }
|
|
255
|
-
}
|
|
256
|
-
|
|
630
|
+
const rows = getPresentQuotaWindows(quota)
|
|
257
631
|
return {
|
|
258
632
|
primaryUsedPercent,
|
|
259
633
|
secondaryUsedPercent,
|
|
260
|
-
text:
|
|
634
|
+
text:
|
|
635
|
+
rows.length === 0
|
|
636
|
+
? null
|
|
637
|
+
: rows
|
|
638
|
+
.map(
|
|
639
|
+
({ label, window }) =>
|
|
640
|
+
`${label}: ${Math.round(window.usedPercent)}%`,
|
|
641
|
+
)
|
|
642
|
+
.join(' '),
|
|
261
643
|
}
|
|
262
644
|
}
|
|
263
645
|
|
|
264
|
-
export const FIVE_HOUR_MS = 5 * 60 * 60 * 1000
|
|
265
|
-
export const SEVEN_DAY_MS = 7 * 24 * 60 * 60 * 1000
|
|
266
|
-
|
|
267
646
|
const PACING_MIN_ELAPSED_MS = 5 * 60 * 1000
|
|
268
647
|
const PACING_MIN_ELAPSED_FRACTION = 0.01
|
|
269
648
|
const ON_PACE_DELTA = 1
|
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
/** @jsxImportSource @opentui/solid */
|
|
2
2
|
import type { TuiPluginApi } from '@opencode-ai/plugin/tui'
|
|
3
3
|
import type { OpenDialogPayload } from '../rpc/protocol.js'
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
type AccountQuota,
|
|
6
|
+
getCollapsedQuotaSummary,
|
|
7
|
+
getSidebarState,
|
|
8
|
+
resolveSessionSidebarRouting,
|
|
9
|
+
type SidebarState,
|
|
10
|
+
} from '../sidebar-state.js'
|
|
5
11
|
import { openUrl } from '../util/open-url'
|
|
6
12
|
|
|
7
13
|
type ApplyFn = (
|
|
@@ -139,6 +145,7 @@ export function openCommandDialog(
|
|
|
139
145
|
api: TuiPluginApi,
|
|
140
146
|
payload: OpenDialogPayload,
|
|
141
147
|
apply: ApplyFn,
|
|
148
|
+
sessionId?: string,
|
|
142
149
|
) {
|
|
143
150
|
if (payload.command === 'openai-routing') {
|
|
144
151
|
const current = (payload.knobs.mode as string) ?? 'main-first'
|
|
@@ -248,7 +255,7 @@ export function openCommandDialog(
|
|
|
248
255
|
{
|
|
249
256
|
title: 'Edit thresholds\u2026',
|
|
250
257
|
value: 'edit',
|
|
251
|
-
description: 'Set per-account
|
|
258
|
+
description: 'Set per-account primary,secondary cutoffs',
|
|
252
259
|
},
|
|
253
260
|
]}
|
|
254
261
|
onSelect={(option) => {
|
|
@@ -345,7 +352,7 @@ export function openCommandDialog(
|
|
|
345
352
|
}
|
|
346
353
|
|
|
347
354
|
if (payload.command === 'openai-account') {
|
|
348
|
-
openAccountDialog(api, apply)
|
|
355
|
+
openAccountDialog(api, apply, sessionId)
|
|
349
356
|
return
|
|
350
357
|
}
|
|
351
358
|
|
|
@@ -355,18 +362,15 @@ export function openCommandDialog(
|
|
|
355
362
|
|
|
356
363
|
// -- Accounts dialog ---------------------------------------------------------
|
|
357
364
|
|
|
358
|
-
function
|
|
359
|
-
quota:
|
|
360
|
-
| { primary?: { usedPercent: number }; secondary?: { usedPercent: number } }
|
|
361
|
-
| null
|
|
362
|
-
| undefined,
|
|
365
|
+
export function formatQuotaWindows(
|
|
366
|
+
quota: AccountQuota | null | undefined,
|
|
363
367
|
): string {
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
if (quota
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
return
|
|
368
|
+
const windowSummary = getCollapsedQuotaSummary(quota ?? null).text
|
|
369
|
+
if (windowSummary) return windowSummary
|
|
370
|
+
if (quota?.resetCreditsAvailable !== undefined) {
|
|
371
|
+
return `resets: ${quota.resetCreditsAvailable}`
|
|
372
|
+
}
|
|
373
|
+
return 'no quota data'
|
|
370
374
|
}
|
|
371
375
|
|
|
372
376
|
function osc52Copy(api: TuiPluginApi, text: string): boolean {
|
|
@@ -386,7 +390,30 @@ function osc52Copy(api: TuiPluginApi, text: string): boolean {
|
|
|
386
390
|
return false
|
|
387
391
|
}
|
|
388
392
|
|
|
389
|
-
function
|
|
393
|
+
export function buildAccountDialogRows(
|
|
394
|
+
state: SidebarState,
|
|
395
|
+
sessionId?: string,
|
|
396
|
+
) {
|
|
397
|
+
const activeId = resolveSessionSidebarRouting(state, sessionId).activeId
|
|
398
|
+
return [
|
|
399
|
+
{
|
|
400
|
+
title: `main${activeId === 'main' ? ' \u2022 active' : ''}`,
|
|
401
|
+
value: 'main',
|
|
402
|
+
description: formatQuotaWindows(state.main.quota),
|
|
403
|
+
},
|
|
404
|
+
...state.fallbacks.map((fb) => ({
|
|
405
|
+
title: `${fb.label ?? fb.id}${activeId === fb.id ? ' \u2022 active' : ''}${!fb.enabled ? ' (disabled)' : ''}`,
|
|
406
|
+
value: fb.id,
|
|
407
|
+
description: formatQuotaWindows(fb.quota),
|
|
408
|
+
})),
|
|
409
|
+
]
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
function openAccountDialog(
|
|
413
|
+
api: TuiPluginApi,
|
|
414
|
+
apply: ApplyFn,
|
|
415
|
+
sessionId?: string,
|
|
416
|
+
) {
|
|
390
417
|
const DialogConfirm = api.ui.DialogConfirm
|
|
391
418
|
|
|
392
419
|
function showL1() {
|
|
@@ -397,16 +424,7 @@ function openAccountDialog(api: TuiPluginApi, apply: ApplyFn) {
|
|
|
397
424
|
<DialogSelectInner
|
|
398
425
|
title='OpenAI Accounts'
|
|
399
426
|
options={[
|
|
400
|
-
|
|
401
|
-
title: `main${state.activeId === 'main' || !state.activeId ? ' \u2022 active' : ''}`,
|
|
402
|
-
value: 'main',
|
|
403
|
-
description: formatQuota5h7d(state.main.quota),
|
|
404
|
-
},
|
|
405
|
-
...state.fallbacks.map((fb) => ({
|
|
406
|
-
title: `${fb.label ?? fb.id}${state.activeId === fb.id ? ' \u2022 active' : ''}${!fb.enabled ? ' (disabled)' : ''}`,
|
|
407
|
-
value: fb.id,
|
|
408
|
-
description: formatQuota5h7d(fb.quota),
|
|
409
|
-
})),
|
|
427
|
+
...buildAccountDialogRows(state, sessionId),
|
|
410
428
|
{
|
|
411
429
|
title: 'Add account\u2026',
|
|
412
430
|
value: '__add__',
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// Prefer the host OpenTUI runtime registry when it exists. OpenTUI 0.4.x
|
|
2
|
+
// registers these virtual modules process-wide, allowing the precompiled TUI to
|
|
3
|
+
// share the host's single Solid/OpenTUI runtime when loaded from node_modules.
|
|
4
|
+
const runtimeProbe = `opentui:runtime-module:${encodeURIComponent('@opentui/solid')}`
|
|
5
|
+
|
|
6
|
+
function isMissingRuntimeRegistry(error) {
|
|
7
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
8
|
+
return (
|
|
9
|
+
/Cannot find|Could not resolve|Module not found|Unable to resolve/.test(
|
|
10
|
+
message,
|
|
11
|
+
) && message.includes('opentui:runtime-module:')
|
|
12
|
+
)
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
let mod
|
|
16
|
+
try {
|
|
17
|
+
await import(runtimeProbe)
|
|
18
|
+
} catch (error) {
|
|
19
|
+
if (!isMissingRuntimeRegistry(error)) {
|
|
20
|
+
console.error('OpenAI Auth TUI runtime registry probe failed', error)
|
|
21
|
+
throw error
|
|
22
|
+
}
|
|
23
|
+
// Older hosts and bare Bun do not provide the virtual registry. Their source
|
|
24
|
+
// loader still applies the Solid transform, so retain the raw TSX fallback.
|
|
25
|
+
mod = await import('../tui.tsx')
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (!mod) {
|
|
29
|
+
try {
|
|
30
|
+
mod = await import('../tui-compiled/tui.tsx')
|
|
31
|
+
} catch (error) {
|
|
32
|
+
console.error('OpenAI Auth compiled TUI failed to load', error)
|
|
33
|
+
throw error
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export default mod.default
|