// Live editor + terminal panel that drives the same plan/tool/reflection loop // the @szl/a11oy-code CLI runs. The scripted demo block was previously a // marketing surface; this panel is real — every keystroke flows through // Ouroboros, Lutar, MirrorEval and writes to the in-browser proof ledger. import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { Link } from 'wouter'; import { proof, runTurn, startSession, type ProofEntry, type Session, type Turn, type VirtualFS, } from '../lib/a11oy-code-engine'; const T = { bg: '#0a0a0a', surface: 'rgba(255,255,255,0.025)', border: 'rgba(255,255,255,0.08)', borderStrong: 'rgba(255,255,255,0.12)', text: '#f5f5f5', dim: '#8a8a8a', muted: '#5e5e5e', accent: '#c9b787', good: '#28c840', bad: '#ef4444', mono: "var(--font-mono, ui-monospace, SFMono-Regular, Menlo, monospace)", }; const SEED_FILES: Record = { 'README.md': '# scratch workspace\n\nThis is the in-browser sandbox the /code panel hands to a11oy-code.\nTry: "read README.md", "edit README.md", "lookup formula lutar", "show proof ledger".\n', 'src/eta-calculator.ts': "// ETA calculator — refactor target.\nexport function eta(distanceKm: number, knots: number) {\n return distanceKm / (knots * 1.852);\n}\n", 'package.json': '{\n "name": "scratch",\n "version": "0.0.0"\n}\n', }; interface LineOut { kind: 'sys' | 'usr' | 'agt' | 'gate' | 'div' | 'err'; text: string; } function turnToLines(turn: Turn): LineOut[] { const lines: LineOut[] = []; lines.push({ kind: 'usr', text: `→ ${turn.user}` }); const planSteps = turn.plan.steps.map(s => s.tool).join(' › '); lines.push({ kind: 'agt', text: ` ▶ Plan (${turn.plan.steps.length} step${turn.plan.steps.length === 1 ? '' : 's'}): ${planSteps}` }); if (turn.plan.revised_by) { lines.push({ kind: 'agt', text: ` ▶ Ouroboros revised plan @ ${turn.plan.revised_at?.slice(11, 19)}` }); } lines.push({ kind: 'agt', text: ` ▶ Lutar pick: ${turn.tool.name} score=${turn.tool.score.toFixed(3)}${turn.tool.why ? ` · ${turn.tool.why}` : ''}`, }); if (turn.result.ok === false) { lines.push({ kind: 'err', text: ` ✗ ${turn.tool.name} failed: ${String(turn.result.error ?? 'error')}` }); } else { const r = turn.result; if (r.kind === 'file' && typeof r.content === 'string') { const preview = (r.content as string).split('\n').slice(0, 6).join('\n'); lines.push({ kind: 'agt', text: ` ✓ ${turn.tool.name} → file (${(r.content as string).length} bytes)` }); for (const ln of preview.split('\n')) lines.push({ kind: 'agt', text: ` │ ${ln}` }); } else if (r.kind === 'dir' && Array.isArray(r.entries)) { lines.push({ kind: 'agt', text: ` ✓ ${turn.tool.name} → dir (${(r.entries as string[]).length} entries)` }); for (const e of (r.entries as string[]).slice(0, 8)) lines.push({ kind: 'agt', text: ` · ${e}` }); } else if (typeof r.stdout === 'string') { lines.push({ kind: 'agt', text: ` ✓ ${turn.tool.name} → ${(r.stdout as string).slice(0, 200)}` }); } else if (Array.isArray(r.hits)) { lines.push({ kind: 'agt', text: ` ✓ ${turn.tool.name} → ${(r.hits as unknown[]).map(h => typeof h === 'string' ? h : JSON.stringify(h)).join(', ')}` }); } else if (Array.isArray(r.entries)) { lines.push({ kind: 'agt', text: ` ✓ ${turn.tool.name} → ${(r.entries as unknown[]).length} ledger entries` }); } else { lines.push({ kind: 'agt', text: ` ✓ ${turn.tool.name} → ok` }); } } lines.push({ kind: 'gate', text: ` ⨡ MirrorEval score=${turn.score.toFixed(3)} · proof appended` }); lines.push({ kind: 'div', text: '─'.repeat(72) }); return lines; } const KIND_COLOR: Record = { sys: T.muted, usr: T.text, agt: T.dim, gate: T.accent, div: 'rgba(255,255,255,0.06)', err: T.bad, }; interface Props { chatPath: string } export function A11oyCodeLivePanel({ chatPath }: Props) { const [session, setSession] = useState(null); const [activePath, setActivePath] = useState('README.md'); const [editorBuf, setEditorBuf] = useState(SEED_FILES['README.md']); const [input, setInput] = useState(''); const [lines, setLines] = useState([]); const [busy, setBusy] = useState(false); const [ledger, setLedger] = useState([]); const fsRef = useRef({ files: { ...SEED_FILES } }); const termRef = useRef(null); useEffect(() => { let cancelled = false; void (async () => { const s = await startSession({ provider: 'local-stub', model: 'a11oy-code-web', autonomy: false }); if (cancelled) return; setSession(s); setLines([ { kind: 'sys', text: `a11oy-code web v1.0 — same engine as @szl/a11oy-code CLI` }, { kind: 'sys', text: `session ${s.id} · provider: local-stub · model: a11oy-code-web` }, { kind: 'sys', text: `tools: read, write, edit, shell, git, web_search, hf_search, thesis_lookup, formula_lookup, proof_query, subagent, finish` }, { kind: 'div', text: '─'.repeat(72) }, ]); })(); return () => { cancelled = true; }; }, []); useEffect(() => proof.subscribe(setLedger), []); useEffect(() => { if (termRef.current) termRef.current.scrollTop = termRef.current.scrollHeight; }, [lines]); const sessionLedger = useMemo( () => session ? ledger.filter(e => !e.session || e.session === session.id) : ledger, [ledger, session], ); const onSelectFile = useCallback((p: string) => { fsRef.current.files[activePath] = editorBuf; setActivePath(p); setEditorBuf(fsRef.current.files[p] ?? ''); }, [activePath, editorBuf]); const onSubmit = useCallback(async (e?: React.FormEvent) => { e?.preventDefault(); if (!session || !input.trim() || busy) return; fsRef.current.files[activePath] = editorBuf; const text = input.trim(); setInput(''); setBusy(true); try { const turn = await runTurn(session, text, { fs: fsRef.current }); setLines(prev => [...prev, ...turnToLines(turn)]); // If the engine edited the active file, reflect changes in the editor. const refreshed = fsRef.current.files[activePath]; if (refreshed !== undefined && refreshed !== editorBuf) setEditorBuf(refreshed); // Surface unknown new files in the picker setSession({ ...session }); } catch (err) { setLines(prev => [...prev, { kind: 'err', text: `[turn-error] ${(err as Error).message}` }]); } finally { setBusy(false); } }, [session, input, busy, activePath, editorBuf]); const handoffHref = session ? `${chatPath}?session=${encodeURIComponent(session.id)}&from=code` : chatPath; const fileNames = useMemo(() => Object.keys(fsRef.current.files).sort(), [session, lines]); return (
{/* Editor pane */}
Editor