Files

318 lines
19 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.
// app.jsx — Correx TUI shell: layout, live state, keybind engine, theme + tweaks.
const { useState, useEffect, useRef, useCallback } = React;
const D = window.CORREX;
const { applyTheme, THEMES, ACCENTS } = window.CORREX_THEMES;
const DIR_ORDER = ["boxed", "soft", "bare"];
const ACC_ORDER = ["cyan", "amber", "green", "magenta", "neutral"];
const SPIN = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{
"direction": "boxed",
"accent": "cyan",
"fontSize": 13,
"motion": true,
"scanlines": false
}/*EDITMODE-END*/;
function Panel({ n, title, right, active, className, footer, bodyRef, children }) {
return (
<div className={"panel" + (active ? " active" : "") + (className ? " " + className : "")}>
<div className="panel-h">
{n && <span className="pn">{n}</span>}
<span className="pt">{title}</span>
{right && <span className="ph-r">{right}</span>}
</div>
<div className="panel-b" ref={bodyRef}>{children}</div>
{footer}
</div>
);
}
function App() {
const [t, setTweak] = useTweaks(TWEAK_DEFAULTS);
const rootRef = useRef(null);
const evRef = useRef(null);
const [overlay, setOverlay] = useState(null); // 'cmd'|'events'|'context'|'graph'|'models'|'tools'|'approval'|null
const [focusPanel, setFocusPanel] = useState("conv"); // 'conv'|'events'
const [sessionState, setSessionState] = useState("awaiting_approval");
const [activeModel, setActiveModel] = useState(D.SESSION.model);
const [tools, setTools] = useState(D.TOOLS);
const [autoApprove, setAutoApprove] = useState(false);
const [approvalResolved, setApprovalResolved] = useState(false);
const [msgs, setMsgs] = useState(D.CONVERSATION);
const [events, setEvents] = useState(D.EVENTS);
const [stages, setStages] = useState(D.STAGES);
const [toast, setToast] = useState(null);
const [streamTarget, setStreamTarget] = useState(D.CONVERSATION.length - 1);
const [streamLen, setStreamLen] = useState(0);
const [frame, setFrame] = useState(0);
const [draft, setDraft] = useState("");
const [promptFocus, setPromptFocus] = useState(false);
const promptRef = useRef(null);
const convRef = useRef(null);
// theme application
useEffect(() => { if (rootRef.current) applyTheme(rootRef.current, t.direction, t.accent); }, [t.direction, t.accent]);
// spinner / clock frames
useEffect(() => {
if (!t.motion) return;
const id = setInterval(() => setFrame((f) => f + 1), 110);
return () => clearInterval(id);
}, [t.motion]);
// streaming reveal
useEffect(() => {
if (streamTarget == null) return;
const full = (msgs[streamTarget] || {}).text || "";
if (streamLen >= full.length) return;
if (!t.motion) { setStreamLen(full.length); return; }
const id = setTimeout(() => setStreamLen((n) => Math.min(n + 3, full.length)), 16);
return () => clearTimeout(id);
}, [streamTarget, streamLen, msgs, t.motion]);
// autoscroll event panel
useEffect(() => { if (evRef.current) evRef.current.scrollTop = evRef.current.scrollHeight; }, [events]);
// autoscroll conversation
useEffect(() => { if (convRef.current) convRef.current.scrollTop = convRef.current.scrollHeight; }, [msgs, streamLen]);
const flash = (m) => { setToast(m); setTimeout(() => setToast(null), 2600); };
const pushEvent = (e) => setEvents((arr) => [...arr, { id: "x" + arr.length + Math.random().toString(36).slice(2, 5), ...e }]);
const addMsg = (m) => { setMsgs((arr) => { const ni = arr.length; setStreamTarget(ni); setStreamLen(0); return [...arr, m]; }); };
const appendMsg = (m) => setMsgs((arr) => [...arr, m]);
// user steering input → routes back through Correx
const hm = () => new Date().toTimeString().slice(0, 5);
const replyFor = (q) => {
const s = q.toLowerCase();
if (/test|spec|cover/.test(s)) return "Queued — I'll add SegmentStoreTest with an append→read round-trip and a dedup assertion (identical bytes ⇒ one object). Runs at the validation gate.";
if (/perf|bench|fast|speed|throughput/.test(s)) return "Noted. I'll add a JMH harness for append throughput; the mmap read path stays zero-copy so reads aren't affected by compression.";
if (/revert|undo|stop|cancel|rollback/.test(s)) return "Holding edits. Nothing destructive ran — the gated gradle resolve was the only external op. Awaiting your next instruction.";
if (/compress|zstd|gzip|lz4|codec/.test(s)) return "Compression is pluggable behind a SegmentCodec — zstd for cold, raw for hot. I can swap lz4 for lower CPU at a worse ratio; say the word.";
if (/index|offset|seek|lookup/.test(s)) return "SegmentIndex maps logical offset ranges → CAS keys with a binary search over segment bounds; O(log n) seeks. I'll memo the hot range.";
return "Understood — folding that into the implementation plan. I'll surface a revised ImplementationArtifact at the validation gate for review.";
};
const send = () => {
const text = draft.trim(); if (!text) return;
setDraft("");
setStreamTarget(null);
appendMsg({ role: "user", t: hm(), text });
const at = (ms, fn) => seqRef.current.push(setTimeout(fn, ms));
at(320, () => pushEvent({ t: hm() + ":02", cat: "Context", type: "steer.captured", text: "router → agent context (L1)" }));
at(760, () => addMsg({ role: "kernel", kind: "route", text: "Steering synthesized → injected at L1 · agent context updated" }));
at(1500, () => addMsg({ role: "assistant", t: hm(), text: replyFor(text) }));
};
// resolve approval → drive the live sequence
const seqRef = useRef([]);
const clearSeq = () => { seqRef.current.forEach(clearTimeout); seqRef.current = []; };
useEffect(() => () => clearSeq(), []);
const resolveApproval = useCallback((kind, note) => {
setOverlay(null);
setApprovalResolved(true);
setSessionState("active");
setEvents((arr) => arr.map((e) => e.pending ? { ...e, pending: false } : e));
const at = (ms, fn) => seqRef.current.push(setTimeout(fn, ms));
if (kind === "reject") {
flash("Rejected — agent re-plans without the network tool");
pushEvent({ t: "14:12:31", cat: "Approval", type: "approval.rejected", text: "operator rejected T3 gate" });
at(700, () => pushEvent({ t: "14:12:33", cat: "Inference", type: "infer.start", text: "re-planning · no-network constraint" }));
at(1600, () => addMsg({ role: "assistant", t: "14:12", text: "Understood — dropping zstd. I'll compress cold segments with the JDK Deflater instead: no new dependency, no network. Slightly worse ratio, zero supply-chain surface." }));
at(3400, () => pushEvent({ t: "14:12:48", cat: "Domain", type: "artifact.emitted", text: "ImplementationArtifact ✓ (deflater path)" }));
at(4200, () => transition());
return;
}
// approve / auto / steer
const auto = kind === "auto";
if (auto) setAutoApprove(true);
flash(kind === "steer" ? "Steered + approved — note injected into agent context" : auto ? "Auto-approve enabled for session" : "Approved");
pushEvent({ t: "14:12:14", cat: "Approval", type: kind === "steer" ? "approval.steered" : "approval.approved", text: note ? "“" + note + "”" : "operator approved" + (auto ? " · auto-session ON" : "") });
at(500, () => pushEvent({ t: "14:12:15", cat: "Tool", type: "shell.exec", text: "gradle resolving zstd-jni …", running: true }));
at(2100, () => setEvents((arr) => arr.map((e) => e.running ? { id: e.id, t: "14:12:19", cat: "Tool", type: "shell.exec", text: "gradle resolve OK (4.1s) · +1 dep" } : e)));
at(2600, () => addMsg({ role: "assistant", t: "14:12", text: (note ? "Locking the version as asked. " : "") + "Resolved zstd-jni 1.5.6-3. Cold segments now zstd-compress on write; hot segments stay raw for mmap. Emitting the implementation artifact." }));
at(4600, () => pushEvent({ t: "14:12:34", cat: "Domain", type: "artifact.emitted", text: "ImplementationArtifact ✓ valid" }));
at(5400, () => transition());
}, []);
const transition = () => {
pushEvent({ t: "14:12:36", cat: "Lifecycle", type: "stage.transition", text: "implementation → validation" });
setStages((arr) => arr.map((s) => s.id === "implementation" ? { ...s, status: "done", note: "artifact accepted" } : s.id === "validation" ? { ...s, status: "active", enter: "14:12", note: "schema + semantic checks" } : s));
pushEvent({ t: "14:12:36", cat: "Lifecycle", type: "stage.enter", text: "validation (role=reviewer)" });
};
// auto-open the approval gate once (modal interrupt demonstration)
const raised = useRef(false);
useEffect(() => {
if (raised.current) return; raised.current = true;
const id = setTimeout(() => { if (!approvalResolved) setOverlay("approval"); }, 1100);
return () => clearTimeout(id);
}, []);
// ── keybind engine ──
const cycle = (key, order) => setTweak(key, order[(order.indexOf(t[key]) + 1) % order.length]);
const setDirection = (dir) => setTweak({ direction: dir, accent: THEMES[dir].defaultAccent });
const cycleDir = () => setDirection(DIR_ORDER[(DIR_ORDER.indexOf(t.direction) + 1) % DIR_ORDER.length]);
const open = (id) => {
if (id === "appearance") { cycleDir(); return; }
if (id === "approval") { if (approvalResolved) { flash("No pending approvals"); return; } setOverlay("approval"); return; }
setOverlay(id);
};
useEffect(() => {
const onKey = (e) => {
const typing = /^(INPUT|TEXTAREA)$/.test(document.activeElement && document.activeElement.tagName);
// approval modal chords
if (overlay === "approval") {
if (typing) return;
if (e.key === "a") { resolveApproval("approve"); e.preventDefault(); }
else if (e.key === "A") { resolveApproval("auto"); e.preventDefault(); }
else if (e.key === "r") { resolveApproval("reject"); e.preventDefault(); }
else if (e.key === "Escape") setOverlay(null);
return;
}
if (e.key === "Escape") { if (overlay) { setOverlay(null); e.preventDefault(); } return; }
if (typing) return;
if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === "k") { setOverlay("cmd"); e.preventDefault(); return; }
if (overlay === "cmd") return; // palette owns keys
if (overlay) return; // other overlays: only esc (above)
switch (e.key) {
case "p": case ":": case "/": setOverlay("cmd"); e.preventDefault(); break;
case "e": open("events"); break;
case "c": open("context"); break;
case "g": open("graph"); break;
case "m": open("models"); break;
case "t": open("tools"); break;
case "i": promptRef.current && promptRef.current.focus(); e.preventDefault(); break;
case "a": open("approval"); break;
case "F2": cycleDir(); e.preventDefault(); break;
case "F3": cycle("accent", ACC_ORDER); e.preventDefault(); break;
case "Tab": setFocusPanel((p) => p === "conv" ? "events" : "conv"); e.preventDefault(); break;
default: break;
}
};
window.addEventListener("keydown", onKey);
return () => window.removeEventListener("keydown", onKey);
}, [overlay, approvalResolved, t.direction, t.accent, resolveApproval]);
// ── footer hints (contextual) ──
const hints = (() => {
if (overlay === "approval") return [["a", "approve"], ["A", "auto-session"], ["s", "steer"], ["r", "reject"], ["esc", "later"]];
if (overlay === "cmd") return [["↑↓", "move"], ["↵", "run"], ["esc", "close"]];
if (overlay) return [["↑↓", "navigate"], ["↵", "select"], ["esc", "back"]];
if (promptFocus) return [["↵", "send to correx"], ["esc", "blur"]];
return [["i", "message"], ["p", "commands"], ["a", "approval"], ["e", "events"], ["c", "context"], ["g", "graph"], ["m", "models"], ["t", "tools"], ["F2", "skin"]];
})();
const tokPct = Math.min(100, Math.round((D.CONTEXT_PACK.used / D.CONTEXT_PACK.budget) * 100) + (approvalResolved ? 4 : 0));
const spin = SPIN[frame % SPIN.length];
return (
<div className="crx" ref={rootRef} style={{ fontSize: t.fontSize, "--scan": t.scanlines ? 1 : 0 }}>
{t.scanlines && <div style={{ position: "absolute", inset: 0, zIndex: 30, pointerEvents: "none", background: "repeating-linear-gradient(0deg, rgb(0 0 0 / 0.16) 0 1px, transparent 1px 3px)" }} />}
{/* top status line */}
<div className="crx-top">
<span className="crx-brand">corre<b>x</b></span>
<span className="dot">·</span>
<span className="seg">session <b>{D.SESSION.name}</b></span>
<span className="dot">·</span>
<span className="seg"><b>{D.SESSION.branch}</b></span>
<span className="dot">·</span>
<span className="seg">{activeModel} <span style={{ color: "var(--faint)" }}>({D.MODELS.find((m) => m.id === activeModel).provider})</span></span>
{autoApprove && <span className="pill live">auto-approve</span>}
<span className="crx-spacer" />
<span className="crx-budget">ctx <div className="crx-meter"><i style={{ width: tokPct + "%" }} /></div> {tokPct}%</span>
<span className={"crx-state"} style={sessionState === "active" ? { color: "var(--ok)", borderColor: "color-mix(in oklab,var(--ok),transparent 60%)", background: "color-mix(in oklab,var(--ok),transparent 88%)" } : null}>
{sessionState === "awaiting_approval" ? "⏸ awaiting approval" : "▸ active"}
</span>
</div>
{/* main split */}
<div className="crx-main">
<Panel n="[1]" title="router · conversation" right={D.SESSION.project} active={focusPanel === "conv"} bodyRef={convRef}
footer={
<div className={"prompt" + (promptFocus ? " foc" : "")} onClick={() => promptRef.current && promptRef.current.focus()}>
<span className="pr">{promptFocus ? "" : ""}</span>
<input ref={promptRef} value={draft} placeholder="message correx — steer the run…"
onFocus={() => setPromptFocus(true)} onBlur={() => setPromptFocus(false)}
onChange={(e) => setDraft(e.target.value)}
onKeyDown={(e) => { e.stopPropagation(); if (e.key === "Enter") send(); else if (e.key === "Escape") promptRef.current.blur(); }} />
{!promptFocus && !draft && <span className="ph"><kbd>i</kbd> focus · <kbd></kbd> send</span>}
</div>
}>
{msgs.map((m, i) => {
const streaming = i === streamTarget;
const text = streaming ? m.text.slice(0, streamLen) : m.text;
const who = m.role === "user" ? "you" : m.role === "kernel" ? "correx" : "agent";
return (
<div className={"msg " + m.role} key={i}>
<span className="msg-gut"><span className="who">{who}</span>{m.t}</span>
<span className="msg-body">{text}{streaming && streamLen < m.text.length && <span className="cursor" />}{streaming && streamLen >= m.text.length && sessionState === "awaiting_approval" && <span className="cursor" />}</span>
</div>
);
})}
</Panel>
<Panel n="[2]" title="event stream" active={focusPanel === "events"}
right={sessionState === "awaiting_approval" ? <span style={{ color: "var(--warn)" }}>{spin} paused</span> : <span style={{ color: "var(--ok)" }}>{spin} live</span>}>
<div ref={evRef} style={{ height: "100%", overflow: "auto" }}>
{events.map((e) => (
<div className={"ev" + (e.pending ? " pending" : "")} key={e.id} onClick={() => setOverlay("events")}>
<span className="ev-t">{e.t}</span>
<span className={"ev-cat cat-" + e.cat}>{e.cat}</span>
<span className="ev-tx">{e.running && <span className="spin">{spin} </span>}<b>{e.type}</b> {e.text}{e.pending && <span className="kbd-inline" style={{ marginLeft: 6 }}>a</span>}</span>
</div>
))}
</div>
</Panel>
</div>
{/* footer keybind bar */}
<div className="crx-foot">
{hints.map(([k, l], i) => (
<span className="kb" key={i} onClick={() => {
const map = { p: () => setOverlay("cmd"), a: () => open("approval"), e: () => open("events"), c: () => open("context"), g: () => open("graph"), m: () => open("models"), t: () => open("tools"), i: () => promptRef.current && promptRef.current.focus(), F2: () => cycleDir() };
map[k] && map[k]();
}}>
<kbd>{k}</kbd><b>{l}</b>
</span>
))}
<span className="foot-mode">{THEMES[t.direction].label} · <b>{t.accent}</b></span>
</div>
{toast && <div style={{ position: "absolute", bottom: 46, left: "50%", transform: "translateX(-50%)", zIndex: 50, background: "var(--panel-2)", border: "1px solid var(--accent)", color: "var(--fg-strong)", padding: "8px 16px", borderRadius: 8, fontSize: 12.5, boxShadow: "0 14px 40px -16px #000" }}>{toast}</div>}
{/* overlays */}
{overlay === "cmd" && <CommandPalette commands={D.COMMANDS} onRun={(id) => { setOverlay(null); setTimeout(() => open(id), 0); }} onClose={() => setOverlay(null)} />}
{overlay === "approval" && <ApprovalModal data={D.APPROVAL} onApprove={(auto) => resolveApproval(auto ? "auto" : "approve")} onReject={() => resolveApproval("reject")} onSteer={(n) => resolveApproval("steer", n)} onClose={() => setOverlay(null)} />}
{overlay === "events" && <EventInspector events={events} onClose={() => setOverlay(null)} />}
{overlay === "context" && <ContextPackInspector pack={D.CONTEXT_PACK} onClose={() => setOverlay(null)} />}
{overlay === "graph" && <StageGraph stages={stages} transitions={D.TRANSITIONS} onClose={() => setOverlay(null)} />}
{overlay === "models" && <ModelRegistry models={D.MODELS} active={activeModel} onSwitch={(id) => { setActiveModel(id); setOverlay(null); flash("Switched → " + id + " · re-synthesizing Context Pack"); pushEvent({ t: "14:12", cat: "Context", type: "pack.resynth", text: "for " + id }); }} onClose={() => setOverlay(null)} />}
{overlay === "tools" && <ToolPalette tools={tools} onToggle={(id) => setTools((arr) => arr.map((x) => x.id === id ? { ...x, enabled: !x.enabled } : x))} onClose={() => setOverlay(null)} />}
{/* tweaks */}
<TweaksPanel>
<TweakSection label="Aesthetic direction" />
<TweakRadio label="Chrome" value={t.direction} options={["boxed", "soft", "bare"]} onChange={(v) => setDirection(v)} />
<TweakColor label="Accent" value={t.accent === "cyan" ? "#56c8d8" : t.accent === "amber" ? "#e8b765" : t.accent === "green" ? "#8fd96a" : t.accent === "magenta" ? "#c98fd9" : "#b9c0c9"}
options={["#56c8d8", "#e8b765", "#8fd96a", "#c98fd9", "#b9c0c9"]}
onChange={(hex) => { const map = { "#56c8d8": "cyan", "#e8b765": "amber", "#8fd96a": "green", "#c98fd9": "magenta", "#b9c0c9": "neutral" }; setTweak("accent", map[hex] || "cyan"); }} />
<TweakSection label="Display" />
<TweakSlider label="Font size" value={t.fontSize} min={11} max={17} step={1} unit="px" onChange={(v) => setTweak("fontSize", v)} />
<TweakToggle label="Live motion" value={t.motion} onChange={(v) => setTweak("motion", v)} />
<TweakToggle label="CRT scanlines" value={t.scanlines} onChange={(v) => setTweak("scanlines", v)} />
</TweaksPanel>
</div>
);
}
ReactDOM.createRoot(document.getElementById("root")).render(<App />);