feat(desktop): add desktop.reset_model_on_new_session config flag - #340
Conversation
By default the desktop app persists the last composer model pick to localStorage and carries it across new chats (Cmd+N) and restarts, so picking an expensive model makes it the sticky default for every new session. This adds an opt-in config knob to instead reseed a fresh draft's model from the profile default. - HermesConfig type gains desktop.reset_model_on_new_session (optional). - useHermesConfig mirrors it into a new $resetModelOnNewSession atom. - refreshCurrentModel treats a fresh draft as a forced reseed when the flag is on, overwriting the sticky pick with the profile default. A live session still owns its model (never disturbed mid-conversation). - The fresh-draft effect loads config before the model reseed so the flag is in the store first (avoids a cold-boot race). Default false preserves the existing sticky-last-pick behavior. Backend needs no change: /api/config already returns the whole config, so the desktop section flows through untouched. Tests: use-model-controls (flag on reseeds, flag off keeps pick, active session untouched, explicit force still works) + use-hermes-config (mirrors true, defaults false when absent). Full desktop UI suite green (1283 passed), typecheck clean.
|
| Filename | Overview |
|---|---|
| apps/desktop/src/types/hermes.ts | Adds optional desktop.reset_model_on_new_session field to HermesConfig; clean type extension with a helpful inline comment. |
| apps/desktop/src/store/session.ts | Adds $resetModelOnNewSession atom (default false, no localStorage persistence) and its setter; mirrors the pattern used by $yoloActive and other config-reflection atoms. |
| apps/desktop/src/app/session/hooks/use-hermes-config.ts | Calls setResetModelOnNewSession(config.desktop?.reset_model_on_new_session === true) unconditionally (no active-session guard), which is intentional — the flag governs new-session behavior, not the live session's model. |
| apps/desktop/src/app/session/hooks/use-model-controls.ts | Introduces `effectiveForce = force |
| apps/desktop/src/app/desktop-controller.tsx | Serialises config fetch before model seeding to avoid a cold-boot race; this is correct but adds a sequential round-trip on every fresh-draft open regardless of whether the flag is enabled. |
| apps/desktop/src/app/session/hooks/use-model-controls.test.tsx | Adds two focused tests (flag ON reseeds; active session not disturbed); cleans atom state in both beforeEach and afterEach for isolation. |
| apps/desktop/src/app/session/hooks/use-hermes-config.test.ts | Adds two tests for the store mirror (flag=true written; absent key clears a stale true); correctly resets state in beforeEach. |
Sequence Diagram
%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
participant DC as DesktopController
participant HC as useHermesConfig
participant MC as useModelControls
participant Store as $store
participant API as /api/config + getGlobalModelInfo
Note over DC: gatewayState=open, !activeSession, freshDraftReady
DC->>HC: refreshHermesConfig()
HC->>API: getHermesConfig()
API-->>HC: "{ desktop: { reset_model_on_new_session: true } }"
HC->>Store: setResetModelOnNewSession(true)
HC-->>DC: resolved (.finally fires)
DC->>MC: refreshCurrentModel()
MC->>Store: $activeSessionId.get() → null (proceed)
MC->>Store: $resetModelOnNewSession.get() → true
Note over MC: effectiveForce = true, skip early-exit guard
MC->>API: getGlobalModelInfo()
API-->>MC: "{ model: "profile-default", provider: "..." }"
MC->>Store: $activeSessionId.get() → null (proceed)
MC->>Store: setCurrentModel("profile-default")
MC->>Store: setCurrentProvider("...")
%%{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 DC as DesktopController
participant HC as useHermesConfig
participant MC as useModelControls
participant Store as $store
participant API as /api/config + getGlobalModelInfo
Note over DC: gatewayState=open, !activeSession, freshDraftReady
DC->>HC: refreshHermesConfig()
HC->>API: getHermesConfig()
API-->>HC: "{ desktop: { reset_model_on_new_session: true } }"
HC->>Store: setResetModelOnNewSession(true)
HC-->>DC: resolved (.finally fires)
DC->>MC: refreshCurrentModel()
MC->>Store: $activeSessionId.get() → null (proceed)
MC->>Store: $resetModelOnNewSession.get() → true
Note over MC: effectiveForce = true, skip early-exit guard
MC->>API: getGlobalModelInfo()
API-->>MC: "{ model: "profile-default", provider: "..." }"
MC->>Store: $activeSessionId.get() → null (proceed)
MC->>Store: setCurrentModel("profile-default")
MC->>Store: setCurrentProvider("...")
Reviews (1): Last reviewed commit: "feat(desktop): add desktop.reset_model_o..." | Re-trigger Greptile
| useEffect(() => { | ||
| if (gatewayState === 'open' && !activeSessionId && freshDraftReady) { | ||
| void refreshCurrentModel() | ||
| void refreshHermesConfig() | ||
| // Load config first so the reset-on-new-session flag is in the store | ||
| // before refreshCurrentModel decides whether to force-reseed the model. | ||
| void refreshHermesConfig().finally(() => { | ||
| void refreshCurrentModel() | ||
| }) | ||
| } | ||
| }, [activeSessionId, freshDraftReady, gatewayState, refreshCurrentModel, refreshHermesConfig]) |
There was a problem hiding this comment.
Sequential fetch adds latency for the default (flag-off) case
Previously refreshCurrentModel and refreshHermesConfig raced in parallel, so the model was typically seeded as fast as the faster of the two calls. Now refreshCurrentModel is always blocked behind a full /api/config round-trip on every fresh-draft open — even when reset_model_on_new_session is false (the default for all users). On a slow connection the composer model slot will appear blank noticeably longer on startup.
One option is to fire both calls in parallel but have refreshCurrentModel re-check the flag after config settles, e.g. by awaiting the config promise first only if the flag is known to be true from a previous fetch, or by passing the flag as a parameter. The current approach is safe and correct; this is a tradeoff worth being aware of if startup latency is a concern.
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
What
Adds an opt-in config knob
desktop.reset_model_on_new_session(default false). When on, a fresh desktop chat (Cmd+N / app relaunch) reseeds the composer model to the profile default instead of carrying the last-picked model forward.Why
Today the desktop app deliberately persists the last composer model pick to localStorage (
hermes.desktop.composer.model) and carries it across new chats and restarts (see the comment block instore/session.ts). That's great for most workflows, but it means picking an expensive model once makes it the sticky default for every new session until manually switched back. This flag lets a user keep new chats pinned to their configured default and opt into the pricey model per-session.How
HermesConfigtype gains optionaldesktop.reset_model_on_new_session.useHermesConfigmirrors it into a new$resetModelOnNewSessionstore atom.refreshCurrentModeltreats a fresh draft as a forced reseed when the flag is on (overwrites the sticky pick with the profile default). A live session still owns its model — never disturbed mid-conversation (the existing$activeSessionIdguard).Default
falsepreserves the existing sticky-last-pick behavior exactly. No backend change:/api/configalready returns the whole config, so thedesktop:section flows through untouched.Tests
use-model-controls: flag ON reseeds a sticky pick to default; flag OFF keeps the pick (default is not even consulted); active session untouched with flag ON; explicitforce=truestill reseeds.use-hermes-config: mirrorstrue; defaults tofalsewhen the key is absent (clears a stale true).tsc --noEmitclean.