Skip to content

Commit 2a50e08

Browse files
committed
work on onboarding screens for durable sessions
1 parent 3abee16 commit 2a50e08

6 files changed

Lines changed: 191 additions & 54 deletions

File tree

docs/docs/durable-sessions.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ Configuration hierarchy (highest to lowest priority):
9191

9292
### Default Behavior
9393

94-
- **SSH connections**: Durable sessions enabled by default
94+
- **SSH connections**: Durable sessions disabled by default (opt-in via configuration)
9595
- **Local terminals**: Always use standard sessions (durability not applicable)
9696
- **WSL connections**: Always use standard sessions (durability not applicable)
9797

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
// Copyright 2026, Command Line Inc.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
import Logo from "@/app/asset/logo.svg";
5+
import { EmojiButton } from "@/app/element/emojibutton";
6+
import { RpcApi } from "@/app/store/wshclientapi";
7+
import { TabRpcClient } from "@/app/store/wshrpcutil";
8+
import { useState } from "react";
9+
import { CurrentOnboardingVersion } from "./onboarding-common";
10+
import { OnboardingFooter } from "./onboarding-features-footer";
11+
12+
export const DurableSessionPage = ({
13+
onNext,
14+
onSkip,
15+
onPrev,
16+
}: {
17+
onNext: () => void;
18+
onSkip: () => void;
19+
onPrev?: () => void;
20+
}) => {
21+
const [fireClicked, setFireClicked] = useState(false);
22+
23+
const handleFireClick = () => {
24+
setFireClicked(!fireClicked);
25+
if (!fireClicked) {
26+
RpcApi.RecordTEventCommand(TabRpcClient, {
27+
event: "onboarding:fire",
28+
props: {
29+
"onboarding:feature": "durable",
30+
"onboarding:version": CurrentOnboardingVersion,
31+
},
32+
});
33+
}
34+
};
35+
36+
return (
37+
<div className="flex flex-col h-full">
38+
<header className="flex items-center gap-4 mb-6 w-full unselectable flex-shrink-0">
39+
<div>
40+
<Logo />
41+
</div>
42+
<div className="text-[25px] font-normal text-foreground">Durable SSH Sessions</div>
43+
</header>
44+
<div className="flex-1 flex flex-row gap-0 min-h-0">
45+
<div className="flex-1 flex flex-col items-center justify-center gap-8 pr-6 unselectable">
46+
<div className="flex flex-col items-start gap-6 max-w-md">
47+
<div className="flex h-[52px] ml-[-4px] pl-3 pr-4 items-center rounded-lg bg-hover text-[18px]">
48+
<i className="fa-sharp fa-solid fa-shield text-sky-500" />
49+
<span className="font-bold ml-2 text-primary">Your SSH Sessions, Protected</span>
50+
</div>
51+
52+
<div className="flex flex-col items-start gap-4 text-secondary">
53+
<p>Close your laptop, switch networks, restart Wave — your remote sessions keep running.</p>
54+
55+
<div className="flex items-start gap-3 w-full">
56+
<i className="fa-sharp fa-solid fa-link text-accent text-lg mt-1 flex-shrink-0" />
57+
<p>Shell state, running programs, and terminal history are all preserved</p>
58+
</div>
59+
60+
<div className="flex items-start gap-3 w-full">
61+
<i className="fa-sharp fa-solid fa-rotate text-accent text-lg mt-1 flex-shrink-0" />
62+
<p>Sessions automatically reconnect when your connection is restored</p>
63+
</div>
64+
65+
<div className="flex items-start gap-3 w-full">
66+
<i className="fa-sharp fa-solid fa-box text-accent text-lg mt-1 flex-shrink-0" />
67+
<p>Buffered output streams back in — you never miss a line</p>
68+
</div>
69+
70+
<p className="italic">
71+
All the persistence of tmux, built into your terminal. Look for the shield icon to
72+
enable durability on any SSH session.
73+
</p>
74+
75+
<EmojiButton emoji="🔥" isClicked={fireClicked} onClick={handleFireClick} />
76+
</div>
77+
</div>
78+
</div>
79+
<div className="w-[2px] bg-border flex-shrink-0"></div>
80+
<div className="flex items-center justify-center pl-6 flex-shrink-0 w-[400px]">
81+
<div className="flex flex-col gap-6 text-secondary">
82+
<div className="text-lg font-semibold text-foreground">Session States</div>
83+
84+
<div className="flex items-start gap-3">
85+
<i className="fa-sharp fa-solid fa-shield text-sky-500 text-xl mt-0.5" />
86+
<div>
87+
<div className="font-semibold text-foreground">Attached</div>
88+
<div className="text-sm">Session is protected and connected</div>
89+
</div>
90+
</div>
91+
92+
<div className="flex items-start gap-3">
93+
<i className="fa-sharp fa-solid fa-shield text-sky-300 text-xl mt-0.5" />
94+
<div>
95+
<div className="font-semibold text-foreground">Detached</div>
96+
<div className="text-sm">Session running, currently disconnected</div>
97+
</div>
98+
</div>
99+
100+
<div className="flex items-start gap-3">
101+
<i className="fa-sharp fa-regular fa-shield text-muted text-xl mt-0.5" />
102+
<div>
103+
<div className="font-semibold text-foreground">Standard</div>
104+
<div className="text-sm">Connection drops will end the session</div>
105+
</div>
106+
</div>
107+
108+
<div className="mt-4 p-4 bg-hover rounded-lg border border-border/50">
109+
<div className="text-sm">
110+
<div className="font-semibold text-foreground mb-2">Common use cases:</div>
111+
<ul className="space-y-1.5 ml-2">
112+
<li>• Alternative to tmux or screen</li>
113+
<li>• Long-running builds and deployments</li>
114+
<li>• Working from unstable networks</li>
115+
<li>• Surviving Wave restarts</li>
116+
</ul>
117+
</div>
118+
</div>
119+
</div>
120+
</div>
121+
</div>
122+
<OnboardingFooter currentStep={2} totalSteps={4} onNext={onNext} onPrev={onPrev} onSkip={onSkip} />
123+
</div>
124+
);
125+
};
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
// Copyright 2026, Command Line Inc.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
import { Button } from "@/app/element/button";
5+
6+
export const OnboardingFooter = ({
7+
currentStep,
8+
totalSteps,
9+
onNext,
10+
onPrev,
11+
onSkip,
12+
}: {
13+
currentStep: number;
14+
totalSteps: number;
15+
onNext: () => void;
16+
onPrev?: () => void;
17+
onSkip?: () => void;
18+
}) => {
19+
const isLastStep = currentStep === totalSteps;
20+
const buttonText = isLastStep ? "Get Started" : "Next";
21+
22+
return (
23+
<footer className="unselectable flex-shrink-0 mt-5 relative">
24+
<div className="absolute left-0 top-1/2 -translate-y-1/2 flex items-center gap-2">
25+
{currentStep > 1 && onPrev && (
26+
<button className="text-muted cursor-pointer hover:text-foreground text-[13px]" onClick={onPrev}>
27+
&lt; Prev
28+
</button>
29+
)}
30+
<span className="text-muted text-[13px]">
31+
{currentStep} of {totalSteps}
32+
</span>
33+
</div>
34+
<div className="flex flex-row items-center justify-center [&>button]:!px-5 [&>button]:!py-2 [&>button]:text-sm">
35+
<Button className="font-[600]" onClick={onNext}>
36+
{buttonText}
37+
</Button>
38+
</div>
39+
{!isLastStep && onSkip && (
40+
<button
41+
className="absolute right-0 top-1/2 -translate-y-1/2 text-muted cursor-pointer hover:text-muted-hover text-[13px]"
42+
onClick={onSkip}
43+
>
44+
Skip Feature Tour &gt;
45+
</button>
46+
)}
47+
</footer>
48+
);
49+
};

