opentui(v6): dedupe model.options prefetch with /model open

This commit is contained in:
alt-glitch 2026-06-10 23:10:11 +05:30
parent 036e863e4a
commit b3efafcc73
3 changed files with 85 additions and 7 deletions

View file

@ -42,6 +42,7 @@ import {
mapModelOptions,
planCompletion,
readReplaceFrom,
registerModelPrefetch,
type SlashContext
} from '../logic/slash.ts'
import { createSessionStore, type SessionStore } from '../logic/store.ts'
@ -196,11 +197,19 @@ const bootstrapSession = (gateway: GatewayServiceShape, store: SessionStore, inp
// that cost ONCE here in this already-forked bootstrap fiber; `/model` then
// paints from memory on the same frame. Best-effort: if this hasn't landed
// (or the RPC is missing/old), /model falls back to fetching on first open.
const modelOpts = yield* gateway
.request<unknown>('model.options', { session_id: sid })
.pipe(Effect.catchCause(() => Effect.succeed(undefined)))
const modelItems = mapModelOptions(modelOpts)
if (modelItems.length) store.setModelItems(modelItems)
// The promise is STASHED in the slash seam so a `/model` opened while the
// prefetch is still in flight awaits THIS request (bounded) instead of
// issuing a second concurrent model.options RPC (prefetch dedupe).
const prefetch = Effect.runPromise(
gateway
.request<unknown>('model.options', { session_id: sid })
.pipe(Effect.catchCause(() => Effect.succeed(undefined)))
).then(modelOpts => {
const modelItems = mapModelOptions(modelOpts)
if (modelItems.length) store.setModelItems(modelItems)
})
registerModelPrefetch(prefetch)
yield* Effect.promise(() => prefetch)
}).pipe(Effect.catchCause(cause => Effect.sync(() => getLog().warn('bootstrap', 'failed', { cause: String(cause) }))))
/** The entry Effect. Mirrors opencode `app.tsx:177` `run = Effect.fn("Tui.run")`. */

View file

@ -336,6 +336,30 @@ export function pickerTabs(items: readonly PickerItem[]): string[] {
return activePickerTabs?.(items) ?? []
}
/**
* The bootstrap `model.options` prefetch seam (perf: prefetch dedupe). The
* entry stashes its in-flight prefetch promise here; a bare `/model` that
* finds the cache empty AWAITS it (bounded by `waitMs`) and re-checks the
* cache instead of issuing a second concurrent `model.options` RPC. A hung
* prefetch only delays the picker by the bound `/model` then opens via its
* own fetch as before.
*/
let modelPrefetch: { promise: Promise<unknown>; waitMs: number } | undefined
/** Register (or clear, with `undefined`) the in-flight bootstrap prefetch. */
export function registerModelPrefetch(promise: Promise<unknown> | undefined, waitMs = 2000): void {
modelPrefetch = promise ? { promise, waitMs } : undefined
}
/** Await the registered prefetch (bounded); resolves immediately when none. */
function awaitModelPrefetch(): Promise<void> {
const pending = modelPrefetch
if (!pending) return Promise.resolve()
return Promise.race([pending.promise, new Promise(resolve => setTimeout(resolve, pending.waitMs))]).then(
() => undefined
)
}
/** Switch the model via the server (shared by `/model <name>` and the picker pick).
* A successful switch refreshes the cached rows in the background (fresh ). */
async function switchModel(ctx: SlashContext, name: string): Promise<void> {
@ -350,7 +374,9 @@ async function switchModel(ctx: SlashContext, name: string): Promise<void> {
/** `/model` bare opens the model picker; `/model <name>` switches directly.
* Opens from the CACHED catalog when present zero RPCs, same-frame paint
* (Epic 7; the catalog is prefetched at bootstrap and refreshed on switch). */
* (Epic 7; the catalog is prefetched at bootstrap and refreshed on switch).
* An empty cache first awaits the in-flight bootstrap prefetch (bounded) so
* an early `/model` never doubles the slow `model.options` RPC. */
const modelCmd: ClientHandler = async (arg, ctx) => {
if (arg.trim()) {
await switchModel(ctx, arg.trim())
@ -368,6 +394,14 @@ const modelCmd: ClientHandler = async (arg, ctx) => {
open(cached)
return
}
// Cache empty but the bootstrap prefetch may be in flight — await it
// (bounded) and re-check instead of racing a SECOND model.options RPC.
await awaitModelPrefetch()
const prefetched = ctx.modelItems()
if (prefetched?.length) {
open(prefetched)
return
}
const items = mapModelOptions(await ctx.request('model.options', { session_id: ctx.sessionId() }))
// Unavailable hint rows alone are not a usable catalog — keep the notice.
if (!items.some(i => !i.unavailable)) {

View file

@ -13,6 +13,7 @@ import {
pickerTabs,
planCompletion,
readReplaceFrom,
registerModelPrefetch,
registerPickerRefresh,
registerPickerTabs,
runPickerRefresh,
@ -22,10 +23,11 @@ import type { PickerItem, SessionItem } from '../logic/store.ts'
const FAKE_SESSIONS: SessionItem[] = [{ id: 's1', messageCount: 5, preview: 'hello there', title: 'First chat' }]
// the picker-refresh/tabs seams are module-level state — never leak them across tests
// the picker-refresh/tabs/prefetch seams are module-level state — never leak them across tests
afterEach(() => {
registerPickerRefresh(undefined)
registerPickerTabs(undefined)
registerModelPrefetch(undefined)
})
/** A `model.options` payload: two authed providers + two unconfigured skeleton
@ -399,6 +401,39 @@ describe('dispatchSlash — client commands', () => {
expect(pickerTabs(p.pickers[0]!.items)).toEqual([])
})
test('/model during an in-flight bootstrap prefetch performs ZERO additional model.options RPCs', async () => {
const p = makeCtx(async () => {
throw new Error('no RPC expected — the prefetch owns model.options')
})
// a slow prefetch (entry/main.tsx shape): resolving it fills the cache
let finish: () => void = () => {}
registerModelPrefetch(
new Promise<void>(resolve => {
finish = () => {
p.modelCache.value = [
{ group: 'Anthropic', haystacks: ['anthropic'], label: 'claude-sonnet-4.6', value: 'a' }
]
resolve()
}
}),
5000
)
const dispatched = dispatchSlash('/model', p.ctx)
finish() // prefetch lands while /model awaits it
await dispatched
expect(p.pickers).toHaveLength(1) // opened from the prefetched cache
expect(p.pickers[0]!.items).toHaveLength(1)
expect(p.calls).toHaveLength(0) // the dedupe: no second model.options
})
test('/model with a HUNG prefetch still opens via its own fetch after the bound', async () => {
const p = makeCtx(async method => (method === 'model.options' ? MODEL_OPTIONS : { output: 'switched' }))
registerModelPrefetch(new Promise(() => {}), 10) // never settles; tiny test bound
await dispatchSlash('/model', p.ctx)
expect(p.pickers).toHaveLength(1) // fell back to fetching itself
expect(p.calls.filter(c => c.method === 'model.options')).toHaveLength(1)
})
test('/model <name> switches directly without opening the picker', async () => {
const p = makeCtx(async () => ({ output: 'ok' }))
await dispatchSlash('/model anthropic/claude-opus-4.6', p.ctx)