@cortexkit/opencode-anthropic-auth 1.9.1 → 1.9.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.
package/src/tui.tsx CHANGED
@@ -1,31 +1,47 @@
1
1
  /** @jsxImportSource @opentui/solid */
2
2
 
3
+ import { readFileSync } from 'node:fs'
4
+ import { dirname, join } from 'node:path'
5
+ import { fileURLToPath } from 'node:url'
3
6
  import type {
4
7
  TuiPlugin,
5
8
  TuiPluginApi,
6
9
  TuiPluginModule,
7
10
  } from '@opencode-ai/plugin/tui'
8
- import { readFileSync } from 'node:fs'
9
- import { dirname, join } from 'node:path'
10
- import { fileURLToPath } from 'node:url'
11
- import { For, type JSX, Show, createSignal, onCleanup } from 'solid-js'
11
+ import {
12
+ createEffect,
13
+ createSignal,
14
+ For,
15
+ type JSX,
16
+ onCleanup,
17
+ Show,
18
+ } from 'solid-js'
12
19
 
13
20
  import {
14
21
  type AccountQuota,
22
+ computeQuotaPacing,
15
23
  DEFAULT_SIDEBAR_STATE,
16
- type SidebarState,
24
+ FIVE_HOUR_MS,
17
25
  getCollapsedQuotaSummary,
18
26
  getSidebarState,
27
+ type QuotaPacing,
19
28
  resolveActiveAccount,
29
+ SEVEN_DAY_MS,
30
+ type SidebarState,
20
31
  } from './sidebar-state.js'
21
-
22
- const POLL_MS = 1500
23
- const REFRESH_DEBOUNCE_MS = 200
32
+ import {
33
+ type AnthropicAuthTuiPrefs,
34
+ type AppearancePrefs,
35
+ computeEffectiveOrder,
36
+ DEFAULT_SLOT_ORDER,
37
+ PLUGIN_KEY,
38
+ queueTuiPreferenceUpdate,
39
+ readTuiPreferencesFile,
40
+ resolveAnthropicAuthPrefs,
41
+ watchTuiPreferences,
42
+ } from './tui-preferences.js'
24
43
 
25
44
  const ID = 'cortexkit.anthropic-auth'
26
- const BAR_WIDTH = 10
27
- const BAR_FILLED = '\u2588'
28
- const BAR_EMPTY = '\u2591'
29
45
 
30
46
  // Plugin version for the header (mirrors the Magic Context / AFT convention).
31
47
  // Read at runtime from package.json relative to this module — NOT a TS JSON
@@ -70,18 +86,61 @@ function toneColor(theme: ThemeCurrent, tone: Tone): ThemeColor {
70
86
  }
71
87
  }
72
88
 
