@fonlan/dsh-model-router 0.3.0 → 0.3.3

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.
@@ -0,0 +1,623 @@
1
+ import {
2
+ useCallback,
3
+ useEffect,
4
+ useId,
5
+ useMemo,
6
+ useRef,
7
+ useState,
8
+ useSyncExternalStore,
9
+ type FocusEvent as ReactFocusEvent,
10
+ type KeyboardEvent as ReactKeyboardEvent,
11
+ } from 'react'
12
+ import type { ClientContext, SessionId, SnapshotStore } from '@deepseek-ai/dsh-client-runtime/client'
13
+ import type { ModelDirectoryState } from '@deepseek-ai/dsh-client-ui-model-selection/client'
14
+ import type { PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots'
15
+ import {
16
+ IconCheckOutline16,
17
+ IconChevronDownOutline14,
18
+ IconChevronRightOutline14,
19
+ IconWarningOutline16,
20
+ Toast,
21
+ } from '@deepseek-ai/dsh-client-ui-primitives'
22
+ import type {} from '@deepseek-ai/dsh-client-ui-conversation/client'
23
+ import type {} from '@deepseek-ai/dsh-client-ui-model-selection/client'
24
+ import { api, type ModelRouterState, type RouterModelView } from './api'
25
+ import { LOCALE_NS } from './locales'
26
+ import './model-seat.css'
27
+
28
+ const ROUTER_PROVIDER_ID = 'model-router'
29
+
30
+ /**
31
+ * Minimal structural ModelSelection (wire shape: provider / model /
32
+ * reasoningEffort). Declared locally to avoid pulling a new peer dependency
33
+ * for a type-only surface; structurally identical to the host wire type.
34
+ */
35
+ interface ModelSelection {
36
+ provider: string
37
+ model: string
38
+ reasoningEffort?: string
39
+ }
40
+
41
+ /** The injected business face of the composer model seat (same share the host ModelSelect receives). */
42
+ interface ModelSeatInjected {
43
+ available: boolean
44
+ directory: SnapshotStore<ModelDirectoryState>
45
+ load: () => void
46
+ select: (selection: ModelSelection) => Promise<boolean>
47
+ }
48
+
49
+ type ModelSeatProps = PropsRuntime<'conversation.input.model'>
50
+ & ModelSeatInjected
51
+ & PropsLocale<typeof LOCALE_NS>
52
+
53
+ type RouteProvider = RouterModelView['providers'][number]
54
+
55
+ /**
56
+ * The composer model seat, shadowing the host ModelSelect at a lower slot
57
+ * priority. It renders the same two-level Model / Effort menu as the host
58
+ * (model list grouped by provider, reasoning effort levels), plus a third
59
+ * Provider row that opens the model-router provider list — the former quick
60
+ * route-switcher button moved into the model picker, on par with the model
61
+ * and effort menus.
62
+ *
63
+ * Shadowing contract: the host ui-model-selection registers the
64
+ * `conversation.input.model` occupant at priority 0; this entry registers at
65
+ * priority -1, so the renderer projects this component as the single-slot
66
+ * winner while the host entry stays as the abdication fallback. Both entries
67
+ * share the same per-session ModelDirectory, so selections stay consistent
68
+ * with the /model popup and the host picker.
69
+ */
70
+ export function ModelRouterSeat({
71
+ locked,
72
+ available,
73
+ directory,
74
+ load,
75
+ select,
76
+ t,
77
+ }: ModelSeatProps): JSX.Element | null {
78
+ const directoryState = useSyncExternalStore(
79
+ onChange => directory.subscribe(onChange),
80
+ () => directory.getSnapshot(),
81
+ () => directory.getSnapshot(),
82
+ )
83
+ const current = directoryState.current
84
+
85
+ const [open, setOpen] = useState(false)
86
+ const [pane, setPane] = useState<'root' | 'model' | 'provider' | 'effort'>('root')
87
+ const lastActionRef = useRef<'load' | 'select'>('load')
88
+ const [toast, setToast] = useState<{ seq: number; text: string } | null>(null)
89
+ const toastSeq = useRef(0)
90
+ const rootRef = useRef<HTMLDivElement | null>(null)
91
+ const triggerRef = useRef<HTMLButtonElement | null>(null)
92
+ const itemRefs = useRef<(HTMLButtonElement | null)[]>([])
93
+ const id = useId()
94
+
95
+ // ---- model-router state (moved from the quick route switcher) ----
96
+ const [routerState, setRouterState] = useState<ModelRouterState | null>(null)
97
+ const [routerLoading, setRouterLoading] = useState(false)
98
+ const [routerError, setRouterError] = useState<string | null>(null)
99
+ const [busyProvider, setBusyProvider] = useState<string | null>(null)
100
+
101
+ const isRouterModel = available && current?.provider === ROUTER_PROVIDER_ID
102
+ const modelId = isRouterModel ? current.model : null
103
+
104
+ const loadRouterState = useCallback(async (silent = false) => {
105
+ if (!silent) setRouterLoading(true)
106
+ try {
107
+ setRouterState(await api.state())
108
+ setRouterError(null)
109
+ } catch (cause) {
110
+ setRouterError(cause instanceof Error ? cause.message : String(cause))
111
+ } finally {
112
+ if (!silent) setRouterLoading(false)
113
+ }
114
+ }, [])
115
+
116
+ // Load the advisory directory on mount (host behavior).
117
+ useEffect(() => {
118
+ if (!available) return
119
+ load()
120
+ }, [available, load])
121
+
122
+ // Fetch the router state (and thus the global toggle) on mount too, so the
123
+ // "hide provider switching" setting applies as soon as it resolves.
124
+ useEffect(() => {
125
+ if (!available) return
126
+ void loadRouterState()
127
+ }, [available, loadRouterState])
128
+
129
+ // Reset the open pane when the selected model changes (a different model may
130
+ // no longer be a model-router route).
131
+ useEffect(() => {
132
+ setPane('root')
133
+ setBusyProvider(null)
134
+ setRouterError(null)
135
+ }, [modelId])
136
+
137
+ // Silent debounced refresh of the router state after directory refreshes
138
+ // (settings edits, adapter updates), so the provider row never shows a stale
139
+ // active provider.
140
+ const refreshTimer = useRef<ReturnType<typeof setTimeout> | null>(null)
141
+ useEffect(() => {
142
+ if (!isRouterModel || modelId === null || directoryState.status !== 'ready') return
143
+ if (refreshTimer.current !== null) clearTimeout(refreshTimer.current)
144
+ refreshTimer.current = setTimeout(() => {
145
+ refreshTimer.current = null
146
+ void loadRouterState(true)
147
+ }, 300)
148
+ return () => {
149
+ if (refreshTimer.current !== null) clearTimeout(refreshTimer.current)
150
+ refreshTimer.current = null
151
+ }
152
+ }, [directoryState.status, isRouterModel, loadRouterState, modelId])
153
+
154
+ const routeModel = useMemo(
155
+ () => modelId === null
156
+ ? undefined
157
+ : routerState?.models.find(model => model.id === modelId),
158
+ [modelId, routerState],
159
+ )
160
+
161
+ const providers = useMemo(() => {
162
+ if (routeModel === undefined) return []
163
+ const byId = new Map(routeModel.providers.map(provider => [provider.id, provider]))
164
+ const ordered = routeModel.order
165
+ .map(id => byId.get(id))
166
+ .filter((provider): provider is RouteProvider => provider !== undefined)
167
+ const seen = new Set(ordered.map(provider => provider.id))
168
+ return [...ordered, ...routeModel.providers.filter(provider => !seen.has(provider.id))]
169
+ }, [routeModel])
170
+
171
+ const switchProvider = useCallback(async (providerId: string) => {
172
+ if (modelId === null || routeModel === undefined || routeModel.active === providerId) {
173
+ setOpen(false)
174
+ return
175
+ }
176
+ setBusyProvider(providerId)
177
+ try {
178
+ const next = await api.switchActive(modelId, providerId)
179
+ setRouterState(next)
180
+ setRouterError(null)
181
+ setOpen(false)
182
+ // Keep the host model picker description in sync immediately.
183
+ load()
184
+ } catch (cause) {
185
+ setRouterError(t('switchFailed', { message: cause instanceof Error ? cause.message : String(cause) }))
186
+ } finally {
187
+ setBusyProvider(null)
188
+ }
189
+ }, [load, modelId, routeModel, t])
190
+
191
+ // ---- host ModelSelect logic (faithful copy) ----
192
+ const choices = useMemo(() => directoryState.groups.flatMap(group => group.models.map(model => ({
193
+ group,
194
+ model,
195
+ selection: {
196
+ provider: group.id,
197
+ model: model.id,
198
+ ...model.reasoning?.defaultEffort === undefined ? {} : { reasoningEffort: model.reasoning.defaultEffort },
199
+ },
200
+ }))), [directoryState.groups])
201
+ const currentChoice = choices[current === null ? -1 : choices.findIndex(c =>
202
+ c.selection.provider === current?.provider && c.selection.model === current.model)]
203
+ const reasoning = currentChoice?.model.reasoning
204
+ const effectiveEffort = current?.reasoningEffort ?? reasoning?.defaultEffort
205
+ const effortLabel = reasoning === undefined ? undefined
206
+ : effectiveEffort === undefined ? t('effortProviderDefault')
207
+ : reasoning.efforts.find(level => level.id === effectiveEffort)?.name ?? effectiveEffort
208
+ const effortChoices = useMemo<Array<{ key: string; effort: string | undefined; label: string; description?: string }>>(() => reasoning === undefined ? [] : [
209
+ ...(reasoning.defaultEffort === undefined ? [{
210
+ key: 'provider-default',
211
+ effort: undefined as string | undefined,
212
+ label: t('effortProviderDefault'),
213
+ }] : []),
214
+ ...reasoning.efforts.map(effort => ({
215
+ key: `effort:${effort.id}`,
216
+ effort: effort.id,
217
+ label: effort.name,
218
+ ...effort.description === undefined ? {} : { description: effort.description },
219
+ })),
220
+ ], [reasoning, t])
221
+
222
+ const busy = directoryState.status === 'selecting'
223
+ const reload = () => {
224
+ lastActionRef.current = 'load'
225
+ load()
226
+ }
227
+
228
+ // Close on outside mousedown (host behavior).
229
+ useEffect(() => {
230
+ if (!open) return
231
+ const closeOutside = (event: MouseEvent) => {
232
+ if (!rootRef.current?.contains(event.target as Node)) setOpen(false)
233
+ }
234
+ document.addEventListener('mousedown', closeOutside)
235
+ return () => {
236
+ document.removeEventListener('mousedown', closeOutside)
237
+ }
238
+ }, [open])
239
+
240
+ if (!available) return null
241
+
242
+ const show = () => {
243
+ setPane('root')
244
+ setOpen(true)
245
+ reload()
246
+ void loadRouterState()
247
+ }
248
+ const close = (restoreFocus = false) => {
249
+ setOpen(false)
250
+ setPane('root')
251
+ if (restoreFocus) queueMicrotask(() => {
252
+ triggerRef.current?.focus()
253
+ })
254
+ }
255
+ const moveFocus = (offset: number) => {
256
+ const items = itemRefs.current.filter((item): item is HTMLButtonElement => item !== null)
257
+ if (items.length === 0) return
258
+ const active = items.findIndex(item => item === document.activeElement)
259
+ items[(Math.max(active, 0) + offset + items.length) % items.length]?.focus()
260
+ }
261
+ const onRootKeyDown = (event: ReactKeyboardEvent) => {
262
+ if (event.key === 'Escape' && open) {
263
+ event.preventDefault()
264
+ if (pane !== 'root') setPane('root')
265
+ else close(true)
266
+ return
267
+ }
268
+ if (!open) return
269
+ if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {
270
+ event.preventDefault()
271
+ moveFocus(event.key === 'ArrowDown' ? 1 : -1)
272
+ }
273
+ }
274
+ const onBlur = (event: ReactFocusEvent) => {
275
+ if (event.relatedTarget instanceof Node && rootRef.current?.contains(event.relatedTarget)) return
276
+ close()
277
+ }
278
+ const settleSelection = (accepted: boolean) => {
279
+ if (accepted) {
280
+ if (rootRef.current !== null) close(true)
281
+ return
282
+ }
283
+ const message = directory.getSnapshot().error
284
+ if (message !== null) {
285
+ toastSeq.current += 1
286
+ setToast({ seq: toastSeq.current, text: t('errorAction', { message }) })
287
+ }
288
+ }
289
+ const choose = (selection: ModelSelection) => {
290
+ if (current !== null && current.provider === selection.provider && current.model === selection.model) {
291
+ close(true)
292
+ return
293
+ }
294
+ lastActionRef.current = 'select'
295
+ select(selection).then(settleSelection)
296
+ }
297
+ const chooseEffort = (effort: string | undefined) => {
298
+ if (current === null) return
299
+ if (effectiveEffort === effort) {
300
+ close(true)
301
+ return
302
+ }
303
+ const selection: ModelSelection = {
304
+ provider: current.provider,
305
+ model: current.model,
306
+ ...effort === undefined ? {} : { reasoningEffort: effort },
307
+ }
308
+ lastActionRef.current = 'select'
309
+ select(selection).then(settleSelection)
310
+ }
311
+
312
+ const modelLabel = currentChoice?.model.name ?? t('triggerFallback')
313
+ const triggerLabel = effortLabel === undefined ? modelLabel : `${modelLabel} · ${effortLabel}`
314
+ const triggerAria = currentChoice === undefined ? t('triggerSelectAria')
315
+ : effortLabel === undefined ? t('triggerAria', { model: modelLabel })
316
+ : t('triggerAriaEffort', { model: modelLabel, effort: effortLabel })
317
+
318
+ // ---- provider row visibility (the former quick-switch button conditions) ----
319
+ const activeProvider = providers.find(provider => provider.id === routeModel?.active)
320
+ const activeProviderName = activeProvider?.name ?? routeModel?.active ?? t('routeLoading')
321
+ const providerRowVisible = isRouterModel
322
+ && routerState !== null
323
+ && routerState.showQuickSwitch !== false
324
+ && routerState.providers.length >= 2
325
+ && routeModel !== undefined
326
+ && providers.length >= 2
327
+
328
+ itemRefs.current = []
329
+ let itemIndex = 0
330
+ const itemRef = () => {
331
+ const at = itemIndex++
332
+ return (node: HTMLButtonElement | null) => {
333
+ itemRefs.current[at] = node
334
+ }
335
+ }
336
+
337
+ return (
338
+ <div
339
+ ref={rootRef}
340
+ className="mr-seat-root"
341
+ onKeyDown={onRootKeyDown}
342
+ onBlur={onBlur}
343
+ >
344
+ <button
345
+ ref={triggerRef}
346
+ type="button"
347
+ className="mr-seat-trigger"
348
+ aria-label={triggerAria}
349
+ aria-haspopup="menu"
350
+ aria-expanded={open}
351
+ aria-controls={open ? `${id}-menu` : undefined}
352
+ title={triggerLabel}
353
+ disabled={locked}
354
+ onClick={() => {
355
+ if (open) close()
356
+ else show()
357
+ }}
358
+ >
359
+ <span className="mr-seat-triggerLabel">{modelLabel}</span>
360
+ {effortLabel !== undefined && (
361
+ <span className="mr-seat-triggerEffort">{effortLabel}</span>
362
+ )}
363
+ <span className={`mr-seat-chevron${open ? ' mr-seat-chevronOpen' : ''}`} aria-hidden>
364
+ <IconChevronDownOutline14 />
365
+ </span>
366
+ </button>
367
+ {open && (
368
+ <div
369
+ id={`${id}-menu`}
370
+ className="mr-seat-menu"
371
+ role="menu"
372
+ aria-label={t('menuAria')}
373
+ aria-busy={directoryState.status === 'loading' || busy}
374
+ >
375
+ {pane === 'root' && (
376
+ <>
377
+ <button
378
+ ref={itemRef()}
379
+ type="button"
380
+ role="menuitem"
381
+ className="mr-seat-cell"
382
+ onClick={() => setPane('model')}
383
+ >
384
+ <span className="mr-seat-cellLabel">{t('menuModel')}</span>
385
+ <span className="mr-seat-cellValue">{modelLabel}</span>
386
+ <span className="mr-seat-cellChevron" aria-hidden>
387
+ <IconChevronRightOutline14 />
388
+ </span>
389
+ </button>
390
+ {providerRowVisible && (
391
+ <button
392
+ ref={itemRef()}
393
+ type="button"
394
+ role="menuitem"
395
+ className="mr-seat-cell"
396
+ onClick={() => setPane('provider')}
397
+ >
398
+ <span className="mr-seat-cellLabel">{t('menuProvider')}</span>
399
+ <span className="mr-seat-cellValue">{activeProviderName}</span>
400
+ <span className="mr-seat-cellChevron" aria-hidden>
401
+ <IconChevronRightOutline14 />
402
+ </span>
403
+ </button>
404
+ )}
405
+ {reasoning !== undefined && (
406
+ <button
407
+ ref={itemRef()}
408
+ type="button"
409
+ role="menuitem"
410
+ className="mr-seat-cell"
411
+ onClick={() => setPane('effort')}
412
+ >
413
+ <span className="mr-seat-cellLabel">{t('menuEffort')}</span>
414
+ <span className="mr-seat-cellValue">{effortLabel}</span>
415
+ <span className="mr-seat-cellChevron" aria-hidden>
416
+ <IconChevronRightOutline14 />
417
+ </span>
418
+ </button>
419
+ )}
420
+ </>
421
+ )}
422
+ {pane === 'model' && (
423
+ <>
424
+ {directoryState.status === 'loading' && (
425
+ <div className="mr-seat-status">{t('statusLoading')}</div>
426
+ )}
427
+ {directoryState.error !== null && lastActionRef.current === 'load' && (
428
+ <div className="mr-seat-error">
429
+ <span>{t('errorAction', { message: directoryState.error })}</span>
430
+ <button type="button" className="mr-seat-retry" onClick={reload}>
431
+ {t('retry')}
432
+ </button>
433
+ </div>
434
+ )}
435
+ {directoryState.failures.map(failure => (
436
+ <div key={failure.id} className="mr-seat-warning">
437
+ <span>{t('warningGroupLoad', { name: failure.name, message: failure.message })}</span>
438
+ <button type="button" className="mr-seat-retry" onClick={reload}>
439
+ {t('retry')}
440
+ </button>
441
+ </div>
442
+ ))}
443
+ <div className="mr-seat-groups">
444
+ {directoryState.groups.map(group => {
445
+ const headingId = `${id}-${group.id}`
446
+ return (
447
+ <section
448
+ key={group.id}
449
+ role="group"
450
+ aria-labelledby={headingId}
451
+ className="mr-seat-group"
452
+ >
453
+ <div className="mr-seat-groupTitle" id={headingId}>
454
+ {group.name}
455
+ </div>
456
+ {group.models.map(model => {
457
+ const selected = current?.provider === group.id && current.model === model.id
458
+ return (
459
+ <button
460
+ key={model.id}
461
+ ref={itemRef()}
462
+ type="button"
463
+ role="menuitemradio"
464
+ aria-checked={selected}
465
+ className={`mr-seat-option${selected ? ' mr-seat-selected' : ''}`}
466
+ title={model.name}
467
+ disabled={busy}
468
+ onClick={() => choose({ provider: group.id, model: model.id })}
469
+ >
470
+ <span className="mr-seat-optionCopy">
471
+ <span className="mr-seat-modelName">{model.name}</span>
472
+ {model.description !== undefined && (
473
+ <span className="mr-seat-description">{model.description}</span>
474
+ )}
475
+ </span>
476
+ <span className="mr-seat-check" aria-hidden>
477
+ {selected ? <IconCheckOutline16 /> : null}
478
+ </span>
479
+ </button>
480
+ )
481
+ })}
482
+ </section>
483
+ )
484
+ })}
485
+ </div>
486
+ {directoryState.status === 'ready' && choices.length === 0 && (
487
+ <div className="mr-seat-empty">{t('emptyModels')}</div>
488
+ )}
489
+ </>
490
+ )}
491
+ {pane === 'provider' && (
492
+ <>
493
+ {routerError !== null && (
494
+ <div className="mr-seat-error">
495
+ <span>{routerError}</span>
496
+ <button
497
+ type="button"
498
+ className="mr-seat-retry"
499
+ onClick={() => {
500
+ void loadRouterState()
501
+ load()
502
+ }}
503
+ >
504
+ {t('retry')}
505
+ </button>
506
+ </div>
507
+ )}
508
+ {routeModel === undefined ? (
509
+ <div className="mr-seat-status">{t('routeLoading')}</div>
510
+ ) : (
511
+ <>
512
+ <div className="mr-seat-title">{t('routeMenuTitle', { model: modelId ?? '' })}</div>
513
+ <div className="mr-seat-groups">
514
+ {providers.map(provider => {
515
+ const selected = routeModel.active === provider.id
516
+ return (
517
+ <button
518
+ key={provider.id}
519
+ ref={itemRef()}
520
+ type="button"
521
+ role="menuitemradio"
522
+ aria-checked={selected}
523
+ className={`mr-seat-option${selected ? ' mr-seat-selected' : ''}`}
524
+ disabled={!provider.credentialConfigured || busyProvider !== null}
525
+ onClick={() => void switchProvider(provider.id)}
526
+ >
527
+ <span className="mr-seat-optionCopy">
528
+ <span className="mr-seat-modelName">{provider.name}</span>
529
+ {!provider.credentialConfigured && (
530
+ <span className="mr-seat-description">{t('noCredentialShort')}</span>
531
+ )}
532
+ </span>
533
+ <span className="mr-seat-check" aria-hidden>
534
+ {selected ? <IconCheckOutline16 /> : null}
535
+ </span>
536
+ </button>
537
+ )
538
+ })}
539
+ </div>
540
+ <div className="mr-seat-hint">{t('globalRouteHint')}</div>
541
+ </>
542
+ )}
543
+ </>
544
+ )}
545
+ {pane === 'effort' && (
546
+ <>
547
+ {directoryState.error !== null && lastActionRef.current === 'load' && (
548
+ <div className="mr-seat-error">
549
+ <span>{t('errorAction', { message: directoryState.error })}</span>
550
+ <button type="button" className="mr-seat-retry" onClick={reload}>
551
+ {t('actionReload')}
552
+ </button>
553
+ </div>
554
+ )}
555
+ {effortChoices.length === 0 ? (
556
+ <div className="mr-seat-empty">{t('emptyEfforts')}</div>
557
+ ) : effortChoices.map(level => (
558
+ <button
559
+ key={level.key}
560
+ ref={itemRef()}
561
+ type="button"
562
+ role="menuitemradio"
563
+ aria-checked={effectiveEffort === level.effort}
564
+ className={`mr-seat-option${effectiveEffort === level.effort ? ' mr-seat-selected' : ''}`}
565
+ disabled={busy}
566
+ onClick={() => chooseEffort(level.effort)}
567
+ >
568
+ <span className="mr-seat-optionCopy">
569
+ <span className="mr-seat-modelName">{level.label}</span>
570
+ {level.description !== undefined && (
571
+ <span className="mr-seat-description">{level.description}</span>
572
+ )}
573
+ </span>
574
+ <span className="mr-seat-check" aria-hidden>
575
+ {effectiveEffort === level.effort ? <IconCheckOutline16 /> : null}
576
+ </span>
577
+ </button>
578
+ ))}
579
+ </>
580
+ )}
581
+ </div>
582
+ )}
583
+ {toast !== null && (
584
+ <Toast
585
+ key={toast.seq}
586
+ text={toast.text}
587
+ icon={<IconWarningOutline16 />}
588
+ anchor={rootRef.current?.closest('[data-composer-card]') ?? null}
589
+ onDone={() => setToast(null)}
590
+ />
591
+ )}
592
+ </div>
593
+ )
594
+ }
595
+
596
+ /** Register the model seat onto the composer's named model slot (priority -1 shadows the host seat). */
597
+ export function registerModelRouterSeat(ctx: ClientContext): void {
598
+ const t = ctx.locale.bind(LOCALE_NS)
599
+ ctx.inject(['slots', 'modelDirectories'], scope => {
600
+ const models = scope.modelDirectories
601
+ const sessions = scope.sessions
602
+ scope.slots.inject('conversation.input.model', () => scope.slots.register({
603
+ name: 'conversation.input.model',
604
+ id: 'model-router-model-seat',
605
+ priority: -1,
606
+ label: () => t('seatLabel'),
607
+ locale: LOCALE_NS,
608
+ inject: (sessionId: SessionId) => {
609
+ const directory = models.directoryFor(sessionId)
610
+ const available = sessions.subagentAddress(sessionId) === undefined
611
+ return {
612
+ available,
613
+ directory: directory.store,
614
+ load: () => {
615
+ if (available) directory.load().catch(() => {})
616
+ },
617
+ select: (selection: ModelSelection) =>
618
+ available ? directory.select(selection).then(() => true, () => false) : Promise.resolve(false),
619
+ }
620
+ },
621
+ } as never, ModelRouterSeat as never))
622
+ })
623
+ }