frontend/app/onboarding/onboarding-features.tsx

Lines changed: 14 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
// SPDX-License-Identifier: Apache-2.0
33

44
import Logo from "@/app/asset/logo.svg";
5-
import { Button } from "@/app/element/button";
65
import { EmojiButton } from "@/app/element/emojibutton";
76
import { MagnifyIcon } from "@/app/element/magnify";
87
import { ClientModel } from "@/app/store/client-model";
@@ -14,54 +13,11 @@ import { useEffect, useState } from "react";
1413
import { FakeChat } from "./fakechat";
1514
import { EditBashrcCommand, ViewLogoCommand, ViewShortcutsCommand } from "./onboarding-command";
1615
import { CurrentOnboardingVersion } from "./onboarding-common";
16+
import { DurableSessionPage } from "./onboarding-durable";
17+
import { OnboardingFooter } from "./onboarding-features-footer";
1718
import { FakeLayout } from "./onboarding-layout";
1819

19-
type FeaturePageName = "waveai" | "magnify" | "files";
20-
21-
const OnboardingFooter = ({
22-
currentStep,
23-
totalSteps,
24-
onNext,
25-
onPrev,
26-
onSkip,
27-
}: {
28-
currentStep: number;
29-
totalSteps: number;
30-
onNext: () => void;
31-
onPrev?: () => void;
32-
onSkip?: () => void;
33-
}) => {
34-
const isLastStep = currentStep === totalSteps;
35-
const buttonText = isLastStep ? "Get Started" : "Next";
36-
37-
return (
38-
<footer className="unselectable flex-shrink-0 mt-5 relative">
39-
<div className="absolute left-0 top-1/2 -translate-y-1/2 flex items-center gap-2">
40-
{currentStep > 1 && onPrev && (
41-
<button className="text-muted cursor-pointer hover:text-foreground text-[13px]" onClick={onPrev}>
42-
&lt; Prev
43-
</button>
44-
)}
45-
<span className="text-muted text-[13px]">
46-
{currentStep} of {totalSteps}
47-
</span>
48-
</div>
49-
<div className="flex flex-row items-center justify-center [&>button]:!px-5 [&>button]:!py-2 [&>button]:text-sm">
50-
<Button className="font-[600]" onClick={onNext}>
51-
{buttonText}
52-
</Button>
53-
</div>
54-
{!isLastStep && onSkip && (
55-
<button
56-
className="absolute right-0 top-1/2 -translate-y-1/2 text-muted cursor-pointer hover:text-muted-hover text-[13px]"
57-
onClick={onSkip}
58-
>
59-
Skip Feature Tour &gt;
60-
</button>
61-
)}
62-
</footer>
63-
);
64-
};
20+
type FeaturePageName = "waveai" | "durable" | "magnify" | "files";
6521