73
- function usageTone(usedPct: number): Tone {
74
- if (usedPct < 50) return 'ok'
75
- if (usedPct < 80) return 'warn'
89
+ function usageTone(usedPct: number, appearance: AppearancePrefs): Tone {
90
+ if (usedPct < appearance.warnThreshold) return 'ok'
91
+ if (usedPct < appearance.errorThreshold) return 'warn'
76
92
  return 'err'
77
93
  }
78
94
 
79
- function quotaBar(usedPct: number, width = BAR_WIDTH): string {
80
- const filled = Math.max(
81
- 0,
82
- Math.min(Math.round((usedPct / 100) * width), width),
83
- )
84
- return BAR_FILLED.repeat(filled) + BAR_EMPTY.repeat(width - filled)
95
+ interface BarSegment {
96
+ text: string
97
+ tone: Tone
98
+ }
99
+
100
+ const PACE_RESERVE_CHAR = '\u2592'
101
+ const PACE_DEFICIT_CHAR = '\u2593'
102
+
103
+ // Variant C pacing bar: normal fill up to min(used, pace), then a pace
104
+ // segment covering the gap — headroom being banked (reserve, ok tone) or the
105
+ // overshoot itself (deficit, err tone) — then empty cells. Without pacing the
106
+ // bar renders as a single fill+empty pair, identical to the pre-pacing look.
107
+ // Empty text nodes still occupy a phantom cell in the opentui flex row, so
108
+ // zero-length segments are dropped from every return path (plain and pacing).
109
+ function quotaBarSegments(
110
+ usedPct: number,
111
+ appearance: AppearancePrefs,
112
+ pacing: QuotaPacing | null,
113
+ ): BarSegment[] {
114
+ const width = appearance.barWidth
115
+ const cells = (pct: number) =>
116
+ Math.max(0, Math.min(Math.round((pct / 100) * width), width))
117
+ const usedCells = cells(usedPct)
118
+ const fillTone = usageTone(usedPct, appearance)
119
+ const plain: BarSegment[] = [
120
+ { text: appearance.barFilledChar.repeat(usedCells), tone: fillTone },
121
+ {
122
+ text: appearance.barEmptyChar.repeat(width - usedCells),
123
+ tone: fillTone,
124
+ },
125
+ ]
126
+ if (!pacing) return plain.filter((segment) => segment.text.length > 0)
127
+ const paceCells = cells(pacing.pacePercent)
128
+ const lo = Math.min(usedCells, paceCells)
129
+ const hi = Math.max(usedCells, paceCells)
130
+ if (hi === lo) return plain.filter((segment) => segment.text.length > 0)
131
+ const overspent = usedCells > paceCells
132
+ return (
133
+ [
134
+ { text: appearance.barFilledChar.repeat(lo), tone: fillTone },
135
+ {
136
+ text: (overspent ? PACE_DEFICIT_CHAR : PACE_RESERVE_CHAR).repeat(
137
+ hi - lo,
138
+ ),
139
+ tone: overspent ? 'err' : 'ok',
140
+ },
141
+ { text: appearance.barEmptyChar.repeat(width - hi), tone: fillTone },
142
+ ] as BarSegment[]
143
+ ).filter((segment) => segment.text.length > 0)
85
144
  }
86
145
 
87
146
  function formatResetIn(resetsAt: string | undefined): string {
@@ -91,8 +150,13 @@ function formatResetIn(resetsAt: string | undefined): string {
91
150
  const mins = Math.floor(ms / 60_000)
92
151
  if (mins < 60) return `${mins}m`
93
152
  const hrs = Math.floor(mins / 60)
94
- const rm = mins % 60
95
- return rm > 0 ? `${hrs}h${rm}m` : `${hrs}h`
153
+ if (hrs < 24) {
154
+ const rm = mins % 60
155
+ return rm > 0 ? `${hrs}h${rm}m` : `${hrs}h`
156
+ }
157
+ const days = Math.floor(hrs / 24)
158
+ const rh = hrs % 24
159
+ return rh > 0 ? `${days}d${rh}h` : `${days}d`
96
160
  }
97
161
 
98
162
  function formatUntil(until: number | undefined): string {
@@ -154,14 +218,27 @@ function CollapsedRow(props: {
154
218
  }
155
219
 
156
220
  // Quota window row: muted label left, tone-colored bar + percentage right,
157
- // with an optional muted reset suffix.
221
+ // with an optional muted reset suffix. When pacing data is present, the bar
222
+ // gains a pace segment and an off-pace window adds a muted subline with the
223
+ // reserve/deficit delta and the projected runout.
158
224
  function QuotaRow(props: {
159
225
  theme: ThemeCurrent
226
+ appearance: AppearancePrefs
160
227
  label: string
161
228
  window: { usedPercent: number; resetsAt?: string } | undefined
229
+ pacing: QuotaPacing | null
162
230
  }) {
163
231
  const used = () => props.window?.usedPercent ?? 0
164
232
  const reset = () => formatResetIn(props.window?.resetsAt)
233
+ const paceLine = () => {
234
+ const pacing = props.pacing
235
+ if (!pacing || pacing.state === 'on-pace') return null
236
+ const pct = Math.round(Math.abs(pacing.deltaPercent))
237
+ if (pacing.state === 'reserve') return `reserve ${pct}% \u00b7 lasts`
238
+ return pacing.runsOutAt
239
+ ? `deficit ${pct}% \u00b7 out in ${formatResetIn(pacing.runsOutAt)}`
240
+ : `deficit ${pct}% \u00b7 lasts`
241
+ }
165
242
  return (
166
243
  <Show
167
244
  when={props.window}
@@ -178,10 +255,16 @@ function QuotaRow(props: {
178
255
  <box width='100%' flexDirection='row' justifyContent='space-between'>
179
256
  <box flexDirection='row'>
180
257
  <text fg={props.theme.textMuted}>{props.label.padEnd(3)}</text>
181
- <text fg={toneColor(props.theme, usageTone(used()))}>
182
- {quotaBar(used())}
183
- </text>
184
- <text fg={toneColor(props.theme, usageTone(used()))}>
258
+ <For each={quotaBarSegments(used(), props.appearance, props.pacing)}>
259
+ {(segment) => (
260
+ <text fg={toneColor(props.theme, segment.tone)}>
261
+ {segment.text}
262
+ </text>
263
+ )}
264
+ </For>
265
+ <text
266
+ fg={toneColor(props.theme, usageTone(used(), props.appearance))}
267
+ >
185
268
  {` ${String(Math.round(used())).padStart(3)}%`}
186
269
  </text>
187
270
  </box>
@@ -189,20 +272,46 @@ function QuotaRow(props: {
189
272
  <text fg={props.theme.textMuted}>{reset()}</text>
190
273
  </Show>
191
274
  </box>
275
+ <Show when={paceLine()}>
276
+ <box width='100%' flexDirection='row'>
277
+ <text fg={props.theme.textMuted}>{' '}</text>
278
+ <text
279
+ fg={toneColor(
280
+ props.theme,
281
+ props.pacing?.state === 'deficit' ? 'warn' : 'muted',
282
+ )}
283
+ >
284
+ {paceLine()}
285
+ </text>
286
+ </box>
287
+ </Show>
192
288
  </Show>
193
289
  )
194
290
  }
195
291
 
196
292
  // Account block: header row (name + status word) then per-window quota rows.
293
+ // Pacing is computed here — per window, against the wall clock at render time
294
+ // (re-evaluated on every state poll) — and disabled by passing false.
197
295
  function AccountBlock(props: {
198
296
  theme: ThemeCurrent
297
+ appearance: AppearancePrefs
199
298
  name: string
200
299
  quota: AccountQuota | null
201
300
  active: boolean
301
+ pacingEnabled: boolean
202
302
  marginTop?: number
203
303
  }) {
204
304
  const statusWord = () => (props.active ? 'active' : 'idle')
205
305
  const statusTone = (): Tone => (props.active ? 'ok' : 'muted')
306
+ const pacingFor = (
307
+ window:
308
+ | { usedPercent: number; remainingPercent: number; resetsAt?: string }
309
+ | undefined,
310
+ windowMs: number,
311
+ ) =>
312
+ props.pacingEnabled && window
313
+ ? computeQuotaPacing(window, windowMs, Date.now())
314
+ : null
206
315
  return (
207
316
  <box width='100%' flexDirection='column' marginTop={props.marginTop ?? 0}>
208
317
  <box width='100%' flexDirection='row' justifyContent='space-between'>
@@ -219,13 +328,17 @@ function AccountBlock(props: {
219
328
  >
220
329
  <QuotaRow
221
330
  theme={props.theme}
331
+ appearance={props.appearance}
222
332
  label='5h'
223
333
  window={props.quota?.five_hour}
334
+ pacing={pacingFor(props.quota?.five_hour, FIVE_HOUR_MS)}
224
335
  />
225
336
  <QuotaRow
226
337
  theme={props.theme}
338
+ appearance={props.appearance}
227
339
  label='7d'
228
340
  window={props.quota?.seven_day}
341
+ pacing={pacingFor(props.quota?.seven_day, SEVEN_DAY_MS)}
229
342
  />
230
343
  </Show>
231
344
  </box>
@@ -238,9 +351,77 @@ async function readStateFromFile(): Promise<SidebarState> {
238
351
  return getSidebarState()
239
352
  }
240
353
 
241
- function QuotaSidebar(props: { api: TuiPluginApi }) {
354
+ interface SidebarController {
355
+ prefs: () => AnthropicAuthTuiPrefs
356
+ collapsed: () => boolean
357
+ toggleCollapsed: () => void
358
+ }
359
+
360
+ // The TUI may unmount and remount sidebar_content when the user switches
361
+ // views (e.g. main -> subagent -> main). A remount re-runs the component
362
+ // body, so any signal created inside the component would reset to its
363
+ // seed. The controller lives in the plugin closure (process lifetime)
364
+ // and owns the durable prefs/collapse signals plus the single shared
365
+ // watcher subscription, so collapse and live pref reloads survive the
366
+ // remount. No effects or memos here — those need a Solid owner, so the
367
+ // poll-interval createEffect stays inside the component.
368
+ function createSidebarController(
369
+ initialPrefs: AnthropicAuthTuiPrefs,
370
+ ): SidebarController {
371
+ const [prefs, setPrefs] = createSignal<AnthropicAuthTuiPrefs>(initialPrefs)
372
+ const seedCollapsed =
373
+ initialPrefs.rememberCollapsed && initialPrefs.collapsed != null
374
+ ? initialPrefs.collapsed
375
+ : initialPrefs.startCollapsed
376
+ const [collapsed, setCollapsed] = createSignal(seedCollapsed)
377
+ let lastPersistedCollapsed: boolean | null = initialPrefs.collapsed
378
+ let lastApplied = JSON.stringify(initialPrefs)
379
+
380
+ // The watcher lives for the plugin/process lifetime — it is intentionally
381
+ // never disposed. Collapse guard mirrors the race-fix in toggleCollapsed:
382
+ // lastPersistedCollapsed is advanced only once our own write lands, so
383
+ // watcher echoes of the previous persisted value are rejected by the
384
+ // `!==` check and cannot revert a user click.
385
+ watchTuiPreferences(() => {
386
+ void (async () => {
387
+ const next = resolveAnthropicAuthPrefs(await readTuiPreferencesFile())
388
+ const serialized = JSON.stringify(next)
389
+ if (serialized === lastApplied) return
390
+ lastApplied = serialized
391
+ setPrefs(next)
392
+ if (
393
+ next.rememberCollapsed &&
394
+ next.collapsed != null &&
395
+ next.collapsed !== lastPersistedCollapsed
396
+ ) {
397
+ lastPersistedCollapsed = next.collapsed
398
+ setCollapsed(next.collapsed)
399
+ }
400
+ })()
401
+ })
402
+
403
+ function toggleCollapsed() {
404
+ const next = !collapsed()
405
+ setCollapsed(next)
406
+ if (prefs().rememberCollapsed) {
407
+ void queueTuiPreferenceUpdate(PLUGIN_KEY, ['collapsed'], next).then(
408
+ () => {
409
+ lastPersistedCollapsed = next
410
+ },
411
+ )
412
+ }
413
+ }
414
+
415
+ return { prefs, collapsed, toggleCollapsed }
416
+ }
417
+
418
+ function QuotaSidebar(props: {
419
+ api: TuiPluginApi
420
+ controller: SidebarController
421
+ }) {
422
+ const prefs = props.controller.prefs
423
+ const collapsed = props.controller.collapsed
242
424
  const [state, setState] = createSignal<SidebarState>(DEFAULT_SIDEBAR_STATE)
243
- const [collapsed, setCollapsed] = createSignal(false)
244
425
  let lastUpdated = 0
245
426
  let debounce: ReturnType<typeof setTimeout> | null = null
246
427
 
@@ -257,19 +438,23 @@ function QuotaSidebar(props: { api: TuiPluginApi }) {
257
438
  debounce = setTimeout(() => {
258
439
  debounce = null
259
440
  void refresh()
260
- }, REFRESH_DEBOUNCE_MS)
441
+ }, prefs().refreshDebounceMs)
261
442
  }
262
443
 
263
444
  // Background poller (server and TUI run as separate module instances, so we
264
445
  // sync through the state file) plus event-driven refreshes for low latency.
265
- const timer = setInterval(refresh, POLL_MS)
446
+ // The interval rebuilds whenever pollMs changes.
447
+ createEffect(() => {
448
+ const timer = setInterval(refresh, prefs().pollMs)
449
+ onCleanup(() => clearInterval(timer))
450
+ })
266
451
  const unsubs = [
267
452
  props.api.event.on('session.updated', scheduleRefresh),
268
453
  props.api.event.on('message.updated', scheduleRefresh),
269
454
  ]
270
455
  setTimeout(refresh, 300)
456
+
271
457
  onCleanup(() => {
272
- clearInterval(timer)
273
458
  if (debounce) clearTimeout(debounce)
274
459
  for (const u of unsubs) u()
275
460
  })
@@ -279,18 +464,42 @@ function QuotaSidebar(props: { api: TuiPluginApi }) {
279
464
  const hasData = () =>
280
465
  state().main.quota != null || enabledFallbacks().length > 0
281
466
 
282
- const headerLabel = () =>
283
- !hasData() ? 'CLAUDE' : collapsed() ? '\u25b6 CLAUDE' : '\u25bc CLAUDE'
467
+ const headerLabel = () => {
468
+ const name = prefs().header.label
469
+ return !hasData() ? name : collapsed() ? `\u25b6 ${name}` : `\u25bc ${name}`
470
+ }
284
471
  const activeAccount = () => resolveActiveAccount(state())
285
472
  const activeQuotaSummary = () =>
286
473
  getCollapsedQuotaSummary(activeAccount().quota)
474
+ const activePacingDeficit = () => {
475
+ if (!prefs().sections.pacing) return false
476
+ const quota = activeAccount().quota
477
+ if (!quota) return false
478
+ const now = Date.now()
479
+ const windows: Array<[typeof quota.five_hour, number]> = [
480
+ [quota.five_hour, FIVE_HOUR_MS],
481
+ [quota.seven_day, SEVEN_DAY_MS],
482
+ ]
483
+ return windows.some(
484
+ ([w, ms]) =>
485
+ w != null && computeQuotaPacing(w, ms, now)?.state === 'deficit',
486
+ )
487
+ }
287
488
  const activeQuotaTone = (): Tone => {
288
489
  const summary = activeQuotaSummary()
289
490
  const values = [
290
491
  summary.fiveHourUsedPercent,
291
492
  summary.sevenDayUsedPercent,
292
493
  ].filter((value): value is number => value != null)
293
- return values.length > 0 ? usageTone(Math.max(...values)) : 'muted'
494
+ // Pacing deficit is an advisory projection, not actual quota exhaustion,
495
+ // so it can only BUMP the usage tone up to warn at most — never soften a
496
+ // real warn/err usage reading and never paint a true red.
497
+ const base: Tone =
498
+ values.length > 0
499
+ ? usageTone(Math.max(...values), prefs().appearance)
500
+ : 'muted'
501
+ if (!activePacingDeficit()) return base
502
+ return base === 'ok' || base === 'muted' ? 'warn' : base
294
503
  }
295
504
 
296
505
  const quotaBackedOff = () => state().main.quotaBackedOff === true
@@ -298,7 +507,8 @@ function QuotaSidebar(props: { api: TuiPluginApi }) {
298
507
  const degraded = () => quotaBackedOff() || refreshBackedOff()
299
508
 
300
509
  const cacheKeep = () => state().cacheKeep
301
- const showCache = () => cacheKeep() != null && cacheKeep()?.window != null
510
+ const showCache = () =>
511
+ prefs().sections.cache && cacheKeep() != null && cacheKeep()?.window != null
302
512
  const relayValue = () => {
303
513
  const r = state().relay
304
514
  if (!r) return '\u2014'
@@ -324,7 +534,7 @@ function QuotaSidebar(props: { api: TuiPluginApi }) {
324
534
  justifyContent='space-between'
325
535
  alignItems='center'
326
536
  onMouseDown={() => {
327
- if (hasData()) setCollapsed((value) => !value)
537
+ if (hasData()) props.controller.toggleCollapsed()
328
538
  }}
329
539
  >
330
540
  <box paddingLeft={1} paddingRight={1} backgroundColor={theme().accent}>
@@ -335,7 +545,7 @@ function QuotaSidebar(props: { api: TuiPluginApi }) {
335
545
  <Show
336
546
  when={degraded()}
337
547
  fallback={
338
- <Show when={PLUGIN_VERSION !== ''}>
548
+ <Show when={prefs().header.showVersion && PLUGIN_VERSION !== ''}>
339
549
  <text fg={theme().textMuted}>{`v${PLUGIN_VERSION}`}</text>
340
550
  </Show>
341
551
  }
@@ -385,46 +595,56 @@ function QuotaSidebar(props: { api: TuiPluginApi }) {
385
595
  }
386
596
  >
387
597
  {/* Quota */}
388
- <SectionHeader theme={theme()} title='Quota' />
389
- <AccountBlock
390
- theme={theme()}
391
- name='main'
392
- quota={state().main.quota}
393
- active={state().activeId === 'main'}
394
- />
395
- <For each={enabledFallbacks()}>
396
- {(fb) => (
397
- <AccountBlock
398
- theme={theme()}
399
- name={fb.label ?? fb.id}
400
- quota={fb.quota}
401
- active={state().activeId === fb.id}
402
- marginTop={1}
403
- />
404
- )}
405
- </For>
598
+ <Show when={prefs().sections.quota}>
599
+ <SectionHeader theme={theme()} title='Quota' />
600
+ <AccountBlock
601
+ theme={theme()}
602
+ appearance={prefs().appearance}
603
+ name='main'
604
+ quota={state().main.quota}
605
+ active={state().activeId === 'main'}
606
+ pacingEnabled={prefs().sections.pacing}
607
+ />
608
+ <Show when={prefs().sections.fallbackAccounts}>
609
+ <For each={enabledFallbacks()}>
610
+ {(fb) => (
611
+ <AccountBlock
612
+ theme={theme()}
613
+ appearance={prefs().appearance}
614
+ name={fb.label ?? fb.id}
615
+ quota={fb.quota}
616
+ active={state().activeId === fb.id}
617
+ pacingEnabled={prefs().sections.pacing}
618
+ marginTop={1}
619
+ />
620
+ )}
621
+ </For>
622
+ </Show>
623
+ </Show>
406
624
  </Show>
407
625
 
408
626
  {/* Routing */}
409
- <SectionHeader theme={theme()} title='Routing' />
410
- <StatRow
411
- theme={theme()}
412
- label='Route'
413
- value={state().route}
414
- tone='accent'
415
- />
416
- <StatRow
417
- theme={theme()}
418
- label='Mode'
419
- value={state().fastMode ? 'fast' : 'std'}
420
- tone={state().fastMode ? 'accent' : 'muted'}
421
- />
422
- <StatRow
423
- theme={theme()}
424
- label='Relay'
425
- value={relayValue()}
426
- tone={state().relay?.enabled ? 'ok' : 'muted'}
427
- />
627
+ <Show when={prefs().sections.routing}>
628
+ <SectionHeader theme={theme()} title='Routing' />
629
+ <StatRow
630
+ theme={theme()}
631
+ label='Route'
632
+ value={state().route}
633
+ tone='accent'
634
+ />
635
+ <StatRow
636
+ theme={theme()}
637
+ label='Mode'
638
+ value={state().fastMode ? 'fast' : 'std'}
639
+ tone={state().fastMode ? 'accent' : 'muted'}
640
+ />
641
+ <StatRow
642
+ theme={theme()}
643
+ label='Relay'
644
+ value={relayValue()}
645
+ tone={state().relay?.enabled ? 'ok' : 'muted'}
646
+ />
647
+ </Show>
428
648
 
429
649
  {/* Cache */}
430
650
  <Show when={showCache()}>
@@ -446,7 +666,7 @@ function QuotaSidebar(props: { api: TuiPluginApi }) {
446
666
  </Show>
447
667
 
448
668
  {/* Health — only when something is wrong */}
449
- <Show when={degraded()}>
669
+ <Show when={degraded() && prefs().sections.health}>
450
670
  <SectionHeader theme={theme()} title='Health' />
451
671
  <Show when={quotaBackedOff()}>
452
672
  <StatRow
@@ -471,11 +691,13 @@ function QuotaSidebar(props: { api: TuiPluginApi }) {
471
691
  }
472
692
 
473
693
  const tui: TuiPlugin = async (api) => {
694
+ const root = await readTuiPreferencesFile()
695
+ const controller = createSidebarController(resolveAnthropicAuthPrefs(root))
474
696
  api.slots.register({
475
- order: 160,
697
+ order: computeEffectiveOrder(root, PLUGIN_KEY, DEFAULT_SLOT_ORDER),
476
698
  slots: {
477
699
  sidebar_content(_ctx: unknown, _props: { session_id: string }) {
478
- return <QuotaSidebar api={api} />
700
+ return <QuotaSidebar api={api} controller={controller} />
479
701
  },
480
702
  },
481
703
  })
@@ -1,2 +0,0 @@
1
- import{mkdir as O,readFile as Q,writeFile as W}from"node:fs/promises";import{tmpdir as X}from"node:os";import{dirname as Y,join as K}from"node:path";var Z="OPENCODE_ANTHROPIC_AUTH_SIDEBAR_STATE_FILE",$=K(X(),"opencode-anthropic-auth"),x=K($,"sidebar-state.json");function M(){return process.env[Z]||x}var B={main:{quota:null},fallbacks:[],activeId:void 0,route:"main",relay:null,fastMode:!1,lastUpdated:0};async function h(){try{let G=await Q(M(),"utf8");return JSON.parse(G)}catch{return B}}async function j(G){try{let z=M();await O(Y(z),{recursive:!0}),await W(z,JSON.stringify(G),"utf8")}catch{}}function w(G){let z=G.activeId;if(z&&z!=="main"){let C=G.fallbacks.find((J)=>J.enabled&&J.id===z);if(C)return{id:C.id,name:C.label??C.id,quota:C.quota}}return{id:"main",name:"main",quota:G.main.quota}}function R(G){let z=G?.five_hour?.usedPercent??null,C=G?.seven_day?.usedPercent??null;if(z==null&&C==null)return{fiveHourUsedPercent:z,sevenDayUsedPercent:C,text:null};return{fiveHourUsedPercent:z,sevenDayUsedPercent:C,text:`5h: ${z==null?"—":`${Math.round(z)}%`} 7d: ${C==null?"—":`${Math.round(C)}%`}`}}
2
- export{M as Sa,B as Ta,h as Ua,j as Va,w as Wa,R as Xa};