Watch
0
0
Fork
You've already forked hyperhive
0

agents: remove the hive-level /model control; the swarm editor sets the model

The per-agent web UI no longer sets the model: the `/model` slash
command, the model picker, `POST /api/model`, `Bus::set_model` and the
`hyperhive-model` writer are gone. The model chip is display-only and
shows where the model comes from (`model_source` in `/api/state`). The
model is set on the swarm (swarm UI agents page, `swarmctl agent
inference set`).

The read side is unchanged: with no swarm value an agent runs its
agent.nix model, else an existing `hyperhive-model` file, else haiku.
Existing files are not touched.

With the picker gone, its model list goes too: `available_models` in
`/api/state`, the ACP model-list filter, and the harness's global
`HIVE_AVAILABLE_MODELS`. The subagent daemon keeps its own
`HIVE_AVAILABLE_MODELS` from mcp.nix, and the `availableModels`
assertion stays.

`/effort` (command, picker, route, file) is unchanged.

Refs #5034
This commit is contained in:
atlas 2026-10-10 23:06:15 +02:00
commit 594693f4c7
26 changed files with 244 additions and 425 deletions

View file

@ -26,7 +26,7 @@ import { useTodos } from "./hooks/useTodos.js";
import { fmtAge, fmtTokens } from "./lib/format.js";
import { resolveDashboardBase } from "./lib/dashboardBase.js";
import { submitPauseResume } from "./lib/pauseAction.js";
import { postModel, postEffort } from "./lib/modelEffort.js";
import { postEffort } from "./lib/modelEffort.js";
import {
postCancelTurn,
postLogout,
@ -220,8 +220,6 @@ export function Root() {
statusLabel="… connecting"
statusTone="neutral"
model=""
availableModels={[]}
onSelectModel={() => {}}
effort=""
availableEfforts={[]}
onSelectEffort={() => {}}
@ -308,10 +306,7 @@ export function Root() {
statusTooltip={primaryStatus.tooltip}
model={state.model}
resolvedModel={state.resolved_model ?? undefined}
availableModels={state.available_models}
onSelectModel={(name) => {
postModel(name).then(() => refresh());
}}
modelSource={state.model_source}
effort={state.effort}
availableEfforts={state.available_efforts}
onSelectEffort={(level) => {

View file

@ -13,8 +13,8 @@
/* mara, live, on a screenshot of this row: "badges and interactive
elements on same row have inconsistent size." Real cause: Badge.css's
`.ui-badge-interactive` grows to a 2.75em WCAG touch-target floor
(model/effort pickers, pause) while a plain display `<span>` badge
(alive, state, ctx, cost — no `onClick`) stays compact, so the row
(effort picker, pause) while a plain display `<span>` badge
(model, alive, state, ctx, cost — no `onClick`) stays compact, so the row
reads as two different pill sizes even though every badge here is
the same `<Badge>` component. Scoped to this row rather than
Badge.css itself — display-only badges elsewhere (e.g. the dashboard)

View file

@ -1,6 +1,7 @@
// <StatusChips> — the agent page's status row: the consolidated status
// badge (alive/thinking/paused/etc., with pause/resume + cancel-turn in
// its dropdown), the model + effort pickers, and context/cost usage.
// its dropdown), the model (display-only: the swarm sets it), the effort
// picker, and context/cost usage.
// This is the concrete fix for the design guide's own named anti-example
// (the old page's model/effort *pickers* lived in the `⋯` overflow menu
// while the current model/effort only showed as a disconnected chip) —
@ -13,6 +14,7 @@
import { useRef, useState } from "preact/hooks";
import { Badge, type BadgeTone } from "@hive/shared/badge.js";
import { Dropdown, type DropdownOption } from "@hive/shared/dropdown.js";
import type { ModelSource } from "../types.js";
import "./StatusChips.css";
export interface StatusChipsProps {
@ -25,8 +27,8 @@ export interface StatusChipsProps {
* operator can tell what an alias resolved to. `undefined` before any
* turn has completed. */
resolvedModel?: string;
availableModels: string[];
onSelectModel: (name: string) => void;
/** Where `model` comes from (`/api/state` `model_source`). */
modelSource?: ModelSource;
effort: string;
availableEfforts: string[];
onSelectEffort: (level: string) => void;
@ -50,10 +52,11 @@ export interface StatusChipsProps {
onLogout: () => Promise<void>;
}
const MODEL_DESCRIPTIONS: Record<string, string> = {
haiku: "fast",
sonnet: "balanced",
opus: "powerful",
const SOURCE_LABEL: Record<ModelSource, string> = {
swarm: "set on the swarm",
agent_nix: "from agent.nix",
local_file: "from its own earlier pick",
default: "the built-in default",
};
const EFFORT_DESCRIPTIONS: Record<string, string> = {
low: "",
@ -235,8 +238,7 @@ export function StatusChips({
statusTooltip,
model,
resolvedModel,
availableModels,
onSelectModel,
modelSource,
effort,
availableEfforts,
onSelectEffort,
@ -263,13 +265,17 @@ export function StatusChips({
onNewSession={onNewSession}
onLogout={onLogout}
/>
<Picker
<Badge
label="model"
value={model}
options={availableModels}
descriptions={MODEL_DESCRIPTIONS}
onSelect={onSelectModel}
title={resolvedModel ? `resolved to ${resolvedModel}` : undefined}
title={
[
modelSource ? SOURCE_LABEL[modelSource] : null,
resolvedModel ? `resolved to ${resolvedModel}` : null,
]
.filter(Boolean)
.join(" · ") || undefined
}
/>
{availableEfforts.length ? (
<Picker

View file

@ -11,7 +11,7 @@
// (a local note explains what confirming does), typing it again within
// the same "armed" state fires it — a keyboard-native two-step confirm.
import { useRef, useState } from "preact/hooks";
import { postModel, postEffort } from "../lib/modelEffort.js";
import { postEffort } from "../lib/modelEffort.js";
import {
postCancelTurn,
postCompact,
@ -32,10 +32,6 @@ const SLASH_COMMANDS: { name: string; desc: string }[] = [
{ name: "/clear", desc: "wipe the terminal panel (local-only)" },
{ name: "/cancel", desc: "SIGINT the in-flight claude turn" },
{ name: "/compact", desc: "compact the persistent claude session" },
{
name: "/model",
desc: "/model <name> — switch claude model for future turns",
},
{
name: "/effort",
desc: "/effort <level> — set claude effort (medium/high/xhigh) for future turns",
@ -131,16 +127,6 @@ export function TermInput({
);
}
return true;
case "/model":
if (!arg)
onLocalNote(
"✗ /model needs a name (e.g. /model haiku, /model sonnet, /model opus)",
);
else
postModel(arg).then(
(r) => !r.ok && reportFailure("/model", r.detail),
);
return true;
case "/effort":
if (!arg)
onLocalNote(

View file

@ -1,4 +1,4 @@
// POST `/api/model` / `/api/effort` — same-origin (this agent's own
// POST `/api/effort` — same-origin (this agent's own
// backend), so a plain `fetch` is safe unlike pause/resume (see
// pauseAction.ts). `redirect: 'manual'` + the broad "ok" check below is
// ported from app.js unchanged — the endpoints don't actually redirect
@ -34,6 +34,5 @@ async function post(
}
}
export const postModel = (name: string) => post("api/model", "model", name);
export const postEffort = (level: string) =>
post("api/effort", "effort", level);

View file

@ -6,7 +6,7 @@
// hive-c0re doesn't need to grow CORS headers for it — the tradeoff is
// an opaque, unreadable response, which is fine here since the caller
// doesn't need to read it, only trigger a state refresh afterward (same
// shape as `postModel`/`postEffort` in modelEffort.ts). `credentials:
// shape as `postEffort` in modelEffort.ts). `credentials:
// 'include'` matches the cookie-sending behavior of the plain <form>
// submit this replaces.
//

View file

@ -4,6 +4,9 @@
// today, and the frontend only ever reads a handful of these fields.
// Extend as more of the page gets rebuilt against real data.
// Mirrors `swarm_queue_client::agent_inference::Source`.
export type ModelSource = "swarm" | "agent_nix" | "local_file" | "default";
export interface AgentState {
seq: number;
label: string;
@ -24,7 +27,7 @@ export interface AgentState {
cost_usage: TokenUsage | null;
hive_name: string | null;
swarm_name: string | null;
available_models: string[];
model_source: ModelSource;
effort: string;
available_efforts: string[];
paused: boolean;