Watch
0
0
Fork
You've already forked hyperhive
0

Compare commits

..
Author SHA1 Message Date
atlas
baefb32bd5 Merge forge/main into atlas/5034-s4-remove-hive-model 2026-10-11 03:42:22 +02:00
atlas
a7e640514c Merge forge/main into atlas/5034-s4-remove-hive-model 2026-10-11 02:09:05 +02:00
atlas
4719395b31 docs, comments: drop references to the removed model/effort picker
subagent.md no longer says the web UI's model picker offers
availableModels, and the comments in Dropdown, ComponentsPage and
WantedMenu no longer point at the deleted per-agent Picker.
2026-10-11 01:54:23 +02:00
atlas
ed271e655f agents: remove the hive-level /effort control too
mara on #5034: "effort shares /models fate". The per-agent web UI no
longer sets effort: the `/effort` slash command, the effort picker,
`POST /api/effort`, `Bus::set_effort`, the `hyperhive-effort` writer,
`EFFORT_LEVELS`/`is_valid_effort` and the ACP effort-list payload are
gone, and `lib/modelEffort.ts` with them. The effort chip is
display-only and shows its source (`effort_source` in `/api/state`);
on ACP it stays hidden while the session offers no effort level.

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

Refs #5034
2026-10-11 01:28:44 +02:00
atlas
594693f4c7 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
2026-10-10 23:11:53 +02:00
33 changed files with 315 additions and 812 deletions

1
Cargo.lock generated
View file

@ -1725,6 +1725,7 @@ dependencies = [
"tempfile",
"tokio",
"tokio-stream",
"tower",
"tower-http 0.7.0",
"tracing",
]

View file

@ -216,12 +216,12 @@ the retention sweep never deletes open rows.
### `/harness/hyperhive-model` (per agent)
Single-line text file holding the claude model name currently
selected for this agent (default `haiku` when absent). Written by
`Bus::set_model` whenever the operator flips it via `/model
<name>` in the web terminal. Read once at harness boot in
`Bus::new`. Path overridable via `HYPERHIVE_MODEL_FILE`.
Survives destroy/recreate, gone on `--purge`.
Single-line text file holding a claude model name. The harness only
reads it, at boot in `Bus::new` and again when the swarm clears the
model it set. It holds the model the agent runs when neither the swarm nor
`HIVE_DEFAULT_MODEL` (agent.nix) names one. Nothing in the harness
writes it. Path overridable via `HYPERHIVE_MODEL_FILE`. Survives
destroy/recreate, gone on `--purge`.
### `/harness/paused` (per agent)
@ -318,7 +318,8 @@ Under `/var/lib/hyperhive/agents/<name>/`:
tables above.
- `hyperhive-events.sqlite` — turn-loop event log.
- `hyperhive-turn-stats.sqlite` — per-turn timing stats.
- `hyperhive-model` — single-line model name override file.
- `hyperhive-model` / `hyperhive-effort` — single-line model name and
effort level files, read-only fallbacks.
- `work/` — the agent's working space for git worktrees, build dirs
and other large working data. Bind-mounted RW to
`/agents/<name>/work` inside the container and owned by the agent

View file

