@cortexkit/opencode-anthropic-auth 1.4.1 → 1.5.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/dist/cli.js +1 -1
- package/dist/index-4wz5npbg.js +325 -0
- package/dist/index-nfgq84t3.js +2 -0
- package/dist/index.js +3 -3
- package/dist/sidebar-state.d.ts +42 -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 +79 -0
- package/src/tui.tsx +401 -0
- package/dist/index-wc8ekdxb.js +0 -325
package/src/tui.tsx
ADDED
|
@@ -0,0 +1,401 @@
|
|
|
1
|
+
/** @jsxImportSource @opentui/solid */
|
|
2
|
+
|
|
3
|
+
import type {
|
|
4
|
+
TuiPlugin,
|
|
5
|
+
TuiPluginApi,
|
|
6
|
+
TuiPluginModule,
|
|
7
|
+
} from '@opencode-ai/plugin/tui'
|
|
8
|
+
import { For, Show, createSignal, onCleanup } from 'solid-js'
|
|
9
|
+
|
|
10
|
+
import {
|
|
11
|
+
type AccountQuota,
|
|
12
|
+
DEFAULT_SIDEBAR_STATE,
|
|
13
|
+
type SidebarState,
|
|
14
|
+
getSidebarState,
|
|
15
|
+
} from './sidebar-state.js'
|
|
16
|
+
|
|
17
|
+
const POLL_MS = 1500
|
|
18
|
+
const REFRESH_DEBOUNCE_MS = 200
|
|
19
|
+
|
|
20
|
+
const ID = 'cortexkit.anthropic-auth'
|
|
21
|
+
const BAR_WIDTH = 10
|
|
22
|
+
const BAR_FILLED = '\u2588'
|
|
23
|
+
const BAR_EMPTY = '\u2591'
|
|
24
|
+
|
|
25
|
+
// biome-ignore lint/suspicious/noExplicitAny: opentui border prop not typed in plugin tui surface
|
|
26
|
+
const SINGLE_BORDER = { type: 'single' } as any
|
|
27
|
+
|
|
28
|
+
type Tone = 'ok' | 'warn' | 'err' | 'muted' | 'accent' | 'text'
|
|
29
|
+
type ThemeCurrent = TuiPluginApi['theme']['current']
|
|
30
|
+
type ThemeColor = ThemeCurrent['text']
|
|
31
|
+
|
|
32
|
+
// Tone -> theme token. Theme tokens only — never hardcoded colors. Fallbacks
|
|
33
|
+
// resolve to other theme tokens so the sidebar always tracks the active theme.
|
|
34
|
+
function toneColor(theme: ThemeCurrent, tone: Tone): ThemeColor {
|
|
35
|
+
switch (tone) {
|
|
36
|
+
case 'ok':
|
|
37
|
+
return theme.success ?? theme.accent
|
|
38
|
+
case 'warn':
|
|
39
|
+
return theme.warning ?? theme.accent
|
|
40
|
+
case 'err':
|
|
41
|
+
return theme.error ?? theme.accent
|
|
42
|
+
case 'muted':
|
|
43
|
+
return theme.textMuted ?? theme.text
|
|
44
|
+
case 'accent':
|
|
45
|
+
return theme.accent ?? theme.text
|
|
46
|
+
default:
|
|
47
|
+
return theme.text
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function usageTone(usedPct: number): Tone {
|
|
52
|
+
if (usedPct < 50) return 'ok'
|
|
53
|
+
if (usedPct < 80) return 'warn'
|
|
54
|
+
return 'err'
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function quotaBar(usedPct: number, width = BAR_WIDTH): string {
|
|
58
|
+
const filled = Math.max(
|
|
59
|
+
0,
|
|
60
|
+
Math.min(Math.round((usedPct / 100) * width), width),
|
|
61
|
+
)
|
|
62
|
+
return BAR_FILLED.repeat(filled) + BAR_EMPTY.repeat(width - filled)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function formatResetIn(resetsAt: string | undefined): string {
|
|
66
|
+
if (!resetsAt) return ''
|
|
67
|
+
const ms = new Date(resetsAt).getTime() - Date.now()
|
|
68
|
+
if (ms <= 0) return 'now'
|
|
69
|
+
const mins = Math.floor(ms / 60_000)
|
|
70
|
+
if (mins < 60) return `${mins}m`
|
|
71
|
+
const hrs = Math.floor(mins / 60)
|
|
72
|
+
const rm = mins % 60
|
|
73
|
+
return rm > 0 ? `${hrs}h${rm}m` : `${hrs}h`
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function formatUntil(until: number | undefined): string {
|
|
77
|
+
if (!until) return ''
|
|
78
|
+
const ms = until - Date.now()
|
|
79
|
+
if (ms <= 0) return 'now'
|
|
80
|
+
const mins = Math.ceil(ms / 60_000)
|
|
81
|
+
if (mins < 60) return `${mins}m`
|
|
82
|
+
const hrs = Math.floor(mins / 60)
|
|
83
|
+
const rm = mins % 60
|
|
84
|
+
return rm > 0 ? `${hrs}h${rm}m` : `${hrs}h`
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// --- Reusable components (aft-style) ---------------------------------------
|
|
88
|
+
|
|
89
|
+
function SectionHeader(props: {
|
|
90
|
+
theme: ThemeCurrent
|
|
91
|
+
title: string
|
|
92
|
+
marginTop?: number
|
|
93
|
+
}) {
|
|
94
|
+
return (
|
|
95
|
+
<box width='100%' marginTop={props.marginTop ?? 1}>
|
|
96
|
+
<text fg={props.theme.text}>
|
|
97
|
+
<b>{props.title}</b>
|
|
98
|
+
</text>
|
|
99
|
+
</box>
|
|
100
|
+
)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function StatRow(props: {
|
|
104
|
+
theme: ThemeCurrent
|
|
105
|
+
label: string
|
|
106
|
+
value: string
|
|
107
|
+
tone?: Tone
|
|
108
|
+
}) {
|
|
109
|
+
return (
|
|
110
|
+
<box width='100%' flexDirection='row' justifyContent='space-between'>
|
|
111
|
+
<text fg={props.theme.textMuted}>{props.label}</text>
|
|
112
|
+
<text fg={toneColor(props.theme, props.tone ?? 'text')}>
|
|
113
|
+
<b>{props.value}</b>
|
|
114
|
+
</text>
|
|
115
|
+
</box>
|
|
116
|
+
)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Quota window row: muted label left, tone-colored bar + percentage right,
|
|
120
|
+
// with an optional muted reset suffix.
|
|
121
|
+
function QuotaRow(props: {
|
|
122
|
+
theme: ThemeCurrent
|
|
123
|
+
label: string
|
|
124
|
+
window: { usedPercent: number; resetsAt?: string } | undefined
|
|
125
|
+
}) {
|
|
126
|
+
const used = () => props.window?.usedPercent ?? 0
|
|
127
|
+
const reset = () => formatResetIn(props.window?.resetsAt)
|
|
128
|
+
return (
|
|
129
|
+
<Show
|
|
130
|
+
when={props.window}
|
|
131
|
+
fallback={
|
|
132
|
+
<box width='100%' flexDirection='row'>
|
|
133
|
+
<text fg={props.theme.textMuted}>{props.label.padEnd(3)}</text>
|
|
134
|
+
<text fg={props.theme.textMuted}>{'\u2014'}</text>
|
|
135
|
+
</box>
|
|
136
|
+
}
|
|
137
|
+
>
|
|
138
|
+
{/* Left group (label · bar · pct) stays left-aligned in fixed columns so
|
|
139
|
+
bars and percentages line up across rows; the reset time is pushed to
|
|
140
|
+
the right edge so reset times align in their own right column. */}
|
|
141
|
+
<box width='100%' flexDirection='row' justifyContent='space-between'>
|
|
142
|
+
<box flexDirection='row'>
|
|
143
|
+
<text fg={props.theme.textMuted}>{props.label.padEnd(3)}</text>
|
|
144
|
+
<text fg={toneColor(props.theme, usageTone(used()))}>
|
|
145
|
+
{quotaBar(used())}
|
|
146
|
+
</text>
|
|
147
|
+
<text fg={toneColor(props.theme, usageTone(used()))}>
|
|
148
|
+
{` ${String(Math.round(used())).padStart(3)}%`}
|
|
149
|
+
</text>
|
|
150
|
+
</box>
|
|
151
|
+
<Show when={reset()}>
|
|
152
|
+
<text fg={props.theme.textMuted}>{reset()}</text>
|
|
153
|
+
</Show>
|
|
154
|
+
</box>
|
|
155
|
+
</Show>
|
|
156
|
+
)
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Account block: header row (name + status word) then per-window quota rows.
|
|
160
|
+
function AccountBlock(props: {
|
|
161
|
+
theme: ThemeCurrent
|
|
162
|
+
name: string
|
|
163
|
+
quota: AccountQuota | null
|
|
164
|
+
active: boolean
|
|
165
|
+
marginTop?: number
|
|
166
|
+
}) {
|
|
167
|
+
const statusWord = () => (props.active ? 'active' : 'idle')
|
|
168
|
+
const statusTone = (): Tone => (props.active ? 'ok' : 'muted')
|
|
169
|
+
return (
|
|
170
|
+
<box width='100%' flexDirection='column' marginTop={props.marginTop ?? 0}>
|
|
171
|
+
<box width='100%' flexDirection='row' justifyContent='space-between'>
|
|
172
|
+
<text fg={props.theme.text}>
|
|
173
|
+
<b>{props.name}</b>
|
|
174
|
+
</text>
|
|
175
|
+
<text fg={toneColor(props.theme, statusTone())}>
|
|
176
|
+
<b>{statusWord()}</b>
|
|
177
|
+
</text>
|
|
178
|
+
</box>
|
|
179
|
+
<Show
|
|
180
|
+
when={props.quota}
|
|
181
|
+
fallback={<text fg={props.theme.textMuted}>{'checking\u2026'}</text>}
|
|
182
|
+
>
|
|
183
|
+
<QuotaRow
|
|
184
|
+
theme={props.theme}
|
|
185
|
+
label='5h'
|
|
186
|
+
window={props.quota?.five_hour}
|
|
187
|
+
/>
|
|
188
|
+
<QuotaRow
|
|
189
|
+
theme={props.theme}
|
|
190
|
+
label='7d'
|
|
191
|
+
window={props.quota?.seven_day}
|
|
192
|
+
/>
|
|
193
|
+
</Show>
|
|
194
|
+
</box>
|
|
195
|
+
)
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// --- State plumbing ---------------------------------------------------------
|
|
199
|
+
|
|
200
|
+
async function readStateFromFile(): Promise<SidebarState> {
|
|
201
|
+
return getSidebarState()
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function QuotaSidebar(props: { api: TuiPluginApi }) {
|
|
205
|
+
const [state, setState] = createSignal<SidebarState>(DEFAULT_SIDEBAR_STATE)
|
|
206
|
+
let lastUpdated = 0
|
|
207
|
+
let debounce: ReturnType<typeof setTimeout> | null = null
|
|
208
|
+
|
|
209
|
+
async function refresh() {
|
|
210
|
+
const next = await readStateFromFile()
|
|
211
|
+
if (next.lastUpdated !== lastUpdated) {
|
|
212
|
+
lastUpdated = next.lastUpdated
|
|
213
|
+
setState(next)
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function scheduleRefresh() {
|
|
218
|
+
if (debounce) clearTimeout(debounce)
|
|
219
|
+
debounce = setTimeout(() => {
|
|
220
|
+
debounce = null
|
|
221
|
+
void refresh()
|
|
222
|
+
}, REFRESH_DEBOUNCE_MS)
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Background poller (server and TUI run as separate module instances, so we
|
|
226
|
+
// sync through the state file) plus event-driven refreshes for low latency.
|
|
227
|
+
const timer = setInterval(refresh, POLL_MS)
|
|
228
|
+
const unsubs = [
|
|
229
|
+
props.api.event.on('session.updated', scheduleRefresh),
|
|
230
|
+
props.api.event.on('message.updated', scheduleRefresh),
|
|
231
|
+
]
|
|
232
|
+
setTimeout(refresh, 300)
|
|
233
|
+
onCleanup(() => {
|
|
234
|
+
clearInterval(timer)
|
|
235
|
+
if (debounce) clearTimeout(debounce)
|
|
236
|
+
for (const u of unsubs) u()
|
|
237
|
+
})
|
|
238
|
+
|
|
239
|
+
const theme = () => props.api.theme.current
|
|
240
|
+
const enabledFallbacks = () => state().fallbacks.filter((f) => f.enabled)
|
|
241
|
+
const hasData = () =>
|
|
242
|
+
state().main.quota != null || enabledFallbacks().length > 0
|
|
243
|
+
|
|
244
|
+
const quotaBackedOff = () => state().main.quotaBackedOff === true
|
|
245
|
+
const refreshBackedOff = () => state().main.refreshBackedOff === true
|
|
246
|
+
const degraded = () => quotaBackedOff() || refreshBackedOff()
|
|
247
|
+
|
|
248
|
+
const cacheKeep = () => state().cacheKeep
|
|
249
|
+
const showCache = () => cacheKeep() != null && cacheKeep()?.window != null
|
|
250
|
+
const relayValue = () => {
|
|
251
|
+
const r = state().relay
|
|
252
|
+
if (!r) return '\u2014'
|
|
253
|
+
return `${r.transport} \u00b7 ${r.enabled ? 'on' : 'off'}`
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
return (
|
|
257
|
+
<box
|
|
258
|
+
width='100%'
|
|
259
|
+
flexDirection='column'
|
|
260
|
+
border={SINGLE_BORDER}
|
|
261
|
+
borderColor={theme().borderActive}
|
|
262
|
+
paddingTop={1}
|
|
263
|
+
paddingBottom={1}
|
|
264
|
+
paddingLeft={1}
|
|
265
|
+
paddingRight={1}
|
|
266
|
+
>
|
|
267
|
+
{/* Header: CLAUDE badge + optional LIMITED degraded badge */}
|
|
268
|
+
<box
|
|
269
|
+
width='100%'
|
|
270
|
+
flexDirection='row'
|
|
271
|
+
justifyContent='space-between'
|
|
272
|
+
alignItems='center'
|
|
273
|
+
>
|
|
274
|
+
<box paddingLeft={1} paddingRight={1} backgroundColor={theme().accent}>
|
|
275
|
+
<text fg={theme().background}>
|
|
276
|
+
<b>{'CLAUDE'}</b>
|
|
277
|
+
</text>
|
|
278
|
+
</box>
|
|
279
|
+
<Show when={degraded()}>
|
|
280
|
+
<box
|
|
281
|
+
paddingLeft={1}
|
|
282
|
+
paddingRight={1}
|
|
283
|
+
backgroundColor={theme().warning}
|
|
284
|
+
>
|
|
285
|
+
<text fg={theme().background}>
|
|
286
|
+
<b>{'LIMITED'}</b>
|
|
287
|
+
</text>
|
|
288
|
+
</box>
|
|
289
|
+
</Show>
|
|
290
|
+
</box>
|
|
291
|
+
|
|
292
|
+
<Show
|
|
293
|
+
when={hasData()}
|
|
294
|
+
fallback={
|
|
295
|
+
<box marginTop={1} width='100%'>
|
|
296
|
+
<text fg={theme().textMuted}>{'Waiting for quota\u2026'}</text>
|
|
297
|
+
</box>
|
|
298
|
+
}
|
|
299
|
+
>
|
|
300
|
+
{/* Quota */}
|
|
301
|
+
<SectionHeader theme={theme()} title='Quota' />
|
|
302
|
+
<AccountBlock
|
|
303
|
+
theme={theme()}
|
|
304
|
+
name='main'
|
|
305
|
+
quota={state().main.quota}
|
|
306
|
+
active={state().activeId === 'main'}
|
|
307
|
+
/>
|
|
308
|
+
<For each={enabledFallbacks()}>
|
|
309
|
+
{(fb) => (
|
|
310
|
+
<AccountBlock
|
|
311
|
+
theme={theme()}
|
|
312
|
+
name={fb.label ?? fb.id}
|
|
313
|
+
quota={fb.quota}
|
|
314
|
+
active={state().activeId === fb.id}
|
|
315
|
+
marginTop={1}
|
|
316
|
+
/>
|
|
317
|
+
)}
|
|
318
|
+
</For>
|
|
319
|
+
</Show>
|
|
320
|
+
|
|
321
|
+
{/* Routing */}
|
|
322
|
+
<SectionHeader theme={theme()} title='Routing' />
|
|
323
|
+
<StatRow
|
|
324
|
+
theme={theme()}
|
|
325
|
+
label='Route'
|
|
326
|
+
value={state().route}
|
|
327
|
+
tone='accent'
|
|
328
|
+
/>
|
|
329
|
+
<StatRow
|
|
330
|
+
theme={theme()}
|
|
331
|
+
label='Mode'
|
|
332
|
+
value={state().fastMode ? 'fast' : 'std'}
|
|
333
|
+
tone={state().fastMode ? 'accent' : 'muted'}
|
|
334
|
+
/>
|
|
335
|
+
<StatRow
|
|
336
|
+
theme={theme()}
|
|
337
|
+
label='Relay'
|
|
338
|
+
value={relayValue()}
|
|
339
|
+
tone={state().relay?.enabled ? 'ok' : 'muted'}
|
|
340
|
+
/>
|
|
341
|
+
|
|
342
|
+
{/* Cache */}
|
|
343
|
+
<Show when={showCache()}>
|
|
344
|
+
<SectionHeader theme={theme()} title='Cache' />
|
|
345
|
+
<StatRow
|
|
346
|
+
theme={theme()}
|
|
347
|
+
label='1h cache'
|
|
348
|
+
value={`${cacheKeep()?.window} \u00b7 ${cacheKeep()?.enabled ? 'on' : 'off'}`}
|
|
349
|
+
tone={cacheKeep()?.enabled ? 'ok' : 'muted'}
|
|
350
|
+
/>
|
|
351
|
+
<Show when={(cacheKeep()?.trackedSessions ?? 0) > 0}>
|
|
352
|
+
<StatRow
|
|
353
|
+
theme={theme()}
|
|
354
|
+
label='Tracked'
|
|
355
|
+
value={String(cacheKeep()?.trackedSessions)}
|
|
356
|
+
tone='text'
|
|
357
|
+
/>
|
|
358
|
+
</Show>
|
|
359
|
+
</Show>
|
|
360
|
+
|
|
361
|
+
{/* Health — only when something is wrong */}
|
|
362
|
+
<Show when={degraded()}>
|
|
363
|
+
<SectionHeader theme={theme()} title='Health' />
|
|
364
|
+
<Show when={quotaBackedOff()}>
|
|
365
|
+
<StatRow
|
|
366
|
+
theme={theme()}
|
|
367
|
+
label='Quota API'
|
|
368
|
+
value={`backoff ${formatUntil(state().main.quotaBackoffUntil)}`}
|
|
369
|
+
tone='warn'
|
|
370
|
+
/>
|
|
371
|
+
</Show>
|
|
372
|
+
<Show when={refreshBackedOff()}>
|
|
373
|
+
<StatRow
|
|
374
|
+
theme={theme()}
|
|
375
|
+
label='Token refresh'
|
|
376
|
+
value={`backoff ${formatUntil(state().main.refreshBackoffUntil)}`}
|
|
377
|
+
tone='warn'
|
|
378
|
+
/>
|
|
379
|
+
</Show>
|
|
380
|
+
</Show>
|
|
381
|
+
</box>
|
|
382
|
+
)
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
const tui: TuiPlugin = async (api) => {
|
|
386
|
+
api.slots.register({
|
|
387
|
+
order: 160,
|
|
388
|
+
slots: {
|
|
389
|
+
sidebar_content(_ctx: unknown, _props: { session_id: string }) {
|
|
390
|
+
return <QuotaSidebar api={api} />
|
|
391
|
+
},
|
|
392
|
+
},
|
|
393
|
+
})
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
const plugin: TuiPluginModule & { id: string } = {
|
|
397
|
+
id: ID,
|
|
398
|
+
tui,
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
export default plugin
|