Skip to content

fix: model picker in chat no longer overwrites the global default model#713

Merged
fathah merged 6 commits into
fathah:mainfrom
365diascollaboration-prog:fix/model-picker-preserves-default
Jun 17, 2026
Merged

fix: model picker in chat no longer overwrites the global default model#713
fathah merged 6 commits into
fathah:mainfrom
365diascollaboration-prog:fix/model-picker-preserves-default

Conversation

@365diascollaboration-prog

Copy link
Copy Markdown
Contributor

Problem

Selecting a model from the bottom-panel picker in a chat session permanently changes the user's global default model in Settings — even when the intent is only to try a different model for that one conversation. Closes #688.

Steps to reproduce:

  1. Note the current default model in Settings.
  2. Start a new chat and select a different model from the bottom panel.
  3. Open Settings → the default model has been permanently overwritten.

Root cause

useModelConfig.selectModel always called setModelConfig(), which writes to config.yaml. The chat-screen picker and the Settings screen shared the same code path with no distinction between "use for this session" and "save as my default".

Fix

Renderer side (useModelConfig, Chat.tsx, useChatActions):

  • selectModel gains a { persist?: boolean } option (default true — existing callers are unaffected).
  • When persist: false, the local React state updates immediately (so the picker UI reflects the choice) but setModelConfig / getModelConfig IPC calls are skipped — config.yaml stays untouched.
  • The chat-screen ModelPicker now passes persist: false; the Settings screen keeps using the default (persist: true).
  • useChatActions accepts a sessionModel prop and tracks it via a ref so sendToAgent always reads the latest session-scoped model without adding it to the callback's dependency array.

IPC pipeline (preload, index.ts, hermes.ts):

  • A modelOverride?: string parameter is added to every link in the send chain: sendMessage export → sendMessageViaBestApi*sendMessageViaNonGatewayApisendMessageViaRuns / sendMessageViaApi / sendMessageViaCli.
  • Each leaf function uses modelOverride || mc.model || "hermes-agent" so the session choice reaches the gateway without touching persisted config.

No behaviour change for existing callers — all new parameters are optional and default to the current behaviour.

Test plan

  1. Set a default model in Settings (e.g. claude-3-5-sonnet).
  2. Start a new chat, pick a different model from the bottom panel (e.g. gpt-4o).
  3. Send a message — the response comes from gpt-4o (check the session tag in the sidebar).
  4. Open Settings → default model is still claude-3-5-sonnet. ✅
  5. Start another new chat without picking — it uses claude-3-5-sonnet. ✅

Selecting a model from the bottom-panel picker in a chat session previously
called setModelConfig(), which wrote to config.yaml and permanently changed
the user's global default — even though the intent was only to override the
model for that conversation.

The fix separates session-scoped model selection from persistent (settings)
selection:

- `useModelConfig.selectModel` accepts a new `{ persist?: boolean }` option
  (default true). When `persist: false` the local React state is updated
  immediately but `setModelConfig` / `getModelConfig` IPC calls are skipped,
  so config.yaml stays untouched.

- The chat-screen picker now calls `selectModel(..., { persist: false })`,
  while the Settings screen retains the existing persisting behaviour.

- A `modelOverride` parameter is threaded through the full send pipeline
  (`useChatActions` → IPC preload → IPC handler → hermes.ts export →
  `sendMessageViaBestApi*` → `sendMessageViaNonGatewayApi` →
  `sendMessageViaRuns` / `sendMessageViaApi`, and the CLI fallback
  `sendMessageViaCli`) so the gateway receives the session model on every
  message instead of re-reading the now-unchanged config.yaml value.

Fixes fathah#688.
@greptile-apps

greptile-apps Bot commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes issue #688 where picking a model from the in-chat bottom panel was permanently overwriting the user's global default model in config.yaml. The fix introduces a persist: false option to selectModel, threads a modelOverride parameter through the entire IPC send chain, and bypasses the TUI gateway (which has no per-request model support) when an override is active.

  • Renderer side: useModelConfig.selectModel gains a persist option; the chat picker calls it with persist: false and stores the selection in a new sessionModelOverride state that is forwarded to useChatActions via a ref.
  • IPC/main side: modelOverride is plumbed through sendMessagesendMessageViaBestApiWithLocalRecovery → every downstream leaf function (sendMessageViaRuns, sendMessageViaApi, sendMessageViaCli), with the TUI gateway conditionally skipped when an override is set.
  • Gap: The sendViaDashboard fast-path in useChatActions.sendToAgent returns before window.hermesAPI.sendMessage is called, so sessionModelRef.current is never forwarded for users on the dashboard (SSH) transport — the session model pick has no effect on that path.