@ -20,14 +20,16 @@ swarmctl agent inference clear iris
agent's `availableModels` as the models its subagent `start` and
`continue` calls may name, read on each call
([`subagent.md`](../tools/subagent.md#which-models-a-subagent-may-run-on)).
The web UI's model picker doesn't use it.
- `get` shows each field, naming the unset ones (or says the swarm sets
nothing for the agent), and then the model and effort the agent reports
running on, each with where it comes from.
- `clear` removes every field; the agent goes back to its own values.
The full flag reference is [`swarmctl-cli.md`](../tools/swarmctl-cli.md). The
swarm UI's agents page edits the same settings; see [UI](ui.md).
swarm UI's agents page edits the same settings; see [UI](ui.md). The
swarm is the only place to change an agent's model or effort at runtime:
its web UI shows both but has no picker and no `/model` or `/effort`
command.
## What an agent runs on
@ -47,11 +49,15 @@ Each running value names where it comes from:
| ------------ | -------------------------------------------------------------------------- |
| `swarm` | the value set here |
| `agent_nix` | `services.hyperhive.agent.model` or `effortLevel` in the agent's agent.nix |
| `local_file` | a `/model` or `/effort` pick in the agent's web UI |
| `local_file` | a `hyperhive-model` or `hyperhive-effort` file in the agent's harness dir |
| `default` | the harness's built-in default (`haiku`, `medium`) |
When the agent starts without a swarm value, its agent.nix model wins over
its `/model` pick, and its `/effort` pick wins over its agent.nix effort.
a `hyperhive-model` file, and a `hyperhive-effort` file wins over its
agent.nix effort. These files come from runtime picks the agent's web UI no
longer offers; the agent still reads them but never writes them, so a
`local_file` value stays until the swarm sets that field or someone deletes
the file.
The agent reports these whenever they change, and again once a minute.
swarm-controller keeps only what it has received since it started, so for up
@ -63,13 +69,11 @@ Each agent reads its settings every 10 seconds and switches on the next
read: its next turn runs on the new model or effort, and its web UI badge
shows it. A turn already running keeps what it started with.
- **Its own value is the one it runs without the swarm**, from a `/model`
or `/effort` pick in its web UI or from its agent.nix. Setting a swarm
value doesn't overwrite it.
- **Its own value is the one it runs without the swarm**: its agent.nix or a
`hyperhive-model` / `hyperhive-effort` file. Setting a swarm value doesn't
overwrite it.
- **Clearing a field** (or the whole key) puts the agent back on its own
value on the next read.
- **A `/model` or `/effort` pick while the swarm sets that field** changes
the agent's own value, but the swarm value comes back on the next read.
- **After a restart**, the agent runs its own values until its first read
of its settings succeeds.

View file

@ -337,8 +337,6 @@ happens until the daemon's first connection to the swarm queue succeeds.
The match ignores case. On an ACP agent the names are the agent's own, such
as `<provider>/<model>` on `opencode`.
The web UI's model picker still offers `availableModels`.
## Runtime
A subagent runs on its parent agent's runtime

View file

@ -146,7 +146,7 @@ Queues and returns, the same way `agent create` does — watch the swarm UI's jo
Show or change the model, effort and subagent models the swarm sets for an agent.
A field the swarm sets wins over the agent's own value (from its `/model` or `/effort` pick or its agent.nix) from its next turn on; the agent reads its settings every ten seconds. A field the swarm doesn't set, or clears, leaves the agent on its own value.
A field the swarm sets wins over the agent's own value (its agent.nix or a `hyperhive-model`/`hyperhive-effort` file) from its next turn on; the agent reads its settings every ten seconds. A field the swarm doesn't set, or clears, leaves the agent on its own value.
**Usage:** `swarmctl agent inference <COMMAND>`

View file

@ -44,19 +44,18 @@ Hive-enforced settings ship at `/etc/claude-code/managed-settings.json`
read-only, un-overridable), wired in `nix/agent-modules/claude-settings.nix`
from the `prompts/claude-settings.json` asset. `effortLevel` is
deliberately not in that file — the `--effort` flag
(`HIVE_DEFAULT_EFFORT` / the per-agent UI slider) controls effort
(`HIVE_DEFAULT_EFFORT` / the swarm's effort setting) controls effort
live, which managed scope would otherwise lock.
The harness reads `<name>` from `Bus::model()` on each turn.
`services.hyperhive.agent.model` in the agent's `agent.nix` sets the initial
default (NixOS option; propagates via `HIVE_DEFAULT_MODEL` env var; falls
back to `"haiku"` if unset). The operator can flip it at runtime
with `/model <name>` in the web terminal — the next turn picks it
up. The harness persists the choice to `/harness/hyperhive-model` so it
survives restart; override path: `HYPERHIVE_MODEL_FILE` env var
for tests. A model or effort the swarm sets for the agent replaces the
`Bus` value, and the harness doesn't persist it; clearing it goes back to
the persisted or nix value → [`swarm/inference.md`](../swarm/inference.md).
back to an existing `/harness/hyperhive-model` file, then `"haiku"`, if
unset). The swarm changes the model at runtime, not the web
terminal: a model or effort the swarm sets for the agent replaces the
`Bus` value from the next turn, and the harness doesn't persist it;
clearing it goes back to the local value →
[`swarm/inference.md`](../swarm/inference.md).
`harness_state::context_window_tokens(model)` looks up
context-window size per-model. Resolution order (first

View file

@ -47,18 +47,20 @@ at a given viewport width. Two columns:
`◌ needs login` / `◌ logging in` / `○ offline` / `… connecting`.
- **State badge**: `💤 idle` / `🧠 thinking` / `📦 compacting` /
`○ offline` / `… booting` + age suffix.
- **Model badge** (`model · <name> ▾`): a real picker — selecting it opens
a `Dropdown` of `state.available_models`, selecting one POSTs
`/api/model` immediately (same endpoint the `/model <name>` slash
command uses). A header badge: the control and its display sit
together.
- **Effort badge** (`effort · <level> ▾`): same shape, `/api/effort`,
shown when `state.available_efforts` is non-empty.
- On an ACP agent both pickers list what its session offers (its
`model` and `thought_level` config options), and are empty until the
first turn has attached a session. The effort picker is hidden when the
model offers no effort levels, and while a newly picked model waits for
the next turn, since the levels come with the model.
- **Model and effort badges** (`model · <name>`, `effort · <level>`):
display only. Each tooltip says where the value comes from
(`state.model_source` / `state.effort_source`: set on the swarm,
agent.nix, an existing `hyperhive-model` / `hyperhive-effort` file, or
the built-in default); the model's also says what the last turn
resolved it to. Both are set on the swarm: the swarm UI's agents page,
or `swarmctl agent inference set`
([`inference.md`](../swarm/inference.md)).
- On an ACP agent each badge shows the session's own value when it
doesn't offer the requested one. The effort badge is hidden
(`state.effort` is `null`) until the first turn has attached a
session, when the model offers no effort levels, and while a newly
set model waits for the next turn, since the levels come with the
model.
- **Ctx / cost badges**: `ctx · 142k` (last inference's prompt size,
tooltip shows % of context window) / `cost · 1.3M` (cumulative
tokens billed across every inference in the last turn).
@ -82,8 +84,8 @@ at a given viewport width. Two columns:
(`GET /api/state`'s `links` field) also feeds
`DashboardState.links` for the dashboard card's icon strip —
`agent_links()` in hive-agent is the single source of truth for
both. The dashboard link lives in this 🔗 popover; the model/effort
pickers are real badges above; and `/new-session` / `/logout` are
both. The dashboard link lives in this 🔗 popover, and
`/new-session` / `/logout` are
typed slash commands with their own type-twice confirm (see
below).
- `/cancel` (slash command, below) is the only cancel-turn path; the
@ -238,14 +240,6 @@ Slash commands today:
turn, or `turn::run_pending_compact` when idle); output streams into
the live panel. Works mid-turn, not only when idle. Either way the live
panel ends with a `/compact done` or `/compact failed: …` note.
- `/model <name>` — `POST /api/model` flipping `Bus::set_model`.
Takes effect on the next turn; persisted to
`/state/hyperhive-model` so the override survives harness
restart / rebuild.
- `/effort <level>` — `POST /api/effort` setting the claude effort
level (`low` / `medium` / `high` / `xhigh` / `max`). Takes effect on the next
turn. `StatusChips`' effort badge calls the same endpoint; both stay
in sync via `StateSnapshot.effort`.
- `/new-session` — `POST /api/new-session` (confirms first).
Arms a one-shot on the Bus; next turn runs without
`--continue`, dropping the resume session entirely.
@ -280,17 +274,6 @@ shaped).
turn — only the stdin payload differs). Flips state to
`Compacting` via `Bus::set_state`, which emits
`TurnStateChanged`.
- `POST /api/model` (`model=<name>`) — switch the model for
future turns. `Bus::set_model` emits `ModelChanged`.
- `POST /api/effort` (`effort=<level>`) — switch the claude
effort level for future sessions. Validated server-side against
`EFFORT_LEVELS` (`low`/`medium`/`high`/`xhigh`/`max`) — unknown values are
rejected rather than forwarded to `claude --effort`. Persists via
`Bus::set_effort`, which emits `EffortChanged`. Applies on the next
session start (no mid-session swap).
- On an ACP agent, `/api/model` and `/api/effort` accept only values the
session offers, and the runtime sets them with `session/set_config_option`
before the next prompt.
- `POST /api/new-session` — arm a one-shot for the next turn to
drop `--continue`. Emits a `LiveEvent::Note`.
- `POST /api/logout` — three-step teardown that re-uses the
@ -324,7 +307,7 @@ shaped).
the frontend (`useAgentState.ts`) on page load and, on a poll, while
`status === 'needs_login_in_progress'`. Includes `turn_state`,
`context_window_tokens`, `qualified_label`,
`available_models`, `effort`, `available_efforts`, `links`, and
`model`, `model_source`, `effort`, `effort_source`, `links`, and
other fields described inline throughout this document. All
subsequent state updates arrive via SSE.
- `GET /api/dashboard-state` — lean snapshot of agent-owned fields
@ -397,10 +380,10 @@ Bus events (new vocabulary on `/events/stream`):
`needs_login_idle` / `needs_login_in_progress`. Drives the
alive-badge. The harness sets `rate_limited` when it detects a
429 response and clears it when the retry sleep expires.
- `model_changed { model }` — drives the model chip.
- `effort_changed { effort }` — drives the effort picker chip.
Emitted from `Bus::set_effort` on `POST /api/effort`; applies on
the next session start.
- `model_changed { model }` — drives the model chip. The harness emits it
when it applies or clears a swarm value.
- `effort_changed { effort }` — drives the effort chip. The harness
emits it when it applies or clears a swarm value.
- `token_usage_changed { ctx: TokenUsage, cost: TokenUsage }`
— drives the ctx + cost badges. Emitted from
`Bus::record_turn_usage` at turn-end; `ctx` is the last

