@cortexkit/opencode-anthropic-auth 2.1.2 → 2.2.1
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 +36 -36
- package/dist/{index-f2ne65w4.js → index-62x1p34p.js} +1 -1
- package/dist/index-88zg9ztr.js +2 -0
- package/dist/{index-x325vp4e.js → index-8ysjtqek.js} +13 -13
- package/dist/{index-ah5s0bew.js → index-ppdrzs41.js} +1 -1
- package/dist/index-xbwa4x53.js +2 -0
- package/dist/index.js +1 -1
- package/dist/rpc/port-file.js +1 -1
- package/dist/rpc/protocol.js +1 -1
- package/dist/rpc/rpc-client.js +1 -1
- package/dist/rpc/rpc-dir.js +1 -1
- package/dist/sidebar-state.d.ts +54 -1
- package/dist/sidebar-state.js +1 -1
- package/dist/transform.d.ts +4 -0
- package/dist/tui/claude-sidebar.d.ts +99 -0
- package/dist/tui-preferences.js +1 -1
- package/dist/v2/menu-contract.d.ts +3 -0
- package/dist/v2/rpc.d.ts +9 -0
- package/dist/v2/server.js +4 -4
- package/dist/v2/sidebar.d.ts +13 -0
- package/dist/v2/source.d.ts +2 -0
- package/dist/v2/tui.d.ts +9 -1
- package/dist/v2/tui.js +2 -2
- package/dist/v2/wire.d.ts +4 -0
- package/package.json +5 -5
- package/src/sidebar-state.ts +133 -3
- package/src/tui/claude-sidebar.tsx +1146 -0
- package/src/tui/entry.mjs +15 -1
- package/src/tui-compiled/sidebar-state.ts +133 -3
- package/src/tui-compiled/tui/claude-sidebar.tsx +1310 -0
- package/src/tui-compiled/tui.tsx +46 -1137
- package/src/tui.tsx +15 -886
- package/dist/index-53nnxq39.js +0 -2
- package/dist/index-jexctf5w.js +0 -2
package/src/tui.tsx
CHANGED
|
@@ -1,61 +1,34 @@
|
|
|
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'
|
|
6
|
-
import {
|
|
7
|
-
formatCreditAmounts,
|
|
8
|
-
type QuotaWindowPart,
|
|
9
|
-
quotaDisplayProjection,
|
|
10
|
-
quotaWindowParts,
|
|
11
|
-
} from '@cortexkit/anthropic-auth-core'
|
|
12
3
|
import type {
|
|
13
4
|
TuiPlugin,
|
|
14
5
|
TuiPluginApi,
|
|
15
6
|
TuiPluginModule,
|
|
16
7
|
} from '@opencode-ai/plugin/tui'
|
|
17
|
-
import {
|
|
18
|
-
import {
|
|
19
|
-
createEffect,
|
|
20
|
-
createSignal,
|
|
21
|
-
For,
|
|
22
|
-
Index,
|
|
23
|
-
type JSX,
|
|
24
|
-
onCleanup,
|
|
25
|
-
Show,
|
|
26
|
-
} from 'solid-js'
|
|
8
|
+
import { createEffect, createSignal, For, onCleanup, Show } from 'solid-js'
|
|
27
9
|
import { createRpcClient } from './rpc/rpc-client.js'
|
|
28
10
|
import { getRpcDir } from './rpc/rpc-dir.js'
|
|
29
11
|
import {
|
|
30
|
-
type AccountQuota,
|
|
31
|
-
computeQuotaPacing,
|
|
32
12
|
DEFAULT_SIDEBAR_STATE,
|
|
33
|
-
FIVE_HOUR_MS,
|
|
34
|
-
formatPrimeAccountValue,
|
|
35
|
-
formatQuotaRowLabel,
|
|
36
|
-
getCollapsedAccounts,
|
|
37
|
-
getCollapsedQuotaSummary,
|
|
38
|
-
getFableRecoverySummary,
|
|
39
13
|
getSidebarState,
|
|
40
|
-
type QuotaPacing,
|
|
41
|
-
resolveActiveAccount,
|
|
42
|
-
SEVEN_DAY_MS,
|
|
43
14
|
type SidebarState,
|
|
44
15
|
} from './sidebar-state.js'
|
|
16
|
+
import {
|
|
17
|
+
AccountBlock,
|
|
18
|
+
ClaudeSidebar,
|
|
19
|
+
createSidebarController,
|
|
20
|
+
type SidebarController,
|
|
21
|
+
} from './tui/claude-sidebar.js'
|
|
45
22
|
import {
|
|
46
23
|
openCommandDialog,
|
|
47
24
|
openNativeMenuDialog,
|
|
48
25
|
} from './tui/command-dialogs.js'
|
|
49
26
|
import {
|
|
50
|
-
type AnthropicAuthTuiPrefs,
|
|
51
|
-
type AppearancePrefs,
|
|
52
27
|
computeEffectiveOrder,
|
|
53
28
|
DEFAULT_SLOT_ORDER,
|
|
54
29
|
PLUGIN_KEY,
|
|
55
|
-
queueTuiPreferenceUpdate,
|
|
56
30
|
readTuiPreferencesFile,
|
|
57
31
|
resolveAnthropicAuthPrefs,
|
|
58
|
-
watchTuiPreferences,
|
|
59
32
|
} from './tui-preferences.js'
|
|
60
33
|
|
|
61
34
|
const RPC_POLL_MS = 500
|
|
@@ -63,473 +36,6 @@ let rpcPollStarted = false
|
|
|
63
36
|
|
|
64
37
|
const ID = 'cortexkit.anthropic-auth'
|
|
65
38
|
|
|
66
|
-
// Read package metadata from either the raw src/ entry or its generated
|
|
67
|
-
// src/tui-compiled/ counterpart. Avoid a JSON import because package.json sits
|
|
68
|
-
// outside the declaration build's rootDir.
|
|
69
|
-
const PLUGIN_VERSION: string = (() => {
|
|
70
|
-
const here = dirname(fileURLToPath(import.meta.url))
|
|
71
|
-
for (const packageFile of [
|
|
72
|
-
join(here, '..', 'package.json'),
|
|
73
|
-
join(here, '..', '..', 'package.json'),
|
|
74
|
-
]) {
|
|
75
|
-
try {
|
|
76
|
-
const raw = readFileSync(packageFile, 'utf8')
|
|
77
|
-
const version = (JSON.parse(raw) as { version?: string }).version
|
|
78
|
-
if (version) return version
|
|
79
|
-
} catch {
|
|
80
|
-
// Try the path for the other TUI entry layout.
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
return ''
|
|
84
|
-
})()
|
|
85
|
-
|
|
86
|
-
// biome-ignore lint/suspicious/noExplicitAny: opentui border prop not typed in plugin tui surface
|
|
87
|
-
const SINGLE_BORDER = { type: 'single' } as any
|
|
88
|
-
|
|
89
|
-
type Tone = 'ok' | 'warn' | 'err' | 'muted' | 'accent' | 'text'
|
|
90
|
-
type ThemeCurrent = TuiPluginApi['theme']['current']
|
|
91
|
-
type ThemeColor = ThemeCurrent['text']
|
|
92
|
-
|
|
93
|
-
// Tone -> theme token. Theme tokens only — never hardcoded colors. Fallbacks
|
|
94
|
-
// resolve to other theme tokens so the sidebar always tracks the active theme.
|
|
95
|
-
function toneColor(theme: ThemeCurrent, tone: Tone): ThemeColor {
|
|
96
|
-
switch (tone) {
|
|
97
|
-
case 'ok':
|
|
98
|
-
return theme.success ?? theme.accent
|
|
99
|
-
case 'warn':
|
|
100
|
-
return theme.warning ?? theme.accent
|
|
101
|
-
case 'err':
|
|
102
|
-
return theme.error ?? theme.accent
|
|
103
|
-
case 'muted':
|
|
104
|
-
return theme.textMuted ?? theme.text
|
|
105
|
-
case 'accent':
|
|
106
|
-
return theme.accent ?? theme.text
|
|
107
|
-
default:
|
|
108
|
-
return theme.text
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
function usageTone(usedPct: number, appearance: AppearancePrefs): Tone {
|
|
113
|
-
if (usedPct < appearance.warnThreshold) return 'ok'
|
|
114
|
-
if (usedPct < appearance.errorThreshold) return 'warn'
|
|
115
|
-
return 'err'
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
interface BarSegment {
|
|
119
|
-
text: string
|
|
120
|
-
tone: Tone
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
const PACE_RESERVE_CHAR = '\u2592'
|
|
124
|
-
const PACE_DEFICIT_CHAR = '\u2593'
|
|
125
|
-
|
|
126
|
-
// Variant C pacing bar: normal fill up to min(used, pace), then a pace
|
|
127
|
-
// segment covering the gap — headroom being banked (reserve, ok tone) or the
|
|
128
|
-
// overshoot itself (deficit, err tone) — then empty cells. Without pacing the
|
|
129
|
-
// bar renders as a single fill+empty pair, identical to the pre-pacing look.
|
|
130
|
-
// Empty text nodes still occupy a phantom cell in the opentui flex row, so
|
|
131
|
-
// zero-length segments are dropped from every return path (plain and pacing).
|
|
132
|
-
function quotaBarSegments(
|
|
133
|
-
usedPct: number,
|
|
134
|
-
appearance: AppearancePrefs,
|
|
135
|
-
pacing: QuotaPacing | null,
|
|
136
|
-
): BarSegment[] {
|
|
137
|
-
const width = appearance.barWidth
|
|
138
|
-
const cells = (pct: number) =>
|
|
139
|
-
Math.max(0, Math.min(Math.round((pct / 100) * width), width))
|
|
140
|
-
const usedCells = cells(usedPct)
|
|
141
|
-
const fillTone = usageTone(usedPct, appearance)
|
|
142
|
-
const plain: BarSegment[] = [
|
|
143
|
-
{ text: appearance.barFilledChar.repeat(usedCells), tone: fillTone },
|
|
144
|
-
{
|
|
145
|
-
text: appearance.barEmptyChar.repeat(width - usedCells),
|
|
146
|
-
tone: fillTone,
|
|
147
|
-
},
|
|
148
|
-
]
|
|
149
|
-
if (!pacing) return plain.filter((segment) => segment.text.length > 0)
|
|
150
|
-
const paceCells = cells(pacing.pacePercent)
|
|
151
|
-
const lo = Math.min(usedCells, paceCells)
|
|
152
|
-
const hi = Math.max(usedCells, paceCells)
|
|
153
|
-
if (hi === lo) return plain.filter((segment) => segment.text.length > 0)
|
|
154
|
-
const overspent = usedCells > paceCells
|
|
155
|
-
return (
|
|
156
|
-
[
|
|
157
|
-
{ text: appearance.barFilledChar.repeat(lo), tone: fillTone },
|
|
158
|
-
{
|
|
159
|
-
text: (overspent ? PACE_DEFICIT_CHAR : PACE_RESERVE_CHAR).repeat(
|
|
160
|
-
hi - lo,
|
|
161
|
-
),
|
|
162
|
-
tone: overspent ? 'err' : 'ok',
|
|
163
|
-
},
|
|
164
|
-
{ text: appearance.barEmptyChar.repeat(width - hi), tone: fillTone },
|
|
165
|
-
] as BarSegment[]
|
|
166
|
-
).filter((segment) => segment.text.length > 0)
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
function formatResetIn(resetsAt: string | undefined): string {
|
|
170
|
-
if (!resetsAt) return ''
|
|
171
|
-
const ms = new Date(resetsAt).getTime() - Date.now()
|
|
172
|
-
if (ms <= 0) return 'now'
|
|
173
|
-
const mins = Math.floor(ms / 60_000)
|
|
174
|
-
if (mins < 60) return `${mins}m`
|
|
175
|
-
const hrs = Math.floor(mins / 60)
|
|
176
|
-
if (hrs < 24) {
|
|
177
|
-
const rm = mins % 60
|
|
178
|
-
return rm > 0 ? `${hrs}h${rm}m` : `${hrs}h`
|
|
179
|
-
}
|
|
180
|
-
const days = Math.floor(hrs / 24)
|
|
181
|
-
const rh = hrs % 24
|
|
182
|
-
return rh > 0 ? `${days}d${rh}h` : `${days}d`
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
function formatUntil(until: number | undefined): string {
|
|
186
|
-
if (!until) return ''
|
|
187
|
-
const ms = until - Date.now()
|
|
188
|
-
if (ms <= 0) return 'now'
|
|
189
|
-
const mins = Math.ceil(ms / 60_000)
|
|
190
|
-
if (mins < 60) return `${mins}m`
|
|
191
|
-
const hrs = Math.floor(mins / 60)
|
|
192
|
-
const rm = mins % 60
|
|
193
|
-
return rm > 0 ? `${hrs}h${rm}m` : `${hrs}h`
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
// --- Reusable components (aft-style) ---------------------------------------
|
|
197
|
-
|
|
198
|
-
function SectionHeader(props: {
|
|
199
|
-
theme: ThemeCurrent
|
|
200
|
-
title: string
|
|
201
|
-
marginTop?: number
|
|
202
|
-
}) {
|
|
203
|
-
return (
|
|
204
|
-
<box width='100%' marginTop={props.marginTop ?? 1}>
|
|
205
|
-
<text fg={props.theme.text}>
|
|
206
|
-
<b>{props.title}</b>
|
|
207
|
-
</text>
|
|
208
|
-
</box>
|
|
209
|
-
)
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
function StatRow(props: {
|
|
213
|
-
theme: ThemeCurrent
|
|
214
|
-
label: string
|
|
215
|
-
value: string
|
|
216
|
-
tone?: Tone
|
|
217
|
-
}) {
|
|
218
|
-
return (
|
|
219
|
-
<box width='100%' flexDirection='row' justifyContent='space-between'>
|
|
220
|
-
<text fg={props.theme.textMuted}>{props.label}</text>
|
|
221
|
-
<text fg={toneColor(props.theme, props.tone ?? 'text')}>
|
|
222
|
-
<b>{props.value}</b>
|
|
223
|
-
</text>
|
|
224
|
-
</box>
|
|
225
|
-
)
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
// Compact row for the collapsed view: muted label left, caller-provided value
|
|
229
|
-
// right. Mirrors StatRow's layout so columns line up.
|
|
230
|
-
function CollapsedRow(props: {
|
|
231
|
-
theme: ThemeCurrent
|
|
232
|
-
label: string
|
|
233
|
-
children: JSX.Element
|
|
234
|
-
}) {
|
|
235
|
-
return (
|
|
236
|
-
<box width='100%' flexDirection='row' justifyContent='space-between'>
|
|
237
|
-
<text fg={props.theme.textMuted}>{props.label}</text>
|
|
238
|
-
{props.children}
|
|
239
|
-
</box>
|
|
240
|
-
)
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
function CollapsedAccountRow(props: {
|
|
244
|
-
theme: ThemeCurrent
|
|
245
|
-
account: ReturnType<typeof getCollapsedAccounts>[number]
|
|
246
|
-
tone: Tone
|
|
247
|
-
}) {
|
|
248
|
-
const [width, setWidth] = createSignal(40)
|
|
249
|
-
let row: BoxRenderable | undefined
|
|
250
|
-
const summary = () => getCollapsedQuotaSummary(props.account.quota)
|
|
251
|
-
const value = () => summary().text ?? '—'
|
|
252
|
-
const labelWidth = () => Math.max(0, width() - Bun.stringWidth(value()) - 1)
|
|
253
|
-
const dim = () =>
|
|
254
|
-
!props.account.active || props.account.quotaDated || summary().stale
|
|
255
|
-
const attributes = () => (dim() ? TextAttributes.DIM : TextAttributes.BOLD)
|
|
256
|
-
const color = () =>
|
|
257
|
-
dim() ? props.theme.textMuted : toneColor(props.theme, props.tone)
|
|
258
|
-
return (
|
|
259
|
-
<box
|
|
260
|
-
width='100%'
|
|
261
|
-
height={1}
|
|
262
|
-
flexDirection='row'
|
|
263
|
-
ref={(element: BoxRenderable) => {
|
|
264
|
-
row = element
|
|
265
|
-
setWidth(element.width || 40)
|
|
266
|
-
}}
|
|
267
|
-
onSizeChange={() => setWidth(row?.width || 40)}
|
|
268
|
-
>
|
|
269
|
-
<Show when={labelWidth() > 0}>
|
|
270
|
-
<text
|
|
271
|
-
width={labelWidth()}
|
|
272
|
-
flexShrink={0}
|
|
273
|
-
wrapMode='none'
|
|
274
|
-
fg={color()}
|
|
275
|
-
attributes={attributes()}
|
|
276
|
-
>
|
|
277
|
-
{formatQuotaRowLabel(props.account.name, labelWidth())}
|
|
278
|
-
</text>
|
|
279
|
-
<text width={1} flexShrink={0}>
|
|
280
|
-
{' '}
|
|
281
|
-
</text>
|
|
282
|
-
</Show>
|
|
283
|
-
<text
|
|
284
|
-
width={Bun.stringWidth(value())}
|
|
285
|
-
flexShrink={0}
|
|
286
|
-
wrapMode='none'
|
|
287
|
-
fg={color()}
|
|
288
|
-
attributes={attributes()}
|
|
289
|
-
>
|
|
290
|
-
{value()}
|
|
291
|
-
</text>
|
|
292
|
-
</box>
|
|
293
|
-
)
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
// Quota window row: muted label left, tone-colored bar + shared figure right,
|
|
297
|
-
// with an optional muted reset suffix. When pacing data is present, the bar
|
|
298
|
-
// gains a pace segment and an off-pace window adds a muted subline with the
|
|
299
|
-
// reserve/deficit delta and the projected runout.
|
|
300
|
-
function QuotaRow(props: {
|
|
301
|
-
theme: ThemeCurrent
|
|
302
|
-
appearance: AppearancePrefs
|
|
303
|
-
label: string
|
|
304
|
-
part: QuotaWindowPart | undefined
|
|
305
|
-
usedPercent: number | undefined
|
|
306
|
-
pacing: QuotaPacing | null
|
|
307
|
-
binding?: boolean
|
|
308
|
-
}) {
|
|
309
|
-
const used = () => props.usedPercent ?? 0
|
|
310
|
-
const reset = () => props.part?.resetIn
|
|
311
|
-
const paceLine = () => {
|
|
312
|
-
const pacing = props.pacing
|
|
313
|
-
if (!pacing || pacing.state === 'on-pace') return null
|
|
314
|
-
const pct = Math.round(Math.abs(pacing.deltaPercent))
|
|
315
|
-
if (pacing.state === 'reserve') return `reserve ${pct}% \u00b7 lasts`
|
|
316
|
-
return pacing.runsOutAt
|
|
317
|
-
? `deficit ${pct}% \u00b7 out in ${formatResetIn(pacing.runsOutAt)}`
|
|
318
|
-
: `deficit ${pct}% \u00b7 lasts`
|
|
319
|
-
}
|
|
320
|
-
return (
|
|
321
|
-
<Show
|
|
322
|
-
when={props.part}
|
|
323
|
-
fallback={
|
|
324
|
-
<box width='100%' flexDirection='row'>
|
|
325
|
-
<text
|
|
326
|
-
fg={props.theme.textMuted}
|
|
327
|
-
>{`${formatQuotaRowLabel(props.label)} `}</text>
|
|
328
|
-
<text fg={props.theme.textMuted}>{'\u2014'}</text>
|
|
329
|
-
</box>
|
|
330
|
-
}
|
|
331
|
-
>
|
|
332
|
-
{/* Left group (label · bar · figure) stays left-aligned in fixed columns so
|
|
333
|
-
bars and figures line up across rows; the reset time is pushed to
|
|
334
|
-
the right edge so reset times align in their own right column. */}
|
|
335
|
-
<box width='100%' flexDirection='row' justifyContent='space-between'>
|
|
336
|
-
<box flexDirection='row'>
|
|
337
|
-
<text
|
|
338
|
-
fg={props.theme.textMuted}
|
|
339
|
-
>{`${formatQuotaRowLabel(props.label)} `}</text>
|
|
340
|
-
<For each={quotaBarSegments(used(), props.appearance, props.pacing)}>
|
|
341
|
-
{(segment) => (
|
|
342
|
-
<text fg={toneColor(props.theme, segment.tone)}>
|
|
343
|
-
{segment.text}
|
|
344
|
-
</text>
|
|
345
|
-
)}
|
|
346
|
-
</For>
|
|
347
|
-
<text
|
|
348
|
-
fg={toneColor(props.theme, usageTone(used(), props.appearance))}
|
|
349
|
-
>
|
|
350
|
-
{` ${props.part?.figure}${props.binding ? ' •' : ''}`}
|
|
351
|
-
</text>
|
|
352
|
-
</box>
|
|
353
|
-
<Show when={reset()}>
|
|
354
|
-
<text fg={props.theme.textMuted}>{reset()}</text>
|
|
355
|
-
</Show>
|
|
356
|
-
</box>
|
|
357
|
-
<Show when={paceLine()}>
|
|
358
|
-
<box width='100%' flexDirection='row'>
|
|
359
|
-
<text fg={props.theme.textMuted}>{formatQuotaRowLabel('', 11)}</text>
|
|
360
|
-
<text
|
|
361
|
-
fg={toneColor(
|
|
362
|
-
props.theme,
|
|
363
|
-
props.pacing?.state === 'deficit' ? 'warn' : 'muted',
|
|
364
|
-
)}
|
|
365
|
-
>
|
|
366
|
-
{paceLine()}
|
|
367
|
-
</text>
|
|
368
|
-
</box>
|
|
369
|
-
</Show>
|
|
370
|
-
</Show>
|
|
371
|
-
)
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
// The shared credit figure followed by the spent and limit amounts:
|
|
375
|
-
// `credits 75% left · $25.00/$100.00`. When that line is wider than the
|
|
376
|
-
// measured width, the amounts drop their cents (`$1,250/$5,000`) instead of
|
|
377
|
-
// wrapping onto a second line.
|
|
378
|
-
function creditLine(
|
|
379
|
-
figure: string,
|
|
380
|
-
extraUsage: AccountQuota['extraUsage'],
|
|
381
|
-
width: number | undefined,
|
|
382
|
-
) {
|
|
383
|
-
const amounts = formatCreditAmounts(extraUsage)
|
|
384
|
-
if (!amounts) return figure
|
|
385
|
-
const line = `${figure} · ${amounts}`
|
|
386
|
-
if (width === undefined || Bun.stringWidth(line) <= width) return line
|
|
387
|
-
return `${figure} · ${formatCreditAmounts(extraUsage, { wholeUnits: true })}`
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
// Account block: header row (name + status word) then per-window quota rows.
|
|
391
|
-
// Pacing is computed here — per window, against the wall clock at render time
|
|
392
|
-
// (re-evaluated on every state poll) — and disabled by passing false.
|
|
393
|
-
function AccountBlock(props: {
|
|
394
|
-
theme: ThemeCurrent
|
|
395
|
-
appearance: AppearancePrefs
|
|
396
|
-
name: string
|
|
397
|
-
quota: AccountQuota | null
|
|
398
|
-
active: boolean
|
|
399
|
-
pacingEnabled: boolean
|
|
400
|
-
needsReauth?: boolean
|
|
401
|
-
vaultReauth?: boolean
|
|
402
|
-
stateLine?: string
|
|
403
|
-
quotaDated?: boolean
|
|
404
|
-
tierLabel?: string
|
|
405
|
-
marginTop?: number
|
|
406
|
-
}) {
|
|
407
|
-
const statusWord = () =>
|
|
408
|
-
props.quotaDated && props.quota
|
|
409
|
-
? 'dated'
|
|
410
|
-
: props.needsReauth
|
|
411
|
-
? 're-login'
|
|
412
|
-
: props.vaultReauth
|
|
413
|
-
? 'needs sign-in in the vault'
|
|
414
|
-
: props.active
|
|
415
|
-
? 'active'
|
|
416
|
-
: 'idle'
|
|
417
|
-
const statusTone = (): Tone =>
|
|
418
|
-
props.needsReauth || props.vaultReauth || props.stateLine
|
|
419
|
-
? 'warn'
|
|
420
|
-
: props.active
|
|
421
|
-
? 'ok'
|
|
422
|
-
: 'muted'
|
|
423
|
-
const pacingFor = (
|
|
424
|
-
window:
|
|
425
|
-
| { usedPercent: number; remainingPercent: number; resetsAt?: string }
|
|
426
|
-
| undefined,
|
|
427
|
-
windowMs: number,
|
|
428
|
-
) =>
|
|
429
|
-
props.pacingEnabled && !props.quotaDated && window
|
|
430
|
-
? computeQuotaPacing(window, windowMs, Date.now())
|
|
431
|
-
: null
|
|
432
|
-
const parts = () => {
|
|
433
|
-
const now = Date.now()
|
|
434
|
-
return quotaWindowParts(quotaDisplayProjection(props.quota, now), { now })
|
|
435
|
-
}
|
|
436
|
-
const partFor = (name: string) =>
|
|
437
|
-
parts().windows.find((part) => part.name === name)
|
|
438
|
-
return (
|
|
439
|
-
<box width='100%' flexDirection='column' marginTop={props.marginTop ?? 0}>
|
|
440
|
-
<box width='100%' flexDirection='row' justifyContent='space-between'>
|
|
441
|
-
<text fg={props.theme.text}>
|
|
442
|
-
<b>{`${props.name}${props.tierLabel ? ` · ${props.tierLabel}` : ''}`}</b>
|
|
443
|
-
</text>
|
|
444
|
-
<text fg={toneColor(props.theme, statusTone())}>
|
|
445
|
-
<b>{statusWord()}</b>
|
|
446
|
-
</text>
|
|
447
|
-
</box>
|
|
448
|
-
<Show when={props.stateLine}>
|
|
449
|
-
<text fg={props.theme.textMuted}>{props.stateLine}</text>
|
|
450
|
-
</Show>
|
|
451
|
-
<Show
|
|
452
|
-
when={props.quota}
|
|
453
|
-
fallback={<text fg={props.theme.textMuted}>{'checking\u2026'}</text>}
|
|
454
|
-
>
|
|
455
|
-
<QuotaRow
|
|
456
|
-
theme={props.theme}
|
|
457
|
-
appearance={props.appearance}
|
|
458
|
-
label='5h'
|
|
459
|
-
part={partFor('5h')}
|
|
460
|
-
usedPercent={props.quota?.five_hour?.usedPercent}
|
|
461
|
-
pacing={pacingFor(props.quota?.five_hour, FIVE_HOUR_MS)}
|
|
462
|
-
binding={props.quota?.bindingWindow === 'five_hour'}
|
|
463
|
-
/>
|
|
464
|
-
<QuotaRow
|
|
465
|
-
theme={props.theme}
|
|
466
|
-
appearance={props.appearance}
|
|
467
|
-
label='7d'
|
|
468
|
-
part={partFor('7d')}
|
|
469
|
-
usedPercent={props.quota?.seven_day?.usedPercent}
|
|
470
|
-
pacing={pacingFor(props.quota?.seven_day, SEVEN_DAY_MS)}
|
|
471
|
-
binding={props.quota?.bindingWindow === 'seven_day'}
|
|
472
|
-
/>
|
|
473
|
-
<For each={props.quota?.scoped ?? []}>
|
|
474
|
-
{(window) => (
|
|
475
|
-
<QuotaRow
|
|
476
|
-
theme={props.theme}
|
|
477
|
-
appearance={props.appearance}
|
|
478
|
-
label={partFor(window.title)?.name ?? window.title}
|
|
479
|
-
part={partFor(window.title)}
|
|
480
|
-
usedPercent={window.usedPercent}
|
|
481
|
-
pacing={pacingFor(window, SEVEN_DAY_MS)}
|
|
482
|
-
binding={props.quota?.bindingWindow === window.id}
|
|
483
|
-
/>
|
|
484
|
-
)}
|
|
485
|
-
</For>
|
|
486
|
-
<Show when={partFor('credits')}>
|
|
487
|
-
{(budget: () => QuotaWindowPart) => {
|
|
488
|
-
// The line box's laid-out width, read when it mounts and on each
|
|
489
|
-
// resize. It is undefined (amounts keep their cents) until layout.
|
|
490
|
-
const [width, setWidth] = createSignal<number>()
|
|
491
|
-
let line: BoxRenderable | undefined
|
|
492
|
-
return (
|
|
493
|
-
<box
|
|
494
|
-
width='100%'
|
|
495
|
-
ref={(element: BoxRenderable) => {
|
|
496
|
-
line = element
|
|
497
|
-
setWidth(element.width || undefined)
|
|
498
|
-
}}
|
|
499
|
-
onSizeChange={() => setWidth(line?.width || undefined)}
|
|
500
|
-
>
|
|
501
|
-
<text
|
|
502
|
-
fg={toneColor(
|
|
503
|
-
props.theme,
|
|
504
|
-
props.quota?.extraUsage?.severity === 'critical'
|
|
505
|
-
? 'err'
|
|
506
|
-
: props.quota?.extraUsage?.severity === 'warning' ||
|
|
507
|
-
props.quota?.extraUsage?.exhausted
|
|
508
|
-
? 'warn'
|
|
509
|
-
: 'muted',
|
|
510
|
-
)}
|
|
511
|
-
>
|
|
512
|
-
{creditLine(
|
|
513
|
-
budget().figure,
|
|
514
|
-
props.quota?.extraUsage,
|
|
515
|
-
width(),
|
|
516
|
-
)}
|
|
517
|
-
</text>
|
|
518
|
-
</box>
|
|
519
|
-
)
|
|
520
|
-
}}
|
|
521
|
-
</Show>
|
|
522
|
-
<Show when={parts().age}>
|
|
523
|
-
<text fg={props.theme.textMuted}>{parts().age}</text>
|
|
524
|
-
</Show>
|
|
525
|
-
<Show when={props.quota?.fallbackAdvised === true}>
|
|
526
|
-
<text fg={props.theme.textMuted}>{'→ fallback advised'}</text>
|
|
527
|
-
</Show>
|
|
528
|
-
</Show>
|
|
529
|
-
</box>
|
|
530
|
-
)
|
|
531
|
-
}
|
|
532
|
-
|
|
533
39
|
// --- Quota dialog content ---------------------------------------------------
|
|
534
40
|
|
|
535
41
|
// Renders the same rich visualization as the sidebar (account blocks with
|
|
@@ -576,6 +82,7 @@ function QuotaDialogContent(props: {
|
|
|
576
82
|
tierLabel={state().main?.tierLabel}
|
|
577
83
|
stateLine={state().main?.stateLine}
|
|
578
84
|
quotaDated={state().main?.quotaDated}
|
|
85
|
+
now={Date.now()}
|
|
579
86
|
/>
|
|
580
87
|
<Show when={prefs().sections.fallbackAccounts}>
|
|
581
88
|
<For each={enabledFallbacks()}>
|
|
@@ -593,6 +100,7 @@ function QuotaDialogContent(props: {
|
|
|
593
100
|
stateLine={fb.stateLine}
|
|
594
101
|
quotaDated={fb.quotaDated}
|
|
595
102
|
marginTop={1}
|
|
103
|
+
now={Date.now()}
|
|
596
104
|
/>
|
|
597
105
|
)}
|
|
598
106
|
</For>
|
|
@@ -608,77 +116,12 @@ async function readStateFromFile(): Promise<SidebarState> {
|
|
|
608
116
|
return getSidebarState()
|
|
609
117
|
}
|
|
610
118
|
|
|
611
|
-
interface SidebarController {
|
|
612
|
-
prefs: () => AnthropicAuthTuiPrefs
|
|
613
|
-
collapsed: () => boolean
|
|
614
|
-
toggleCollapsed: () => void
|
|
615
|
-
}
|
|
616
|
-
|
|
617
|
-
// The TUI may unmount and remount sidebar_content when the user switches
|
|
618
|
-
// views (e.g. main -> subagent -> main). A remount re-runs the component
|
|
619
|
-
// body, so any signal created inside the component would reset to its
|
|
620
|
-
// seed. The controller lives in the plugin closure (process lifetime)
|
|
621
|
-
// and owns the durable prefs/collapse signals plus the single shared
|
|
622
|
-
// watcher subscription, so collapse and live pref reloads survive the
|
|
623
|
-
// remount. No effects or memos here — those need a Solid owner, so the
|
|
624
|
-
// poll-interval createEffect stays inside the component.
|
|
625
|
-
function createSidebarController(
|
|
626
|
-
initialPrefs: AnthropicAuthTuiPrefs,
|
|
627
|
-
): SidebarController {
|
|
628
|
-
const [prefs, setPrefs] = createSignal<AnthropicAuthTuiPrefs>(initialPrefs)
|
|
629
|
-
const seedCollapsed =
|
|
630
|
-
initialPrefs.rememberCollapsed && initialPrefs.collapsed != null
|
|
631
|
-
? initialPrefs.collapsed
|
|
632
|
-
: initialPrefs.startCollapsed
|
|
633
|
-
const [collapsed, setCollapsed] = createSignal(seedCollapsed)
|
|
634
|
-
let lastPersistedCollapsed: boolean | null = initialPrefs.collapsed
|
|
635
|
-
let lastApplied = JSON.stringify(initialPrefs)
|
|
636
|
-
|
|
637
|
-
// The watcher lives for the plugin/process lifetime — it is intentionally
|
|
638
|
-
// never disposed. Collapse guard mirrors the race-fix in toggleCollapsed:
|
|
639
|
-
// lastPersistedCollapsed is advanced only once our own write lands, so
|
|
640
|
-
// watcher echoes of the previous persisted value are rejected by the
|
|
641
|
-
// `!==` check and cannot revert a user click.
|
|
642
|
-
watchTuiPreferences(() => {
|
|
643
|
-
void (async () => {
|
|
644
|
-
const next = resolveAnthropicAuthPrefs(await readTuiPreferencesFile())
|
|
645
|
-
const serialized = JSON.stringify(next)
|
|
646
|
-
if (serialized === lastApplied) return
|
|
647
|
-
lastApplied = serialized
|
|
648
|
-
setPrefs(next)
|
|
649
|
-
if (
|
|
650
|
-
next.rememberCollapsed &&
|
|
651
|
-
next.collapsed != null &&
|
|
652
|
-
next.collapsed !== lastPersistedCollapsed
|
|
653
|
-
) {
|
|
654
|
-
lastPersistedCollapsed = next.collapsed
|
|
655
|
-
setCollapsed(next.collapsed)
|
|
656
|
-
}
|
|
657
|
-
})()
|
|
658
|
-
})
|
|
659
|
-
|
|
660
|
-
function toggleCollapsed() {
|
|
661
|
-
const next = !collapsed()
|
|
662
|
-
setCollapsed(next)
|
|
663
|
-
if (prefs().rememberCollapsed) {
|
|
664
|
-
void queueTuiPreferenceUpdate(PLUGIN_KEY, ['collapsed'], next).then(
|
|
665
|
-
() => {
|
|
666
|
-
lastPersistedCollapsed = next
|
|
667
|
-
},
|
|
668
|
-
)
|
|
669
|
-
}
|
|
670
|
-
}
|
|
671
|
-
|
|
672
|
-
return { prefs, collapsed, toggleCollapsed }
|
|
673
|
-
}
|
|
674
|
-
|
|
675
119
|
function QuotaSidebar(props: {
|
|
676
120
|
api: TuiPluginApi
|
|
677
121
|
controller: SidebarController
|
|
678
122
|
sessionId: string
|
|
679
123
|
}) {
|
|
680
124
|
const prefs = props.controller.prefs
|
|
681
|
-
const collapsed = props.controller.collapsed
|
|
682
125
|
const [state, setState] = createSignal<SidebarState>(DEFAULT_SIDEBAR_STATE)
|
|
683
126
|
let lastUpdated = 0
|
|
684
127
|
let debounce: ReturnType<typeof setTimeout> | null = null
|
|
@@ -716,327 +159,13 @@ function QuotaSidebar(props: {
|
|
|
716
159
|
if (debounce) clearTimeout(debounce)
|
|
717
160
|
for (const u of unsubs) u()
|
|
718
161
|
})
|
|
719
|
-
|
|
720
|
-
const theme = () => props.api.theme.current
|
|
721
|
-
const enabledFallbacks = () =>
|
|
722
|
-
(state().fallbacks ?? []).filter(
|
|
723
|
-
(f) => f.enabled || (f.custodyState && f.custodyState !== 'off'),
|
|
724
|
-
)
|
|
725
|
-
const hasData = () =>
|
|
726
|
-
state().main?.quota != null ||
|
|
727
|
-
state().main?.stateLine != null ||
|
|
728
|
-
enabledFallbacks().length > 0
|
|
729
|
-
|
|
730
|
-
const headerLabel = () => {
|
|
731
|
-
const name = prefs().header.label
|
|
732
|
-
return !hasData() ? name : collapsed() ? `\u25b6 ${name}` : `\u25bc ${name}`
|
|
733
|
-
}
|
|
734
|
-
const activeAccount = () => resolveActiveAccount(state())
|
|
735
|
-
const activeQuotaSummary = () =>
|
|
736
|
-
getCollapsedQuotaSummary(activeAccount().quota)
|
|
737
|
-
const activePacingDeficit = () => {
|
|
738
|
-
if (!prefs().sections.pacing) return false
|
|
739
|
-
if (activeAccount().quotaDated) return null
|
|
740
|
-
const quota = activeAccount().quota
|
|
741
|
-
if (!quota) return false
|
|
742
|
-
const now = Date.now()
|
|
743
|
-
const windows: Array<
|
|
744
|
-
[
|
|
745
|
-
(
|
|
746
|
-
| { usedPercent: number; remainingPercent: number; resetsAt?: string }
|
|
747
|
-
| undefined
|
|
748
|
-
),
|
|
749
|
-
number,
|
|
750
|
-
]
|
|
751
|
-
> = [
|
|
752
|
-
[quota.five_hour, FIVE_HOUR_MS],
|
|
753
|
-
[quota.seven_day, SEVEN_DAY_MS],
|
|
754
|
-
...(quota.scoped ?? []).map((window): [typeof window, number] => [
|
|
755
|
-
window,
|
|
756
|
-
SEVEN_DAY_MS,
|
|
757
|
-
]),
|
|
758
|
-
]
|
|
759
|
-
return windows.some(
|
|
760
|
-
([w, ms]) =>
|
|
761
|
-
w != null && computeQuotaPacing(w, ms, now)?.state === 'deficit',
|
|
762
|
-
)
|
|
763
|
-
}
|
|
764
|
-
const activeQuotaTone = (): Tone => {
|
|
765
|
-
const summary = activeQuotaSummary()
|
|
766
|
-
const values = [
|
|
767
|
-
summary.fiveHourUsedPercent,
|
|
768
|
-
summary.sevenDayUsedPercent,
|
|
769
|
-
...summary.scopedUsedPercents,
|
|
770
|
-
].filter((value): value is number => value != null)
|
|
771
|
-
// Pacing deficit is an advisory projection, not actual quota exhaustion,
|
|
772
|
-
// so it can only BUMP the usage tone up to warn at most — never soften a
|
|
773
|
-
// real warn/err usage reading and never paint a true red.
|
|
774
|
-
const base: Tone =
|
|
775
|
-
values.length > 0
|
|
776
|
-
? usageTone(Math.max(...values), prefs().appearance)
|
|
777
|
-
: 'muted'
|
|
778
|
-
if (!activePacingDeficit()) return base
|
|
779
|
-
return base === 'ok' || base === 'muted' ? 'warn' : base
|
|
780
|
-
}
|
|
781
|
-
|
|
782
|
-
const quotaBackedOff = () => state().main?.quotaBackedOff === true
|
|
783
|
-
const refreshBackedOff = () => state().main?.refreshBackedOff === true
|
|
784
|
-
const needsReauth = () => enabledFallbacks().some((f) => f.needsReauth)
|
|
785
|
-
const vaultReauth = () => enabledFallbacks().some((f) => f.vaultReauth)
|
|
786
|
-
const degraded = () =>
|
|
787
|
-
quotaBackedOff() || refreshBackedOff() || needsReauth() || vaultReauth()
|
|
788
|
-
|
|
789
|
-
const fableRecoverySummary = () =>
|
|
790
|
-
getFableRecoverySummary(state(), props.sessionId)
|
|
791
|
-
const fableRecoveryTone = (): Tone =>
|
|
792
|
-
state().fableRecoveries?.find(
|
|
793
|
-
(recovery) => recovery.sessionId === props.sessionId,
|
|
794
|
-
)?.mode === 'fable'
|
|
795
|
-
? 'ok'
|
|
796
|
-
: 'warn'
|
|
797
|
-
const cacheKeep = () => state().cacheKeep
|
|
798
|
-
const showCache = () =>
|
|
799
|
-
prefs().sections.cache && cacheKeep() != null && cacheKeep()?.window != null
|
|
800
|
-
const prime = () => state().prime
|
|
801
|
-
const showPrime = () => prime() != null
|
|
802
|
-
const primeHasError = () =>
|
|
803
|
-
prime()?.accounts.some((account) => account.lastResult === 'error') ?? false
|
|
804
|
-
const relayValue = () => {
|
|
805
|
-
const r = state().relay
|
|
806
|
-
if (!r) return '\u2014'
|
|
807
|
-
return `${r.transport} \u00b7 ${r.enabled ? 'on' : 'off'}`
|
|
808
|
-
}
|
|
809
|
-
|
|
810
162
|
return (
|
|
811
|
-
<
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
paddingBottom={1}
|
|
818
|
-
paddingLeft={1}
|
|
819
|
-
paddingRight={1}
|
|
820
|
-
>
|
|
821
|
-
{/* Header: ▼/▶ CLAUDE badge (click to collapse) + version, or LIMITED badge */}
|
|
822
|
-
{/* biome-ignore lint/a11y/noStaticElementInteractions: opentui renders to a terminal, not the DOM — ARIA roles do not apply */}
|
|
823
|
-
<box
|
|
824
|
-
width='100%'
|
|
825
|
-
flexDirection='row'
|
|
826
|
-
justifyContent='space-between'
|
|
827
|
-
alignItems='center'
|
|
828
|
-
onMouseDown={() => {
|
|
829
|
-
if (hasData()) props.controller.toggleCollapsed()
|
|
830
|
-
}}
|
|
831
|
-
>
|
|
832
|
-
<box paddingLeft={1} paddingRight={1} backgroundColor={theme().accent}>
|
|
833
|
-
<text fg={theme().background}>
|
|
834
|
-
<b>{headerLabel()}</b>
|
|
835
|
-
</text>
|
|
836
|
-
</box>
|
|
837
|
-
<Show
|
|
838
|
-
when={degraded()}
|
|
839
|
-
fallback={
|
|
840
|
-
<Show when={prefs().header.showVersion && PLUGIN_VERSION !== ''}>
|
|
841
|
-
<text fg={theme().textMuted}>{`v${PLUGIN_VERSION}`}</text>
|
|
842
|
-
</Show>
|
|
843
|
-
}
|
|
844
|
-
>
|
|
845
|
-
<box
|
|
846
|
-
paddingLeft={1}
|
|
847
|
-
paddingRight={1}
|
|
848
|
-
backgroundColor={theme().warning}
|
|
849
|
-
>
|
|
850
|
-
<text fg={theme().background}>
|
|
851
|
-
<b>{'LIMITED'}</b>
|
|
852
|
-
</text>
|
|
853
|
-
</box>
|
|
854
|
-
</Show>
|
|
855
|
-
</box>
|
|
856
|
-
|
|
857
|
-
{/* Collapsed accounts keep placement order; only a fresh active row is emphasized. */}
|
|
858
|
-
<Show when={collapsed() && hasData()}>
|
|
859
|
-
{/* Index keys rows by position: each state update builds new account
|
|
860
|
-
objects, so For would rebuild every row and its measured width.
|
|
861
|
-
Passing the accessor's value as a prop keeps the row reactive. */}
|
|
862
|
-
<Index
|
|
863
|
-
each={getCollapsedAccounts(
|
|
864
|
-
state(),
|
|
865
|
-
prefs().sections.fallbackAccounts,
|
|
866
|
-
)}
|
|
867
|
-
>
|
|
868
|
-
{(account) => (
|
|
869
|
-
<CollapsedAccountRow
|
|
870
|
-
theme={theme()}
|
|
871
|
-
account={account()}
|
|
872
|
-
tone={activeQuotaTone()}
|
|
873
|
-
/>
|
|
874
|
-
)}
|
|
875
|
-
</Index>
|
|
876
|
-
<Show when={state().fastMode}>
|
|
877
|
-
<CollapsedRow theme={theme()} label='Mode'>
|
|
878
|
-
<text fg={toneColor(theme(), 'accent')}>
|
|
879
|
-
<b>{'fast'}</b>
|
|
880
|
-
</text>
|
|
881
|
-
</CollapsedRow>
|
|
882
|
-
</Show>
|
|
883
|
-
<Show when={fableRecoverySummary()}>
|
|
884
|
-
{(summary: () => string) => (
|
|
885
|
-
<CollapsedRow theme={theme()} label='Recovery'>
|
|
886
|
-
<text fg={toneColor(theme(), fableRecoveryTone())}>
|
|
887
|
-
<b>{summary()}</b>
|
|
888
|
-
</text>
|
|
889
|
-
</CollapsedRow>
|
|
890
|
-
)}
|
|
891
|
-
</Show>
|
|
892
|
-
</Show>
|
|
893
|
-
|
|
894
|
-
{/* Expanded: full sections. Also render when there's no data so the
|
|
895
|
-
sidebar can never go blank if data clears while collapsed. */}
|
|
896
|
-
<Show when={!collapsed() || !hasData()}>
|
|
897
|
-
<Show
|
|
898
|
-
when={hasData()}
|
|
899
|
-
fallback={
|
|
900
|
-
<box marginTop={1} width='100%'>
|
|
901
|
-
<text fg={theme().textMuted}>{'Waiting for quota\u2026'}</text>
|
|
902
|
-
</box>
|
|
903
|
-
}
|
|
904
|
-
>
|
|
905
|
-
{/* Quota */}
|
|
906
|
-
<Show when={prefs().sections.quota}>
|
|
907
|
-
<SectionHeader theme={theme()} title='Quota' />
|
|
908
|
-
<AccountBlock
|
|
909
|
-
theme={theme()}
|
|
910
|
-
appearance={prefs().appearance}
|
|
911
|
-
name='main'
|
|
912
|
-
quota={state().main?.quota}
|
|
913
|
-
active={state().activeId === 'main'}
|
|
914
|
-
pacingEnabled={prefs().sections.pacing}
|
|
915
|
-
tierLabel={state().main?.tierLabel}
|
|
916
|
-
stateLine={state().main?.stateLine}
|
|
917
|
-
quotaDated={state().main?.quotaDated}
|
|
918
|
-
/>
|
|
919
|
-
<Show when={prefs().sections.fallbackAccounts}>
|
|
920
|
-
<For each={enabledFallbacks()}>
|
|
921
|
-
{(fb) => (
|
|
922
|
-
<AccountBlock
|
|
923
|
-
theme={theme()}
|
|
924
|
-
appearance={prefs().appearance}
|
|
925
|
-
name={fb.label ?? fb.id}
|
|
926
|
-
quota={fb.quota}
|
|
927
|
-
active={state().activeId === fb.id}
|
|
928
|
-
pacingEnabled={prefs().sections.pacing}
|
|
929
|
-
tierLabel={fb.tierLabel}
|
|
930
|
-
needsReauth={fb.needsReauth}
|
|
931
|
-
vaultReauth={fb.vaultReauth}
|
|
932
|
-
stateLine={fb.stateLine}
|
|
933
|
-
quotaDated={fb.quotaDated}
|
|
934
|
-
marginTop={1}
|
|
935
|
-
/>
|
|
936
|
-
)}
|
|
937
|
-
</For>
|
|
938
|
-
</Show>
|
|
939
|
-
</Show>
|
|
940
|
-
</Show>
|
|
941
|
-
|
|
942
|
-
{/* Routing */}
|
|
943
|
-
<Show when={prefs().sections.routing}>
|
|
944
|
-
<SectionHeader theme={theme()} title='Routing' />
|
|
945
|
-
<StatRow
|
|
946
|
-
theme={theme()}
|
|
947
|
-
label='Route'
|
|
948
|
-
value={state().route}
|
|
949
|
-
tone='accent'
|
|
950
|
-
/>
|
|
951
|
-
<StatRow
|
|
952
|
-
theme={theme()}
|
|
953
|
-
label='Mode'
|
|
954
|
-
value={state().fastMode ? 'fast' : 'std'}
|
|
955
|
-
tone={state().fastMode ? 'accent' : 'muted'}
|
|
956
|
-
/>
|
|
957
|
-
<StatRow
|
|
958
|
-
theme={theme()}
|
|
959
|
-
label='Relay'
|
|
960
|
-
value={relayValue()}
|
|
961
|
-
tone={state().relay?.enabled ? 'ok' : 'muted'}
|
|
962
|
-
/>
|
|
963
|
-
</Show>
|
|
964
|
-
|
|
965
|
-
<Show when={fableRecoverySummary()}>
|
|
966
|
-
{(summary: () => string) => (
|
|
967
|
-
<StatRow
|
|
968
|
-
theme={theme()}
|
|
969
|
-
label='Recovery'
|
|
970
|
-
value={summary()}
|
|
971
|
-
tone={fableRecoveryTone()}
|
|
972
|
-
/>
|
|
973
|
-
)}
|
|
974
|
-
</Show>
|
|
975
|
-
|
|
976
|
-
{/* Cache */}
|
|
977
|
-
<Show when={showCache()}>
|
|
978
|
-
<SectionHeader theme={theme()} title='Cache' />
|
|
979
|
-
<StatRow
|
|
980
|
-
theme={theme()}
|
|
981
|
-
label='1h cache'
|
|
982
|
-
value={`${cacheKeep()?.window} \u00b7 ${cacheKeep()?.enabled ? 'on' : 'off'}`}
|
|
983
|
-
tone={cacheKeep()?.enabled ? 'ok' : 'muted'}
|
|
984
|
-
/>
|
|
985
|
-
<Show when={(cacheKeep()?.trackedSessions ?? 0) > 0}>
|
|
986
|
-
<StatRow
|
|
987
|
-
theme={theme()}
|
|
988
|
-
label='Tracked'
|
|
989
|
-
value={String(cacheKeep()?.trackedSessions)}
|
|
990
|
-
tone='text'
|
|
991
|
-
/>
|
|
992
|
-
</Show>
|
|
993
|
-
</Show>
|
|
994
|
-
|
|
995
|
-
{/* Prime — only when enabled. Expanded view only (no collapsed surface). */}
|
|
996
|
-
<Show when={showPrime()}>
|
|
997
|
-
<StatRow
|
|
998
|
-
theme={theme()}
|
|
999
|
-
label='Prime'
|
|
1000
|
-
value={primeHasError() ? 'err' : 'on'}
|
|
1001
|
-
tone={primeHasError() ? 'warn' : 'ok'}
|
|
1002
|
-
/>
|
|
1003
|
-
<For each={prime()?.accounts ?? []}>
|
|
1004
|
-
{(account) => {
|
|
1005
|
-
const value = () => formatPrimeAccountValue(account)
|
|
1006
|
-
return (
|
|
1007
|
-
<StatRow
|
|
1008
|
-
theme={theme()}
|
|
1009
|
-
label={account.label}
|
|
1010
|
-
value={value().text}
|
|
1011
|
-
tone={value().hasError ? 'warn' : 'text'}
|
|
1012
|
-
/>
|
|
1013
|
-
)
|
|
1014
|
-
}}
|
|
1015
|
-
</For>
|
|
1016
|
-
</Show>
|
|
1017
|
-
|
|
1018
|
-
{/* Health — only when something is wrong */}
|
|
1019
|
-
<Show when={degraded() && prefs().sections.health}>
|
|
1020
|
-
<SectionHeader theme={theme()} title='Health' />
|
|
1021
|
-
<Show when={quotaBackedOff()}>
|
|
1022
|
-
<StatRow
|
|
1023
|
-
theme={theme()}
|
|
1024
|
-
label='Quota API'
|
|
1025
|
-
value={`backoff ${formatUntil(state().main?.quotaBackoffUntil)}`}
|
|
1026
|
-
tone='warn'
|
|
1027
|
-
/>
|
|
1028
|
-
</Show>
|
|
1029
|
-
<Show when={refreshBackedOff()}>
|
|
1030
|
-
<StatRow
|
|
1031
|
-
theme={theme()}
|
|
1032
|
-
label='Token refresh'
|
|
1033
|
-
value={`backoff ${formatUntil(state().main?.refreshBackoffUntil)}`}
|
|
1034
|
-
tone='warn'
|
|
1035
|
-
/>
|
|
1036
|
-
</Show>
|
|
1037
|
-
</Show>
|
|
1038
|
-
</Show>
|
|
1039
|
-
</box>
|
|
163
|
+
<ClaudeSidebar
|
|
164
|
+
state={state}
|
|
165
|
+
theme={() => props.api.theme.current}
|
|
166
|
+
controller={props.controller}
|
|
167
|
+
sessionId={props.sessionId}
|
|
168
|
+
/>
|
|
1040
169
|
)
|
|
1041
170
|
}
|
|
1042
171
|
|