feat: ship agent-first mission control v0.2.57
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
fund=false
|
||||
audit=false
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"d1": null,
|
||||
"r2": null
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
# Prototype Instructions
|
||||
|
||||
Run the local server yourself and open the preview in the browser available to this environment. Do not give the user server-start instructions when you can run it.
|
||||
|
||||
Before making substantial visual changes, use the Product Design plugin's `get-context` skill when the visual source is unclear or no longer matches the current goal. When the user gives durable prototype-specific design feedback, preferences, or decisions, record them in `AGENTS.md`.
|
||||
|
||||
When implementing from a selected generated mock, treat that image as the source of truth for layout, component anatomy, density, spacing, color, typography, visible content, and hierarchy.
|
||||
|
||||
Build app UI in `src/`. Keep `.openai/hosting.json`, `worker/index.js`, `scripts/prepare-sites-build.mjs`, and `tests/sites-worker.test.mjs` intact so the same local prototype can be handed to Sites. Before a Sites handoff, run `npm run build` and `npm run test:sites`; the build must leave `dist/client/index.html`, `dist/server/index.js`, and `dist/.openai/hosting.json`.
|
||||
|
||||
## Selected Nexus direction
|
||||
|
||||
- Source visual: `reference/selected-option-2.png`, the second displayed
|
||||
Run-Control concept selected on 2026-07-27.
|
||||
- This is a desktop-only controlled prototype. Do not create a mobile view,
|
||||
phone shell, mobile breakpoint, or standalone Mobile Chat destination.
|
||||
- Preserve the selected spatial-orchestration composition and the existing
|
||||
Nexus Galaxy/Glass design contract.
|
||||
- Agent nodes, approval steps, event rows, and provider/session metadata must
|
||||
open useful detail views.
|
||||
- Keep all behavior authored and local. Do not call Nexus, OpenClaw, OpenAI, or
|
||||
another backend, and label the surface as mock data.
|
||||
@@ -0,0 +1,36 @@
|
||||
# Nexus Run Control — Interactive Mockup
|
||||
|
||||
Controlled desktop prototype based on the second selected Run Control concept.
|
||||
It is intentionally isolated from the production Nexus frontend and performs no
|
||||
Nexus, OpenClaw, OpenAI, authentication, persistence, or network operations.
|
||||
|
||||
## Prototype scope
|
||||
|
||||
- Spatial Iris-led run topology
|
||||
- Clickable Iris, Programmer, Reviewer, and Executor nodes
|
||||
- Clickable run-event details
|
||||
- Clickable OpenClaw session and OpenAI-via-OpenClaw routing details
|
||||
- Human-approval review, approval success, and reversible mock decision
|
||||
- Pause, resume, stop, and restart mock states
|
||||
- Local Ask Iris dialog with an explicitly authored response
|
||||
- Desktop-only layout; no Mobile Chat route or mobile interface
|
||||
|
||||
## Evidence level
|
||||
|
||||
This artifact is a controlled browser prototype with authored fixture data. It
|
||||
does not prove a live Gateway, provider connection, agent execution, cost,
|
||||
token count, tool call, approval, or production mutation.
|
||||
|
||||
The selected source visual is
|
||||
[`reference/selected-option-2.png`](reference/selected-option-2.png).
|
||||
|
||||
## Local preview
|
||||
|
||||
```powershell
|
||||
npm install
|
||||
npm run dev -- --host 0.0.0.0 --port 4174 --strictPort
|
||||
```
|
||||
|
||||
The verified visual and interaction report is in
|
||||
[`design-qa.md`](design-qa.md), with the final side-by-side comparison under
|
||||
[`evidence/comparison-final.png`](evidence/comparison-final.png).
|
||||
@@ -0,0 +1,63 @@
|
||||
# Structural proof preflight
|
||||
|
||||
## Product contract
|
||||
|
||||
- **Surface:** desktop operational product UI
|
||||
- **Selected source:** `reference/selected-option-2.png`
|
||||
- **Concept sentence:** For an owner supervising an OpenClaw run, Run Control
|
||||
should feel like live mission instrumentation by combining an authoritative
|
||||
agent topology with one connected decision inspector, while avoiding a
|
||||
dashboard inventory of unrelated cards.
|
||||
- **Evidence boundary:** authored controlled prototype; no backend or provider
|
||||
request
|
||||
- **Supported width:** 1100 CSS px and wider; primary proof at `1440 x 1024`
|
||||
- **Explicit exclusion:** no mobile layout and no standalone Mobile Chat page
|
||||
|
||||
## Entry mode
|
||||
|
||||
`task-first`. The selected run name, runtime state, session reference, active
|
||||
agent path, and pending approval are visible before the first material action.
|
||||
The first material action is `Review approval`; it opens a connected dialog
|
||||
without changing the mock decision.
|
||||
|
||||
## Section grammar
|
||||
|
||||
| Section | Role | User question | Grammar |
|
||||
|---|---|---|---|
|
||||
| Sidebar/topbar | Orient | Where am I and which run is active? | fixed navigation and compact run metadata |
|
||||
| Status strip | Orient | Is anything waiting or blocked? | one lightweight status row |
|
||||
| Agent topology | Explain | Who is doing what, and what depends on whom? | spatial nodes with one highlighted execution path |
|
||||
| Event rail | Prove | What happened and where did the state come from? | chronological rows opening event details |
|
||||
| Inspector | Explain/Act | What is selected and what can I do safely? | one persistent context rail |
|
||||
| Approval dialog | Act | What exactly am I approving? | scope, impact, immutable context, explicit decision |
|
||||
| Toast/recovery | Recover | What changed and can I reverse the mock action? | connected status plus visible undo/restart |
|
||||
|
||||
## Signature gesture
|
||||
|
||||
The active topology path is the single dominant gesture. A restrained glow may
|
||||
echo on the selected node and primary approval action. Event rows, dense
|
||||
metadata, error/recovery copy, and modal body text must remain plain and
|
||||
high-contrast.
|
||||
|
||||
## State and copy truth table
|
||||
|
||||
| State | Visible facts | Allowed action | Forbidden claim | Recovery/focus |
|
||||
|---|---|---|---|---|
|
||||
| Pending approval | Executor waiting; scope not yet approved | inspect agents/events/session/provider; review approval | deployment started | dialog close restores trigger focus |
|
||||
| Approval dialog | exact authored scope and no-production-mutation impact | approve, request changes, close | live policy check | close returns to approval trigger |
|
||||
| Approved | decision recorded; Executor preparing artifacts | inspect; undo mock decision | deployment executed | undo restores pending state |
|
||||
| Paused | run overlay and paused label | resume; inspect details | agents advancing | Resume in topbar |
|
||||
| Stopped | stopped overlay and recovery card | inspect; restart mock run | runtime still active | Restart restores pending approval |
|
||||
| Event detail | actor, time, correlation, source | close | live trace evidence | close restores event-row focus |
|
||||
| Provider/session detail | authored connected state and explicit no-request copy | close | verified live provider or secret visibility | close restores metadata-row focus |
|
||||
| Ask Iris | explicit mock label and authored response | submit local text, close | live model response | close restores topbar trigger |
|
||||
|
||||
## Proof selectors and settle condition
|
||||
|
||||
- Primary canvas: `[data-testid="run-canvas"]`
|
||||
- Executor node: `[data-testid="agent-executor"]`
|
||||
- Approval node: `[data-testid="approval-node"]`
|
||||
- Review trigger: `[data-testid="review-approval"]`
|
||||
- Confirmation: `[data-testid="confirm-approval"]`
|
||||
- Settle condition: local fonts loaded, the canvas is visible, no dialog
|
||||
transition is pending, and the selected node geometry is stable.
|
||||
@@ -0,0 +1,60 @@
|
||||
# Design QA — Run Control Option 2
|
||||
|
||||
## Comparison contract
|
||||
|
||||
- Source: `reference/selected-option-2.png`
|
||||
- Source dimensions: `1487 x 1058`
|
||||
- Implementation: `evidence/implementation-final-1440.png`
|
||||
- Implementation viewport: `1440 x 1024`, CSS pixels, desktop density
|
||||
- State: running, Executor selected, approval pending
|
||||
- Full-view comparison: `evidence/comparison-final.png`
|
||||
- Initial comparison: `evidence/comparison-initial.png`
|
||||
- Scope: complete desktop viewport; no mobile state exists by design
|
||||
|
||||
The source and implementation have the same aspect ratio to rounding. The
|
||||
combined comparison normalizes both views to the same rendered dimensions.
|
||||
|
||||
## Iteration record
|
||||
|
||||
The initial comparison identified P2 mismatches in shell proportions, the
|
||||
full-width status strip, inspector hierarchy, event density, and galaxy
|
||||
contrast. The implementation was corrected by:
|
||||
|
||||
- reducing the sidebar and matching the source workspace/inspector ratio;
|
||||
- constraining the status strip and removing the large canvas border;
|
||||
- replacing the generic Executor agent facts with source-aligned step details;
|
||||
- fitting all six authoritative event rows into the visible rail;
|
||||
- strengthening the galaxy field and inspector depth;
|
||||
- preserving the selected source's blue-violet hierarchy and restrained glow.
|
||||
|
||||
## Final assessment
|
||||
|
||||
- P0: none
|
||||
- P1: none
|
||||
- P2: none
|
||||
- P3: the prototype names the owner Bao rather than the generated source's
|
||||
placeholder identity, and `Review approval` intentionally opens a detail
|
||||
dialog before the reversible mock decision.
|
||||
- Layout: no clipping or unintended horizontal overflow at `1440 x 1024`.
|
||||
- Typography: bundled Manrope, Space Grotesk, and JetBrains Mono load locally.
|
||||
- Accessibility: controls have accessible names, modal focus is restored,
|
||||
Escape closes dialogs, and focus-visible states are present.
|
||||
- Browser console: no warnings or errors after the complete interaction path.
|
||||
|
||||
## Interaction proof
|
||||
|
||||
Passed in the in-app browser:
|
||||
|
||||
1. Agent node to persistent detail inspector.
|
||||
2. Event row to correlation/source detail.
|
||||
3. OpenClaw session and OpenAI-via-OpenClaw routing details.
|
||||
4. Approval review, approve, updated Executor state, and undo.
|
||||
5. Ask Iris local authored response and Escape/focus restoration.
|
||||
6. Pause/resume and stop/restart recovery.
|
||||
|
||||
## Automated proof
|
||||
|
||||
- `npm run test:sites`: 4/4 passed.
|
||||
- `npm run build`: passed; 1807 modules transformed.
|
||||
|
||||
Result: passed
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 760 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 749 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 113 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 116 KiB |
@@ -0,0 +1,12 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Prototype</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.jsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
+1706
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"name": "run-control-option-2",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build && node scripts/prepare-sites-build.mjs",
|
||||
"preview": "vite preview",
|
||||
"test:sites": "node --test tests/sites-worker.test.mjs"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fontsource/jetbrains-mono": "^5.3.0",
|
||||
"@fontsource/manrope": "^5.3.0",
|
||||
"@fontsource/space-grotesk": "^5.3.0",
|
||||
"@vitejs/plugin-react": "5.0.4",
|
||||
"lucide-react": "^1.27.0",
|
||||
"react": "19.2.0",
|
||||
"react-dom": "19.2.0",
|
||||
"vite": "6.4.2"
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 1.4 MiB |
@@ -0,0 +1,21 @@
|
||||
#!/usr/bin/env node
|
||||
import { copyFileSync, existsSync, mkdirSync } from "node:fs";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
||||
const dist = path.join(root, "dist");
|
||||
const index = path.join(dist, "client", "index.html");
|
||||
const worker = path.join(root, "worker", "index.js");
|
||||
const hosting = path.join(root, ".openai", "hosting.json");
|
||||
|
||||
for (const file of [index, worker, hosting]) {
|
||||
if (!existsSync(file)) throw new Error("Missing Sites build input: " + file);
|
||||
}
|
||||
|
||||
mkdirSync(path.join(dist, "server"), { recursive: true });
|
||||
mkdirSync(path.join(dist, ".openai"), { recursive: true });
|
||||
copyFileSync(worker, path.join(dist, "server", "index.js"));
|
||||
copyFileSync(hosting, path.join(dist, ".openai", "hosting.json"));
|
||||
|
||||
console.log("Prepared Sites build: dist/server/index.js and dist/.openai/hosting.json");
|
||||
@@ -0,0 +1,966 @@
|
||||
import { useMemo, useRef, useState } from "react";
|
||||
import {
|
||||
Activity,
|
||||
ArrowRight,
|
||||
BadgeCheck,
|
||||
Bell,
|
||||
BookOpen,
|
||||
Bot,
|
||||
Boxes,
|
||||
Brain,
|
||||
CalendarDays,
|
||||
Check,
|
||||
CheckCircle2,
|
||||
ChevronRight,
|
||||
CircleDollarSign,
|
||||
Clock3,
|
||||
Code2,
|
||||
Cpu,
|
||||
FileCheck2,
|
||||
FolderKanban,
|
||||
History,
|
||||
KeyRound,
|
||||
LayoutDashboard,
|
||||
Link2,
|
||||
ListTodo,
|
||||
MessageSquareText,
|
||||
Network,
|
||||
Pause,
|
||||
Play,
|
||||
RotateCcw,
|
||||
Settings,
|
||||
ShieldCheck,
|
||||
SlidersHorizontal,
|
||||
Sparkles,
|
||||
Square,
|
||||
Terminal,
|
||||
TriangleAlert,
|
||||
UserRound,
|
||||
Wrench,
|
||||
X,
|
||||
} from "lucide-react";
|
||||
import { Modal } from "./Modal.jsx";
|
||||
import {
|
||||
agents,
|
||||
approval,
|
||||
navGroups,
|
||||
runDetails,
|
||||
timelineEvents,
|
||||
} from "./data.js";
|
||||
|
||||
const iconMap = {
|
||||
dashboard: LayoutDashboard,
|
||||
runs: Activity,
|
||||
agents: Bot,
|
||||
projects: FolderKanban,
|
||||
tasks: ListTodo,
|
||||
memory: Brain,
|
||||
docs: BookOpen,
|
||||
models: SlidersHorizontal,
|
||||
activity: Activity,
|
||||
calendar: CalendarDays,
|
||||
security: ShieldCheck,
|
||||
incidents: TriangleAlert,
|
||||
notifications: Bell,
|
||||
iris: Sparkles,
|
||||
programmer: Code2,
|
||||
reviewer: FileCheck2,
|
||||
executor: Terminal,
|
||||
};
|
||||
|
||||
function Sidebar({ onPrototypeNav }) {
|
||||
return (
|
||||
<aside className="sidebar" aria-label="Nexus navigation">
|
||||
<div className="brand">
|
||||
<span className="brand-mark" aria-hidden="true">
|
||||
<Sparkles size={22} />
|
||||
</span>
|
||||
<span>
|
||||
<strong>NEXUS</strong>
|
||||
<small>Mission Control</small>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<nav className="navigation">
|
||||
{navGroups.map((group) => (
|
||||
<section className="nav-group" key={group.label}>
|
||||
<h2>{group.label}</h2>
|
||||
<div className="nav-list">
|
||||
{group.items.map((item) => {
|
||||
const Icon = iconMap[item.icon];
|
||||
return (
|
||||
<button
|
||||
className={`nav-item${item.active ? " is-active" : ""}`}
|
||||
type="button"
|
||||
key={item.id}
|
||||
aria-current={item.active ? "page" : undefined}
|
||||
onClick={() => onPrototypeNav(item.label, item.active)}
|
||||
>
|
||||
<Icon size={17} strokeWidth={1.8} aria-hidden="true" />
|
||||
<span>{item.label}</span>
|
||||
{item.count !== undefined ? (
|
||||
<span className={`nav-count${item.count > 0 ? " has-items" : ""}`}>
|
||||
{item.count}
|
||||
</span>
|
||||
) : null}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</section>
|
||||
))}
|
||||
</nav>
|
||||
|
||||
<div className="sidebar-footer">
|
||||
<button
|
||||
className="nav-item"
|
||||
type="button"
|
||||
onClick={() => onPrototypeNav("Settings", false)}
|
||||
>
|
||||
<Settings size={17} strokeWidth={1.8} aria-hidden="true" />
|
||||
<span>Settings</span>
|
||||
</button>
|
||||
<button
|
||||
className="owner"
|
||||
type="button"
|
||||
onClick={() => onPrototypeNav("Owner profile", false)}
|
||||
>
|
||||
<span className="owner-avatar">B</span>
|
||||
<span>
|
||||
<strong>Bao</strong>
|
||||
<small>Owner</small>
|
||||
</span>
|
||||
<ChevronRight size={16} aria-hidden="true" />
|
||||
</button>
|
||||
</div>
|
||||
</aside>
|
||||
);
|
||||
}
|
||||
|
||||
function Topbar({ runState, onPause, onStop, onAskIris }) {
|
||||
const stateLabel =
|
||||
runState === "paused" ? "Paused" : runState === "stopped" ? "Stopped" : "Running";
|
||||
|
||||
return (
|
||||
<header className="topbar">
|
||||
<div className="run-identity">
|
||||
<span className={`runtime-dot runtime-dot--${runState}`} aria-hidden="true" />
|
||||
<span>
|
||||
<small>Run</small>
|
||||
<strong>{runDetails.title}</strong>
|
||||
</span>
|
||||
<span className={`run-state run-state--${runState}`}>{stateLabel}</span>
|
||||
</div>
|
||||
|
||||
<dl className="topbar-meta">
|
||||
<div>
|
||||
<dt>Elapsed</dt>
|
||||
<dd>{runDetails.elapsed}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Started</dt>
|
||||
<dd>{runDetails.started}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Session</dt>
|
||||
<dd>{runDetails.sessionId}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
|
||||
<div className="topbar-actions">
|
||||
<button
|
||||
className="button button--secondary"
|
||||
type="button"
|
||||
onClick={onPause}
|
||||
disabled={runState === "stopped"}
|
||||
>
|
||||
{runState === "paused" ? (
|
||||
<Play size={17} aria-hidden="true" />
|
||||
) : (
|
||||
<Pause size={17} aria-hidden="true" />
|
||||
)}
|
||||
{runState === "paused" ? "Resume" : "Pause"}
|
||||
</button>
|
||||
<button
|
||||
className="button button--danger"
|
||||
type="button"
|
||||
onClick={onStop}
|
||||
disabled={runState === "stopped"}
|
||||
>
|
||||
<Square size={15} aria-hidden="true" />
|
||||
Stop
|
||||
</button>
|
||||
<button className="button button--primary" type="button" onClick={onAskIris}>
|
||||
<Sparkles size={17} aria-hidden="true" />
|
||||
Ask Iris
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
|
||||
function AgentNode({ agent, selected, approvalState, onSelect }) {
|
||||
const Icon = iconMap[agent.icon];
|
||||
const status =
|
||||
agent.id === "executor" && approvalState === "approved"
|
||||
? "Preparing artifacts"
|
||||
: agent.status;
|
||||
const progress =
|
||||
agent.id === "executor" && approvalState === "approved" ? 16 : agent.progress;
|
||||
const state =
|
||||
agent.id === "executor" && approvalState === "approved" ? "working" : agent.state;
|
||||
|
||||
return (
|
||||
<button
|
||||
className={`agent-node agent-node--${agent.id} state-${state}${
|
||||
selected ? " is-selected" : ""
|
||||
}`}
|
||||
type="button"
|
||||
aria-pressed={selected}
|
||||
aria-label={`Open ${agent.name} details`}
|
||||
onClick={() => onSelect(agent.id)}
|
||||
data-testid={`agent-${agent.id}`}
|
||||
>
|
||||
<span className="node-heading">
|
||||
<span className="agent-icon">
|
||||
<Icon size={18} strokeWidth={1.8} aria-hidden="true" />
|
||||
</span>
|
||||
<span className="node-title">
|
||||
<strong>{agent.name}</strong>
|
||||
<small>{agent.role}</small>
|
||||
</span>
|
||||
<span className={`status-dot status-dot--${state}`} aria-hidden="true" />
|
||||
</span>
|
||||
<span className="node-status">{status}</span>
|
||||
<span className="progress-track" aria-hidden="true">
|
||||
<span className="progress-value" style={{ width: `${progress}%` }} />
|
||||
</span>
|
||||
<span className="node-footer">
|
||||
<span>{state === "completed" ? "Completed" : state === "waiting" ? "Waiting" : "Active"}</span>
|
||||
<span>{progress}%</span>
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
function OrchestrationMap({
|
||||
selectedAgent,
|
||||
approvalState,
|
||||
runState,
|
||||
onSelectAgent,
|
||||
onOpenApproval,
|
||||
}) {
|
||||
return (
|
||||
<section
|
||||
className={`orchestration-map run-${runState}`}
|
||||
aria-label="Live agent orchestration"
|
||||
data-testid="run-canvas"
|
||||
>
|
||||
<div className="connection connection--iris-programmer" aria-hidden="true" />
|
||||
<div className="connection connection--iris-reviewer" aria-hidden="true" />
|
||||
<div className="connection connection--iris-executor" aria-hidden="true" />
|
||||
<div className="connection connection--executor-approval" aria-hidden="true" />
|
||||
<div className="connection connection--approval-deploy" aria-hidden="true" />
|
||||
|
||||
{Object.values(agents).map((agent) => (
|
||||
<AgentNode
|
||||
agent={agent}
|
||||
key={agent.id}
|
||||
selected={selectedAgent === agent.id}
|
||||
approvalState={approvalState}
|
||||
onSelect={onSelectAgent}
|
||||
/>
|
||||
))}
|
||||
|
||||
<button
|
||||
className={`approval-node approval-node--${approvalState}`}
|
||||
type="button"
|
||||
onClick={onOpenApproval}
|
||||
data-testid="approval-node"
|
||||
>
|
||||
<span className="approval-kicker">
|
||||
{approvalState === "pending" ? "Pending approval" : "Approval granted"}
|
||||
</span>
|
||||
<strong>{approval.title}</strong>
|
||||
<small>
|
||||
{approvalState === "pending"
|
||||
? "Owner decision required before Executor continues."
|
||||
: "Executor may prepare versioned deployment artifacts."}
|
||||
</small>
|
||||
<span className="approval-meta">
|
||||
<span>{approval.requestedBy}</span>
|
||||
<span>{approvalState === "pending" ? "Review details" : "View decision"}</span>
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<div className={`deploy-node deploy-node--${approvalState}`}>
|
||||
<span>{approvalState === "pending" ? "Pending" : "In progress"}</span>
|
||||
<strong>Prepare deployment</strong>
|
||||
<small>
|
||||
{approvalState === "pending" ? "Awaiting owner approval" : "Executor started"}
|
||||
</small>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
function EventRail({ approvalState, onOpenEvent }) {
|
||||
const visibleEvents = useMemo(() => {
|
||||
if (approvalState === "approved") {
|
||||
return [
|
||||
...timelineEvents,
|
||||
{
|
||||
id: "event-approved",
|
||||
time: "17:21:12",
|
||||
actor: "Bao",
|
||||
title: "Approval granted",
|
||||
detail: "Executor may prepare deployment artifacts.",
|
||||
type: "success",
|
||||
correlation: "evt_01JZR7K0B4",
|
||||
},
|
||||
];
|
||||
}
|
||||
return timelineEvents;
|
||||
}, [approvalState]);
|
||||
|
||||
return (
|
||||
<section className="event-rail" aria-labelledby="event-rail-title">
|
||||
<header>
|
||||
<div>
|
||||
<p className="eyebrow">Authoritative mock timeline</p>
|
||||
<h2 id="event-rail-title">Run events</h2>
|
||||
</div>
|
||||
<span>{visibleEvents.length} events</span>
|
||||
</header>
|
||||
<div className="event-list">
|
||||
{visibleEvents.map((event) => (
|
||||
<button
|
||||
type="button"
|
||||
className={`event-row event-row--${event.type}`}
|
||||
key={event.id}
|
||||
onClick={() => onOpenEvent(event)}
|
||||
data-testid={`event-${event.id}`}
|
||||
>
|
||||
<time>{event.time}</time>
|
||||
<span className="event-marker" aria-hidden="true" />
|
||||
<span className="event-copy">
|
||||
<strong>{event.actor}</strong>
|
||||
<span>{event.title}</span>
|
||||
<small>{event.detail}</small>
|
||||
</span>
|
||||
<ChevronRight size={16} aria-hidden="true" />
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
function DetailRow({ icon: Icon, label, value, actionLabel, onClick }) {
|
||||
const content = (
|
||||
<>
|
||||
<Icon size={15} strokeWidth={1.8} aria-hidden="true" />
|
||||
<span>{label}</span>
|
||||
<strong>{value}</strong>
|
||||
{onClick ? <ChevronRight size={15} aria-hidden="true" /> : null}
|
||||
</>
|
||||
);
|
||||
|
||||
if (onClick) {
|
||||
return (
|
||||
<button
|
||||
className="detail-row detail-row--action"
|
||||
type="button"
|
||||
aria-label={actionLabel}
|
||||
onClick={onClick}
|
||||
>
|
||||
{content}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
return <div className="detail-row">{content}</div>;
|
||||
}
|
||||
|
||||
function DetailPanel({
|
||||
selectedAgentId,
|
||||
approvalState,
|
||||
runState,
|
||||
onSelectAgent,
|
||||
onOpenApproval,
|
||||
onOpenProvider,
|
||||
onOpenSession,
|
||||
onUndoApproval,
|
||||
onRestart,
|
||||
}) {
|
||||
const selectedAgent = agents[selectedAgentId];
|
||||
const Icon = iconMap[selectedAgent.icon];
|
||||
const executorApproved = selectedAgentId === "executor" && approvalState === "approved";
|
||||
const selectedState = executorApproved ? "working" : selectedAgent.state;
|
||||
|
||||
return (
|
||||
<aside className="detail-panel" aria-label={`${selectedAgent.name} details`}>
|
||||
<header className="detail-agent-header">
|
||||
<span className="detail-agent-icon">
|
||||
<Icon size={20} strokeWidth={1.8} aria-hidden="true" />
|
||||
</span>
|
||||
<span>
|
||||
<strong>{selectedAgent.name}</strong>
|
||||
<small>{executorApproved ? "Preparing artifacts" : selectedAgent.status}</small>
|
||||
</span>
|
||||
<span className={`status-dot status-dot--${selectedState}`} aria-hidden="true" />
|
||||
<button
|
||||
className="icon-button"
|
||||
type="button"
|
||||
aria-label="Return to Iris details"
|
||||
onClick={() => onSelectAgent("iris")}
|
||||
>
|
||||
<X size={17} aria-hidden="true" />
|
||||
</button>
|
||||
</header>
|
||||
|
||||
<section className="detail-section">
|
||||
{selectedAgentId === "executor" ? (
|
||||
<>
|
||||
<p className="eyebrow">Step details</p>
|
||||
<span className={`step-badge step-badge--${approvalState}`}>
|
||||
<ShieldCheck size={13} aria-hidden="true" />
|
||||
{approvalState === "pending" ? "Pending approval" : "Approval granted"}
|
||||
</span>
|
||||
<h2>{approval.title}</h2>
|
||||
<p className="step-description">
|
||||
Assemble artifacts and configure release targets.
|
||||
</p>
|
||||
<dl className="agent-facts approval-facts">
|
||||
<div>
|
||||
<dt>Requested by</dt>
|
||||
<dd>{approval.requestedBy}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Requested at</dt>
|
||||
<dd>{approval.requestedAt}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Estimated duration</dt>
|
||||
<dd>{approval.duration}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Status</dt>
|
||||
<dd>{approvalState === "pending" ? "Waiting for approval" : "Preparing"}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Approval needed</dt>
|
||||
<dd>Bao (you)</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<p className="eyebrow">Agent details</p>
|
||||
<h2>{selectedAgent.task}</h2>
|
||||
<dl className="agent-facts">
|
||||
<div>
|
||||
<dt>Role</dt>
|
||||
<dd>{selectedAgent.role}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Started</dt>
|
||||
<dd>{selectedAgent.started}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Model policy</dt>
|
||||
<dd>{selectedAgent.model}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
<div className="tool-chips" aria-label="Allowed tools">
|
||||
{selectedAgent.tools.map((tool) => (
|
||||
<span key={tool}>{tool}</span>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</section>
|
||||
|
||||
<section className="detail-section">
|
||||
<p className="eyebrow">Run context</p>
|
||||
<DetailRow icon={Boxes} label="Run" value={runDetails.title} />
|
||||
<DetailRow
|
||||
icon={Link2}
|
||||
label="Session"
|
||||
value={runDetails.sessionId}
|
||||
actionLabel="Open OpenClaw session details"
|
||||
onClick={onOpenSession}
|
||||
/>
|
||||
<DetailRow
|
||||
icon={Cpu}
|
||||
label="Provider"
|
||||
value={`${runDetails.provider} via OpenClaw`}
|
||||
actionLabel="Open provider and routing details"
|
||||
onClick={onOpenProvider}
|
||||
/>
|
||||
<DetailRow icon={Bot} label="Model" value="gpt-5.2-codex" />
|
||||
<DetailRow icon={FolderKanban} label="Project" value={runDetails.project} />
|
||||
<DetailRow icon={ShieldCheck} label="Environment" value={runDetails.environment} />
|
||||
</section>
|
||||
|
||||
<section className="detail-section usage-section">
|
||||
<p className="eyebrow">Usage (run total)</p>
|
||||
<DetailRow icon={Clock3} label="Duration" value={runDetails.elapsed} />
|
||||
<DetailRow icon={Activity} label="Tokens" value={runDetails.tokens} />
|
||||
<DetailRow icon={CircleDollarSign} label="Estimated cost" value={runDetails.cost} />
|
||||
<DetailRow icon={Wrench} label="Tool calls" value={String(runDetails.toolCalls)} />
|
||||
</section>
|
||||
|
||||
{selectedAgentId === "executor" ? (
|
||||
<section
|
||||
className={`decision-card decision-card--${approvalState}`}
|
||||
aria-live="polite"
|
||||
>
|
||||
{approvalState === "pending" ? (
|
||||
<>
|
||||
<p className="eyebrow">Pending approval</p>
|
||||
<h2>{approval.title}</h2>
|
||||
<p>{approval.impact}</p>
|
||||
<button
|
||||
className="button button--primary button--full"
|
||||
type="button"
|
||||
onClick={onOpenApproval}
|
||||
data-testid="review-approval"
|
||||
>
|
||||
<ShieldCheck size={17} aria-hidden="true" />
|
||||
Review approval
|
||||
</button>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<p className="eyebrow">Decision recorded</p>
|
||||
<h2>Approval granted</h2>
|
||||
<p>Executor is preparing artifacts. No deployment was executed.</p>
|
||||
<button
|
||||
className="button button--secondary button--full"
|
||||
type="button"
|
||||
onClick={onUndoApproval}
|
||||
>
|
||||
<RotateCcw size={16} aria-hidden="true" />
|
||||
Undo mock decision
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</section>
|
||||
) : null}
|
||||
|
||||
{runState === "stopped" ? (
|
||||
<section className="decision-card decision-card--stopped">
|
||||
<p className="eyebrow">Run stopped</p>
|
||||
<h2>The mock run is no longer advancing.</h2>
|
||||
<button
|
||||
className="button button--primary button--full"
|
||||
type="button"
|
||||
onClick={onRestart}
|
||||
>
|
||||
<RotateCcw size={16} aria-hidden="true" />
|
||||
Restart mock run
|
||||
</button>
|
||||
</section>
|
||||
) : null}
|
||||
</aside>
|
||||
);
|
||||
}
|
||||
|
||||
function ApprovalDialog({ approvalState, onApprove, onUndo, onClose }) {
|
||||
return (
|
||||
<Modal
|
||||
title={approvalState === "pending" ? "Review deployment preparation" : "Approval decision"}
|
||||
eyebrow="Human approval"
|
||||
onClose={onClose}
|
||||
size="wide"
|
||||
>
|
||||
<div className="approval-layout">
|
||||
<section>
|
||||
<div className={`approval-summary approval-summary--${approvalState}`}>
|
||||
{approvalState === "pending" ? (
|
||||
<KeyRound size={22} aria-hidden="true" />
|
||||
) : (
|
||||
<CheckCircle2 size={22} aria-hidden="true" />
|
||||
)}
|
||||
<div>
|
||||
<strong>
|
||||
{approvalState === "pending" ? "Decision required" : "Decision recorded"}
|
||||
</strong>
|
||||
<p>{approval.impact}</p>
|
||||
</div>
|
||||
</div>
|
||||
<h3>Requested scope</h3>
|
||||
<ul className="scope-list">
|
||||
{approval.scope.map((item) => (
|
||||
<li key={item}>
|
||||
<Check size={16} aria-hidden="true" />
|
||||
<span>{item}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</section>
|
||||
<aside className="decision-context">
|
||||
<dl>
|
||||
<div>
|
||||
<dt>Requested by</dt>
|
||||
<dd>{approval.requestedBy}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Requested at</dt>
|
||||
<dd>{approval.requestedAt}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Estimated duration</dt>
|
||||
<dd>{approval.duration}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Session</dt>
|
||||
<dd>{runDetails.sessionId}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</aside>
|
||||
</div>
|
||||
|
||||
<footer className="modal-actions">
|
||||
<button className="button button--ghost" type="button" onClick={onClose}>
|
||||
Close
|
||||
</button>
|
||||
{approvalState === "pending" ? (
|
||||
<>
|
||||
<button className="button button--secondary" type="button" onClick={onClose}>
|
||||
Request changes
|
||||
</button>
|
||||
<button
|
||||
className="button button--primary"
|
||||
type="button"
|
||||
onClick={onApprove}
|
||||
data-testid="confirm-approval"
|
||||
>
|
||||
<Check size={17} aria-hidden="true" />
|
||||
Approve and continue
|
||||
</button>
|
||||
</>
|
||||
) : (
|
||||
<button className="button button--secondary" type="button" onClick={onUndo}>
|
||||
<RotateCcw size={16} aria-hidden="true" />
|
||||
Undo mock decision
|
||||
</button>
|
||||
)}
|
||||
</footer>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
function EventDialog({ event, onClose }) {
|
||||
return (
|
||||
<Modal title={event.title} eyebrow="Run event detail" onClose={onClose}>
|
||||
<div className="event-detail">
|
||||
<span className={`event-detail-icon event-detail-icon--${event.type}`}>
|
||||
<History size={20} aria-hidden="true" />
|
||||
</span>
|
||||
<p>{event.detail}</p>
|
||||
<dl className="detail-list">
|
||||
<div>
|
||||
<dt>Actor</dt>
|
||||
<dd>{event.actor}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Timestamp</dt>
|
||||
<dd>27 Jul 2026, {event.time}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Correlation ID</dt>
|
||||
<dd>{event.correlation}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Source</dt>
|
||||
<dd>OpenClaw session projection</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
function RuntimeDialog({ type, onClose }) {
|
||||
const isProvider = type === "provider";
|
||||
return (
|
||||
<Modal
|
||||
title={isProvider ? "Provider routing" : "OpenClaw session"}
|
||||
eyebrow={isProvider ? "Runtime policy detail" : "Session detail"}
|
||||
onClose={onClose}
|
||||
size="wide"
|
||||
>
|
||||
<div className="runtime-detail-grid">
|
||||
<section className="runtime-overview">
|
||||
<span className="runtime-large-icon">
|
||||
{isProvider ? <Cpu size={26} aria-hidden="true" /> : <Link2 size={26} aria-hidden="true" />}
|
||||
</span>
|
||||
<p className="runtime-status">
|
||||
<span className="status-dot status-dot--active" aria-hidden="true" />
|
||||
Authored connected state
|
||||
</p>
|
||||
<h3>{isProvider ? "OpenAI through OpenClaw" : runDetails.sessionId}</h3>
|
||||
<p>
|
||||
{isProvider
|
||||
? "Nexus displays the effective OpenClaw routing policy. This prototype performs no provider request."
|
||||
: "This mock projection represents the authoritative OpenClaw session without duplicating runtime state."}
|
||||
</p>
|
||||
</section>
|
||||
<dl className="runtime-facts">
|
||||
<div>
|
||||
<dt>{isProvider ? "Primary provider" : "Session owner"}</dt>
|
||||
<dd>{isProvider ? runDetails.provider : "Iris"}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>{isProvider ? "Access path" : "Parent run"}</dt>
|
||||
<dd>{isProvider ? runDetails.access : runDetails.runId}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>{isProvider ? "Model policy" : "Started"}</dt>
|
||||
<dd>{isProvider ? runDetails.modelPolicy : runDetails.started}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>{isProvider ? "Secret visibility" : "Freshness"}</dt>
|
||||
<dd>{isProvider ? "Hidden · reference only" : "Observed 8 seconds ago"}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</div>
|
||||
<footer className="modal-actions">
|
||||
<button className="button button--primary" type="button" onClick={onClose}>
|
||||
Done
|
||||
</button>
|
||||
</footer>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
function AskIrisDialog({ onClose }) {
|
||||
const [message, setMessage] = useState("");
|
||||
const [reply, setReply] = useState("");
|
||||
|
||||
const handleSubmit = (event) => {
|
||||
event.preventDefault();
|
||||
const trimmed = message.trim();
|
||||
if (!trimmed) return;
|
||||
setReply(
|
||||
"Mock response: I would keep the approval paused until you review the requested deployment-preparation scope.",
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal title="Ask Iris" eyebrow="Interactive mockup · no backend calls" onClose={onClose}>
|
||||
<div className="iris-dialog">
|
||||
<div className="iris-message">
|
||||
<span className="detail-agent-icon">
|
||||
<Sparkles size={18} aria-hidden="true" />
|
||||
</span>
|
||||
<p>
|
||||
I am supervising <strong>{runDetails.title}</strong>. Ask about the current
|
||||
plan, agents, or pending approval.
|
||||
</p>
|
||||
</div>
|
||||
{reply ? (
|
||||
<div className="iris-message iris-message--reply" aria-live="polite">
|
||||
<span className="detail-agent-icon">
|
||||
<Sparkles size={18} aria-hidden="true" />
|
||||
</span>
|
||||
<p>{reply}</p>
|
||||
</div>
|
||||
) : null}
|
||||
<form onSubmit={handleSubmit}>
|
||||
<label htmlFor="iris-message">Message</label>
|
||||
<textarea
|
||||
id="iris-message"
|
||||
value={message}
|
||||
onChange={(event) => setMessage(event.target.value)}
|
||||
placeholder="What is blocking this run?"
|
||||
rows={3}
|
||||
/>
|
||||
<button className="button button--primary" type="submit">
|
||||
<MessageSquareText size={17} aria-hidden="true" />
|
||||
Send mock message
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
export function App() {
|
||||
const [selectedAgent, setSelectedAgent] = useState("executor");
|
||||
const [approvalState, setApprovalState] = useState("pending");
|
||||
const [runState, setRunState] = useState("running");
|
||||
const [modal, setModal] = useState(null);
|
||||
const [toast, setToast] = useState("");
|
||||
const toastTimer = useRef(null);
|
||||
|
||||
const announce = (message) => {
|
||||
window.clearTimeout(toastTimer.current);
|
||||
setToast(message);
|
||||
toastTimer.current = window.setTimeout(() => setToast(""), 3200);
|
||||
};
|
||||
|
||||
const closeModal = () => setModal(null);
|
||||
|
||||
const approve = () => {
|
||||
setApprovalState("approved");
|
||||
setRunState("running");
|
||||
setSelectedAgent("executor");
|
||||
closeModal();
|
||||
announce("Mock approval recorded. Executor is preparing artifacts.");
|
||||
};
|
||||
|
||||
const undoApproval = () => {
|
||||
setApprovalState("pending");
|
||||
setSelectedAgent("executor");
|
||||
closeModal();
|
||||
announce("Mock approval restored to pending.");
|
||||
};
|
||||
|
||||
const togglePause = () => {
|
||||
const nextState = runState === "paused" ? "running" : "paused";
|
||||
setRunState(nextState);
|
||||
announce(nextState === "paused" ? "Mock run paused." : "Mock run resumed.");
|
||||
};
|
||||
|
||||
const stopRun = () => {
|
||||
setRunState("stopped");
|
||||
closeModal();
|
||||
announce("Mock run stopped. You can restart it from the detail panel.");
|
||||
};
|
||||
|
||||
const restart = () => {
|
||||
setRunState("running");
|
||||
setApprovalState("pending");
|
||||
setSelectedAgent("executor");
|
||||
announce("Mock run restarted at the pending approval.");
|
||||
};
|
||||
|
||||
const handlePrototypeNav = (label, active) => {
|
||||
if (active) {
|
||||
announce("You are already viewing Run Control.");
|
||||
return;
|
||||
}
|
||||
announce(`${label} is outside this focused mockup.`);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="prototype-root">
|
||||
<div className="desktop-gate">
|
||||
<Network size={30} aria-hidden="true" />
|
||||
<h1>Desktop mockup</h1>
|
||||
<p>This selected concept intentionally has no mobile surface.</p>
|
||||
</div>
|
||||
|
||||
<div className="app-shell">
|
||||
<Sidebar onPrototypeNav={handlePrototypeNav} />
|
||||
<div className="application">
|
||||
<Topbar
|
||||
runState={runState}
|
||||
onPause={togglePause}
|
||||
onStop={() => setModal({ type: "stop" })}
|
||||
onAskIris={() => setModal({ type: "ask" })}
|
||||
/>
|
||||
|
||||
<main className="run-workspace">
|
||||
<section className="workspace-stage">
|
||||
<div className="status-strip" aria-label="Run status summary">
|
||||
<span>
|
||||
<i className="status-dot status-dot--active" />
|
||||
5 active
|
||||
</span>
|
||||
<span>
|
||||
<i className="status-dot status-dot--waiting" />
|
||||
{approvalState === "pending" ? "1 waiting" : "0 waiting"}
|
||||
</span>
|
||||
<span>
|
||||
<i className="status-dot status-dot--idle" />0 blocked
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => announce("Auto-advance remains on for reversible mock steps.")}
|
||||
>
|
||||
Auto-advance <strong>On</strong>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<OrchestrationMap
|
||||
selectedAgent={selectedAgent}
|
||||
approvalState={approvalState}
|
||||
runState={runState}
|
||||
onSelectAgent={setSelectedAgent}
|
||||
onOpenApproval={() => setModal({ type: "approval" })}
|
||||
/>
|
||||
|
||||
<EventRail
|
||||
approvalState={approvalState}
|
||||
onOpenEvent={(event) => setModal({ type: "event", event })}
|
||||
/>
|
||||
</section>
|
||||
|
||||
<DetailPanel
|
||||
selectedAgentId={selectedAgent}
|
||||
approvalState={approvalState}
|
||||
runState={runState}
|
||||
onSelectAgent={setSelectedAgent}
|
||||
onOpenApproval={() => setModal({ type: "approval" })}
|
||||
onOpenProvider={() => setModal({ type: "provider" })}
|
||||
onOpenSession={() => setModal({ type: "session" })}
|
||||
onUndoApproval={undoApproval}
|
||||
onRestart={restart}
|
||||
/>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{modal?.type === "approval" ? (
|
||||
<ApprovalDialog
|
||||
approvalState={approvalState}
|
||||
onApprove={approve}
|
||||
onUndo={undoApproval}
|
||||
onClose={closeModal}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{modal?.type === "event" ? (
|
||||
<EventDialog event={modal.event} onClose={closeModal} />
|
||||
) : null}
|
||||
|
||||
{modal?.type === "provider" || modal?.type === "session" ? (
|
||||
<RuntimeDialog type={modal.type} onClose={closeModal} />
|
||||
) : null}
|
||||
|
||||
{modal?.type === "ask" ? <AskIrisDialog onClose={closeModal} /> : null}
|
||||
|
||||
{modal?.type === "stop" ? (
|
||||
<Modal title="Stop this mock run?" eyebrow="Reversible prototype action" onClose={closeModal}>
|
||||
<div className="stop-dialog">
|
||||
<span className="event-detail-icon event-detail-icon--approval">
|
||||
<Square size={20} aria-hidden="true" />
|
||||
</span>
|
||||
<p>
|
||||
The run will enter a stopped state. No backend or OpenClaw action is
|
||||
performed, and the mock state can be restarted immediately.
|
||||
</p>
|
||||
</div>
|
||||
<footer className="modal-actions">
|
||||
<button className="button button--ghost" type="button" onClick={closeModal}>
|
||||
Keep running
|
||||
</button>
|
||||
<button className="button button--danger" type="button" onClick={stopRun}>
|
||||
<Square size={15} aria-hidden="true" />
|
||||
Stop mock run
|
||||
</button>
|
||||
</footer>
|
||||
</Modal>
|
||||
) : null}
|
||||
|
||||
<div className={`toast${toast ? " is-visible" : ""}`} role="status" aria-live="polite">
|
||||
<BadgeCheck size={17} aria-hidden="true" />
|
||||
<span>{toast}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
import { useEffect, useId, useRef } from "react";
|
||||
import { X } from "lucide-react";
|
||||
|
||||
export function Modal({ title, eyebrow, children, onClose, size = "medium" }) {
|
||||
const titleId = useId();
|
||||
const closeRef = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
const previousFocus = document.activeElement;
|
||||
const handleKeyDown = (event) => {
|
||||
if (event.key === "Escape") {
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener("keydown", handleKeyDown);
|
||||
document.body.classList.add("dialog-open");
|
||||
closeRef.current?.focus();
|
||||
|
||||
return () => {
|
||||
document.removeEventListener("keydown", handleKeyDown);
|
||||
document.body.classList.remove("dialog-open");
|
||||
previousFocus?.focus?.();
|
||||
};
|
||||
}, [onClose]);
|
||||
|
||||
return (
|
||||
<div className="modal-layer" role="presentation">
|
||||
<button
|
||||
className="modal-backdrop"
|
||||
type="button"
|
||||
aria-label="Close detail view"
|
||||
onClick={onClose}
|
||||
/>
|
||||
<section
|
||||
className={`modal-panel modal-panel--${size}`}
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby={titleId}
|
||||
>
|
||||
<header className="modal-header">
|
||||
<div>
|
||||
{eyebrow ? <p className="eyebrow">{eyebrow}</p> : null}
|
||||
<h2 id={titleId}>{title}</h2>
|
||||
</div>
|
||||
<button
|
||||
ref={closeRef}
|
||||
className="icon-button"
|
||||
type="button"
|
||||
aria-label="Close detail view"
|
||||
onClick={onClose}
|
||||
>
|
||||
<X size={18} aria-hidden="true" />
|
||||
</button>
|
||||
</header>
|
||||
<div className="modal-content">{children}</div>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,181 @@
|
||||
export const navGroups = [
|
||||
{
|
||||
label: "Operations",
|
||||
items: [
|
||||
{ id: "dashboard", label: "Dashboard", icon: "dashboard" },
|
||||
{ id: "runs", label: "Runs", icon: "runs", active: true },
|
||||
{ id: "agents", label: "Agents", icon: "agents" },
|
||||
{ id: "projects", label: "Projects", icon: "projects" },
|
||||
{ id: "tasks", label: "Task Board", icon: "tasks", count: 3 },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: "Knowledge",
|
||||
items: [
|
||||
{ id: "memory", label: "Memory", icon: "memory" },
|
||||
{ id: "docs", label: "Docs", icon: "docs" },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: "Infrastructure",
|
||||
items: [
|
||||
{ id: "models", label: "Models", icon: "models" },
|
||||
{ id: "activity", label: "Activity", icon: "activity" },
|
||||
{ id: "calendar", label: "Calendar", icon: "calendar" },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: "Governance",
|
||||
items: [
|
||||
{ id: "security", label: "Security", icon: "security" },
|
||||
{ id: "incidents", label: "Incidents", icon: "incidents", count: 0 },
|
||||
{ id: "notifications", label: "Notifications", icon: "notifications", count: 3 },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export const agents = {
|
||||
iris: {
|
||||
id: "iris",
|
||||
name: "Iris",
|
||||
initials: "IR",
|
||||
role: "Lead · Orchestrator",
|
||||
status: "Delegating work",
|
||||
state: "active",
|
||||
progress: 100,
|
||||
icon: "iris",
|
||||
model: "OpenAI primary",
|
||||
task: "Coordinates the release-validation plan and human approvals.",
|
||||
started: "17:02:13",
|
||||
tools: ["sessions_send", "tasks_delegate", "approvals_request"],
|
||||
},
|
||||
programmer: {
|
||||
id: "programmer",
|
||||
name: "Programmer",
|
||||
initials: "PR",
|
||||
role: "Code Engineer",
|
||||
status: "Validation complete",
|
||||
state: "completed",
|
||||
progress: 100,
|
||||
icon: "programmer",
|
||||
model: "OpenAI coding policy",
|
||||
task: "Validated the change set and completed 128 unit and integration checks.",
|
||||
started: "17:02:28",
|
||||
tools: ["workspace_read", "exec_sandboxed", "test_report"],
|
||||
},
|
||||
reviewer: {
|
||||
id: "reviewer",
|
||||
name: "Reviewer",
|
||||
initials: "RV",
|
||||
role: "Quality Gate",
|
||||
status: "Checking changes",
|
||||
state: "working",
|
||||
progress: 43,
|
||||
icon: "reviewer",
|
||||
model: "OpenAI review policy",
|
||||
task: "Reviews release notes, policy constraints, and validation evidence.",
|
||||
started: "17:06:10",
|
||||
tools: ["diff_read", "policy_check", "review_submit"],
|
||||
},
|
||||
executor: {
|
||||
id: "executor",
|
||||
name: "Executor",
|
||||
initials: "EX",
|
||||
role: "Release Engineer",
|
||||
status: "Waiting for approval",
|
||||
state: "waiting",
|
||||
progress: 0,
|
||||
icon: "executor",
|
||||
model: "OpenAI execution policy",
|
||||
task: "Will prepare deployment artifacts after explicit owner approval.",
|
||||
started: "Queued",
|
||||
tools: ["artifact_prepare", "release_validate", "deployment_stage"],
|
||||
},
|
||||
};
|
||||
|
||||
export const timelineEvents = [
|
||||
{
|
||||
id: "event-start",
|
||||
time: "17:02:13",
|
||||
actor: "Iris",
|
||||
title: "Run started",
|
||||
detail: "Created the execution plan for production release validation.",
|
||||
type: "system",
|
||||
correlation: "evt_01JZR6C90A",
|
||||
},
|
||||
{
|
||||
id: "event-delegate-programmer",
|
||||
time: "17:02:28",
|
||||
actor: "Iris",
|
||||
title: "Delegated to Programmer",
|
||||
detail: "Validate changes and run the complete test suite.",
|
||||
type: "delegation",
|
||||
correlation: "evt_01JZR6D2KR",
|
||||
},
|
||||
{
|
||||
id: "event-tests",
|
||||
time: "17:05:21",
|
||||
actor: "Programmer",
|
||||
title: "Validation complete",
|
||||
detail: "All 128 unit and integration checks passed.",
|
||||
type: "success",
|
||||
correlation: "evt_01JZR6M8QF",
|
||||
},
|
||||
{
|
||||
id: "event-review",
|
||||
time: "17:06:10",
|
||||
actor: "Reviewer",
|
||||
title: "Review started",
|
||||
detail: "Checking release notes, changed files, and policy evidence.",
|
||||
type: "working",
|
||||
correlation: "evt_01JZR6Q8HN",
|
||||
},
|
||||
{
|
||||
id: "event-review-ready",
|
||||
time: "17:18:34",
|
||||
actor: "Reviewer",
|
||||
title: "Quality gate clear",
|
||||
detail: "Release evidence is complete and the deployment step can be prepared.",
|
||||
type: "success",
|
||||
correlation: "evt_01JZR7C0PD",
|
||||
},
|
||||
{
|
||||
id: "event-approval",
|
||||
time: "17:20:55",
|
||||
actor: "Iris",
|
||||
title: "Approval requested",
|
||||
detail: "Executor needs permission to prepare deployment artifacts.",
|
||||
type: "approval",
|
||||
correlation: "evt_01JZR7J4SX",
|
||||
},
|
||||
];
|
||||
|
||||
export const runDetails = {
|
||||
title: "Production release validation",
|
||||
runId: "run_01JZR6C7Q8M9",
|
||||
sessionId: "oc_7f3a2d1e",
|
||||
started: "27 Jul 2026, 17:02:13",
|
||||
elapsed: "00:18:42",
|
||||
provider: "OpenAI",
|
||||
access: "OpenClaw Gateway",
|
||||
modelPolicy: "openai-primary / release-safe",
|
||||
project: "Nexus Platform",
|
||||
environment: "production",
|
||||
tokens: "1.42M in / 368k out",
|
||||
cost: "$0.186",
|
||||
toolCalls: 18,
|
||||
};
|
||||
|
||||
export const approval = {
|
||||
id: "approval_01JZR7J5B2",
|
||||
title: "Prepare deployment",
|
||||
requestedBy: "Iris",
|
||||
requestedAt: "27 Jul 2026, 17:20:55",
|
||||
duration: "~ 6 min",
|
||||
scope: [
|
||||
"Assemble versioned release artifacts",
|
||||
"Validate production configuration",
|
||||
"Prepare, but do not execute, the deployment",
|
||||
],
|
||||
impact: "No production mutation is included in this approval.",
|
||||
};
|
||||
@@ -0,0 +1,19 @@
|
||||
import React from "react";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import "@fontsource/manrope/400.css";
|
||||
import "@fontsource/manrope/500.css";
|
||||
import "@fontsource/manrope/600.css";
|
||||
import "@fontsource/manrope/700.css";
|
||||
import "@fontsource/space-grotesk/500.css";
|
||||
import "@fontsource/space-grotesk/600.css";
|
||||
import "@fontsource/space-grotesk/700.css";
|
||||
import "@fontsource/jetbrains-mono/400.css";
|
||||
import "@fontsource/jetbrains-mono/500.css";
|
||||
import { App } from "./App.jsx";
|
||||
import "./styles.css";
|
||||
|
||||
createRoot(document.getElementById("root")).render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>,
|
||||
);
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,68 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { access } from "node:fs/promises";
|
||||
import test from "node:test";
|
||||
import worker from "../worker/index.js";
|
||||
|
||||
test("serves existing static assets without a fallback", async () => {
|
||||
const calls = [];
|
||||
const response = await worker.fetch(new Request("https://example.test/assets/app.js"), {
|
||||
ASSETS: {
|
||||
fetch: async (request) => {
|
||||
calls.push(new URL(request.url).pathname);
|
||||
return new Response("asset", { status: 200 });
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
assert.equal(response.status, 200);
|
||||
assert.deepEqual(calls, ["/assets/app.js"]);
|
||||
});
|
||||
|
||||
test("falls back to index.html for an unknown app route", async () => {
|
||||
const calls = [];
|
||||
const response = await worker.fetch(
|
||||
new Request("https://example.test/flow/step-two?source=share", {
|
||||
headers: { accept: "text/html" },
|
||||
}),
|
||||
{
|
||||
ASSETS: {
|
||||
fetch: async (request) => {
|
||||
const url = new URL(request.url);
|
||||
calls.push(url.pathname + url.search);
|
||||
return new Response(url.pathname === "/index.html" ? "app" : "missing", {
|
||||
status: url.pathname === "/index.html" ? 200 : 404,
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
assert.equal(response.status, 200);
|
||||
assert.deepEqual(calls, ["/flow/step-two?source=share", "/index.html"]);
|
||||
});
|
||||
|
||||
test("does not turn missing API or write requests into the app shell", async () => {
|
||||
for (const request of [
|
||||
new Request("https://example.test/api/missing", { headers: { accept: "application/json" } }),
|
||||
new Request("https://example.test/flow", { method: "POST", headers: { accept: "text/html" } }),
|
||||
]) {
|
||||
let calls = 0;
|
||||
const response = await worker.fetch(request, {
|
||||
ASSETS: {
|
||||
fetch: async () => {
|
||||
calls += 1;
|
||||
return new Response("missing", { status: 404 });
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
assert.equal(response.status, 404);
|
||||
assert.equal(calls, 1);
|
||||
}
|
||||
});
|
||||
|
||||
test("emits the files required by Sites packaging", async () => {
|
||||
await access(new URL("../dist/client/index.html", import.meta.url));
|
||||
await access(new URL("../dist/server/index.js", import.meta.url));
|
||||
await access(new URL("../dist/.openai/hosting.json", import.meta.url));
|
||||
});
|
||||
@@ -0,0 +1,19 @@
|
||||
import { defineConfig } from "vite";
|
||||
import react from "@vitejs/plugin-react";
|
||||
|
||||
export default defineConfig({
|
||||
build: {
|
||||
outDir: "dist/client",
|
||||
},
|
||||
optimizeDeps: {
|
||||
include: ["react", "react-dom/client"],
|
||||
},
|
||||
server: {
|
||||
host: "0.0.0.0",
|
||||
allowedHosts: ["terminal.local"],
|
||||
warmup: {
|
||||
clientFiles: ["./src/main.jsx"],
|
||||
},
|
||||
},
|
||||
plugins: [react()],
|
||||
});
|
||||
@@ -0,0 +1,15 @@
|
||||
export default {
|
||||
async fetch(request, env) {
|
||||
const response = await env.ASSETS.fetch(request);
|
||||
const acceptsHtml = request.headers.get("accept")?.includes("text/html");
|
||||
|
||||
if (response.status !== 404 || !acceptsHtml || !["GET", "HEAD"].includes(request.method)) {
|
||||
return response;
|
||||
}
|
||||
|
||||
const indexUrl = new URL(request.url);
|
||||
indexUrl.pathname = "/index.html";
|
||||
indexUrl.search = "";
|
||||
return env.ASSETS.fetch(new Request(indexUrl, request));
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user