View file

@ -26,7 +26,6 @@ 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 {
postCancelTurn,
postLogout,
@ -220,11 +219,6 @@ export function Root() {
statusLabel="… connecting"
statusTone="neutral"
model=""
availableModels={[]}
onSelectModel={() => {}}
effort=""
availableEfforts={[]}
onSelectEffort={() => {}}
paused={false}
onTogglePause={() => {}}
thinking={false}
@ -308,15 +302,9 @@ 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) => {
postEffort(level).then(() => refresh());
}}
effortSource={state.effort_source}
ctxLabel={ctx !== null ? fmtTokens(ctx) : undefined}
costLabel={cost !== null ? fmtTokens(cost) : undefined}
paused={state.paused}

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
(pause) while a plain display `<span>` badge (model, effort,
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 and effort (display-only: the swarm sets
// them), 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 { InferenceSource } from "../types.js";
import "./StatusChips.css";
export interface StatusChipsProps {
@ -25,11 +27,11 @@ 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;
effort: string;
availableEfforts: string[];
onSelectEffort: (level: string) => void;
/** Where `model` comes from (`/api/state` `model_source`). */
modelSource?: InferenceSource;
/** Hidden when empty: an ACP session that offers no effort now. */
effort?: string | null;
effortSource?: InferenceSource;
ctxLabel?: string;
costLabel?: string;
paused: boolean;
@ -50,66 +52,12 @@ export interface StatusChipsProps {
onLogout: () => Promise<void>;
}
const MODEL_DESCRIPTIONS: Record<string, string> = {
haiku: "fast",
sonnet: "balanced",
opus: "powerful",
const SOURCE_LABEL: Record<InferenceSource, 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: "",
medium: "default",
high: "",
xhigh: "",
max: "",
};
function Picker({
label,
value,
options,
descriptions,
onSelect,
title,
}: {
label: string;
value: string;
options: string[];
descriptions: Record<string, string>;
onSelect: (v: string) => void;
title?: string;
}) {
const [open, setOpen] = useState(false);
const anchorRef = useRef<HTMLDivElement>(null);
const dropdownOptions: DropdownOption[] = options.map((name) => ({
value: name,
label: name,
description: descriptions[name] || undefined,
}));
return (
<div class="status-chip-anchor" ref={anchorRef}>
<Badge
label={label}
value={value}
onClick={() => setOpen((o) => !o)}
expanded={open}
title={title}
/>
<Dropdown
open={open}
options={dropdownOptions}
activeValue={value}
label={`select ${label}`}
onSelect={(v) => {
onSelect(v);
setOpen(false);
}}
onClose={() => setOpen(false)}
anchorRef={anchorRef}
/>
</div>
);
}
// The consolidated alive/thinking/paused badge (mara: "why do we
// have separate alive badge? ... it could be one badge that shows
// thinking / idle / paused / ... with a dropdown that opens when
@ -235,11 +183,9 @@ export function StatusChips({
statusTooltip,
model,
resolvedModel,
availableModels,
onSelectModel,
modelSource,
effort,
availableEfforts,
onSelectEffort,
effortSource,
ctxLabel,
costLabel,
paused,
@ -263,21 +209,23 @@ 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
{effort ? (
<Badge
label="effort"
value={effort}
options={availableEfforts}
descriptions={EFFORT_DESCRIPTIONS}
onSelect={onSelectEffort}
title={effortSource ? SOURCE_LABEL[effortSource] : undefined}
/>
) : null}
{ctxLabel ? (

View file

@ -11,7 +11,6 @@
// (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 {
postCancelTurn,
postCompact,
@ -32,14 +31,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",
},
{
name: "/new-session",
desc: "fresh claude session next turn (type twice to confirm)",
@ -84,8 +75,7 @@ export function TermInput({
function handleSlash(line: string): boolean {
const trimmed = line.trim();
const [cmd, ...rest] = trimmed.split(/\s+/);
const arg = rest.join(" ");
const [cmd] = trimmed.split(/\s+/);
const wasArmed = armedRef.current === cmd;
armedRef.current = null;
@ -131,26 +121,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(
"✗ /effort needs a level (e.g. /effort medium, /effort high, /effort xhigh)",
);
else
postEffort(arg).then(
(r) => !r.ok && reportFailure("/effort", r.detail),
);
return true;
default:
onLocalNote(`✗ unknown slash command: ${cmd} — try /help`);
return true;

View file

@ -14,7 +14,7 @@ const RETRY_MS = 5000;
export interface UseAgentStateResult {
state: AgentState | null;
error: string | null;
/** Force an immediate refresh (e.g. right after a model/effort POST). */
/** Force an immediate refresh (e.g. right after a pause/resume POST). */
refresh: () => void;
}

View file

@ -2,7 +2,7 @@
// (this agent's own backend, `hive-agent/src/web_ui/auth.rs`), so a
// plain `fetch` is safe (unlike `pauseAction.ts`'s dashboard-origin
// actions). Same `{ ok, detail }` shape and `redirect: 'manual'` /
// broad-2xx-4xx-treated-as-ok contract as `modelEffort.ts` — none of
// broad-2xx-4xx-treated-as-ok contract as `termActions.ts` — none of
// these three routes actually redirect today, but the same tolerant
// check costs nothing and keeps every same-origin POST in this package
// behaving identically.

View file

@ -1,39 +0,0 @@
// POST `/api/model` / `/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
// today, but treating an opaque redirect as success costs nothing and
// matches the existing contract exactly.
async function post(
path: string,
field: string,
value: string,
): Promise<{ ok: boolean; detail?: string }> {
try {
const resp = await fetch(path, {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: new URLSearchParams({ [field]: value }),
redirect: "manual",
});
const ok =
resp.ok ||
resp.type === "opaqueredirect" ||
(resp.status >= 200 && resp.status < 400);
if (ok) return { ok: true };
const detail = await resp.text().catch(() => "");
return {
ok: false,
detail: `http ${resp.status}${detail ? " — " + detail : ""}`,
};
} catch (err) {
return {
ok: false,
detail: err instanceof Error ? err.message : String(err),
};
}
}
export const postModel = (name: string) => post("api/model", "model", name);
export const postEffort = (level: string) =>
post("api/effort", "effort", level);

View file

@ -5,8 +5,7 @@
// origin, but accessed directly they're different ports) and this way
// 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:
// doesn't need to read it, only trigger a state refresh afterward. `credentials:
// 'include'` matches the cookie-sending behavior of the plain <form>
// submit this replaces.
//

View file

@ -1,6 +1,5 @@
// Same-origin fetch POST helpers for the term-input slash commands —
// same shape as modelEffort.ts's `post()` ({ ok, detail } rather than
// throwing), reused by TermInput.tsx.
// Same-origin fetch POST helpers for the term-input slash commands
// ({ ok, detail } rather than throwing), reused by TermInput.tsx.
async function post(
path: string,
body?: URLSearchParams,

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 InferenceSource = "swarm" | "agent_nix" | "local_file" | "default";
export interface AgentState {
seq: number;
label: string;
@ -24,9 +27,9 @@ export interface AgentState {
cost_usage: TokenUsage | null;
hive_name: string | null;
swarm_name: string | null;
available_models: string[];
effort: string;
available_efforts: string[];
model_source: InferenceSource;
effort: string | null;
effort_source: InferenceSource;
paused: boolean;
inbox: InboxRow[];
links: AgentLink[];

View file

@ -2,8 +2,7 @@
// the `Badge` that opened it (../badge/Badge.tsx) so the control is
// visually attached to the thing it affects, instead of living three
// clicks away in a catch-all menu (the junk-drawer pattern
// `docs/web-ui/design-guide.md` calls out by name against the old
// per-agent terminal's model picker). Not a native `<dialog>` — a
// `docs/web-ui/design-guide.md` calls out by name). Not a native `<dialog>` — a
// `showModal()` dialog's backdrop would visually detach it from its
// anchor badge, and a non-modal `show()` gets no click-outside-to-close
// for free, so this hand-rolls the same "outside click or Escape

View file

@ -167,8 +167,7 @@ function RefreshIntervalPickerSample() {
}
// The badge-triggers-a-dropdown-right-underneath-itself pattern this is
// meant to land — the model/effort picker shape from the per-agent
// terminal, demoed here with sample data. The wrapper
// meant to land, demoed here with sample data. The wrapper
// needs `position: relative` (`.components-badge-anchor`) so `Dropdown`
// anchors under this badge specifically, not the page.
function BadgePickerSample() {
@ -528,7 +527,7 @@ export function ComponentsPage() {
<Sample label="with a label prefix">
<Badge label="ctx" value="534k" />
</Sample>
<Sample label="interactive, opens a Dropdown (model/effort picker shape)">
<Sample label="interactive, opens a Dropdown">
<BadgePickerSample />
</Sample>
<Sample label="interactive, toggles itself (pause/resume shape)">

View file

@ -8,7 +8,7 @@
// explicit — a dropdown just lets "up" be picked directly regardless
// of what's inferred, no flip-to-the-opposite-of-a-guess involved
// (mara: "when no state is declared, i want to set it to online").
// Presentational only, same split as `StatusChips`'s `Picker`/
// Presentational only, same split as `StatusChips`'s
// `StatusMenu`: the caller owns what each selection actually does.
//
// "paused" has no separate resume option here — selecting "up" from a

Some files were not shown because too many files have changed in this diff Show more