+
+
- Your SSH Sessions, Protected
+ SSH Sessions, Protected
@@ -64,7 +65,7 @@ export const DurableSessionPage = ({
-
Buffered output streams back in — you never miss a line
+
Buffered output streams back in, never miss a line
@@ -77,46 +78,8 @@ export const DurableSessionPage = ({
-
-
-
Session States
-
-
-
-
-
Attached
-
Session is protected and connected
-
-
-
-
-
-
-
Detached
-
Session running, currently disconnected
-
-
-
-
-
-
-
Standard
-
Connection drops will end the session
-
-
-
-
-
-
Common use cases:
-
- - • Alternative to tmux or screen
- - • Long-running builds and deployments
- - • Working from unstable networks
- - • Surviving Wave restarts
-
-
-
-
+
+
diff --git a/frontend/app/onboarding/onboarding-layout-term.tsx b/frontend/app/onboarding/onboarding-layout-term.tsx
new file mode 100644
index 0000000000..95a4bde9e9
--- /dev/null
+++ b/frontend/app/onboarding/onboarding-layout-term.tsx
@@ -0,0 +1,262 @@
+// Copyright 2026, Command Line Inc.
+// SPDX-License-Identifier: Apache-2.0
+
+import { MagnifyIcon } from "@/app/element/magnify";
+import { cn, makeIconClass } from "@/util/util";
+import { useCallback, useLayoutEffect, useState } from "react";
+import { CommandReveal } from "./onboarding-command";
+
+export type FakeTermBlockProps = {
+ connectionName?: string;
+ durableStatus?: "connected" | "detached" | null;
+ className?: string;
+ command?: string;
+ typeIntervalMs?: number;
+ onComplete?: () => void;
+ children?: React.ReactNode;
+};
+
+export const FakeTermBlock = ({
+ connectionName = "ubuntu@remoteserver",
+ durableStatus = null,
+ className,
+ command,
+ typeIntervalMs = 80,
+ onComplete,
+ children,
+}: FakeTermBlockProps) => {
+ const color = "var(--conn-icon-color-1)";
+
+ const durableIconColor = durableStatus === "connected" ? "text-sky-500" : "text-sky-300";
+
+ return (
+
+
+
+
+
+
+
{connectionName}
+
+ {durableStatus && (
+
+
+
+ )}
+
+
+
+
+
+
+
+ {children ? (
+ children
+ ) : command ? (
+
+
+
+ ) : (
+
+
+
+ )}
+
+
+ );
+};
+
+const deployMessages = [
+ "[1/8] Installing dependencies...",
+ "[2/8] Generating TypeScript types from Go...",
+ "[3/8] Building Go backend (wavesrv)...",
+ "[4/8] Compiling TypeScript frontend...",
+ "[5/8] Bundling Electron renderer...",
+ "[6/8] Packaging application artifacts...",
+ "[7/8] Code signing binaries...",
+ "[8/8] Deploy complete ✓",
+];
+
+type OverlayState = null | "disconnected" | "connected";
+
+const ConnectionOverlay = ({ state }: { state: OverlayState }) => {
+ if (!state) return null;
+
+ const isConnected = state === "connected";
+
+ return (
+
+
+
+
+ {isConnected ? "Connected" : "Disconnected"}
+
+
+
+ );
+};
+
+const DeployLogOutput = ({
+ onComplete,
+ onOverlayStateChange,
+}: {
+ onComplete?: () => void;
+ onOverlayStateChange?: (state: OverlayState) => void;
+}) => {
+ const [key, setKey] = useState(0);
+ const [commandComplete, setCommandComplete] = useState(false);
+ const [visibleLines, setVisibleLines] = useState(0);
+ const [showPrompt, setShowPrompt] = useState(false);
+ const [showCursor, setShowCursor] = useState(false);
+ const [overlayState, setOverlayState] = useState
(null);
+
+ useLayoutEffect(() => {
+ if (onOverlayStateChange) {
+ onOverlayStateChange(overlayState);
+ }
+ }, [overlayState, onOverlayStateChange]);
+
+ const handleCommandComplete = useCallback(() => {
+ setCommandComplete(true);
+ }, []);
+
+ const resetAnimation = useCallback(() => {
+ setCommandComplete(false);
+ setVisibleLines(0);
+ setShowPrompt(false);
+ setShowCursor(false);
+ setOverlayState(null);
+ setKey((prev) => prev + 1);
+ }, []);
+
+ useLayoutEffect(() => {
+ if (!commandComplete) return;
+
+ let timeoutId: NodeJS.Timeout;
+
+ const runSequence = async () => {
+ // Show message 1
+ setVisibleLines(1);
+ await new Promise((resolve) => {
+ timeoutId = setTimeout(resolve, 1000);
+ });
+
+ // Show message 2
+ setVisibleLines(2);
+ await new Promise((resolve) => {
+ timeoutId = setTimeout(resolve, 1000);
+ });
+
+ // Show disconnected overlay
+ setOverlayState("disconnected");
+ await new Promise((resolve) => {
+ timeoutId = setTimeout(resolve, 2500);
+ });
+
+ // Change to connected
+ setOverlayState("connected");
+ await new Promise((resolve) => {
+ timeoutId = setTimeout(resolve, 1000);
+ });
+
+ // Remove overlay and show messages 3-7 instantly
+ setOverlayState(null);
+ setVisibleLines(7);
+
+ // Show message 8
+ setVisibleLines(8);
+ await new Promise((resolve) => {
+ timeoutId = setTimeout(resolve, 1000);
+ });
+
+ // Show prompt
+ setShowPrompt(true);
+ setShowCursor(true);
+ if (onComplete) {
+ onComplete();
+ }
+
+ // Wait 6 seconds then restart
+ await new Promise((resolve) => {
+ timeoutId = setTimeout(resolve, 6000);
+ });
+
+ resetAnimation();
+ };
+
+ runSequence();
+
+ return () => {
+ if (timeoutId) {
+ clearTimeout(timeoutId);
+ }
+ };
+ }, [commandComplete, onComplete, resetAnimation]);
+
+ useLayoutEffect(() => {
+ if (!showPrompt) return;
+
+ const cursorInterval = setInterval(() => {
+ setShowCursor((prev) => !prev);
+ }, 500);
+
+ return () => clearInterval(cursorInterval);
+ }, [showPrompt]);
+
+ return (
+ <>
+
+
+ {commandComplete && (
+ <>
+ {deployMessages.slice(0, visibleLines).map((msg, idx) => (
+
+ {msg}
+
+ ))}
+ {showPrompt && (
+
+ >
+ {showCursor && (
+
+ )}
+
+ )}
+ >
+ )}
+
+ {overlayState && }
+ >
+ );
+};
+
+export const TailDeployLogCommand = ({ onComplete }: { onComplete?: () => void }) => {
+ const [overlayState, setOverlayState] = useState(null);
+
+ const durableStatus = overlayState === "disconnected" ? "detached" : "connected";
+
+ return (
+
+
+
+ );
+};