mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-20 15:33:54 +00:00
fix(desktop): refresh default-derived composer model (#65896)
Co-authored-by: Koho Zheng <koho.jung@outlook.com>
This commit is contained in:
commit
2655c725cc
4 changed files with 69 additions and 4 deletions
|
|
@ -51,6 +51,7 @@ import {
|
|||
setCurrentBranch,
|
||||
setCurrentCwd,
|
||||
setCurrentModel,
|
||||
setCurrentModelSource,
|
||||
setCurrentProvider,
|
||||
setMessages
|
||||
} from '@/store/session'
|
||||
|
|
@ -898,6 +899,7 @@ export function ContribWiring({ children }: { children: ReactNode }) {
|
|||
onMainModelChanged={(provider, model) => {
|
||||
setCurrentProvider(provider)
|
||||
setCurrentModel(model)
|
||||
setCurrentModelSource('default')
|
||||
updateModelOptionsCache(provider, model, true)
|
||||
void refreshCurrentModel()
|
||||
void queryClient.invalidateQueries({ queryKey: ['model-options'] })
|
||||
|
|
|
|||
|
|
@ -3,7 +3,15 @@ import { cleanup, render, renderHook } from '@testing-library/react'
|
|||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { getGlobalModelInfo } from '@/hermes'
|
||||
import { $activeSessionId, $currentModel, $currentProvider, setCurrentModel, setCurrentProvider } from '@/store/session'
|
||||
import {
|
||||
$activeSessionId,
|
||||
$currentModel,
|
||||
$currentProvider,
|
||||
getCurrentModelSource,
|
||||
setCurrentModel,
|
||||
setCurrentModelSource,
|
||||
setCurrentProvider
|
||||
} from '@/store/session'
|
||||
|
||||
import { useModelControls } from './use-model-controls'
|
||||
|
||||
|
|
@ -52,6 +60,7 @@ describe('useModelControls', () => {
|
|||
beforeEach(() => {
|
||||
$activeSessionId.set(null)
|
||||
setCurrentModel('')
|
||||
setCurrentModelSource('')
|
||||
setCurrentProvider('')
|
||||
})
|
||||
|
||||
|
|
@ -60,6 +69,7 @@ describe('useModelControls', () => {
|
|||
vi.restoreAllMocks()
|
||||
$activeSessionId.set(null)
|
||||
setCurrentModel('')
|
||||
setCurrentModelSource('')
|
||||
setCurrentProvider('')
|
||||
})
|
||||
|
||||
|
|
@ -80,6 +90,7 @@ describe('useModelControls', () => {
|
|||
|
||||
expect($currentModel.get()).toBe('openai/gpt-5.5')
|
||||
expect($currentProvider.get()).toBe('openai-codex')
|
||||
expect(getCurrentModelSource()).toBe('default')
|
||||
})
|
||||
|
||||
it('does not clobber the active session footer state with global model info', async () => {
|
||||
|
|
@ -164,6 +175,7 @@ describe('useModelControls', () => {
|
|||
// the gateway or the profile default here.
|
||||
expect($currentModel.get()).toBe('claude-sonnet-4.6')
|
||||
expect($currentProvider.get()).toBe('anthropic')
|
||||
expect(getCurrentModelSource()).toBe('manual')
|
||||
expect(requestGateway).not.toHaveBeenCalled()
|
||||
expect(setGlobalModel).not.toHaveBeenCalled()
|
||||
})
|
||||
|
|
@ -185,6 +197,7 @@ describe('useModelControls', () => {
|
|||
// A user pick must survive the lifecycle refreshes that fire on boot / fresh
|
||||
// draft / session events.
|
||||
setCurrentModel('anthropic/claude-sonnet-4.6')
|
||||
setCurrentModelSource('manual')
|
||||
setCurrentProvider('anthropic')
|
||||
await result.current.refreshCurrentModel()
|
||||
expect($currentModel.get()).toBe('anthropic/claude-sonnet-4.6')
|
||||
|
|
@ -193,4 +206,27 @@ describe('useModelControls', () => {
|
|||
await result.current.refreshCurrentModel(true)
|
||||
expect($currentModel.get()).toBe('openai/gpt-5.5')
|
||||
})
|
||||
|
||||
it('refreshes legacy/default-derived composer state from the profile default', async () => {
|
||||
setCurrentModel('openai/gpt-5.5')
|
||||
setCurrentProvider('nous')
|
||||
setCurrentModelSource('')
|
||||
vi.mocked(getGlobalModelInfo).mockResolvedValue({ model: 'gpt-5.5', provider: 'openai-codex' })
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
useModelControls({
|
||||
queryClient: new QueryClient(),
|
||||
requestGateway: vi.fn()
|
||||
})
|
||||
)
|
||||
|
||||
expect(getCurrentModelSource()).toBe('')
|
||||
|
||||
await result.current.refreshCurrentModel()
|
||||
|
||||
expect(getGlobalModelInfo).toHaveBeenCalled()
|
||||
expect($currentModel.get()).toBe('gpt-5.5')
|
||||
expect($currentProvider.get()).toBe('openai-codex')
|
||||
expect(getCurrentModelSource()).toBe('default')
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -4,7 +4,15 @@ import { useCallback } from 'react'
|
|||
import { getGlobalModelInfo } from '@/hermes'
|
||||
import { useI18n } from '@/i18n'
|
||||
import { notifyError } from '@/store/notifications'
|
||||
import { $activeSessionId, $currentModel, $currentProvider, setCurrentModel, setCurrentProvider } from '@/store/session'
|
||||
import {
|
||||
$activeSessionId,
|
||||
$currentModel,
|
||||
$currentProvider,
|
||||
getCurrentModelSource,
|
||||
setCurrentModel,
|
||||
setCurrentModelSource,
|
||||
setCurrentProvider
|
||||
} from '@/store/session'
|
||||
import type { ModelOptionsResponse } from '@/types/hermes'
|
||||
|
||||
interface ModelSelection {
|
||||
|
|
@ -50,13 +58,13 @@ export function useModelControls({ queryClient, requestGateway }: ModelControlsO
|
|||
return
|
||||
}
|
||||
|
||||
if (!force && $currentModel.get()) {
|
||||
if (!force && $currentModel.get() && getCurrentModelSource() === 'manual') {
|
||||
return
|
||||
}
|
||||
|
||||
const result = await getGlobalModelInfo()
|
||||
|
||||
if ($activeSessionId.get() || (!force && $currentModel.get())) {
|
||||
if ($activeSessionId.get() || (!force && $currentModel.get() && getCurrentModelSource() === 'manual')) {
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -67,6 +75,10 @@ export function useModelControls({ queryClient, requestGateway }: ModelControlsO
|
|||
if (typeof result.provider === 'string') {
|
||||
setCurrentProvider(result.provider)
|
||||
}
|
||||
|
||||
if (typeof result.model === 'string' || typeof result.provider === 'string') {
|
||||
setCurrentModelSource('default')
|
||||
}
|
||||
} catch {
|
||||
// The delayed session.info event still updates this once the agent is ready.
|
||||
}
|
||||
|
|
@ -85,11 +97,13 @@ export function useModelControls({ queryClient, requestGateway }: ModelControlsO
|
|||
// rather than leave the UI showing a model the backend never selected.
|
||||
const prevModel = $currentModel.get()
|
||||
const prevProvider = $currentProvider.get()
|
||||
const prevSource = getCurrentModelSource()
|
||||
|
||||
const liveSessionId = $activeSessionId.get()
|
||||
|
||||
setCurrentModel(selection.model)
|
||||
setCurrentProvider(selection.provider)
|
||||
setCurrentModelSource('manual')
|
||||
updateModelOptionsCache(selection.provider, selection.model, !liveSessionId)
|
||||
|
||||
// No live session yet: the pick is pure UI state. session.create reads
|
||||
|
|
@ -111,6 +125,7 @@ export function useModelControls({ queryClient, requestGateway }: ModelControlsO
|
|||
} catch (err) {
|
||||
setCurrentModel(prevModel)
|
||||
setCurrentProvider(prevProvider)
|
||||
setCurrentModelSource(prevSource)
|
||||
updateModelOptionsCache(prevProvider, prevModel, !liveSessionId)
|
||||
notifyError(err, copy.modelSwitchFailed)
|
||||
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import { persistBoolean, persistString, storedBoolean, storedString } from '@/li
|
|||
import type { SessionInfo, UsageStats } from '@/types/hermes'
|
||||
|
||||
type Updater<T> = T | ((current: T) => T)
|
||||
export type ComposerModelSource = '' | 'default' | 'manual'
|
||||
|
||||
const WORKSPACE_CWD_KEY = 'hermes.desktop.workspace-cwd'
|
||||
|
||||
|
|
@ -18,6 +19,7 @@ const WORKSPACE_CWD_KEY = 'hermes.desktop.workspace-cwd'
|
|||
// that profile's default, while within a profile new chats keep your last pick.
|
||||
const COMPOSER_MODEL_KEY = 'hermes.desktop.composer.model'
|
||||
const COMPOSER_PROVIDER_KEY = 'hermes.desktop.composer.provider'
|
||||
const COMPOSER_MODEL_SOURCE_KEY = 'hermes.desktop.composer.model-source'
|
||||
const COMPOSER_EFFORT_KEY = 'hermes.desktop.composer.reasoning-effort'
|
||||
const COMPOSER_FAST_KEY = 'hermes.desktop.composer.fast'
|
||||
|
||||
|
|
@ -347,6 +349,16 @@ export const setCurrentProvider = (next: Updater<string>) => {
|
|||
persistString(COMPOSER_PROVIDER_KEY, $currentProvider.get() || null)
|
||||
}
|
||||
|
||||
export const getCurrentModelSource = (): ComposerModelSource => {
|
||||
const source = storedString(COMPOSER_MODEL_SOURCE_KEY)
|
||||
|
||||
return source === 'default' || source === 'manual' ? source : ''
|
||||
}
|
||||
|
||||
export const setCurrentModelSource = (source: ComposerModelSource) => {
|
||||
persistString(COMPOSER_MODEL_SOURCE_KEY, source || null)
|
||||
}
|
||||
|
||||
export const setCurrentReasoningEffort = (next: Updater<string>) => {
|
||||
updateAtom($currentReasoningEffort, next)
|
||||
persistString(COMPOSER_EFFORT_KEY, $currentReasoningEffort.get() || null)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue