team runs modal: expose runtime posture (risk_profile + mcp_bundles)
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:
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" }} />
|
||||||
|
|||||||
Reference in New Issue
Block a user