@cortexkit/opencode-anthropic-auth 2.0.1 → 2.0.3

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.
Files changed (54) hide show
  1. package/README.md +35 -845
  2. package/dist/cli-failure.d.ts +6 -5
  3. package/dist/cli.js +57 -103
  4. package/dist/custody-mode.d.ts +1 -0
  5. package/dist/{index-52r2e1h9.js → index-5hyennzm.js} +1 -1
  6. package/dist/{index-p4wvcwhh.js → index-fncp6r0j.js} +1 -1
  7. package/dist/index-jmjevtm1.js +2 -0
  8. package/dist/index-kd89hcr3.js +2 -0
  9. package/dist/index-mdpyfkv3.js +464 -0
  10. package/dist/index-nfcp0m1t.js +42 -0
  11. package/dist/index-qttfa42y.js +2 -0
  12. package/dist/{index-3xqv3rw3.js → index-rftced5v.js} +1 -1
  13. package/dist/{index-zdgceztz.js → index-vjehtqnb.js} +1 -1
  14. package/dist/index.js +1 -1
  15. package/dist/quota-preflight.d.ts +1 -0
  16. package/dist/rpc/port-file.js +1 -1
  17. package/dist/rpc/protocol.js +1 -1
  18. package/dist/rpc/rpc-client.js +1 -1
  19. package/dist/rpc/rpc-dir.js +1 -1
  20. package/dist/setup/command.d.ts +5 -14
  21. package/dist/setup/native-activation.d.ts +14 -6
  22. package/dist/setup/native-enrollment.d.ts +15 -0
  23. package/dist/setup/native-local.d.ts +31 -1
  24. package/dist/setup/process-fence.d.ts +0 -6
  25. package/dist/setup/types.d.ts +9 -0
  26. package/dist/sidebar-state.d.ts +20 -2
  27. package/dist/sidebar-state.js +1 -1
  28. package/dist/transform.d.ts +17 -2
  29. package/dist/tui-preferences.js +1 -1
  30. package/dist/tui.d.ts +0 -2
  31. package/dist/v2/activation.d.ts +2 -0
  32. package/dist/v2/adapter.d.ts +8 -2
  33. package/dist/v2/cachekeep.d.ts +19 -0
  34. package/dist/v2/menu.d.ts +2 -1
  35. package/dist/v2/server.js +4 -4
  36. package/dist/v2/setup.d.ts +2 -0
  37. package/dist/v2/source.d.ts +1 -0
  38. package/dist/v2/tui.js +1 -1
  39. package/package.json +5 -5
  40. package/src/sidebar-state.ts +165 -29
  41. package/src/tui-compiled/sidebar-state.ts +165 -29
  42. package/src/tui-compiled/tui.tsx +363 -239
  43. package/src/tui.tsx +149 -48
  44. package/dist/index-04syas8a.js +0 -2
  45. package/dist/index-b3mxpp4w.js +0 -2
  46. package/dist/index-ynq181e8.js +0 -42
  47. package/dist/index-yxtdke11.js +0 -449
  48. package/dist/setup/activation.d.ts +0 -7
  49. package/dist/setup/command.test.d.ts +0 -1
  50. package/dist/setup/detect.d.ts +0 -8
  51. package/dist/setup/host-executable.d.ts +0 -3
  52. package/dist/setup/jsonc.d.ts +0 -9
  53. package/dist/setup/opencode-config.d.ts +0 -18
  54. package/dist/setup/pi.d.ts +0 -18
@@ -2,6 +2,7 @@ export interface QuotaWindow {
2
2
  usedPercent: number
3
3
  remainingPercent: number
4
4
  resetsAt?: string
5
+ checkedAt?: number
5
6
  }
6
7
 
