@cortexkit/opencode-anthropic-auth 2.4.7 → 2.4.8

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.
@@ -13,7 +13,7 @@ import {
13
13
  quotaDisplayProjection,
14
14
  quotaWindowParts,
15
15
  } from '@cortexkit/anthropic-auth-core'
16
- import { type BoxRenderable, type RGBA, TextAttributes } from '@opentui/core'
16
+ import type { BoxRenderable, RGBA } from '@opentui/core'
17
17
  import {
18
18
  createEffect,
19
19
  createMemo,
@@ -24,6 +24,7 @@ import {
24
24
  onCleanup,
25
25
  Show,
26
26
  } from 'solid-js'
27
+ import { createStore, reconcile } from 'solid-js/store'
27
28
  import {
28
29
  type AccountQuota,
29
30
  computeQuotaPacing,
@@ -32,7 +33,6 @@ import {
32
33
  fitPaceLine,
33
34
  fitQuotaRow,
34
35
  formatPrimeAccountValue,
35
- formatQuotaRowLabel,
36
36
  getCollapsedAccounts,
37
37
  getCollapsedQuotaSummary,
38
38
  getFableRecoverySummary,
@@ -52,6 +52,13 @@ import {
52
52
  resolveAnthropicAuthPrefs,
53
53
  watchTuiPreferences,
54
54
  } from '../tui-preferences.js'
55
+ import {
56
+ type AccountView,
57
+ type PaceView,
58
+ type Tone as SharedTone,
59
+ SidebarFrame,
60
+ SidebarTextRow,
61
+ } from './quota-sidebar.js'
55
62
 
56
63
  // Read package metadata from either the raw src/ entry or its generated
57
64
  // src/tui-compiled/tui/ counterpart. Avoid a JSON import because package.json sits
@@ -114,6 +121,18 @@ function usageTone(usedPct: number, appearance: AppearancePrefs): Tone {
114
121
  return 'err'
115
122
  }
116
123
 
124
+ function sharedTone(tone: Tone): SharedTone {
125
+ return tone === 'ok'
126
+ ? 'success'
127
+ : tone === 'warn'
128
+ ? 'warning'
129
+ : tone === 'err'
130
+ ? 'error'
131
+ : tone === 'muted'
132
+ ? 'textMuted'
133
+ : tone
134
+ }
135
+
117
136
  interface BarSegment {
118
137
  text: string
119
138
  tone: Tone
@@ -242,77 +261,6 @@ function CollapsedRow(props: {
242
261
  )
243
262
  }
244
263
 
245
- function CollapsedAccountRow(props: {
246
- theme: SidebarTheme
247
- account: ReturnType<typeof getCollapsedAccounts>[number]
248
- tone: Tone
249
- now: number
250
- }) {
251
- const [width, setWidth] = createSignal(40)
252
- let row: BoxRenderable | undefined
253
- const summary = () => getCollapsedQuotaSummary(props.account.quota, props.now)
254
- const value = () => summary().text ?? '—'
255
- // Keep the quota and its two-cell status suffix intact; only the label shrinks.
256
- const labelWidth = () =>
257
- Math.max(0, width() - 1 - Bun.stringWidth(value()) - 2)
258
- const dim = () =>
259
- !props.account.active || props.account.quotaDated || summary().stale
260
- const attributes = () => (dim() ? TextAttributes.DIM : TextAttributes.BOLD)
261
- const color = () =>
262
- dim() ? props.theme.textMuted : toneColor(props.theme, props.tone)
263
- return (
264
- <box
265
- width='100%'
266
- height={1}
267
- flexDirection='row'
268
- ref={(element: BoxRenderable) => {
269
- row = element
270
- setWidth(element.width || 40)
271
- }}
272
- onSizeChange={() => setWidth(row?.width || 40)}
273
- >
274
- <Show when={labelWidth() > 0}>
275
- <text
276
- width={labelWidth()}
277
- flexShrink={0}
278
- wrapMode='none'
279
- fg={color()}
280
- attributes={attributes()}
281
- >
282
- {formatQuotaRowLabel(props.account.name, labelWidth())}
283
- </text>
284
- <text width={1} flexShrink={0}>
285
- {' '}
286
- </text>
287
- </Show>
288
- <text
289
- width={Bun.stringWidth(value())}
290
- flexShrink={0}
291
- wrapMode='none'
292
- fg={color()}
293
- attributes={attributes()}
294
- >
295
- {value()}
296
- </text>
297
- <text
298
- width={2}
299
- flexShrink={0}
300
- fg={toneColor(
301
- props.theme,
302
- props.account.blocked
303
- ? 'warn'
304
- : props.account.active
305
- ? 'ok'
306
- : 'muted',
307
- )}
308
- attributes={attributes()}
309
- >
310
- {props.account.blocked ? ' ⊘' : ' ●'}
311
- </text>
312
- </box>
313
- )
314
- }
315
-
316
264
  // One cell run of a single-line row: drawn at its own width, never shrunk and
317
265
  // never wrapped, so the row's layout is exactly what `fitQuotaRow` measured.
318
266
  function Cell(props: { fg: SidebarColor; text: string }) {
@@ -618,6 +566,54 @@ export function AccountBlock(props: {
618
566
  )
619
567
  }
620
568
 
569
+ function AccountExtras(props: {
570
+ quota: AccountQuota | null
571
+ now: number
572
+ theme: SidebarTheme
573
+ }) {
574
+ const [width, setWidth] = createSignal<number>()
575
+ let block: BoxRenderable | undefined
576
+ const budget = () =>
577
+ quotaWindowParts(quotaDisplayProjection(props.quota, props.now), {
578
+ now: props.now,
579
+ }).windows.find((part) => part.kind === 'budget')
580
+ return (
581
+ <box
582
+ width='100%'
583
+ flexDirection='column'
584
+ ref={(element: BoxRenderable) => {
585
+ block = element
586
+ setWidth(element.width || undefined)
587
+ }}
588
+ onSizeChange={() => setWidth(block?.width || undefined)}
589
+ >
590
+ <Show when={budget()}>
591
+ {(part: () => QuotaWindowPart) => (
592
+ <box width='100%' height={1} flexDirection='row'>
593
+ <text
594
+ wrapMode='none'
595
+ fg={toneColor(
596
+ props.theme,
597
+ props.quota?.extraUsage?.severity === 'critical'
598
+ ? 'err'
599
+ : props.quota?.extraUsage?.severity === 'warning' ||
600
+ props.quota?.extraUsage?.exhausted
601
+ ? 'warn'
602
+ : 'muted',
603
+ )}
604
+ >
605
+ {creditLine(part().figure, props.quota?.extraUsage, width())}
606
+ </text>
607
+ </box>
608
+ )}
609
+ </Show>
610
+ <Show when={props.quota?.fallbackAdvised === true}>
611
+ <SidebarTextRow text='→ fallback advised' />
612
+ </Show>
613
+ </box>
614
+ )
615
+ }
616
+
621
617
  export interface SidebarController {
622
618
  prefs: () => AnthropicAuthTuiPrefs
623
619
  collapsed: () => boolean
@@ -721,22 +717,6 @@ export function ClaudeSidebar(props: {
721
717
  state().main?.quota != null ||
722
718
  state().main?.stateLine != null ||
723
719
  enabledFallbacks().length > 0
724
- // One label column for every expanded account, so all their bars line up.
725
- const quotaLabelWidth = () =>
726
- quotaLabelColumnWidth(
727
- [
728
- state().main?.quota,
729
- ...(prefs().sections.fallbackAccounts
730
- ? enabledFallbacks().map((fallback) => fallback.quota)
731
- : []),
732
- ],
733
- now(),
734
- )
735
-
736
- const headerLabel = () => {
737
- const name = prefs().header.label
738
- return !hasData() ? name : collapsed() ? `\u25b6 ${name}` : `\u25bc ${name}`
739
- }
740
720
  const activeAccount = () => resolveActiveAccount(state())
741
721
  const activeQuotaSummary = () =>
742
722
  getCollapsedQuotaSummary(activeAccount().quota, now())
@@ -812,217 +792,284 @@ export function ClaudeSidebar(props: {
812
792
  return `${r.transport} \u00b7 ${r.enabled ? 'on' : 'off'}`
813
793
  }
814
794
 
815
- return (
816
- <box width='100%' flexDirection='column'>
817
- {/* The collapse control stays on the left; the version stays on the right. */}
818
- {/* biome-ignore lint/a11y/noStaticElementInteractions: opentui renders to a terminal, not the DOM — ARIA roles do not apply */}
819
- <box
820
- width='100%'
821
- flexDirection='row'
822
- justifyContent='space-between'
823
- alignItems='center'
824
- onMouseDown={() => {
825
- if (hasData()) props.controller.toggleCollapsed()
826
- }}
827
- >
828
- <box paddingLeft={1} paddingRight={1} backgroundColor={theme().accent}>
829
- <text fg={theme().background}>
830
- <b>{headerLabel()}</b>
831
- </text>
832
- </box>
833
- <Show when={prefs().header.showVersion && PLUGIN_VERSION !== ''}>
834
- <text fg={theme().textMuted}>{`v${PLUGIN_VERSION}`}</text>
835
- </Show>
836
- </box>
795
+ const accountDetail = (id: string) =>
796
+ id === 'main'
797
+ ? state().main
798
+ : state().fallbacks?.find((account) => account.id === id)
799
+ const accountViews = createMemo((): AccountView[] => {
800
+ if (!hasData()) return []
801
+ return getCollapsedAccounts(state(), prefs().sections.fallbackAccounts).map(
802
+ (account): AccountView => {
803
+ const detail = accountDetail(account.id)
804
+ const quota = quotaDisplayProjection(account.quota, now())
805
+ const needsReauth =
806
+ detail && 'needsReauth' in detail && detail.needsReauth
807
+ const vaultReauth =
808
+ detail && 'vaultReauth' in detail && detail.vaultReauth
809
+ const active = state().activeId === account.id
810
+ const status =
811
+ account.quotaDated && account.quota
812
+ ? 'dated'
813
+ : needsReauth
814
+ ? 're-login'
815
+ : vaultReauth
816
+ ? 'needs sign-in in the vault'
817
+ : active
818
+ ? 'active'
819
+ : 'idle'
820
+ const groups = [
821
+ {
822
+ rows: quotaWindowParts(quota, { now: now() })
823
+ .windows.filter((part) => part.kind === 'window')
824
+ .map((part) => {
825
+ const scoped = account.quota?.scoped?.find(
826
+ (window) => window.title === part.name,
827
+ )
828
+ const window =
829
+ part.name === '5h'
830
+ ? account.quota?.five_hour
831
+ : part.name === '7d'
832
+ ? account.quota?.seven_day
833
+ : scoped
834
+ const pacing =
835
+ prefs().sections.pacing && !account.quotaDated && window
836
+ ? computeQuotaPacing(
837
+ window,
838
+ part.name === '5h' ? FIVE_HOUR_MS : SEVEN_DAY_MS,
839
+ now(),
840
+ )
841
+ : null
842
+ const text = fitPaceLine(
843
+ pacing,
844
+ undefined,
845
+ pacing?.runsOutAt
846
+ ? formatResetIn(pacing.runsOutAt, now())
847
+ : undefined,
848
+ part.resetIn,
849
+ part.remainingPercent,
850
+ )
851
+ const pace: PaceView | undefined = pacing
852
+ ? {
853
+ text: text ?? '',
854
+ tone:
855
+ pacing.state === 'deficit' ? 'warning' : 'textMuted',
856
+ overlay: {
857
+ kind: pacing.deltaPercent > 0 ? 'deficit' : 'reserve',
858
+ percent: pacing.pacePercent,
859
+ },
860
+ }
861
+ : undefined
862
+ const binding =
863
+ account.quota?.bindingWindow ===
864
+ (part.name === '5h'
865
+ ? 'five_hour'
866
+ : part.name === '7d'
867
+ ? 'seven_day'
868
+ : scoped?.id)
869
+ return {
870
+ part: {
871
+ ...part,
872
+ name: part.short,
873
+ figure: `${part.figure}${binding ? ' •' : ''}`,
874
+ },
875
+ pace,
876
+ }
877
+ }),
878
+ },
879
+ ]
880
+ return {
881
+ id: account.id,
882
+ label: account.name,
883
+ titleSuffix: detail?.tierLabel,
884
+ quota,
885
+ groups,
886
+ status: {
887
+ text: status,
888
+ tone:
889
+ needsReauth || vaultReauth || detail?.stateLine
890
+ ? 'warning'
891
+ : active
892
+ ? 'success'
893
+ : 'textMuted',
894
+ },
895
+ blocked: Boolean(account.blocked),
896
+ indicatorTone: account.blocked
897
+ ? 'warning'
898
+ : account.active
899
+ ? 'success'
900
+ : 'textMuted',
901
+ collapsed: {
902
+ muted: Boolean(
903
+ !account.active ||
904
+ account.quotaDated ||
905
+ getCollapsedQuotaSummary(account.quota, now()).stale,
906
+ ),
907
+ quotaTone: sharedTone(activeQuotaTone()),
908
+ },
909
+ emptyQuotaText: { expanded: 'checking…', collapsed: '—' },
910
+ }
911
+ },
912
+ )
913
+ })
914
+ // Polls rebuild snapshots, but account ids keep the shared frame's rows mounted.
915
+ const [accounts, setAccounts] = createStore<AccountView[]>([])
916
+ createEffect(() => setAccounts(reconcile(accountViews(), { key: 'id' })))
837
917
 
838
- {/* Collapsed accounts keep placement order; only a fresh active row is emphasized. */}
839
- <Show when={collapsed() && hasData()}>
840
- {/* Index keys rows by position: each state update builds new account
841
- objects, so For would rebuild every row and its measured width.
842
- Passing the accessor's value as a prop keeps the row reactive. */}
843
- <Index
844
- each={getCollapsedAccounts(
845
- state(),
846
- prefs().sections.fallbackAccounts,
847
- )}
848
- >
849
- {(account) => (
850
- <CollapsedAccountRow
851
- theme={theme()}
852
- account={account()}
853
- tone={activeQuotaTone()}
854
- now={now()}
855
- />
856
- )}
857
- </Index>
858
- <Show when={state().fastMode}>
859
- <CollapsedRow theme={theme()} label='Mode'>
860
- <text fg={toneColor(theme(), 'accent')}>
861
- <b>{'fast'}</b>
862
- </text>
863
- </CollapsedRow>
918
+ return (
919
+ <SidebarFrame
920
+ header={{
921
+ badge: prefs().header.label,
922
+ version:
923
+ prefs().header.showVersion && PLUGIN_VERSION !== ''
924
+ ? `v${PLUGIN_VERSION}`
925
+ : undefined,
926
+ }}
927
+ accounts={accounts}
928
+ theme={theme()}
929
+ appearance={prefs().appearance}
930
+ showQuota={prefs().sections.quota}
931
+ now={now()}
932
+ collapsed={collapsed()}
933
+ onToggle={props.controller.toggleCollapsed}
934
+ beforeQuota={(account) => (
935
+ <Show when={accountDetail(account.id)?.stateLine}>
936
+ {(text: () => string) => <SidebarTextRow text={text()} />}
864
937
  </Show>
865
- <Show when={fableRecoverySummary()}>
866
- {(summary: () => string) => (
867
- <CollapsedRow theme={theme()} label='Recovery'>
868
- <text fg={toneColor(theme(), fableRecoveryTone())}>
869
- <b>{summary()}</b>
938
+ )}
939
+ afterQuota={(account) => (
940
+ <AccountExtras
941
+ quota={accountDetail(account.id)?.quota ?? null}
942
+ now={now()}
943
+ theme={theme()}
944
+ />
945
+ )}
946
+ collapsedSections={() => (
947
+ <>
948
+ <Show when={state().fastMode}>
949
+ <CollapsedRow theme={theme()} label='Mode'>
950
+ <text fg={toneColor(theme(), 'accent')}>
951
+ <b>{'fast'}</b>
870
952
  </text>
871
953
  </CollapsedRow>
872
- )}
873
- </Show>
874
- </Show>
875
-
876
- {/* Expanded: full sections. Also render when there's no data so the
877
- sidebar can never go blank if data clears while collapsed. */}
878
- <Show when={!collapsed() || !hasData()}>
879
- <Show
880
- when={hasData()}
881
- fallback={
954
+ </Show>
955
+ <Show when={fableRecoverySummary()}>
956
+ {(summary: () => string) => (
957
+ <CollapsedRow theme={theme()} label='Recovery'>
958
+ <text fg={toneColor(theme(), fableRecoveryTone())}>
959
+ <b>{summary()}</b>
960
+ </text>
961
+ </CollapsedRow>
962
+ )}
963
+ </Show>
964
+ </>
965
+ )}
966
+ sections={() => (
967
+ <>
968
+ <Show when={!hasData()}>
882
969
  <box marginTop={1} width='100%'>
883
970
  <text fg={theme().textMuted}>{'Waiting for quota\u2026'}</text>
884
971
  </box>
885
- }
886
- >
887
- {/* Quota */}
888
- <Show when={prefs().sections.quota}>
889
- <SectionHeader theme={theme()} title='Quota' />
890
- <AccountBlock
891
- theme={theme()}
892
- appearance={prefs().appearance}
893
- name='main'
894
- quota={state().main?.quota}
895
- active={state().activeId === 'main'}
896
- pacingEnabled={prefs().sections.pacing}
897
- tierLabel={state().main?.tierLabel}
898
- labelWidth={quotaLabelWidth()}
899
- stateLine={state().main?.stateLine}
900
- quotaDated={state().main?.quotaDated}
901
- now={now()}
902
- />
903
- <Show when={prefs().sections.fallbackAccounts}>
904
- <For each={enabledFallbacks()}>
905
- {(fb) => (
906
- <AccountBlock
907
- theme={theme()}
908
- appearance={prefs().appearance}
909
- name={fb.label ?? fb.id}
910
- quota={fb.quota}
911
- active={state().activeId === fb.id}
912
- pacingEnabled={prefs().sections.pacing}
913
- tierLabel={fb.tierLabel}
914
- labelWidth={quotaLabelWidth()}
915
- needsReauth={fb.needsReauth}
916
- vaultReauth={fb.vaultReauth}
917
- stateLine={fb.stateLine}
918
- quotaDated={fb.quotaDated}
919
- marginTop={1}
920
- now={now()}
921
- />
922
- )}
923
- </For>
924
- </Show>
925
972
  </Show>
926
- </Show>
927
-
928
- {/* Routing */}
929
- <Show when={prefs().sections.routing}>
930
- <SectionHeader theme={theme()} title='Routing' />
931
- <StatRow
932
- theme={theme()}
933
- label='Route'
934
- value={state().route}
935
- tone='accent'
936
- />
937
- <StatRow
938
- theme={theme()}
939
- label='Mode'
940
- value={state().fastMode ? 'fast' : 'std'}
941
- tone={state().fastMode ? 'accent' : 'muted'}
942
- />
943
- <StatRow
944
- theme={theme()}
945
- label='Relay'
946
- value={relayValue()}
947
- tone={state().relay?.enabled ? 'ok' : 'muted'}
948
- />
949
- </Show>
950
973
 
951
- <Show when={fableRecoverySummary()}>
952
- {(summary: () => string) => (
974
+ {/* Routing */}
975
+ <Show when={prefs().sections.routing}>
976
+ <SectionHeader theme={theme()} title='Routing' />
953
977
  <StatRow
954
978
  theme={theme()}
955
- label='Recovery'
956
- value={summary()}
957
- tone={fableRecoveryTone()}
979
+ label='Route'
980
+ value={state().route}
981
+ tone='accent'
958
982
  />
959
- )}
960
- </Show>
961
-
962
- {/* Cache */}
963
- <Show when={showCache()}>
964
- <SectionHeader theme={theme()} title='Cache' />
965
- <StatRow
966
- theme={theme()}
967
- label='1h cache'
968
- value={`${cacheKeep()?.window} \u00b7 ${cacheKeep()?.enabled ? 'on' : 'off'}`}
969
- tone={cacheKeep()?.enabled ? 'ok' : 'muted'}
970
- />
971
- <Show when={(cacheKeep()?.trackedSessions ?? 0) > 0}>
972
983
  <StatRow
973
984
  theme={theme()}
974
- label='Tracked'
975
- value={String(cacheKeep()?.trackedSessions)}
976
- tone='text'
985
+ label='Mode'
986
+ value={state().fastMode ? 'fast' : 'std'}
987
+ tone={state().fastMode ? 'accent' : 'muted'}
988
+ />
989
+ <StatRow
990
+ theme={theme()}
991
+ label='Relay'
992
+ value={relayValue()}
993
+ tone={state().relay?.enabled ? 'ok' : 'muted'}
977
994
  />
978
995
  </Show>
979
- </Show>
980
996
 
981
- {/* Prime — only when enabled. Expanded view only (no collapsed surface). */}
982
- <Show when={showPrime()}>
983
- <StatRow
984
- theme={theme()}
985
- label='Prime'
986
- value={primeHasError() ? 'err' : 'on'}
987
- tone={primeHasError() ? 'warn' : 'ok'}
988
- />
989
- <For each={prime()?.accounts ?? []}>
990
- {(account) => {
991
- const value = () => formatPrimeAccountValue(account, now())
992
- return (
993
- <StatRow
994
- theme={theme()}
995
- label={account.label}
996
- value={value().text}
997
- tone={value().hasError ? 'warn' : 'text'}
998
- />
999
- )
1000
- }}
1001
- </For>
1002
- </Show>
997
+ <Show when={fableRecoverySummary()}>
998
+ {(summary: () => string) => (
999
+ <StatRow
1000
+ theme={theme()}
1001
+ label='Recovery'
1002
+ value={summary()}
1003
+ tone={fableRecoveryTone()}
1004
+ />
1005
+ )}
1006
+ </Show>
1003
1007
 
1004
- {/* Health — only when something is wrong */}
1005
- <Show when={degraded() && prefs().sections.health}>
1006
- <SectionHeader theme={theme()} title='Health' />
1007
- <Show when={quotaBackedOff()}>
1008
+ {/* Cache */}
1009
+ <Show when={showCache()}>
1010
+ <SectionHeader theme={theme()} title='Cache' />
1008
1011
  <StatRow
1009
1012
  theme={theme()}
1010
- label='Quota API'
1011
- value={`backoff ${formatUntil(state().main?.quotaBackoffUntil, now())}`}
1012
- tone='warn'
1013
+ label='1h cache'
1014
+ value={`${cacheKeep()?.window} \u00b7 ${cacheKeep()?.enabled ? 'on' : 'off'}`}
1015
+ tone={cacheKeep()?.enabled ? 'ok' : 'muted'}
1013
1016
  />
1017
+ <Show when={(cacheKeep()?.trackedSessions ?? 0) > 0}>
1018
+ <StatRow
1019
+ theme={theme()}
1020
+ label='Tracked'
1021
+ value={String(cacheKeep()?.trackedSessions)}
1022
+ tone='text'
1023
+ />
1024
+ </Show>
1014
1025
  </Show>
1015
- <Show when={refreshBackedOff()}>
1026
+
1027
+ {/* Prime — only when enabled. Expanded view only (no collapsed surface). */}
1028
+ <Show when={showPrime()}>
1016
1029
  <StatRow
1017
1030
  theme={theme()}
1018
- label='Token refresh'
1019
- value={`backoff ${formatUntil(state().main?.refreshBackoffUntil, now())}`}
1020
- tone='warn'
1031
+ label='Prime'
1032
+ value={primeHasError() ? 'err' : 'on'}
1033
+ tone={primeHasError() ? 'warn' : 'ok'}
1021
1034
  />
1035
+ <For each={prime()?.accounts ?? []}>
1036
+ {(account) => {
1037
+ const value = () => formatPrimeAccountValue(account, now())
1038
+ return (
1039
+ <StatRow
1040
+ theme={theme()}
1041
+ label={account.label}
1042
+ value={value().text}
1043
+ tone={value().hasError ? 'warn' : 'text'}
1044
+ />
1045
+ )
1046
+ }}
1047
+ </For>
1022
1048
  </Show>
1023
- </Show>
1024
- </Show>
1025
- </box>
1049
+
1050
+ {/* Health — only when something is wrong */}
1051
+ <Show when={degraded() && prefs().sections.health}>
1052
+ <SectionHeader theme={theme()} title='Health' />
1053
+ <Show when={quotaBackedOff()}>
1054
+ <StatRow
1055
+ theme={theme()}
1056
+ label='Quota API'
1057
+ value={`backoff ${formatUntil(state().main?.quotaBackoffUntil, now())}`}
1058
+ tone='warn'
1059
+ />
1060
+ </Show>
1061
+ <Show when={refreshBackedOff()}>
1062
+ <StatRow
1063
+ theme={theme()}
1064
+ label='Token refresh'
1065
+ value={`backoff ${formatUntil(state().main?.refreshBackoffUntil, now())}`}
1066
+ tone='warn'
1067
+ />
1068
+ </Show>
1069
+ </Show>
1070
+ </>
1071
+ )}
1072
+ />
1026
1073
  )
1027
1074
  }
1028
1075