244 lines
13 KiB
React
244 lines
13 KiB
React
// overlays.jsx — Correx overlays: command palette, approval gate, and inspectors.
|
||
const { useState, useEffect, useRef, useMemo } = React;
|
||
|
||
/* ── generic overlay shell ── */
|
||
function Overlay({ tag, title, esc = "esc", center, width, onClose, children }) {
|
||
return (
|
||
<div className={"scrim" + (center ? " center" : "")} onMouseDown={(e) => { if (e.target === e.currentTarget) onClose(); }}>
|
||
<div className="ovl" style={width ? { width } : null} onMouseDown={(e) => e.stopPropagation()}>
|
||
<div className="ovl-h">
|
||
<span className="tag">{tag}</span>
|
||
<span className="ti">{title}</span>
|
||
<span className="esc">{esc} to close</span>
|
||
</div>
|
||
<div className="ovl-b">{children}</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
/* ── command palette ── */
|
||
function CommandPalette({ commands, onRun, onClose }) {
|
||
const [q, setQ] = useState("");
|
||
const [i, setI] = useState(0);
|
||
const inp = useRef(null);
|
||
useEffect(() => { inp.current && inp.current.focus(); }, []);
|
||
const score = (c) => {
|
||
const s = (c.label + " " + c.hint).toLowerCase(), t = q.toLowerCase().trim();
|
||
if (!t) return 1;
|
||
if (s.includes(t)) return 2;
|
||
let qi = 0; for (const ch of s) if (ch === t[qi]) qi++;
|
||
return qi === t.length ? 1 : 0;
|
||
};
|
||
const list = useMemo(() => commands.map((c) => [c, score(c)]).filter(([, s]) => s > 0)
|
||
.sort((a, b) => b[1] - a[1]).map(([c]) => c), [q]);
|
||
useEffect(() => { setI(0); }, [q]);
|
||
const key = (e) => {
|
||
if (e.key === "ArrowDown") { setI((x) => Math.min(x + 1, list.length - 1)); e.preventDefault(); }
|
||
else if (e.key === "ArrowUp") { setI((x) => Math.max(x - 1, 0)); e.preventDefault(); }
|
||
else if (e.key === "Enter") { list[i] && onRun(list[i].id); e.preventDefault(); }
|
||
else if (e.key === "Escape") { onClose(); }
|
||
e.stopPropagation();
|
||
};
|
||
return (
|
||
<div className="scrim" onMouseDown={(e) => { if (e.target === e.currentTarget) onClose(); }}>
|
||
<div className="ovl" style={{ width: "min(560px,94%)" }} onMouseDown={(e) => e.stopPropagation()}>
|
||
<div className="cmd-input">
|
||
<span className="pr">⌘</span>
|
||
<input ref={inp} value={q} placeholder="Jump to a capability…" onChange={(e) => setQ(e.target.value)} onKeyDown={key} />
|
||
</div>
|
||
<div className="cmd-list">
|
||
{list.length === 0 && <div className="hint-empty">no matches</div>}
|
||
{list.map((c, n) => (
|
||
<div key={c.id} className={"cmd-it" + (n === i ? " sel" : "")} onMouseEnter={() => setI(n)} onClick={() => onRun(c.id)}>
|
||
<span className="ci-k">{c.key}</span>
|
||
<span className="ci-l">{c.label}</span>
|
||
<span className="ci-h">{c.hint}</span>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
/* ── approval gate (modal interrupt) ── */
|
||
function ApprovalModal({ data, onApprove, onReject, onSteer, onClose }) {
|
||
const [steer, setSteer] = useState("");
|
||
const sref = useRef(null);
|
||
return (
|
||
<div className="scrim center" onMouseDown={(e) => e.stopPropagation()}>
|
||
<div className="ovl appr" onMouseDown={(e) => e.stopPropagation()}>
|
||
<div className="appr-tier">
|
||
<span className="appr-badge">{data.tier}</span>
|
||
<div>
|
||
<div className="lab">Approval required · <b>{data.tierLabel}</b></div>
|
||
<div className="sub">{data.intent}</div>
|
||
</div>
|
||
<span className="esc" style={{ marginLeft: "auto" }}>execution paused</span>
|
||
</div>
|
||
<div className="ovl-b">
|
||
<div className="appr-cmd"><span className="pr">$ </span>{data.command}</div>
|
||
<div style={{ color: "var(--dim)", fontSize: 12.5, marginBottom: 6 }}>{data.detail}</div>
|
||
<dl className="kv">
|
||
<dt>tool</dt><dd>{data.tool} <span className="tag-tier tier-T3">{data.tier}</span></dd>
|
||
<dt>reversible</dt><dd>{data.reversible ? "yes" : "no"}</dd>
|
||
<dt>policy</dt><dd style={{ color: "var(--dim)" }}>{data.policy}</dd>
|
||
</dl>
|
||
<div style={{ margin: "10px 0 4px", color: "var(--dim)", fontSize: 11, letterSpacing: ".08em", textTransform: "uppercase" }}>semantic risk scan</div>
|
||
{data.risks.map((r, n) => <div className="risk" key={n}>{r}</div>)}
|
||
<div className="steer">
|
||
<span style={{ color: "var(--accent)" }}>↳</span>
|
||
<input ref={sref} value={steer} placeholder='steer + approve, e.g. "ok, but lock the version"'
|
||
onChange={(e) => setSteer(e.target.value)}
|
||
onKeyDown={(e) => { e.stopPropagation(); if (e.key === "Enter" && steer.trim()) onSteer(steer.trim()); if (e.key === "Escape") sref.current.blur(); }} />
|
||
</div>
|
||
</div>
|
||
<div className="appr-acts">
|
||
<button className="btn ok" onClick={() => onApprove(false)}>approve <kbd>a</kbd></button>
|
||
<button className="btn ghost" onClick={() => onApprove(true)}>auto-approve session <kbd>A</kbd></button>
|
||
<button className="btn" onClick={() => steer.trim() ? onSteer(steer.trim()) : sref.current.focus()}>steer <kbd>s</kbd></button>
|
||
<button className="btn bad" onClick={onReject}>reject <kbd>r</kbd></button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
/* ── event stream inspector ── */
|
||
function EventInspector({ events, onClose }) {
|
||
const cats = ["All", "Lifecycle", "Context", "Inference", "Tool", "Domain", "Approval"];
|
||
const [cat, setCat] = useState("All");
|
||
const [sel, setSel] = useState(events[events.length - 1]);
|
||
const list = events.filter((e) => cat === "All" || e.cat === cat);
|
||
const payload = {
|
||
id: sel.id, type: sel.type, category: sel.cat, ts: "2026-05-29T" + sel.t,
|
||
session_id: window.CORREX.SESSION.id, correlation_id: "cor-7f2a", causation_id: sel.id === "e49" ? "e48" : null,
|
||
payload: { detail: sel.text },
|
||
};
|
||
return (
|
||
<Overlay tag="events" title="Event stream inspector" onClose={onClose} width="min(820px,95%)">
|
||
<div style={{ display: "flex", gap: 6, flexWrap: "wrap", marginBottom: 10 }}>
|
||
{cats.map((c) => <span key={c} className={"pill" + (c === cat ? " live" : "")} style={{ cursor: "pointer" }} onClick={() => setCat(c)}>{c}</span>)}
|
||
</div>
|
||
<div style={{ display: "grid", gridTemplateColumns: "1.3fr 1fr", gap: 14 }}>
|
||
<div>
|
||
{list.map((e) => (
|
||
<div key={e.id} className={"ev" + (e === sel ? " sel" : "")} onClick={() => setSel(e)}>
|
||
<span className="ev-t">{e.t}</span>
|
||
<span className={"ev-cat cat-" + e.cat}>{e.cat}</span>
|
||
<span className="ev-tx"><b>{e.type}</b> {e.text}</span>
|
||
</div>
|
||
))}
|
||
</div>
|
||
<div className="appr-cmd" style={{ margin: 0, alignSelf: "start", fontSize: 12 }}>
|
||
<pre style={{ whiteSpace: "pre-wrap", margin: 0, color: "var(--fg)" }}>{JSON.stringify(payload, null, 2)}</pre>
|
||
</div>
|
||
</div>
|
||
</Overlay>
|
||
);
|
||
}
|
||
|
||
/* ── context pack inspector ── */
|
||
function ContextPackInspector({ pack, onClose }) {
|
||
const pct = Math.round((pack.used / pack.budget) * 100);
|
||
return (
|
||
<Overlay tag="context" title="Context Pack — what the model receives" onClose={onClose} width="min(760px,95%)">
|
||
<div style={{ display: "flex", alignItems: "center", gap: 12, marginBottom: 12 }}>
|
||
<div style={{ color: "var(--fg-strong)", fontSize: 15 }}>{pack.used.toLocaleString()} <span style={{ color: "var(--dim)", fontSize: 12 }}>/ {pack.budget.toLocaleString()} tok</span></div>
|
||
<div className="mono-bar" style={{ flex: 1 }}><i style={{ width: pct + "%" }} /></div>
|
||
<div style={{ color: "var(--accent)" }}>{pct}%</div>
|
||
</div>
|
||
{pack.layers.map((l) => (
|
||
<div className="row" key={l.id} style={{ gridTemplateColumns: "38px 150px 1fr 64px" }}>
|
||
<span style={{ color: "var(--accent)", fontWeight: 600 }}>{l.id}</span>
|
||
<span style={{ color: "var(--fg)" }}>{l.name}<br /><span style={{ color: "var(--faint)", fontSize: 11 }}>{l.mode}</span></span>
|
||
<span style={{ color: "var(--dim)", fontSize: 12 }}>{l.items.join(" · ")}</span>
|
||
<span style={{ color: "var(--dim)", textAlign: "right" }}>{l.tok}t</span>
|
||
</div>
|
||
))}
|
||
<div style={{ marginTop: 12, color: "var(--dim)", fontSize: 11, letterSpacing: ".08em", textTransform: "uppercase" }}>excluded by synthesis</div>
|
||
{pack.dropped.map((d, n) => <div key={n} style={{ color: "var(--faint)", fontSize: 12.5, padding: "2px 0" }}>− {d}</div>)}
|
||
</Overlay>
|
||
);
|
||
}
|
||
|
||
/* ── stage / transition graph ── */
|
||
function StageGraph({ stages, transitions, onClose }) {
|
||
const glyph = { done: "✔", active: "▸", pending: "·" };
|
||
return (
|
||
<Overlay tag="graph" title="Stage / transition graph" onClose={onClose} width="min(720px,95%)">
|
||
<div style={{ display: "grid", gridTemplateColumns: "1.4fr 1fr", gap: 18 }}>
|
||
<div>
|
||
{stages.map((s, n) => (
|
||
<div key={s.id}>
|
||
<div style={{ display: "flex", alignItems: "baseline", gap: 10, padding: "5px 0" }}>
|
||
<span style={{ width: 16, color: s.status === "active" ? "var(--accent)" : s.status === "done" ? "var(--ok)" : "var(--faint)" }}>{glyph[s.status]}</span>
|
||
<span style={{ color: s.status === "pending" ? "var(--dim)" : "var(--fg-strong)", fontWeight: s.status === "active" ? 600 : 400, minWidth: 120 }}>{s.id}</span>
|
||
<span style={{ color: "var(--faint)", fontSize: 11 }}>role:{s.role}</span>
|
||
{s.status === "active" && <span className="pill live">running · {s.enter}</span>}
|
||
<span style={{ color: "var(--dim)", fontSize: 12, marginLeft: "auto" }}>{s.note}</span>
|
||
</div>
|
||
{n < stages.length - 1 && <div style={{ marginLeft: 7, color: "var(--faint)", lineHeight: 1 }}>│</div>}
|
||
</div>
|
||
))}
|
||
</div>
|
||
<div>
|
||
<div style={{ color: "var(--dim)", fontSize: 11, letterSpacing: ".08em", textTransform: "uppercase", marginBottom: 8 }}>transition rules</div>
|
||
<div style={{ color: "var(--faint)", fontSize: 11, marginBottom: 10 }}>no hardcoded graph — rules eval per artifact</div>
|
||
{transitions.map((t, n) => (
|
||
<div key={n} className="appr-cmd" style={{ margin: "0 0 8px", fontSize: 12 }}>
|
||
<span style={{ color: "var(--accent-2)" }}>when</span> {t.when}<br />
|
||
<span style={{ color: "var(--accent)" }}>→ goto</span> {t.goto}
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
</Overlay>
|
||
);
|
||
}
|
||
|
||
/* ── model registry / provider switch ── */
|
||
function ModelRegistry({ models, active, onSwitch, onClose }) {
|
||
return (
|
||
<Overlay tag="models" title="Model registry" onClose={onClose} width="min(800px,95%)">
|
||
<div style={{ color: "var(--faint)", fontSize: 12, marginBottom: 8 }}>models are stateless engines — switching re-synthesizes the Context Pack for the new window.</div>
|
||
{models.map((m) => (
|
||
<div className={"row"} key={m.id} style={{ gridTemplateColumns: "1fr auto", cursor: m.id === active ? "default" : "pointer", opacity: m.status === "cold" ? 0.7 : 1 }} onClick={() => m.id !== active && onSwitch(m.id)}>
|
||
<div>
|
||
<div style={{ display: "flex", alignItems: "center", gap: 9 }}>
|
||
<span style={{ color: m.id === active ? "var(--accent)" : "var(--fg-strong)", fontWeight: 600 }}>{m.id}</span>
|
||
<span className={"pill " + (m.provider === "local" ? "live" : "")}>{m.provider}</span>
|
||
{m.id === active && <span className="pill on">active</span>}
|
||
<span className="pill off">{m.status}</span>
|
||
</div>
|
||
<div style={{ color: "var(--dim)", fontSize: 11.5, marginTop: 3 }}>{m.caps.join(" · ")} — ctx {m.ctx.toLocaleString()} · t={m.temp} · {m.gpu}</div>
|
||
</div>
|
||
<div style={{ textAlign: "right", color: "var(--faint)", fontSize: 11 }}>
|
||
sessions {m.parallel}{m.id !== active && <div style={{ color: "var(--accent)" }}>switch →</div>}
|
||
</div>
|
||
</div>
|
||
))}
|
||
</Overlay>
|
||
);
|
||
}
|
||
|
||
/* ── tool palette ── */
|
||
function ToolPalette({ tools, onToggle, onClose }) {
|
||
return (
|
||
<Overlay tag="tools" title="Tool palette" onClose={onClose} width="min(720px,95%)">
|
||
<div style={{ color: "var(--faint)", fontSize: 12, marginBottom: 8 }}>config-driven · capability-scoped. tier sets the approval gate; disabled tools are invisible to the agent.</div>
|
||
{tools.map((t) => (
|
||
<div className="row" key={t.id} style={{ gridTemplateColumns: "auto 1fr auto auto", cursor: "pointer", opacity: t.enabled ? 1 : 0.55 }} onClick={() => onToggle(t.id)}>
|
||
<span className={"tag-tier tier-" + t.tier}>{t.tier}</span>
|
||
<span><b style={{ color: "var(--fg-strong)", fontWeight: 600 }}>{t.id}</b> <span style={{ color: "var(--dim)", fontSize: 12 }}>— {t.desc}</span></span>
|
||
<span className="pill" style={{ color: t.mode === "deny" ? "var(--bad)" : t.mode === "prompt" ? "var(--warn)" : "var(--ok)" }}>{t.mode}</span>
|
||
<span className={"pill " + (t.enabled ? "on" : "off")}>{t.enabled ? "enabled" : "disabled"}</span>
|
||
</div>
|
||
))}
|
||
</Overlay>
|
||
);
|
||
}
|
||
|
||
Object.assign(window, { Overlay, CommandPalette, ApprovalModal, EventInspector, ContextPackInspector, StageGraph, ModelRegistry, ToolPalette });
|