Confidence Score: 4/5

Safe to merge for local-mode users, but the dashboard (SSH) transport path does not forward the session model override, so the fix is incomplete for that deployment scenario.

The local-mode IPC chain is correctly updated end-to-end and the TUI gateway bypass is a sound workaround for its lack of per-request model support. However, in useChatActions.sendToAgent, when sendViaDashboard handles the message and returns true, execution returns before window.hermesAPI.sendMessage is reached — the sessionModelRef.current value is never sent to the backend. SSH/remote users with dashboard transport enabled will find that picking a session model in the chat picker has no effect, the same silent-ignore behaviour the PR is trying to eliminate.

src/renderer/src/screens/Chat/hooks/useChatActions.ts — the sendViaDashboard early-return path needs to accept and forward modelOverride.

Important Files Changed

Filename Overview
src/renderer/src/screens/Chat/hooks/useChatActions.ts Adds sessionModel prop tracked via ref to avoid stale closures; correctly passes modelOverride in the hermesAPI path, but the sendViaDashboard fast-path returns before modelOverride is forwarded, silently dropping the session model for dashboard transport users.
src/main/hermes.ts modelOverride threaded through the full send chain (sendMessage → sendMessageViaBestApiWithLocalRecovery → sendMessageViaBestApi → sendMessageViaNonGatewayApi → Runs/Api/Cli); TUI gateway correctly bypassed when override is set; fallbackToChatCompletions inside sendMessageViaRuns now propagates the override.
src/renderer/src/screens/Chat/hooks/useModelConfig.ts Adds persist option to selectModel; uses loadSeqRef increment to block stale reload from overwriting session-scoped selection. Core logic is sound for the local transport path.
src/renderer/src/screens/Chat/Chat.tsx Adds sessionModelOverride state; correctly wires ModelPicker to persist:false path and stores the override for useChatActions. Clean separation from global model state.
src/main/index.ts Adds modelOverride as the last parameter to the send-message IPC handler and passes it to sendMessage. Backward-compatible change.
src/preload/index.ts Adds modelOverride to sendMessage IPC invoke call as the last positional argument, matching the updated handler signature.
src/preload/index.d.ts Type declaration updated to include modelOverride?: string on sendMessage. Backward-compatible.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant User
    participant ModelPicker
    participant useModelConfig
    participant Chat.tsx
    participant useChatActions
    participant IPC as IPC (preload → index.ts)
    participant sendMessage as sendMessage (hermes.ts)
    participant Gateway as TUI Gateway
    participant API as Non-Gateway API

    User->>ModelPicker: picks model (session only)
    ModelPicker->>useModelConfig: "selectModel(provider, model, baseUrl, {persist:false})"
    useModelConfig->>useModelConfig: setCurrentModel / setCurrentProvider (UI state only)
    useModelConfig->>useModelConfig: ++loadSeqRef (blocks stale reload)
    useModelConfig-->>ModelPicker: returns (config.yaml untouched)
    ModelPicker->>Chat.tsx: setSessionModelOverride(model)
    Chat.tsx->>useChatActions: "sessionModel = sessionModelOverride"

    User->>Chat.tsx: sends message
    Chat.tsx->>useChatActions: handleSend(text)
    useChatActions->>useChatActions: reads sessionModelRef.current
    useChatActions->>IPC: sendMessage(..., modelOverride)
    IPC->>sendMessage: sendMessage(..., modelOverride)

    alt modelOverride is set
        sendMessage->>sendMessage: skip TUI gateway check
        sendMessage->>API: sendMessageViaNonGatewayApi(..., modelOverride)
        API-->>User: response with session model
    else no override (normal flow)
        sendMessage->>Gateway: sendMessageViaTuiGateway(...)
        Gateway-->>User: response with config.yaml model
    end
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant User
    participant ModelPicker
    participant useModelConfig
    participant Chat.tsx
    participant useChatActions
    participant IPC as IPC (preload → index.ts)
    participant sendMessage as sendMessage (hermes.ts)
    participant Gateway as TUI Gateway
    participant API as Non-Gateway API

    User->>ModelPicker: picks model (session only)
    ModelPicker->>useModelConfig: "selectModel(provider, model, baseUrl, {persist:false})"
    useModelConfig->>useModelConfig: setCurrentModel / setCurrentProvider (UI state only)
    useModelConfig->>useModelConfig: ++loadSeqRef (blocks stale reload)
    useModelConfig-->>ModelPicker: returns (config.yaml untouched)
    ModelPicker->>Chat.tsx: setSessionModelOverride(model)
    Chat.tsx->>useChatActions: "sessionModel = sessionModelOverride"

    User->>Chat.tsx: sends message
    Chat.tsx->>useChatActions: handleSend(text)
    useChatActions->>useChatActions: reads sessionModelRef.current
    useChatActions->>IPC: sendMessage(..., modelOverride)
    IPC->>sendMessage: sendMessage(..., modelOverride)

    alt modelOverride is set
        sendMessage->>sendMessage: skip TUI gateway check
        sendMessage->>API: sendMessageViaNonGatewayApi(..., modelOverride)
        API-->>User: response with session model
    else no override (normal flow)
        sendMessage->>Gateway: sendMessageViaTuiGateway(...)
        Gateway-->>User: response with config.yaml model
    end
