Files

244 lines
13 KiB
React
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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 });