@cortexkit/opencode-antigravity-auth 2.0.0 → 2.0.1
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.js +308 -81
- package/dist/cli.js.map +4 -4
- package/dist/index.js +16634 -15416
- package/dist/index.js.map +4 -4
- package/dist/src/plugin/account-command-oauth.d.ts +34 -0
- package/dist/src/plugin/account-command-oauth.d.ts.map +1 -0
- package/dist/src/plugin/account-command-oauth.js +156 -0
- package/dist/src/plugin/account-command-oauth.js.map +1 -0
- package/dist/src/plugin/auth-loader.d.ts +16 -1
- package/dist/src/plugin/auth-loader.d.ts.map +1 -1
- package/dist/src/plugin/auth-loader.js +113 -32
- package/dist/src/plugin/auth-loader.js.map +1 -1
- package/dist/src/plugin/background-quota-refresh.d.ts +146 -0
- package/dist/src/plugin/background-quota-refresh.d.ts.map +1 -0
- package/dist/src/plugin/background-quota-refresh.js +347 -0
- package/dist/src/plugin/background-quota-refresh.js.map +1 -0
- package/dist/src/plugin/command-data.d.ts +61 -8
- package/dist/src/plugin/command-data.d.ts.map +1 -1
- package/dist/src/plugin/command-data.js +231 -39
- package/dist/src/plugin/command-data.js.map +1 -1
- package/dist/src/plugin/commands.d.ts +12 -9
- package/dist/src/plugin/commands.d.ts.map +1 -1
- package/dist/src/plugin/commands.js +140 -33
- package/dist/src/plugin/commands.js.map +1 -1
- package/dist/src/plugin/config/schema.d.ts +2 -0
- package/dist/src/plugin/config/schema.d.ts.map +1 -1
- package/dist/src/plugin/config/schema.js +24 -0
- package/dist/src/plugin/config/schema.js.map +1 -1
- package/dist/src/plugin/debug.d.ts +7 -0
- package/dist/src/plugin/debug.d.ts.map +1 -1
- package/dist/src/plugin/debug.js +25 -0
- package/dist/src/plugin/debug.js.map +1 -1
- package/dist/src/plugin/fetch-interceptor.d.ts.map +1 -1
- package/dist/src/plugin/fetch-interceptor.js +13 -1
- package/dist/src/plugin/fetch-interceptor.js.map +1 -1
- package/dist/src/plugin/index.d.ts +7 -0
- package/dist/src/plugin/index.d.ts.map +1 -1
- package/dist/src/plugin/index.js +127 -24
- package/dist/src/plugin/index.js.map +1 -1
- package/dist/src/plugin/killswitch.d.ts.map +1 -1
- package/dist/src/plugin/killswitch.js +12 -9
- package/dist/src/plugin/killswitch.js.map +1 -1
- package/dist/src/plugin/oauth-methods.d.ts.map +1 -1
- package/dist/src/plugin/oauth-methods.js +2 -10
- package/dist/src/plugin/oauth-methods.js.map +1 -1
- package/dist/src/plugin/quota.d.ts +42 -1
- package/dist/src/plugin/quota.d.ts.map +1 -1
- package/dist/src/plugin/quota.js +185 -41
- package/dist/src/plugin/quota.js.map +1 -1
- package/dist/src/plugin/ui/auth-menu.d.ts.map +1 -1
- package/dist/src/plugin/ui/auth-menu.js +2 -4
- package/dist/src/plugin/ui/auth-menu.js.map +1 -1
- package/dist/src/plugin/ui/quota-status.d.ts.map +1 -1
- package/dist/src/plugin/ui/quota-status.js +3 -5
- package/dist/src/plugin/ui/quota-status.js.map +1 -1
- package/dist/src/sidebar-state.d.ts +126 -9
- package/dist/src/sidebar-state.d.ts.map +1 -1
- package/dist/src/sidebar-state.js +185 -25
- package/dist/src/sidebar-state.js.map +1 -1
- package/dist/src/tui/command-dialogs.js +101 -3
- package/dist/src/tui/command-dialogs.js.map +1 -1
- package/dist/src/tui.d.ts.map +1 -1
- package/dist/src/tui.js +55 -20
- package/dist/src/tui.js.map +1 -1
- package/package.json +6 -6
- package/src/plugin/command-data.ts +331 -51
- package/src/sidebar-state.ts +306 -32
- package/src/tui/command-dialogs.tsx +136 -3
- package/src/tui-compiled/plugin/command-data.ts +331 -51
- package/src/tui-compiled/sidebar-state.ts +306 -32
- package/src/tui-compiled/tui/command-dialogs.tsx +189 -69
- package/src/tui-compiled/tui.tsx +73 -30
- package/src/tui.tsx +82 -28
|
@@ -40,10 +40,14 @@
|
|
|
40
40
|
* wrong account.
|
|
41
41
|
*/
|
|
42
42
|
|
|
43
|
+
import { createHash } from 'node:crypto'
|
|
44
|
+
|
|
43
45
|
import {
|
|
44
46
|
buildSidebarMachineStateFromAccounts,
|
|
47
|
+
normalizeLegacyCachedQuota,
|
|
45
48
|
type SidebarAccountRedactionInput,
|
|
46
49
|
setSidebarMachineState,
|
|
50
|
+
toCapturedTier,
|
|
47
51
|
} from '../sidebar-state'
|
|
48
52
|
|
|
49
53
|
/**
|
|
@@ -70,19 +74,21 @@ type CommandDataAccountMetadata = {
|
|
|
70
74
|
Record<CommandDataQuotaGroup, CommandDataQuotaGroupSummary>
|
|
71
75
|
>
|
|
72
76
|
cachedQuotaUpdatedAt?: number
|
|
77
|
+
cachedQuotaAccountId?: string
|
|
73
78
|
accountIneligible?: boolean
|
|
74
79
|
}
|
|
75
80
|
|
|
76
|
-
type CommandDataQuotaGroup =
|
|
77
|
-
| 'claude'
|
|
78
|
-
| 'gemini-pro'
|
|
79
|
-
| 'gemini-flash'
|
|
80
|
-
| 'gpt-oss'
|
|
81
|
+
type CommandDataQuotaGroup = 'gemini' | 'non-gemini'
|
|
81
82
|
|
|
82
83
|
type CommandDataQuotaGroupSummary = {
|
|
83
84
|
remainingFraction?: number
|
|
84
85
|
resetTime?: string
|
|
85
86
|
modelCount: number
|
|
87
|
+
windows?: Array<{
|
|
88
|
+
window: 'weekly' | '5h'
|
|
89
|
+
remainingFraction: number
|
|
90
|
+
resetTime: string
|
|
91
|
+
}>
|
|
86
92
|
}
|
|
87
93
|
|
|
88
94
|
type CommandDataAccountQuotaResult = {
|
|
@@ -105,7 +111,7 @@ type CommandDataAccountQuotaResult = {
|
|
|
105
111
|
updatedAccount?: CommandDataAccountMetadata
|
|
106
112
|
}
|
|
107
113
|
|
|
108
|
-
|
|
114
|
+
export interface CommandDataAccountStorage {
|
|
109
115
|
version: 4
|
|
110
116
|
activeIndex: number
|
|
111
117
|
activeIndexByFamily?: { claude?: number; gemini?: number }
|
|
@@ -133,12 +139,27 @@ export interface CommandAccountRow {
|
|
|
133
139
|
enabled: boolean
|
|
134
140
|
/** `true` when this row matches the harness-active account. */
|
|
135
141
|
current: boolean
|
|
142
|
+
/**
|
|
143
|
+
* Absolute timestamp (ms) until which this account is rate-limited.
|
|
144
|
+
* Carried on the row so the sidebar refresher can match cooldowns
|
|
145
|
+
* without relying on unstable numeric indices.
|
|
146
|
+
*/
|
|
147
|
+
coolingDownUntil?: number
|
|
148
|
+
/** Health score in [0, 100], forwarded from the live tracker. */
|
|
149
|
+
healthScore?: number
|
|
136
150
|
quota: Array<{
|
|
137
|
-
key: '
|
|
151
|
+
key: 'gemini' | 'non-gemini'
|
|
138
152
|
label: string
|
|
139
153
|
remainingPercent: number | null
|
|
140
154
|
resetAt?: number
|
|
155
|
+
windows?: Array<{
|
|
156
|
+
window: 'weekly' | '5h'
|
|
157
|
+
remainingPercent: number
|
|
158
|
+
resetAt?: number
|
|
159
|
+
}>
|
|
141
160
|
}>
|
|
161
|
+
/** Captured plan tier. Absent when unknown; never defaulted. */
|
|
162
|
+
tier?: { id: string; capturedAt: number }
|
|
142
163
|
}
|
|
143
164
|
|
|
144
165
|
/**
|
|
@@ -149,17 +170,13 @@ const QUOTA_GROUP_LABELS: Record<
|
|
|
149
170
|
CommandAccountRow['quota'][number]['key'],
|
|
150
171
|
string
|
|
151
172
|
> = {
|
|
152
|
-
|
|
153
|
-
'gemini
|
|
154
|
-
'gemini-flash': 'Gemini Flash',
|
|
155
|
-
'gpt-oss': 'GPT-OSS',
|
|
173
|
+
gemini: 'Gemini',
|
|
174
|
+
'non-gemini': 'Non-Gemini',
|
|
156
175
|
}
|
|
157
176
|
|
|
158
177
|
const SUPPORTED_QUOTA_KEYS = [
|
|
159
|
-
'
|
|
160
|
-
'gemini
|
|
161
|
-
'gemini-flash',
|
|
162
|
-
'gpt-oss',
|
|
178
|
+
'gemini',
|
|
179
|
+
'non-gemini',
|
|
163
180
|
] as const satisfies readonly CommandAccountRow['quota'][number]['key'][]
|
|
164
181
|
|
|
165
182
|
interface LiveAccountSnapshot {
|
|
@@ -172,11 +189,36 @@ interface LiveAccountSnapshot {
|
|
|
172
189
|
Record<CommandDataQuotaGroup, CommandDataQuotaGroupSummary>
|
|
173
190
|
>
|
|
174
191
|
cachedQuotaUpdatedAt?: number
|
|
192
|
+
cachedQuotaAccountId?: string
|
|
175
193
|
accountIneligible?: boolean
|
|
194
|
+
coolingDownUntil?: number
|
|
195
|
+
healthScore?: number
|
|
196
|
+
/** Captured plan tier from the most recent loadCodeAssist response. */
|
|
197
|
+
capturedTierId?: string
|
|
198
|
+
/** Captured paid-tier ID from the most recent loadCodeAssist response. */
|
|
199
|
+
capturedPaidTierId?: string
|
|
200
|
+
/** Epoch ms when capturedTierId was last recorded. */
|
|
201
|
+
capturedTierAt?: number
|
|
176
202
|
}
|
|
177
203
|
|
|
178
204
|
function toCommandAccountRow(entry: LiveAccountSnapshot): CommandAccountRow {
|
|
179
|
-
|
|
205
|
+
// Stamp mismatch: the cached quota was captured for a different account
|
|
206
|
+
// (the refresh token changed, or an index shift placed another account's
|
|
207
|
+
// snapshot at this position). Drop the stale cache rather than rendering
|
|
208
|
+
// the wrong account's quota percentages.
|
|
209
|
+
const rawCached =
|
|
210
|
+
entry.cachedQuotaAccountId &&
|
|
211
|
+
entry.cachedQuotaAccountId !== quotaAccountIdentity(entry.refreshToken)
|
|
212
|
+
? undefined
|
|
213
|
+
: entry.cachedQuota
|
|
214
|
+
// Normalize legacy pool keys (gemini-pro, gemini-flash, claude, gpt-oss)
|
|
215
|
+
// to canonical keys before the SUPPORTED_QUOTA_KEYS loop. Without this
|
|
216
|
+
// a legacy snapshot produces empty quota rows, and writeSidebar would
|
|
217
|
+
// permanently re-emit legacy keys on every dialog open until the next
|
|
218
|
+
// real quota refresh rewrites the on-disk snapshot.
|
|
219
|
+
const cached = normalizeLegacyCachedQuota(
|
|
220
|
+
rawCached as Parameters<typeof normalizeLegacyCachedQuota>[0],
|
|
221
|
+
)
|
|
180
222
|
const quota: CommandAccountRow['quota'] = []
|
|
181
223
|
for (const key of SUPPORTED_QUOTA_KEYS) {
|
|
182
224
|
const cachedEntry = cached?.[key]
|
|
@@ -194,11 +236,27 @@ function toCommandAccountRow(entry: LiveAccountSnapshot): CommandAccountRow {
|
|
|
194
236
|
const parsed = Date.parse(cachedEntry.resetTime)
|
|
195
237
|
if (Number.isFinite(parsed)) resetAt = parsed
|
|
196
238
|
}
|
|
239
|
+
// Carry per-window breakdown when the cache was populated from the
|
|
240
|
+
// windowed RUQS path — the sidebar writer projects through here.
|
|
241
|
+
const windows = cachedEntry.windows?.length
|
|
242
|
+
? cachedEntry.windows.map((w) => ({
|
|
243
|
+
window: w.window,
|
|
244
|
+
remainingPercent: Math.round(w.remainingFraction * 100),
|
|
245
|
+
resetAt:
|
|
246
|
+
typeof w.resetTime === 'string' && w.resetTime.length > 0
|
|
247
|
+
? (() => {
|
|
248
|
+
const parsed = Date.parse(w.resetTime)
|
|
249
|
+
return Number.isFinite(parsed) ? parsed : undefined
|
|
250
|
+
})()
|
|
251
|
+
: undefined,
|
|
252
|
+
}))
|
|
253
|
+
: undefined
|
|
197
254
|
quota.push({
|
|
198
255
|
key,
|
|
199
256
|
label: QUOTA_GROUP_LABELS[key],
|
|
200
257
|
remainingPercent,
|
|
201
258
|
resetAt,
|
|
259
|
+
windows,
|
|
202
260
|
})
|
|
203
261
|
}
|
|
204
262
|
const label = `Account ${entry.index + 1}`
|
|
@@ -208,10 +266,41 @@ function toCommandAccountRow(entry: LiveAccountSnapshot): CommandAccountRow {
|
|
|
208
266
|
label,
|
|
209
267
|
enabled: entry.enabled,
|
|
210
268
|
current: entry.active,
|
|
269
|
+
coolingDownUntil: entry.coolingDownUntil,
|
|
270
|
+
healthScore: entry.healthScore,
|
|
211
271
|
quota,
|
|
272
|
+
tier: toCapturedTier(entry),
|
|
212
273
|
}
|
|
213
274
|
}
|
|
214
275
|
|
|
276
|
+
/**
|
|
277
|
+
* Opaque identity derived from a refresh token. Antigravity refresh tokens
|
|
278
|
+
* are stable (they do not rotate), so this hash is a durable, prunable
|
|
279
|
+
* identity to detect a stale cached quota after an account-index shift.
|
|
280
|
+
*/
|
|
281
|
+
function quotaAccountIdentity(refreshToken: string): string {
|
|
282
|
+
return createHash('sha256').update(refreshToken).digest('hex').slice(0, 16)
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
export function projectCommandAccountRows(
|
|
286
|
+
storage: CommandDataAccountStorage | null | undefined,
|
|
287
|
+
): CommandAccountRow[] {
|
|
288
|
+
if (!storage) return []
|
|
289
|
+
const activeIndex = storage.activeIndexByFamily?.claude ?? storage.activeIndex
|
|
290
|
+
return storage.accounts.map((entry, index) =>
|
|
291
|
+
toCommandAccountRow({
|
|
292
|
+
index,
|
|
293
|
+
refreshToken: entry.refreshToken,
|
|
294
|
+
label: entry.label,
|
|
295
|
+
enabled: entry.enabled !== false,
|
|
296
|
+
active: index === activeIndex,
|
|
297
|
+
cachedQuota: entry.cachedQuota,
|
|
298
|
+
cachedQuotaUpdatedAt: entry.cachedQuotaUpdatedAt,
|
|
299
|
+
cachedQuotaAccountId: entry.cachedQuotaAccountId,
|
|
300
|
+
}),
|
|
301
|
+
)
|
|
302
|
+
}
|
|
303
|
+
|
|
215
304
|
/**
|
|
216
305
|
* Live AccountManager view the data service needs.
|
|
217
306
|
*
|
|
@@ -234,8 +323,6 @@ function toCommandAccountRow(entry: LiveAccountSnapshot): CommandAccountRow {
|
|
|
234
323
|
* dialog-triggered mutation lands on disk before the dialog's response
|
|
235
324
|
* returns. Without it, the dialog could toast "Account removed" while
|
|
236
325
|
* the file still carries the old pool.
|
|
237
|
-
* - `activeIndex()` returns the position the harness considers active;
|
|
238
|
-
* the service uses it to mark the matching row as `current: true`.
|
|
239
326
|
*/
|
|
240
327
|
export interface CommandDataAccountManagerView {
|
|
241
328
|
getAccounts(): LiveAccountSnapshot[]
|
|
@@ -245,10 +332,17 @@ export interface CommandDataAccountManagerView {
|
|
|
245
332
|
groups: Partial<
|
|
246
333
|
Record<CommandDataQuotaGroup, CommandDataQuotaGroupSummary>
|
|
247
334
|
>,
|
|
335
|
+
expectedRefreshToken?: string,
|
|
248
336
|
): void
|
|
249
337
|
requestSaveToDisk(): void
|
|
250
338
|
flushSaveToDisk(): Promise<void>
|
|
251
|
-
|
|
339
|
+
/**
|
|
340
|
+
* Per-family live cursor. Used by the remove path so the persisted
|
|
341
|
+
* `activeIndexByFamily` follows each model's currently-active account
|
|
342
|
+
* independently — collapsing both families to a single index would
|
|
343
|
+
* silently unelect one family on every restart.
|
|
344
|
+
*/
|
|
345
|
+
getActiveIndexByFamily(): { claude: number; gemini: number }
|
|
252
346
|
/** Enable/disable the account at `index`. Returns true when it changed. */
|
|
253
347
|
setAccountEnabled(index: number, enabled: boolean): boolean
|
|
254
348
|
/** Pin `index` as the active account for every family the dialog cares about. */
|
|
@@ -328,8 +422,20 @@ export interface CommandDataService {
|
|
|
328
422
|
* refresh token, bump `cachedQuotaUpdatedAt`, and push a label-only
|
|
329
423
|
* sidebar snapshot. Returns the freshly persisted rows so the
|
|
330
424
|
* dialog can re-render in place.
|
|
425
|
+
*
|
|
426
|
+
* Bypass flag is intentional: explicit user-triggered refreshes
|
|
427
|
+
* ("Refresh" button, direct slash-command with `refresh` argument)
|
|
428
|
+
* must always fetch, even if the quota manager has backed off.
|
|
331
429
|
*/
|
|
332
430
|
refreshQuota(): Promise<CommandAccountRow[]>
|
|
431
|
+
/**
|
|
432
|
+
* Non-forced quota check that RESPECTS the quota manager's per-account
|
|
433
|
+
* backoff. Used by the dialog OPEN path — which is a VIEW, not a user
|
|
434
|
+
* request to fetch. Accounts already fresh (or in backoff) are skipped;
|
|
435
|
+
* the result is discarded (the sidebar poller carries the freshness
|
|
436
|
+
* guarantee for idle accounts).
|
|
437
|
+
*/
|
|
438
|
+
refreshQuotaRespectingBackoff(): Promise<void>
|
|
333
439
|
/**
|
|
334
440
|
* Pin `index` as the active account for every family the dialog
|
|
335
441
|
* tracks (claude + gemini). Mutates the live AccountManager AND the
|
|
@@ -381,39 +487,70 @@ export function createCommandDataService(
|
|
|
381
487
|
const projectRows = (): CommandAccountRow[] =>
|
|
382
488
|
accountManagerView.getAccounts().map(toCommandAccountRow)
|
|
383
489
|
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
490
|
+
// Map a CommandAccountRow back to the SidebarAccountRedactionInput shape.
|
|
491
|
+
// Extracted to prevent the "sixth dropped field" pattern: every field in
|
|
492
|
+
// CommandAccountRow that must reach the sidebar lives here once, not as an
|
|
493
|
+
// inline literal that a future author has to remember to update in two places.
|
|
494
|
+
const toRedactionInput = (
|
|
495
|
+
row: CommandAccountRow,
|
|
496
|
+
): SidebarAccountRedactionInput => {
|
|
497
|
+
const gemini = row.quota.find((q) => q.key === 'gemini')
|
|
498
|
+
const nonGemini = row.quota.find((q) => q.key === 'non-gemini')
|
|
499
|
+
// Map a CommandAccountRow quota entry to cachedQuota pool shape
|
|
500
|
+
// so projectQuotaPoolForSidebar (the canonical projection seam)
|
|
501
|
+
// carries the per-window breakdown into the sidebar state.
|
|
502
|
+
const toPool = (
|
|
503
|
+
q:
|
|
504
|
+
| {
|
|
505
|
+
remainingPercent: number | null
|
|
506
|
+
resetAt?: number
|
|
507
|
+
windows?: CommandAccountRow['quota'][number]['windows']
|
|
508
|
+
}
|
|
509
|
+
| undefined,
|
|
510
|
+
) => {
|
|
511
|
+
if (!q || q.remainingPercent == null) return undefined
|
|
396
512
|
return {
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
513
|
+
remainingFraction: q.remainingPercent / 100,
|
|
514
|
+
resetTime:
|
|
515
|
+
typeof q.resetAt === 'number' && Number.isFinite(q.resetAt)
|
|
516
|
+
? new Date(q.resetAt).toISOString()
|
|
517
|
+
: undefined,
|
|
518
|
+
windows: q.windows?.map((w) => ({
|
|
519
|
+
window: w.window,
|
|
520
|
+
remainingFraction: w.remainingPercent / 100,
|
|
521
|
+
resetTime:
|
|
522
|
+
typeof w.resetAt === 'number' && Number.isFinite(w.resetAt)
|
|
523
|
+
? new Date(w.resetAt).toISOString()
|
|
524
|
+
: '',
|
|
525
|
+
})),
|
|
407
526
|
}
|
|
408
|
-
}
|
|
409
|
-
|
|
527
|
+
}
|
|
528
|
+
return {
|
|
529
|
+
index: row.index,
|
|
530
|
+
label: row.label,
|
|
531
|
+
enabled: row.enabled,
|
|
532
|
+
current: row.current,
|
|
533
|
+
healthScore: row.healthScore,
|
|
534
|
+
// Tier passes the redaction boundary unchanged (plan metadata, not PII).
|
|
535
|
+
// The stamp check was already done by toCommandAccountRow.
|
|
536
|
+
tier: row.tier,
|
|
537
|
+
cachedQuota: {
|
|
538
|
+
gemini: toPool(gemini),
|
|
539
|
+
'non-gemini': toPool(nonGemini),
|
|
540
|
+
},
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
const writeSidebar = (rows: CommandAccountRow[]): void => {
|
|
545
|
+
const accounts = rows.map(toRedactionInput)
|
|
546
|
+
// Fire-and-forget -- the sidebar writer is fenced by its own queue,
|
|
410
547
|
// so a transient lock contention cannot block the dialog response.
|
|
411
548
|
void setSidebarMachineState(
|
|
412
549
|
buildSidebarMachineStateFromAccounts(accounts, { checkedAt: now() }),
|
|
413
550
|
{ stateFile: sidebarStateFile },
|
|
414
551
|
).catch(() => {
|
|
415
|
-
//
|
|
416
|
-
//
|
|
552
|
+
// Sidebar writes are best-effort; the next command or quota refresh
|
|
553
|
+
// will publish the current snapshot.
|
|
417
554
|
})
|
|
418
555
|
}
|
|
419
556
|
|
|
@@ -466,7 +603,11 @@ export function createCommandDataService(
|
|
|
466
603
|
for (const update of updates) {
|
|
467
604
|
const liveIndex = liveIndexByRefreshToken.get(update.refreshToken)
|
|
468
605
|
if (liveIndex === undefined || !update.groups) continue
|
|
469
|
-
accountManagerView.updateQuotaCache(
|
|
606
|
+
accountManagerView.updateQuotaCache(
|
|
607
|
+
liveIndex,
|
|
608
|
+
update.groups,
|
|
609
|
+
update.refreshToken,
|
|
610
|
+
)
|
|
470
611
|
liveQuotaChanged = true
|
|
471
612
|
}
|
|
472
613
|
if (liveQuotaChanged) accountManagerView.requestSaveToDisk()
|
|
@@ -486,6 +627,10 @@ export function createCommandDataService(
|
|
|
486
627
|
return {
|
|
487
628
|
...entry,
|
|
488
629
|
cachedQuota: update.groups,
|
|
630
|
+
// Stamp the persisted quota with an opaque identity derived
|
|
631
|
+
// from the refresh token so a later projection can detect
|
|
632
|
+
// a stale snapshot after an account-index shift.
|
|
633
|
+
cachedQuotaAccountId: quotaAccountIdentity(entry.refreshToken),
|
|
489
634
|
cachedQuotaUpdatedAt: refreshedAt,
|
|
490
635
|
}
|
|
491
636
|
}
|
|
@@ -510,6 +655,77 @@ export function createCommandDataService(
|
|
|
510
655
|
return rows
|
|
511
656
|
},
|
|
512
657
|
|
|
658
|
+
async refreshQuotaRespectingBackoff() {
|
|
659
|
+
const accountsForQuota = accountManagerView.getAccountsForQuotaCheck()
|
|
660
|
+
if (accountsForQuota.length === 0) return
|
|
661
|
+
|
|
662
|
+
// Non-forced: the quota manager's per-account backoff and dedup
|
|
663
|
+
// apply. Accounts that are fresh or in backoff are skipped. Unlike
|
|
664
|
+
// the earlier fire-and-forget path, successful results ARE folded
|
|
665
|
+
// into the live cache so the sidebar snapshot reflects fresh data
|
|
666
|
+
// immediately — the dialog renders ONLY from the sidebar file, and
|
|
667
|
+
// the quota manager's push path does not update AccountManager
|
|
668
|
+
// entries for this caller.
|
|
669
|
+
let results: Awaited<ReturnType<typeof quotaManager.refreshAccounts>>
|
|
670
|
+
try {
|
|
671
|
+
results = await quotaManager.refreshAccounts(accountsForQuota, {
|
|
672
|
+
indexFor: (account) => accountsForQuota.indexOf(account),
|
|
673
|
+
force: false,
|
|
674
|
+
})
|
|
675
|
+
} catch {
|
|
676
|
+
// Non-critical: dialog open must never fail because a background
|
|
677
|
+
// refresh check encountered backoff or a transient error.
|
|
678
|
+
return
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
// Build updates akin to the forced refresh path — resolve live
|
|
682
|
+
// indexes after the network call (a concurrent add/remove may have
|
|
683
|
+
// shifted positions), fold successful results into the live cache,
|
|
684
|
+
// then push a sidebar snapshot.
|
|
685
|
+
const updates: Array<{
|
|
686
|
+
refreshToken: string
|
|
687
|
+
groups?: Partial<
|
|
688
|
+
Record<CommandDataQuotaGroup, CommandDataQuotaGroupSummary>
|
|
689
|
+
>
|
|
690
|
+
}> = []
|
|
691
|
+
for (const result of results) {
|
|
692
|
+
const refreshToken =
|
|
693
|
+
result.updatedAccount?.refreshToken ??
|
|
694
|
+
accountsForQuota[result.index]?.refreshToken
|
|
695
|
+
if (!refreshToken) continue
|
|
696
|
+
const groups =
|
|
697
|
+
result.status === 'ok' && result.quota?.groups
|
|
698
|
+
? result.quota.groups
|
|
699
|
+
: undefined
|
|
700
|
+
updates.push({ refreshToken, groups })
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
if (updates.length === 0) return
|
|
704
|
+
|
|
705
|
+
const liveIndexByRefreshToken = new Map<string, number>()
|
|
706
|
+
for (const entry of accountManagerView.getAccounts()) {
|
|
707
|
+
liveIndexByRefreshToken.set(entry.refreshToken, entry.index)
|
|
708
|
+
}
|
|
709
|
+
let liveQuotaChanged = false
|
|
710
|
+
for (const update of updates) {
|
|
711
|
+
const liveIndex = liveIndexByRefreshToken.get(update.refreshToken)
|
|
712
|
+
if (liveIndex === undefined || !update.groups) continue
|
|
713
|
+
accountManagerView.updateQuotaCache(
|
|
714
|
+
liveIndex,
|
|
715
|
+
update.groups,
|
|
716
|
+
update.refreshToken,
|
|
717
|
+
)
|
|
718
|
+
liveQuotaChanged = true
|
|
719
|
+
}
|
|
720
|
+
if (liveQuotaChanged) accountManagerView.requestSaveToDisk()
|
|
721
|
+
|
|
722
|
+
// Push a fresh sidebar snapshot — the live view now carries the
|
|
723
|
+
// just-refreshed quota plus any skipped (backoff/fresh) accounts
|
|
724
|
+
// whose cached percentages are unchanged.
|
|
725
|
+
const rows = projectRows()
|
|
726
|
+
writeSidebar(rows)
|
|
727
|
+
},
|
|
728
|
+
|
|
513
729
|
async setCurrentAccount(index) {
|
|
514
730
|
return mutateLiveAndStorage({ action: 'setCurrent', index })
|
|
515
731
|
},
|
|
@@ -549,9 +765,32 @@ export function createCommandDataService(
|
|
|
549
765
|
)
|
|
550
766
|
}
|
|
551
767
|
|
|
768
|
+
// Capture the live view's current-account identity per family BEFORE
|
|
769
|
+
// the storage mutation. The remove action must persist the index that
|
|
770
|
+
// the same account will occupy AFTER the removal — unconditionally
|
|
771
|
+
// resetting to 0 made a non-current removal promote whichever account
|
|
772
|
+
// shifted into slot 0 to "active" on the next restart. Each model's
|
|
773
|
+
// cursor is tracked independently, so collapse-tokens does not work
|
|
774
|
+
// here: a Claude-active account and a Gemini-active account can
|
|
775
|
+
// point at different rows.
|
|
776
|
+
const liveCurrentTokens: { claude?: string; gemini?: string } = {}
|
|
777
|
+
if (action === 'remove') {
|
|
778
|
+
const liveAccounts = accountManagerView.getAccounts()
|
|
779
|
+
const liveIndexes = accountManagerView.getActiveIndexByFamily()
|
|
780
|
+
liveCurrentTokens.claude =
|
|
781
|
+
liveAccounts[liveIndexes.claude]?.refreshToken ?? undefined
|
|
782
|
+
liveCurrentTokens.gemini =
|
|
783
|
+
liveAccounts[liveIndexes.gemini]?.refreshToken ?? undefined
|
|
784
|
+
}
|
|
785
|
+
|
|
552
786
|
let foundInStorage = false
|
|
553
787
|
let desiredEnabled: boolean | undefined
|
|
554
788
|
let previousEnabled: boolean | undefined
|
|
789
|
+
let nextActiveIndex = 0
|
|
790
|
+
let nextActiveIndexByFamily: { claude?: number; gemini?: number } = {
|
|
791
|
+
claude: 0,
|
|
792
|
+
gemini: 0,
|
|
793
|
+
}
|
|
555
794
|
await storage.mutate((current) => {
|
|
556
795
|
const tokenIdx = current.accounts.findIndex(
|
|
557
796
|
(entry) => entry.refreshToken === refreshToken,
|
|
@@ -587,13 +826,54 @@ export function createCommandDataService(
|
|
|
587
826
|
}
|
|
588
827
|
}
|
|
589
828
|
|
|
829
|
+
// remove: build the post-removal account list, then resolve the
|
|
830
|
+
// current-account's NEW index in that list. If the removed
|
|
831
|
+
// account was the live current, the captured token falls out of
|
|
832
|
+
// the list — the live AccountManager leaves the numeric current
|
|
833
|
+
// cursor at the SAME index (now occupied by whichever account
|
|
834
|
+
// shifted in), so the persisted index must follow that numeric
|
|
835
|
+
// slot rather than resetting to 0.
|
|
836
|
+
const nextAccounts = current.accounts.filter(
|
|
837
|
+
(account) => account.refreshToken !== refreshToken,
|
|
838
|
+
)
|
|
839
|
+
const resolveNextIndex = (
|
|
840
|
+
liveToken: string | undefined,
|
|
841
|
+
legacyIndex: number,
|
|
842
|
+
): number => {
|
|
843
|
+
if (!liveToken)
|
|
844
|
+
return Math.max(0, Math.min(legacyIndex, nextAccounts.length - 1))
|
|
845
|
+
const found = nextAccounts.findIndex(
|
|
846
|
+
(account) => account.refreshToken === liveToken,
|
|
847
|
+
)
|
|
848
|
+
if (found !== -1) return found
|
|
849
|
+
// The captured current token was the one removed. Mirror the
|
|
850
|
+
// core's in-memory semantics: keep the cursor at the removed
|
|
851
|
+
// slot when it still exists in the new array; when the removed
|
|
852
|
+
// slot was the last position and no longer exists, persist 0
|
|
853
|
+
// (the core's buildStorageSnapshot clamps negative sentinels
|
|
854
|
+
// to 0, and auth-doctor treats a negative activeIndex as
|
|
855
|
+
// corruption — cf. auth-doctor.ts:149-156).
|
|
856
|
+
//
|
|
857
|
+
// Use tokenIdx (the storage-snapshot position) rather than the
|
|
858
|
+
// outer-scope `index` (live-read position): a concurrent mutation
|
|
859
|
+
// between the live read and the lock can diverge the two, and the
|
|
860
|
+
// storage-level cursor must match the storage-level array.
|
|
861
|
+
return tokenIdx < nextAccounts.length ? tokenIdx : 0
|
|
862
|
+
}
|
|
863
|
+
const legacyClaude =
|
|
864
|
+
current.activeIndexByFamily?.claude ?? current.activeIndex
|
|
865
|
+
const legacyGemini =
|
|
866
|
+
current.activeIndexByFamily?.gemini ?? current.activeIndex
|
|
867
|
+
nextActiveIndex = resolveNextIndex(liveCurrentTokens.claude, legacyClaude)
|
|
868
|
+
nextActiveIndexByFamily = {
|
|
869
|
+
claude: resolveNextIndex(liveCurrentTokens.claude, legacyClaude),
|
|
870
|
+
gemini: resolveNextIndex(liveCurrentTokens.gemini, legacyGemini),
|
|
871
|
+
}
|
|
590
872
|
return {
|
|
591
873
|
...current,
|
|
592
|
-
accounts:
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
activeIndex: 0,
|
|
596
|
-
activeIndexByFamily: { claude: 0, gemini: 0 },
|
|
874
|
+
accounts: nextAccounts,
|
|
875
|
+
activeIndex: nextActiveIndex,
|
|
876
|
+
activeIndexByFamily: nextActiveIndexByFamily,
|
|
597
877
|
}
|
|
598
878
|
})
|
|
599
879
|
|