7
8
  export interface ScopedQuotaWindow extends QuotaWindow {
@@ -12,6 +13,7 @@ export interface ScopedQuotaWindow extends QuotaWindow {
12
13
  }
13
14
 
14
15
  export interface AccountQuota {
16
+ checkedAt?: number
15
17
  five_hour?: QuotaWindow
16
18
  seven_day?: QuotaWindow
17
19
  scoped?: ScopedQuotaWindow[]
@@ -31,10 +33,12 @@ export interface SidebarAccountState {
31
33
  label: string | undefined
32
34
  quota: AccountQuota | null
33
35
  enabled: boolean
36
+ stateLine?: string
37
+ quotaDated?: boolean
34
38
  // True when the account's refresh token is permanently dead (400
35
39
  // invalid_grant) and it needs a re-login — distinct from a transient backoff.
36
40
  needsReauth: boolean
37
- // True when the vault copy needs re-importing while the sidecar remains usable.
41
+ // True when the vault credential needs a new sign-in.
38
42
  vaultReauth?: boolean
39
43
  // A gate alone does not prove the sidecar can serve a request.
40
44
  vaultServed?: boolean
@@ -77,7 +81,10 @@ export interface PrimeSidebarAccountState {
77
81
  export interface SidebarState {
78
82
  main: {
79
83
  quota: AccountQuota | null
84
+ enabled?: boolean
80
85
  tierLabel?: string
86
+ stateLine?: string
87
+ quotaDated?: boolean
81
88
  quotaBackedOff?: boolean
82
89
  quotaBackoffUntil?: number
83
90
  refreshBackedOff?: boolean
@@ -120,7 +127,11 @@ import {
120
127
  import { tmpdir } from 'node:os'
121
128
  import { dirname, join } from 'node:path'
122
129
  import type { PrimeUsageCounters } from '@cortexkit/anthropic-auth-core'
123
- import { logger } from '@cortexkit/anthropic-auth-core'
130
+ import {
131
+ logger,
132
+ quotaDisplayProjection,
133
+ quotaWindowParts,
134
+ } from '@cortexkit/anthropic-auth-core'
124
135
 
125
136
  const STATE_FILE_ENV = 'OPENCODE_ANTHROPIC_AUTH_SIDEBAR_STATE_FILE'
126
137
  const DEFAULT_STATE_DIR = join(tmpdir(), 'opencode-anthropic-auth')
@@ -229,12 +240,14 @@ function normalizeQuotaWindow(value: unknown): QuotaWindow | undefined {
229
240
  usedPercent,
230
241
  remainingPercent,
231
242
  resetsAt: typeof value.resetsAt === 'string' ? value.resetsAt : undefined,
243
+ ...(isFiniteNumber(value.checkedAt) && { checkedAt: value.checkedAt }),
232
244
  }
233
245
  }
234
246
 
235
247
  function normalizeAccountQuota(value: unknown): AccountQuota | null {
236
248
  if (!isRecord(value)) return null
237
249
  const quota: AccountQuota = {}
250
+ if (isFiniteNumber(value.checkedAt)) quota.checkedAt = value.checkedAt
238
251
  const fiveHour = normalizeQuotaWindow(value.five_hour)
239
252
  const sevenDay = normalizeQuotaWindow(value.seven_day)
240
253
  if (fiveHour) quota.five_hour = fiveHour
@@ -327,6 +340,9 @@ export function normalizeSidebarState(raw: unknown): SidebarState {
327
340
  if (isRecord(raw.main)) {
328
341
  const m = raw.main
329
342
  main.quota = normalizeAccountQuota(m.quota)
343
+ if (typeof m.enabled === 'boolean') main.enabled = m.enabled
344
+ if (typeof m.stateLine === 'string') main.stateLine = m.stateLine
345
+ if (m.quotaDated === true) main.quotaDated = true
330
346
  if (typeof m.tierLabel === 'string' && m.tierLabel.trim()) {
331
347
  main.tierLabel = m.tierLabel.trim()
332
348
  }
@@ -349,6 +365,10 @@ export function normalizeSidebarState(raw: unknown): SidebarState {
349
365
  label: typeof entry.label === 'string' ? entry.label : undefined,
350
366
  quota: normalizeAccountQuota(entry.quota),
351
367
  enabled: typeof entry.enabled === 'boolean' ? entry.enabled : false,
368
+ ...(typeof entry.stateLine === 'string' && {
369
+ stateLine: entry.stateLine,
370
+ }),
371
+ ...(entry.quotaDated === true && { quotaDated: true }),
352
372
  needsReauth:
353
373
  typeof entry.needsReauth === 'boolean' ? entry.needsReauth : false,
354
374
  ...(entry.vaultReauth === true && { vaultReauth: true }),
@@ -792,13 +812,13 @@ export function resolveActiveAccount(state: SidebarState): {
792
812
  id: string
793
813
  name: string
794
814
  quota: AccountQuota | null
815
+ stateLine?: string
816
+ quotaDated?: boolean
795
817
  } {
796
818
  const activeId = state.activeId
797
819
  if (activeId && activeId !== 'main') {
798
- // `account.enabled` is defensive: writeSidebarState already filters disabled
799
- // accounts out of state.fallbacks, so in normal operation every entry is
800
- // enabled. Kept so this pure helper stays correct for any caller and is
801
- // exercised directly by the unit tests.
820
+ // A disabled fallback cannot remain the active route, even though vault
821
+ // accounts stay visible in the expanded inventory while turned off.
802
822
  const fallback = state.fallbacks?.find(
803
823
  (account) => account.enabled && account.id === activeId,
804
824
  )
@@ -807,15 +827,22 @@ export function resolveActiveAccount(state: SidebarState): {
807
827
  id: fallback.id,
808
828
  name: fallback.label ?? fallback.id,
809
829
  quota: fallback.quota,
830
+ ...(fallback.stateLine && {
831
+ stateLine: fallback.stateLine,
832
+ quotaDated: fallback.quotaDated,
833
+ }),
810
834
  }
811
835
  }
812
836
  }
813
- return { id: 'main', name: 'main', quota: state.main?.quota ?? null }
814
- }
815
-
816
- export function formatScopedQuotaLabel(title: string) {
817
- const label = title.replace(/\s+only$/i, '').trim()
818
- return /^fable$/i.test(label) ? 'Fa' : label
837
+ return {
838
+ id: 'main',
839
+ name: 'main',
840
+ quota: state.main?.quota ?? null,
841
+ ...(state.main?.stateLine && {
842
+ stateLine: state.main.stateLine,
843
+ quotaDated: state.main.quotaDated,
844
+ }),
845
+ }
819
846
  }
820
847
 
821
848
  /**
@@ -903,49 +930,158 @@ export function getFableRecoverySummary(
903
930
  return `Opus 4.8 · ${recovery.remaining} left`
904
931
  }
905
932
 
906
- export function getCollapsedQuotaSummary(quota: AccountQuota | null): {
933
+ /** Keep full shared names when they fit; truncate only to preserve row columns. */
934
+ export function formatQuotaRowLabel(name: string, width = 10): string {
935
+ if (width <= 0) return ''
936
+ let label = name
937
+ if (Bun.stringWidth(label) > width) {
938
+ label = ''
939
+ for (const character of name) {
940
+ if (Bun.stringWidth(label + character) > width - 1) break
941
+ label += character
942
+ }
943
+ label += '…'
944
+ }
945
+ return label + ' '.repeat(Math.max(0, width - Bun.stringWidth(label)))
946
+ }
947
+
948
+ // common-auth 0.18.0 has no narrow form, so keep its one-line quota wording here.
949
+ // Once supported, replace this body with formatQuota(quotaDisplayProjection(quota, now),
950
+ // { now, form: 'narrow' }) from common-auth.
951
+ function formatNarrowQuota(quota: AccountQuota, now: number): string {
952
+ const parts = quotaWindowParts(quotaDisplayProjection(quota, now), { now })
953
+ const percent = (remaining: number | undefined) =>
954
+ remaining === undefined ? '—' : `${Math.round(remaining)}%`
955
+ const values = ['5h', '7d'].map((name) => {
956
+ const part = parts.windows.find(
957
+ (part) => part.name === name && part.kind === 'window',
958
+ )
959
+ return `${name} ${percent(part?.remainingPercent)}`
960
+ })
961
+ const scoped = quota.scoped ?? []
962
+ const names = [...new Set(scoped.map((window) => window.modelName))]
963
+ const lengths = names.map(() => 2)
964
+ while (true) {
965
+ const aliases = names.map((name, index) =>
966
+ [...name].slice(0, lengths[index]).join(''),
967
+ )
968
+ let extended = false
969
+ for (const [index, alias] of aliases.entries()) {
970
+ const length = lengths[index] ?? 2
971
+ if (
972
+ aliases.some(
973
+ (other, otherIndex) => otherIndex !== index && other === alias,
974
+ ) &&
975
+ length < [...(names[index] ?? '')].length
976
+ ) {
977
+ lengths[index] = length + 1
978
+ extended = true
979
+ }
980
+ }
981
+ if (!extended) break
982
+ }
983
+ const tightest = scoped.reduce<ScopedQuotaWindow | undefined>(
984
+ (lowest, window) =>
985
+ !lowest || window.remainingPercent < lowest.remainingPercent
986
+ ? window
987
+ : lowest,
988
+ undefined,
989
+ )
990
+ const budget = parts.windows.find((part) => part.kind === 'budget')
991
+ const budgetRemaining = quota.extraUsage?.exhausted
992
+ ? 0
993
+ : budget?.remainingPercent
994
+ // Monetary ratios can land a few floating-point bits below an equal model reading.
995
+ const budgetTighter =
996
+ budget &&
997
+ (!tightest ||
998
+ (budgetRemaining !== undefined &&
999
+ tightest.remainingPercent - budgetRemaining > 1e-9))
1000
+ if (budgetTighter) {
1001
+ values.push(`cr ${percent(budgetRemaining)}`)
1002
+ } else if (tightest) {
1003
+ const index = names.indexOf(tightest.modelName)
1004
+ const name = [...tightest.modelName].slice(0, lengths[index]).join('')
1005
+ const part = parts.windows.find(
1006
+ (part) => part.kind === 'window' && part.name === tightest.title,
1007
+ )
1008
+ values.push(`${name} ${percent(part?.remainingPercent)}`)
1009
+ }
1010
+ return `${values.join(' · ')} left`
1011
+ }
1012
+
1013
+ export function getCollapsedQuotaSummary(
1014
+ quota: AccountQuota | null,
1015
+ now = Date.now(),
1016
+ ): {
907
1017
  fiveHourUsedPercent: number | null
908
1018
  sevenDayUsedPercent: number | null
909
1019
  scopedUsedPercents: number[]
910
1020
  text: string | null
1021
+ stale: boolean
911
1022
  } {
912
1023
  const fiveHourUsedPercent = quota?.five_hour?.usedPercent ?? null
913
1024
  const sevenDayUsedPercent = quota?.seven_day?.usedPercent ?? null
914
1025
  const scoped = quota?.scoped ?? []
915
1026
  const scopedUsedPercents = scoped.map((window) => window.usedPercent)
916
1027
  if (
917
- fiveHourUsedPercent == null &&
918
- sevenDayUsedPercent == null &&
919
- scoped.length === 0
1028
+ !quota ||
1029
+ (fiveHourUsedPercent == null &&
1030
+ sevenDayUsedPercent == null &&
1031
+ scoped.length === 0 &&
1032
+ !quota.extraUsage)
920
1033
  ) {
921
1034
  return {
922
1035
  fiveHourUsedPercent,
923
1036
  sevenDayUsedPercent,
924
1037
  scopedUsedPercents,
925
1038
  text: null,
1039
+ stale: false,
926
1040
  }
927
1041
  }
928
1042
 
929
- const scopedSegments = scoped.map(
930
- (window) =>
931
- `${formatScopedQuotaLabel(window.title)}: ${Math.round(window.usedPercent)}%`,
932
- )
933
- const primarySegments =
934
- fiveHourUsedPercent == null && sevenDayUsedPercent == null
935
- ? []
936
- : [
937
- `5h: ${fiveHourUsedPercent == null ? '—' : `${Math.round(fiveHourUsedPercent)}%`}`,
938
- `7d: ${sevenDayUsedPercent == null ? '—' : `${Math.round(sevenDayUsedPercent)}%`}`,
939
- ]
940
-
941
1043
  return {
942
1044
  fiveHourUsedPercent,
943
1045
  sevenDayUsedPercent,
944
1046
  scopedUsedPercents,
945
- text: [...primarySegments, ...scopedSegments].join(' '),
1047
+ text: formatNarrowQuota(quota, now),
1048
+ stale:
1049
+ quotaWindowParts(quotaDisplayProjection(quota, now), { now }).age !==
1050
+ undefined,
946
1051
  }
947
1052
  }
948
1053
 
1054
+ export function getCollapsedAccounts(
1055
+ state: SidebarState,
1056
+ showFallbacks = true,
1057
+ ) {
1058
+ const activeId = resolveActiveAccount(state).id
1059
+ return [
1060
+ {
1061
+ id: 'main',
1062
+ name: 'main',
1063
+ quota: state.main?.quota ?? null,
1064
+ quotaDated: state.main?.quotaDated,
1065
+ active: activeId === 'main',
1066
+ },
1067
+ ...(showFallbacks
1068
+ ? (state.fallbacks ?? [])
1069
+ .filter(
1070
+ (account) =>
1071
+ account.enabled ||
1072
+ (account.custodyState && account.custodyState !== 'off'),
1073
+ )
1074
+ .map((account) => ({
1075
+ id: account.id,
1076
+ name: account.label ?? account.id,
1077
+ quota: account.quota,
1078
+ quotaDated: account.quotaDated,
1079
+ active: activeId === account.id,
1080
+ }))
1081
+ : []),
1082
+ ]
1083
+ }
1084
+
949
1085
  export const FIVE_HOUR_MS = 5 * 60 * 60 * 1000
950
1086
  export const SEVEN_DAY_MS = 7 * 24 * 60 * 60 * 1000
951
1087
 
@@ -2,6 +2,7 @@ export interface QuotaWindow {
2
2
  usedPercent: number
3
3
  remainingPercent: number
4
4
  resetsAt?: string
5
+ checkedAt?: number
5
6
  }
6
7
 
7
8
  export interface ScopedQuotaWindow extends QuotaWindow {
@@ -12,6 +13,7 @@ export interface ScopedQuotaWindow extends QuotaWindow {
12
13
  }
13
14
 
14
15
  export interface AccountQuota {
16
+ checkedAt?: number
15
17
  five_hour?: QuotaWindow
16
18
  seven_day?: QuotaWindow
17
19
  scoped?: ScopedQuotaWindow[]
@@ -31,10 +33,12 @@ export interface SidebarAccountState {
31
33
  label: string | undefined
32
34
  quota: AccountQuota | null
33
35
  enabled: boolean
36
+ stateLine?: string
37
+ quotaDated?: boolean
34
38
  // True when the account's refresh token is permanently dead (400
35
39
  // invalid_grant) and it needs a re-login — distinct from a transient backoff.
36
40
  needsReauth: boolean
37
- // True when the vault copy needs re-importing while the sidecar remains usable.
41
+ // True when the vault credential needs a new sign-in.
38
42
  vaultReauth?: boolean
39
43
  // A gate alone does not prove the sidecar can serve a request.
40
44
  vaultServed?: boolean
@@ -77,7 +81,10 @@ export interface PrimeSidebarAccountState {
77
81
  export interface SidebarState {
78
82
  main: {
79
83
  quota: AccountQuota | null
84
+ enabled?: boolean
80
85
  tierLabel?: string
86
+ stateLine?: string
87
+ quotaDated?: boolean
81
88
  quotaBackedOff?: boolean
82
89
  quotaBackoffUntil?: number
83
90
  refreshBackedOff?: boolean
@@ -120,7 +127,11 @@ import {
120
127
  import { tmpdir } from 'node:os'
121
128
  import { dirname, join } from 'node:path'
122
129
  import type { PrimeUsageCounters } from '@cortexkit/anthropic-auth-core'
123
- import { logger } from '@cortexkit/anthropic-auth-core'
130
+ import {
131
+ logger,
132
+ quotaDisplayProjection,
133
+ quotaWindowParts,
134
+ } from '@cortexkit/anthropic-auth-core'
124
135
 
125
136
  const STATE_FILE_ENV = 'OPENCODE_ANTHROPIC_AUTH_SIDEBAR_STATE_FILE'
126
137
  const DEFAULT_STATE_DIR = join(tmpdir(), 'opencode-anthropic-auth')
@@ -229,12 +240,14 @@ function normalizeQuotaWindow(value: unknown): QuotaWindow | undefined {
229
240
  usedPercent,
230
241
  remainingPercent,
231
242
  resetsAt: typeof value.resetsAt === 'string' ? value.resetsAt : undefined,
243
+ ...(isFiniteNumber(value.checkedAt) && { checkedAt: value.checkedAt }),
232
244
  }
233
245
  }
234
246
 
235
247
  function normalizeAccountQuota(value: unknown): AccountQuota | null {
236
248
  if (!isRecord(value)) return null
237
249
  const quota: AccountQuota = {}
250
+ if (isFiniteNumber(value.checkedAt)) quota.checkedAt = value.checkedAt
238
251
  const fiveHour = normalizeQuotaWindow(value.five_hour)
239
252
  const sevenDay = normalizeQuotaWindow(value.seven_day)
240
253
  if (fiveHour) quota.five_hour = fiveHour
@@ -327,6 +340,9 @@ export function normalizeSidebarState(raw: unknown): SidebarState {
327
340
  if (isRecord(raw.main)) {
328
341
  const m = raw.main
329
342
  main.quota = normalizeAccountQuota(m.quota)
343
+ if (typeof m.enabled === 'boolean') main.enabled = m.enabled
344
+ if (typeof m.stateLine === 'string') main.stateLine = m.stateLine
345
+ if (m.quotaDated === true) main.quotaDated = true
330
346
  if (typeof m.tierLabel === 'string' && m.tierLabel.trim()) {
331
347
  main.tierLabel = m.tierLabel.trim()
332
348
  }
@@ -349,6 +365,10 @@ export function normalizeSidebarState(raw: unknown): SidebarState {
349
365
  label: typeof entry.label === 'string' ? entry.label : undefined,
350
366
  quota: normalizeAccountQuota(entry.quota),
351
367
  enabled: typeof entry.enabled === 'boolean' ? entry.enabled : false,
368
+ ...(typeof entry.stateLine === 'string' && {
369
+ stateLine: entry.stateLine,
370
+ }),
371
+ ...(entry.quotaDated === true && { quotaDated: true }),
352
372
  needsReauth:
353
373
  typeof entry.needsReauth === 'boolean' ? entry.needsReauth : false,
354
374
  ...(entry.vaultReauth === true && { vaultReauth: true }),
@@ -792,13 +812,13 @@ export function resolveActiveAccount(state: SidebarState): {
792
812
  id: string
793
813
  name: string
794
814
  quota: AccountQuota | null
815
+ stateLine?: string
816
+ quotaDated?: boolean
795
817
  } {
796
818
  const activeId = state.activeId
797
819
  if (activeId && activeId !== 'main') {
798
- // `account.enabled` is defensive: writeSidebarState already filters disabled
799
- // accounts out of state.fallbacks, so in normal operation every entry is
800
- // enabled. Kept so this pure helper stays correct for any caller and is
801
- // exercised directly by the unit tests.
820
+ // A disabled fallback cannot remain the active route, even though vault
821
+ // accounts stay visible in the expanded inventory while turned off.
802
822
  const fallback = state.fallbacks?.find(
803
823
  (account) => account.enabled && account.id === activeId,
804
824
  )
@@ -807,15 +827,22 @@ export function resolveActiveAccount(state: SidebarState): {
807
827
  id: fallback.id,
808
828
  name: fallback.label ?? fallback.id,
809
829
  quota: fallback.quota,
830
+ ...(fallback.stateLine && {
831
+ stateLine: fallback.stateLine,
832
+ quotaDated: fallback.quotaDated,
833
+ }),
810
834
  }
811
835
  }
812
836
  }
813
- return { id: 'main', name: 'main', quota: state.main?.quota ?? null }
814
- }
815
-
816
- export function formatScopedQuotaLabel(title: string) {
817
- const label = title.replace(/\s+only$/i, '').trim()
818
- return /^fable$/i.test(label) ? 'Fa' : label
837
+ return {
838
+ id: 'main',
839
+ name: 'main',
840
+ quota: state.main?.quota ?? null,
841
+ ...(state.main?.stateLine && {
842
+ stateLine: state.main.stateLine,
843
+ quotaDated: state.main.quotaDated,
844
+ }),
845
+ }
819
846
  }
820
847
 
821
848
  /**
@@ -903,49 +930,158 @@ export function getFableRecoverySummary(
903
930
  return `Opus 4.8 · ${recovery.remaining} left`
904
931
  }
905
932
 
906
- export function getCollapsedQuotaSummary(quota: AccountQuota | null): {
933
+ /** Keep full shared names when they fit; truncate only to preserve row columns. */
934
+ export function formatQuotaRowLabel(name: string, width = 10): string {
935
+ if (width <= 0) return ''
936
+ let label = name
937
+ if (Bun.stringWidth(label) > width) {
938
+ label = ''
939
+ for (const character of name) {
940
+ if (Bun.stringWidth(label + character) > width - 1) break
941
+ label += character
942
+ }
943
+ label += '…'
944
+ }
945
+ return label + ' '.repeat(Math.max(0, width - Bun.stringWidth(label)))
946
+ }
947
+
948
+ // common-auth 0.18.0 has no narrow form, so keep its one-line quota wording here.
949
+ // Once supported, replace this body with formatQuota(quotaDisplayProjection(quota, now),
950
+ // { now, form: 'narrow' }) from common-auth.
951
+ function formatNarrowQuota(quota: AccountQuota, now: number): string {
952
+ const parts = quotaWindowParts(quotaDisplayProjection(quota, now), { now })
953
+ const percent = (remaining: number | undefined) =>
954
+ remaining === undefined ? '—' : `${Math.round(remaining)}%`
955
+ const values = ['5h', '7d'].map((name) => {
956
+ const part = parts.windows.find(
957
+ (part) => part.name === name && part.kind === 'window',
958
+ )
959
+ return `${name} ${percent(part?.remainingPercent)}`
960
+ })
961
+ const scoped = quota.scoped ?? []
962
+ const names = [...new Set(scoped.map((window) => window.modelName))]
963
+ const lengths = names.map(() => 2)
964
+ while (true) {
965
+ const aliases = names.map((name, index) =>
966
+ [...name].slice(0, lengths[index]).join(''),
967
+ )
968
+ let extended = false
969
+ for (const [index, alias] of aliases.entries()) {
970
+ const length = lengths[index] ?? 2
971
+ if (
972
+ aliases.some(
973
+ (other, otherIndex) => otherIndex !== index && other === alias,
974
+ ) &&
975
+ length < [...(names[index] ?? '')].length
976
+ ) {
977
+ lengths[index] = length + 1
978
+ extended = true
979
+ }
980
+ }
981
+ if (!extended) break
982
+ }
983
+ const tightest = scoped.reduce<ScopedQuotaWindow | undefined>(
984
+ (lowest, window) =>
985
+ !lowest || window.remainingPercent < lowest.remainingPercent
986
+ ? window
987
+ : lowest,
988
+ undefined,
989
+ )
990
+ const budget = parts.windows.find((part) => part.kind === 'budget')
991
+ const budgetRemaining = quota.extraUsage?.exhausted
992
+ ? 0
993
+ : budget?.remainingPercent
994
+ // Monetary ratios can land a few floating-point bits below an equal model reading.
995
+ const budgetTighter =
996
+ budget &&
997
+ (!tightest ||
998
+ (budgetRemaining !== undefined &&
999
+ tightest.remainingPercent - budgetRemaining > 1e-9))
1000
+ if (budgetTighter) {
1001
+ values.push(`cr ${percent(budgetRemaining)}`)
1002
+ } else if (tightest) {
1003
+ const index = names.indexOf(tightest.modelName)
1004
+ const name = [...tightest.modelName].slice(0, lengths[index]).join('')
1005
+ const part = parts.windows.find(
1006
+ (part) => part.kind === 'window' && part.name === tightest.title,
1007
+ )
1008
+ values.push(`${name} ${percent(part?.remainingPercent)}`)
1009
+ }
1010
+ return `${values.join(' · ')} left`
1011
+ }
1012
+
1013
+ export function getCollapsedQuotaSummary(
1014
+ quota: AccountQuota | null,
1015
+ now = Date.now(),
1016
+ ): {
907
1017
  fiveHourUsedPercent: number | null
908
1018
  sevenDayUsedPercent: number | null
909
1019
  scopedUsedPercents: number[]
910
1020
  text: string | null
1021
+ stale: boolean
911
1022
  } {
912
1023
  const fiveHourUsedPercent = quota?.five_hour?.usedPercent ?? null
913
1024
  const sevenDayUsedPercent = quota?.seven_day?.usedPercent ?? null
914
1025
  const scoped = quota?.scoped ?? []
915
1026
  const scopedUsedPercents = scoped.map((window) => window.usedPercent)
916
1027
  if (
917
- fiveHourUsedPercent == null &&
918
- sevenDayUsedPercent == null &&
919
- scoped.length === 0
1028
+ !quota ||
1029
+ (fiveHourUsedPercent == null &&
1030
+ sevenDayUsedPercent == null &&
1031
+ scoped.length === 0 &&
1032
+ !quota.extraUsage)
920
1033
  ) {
921
1034
  return {
922
1035
  fiveHourUsedPercent,
923
1036
  sevenDayUsedPercent,
924
1037
  scopedUsedPercents,
925
1038
  text: null,
1039
+ stale: false,
926
1040
  }
927
1041
  }
928
1042
 
929
- const scopedSegments = scoped.map(
930
- (window) =>
931
- `${formatScopedQuotaLabel(window.title)}: ${Math.round(window.usedPercent)}%`,
932
- )
933
- const primarySegments =
934
- fiveHourUsedPercent == null && sevenDayUsedPercent == null
935
- ? []
936
- : [
937
- `5h: ${fiveHourUsedPercent == null ? '—' : `${Math.round(fiveHourUsedPercent)}%`}`,
938
- `7d: ${sevenDayUsedPercent == null ? '—' : `${Math.round(sevenDayUsedPercent)}%`}`,
939
- ]
940
-
941
1043
  return {
942
1044
  fiveHourUsedPercent,
943
1045
  sevenDayUsedPercent,
944
1046
  scopedUsedPercents,
945
- text: [...primarySegments, ...scopedSegments].join(' '),
1047
+ text: formatNarrowQuota(quota, now),
1048
+ stale:
1049
+ quotaWindowParts(quotaDisplayProjection(quota, now), { now }).age !==
1050
+ undefined,
946
1051
  }
947
1052
  }
948
1053
 
1054
+ export function getCollapsedAccounts(
1055
+ state: SidebarState,
1056
+ showFallbacks = true,
1057
+ ) {
1058
+ const activeId = resolveActiveAccount(state).id
1059
+ return [
1060
+ {
1061
+ id: 'main',
1062
+ name: 'main',
1063
+ quota: state.main?.quota ?? null,
1064
+ quotaDated: state.main?.quotaDated,
1065
+ active: activeId === 'main',
1066
+ },
1067
+ ...(showFallbacks
1068
+ ? (state.fallbacks ?? [])
1069
+ .filter(
1070
+ (account) =>
1071
+ account.enabled ||
1072
+ (account.custodyState && account.custodyState !== 'off'),
1073
+ )
1074
+ .map((account) => ({
1075
+ id: account.id,
1076
+ name: account.label ?? account.id,
1077
+ quota: account.quota,
1078
+ quotaDated: account.quotaDated,
1079
+ active: activeId === account.id,
1080
+ }))
1081
+ : []),
1082
+ ]
1083
+ }
1084
+
949
1085
  export const FIVE_HOUR_MS = 5 * 60 * 60 * 1000
950
1086
  export const SEVEN_DAY_MS = 7 * 24 * 60 * 60 * 1000
951
1087