team runs modal: expose runtime posture (risk_profile + mcp_bundles)
ci / gates (push) Successful in 6s
ci / rust (push) Failing after 9s
ci / frontend (push) Failing after 42s
ci / e2e (push) Skipped
ci / publish (push) Skipped

PATCH /api/teams/{id}/runtime-config existed since the team-wizard work
but had no UI — you needed curl to swap a team's risk_profile or add
gitea_forge to its bundles. Add compact pickers to TeamRunsModal above
"Run now": a Risk profile <select> and a Bundle checkbox list.

Fetches current settings from GET /api/teams/{id} on open and PATCHes
inline on every change. No save button — the debounce is the user's
next click. Optimistic state; next-open resync corrects any drift.

Co-Authored-By: Claude Opus 4.7 <[email protected]>
This commit is contained in:
Omar Sobh
2026-07-18 16:19:12 -07:00
co-authored by Claude Opus 4.7
parent 9cab32c354
commit 6f1515b1b0
@@ -31,6 +31,50 @@ export function TeamRunsModal({ teamId, teamName, onClose }: { teamId: string; t
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const esRef = useRef<EventSource | null>(null); const esRef = useRef<EventSource | null>(null);
// Runtime posture: pickers for risk_profile + clawmates_door bundle.
// Loaded lazily from GET /api/teams/{id} on mount; PATCH on change.
const [riskProfile, setRiskProfile] = useState<string>("");
const [bundles, setBundles] = useState<string[]>([]);
const [savingRuntime, setSavingRuntime] = useState(false);
const [runtimeSaved, setRuntimeSaved] = useState(false);
const RISK_OPTIONS = [
{ v: "", label: "(inherit default)" },
{ v: "toolfree", label: "toolfree — no side-effect tools" },
{ v: "research_readonly", label: "research_readonly — file+repo read" },
{ v: "research_web_readonly", label: "research_web_readonly — + web fetch" },
{ v: "coding_readwrite", label: "coding_readwrite — writes to /workspace/repo" },
];
const BUNDLE_OPTIONS = ["clawmates_door", "gitea_forge"];
useEffect(() => {
(async () => {
try {
const r = await fetch(`/api/teams/${encodeURIComponent(teamId)}`);
if (!r.ok) return;
const d = await r.json();
setRiskProfile(d.risk_profile ?? "");
setBundles(Array.isArray(d.mcp_bundles) ? d.mcp_bundles : []);
} catch { /* silent — pickers stay at defaults */ }
})();
}, [teamId]);
async function saveRuntime(next: { risk_profile?: string | null; mcp_bundles?: string[] }) {
setSavingRuntime(true);
setRuntimeSaved(false);
try {
await fetch(`/api/teams/${encodeURIComponent(teamId)}/runtime-config`, {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
risk_profile: next.risk_profile === "" ? null : next.risk_profile,
mcp_bundles: next.mcp_bundles,
}),
});
setRuntimeSaved(true);
} catch { /* keep the optimistic UI; next open will resync */ }
finally { setSavingRuntime(false); }
}
useEffect(() => { useEffect(() => {
const onKey = (e: KeyboardEvent) => { if (e.key === "Escape") onClose(); }; const onKey = (e: KeyboardEvent) => { if (e.key === "Escape") onClose(); };
window.addEventListener("keydown", onKey); window.addEventListener("keydown", onKey);
@@ -94,6 +138,45 @@ export function TeamRunsModal({ teamId, teamName, onClose }: { teamId: string; t
<div style={{ flex: 1, minHeight: 0, display: "flex" }}> <div style={{ flex: 1, minHeight: 0, display: "flex" }}>
{/* Left: run-now + recent runs */} {/* Left: run-now + recent runs */}
<div style={{ flex: "none", width: 320, display: "flex", flexDirection: "column", borderRight: "1px solid rgba(255,255,255,.07)" }}> <div style={{ flex: "none", width: 320, display: "flex", flexDirection: "column", borderRight: "1px solid rgba(255,255,255,.07)" }}>
<div style={{ flex: "none", padding: 14, borderBottom: "1px solid rgba(255,255,255,.06)" }}>
<div style={{ fontFamily: mono, fontSize: 9.5, letterSpacing: ".1em", color: "#c9a659", marginBottom: 6 }}>
RUNTIME {savingRuntime ? "· saving…" : runtimeSaved ? "· saved" : ""}
</div>
<label style={{ display: "block", fontSize: 11, color: "#8a8a92", marginBottom: 4 }}>Risk profile</label>
<select
value={riskProfile}
onChange={(e) => {
const v = e.target.value;
setRiskProfile(v);
void saveRuntime({ risk_profile: v, mcp_bundles: bundles });
}}
style={{ width: "100%", boxSizing: "border-box", padding: "6px 8px", borderRadius: 8, border: "1px solid rgba(255,255,255,.12)", background: "#141417", color: "#eaeaee", fontSize: 12, marginBottom: 10 }}
>
{RISK_OPTIONS.map((o) => (
<option key={o.v} value={o.v}>{o.label}</option>
))}
</select>
<label style={{ display: "block", fontSize: 11, color: "#8a8a92", marginBottom: 4 }}>MCP bundles</label>
<div style={{ display: "flex", flexDirection: "column", gap: 4 }}>
{BUNDLE_OPTIONS.map((b) => {
const on = bundles.includes(b);
return (
<label key={b} style={{ display: "flex", alignItems: "center", gap: 6, fontSize: 12, color: "#d6d6da", cursor: "pointer" }}>
<input
type="checkbox"
checked={on}
onChange={() => {
const next = on ? bundles.filter((x) => x !== b) : [...bundles, b];
setBundles(next);
void saveRuntime({ risk_profile: riskProfile, mcp_bundles: next });
}}
/>
<span style={{ fontFamily: mono, fontSize: 11 }}>{b}</span>
</label>
);
})}
</div>
</div>
<div style={{ flex: "none", padding: 14, borderBottom: "1px solid rgba(255,255,255,.06)" }}> <div style={{ flex: "none", padding: 14, borderBottom: "1px solid rgba(255,255,255,.06)" }}>
<div style={{ fontFamily: mono, fontSize: 9.5, letterSpacing: ".1em", color: "#5ec8d8", marginBottom: 6 }}>RUN NOW</div> <div style={{ fontFamily: mono, fontSize: 9.5, letterSpacing: ".1em", color: "#5ec8d8", marginBottom: 6 }}>RUN NOW</div>
<textarea value={task} onChange={(e) => setTask(e.target.value)} rows={2} placeholder="Task for this run…" style={{ width: "100%", boxSizing: "border-box", resize: "none", padding: "8px 10px", borderRadius: 9, border: "1px solid rgba(255,255,255,.12)", background: "#141417", color: "#eaeaee", fontSize: 12.5, fontFamily: "inherit" }} /> <textarea value={task} onChange={(e) => setTask(e.target.value)} rows={2} placeholder="Task for this run…" style={{ width: "100%", boxSizing: "border-box", resize: "none", padding: "8px 10px", borderRadius: 9, border: "1px solid rgba(255,255,255,.12)", background: "#141417", color: "#eaeaee", fontSize: 12.5, fontFamily: "inherit" }} />