Menu

#64 feat(tui): reopenable cloud model picker, wired to /model

closed
nobody
None
2026-08-06
2026-08-06
Anonymous
No

Originally created by: sosidudku1

Closes [#62]. Based on the branch of the base-URL prefill fix; merge that one first.

Problem

After first-time setup the /v1/models list is unreachable: the Cloud pane renders a single model row for an openai-compatible provider and Enter re-selects it, /model only jumps to the LLM tab, and the only in-TUI way to switch models is re-running the wizard from the API key screen.

Change

  • New modelPicker modal in the LLM panel. Enter on the model row of an openai-compatible provider reopens the server's model list: 12-row window with wrap-around cursor, the current model marked, count in the footer. Enter switches the model mid-session through the existing providers_select_chat_model path (which already persists and applies the change), Esc cancels. An unreachable or unauthorized endpoint degrades to a closable error box instead of trapping the user.
  • /model now opens the picker for the active text provider by emitting providers_chat_model_picker_requested. The request no-ops for curated kinds (their models are already first-class rows), so the previous tab-jump behavior is unchanged there.
  • The async fetch is owned by ProvidersOrchestrator, following the existing bus-subscription pattern, and reuses fetchOpenAiCompatModels with its 1h per-URL-and-key cache from [#41]. Stale responses arriving after the picker was closed or reopened for another provider are dropped in the reducer.

Out of scope, possible follow-ups: fuzzy search inside the picker, recent-models ordering, a global hotkey.

Testing

10 new tests: reducer transitions (open/loaded with cursor on the current model/stale-response guards/failed) and modal keys (wrap-around navigation, select-and-close routing, Esc, loading swallows keys, error closes on Enter). The pre-named /model slash test now asserts the picker request it was named after. src/tui plus src/llm/provider: 730 passing, the 6 remaining failures are the pre-existing set documented in [#50], verified identical on clean main. tsc --noEmit clean.

Related

Tickets: #41
Tickets: #50
Tickets: #62

Discussion

  • Anonymous

    Anonymous - 2026-08-06

    Originally posted by: sosidudku1

    All three addressed, thanks for the review.

    1. Namespace/ownership. The picker state moved to providersPanel (chatModelPicker) and the actions are now providers_chat_model_picker_*, so ProvidersOrchestrator only emits its own namespace. The reducer cases moved to providers-reducer.ts alongside wizard/removeConfirm, and the tests moved with them to src/tui/providers/providers-chat-model-picker.test.ts.

    2. llmTabBusy. Added the open picker. While there I also added llmPanel.externalUrlDraft, which has been missing from that guard since [#50], so Tab/Ctrl+B leaked past the external URL editor too.

    3. Stale-response guard. Now keyed on a generation counter bumped on every open (chatModelPickerGeneration on the orchestrator, mirrored into the picker state) rather than on providerId, so the close-and-reopen-same-provider race you described is rejected properly rather than incidentally. There is a test for exactly that sequence.

    tsc clean; src/tui plus src/llm/provider show the same pre-existing failures as main (the two LlmHealthPoller ones are timing-flaky and pass in isolation).

     

    Related

    Tickets: #50

  • Anonymous

    Anonymous - 2026-08-06

    Ticket changed by: Ooooze

    • status: open --> closed
     

Log in to post a comment.