@cortexkit/opencode-anthropic-auth 2.0.2 → 2.1.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.
Files changed (59) hide show
  1. package/README.md +35 -845
  2. package/dist/cli-failure.d.ts +38 -7
  3. package/dist/cli.js +50 -102
  4. package/dist/custody-mode.d.ts +1 -0
  5. package/dist/fable-fallback.d.ts +6 -2
  6. package/dist/index-0kxy8k2c.js +2 -0
  7. package/dist/index-1nwcnxp8.js +42 -0
  8. package/dist/index-53nnxq39.js +2 -0
  9. package/dist/{index-2arfb1y3.js → index-6chd6jnr.js} +1 -1
  10. package/dist/{index-1tbpt45x.js → index-ah5s0bew.js} +1 -1
  11. package/dist/index-b1dvkp15.js +2 -0
  12. package/dist/{index-mge0rw54.js → index-f2ne65w4.js} +1 -1
  13. package/dist/{index-rqync0pc.js → index-hazrk5c3.js} +1 -1
  14. package/dist/{index-1jkyhgyr.js → index-jexctf5w.js} +1 -1
  15. package/dist/index.js +1 -1
  16. package/dist/quota-preflight.d.ts +1 -0
  17. package/dist/request-class.d.ts +10 -0
  18. package/dist/rpc/port-file.js +1 -1
  19. package/dist/rpc/protocol.js +1 -1
  20. package/dist/rpc/rpc-client.js +1 -1
  21. package/dist/rpc/rpc-dir.js +1 -1
  22. package/dist/setup/command.d.ts +5 -14
  23. package/dist/setup/native-activation.d.ts +21 -8
  24. package/dist/setup/native-enrollment.d.ts +21 -0
  25. package/dist/setup/native-local.d.ts +31 -1
  26. package/dist/setup/native-login.d.ts +12 -1
  27. package/dist/setup/process-fence.d.ts +0 -6
  28. package/dist/setup/types.d.ts +9 -0
  29. package/dist/sidebar-state.d.ts +20 -2
  30. package/dist/sidebar-state.js +1 -1
  31. package/dist/transform.d.ts +2 -8
  32. package/dist/tui-preferences.js +1 -1
  33. package/dist/tui.d.ts +0 -2
  34. package/dist/v2/activation.d.ts +2 -0
  35. package/dist/v2/adapter.d.ts +11 -1
  36. package/dist/v2/cachekeep.d.ts +19 -0
  37. package/dist/v2/menu.d.ts +2 -1
  38. package/dist/v2/server.js +4 -4
  39. package/dist/v2/setup.d.ts +2 -0
  40. package/dist/v2/source.d.ts +1 -0
  41. package/dist/v2/tui.js +1 -1
  42. package/package.json +10 -6
  43. package/src/sidebar-state.ts +105 -30
  44. package/src/tui-compiled/sidebar-state.ts +105 -30
  45. package/src/tui-compiled/tui.tsx +395 -241
  46. package/src/tui.tsx +199 -58
  47. package/dist/index-227rsmfv.js +0 -42
  48. package/dist/index-74r84r5f.js +0 -1
  49. package/dist/index-b3mxpp4w.js +0 -2
  50. package/dist/index-fn0zqnnw.js +0 -3
  51. package/dist/index-j0wsgy54.js +0 -449
  52. package/dist/index-jc8p0tkv.js +0 -2
  53. package/dist/setup/activation.d.ts +0 -7
  54. package/dist/setup/command.test.d.ts +0 -1
  55. package/dist/setup/detect.d.ts +0 -8
  56. package/dist/setup/host-executable.d.ts +0 -3
  57. package/dist/setup/jsonc.d.ts +0 -9
  58. package/dist/setup/opencode-config.d.ts +0 -18
  59. 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,12 @@ 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
+ formatQuota,
132
+ isQuotaStale,
133
+ logger,
134
+ quotaDisplayProjection,
135
+ } from '@cortexkit/anthropic-auth-core'
124
136
 
125
137
  const STATE_FILE_ENV = 'OPENCODE_ANTHROPIC_AUTH_SIDEBAR_STATE_FILE'
126
138
  const DEFAULT_STATE_DIR = join(tmpdir(), 'opencode-anthropic-auth')
