Skip to content

Commit 50cf6b5

Browse files
authored
add an onboarding upgrade modal (#2433)
1 parent 3f41c64 commit 50cf6b5

File tree

23 files changed

+305
-42
lines changed

23 files changed

+305
-42
lines changed

docs/docs/waveai.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
---
2-
sidebar_position: 3.4
2+
sidebar_position: 1.5
33
id: "waveai"
44
title: "Wave AI"
55
---

emain/emain-window.ts

Lines changed: 39 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import { updater } from "./updater";
2424

2525
export type WindowOpts = {
2626
unamePlatform: string;
27+
isPrimaryStartupWindow?: boolean;
2728
};
2829

2930
const MIN_WINDOW_WIDTH = 800;
@@ -36,6 +37,7 @@ export const waveWindowMap = new Map<string, WaveBrowserWindow>(); // waveWindow
3637
export let focusedWaveWindow: WaveBrowserWindow = null;
3738

3839
let cachedClientId: string = null;
40+
let hasCompletedFirstRelaunch = false;
3941

4042
async function getClientId() {
4143
if (cachedClientId != null) {
@@ -51,6 +53,7 @@ type WindowActionQueueEntry =
5153
op: "switchtab";
5254
tabId: string;
5355
setInBackend: boolean;
56+
primaryStartupTab?: boolean;
5457
}
5558
| {
5659
op: "createtab";
@@ -346,31 +349,35 @@ export class WaveBrowserWindow extends BaseWindow {
346349
await this._queueActionInternal({ op: "switchworkspace", workspaceId });
347350
}
348351

349-
async setActiveTab(tabId: string, setInBackend: boolean) {
350-
console.log("setActiveTab", tabId, this.waveWindowId, this.workspaceId, setInBackend);
351-
await this._queueActionInternal({ op: "switchtab", tabId, setInBackend });
352+
async setActiveTab(tabId: string, setInBackend: boolean, primaryStartupTab = false) {
353+
console.log("setActiveTab", tabId, this.waveWindowId, this.workspaceId, setInBackend, primaryStartupTab ? "(primary startup)" : "");
354+
await this._queueActionInternal({ op: "switchtab", tabId, setInBackend, primaryStartupTab });
352355
}
353356

354-
private async initializeTab(tabView: WaveTabView) {
357+
private async initializeTab(tabView: WaveTabView, primaryStartupTab: boolean) {
355358
const clientId = await getClientId();
356359
await tabView.initPromise;
357360
this.contentView.addChildView(tabView);
358-
const initOpts = {
361+
const initOpts: WaveInitOpts = {
359362
tabId: tabView.waveTabId,
360363
clientId: clientId,
361364
windowId: this.waveWindowId,
362365
activate: true,
363366
};
367+
if (primaryStartupTab) {
368+
initOpts.primaryTabStartup = true;
369+
}
364370
tabView.savedInitOpts = { ...initOpts };
365371
tabView.savedInitOpts.activate = false;
372+
delete tabView.savedInitOpts.primaryTabStartup;
366373
let startTime = Date.now();
367-
console.log("before wave ready, init tab, sending wave-init", tabView.waveTabId);
374+
console.log("before wave ready, init tab, sending wave-init", tabView.waveTabId, primaryStartupTab ? "(primary startup)" : "");
368375
tabView.webContents.send("wave-init", initOpts);
369376
await tabView.waveReadyPromise;
370377
console.log("wave-ready init time", Date.now() - startTime + "ms");
371378
}
372379

373-
private async setTabViewIntoWindow(tabView: WaveTabView, tabInitialized: boolean) {
380+
private async setTabViewIntoWindow(tabView: WaveTabView, tabInitialized: boolean, primaryStartupTab = false) {
374381
if (this.activeTabView == tabView) {
375382
return;
376383
}
@@ -382,8 +389,8 @@ export class WaveBrowserWindow extends BaseWindow {
382389
this.activeTabView = tabView;
383390
this.allLoadedTabViews.set(tabView.waveTabId, tabView);
384391
if (!tabInitialized) {
385-
console.log("initializing a new tab");
386-
const p1 = this.initializeTab(tabView);
392+
console.log("initializing a new tab", primaryStartupTab ? "(primary startup)" : "");
393+
const p1 = this.initializeTab(tabView, primaryStartupTab);
387394
const p2 = this.repositionTabsSlowly(100);
388395
await Promise.all([p1, p2]);
389396
} else {
@@ -541,7 +548,8 @@ export class WaveBrowserWindow extends BaseWindow {
541548
return;
542549
}
543550
const [tabView, tabInitialized] = await getOrCreateWebViewForTab(this.waveWindowId, tabId);
544-
await this.setTabViewIntoWindow(tabView, tabInitialized);
551+
const primaryStartupTabFlag = entry.op === "switchtab" ? entry.primaryStartupTab ?? false : false;
552+
await this.setTabViewIntoWindow(tabView, tabInitialized, primaryStartupTabFlag);
545553
} catch (e) {
546554
console.log("error caught in processActionQueue", e);
547555
} finally {
@@ -628,6 +636,7 @@ export async function createWindowForWorkspace(workspaceId: string) {
628636
}
629637
const newBwin = await createBrowserWindow(newWin, await RpcApi.GetFullConfigCommand(ElectronWshClient), {
630638
unamePlatform,
639+
isPrimaryStartupWindow: false,
631640
});
632641
newBwin.show();
633642
}
@@ -653,7 +662,7 @@ export async function createBrowserWindow(
653662
console.log("createBrowserWindow", waveWindow.oid, workspace.oid, workspace);
654663
const bwin = new WaveBrowserWindow(waveWindow, fullConfig, opts);
655664
if (workspace.activetabid) {
656-
await bwin.setActiveTab(workspace.activetabid, false);
665+
await bwin.setActiveTab(workspace.activetabid, false, opts.isPrimaryStartupWindow ?? false);
657666
}
658667
return bwin;
659668
}
@@ -764,7 +773,10 @@ export async function createNewWaveWindow() {
764773
const existingWindowId = clientData.windowids[0];
765774
const existingWindowData = (await ObjectService.GetObject("window:" + existingWindowId)) as WaveWindow;
766775
if (existingWindowData != null) {
767-
const win = await createBrowserWindow(existingWindowData, fullConfig, { unamePlatform });
776+
const win = await createBrowserWindow(existingWindowData, fullConfig, {
777+
unamePlatform,
778+
isPrimaryStartupWindow: false,
779+
});
768780
win.show();
769781
recreatedWindow = true;
770782
}
@@ -774,7 +786,10 @@ export async function createNewWaveWindow() {
774786
return;
775787
}
776788
console.log("creating new window");
777-
const newBrowserWindow = await createBrowserWindow(null, fullConfig, { unamePlatform });
789+
const newBrowserWindow = await createBrowserWindow(null, fullConfig, {
790+
unamePlatform,
791+
isPrimaryStartupWindow: false,
792+
});
778793
newBrowserWindow.show();
779794
}
780795

@@ -793,18 +808,26 @@ export async function relaunchBrowserWindows() {
793808

794809
const clientData = await ClientService.GetClientData();
795810
const fullConfig = await RpcApi.GetFullConfigCommand(ElectronWshClient);
811+
const windowIds = clientData.windowids ?? [];
796812
const wins: WaveBrowserWindow[] = [];
797-
for (const windowId of clientData.windowids.slice().reverse()) {
813+
const isFirstRelaunch = !hasCompletedFirstRelaunch;
814+
const primaryWindowId = windowIds.length > 0 ? windowIds[0] : null;
815+
for (const windowId of windowIds.slice().reverse()) {
798816
const windowData: WaveWindow = await WindowService.GetWindow(windowId);
799817
if (windowData == null) {
800818
console.log("relaunch -- window data not found, closing window", windowId);
801819
await WindowService.CloseWindow(windowId, true);
802820
continue;
803821
}
804-
console.log("relaunch -- creating window", windowId, windowData);
805-
const win = await createBrowserWindow(windowData, fullConfig, { unamePlatform });
822+
const isPrimaryStartupWindow = isFirstRelaunch && windowId === primaryWindowId;
823+
console.log("relaunch -- creating window", windowId, windowData, isPrimaryStartupWindow ? "(primary startup)" : "");
824+
const win = await createBrowserWindow(windowData, fullConfig, {
825+
unamePlatform,
826+
isPrimaryStartupWindow
827+
});
806828
wins.push(win);
807829
}
830+
hasCompletedFirstRelaunch = true;
808831
for (const win of wins) {
809832
console.log("show window", win.waveWindowId);
810833
win.show();

emain/emain-wsh.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,10 @@ export class ElectronWshClientType extends WshClient {
5252
if (window == null) {
5353
throw new Error(`window ${windowId} not found`);
5454
}
55-
ww = await createBrowserWindow(window, fullConfig, { unamePlatform });
55+
ww = await createBrowserWindow(window, fullConfig, {
56+
unamePlatform,
57+
isPrimaryStartupWindow: false,
58+
});
5659
}
5760
ww.focus();
5861
}

emain/emain.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,10 @@ function handleWSEvent(evtMsg: WSEventType) {
9898
return;
9999
}
100100
const fullConfig = await RpcApi.GetFullConfigCommand(ElectronWshClient);
101-
const newWin = await createBrowserWindow(windowData, fullConfig, { unamePlatform });
101+
const newWin = await createBrowserWindow(windowData, fullConfig, {
102+
unamePlatform,
103+
isPrimaryStartupWindow: false,
104+
});
102105
newWin.show();
103106
} else if (evtMsg.eventtype == "electron:closewindow") {
104107
console.log("electron:closewindow", evtMsg.data);

frontend/app/aipanel/telemetryrequired.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { RpcApi } from "@/app/store/wshclientapi";
55
import { TabRpcClient } from "@/app/store/wshrpcutil";
66
import { cn } from "@/util/util";
77
import { useState } from "react";
8+
import { WaveAIModel } from "./waveai-model";
89

910
interface TelemetryRequiredMessageProps {
1011
className?: string;
@@ -17,6 +18,9 @@ const TelemetryRequiredMessage = ({ className }: TelemetryRequiredMessageProps)
1718
setIsEnabling(true);
1819
try {
1920
await RpcApi.WaveAIEnableTelemetryCommand(TabRpcClient);
21+
setTimeout(() => {
22+
WaveAIModel.getInstance().focusInput();
23+
}, 100);
2024
} catch (error) {
2125
console.error("Failed to enable telemetry:", error);
2226
setIsEnabling(false);

frontend/app/modals/modalregistry.tsx

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

44
import { MessageModal } from "@/app/modals/messagemodal";
5-
import { OnboardingModal } from "@/app/onboarding/onboarding";
5+
import { NewInstallOnboardingModal } from "@/app/onboarding/onboarding";
6+
import { UpgradeOnboardingModal } from "@/app/onboarding/onboarding-upgrade";
67
import { AboutModal } from "./about";
78
import { UserInputModal } from "./userinputmodal";
89

910
const modalRegistry: { [key: string]: React.ComponentType<any> } = {
10-
[OnboardingModal.displayName || "OnboardingModal"]: OnboardingModal,
11+
[NewInstallOnboardingModal.displayName || "NewInstallOnboardingModal"]: NewInstallOnboardingModal,
12+
[UpgradeOnboardingModal.displayName || "UpgradeOnboardingModal"]: UpgradeOnboardingModal,
1113
[UserInputModal.displayName || "UserInputModal"]: UserInputModal,
1214
[AboutModal.displayName || "AboutModal"]: AboutModal,
1315
[MessageModal.displayName || "MessageModal"]: MessageModal,

frontend/app/modals/modalsrenderer.tsx

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,20 @@
11
// Copyright 2025, Command Line Inc.
22
// SPDX-License-Identifier: Apache-2.0
33

4-
import { OnboardingModal } from "@/app/onboarding/onboarding";
5-
import { atoms, globalStore } from "@/store/global";
4+
import { NewInstallOnboardingModal } from "@/app/onboarding/onboarding";
5+
import { CurrentOnboardingVersion } from "@/app/onboarding/onboarding-features";
6+
import { UpgradeOnboardingModal } from "@/app/onboarding/onboarding-upgrade";
7+
import { atoms, globalPrimaryTabStartup, globalStore } from "@/store/global";
68
import { modalsModel } from "@/store/modalmodel";
79
import * as jotai from "jotai";
810
import { useEffect } from "react";
11+
import semver from "semver";
912
import { getModalComponent } from "./modalregistry";
1013

1114
const ModalsRenderer = () => {
1215
const clientData = jotai.useAtomValue(atoms.client);
13-
const [tosOpen, setTosOpen] = jotai.useAtom(modalsModel.tosOpen);
16+
const [newInstallOnboardingOpen, setNewInstallOnboardingOpen] = jotai.useAtom(modalsModel.newInstallOnboardingOpen);
17+
const [upgradeOnboardingOpen, setUpgradeOnboardingOpen] = jotai.useAtom(modalsModel.upgradeOnboardingOpen);
1418
const [modals] = jotai.useAtom(modalsModel.modalsAtom);
1519
const rtn: React.ReactElement[] = [];
1620
for (const modal of modals) {
@@ -19,14 +23,30 @@ const ModalsRenderer = () => {
1923
rtn.push(<ModalComponent key={modal.displayName} {...modal.props} />);
2024
}
2125
}
22-
if (tosOpen) {
23-
rtn.push(<OnboardingModal key={OnboardingModal.displayName} />);
26+
if (newInstallOnboardingOpen) {
27+
rtn.push(<NewInstallOnboardingModal key={NewInstallOnboardingModal.displayName} />);
28+
}
29+
if (upgradeOnboardingOpen) {
30+
rtn.push(<UpgradeOnboardingModal key={UpgradeOnboardingModal.displayName} />);
2431
}
2532
useEffect(() => {
2633
if (!clientData.tosagreed) {
27-
setTosOpen(true);
34+
setNewInstallOnboardingOpen(true);
2835
}
2936
}, [clientData]);
37+
38+
useEffect(() => {
39+
if (!globalPrimaryTabStartup) {
40+
return;
41+
}
42+
if (!clientData.tosagreed) {
43+
return;
44+
}
45+
const lastVersion = clientData.meta?.["onboarding:lastversion"] ?? "v0.0.0";
46+
if (semver.lt(lastVersion, CurrentOnboardingVersion)) {
47+
setUpgradeOnboardingOpen(true);
48+
}
49+
}, []);
3050
useEffect(() => {
3151
globalStore.set(atoms.modalOpen, rtn.length > 0);
3252
}, [rtn]);

frontend/app/onboarding/onboarding-features.tsx

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import Logo from "@/app/asset/logo.svg";
55
import { Button } from "@/app/element/button";
66
import { EmojiButton } from "@/app/element/emojibutton";
77
import { MagnifyIcon } from "@/app/element/magnify";
8+
import { atoms, globalStore } from "@/app/store/global";
9+
import * as WOS from "@/app/store/wos";
810
import { RpcApi } from "@/app/store/wshclientapi";
911
import { TabRpcClient } from "@/app/store/wshrpcutil";
1012
import { isMacOS } from "@/util/platformutil";
@@ -13,6 +15,8 @@ import { FakeChat } from "./fakechat";
1315
import { EditBashrcCommand, ViewLogoCommand, ViewShortcutsCommand } from "./onboarding-command";
1416
import { FakeLayout } from "./onboarding-layout";
1517

18+
export const CurrentOnboardingVersion = "v0.12.0";
19+
1620
type FeaturePageName = "waveai" | "magnify" | "files";
1721

1822
const OnboardingFooter = ({
@@ -72,6 +76,7 @@ const WaveAIPage = ({ onNext, onSkip }: { onNext: () => void; onSkip: () => void
7276
event: "onboarding:fire",
7377
props: {
7478
"onboarding:feature": "waveai",
79+
"onboarding:version": CurrentOnboardingVersion,
7580
},
7681
});
7782
}
@@ -158,6 +163,7 @@ const MagnifyBlocksPage = ({
158163
event: "onboarding:fire",
159164
props: {
160165
"onboarding:feature": "magnify",
166+
"onboarding:version": CurrentOnboardingVersion,
161167
},
162168
});
163169
}
@@ -216,6 +222,7 @@ const FilesPage = ({ onFinish, onPrev }: { onFinish: () => void; onPrev?: () =>
216222
event: "onboarding:fire",
217223
props: {
218224
"onboarding:feature": "wsh",
225+
"onboarding:version": CurrentOnboardingVersion,
219226
},
220227
});
221228
}
@@ -300,9 +307,16 @@ export const OnboardingFeatures = ({ onComplete }: { onComplete: () => void }) =
300307
const [currentPage, setCurrentPage] = useState<FeaturePageName>("waveai");
301308

302309
useEffect(() => {
310+
const clientId = globalStore.get(atoms.clientId);
311+
RpcApi.SetMetaCommand(TabRpcClient, {
312+
oref: WOS.makeORef("client", clientId),
313+
meta: { "onboarding:lastversion": CurrentOnboardingVersion },
314+
});
303315
RpcApi.RecordTEventCommand(TabRpcClient, {
304316
event: "onboarding:start",
305-
props: {},
317+
props: {
318+
"onboarding:version": CurrentOnboardingVersion,
319+
},
306320
});
307321
}, []);
308322

0 commit comments

Comments
 (0)