AgentComputer: clickable model picker (dropdown → PATCH /api/claws/:id/model)
The '● <model>' text on the per-agent Computer slide-out was
read-only — swapping a claw's model required curl. Now it's a
button that opens a small dropdown of the curated fleet models:
- Sonnet 5 / Opus 4.8 / Haiku 4.5 / Sonnet 4.6 (Anthropic)
- GLM 4.6 / 5.2 (Z.AI)
- Kimi K2 (Moonshot)
- Gemini 2.0 Flash (Google)
- Llama 3.3 70B (Groq)
Click a model → PATCH /api/claws/{id}/model → picker closes, the
button relabels to the new selection. The endpoint persists the DB
binding + best-effort re-provisions the runtime; the swap applies
on the claw's next turn. Long-tail models still work via curl with
any string.
Adds an optional onModelChanged callback so callers (Dashboard's
runtime-config enrich cache) can refresh their state without a
full re-render.
This commit is contained in:
@@ -3,10 +3,144 @@
|
|||||||
// The per-agent "computer" slide-out — templated from the selected agent's own
|
// The per-agent "computer" slide-out — templated from the selected agent's own
|
||||||
// model / apps / running tasks (each agent differs).
|
// model / apps / running tasks (each agent differs).
|
||||||
|
|
||||||
import { Bell, BookOpen, Boxes, FolderClosed, Phone, Settings, Zap } from "lucide-react";
|
import { useState } from "react";
|
||||||
|
import { Bell, BookOpen, Boxes, ChevronDown, FolderClosed, Phone, Settings, Zap } from "lucide-react";
|
||||||
|
|
||||||
import type { DemoAgent, DemoApp } from "@/lib/dashboard-demo";
|
import type { DemoAgent, DemoApp } from "@/lib/dashboard-demo";
|
||||||
|
|
||||||
|
// Curated model roster the picker offers. Kept short so users pick
|
||||||
|
// from what actually runs in-fleet; long-tail models still work via
|
||||||
|
// `curl PATCH /api/claws/:id/model` with any string.
|
||||||
|
const MODEL_OPTIONS: { value: string; label: string; family: string }[] = [
|
||||||
|
{ value: "claude-sonnet-5", label: "Sonnet 5", family: "Anthropic" },
|
||||||
|
{ value: "claude-opus-4-8", label: "Opus 4.8", family: "Anthropic" },
|
||||||
|
{ value: "claude-haiku-4-5-20251001", label: "Haiku 4.5", family: "Anthropic" },
|
||||||
|
{ value: "claude-sonnet-4-6", label: "Sonnet 4.6", family: "Anthropic" },
|
||||||
|
{ value: "glm-4.6", label: "GLM 4.6", family: "Z.AI" },
|
||||||
|
{ value: "glm-5.2", label: "GLM 5.2", family: "Z.AI" },
|
||||||
|
{ value: "kimi-k2", label: "Kimi K2", family: "Moonshot" },
|
||||||
|
{ value: "gemini-2.0-flash", label: "Gemini 2.0 Flash", family: "Google" },
|
||||||
|
{ value: "llama-3.3-70b-versatile", label: "Llama 3.3 70B", family: "Groq" },
|
||||||
|
];
|
||||||
|
|
||||||
|
function ModelPicker({
|
||||||
|
agent,
|
||||||
|
onChanged,
|
||||||
|
}: {
|
||||||
|
agent: DemoAgent;
|
||||||
|
onChanged?: (model: string) => void;
|
||||||
|
}) {
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
const [busy, setBusy] = useState(false);
|
||||||
|
const [err, setErr] = useState<string | null>(null);
|
||||||
|
const current = MODEL_OPTIONS.find((o) => o.value === agent.model);
|
||||||
|
const label = current?.label ?? agent.model;
|
||||||
|
|
||||||
|
async function set(value: string) {
|
||||||
|
if (value === agent.model) {
|
||||||
|
setOpen(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setBusy(true);
|
||||||
|
setErr(null);
|
||||||
|
try {
|
||||||
|
const r = await fetch(`/api/claws/${agent.id}/model`, {
|
||||||
|
method: "PATCH",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ model: value }),
|
||||||
|
});
|
||||||
|
if (!r.ok) throw new Error(`PATCH /api/claws/${agent.id}/model → ${r.status}`);
|
||||||
|
onChanged?.(value);
|
||||||
|
setOpen(false);
|
||||||
|
} catch (e) {
|
||||||
|
setErr(e instanceof Error ? e.message : "swap failed");
|
||||||
|
} finally {
|
||||||
|
setBusy(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ position: "relative", display: "inline-block" }}>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setOpen((v) => !v)}
|
||||||
|
disabled={busy}
|
||||||
|
title="Change model"
|
||||||
|
style={{
|
||||||
|
display: "inline-flex",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: 4,
|
||||||
|
padding: "0 4px",
|
||||||
|
background: "transparent",
|
||||||
|
border: "none",
|
||||||
|
fontFamily: mono,
|
||||||
|
fontSize: 10,
|
||||||
|
color: "#5ec8d8",
|
||||||
|
cursor: busy ? "wait" : "pointer",
|
||||||
|
opacity: busy ? 0.6 : 1,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span>● {label}</span>
|
||||||
|
<ChevronDown aria-hidden size={11} />
|
||||||
|
</button>
|
||||||
|
{open && (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
top: "100%",
|
||||||
|
left: 0,
|
||||||
|
marginTop: 4,
|
||||||
|
minWidth: 200,
|
||||||
|
background: "#0b0b0e",
|
||||||
|
border: "1px solid rgba(255,255,255,.12)",
|
||||||
|
borderRadius: 8,
|
||||||
|
boxShadow: "0 8px 24px rgba(0,0,0,.5)",
|
||||||
|
zIndex: 20,
|
||||||
|
padding: 4,
|
||||||
|
maxHeight: 320,
|
||||||
|
overflow: "auto",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{MODEL_OPTIONS.map((o) => {
|
||||||
|
const active = o.value === agent.model;
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={o.value}
|
||||||
|
type="button"
|
||||||
|
onClick={() => set(o.value)}
|
||||||
|
disabled={busy || active}
|
||||||
|
style={{
|
||||||
|
display: "block",
|
||||||
|
width: "100%",
|
||||||
|
padding: "6px 10px",
|
||||||
|
border: 0,
|
||||||
|
borderRadius: 6,
|
||||||
|
background: active ? "rgba(94,200,216,.15)" : "transparent",
|
||||||
|
color: active ? "#e5f6fb" : "#cfcfd5",
|
||||||
|
cursor: active || busy ? "default" : "pointer",
|
||||||
|
fontFamily: mono,
|
||||||
|
fontSize: 11,
|
||||||
|
textAlign: "left",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div>{o.label}</div>
|
||||||
|
<div style={{ fontSize: 9, color: "#5a5a62" }}>
|
||||||
|
{o.family} · {o.value}
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
{err && (
|
||||||
|
<div style={{ padding: "6px 10px", fontFamily: mono, fontSize: 10, color: "#ff8a7a" }}>
|
||||||
|
{err}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const mono = "'JetBrains Mono', ui-monospace, monospace";
|
const mono = "'JetBrains Mono', ui-monospace, monospace";
|
||||||
|
|
||||||
function appIcon(kind: DemoApp["kind"]) {
|
function appIcon(kind: DemoApp["kind"]) {
|
||||||
@@ -31,7 +165,15 @@ const dockTile = (label: string, coral: boolean, icon: React.ReactNode) => (
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
export function AgentComputer({ agent, onClose }: { agent: DemoAgent; onClose: () => void }) {
|
export function AgentComputer({
|
||||||
|
agent,
|
||||||
|
onClose,
|
||||||
|
onModelChanged,
|
||||||
|
}: {
|
||||||
|
agent: DemoAgent;
|
||||||
|
onClose: () => void;
|
||||||
|
onModelChanged?: (model: string) => void;
|
||||||
|
}) {
|
||||||
return (
|
return (
|
||||||
<div style={{ width: 330, flex: "none", borderLeft: "1px solid rgba(255,255,255,.06)", background: "#0b0b0e", display: "flex", flexDirection: "column", minHeight: 0, animation: "cm-fade .25s ease" }}>
|
<div style={{ width: 330, flex: "none", borderLeft: "1px solid rgba(255,255,255,.06)", background: "#0b0b0e", display: "flex", flexDirection: "column", minHeight: 0, animation: "cm-fade .25s ease" }}>
|
||||||
<div style={{ padding: "16px 16px 12px", borderBottom: "1px solid rgba(255,255,255,.06)" }}>
|
<div style={{ padding: "16px 16px 12px", borderBottom: "1px solid rgba(255,255,255,.06)" }}>
|
||||||
@@ -42,7 +184,7 @@ export function AgentComputer({ agent, onClose }: { agent: DemoAgent; onClose: (
|
|||||||
</div>
|
</div>
|
||||||
<div style={{ flex: 1, minWidth: 0 }}>
|
<div style={{ flex: 1, minWidth: 0 }}>
|
||||||
<div style={{ fontSize: 14, fontWeight: 700, color: "#f3f3f5" }}>{agent.name}'s Computer</div>
|
<div style={{ fontSize: 14, fontWeight: 700, color: "#f3f3f5" }}>{agent.name}'s Computer</div>
|
||||||
<div style={{ fontFamily: mono, fontSize: 10, color: "#5ec8d8" }}>● {agent.model}</div>
|
<ModelPicker agent={agent} onChanged={onModelChanged} />
|
||||||
</div>
|
</div>
|
||||||
<div onClick={onClose} style={{ width: 26, height: 26, borderRadius: 7, border: "1px solid rgba(255,255,255,.1)", display: "flex", alignItems: "center", justifyContent: "center", color: "#6a6a72", fontSize: 13, cursor: "pointer" }}>⤢</div>
|
<div onClick={onClose} style={{ width: 26, height: 26, borderRadius: 7, border: "1px solid rgba(255,255,255,.1)", display: "flex", alignItems: "center", justifyContent: "center", color: "#6a6a72", fontSize: 13, cursor: "pointer" }}>⤢</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user