From b3efafcc73d29769ec55d38f324666f99ad9e97c Mon Sep 17 00:00:00 2001 From: alt-glitch Date: Wed, 10 Jun 2026 23:10:11 +0530 Subject: [PATCH] opentui(v6): dedupe model.options prefetch with /model open --- ui-opentui/src/entry/main.tsx | 19 +++++++++++----- ui-opentui/src/logic/slash.ts | 36 +++++++++++++++++++++++++++++- ui-opentui/src/test/slash.test.ts | 37 ++++++++++++++++++++++++++++++- 3 files changed, 85 insertions(+), 7 deletions(-) diff --git a/ui-opentui/src/entry/main.tsx b/ui-opentui/src/entry/main.tsx index 6b9d709dc4d..243a0834bc9 100644 --- a/ui-opentui/src/entry/main.tsx +++ b/ui-opentui/src/entry/main.tsx @@ -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('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('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")`. */ diff --git a/ui-opentui/src/logic/slash.ts b/ui-opentui/src/logic/slash.ts index b9c390fac6e..cda0837a119 100644 --- a/ui-opentui/src/logic/slash.ts +++ b/ui-opentui/src/logic/slash.ts @@ -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; waitMs: number } | undefined + +/** Register (or clear, with `undefined`) the in-flight bootstrap prefetch. */ +export function registerModelPrefetch(promise: Promise | undefined, waitMs = 2000): void { + modelPrefetch = promise ? { promise, waitMs } : undefined +} + +/** Await the registered prefetch (bounded); resolves immediately when none. */ +function awaitModelPrefetch(): Promise { + 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 ` and the picker pick). * A successful switch refreshes the cached rows in the background (fresh ✓). */ async function switchModel(ctx: SlashContext, name: string): Promise { @@ -350,7 +374,9 @@ async function switchModel(ctx: SlashContext, name: string): Promise { /** `/model` — bare opens the model picker; `/model ` 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)) { diff --git a/ui-opentui/src/test/slash.test.ts b/ui-opentui/src/test/slash.test.ts index 94bd9b7df0a..fb2994f8531 100644 --- a/ui-opentui/src/test/slash.test.ts +++ b/ui-opentui/src/test/slash.test.ts @@ -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(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 switches directly without opening the picker', async () => { const p = makeCtx(async () => ({ output: 'ok' })) await dispatchSlash('/model anthropic/claude-opus-4.6', p.ctx)