Watch
0
0
Fork
You've already forked hyperhive
0

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
This commit is contained in:
atlas 2026-10-11 01:26:39 +02:00
commit ed271e655f
25 changed files with 170 additions and 464 deletions

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 { postEffort } from "./lib/modelEffort.js";
import {
postCancelTurn,
postLogout,
@ -220,9 +219,6 @@ export function Root() {
statusLabel="… connecting"
statusTone="neutral"
model=""
effort=""
availableEfforts={[]}
onSelectEffort={() => {}}
paused={false}
onTogglePause={() => {}}
thinking={false}
@ -308,10 +304,7 @@ export function Root() {
resolvedModel={state.resolved_model ?? undefined}
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
(effort picker, pause) while a plain display `<span>` badge
(model, 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,7 +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 (display-only: the swarm sets it), the effort
// picker, 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) —
@ -14,7 +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 type { InferenceSource } from "../types.js";
import "./StatusChips.css";
export interface StatusChipsProps {
@ -28,10 +28,10 @@ export interface StatusChipsProps {
* turn has completed. */
resolvedModel?: string;
/** Where `model` comes from (`/api/state` `model_source`). */
modelSource?: ModelSource;
effort: string;
availableEfforts: string[];
onSelectEffort: (level: string) => void;
modelSource?: InferenceSource;
/** Hidden when empty: an ACP session that offers no effort now. */
effort?: string | null;
effortSource?: InferenceSource;
ctxLabel?: string;
costLabel?: string;
paused: boolean;
@ -52,67 +52,12 @@ export interface StatusChipsProps {
onLogout: () => Promise<void>;
}
const SOURCE_LABEL: Record<ModelSource, string> = {
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
@ -240,8 +185,7 @@ export function StatusChips({
resolvedModel,
modelSource,
effort,
availableEfforts,
onSelectEffort,
effortSource,
ctxLabel,
costLabel,
paused,
@ -277,13 +221,11 @@ export function StatusChips({
.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 { postEffort } from "../lib/modelEffort.js";
import {
postCancelTurn,
postCompact,
@ -32,10 +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: "/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)",
@ -80,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;
@ -127,16 +121,6 @@ export function TermInput({
);
}
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,38 +0,0 @@
// 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
// 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 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 `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

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