From 4f37821593c7bc6f950ba000de20adc908740f88 Mon Sep 17 00:00:00 2001 From: Adrian Llopart Date: Wed, 8 Jul 2026 09:34:20 +0200 Subject: [PATCH] feat(install): add CLI/CURL tabs with literal terminal output MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The 01 — Install terminal now has two tabs: CLI (default) walks through git clone -> just quickstart -> real colcon build lines -> the actual openral REPL banner -> openral doctor, and CURL shows the Tier-0 curl-install path with a "Coming soon" badge. The banner and doctor table are captured verbatim via console.export_text() from the real openral_cli code so the site shows literal terminal output rather than a re-styled approximation, sized to fit the terminal without horizontal scroll. Also fixes a stale-closure bug that silently dropped the first line of every streamed output block, and a UA-stylesheet quirk that let /
 fall back to the wrong monospace font.

Co-Authored-By: Claude Sonnet 5 
Claude-Session: https://claude.ai/code/session_01UGUszRrjkB7AV98n6mGd7r
---
 src/components/Terminal.css | 118 +++++++++++++-
 src/components/Terminal.jsx | 300 +++++++++++++++++++++++++++++-------
 2 files changed, 354 insertions(+), 64 deletions(-)

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 (
+    
+ {line.split(/(ok)/g).map((chunk, j) => + chunk === "ok" ? ( + + ok + + ) : ( + chunk + ) + )} +
+ ); +} + +function Banner() { + 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 ( +
+ {rows.map((r, i) => { + if (r.kind === "cmd") { + return ( +
+ $ + {r.text} +
+ ); + } + if (r.kind === "banner") return ; + if (r.kind === "table") return ; + return ( + + {r.t} + + ); + })} + {typing !== null && ( +
+ $ + + {typing} + {!reduce && } + +
+ )} +
+ ); +} + +export default function Terminal() { + const reveal = useReveal(); + const reduce = useReducedMotion(); + const ref = useRef(null); + const inView = useInView(ref, { once: true, amount: 0.4 }); + const [tab, setTab] = useState("cli"); + const [copied, setCopied] = useState(false); const copy = async () => { try { - await navigator.clipboard.writeText(CMD); + await navigator.clipboard.writeText(tab === "cli" ? CLI_COPY : CURL_CMD); setCopied(true); setTimeout(() => setCopied(false), 1800); } catch { @@ -83,33 +266,32 @@ export default function Terminal() {
- openral · install +
+ + +
-
-
- $ - - {typed} - {!reduce && typed.length < CMD.length && } - -
-
- {OUTPUT.slice(0, lines).map((l, i) => ( - - {l.t} - - ))} -
-
+ {tab === "cli" ? ( + + ) : ( + + )}

Prerequisites: ROS 2 Jazzy · Python 3.12+ · a CUDA GPU for VLA inference. Full guide in the{" "}