diff --git a/docs/web-ui/agent.md b/docs/web-ui/agent.md index bc2413fa..c8271f67 100644 --- a/docs/web-ui/agent.md +++ b/docs/web-ui/agent.md @@ -73,20 +73,14 @@ through. Three flex columns: preserved). All destructive actions require one extra click to acknowledge — rare ops shouldn't live in the primary state strip. Below a separator, a **model quick-picker** section labelled - `model` renders one button per model in the operator-configured - list. The list is driven by `state.available_models` (sourced from - the `HIVE_AVAILABLE_MODELS` env var, injected by the - `services.hyperhive.availableModels` NixOS option; defaults to - `["haiku", "sonnet", "opus"]` when unset). Well-known aliases get - a parenthetical description (`haiku (fast)`, `sonnet (balanced)`, - `opus (powerful)`); operator-declared custom names show as-is. - Clicking a button POSTs `/api/model` with the alias (same path as - the `/model ` slash command). The button for the - currently-active model is highlighted via the `active` class; - `renderModelChip` keeps the picker state in sync with live - `model_changed` events so it stays accurate when the model is - changed from another session. Clicking the already-active model - closes the menu without an extra POST. + `model` renders three one-click shortcuts: `haiku (fast)`, + `sonnet (balanced)`, `opus (powerful)`. Clicking a button POSTs + `/api/model` with the alias (same path as the `/model ` + slash command). The button for the currently-active model is + highlighted via the `active` class; `renderModelChip` keeps the + picker state in sync with live `model_changed` events so it stays + accurate when the model is changed from another session. Clicking + the already-active model closes the menu without an extra POST. The popover's display rules are scoped to `:not([hidden])` so the `[hidden]` HTML attribute's UA `display: none` isn't overridden by the author CSS's `display: flex` — the popover stays hidden until diff --git a/frontend/packages/agent/src/app.js b/frontend/packages/agent/src/app.js index 12044a1e..c47683df 100644 --- a/frontend/packages/agent/src/app.js +++ b/frontend/packages/agent/src/app.js @@ -273,37 +273,33 @@ window.marked = marked; menu.append(logoutBtn); // ─── model quick-picker ──────────────────────────────────────── - // One-click shortcuts for each model in `availableModels` (seeded - // from `state.available_models` / `HIVE_AVAILABLE_MODELS` nix option). + // Three one-click shortcuts for the most common model aliases. // The active model is highlighted via the `active` class; see // `renderModelChip` which updates `modelPickerBtns` live. const modelSep = el('div', { class: 'overflow-sep', 'aria-hidden': 'true' }); const modelLabel = el('div', { class: 'overflow-section-label' }, 'model'); menu.append(modelSep, modelLabel); - // Well-known aliases get a parenthetical description; unknown aliases - // (operator-declared custom models) show just the name. - const MODEL_DESCRIPTIONS = { - haiku: 'haiku (fast)', - sonnet: 'sonnet (balanced)', - opus: 'opus (powerful)', - }; + const MODEL_ALIASES = [ + { name: 'haiku', label: 'haiku (fast)' }, + { name: 'sonnet', label: 'sonnet (balanced)' }, + { name: 'opus', label: 'opus (powerful)' }, + ]; modelPickerBtns = []; - for (const name of availableModels) { - const label = MODEL_DESCRIPTIONS[name] ?? name; + for (const m of MODEL_ALIASES) { const btn = el('button', { type: 'button', class: 'overflow-item overflow-item-model', role: 'menuitem', - title: `/model ${name}`, - 'data-model': name, + title: `/model ${m.name}`, + 'data-model': m.name, }, el('span', { class: 'overflow-item-icon', 'aria-hidden': 'true' }, '⊞'), - label, + m.label, ); btn.addEventListener('click', () => { - if (currentModel === name) { closeOverflowMenu(); return; } + if (currentModel === m.name) { closeOverflowMenu(); return; } closeOverflowMenu(); - postModel(name); + postModel(m.name); }); modelPickerBtns.push(btn); menu.append(btn); @@ -475,10 +471,6 @@ window.marked = marked; // menu so renderModelChip can update their `active` state without // rebuilding the whole menu. let modelPickerBtns = []; - // Ordered list of model short-names available on this hive. Seeded from - // `state.available_models` (injected by the nix option); falls back to - // the built-in default until the first /api/state cold-load completes. - let availableModels = ['haiku', 'sonnet', 'opus']; const SLASH_COMMANDS = [ { name: '/help', desc: 'list slash commands' }, @@ -1121,12 +1113,6 @@ window.marked = marked; const resp = await fetch('api/state'); if (!resp.ok) throw new Error('http ' + resp.status); const s = await resp.json(); - // Seed available_models before populateOverflowMenu (called from - // setHeader on the first load) so the picker uses the operator-declared - // list rather than the JS fallback default. - if (Array.isArray(s.available_models) && s.available_models.length > 0) { - availableModels = s.available_models; - } if (!headerSet) { setHeader(s.label, s.qualified_label, s.dashboard_port, s.hive_name, s.swarm_name); headerSet = true; } currentLabel = s.label; // Render server-supplied navigation links — see diff --git a/hive-ag3nt/src/web_ui.rs b/hive-ag3nt/src/web_ui.rs index 6d3e62b0..5c1f2ab8 100644 --- a/hive-ag3nt/src/web_ui.rs +++ b/hive-ag3nt/src/web_ui.rs @@ -436,14 +436,6 @@ struct StateSnapshot { /// Human name of the swarm (e.g. `"constellat1on"`). Sourced from /// `HYPERHIVE_SWARM_NAME`; `None` when unset. swarm_name: Option, - /// Ordered list of model short-names the operator has declared as - /// available on this hive. Sourced from `HIVE_AVAILABLE_MODELS` - /// (comma-separated, set by `services.hyperhive.availableModels`). - /// Falls back to `["haiku", "sonnet", "opus"]` when the env var is - /// absent or empty. The frontend model quick-picker renders one button - /// per entry in this list, so operators can add new models or drop - /// ones they don't want without touching the frontend code. - available_models: Vec, } /// One navigation link in the agent page header row. The same JSON @@ -569,7 +561,6 @@ async fn api_state(State(state): State) -> axum::Json { .filter(|s| !s.is_empty()), hive_name: crate::identity::hive_name(), swarm_name: crate::identity::swarm_name(), - available_models: available_models(), }) } @@ -1085,25 +1076,3 @@ fn error_response(message: &str) -> Response { // be noise. (StatusCode::INTERNAL_SERVER_ERROR, message.to_owned()).into_response() } - -/// Read `HIVE_AVAILABLE_MODELS` (comma-separated short names injected by -/// `services.hyperhive.availableModels`) and return the parsed list. -/// Falls back to `["haiku", "sonnet", "opus"]` when the env var is absent -/// or resolves to an empty list after trimming. -fn available_models() -> Vec { - const DEFAULT: &[&str] = &["haiku", "sonnet", "opus"]; - let raw = match std::env::var("HIVE_AVAILABLE_MODELS") { - Ok(v) if !v.trim().is_empty() => v, - _ => return DEFAULT.iter().map(|s| s.to_string()).collect(), - }; - let models: Vec = raw - .split(',') - .map(|s| s.trim().to_string()) - .filter(|s| !s.is_empty()) - .collect(); - if models.is_empty() { - DEFAULT.iter().map(|s| s.to_string()).collect() - } else { - models - } -}