Skip to content

feat(desktop): add desktop.reset_model_on_new_session config flag - #340

Merged
Kyzcreig merged 1 commit into
mainfrom
fix/desktop-reset-model-on-new-session
Jul 14, 2026
Merged

Kyzcreig merged 1 commit into
mainfrom
fix/desktop-reset-model-on-new-session

Conversation

@Kyzcreig

Copy link
Copy Markdown
Collaborator

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 in store/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

  • HermesConfig type gains optional desktop.reset_model_on_new_session.
  • useHermesConfig mirrors it into a new $resetModelOnNewSession store atom.
  • refreshCurrentModel treats 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 $activeSessionId guard).
  • The fresh-draft effect now 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 exactly. No backend change: /api/config already returns the whole config, so the desktop: 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; explicit force=true still reseeds.
  • use-hermes-config: mirrors true; defaults to false when the key is absent (clears a stale true).
  • RED-proven the flag tests fail without the logic change.
  • Full desktop UI suite green (1283 passed), tsc --noEmit clean.

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.
@Kyzcreig
Kyzcreig enabled auto-merge (squash) July 14, 2026 19:46
@Kyzcreig
Kyzcreig merged commit 77b9fc1 into main Jul 14, 2026
24 checks passed
@Kyzcreig
Kyzcreig deleted the fix/desktop-reset-model-on-new-session branch July 14, 2026 19:46
@greptile-apps

greptile-apps Bot commented Jul 14, 2026

Copy link
Copy Markdown

Greptile Summary

Adds an opt-in desktop.reset_model_on_new_session config flag (default false) that forces the composer model back to the profile default on every fresh draft, rather than carrying the last-picked model forward via localStorage. When false the existing sticky-last-pick behavior is preserved exactly.

  • Type / store / hook wiring: HermesConfig gets the optional field, $resetModelOnNewSession is a plain (non-persisted) atom mirrored from config by useHermesConfig, and refreshCurrentModel computes effectiveForce = force || $resetModelOnNewSession.get() to optionally bypass the sticky-model guard.
  • Boot sequencing: desktop-controller.tsx now serialises the fresh-draft effect — config is fetched first, then model seeding — so the flag is in the store before refreshCurrentModel reads it. This is correct but adds a sequential round-trip on every fresh-draft open for all users, including those with the flag off.
  • Guards preserved: the $activeSessionId early-exit and the post-getGlobalModelInfo re-check remain intact, so live sessions are never disturbed regardless of the flag.

Confidence Score: 4/5

Safe to merge. The flag-off default preserves existing behavior exactly, live sessions are protected by the existing $activeSessionId guard, and the four new tests confirm the intended semantics.

The logic is sound and well-tested. The one thing worth tracking is that serialising config-fetch before model-seeding now applies on every fresh-draft open, even for users who never enable the flag — a minor startup-latency tradeoff that's intentional but undocumented as a known side-effect.

apps/desktop/src/app/desktop-controller.tsx — the newly sequential refreshHermesConfig → refreshCurrentModel chain is the only path worth re-examining if startup latency regressions are reported.

Important Files Changed

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("...")
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 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("...")
Loading

Reviews (1): Last reviewed commit: "feat(desktop): add desktop.reset_model_o..." | Re-trigger Greptile

Comment on lines 1018 to 1026
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])

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 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!

@Kyzcreig
Kyzcreig restored the fix/desktop-reset-model-on-new-session branch September 21, 2026 10:31
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.

1 participant