@cortexkit/opencode-anthropic-auth 2.2.7 → 2.3.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 +43 -42
- package/dist/index-2jsr8wvj.js +43 -0
- package/dist/index-46e2gk3c.js +2 -0
- package/dist/{index-62x1p34p.js → index-71temqvq.js} +1 -1
- package/dist/{index-ppdrzs41.js → index-bcp23wwb.js} +1 -1
- package/dist/{index-hazrk5c3.js → index-c8ehrd0p.js} +1 -1
- package/dist/index-dpnhamzw.js +2 -0
- package/dist/{index-0kxy8k2c.js → index-m4dazzyf.js} +1 -1
- package/dist/{index-b1dvkp15.js → index-naff9h33.js} +1 -1
- package/dist/{index-6chd6jnr.js → index-sg0knfec.js} +1 -1
- package/dist/index.js +1 -1
- package/dist/oauth-destination.d.ts +35 -0
- package/dist/plugin-test-handles.d.ts +26 -0
- 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 +6 -3
- package/dist/sidebar-state.js +1 -1
- package/dist/tui/claude-sidebar.d.ts +58 -0
- package/dist/tui/menu-status.d.ts +70 -0
- package/dist/tui-preferences.js +2 -2
- package/dist/v2/adapter.d.ts +13 -8
- package/dist/v2/server.js +10 -10
- package/dist/v2/tui.d.ts +12 -0
- package/dist/v2/tui.js +4 -5
- package/package.json +2 -2
- package/src/sidebar-state.ts +55 -46
- package/src/tui/claude-sidebar.tsx +155 -15
- package/src/tui/entry.mjs +13 -0
- package/src/tui/menu-status.tsx +300 -0
- package/src/tui-compiled/sidebar-state.ts +55 -46
- package/src/tui-compiled/tui/claude-sidebar.tsx +112 -12
- package/src/tui-compiled/tui/menu-status.tsx +388 -0
- package/src/tui-compiled/tui-preferences.ts +32 -3
- package/src/tui-preferences.ts +32 -3
- package/dist/index-20d6rmjj.js +0 -2
- package/dist/index-cjjjgewq.js +0 -43
- package/dist/index-jvhs8sxv.js +0 -2
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
/** @jsxImportSource @opentui/solid */
|
|
2
|
+
|
|
3
|
+
// The status view of the OpenCode 2 `/claude` menu. Core writes a section's
|
|
4
|
+
// status as plain lines, some of them Markdown: the quota summary has `##`
|
|
5
|
+
// headings and indented `-` bullets. This view draws headings and bullets as
|
|
6
|
+
// styled text instead of showing their markers. For the Quota section it draws
|
|
7
|
+
// the account blocks of OpenCode 1's quota view, read from the same server
|
|
8
|
+
// state as the sidebar, and falls back to the text when that read fails.
|
|
9
|
+
|
|
10
|
+
import { TextAttributes } from '@opentui/core'
|
|
11
|
+
import { createSignal, For, type JSX, Show } from 'solid-js'
|
|
12
|
+
import type { SidebarState } from '../sidebar-state.js'
|
|
13
|
+
import {
|
|
14
|
+
type AnthropicAuthTuiPrefs,
|
|
15
|
+
readTuiPreferencesFile,
|
|
16
|
+
resolveAnthropicAuthPrefs,
|
|
17
|
+
} from '../tui-preferences.js'
|
|
18
|
+
import {
|
|
19
|
+
AccountBlock,
|
|
20
|
+
type HostTheme,
|
|
21
|
+
type SidebarTheme,
|
|
22
|
+
sidebarThemeFromHost,
|
|
23
|
+
} from './claude-sidebar.js'
|
|
24
|
+
|
|
25
|
+
/** One status line, classified for drawing. */
|
|
26
|
+
export type StatusLine =
|
|
27
|
+
| { readonly kind: 'heading'; readonly level: number; readonly text: string }
|
|
28
|
+
| { readonly kind: 'bullet'; readonly indent: number; readonly text: string }
|
|
29
|
+
| { readonly kind: 'text'; readonly text: string }
|
|
30
|
+
| { readonly kind: 'blank' }
|
|
31
|
+
|
|
32
|
+
// The widest indent kept for a bullet; deeper nesting is drawn at this indent.
|
|
33
|
+
const MAX_BULLET_INDENT = 8
|
|
34
|
+
|
|
35
|
+
// The width of OpenCode 1's quota view (`QuotaDialogContent` in `tui.tsx`).
|
|
36
|
+
const QUOTA_VIEW_WIDTH = 58
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Splits a section's status lines (each may hold several lines) and classifies
|
|
40
|
+
* each one. Only line-leading Markdown is read: `#` headings and `-`, `*` or
|
|
41
|
+
* `+` bullets. Everything else, including any `*` inside a line, stays as
|
|
42
|
+
* written. Runs of blank lines collapse to one, and leading or trailing blank
|
|
43
|
+
* lines are dropped.
|
|
44
|
+
*/
|
|
45
|
+
export function parseStatusLines(lines: readonly string[]): StatusLine[] {
|
|
46
|
+
const parsed: StatusLine[] = []
|
|
47
|
+
for (const raw of lines.join('\n').split(/\r?\n/)) {
|
|
48
|
+
const line = raw.trimEnd()
|
|
49
|
+
if (line.trim() === '') {
|
|
50
|
+
if (parsed.length && parsed.at(-1)?.kind !== 'blank')
|
|
51
|
+
parsed.push({ kind: 'blank' })
|
|
52
|
+
continue
|
|
53
|
+
}
|
|
54
|
+
const heading = /^\s{0,3}(#{1,6})\s+(.+?)(?:\s+#+)?$/.exec(line)
|
|
55
|
+
if (heading?.[1] && heading[2]) {
|
|
56
|
+
parsed.push({
|
|
57
|
+
kind: 'heading',
|
|
58
|
+
level: heading[1].length,
|
|
59
|
+
text: heading[2],
|
|
60
|
+
})
|
|
61
|
+
continue
|
|
62
|
+
}
|
|
63
|
+
const bullet = /^(\s*)[-*+]\s+(.+)$/.exec(line)
|
|
64
|
+
if (bullet?.[2]) {
|
|
65
|
+
parsed.push({
|
|
66
|
+
kind: 'bullet',
|
|
67
|
+
indent: Math.min(bullet[1]?.length ?? 0, MAX_BULLET_INDENT),
|
|
68
|
+
text: bullet[2],
|
|
69
|
+
})
|
|
70
|
+
continue
|
|
71
|
+
}
|
|
72
|
+
parsed.push({ kind: 'text', text: line })
|
|
73
|
+
}
|
|
74
|
+
if (parsed.at(-1)?.kind === 'blank') parsed.pop()
|
|
75
|
+
return parsed
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Draws one status line; headings and bullets lose their Markdown markers. */
|
|
79
|
+
function StatusRow(props: {
|
|
80
|
+
readonly line: StatusLine
|
|
81
|
+
readonly theme: SidebarTheme
|
|
82
|
+
}): JSX.Element {
|
|
83
|
+
const line = props.line
|
|
84
|
+
switch (line.kind) {
|
|
85
|
+
case 'heading':
|
|
86
|
+
return (
|
|
87
|
+
<text
|
|
88
|
+
// Top-level headings take the accent color; lower levels, such as
|
|
89
|
+
// one account in the quota summary, stay in the text color.
|
|
90
|
+
fg={line.level <= 2 ? props.theme.accent : props.theme.text}
|
|
91
|
+
attributes={TextAttributes.BOLD}
|
|
92
|
+
>
|
|
93
|
+
{line.text}
|
|
94
|
+
</text>
|
|
95
|
+
)
|
|
96
|
+
case 'bullet':
|
|
97
|
+
return (
|
|
98
|
+
// The text sits in its own column, so a long bullet wraps under its
|
|
99
|
+
// first word instead of under the bullet mark.
|
|
100
|
+
<box flexDirection='row' paddingLeft={line.indent}>
|
|
101
|
+
<text fg={props.theme.textMuted} flexShrink={0}>
|
|
102
|
+
{'• '}
|
|
103
|
+
</text>
|
|
104
|
+
<text fg={props.theme.text} flexGrow={1} flexShrink={1}>
|
|
105
|
+
{line.text}
|
|
106
|
+
</text>
|
|
107
|
+
</box>
|
|
108
|
+
)
|
|
109
|
+
case 'text':
|
|
110
|
+
return <text fg={props.theme.text}>{line.text}</text>
|
|
111
|
+
default:
|
|
112
|
+
return <box height={1} />
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** Draws status lines with headings and bullets styled, not as Markdown. */
|
|
117
|
+
export function StatusText(props: {
|
|
118
|
+
readonly lines: readonly string[]
|
|
119
|
+
readonly theme: SidebarTheme
|
|
120
|
+
}): JSX.Element {
|
|
121
|
+
return (
|
|
122
|
+
<box flexDirection='column' width='100%'>
|
|
123
|
+
<For each={parseStatusLines(props.lines)}>
|
|
124
|
+
{(line) => <StatusRow line={line} theme={props.theme} />}
|
|
125
|
+
</For>
|
|
126
|
+
</box>
|
|
127
|
+
)
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* OpenCode 1's quota view: one account block for main and for each enabled or
|
|
132
|
+
* vault-held fallback, as the sidebar draws them.
|
|
133
|
+
*/
|
|
134
|
+
export function QuotaStatus(props: {
|
|
135
|
+
readonly state: SidebarState
|
|
136
|
+
readonly theme: SidebarTheme
|
|
137
|
+
readonly prefs: AnthropicAuthTuiPrefs
|
|
138
|
+
readonly now: number
|
|
139
|
+
}): JSX.Element {
|
|
140
|
+
const fallbacks = () =>
|
|
141
|
+
(props.state.fallbacks ?? []).filter(
|
|
142
|
+
(fallback) =>
|
|
143
|
+
fallback.enabled ||
|
|
144
|
+
(fallback.custodyState && fallback.custodyState !== 'off'),
|
|
145
|
+
)
|
|
146
|
+
return (
|
|
147
|
+
// As wide as OpenCode 1's quota view: wider, the reset times drift far
|
|
148
|
+
// from the bars they belong to.
|
|
149
|
+
<box flexDirection='column' width='100%' maxWidth={QUOTA_VIEW_WIDTH}>
|
|
150
|
+
<AccountBlock
|
|
151
|
+
theme={props.theme}
|
|
152
|
+
appearance={props.prefs.appearance}
|
|
153
|
+
name='main'
|
|
154
|
+
quota={props.state.main?.quota}
|
|
155
|
+
active={props.state.activeId === 'main'}
|
|
156
|
+
pacingEnabled={props.prefs.sections.pacing}
|
|
157
|
+
tierLabel={props.state.main?.tierLabel}
|
|
158
|
+
stateLine={props.state.main?.stateLine}
|
|
159
|
+
quotaDated={props.state.main?.quotaDated}
|
|
160
|
+
now={props.now}
|
|
161
|
+
/>
|
|
162
|
+
<Show when={props.prefs.sections.fallbackAccounts}>
|
|
163
|
+
<For each={fallbacks()}>
|
|
164
|
+
{(fallback) => (
|
|
165
|
+
<AccountBlock
|
|
166
|
+
theme={props.theme}
|
|
167
|
+
appearance={props.prefs.appearance}
|
|
168
|
+
name={fallback.label ?? fallback.id}
|
|
169
|
+
quota={fallback.quota}
|
|
170
|
+
active={props.state.activeId === fallback.id}
|
|
171
|
+
pacingEnabled={props.prefs.sections.pacing}
|
|
172
|
+
tierLabel={fallback.tierLabel}
|
|
173
|
+
needsReauth={fallback.needsReauth}
|
|
174
|
+
vaultReauth={fallback.vaultReauth}
|
|
175
|
+
stateLine={fallback.stateLine}
|
|
176
|
+
quotaDated={fallback.quotaDated}
|
|
177
|
+
marginTop={1}
|
|
178
|
+
now={props.now}
|
|
179
|
+
/>
|
|
180
|
+
)}
|
|
181
|
+
</For>
|
|
182
|
+
</Show>
|
|
183
|
+
</box>
|
|
184
|
+
)
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// --- OpenCode 2 -------------------------------------------------------------
|
|
188
|
+
|
|
189
|
+
/** One section's status, as the `/claude` menu hands it over. */
|
|
190
|
+
export interface MenuStatusView {
|
|
191
|
+
readonly title: string
|
|
192
|
+
readonly lines: readonly string[]
|
|
193
|
+
/**
|
|
194
|
+
* Reads the sidebar's quota state; given for the Quota section only, whose
|
|
195
|
+
* view then draws account blocks instead of the text.
|
|
196
|
+
*/
|
|
197
|
+
readonly readQuota?: (signal: AbortSignal) => Promise<SidebarState>
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/** The OpenCode 2 TUI context members the status dialog uses. */
|
|
201
|
+
export interface MenuStatusHost {
|
|
202
|
+
readonly theme: HostTheme
|
|
203
|
+
readonly ui: {
|
|
204
|
+
readonly dialog: {
|
|
205
|
+
show(render: () => JSX.Element, onClose?: () => void): void
|
|
206
|
+
set(options: { readonly size?: 'medium' | 'large' | 'xlarge' }): void
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
type QuotaView =
|
|
212
|
+
| { readonly kind: 'loading' }
|
|
213
|
+
| {
|
|
214
|
+
readonly kind: 'ready'
|
|
215
|
+
readonly state: SidebarState
|
|
216
|
+
readonly prefs: AnthropicAuthTuiPrefs
|
|
217
|
+
}
|
|
218
|
+
| { readonly kind: 'failed' }
|
|
219
|
+
|
|
220
|
+
type ReadyQuota = Extract<QuotaView, { readonly kind: 'ready' }>
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Shows a section's status in a wide OpenCode 2 dialog. The returned promise
|
|
224
|
+
* settles when the person closes the dialog, or when `signal` aborts.
|
|
225
|
+
*/
|
|
226
|
+
export function showMenuStatusDialog(
|
|
227
|
+
host: MenuStatusHost,
|
|
228
|
+
view: MenuStatusView,
|
|
229
|
+
signal?: AbortSignal,
|
|
230
|
+
): Promise<void> {
|
|
231
|
+
return new Promise((resolve) => {
|
|
232
|
+
if (signal?.aborted) return resolve()
|
|
233
|
+
// Stops the quota read once the dialog closes or the menu goes away.
|
|
234
|
+
const reading = new AbortController()
|
|
235
|
+
const close = () => {
|
|
236
|
+
reading.abort()
|
|
237
|
+
resolve()
|
|
238
|
+
}
|
|
239
|
+
signal?.addEventListener('abort', close, { once: true })
|
|
240
|
+
const [quota, setQuota] = createSignal<QuotaView>({ kind: 'loading' })
|
|
241
|
+
const readQuota = view.readQuota
|
|
242
|
+
if (readQuota)
|
|
243
|
+
void Promise.all([
|
|
244
|
+
readQuota(reading.signal),
|
|
245
|
+
readTuiPreferencesFile().then(resolveAnthropicAuthPrefs),
|
|
246
|
+
]).then(
|
|
247
|
+
([state, prefs]) => {
|
|
248
|
+
if (!reading.signal.aborted) setQuota({ kind: 'ready', state, prefs })
|
|
249
|
+
},
|
|
250
|
+
() => {
|
|
251
|
+
// An older server without the sidebar method, or a failed read:
|
|
252
|
+
// the status text still says everything the blocks would.
|
|
253
|
+
if (!reading.signal.aborted) setQuota({ kind: 'failed' })
|
|
254
|
+
},
|
|
255
|
+
)
|
|
256
|
+
const ready = () => {
|
|
257
|
+
const current = quota()
|
|
258
|
+
return current.kind === 'ready' ? current : undefined
|
|
259
|
+
}
|
|
260
|
+
const theme = () => sidebarThemeFromHost(host.theme)
|
|
261
|
+
host.ui.dialog.show(
|
|
262
|
+
() => (
|
|
263
|
+
<box flexDirection='column' paddingLeft={2} paddingRight={2}>
|
|
264
|
+
<box paddingBottom={1}>
|
|
265
|
+
<text fg={theme().text} attributes={TextAttributes.BOLD}>
|
|
266
|
+
{view.title}
|
|
267
|
+
</text>
|
|
268
|
+
</box>
|
|
269
|
+
<Show
|
|
270
|
+
when={readQuota && quota().kind !== 'failed'}
|
|
271
|
+
fallback={<StatusText lines={view.lines} theme={theme()} />}
|
|
272
|
+
>
|
|
273
|
+
<Show
|
|
274
|
+
when={ready()}
|
|
275
|
+
fallback={
|
|
276
|
+
<text fg={theme().textMuted}>{'Reading quota\u2026'}</text>
|
|
277
|
+
}
|
|
278
|
+
>
|
|
279
|
+
{(ready: () => ReadyQuota) => (
|
|
280
|
+
<QuotaStatus
|
|
281
|
+
state={ready().state}
|
|
282
|
+
prefs={ready().prefs}
|
|
283
|
+
theme={theme()}
|
|
284
|
+
now={Date.now()}
|
|
285
|
+
/>
|
|
286
|
+
)}
|
|
287
|
+
</Show>
|
|
288
|
+
</Show>
|
|
289
|
+
<box paddingTop={1}>
|
|
290
|
+
<text fg={theme().textMuted}>Press Esc to close.</text>
|
|
291
|
+
</box>
|
|
292
|
+
</box>
|
|
293
|
+
),
|
|
294
|
+
close,
|
|
295
|
+
)
|
|
296
|
+
// The widest dialog gives the account blocks their full width and wraps
|
|
297
|
+
// status text as little as it can.
|
|
298
|
+
host.ui.dialog.set({ size: 'xlarge' })
|
|
299
|
+
})
|
|
300
|
+
}
|
|
@@ -30,7 +30,9 @@ export interface AccountQuota {
|
|
|
30
30
|
|
|
31
31
|
export interface SidebarAccountState {
|
|
32
32
|
id: string
|
|
33
|
-
|
|
33
|
+
// Absent, rather than present and `undefined`, when the account has no
|
|
34
|
+
// label: the OpenCode 2 RPC boundary refuses a value carrying `undefined`.
|
|
35
|
+
label?: string
|
|
34
36
|
quota: AccountQuota | null
|
|
35
37
|
enabled: boolean
|
|
36
38
|
stateLine?: string
|
|
@@ -91,7 +93,9 @@ export interface SidebarState {
|
|
|
91
93
|
refreshBackoffUntil?: number
|
|
92
94
|
}
|
|
93
95
|
fallbacks: SidebarAccountState[]
|
|
94
|
-
|
|
96
|
+
// Absent rather than `undefined`: `normalizeSidebarState` never writes the
|
|
97
|
+
// key, and the OpenCode 2 RPC boundary refuses a value carrying `undefined`.
|
|
98
|
+
activeId?: string
|
|
95
99
|
route: string
|
|
96
100
|
relay: { enabled: boolean; transport: string } | null
|
|
97
101
|
fastMode: boolean
|
|
@@ -146,7 +150,6 @@ export function getSidebarStateFile(): string {
|
|
|
146
150
|
export const DEFAULT_SIDEBAR_STATE: SidebarState = {
|
|
147
151
|
main: { quota: null },
|
|
148
152
|
fallbacks: [],
|
|
149
|
-
activeId: undefined,
|
|
150
153
|
route: 'main',
|
|
151
154
|
relay: null,
|
|
152
155
|
fastMode: false,
|
|
@@ -350,40 +353,46 @@ export function normalizeSidebarState(raw: unknown): SidebarState {
|
|
|
350
353
|
}
|
|
351
354
|
if (typeof m.quotaBackedOff === 'boolean')
|
|
352
355
|
main.quotaBackedOff = m.quotaBackedOff
|
|
353
|
-
|
|
356
|
+
// Only finite numbers are display data: the OpenCode 2 RPC boundary
|
|
357
|
+
// refuses NaN and Infinity, so they are dropped like any wrong type.
|
|
358
|
+
if (isFiniteNumber(m.quotaBackoffUntil)) {
|
|
354
359
|
main.quotaBackoffUntil = m.quotaBackoffUntil
|
|
360
|
+
}
|
|
355
361
|
if (typeof m.refreshBackedOff === 'boolean')
|
|
356
362
|
main.refreshBackedOff = m.refreshBackedOff
|
|
357
|
-
if (
|
|
363
|
+
if (isFiniteNumber(m.refreshBackoffUntil)) {
|
|
358
364
|
main.refreshBackoffUntil = m.refreshBackoffUntil
|
|
365
|
+
}
|
|
359
366
|
}
|
|
360
367
|
|
|
361
368
|
const fallbacks: SidebarAccountState[] = Array.isArray(raw.fallbacks)
|
|
362
369
|
? raw.fallbacks
|
|
363
370
|
.filter(isRecord)
|
|
364
371
|
.filter((entry) => typeof entry.id === 'string')
|
|
365
|
-
.map(
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
372
|
+
.map(
|
|
373
|
+
(entry): SidebarAccountState => ({
|
|
374
|
+
id: entry.id as string,
|
|
375
|
+
...(typeof entry.label === 'string' && { label: entry.label }),
|
|
376
|
+
quota: normalizeAccountQuota(entry.quota),
|
|
377
|
+
enabled: typeof entry.enabled === 'boolean' ? entry.enabled : false,
|
|
378
|
+
...(typeof entry.stateLine === 'string' && {
|
|
379
|
+
stateLine: entry.stateLine,
|
|
380
|
+
}),
|
|
381
|
+
...(entry.quotaDated === true && { quotaDated: true }),
|
|
382
|
+
needsReauth:
|
|
383
|
+
typeof entry.needsReauth === 'boolean'
|
|
384
|
+
? entry.needsReauth
|
|
385
|
+
: false,
|
|
386
|
+
...(entry.vaultReauth === true && { vaultReauth: true }),
|
|
387
|
+
...(entry.vaultServed === true && { vaultServed: true }),
|
|
388
|
+
...(typeof entry.custodyState === 'string' && {
|
|
389
|
+
custodyState:
|
|
390
|
+
entry.custodyState as SidebarAccountState['custodyState'],
|
|
391
|
+
}),
|
|
392
|
+
...(typeof entry.tierLabel === 'string' &&
|
|
393
|
+
entry.tierLabel.trim() && { tierLabel: entry.tierLabel.trim() }),
|
|
381
394
|
}),
|
|
382
|
-
|
|
383
|
-
typeof entry.tierLabel === 'string' && entry.tierLabel.trim()
|
|
384
|
-
? entry.tierLabel.trim()
|
|
385
|
-
: undefined,
|
|
386
|
-
}))
|
|
395
|
+
)
|
|
387
396
|
: []
|
|
388
397
|
|
|
389
398
|
let relay: SidebarState['relay'] = null
|
|
@@ -400,11 +409,10 @@ export function normalizeSidebarState(raw: unknown): SidebarState {
|
|
|
400
409
|
if (typeof ck.enabled === 'boolean') {
|
|
401
410
|
cacheKeep = {
|
|
402
411
|
enabled: ck.enabled,
|
|
403
|
-
|
|
404
|
-
trackedSessions
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
: undefined,
|
|
412
|
+
...(typeof ck.window === 'string' && { window: ck.window }),
|
|
413
|
+
...(isFiniteNumber(ck.trackedSessions) && {
|
|
414
|
+
trackedSessions: ck.trackedSessions,
|
|
415
|
+
}),
|
|
408
416
|
}
|
|
409
417
|
}
|
|
410
418
|
}
|
|
@@ -432,23 +440,23 @@ export function normalizeSidebarState(raw: unknown): SidebarState {
|
|
|
432
440
|
mode: recovery.mode,
|
|
433
441
|
remaining: Math.max(0, Math.floor(recovery.remaining)),
|
|
434
442
|
changedAt: recovery.changedAt,
|
|
435
|
-
requestedModelId
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
? recovery.targetModelId
|
|
442
|
-
: undefined,
|
|
443
|
+
...(typeof recovery.requestedModelId === 'string' && {
|
|
444
|
+
requestedModelId: recovery.requestedModelId,
|
|
445
|
+
}),
|
|
446
|
+
...(typeof recovery.targetModelId === 'string' && {
|
|
447
|
+
targetModelId: recovery.targetModelId,
|
|
448
|
+
}),
|
|
443
449
|
},
|
|
444
450
|
]
|
|
445
451
|
})
|
|
446
452
|
: []
|
|
447
453
|
|
|
454
|
+
const prime = normalizePrimeSection(raw.prime)
|
|
455
|
+
|
|
448
456
|
return {
|
|
449
457
|
main,
|
|
450
458
|
fallbacks,
|
|
451
|
-
|
|
459
|
+
...(typeof raw.activeId === 'string' && { activeId: raw.activeId }),
|
|
452
460
|
route:
|
|
453
461
|
typeof raw.route === 'string' ? raw.route : DEFAULT_SIDEBAR_STATE.route,
|
|
454
462
|
relay,
|
|
@@ -457,9 +465,9 @@ export function normalizeSidebarState(raw: unknown): SidebarState {
|
|
|
457
465
|
? raw.fastMode
|
|
458
466
|
: DEFAULT_SIDEBAR_STATE.fastMode,
|
|
459
467
|
cacheKeep,
|
|
460
|
-
prime
|
|
461
|
-
fableRecoveries
|
|
462
|
-
lastUpdated:
|
|
468
|
+
...(prime && { prime }),
|
|
469
|
+
...(fableRecoveries.length > 0 && { fableRecoveries }),
|
|
470
|
+
lastUpdated: isFiniteNumber(raw.lastUpdated) ? raw.lastUpdated : 0,
|
|
463
471
|
}
|
|
464
472
|
}
|
|
465
473
|
|
|
@@ -692,9 +700,10 @@ export interface SidebarStateWriteOptions {
|
|
|
692
700
|
})
|
|
693
701
|
| undefined
|
|
694
702
|
>
|
|
695
|
-
onRoutingResolved?: (
|
|
696
|
-
|
|
697
|
-
|
|
703
|
+
onRoutingResolved?: (routing: {
|
|
704
|
+
activeId: string | undefined
|
|
705
|
+
route: string
|
|
706
|
+
}) => void
|
|
698
707
|
}
|
|
699
708
|
|
|
700
709
|
export async function setSidebarState(
|
|
@@ -698,7 +698,8 @@ export function createSidebarController(initialPrefs) {
|
|
|
698
698
|
prefs,
|
|
699
699
|
collapsed,
|
|
700
700
|
toggleCollapsed,
|
|
701
|
-
dispose
|
|
701
|
+
dispose,
|
|
702
|
+
retryMaxMs: 30_000
|
|
702
703
|
};
|
|
703
704
|
}
|
|
704
705
|
|
|
@@ -1239,38 +1240,137 @@ export function sidebarThemeFromHost(theme) {
|
|
|
1239
1240
|
|
|
1240
1241
|
/** Reads one session's sidebar state from the location's server. */
|
|
1241
1242
|
|
|
1242
|
-
|
|
1243
|
-
|
|
1243
|
+
/**
|
|
1244
|
+
* What the sidebar reads and draws with, independent of the host chrome.
|
|
1245
|
+
* The timer functions default to the global ones; a test passes its own so it
|
|
1246
|
+
* can drive the refresh schedule without touching the shared globals.
|
|
1247
|
+
*/
|
|
1248
|
+
|
|
1249
|
+
/** The refresh loop's own timers, so a test can drive its schedule. */
|
|
1250
|
+
|
|
1251
|
+
/**
|
|
1252
|
+
* Delay before the next refresh after `failures` failed reads in a row, each
|
|
1253
|
+
* one longer than the last until the controller's limit. A successful read
|
|
1254
|
+
* resets the count, so a healthy server is polled at the configured rate. A
|
|
1255
|
+
* failed read still costs the server a full runtime read, so retrying at the
|
|
1256
|
+
* poll rate keeps that cost alive for as long as the failure lasts.
|
|
1257
|
+
*/
|
|
1258
|
+
export function sidebarRetryDelay(pollMs, failures, retryMaxMs) {
|
|
1259
|
+
if (failures <= 0) return pollMs;
|
|
1260
|
+
return Math.min(pollMs * 2 ** failures, retryMaxMs);
|
|
1261
|
+
}
|
|
1262
|
+
|
|
1263
|
+
/** Starts and stops the refresh schedule the sidebar lives on. */
|
|
1264
|
+
|
|
1265
|
+
// One read on start, then a timer that backs off after failures and resets
|
|
1266
|
+
// after a success. Lives outside Solid: the component drives it from an
|
|
1267
|
+
// effect, and tests drive it with their own timers.
|
|
1268
|
+
export function createSidebarSchedule(options) {
|
|
1269
|
+
const {
|
|
1270
|
+
read,
|
|
1271
|
+
sessionId,
|
|
1272
|
+
pollRate,
|
|
1273
|
+
onState,
|
|
1274
|
+
setTimeoutImpl = setTimeout,
|
|
1275
|
+
clearTimeoutImpl = clearTimeout
|
|
1276
|
+
} = options;
|
|
1244
1277
|
const abort = new AbortController();
|
|
1278
|
+
// The timer waiting to run. Every schedule replaces it, so stopping always
|
|
1279
|
+
// clears the one that is actually queued.
|
|
1280
|
+
let timer;
|
|
1281
|
+
let running = false;
|
|
1245
1282
|
let inFlight = false;
|
|
1246
1283
|
let shown = '';
|
|
1284
|
+
let failures = 0;
|
|
1285
|
+
// `undefined` means the previous read was still running, which is not a
|
|
1286
|
+
// failure: counting it as one would back off before the server answered.
|
|
1247
1287
|
const refresh = async () => {
|
|
1248
|
-
if (inFlight || abort.signal.aborted) return;
|
|
1288
|
+
if (inFlight || abort.signal.aborted) return undefined;
|
|
1249
1289
|
inFlight = true;
|
|
1250
1290
|
try {
|
|
1251
|
-
const next = await
|
|
1252
|
-
// Every answer is stamped
|
|
1253
|
-
// Countdowns and ages advance on the sidebar's own clock.
|
|
1291
|
+
const next = await read(sessionId, abort.signal);
|
|
1292
|
+
// Every answer is stamped by the server; redraw only when its content
|
|
1293
|
+
// changed. Countdowns and ages advance on the sidebar's own clock.
|
|
1254
1294
|
const content = JSON.stringify({
|
|
1255
1295
|
...next,
|
|
1256
1296
|
lastUpdated: 0
|
|
1257
1297
|
});
|
|
1258
1298
|
if (!abort.signal.aborted && content !== shown) {
|
|
1259
1299
|
shown = content;
|
|
1260
|
-
|
|
1300
|
+
onState(next);
|
|
1261
1301
|
}
|
|
1302
|
+
return true;
|
|
1262
1303
|
} catch {
|
|
1263
1304
|
// Keep the last state; an older server without `sidebar` keeps waiting.
|
|
1305
|
+
return false;
|
|
1264
1306
|
} finally {
|
|
1265
1307
|
inFlight = false;
|
|
1266
1308
|
}
|
|
1267
1309
|
};
|
|
1310
|
+
const schedule = () => {
|
|
1311
|
+
if (!running) return;
|
|
1312
|
+
const {
|
|
1313
|
+
pollMs,
|
|
1314
|
+
retryMaxMs
|
|
1315
|
+
} = pollRate();
|
|
1316
|
+
const wait = sidebarRetryDelay(pollMs, failures, retryMaxMs);
|
|
1317
|
+
timer = setTimeoutImpl(async () => {
|
|
1318
|
+
timer = undefined;
|
|
1319
|
+
if (!running) return;
|
|
1320
|
+
const ok = await refresh();
|
|
1321
|
+
if (!running) return;
|
|
1322
|
+
if (ok === true) failures = 0;else if (ok === false) failures += 1;
|
|
1323
|
+
schedule();
|
|
1324
|
+
}, wait);
|
|
1325
|
+
};
|
|
1326
|
+
return {
|
|
1327
|
+
start() {
|
|
1328
|
+
if (running || abort.signal.aborted) return;
|
|
1329
|
+
running = true;
|
|
1330
|
+
// The first read happens here rather than in the timer, so a caller that
|
|
1331
|
+
// starts before any draw still has an answer waiting for it. The loop
|
|
1332
|
+
// then continues on the retry timer below.
|
|
1333
|
+
void refresh().then(ok => {
|
|
1334
|
+
if (running && ok === false) failures += 1;
|
|
1335
|
+
});
|
|
1336
|
+
schedule();
|
|
1337
|
+
},
|
|
1338
|
+
stop() {
|
|
1339
|
+
running = false;
|
|
1340
|
+
if (timer !== undefined) clearTimeoutImpl(timer);
|
|
1341
|
+
timer = undefined;
|
|
1342
|
+
abort.abort();
|
|
1343
|
+
}
|
|
1344
|
+
};
|
|
1345
|
+
}
|
|
1346
|
+
|
|
1347
|
+
// The OpenCode 2 sidebar's state: its refresh schedule, redrawn through a
|
|
1348
|
+
// signal the component renders from.
|
|
1349
|
+
export function useHostSidebarState(props) {
|
|
1350
|
+
const [state, setState] = createSignal(DEFAULT_SIDEBAR_STATE);
|
|
1268
1351
|
createEffect(() => {
|
|
1269
|
-
|
|
1270
|
-
|
|
1352
|
+
// Reading the poll rate here re-runs this effect when it changes. A
|
|
1353
|
+
// stopped schedule never restarts, so each run builds a fresh one and the
|
|
1354
|
+
// cleanup stops the run it belongs to.
|
|
1355
|
+
const pollMs = props.controller.prefs().pollMs;
|
|
1356
|
+
const schedule = createSidebarSchedule({
|
|
1357
|
+
read: props.read,
|
|
1358
|
+
sessionId: props.sessionId,
|
|
1359
|
+
pollRate: () => ({
|
|
1360
|
+
pollMs,
|
|
1361
|
+
retryMaxMs: props.controller.retryMaxMs
|
|
1362
|
+
}),
|
|
1363
|
+
onState: setState,
|
|
1364
|
+
setTimeoutImpl: props.setTimeoutImpl,
|
|
1365
|
+
clearTimeoutImpl: props.clearTimeoutImpl
|
|
1366
|
+
});
|
|
1367
|
+
schedule.start();
|
|
1368
|
+
onCleanup(() => schedule.stop());
|
|
1271
1369
|
});
|
|
1272
|
-
|
|
1273
|
-
|
|
1370
|
+
return state;
|
|
1371
|
+
}
|
|
1372
|
+
function HostSidebar(props) {
|
|
1373
|
+
const state = useHostSidebarState(props);
|
|
1274
1374
|
return _$createComponent(ClaudeSidebar, {
|
|
1275
1375
|
state: state,
|
|
1276
1376
|
theme: () => sidebarThemeFromHost(props.host.theme),
|