From c732ef975ee8a9b023e7740c982adeb7e82c6bc4 Mon Sep 17 00:00:00 2001 From: SilentKnight87 <54852388+SilentKnight87@users.noreply.github.com> Date: Wed, 8 Jul 2026 08:39:31 -0400 Subject: [PATCH] feat(tui): ctrl+a toggle to hide unconfigured providers in the model picker --- ui-tui/src/__tests__/modelPicker.test.ts | 36 ++++++++++-- ui-tui/src/components/modelPicker.tsx | 71 ++++++++++++++++++++---- 2 files changed, 91 insertions(+), 16 deletions(-) diff --git a/ui-tui/src/__tests__/modelPicker.test.ts b/ui-tui/src/__tests__/modelPicker.test.ts index 1557973e8b3af..aa97792dc8903 100644 --- a/ui-tui/src/__tests__/modelPicker.test.ts +++ b/ui-tui/src/__tests__/modelPicker.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from 'vitest' -import { providerIndexAfterClearingFilter } from '../components/modelPicker.js' +import { modelOptionsRequestParams, providerIndexAfterClearingFilter } from '../components/modelPicker.js' import type { ModelOptionProvider } from '../gatewayTypes.js' const provider = (slug: string, name = slug): ModelOptionProvider => ({ name, slug }) @@ -21,9 +21,7 @@ describe('ModelPicker provider filtering', () => { }) it('returns -1 when provider is undefined', () => { - const rows = [ - { name: 'A', provider: provider('a') } - ] + const rows = [{ name: 'A', provider: provider('a') }] expect(providerIndexAfterClearingFilter(rows, undefined)).toBe(-1) }) @@ -52,3 +50,33 @@ describe('ModelPicker provider filtering', () => { expect(providerIndexAfterClearingFilter(rows, p)).toBe(0) }) }) + +describe('ModelPicker model.options params', () => { + it('requests the full provider universe by default', () => { + expect(modelOptionsRequestParams('sess-1', false, false)).toEqual({ + session_id: 'sess-1', + include_unconfigured: true + }) + }) + + it('requests explicit configured providers when hiding unconfigured providers', () => { + expect(modelOptionsRequestParams('sess-1', false, true)).toEqual({ + session_id: 'sess-1', + explicit_only: true + }) + }) + + it('preserves refresh while requesting all providers', () => { + expect(modelOptionsRequestParams(null, true, false)).toEqual({ + refresh: true, + include_unconfigured: true + }) + }) + + it('preserves refresh while requesting configured providers', () => { + expect(modelOptionsRequestParams(null, true, true)).toEqual({ + refresh: true, + explicit_only: true + }) + }) +}) diff --git a/ui-tui/src/components/modelPicker.tsx b/ui-tui/src/components/modelPicker.tsx index 149047e4002e0..ec8a2453dd6e2 100644 --- a/ui-tui/src/components/modelPicker.tsx +++ b/ui-tui/src/components/modelPicker.tsx @@ -17,9 +17,19 @@ const MAX_WIDTH = 90 type Stage = 'provider' | 'key' | 'model' | 'disconnect' +type ModelOptionsRequestParams = { + explicit_only?: true + include_unconfigured?: true + refresh?: true + session_id?: string +} + type ProviderRow = { name: string; provider: ModelOptionProvider } -export function providerIndexAfterClearingFilter(providerRows: ProviderRow[], provider: ModelOptionProvider | undefined) { +export function providerIndexAfterClearingFilter( + providerRows: ProviderRow[], + provider: ModelOptionProvider | undefined +) { if (!provider) { return -1 } @@ -27,6 +37,22 @@ export function providerIndexAfterClearingFilter(providerRows: ProviderRow[], pr return providerRows.findIndex(row => row.provider.slug === provider.slug) } +// The TUI picker defaults to the full provider universe with setup +// affordances ("paste KEY to activate"), so it opts into unconfigured +// rows — the backend defaults to the configured subset for desktop chat +// pickers (#56974). ^a flips to that same explicit-providers subset. +export function modelOptionsRequestParams( + sessionId: string | null, + initialRefresh: boolean, + hideUnconfigured: boolean +): ModelOptionsRequestParams { + return { + ...(sessionId ? { session_id: sessionId } : {}), + ...(initialRefresh ? { refresh: true } : {}), + ...(hideUnconfigured ? { explicit_only: true } : { include_unconfigured: true }) + } +} + export function ModelPicker({ allowPersistGlobal = true, gw, @@ -49,6 +75,7 @@ export function ModelPicker({ const [keyError, setKeyError] = useState('') // Type-to-filter query, scoped per stage (cleared on stage change). const [filter, setFilter] = useState('') + const [hideUnconfigured, setHideUnconfigured] = useState(false) const { stdout } = useStdout() // Pin the picker to a stable width so the FloatBox parent (which shrinks- @@ -58,16 +85,18 @@ export function ModelPicker({ const width = Math.max(MIN_WIDTH, Math.min(MAX_WIDTH, (stdout?.columns ?? 80) - 6)) useEffect(() => { - gw.request('model.options', { - ...(sessionId ? { session_id: sessionId } : {}), - ...(initialRefresh ? { refresh: true } : {}), - // The TUI picker shows the full provider universe with setup - // affordances ("paste KEY to activate"), so opt into unconfigured - // rows — the backend now defaults to the configured subset for - // desktop chat pickers (#56974). - include_unconfigured: true - }) + let cancelled = false + + setLoading(true) + gw.request( + 'model.options', + modelOptionsRequestParams(sessionId, initialRefresh, hideUnconfigured) + ) .then(raw => { + if (cancelled) { + return + } + const r = asRpcResult(raw) if (!r) { @@ -92,10 +121,18 @@ export function ModelPicker({ setLoading(false) }) .catch((e: unknown) => { + if (cancelled) { + return + } + setErr(rpcErrorMessage(e)) setLoading(false) }) - }, [gw, initialRefresh, sessionId]) + + return () => { + cancelled = true + } + }, [gw, hideUnconfigured, initialRefresh, sessionId]) const names = useMemo(() => providerDisplayNames(providers), [providers]) @@ -424,6 +461,14 @@ export function ModelPicker({ return } + if (key.ctrl && ch === 'a' && stage === 'provider') { + setHideUnconfigured(v => !v) + setFilter('') + setProviderIdx(0) + + return + } + // Any other printable single character extends the filter. if (ch && !key.ctrl && !key.meta && ch.length === 1 && ch >= ' ') { setFilter(v => v + ch) @@ -553,6 +598,8 @@ export function ModelPicker({ const { items, offset } = windowItems(rows, providerIdx, VISIBLE) const noMatches = !!filter.trim() && rows.length === 0 + const providerToggleHint = hideUnconfigured ? '^a show all' : '^a hide unconfigured' + const providerOverlayHint = `↑/↓ select · Enter choose · ^d disconnect · ${providerToggleHint} · Esc clear/back · q close` return ( @@ -615,7 +662,7 @@ export function ModelPicker({ persist: {allowPersistGlobal ? (persistGlobal ? 'global' : 'session') : 'session'} {allowPersistGlobal ? ' · ^g toggle' : ' only'} - ↑/↓ select · Enter choose · ^d disconnect · Esc clear/back · q close + {providerOverlayHint} ) }