Skip to content

add provider-specific model picker with mid-chat switching#91

Open
tulsi-builder wants to merge 1 commit intomainfrom
tulsi/model-picker
Open

add provider-specific model picker with mid-chat switching#91
tulsi-builder wants to merge 1 commit intomainfrom
tulsi/model-picker

Conversation

@tulsi-builder
Copy link
Copy Markdown
Collaborator

Overview

Category: new-feature
User Impact: Users can now see and select which AI model their provider uses, switch models or providers mid-conversation, and see a notification when the switch happens.

Problem: The model picker dropdown existed in the UI but was completely unpowered — it showed no models, couldn't be clicked, and changing providers mid-chat caused "Resource not found" errors from stale ACP session IDs.

Solution: Wire the existing dropdown to real model data from ACP providers (supporting both SessionModelState and SessionConfigOption paths), cache model lists in a shared store so navigation between Home and Chat is instant, and scope ACP session storage by provider to prevent cross-provider session reuse.

Known limitations

  • Within-provider model switching is best-effort. The ACP throwaway-process architecture means set_model can't reliably apply to an existing session. The user's model selection is stored and the pre-send sync attempts to apply it, but the provider may use its default. Cross-provider switching (Goose → Codex → Claude Code) works reliably.
  • Claude Code model discovery is slow (~60s). This is due to ACP spawning a fresh process for each acp_get_model_state call. A potential fix is querying model state from the long-lived AcpSessionRegistry session instead of spawning throwaway processes — flagged for engineering input.
  • Model state is not persisted across app restarts. modelStateByProvider is in-memory only. Each provider's models are re-fetched on first visit to the Home screen.
File changes

src-tauri/src/services/acp/payloads.rs
Added AvailableModelPayload and expanded ModelStatePayload to include source, configId, and availableModels fields.

src-tauri/src/services/acp/writer.rs
Emit model state from both on_model_state_update (SessionModelState) and on_config_option_update (SessionConfigOption with model category). Added normalization helpers for both paths.

src-tauri/src/commands/acp.rs
Added acp_get_model_state and acp_set_model Tauri commands. acp_get_model_state normalizes model state from either ACP path and falls back from load_session to new_session when stored session IDs are stale. Bootstrap no longer persists agent session IDs to avoid poisoning the send path.

src-tauri/src/lib.rs
Registered new acp_get_model_state and acp_set_model commands.

src-tauri/src/services/acp/store.rs
TauriStore now tracks provider_id. Cached agent session IDs are only returned when the stored provider matches, preventing cross-provider session reuse.

src-tauri/src/services/acp/mod.rs
Send path now passes provider ID to TauriStore::with_provider so session lookups are provider-scoped.

src/shared/types/chat.ts
Added ModelOption, ModelSelectionSource, and ProviderModelState types.

src/shared/api/acp.ts
Added acpGetModelState and acpSetModel frontend API wrappers.

src/features/chat/stores/chatSessionStore.ts
Added modelStateByProvider to store and setModelState action. setModelState preserves explicit user model selections when the provider reports a different default.

src/features/chat/hooks/useAcpStream.ts
Captures acp:model_state events and writes to the shared store.

src/features/chat/hooks/useChat.ts
Pre-send model sync attempts to apply the user's selected model before sending. Wrapped in try/catch so failures don't block the message.

src/features/chat/ui/ChatView.tsx
Bootstrap effect waits for effectiveWorkingDir, uses cached models from shared store when available, and avoids cascading re-fires. Model and provider switches inject system notifications. currentModel derivation simplified to a clear useMemo with explicit priority.

src/features/chat/ui/ChatInput.tsx
Accepts and passes through model picker props (currentModel, currentModelId, modelsLoading, availableModels, onModelChange).

src/features/chat/ui/ChatInputToolbar.tsx
Model dropdown renders from availableModels, shows loading state, and calls onModelChange on selection.

src/features/home/ui/HomeScreen.tsx
Model state reads/writes from shared modelStateByProvider store instead of local useState. Bootstrap writes to store so ChatView reads cached data on navigation.

src/app/AppShell.tsx
Passes model ID and name from Home screen into new chat session creation.

src/app/hooks/useAppStartup.ts
No functional change (pre-warm was added then reverted).

scripts/check-file-sizes.mjs
Added size exceptions for ChatView.tsx and acp.rs with justification.

src/features/chat/ui/tests/ChatInput.test.tsx
Added tests for model picker rendering, loading state, and selection callback.

src/features/home/ui/HomeScreen.test.tsx
Updated mock shapes for model bootstrap.

Reproduction Steps

  1. Start the app and navigate to the Home screen
  2. Select a provider (Goose, Codex, or Claude Code) — model dropdown should populate after bootstrap
  3. Select a model from the dropdown — label should update
  4. Send a message — response should come from the selected provider
  5. Switch providers mid-chat — "Switched to [Provider] ([Model])" notification should appear
  6. Send another message — response should come from the new provider
  7. Navigate from Home to Chat — model dropdown should populate instantly from cache (no "Loading models..." flash)

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f8e3640391

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +81 to +85
if let Some(stored_provider) = mapping["provider_id"].as_str() {
if stored_provider != expected_provider.as_str() {
return None;
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Reject legacy session mappings without provider IDs

The new provider guard only rejects cached ACP session IDs when provider_id exists and differs, but pre-upgrade mapping files do not contain provider_id, so they are still reused across providers. In that upgrade path, switching providers can still load an old session ID from another provider and re-trigger the same load_session/"Resource not found" failures this change is meant to eliminate. Treat missing provider_id as incompatible when expected_provider is set.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed. Legacy files without provider_id are now rejected when we expect a provider

Comment on lines +168 to +170
currentModelId: modelState.currentModelId,
currentModelName:
modelState.currentModelName ?? modelState.currentModelId,
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve home model choice during model-state refresh

This write always replaces the provider's current model with ACP's reported default, and the effect reruns on selectedProject/persona/provider changes. If a user picks a non-default model on Home and then changes project (or another dependency triggers bootstrap), their selection is silently overwritten before sending. This makes the picker state unstable and can cause the first chat to start with the wrong model.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Valid edge case. Unlikely to hit in normal usage (requires model pick --> project/persona change before sending), and the upcoming backend rewrite will change this bootstrap flow. Leaving as-is for now.

…tching

Backend:
- Expand acp:model_state payload to include availableModels from both
  SessionModelState and SessionConfigOption paths.
- Add acp_get_model_state and acp_set_model Tauri commands.
- Provider-scoped session storage prevents cross-provider session reuse.
- load_session falls back to new_session when stored session ID is stale.
- Bootstrap no longer persists agent session IDs (only driver.run does).

Frontend:
- Wire ChatView -> ChatInput -> ChatInputToolbar with real model data from
  shared modelStateByProvider store.
- HomeScreen writes to shared store so ChatView reads cached models instantly.
- ChatView skips ACP bootstrap when cached models exist for the provider.
- setModelState preserves explicit user model selection over provider defaults.
- Model and provider switches show system notifications in chat timeline.
- Pre-send model sync in useChat applies selected model (best-effort).
- Fix modelsLoading stuck true when bootstrap effects were cancelled.

Tests:
- Model picker rendering, loading state, and selection callback coverage.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants