@cortexkit/opencode-anthropic-auth 1.4.1 → 1.6.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.
- package/README.md +54 -4
- package/dist/cli.js +1 -1
- package/dist/index-q0d64q8d.js +2 -0
- package/dist/index-zx0zb5c8.js +335 -0
- package/dist/index.js +7 -3
- package/dist/sidebar-state.d.ts +47 -0
- package/dist/sidebar-state.js +1 -0
- package/dist/tui.d.ts +6 -0
- package/package.json +16 -3
- package/src/sidebar-state.ts +107 -0
- package/src/tui.tsx +497 -0
- package/dist/index-wc8ekdxb.js +0 -325
package/src/tui.tsx
ADDED
|
@@ -0,0 +1,497 @@
|
|
|
1
|
+
/** @jsxImportSource @opentui/solid */
|
|
2
|
+
|
|
3
|
+
import { readFileSync } from 'node:fs'
|
|
4
|
+
import { dirname, join } from 'node:path'
|
|
5
|
+
import { fileURLToPath } from 'node:url'
|
|
6
|
+
import type {
|
|
7
|
+
TuiPlugin,
|
|
8
|
+
TuiPluginApi,
|
|
9
|
+
TuiPluginModule,
|
|
10
|
+
} from '@opencode-ai/plugin/tui'
|
|
11
|
+
import { For, type JSX, Show, createSignal, onCleanup } from 'solid-js'
|
|
12
|
+
|
|
13
|
+
import {
|
|
14
|
+
type AccountQuota,
|
|
15
|
+
DEFAULT_SIDEBAR_STATE,
|
|
16
|
+
type SidebarState,
|
|
17
|
+
getSidebarState,
|
|
18
|
+
resolveActiveAccount,
|
|
19
|
+
} from './sidebar-state.js'
|
|
20
|
+
|
|
21
|
+
const POLL_MS = 1500
|
|
22
|
+
const REFRESH_DEBOUNCE_MS = 200
|
|
23
|
+
|
|
24
|
+
const ID = 'cortexkit.anthropic-auth'
|
|
25
|
+
const BAR_WIDTH = 10
|
|
26
|
+
const BAR_FILLED = '\u2588'
|
|
27
|
+
const BAR_EMPTY = '\u2591'
|
|
28
|
+
|
|
29
|
+
// Plugin version for the header (mirrors the Magic Context / AFT convention).
|
|
30
|
+
// Read at runtime from package.json relative to this module — NOT a TS JSON
|
|
31
|
+
// import, which would break the declaration build (package.json is outside
|
|
32
|
+
// rootDir). tui.tsx ships as source (package.json exports["./tui"] →
|
|
33
|
+
// "./src/tui.tsx") and is never compiled into a deeper dist tree, so `..` from
|
|
34
|
+
// src/ is always the package root. Empty string on any failure → header shows
|
|
35
|
+
// the badge with no version.
|
|
36
|
+
const PLUGIN_VERSION: string = (() => {
|
|
37
|
+
try {
|
|
38
|
+
const here = dirname(fileURLToPath(import.meta.url))
|
|
39
|
+
const raw = readFileSync(join(here, '..', 'package.json'), 'utf8')
|
|
40
|
+
return (JSON.parse(raw) as { version?: string }).version ?? ''
|
|
41
|
+
} catch {
|
|
42
|
+
return ''
|
|
43
|
+
}
|
|
44
|
+
})()
|
|
45
|
+
|
|
46
|
+
// biome-ignore lint/suspicious/noExplicitAny: opentui border prop not typed in plugin tui surface
|
|
47
|
+
const SINGLE_BORDER = { type: 'single' } as any
|
|
48
|
+
|
|
49
|
+
type Tone = 'ok' | 'warn' | 'err' | 'muted' | 'accent' | 'text'
|
|
50
|
+
type ThemeCurrent = TuiPluginApi['theme']['current']
|
|
51
|
+
type ThemeColor = ThemeCurrent['text']
|
|
52
|
+
|
|
53
|
+
// Tone -> theme token. Theme tokens only — never hardcoded colors. Fallbacks
|
|
54
|
+
// resolve to other theme tokens so the sidebar always tracks the active theme.
|
|
55
|
+
function toneColor(theme: ThemeCurrent, tone: Tone): ThemeColor {
|
|
56
|
+
switch (tone) {
|
|
57
|
+
case 'ok':
|
|
58
|
+
return theme.success ?? theme.accent
|
|
59
|
+
case 'warn':
|
|
60
|
+
return theme.warning ?? theme.accent
|
|
61
|
+
case 'err':
|
|
62
|
+
return theme.error ?? theme.accent
|
|
63
|
+
case 'muted':
|
|
64
|
+
return theme.textMuted ?? theme.text
|
|
65
|
+
case 'accent':
|
|
66
|
+
return theme.accent ?? theme.text
|
|
67
|
+
default:
|
|
68
|
+
return theme.text
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function usageTone(usedPct: number): Tone {
|
|
73
|
+
if (usedPct < 50) return 'ok'
|
|
74
|
+
if (usedPct < 80) return 'warn'
|
|
75
|
+
return 'err'
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function quotaBar(usedPct: number, width = BAR_WIDTH): string {
|
|
79
|
+
const filled = Math.max(
|
|
80
|
+
0,
|
|
81
|
+
Math.min(Math.round((usedPct / 100) * width), width),
|
|
82
|
+
)
|
|
83
|
+
return BAR_FILLED.repeat(filled) + BAR_EMPTY.repeat(width - filled)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function formatResetIn(resetsAt: string | undefined): string {
|
|
87
|
+
if (!resetsAt) return ''
|
|
88
|
+
const ms = new Date(resetsAt).getTime() - Date.now()
|
|
89
|
+
if (ms <= 0) return 'now'
|
|
90
|
+
const mins = Math.floor(ms / 60_000)
|
|
91
|
+
if (mins < 60) return `${mins}m`
|
|
92
|
+
const hrs = Math.floor(mins / 60)
|
|
93
|
+
const rm = mins % 60
|
|
94
|
+
return rm > 0 ? `${hrs}h${rm}m` : `${hrs}h`
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function formatUntil(until: number | undefined): string {
|
|
98
|
+
if (!until) return ''
|
|
99
|
+
const ms = until - Date.now()
|
|
100
|
+
if (ms <= 0) return 'now'
|
|
101
|
+
const mins = Math.ceil(ms / 60_000)
|
|
102
|
+
if (mins < 60) return `${mins}m`
|
|
103
|
+
const hrs = Math.floor(mins / 60)
|
|
104
|
+
const rm = mins % 60
|
|
105
|
+
return rm > 0 ? `${hrs}h${rm}m` : `${hrs}h`
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// --- Reusable components (aft-style) ---------------------------------------
|
|
109
|
+
|
|
110
|
+
function SectionHeader(props: {
|
|
111
|
+
theme: ThemeCurrent
|
|
112
|
+
title: string
|
|
113
|
+
marginTop?: number
|
|
114
|
+
}) {
|
|
115
|
+
return (
|
|
116
|
+
<box width='100%' marginTop={props.marginTop ?? 1}>
|
|
117
|
+
<text fg={props.theme.text}>
|
|
118
|
+
<b>{props.title}</b>
|
|
119
|
+
</text>
|
|
120
|
+
</box>
|
|
121
|
+
)
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function StatRow(props: {
|
|
125
|
+
theme: ThemeCurrent
|
|
126
|
+
label: string
|
|
127
|
+
value: string
|
|
128
|
+
tone?: Tone
|
|
129
|
+
}) {
|
|
130
|
+
return (
|
|
131
|
+
<box width='100%' flexDirection='row' justifyContent='space-between'>
|
|
132
|
+
<text fg={props.theme.textMuted}>{props.label}</text>
|
|
133
|
+
<text fg={toneColor(props.theme, props.tone ?? 'text')}>
|
|
134
|
+
<b>{props.value}</b>
|
|
135
|
+
</text>
|
|
136
|
+
</box>
|
|
137
|
+
)
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Compact row for the collapsed view: muted label left, caller-provided value
|
|
141
|
+
// right. Mirrors StatRow's layout so columns line up.
|
|
142
|
+
function CollapsedRow(props: {
|
|
143
|
+
theme: ThemeCurrent
|
|
144
|
+
label: string
|
|
145
|
+
children: JSX.Element
|
|
146
|
+
}) {
|
|
147
|
+
return (
|
|
148
|
+
<box width='100%' flexDirection='row' justifyContent='space-between'>
|
|
149
|
+
<text fg={props.theme.textMuted}>{props.label}</text>
|
|
150
|
+
{props.children}
|
|
151
|
+
</box>
|
|
152
|
+
)
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Quota window row: muted label left, tone-colored bar + percentage right,
|
|
156
|
+
// with an optional muted reset suffix.
|
|
157
|
+
function QuotaRow(props: {
|
|
158
|
+
theme: ThemeCurrent
|
|
159
|
+
label: string
|
|
160
|
+
window: { usedPercent: number; resetsAt?: string } | undefined
|
|
161
|
+
}) {
|
|
162
|
+
const used = () => props.window?.usedPercent ?? 0
|
|
163
|
+
const reset = () => formatResetIn(props.window?.resetsAt)
|
|
164
|
+
return (
|
|
165
|
+
<Show
|
|
166
|
+
when={props.window}
|
|
167
|
+
fallback={
|
|
168
|
+
<box width='100%' flexDirection='row'>
|
|
169
|
+
<text fg={props.theme.textMuted}>{props.label.padEnd(3)}</text>
|
|
170
|
+
<text fg={props.theme.textMuted}>{'\u2014'}</text>
|
|
171
|
+
</box>
|
|
172
|
+
}
|
|
173
|
+
>
|
|
174
|
+
{/* Left group (label · bar · pct) stays left-aligned in fixed columns so
|
|
175
|
+
bars and percentages line up across rows; the reset time is pushed to
|
|
176
|
+
the right edge so reset times align in their own right column. */}
|
|
177
|
+
<box width='100%' flexDirection='row' justifyContent='space-between'>
|
|
178
|
+
<box flexDirection='row'>
|
|
179
|
+
<text fg={props.theme.textMuted}>{props.label.padEnd(3)}</text>
|
|
180
|
+
<text fg={toneColor(props.theme, usageTone(used()))}>
|
|
181
|
+
{quotaBar(used())}
|
|
182
|
+
</text>
|
|
183
|
+
<text fg={toneColor(props.theme, usageTone(used()))}>
|
|
184
|
+
{` ${String(Math.round(used())).padStart(3)}%`}
|
|
185
|
+
</text>
|
|
186
|
+
</box>
|
|
187
|
+
<Show when={reset()}>
|
|
188
|
+
<text fg={props.theme.textMuted}>{reset()}</text>
|
|
189
|
+
</Show>
|
|
190
|
+
</box>
|
|
191
|
+
</Show>
|
|
192
|
+
)
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Account block: header row (name + status word) then per-window quota rows.
|
|
196
|
+
function AccountBlock(props: {
|
|
197
|
+
theme: ThemeCurrent
|
|
198
|
+
name: string
|
|
199
|
+
quota: AccountQuota | null
|
|
200
|
+
active: boolean
|
|
201
|
+
marginTop?: number
|
|
202
|
+
}) {
|
|
203
|
+
const statusWord = () => (props.active ? 'active' : 'idle')
|
|
204
|
+
const statusTone = (): Tone => (props.active ? 'ok' : 'muted')
|
|
205
|
+
return (
|
|
206
|
+
<box width='100%' flexDirection='column' marginTop={props.marginTop ?? 0}>
|
|
207
|
+
<box width='100%' flexDirection='row' justifyContent='space-between'>
|
|
208
|
+
<text fg={props.theme.text}>
|
|
209
|
+
<b>{props.name}</b>
|
|
210
|
+
</text>
|
|
211
|
+
<text fg={toneColor(props.theme, statusTone())}>
|
|
212
|
+
<b>{statusWord()}</b>
|
|
213
|
+
</text>
|
|
214
|
+
</box>
|
|
215
|
+
<Show
|
|
216
|
+
when={props.quota}
|
|
217
|
+
fallback={<text fg={props.theme.textMuted}>{'checking\u2026'}</text>}
|
|
218
|
+
>
|
|
219
|
+
<QuotaRow
|
|
220
|
+
theme={props.theme}
|
|
221
|
+
label='5h'
|
|
222
|
+
window={props.quota?.five_hour}
|
|
223
|
+
/>
|
|
224
|
+
<QuotaRow
|
|
225
|
+
theme={props.theme}
|
|
226
|
+
label='7d'
|
|
227
|
+
window={props.quota?.seven_day}
|
|
228
|
+
/>
|
|
229
|
+
</Show>
|
|
230
|
+
</box>
|
|
231
|
+
)
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// --- State plumbing ---------------------------------------------------------
|
|
235
|
+
|
|
236
|
+
async function readStateFromFile(): Promise<SidebarState> {
|
|
237
|
+
return getSidebarState()
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function QuotaSidebar(props: { api: TuiPluginApi }) {
|
|
241
|
+
const [state, setState] = createSignal<SidebarState>(DEFAULT_SIDEBAR_STATE)
|
|
242
|
+
const [collapsed, setCollapsed] = createSignal(false)
|
|
243
|
+
let lastUpdated = 0
|
|
244
|
+
let debounce: ReturnType<typeof setTimeout> | null = null
|
|
245
|
+
|
|
246
|
+
async function refresh() {
|
|
247
|
+
const next = await readStateFromFile()
|
|
248
|
+
if (next.lastUpdated !== lastUpdated) {
|
|
249
|
+
lastUpdated = next.lastUpdated
|
|
250
|
+
setState(next)
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function scheduleRefresh() {
|
|
255
|
+
if (debounce) clearTimeout(debounce)
|
|
256
|
+
debounce = setTimeout(() => {
|
|
257
|
+
debounce = null
|
|
258
|
+
void refresh()
|
|
259
|
+
}, REFRESH_DEBOUNCE_MS)
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Background poller (server and TUI run as separate module instances, so we
|
|
263
|
+
// sync through the state file) plus event-driven refreshes for low latency.
|
|
264
|
+
const timer = setInterval(refresh, POLL_MS)
|
|
265
|
+
const unsubs = [
|
|
266
|
+
props.api.event.on('session.updated', scheduleRefresh),
|
|
267
|
+
props.api.event.on('message.updated', scheduleRefresh),
|
|
268
|
+
]
|
|
269
|
+
setTimeout(refresh, 300)
|
|
270
|
+
onCleanup(() => {
|
|
271
|
+
clearInterval(timer)
|
|
272
|
+
if (debounce) clearTimeout(debounce)
|
|
273
|
+
for (const u of unsubs) u()
|
|
274
|
+
})
|
|
275
|
+
|
|
276
|
+
const theme = () => props.api.theme.current
|
|
277
|
+
const enabledFallbacks = () => state().fallbacks.filter((f) => f.enabled)
|
|
278
|
+
const hasData = () =>
|
|
279
|
+
state().main.quota != null || enabledFallbacks().length > 0
|
|
280
|
+
|
|
281
|
+
const headerLabel = () =>
|
|
282
|
+
!hasData() ? 'CLAUDE' : collapsed() ? '\u25b6 CLAUDE' : '\u25bc CLAUDE'
|
|
283
|
+
const activeAccount = () => resolveActiveAccount(state())
|
|
284
|
+
const activeFiveHourPct = () =>
|
|
285
|
+
activeAccount().quota?.five_hour?.usedPercent ?? null
|
|
286
|
+
|
|
287
|
+
const quotaBackedOff = () => state().main.quotaBackedOff === true
|
|
288
|
+
const refreshBackedOff = () => state().main.refreshBackedOff === true
|
|
289
|
+
const degraded = () => quotaBackedOff() || refreshBackedOff()
|
|
290
|
+
|
|
291
|
+
const cacheKeep = () => state().cacheKeep
|
|
292
|
+
const showCache = () => cacheKeep() != null && cacheKeep()?.window != null
|
|
293
|
+
const relayValue = () => {
|
|
294
|
+
const r = state().relay
|
|
295
|
+
if (!r) return '\u2014'
|
|
296
|
+
return `${r.transport} \u00b7 ${r.enabled ? 'on' : 'off'}`
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
return (
|
|
300
|
+
<box
|
|
301
|
+
width='100%'
|
|
302
|
+
flexDirection='column'
|
|
303
|
+
border={SINGLE_BORDER}
|
|
304
|
+
borderColor={theme().borderActive}
|
|
305
|
+
paddingTop={1}
|
|
306
|
+
paddingBottom={1}
|
|
307
|
+
paddingLeft={1}
|
|
308
|
+
paddingRight={1}
|
|
309
|
+
>
|
|
310
|
+
{/* Header: ▼/▶ CLAUDE badge (click to collapse) + version, or LIMITED badge */}
|
|
311
|
+
{/* biome-ignore lint/a11y/noStaticElementInteractions: opentui renders to a terminal, not the DOM — ARIA roles do not apply */}
|
|
312
|
+
<box
|
|
313
|
+
width='100%'
|
|
314
|
+
flexDirection='row'
|
|
315
|
+
justifyContent='space-between'
|
|
316
|
+
alignItems='center'
|
|
317
|
+
onMouseDown={() => {
|
|
318
|
+
if (hasData()) setCollapsed((value) => !value)
|
|
319
|
+
}}
|
|
320
|
+
>
|
|
321
|
+
<box paddingLeft={1} paddingRight={1} backgroundColor={theme().accent}>
|
|
322
|
+
<text fg={theme().background}>
|
|
323
|
+
<b>{headerLabel()}</b>
|
|
324
|
+
</text>
|
|
325
|
+
</box>
|
|
326
|
+
<Show
|
|
327
|
+
when={degraded()}
|
|
328
|
+
fallback={
|
|
329
|
+
<Show when={PLUGIN_VERSION !== ''}>
|
|
330
|
+
<text fg={theme().textMuted}>{`v${PLUGIN_VERSION}`}</text>
|
|
331
|
+
</Show>
|
|
332
|
+
}
|
|
333
|
+
>
|
|
334
|
+
<box
|
|
335
|
+
paddingLeft={1}
|
|
336
|
+
paddingRight={1}
|
|
337
|
+
backgroundColor={theme().warning}
|
|
338
|
+
>
|
|
339
|
+
<text fg={theme().background}>
|
|
340
|
+
<b>{'LIMITED'}</b>
|
|
341
|
+
</text>
|
|
342
|
+
</box>
|
|
343
|
+
</Show>
|
|
344
|
+
</box>
|
|
345
|
+
|
|
346
|
+
{/* Collapsed: active account 5h quota + dot, plus fast-mode when on */}
|
|
347
|
+
<Show when={collapsed() && hasData()}>
|
|
348
|
+
<CollapsedRow theme={theme()} label={activeAccount().name}>
|
|
349
|
+
<Show
|
|
350
|
+
when={activeFiveHourPct() != null}
|
|
351
|
+
fallback={<text fg={theme().textMuted}>{'\u2014'}</text>}
|
|
352
|
+
>
|
|
353
|
+
<box flexDirection='row'>
|
|
354
|
+
<text
|
|
355
|
+
fg={toneColor(
|
|
356
|
+
theme(),
|
|
357
|
+
usageTone(activeFiveHourPct() as number),
|
|
358
|
+
)}
|
|
359
|
+
>
|
|
360
|
+
<b>
|
|
361
|
+
{`${String(Math.round(activeFiveHourPct() as number)).padStart(3)}%`}
|
|
362
|
+
</b>
|
|
363
|
+
</text>
|
|
364
|
+
<text
|
|
365
|
+
fg={toneColor(
|
|
366
|
+
theme(),
|
|
367
|
+
usageTone(activeFiveHourPct() as number),
|
|
368
|
+
)}
|
|
369
|
+
>
|
|
370
|
+
{' \u25cf'}
|
|
371
|
+
</text>
|
|
372
|
+
</box>
|
|
373
|
+
</Show>
|
|
374
|
+
</CollapsedRow>
|
|
375
|
+
<Show when={state().fastMode}>
|
|
376
|
+
<CollapsedRow theme={theme()} label='Mode'>
|
|
377
|
+
<text fg={toneColor(theme(), 'accent')}>
|
|
378
|
+
<b>{'fast'}</b>
|
|
379
|
+
</text>
|
|
380
|
+
</CollapsedRow>
|
|
381
|
+
</Show>
|
|
382
|
+
</Show>
|
|
383
|
+
|
|
384
|
+
{/* Expanded: full sections. Also render when there's no data so the
|
|
385
|
+
sidebar can never go blank if data clears while collapsed. */}
|
|
386
|
+
<Show when={!collapsed() || !hasData()}>
|
|
387
|
+
<Show
|
|
388
|
+
when={hasData()}
|
|
389
|
+
fallback={
|
|
390
|
+
<box marginTop={1} width='100%'>
|
|
391
|
+
<text fg={theme().textMuted}>{'Waiting for quota\u2026'}</text>
|
|
392
|
+
</box>
|
|
393
|
+
}
|
|
394
|
+
>
|
|
395
|
+
{/* Quota */}
|
|
396
|
+
<SectionHeader theme={theme()} title='Quota' />
|
|
397
|
+
<AccountBlock
|
|
398
|
+
theme={theme()}
|
|
399
|
+
name='main'
|
|
400
|
+
quota={state().main.quota}
|
|
401
|
+
active={state().activeId === 'main'}
|
|
402
|
+
/>
|
|
403
|
+
<For each={enabledFallbacks()}>
|
|
404
|
+
{(fb) => (
|
|
405
|
+
<AccountBlock
|
|
406
|
+
theme={theme()}
|
|
407
|
+
name={fb.label ?? fb.id}
|
|
408
|
+
quota={fb.quota}
|
|
409
|
+
active={state().activeId === fb.id}
|
|
410
|
+
marginTop={1}
|
|
411
|
+
/>
|
|
412
|
+
)}
|
|
413
|
+
</For>
|
|
414
|
+
</Show>
|
|
415
|
+
|
|
416
|
+
{/* Routing */}
|
|
417
|
+
<SectionHeader theme={theme()} title='Routing' />
|
|
418
|
+
<StatRow
|
|
419
|
+
theme={theme()}
|
|
420
|
+
label='Route'
|
|
421
|
+
value={state().route}
|
|
422
|
+
tone='accent'
|
|
423
|
+
/>
|
|
424
|
+
<StatRow
|
|
425
|
+
theme={theme()}
|
|
426
|
+
label='Mode'
|
|
427
|
+
value={state().fastMode ? 'fast' : 'std'}
|
|
428
|
+
tone={state().fastMode ? 'accent' : 'muted'}
|
|
429
|
+
/>
|
|
430
|
+
<StatRow
|
|
431
|
+
theme={theme()}
|
|
432
|
+
label='Relay'
|
|
433
|
+
value={relayValue()}
|
|
434
|
+
tone={state().relay?.enabled ? 'ok' : 'muted'}
|
|
435
|
+
/>
|
|
436
|
+
|
|
437
|
+
{/* Cache */}
|
|
438
|
+
<Show when={showCache()}>
|
|
439
|
+
<SectionHeader theme={theme()} title='Cache' />
|
|
440
|
+
<StatRow
|
|
441
|
+
theme={theme()}
|
|
442
|
+
label='1h cache'
|
|
443
|
+
value={`${cacheKeep()?.window} \u00b7 ${cacheKeep()?.enabled ? 'on' : 'off'}`}
|
|
444
|
+
tone={cacheKeep()?.enabled ? 'ok' : 'muted'}
|
|
445
|
+
/>
|
|
446
|
+
<Show when={(cacheKeep()?.trackedSessions ?? 0) > 0}>
|
|
447
|
+
<StatRow
|
|
448
|
+
theme={theme()}
|
|
449
|
+
label='Tracked'
|
|
450
|
+
value={String(cacheKeep()?.trackedSessions)}
|
|
451
|
+
tone='text'
|
|
452
|
+
/>
|
|
453
|
+
</Show>
|
|
454
|
+
</Show>
|
|
455
|
+
|
|
456
|
+
{/* Health — only when something is wrong */}
|
|
457
|
+
<Show when={degraded()}>
|
|
458
|
+
<SectionHeader theme={theme()} title='Health' />
|
|
459
|
+
<Show when={quotaBackedOff()}>
|
|
460
|
+
<StatRow
|
|
461
|
+
theme={theme()}
|
|
462
|
+
label='Quota API'
|
|
463
|
+
value={`backoff ${formatUntil(state().main.quotaBackoffUntil)}`}
|
|
464
|
+
tone='warn'
|
|
465
|
+
/>
|
|
466
|
+
</Show>
|
|
467
|
+
<Show when={refreshBackedOff()}>
|
|
468
|
+
<StatRow
|
|
469
|
+
theme={theme()}
|
|
470
|
+
label='Token refresh'
|
|
471
|
+
value={`backoff ${formatUntil(state().main.refreshBackoffUntil)}`}
|
|
472
|
+
tone='warn'
|
|
473
|
+
/>
|
|
474
|
+
</Show>
|
|
475
|
+
</Show>
|
|
476
|
+
</Show>
|
|
477
|
+
</box>
|
|
478
|
+
)
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
const tui: TuiPlugin = async (api) => {
|
|
482
|
+
api.slots.register({
|
|
483
|
+
order: 160,
|
|
484
|
+
slots: {
|
|
485
|
+
sidebar_content(_ctx: unknown, _props: { session_id: string }) {
|
|
486
|
+
return <QuotaSidebar api={api} />
|
|
487
|
+
},
|
|
488
|
+
},
|
|
489
|
+
})
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
const plugin: TuiPluginModule & { id: string } = {
|
|
493
|
+
id: ID,
|
|
494
|
+
tui,
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
export default plugin
|