6622
const WaveAIPage = ({ onNext, onSkip }: { onNext: () => void; onSkip: () => void }) => {
6723
const isMac = isMacOS();
@@ -145,7 +101,7 @@ const WaveAIPage = ({ onNext, onSkip }: { onNext: () => void; onSkip: () => void
145101
</div>
146102
</div>
147103
</div>
148-
<OnboardingFooter currentStep={1} totalSteps={3} onNext={onNext} onSkip={onSkip} />
104+
<OnboardingFooter currentStep={1} totalSteps={4} onNext={onNext} onSkip={onSkip} />
149105
</div>
150106
);
151107
};
@@ -211,7 +167,7 @@ const MagnifyBlocksPage = ({
211167
<FakeLayout />
212168
</div>
213169
</div>
214-
<OnboardingFooter currentStep={2} totalSteps={3} onNext={onNext} onPrev={onPrev} onSkip={onSkip} />
170+
<OnboardingFooter currentStep={3} totalSteps={4} onNext={onNext} onPrev={onPrev} onSkip={onSkip} />
215171
</div>
216172
);
217173
};
@@ -305,7 +261,7 @@ const FilesPage = ({ onFinish, onPrev }: { onFinish: () => void; onPrev?: () =>
305261
{commands[commandIndex](handleCommandComplete)}
306262
</div>
307263
</div>
308-
<OnboardingFooter currentStep={3} totalSteps={3} onNext={onFinish} onPrev={onPrev} />
264+
<OnboardingFooter currentStep={4} totalSteps={4} onNext={onFinish} onPrev={onPrev} />
309265
</div>
310266
);
311267
};
@@ -329,15 +285,19 @@ export const OnboardingFeatures = ({ onComplete }: { onComplete: () => void }) =
329285

330286
const handleNext = () => {
331287
if (currentPage === "waveai") {
288+
setCurrentPage("durable");
289+
} else if (currentPage === "durable") {
332290
setCurrentPage("magnify");
333291
} else if (currentPage === "magnify") {
334292
setCurrentPage("files");
335293
}
336294
};
337295

338296
const handlePrev = () => {
339-
if (currentPage === "magnify") {
297+
if (currentPage === "durable") {
340298
setCurrentPage("waveai");
299+
} else if (currentPage === "magnify") {
300+
setCurrentPage("durable");
341301
} else if (currentPage === "files") {
342302
setCurrentPage("magnify");
343303
}
@@ -360,6 +320,9 @@ export const OnboardingFeatures = ({ onComplete }: { onComplete: () => void }) =
360320
case "waveai":
361321
pageComp = <WaveAIPage onNext={handleNext} onSkip={handleSkip} />;
362322
break;
323+
case "durable":
324+
pageComp = <DurableSessionPage onNext={handleNext} onSkip={handleSkip} onPrev={handlePrev} />;
325+
break;
363326
case "magnify":
364327
pageComp = <MagnifyBlocksPage onNext={handleNext} onSkip={handleSkip} onPrev={handlePrev} />;
365328
break;

frontend/types/gotypes.d.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1428,7 +1428,7 @@ declare global {
14281428
"wsh:haderror"?: boolean;
14291429
"conn:conntype"?: string;
14301430
"conn:wsherrorcode"?: string;
1431-
"onboarding:feature"?: "waveai" | "magnify" | "wsh";
1431+
"onboarding:feature"?: "waveai" | "durable" | "magnify" | "wsh";
14321432
"onboarding:version"?: string;
14331433
"onboarding:githubstar"?: "already" | "star" | "later";
14341434
"display:height"?: number;

pkg/telemetry/telemetrydata/telemetrydata.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@ type TEventProps struct {
122122
ConnType string `json:"conn:conntype,omitempty"`
123123
ConnWshErrorCode string `json:"conn:wsherrorcode,omitempty"`
124124

125-
OnboardingFeature string `json:"onboarding:feature,omitempty" tstype:"\"waveai\" | \"magnify\" | \"wsh\""`
125+
OnboardingFeature string `json:"onboarding:feature,omitempty" tstype:"\"waveai\" | \"durable\" | \"magnify\" | \"wsh\""`
126126
OnboardingVersion string `json:"onboarding:version,omitempty"`
127127
OnboardingGithubStar string `json:"onboarding:githubstar,omitempty" tstype:"\"already\" | \"star\" | \"later\""`
128128

0 commit comments

Comments
 (0)