From b328f68352855b2933642bed08dd8bdbfbcf05d5 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Wed, 29 Apr 2026 17:22:46 +0100 Subject: [PATCH] Add usable GUI lane controls --- apps/epiphany-gui/README.md | 13 +- apps/epiphany-gui/scripts/visual-smoke.mjs | 14 + apps/epiphany-gui/src-tauri/src/lib.rs | 123 +++++++- apps/epiphany-gui/src/App.tsx | 178 +++++++++-- apps/epiphany-gui/src/operatorApi.ts | 1 + apps/epiphany-gui/src/types.ts | 13 +- notes/epiphany-fork-implementation-plan.md | 22 +- notes/fresh-workspace-handoff.md | 28 +- state/evidence.jsonl | 1 + state/map.yaml | 15 +- tools/epiphany_gui_action.py | 341 +++++++++++++++++++++ tools/epiphany_mvp_status.py | 2 + 12 files changed, 690 insertions(+), 61 deletions(-) create mode 100644 tools/epiphany_gui_action.py diff --git a/apps/epiphany-gui/README.md b/apps/epiphany-gui/README.md index e93a9856..f66bdfb3 100644 --- a/apps/epiphany-gui/README.md +++ b/apps/epiphany-gui/README.md @@ -8,12 +8,15 @@ artifact state without working through a terminal. Current slice: -- read-only operator console +- operator console over the existing app-server APIs - calls `tools/epiphany_mvp_status.py --json` through a Tauri command - renders coordinator recommendation, pressure, CRRC/reorientation status, role lanes, role findings, jobs, and artifact bundles - writes explicit status-snapshot and coordinator-plan artifact bundles through bounded operator buttons +- prepares durable Epiphany checkpoint state, then launches and reads the fixed + modeling, verification, and reorient lanes from GUI buttons, with reorient + acceptance kept explicit and review-gated Run from this directory: @@ -43,9 +46,15 @@ $env:CARGO_TARGET_DIR='C:\Users\Meta\.cargo-target-codex' cargo build -p codex-app-server ``` -The first bounded buttons are intentionally narrow: +The bounded buttons are intentionally narrow: - **Status Snapshot** writes a status JSON/transcript/stderr bundle under `.epiphany-gui/status-snapshots`. - **Coordinator Plan** runs `tools/epiphany_mvp_coordinator.py --mode plan` and writes an auditable bundle under `.epiphany-dogfood`. +- **Prepare Checkpoint** seeds durable Epiphany state for a GUI operator thread + and returns the persisted thread id that unlocks lane actions. +- **Launch/Read Modeling** and **Launch/Read Verification** call the fixed role + lane APIs and write GUI action bundles under `.epiphany-gui/actions`. +- **Launch/Read/Accept Reorient** calls the fixed reorient-worker APIs. + Acceptance stays disabled until a completed reorient result is present. diff --git a/apps/epiphany-gui/scripts/visual-smoke.mjs b/apps/epiphany-gui/scripts/visual-smoke.mjs index 673b43c0..ef9cffb5 100644 --- a/apps/epiphany-gui/scripts/visual-smoke.mjs +++ b/apps/epiphany-gui/scripts/visual-smoke.mjs @@ -52,11 +52,25 @@ async function smokeViewport(browser, viewport, screenshotPath) { await page.getByText("prepareCheckpoint").waitFor(); await page.getByText("Role Lanes").waitFor(); await page.getByText("Artifact Bundles").waitFor(); + await page.getByRole("button", { name: "Prepare Checkpoint" }).waitFor(); + await page.getByRole("button", { name: "Launch Modeling" }).waitFor(); + await page.getByRole("button", { name: "Read Modeling" }).waitFor(); + await page.getByRole("button", { name: "Launch Verification" }).waitFor(); + await page.getByRole("button", { name: "Read Verification" }).waitFor(); + await page.getByRole("button", { name: "Launch Reorient" }).waitFor(); + await page.getByRole("button", { name: "Read Reorient" }).waitFor(); + await page.getByRole("button", { name: "Accept Reorient" }).waitFor(); if (viewport.width >= 900) { await page.getByRole("button", { name: "Status Snapshot" }).click(); await page.getByText("statusSnapshot sample completed.").waitFor(); await page.getByRole("button", { name: "Coordinator Plan" }).click(); await page.getByText("coordinatorPlan sample completed.").waitFor(); + await page.getByRole("button", { name: "Prepare Checkpoint" }).click(); + await page.getByText("prepareCheckpoint sample completed.").waitFor(); + await page.getByRole("button", { name: "Read Modeling" }).click(); + await page.getByText("readModelingResult sample completed.").waitFor(); + await page.getByRole("button", { name: "Read Reorient" }).click(); + await page.getByText("readReorientResult sample completed.").waitFor(); } await page.screenshot({ path: screenshotPath, fullPage: true }); diff --git a/apps/epiphany-gui/src-tauri/src/lib.rs b/apps/epiphany-gui/src-tauri/src/lib.rs index 608f1c0b..74d61e0e 100644 --- a/apps/epiphany-gui/src-tauri/src/lib.rs +++ b/apps/epiphany-gui/src-tauri/src/lib.rs @@ -20,6 +20,7 @@ struct OperatorActionResult { action: String, artifact_path: String, summary: String, + thread_id: Option, } #[derive(Debug, Serialize)] @@ -66,6 +67,14 @@ fn run_operator_action( match action.as_str() { "statusSnapshot" => run_status_snapshot(&repo_root, request), "coordinatorPlan" => run_coordinator_plan(&repo_root, request), + "launchModeling" + | "readModelingResult" + | "launchVerification" + | "readVerificationResult" + | "launchReorient" + | "readReorientResult" + | "acceptReorient" + | "prepareCheckpoint" => run_gui_action_bridge(&repo_root, request, action), _ => Err(format!("unknown operator action: {action}")), } } @@ -100,7 +109,8 @@ fn load_status(repo_root: &Path, request: StatusRequest) -> Result Result { + let thread_id = request.thread_id.clone(); + let python = find_python()?; + let artifact_root = repo_root.join(".epiphany-gui").join("actions"); + let workspace = request + .cwd + .map(PathBuf::from) + .unwrap_or_else(|| repo_root.to_path_buf()); + let codex_home = request + .codex_home + .map(PathBuf::from) + .unwrap_or_else(|| repo_root.join(".epiphany-gui").join("codex-home")); + + let mut command = Command::new(python); + command + .current_dir(repo_root) + .arg(repo_root.join("tools").join("epiphany_gui_action.py")) + .arg("--action") + .arg(&action) + .arg("--cwd") + .arg(workspace) + .arg("--codex-home") + .arg(codex_home) + .arg("--artifact-root") + .arg(artifact_root); + if let Some(thread_id) = thread_id { + command.arg("--thread-id").arg(thread_id); + } + if let Some(app_server) = request.app_server { + command.arg("--app-server").arg(app_server); + } + let value = run_json_command(command, &action)?; + Ok(OperatorActionResult { + action, + artifact_path: json_string(&value, "artifactPath")?, + summary: json_string(&value, "summary")?, + thread_id: value + .get("threadId") + .and_then(Value::as_str) + .map(ToString::to_string), }) } @@ -232,13 +292,53 @@ fn run_command(mut command: Command, label: &str) -> Result<(), String> { Err(format!("{label} exited with {}: {}", output.status, stderr)) } +fn run_json_command(mut command: Command, label: &str) -> Result { + let output = command + .output() + .map_err(|err| format!("failed to run {label}: {err}"))?; + if !output.status.success() { + let stderr = String::from_utf8_lossy(&output.stderr); + return Err(format!("{label} exited with {}: {}", output.status, stderr)); + } + serde_json::from_slice(&output.stdout) + .map_err(|err| format!("failed to parse {label} JSON: {err}")) +} + +fn json_string(value: &Value, key: &str) -> Result { + value + .get(key) + .and_then(Value::as_str) + .map(ToString::to_string) + .ok_or_else(|| format!("missing string field in GUI action result: {key}")) +} + fn list_artifacts(repo_root: &Path) -> Result, String> { - let root = repo_root.join(".epiphany-dogfood"); + let mut bundles = Vec::new(); + collect_artifact_root(&mut bundles, &repo_root.join(".epiphany-dogfood"), "")?; + collect_artifact_root( + &mut bundles, + &repo_root.join(".epiphany-gui").join("actions"), + "actions/", + )?; + collect_artifact_root( + &mut bundles, + &repo_root.join(".epiphany-gui").join("status-snapshots"), + "status/", + )?; + + bundles.sort_by(|a, b| b.modified_millis.cmp(&a.modified_millis)); + Ok(bundles) +} + +fn collect_artifact_root( + bundles: &mut Vec, + root: &Path, + name_prefix: &str, +) -> Result<(), String> { if !root.exists() { - return Ok(Vec::new()); + return Ok(()); } - let mut bundles = Vec::new(); for entry in fs::read_dir(&root).map_err(|err| format!("failed to read artifacts: {err}"))? { let entry = entry.map_err(|err| format!("failed to read artifact entry: {err}"))?; let path = entry.path(); @@ -255,24 +355,27 @@ fn list_artifacts(repo_root: &Path) -> Result, String> { } } files.sort(); + let raw_name = entry.file_name().to_string_lossy().to_string(); let modified_millis = entry .metadata() .ok() .and_then(|metadata| metadata.modified().ok()) .and_then(system_time_millis); bundles.push(ArtifactBundle { - name: entry.file_name().to_string_lossy().to_string(), + name: format!("{name_prefix}{raw_name}"), path: path.display().to_string(), - summary_path: existing_path(&path, "epiphany-dogfood-summary.json"), - final_status_path: existing_path(&path, "epiphany-final-status.json"), + summary_path: existing_path(&path, "epiphany-dogfood-summary.json") + .or_else(|| existing_path(&path, "gui-action-summary.json")) + .or_else(|| existing_path(&path, "status.json")), + final_status_path: existing_path(&path, "epiphany-final-status.json") + .or_else(|| existing_path(&path, "after-status.json")), comparison_path: existing_path(&path, "comparison.md"), files, modified_millis, }); } - bundles.sort_by(|a, b| b.modified_millis.cmp(&a.modified_millis)); - Ok(bundles) + Ok(()) } fn existing_path(root: &Path, name: &str) -> Option { diff --git a/apps/epiphany-gui/src/App.tsx b/apps/epiphany-gui/src/App.tsx index 6669c59b..e70415f2 100644 --- a/apps/epiphany-gui/src/App.tsx +++ b/apps/epiphany-gui/src/App.tsx @@ -1,9 +1,113 @@ -import { AlertTriangle, BriefcaseBusiness, ClipboardCheck, Database, FileText, GitBranch, RefreshCw } from "lucide-react"; +import { + AlertTriangle, + BriefcaseBusiness, + CheckCircle2, + ClipboardCheck, + Database, + Eye, + FileText, + GitBranch, + Play, + RefreshCw, +} from "lucide-react"; import { useEffect, useMemo, useState } from "react"; import { loadOperatorSnapshot, runOperatorAction } from "./operatorApi"; import type { ArtifactBundle, OperatorAction, OperatorActionResult, OperatorSnapshot, StatusRequest } from "./types"; const roleOrder = ["implementation", "modeling", "verification", "reorientation"]; +const actionButtons: Array<{ + action: OperatorAction; + label: string; + runningLabel: string; + title: string; + requiresThread?: boolean; + requiresReadyState?: boolean; + requiresReorientResult?: boolean; + icon: "file" | "check" | "play" | "eye" | "accept"; +}> = [ + { + action: "statusSnapshot", + label: "Status Snapshot", + runningLabel: "Writing", + title: "Write an auditable status snapshot", + icon: "file", + }, + { + action: "coordinatorPlan", + label: "Coordinator Plan", + runningLabel: "Running", + title: "Run a review-gated coordinator plan", + icon: "check", + }, + { + action: "prepareCheckpoint", + label: "Prepare Checkpoint", + runningLabel: "Preparing", + title: "Seed durable Epiphany state for this GUI operator thread", + icon: "accept", + }, + { + action: "launchModeling", + label: "Launch Modeling", + runningLabel: "Launching", + title: "Launch the fixed modeling/checkpoint worker for this thread", + requiresThread: true, + requiresReadyState: true, + icon: "play", + }, + { + action: "readModelingResult", + label: "Read Modeling", + runningLabel: "Reading", + title: "Read the latest modeling/checkpoint finding", + requiresThread: true, + icon: "eye", + }, + { + action: "launchVerification", + label: "Launch Verification", + runningLabel: "Launching", + title: "Launch the fixed verification/review worker for this thread", + requiresThread: true, + requiresReadyState: true, + icon: "play", + }, + { + action: "readVerificationResult", + label: "Read Verification", + runningLabel: "Reading", + title: "Read the latest verification/review finding", + requiresThread: true, + icon: "eye", + }, + { + action: "launchReorient", + label: "Launch Reorient", + runningLabel: "Launching", + title: "Launch the bounded reorient-worker for this thread", + requiresThread: true, + requiresReadyState: true, + icon: "play", + }, + { + action: "readReorientResult", + label: "Read Reorient", + runningLabel: "Reading", + title: "Read the latest reorient-worker finding", + requiresThread: true, + icon: "eye", + }, + { + action: "acceptReorient", + label: "Accept Reorient", + runningLabel: "Accepting", + title: "Accept a completed reorient-worker finding into Epiphany state", + requiresThread: true, + requiresReadyState: true, + requiresReorientResult: true, + icon: "accept", + }, +]; function text(value: unknown, fallback = "none"): string { if (value === null || value === undefined || value === "") { @@ -44,6 +148,16 @@ function useSnapshot() { try { const result = await loadOperatorSnapshot(nextRequest); setSnapshot(result); + const loadedThreadId = result.status?.threadId; + const loadedState = result.status?.scene?.scene?.stateStatus; + if ( + !nextRequest.threadId && + loadedState !== "missing" && + typeof loadedThreadId === "string" && + loadedThreadId.length > 0 + ) { + setRequest((current) => (current.threadId ? current : { ...current, threadId: loadedThreadId })); + } } catch (err) { setError(err instanceof Error ? err.message : String(err)); } finally { @@ -58,7 +172,11 @@ function useSnapshot() { try { const result = await runOperatorAction(action, request); setActionResult(result); - await refresh(request); + const nextRequest = result.threadId ? { ...request, threadId: result.threadId } : request; + if (result.threadId) { + setRequest(nextRequest); + } + await refresh(nextRequest); } catch (err) { setError(err instanceof Error ? err.message : String(err)); } finally { @@ -90,6 +208,9 @@ export function App() { const jobs: any[] = Array.isArray(status?.jobs?.jobs) ? status.jobs.jobs : []; const roleResults = status?.roleResults ?? {}; const reorientResult = status?.reorientResult ?? {}; + const readyState = scene.stateStatus === "ready"; + const currentThreadId = request.threadId; + const canAcceptReorient = text(reorientResult?.status).toLowerCase() === "completed"; return (
@@ -108,7 +229,7 @@ export function App() {