mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-06-16 09:31:37 +00:00
Replace the status-bar model chip's modal with a Cursor-style dropdown: - providers grouped by name in a stable order (no recency reshuffle on select) - per-model hover-Edit submenu for reasoning effort + fast, gated by per-model capabilities now surfaced in the model.options payload - unified Fast toggle: flips the speed=fast param where supported, else swaps to the model's `-fast` variant (base and variant collapse into one row) - localStorage-backed "Edit Models" dialog to choose which models appear Adds reusable dropdown primitives (DropdownMenuSearch, shared row/label tokens, portaled + collision-aware submenus) and reads session state from nanostores rather than prop-drilling, so editing options doesn't rebuild and close the menu.
103 lines
2.9 KiB
TypeScript
103 lines
2.9 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
|
|
}
|
|
|
|
/** 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
|
|
}
|
|
}
|
|
|
|
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(' ')}`
|
|
}
|