From 154954b67e147123c609f1feb35c77ffb2eb8a0f Mon Sep 17 00:00:00 2001 From: Denis <276550298+deniqlab@users.noreply.github.com> Date: Sun, 19 Jul 2026 03:07:45 +0300 Subject: [PATCH] fix(desktop): reseed new chats from profile defaults --- .../hooks/use-background-sync.test.tsx | 56 +++++++++++++++++++ .../app/contrib/hooks/use-background-sync.ts | 6 +- .../session/hooks/use-model-controls.test.tsx | 7 ++- .../hooks/use-session-actions/index.ts | 9 +-- 4 files changed, 68 insertions(+), 10 deletions(-) create mode 100644 apps/desktop/src/app/contrib/hooks/use-background-sync.test.tsx diff --git a/apps/desktop/src/app/contrib/hooks/use-background-sync.test.tsx b/apps/desktop/src/app/contrib/hooks/use-background-sync.test.tsx new file mode 100644 index 0000000000000..ca51393b78371 --- /dev/null +++ b/apps/desktop/src/app/contrib/hooks/use-background-sync.test.tsx @@ -0,0 +1,56 @@ +import { renderHook } from '@testing-library/react' +import { beforeEach, describe, expect, it, vi } from 'vitest' + +import { $activeSessionId } from '@/store/session' + +import { useBackgroundSync } from './use-background-sync' + +vi.mock('@/store/profile', () => ({ refreshActiveProfile: vi.fn() })) + +function setup(overrides: Partial[0]> = {}) { + const params: Parameters[0] = { + activeIsMessaging: false, + activeSessionId: null, + freshDraftReady: false, + gatewayState: 'open', + refreshActiveMessagingTranscript: vi.fn(), + refreshCronJobs: vi.fn(), + refreshCurrentModel: vi.fn(), + refreshHermesConfig: vi.fn(), + refreshMessagingSessions: vi.fn(), + refreshSessions: vi.fn(), + requestGateway: vi.fn(), + ...overrides + } + + renderHook(() => useBackgroundSync(params)) + + return params +} + +describe('useBackgroundSync model reseeding', () => { + beforeEach(() => { + $activeSessionId.set(null) + }) + + it('force-reseeds the composer when booting onto a fresh draft', () => { + const { refreshCurrentModel } = setup() + + expect(refreshCurrentModel).toHaveBeenCalledWith(true) + }) + + it('does not force-reseed when booting with a live session', () => { + $activeSessionId.set('runtime-1') + const { refreshCurrentModel } = setup({ activeSessionId: 'runtime-1' }) + + expect(refreshCurrentModel).toHaveBeenCalledTimes(1) + expect(refreshCurrentModel).toHaveBeenCalledWith(false) + }) + + it('force-reseeds when a new-session draft becomes ready', () => { + const { refreshCurrentModel, refreshHermesConfig } = setup({ freshDraftReady: true }) + + expect(refreshCurrentModel).toHaveBeenCalledWith(true) + expect(refreshHermesConfig).toHaveBeenCalledOnce() + }) +}) diff --git a/apps/desktop/src/app/contrib/hooks/use-background-sync.ts b/apps/desktop/src/app/contrib/hooks/use-background-sync.ts index 58e669bcabbbb..8660c38bff323 100644 --- a/apps/desktop/src/app/contrib/hooks/use-background-sync.ts +++ b/apps/desktop/src/app/contrib/hooks/use-background-sync.ts @@ -68,7 +68,9 @@ export function useBackgroundSync({ return } - void refreshCurrentModel() + // Opening onto a fresh draft must discard the previous chat's sticky + // composer pick. A live session keeps its session-scoped selection. + void refreshCurrentModel(!$activeSessionId.get()) void refreshActiveProfile() void refreshSessions() @@ -130,7 +132,7 @@ export function useBackgroundSync({ // model + config so the composer pill reflects the profile default. useEffect(() => { if (gatewayState === 'open' && !activeSessionId && freshDraftReady) { - void refreshCurrentModel() + void refreshCurrentModel(true) void refreshHermesConfig() } }, [activeSessionId, freshDraftReady, gatewayState, refreshCurrentModel, refreshHermesConfig]) diff --git a/apps/desktop/src/app/session/hooks/use-model-controls.test.tsx b/apps/desktop/src/app/session/hooks/use-model-controls.test.tsx index 2c8c4e1f6fbd3..81e0a33f4c855 100644 --- a/apps/desktop/src/app/session/hooks/use-model-controls.test.tsx +++ b/apps/desktop/src/app/session/hooks/use-model-controls.test.tsx @@ -93,7 +93,7 @@ describe('useModelControls', () => { expect(getCurrentModelSource()).toBe('default') }) - it('does not clobber the active session footer state with global model info', async () => { + it('does not clobber the active session footer state even when force-reseeded', async () => { setCurrentModel('deepseek/deepseek-v4-pro') setCurrentProvider('deepseek') $activeSessionId.set('runtime-1') @@ -109,7 +109,7 @@ describe('useModelControls', () => { }) ) - await result.current.refreshCurrentModel() + await result.current.refreshCurrentModel(true) expect($currentModel.get()).toBe('deepseek/deepseek-v4-pro') expect($currentProvider.get()).toBe('deepseek') @@ -201,10 +201,13 @@ describe('useModelControls', () => { setCurrentProvider('anthropic') await result.current.refreshCurrentModel() expect($currentModel.get()).toBe('anthropic/claude-sonnet-4.6') + expect($currentProvider.get()).toBe('anthropic') // A profile swap forces a reseed to the new profile's default. await result.current.refreshCurrentModel(true) expect($currentModel.get()).toBe('openai/gpt-5.5') + expect($currentProvider.get()).toBe('openai-codex') + expect(getCurrentModelSource()).toBe('default') }) it('refreshes legacy/default-derived composer state from the profile default', async () => { diff --git a/apps/desktop/src/app/session/hooks/use-session-actions/index.ts b/apps/desktop/src/app/session/hooks/use-session-actions/index.ts index 84acfda63e16b..dab012261f7d6 100644 --- a/apps/desktop/src/app/session/hooks/use-session-actions/index.ts +++ b/apps/desktop/src/app/session/hooks/use-session-actions/index.ts @@ -258,12 +258,9 @@ export function useSessionActions({ }) setSessionStartedAt(null) setTurnStartedAt(null) - // The composer's model/effort/fast is sticky UI state (persisted in - // localStorage) — a new chat FOLLOWS your last pick instead of snapping - // back to the profile default, so we deliberately don't reset it here. The - // profile default still owns first-run seeding and profile switches (see - // refreshCurrentModel). Only $currentServiceTier (a live-session mirror) - // is cleared. + // Model/provider reseeding happens after this transition, once the fresh + // draft is ready (see useBackgroundSync). Effort/fast remain sticky UI + // state. Only $currentServiceTier (a live-session mirror) is cleared here. setCurrentServiceTier('') setYoloActive(false) setNewChatWorkspaceTarget(hasWorkspaceTarget ? workspaceTarget : undefined)