Loading

Reviews (6): Last reviewed commit: "Merge branch 'main' into pr/713" | Re-trigger Greptile

Comment thread src/renderer/src/screens/Chat/hooks/useModelConfig.ts Outdated
Two issues raised in the Greptile review of fathah#713:

1. TUI gateway silently drops modelOverride (P1)
   sendMessageViaBestApi tried the TUI gateway first, which reads its model
   from config.yaml and has no per-request override mechanism. A one-line
   `!modelOverride` guard now bypasses the TUI path when a session model is
   active, routing directly to sendMessageViaNonGatewayApi which already
   propagates modelOverride correctly.

2. Background reload() can clobber session-scoped model selection
   When selectModel is called with persist:false, loadSeqRef was not
   advanced, so a concurrent reload() triggered by onConnectionConfigChanged
   or onModelLibraryChanged could race and overwrite the in-session choice
   with the persisted value. Incrementing loadSeqRef on the non-persist path
   cancels any in-flight reload, consistent with how the persist path already
   handles seq races.
@365diascollaboration-prog

Copy link
Copy Markdown
Contributor Author

Hey @fathah 👋

Quería tomarte un momento para decirte lo mucho que aprecio lo que has construido con Hermes Desktop. Mis 12 colegas y yo lo usamos todos los días — ha cambiado genuinamente la forma en que trabajamos.

Me siento muy orgulloso de poder aportar algo de vuelta al proyecto. Mantengo la traducción al español LATAM porque para nosotros es importante tener la app disponible en nuestro idioma, y reviso las actualizaciones diariamente para mantenerla al día con los cambios del README.

También abrí este PR para corregir el bug del model picker (#688) — todos en mi equipo lo teníamos: cada vez que cambiábamos el modelo en el chat, se sobreescribía el modelo por defecto en Settings sin querer. Imagino que todos los usuarios que usan el picker lo sufren también.

Gracias por construir algo tan bueno y por dejarme ser parte de esto. Significa mucho para nosotros. 🙏

Passing modelConfig.currentModel as sessionModel caused modelOverride to
always be non-empty after the initial config load, permanently bypassing
the TUI gateway for all users regardless of whether they had explicitly
picked a session model.

Use a separate sessionModelOverride state variable (undefined by default)
that is only set when the user explicitly selects a model from the chat
picker. The TUI gateway bypass now triggers only for sessions where the
user made an explicit in-chat model change, which is the intended behaviour.
The fallbackToChatCompletions closure inside sendMessageViaRuns called
sendMessageViaApi without forwarding the modelOverride captured in the
outer scope. If the Runs transport failed mid-flight, the session model
selection was silently dropped on the fallback path.
@fathah fathah merged commit 308a089 into fathah:main Jun 17, 2026
1 check passed
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.

Bug: Selecting a model from the bottom panel in a new chat permanently changes the default model setting

2 participants