Skip to content

fix(desktop): single instance, model picker, and theme boot - #43457

Open
Nigmat-future wants to merge 2 commits into
NousResearch:mainfrom
Nigmat-future:fix/desktop-single-instance-model-picker-theme
Open

fix(desktop): single instance, model picker, and theme boot#43457
Nigmat-future wants to merge 2 commits into
NousResearch:mainfrom
Nigmat-future:fix/desktop-single-instance-model-picker-theme

Conversation

@Nigmat-future

@Nigmat-future Nigmat-future commented Jun 10, 2026

Copy link
Copy Markdown

Summary

  • Enforce Electron single-instance lock so launching Hermes Desktop again focuses the existing window instead of opening a second copy.
  • Fix empty model picker when cached model visibility is an empty set; stabilize model menu queries and patch model-control cache updates.
  • Apply boot-time theme/titlebar styling before first paint to reduce light/dark flash on startup.

Test plan

  • Launch Hermes Desktop twice quickly; second launch should focus existing window (no duplicate).
  • Open model picker with fresh/cleared visibility cache; models should list correctly.
  • Toggle system dark/light mode and cold-start the app; titlebar/theme should match without obvious flash.
  • Run desktop unit tests: npm test in apps/desktop (model visibility, use-model-controls, single-instance).

Made with Cursor

@alt-glitch alt-glitch added type/bug Something isn't working P3 Low — cosmetic, nice to have labels Jun 10, 2026
@Nigmat-future
Nigmat-future requested a review from a team June 10, 2026 09:56
austinpickett
austinpickett previously approved these changes Jun 10, 2026

@austinpickett austinpickett left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

✅ Approved

Three focused desktop fixes bundled correctly:

  1. Single-instance lock: app.requestSingleInstanceLock() prevents duplicate processes; second-instance handler focuses the existing window or creates a new one. Standard Electron pattern.

  2. Theme boot flash fix: getWindowBackgroundColor() reads rendererTitleBarTheme?.background (already loaded from prior sessions) or falls back to a dark/light default matching the OS theme. Applied to both createWindow() and createSessionWindow() so windows never repaint white on dark-mode startup.

  3. Title bar theme propagation: applyTitleBarThemeToWindow is a proper helper that guards against destroyed windows before calling setTitleBarOverlay and setBackgroundColor. The hermes:titlebar-theme IPC handler now correctly targets the sender window (not just mainWindow) and propagates to all open BrowserWindow instances.

Well-scoped trio of fixes, each with good defensive guards.

Reviewed by Hermes Agent

austinpickett
austinpickett previously approved these changes Jun 11, 2026

@austinpickett austinpickett left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Clean, focused set of fixes — all three concerns (single-instance, model picker stubs, theme flash on cold boot) are well-reasoned and the implementations are solid.

Single instance lock — correct placement before any window creation; the second-instance handler properly falls back to createWindow() when the original window was already destroyed. Guard call of focusWindow(mainWindow) is safe.

Theme boot script — the inline <script> in <head> runs synchronously before any render frame. The three-key localStorage schema (MODE_KEY, PROFILE_MODES_KEY, LAST_PROFILE_KEY) mirrors what ThemeProvider writes, so the boot paint genuinely matches the stored preference. All JSON parsing is guarded; silent catch means a corrupt store never breaks cold start.

Background color / title-bar theme propagationgetWindowBackgroundColor() consulting rendererTitleBarTheme?.background before falling back to nativeTheme.shouldUseDarkColors is the right order of precedence. Changing _eventevent and dispatching the overlay update through BrowserWindow.fromWebContents(event.sender) correctly handles multi-window setups (previously only mainWindow received the overlay).

Model picker cache guard — returning prev (not undefined) when !prev?.providers?.length avoids persisting a provider/model-only stub. The explicit read-then-write (getQueryDatasetQueryData) instead of the functional-updater form is a small but necessary change since the guard needs the value.

Model visibility empty-set fallback — folding stored.size === 0 into the !stored branch (both yield defaultVisibleKeys) corrects the prior behaviour where a user who had explicitly cleared all visibility choices saw zero models. The new test covers it.

One warning to address before merge:

