@cortexkit/opencode-anthropic-auth 2.4.6 → 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 }) {
@@ -541,30 +489,34 @@ export function AccountBlock(props: {
541
489
  when={props.quota}
542
490
  fallback={<text fg={props.theme.textMuted}>{'checking\u2026'}</text>}
543
491
  >
544
- <QuotaRow
545
- theme={props.theme}
546
- appearance={props.appearance}
547
- label='5h'
548
- labelWidth={labelWidth()}
549
- width={width()}
550
- part={partFor('5h')}
551
- usedPercent={props.quota?.five_hour?.usedPercent}
552
- pacing={pacingFor(props.quota?.five_hour, FIVE_HOUR_MS)}
553
- binding={props.quota?.bindingWindow === 'five_hour'}
554
- now={props.now}
555
- />
556
- <QuotaRow
557
- theme={props.theme}
558
- appearance={props.appearance}
559
- label='7d'
560
- labelWidth={labelWidth()}
561
- width={width()}
562
- part={partFor('7d')}
563
- usedPercent={props.quota?.seven_day?.usedPercent}
564
- pacing={pacingFor(props.quota?.seven_day, SEVEN_DAY_MS)}
565
- binding={props.quota?.bindingWindow === 'seven_day'}
566
- now={props.now}
567
- />
492
+ <Show when={props.quota?.five_hour}>
493
+ <QuotaRow
494
+ theme={props.theme}
495
+ appearance={props.appearance}
496
+ label='5h'
497
+ labelWidth={labelWidth()}
498
+ width={width()}
499
+ part={partFor('5h')}
500
+ usedPercent={props.quota?.five_hour?.usedPercent}
501
+ pacing={pacingFor(props.quota?.five_hour, FIVE_HOUR_MS)}
502
+ binding={props.quota?.bindingWindow === 'five_hour'}
503
+ now={props.now}
504
+ />
505
+ </Show>
506
+ <Show when={props.quota?.seven_day}>
507
+ <QuotaRow
508
+ theme={props.theme}
509
+ appearance={props.appearance}
510
+ label='7d'
511
+ labelWidth={labelWidth()}
512
+ width={width()}
513
+ part={partFor('7d')}
514
+ usedPercent={props.quota?.seven_day?.usedPercent}
515
+ pacing={pacingFor(props.quota?.seven_day, SEVEN_DAY_MS)}
516
+ binding={props.quota?.bindingWindow === 'seven_day'}
517
+ now={props.now}
518
+ />
519
+ </Show>
568
520
  <For each={props.quota?.scoped ?? []}>
569
521
  {(window, index) => (
570
522
  <QuotaRow
@@ -614,6 +566,54 @@ export function AccountBlock(props: {
614
566
  )
615
567
  }
616
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
+
617
617
  export interface SidebarController {
618
618
  prefs: () => AnthropicAuthTuiPrefs
619
619
  collapsed: () => boolean
@@ -717,22 +717,6 @@ export function ClaudeSidebar(props: {
717
717
  state().main?.quota != null ||
718
718
  state().main?.stateLine != null ||
719
719
  enabledFallbacks().length > 0
720
- // One label column for every expanded account, so all their bars line up.
721
- const quotaLabelWidth = () =>
722
- quotaLabelColumnWidth(
723
- [
724
- state().main?.quota,
725
- ...(prefs().sections.fallbackAccounts
726
- ? enabledFallbacks().map((fallback) => fallback.quota)
727
- : []),
728
- ],
729
- now(),
730
- )
731
-
732
- const headerLabel = () => {
733
- const name = prefs().header.label
734
- return !hasData() ? name : collapsed() ? `\u25b6 ${name}` : `\u25bc ${name}`
735
- }
736
720
  const activeAccount = () => resolveActiveAccount(state())
737
721
  const activeQuotaSummary = () =>
738
722
  getCollapsedQuotaSummary(activeAccount().quota, now())
@@ -808,217 +792,284 @@ export function ClaudeSidebar(props: {
808
792
  return `${r.transport} \u00b7 ${r.enabled ? 'on' : 'off'}`
809
793
  }
810
794
 
811
- return (
812
- <box width='100%' flexDirection='column'>
813
- {/* The collapse control stays on the left; the version stays on the right. */}
814
- {/* biome-ignore lint/a11y/noStaticElementInteractions: opentui renders to a terminal, not the DOM — ARIA roles do not apply */}
815
- <box
816
- width='100%'
817
- flexDirection='row'
818
- justifyContent='space-between'
819
- alignItems='center'
820
- onMouseDown={() => {
821
- if (hasData()) props.controller.toggleCollapsed()
822
- }}
823
- >
824
- <box paddingLeft={1} paddingRight={1} backgroundColor={theme().accent}>
825
- <text fg={theme().background}>
826
- <b>{headerLabel()}</b>
827
- </text>
828
- </box>
829
- <Show when={prefs().header.showVersion && PLUGIN_VERSION !== ''}>
830
- <text fg={theme().textMuted}>{`v${PLUGIN_VERSION}`}</text>
831
- </Show>
832
- </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' })))
833
917
 
834
- {/* Collapsed accounts keep placement order; only a fresh active row is emphasized. */}
835
- <Show when={collapsed() && hasData()}>
836
- {/* Index keys rows by position: each state update builds new account
837
- objects, so For would rebuild every row and its measured width.
838
- Passing the accessor's value as a prop keeps the row reactive. */}
839
- <Index
840
- each={getCollapsedAccounts(
841
- state(),
842
- prefs().sections.fallbackAccounts,
843
- )}
844
- >
845
- {(account) => (
846
- <CollapsedAccountRow
847
- theme={theme()}
848
- account={account()}
849
- tone={activeQuotaTone()}
850
- now={now()}
851
- />
852
- )}
853
- </Index>
854
- <Show when={state().fastMode}>
855
- <CollapsedRow theme={theme()} label='Mode'>
856
- <text fg={toneColor(theme(), 'accent')}>
857
- <b>{'fast'}</b>
858
- </text>
859
- </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()} />}
860
937
  </Show>
861
- <Show when={fableRecoverySummary()}>
862
- {(summary: () => string) => (
863
- <CollapsedRow theme={theme()} label='Recovery'>
864
- <text fg={toneColor(theme(), fableRecoveryTone())}>
865
- <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>
866
952
  </text>
867
953
  </CollapsedRow>
868
- )}
869
- </Show>
870
- </Show>
871
-
872
- {/* Expanded: full sections. Also render when there's no data so the
873
- sidebar can never go blank if data clears while collapsed. */}
874
- <Show when={!collapsed() || !hasData()}>
875
- <Show
876
- when={hasData()}
877
- 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()}>
878
969
  <box marginTop={1} width='100%'>
879
970
  <text fg={theme().textMuted}>{'Waiting for quota\u2026'}</text>
880
971
  </box>
881
- }
882
- >
883
- {/* Quota */}
884
- <Show when={prefs().sections.quota}>
885
- <SectionHeader theme={theme()} title='Quota' />
886
- <AccountBlock
887
- theme={theme()}
888
- appearance={prefs().appearance}
889
- name='main'
890
- quota={state().main?.quota}
891
- active={state().activeId === 'main'}
892
- pacingEnabled={prefs().sections.pacing}
893
- tierLabel={state().main?.tierLabel}
894
- labelWidth={quotaLabelWidth()}
895
- stateLine={state().main?.stateLine}
896
- quotaDated={state().main?.quotaDated}
897
- now={now()}
898
- />
899
- <Show when={prefs().sections.fallbackAccounts}>
900
- <For each={enabledFallbacks()}>
901
- {(fb) => (
902
- <AccountBlock
903
- theme={theme()}
904
- appearance={prefs().appearance}
905
- name={fb.label ?? fb.id}
906
- quota={fb.quota}
907
- active={state().activeId === fb.id}
908
- pacingEnabled={prefs().sections.pacing}
909
- tierLabel={fb.tierLabel}
910
- labelWidth={quotaLabelWidth()}
911
- needsReauth={fb.needsReauth}
912
- vaultReauth={fb.vaultReauth}
913
- stateLine={fb.stateLine}
914
- quotaDated={fb.quotaDated}
915
- marginTop={1}
916
- now={now()}
917
- />
918
- )}
919
- </For>
920
- </Show>
921
972
  </Show>
922
- </Show>
923
-
924
- {/* Routing */}
925
- <Show when={prefs().sections.routing}>
926
- <SectionHeader theme={theme()} title='Routing' />
927
- <StatRow
928
- theme={theme()}
929
- label='Route'
930
- value={state().route}
931
- tone='accent'
932
- />
933
- <StatRow
934
- theme={theme()}
935
- label='Mode'
936
- value={state().fastMode ? 'fast' : 'std'}
937
- tone={state().fastMode ? 'accent' : 'muted'}
938
- />
939
- <StatRow
940
- theme={theme()}
941
- label='Relay'
942
- value={relayValue()}
943
- tone={state().relay?.enabled ? 'ok' : 'muted'}
944
- />
945
- </Show>
946
973
 
947
- <Show when={fableRecoverySummary()}>
948
- {(summary: () => string) => (
974
+ {/* Routing */}
975
+ <Show when={prefs().sections.routing}>
976
+ <SectionHeader theme={theme()} title='Routing' />
949
977
  <StatRow
950
978
  theme={theme()}
951
- label='Recovery'
952
- value={summary()}
953
- tone={fableRecoveryTone()}
979
+ label='Route'
980
+ value={state().route}
981
+ tone='accent'
982
+ />
983
+ <StatRow
984
+ theme={theme()}
985
+ label='Mode'
986
+ value={state().fastMode ? 'fast' : 'std'}
987
+ tone={state().fastMode ? 'accent' : 'muted'}
954
988
  />
955
- )}
956
- </Show>
957
-
958
- {/* Cache */}
959
- <Show when={showCache()}>
960
- <SectionHeader theme={theme()} title='Cache' />
961
- <StatRow
962
- theme={theme()}
963
- label='1h cache'
964
- value={`${cacheKeep()?.window} \u00b7 ${cacheKeep()?.enabled ? 'on' : 'off'}`}
965
- tone={cacheKeep()?.enabled ? 'ok' : 'muted'}
966
- />
967
- <Show when={(cacheKeep()?.trackedSessions ?? 0) > 0}>
968
989
  <StatRow
969
990
  theme={theme()}
970
- label='Tracked'
971
- value={String(cacheKeep()?.trackedSessions)}
972
- tone='text'
991
+ label='Relay'
992
+ value={relayValue()}
993
+ tone={state().relay?.enabled ? 'ok' : 'muted'}
973
994
  />
974
995
  </Show>
975
- </Show>
976
996
 
977
- {/* Prime — only when enabled. Expanded view only (no collapsed surface). */}
978
- <Show when={showPrime()}>
979
- <StatRow
980
- theme={theme()}
981
- label='Prime'
982
- value={primeHasError() ? 'err' : 'on'}
983
- tone={primeHasError() ? 'warn' : 'ok'}
984
- />
985
- <For each={prime()?.accounts ?? []}>
986
- {(account) => {
987
- const value = () => formatPrimeAccountValue(account, now())
988
- return (
989
- <StatRow
990
- theme={theme()}
991
- label={account.label}
992
- value={value().text}
993
- tone={value().hasError ? 'warn' : 'text'}
994
- />
995
- )
996
- }}
997
- </For>
998
- </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>
999
1007
 
1000
- {/* Health — only when something is wrong */}
1001
- <Show when={degraded() && prefs().sections.health}>
1002
- <SectionHeader theme={theme()} title='Health' />
1003
- <Show when={quotaBackedOff()}>
1008
+ {/* Cache */}
1009
+ <Show when={showCache()}>
1010
+ <SectionHeader theme={theme()} title='Cache' />
1004
1011
  <StatRow
1005
1012
  theme={theme()}
1006
- label='Quota API'
1007
- value={`backoff ${formatUntil(state().main?.quotaBackoffUntil, now())}`}
1008
- tone='warn'
1013
+ label='1h cache'
1014
+ value={`${cacheKeep()?.window} \u00b7 ${cacheKeep()?.enabled ? 'on' : 'off'}`}
1015
+ tone={cacheKeep()?.enabled ? 'ok' : 'muted'}
1009
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>
1010
1025
  </Show>
1011
- <Show when={refreshBackedOff()}>
1026
+
1027
+ {/* Prime — only when enabled. Expanded view only (no collapsed surface). */}
1028
+ <Show when={showPrime()}>
1012
1029
  <StatRow
1013
1030
  theme={theme()}
1014
- label='Token refresh'
1015
- value={`backoff ${formatUntil(state().main?.refreshBackoffUntil, now())}`}
1016
- tone='warn'
1031
+ label='Prime'
1032
+ value={primeHasError() ? 'err' : 'on'}
1033
+ tone={primeHasError() ? 'warn' : 'ok'}
1017
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>
1018
1048
  </Show>
1019
- </Show>
1020
- </Show>
1021
- </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
+ />
1022
1073
  )
1023
1074
  }
1024
1075