@@ -229,12 +241,14 @@ function normalizeQuotaWindow(value: unknown): QuotaWindow | undefined {
229
241
  usedPercent,
230
242
  remainingPercent,
231
243
  resetsAt: typeof value.resetsAt === 'string' ? value.resetsAt : undefined,
244
+ ...(isFiniteNumber(value.checkedAt) && { checkedAt: value.checkedAt }),
232
245
  }
233
246
  }
234
247
 
235
248
  function normalizeAccountQuota(value: unknown): AccountQuota | null {
236
249
  if (!isRecord(value)) return null
237
250
  const quota: AccountQuota = {}
251
+ if (isFiniteNumber(value.checkedAt)) quota.checkedAt = value.checkedAt
238
252
  const fiveHour = normalizeQuotaWindow(value.five_hour)
239
253
  const sevenDay = normalizeQuotaWindow(value.seven_day)
240
254
  if (fiveHour) quota.five_hour = fiveHour
@@ -327,6 +341,9 @@ export function normalizeSidebarState(raw: unknown): SidebarState {
327
341
  if (isRecord(raw.main)) {
328
342
  const m = raw.main
329
343
  main.quota = normalizeAccountQuota(m.quota)
344
+ if (typeof m.enabled === 'boolean') main.enabled = m.enabled
345
+ if (typeof m.stateLine === 'string') main.stateLine = m.stateLine
346
+ if (m.quotaDated === true) main.quotaDated = true
330
347
  if (typeof m.tierLabel === 'string' && m.tierLabel.trim()) {
331
348
  main.tierLabel = m.tierLabel.trim()
332
349
  }
@@ -349,6 +366,10 @@ export function normalizeSidebarState(raw: unknown): SidebarState {
349
366
  label: typeof entry.label === 'string' ? entry.label : undefined,
350
367
  quota: normalizeAccountQuota(entry.quota),
351
368
  enabled: typeof entry.enabled === 'boolean' ? entry.enabled : false,
369
+ ...(typeof entry.stateLine === 'string' && {
370
+ stateLine: entry.stateLine,
371
+ }),
372
+ ...(entry.quotaDated === true && { quotaDated: true }),
352
373
  needsReauth:
353
374
  typeof entry.needsReauth === 'boolean' ? entry.needsReauth : false,
354
375
  ...(entry.vaultReauth === true && { vaultReauth: true }),
@@ -635,7 +656,9 @@ async function writeSidebarStateAtomic(
635
656
  return 'lock-lost-before-rename'
636
657
  }
637
658
  await rename(tempFile, stateFile)
638
- await testHooks?.afterRename?.(stateFile)
659
+ // Look the hooks up again: a test may start listening while this write is
660
+ // in flight, after reading the old file, and must still hear this rename.
661
+ await sidebarStateWriteHooksFor(stateFile)?.afterRename?.(stateFile)
639
662
  if (!(await ownsLock())) return 'lock-lost-after-rename'
640
663
  return 'written'
641
664
  } catch (error) {
@@ -792,13 +815,13 @@ export function resolveActiveAccount(state: SidebarState): {
792
815
  id: string
793
816
  name: string
794
817
  quota: AccountQuota | null
818
+ stateLine?: string
819
+ quotaDated?: boolean
795
820
  } {
796
821
  const activeId = state.activeId
797
822
  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.
823
+ // A disabled fallback cannot remain the active route, even though vault
824
+ // accounts stay visible in the expanded inventory while turned off.
802
825
  const fallback = state.fallbacks?.find(
803
826
  (account) => account.enabled && account.id === activeId,
804
827
  )
@@ -807,15 +830,22 @@ export function resolveActiveAccount(state: SidebarState): {
807
830
  id: fallback.id,
808
831
  name: fallback.label ?? fallback.id,
809
832
  quota: fallback.quota,
833
+ ...(fallback.stateLine && {
834
+ stateLine: fallback.stateLine,
835
+ quotaDated: fallback.quotaDated,
836
+ }),
810
837
  }
811
838
  }
812
839
  }
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
840
+ return {
841
+ id: 'main',
842
+ name: 'main',
843
+ quota: state.main?.quota ?? null,
844
+ ...(state.main?.stateLine && {
845
+ stateLine: state.main.stateLine,
846
+ quotaDated: state.main.quotaDated,
847
+ }),
848
+ }
819
849
  }
820
850
 
821
851
  /**
@@ -903,49 +933,94 @@ export function getFableRecoverySummary(
903
933
  return `Opus 4.8 · ${recovery.remaining} left`
904
934
  }
905
935
 
906
- export function getCollapsedQuotaSummary(quota: AccountQuota | null): {
936
+ /** Keep full shared names when they fit; truncate only to preserve row columns. */
937
+ export function formatQuotaRowLabel(name: string, width = 10): string {
938
+ if (width <= 0) return ''
939
+ let label = name
940
+ if (Bun.stringWidth(label) > width) {
941
+ label = ''
942
+ for (const character of name) {
943
+ if (Bun.stringWidth(label + character) > width - 1) break
944
+ label += character
945
+ }
946
+ label += '…'
947
+ }
948
+ return label + ' '.repeat(Math.max(0, width - Bun.stringWidth(label)))
949
+ }
950
+
951
+ export function getCollapsedQuotaSummary(
952
+ quota: AccountQuota | null,
953
+ now = Date.now(),
954
+ ): {
907
955
  fiveHourUsedPercent: number | null
908
956
  sevenDayUsedPercent: number | null
909
957
  scopedUsedPercents: number[]
910
958
  text: string | null
959
+ stale: boolean
911
960
  } {
912
961
  const fiveHourUsedPercent = quota?.five_hour?.usedPercent ?? null
913
962
  const sevenDayUsedPercent = quota?.seven_day?.usedPercent ?? null
914
963
  const scoped = quota?.scoped ?? []
915
964
  const scopedUsedPercents = scoped.map((window) => window.usedPercent)
916
965
  if (
917
- fiveHourUsedPercent == null &&
918
- sevenDayUsedPercent == null &&
919
- scoped.length === 0
966
+ !quota ||
967
+ (fiveHourUsedPercent == null &&
968
+ sevenDayUsedPercent == null &&
969
+ scoped.length === 0 &&
970
+ !quota.extraUsage)
920
971
  ) {
921
972
  return {
922
973
  fiveHourUsedPercent,
923
974
  sevenDayUsedPercent,
924
975
  scopedUsedPercents,
925
976
  text: null,
977
+ stale: false,
926
978
  }
927
979
  }
928
980
 
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
-
981
+ const projection = quotaDisplayProjection(quota, now)
941
982
  return {
942
983
  fiveHourUsedPercent,
943
984
  sevenDayUsedPercent,
944
985
  scopedUsedPercents,
945
- text: [...primarySegments, ...scopedSegments].join(' '),
986
+ // The narrow form leaves out the reading's age to stay on one line, so
987
+ // a stale reading is dimmed instead of labelled.
988
+ text: formatQuota(projection, { now, form: 'narrow' }),
989
+ stale: isQuotaStale(projection, { now }),
946
990
  }
947
991
  }
948
992
 
993
+ export function getCollapsedAccounts(
994
+ state: SidebarState,
995
+ showFallbacks = true,
996
+ ) {
997
+ const activeId = resolveActiveAccount(state).id
998
+ return [
999
+ {
1000
+ id: 'main',
1001
+ name: 'main',
1002
+ quota: state.main?.quota ?? null,
1003
+ quotaDated: state.main?.quotaDated,
1004
+ active: activeId === 'main',
1005
+ },
1006
+ ...(showFallbacks
1007
+ ? (state.fallbacks ?? [])
1008
+ .filter(
1009
+ (account) =>
1010
+ account.enabled ||
1011
+ (account.custodyState && account.custodyState !== 'off'),
1012
+ )
1013
+ .map((account) => ({
1014
+ id: account.id,
1015
+ name: account.label ?? account.id,
1016
+ quota: account.quota,
1017
+ quotaDated: account.quotaDated,
1018
+ active: activeId === account.id,
1019
+ }))
1020
+ : []),
1021
+ ]
1022
+ }
1023
+
949
1024
  export const FIVE_HOUR_MS = 5 * 60 * 60 * 1000
950
1025
  export const SEVEN_DAY_MS = 7 * 24 * 60 * 60 * 1000
951
1026
 
@@ -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,12 @@ 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
+ formatQuota,
132
+ isQuotaStale,
133
+ logger,
134
+ quotaDisplayProjection,
135
+ } from '@cortexkit/anthropic-auth-core'
124
136
 
125
137
  const STATE_FILE_ENV = 'OPENCODE_ANTHROPIC_AUTH_SIDEBAR_STATE_FILE'
126
138
  const DEFAULT_STATE_DIR = join(tmpdir(), 'opencode-anthropic-auth')
@@ -229,12 +241,14 @@ function normalizeQuotaWindow(value: unknown): QuotaWindow | undefined {
229
241
  usedPercent,
230
242
  remainingPercent,
231
243
  resetsAt: typeof value.resetsAt === 'string' ? value.resetsAt : undefined,
244
+ ...(isFiniteNumber(value.checkedAt) && { checkedAt: value.checkedAt }),
232
245
  }
233
246
  }