The package.json diff drops electron/windows-child-process.test.cjs from test:desktop:platforms. It looks unintentional — the line was replaced when inserting single-instance.test.cjs without preserving the trailing entry. Two other open PRs (#43720, #43642) re-add it on top of the original line, so this will surface as a merge conflict. Please restore the dropped test file in the command before this merges.

Overall: approvable once the package.json test-script regression is fixed — the functional changes are correct and well-tested.

Reviewed by Hermes Agent

@austinpickett

Copy link
Copy Markdown
Collaborator

Code Review Summary

PR #43457 -- fix(desktop): single instance, model picker, and theme boot
Author: @Nigmat-future | Priority: P3
Verdict: ✅ Approve

Warnings

Suggestions

  • The applyTitleBarThemeToWindow helper broadcasts the theme to ALL BrowserWindow.getAllWindows() instances. Consider whether extension/popup windows should also receive the title-bar overlay update, or if a type/tag guard is needed to restrict it to main Hermes windows only.

Looks Good

  • Single instance lock -- correct placement before window creation; second-instance handler gracefully handles a destroyed original window.
  • Boot-time theme script -- runs synchronously in <head> before any render frame; three-key localStorage schema mirrors ThemeProvider; all JSON parsing is guarded; silent outer catch means a corrupt store never blocks cold start.
  • Background color / title-bar propagation -- getWindowBackgroundColor() preference order (rendererTitleBarTheme?.background > nativeTheme.shouldUseDarkColors) is correct; fixing _eventevent and dispatching via BrowserWindow.fromWebContents(event.sender) properly handles multi-window setups.
  • Model picker cache guard -- returning prev (not undefined) when !prev?.providers?.length prevents a provider/model stub from making the menu think options are loaded while providers is empty; explicit read-then-write pattern is correct.
  • Model visibility empty-set fallback -- folding stored.size === 0 into the !stored branch fixes the prior behaviour where a user who had explicitly cleared all choices saw zero models. New test covers it.
  • Unit tests for single-instance, use-model-controls, and model-visibility are well-targeted and pass.

Reviewed by Hermes Agent

@Nigmat-future
Nigmat-future force-pushed the fix/desktop-single-instance-model-picker-theme branch from c9a4967 to 64bff1a Compare June 11, 2026 10:07
@Nigmat-future

Copy link
Copy Markdown
Author

Rebased onto latest main and resolved the package.json conflict.

Changes since last review:

  • Restored electron/windows-child-process.test.cjs and electron/update-remote.test.cjs in test:desktop:platforms, while keeping electron/single-instance.test.cjs.
  • Kept main's typecheck script (tsc -p . --noEmit).

Verification:

  • PR-related vitest: model-visibility, use-model-controls — 7/7 passed
  • single-instance.test.cjs passes in test:desktop:platforms

Please re-review when you have a moment — the previous approval was dismissed after the rebase. Thanks!

Nigmat-future and others added 2 commits June 14, 2026 03:05
Prevent a second Electron process from opening a duplicate window, fix empty
model picker when visibility cache is an empty set, and apply theme/titlebar
before first paint to avoid flash on launch.
Co-authored-by: Cursor <cursoragent@cursor.com>
@Nigmat-future
Nigmat-future force-pushed the fix/desktop-single-instance-model-picker-theme branch from 64bff1a to dae7131 Compare June 13, 2026 19:13
@Nigmat-future

Copy link
Copy Markdown
Author

Thanks again for the earlier review, @austinpickett 👍

I rebased onto the latest main to clear the conflicts that landed after your approval (main moved on main.cjs, index.html, and package.json). Force-pushing dismissed your previous approval, so I'd appreciate a re-look when you have a moment.

How the conflicts resolved:

  • main.cjs — kept the single-instance lock (main didn't touch that spot), and adopted main's new show: false + ready-to-show deferred-show for both createSessionWindow and createWindow — it tackles the same theme-flash problem from the main-process side. applyTitleBarThemeToWindow(win) is kept alongside the ready-to-show handler since they're independent concerns.
  • index.html — switched to main's boot background pre-paint (hermes-boot-background / hermes-boot-color-scheme), since main now implements theme-boot there directly. Dropped my earlier profile-aware boot script rather than carry two parallel mechanisms.
  • package.jsontest:desktop:platforms now lists main's latest files plus single-instance.test.cjs (17 total), which should also cover the test-script regression you flagged last time.

Behavior is unchanged — just reconciled with main's newer theme-boot work. Happy to adjust if any of the merge calls look off.

@teknium1 teknium1 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Thanks for the focused Desktop fixes. The model-options cache concern is still relevant, but the branch needs selective salvage rather than a direct port.

Problems

  • The empty-set visibility fallback in apps/desktop/src/store/model-visibility.ts conflicts with current main's intentional contract: an empty non-null stored set means “everything hidden” (apps/desktop/src/store/model-visibility.ts:130-137), covered at apps/desktop/src/store/model-visibility.test.ts:183-193 and documented by 461fcc096.
  • apps/desktop/electron/main.cjs no longer exists after the TypeScript migration (39d09453f). Current main.ts already implements a stronger single-instance/deep-link flow at apps/desktop/electron/main.ts:9039-9058 and themed pre-paint behavior at apps/desktop/electron/main.ts:7043-7046,7250-7254.
  • The new cache test reimplements the logic in a local helper rather than testing useModelControls, so it would not catch a regression in the production writer (apps/desktop/src/app/session/hooks/use-model-controls.ts:27-32).

Suggested changes

  • Port only the model-options cache guard to the current TypeScript hook and cover that production path. Keep the current null-versus-empty visibility semantics.

Automated hermes-sweeper review.

providers: readonly ModelOptionProvider[]
): Set<string> {
if (!stored) {
if (!stored || stored.size === 0) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Please do not conflate an explicit empty stored set with an uncustomized selection. Current main intentionally treats empty non-null storage as “everything hidden” (apps/desktop/src/store/model-visibility.ts:130-137), with regression coverage at apps/desktop/src/store/model-visibility.test.ts:183-193.

import type { ModelOptionsResponse } from '@/types/hermes'

function patchModelOptionsCache(
queryClient: QueryClient,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This helper duplicates rather than invokes the production cache writer, so the test can remain green if useModelControls regresses. Extract/import a production pure helper or render the hook and assert its actual cache update.


test('main process enforces a single app instance on Windows launches', () => {
const mainSource = fs.readFileSync(path.join(__dirname, 'main.cjs'), 'utf8')

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This is a source-pattern test, not a behavior contract. The current Desktop test surface is TypeScript, and the Electron entry point has moved to electron/main.ts; replace this with a behavioral test if the relevant lifecycle logic is salvaged.

@teknium1 teknium1 added sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades sweeper:risk-platform-windows Sweeper risk: may break or behave differently on native Windows sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform labels Jul 14, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

P3 Low — cosmetic, nice to have sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades sweeper:risk-platform-windows Sweeper risk: may break or behave differently on native Windows type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants