Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
baefb32bd5 | ||
|
|
a7e640514c | ||
|
|
4719395b31 | ||
|
|
ed271e655f | ||
|
|
594693f4c7 |
33 changed files with 315 additions and 812 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
|
@ -1725,6 +1725,7 @@ dependencies = [
|
||||||
"tempfile",
|
"tempfile",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tokio-stream",
|
"tokio-stream",
|
||||||
|
"tower",
|
||||||
"tower-http 0.7.0",
|
"tower-http 0.7.0",
|
||||||
"tracing",
|
"tracing",
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -216,12 +216,12 @@ the retention sweep never deletes open rows.
|
||||||
|
|
||||||
### `/harness/hyperhive-model` (per agent)
|
### `/harness/hyperhive-model` (per agent)
|
||||||
|
|
||||||
Single-line text file holding the claude model name currently
|
Single-line text file holding a claude model name. The harness only
|
||||||
selected for this agent (default `haiku` when absent). Written by
|
reads it, at boot in `Bus::new` and again when the swarm clears the
|
||||||
`Bus::set_model` whenever the operator flips it via `/model
|
model it set. It holds the model the agent runs when neither the swarm nor
|
||||||
<name>` in the web terminal. Read once at harness boot in
|
`HIVE_DEFAULT_MODEL` (agent.nix) names one. Nothing in the harness
|
||||||
`Bus::new`. Path overridable via `HYPERHIVE_MODEL_FILE`.
|
writes it. Path overridable via `HYPERHIVE_MODEL_FILE`. Survives
|
||||||
Survives destroy/recreate, gone on `--purge`.
|
destroy/recreate, gone on `--purge`.
|
||||||
|
|
||||||
### `/harness/paused` (per agent)
|
### `/harness/paused` (per agent)
|
||||||
|
|
||||||
|
|
@ -318,7 +318,8 @@ Under `/var/lib/hyperhive/agents/<name>/`:
|
||||||
tables above.
|
tables above.
|
||||||
- `hyperhive-events.sqlite` — turn-loop event log.
|
- `hyperhive-events.sqlite` — turn-loop event log.
|
||||||
- `hyperhive-turn-stats.sqlite` — per-turn timing stats.
|
- `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
|
- `work/` — the agent's working space for git worktrees, build dirs
|
||||||
and other large working data. Bind-mounted RW to
|
and other large working data. Bind-mounted RW to
|
||||||
`/agents/<name>/work` inside the container and owned by the agent
|
`/agents/<name>/work` inside the container and owned by the agent
|
||||||
|
|
|
||||||
|
|
@ -20,14 +20,16 @@ swarmctl agent inference clear iris
|
||||||
agent's `availableModels` as the models its subagent `start` and
|
agent's `availableModels` as the models its subagent `start` and
|
||||||
`continue` calls may name, read on each call
|
`continue` calls may name, read on each call
|
||||||
([`subagent.md`](../tools/subagent.md#which-models-a-subagent-may-run-on)).
|
([`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
|
- `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
|
nothing for the agent), and then the model and effort the agent reports
|
||||||
running on, each with where it comes from.
|
running on, each with where it comes from.
|
||||||
- `clear` removes every field; the agent goes back to its own values.
|
- `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
|
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
|
## What an agent runs on
|
||||||
|
|
||||||
|
|
@ -47,11 +49,15 @@ Each running value names where it comes from:
|
||||||
| ------------ | -------------------------------------------------------------------------- |
|
| ------------ | -------------------------------------------------------------------------- |
|
||||||
| `swarm` | the value set here |
|
| `swarm` | the value set here |
|
||||||
| `agent_nix` | `services.hyperhive.agent.model` or `effortLevel` in the agent's agent.nix |
|
| `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`) |
|
| `default` | the harness's built-in default (`haiku`, `medium`) |
|
||||||
|
|
||||||
When the agent starts without a swarm value, its agent.nix model wins over
|
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.
|
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
|
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
|
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.
|
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`
|
- **Its own value is the one it runs without the swarm**: its agent.nix or a
|
||||||
or `/effort` pick in its web UI or from its agent.nix. Setting a swarm
|
`hyperhive-model` / `hyperhive-effort` file. Setting a swarm value doesn't
|
||||||
value doesn't overwrite it.
|
overwrite it.
|
||||||
- **Clearing a field** (or the whole key) puts the agent back on its own
|
- **Clearing a field** (or the whole key) puts the agent back on its own
|
||||||
value on the next read.
|
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
|
- **After a restart**, the agent runs its own values until its first read
|
||||||
of its settings succeeds.
|
of its settings succeeds.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
The match ignores case. On an ACP agent the names are the agent's own, such
|
||||||
as `<provider>/<model>` on `opencode`.
|
as `<provider>/<model>` on `opencode`.
|
||||||
|
|
||||||
The web UI's model picker still offers `availableModels`.
|
|
||||||
|
|
||||||
## Runtime
|
## Runtime
|
||||||
|
|
||||||
A subagent runs on its parent agent's runtime
|
A subagent runs on its parent agent's runtime
|
||||||
|
|
|
||||||
|
|
@ -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.
|
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>`
|
**Usage:** `swarmctl agent inference <COMMAND>`
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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`
|
read-only, un-overridable), wired in `nix/agent-modules/claude-settings.nix`
|
||||||
from the `prompts/claude-settings.json` asset. `effortLevel` is
|
from the `prompts/claude-settings.json` asset. `effortLevel` is
|
||||||
deliberately not in that file — the `--effort` flag
|
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.
|
live, which managed scope would otherwise lock.
|
||||||
|
|
||||||
The harness reads `<name>` from `Bus::model()` on each turn.
|
The harness reads `<name>` from `Bus::model()` on each turn.
|
||||||
`services.hyperhive.agent.model` in the agent's `agent.nix` sets the initial
|
`services.hyperhive.agent.model` in the agent's `agent.nix` sets the initial
|
||||||
default (NixOS option; propagates via `HIVE_DEFAULT_MODEL` env var; falls
|
default (NixOS option; propagates via `HIVE_DEFAULT_MODEL` env var; falls
|
||||||
back to `"haiku"` if unset). The operator can flip it at runtime
|
back to an existing `/harness/hyperhive-model` file, then `"haiku"`, if
|
||||||
with `/model <name>` in the web terminal — the next turn picks it
|
unset). The swarm changes the model at runtime, not the web
|
||||||
up. The harness persists the choice to `/harness/hyperhive-model` so it
|
terminal: a model or effort the swarm sets for the agent replaces the
|
||||||
survives restart; override path: `HYPERHIVE_MODEL_FILE` env var
|
`Bus` value from the next turn, and the harness doesn't persist it;
|
||||||
for tests. A model or effort the swarm sets for the agent replaces the
|
clearing it goes back to the local value →
|
||||||
`Bus` value, and the harness doesn't persist it; clearing it goes back to
|
[`swarm/inference.md`](../swarm/inference.md).
|
||||||
the persisted or nix value → [`swarm/inference.md`](../swarm/inference.md).
|
|
||||||
|
|
||||||
`harness_state::context_window_tokens(model)` looks up
|
`harness_state::context_window_tokens(model)` looks up
|
||||||
context-window size per-model. Resolution order (first
|
context-window size per-model. Resolution order (first
|
||||||
|
|
|
||||||
|
|
@ -47,18 +47,20 @@ at a given viewport width. Two columns:
|
||||||
`◌ needs login` / `◌ logging in` / `○ offline` / `… connecting`.
|
`◌ needs login` / `◌ logging in` / `○ offline` / `… connecting`.
|
||||||
- **State badge**: `💤 idle` / `🧠 thinking` / `📦 compacting` /
|
- **State badge**: `💤 idle` / `🧠 thinking` / `📦 compacting` /
|
||||||
`○ offline` / `… booting` + age suffix.
|
`○ offline` / `… booting` + age suffix.
|
||||||
- **Model badge** (`model · <name> ▾`): a real picker — selecting it opens
|
- **Model and effort badges** (`model · <name>`, `effort · <level>`):
|
||||||
a `Dropdown` of `state.available_models`, selecting one POSTs
|
display only. Each tooltip says where the value comes from
|
||||||
`/api/model` immediately (same endpoint the `/model <name>` slash
|
(`state.model_source` / `state.effort_source`: set on the swarm,
|
||||||
command uses). A header badge: the control and its display sit
|
agent.nix, an existing `hyperhive-model` / `hyperhive-effort` file, or
|
||||||
together.
|
the built-in default); the model's also says what the last turn
|
||||||
- **Effort badge** (`effort · <level> ▾`): same shape, `/api/effort`,
|
resolved it to. Both are set on the swarm: the swarm UI's agents page,
|
||||||
shown when `state.available_efforts` is non-empty.
|
or `swarmctl agent inference set`
|
||||||
- On an ACP agent both pickers list what its session offers (its
|
([`inference.md`](../swarm/inference.md)).
|
||||||
`model` and `thought_level` config options), and are empty until the
|
- On an ACP agent each badge shows the session's own value when it
|
||||||
first turn has attached a session. The effort picker is hidden when the
|
doesn't offer the requested one. The effort badge is hidden
|
||||||
model offers no effort levels, and while a newly picked model waits for
|
(`state.effort` is `null`) until the first turn has attached a
|
||||||
the next turn, since the levels come with the model.
|
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,
|
- **Ctx / cost badges**: `ctx · 142k` (last inference's prompt size,
|
||||||
tooltip shows % of context window) / `cost · 1.3M` (cumulative
|
tooltip shows % of context window) / `cost · 1.3M` (cumulative
|
||||||
tokens billed across every inference in the last turn).
|
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
|
(`GET /api/state`'s `links` field) also feeds
|
||||||
`DashboardState.links` for the dashboard card's icon strip —
|
`DashboardState.links` for the dashboard card's icon strip —
|
||||||
`agent_links()` in hive-agent is the single source of truth for
|
`agent_links()` in hive-agent is the single source of truth for
|
||||||
both. The dashboard link lives in this 🔗 popover; the model/effort
|
both. The dashboard link lives in this 🔗 popover, and
|
||||||
pickers are real badges above; and `/new-session` / `/logout` are
|
`/new-session` / `/logout` are
|
||||||
typed slash commands with their own type-twice confirm (see
|
typed slash commands with their own type-twice confirm (see
|
||||||
below).
|
below).
|
||||||
- `/cancel` (slash command, below) is the only cancel-turn path; the
|
- `/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
|
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
|
the live panel. Works mid-turn, not only when idle. Either way the live
|
||||||
panel ends with a `/compact done` or `/compact failed: …` note.
|
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).
|
- `/new-session` — `POST /api/new-session` (confirms first).
|
||||||
Arms a one-shot on the Bus; next turn runs without
|
Arms a one-shot on the Bus; next turn runs without
|
||||||
`--continue`, dropping the resume session entirely.
|
`--continue`, dropping the resume session entirely.
|
||||||
|
|
@ -280,17 +274,6 @@ shaped).
|
||||||
turn — only the stdin payload differs). Flips state to
|
turn — only the stdin payload differs). Flips state to
|
||||||
`Compacting` via `Bus::set_state`, which emits
|
`Compacting` via `Bus::set_state`, which emits
|
||||||
`TurnStateChanged`.
|
`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
|
- `POST /api/new-session` — arm a one-shot for the next turn to
|
||||||
drop `--continue`. Emits a `LiveEvent::Note`.
|
drop `--continue`. Emits a `LiveEvent::Note`.
|
||||||
- `POST /api/logout` — three-step teardown that re-uses the
|
- `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
|
the frontend (`useAgentState.ts`) on page load and, on a poll, while
|
||||||
`status === 'needs_login_in_progress'`. Includes `turn_state`,
|
`status === 'needs_login_in_progress'`. Includes `turn_state`,
|
||||||
`context_window_tokens`, `qualified_label`,
|
`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
|
other fields described inline throughout this document. All
|
||||||
subsequent state updates arrive via SSE.
|
subsequent state updates arrive via SSE.
|
||||||
- `GET /api/dashboard-state` — lean snapshot of agent-owned fields
|
- `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
|
`needs_login_idle` / `needs_login_in_progress`. Drives the
|
||||||
alive-badge. The harness sets `rate_limited` when it detects a
|
alive-badge. The harness sets `rate_limited` when it detects a
|
||||||
429 response and clears it when the retry sleep expires.
|
429 response and clears it when the retry sleep expires.
|
||||||
- `model_changed { model }` — drives the model chip.
|
- `model_changed { model }` — drives the model chip. The harness emits it
|
||||||
- `effort_changed { effort }` — drives the effort picker chip.
|
when it applies or clears a swarm value.
|
||||||
Emitted from `Bus::set_effort` on `POST /api/effort`; applies on
|
- `effort_changed { effort }` — drives the effort chip. The harness
|
||||||
the next session start.
|
emits it when it applies or clears a swarm value.
|
||||||
- `token_usage_changed { ctx: TokenUsage, cost: TokenUsage }`
|
- `token_usage_changed { ctx: TokenUsage, cost: TokenUsage }`
|
||||||
— drives the ctx + cost badges. Emitted from
|
— drives the ctx + cost badges. Emitted from
|
||||||
`Bus::record_turn_usage` at turn-end; `ctx` is the last
|
`Bus::record_turn_usage` at turn-end; `ctx` is the last
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,6 @@ import { useTodos } from "./hooks/useTodos.js";
|
||||||
import { fmtAge, fmtTokens } from "./lib/format.js";
|
import { fmtAge, fmtTokens } from "./lib/format.js";
|
||||||
import { resolveDashboardBase } from "./lib/dashboardBase.js";
|
import { resolveDashboardBase } from "./lib/dashboardBase.js";
|
||||||
import { submitPauseResume } from "./lib/pauseAction.js";
|
import { submitPauseResume } from "./lib/pauseAction.js";
|
||||||
import { postModel, postEffort } from "./lib/modelEffort.js";
|
|
||||||
import {
|
import {
|
||||||
postCancelTurn,
|
postCancelTurn,
|
||||||
postLogout,
|
postLogout,
|
||||||
|
|
@ -220,11 +219,6 @@ export function Root() {
|
||||||
statusLabel="… connecting"
|
statusLabel="… connecting"
|
||||||
statusTone="neutral"
|
statusTone="neutral"
|
||||||
model=""
|
model=""
|
||||||
availableModels={[]}
|
|
||||||
onSelectModel={() => {}}
|
|
||||||
effort=""
|
|
||||||
availableEfforts={[]}
|
|
||||||
onSelectEffort={() => {}}
|
|
||||||
paused={false}
|
paused={false}
|
||||||
onTogglePause={() => {}}
|
onTogglePause={() => {}}
|
||||||
thinking={false}
|
thinking={false}
|
||||||
|
|
@ -308,15 +302,9 @@ export function Root() {
|
||||||
statusTooltip={primaryStatus.tooltip}
|
statusTooltip={primaryStatus.tooltip}
|
||||||
model={state.model}
|
model={state.model}
|
||||||
resolvedModel={state.resolved_model ?? undefined}
|
resolvedModel={state.resolved_model ?? undefined}
|
||||||
availableModels={state.available_models}
|
modelSource={state.model_source}
|
||||||
onSelectModel={(name) => {
|
|
||||||
postModel(name).then(() => refresh());
|
|
||||||
}}
|
|
||||||
effort={state.effort}
|
effort={state.effort}
|
||||||
availableEfforts={state.available_efforts}
|
effortSource={state.effort_source}
|
||||||
onSelectEffort={(level) => {
|
|
||||||
postEffort(level).then(() => refresh());
|
|
||||||
}}
|
|
||||||
ctxLabel={ctx !== null ? fmtTokens(ctx) : undefined}
|
ctxLabel={ctx !== null ? fmtTokens(ctx) : undefined}
|
||||||
costLabel={cost !== null ? fmtTokens(cost) : undefined}
|
costLabel={cost !== null ? fmtTokens(cost) : undefined}
|
||||||
paused={state.paused}
|
paused={state.paused}
|
||||||
|
|
|
||||||
|
|
@ -13,8 +13,8 @@
|
||||||
/* mara, live, on a screenshot of this row: "badges and interactive
|
/* mara, live, on a screenshot of this row: "badges and interactive
|
||||||
elements on same row have inconsistent size." Real cause: Badge.css's
|
elements on same row have inconsistent size." Real cause: Badge.css's
|
||||||
`.ui-badge-interactive` grows to a 2.75em WCAG touch-target floor
|
`.ui-badge-interactive` grows to a 2.75em WCAG touch-target floor
|
||||||
(model/effort pickers, pause) while a plain display `<span>` badge
|
(pause) while a plain display `<span>` badge (model, effort,
|
||||||
(alive, state, ctx, cost — no `onClick`) stays compact, so the row
|
alive, state, ctx, cost — no `onClick`) stays compact, so the row
|
||||||
reads as two different pill sizes even though every badge here is
|
reads as two different pill sizes even though every badge here is
|
||||||
the same `<Badge>` component. Scoped to this row rather than
|
the same `<Badge>` component. Scoped to this row rather than
|
||||||
Badge.css itself — display-only badges elsewhere (e.g. the dashboard)
|
Badge.css itself — display-only badges elsewhere (e.g. the dashboard)
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
// <StatusChips> — the agent page's status row: the consolidated status
|
// <StatusChips> — the agent page's status row: the consolidated status
|
||||||
// badge (alive/thinking/paused/etc., with pause/resume + cancel-turn in
|
// 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
|
// 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
|
// (the old page's model/effort *pickers* lived in the `⋯` overflow menu
|
||||||
// while the current model/effort only showed as a disconnected chip) —
|
// while the current model/effort only showed as a disconnected chip) —
|
||||||
|
|
@ -13,6 +14,7 @@
|
||||||
import { useRef, useState } from "preact/hooks";
|
import { useRef, useState } from "preact/hooks";
|
||||||
import { Badge, type BadgeTone } from "@hive/shared/badge.js";
|
import { Badge, type BadgeTone } from "@hive/shared/badge.js";
|
||||||
import { Dropdown, type DropdownOption } from "@hive/shared/dropdown.js";
|
import { Dropdown, type DropdownOption } from "@hive/shared/dropdown.js";
|
||||||
|
import type { InferenceSource } from "../types.js";
|
||||||
import "./StatusChips.css";
|
import "./StatusChips.css";
|
||||||
|
|
||||||
export interface StatusChipsProps {
|
export interface StatusChipsProps {
|
||||||
|
|
@ -25,11 +27,11 @@ export interface StatusChipsProps {
|
||||||
* operator can tell what an alias resolved to. `undefined` before any
|
* operator can tell what an alias resolved to. `undefined` before any
|
||||||
* turn has completed. */
|
* turn has completed. */
|
||||||
resolvedModel?: string;
|
resolvedModel?: string;
|
||||||
availableModels: string[];
|
/** Where `model` comes from (`/api/state` `model_source`). */
|
||||||
onSelectModel: (name: string) => void;
|
modelSource?: InferenceSource;
|
||||||
effort: string;
|
/** Hidden when empty: an ACP session that offers no effort now. */
|
||||||
availableEfforts: string[];
|
effort?: string | null;
|
||||||
onSelectEffort: (level: string) => void;
|
effortSource?: InferenceSource;
|
||||||
ctxLabel?: string;
|
ctxLabel?: string;
|
||||||
costLabel?: string;
|
costLabel?: string;
|
||||||
paused: boolean;
|
paused: boolean;
|
||||||
|
|
@ -50,66 +52,12 @@ export interface StatusChipsProps {
|
||||||
onLogout: () => Promise<void>;
|
onLogout: () => Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const MODEL_DESCRIPTIONS: Record<string, string> = {
|
const SOURCE_LABEL: Record<InferenceSource, string> = {
|
||||||
haiku: "fast",
|
swarm: "set on the swarm",
|
||||||
sonnet: "balanced",
|
agent_nix: "from agent.nix",
|
||||||
opus: "powerful",
|
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
|
// The consolidated alive/thinking/paused badge (mara: "why do we
|
||||||
// have separate alive badge? ... it could be one badge that shows
|
// have separate alive badge? ... it could be one badge that shows
|
||||||
// thinking / idle / paused / ... with a dropdown that opens when
|
// thinking / idle / paused / ... with a dropdown that opens when
|
||||||
|
|
@ -235,11 +183,9 @@ export function StatusChips({
|
||||||
statusTooltip,
|
statusTooltip,
|
||||||
model,
|
model,
|
||||||
resolvedModel,
|
resolvedModel,
|
||||||
availableModels,
|
modelSource,
|
||||||
onSelectModel,
|
|
||||||
effort,
|
effort,
|
||||||
availableEfforts,
|
effortSource,
|
||||||
onSelectEffort,
|
|
||||||
ctxLabel,
|
ctxLabel,
|
||||||
costLabel,
|
costLabel,
|
||||||
paused,
|
paused,
|
||||||
|
|
@ -263,21 +209,23 @@ export function StatusChips({
|
||||||
onNewSession={onNewSession}
|
onNewSession={onNewSession}
|
||||||
onLogout={onLogout}
|
onLogout={onLogout}
|
||||||
/>
|
/>
|
||||||
<Picker
|
<Badge
|
||||||
label="model"
|
label="model"
|
||||||
value={model}
|
value={model}
|
||||||
options={availableModels}
|
title={
|
||||||
descriptions={MODEL_DESCRIPTIONS}
|
[
|
||||||
onSelect={onSelectModel}
|
modelSource ? SOURCE_LABEL[modelSource] : null,
|
||||||
title={resolvedModel ? `resolved to ${resolvedModel}` : undefined}
|
resolvedModel ? `resolved to ${resolvedModel}` : null,
|
||||||
|
]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(" · ") || undefined
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
{availableEfforts.length ? (
|
{effort ? (
|
||||||
<Picker
|
<Badge
|
||||||
label="effort"
|
label="effort"
|
||||||
value={effort}
|
value={effort}
|
||||||
options={availableEfforts}
|
title={effortSource ? SOURCE_LABEL[effortSource] : undefined}
|
||||||
descriptions={EFFORT_DESCRIPTIONS}
|
|
||||||
onSelect={onSelectEffort}
|
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
{ctxLabel ? (
|
{ctxLabel ? (
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,6 @@
|
||||||
// (a local note explains what confirming does), typing it again within
|
// (a local note explains what confirming does), typing it again within
|
||||||
// the same "armed" state fires it — a keyboard-native two-step confirm.
|
// the same "armed" state fires it — a keyboard-native two-step confirm.
|
||||||
import { useRef, useState } from "preact/hooks";
|
import { useRef, useState } from "preact/hooks";
|
||||||
import { postModel, postEffort } from "../lib/modelEffort.js";
|
|
||||||
import {
|
import {
|
||||||
postCancelTurn,
|
postCancelTurn,
|
||||||
postCompact,
|
postCompact,
|
||||||
|
|
@ -32,14 +31,6 @@ const SLASH_COMMANDS: { name: string; desc: string }[] = [
|
||||||
{ name: "/clear", desc: "wipe the terminal panel (local-only)" },
|
{ name: "/clear", desc: "wipe the terminal panel (local-only)" },
|
||||||
{ name: "/cancel", desc: "SIGINT the in-flight claude turn" },
|
{ name: "/cancel", desc: "SIGINT the in-flight claude turn" },
|
||||||
{ name: "/compact", desc: "compact the persistent claude session" },
|
{ 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",
|
name: "/new-session",
|
||||||
desc: "fresh claude session next turn (type twice to confirm)",
|
desc: "fresh claude session next turn (type twice to confirm)",
|
||||||
|
|
@ -84,8 +75,7 @@ export function TermInput({
|
||||||
|
|
||||||
function handleSlash(line: string): boolean {
|
function handleSlash(line: string): boolean {
|
||||||
const trimmed = line.trim();
|
const trimmed = line.trim();
|
||||||
const [cmd, ...rest] = trimmed.split(/\s+/);
|
const [cmd] = trimmed.split(/\s+/);
|
||||||
const arg = rest.join(" ");
|
|
||||||
const wasArmed = armedRef.current === cmd;
|
const wasArmed = armedRef.current === cmd;
|
||||||
armedRef.current = null;
|
armedRef.current = null;
|
||||||
|
|
||||||
|
|
@ -131,26 +121,6 @@ export function TermInput({
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return true;
|
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:
|
default:
|
||||||
onLocalNote(`✗ unknown slash command: ${cmd} — try /help`);
|
onLocalNote(`✗ unknown slash command: ${cmd} — try /help`);
|
||||||
return true;
|
return true;
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ const RETRY_MS = 5000;
|
||||||
export interface UseAgentStateResult {
|
export interface UseAgentStateResult {
|
||||||
state: AgentState | null;
|
state: AgentState | null;
|
||||||
error: string | 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;
|
refresh: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
// (this agent's own backend, `hive-agent/src/web_ui/auth.rs`), so a
|
// (this agent's own backend, `hive-agent/src/web_ui/auth.rs`), so a
|
||||||
// plain `fetch` is safe (unlike `pauseAction.ts`'s dashboard-origin
|
// plain `fetch` is safe (unlike `pauseAction.ts`'s dashboard-origin
|
||||||
// actions). Same `{ ok, detail }` shape and `redirect: 'manual'` /
|
// 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
|
// these three routes actually redirect today, but the same tolerant
|
||||||
// check costs nothing and keeps every same-origin POST in this package
|
// check costs nothing and keeps every same-origin POST in this package
|
||||||
// behaving identically.
|
// behaving identically.
|
||||||
|
|
|
||||||
|
|
@ -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);
|
|
||||||
|
|
@ -5,8 +5,7 @@
|
||||||
// origin, but accessed directly they're different ports) and this way
|
// 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
|
// 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
|
// an opaque, unreadable response, which is fine here since the caller
|
||||||
// doesn't need to read it, only trigger a state refresh afterward (same
|
// doesn't need to read it, only trigger a state refresh afterward. `credentials:
|
||||||
// shape as `postModel`/`postEffort` in modelEffort.ts). `credentials:
|
|
||||||
// 'include'` matches the cookie-sending behavior of the plain <form>
|
// 'include'` matches the cookie-sending behavior of the plain <form>
|
||||||
// submit this replaces.
|
// submit this replaces.
|
||||||
//
|
//
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
// Same-origin fetch POST helpers for the term-input slash commands —
|
// Same-origin fetch POST helpers for the term-input slash commands
|
||||||
// same shape as modelEffort.ts's `post()` ({ ok, detail } rather than
|
// ({ ok, detail } rather than throwing), reused by TermInput.tsx.
|
||||||
// throwing), reused by TermInput.tsx.
|
|
||||||
async function post(
|
async function post(
|
||||||
path: string,
|
path: string,
|
||||||
body?: URLSearchParams,
|
body?: URLSearchParams,
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,9 @@
|
||||||
// today, and the frontend only ever reads a handful of these fields.
|
// today, and the frontend only ever reads a handful of these fields.
|
||||||
// Extend as more of the page gets rebuilt against real data.
|
// 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 {
|
export interface AgentState {
|
||||||
seq: number;
|
seq: number;
|
||||||
label: string;
|
label: string;
|
||||||
|
|
@ -24,9 +27,9 @@ export interface AgentState {
|
||||||
cost_usage: TokenUsage | null;
|
cost_usage: TokenUsage | null;
|
||||||
hive_name: string | null;
|
hive_name: string | null;
|
||||||
swarm_name: string | null;
|
swarm_name: string | null;
|
||||||
available_models: string[];
|
model_source: InferenceSource;
|
||||||
effort: string;
|
effort: string | null;
|
||||||
available_efforts: string[];
|
effort_source: InferenceSource;
|
||||||
paused: boolean;
|
paused: boolean;
|
||||||
inbox: InboxRow[];
|
inbox: InboxRow[];
|
||||||
links: AgentLink[];
|
links: AgentLink[];
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,7 @@
|
||||||
// the `Badge` that opened it (../badge/Badge.tsx) so the control is
|
// the `Badge` that opened it (../badge/Badge.tsx) so the control is
|
||||||
// visually attached to the thing it affects, instead of living three
|
// visually attached to the thing it affects, instead of living three
|
||||||
// clicks away in a catch-all menu (the junk-drawer pattern
|
// clicks away in a catch-all menu (the junk-drawer pattern
|
||||||
// `docs/web-ui/design-guide.md` calls out by name against the old
|
// `docs/web-ui/design-guide.md` calls out by name). Not a native `<dialog>` — a
|
||||||
// per-agent terminal's model picker). Not a native `<dialog>` — a
|
|
||||||
// `showModal()` dialog's backdrop would visually detach it from its
|
// `showModal()` dialog's backdrop would visually detach it from its
|
||||||
// anchor badge, and a non-modal `show()` gets no click-outside-to-close
|
// 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
|
// for free, so this hand-rolls the same "outside click or Escape
|
||||||
|
|
|
||||||
|
|
@ -167,8 +167,7 @@ function RefreshIntervalPickerSample() {
|
||||||
}
|
}
|
||||||
|
|
||||||
// The badge-triggers-a-dropdown-right-underneath-itself pattern this is
|
// The badge-triggers-a-dropdown-right-underneath-itself pattern this is
|
||||||
// meant to land — the model/effort picker shape from the per-agent
|
// meant to land, demoed here with sample data. The wrapper
|
||||||
// terminal, demoed here with sample data. The wrapper
|
|
||||||
// needs `position: relative` (`.components-badge-anchor`) so `Dropdown`
|
// needs `position: relative` (`.components-badge-anchor`) so `Dropdown`
|
||||||
// anchors under this badge specifically, not the page.
|
// anchors under this badge specifically, not the page.
|
||||||
function BadgePickerSample() {
|
function BadgePickerSample() {
|
||||||
|
|
@ -528,7 +527,7 @@ export function ComponentsPage() {
|
||||||
<Sample label="with a label prefix">
|
<Sample label="with a label prefix">
|
||||||
<Badge label="ctx" value="534k" />
|
<Badge label="ctx" value="534k" />
|
||||||
</Sample>
|
</Sample>
|
||||||
<Sample label="interactive, opens a Dropdown (model/effort picker shape)">
|
<Sample label="interactive, opens a Dropdown">
|
||||||
<BadgePickerSample />
|
<BadgePickerSample />
|
||||||
</Sample>
|
</Sample>
|
||||||
<Sample label="interactive, toggles itself (pause/resume shape)">
|
<Sample label="interactive, toggles itself (pause/resume shape)">
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@
|
||||||
// explicit — a dropdown just lets "up" be picked directly regardless
|
// explicit — a dropdown just lets "up" be picked directly regardless
|
||||||
// of what's inferred, no flip-to-the-opposite-of-a-guess involved
|
// 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").
|
// (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.
|
// `StatusMenu`: the caller owns what each selection actually does.
|
||||||
//
|
//
|
||||||
// "paused" has no separate resume option here — selecting "up" from a
|
// "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
Loading…
Reference in a new issue