234
247
 
235
248
  function normalizeAccountQuota(value: unknown): AccountQuota | null {
236
249
  if (!isRecord(value)) return null
237
250
  const quota: AccountQuota = {}
251
+ if (isFiniteNumber(value.checkedAt)) quota.checkedAt = value.checkedAt
238
252
  const fiveHour = normalizeQuotaWindow(value.five_hour)
239
253
  const sevenDay = normalizeQuotaWindow(value.seven_day)
240
254
  if (fiveHour) quota.five_hour = fiveHour
@@ -327,6 +341,9 @@ export function normalizeSidebarState(raw: unknown): SidebarState {
327
341
  if (isRecord(raw.main)) {
328
342
  const m = raw.main
329
343
  main.quota = normalizeAccountQuota(m.quota)
344
+ if (typeof m.enabled === 'boolean') main.enabled = m.enabled
345
+ if (typeof m.stateLine === 'string') main.stateLine = m.stateLine
346
+ if (m.quotaDated === true) main.quotaDated = true
330
347
  if (typeof m.tierLabel === 'string' && m.tierLabel.trim()) {
331
348
  main.tierLabel = m.tierLabel.trim()
332
349
  }
@@ -349,6 +366,10 @@ export function normalizeSidebarState(raw: unknown): SidebarState {
349
366
  label: typeof entry.label === 'string' ? entry.label : undefined,
350
367
  quota: normalizeAccountQuota(entry.quota),
351
368
  enabled: typeof entry.enabled === 'boolean' ? entry.enabled : false,
369
+ ...(typeof entry.stateLine === 'string' && {
370
+ stateLine: entry.stateLine,
371
+ }),
372
+ ...(entry.quotaDated === true && { quotaDated: true }),
352
373
  needsReauth:
353
374
  typeof entry.needsReauth === 'boolean' ? entry.needsReauth : false,
354
375
  ...(entry.vaultReauth === true && { vaultReauth: true }),
@@ -635,7 +656,9 @@ async function writeSidebarStateAtomic(
635
656
  return 'lock-lost-before-rename'
636
657
  }
637
658
  await rename(tempFile, stateFile)
638
- await testHooks?.afterRename?.(stateFile)
659
+ // Look the hooks up again: a test may start listening while this write is
660
+ // in flight, after reading the old file, and must still hear this rename.
661
+ await sidebarStateWriteHooksFor(stateFile)?.afterRename?.(stateFile)
639
662
  if (!(await ownsLock())) return 'lock-lost-after-rename'
640
663
  return 'written'
641
664
  } catch (error) {
@@ -792,13 +815,13 @@ export function resolveActiveAccount(state: SidebarState): {
792
815
  id: string
793
816
  name: string
794
817
  quota: AccountQuota | null
818
+ stateLine?: string
819
+ quotaDated?: boolean
795
820
  } {
796
821
  const activeId = state.activeId
797
822
  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.
823
+ // A disabled fallback cannot remain the active route, even though vault
824
+ // accounts stay visible in the expanded inventory while turned off.
802
825
  const fallback = state.fallbacks?.find(
803
826
  (account) => account.enabled && account.id === activeId,
804
827
  )
@@ -807,15 +830,22 @@ export function resolveActiveAccount(state: SidebarState): {
807
830
  id: fallback.id,
808
831
  name: fallback.label ?? fallback.id,
809
832
  quota: fallback.quota,
833
+ ...(fallback.stateLine && {
834
+ stateLine: fallback.stateLine,
835
+ quotaDated: fallback.quotaDated,
836
+ }),
810
837
  }
811
838
  }
812
839
  }
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
840
+ return {
841
+ id: 'main',
842
+ name: 'main',
843
+ quota: state.main?.quota ?? null,
844
+ ...(state.main?.stateLine && {
845
+ stateLine: state.main.stateLine,
846
+ quotaDated: state.main.quotaDated,
847
+ }),
848
+ }
819
849
  }
820
850
 
821
851
  /**
@@ -903,49 +933,94 @@ export function getFableRecoverySummary(
903
933
  return `Opus 4.8 · ${recovery.remaining} left`
904
934
  }
905
935
 
906
- export function getCollapsedQuotaSummary(quota: AccountQuota | null): {
936
+ /** Keep full shared names when they fit; truncate only to preserve row columns. */
937
+ export function formatQuotaRowLabel(name: string, width = 10): string {
938
+ if (width <= 0) return ''
939
+ let label = name
940
+ if (Bun.stringWidth(label) > width) {
941
+ label = ''
942
+ for (const character of name) {
943
+ if (Bun.stringWidth(label + character) > width - 1) break
944
+ label += character
945
+ }
946
+ label += '…'
947
+ }
948
+ return label + ' '.repeat(Math.max(0, width - Bun.stringWidth(label)))
949
+ }
950
+
951
+ export function getCollapsedQuotaSummary(
952
+ quota: AccountQuota | null,
953
+ now = Date.now(),
954
+ ): {
907
955
  fiveHourUsedPercent: number | null
908
956
  sevenDayUsedPercent: number | null
909
957
  scopedUsedPercents: number[]
910
958
  text: string | null
959
+ stale: boolean
911
960
  } {
912
961
  const fiveHourUsedPercent = quota?.five_hour?.usedPercent ?? null
913
962
  const sevenDayUsedPercent = quota?.seven_day?.usedPercent ?? null
914
963
  const scoped = quota?.scoped ?? []
915
964
  const scopedUsedPercents = scoped.map((window) => window.usedPercent)
916
965
  if (
917
- fiveHourUsedPercent == null &&
918
- sevenDayUsedPercent == null &&
919
- scoped.length === 0
966
+ !quota ||
967
+ (fiveHourUsedPercent == null &&
968
+ sevenDayUsedPercent == null &&
969
+ scoped.length === 0 &&
970
+ !quota.extraUsage)
920
971
  ) {
921
972
  return {
922
973
  fiveHourUsedPercent,
923
974
  sevenDayUsedPercent,
924
975
  scopedUsedPercents,
925
976
  text: null,
977
+ stale: false,
926
978
  }
927
979
  }
928
980
 
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
-
981
+ const projection = quotaDisplayProjection(quota, now)
941
982
  return {
942
983
  fiveHourUsedPercent,
943
984
  sevenDayUsedPercent,
944
985
  scopedUsedPercents,
945
- text: [...primarySegments, ...scopedSegments].join(' '),
986
+ // The narrow form leaves out the reading's age to stay on one line, so
987
+ // a stale reading is dimmed instead of labelled.
988
+ text: formatQuota(projection, { now, form: 'narrow' }),
989
+ stale: isQuotaStale(projection, { now }),
946
990
  }
947
991
  }
948
992
 
993
+ export function getCollapsedAccounts(
994
+ state: SidebarState,
995
+ showFallbacks = true,
996
+ ) {
997
+ const activeId = resolveActiveAccount(state).id
998
+ return [
999
+ {
1000
+ id: 'main',
1001
+ name: 'main',
1002
+ quota: state.main?.quota ?? null,
1003
+ quotaDated: state.main?.quotaDated,
1004
+ active: activeId === 'main',
1005
+ },
1006
+ ...(showFallbacks
1007
+ ? (state.fallbacks ?? [])
1008
+ .filter(
1009
+ (account) =>
1010
+ account.enabled ||
1011
+ (account.custodyState && account.custodyState !== 'off'),
1012
+ )
1013
+ .map((account) => ({
1014
+ id: account.id,
1015
+ name: account.label ?? account.id,
1016
+ quota: account.quota,
1017
+ quotaDated: account.quotaDated,
1018
+ active: activeId === account.id,
1019
+ }))
1020
+ : []),
1021
+ ]
1022
+ }
1023
+
949
1024
  export const FIVE_HOUR_MS = 5 * 60 * 60 * 1000
950
1025
  export const SEVEN_DAY_MS = 7 * 24 * 60 * 60 * 1000
951
1026