From b61e8722644100f857614e2ad67e69f0239c74bf Mon Sep 17 00:00:00 2001 From: sawka Date: Wed, 11 Feb 2026 11:34:27 -0800 Subject: [PATCH 1/3] checkpoint --- .../app/onboarding/onboarding-command.tsx | 3 +- .../app/onboarding/onboarding-durable.tsx | 51 ++---------- .../app/onboarding/onboarding-layout-term.tsx | 82 +++++++++++++++++++ 3 files changed, 91 insertions(+), 45 deletions(-) create mode 100644 frontend/app/onboarding/onboarding-layout-term.tsx diff --git a/frontend/app/onboarding/onboarding-command.tsx b/frontend/app/onboarding/onboarding-command.tsx index d98065dbf6..9cf8c2aae9 100644 --- a/frontend/app/onboarding/onboarding-command.tsx +++ b/frontend/app/onboarding/onboarding-command.tsx @@ -1,8 +1,9 @@ -// Copyright 2025, Command Line Inc. +// Copyright 2026, Command Line Inc. // SPDX-License-Identifier: Apache-2.0 import { useCallback, useLayoutEffect, useRef, useState } from "react"; import { FakeBlock } from "./onboarding-layout"; +import { FakeTermBlock } from "./onboarding-layout-term"; import waveLogo from "/logos/wave-logo.png"; export type CommandRevealProps = { diff --git a/frontend/app/onboarding/onboarding-durable.tsx b/frontend/app/onboarding/onboarding-durable.tsx index 9a4286f8fd..0d86194b89 100644 --- a/frontend/app/onboarding/onboarding-durable.tsx +++ b/frontend/app/onboarding/onboarding-durable.tsx @@ -8,6 +8,7 @@ import { TabRpcClient } from "@/app/store/wshrpcutil"; import { useState } from "react"; import { CurrentOnboardingVersion } from "./onboarding-common"; import { OnboardingFooter } from "./onboarding-features-footer"; +import { TailDeployLogCommand } from "./onboarding-layout-term"; export const DurableSessionPage = ({ onNext, @@ -42,11 +43,11 @@ export const DurableSessionPage = ({
Durable SSH Sessions
-
-
-
+
+
+
- Your SSH Sessions, Protected + SSH Sessions, Protected
@@ -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..0c74b05bd4 --- /dev/null +++ b/frontend/app/onboarding/onboarding-layout-term.tsx @@ -0,0 +1,82 @@ +// Copyright 2026, Command Line Inc. +// SPDX-License-Identifier: Apache-2.0 + +import { MagnifyIcon } from "@/app/element/magnify"; +import { cn, makeIconClass } from "@/util/util"; +import { CommandReveal } from "./onboarding-command"; + +export type FakeTermBlockProps = { + connectionName?: string; + durableStatus?: "connected" | "detached" | null; + className?: string; + command?: string; + typeIntervalMs?: number; + onComplete?: () => void; +}; + +export const FakeTermBlock = ({ + connectionName = "ubuntu@remoteserver", + durableStatus = null, + className, + command, + typeIntervalMs = 80, + onComplete, +}: FakeTermBlockProps) => { + const color = "var(--conn-icon-color-1)"; + + const durableIconColor = durableStatus === "connected" ? "text-sky-500" : "text-sky-300"; + + return ( +
+
+
+ + + +
{connectionName}
+
+ {durableStatus && ( +
+ +
+ )} +
+ + + + +
+
+ {command ? ( +
+ +
+ ) : ( +
+ +
+ )} +
+
+ ); +}; + +export const TailDeployLogCommand = ({ onComplete }: { onComplete?: () => void }) => { + return ( + + ); +}; From e59c155c9d2a612dc551206d55b3cc25a3283645 Mon Sep 17 00:00:00 2001 From: sawka Date: Wed, 11 Feb 2026 12:39:00 -0800 Subject: [PATCH 2/3] checkpoint show fake build output --- .../app/onboarding/onboarding-layout-term.tsx | 93 +++++++++++++++++-- 1 file changed, 85 insertions(+), 8 deletions(-) diff --git a/frontend/app/onboarding/onboarding-layout-term.tsx b/frontend/app/onboarding/onboarding-layout-term.tsx index 0c74b05bd4..890bcd1a16 100644 --- a/frontend/app/onboarding/onboarding-layout-term.tsx +++ b/frontend/app/onboarding/onboarding-layout-term.tsx @@ -3,6 +3,7 @@ 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 = { @@ -12,6 +13,7 @@ export type FakeTermBlockProps = { command?: string; typeIntervalMs?: number; onComplete?: () => void; + children?: React.ReactNode; }; export const FakeTermBlock = ({ @@ -21,6 +23,7 @@ export const FakeTermBlock = ({ command, typeIntervalMs = 80, onComplete, + children, }: FakeTermBlockProps) => { const color = "var(--conn-icon-color-1)"; @@ -55,7 +58,9 @@ export const FakeTermBlock = ({
- {command ? ( + {children ? ( + children + ) : command ? (
@@ -69,14 +74,86 @@ export const FakeTermBlock = ({ ); }; +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 ✓", +]; + +const DeployLogOutput = ({ onComplete }: { onComplete?: () => void }) => { + const [commandComplete, setCommandComplete] = useState(false); + const [visibleLines, setVisibleLines] = useState(0); + const [showPrompt, setShowPrompt] = useState(false); + const [showCursor, setShowCursor] = useState(false); + + const handleCommandComplete = useCallback(() => { + setCommandComplete(true); + }, []); + + useLayoutEffect(() => { + if (!commandComplete) return; + + let lineIndex = 0; + const lineInterval = setInterval(() => { + if (lineIndex < deployMessages.length) { + setVisibleLines(lineIndex + 1); + lineIndex++; + } else { + clearInterval(lineInterval); + setTimeout(() => { + setShowPrompt(true); + setShowCursor(true); + if (onComplete) { + onComplete(); + } + }, 200); + } + }, 1000); + + return () => clearInterval(lineInterval); + }, [commandComplete, onComplete]); + + useLayoutEffect(() => { + if (!showCursor) return; + + const cursorInterval = setInterval(() => { + setShowCursor((prev) => !prev); + }, 500); + + return () => clearInterval(cursorInterval); + }, [showCursor]); + + return ( +
+ + {commandComplete && ( + <> + {deployMessages.slice(0, visibleLines).map((msg, idx) => ( +
+ {msg} +
+ ))} + {showPrompt && ( +
+ > + {showCursor && } +
+ )} + + )} +
+ ); +}; + export const TailDeployLogCommand = ({ onComplete }: { onComplete?: () => void }) => { return ( - + + + ); }; From 5ac66045df9c9499c6a82605b114b527594cad3d Mon Sep 17 00:00:00 2001 From: sawka Date: Wed, 11 Feb 2026 13:05:14 -0800 Subject: [PATCH 3/3] finish durable session onboarding animation --- .../app/onboarding/onboarding-durable.tsx | 2 +- .../app/onboarding/onboarding-layout-term.tsx | 183 ++++++++++++++---- 2 files changed, 144 insertions(+), 41 deletions(-) diff --git a/frontend/app/onboarding/onboarding-durable.tsx b/frontend/app/onboarding/onboarding-durable.tsx index 0d86194b89..b716b3d7da 100644 --- a/frontend/app/onboarding/onboarding-durable.tsx +++ b/frontend/app/onboarding/onboarding-durable.tsx @@ -65,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

diff --git a/frontend/app/onboarding/onboarding-layout-term.tsx b/frontend/app/onboarding/onboarding-layout-term.tsx index 890bcd1a16..95a4bde9e9 100644 --- a/frontend/app/onboarding/onboarding-layout-term.tsx +++ b/frontend/app/onboarding/onboarding-layout-term.tsx @@ -85,75 +85,178 @@ const deployMessages = [ "[8/8] Deploy complete ✓", ]; -const DeployLogOutput = ({ onComplete }: { onComplete?: () => void }) => { +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 lineIndex = 0; - const lineInterval = setInterval(() => { - if (lineIndex < deployMessages.length) { - setVisibleLines(lineIndex + 1); - lineIndex++; - } else { - clearInterval(lineInterval); - setTimeout(() => { - setShowPrompt(true); - setShowCursor(true); - if (onComplete) { - onComplete(); - } - }, 200); + 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(); } - }, 1000); - return () => clearInterval(lineInterval); - }, [commandComplete, 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 (!showCursor) return; + if (!showPrompt) return; const cursorInterval = setInterval(() => { setShowCursor((prev) => !prev); }, 500); return () => clearInterval(cursorInterval); - }, [showCursor]); + }, [showPrompt]); return ( -
- - {commandComplete && ( - <> - {deployMessages.slice(0, visibleLines).map((msg, idx) => ( -
- {msg} -
- ))} - {showPrompt && ( -
- > - {showCursor && } -
- )} - - )} -
+ <> +
+ + {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 ( - - + + ); };