diff --git a/src/components/Terminal.css b/src/components/Terminal.css
index 29c3d54..d5df426 100644
--- a/src/components/Terminal.css
+++ b/src/components/Terminal.css
@@ -33,6 +33,45 @@
color: var(--muted);
flex: 1;
}
+.term-tabs {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ flex: 1;
+}
+.term-tab {
+ position: relative;
+ min-width: 96px;
+ font-family: var(--mono);
+ font-size: 13.5px;
+ letter-spacing: 0.04em;
+ color: var(--muted);
+ background: transparent;
+ border: 1px solid var(--hair-2);
+ border-radius: 8px;
+ padding: 9px 20px;
+ cursor: pointer;
+ transition: color 0.2s, border-color 0.2s, background 0.2s;
+}
+.term-tab:hover {
+ color: var(--text-2);
+}
+.term-tab.active {
+ color: var(--text);
+ border-color: var(--accent-line);
+ background: rgba(255, 255, 255, 0.05);
+}
+.term-tab-curl {
+ min-width: 144px;
+ text-align: left;
+}
+.term-tab .soon {
+ position: absolute;
+ top: 4px;
+ right: 8px;
+ font-size: 7px;
+ padding: 1px 5px;
+}
.term-copy {
font-family: var(--mono);
font-size: 12px;
@@ -52,18 +91,48 @@
padding: 22px 20px 26px;
font-family: var(--mono);
font-size: clamp(12px, 1.4vw, 14.5px);
- line-height: 1.9;
+ line-height: 1.7;
+ height: 460px;
+ overflow-y: auto;
+ scrollbar-width: thin;
+ scrollbar-color: var(--hair-2) transparent;
+}
+.term-body::-webkit-scrollbar {
+ width: 8px;
+}
+.term-body::-webkit-scrollbar-track {
+ background: transparent;
+}
+.term-body::-webkit-scrollbar-thumb {
+ background-color: var(--hair-2);
+ background-clip: padding-box;
+ border: 2px solid transparent;
+ border-radius: 8px;
+}
+.term-body::-webkit-scrollbar-thumb:hover {
+ background-color: var(--text-2);
+}
+.term-body > * + * {
+ margin-top: 3px;
}
.term-cmd {
display: flex;
gap: 10px;
align-items: flex-start;
+ margin-top: 10px;
+}
+.term-cmd:first-child {
+ margin-top: 0;
}
.term-prompt {
color: var(--accent);
user-select: none;
}
.term-cmd code {
+ /* same UA-stylesheet override as .term-pre: `code { font-family: monospace }`
+ beats the inherited --mono unless restated here. */
+ font-family: inherit;
+ font-size: inherit;
background: none;
color: var(--text);
padding: 0;
@@ -83,10 +152,6 @@
opacity: 0;
}
}
-.term-out {
- margin-top: 6px;
- min-height: 116px;
-}
.term-line {
color: var(--text-2);
}
@@ -94,6 +159,49 @@
color: var(--green);
}
+.term-pre {
+ margin: 10px 0;
+ color: var(--text-2);
+ /* the UA stylesheet sets `pre { font-family: monospace }`, which beats
+ inheriting --mono from .term-body — restate it explicitly here. */
+ font-family: var(--mono);
+ font-size: inherit;
+ white-space: pre;
+ overflow-x: auto;
+ scrollbar-width: thin;
+ scrollbar-color: var(--hair-2) transparent;
+}
+.term-pre::-webkit-scrollbar {
+ height: 8px;
+}
+.term-pre::-webkit-scrollbar-track {
+ background: transparent;
+}
+.term-pre::-webkit-scrollbar-thumb {
+ background-color: var(--hair-2);
+ background-clip: padding-box;
+ border: 2px solid transparent;
+ border-radius: 8px;
+}
+.term-pre::-webkit-scrollbar-thumb:hover {
+ background-color: var(--text-2);
+}
+.term-banner,
+.term-doctor {
+ /* Tighter than the 1.7 used for prose-like output, so box-drawing borders sit flush. */
+ line-height: 1.35;
+}
+.term-banner {
+ /* The banner is 127 real terminal columns — at the shared 14.5px body size it's
+ wider than the terminal box. 10.2px is the largest size that still fits the
+ 900px-wide box without a horizontal scrollbar (measured empirically); the
+ inherited overflow-x: auto from .term-pre stays as a fallback on narrow viewports. */
+ font-size: 10.2px;
+}
+.term-ok {
+ color: var(--green);
+}
+
.term-note {
margin-top: 20px;
font-family: var(--mono);
diff --git a/src/components/Terminal.jsx b/src/components/Terminal.jsx
index 318ec5c..99435e5 100644
--- a/src/components/Terminal.jsx
+++ b/src/components/Terminal.jsx
@@ -3,59 +3,242 @@ import { motion, useInView, useReducedMotion } from "framer-motion";
import { useReveal } from "../hooks/useReveal.js";
import "./Terminal.css";
-const CMD = "curl -fsSL https://raw.githubusercontent.com/OpenRAL/openral/master/scripts/install.sh | sh";
-const OUTPUT = [
- { t: "▸ fetching openral install script…", k: "run" },
- { t: "▸ resolving ROS 2 + Python deps…", k: "run" },
- { t: "▸ installing harness · L0–L7…", k: "run" },
- { t: "✓ openral ready — run `openral doctor` to verify", k: "ok" },
+// Exact `console.export_text()` output of openral_cli.main.render_banner("0.1.0", width=127)
+// and of the `openral doctor` Rich table — captured from the real CLI so the site shows the
+// literal terminal output, not a re-styled approximation.
+const BANNER_TEXT = `╭─ OPENRAL v0.1.0 ────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
+│ │
+│ █ █ ██████╗ ██████╗ ███████╗███╗ ██╗██████╗ █████╗ ██╗ │ Discord discord.gg/3paXT2bVyB │
+│ ██▄ ▄██ ██╔═══██╗██╔══██╗██╔════╝████╗ ██║██╔══██╗██╔══██╗██║ │ GitHub github.com/OpenRAL/openral │
+│ ████▄▄ ▄▄████ ██║ ██║██████╔╝█████╗ ██╔██╗ ██║██████╔╝███████║██║ │ Hugging Face huggingface.co/OpenRAL │
+│ ▀██████ ██████▀ ██║ ██║██╔═══╝ ██╔══╝ ██║╚██╗██║██╔══██╗██╔══██║██║ │ Website openral.com │
+│ ▀███████▀ ╚██████╔╝██║ ███████╗██║ ╚████║██║ ██║██║ ██║███████╗ │ ──────────────────────────────────────── │
+│ ▀ ▀███▀ ▀ ╚═════╝ ╚═╝ ╚══════╝╚═╝ ╚═══╝╚═╝ ╚═╝╚═╝ ╚═╝╚══════╝ │ doctor diagnose your host setup │
+│ │ rskill search find installable skills │
+│ OpenRAL — Open Robot Agentic Layer (harness) for embodied AI │ help list every command │
+│ fast policies · slow reasoning · rewards · perception · control │ exit leave the repl · Ctrl-D │
+│ │
+╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯`;
+
+const DOCTOR_TEXT = ` openral doctor
+┏━━━━━━━━━━━━━━┳━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
+┃ check ┃ status ┃ details ┃
+┡━━━━━━━━━━━━━━╇━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩
+│ Python │ ok │ 3.12.4 │
+│ Platform │ ok │ Ubuntu 24.04 (noble) x86_64 │
+│ ROS 2 distro │ ok │ jazzy │
+│ colcon │ ok │ 0.20.0 │
+│ GPU │ ok │ NVIDIA RTX 4090 · CUDA 12.6 │
+│ USB devices │ ok │ so101, realsense │
+└──────────────┴────────┴─────────────────────────────┘`;
+
+// Real openral ROS 2 package names (~/openral), one per product pillar — observability,
+// safety, perception, reasoning, skill execution — interleaved like colcon's parallel build.
+const CLI_CLONE_CMD = "git clone git@github.com:OpenRAL/openral.git";
+const CLI_SCRIPT = [
+ { cmd: CLI_CLONE_CMD },
+ { cmd: "just quickstart" },
+ {
+ out: [
+ { t: "Starting >>> opentelemetry_cpp_vendor" },
+ { t: "Starting >>> openral_safety_kernel" },
+ { t: "Finished <<< opentelemetry_cpp_vendor [17.9s]", k: "ok" },
+ { t: "Starting >>> openral_perception_ros" },
+ { t: "Finished <<< openral_safety_kernel [9.2s]", k: "ok" },
+ { t: "Starting >>> openral_reasoner_ros" },
+ { t: "Finished <<< openral_perception_ros [14.1s]", k: "ok" },
+ { t: "Starting >>> openral_rskill_ros" },
+ { t: "Finished <<< openral_reasoner_ros [11.6s]", k: "ok" },
+ { t: "Finished <<< openral_rskill_ros [20.9s]", k: "ok" },
+ ],
+ },
+ { banner: true },
+ { cmd: "doctor" },
+ { table: true },
];
+const CLI_COPY = `${CLI_CLONE_CMD} && cd openral && just quickstart`;
-export default function Terminal() {
- const reveal = useReveal();
- const reduce = useReducedMotion();
- const ref = useRef(null);
- const inView = useInView(ref, { once: true, amount: 0.4 });
+// Tier-0 (ADR-0021): installs uv + CPython 3.12 + openral-cli only — no ROS 2,
+// no colcon build, no REPL launch — so no Starting/Finished lines here, and the
+// ending below is the real "next steps" text from scripts/install.sh, condensed.
+const CURL_CMD = "curl -fsSL https://raw.githubusercontent.com/OpenRAL/openral/master/scripts/install.sh | sh";
+const CURL_SCRIPT = [
+ { cmd: CURL_CMD },
+ {
+ out: [
+ { t: "▸ installing uv + CPython 3.12…" },
+ { t: "▸ installing openral-cli (uv tool install)…" },
+ { t: "✓ openral installed: ~/.local/bin/openral", k: "ok" },
+ { t: "==> Tier-0 install complete.", k: "ok" },
+ { t: "openral doctor — diagnose the host (Python, OS, GPU, USB)" },
+ { t: "openral install ros — ROS 2 + libusb + udev (sudo, opt-in)" },
+ ],
+ },
+];
- const [typed, setTyped] = useState(reduce ? CMD : "");
- const [lines, setLines] = useState(reduce ? OUTPUT.length : 0);
- const [copied, setCopied] = useState(false);
+function flattenReduced(script) {
+ return script.flatMap((b) => {
+ if (b.cmd) return [{ kind: "cmd", text: b.cmd }];
+ if (b.out) return b.out.map((l) => ({ kind: "out", ...l }));
+ if (b.banner) return [{ kind: "banner" }];
+ return [{ kind: "table" }];
+ });
+}
+
+function useScriptRunner(script, active, reduce) {
+ const [rows, setRows] = useState([]);
+ const [typing, setTyping] = useState(null);
useEffect(() => {
- if (!inView || reduce) return;
+ if (reduce) {
+ setRows(flattenReduced(script));
+ return;
+ }
+ if (!active) return;
let cancelled = false;
const timers = [];
+ const after = (fn, ms) => timers.push(setTimeout(() => !cancelled && fn(), ms));
- const run = () => {
- setTyped("");
- setLines(0);
- let i = 0;
- const type = () => {
- if (cancelled) return;
- i += 1;
- setTyped(CMD.slice(0, i));
- if (i < CMD.length) {
- timers.push(setTimeout(type, 26));
- } else {
- OUTPUT.forEach((_, idx) =>
- timers.push(setTimeout(() => !cancelled && setLines(idx + 1), 420 * (idx + 1)))
- );
- timers.push(setTimeout(run, 420 * OUTPUT.length + 4200)); // loop
- }
- };
- timers.push(setTimeout(type, 500));
+ const step = (i) => {
+ if (cancelled) return;
+ if (i >= script.length) {
+ after(() => {
+ setRows([]);
+ step(0);
+ }, 3600);
+ return;
+ }
+ const block = script[i];
+ if (block.cmd) {
+ let c = 0;
+ const type = () => {
+ if (cancelled) return;
+ c += 1;
+ setTyping(block.cmd.slice(0, c));
+ if (c < block.cmd.length) {
+ after(type, 22);
+ } else {
+ after(() => {
+ setRows((r) => [...r, { kind: "cmd", text: block.cmd }]);
+ setTyping(null);
+ step(i + 1);
+ }, 300);
+ }
+ };
+ after(type, 300);
+ } else if (block.out) {
+ let li = 0;
+ const reveal = () => {
+ if (cancelled) return;
+ const item = block.out[li];
+ setRows((r) => [...r, { kind: "out", ...item }]);
+ li += 1;
+ if (li < block.out.length) after(reveal, 260);
+ else after(() => step(i + 1), 500);
+ };
+ after(reveal, 220);
+ } else {
+ after(() => {
+ setRows((r) => [...r, { kind: block.banner ? "banner" : "table" }]);
+ step(i + 1);
+ }, 400);
+ }
};
- run();
+ step(0);
return () => {
cancelled = true;
timers.forEach(clearTimeout);
};
- }, [inView, reduce]);
+ }, [script, active, reduce]);
+
+ return { rows, typing };
+}
+
+// Colors the literal "ok" status cells without disturbing column alignment —
+// a adds no character width, so the monospace grid stays intact.
+function colorizeOk(line, i) {
+ return (
+ {BANNER_TEXT};
+}
+
+function DoctorTable() {
+ return {DOCTOR_TEXT.split("\n").map(colorizeOk)};
+}
+
+function ScriptPane({ script, active, reduce }) {
+ const { rows, typing } = useScriptRunner(script, active, reduce);
+ const bodyRef = useRef(null);
+
+ useEffect(() => {
+ const el = bodyRef.current;
+ if (el) el.scrollTop = el.scrollHeight;
+ }, [rows, typing]);
+
+ return (
+ {r.text}
+
+ {typing}
+ {!reduce && }
+
+
- {typed}
- {!reduce && typed.length < CMD.length && }
-
- Prerequisites: ROS 2 Jazzy · Python 3.12+ · a CUDA GPU for VLA inference. Full guide in the{" "}