fix(desktop): render reasoning effort as a badge chip, not part of the model name

The model catalog menu appended the reasoning effort (and fast mode, and
variant tag) to the model name as plain tertiary text with only a
leading space, so a row read 'Qwen3.7 Max High' — the same model with a
High reasoning setting looked like a differently-named model.

Render each row-meta value as a discrete bordered chip beside the (now
independently truncating) name span, so the setting can't be mistaken
for part of the name.

Fixes https://github.com/NousResearch/hermes-agent/issues/51833

Co-Authored-By: PRATHAMESH75 <prathamesh290504@gmail.com>
This commit is contained in:
Hermes Agent
2026-09-25 12:26:40 -05:00
committed by brooklyn!
parent 5dcfd72a96
commit 29aa6dcca0
2 changed files with 61 additions and 7 deletions

View File

@@ -99,6 +99,49 @@ describe('the current row effort', () => {
})
})
describe('the reasoning-effort badge (#51833)', () => {
it('renders the effort as its own bordered chip beside the name, never inside it', async () => {
renderMenu({ effort: 'high', model: 'gemini-2.5-flash', provider: 'google' })
// The effort chip renders exactly "High" in its own element…
const badge = await screen.findByText('High')
expect(badge.textContent).toBe('High')
expect(badge.className).toContain('border')
expect(badge.className).toContain('rounded-sm')
// …as a SIBLING of the truncating model-name span, so it can never read as
// part of a differently-named model.
const nameSpan = badge.previousElementSibling
expect(nameSpan?.className).toContain('truncate')
expect(nameSpan?.contains(badge)).toBe(false)
expect(nameSpan?.textContent?.toLowerCase()).toContain('gemini 2.5 flash')
})
it('drops the effort badge entirely when the model has no reasoning support', async () => {
getGlobalModelOptions.mockResolvedValue({
providers: [
{
name: 'Google',
slug: 'google',
models: ['gemini-2.5-flash'],
capabilities: { 'gemini-2.5-flash': { fast: false, reasoning: false } }
}
]
})
renderMenu({ effort: 'high', model: 'gemini-2.5-flash', provider: 'google' })
await screen.findByText(/Gemini 2\.5 Flash/i)
await waitFor(() => {
expect(screen.queryByText('High')).toBeNull()
expect(screen.queryByText('Med')).toBeNull()
})
})
})
// A minimal controller — these tests are about the CATALOG's own behaviour
// (what it lists, what it offers), not about what any host does with a pick.
function renderMenu(current: Partial<ModelMenuController['current']> = {}) {

View File

@@ -575,15 +575,17 @@ export function ModelCatalogMenu({
effFast
)
const meta = [
// Row meta (variant tag, fast mode, reasoning effort) renders as
// discrete badge chips BESIDE the name — not appended to it — so
// "High" reads as the model's reasoning setting, never as part of a
// differently-named model.
const metaTags = [
tag || null,
fastControl.kind !== 'none' && fastControl.on ? copy.fast : null,
(caps?.reasoning ?? true) && !(isCurrent && current.effortPending)
? reasoningEffortLabel(effEffort || defaultEffort, isCurrent ? current.effortWire : undefined)
: null
]
.filter(Boolean)
.join(' ')
].filter((chip): chip is string => Boolean(chip))
// Clicking the row commits the model and closes; the edit
// submenu (reasoning/fast) is reached by HOVER, so you can
@@ -608,9 +610,18 @@ export function ModelCatalogMenu({
}}
{...kbRowProps(`${group.provider.slug}:${family.id}`)}
>
<span className="min-w-0 flex-1 truncate">
<HighlightMatches foldSeparators query={search} text={name} />
{meta ? <span className="text-(--ui-text-tertiary)"> {meta}</span> : null}
<span className="flex min-w-0 flex-1 items-center gap-1.5">
<span className="min-w-0 truncate">
<HighlightMatches foldSeparators query={search} text={name} />
</span>
{metaTags.map(chip => (
<span
className="shrink-0 rounded-sm border border-(--ui-stroke-secondary) bg-(--chrome-action-hover) px-1 py-px text-[0.625rem] font-medium uppercase leading-none tracking-wide text-(--ui-text-tertiary)"
key={chip}
>
{chip}
</span>
))}
</span>
{loadProgress ? (
<span