mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-08 13:12:08 +00:00
The MoA preset section in the composer model dropdown presented presets like
persistent model selections, but selecting one dispatched the one-shot `/moa`
command (command.dispatch name=moa) — it ran a single turn through MoA and then
silently reverted to the prior model. The user saw MoA context for one message,
then it vanished with no indication.
Route MoA preset selection through the same persistent path real provider
selections use: onSelectModel({ model: preset, provider: 'moa' }) →
config.set model="<preset> --provider moa" → the gateway's switch_model. The
check mark now reflects the real current selection (currentProvider === 'moa'
&& currentModel === preset) instead of transient local state, and the
now-unused activeMoaPreset state is removed.
Tests: new model-menu-panel.test.tsx (2) — selecting a preset calls
onSelectModel with provider 'moa' (persistent), and the check renders on the
active preset. tsc -b clean.
This commit is contained in:
parent
5eaccf5802
commit
9be292f1e6
2 changed files with 120 additions and 20 deletions
92
apps/desktop/src/app/shell/model-menu-panel.test.tsx
Normal file
92
apps/desktop/src/app/shell/model-menu-panel.test.tsx
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
||||
import { cleanup, findByText, fireEvent, render } from '@testing-library/react'
|
||||
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { DropdownMenu, DropdownMenuContent } from '@/components/ui/dropdown-menu'
|
||||
import { $activeSessionId, $currentModel, $currentProvider } from '@/store/session'
|
||||
|
||||
import { ModelMenuPanel } from './model-menu-panel'
|
||||
|
||||
// Radix calls these on open; jsdom doesn't implement them.
|
||||
beforeAll(() => {
|
||||
Element.prototype.scrollIntoView = vi.fn()
|
||||
Element.prototype.hasPointerCapture = vi.fn(() => false)
|
||||
Element.prototype.releasePointerCapture = vi.fn()
|
||||
})
|
||||
|
||||
const getMoaModels = vi.fn()
|
||||
const getGlobalModelOptions = vi.fn()
|
||||
|
||||
vi.mock('@/hermes', () => ({
|
||||
getGlobalModelOptions: (...args: unknown[]) => getGlobalModelOptions(...args),
|
||||
getMoaModels: (...args: unknown[]) => getMoaModels(...args)
|
||||
}))
|
||||
|
||||
function moaPreset() {
|
||||
return {
|
||||
aggregator: { provider: 'deepseek', model: 'deepseek-v4-pro' },
|
||||
aggregator_temperature: 0.7,
|
||||
enabled: true,
|
||||
max_tokens: 4096,
|
||||
reference_models: [{ provider: 'zai', model: 'glm-5.2' }],
|
||||
reference_temperature: 0.7
|
||||
}
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
$activeSessionId.set('runtime-1')
|
||||
$currentModel.set('')
|
||||
$currentProvider.set('')
|
||||
getGlobalModelOptions.mockResolvedValue({ providers: [] })
|
||||
getMoaModels.mockResolvedValue({
|
||||
default_preset: 'default',
|
||||
active_preset: 'default',
|
||||
presets: { default: moaPreset(), BeastMode: moaPreset() }
|
||||
})
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
cleanup()
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
function renderPanel(onSelectModel = vi.fn()) {
|
||||
const client = new QueryClient({ defaultOptions: { queries: { retry: false } } })
|
||||
render(
|
||||
<QueryClientProvider client={client}>
|
||||
<DropdownMenu open>
|
||||
<DropdownMenuContent>
|
||||
<ModelMenuPanel onSelectModel={onSelectModel} requestGateway={vi.fn() as never} />
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</QueryClientProvider>
|
||||
)
|
||||
|
||||
return onSelectModel
|
||||
}
|
||||
|
||||
describe('ModelMenuPanel MoA presets', () => {
|
||||
it('selecting a MoA preset switches PERSISTENTLY via onSelectModel (not the one-shot dispatch)', async () => {
|
||||
const onSelectModel = renderPanel()
|
||||
|
||||
// moaOptions is async (useQuery) — wait for the preset row to mount.
|
||||
const row = await findByText(document.body, 'MoA: BeastMode')
|
||||
fireEvent.click(row)
|
||||
|
||||
// #54670: must route through the persistent model-switch path
|
||||
// (config.set model="<preset> --provider moa"), i.e. onSelectModel with
|
||||
// provider 'moa', NOT a one-shot command.dispatch that reverts after a turn.
|
||||
expect(onSelectModel).toHaveBeenCalledWith({ model: 'BeastMode', provider: 'moa' })
|
||||
})
|
||||
|
||||
it('shows the check on the preset that matches the current moa selection', async () => {
|
||||
$currentProvider.set('moa')
|
||||
$currentModel.set('BeastMode')
|
||||
renderPanel()
|
||||
|
||||
const row = await findByText(document.body, 'MoA: BeastMode')
|
||||
// The check codicon renders as a sibling within the same row item.
|
||||
const item = row.closest('[role="menuitem"]') ?? row.parentElement
|
||||
expect(item?.querySelector('.codicon-check')).not.toBeNull()
|
||||
})
|
||||
})
|
||||
|
|
@ -69,7 +69,6 @@ export function ModelMenuPanel({ gateway, onSelectModel, requestGateway }: Model
|
|||
const [search, setSearch] = useState('')
|
||||
const [refreshing, setRefreshing] = useState(false)
|
||||
const queryClient = useQueryClient()
|
||||
const [activeMoaPreset, setActiveMoaPreset] = useState('')
|
||||
// Reactive session state is read from the stores here (not drilled in), so
|
||||
// toggling effort/fast/model re-renders this panel in place without forcing
|
||||
// the parent to rebuild the menu content (which would close the dropdown).
|
||||
|
|
@ -180,13 +179,18 @@ export function ModelMenuPanel({ gateway, onSelectModel, requestGateway }: Model
|
|||
)
|
||||
}
|
||||
|
||||
const toggleMoaPreset = async (preset: string) => {
|
||||
// Selecting a MoA preset switches the session to it PERSISTENTLY, using the
|
||||
// same path real provider selections use (config.set model="<preset>
|
||||
// --provider moa" via onSelectModel → the gateway's persistent switch_model).
|
||||
// Previously this dispatched the one-shot `/moa` command, which ran a single
|
||||
// turn through MoA and then silently reverted to the prior model (#54670) —
|
||||
// the dropdown presented presets like persistent selections but they weren't.
|
||||
const selectMoaPreset = async (preset: string) => {
|
||||
if (!activeSessionId) {
|
||||
return
|
||||
}
|
||||
|
||||
await requestGateway('command.dispatch', { name: 'moa', arg: preset, session_id: activeSessionId })
|
||||
setActiveMoaPreset(current => (current === preset ? '' : preset))
|
||||
await switchTo(preset, 'moa')
|
||||
}
|
||||
|
||||
const groups = useMemo(
|
||||
|
|
@ -321,22 +325,26 @@ export function ModelMenuPanel({ gateway, onSelectModel, requestGateway }: Model
|
|||
{moaOptions.data && Object.keys(moaOptions.data.presets ?? {}).length > 0 ? (
|
||||
<>
|
||||
<DropdownMenuLabel className={dropdownMenuSectionLabel}>MoA presets</DropdownMenuLabel>
|
||||
{Object.keys(moaOptions.data.presets).map(preset => (
|
||||
<DropdownMenuItem
|
||||
className={dropdownMenuRow}
|
||||
disabled={!activeSessionId}
|
||||
key={`moa:${preset}`}
|
||||
onSelect={event => {
|
||||
event.preventDefault()
|
||||
void toggleMoaPreset(preset)
|
||||
}}
|
||||
>
|
||||
<span className="min-w-0 flex-1 truncate">MoA: {preset}</span>
|
||||
{activeMoaPreset === preset ? (
|
||||
<Codicon className="ml-auto text-foreground" name="check" size="0.75rem" />
|
||||
) : null}
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
{Object.keys(moaOptions.data.presets).map(preset => {
|
||||
const isCurrentMoa = currentProvider === 'moa' && currentModel === preset
|
||||
|
||||
return (
|
||||
<DropdownMenuItem
|
||||
className={dropdownMenuRow}
|
||||
disabled={!activeSessionId}
|
||||
key={`moa:${preset}`}
|
||||
onSelect={event => {
|
||||
event.preventDefault()
|
||||
void selectMoaPreset(preset)
|
||||
}}
|
||||
>
|
||||
<span className="min-w-0 flex-1 truncate">MoA: {preset}</span>
|
||||
{isCurrentMoa ? (
|
||||
<Codicon className="ml-auto text-foreground" name="check" size="0.75rem" />
|
||||
) : null}
|
||||
</DropdownMenuItem>
|
||||
)
|
||||
})}
|
||||
<DropdownMenuSeparator className="mx-0" />
|
||||
</>
|
||||
) : null}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue