mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-06-19 10:02:16 +00:00
* fix(desktop): keep the pre-session model pick selected in the picker The composer picker derived its "current" row from `model.options ?? store`, so model.options always won. Pre-session that query returns the PROFILE DEFAULT, not the sticky composer pick — so selecting a model before a session exists left the checkmark (and the picker's "current" line) on the default, making the pick look ignored even though the pill updated. Add `currentPickerSelection()`: with a live session the gateway's model.options is authoritative; pre-session the sticky `$currentModel`/`$currentProvider` wins, falling back to options. Wire it into ModelMenuPanel and ModelPickerDialog. * feat(desktop): global reasoning/speed defaults in Settings → Model The composer picker is now sticky-UI/per-session only and never writes the profile default (#46959), but Settings → Model had no reasoning/speed control and `agent.reasoning_effort` wasn't in the curated config surface at all (`service_tier` was buried in Advanced) — so there was nowhere to set the profile default that crons/subagents/messaging resolve from. Add capability-gated Reasoning (effort) + Fast controls beside the main model, gated by the applied model's reported capabilities (reasoning defaults on, fast off when unreported — same as the composer). They read/write `agent.reasoning_effort` and `agent.service_tier` by round-tripping the config record, matching the gateway's value semantics (service_tier "fast"/"priority"/"on" ⇒ fast). * refactor(desktop): don't open the reasoning select from its row label A <label> wrapping the Select forwarded text clicks to the trigger, opening the dropdown unexpectedly. Plain row for reasoning; Fast stays a <label> so clicking its text toggles the switch (expected for a checkbox-like control).
122 lines
3.7 KiB
TypeScript
122 lines
3.7 KiB
TypeScript
const REASONING_LABELS: Record<string, string> = {
|
|
none: 'Off',
|
|
minimal: 'Min',
|
|
low: 'Low',
|
|
medium: 'Med',
|
|
high: 'High',
|
|
xhigh: 'Max'
|
|
}
|
|
|
|
export function reasoningEffortLabel(effort: string): string {
|
|
const key = effort.trim().toLowerCase()
|
|
|
|
if (!key) {
|
|
return ''
|
|
}
|
|
|
|
return REASONING_LABELS[key] ?? effort
|
|
}
|
|
|
|
/** Which model/provider a picker should mark "current". With a live session the
|
|
* gateway's `model.options` is authoritative; pre-session there is no server
|
|
* "current", so the sticky composer pick wins over the profile default the
|
|
* global options query returns — else the checkmark snaps back to the default
|
|
* and the pick looks ignored. */
|
|
export function currentPickerSelection(
|
|
hasSession: boolean,
|
|
store: { model: string; provider: string },
|
|
options?: { model?: string; provider?: string }
|
|
): { model: string; provider: string } {
|
|
return {
|
|
model: String((hasSession && options?.model) || store.model || options?.model || ''),
|
|
provider: String((hasSession && options?.provider) || store.provider || options?.provider || '')
|
|
}
|
|
}
|
|
|
|
/** Strip provider prefix and normalize for display. */
|
|
export function modelBaseId(model: string): string {
|
|
const trimmed = model.trim()
|
|
const slash = trimmed.lastIndexOf('/')
|
|
|
|
return slash >= 0 ? trimmed.slice(slash + 1) : trimmed
|
|
}
|
|
|
|
// Trailing model-id variants that should render as a grayed tag beside the
|
|
// name (e.g. "Opus 4.8" + "Fast") rather than collapsing two distinct ids to
|
|
// the same display name.
|
|
const VARIANT_TAGS: ReadonlyArray<readonly [RegExp, string]> = [
|
|
[/-fast$/i, 'Fast'],
|
|
[/-thinking$/i, 'Thinking'],
|
|
[/-preview$/i, 'Preview'],
|
|
[/-latest$/i, 'Latest']
|
|
]
|
|
|
|
const titleCase = (text: string): string => text.replace(/\b\w/g, char => char.toUpperCase()).trim()
|
|
|
|
function prettifyBase(base: string): string {
|
|
if (/^claude-/i.test(base)) {
|
|
return titleCase(base.replace(/^claude-/i, '').replace(/-/g, ' '))
|
|
}
|
|
|
|
if (/^gpt-/i.test(base)) {
|
|
return base.replace(/^gpt-/i, 'GPT-')
|
|
}
|
|
|
|
if (/^gemini-/i.test(base)) {
|
|
return base.replace(/^gemini-/i, 'Gemini ').replace(/-/g, ' ')
|
|
}
|
|
|
|
return titleCase(base.replace(/-/g, ' '))
|
|
}
|
|
|
|
/** Split a model id into a clean display name plus an optional grayed variant
|
|
* tag, so distinct ids (e.g. `…-4.8` vs `…-4.8-fast`) don't collapse. */
|
|
export function modelDisplayParts(model: string): { name: string; tag: string } {
|
|
let base = modelBaseId(model)
|
|
let tag = ''
|
|
|
|
for (const [pattern, label] of VARIANT_TAGS) {
|
|
if (pattern.test(base)) {
|
|
tag = label
|
|
base = base.replace(pattern, '')
|
|
|
|
break
|
|
}
|
|
}
|
|
|
|
// Drop a trailing date-pin (`…-20251101`) — snapshot noise, not a name.
|
|
base = base.replace(/-\d{8}$/, '')
|
|
|
|
return { name: prettifyBase(base) || model.trim() || 'No model', tag }
|
|
}
|
|
|
|
/** Friendly one-line model name for menus and the status bar. */
|
|
export function displayModelName(model: string): string {
|
|
return modelDisplayParts(model).name
|
|
}
|
|
|
|
/** Status bar trigger label — model name plus the live session state (effort/fast). */
|
|
export function formatModelStatusLabel(
|
|
model: string,
|
|
options?: { fastMode?: boolean; reasoningEffort?: string }
|
|
): string {
|
|
const name = displayModelName(model)
|
|
|
|
if (!model.trim()) {
|
|
return name
|
|
}
|
|
|
|
const parts: string[] = []
|
|
|
|
// Fast is shown when the speed=fast param is on (options.fastMode) OR the
|
|
// active model is a `…-fast` variant (fast via a separate model id).
|
|
if (options?.fastMode || /-fast$/i.test(modelBaseId(model))) {
|
|
parts.push('Fast')
|
|
}
|
|
|
|
// Always surface the effort (empty = Hermes default of medium) so the
|
|
// current reasoning level is visible at a glance, not just when non-default.
|
|
parts.push(reasoningEffortLabel(options?.reasoningEffort ?? '') || 'Med')
|
|
|
|
return `${name} · ${parts.join(' ')}`
|
|
}
|