opencode-auth-load-balancer 0.2.2 → 0.4.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 +47 -7
- package/dist/fetch.d.ts +13 -1
- package/dist/index.js +2958 -2072
- package/dist/notify.d.ts +8 -0
- package/dist/pending/coordinator.d.ts +135 -0
- package/dist/pending/lease.d.ts +3 -0
- package/dist/pending/recovery.d.ts +16 -0
- package/dist/pending/store.d.ts +12 -0
- package/dist/pending/types.d.ts +23 -0
- package/dist/pool/lock.d.ts +1 -2
- package/dist/pool/paths.d.ts +11 -0
- package/dist/providers/anthropic/constants.d.ts +55 -10
- package/dist/providers/anthropic/fallback.d.ts +3 -1
- package/dist/providers/anthropic/version.d.ts +29 -0
- package/dist/providers/openai/usage.d.ts +14 -8
- package/dist/providers/registry.d.ts +26 -0
- package/dist/providers/types.d.ts +23 -6
- package/dist/providers/usage-http.d.ts +7 -6
- package/dist/scheduler/select.d.ts +5 -2
- package/dist/session.d.ts +2 -0
- package/dist/status.d.ts +4 -1
- package/dist/types.d.ts +58 -1
- package/dist/usage-refresh.d.ts +35 -16
- package/dist/util.d.ts +4 -3
- package/package.json +11 -11
- package/tui/auth-load-balancer-tui.logic.ts +139 -8
- package/tui/auth-load-balancer-tui.view.tsx +284 -42
|
@@ -11,8 +11,8 @@
|
|
|
11
11
|
* - sidebar_content — a panel next to Context / MCP / LSP listing ALL accounts
|
|
12
12
|
* per provider, sorted by the scheduler's score (highest = use next), showing
|
|
13
13
|
* the score + use-order, usage + reset countdowns, in-use marker, and state.
|
|
14
|
-
* Click any account row for a menu to Rename (prompt)
|
|
15
|
-
*
|
|
14
|
+
* Click any account row for a menu to Rename (prompt), Disable/Enable
|
|
15
|
+
* (toggle), Re-login (provider OAuth), or Delete (confirm) it.
|
|
16
16
|
*
|
|
17
17
|
* The ranking is computed by the SAME code the server scheduler uses — imported from a
|
|
18
18
|
* byte-identical copy of src/scheduler/score-core.ts installed alongside this file
|
|
@@ -31,12 +31,19 @@ import {
|
|
|
31
31
|
} from './auth-load-balancer-scoring'
|
|
32
32
|
import {
|
|
33
33
|
cfg,
|
|
34
|
+
clearReloginTargetInPool,
|
|
34
35
|
compareAscii,
|
|
35
36
|
deleteFromPool,
|
|
37
|
+
MANUAL_DISABLED_REASON,
|
|
36
38
|
pct,
|
|
39
|
+
pickAuthMethodIndex,
|
|
37
40
|
type PoolShape,
|
|
38
41
|
readPool,
|
|
39
42
|
renameInPool,
|
|
43
|
+
sessionAccountId,
|
|
44
|
+
sessionFallback,
|
|
45
|
+
setDisabledInPool,
|
|
46
|
+
setReloginTargetInPool,
|
|
40
47
|
stateOf,
|
|
41
48
|
tierResets,
|
|
42
49
|
toScore,
|
|
@@ -57,16 +64,51 @@ function providerLabel(id: string): string {
|
|
|
57
64
|
}
|
|
58
65
|
const POLL_MS = 3000
|
|
59
66
|
|
|
67
|
+
/**
|
|
68
|
+
* The current session id from the TUI route, or undefined on the home screen.
|
|
69
|
+
* Unlike the `sidebar_content` slot (handed a `session_id` prop), the always-
|
|
70
|
+
* visible `app_bottom` bar receives no slot props, so it reads the session from
|
|
71
|
+
* the route to scope its in-use account to the VIEWER's own session.
|
|
72
|
+
*/
|
|
73
|
+
function routeSessionId(api: TuiPluginApi): string | undefined {
|
|
74
|
+
const r = api.route.current
|
|
75
|
+
if (r.name !== 'session') return undefined
|
|
76
|
+
const sid = r.params?.sessionID
|
|
77
|
+
return typeof sid === 'string' ? sid : undefined
|
|
78
|
+
}
|
|
79
|
+
|
|
60
80
|
// Scoring knobs read from env exactly like the server — single-sourced in the shared
|
|
61
81
|
// ./auth-load-balancer-scoring module (a byte copy of src/scheduler/score-core.ts),
|
|
62
82
|
// shared with auth-load-balancer-tui.logic.ts (imported above as `cfg`) so it is
|
|
63
83
|
// computed exactly once.
|
|
64
84
|
|
|
85
|
+
/** One poll sample: the pool, plus the instant it was read. */
|
|
86
|
+
interface PoolSample {
|
|
87
|
+
pool: PoolShape
|
|
88
|
+
now: number
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Capture the pool AND the clock together, beside each other.
|
|
93
|
+
*
|
|
94
|
+
* The timestamp deliberately lives HERE rather than in each consuming memo.
|
|
95
|
+
* A memo that calls `Date.now()` itself is impure — it depends on something
|
|
96
|
+
* Solid cannot track, so its countdowns refresh only by the coincidence that
|
|
97
|
+
* the poll signal happens to change on the same tick — and it measures every
|
|
98
|
+
* `until()` against an instant slightly LATER than the snapshot it is
|
|
99
|
+
* describing. Sampling both at once makes the memos pure functions of one
|
|
100
|
+
* reactive input and keeps each countdown consistent with the pool state it
|
|
101
|
+
* is rendered from.
|
|
102
|
+
*/
|
|
103
|
+
function pollSample(): PoolSample {
|
|
104
|
+
return { pool: readPool(), now: Date.now() }
|
|
105
|
+
}
|
|
106
|
+
|
|
65
107
|
function usePool() {
|
|
66
|
-
const [
|
|
67
|
-
const timer = setInterval(() =>
|
|
108
|
+
const [sample, setSample] = createSignal<PoolSample>(pollSample())
|
|
109
|
+
const timer = setInterval(() => setSample(pollSample()), POLL_MS)
|
|
68
110
|
onCleanup(() => clearInterval(timer))
|
|
69
|
-
return
|
|
111
|
+
return sample
|
|
70
112
|
}
|
|
71
113
|
|
|
72
114
|
/** Shared theme-color accessor used identically by `BottomBar` and `SidebarPanel`. */
|
|
@@ -85,24 +127,32 @@ interface WindowDisplay {
|
|
|
85
127
|
interface Chip extends WindowDisplay {
|
|
86
128
|
name: string
|
|
87
129
|
label: string
|
|
130
|
+
/** Raw model ids (requested → served) when this session runs on a fallback model, else undefined. */
|
|
131
|
+
fallback?: { from: string; to: string }
|
|
88
132
|
}
|
|
89
133
|
|
|
90
134
|
/** Always-visible bottom bar (app_bottom): the in-use account per provider. */
|
|
91
135
|
function BottomBar(props: { api: TuiPluginApi }) {
|
|
92
|
-
const
|
|
136
|
+
const sample = usePool()
|
|
93
137
|
const color = themeColor(props.api)
|
|
94
138
|
const chips = createMemo<Chip[]>(() => {
|
|
95
|
-
const p =
|
|
139
|
+
const { pool: p, now } = sample()
|
|
96
140
|
const accounts = p.accounts ?? []
|
|
97
|
-
const now = Date.now()
|
|
98
141
|
const out: Chip[] = []
|
|
99
|
-
|
|
100
|
-
//
|
|
101
|
-
//
|
|
102
|
-
|
|
103
|
-
|
|
142
|
+
const sid = routeSessionId(props.api)
|
|
143
|
+
// Show the account THIS session is using per provider — NOT the global
|
|
144
|
+
// `lastSelected` (whichever session served last across the whole pool). On
|
|
145
|
+
// the home screen (no session) fall back to `lastSelected` so the bar isn't
|
|
146
|
+
// blank. Providers byte-sorted (`< / >`, no locale) so the bar lists them in
|
|
147
|
+
// the same order as the sidebar, CLI, and status tool.
|
|
148
|
+
const providerIds = [...new Set(accounts.map((x) => x.providerID))].sort(
|
|
149
|
+
compareAscii,
|
|
104
150
|
)
|
|
105
|
-
for (const
|
|
151
|
+
for (const providerID of providerIds) {
|
|
152
|
+
const id =
|
|
153
|
+
sessionAccountId(p, providerID, sid) ??
|
|
154
|
+
(sid ? undefined : p.lastSelected?.[providerID])
|
|
155
|
+
if (!id) continue
|
|
106
156
|
const a = accounts.find((x) => x.id === id)
|
|
107
157
|
if (!a) continue
|
|
108
158
|
out.push({
|
|
@@ -112,6 +162,9 @@ function BottomBar(props: { api: TuiPluginApi }) {
|
|
|
112
162
|
weeklyReset: until(a.usage?.weekly?.resetAt, now),
|
|
113
163
|
hourlyPct: winPct(a.usage?.hourly, now),
|
|
114
164
|
hourlyReset: until(a.usage?.hourly?.resetAt, now),
|
|
165
|
+
// Only THIS session's fallback (keyed by sid); the `lastSelected` home-screen
|
|
166
|
+
// fallback path has no session, so no downgrade to surface there.
|
|
167
|
+
fallback: sessionFallback(p, providerID, sid),
|
|
115
168
|
})
|
|
116
169
|
}
|
|
117
170
|
return out
|
|
@@ -125,6 +178,11 @@ function BottomBar(props: { api: TuiPluginApi }) {
|
|
|
125
178
|
<span style={{ fg: color().primary }}>
|
|
126
179
|
{a.name} {a.label}
|
|
127
180
|
</span>
|
|
181
|
+
{a.fallback ? (
|
|
182
|
+
<span style={{ fg: color().warning }}>
|
|
183
|
+
{` fallback ${a.fallback.from}→${a.fallback.to}`}
|
|
184
|
+
</span>
|
|
185
|
+
) : null}
|
|
128
186
|
{` wk ${a.weeklyPct} (${a.weeklyReset}) · 5h ${a.hourlyPct} (${a.hourlyReset})`}
|
|
129
187
|
</text>
|
|
130
188
|
)}
|
|
@@ -136,10 +194,12 @@ function BottomBar(props: { api: TuiPluginApi }) {
|
|
|
136
194
|
interface Row extends WindowDisplay {
|
|
137
195
|
id: string
|
|
138
196
|
label: string
|
|
197
|
+
providerID: string
|
|
139
198
|
current: boolean
|
|
140
199
|
score: number | null
|
|
141
200
|
rank: number | null
|
|
142
201
|
state: string
|
|
202
|
+
manuallyDisabled: boolean
|
|
143
203
|
}
|
|
144
204
|
interface Group {
|
|
145
205
|
provider: string
|
|
@@ -147,17 +207,23 @@ interface Group {
|
|
|
147
207
|
}
|
|
148
208
|
|
|
149
209
|
/** Sidebar panel: all accounts per provider, scored + sorted, click to rename. */
|
|
150
|
-
function SidebarPanel(props: {
|
|
151
|
-
|
|
210
|
+
function SidebarPanel(props: {
|
|
211
|
+
api: TuiPluginApi
|
|
212
|
+
sessionId: string | undefined
|
|
213
|
+
}) {
|
|
214
|
+
const sample = usePool()
|
|
152
215
|
const color = themeColor(props.api)
|
|
153
216
|
const groups = createMemo<Group[]>(() => {
|
|
154
|
-
const p =
|
|
217
|
+
const { pool: p, now } = sample()
|
|
155
218
|
const accounts = p.accounts ?? []
|
|
156
|
-
const now = Date.now()
|
|
157
219
|
const providerIds = [...new Set(accounts.map((a) => a.providerID))].sort(
|
|
158
220
|
compareAscii,
|
|
159
221
|
)
|
|
160
222
|
return providerIds.map((providerID) => {
|
|
223
|
+
// The account the VIEWER's session is pinned to for this provider — used
|
|
224
|
+
// for the in-use (▶) marker instead of the global `lastSelected`, so the
|
|
225
|
+
// marker reflects THIS session, not whichever session served last.
|
|
226
|
+
const sessionAcct = sessionAccountId(p, providerID, props.sessionId)
|
|
161
227
|
const ranked = accounts
|
|
162
228
|
.filter((a) => a.providerID === providerID)
|
|
163
229
|
.map((a) => {
|
|
@@ -180,7 +246,8 @@ function SidebarPanel(props: { api: TuiPluginApi }) {
|
|
|
180
246
|
return {
|
|
181
247
|
id: a.id,
|
|
182
248
|
label: a.label,
|
|
183
|
-
|
|
249
|
+
providerID,
|
|
250
|
+
current: sessionAcct === a.id,
|
|
184
251
|
score: available ? score : null,
|
|
185
252
|
rank: available ? rank : null,
|
|
186
253
|
// Reuse the windows `toScore(a)` already normalized instead of
|
|
@@ -196,6 +263,7 @@ function SidebarPanel(props: { api: TuiPluginApi }) {
|
|
|
196
263
|
// `1e999` entry (Infinity via JSON.parse) would otherwise render its
|
|
197
264
|
// tier annotation forever until the server heals the file.
|
|
198
265
|
state: stateOf(sa, tierResets(a, now), now),
|
|
266
|
+
manuallyDisabled: a.disabledReason === MANUAL_DISABLED_REASON,
|
|
199
267
|
}
|
|
200
268
|
})
|
|
201
269
|
return { provider: providerLabel(providerID), rows }
|
|
@@ -234,25 +302,187 @@ function SidebarPanel(props: { api: TuiPluginApi }) {
|
|
|
234
302
|
)
|
|
235
303
|
}
|
|
236
304
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
305
|
+
async function openRelogin(
|
|
306
|
+
id: string,
|
|
307
|
+
label: string,
|
|
308
|
+
providerID: string,
|
|
309
|
+
): Promise<void> {
|
|
310
|
+
try {
|
|
311
|
+
const methods = (await props.api.client.provider.auth()).data?.[
|
|
312
|
+
providerID
|
|
313
|
+
]
|
|
314
|
+
const method = pickAuthMethodIndex(methods)
|
|
315
|
+
if (method === null) {
|
|
316
|
+
props.api.ui.toast({
|
|
317
|
+
variant: 'error',
|
|
318
|
+
message: `No OAuth login method is registered for ${providerID}.`,
|
|
319
|
+
})
|
|
320
|
+
return
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
const auth = (
|
|
324
|
+
await props.api.client.provider.oauth.authorize({
|
|
325
|
+
providerID,
|
|
326
|
+
method,
|
|
327
|
+
})
|
|
328
|
+
).data
|
|
329
|
+
if (!auth) {
|
|
330
|
+
props.api.ui.toast({
|
|
331
|
+
variant: 'error',
|
|
332
|
+
message: `Unable to start OAuth login for ${providerID}.`,
|
|
333
|
+
})
|
|
334
|
+
return
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// Timestamp intentionally left to the callee's default — see its doc.
|
|
338
|
+
setReloginTargetInPool(id, providerID)
|
|
339
|
+
|
|
340
|
+
const completeRelogin = async (code?: string): Promise<void> => {
|
|
341
|
+
try {
|
|
342
|
+
const completed = (
|
|
343
|
+
await props.api.client.provider.oauth.callback({
|
|
344
|
+
providerID,
|
|
345
|
+
method,
|
|
346
|
+
...(code === undefined ? {} : { code }),
|
|
347
|
+
})
|
|
348
|
+
).data
|
|
349
|
+
if (completed) {
|
|
350
|
+
props.api.ui.toast({
|
|
351
|
+
variant: 'success',
|
|
352
|
+
message: `Re-login complete for ${label}.`,
|
|
353
|
+
})
|
|
354
|
+
// The existing 3s usePool poll observes the consumed intent and
|
|
355
|
+
// cleared disabledReason; no manual state refresh is needed.
|
|
356
|
+
} else {
|
|
357
|
+
clearReloginTargetInPool()
|
|
358
|
+
props.api.ui.toast({
|
|
359
|
+
variant: 'error',
|
|
360
|
+
message: `Re-login failed for ${label}.`,
|
|
361
|
+
})
|
|
362
|
+
}
|
|
363
|
+
dialog().clear()
|
|
364
|
+
} catch {
|
|
365
|
+
clearReloginTargetInPool()
|
|
366
|
+
dialog().clear()
|
|
367
|
+
props.api.ui.toast({
|
|
368
|
+
variant: 'error',
|
|
369
|
+
message: `Re-login failed for ${label}.`,
|
|
370
|
+
})
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
if (auth.method === 'auto') {
|
|
375
|
+
await completeRelogin()
|
|
376
|
+
return
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
dialog().replace(() =>
|
|
380
|
+
props.api.ui.DialogPrompt({
|
|
381
|
+
title: `Re-login "${label}"`,
|
|
382
|
+
description: () => (
|
|
383
|
+
<box>
|
|
384
|
+
<text>{auth.instructions}</text>
|
|
385
|
+
<text>{auth.url}</text>
|
|
386
|
+
</box>
|
|
387
|
+
),
|
|
388
|
+
placeholder: 'Paste the authorization code',
|
|
389
|
+
onConfirm: (code: string) => {
|
|
390
|
+
const trimmed = code.trim()
|
|
391
|
+
if (!trimmed) {
|
|
392
|
+
clearReloginTargetInPool()
|
|
393
|
+
dialog().clear()
|
|
394
|
+
return
|
|
395
|
+
}
|
|
396
|
+
void completeRelogin(trimmed)
|
|
247
397
|
},
|
|
248
|
-
{
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
onSelect: () => openDelete(id, label),
|
|
398
|
+
onCancel: () => {
|
|
399
|
+
clearReloginTargetInPool()
|
|
400
|
+
dialog().clear()
|
|
252
401
|
},
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
402
|
+
}),
|
|
403
|
+
)
|
|
404
|
+
} catch {
|
|
405
|
+
clearReloginTargetInPool()
|
|
406
|
+
dialog().clear()
|
|
407
|
+
props.api.ui.toast({
|
|
408
|
+
variant: 'error',
|
|
409
|
+
message: `Re-login failed for ${label}.`,
|
|
410
|
+
})
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// Click an account -> a small menu so Rename, Disable/Enable, Re-login, and Delete are
|
|
415
|
+
// all reachable. Deliberately NOT api.ui.DialogSelect: that always renders an
|
|
416
|
+
// auto-focused filter <input>, and a focused opentui input swallows the FIRST
|
|
417
|
+
// Esc (to blur itself) — so the menu needed TWO Esc presses to close. A plain
|
|
418
|
+
// clickable list has no input, so the dialog stack's own Esc binding closes it
|
|
419
|
+
// in ONE press. The menu is opened by a mouse click on the row, so mouse-driven
|
|
420
|
+
// options stay consistent (there is no keyboard path that opens it).
|
|
421
|
+
function openMenu(
|
|
422
|
+
id: string,
|
|
423
|
+
label: string,
|
|
424
|
+
providerID: string,
|
|
425
|
+
manuallyDisabled: boolean,
|
|
426
|
+
): void {
|
|
427
|
+
const items: { title: string; run: () => void }[] = [
|
|
428
|
+
{ title: 'Rename', run: () => openRename(id, label) },
|
|
429
|
+
{
|
|
430
|
+
// Reversible (just a scheduler skip), so no confirm step — flip the
|
|
431
|
+
// pool flag and close, unlike Delete which drops the tokens.
|
|
432
|
+
title: manuallyDisabled
|
|
433
|
+
? 'Enable — include in selection'
|
|
434
|
+
: 'Disable — exclude from selection',
|
|
435
|
+
run: () => {
|
|
436
|
+
setDisabledInPool(id, !manuallyDisabled)
|
|
437
|
+
dialog().clear()
|
|
438
|
+
},
|
|
439
|
+
},
|
|
440
|
+
{
|
|
441
|
+
title: 'Re-login — re-authorize with the provider',
|
|
442
|
+
run: () => {
|
|
443
|
+
void openRelogin(id, label, providerID)
|
|
444
|
+
},
|
|
445
|
+
},
|
|
446
|
+
{ title: 'Delete — remove from pool', run: () => openDelete(id, label) },
|
|
447
|
+
]
|
|
448
|
+
dialog().replace(() => {
|
|
449
|
+
const c = color()
|
|
450
|
+
const [hovered, setHovered] = createSignal(-1)
|
|
451
|
+
return (
|
|
452
|
+
<box
|
|
453
|
+
gap={1}
|
|
454
|
+
paddingBottom={1}
|
|
455
|
+
paddingLeft={4}
|
|
456
|
+
paddingRight={4}
|
|
457
|
+
paddingTop={1}
|
|
458
|
+
>
|
|
459
|
+
<box flexDirection="row" justifyContent="space-between">
|
|
460
|
+
<text fg={c.text}>
|
|
461
|
+
<b>{label}</b>
|
|
462
|
+
</text>
|
|
463
|
+
<text fg={c.textMuted} onMouseUp={() => dialog().clear()}>
|
|
464
|
+
esc
|
|
465
|
+
</text>
|
|
466
|
+
</box>
|
|
467
|
+
<box>
|
|
468
|
+
<For each={items}>
|
|
469
|
+
{(item, i) => (
|
|
470
|
+
<box
|
|
471
|
+
onMouseMove={() => setHovered(i())}
|
|
472
|
+
onMouseUp={() => item.run()}
|
|
473
|
+
paddingLeft={1}
|
|
474
|
+
paddingRight={1}
|
|
475
|
+
>
|
|
476
|
+
<text fg={hovered() === i() ? c.primary : c.text}>
|
|
477
|
+
{item.title}
|
|
478
|
+
</text>
|
|
479
|
+
</box>
|
|
480
|
+
)}
|
|
481
|
+
</For>
|
|
482
|
+
</box>
|
|
483
|
+
</box>
|
|
484
|
+
)
|
|
485
|
+
})
|
|
256
486
|
}
|
|
257
487
|
|
|
258
488
|
return (
|
|
@@ -262,7 +492,7 @@ function SidebarPanel(props: { api: TuiPluginApi }) {
|
|
|
262
492
|
<b>Auth accounts</b>
|
|
263
493
|
<span style={{ fg: color().textMuted }}>
|
|
264
494
|
{' '}
|
|
265
|
-
(click: rename / delete)
|
|
495
|
+
(click: rename / disable / re-login / delete)
|
|
266
496
|
</span>
|
|
267
497
|
</text>
|
|
268
498
|
<For each={groups()}>
|
|
@@ -271,9 +501,19 @@ function SidebarPanel(props: { api: TuiPluginApi }) {
|
|
|
271
501
|
<text fg={color().textMuted}>{g.provider}</text>
|
|
272
502
|
<For each={g.rows}>
|
|
273
503
|
{(r) => (
|
|
274
|
-
<box
|
|
504
|
+
<box
|
|
505
|
+
onMouseUp={() =>
|
|
506
|
+
openMenu(r.id, r.label, r.providerID, r.manuallyDisabled)
|
|
507
|
+
}
|
|
508
|
+
>
|
|
275
509
|
<text
|
|
276
|
-
fg={
|
|
510
|
+
fg={
|
|
511
|
+
r.manuallyDisabled
|
|
512
|
+
? color().textMuted
|
|
513
|
+
: r.current
|
|
514
|
+
? color().primary
|
|
515
|
+
: color().text
|
|
516
|
+
}
|
|
277
517
|
wrapMode="word"
|
|
278
518
|
>
|
|
279
519
|
{(r.current ? '▶ ' : ' ') +
|
|
@@ -313,9 +553,11 @@ function makeSlots(api: TuiPluginApi): TuiSlotPlugin {
|
|
|
313
553
|
app_bottom() {
|
|
314
554
|
return <BottomBar api={api} />
|
|
315
555
|
},
|
|
316
|
-
// Panel in the session sidebar, next to Context / MCP / LSP.
|
|
317
|
-
|
|
318
|
-
|
|
556
|
+
// Panel in the session sidebar, next to Context / MCP / LSP. The host
|
|
557
|
+
// hands this slot the current `session_id`, so the in-use marker can scope
|
|
558
|
+
// to the VIEWER's session instead of the global last-selected account.
|
|
559
|
+
sidebar_content(_ctx, props) {
|
|
560
|
+
return <SidebarPanel api={api} sessionId={props.session_id} />
|
|
319
561
|
},
|
|
320
562
|
},
|
|
321
563
|
}
|