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:
parent
594693f4c7
commit
ed271e655f
25 changed files with 170 additions and 464 deletions
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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 ? (
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
@ -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.
|
||||
//
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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[];
|
||||
|
|
|
|||
Loading…
Reference in a new issue