mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-31 19:16:29 +00:00
fix(desktop): preserve live model after settings save (#72903)
This commit is contained in:
parent
731aa0ccc9
commit
dbc18c6d62
3 changed files with 105 additions and 9 deletions
|
|
@ -51,9 +51,6 @@ import {
|
|||
sessionPinId,
|
||||
setAwaitingResponse,
|
||||
setBusy,
|
||||
setCurrentModel,
|
||||
setCurrentModelSource,
|
||||
setCurrentProvider,
|
||||
setMessages
|
||||
} from '@/store/session'
|
||||
import { focusedSessionNeedsRoute, focusOpenSession } from '@/store/session-states'
|
||||
|
|
@ -269,7 +266,7 @@ export function ContribWiring({ children }: { children: ReactNode }) {
|
|||
|
||||
const { refreshHermesConfig, sttEnabled, voiceMaxRecordingSeconds } = useHermesConfig({ activeSessionIdRef })
|
||||
|
||||
const { refreshCurrentModel, selectModel, updateModelOptionsCache } = useModelControls({
|
||||
const { applySavedMainModel, refreshCurrentModel, selectModel } = useModelControls({
|
||||
queryClient,
|
||||
requestGateway
|
||||
})
|
||||
|
|
@ -1000,10 +997,7 @@ export function ContribWiring({ children }: { children: ReactNode }) {
|
|||
void queryClient.invalidateQueries({ queryKey: ['model-options'] })
|
||||
}}
|
||||
onMainModelChanged={(provider, model) => {
|
||||
setCurrentProvider(provider)
|
||||
setCurrentModel(model)
|
||||
setCurrentModelSource('default')
|
||||
updateModelOptionsCache($activeSessionId.get(), provider, model, true)
|
||||
applySavedMainModel(provider, model)
|
||||
void refreshCurrentModel()
|
||||
void queryClient.invalidateQueries({ queryKey: ['model-options'] })
|
||||
}}
|
||||
|
|
|
|||
|
|
@ -140,6 +140,85 @@ describe('useModelControls', () => {
|
|||
expect($currentProvider.get()).toBe('deepseek')
|
||||
})
|
||||
|
||||
it('keeps a live session authoritative when Settings saves a new profile default', async () => {
|
||||
const queryClient = new QueryClient()
|
||||
$activeSessionId.set('runtime-1')
|
||||
setCurrentModel('tencent/hy3:free')
|
||||
setCurrentProvider('nous')
|
||||
setCurrentModelSource('manual')
|
||||
queryClient.setQueryData(modelOptionsQueryKey('default'), {
|
||||
model: 'tencent/hy3:free',
|
||||
provider: 'nous',
|
||||
providers: []
|
||||
})
|
||||
queryClient.setQueryData(modelOptionsQueryKey('default', 'runtime-1'), {
|
||||
model: 'tencent/hy3:free',
|
||||
provider: 'nous',
|
||||
providers: []
|
||||
})
|
||||
vi.mocked(getGlobalModelInfo).mockResolvedValue({
|
||||
model: 'poolside/laguna-xs-2.1:free',
|
||||
provider: 'nous'
|
||||
})
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
useModelControls({
|
||||
queryClient,
|
||||
requestGateway: vi.fn()
|
||||
})
|
||||
)
|
||||
|
||||
result.current.applySavedMainModel('nous', 'poolside/laguna-xs-2.1:free')
|
||||
await result.current.refreshCurrentModel()
|
||||
|
||||
// Settings changes the profile default, not the active session. The footer
|
||||
// and its session-scoped picker cache must keep showing the live runtime.
|
||||
expect($currentModel.get()).toBe('tencent/hy3:free')
|
||||
expect($currentProvider.get()).toBe('nous')
|
||||
expect(queryClient.getQueryData(modelOptionsQueryKey('default', 'runtime-1'))).toMatchObject({
|
||||
model: 'tencent/hy3:free',
|
||||
provider: 'nous'
|
||||
})
|
||||
|
||||
// The global cache reflects the save, and the next fresh draft may reseed
|
||||
// from that default instead of preserving the old session's model.
|
||||
expect(getCurrentModelSource()).toBe('default')
|
||||
expect(queryClient.getQueryData(modelOptionsQueryKey('default'))).toMatchObject({
|
||||
model: 'poolside/laguna-xs-2.1:free',
|
||||
provider: 'nous'
|
||||
})
|
||||
|
||||
$activeSessionId.set(null)
|
||||
await result.current.refreshCurrentModel()
|
||||
|
||||
expect($currentModel.get()).toBe('poolside/laguna-xs-2.1:free')
|
||||
expect($currentProvider.get()).toBe('nous')
|
||||
})
|
||||
|
||||
it('paints a saved profile default immediately when no session is active', () => {
|
||||
const queryClient = new QueryClient()
|
||||
setCurrentModel('tencent/hy3:free')
|
||||
setCurrentProvider('nous')
|
||||
setCurrentModelSource('manual')
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
useModelControls({
|
||||
queryClient,
|
||||
requestGateway: vi.fn()
|
||||
})
|
||||
)
|
||||
|
||||
result.current.applySavedMainModel('nous', 'poolside/laguna-xs-2.1:free')
|
||||
|
||||
expect($currentModel.get()).toBe('poolside/laguna-xs-2.1:free')
|
||||
expect($currentProvider.get()).toBe('nous')
|
||||
expect(getCurrentModelSource()).toBe('default')
|
||||
expect(queryClient.getQueryData(modelOptionsQueryKey('default'))).toMatchObject({
|
||||
model: 'poolside/laguna-xs-2.1:free',
|
||||
provider: 'nous'
|
||||
})
|
||||
})
|
||||
|
||||
it('routes active-session picker changes through config.set with an explicit session-scoped provider', async () => {
|
||||
$activeSessionId.set('session-1')
|
||||
const requestGateway = vi.fn(async () => ({ key: 'model', value: 'claude-sonnet-4.6' }) as never)
|
||||
|
|
|
|||
|
|
@ -55,6 +55,29 @@ export function useModelControls({ queryClient, requestGateway }: ModelControlsO
|
|||
[queryClient]
|
||||
)
|
||||
|
||||
// Settings → Model writes the profile default, which the backend applies to
|
||||
// new sessions only. Keep a live session's renderer state and session-scoped
|
||||
// model-options cache authoritative instead of briefly painting the saved
|
||||
// default as if the active agent had switched. Marking the composer as
|
||||
// default-derived still lets the next fresh draft reseed from profile config.
|
||||
const applySavedMainModel = useCallback(
|
||||
(provider: string, model: string) => {
|
||||
const liveSessionId = $activeSessionId.get()
|
||||
|
||||
setCurrentModelSource('default')
|
||||
|
||||
if (!liveSessionId) {
|
||||
setCurrentProvider(provider)
|
||||
setCurrentModel(model)
|
||||
}
|
||||
|
||||
// A null session id is the profile-global model-options key. Never patch
|
||||
// the live session key here: only config.set --session may change it.
|
||||
updateModelOptionsCache(null, provider, model, false)
|
||||
},
|
||||
[updateModelOptionsCache]
|
||||
)
|
||||
|
||||
// Seed the composer's model state from the profile default. `force` reseeds
|
||||
// for a profile swap (the new profile has its own default); otherwise this
|
||||
// only fills an EMPTY selection so a user's pick (plain UI state in
|
||||
|
|
@ -220,5 +243,5 @@ export function useModelControls({ queryClient, requestGateway }: ModelControlsO
|
|||
[copy.modelSwitchFailed, queryClient, requestGateway, updateModelOptionsCache]
|
||||
)
|
||||
|
||||
return { refreshCurrentModel, selectModel, updateModelOptionsCache }
|
||||
return { applySavedMainModel, refreshCurrentModel, selectModel }
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue