From f736f17700b42f4c6559a38a9d2677e0582cd9ce Mon Sep 17 00:00:00 2001
From: sawka
Date: Fri, 10 Oct 2025 12:35:59 -0700
Subject: [PATCH 01/16] onboarding updates for waveai, telemetry, github stars
---
frontend/app/modals/tos.tsx | 102 +++++++++++++++++++++++++++++++-----
frontend/types/gotypes.d.ts | 2 +
pkg/waveobj/metaconsts.go | 2 +
pkg/waveobj/wtypemeta.go | 2 +
pkg/wcore/workspace.go | 8 +--
5 files changed, 95 insertions(+), 21 deletions(-)
diff --git a/frontend/app/modals/tos.tsx b/frontend/app/modals/tos.tsx
index a55760e1e0..c46fea0b57 100644
--- a/frontend/app/modals/tos.tsx
+++ b/frontend/app/modals/tos.tsx
@@ -4,6 +4,7 @@
import Logo from "@/app/asset/logo.svg";
import { Button } from "@/app/element/button";
import { Toggle } from "@/app/element/toggle";
+import { WorkspaceLayoutModel } from "@/app/workspace/workspace-layout-model";
import * as services from "@/store/services";
import { OverlayScrollbarsComponent } from "overlayscrollbars-react";
import { useEffect, useRef, useState } from "react";
@@ -11,24 +12,36 @@ import { debounce } from "throttle-debounce";
import { FlexiModal } from "./modal";
import { QuickTips } from "@/app/element/quicktips";
-import { atoms } from "@/app/store/global";
+import { atoms, globalStore } from "@/app/store/global";
import { modalsModel } from "@/app/store/modalmodel";
+import * as WOS from "@/app/store/wos";
+import { RpcApi } from "@/app/store/wshclientapi";
+import { TabRpcClient } from "@/app/store/wshrpcutil";
import { fireAndForget } from "@/util/util";
import { atom, PrimitiveAtom, useAtom, useAtomValue, useSetAtom } from "jotai";
-const pageNumAtom: PrimitiveAtom = atom(1);
+// Page flow:
+// init -> (telemetry enabled) -> quicktips
+// init -> (telemetry disabled) -> notelemetrystar -> quicktips
-const ModalPage1 = ({ isCompact }: { isCompact: boolean }) => {
+type PageName = "init" | "notelemetrystar" | "quicktips";
+
+const pageNameAtom: PrimitiveAtom = atom("init");
+
+const InitPage = ({ isCompact }: { isCompact: boolean }) => {
const settings = useAtomValue(atoms.settingsAtom);
const clientData = useAtomValue(atoms.client);
const [telemetryEnabled, setTelemetryEnabled] = useState(!!settings["telemetry:enabled"]);
- const setPageNum = useSetAtom(pageNumAtom);
+ const setPageName = useSetAtom(pageNameAtom);
const acceptTos = () => {
if (!clientData.tosagreed) {
fireAndForget(services.ClientService.AgreeTos);
}
- setPageNum(2);
+ if (telemetryEnabled) {
+ WorkspaceLayoutModel.getInstance().setAIPanelVisible(true);
+ }
+ setPageName(telemetryEnabled ? "quicktips" : "notelemetrystar");
};
const setTelemetry = (value: boolean) => {
@@ -129,7 +142,65 @@ const ModalPage1 = ({ isCompact }: { isCompact: boolean }) => {
);
};
-const ModalPage2 = ({ isCompact }: { isCompact: boolean }) => {
+const NoTelemetryStarPage = ({ isCompact }: { isCompact: boolean }) => {
+ const setPageName = useSetAtom(pageNameAtom);
+
+ const handleStarClick = async () => {
+ const clientId = globalStore.get(atoms.clientId);
+ await RpcApi.SetMetaCommand(TabRpcClient, {
+ oref: WOS.makeORef("client", clientId),
+ meta: { "onboarding:githubstar": true },
+ });
+ window.open("https://github.com/wavetermdev/waveterm", "_blank");
+ setPageName("quicktips");
+ };
+
+ const handleMaybeLater = async () => {
+ const clientId = globalStore.get(atoms.clientId);
+ await RpcApi.SetMetaCommand(TabRpcClient, {
+ oref: WOS.makeORef("client", clientId),
+ meta: { "onboarding:githubstar": false },
+ });
+ setPageName("quicktips");
+ };
+
+ return (
+
+
+
+
+
+ Telemetry Disabled β
+
+
+
+
+
No problem, we respect your privacy.
+
+ But, without usage data, we're flying blind. A GitHub star helps us know Wave is useful and
+ worth maintaining.
+
+
+
+
+
+
+ );
+};
+
+const QuickTipsPage = ({ isCompact }: { isCompact: boolean }) => {
const [tosOpen, setTosOpen] = useAtom(modalsModel.tosOpen);
const handleGetStarted = () => {
@@ -167,7 +238,7 @@ const ModalPage2 = ({ isCompact }: { isCompact: boolean }) => {
const TosModal = () => {
const modalRef = useRef(null);
- const [pageNum, setPageNum] = useAtom(pageNumAtom);
+ const [pageName, setPageName] = useAtom(pageNameAtom);
const clientData = useAtomValue(atoms.client);
const [isCompact, setIsCompact] = useState(window.innerHeight < 800);
@@ -187,10 +258,10 @@ const TosModal = () => {
useEffect(() => {
if (clientData.tosagreed) {
- setPageNum(2);
+ setPageName("quicktips");
}
return () => {
- setPageNum(1);
+ setPageName("init");
};
}, []);
@@ -204,12 +275,15 @@ const TosModal = () => {
}, []);
let pageComp: React.JSX.Element = null;
- switch (pageNum) {
- case 1:
- pageComp = ;
+ switch (pageName) {
+ case "init":
+ pageComp = ;
+ break;
+ case "notelemetrystar":
+ pageComp = ;
break;
- case 2:
- pageComp = ;
+ case "quicktips":
+ pageComp = ;
break;
}
if (pageComp == null) {
diff --git a/frontend/types/gotypes.d.ts b/frontend/types/gotypes.d.ts
index 14ea692ffa..b283757b5f 100644
--- a/frontend/types/gotypes.d.ts
+++ b/frontend/types/gotypes.d.ts
@@ -667,6 +667,7 @@ declare global {
"vdom:correlationid"?: string;
"vdom:route"?: string;
"vdom:persist"?: boolean;
+ "onboarding:githubstar"?: boolean;
count?: number;
};
@@ -949,6 +950,7 @@ declare global {
"waveai:model"?: string;
"waveai:inputtokens"?: number;
"waveai:outputtokens"?: number;
+ "waveai:nativewebsearchcount"?: number;
"waveai:requestcount"?: number;
"waveai:toolusecount"?: number;
"waveai:tooluseerrorcount"?: number;
diff --git a/pkg/waveobj/metaconsts.go b/pkg/waveobj/metaconsts.go
index 4e762a7ab8..bbd05a1701 100644
--- a/pkg/waveobj/metaconsts.go
+++ b/pkg/waveobj/metaconsts.go
@@ -132,6 +132,8 @@ const (
MetaKey_VDomRoute = "vdom:route"
MetaKey_VDomPersist = "vdom:persist"
+ MetaKey_OnboardingGithubStar = "onboarding:githubstar"
+
MetaKey_Count = "count"
)
diff --git a/pkg/waveobj/wtypemeta.go b/pkg/waveobj/wtypemeta.go
index 4651f14126..c8b73619cb 100644
--- a/pkg/waveobj/wtypemeta.go
+++ b/pkg/waveobj/wtypemeta.go
@@ -136,6 +136,8 @@ type MetaTSType struct {
VDomRoute string `json:"vdom:route,omitempty"`
VDomPersist bool `json:"vdom:persist,omitempty"`
+ OnboardingGithubStar bool `json:"onboarding:githubstar,omitempty"` // for client
+
Count int `json:"count,omitempty"` // temp for cpu plot. will remove later
}
diff --git a/pkg/wcore/workspace.go b/pkg/wcore/workspace.go
index 385255378e..fa430c70dd 100644
--- a/pkg/wcore/workspace.go
+++ b/pkg/wcore/workspace.go
@@ -208,13 +208,7 @@ func CreateTab(ctx context.Context, workspaceId string, tabName string, activate
}
// The initial tab for the initial launch should be pinned
- var meta waveobj.MetaMapType
- if isInitialLaunch {
- meta = waveobj.MetaMapType{
- "waveai:panelopen": true,
- }
- }
- tab, err := createTabObj(ctx, workspaceId, tabName, pinned || isInitialLaunch, meta)
+ tab, err := createTabObj(ctx, workspaceId, tabName, pinned || isInitialLaunch, nil)
if err != nil {
return "", fmt.Errorf("error creating tab: %w", err)
}
From c33357cc7b401dca7f87f49ffc96fae891bfb2eb Mon Sep 17 00:00:00 2001
From: sawka
Date: Fri, 10 Oct 2025 14:00:17 -0700
Subject: [PATCH 02/16] move tos to new "onboarding" directory, rename...
---
frontend/app/modals/modalregistry.tsx | 2 +-
frontend/app/modals/modalsrenderer.tsx | 2 +-
frontend/app/{modals/tos.tsx => onboarding/onboarding.tsx} | 4 ++--
3 files changed, 4 insertions(+), 4 deletions(-)
rename frontend/app/{modals/tos.tsx => onboarding/onboarding.tsx} (99%)
diff --git a/frontend/app/modals/modalregistry.tsx b/frontend/app/modals/modalregistry.tsx
index b624380f26..434a9c0dde 100644
--- a/frontend/app/modals/modalregistry.tsx
+++ b/frontend/app/modals/modalregistry.tsx
@@ -2,8 +2,8 @@
// SPDX-License-Identifier: Apache-2.0
import { MessageModal } from "@/app/modals/messagemodal";
+import { TosModal } from "@/app/onboarding/onboarding";
import { AboutModal } from "./about";
-import { TosModal } from "./tos";
import { UserInputModal } from "./userinputmodal";
const modalRegistry: { [key: string]: React.ComponentType } = {
diff --git a/frontend/app/modals/modalsrenderer.tsx b/frontend/app/modals/modalsrenderer.tsx
index ae9e57fca4..8bc7944b55 100644
--- a/frontend/app/modals/modalsrenderer.tsx
+++ b/frontend/app/modals/modalsrenderer.tsx
@@ -3,10 +3,10 @@
import { atoms, globalStore } from "@/store/global";
import { modalsModel } from "@/store/modalmodel";
+import { TosModal } from "@/app/onboarding/onboarding";
import * as jotai from "jotai";
import { useEffect } from "react";
import { getModalComponent } from "./modalregistry";
-import { TosModal } from "./tos";
const ModalsRenderer = () => {
const clientData = jotai.useAtomValue(atoms.client);
diff --git a/frontend/app/modals/tos.tsx b/frontend/app/onboarding/onboarding.tsx
similarity index 99%
rename from frontend/app/modals/tos.tsx
rename to frontend/app/onboarding/onboarding.tsx
index c46fea0b57..f73f3f3073 100644
--- a/frontend/app/modals/tos.tsx
+++ b/frontend/app/onboarding/onboarding.tsx
@@ -9,7 +9,7 @@ import * as services from "@/store/services";
import { OverlayScrollbarsComponent } from "overlayscrollbars-react";
import { useEffect, useRef, useState } from "react";
import { debounce } from "throttle-debounce";
-import { FlexiModal } from "./modal";
+import { FlexiModal } from "@/app/modals/modal";
import { QuickTips } from "@/app/element/quicktips";
import { atoms, globalStore } from "@/app/store/global";
@@ -301,4 +301,4 @@ const TosModal = () => {
TosModal.displayName = "TosModal";
-export { TosModal };
+export { TosModal };
\ No newline at end of file
From 89c05db0bcecee7471f30b8d04c637d1f11cdb7d Mon Sep 17 00:00:00 2001
From: sawka
Date: Fri, 10 Oct 2025 16:04:27 -0700
Subject: [PATCH 03/16] tighten up spacing
---
frontend/app/element/streamdown.tsx | 20 +++++++++++++++-----
1 file changed, 15 insertions(+), 5 deletions(-)
diff --git a/frontend/app/element/streamdown.tsx b/frontend/app/element/streamdown.tsx
index 3eadb271b5..7318705899 100644
--- a/frontend/app/element/streamdown.tsx
+++ b/frontend/app/element/streamdown.tsx
@@ -152,7 +152,11 @@ const CodeBlock = ({ children, onClickExecute, codeBlockMaxWidthAtom }: CodeBloc
return (
{language}
@@ -254,13 +258,19 @@ export const WaveStreamdown = ({
),
ul: (props: React.HTMLAttributes
) => (
-
+
),
ol: (props: React.HTMLAttributes) => (
-
+
),
li: (props: React.HTMLAttributes) => (
-
+
),
blockquote: (props: React.HTMLAttributes) => (
@@ -295,7 +305,7 @@ export const WaveStreamdown = ({
*:first-child]:mt-0 [&>*:first-child>*:first-child]:mt-0",
+ "wave-streamdown text-secondary [&>*:first-child]:mt-0 [&>*:first-child>*:first-child]:mt-0 space-y-2",
className
)}
shikiTheme={[ShikiTheme, ShikiTheme]}
From 1b07fde9a01cfd2f51f8cf7f6a5730fdc51bb285 Mon Sep 17 00:00:00 2001
From: sawka
Date: Fri, 10 Oct 2025 17:12:44 -0700
Subject: [PATCH 04/16] really nice magnify feature card and new emojibutton
for feedback
---
frontend/app/element/emojibutton.tsx | 46 +++
frontend/app/modals/modalsrenderer.tsx | 4 +-
.../app/onboarding/onboarding-features.tsx | 328 ++++++++++++++++++
frontend/app/onboarding/onboarding.tsx | 31 +-
frontend/tailwindsetup.css | 11 +
5 files changed, 410 insertions(+), 10 deletions(-)
create mode 100644 frontend/app/element/emojibutton.tsx
create mode 100644 frontend/app/onboarding/onboarding-features.tsx
diff --git a/frontend/app/element/emojibutton.tsx b/frontend/app/element/emojibutton.tsx
new file mode 100644
index 0000000000..00265ba070
--- /dev/null
+++ b/frontend/app/element/emojibutton.tsx
@@ -0,0 +1,46 @@
+// Copyright 2025, Command Line Inc.
+// SPDX-License-Identifier: Apache-2.0
+
+import { cn } from "@/util/util";
+import { useLayoutEffect, useRef, useState } from "react";
+
+export const EmojiButton = ({ emoji, isClicked, onClick, className }: { emoji: string; isClicked: boolean; onClick: () => void; className?: string }) => {
+ const [showFloating, setShowFloating] = useState(false);
+ const prevClickedRef = useRef(isClicked);
+
+ useLayoutEffect(() => {
+ if (isClicked && !prevClickedRef.current) {
+ setShowFloating(true);
+ setTimeout(() => setShowFloating(false), 600);
+ }
+ prevClickedRef.current = isClicked;
+ }, [isClicked]);
+
+ return (
+
+
+ {emoji}
+
+ {showFloating && (
+
+ {emoji}
+
+ )}
+
+ );
+};
\ No newline at end of file
diff --git a/frontend/app/modals/modalsrenderer.tsx b/frontend/app/modals/modalsrenderer.tsx
index 8bc7944b55..4513f5f7da 100644
--- a/frontend/app/modals/modalsrenderer.tsx
+++ b/frontend/app/modals/modalsrenderer.tsx
@@ -1,9 +1,9 @@
// Copyright 2025, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
+import { TosModal } from "@/app/onboarding/onboarding";
import { atoms, globalStore } from "@/store/global";
import { modalsModel } from "@/store/modalmodel";
-import { TosModal } from "@/app/onboarding/onboarding";
import * as jotai from "jotai";
import { useEffect } from "react";
import { getModalComponent } from "./modalregistry";
@@ -23,7 +23,7 @@ const ModalsRenderer = () => {
rtn.push( );
}
useEffect(() => {
- if (!clientData.tosagreed) {
+ if (!clientData.tosagreed || true) {
setTosOpen(true);
}
}, [clientData]);
diff --git a/frontend/app/onboarding/onboarding-features.tsx b/frontend/app/onboarding/onboarding-features.tsx
new file mode 100644
index 0000000000..5c1bd53e92
--- /dev/null
+++ b/frontend/app/onboarding/onboarding-features.tsx
@@ -0,0 +1,328 @@
+// Copyright 2025, Command Line Inc.
+// SPDX-License-Identifier: Apache-2.0
+
+import Logo from "@/app/asset/logo.svg";
+import { Button } from "@/app/element/button";
+import { EmojiButton } from "@/app/element/emojibutton";
+import { MagnifyIcon } from "@/app/element/magnify";
+import { isMacOS } from "@/util/platformutil";
+import { cn, makeIconClass } from "@/util/util";
+import { OverlayScrollbarsComponent } from "overlayscrollbars-react";
+import { useLayoutEffect, useRef, useState } from "react";
+
+type FeaturePageName = "waveai" | "magnify" | "files";
+
+const FakeBlock = ({ icon, name, highlighted, className }: { icon: string; name: string; highlighted?: boolean; className?: string }) => {
+ return (
+
+
+
+ {name}
+
+
+
+
+
+
+
+
+
+ );
+};
+
+const FakeLayout = () => {
+ const layoutRef = useRef(null);
+ const highlightedContainerRef = useRef(null);
+ const [blockRect, setBlockRect] = useState<{ left: number; top: number; width: number; height: number } | null>(null);
+ const [isExpanded, setIsExpanded] = useState(false);
+
+ useLayoutEffect(() => {
+ if (highlightedContainerRef.current) {
+ const elem = highlightedContainerRef.current;
+ setBlockRect({
+ left: elem.offsetLeft,
+ top: elem.offsetTop,
+ width: elem.offsetWidth,
+ height: elem.offsetHeight,
+ });
+ }
+ }, []);
+
+ useLayoutEffect(() => {
+ if (!blockRect) return;
+
+ const timeouts: NodeJS.Timeout[] = [];
+
+ const addTimeout = (callback: () => void, delay: number) => {
+ const id = setTimeout(callback, delay);
+ timeouts.push(id);
+ };
+
+ const runAnimationCycle = (isFirstRun: boolean) => {
+ const initialDelay = isFirstRun ? 1500 : 3000;
+
+ addTimeout(() => {
+ setIsExpanded(true);
+ addTimeout(() => {
+ setIsExpanded(false);
+ addTimeout(() => runAnimationCycle(false), 3000);
+ }, 3200);
+ }, initialDelay);
+ };
+
+ runAnimationCycle(true);
+
+ return () => {
+ timeouts.forEach(clearTimeout);
+ };
+ }, [blockRect]);
+
+ const getAnimatedStyle = () => {
+ if (!blockRect || !layoutRef.current) {
+ return {
+ left: blockRect?.left ?? 0,
+ top: blockRect?.top ?? 0,
+ width: blockRect?.width ?? 0,
+ height: blockRect?.height ?? 0,
+ };
+ }
+
+ if (isExpanded) {
+ const layoutWidth = layoutRef.current.offsetWidth;
+ const layoutHeight = layoutRef.current.offsetHeight;
+ const targetWidth = layoutWidth * 0.85;
+ const targetHeight = layoutHeight * 0.85;
+
+ return {
+ left: (layoutWidth - targetWidth) / 2,
+ top: (layoutHeight - targetHeight) / 2,
+ width: targetWidth,
+ height: targetHeight,
+ };
+ }
+
+ return {
+ left: blockRect.left,
+ top: blockRect.top,
+ width: blockRect.width,
+ height: blockRect.height,
+ };
+ };
+
+ return (
+
+
+
+
+
+ {blockRect && (
+ <>
+
+
+
+
+ >
+ )}
+
+ );
+};
+
+const OnboardingFooter = ({
+ currentStep,
+ totalSteps,
+ onNext,
+ onSkip,
+}: {
+ currentStep: number;
+ totalSteps: number;
+ onNext: () => void;
+ onSkip?: () => void;
+}) => {
+ const isLastStep = currentStep === totalSteps;
+ const buttonText = isLastStep ? "Get Started" : "Next";
+
+ return (
+
+
+ {currentStep} of {totalSteps}
+
+
+
+ {buttonText}
+
+
+ {!isLastStep && onSkip && (
+
+ Skip Feature Tour >
+
+ )}
+
+ );
+};
+
+const WaveAIPage = ({ onNext, onSkip }: { onNext: () => void; onSkip: () => void }) => {
+ return (
+
+
+
+
+
+
Wave AI helps you work smarter with intelligent assistance directly in your terminal.
+
+ Ask questions, get code suggestions, and leverage AI-powered features to enhance your
+ workflow.
+
+
+
+
+
+
+
+
+ );
+};
+
+const MagnifyBlocksPage = ({ onNext, onSkip }: { onNext: () => void; onSkip: () => void }) => {
+ const isMac = isMacOS();
+ const shortcutKey = isMac ? "β" : "Alt";
+ const [fireClicked, setFireClicked] = useState(false);
+
+ return (
+
+
+
+
+
{shortcutKey}-M
+
+
+ Magnify any block to focus on what matters. Expand terminals, editors, and previews for a
+ better view.
+
+
Use the magnify feature to work with complex outputs and large files more efficiently.
+
+ You can also magnify a block by clicking on the{" "}
+
+
+ {" "}
+ icon in the block header.
+
+
+ A quick {shortcutKey}-M to magnify and another {shortcutKey}-M to unmagnify
+
+
setFireClicked(!fireClicked)} />
+
+
+
+
+
+
+
+
+
+ );
+};
+
+const FilesPage = ({ onFinish }: { onFinish: () => void }) => {
+ return (
+
+
+
+
+
+ Viewing/Editing Files
+
+
+
+
+
+ View and edit files directly in Wave Terminal with syntax highlighting and code completion.
+
+
Seamlessly switch between terminal commands and file editing in one unified interface.
+
+
+
+
+
+
+
+ );
+};
+
+export const OnboardingFeatures = ({ onComplete }: { onComplete: () => void }) => {
+ const [currentPage, setCurrentPage] = useState("waveai");
+
+ const handleNext = () => {
+ if (currentPage === "waveai") {
+ setCurrentPage("magnify");
+ } else if (currentPage === "magnify") {
+ setCurrentPage("files");
+ }
+ };
+
+ const handleSkip = () => {
+ onComplete();
+ };
+
+ const handleFinish = () => {
+ onComplete();
+ };
+
+ let pageComp: React.JSX.Element = null;
+ switch (currentPage) {
+ case "waveai":
+ pageComp = ;
+ break;
+ case "magnify":
+ pageComp = ;
+ break;
+ case "files":
+ pageComp = ;
+ break;
+ }
+
+ return {pageComp}
;
+};
diff --git a/frontend/app/onboarding/onboarding.tsx b/frontend/app/onboarding/onboarding.tsx
index f73f3f3073..d55b81d129 100644
--- a/frontend/app/onboarding/onboarding.tsx
+++ b/frontend/app/onboarding/onboarding.tsx
@@ -12,6 +12,7 @@ import { debounce } from "throttle-debounce";
import { FlexiModal } from "@/app/modals/modal";
import { QuickTips } from "@/app/element/quicktips";
+import { OnboardingFeatures } from "@/app/onboarding/onboarding-features";
import { atoms, globalStore } from "@/app/store/global";
import { modalsModel } from "@/app/store/modalmodel";
import * as WOS from "@/app/store/wos";
@@ -21,10 +22,10 @@ import { fireAndForget } from "@/util/util";
import { atom, PrimitiveAtom, useAtom, useAtomValue, useSetAtom } from "jotai";
// Page flow:
-// init -> (telemetry enabled) -> quicktips
-// init -> (telemetry disabled) -> notelemetrystar -> quicktips
+// init -> (telemetry enabled) -> features
+// init -> (telemetry disabled) -> notelemetrystar -> features
-type PageName = "init" | "notelemetrystar" | "quicktips";
+type PageName = "init" | "notelemetrystar" | "features" | "quicktips";
const pageNameAtom: PrimitiveAtom = atom("init");
@@ -41,7 +42,7 @@ const InitPage = ({ isCompact }: { isCompact: boolean }) => {
if (telemetryEnabled) {
WorkspaceLayoutModel.getInstance().setAIPanelVisible(true);
}
- setPageName(telemetryEnabled ? "quicktips" : "notelemetrystar");
+ setPageName(telemetryEnabled ? "features" : "notelemetrystar");
};
const setTelemetry = (value: boolean) => {
@@ -152,7 +153,7 @@ const NoTelemetryStarPage = ({ isCompact }: { isCompact: boolean }) => {
meta: { "onboarding:githubstar": true },
});
window.open("https://github.com/wavetermdev/waveterm", "_blank");
- setPageName("quicktips");
+ setPageName("features");
};
const handleMaybeLater = async () => {
@@ -161,7 +162,7 @@ const NoTelemetryStarPage = ({ isCompact }: { isCompact: boolean }) => {
oref: WOS.makeORef("client", clientId),
meta: { "onboarding:githubstar": false },
});
- setPageName("quicktips");
+ setPageName("features");
};
return (
@@ -200,6 +201,16 @@ const NoTelemetryStarPage = ({ isCompact }: { isCompact: boolean }) => {
);
};
+const FeaturesPage = () => {
+ const [tosOpen, setTosOpen] = useAtom(modalsModel.tosOpen);
+
+ const handleComplete = () => {
+ setTosOpen(false);
+ };
+
+ return ;
+};
+
const QuickTipsPage = ({ isCompact }: { isCompact: boolean }) => {
const [tosOpen, setTosOpen] = useAtom(modalsModel.tosOpen);
@@ -258,7 +269,7 @@ const TosModal = () => {
useEffect(() => {
if (clientData.tosagreed) {
- setPageName("quicktips");
+ setPageName("features");
}
return () => {
setPageName("init");
@@ -282,6 +293,9 @@ const TosModal = () => {
case "notelemetrystar":
pageComp = ;
break;
+ case "features":
+ pageComp = ;
+ break;
case "quicktips":
pageComp = ;
break;
@@ -291,9 +305,10 @@ const TosModal = () => {
}
const paddingClass = isCompact ? "!py-3 !px-[30px]" : "!p-[30px]";
+ const widthClass = pageName === "features" ? "w-[800px]" : "w-[560px]";
return (
-
+
{pageComp}
);
diff --git a/frontend/tailwindsetup.css b/frontend/tailwindsetup.css
index 858c7cde05..a0963065d0 100644
--- a/frontend/tailwindsetup.css
+++ b/frontend/tailwindsetup.css
@@ -97,3 +97,14 @@ svg [aria-label="tip"] g path {
overflow: hidden;
text-overflow: ellipsis;
}
+
+@keyframes float-up {
+ 0% {
+ transform: translate(-50%, 0);
+ opacity: 1;
+ }
+ 100% {
+ transform: translate(-50%, -40px);
+ opacity: 0;
+ }
+}
From bc7d7a21d4d745be6b2fedb588c908ef4bdbabbb Mon Sep 17 00:00:00 2001
From: sawka
Date: Fri, 10 Oct 2025 18:41:36 -0700
Subject: [PATCH 05/16] new fake chat interface for the feature tour, looks
great with streamdown support and everything
---
frontend/app/onboarding/fakechat.tsx | 271 ++++++++++++++++++
.../app/onboarding/onboarding-features.tsx | 55 +++-
2 files changed, 314 insertions(+), 12 deletions(-)
create mode 100644 frontend/app/onboarding/fakechat.tsx
diff --git a/frontend/app/onboarding/fakechat.tsx b/frontend/app/onboarding/fakechat.tsx
new file mode 100644
index 0000000000..0224cf7666
--- /dev/null
+++ b/frontend/app/onboarding/fakechat.tsx
@@ -0,0 +1,271 @@
+// Copyright 2025, Command Line Inc.
+// SPDX-License-Identifier: Apache-2.0
+
+import { WaveStreamdown } from "@/app/element/streamdown";
+import { memo, useEffect, useRef, useState } from "react";
+
+interface ChatConfig {
+ userPrompt: string;
+ toolName: string;
+ toolDescription: string;
+ markdownResponse: string;
+}
+
+const chatConfigs: ChatConfig[] = [
+ {
+ userPrompt: "Check out ~/waveterm and summarize the project β what it does and how it's organized.",
+ toolName: "read_dir",
+ toolDescription: 'reading directory "~/waveterm"',
+ markdownResponse: `Here's a quick, file-structureβdriven overview of this repo (Wave Terminal):
+
+## What it is
+- Electron + React front end with a Go backend ("wavesrv"). Provides a terminal with GUI widgets, previews, web, and AI. (README.md)
+- Licensed Apache-2.0. (LICENSE)
+
+## Architecture at a glance
+- **Electron main process:** \`emain/*.ts\` configures windows, menus, preload scripts, updater, and ties into the Go backend via local RPC. (\`emain/\`)
+- **Renderer UI:** React/TS built with Vite, Tailwind. (\`frontend/\`, \`index.html\`, \`electron.vite.config.ts\`)
+- **Go backend ("wavesrv"):** starts services, web and websocket listeners, telemetry loops, config watcher, local RPC, filestore and SQLite-backed object store. (\`cmd/server/main-server.go\`, \`pkg/*\`)
+- **CLI/helper ("wsh"):** built for multiple OS/arch; used for shell integration and remote operations. (\`cmd/wsh/\`, \`Taskfile.yml build:wsh\`)
+
+## Key directories
+- **cmd/:** entrypoints and generators
+ - \`server/\`: wavesrv main
+ - \`generategs/\`, \`generatego/\`: TS/Go bindings generation
+ - \`wsh/\`: shell helper
+
+- **pkg/:** backend packages
+ - \`wcore/\`: startup coordination, initial data, window/workspace creation
+ - \`web/\`: HTTP+WS servers, Unix listener
+ - \`waveai/\`: AI backends (OpenAI, Anthropic, Google, Perplexity)
+ - \`wshrpc\`, \`wshutil\`, \`wslconn\`, \`remote/*\`: local/remote RPC, WSL, SSH, fileshare
+ - \`wstore/\`: persistent store (SQLite via sqlx); \`waveobj/\`: object model
+ - \`filestore/\`: local file cache/storage
+ - \`telemetry/\`: metrics/events, periodic reporting`,
+ },
+ {
+ userPrompt: "can you analyze the log output in my terminal?",
+ toolName: "term_get_scrollback",
+ toolDescription: "reading terminal output from widget",
+ markdownResponse: `Here's what your log shows:
+
+- Normal startup/init at 18:06:51 (router/register, DBGetAllObjsByType, Stat/Read).
+- Many FocusWindow events β normal UI activity.
+- 18:08:28.558 WebSocket ReadPump error: close 1001 (going away) β usually a tab/app reload, sleep, or brief network hiccup.
+- 18:08:29 re-init + new WebSocket connection β successful auto-reconnect.
+- SetMetaCommand and periodic filestore flushes with err: β normal; no failures.
+- No panics/fatal errors; just the transient disconnect.
+
+If that disconnect was unexpected, check for a tab reload/sleep at 18:08:28, and watch for repeats.
+
+Want deeper triage? Run these and paste results:
+
+\`\`\`bash
+# 1) Scan recent logs for problems
+grep -Ei 'error|panic|fatal|websocket|1006|1011' \\
+ "$HOME/Library/Application Support/waveterm-dev/waveapp.log" | tail -n 200
+
+# 2) Inspect around the disconnect window
+awk '($0 ~ /2025-10-10 18:08:2[0-9]/){print}' \\
+ "$HOME/Library/Application Support/waveterm-dev/waveapp.log"
+
+# 3) Live follow for recurring drops
+tail -f "$HOME/Library/Application Support/waveterm-dev/waveapp.log" \\
+ | grep -Ei 'error|panic|fatal|websocket|close'
+\`\`\`
+
+Need me to look at a longer slice (e.g., last 1000 lines) or a different time range?`,
+ },
+];
+
+const AIThinking = memo(() => (
+
+
+
+
+
+
+
AI is thinking...
+
+));
+
+AIThinking.displayName = "AIThinking";
+
+const FakeToolCall = memo(({ toolName, toolDescription }: { toolName: string; toolDescription: string }) => {
+ return (
+
+
β
+
+
{toolName}
+
{toolDescription}
+
+
+ );
+});
+
+FakeToolCall.displayName = "FakeToolCall";
+
+const FakeUserMessage = memo(({ userPrompt }: { userPrompt: string }) => {
+ return (
+
+ );
+});
+
+FakeUserMessage.displayName = "FakeUserMessage";
+
+const FakeAssistantMessage = memo(({ config, onComplete }: { config: ChatConfig; onComplete?: () => void }) => {
+ const [phase, setPhase] = useState<"thinking" | "tool" | "streaming">("thinking");
+ const [streamedText, setStreamedText] = useState("");
+
+ useEffect(() => {
+ const timeouts: NodeJS.Timeout[] = [];
+ let streamInterval: NodeJS.Timeout | null = null;
+
+ const runAnimation = () => {
+ setPhase("thinking");
+ setStreamedText("");
+
+ timeouts.push(
+ setTimeout(() => {
+ setPhase("tool");
+ }, 2000)
+ );
+
+ timeouts.push(
+ setTimeout(() => {
+ setPhase("streaming");
+ }, 4000)
+ );
+
+ timeouts.push(
+ setTimeout(() => {
+ let currentIndex = 0;
+ streamInterval = setInterval(() => {
+ if (currentIndex >= config.markdownResponse.length) {
+ if (streamInterval) {
+ clearInterval(streamInterval);
+ streamInterval = null;
+ }
+ if (onComplete) {
+ onComplete();
+ }
+ return;
+ }
+ currentIndex += 10;
+ setStreamedText(config.markdownResponse.slice(0, currentIndex));
+ }, 100);
+ }, 4000)
+ );
+ };
+
+ runAnimation();
+
+ return () => {
+ timeouts.forEach(clearTimeout);
+ if (streamInterval) {
+ clearInterval(streamInterval);
+ }
+ };
+ }, [config.markdownResponse, onComplete]);
+
+ return (
+
+
+ {phase === "thinking" &&
}
+ {phase === "tool" && (
+ <>
+
+
+
+
+ >
+ )}
+ {phase === "streaming" && (
+ <>
+
+
+
+
+ >
+ )}
+
+
+ );
+});
+
+FakeAssistantMessage.displayName = "FakeAssistantMessage";
+
+const FakeAIPanelHeader = memo(() => {
+ return (
+
+
+
+ Wave AI
+
+
+
+
+ Context
+
+
+
+ ON
+
+
+
+
+
+
+
+
+
+ );
+});
+
+FakeAIPanelHeader.displayName = "FakeAIPanelHeader";
+
+export const FakeChat = memo(() => {
+ const scrollRef = useRef(null);
+ const [chatIndex, setChatIndex] = useState(1);
+ const config = chatConfigs[chatIndex] || chatConfigs[0];
+
+ useEffect(() => {
+ const interval = setInterval(() => {
+ if (scrollRef.current) {
+ scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
+ }
+ }, 1000);
+
+ return () => clearInterval(interval);
+ }, []);
+
+ const handleComplete = () => {
+ setTimeout(() => {
+ setChatIndex((prev) => (prev + 1) % chatConfigs.length);
+ }, 2000);
+ };
+
+ return (
+
+ );
+});
+
+FakeChat.displayName = "FakeChat";
diff --git a/frontend/app/onboarding/onboarding-features.tsx b/frontend/app/onboarding/onboarding-features.tsx
index 5c1bd53e92..94f6d0c6b1 100644
--- a/frontend/app/onboarding/onboarding-features.tsx
+++ b/frontend/app/onboarding/onboarding-features.tsx
@@ -9,6 +9,7 @@ import { isMacOS } from "@/util/platformutil";
import { cn, makeIconClass } from "@/util/util";
import { OverlayScrollbarsComponent } from "overlayscrollbars-react";
import { useLayoutEffect, useRef, useState } from "react";
+import { FakeChat } from "./fakechat";
type FeaturePageName = "waveai" | "magnify" | "files";
@@ -185,6 +186,10 @@ const OnboardingFooter = ({
};
const WaveAIPage = ({ onNext, onSkip }: { onNext: () => void; onSkip: () => void }) => {
+ const isMac = isMacOS();
+ const shortcutKey = isMac ? "β-Shift-A" : "Alt-Shift-A";
+ const [fireClicked, setFireClicked] = useState(false);
+
return (
@@ -194,21 +199,47 @@ const WaveAIPage = ({ onNext, onSkip }: { onNext: () => void; onSkip: () => void
Wave AI
-
-
-
Wave AI helps you work smarter with intelligent assistance directly in your terminal.
-
- Ask questions, get code suggestions, and leverage AI-powered features to enhance your
- workflow.
-
+
+
+
+
+ AI
+
+
+
+
+ Wave AI is your terminal assistant with context. I can read your terminal output, analyze
+ widgets, access files, and help you solve problems faster.
+
+
+
+
+
+ Toggle the Wave AI panel with the{" "}
+
+
+ AI
+ {" "}
+ button in the header (top left)
+
+
+
+
+
+
+ Or use the keyboard shortcut {shortcutKey} to quickly toggle
+
+
+
+
setFireClicked(!fireClicked)} />
+
-
+
From 03aebf65efc4aef092d157ff8fc69a8c3e19f423 Mon Sep 17 00:00:00 2001
From: sawka
Date: Mon, 13 Oct 2025 11:37:44 -0700
Subject: [PATCH 06/16] refactor, working on viewing/editing files section
---
.../app/onboarding/onboarding-command.tsx | 55 +++++
.../app/onboarding/onboarding-features.tsx | 225 ++++++------------
frontend/app/onboarding/onboarding-layout.tsx | 148 ++++++++++++
3 files changed, 270 insertions(+), 158 deletions(-)
create mode 100644 frontend/app/onboarding/onboarding-command.tsx
create mode 100644 frontend/app/onboarding/onboarding-layout.tsx
diff --git a/frontend/app/onboarding/onboarding-command.tsx b/frontend/app/onboarding/onboarding-command.tsx
new file mode 100644
index 0000000000..c3097d9bca
--- /dev/null
+++ b/frontend/app/onboarding/onboarding-command.tsx
@@ -0,0 +1,55 @@
+// Copyright 2025, Command Line Inc.
+// SPDX-License-Identifier: Apache-2.0
+
+import { useLayoutEffect, useState } from "react";
+
+export type CommandRevealProps = {
+ command: string;
+ typeIntervalMs?: number;
+ onComplete?: () => void;
+ showCursor?: boolean;
+};
+
+export const CommandReveal = ({
+ command,
+ typeIntervalMs = 100,
+ onComplete,
+ showCursor: showCursorProp = true,
+}: CommandRevealProps) => {
+ const [displayedText, setDisplayedText] = useState("");
+ const [showCursor, setShowCursor] = useState(true);
+
+ useLayoutEffect(() => {
+ let charIndex = 0;
+ const typeInterval = setInterval(() => {
+ if (charIndex < command.length) {
+ setDisplayedText(command.slice(0, charIndex + 1));
+ charIndex++;
+ } else {
+ clearInterval(typeInterval);
+ if (onComplete) {
+ onComplete();
+ }
+ }
+ }, typeIntervalMs);
+
+ const cursorInterval = setInterval(() => {
+ setShowCursor((prev) => !prev);
+ }, 500);
+
+ return () => {
+ clearInterval(typeInterval);
+ clearInterval(cursorInterval);
+ };
+ }, [command, typeIntervalMs, onComplete]);
+
+ return (
+
+ >
+
+ {displayedText}
+ {showCursorProp && showCursor && }
+
+
+ );
+};
\ No newline at end of file
diff --git a/frontend/app/onboarding/onboarding-features.tsx b/frontend/app/onboarding/onboarding-features.tsx
index 94f6d0c6b1..75e2afe94b 100644
--- a/frontend/app/onboarding/onboarding-features.tsx
+++ b/frontend/app/onboarding/onboarding-features.tsx
@@ -6,149 +6,13 @@ import { Button } from "@/app/element/button";
import { EmojiButton } from "@/app/element/emojibutton";
import { MagnifyIcon } from "@/app/element/magnify";
import { isMacOS } from "@/util/platformutil";
-import { cn, makeIconClass } from "@/util/util";
-import { OverlayScrollbarsComponent } from "overlayscrollbars-react";
-import { useLayoutEffect, useRef, useState } from "react";
+import { useState } from "react";
+import { CommandReveal } from "./onboarding-command";
+import { FakeLayout } from "./onboarding-layout";
import { FakeChat } from "./fakechat";
type FeaturePageName = "waveai" | "magnify" | "files";
-const FakeBlock = ({ icon, name, highlighted, className }: { icon: string; name: string; highlighted?: boolean; className?: string }) => {
- return (
-
-
-
- {name}
-
-
-
-
-
-
-
-
-
- );
-};
-
-const FakeLayout = () => {
- const layoutRef = useRef(null);
- const highlightedContainerRef = useRef(null);
- const [blockRect, setBlockRect] = useState<{ left: number; top: number; width: number; height: number } | null>(null);
- const [isExpanded, setIsExpanded] = useState(false);
-
- useLayoutEffect(() => {
- if (highlightedContainerRef.current) {
- const elem = highlightedContainerRef.current;
- setBlockRect({
- left: elem.offsetLeft,
- top: elem.offsetTop,
- width: elem.offsetWidth,
- height: elem.offsetHeight,
- });
- }
- }, []);
-
- useLayoutEffect(() => {
- if (!blockRect) return;
-
- const timeouts: NodeJS.Timeout[] = [];
-
- const addTimeout = (callback: () => void, delay: number) => {
- const id = setTimeout(callback, delay);
- timeouts.push(id);
- };
-
- const runAnimationCycle = (isFirstRun: boolean) => {
- const initialDelay = isFirstRun ? 1500 : 3000;
-
- addTimeout(() => {
- setIsExpanded(true);
- addTimeout(() => {
- setIsExpanded(false);
- addTimeout(() => runAnimationCycle(false), 3000);
- }, 3200);
- }, initialDelay);
- };
-
- runAnimationCycle(true);
-
- return () => {
- timeouts.forEach(clearTimeout);
- };
- }, [blockRect]);
-
- const getAnimatedStyle = () => {
- if (!blockRect || !layoutRef.current) {
- return {
- left: blockRect?.left ?? 0,
- top: blockRect?.top ?? 0,
- width: blockRect?.width ?? 0,
- height: blockRect?.height ?? 0,
- };
- }
-
- if (isExpanded) {
- const layoutWidth = layoutRef.current.offsetWidth;
- const layoutHeight = layoutRef.current.offsetHeight;
- const targetWidth = layoutWidth * 0.85;
- const targetHeight = layoutHeight * 0.85;
-
- return {
- left: (layoutWidth - targetWidth) / 2,
- top: (layoutHeight - targetHeight) / 2,
- width: targetWidth,
- height: targetHeight,
- };
- }
-
- return {
- left: blockRect.left,
- top: blockRect.top,
- width: blockRect.width,
- height: blockRect.height,
- };
- };
-
- return (
-
-
-
-
-
- {blockRect && (
- <>
-
-
-
-
- >
- )}
-
- );
-};
-
const OnboardingFooter = ({
currentStep,
totalSteps,
@@ -205,13 +69,13 @@ const WaveAIPage = ({ onNext, onSkip }: { onNext: () => void; onSkip: () => void
AI
-
+
- Wave AI is your terminal assistant with context. I can read your terminal output, analyze
- widgets, access files, and help you solve problems faster.
+ Wave AI is your terminal assistant with context. I can read your terminal output,
+ analyze widgets, access files, and help you solve problems faster.
-
+
@@ -223,15 +87,23 @@ const WaveAIPage = ({ onNext, onSkip }: { onNext: () => void; onSkip: () => void
button in the header (top left)
-
+
- Or use the keyboard shortcut {shortcutKey} to quickly toggle
+ Or use the keyboard shortcut{" "}
+
+ {shortcutKey}
+ {" "}
+ to quickly toggle
-
-
setFireClicked(!fireClicked)} />
+
+ setFireClicked(!fireClicked)}
+ />
@@ -293,6 +165,8 @@ const MagnifyBlocksPage = ({ onNext, onSkip }: { onNext: () => void; onSkip: ()
};
const FilesPage = ({ onFinish }: { onFinish: () => void }) => {
+ const [fireClicked, setFireClicked] = useState(false);
+
return (
@@ -302,20 +176,55 @@ const FilesPage = ({ onFinish }: { onFinish: () => void }) => {
Viewing/Editing Files
-
-
-
- View and edit files directly in Wave Terminal with syntax highlighting and code completion.
-
-
Seamlessly switch between terminal commands and file editing in one unified interface.
+
+
+
+
Wave can preview markdown, images, and video files on both local and remote machines.
+
+
+
+
+
+ Use{" "}
+
+ wsh view [filename]
+ {" "}
+ to preview files in Wave's graphical viewer
+
+
+
+
+
+
+
+
+ Use{" "}
+
+ wsh edit [filename]
+ {" "}
+ to open config files or code files in Wave's graphical editor
+
+
+
+
+
+ These commands work seamlessly on both local and remote machines, making it easy to view
+ and edit files wherever they are.
+
+
+
setFireClicked(!fireClicked)}
+ />
+
-
+
diff --git a/frontend/app/onboarding/onboarding-layout.tsx b/frontend/app/onboarding/onboarding-layout.tsx
new file mode 100644
index 0000000000..53bb7f637a
--- /dev/null
+++ b/frontend/app/onboarding/onboarding-layout.tsx
@@ -0,0 +1,148 @@
+// Copyright 2025, Command Line Inc.
+// SPDX-License-Identifier: Apache-2.0
+
+import { MagnifyIcon } from "@/app/element/magnify";
+import { cn, makeIconClass } from "@/util/util";
+import { useLayoutEffect, useRef, useState } from "react";
+
+export type FakeBlockProps = {
+ icon: string;
+ name: string;
+ highlighted?: boolean;
+ className?: string;
+};
+
+export const FakeBlock = ({ icon, name, highlighted, className }: FakeBlockProps) => {
+ return (
+
+
+
+ {name}
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export const FakeLayout = () => {
+ const layoutRef = useRef(null);
+ const highlightedContainerRef = useRef(null);
+ const [blockRect, setBlockRect] = useState<{ left: number; top: number; width: number; height: number } | null>(
+ null
+ );
+ const [isExpanded, setIsExpanded] = useState(false);
+
+ useLayoutEffect(() => {
+ if (highlightedContainerRef.current) {
+ const elem = highlightedContainerRef.current;
+ setBlockRect({
+ left: elem.offsetLeft,
+ top: elem.offsetTop,
+ width: elem.offsetWidth,
+ height: elem.offsetHeight,
+ });
+ }
+ }, []);
+
+ useLayoutEffect(() => {
+ if (!blockRect) return;
+
+ const timeouts: NodeJS.Timeout[] = [];
+
+ const addTimeout = (callback: () => void, delay: number) => {
+ const id = setTimeout(callback, delay);
+ timeouts.push(id);
+ };
+
+ const runAnimationCycle = (isFirstRun: boolean) => {
+ const initialDelay = isFirstRun ? 1500 : 3000;
+
+ addTimeout(() => {
+ setIsExpanded(true);
+ addTimeout(() => {
+ setIsExpanded(false);
+ addTimeout(() => runAnimationCycle(false), 3000);
+ }, 3200);
+ }, initialDelay);
+ };
+
+ runAnimationCycle(true);
+
+ return () => {
+ timeouts.forEach(clearTimeout);
+ };
+ }, [blockRect]);
+
+ const getAnimatedStyle = () => {
+ if (!blockRect || !layoutRef.current) {
+ return {
+ left: blockRect?.left ?? 0,
+ top: blockRect?.top ?? 0,
+ width: blockRect?.width ?? 0,
+ height: blockRect?.height ?? 0,
+ };
+ }
+
+ if (isExpanded) {
+ const layoutWidth = layoutRef.current.offsetWidth;
+ const layoutHeight = layoutRef.current.offsetHeight;
+ const targetWidth = layoutWidth * 0.85;
+ const targetHeight = layoutHeight * 0.85;
+
+ return {
+ left: (layoutWidth - targetWidth) / 2,
+ top: (layoutHeight - targetHeight) / 2,
+ width: targetWidth,
+ height: targetHeight,
+ };
+ }
+
+ return {
+ left: blockRect.left,
+ top: blockRect.top,
+ width: blockRect.width,
+ height: blockRect.height,
+ };
+ };
+
+ return (
+
+
+
+
+
+ {blockRect && (
+ <>
+
+
+
+
+ >
+ )}
+
+ );
+};
\ No newline at end of file
From 6169b854fc35195bc3e16e816e0101a005098511 Mon Sep 17 00:00:00 2001
From: sawka
Date: Mon, 13 Oct 2025 12:08:46 -0700
Subject: [PATCH 07/16] viewing markdown and logo
---
.../app/onboarding/onboarding-command.tsx | 60 ++++++++++++++++++-
.../app/onboarding/onboarding-features.tsx | 23 +++++--
frontend/app/onboarding/onboarding-layout.tsx | 17 +++++-
3 files changed, 90 insertions(+), 10 deletions(-)
diff --git a/frontend/app/onboarding/onboarding-command.tsx b/frontend/app/onboarding/onboarding-command.tsx
index c3097d9bca..ff84e15aa5 100644
--- a/frontend/app/onboarding/onboarding-command.tsx
+++ b/frontend/app/onboarding/onboarding-command.tsx
@@ -1,7 +1,8 @@
// Copyright 2025, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
-import { useLayoutEffect, useState } from "react";
+import { useCallback, useLayoutEffect, useState } from "react";
+import { FakeBlock } from "./onboarding-layout";
export type CommandRevealProps = {
command: string;
@@ -18,6 +19,7 @@ export const CommandReveal = ({
}: CommandRevealProps) => {
const [displayedText, setDisplayedText] = useState("");
const [showCursor, setShowCursor] = useState(true);
+ const [isComplete, setIsComplete] = useState(false);
useLayoutEffect(() => {
let charIndex = 0;
@@ -27,6 +29,8 @@ export const CommandReveal = ({
charIndex++;
} else {
clearInterval(typeInterval);
+ setIsComplete(true);
+ setShowCursor(false);
if (onComplete) {
onComplete();
}
@@ -48,8 +52,60 @@ export const CommandReveal = ({
>
{displayedText}
- {showCursorProp && showCursor && }
+ {showCursorProp && !isComplete && showCursor && }
);
+};
+
+export type FakeCommandProps = {
+ command: string;
+ typeIntervalMs?: number;
+ onComplete?: () => void;
+ children: React.ReactNode;
+};
+
+export const FakeCommand = ({ command, typeIntervalMs = 100, onComplete, children }: FakeCommandProps) => {
+ const [commandComplete, setCommandComplete] = useState(false);
+
+ const handleCommandComplete = useCallback(() => {
+ setCommandComplete(true);
+ if (onComplete) {
+ onComplete();
+ }
+ }, [onComplete]);
+
+ return (
+
+
+ {commandComplete &&
{children}
}
+
+ );
+};
+
+export const ViewShortcutsCommand = ({ isMac, onComplete }: { isMac: boolean; onComplete?: () => void }) => {
+ const modKey = isMac ? "β Cmd" : "Alt";
+ const markdown = `### Keyboard Shortcuts
+
+**Switch Tabs**
+Press ${modKey} + Number (1-9) to quickly switch between tabs.
+
+**Navigate Blocks**
+Use Ctrl-Shift + Arrow Keys (ββββ) to move between blocks in the current tab.
+
+Use Ctrl-Shift + Number (1-9) to focus a specific block by its position.`;
+
+ return (
+
+
+
+ );
+};
+
+export const ViewLogoCommand = ({ onComplete }: { onComplete?: () => void }) => {
+ return (
+
+
+
+ );
};
\ No newline at end of file
diff --git a/frontend/app/onboarding/onboarding-features.tsx b/frontend/app/onboarding/onboarding-features.tsx
index 75e2afe94b..296243fb15 100644
--- a/frontend/app/onboarding/onboarding-features.tsx
+++ b/frontend/app/onboarding/onboarding-features.tsx
@@ -6,8 +6,8 @@ import { Button } from "@/app/element/button";
import { EmojiButton } from "@/app/element/emojibutton";
import { MagnifyIcon } from "@/app/element/magnify";
import { isMacOS } from "@/util/platformutil";
-import { useState } from "react";
-import { CommandReveal } from "./onboarding-command";
+import { useEffect, useState } from "react";
+import { ViewShortcutsCommand, ViewLogoCommand } from "./onboarding-command";
import { FakeLayout } from "./onboarding-layout";
import { FakeChat } from "./fakechat";
@@ -166,6 +166,21 @@ const MagnifyBlocksPage = ({ onNext, onSkip }: { onNext: () => void; onSkip: ()
const FilesPage = ({ onFinish }: { onFinish: () => void }) => {
const [fireClicked, setFireClicked] = useState(false);
+ const isMac = isMacOS();
+ const [commandIndex, setCommandIndex] = useState(0);
+ const [key, setKey] = useState(0);
+
+ const commands = [
+ (onComplete: () => void) =>
,
+ (onComplete: () => void) =>
,
+ ];
+
+ const handleCommandComplete = () => {
+ setTimeout(() => {
+ setCommandIndex((prev) => (prev + 1) % commands.length);
+ setKey((prev) => prev + 1);
+ }, 1500);
+ };
return (
@@ -222,9 +237,7 @@ const FilesPage = ({ onFinish }: { onFinish: () => void }) => {
-
-
-
+ {commands[commandIndex](handleCommandComplete)}
diff --git a/frontend/app/onboarding/onboarding-layout.tsx b/frontend/app/onboarding/onboarding-layout.tsx
index 53bb7f637a..5b2861df48 100644
--- a/frontend/app/onboarding/onboarding-layout.tsx
+++ b/frontend/app/onboarding/onboarding-layout.tsx
@@ -2,6 +2,7 @@
// SPDX-License-Identifier: Apache-2.0
import { MagnifyIcon } from "@/app/element/magnify";
+import { WaveStreamdown } from "@/app/element/streamdown";
import { cn, makeIconClass } from "@/util/util";
import { useLayoutEffect, useRef, useState } from "react";
@@ -10,9 +11,11 @@ export type FakeBlockProps = {
name: string;
highlighted?: boolean;
className?: string;
+ markdown?: string;
+ imgsrc?: string;
};
-export const FakeBlock = ({ icon, name, highlighted, className }: FakeBlockProps) => {
+export const FakeBlock = ({ icon, name, highlighted, className, markdown, imgsrc }: FakeBlockProps) => {
return (
-
-
+
+ {imgsrc ? (
+
+ ) : markdown ? (
+
+
+
+ ) : (
+
+ )}
);
From d05ff72583080399ee6694ed0dc1f3daed9a8b3d Mon Sep 17 00:00:00 2001
From: sawka
Date: Mon, 13 Oct 2025 13:18:24 -0700
Subject: [PATCH 08/16] finish up view/edit w/ codeeditor preview
---
.../app/onboarding/onboarding-command.tsx | 19 +++++++
.../app/onboarding/onboarding-features.tsx | 52 ++++++++++++++-----
frontend/app/onboarding/onboarding-layout.tsx | 10 +++-
3 files changed, 65 insertions(+), 16 deletions(-)
diff --git a/frontend/app/onboarding/onboarding-command.tsx b/frontend/app/onboarding/onboarding-command.tsx
index ff84e15aa5..67019a02f9 100644
--- a/frontend/app/onboarding/onboarding-command.tsx
+++ b/frontend/app/onboarding/onboarding-command.tsx
@@ -108,4 +108,23 @@ export const ViewLogoCommand = ({ onComplete }: { onComplete?: () => void }) =>
);
+};
+
+export const EditBashrcCommand = ({ onComplete }: { onComplete?: () => void }) => {
+ const bashrcContent = `# Aliases
+alias ll="ls -lah"
+alias gst="git status"
+alias wave="wsh"
+
+# Custom prompt
+PS1="\\[\\e[32m\\]\\u@\\h\\[\\e[0m\\]:\\[\\e[34m\\]\\w\\[\\e[0m\\]\\$ "
+
+# PATH
+export PATH="$HOME/.local/bin:$PATH"`;
+
+ return (
+
+
+
+ );
};
\ No newline at end of file
diff --git a/frontend/app/onboarding/onboarding-features.tsx b/frontend/app/onboarding/onboarding-features.tsx
index 296243fb15..db186c1ee1 100644
--- a/frontend/app/onboarding/onboarding-features.tsx
+++ b/frontend/app/onboarding/onboarding-features.tsx
@@ -6,10 +6,10 @@ import { Button } from "@/app/element/button";
import { EmojiButton } from "@/app/element/emojibutton";
import { MagnifyIcon } from "@/app/element/magnify";
import { isMacOS } from "@/util/platformutil";
-import { useEffect, useState } from "react";
-import { ViewShortcutsCommand, ViewLogoCommand } from "./onboarding-command";
-import { FakeLayout } from "./onboarding-layout";
+import { useState } from "react";
import { FakeChat } from "./fakechat";
+import { EditBashrcCommand, ViewLogoCommand, ViewShortcutsCommand } from "./onboarding-command";
+import { FakeLayout } from "./onboarding-layout";
type FeaturePageName = "waveai" | "magnify" | "files";
@@ -17,11 +17,13 @@ const OnboardingFooter = ({
currentStep,
totalSteps,
onNext,
+ onPrev,
onSkip,
}: {
currentStep: number;
totalSteps: number;
onNext: () => void;
+ onPrev?: () => void;
onSkip?: () => void;
}) => {
const isLastStep = currentStep === totalSteps;
@@ -29,9 +31,19 @@ const OnboardingFooter = ({
return (
-
- {currentStep} of {totalSteps}
-
+
+ {currentStep > 1 && onPrev && (
+
+ < Prev
+
+ )}
+
+ {currentStep} of {totalSteps}
+
+
{buttonText}
@@ -119,7 +131,7 @@ const WaveAIPage = ({ onNext, onSkip }: { onNext: () => void; onSkip: () => void
);
};
-const MagnifyBlocksPage = ({ onNext, onSkip }: { onNext: () => void; onSkip: () => void }) => {
+const MagnifyBlocksPage = ({ onNext, onSkip, onPrev }: { onNext: () => void; onSkip: () => void; onPrev?: () => void }) => {
const isMac = isMacOS();
const shortcutKey = isMac ? "β" : "Alt";
const [fireClicked, setFireClicked] = useState(false);
@@ -159,18 +171,19 @@ const MagnifyBlocksPage = ({ onNext, onSkip }: { onNext: () => void; onSkip: ()
-
+
);
};
-const FilesPage = ({ onFinish }: { onFinish: () => void }) => {
+const FilesPage = ({ onFinish, onPrev }: { onFinish: () => void; onPrev?: () => void }) => {
const [fireClicked, setFireClicked] = useState(false);
const isMac = isMacOS();
const [commandIndex, setCommandIndex] = useState(0);
const [key, setKey] = useState(0);
const commands = [
+ (onComplete: () => void) => ,
(onComplete: () => void) => ,
(onComplete: () => void) => ,
];
@@ -179,7 +192,7 @@ const FilesPage = ({ onFinish }: { onFinish: () => void }) => {
setTimeout(() => {
setCommandIndex((prev) => (prev + 1) % commands.length);
setKey((prev) => prev + 1);
- }, 1500);
+ }, 2500);
};
return (
@@ -194,7 +207,10 @@ const FilesPage = ({ onFinish }: { onFinish: () => void }) => {
-
Wave can preview markdown, images, and video files on both local and remote machines.
+
+ Wave can preview markdown, images, and video files on both local and remote {" "}
+ machines.
+
@@ -240,7 +256,7 @@ const FilesPage = ({ onFinish }: { onFinish: () => void }) => {
{commands[commandIndex](handleCommandComplete)}
-
+
);
};
@@ -256,6 +272,14 @@ export const OnboardingFeatures = ({ onComplete }: { onComplete: () => void }) =
}
};
+ const handlePrev = () => {
+ if (currentPage === "magnify") {
+ setCurrentPage("waveai");
+ } else if (currentPage === "files") {
+ setCurrentPage("magnify");
+ }
+ };
+
const handleSkip = () => {
onComplete();
};
@@ -270,10 +294,10 @@ export const OnboardingFeatures = ({ onComplete }: { onComplete: () => void }) =
pageComp =
;
break;
case "magnify":
- pageComp =
;
+ pageComp =
;
break;
case "files":
- pageComp =
;
+ pageComp =
;
break;
}
diff --git a/frontend/app/onboarding/onboarding-layout.tsx b/frontend/app/onboarding/onboarding-layout.tsx
index 5b2861df48..7acfc0b5ce 100644
--- a/frontend/app/onboarding/onboarding-layout.tsx
+++ b/frontend/app/onboarding/onboarding-layout.tsx
@@ -3,6 +3,7 @@
import { MagnifyIcon } from "@/app/element/magnify";
import { WaveStreamdown } from "@/app/element/streamdown";
+import { CodeEditor } from "@/app/view/codeeditor/codeeditor";
import { cn, makeIconClass } from "@/util/util";
import { useLayoutEffect, useRef, useState } from "react";
@@ -13,9 +14,10 @@ export type FakeBlockProps = {
className?: string;
markdown?: string;
imgsrc?: string;
+ editorText?: string;
};
-export const FakeBlock = ({ icon, name, highlighted, className, markdown, imgsrc }: FakeBlockProps) => {
+export const FakeBlock = ({ icon, name, highlighted, className, markdown, imgsrc, editorText }: FakeBlockProps) => {
return (
- {imgsrc ? (
+ {editorText ? (
+
+
+
+ ) : imgsrc ? (
) : markdown ? (
From 12f9fe886a30bb1bbc8d8a2d0a5846688cce2516 Mon Sep 17 00:00:00 2001
From: sawka
Date: Mon, 13 Oct 2025 13:29:14 -0700
Subject: [PATCH 09/16] disable global keybindings while onboarding modal is
open
---
frontend/app/onboarding/onboarding.tsx | 8 +++++++
frontend/app/store/keymodel.ts | 29 +++++++++++++-------------
2 files changed, 22 insertions(+), 15 deletions(-)
diff --git a/frontend/app/onboarding/onboarding.tsx b/frontend/app/onboarding/onboarding.tsx
index d55b81d129..16202ea2ed 100644
--- a/frontend/app/onboarding/onboarding.tsx
+++ b/frontend/app/onboarding/onboarding.tsx
@@ -4,6 +4,7 @@
import Logo from "@/app/asset/logo.svg";
import { Button } from "@/app/element/button";
import { Toggle } from "@/app/element/toggle";
+import { disableGlobalKeybindings, enableGlobalKeybindings } from "@/app/store/keymodel";
import { WorkspaceLayoutModel } from "@/app/workspace/workspace-layout-model";
import * as services from "@/store/services";
import { OverlayScrollbarsComponent } from "overlayscrollbars-react";
@@ -285,6 +286,13 @@ const TosModal = () => {
};
}, []);
+ useEffect(() => {
+ disableGlobalKeybindings();
+ return () => {
+ enableGlobalKeybindings();
+ };
+ }, []);
+
let pageComp: React.JSX.Element = null;
switch (pageName) {
case "init":
diff --git a/frontend/app/store/keymodel.ts b/frontend/app/store/keymodel.ts
index 83e2f78967..3842662beb 100644
--- a/frontend/app/store/keymodel.ts
+++ b/frontend/app/store/keymodel.ts
@@ -33,6 +33,7 @@ type KeyHandler = (event: WaveKeyboardEvent) => boolean;
const simpleControlShiftAtom = jotai.atom(false);
const globalKeyMap = new Map boolean>();
const globalChordMap = new Map>();
+let globalKeybindingsDisabled = false;
// track current chord state and timeout (for resetting)
let activeChord: string | null = null;
@@ -86,6 +87,14 @@ function unsetControlShift() {
globalStore.set(atoms.controlShiftDelayAtom, false);
}
+function disableGlobalKeybindings() {
+ globalKeybindingsDisabled = true;
+}
+
+function enableGlobalKeybindings() {
+ globalKeybindingsDisabled = false;
+}
+
function shouldDispatchToBlock(e: WaveKeyboardEvent): boolean {
if (globalStore.get(atoms.modalOpen)) {
return false;
@@ -361,6 +370,9 @@ function checkKeyMap(waveEvent: WaveKeyboardEvent, keyMap: Map): [
}
function appHandleKeyDown(waveEvent: WaveKeyboardEvent): boolean {
+ if (globalKeybindingsDisabled) {
+ return false;
+ }
const nativeEvent = (waveEvent as any).nativeEvent;
if (lastHandledEvent != null && nativeEvent != null && lastHandledEvent === nativeEvent) {
console.log("lastHandledEvent return false");
@@ -636,23 +648,10 @@ function getAllGlobalKeyBindings(): string[] {
return allKeys;
}
-// these keyboard events happen *anywhere*, even if you have focus in an input or somewhere else.
-function handleGlobalWaveKeyboardEvents(waveEvent: WaveKeyboardEvent): boolean {
- for (const key of globalKeyMap.keys()) {
- if (keyutil.checkKeyPressed(waveEvent, key)) {
- const handler = globalKeyMap.get(key);
- if (handler == null) {
- return false;
- }
- return handler(waveEvent);
- }
- }
- return false;
-}
-
export {
appHandleKeyDown,
- getAllGlobalKeyBindings,
+ disableGlobalKeybindings,
+ enableGlobalKeybindings,
getSimpleControlShiftAtom,
globalRefocus,
globalRefocusWithTimeout,
From 323a2205a8c7d3318056ee3b493e25d489c0763c Mon Sep 17 00:00:00 2001
From: sawka
Date: Mon, 13 Oct 2025 14:00:19 -0700
Subject: [PATCH 10/16] onboarding events
---
frontend/app/modals/modalregistry.tsx | 4 +-
frontend/app/modals/modalsrenderer.tsx | 6 +-
.../app/onboarding/onboarding-features.tsx | 80 +++++++++++++++----
frontend/app/onboarding/onboarding.tsx | 8 +-
frontend/types/gotypes.d.ts | 1 +
pkg/telemetry/telemetrydata/telemetrydata.go | 20 +++--
6 files changed, 85 insertions(+), 34 deletions(-)
diff --git a/frontend/app/modals/modalregistry.tsx b/frontend/app/modals/modalregistry.tsx
index 434a9c0dde..d45a1699d7 100644
--- a/frontend/app/modals/modalregistry.tsx
+++ b/frontend/app/modals/modalregistry.tsx
@@ -2,12 +2,12 @@
// SPDX-License-Identifier: Apache-2.0
import { MessageModal } from "@/app/modals/messagemodal";
-import { TosModal } from "@/app/onboarding/onboarding";
+import { OnboardingModal } from "@/app/onboarding/onboarding";
import { AboutModal } from "./about";
import { UserInputModal } from "./userinputmodal";
const modalRegistry: { [key: string]: React.ComponentType } = {
- [TosModal.displayName || "TosModal"]: TosModal,
+ [OnboardingModal.displayName || "OnboardingModal"]: OnboardingModal,
[UserInputModal.displayName || "UserInputModal"]: UserInputModal,
[AboutModal.displayName || "AboutModal"]: AboutModal,
[MessageModal.displayName || "MessageModal"]: MessageModal,
diff --git a/frontend/app/modals/modalsrenderer.tsx b/frontend/app/modals/modalsrenderer.tsx
index 4513f5f7da..a0b2620a1b 100644
--- a/frontend/app/modals/modalsrenderer.tsx
+++ b/frontend/app/modals/modalsrenderer.tsx
@@ -1,7 +1,7 @@
// Copyright 2025, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
-import { TosModal } from "@/app/onboarding/onboarding";
+import { OnboardingModal } from "@/app/onboarding/onboarding";
import { atoms, globalStore } from "@/store/global";
import { modalsModel } from "@/store/modalmodel";
import * as jotai from "jotai";
@@ -20,10 +20,10 @@ const ModalsRenderer = () => {
}
}
if (tosOpen) {
- rtn.push( );
+ rtn.push( );
}
useEffect(() => {
- if (!clientData.tosagreed || true) {
+ if (!clientData.tosagreed) {
setTosOpen(true);
}
}, [clientData]);
diff --git a/frontend/app/onboarding/onboarding-features.tsx b/frontend/app/onboarding/onboarding-features.tsx
index db186c1ee1..794a82b791 100644
--- a/frontend/app/onboarding/onboarding-features.tsx
+++ b/frontend/app/onboarding/onboarding-features.tsx
@@ -5,8 +5,10 @@ import Logo from "@/app/asset/logo.svg";
import { Button } from "@/app/element/button";
import { EmojiButton } from "@/app/element/emojibutton";
import { MagnifyIcon } from "@/app/element/magnify";
+import { RpcApi } from "@/app/store/wshclientapi";
+import { TabRpcClient } from "@/app/store/wshrpcutil";
import { isMacOS } from "@/util/platformutil";
-import { useState } from "react";
+import { useEffect, useState } from "react";
import { FakeChat } from "./fakechat";
import { EditBashrcCommand, ViewLogoCommand, ViewShortcutsCommand } from "./onboarding-command";
import { FakeLayout } from "./onboarding-layout";
@@ -33,10 +35,7 @@ const OnboardingFooter = ({
@@ -131,11 +138,31 @@ const WaveAIPage = ({ onNext, onSkip }: { onNext: () => void; onSkip: () => void
);
};
-const MagnifyBlocksPage = ({ onNext, onSkip, onPrev }: { onNext: () => void; onSkip: () => void; onPrev?: () => void }) => {
+const MagnifyBlocksPage = ({
+ onNext,
+ onSkip,
+ onPrev,
+}: {
+ onNext: () => void;
+ onSkip: () => void;
+ onPrev?: () => void;
+}) => {
const isMac = isMacOS();
const shortcutKey = isMac ? "β" : "Alt";
const [fireClicked, setFireClicked] = useState(false);
+ const handleFireClick = () => {
+ setFireClicked(!fireClicked);
+ if (!fireClicked) {
+ RpcApi.RecordTEventCommand(TabRpcClient, {
+ event: "onboarding:fire",
+ props: {
+ "onboarding:feature": "magnify",
+ },
+ });
+ }
+ };
+
return (
@@ -182,6 +209,18 @@ const FilesPage = ({ onFinish, onPrev }: { onFinish: () => void; onPrev?: () =>
const [commandIndex, setCommandIndex] = useState(0);
const [key, setKey] = useState(0);
+ const handleFireClick = () => {
+ setFireClicked(!fireClicked);
+ if (!fireClicked) {
+ RpcApi.RecordTEventCommand(TabRpcClient, {
+ event: "onboarding:fire",
+ props: {
+ "onboarding:feature": "wsh",
+ },
+ });
+ }
+ };
+
const commands = [
(onComplete: () => void) => ,
(onComplete: () => void) => ,
@@ -243,11 +282,7 @@ const FilesPage = ({ onFinish, onPrev }: { onFinish: () => void; onPrev?: () =>
and edit files wherever they are.
- setFireClicked(!fireClicked)}
- />
+
@@ -264,6 +299,13 @@ const FilesPage = ({ onFinish, onPrev }: { onFinish: () => void; onPrev?: () =>
export const OnboardingFeatures = ({ onComplete }: { onComplete: () => void }) => {
const [currentPage, setCurrentPage] = useState("waveai");
+ useEffect(() => {
+ RpcApi.RecordTEventCommand(TabRpcClient, {
+ event: "onboarding:start",
+ props: {},
+ });
+ }, []);
+
const handleNext = () => {
if (currentPage === "waveai") {
setCurrentPage("magnify");
@@ -281,6 +323,10 @@ export const OnboardingFeatures = ({ onComplete }: { onComplete: () => void }) =
};
const handleSkip = () => {
+ RpcApi.RecordTEventCommand(TabRpcClient, {
+ event: "onboarding:skip",
+ props: {},
+ });
onComplete();
};
diff --git a/frontend/app/onboarding/onboarding.tsx b/frontend/app/onboarding/onboarding.tsx
index 16202ea2ed..3a629de4e8 100644
--- a/frontend/app/onboarding/onboarding.tsx
+++ b/frontend/app/onboarding/onboarding.tsx
@@ -4,13 +4,13 @@
import Logo from "@/app/asset/logo.svg";
import { Button } from "@/app/element/button";
import { Toggle } from "@/app/element/toggle";
+import { FlexiModal } from "@/app/modals/modal";
import { disableGlobalKeybindings, enableGlobalKeybindings } from "@/app/store/keymodel";
import { WorkspaceLayoutModel } from "@/app/workspace/workspace-layout-model";
import * as services from "@/store/services";
import { OverlayScrollbarsComponent } from "overlayscrollbars-react";
import { useEffect, useRef, useState } from "react";
import { debounce } from "throttle-debounce";
-import { FlexiModal } from "@/app/modals/modal";
import { QuickTips } from "@/app/element/quicktips";
import { OnboardingFeatures } from "@/app/onboarding/onboarding-features";
@@ -248,7 +248,7 @@ const QuickTipsPage = ({ isCompact }: { isCompact: boolean }) => {
);
};
-const TosModal = () => {
+const OnboardingModal = () => {
const modalRef = useRef(null);
const [pageName, setPageName] = useAtom(pageNameAtom);
const clientData = useAtomValue(atoms.client);
@@ -322,6 +322,6 @@ const TosModal = () => {
);
};
-TosModal.displayName = "TosModal";
+OnboardingModal.displayName = "OnboardingModal";
-export { TosModal };
\ No newline at end of file
+export { OnboardingModal };
diff --git a/frontend/types/gotypes.d.ts b/frontend/types/gotypes.d.ts
index b283757b5f..65f4d42950 100644
--- a/frontend/types/gotypes.d.ts
+++ b/frontend/types/gotypes.d.ts
@@ -934,6 +934,7 @@ declare global {
"wsh:cmd"?: string;
"wsh:haderror"?: boolean;
"conn:conntype"?: string;
+ "onboarding:feature"?: "waveai" | "magnify" | "wsh";
"display:height"?: number;
"display:width"?: number;
"display:dpr"?: number;
diff --git a/pkg/telemetry/telemetrydata/telemetrydata.go b/pkg/telemetry/telemetrydata/telemetrydata.go
index 26b72dc275..c0cd01baa7 100644
--- a/pkg/telemetry/telemetrydata/telemetrydata.go
+++ b/pkg/telemetry/telemetrydata/telemetrydata.go
@@ -31,6 +31,9 @@ var ValidEventNames = map[string]bool{
"conn:connecterror": true,
"waveai:enabletelemetry": true,
"waveai:post": true,
+ "onboarding:start": true,
+ "onboarding:skip": true,
+ "onboarding:fire": true,
}
type TEvent struct {
@@ -78,14 +81,15 @@ type TEventProps struct {
AppFirstDay bool `json:"app:firstday,omitempty"`
AppFirstLaunch bool `json:"app:firstlaunch,omitempty"`
- ActionInitiator string `json:"action:initiator,omitempty" tstype:"\"keyboard\" | \"mouse\""`
- PanicType string `json:"debug:panictype,omitempty"`
- BlockView string `json:"block:view,omitempty"`
- AiBackendType string `json:"ai:backendtype,omitempty"`
- AiLocal bool `json:"ai:local,omitempty"`
- WshCmd string `json:"wsh:cmd,omitempty"`
- WshHadError bool `json:"wsh:haderror,omitempty"`
- ConnType string `json:"conn:conntype,omitempty"`
+ ActionInitiator string `json:"action:initiator,omitempty" tstype:"\"keyboard\" | \"mouse\""`
+ PanicType string `json:"debug:panictype,omitempty"`
+ BlockView string `json:"block:view,omitempty"`
+ AiBackendType string `json:"ai:backendtype,omitempty"`
+ AiLocal bool `json:"ai:local,omitempty"`
+ WshCmd string `json:"wsh:cmd,omitempty"`
+ WshHadError bool `json:"wsh:haderror,omitempty"`
+ ConnType string `json:"conn:conntype,omitempty"`
+ OnboardingFeature string `json:"onboarding:feature,omitempty" tstype:"\"waveai\" | \"magnify\" | \"wsh\""`
DisplayHeight int `json:"display:height,omitempty"`
DisplayWidth int `json:"display:width,omitempty"`
From 07e342cf3a17305fb663688a18e99de7689893f4 Mon Sep 17 00:00:00 2001
From: sawka
Date: Mon, 13 Oct 2025 14:17:51 -0700
Subject: [PATCH 11/16] update keybindings, small updates to quicktips
---
docs/docs/keybindings.mdx | 10 +++---
frontend/app/element/quicktips.tsx | 20 ++++++++++++
frontend/app/onboarding/onboarding.tsx | 42 +-------------------------
frontend/app/workspace/widgets.tsx | 1 +
4 files changed, 28 insertions(+), 45 deletions(-)
diff --git a/docs/docs/keybindings.mdx b/docs/docs/keybindings.mdx
index 4f2125ebed..480ce4842f 100644
--- a/docs/docs/keybindings.mdx
+++ b/docs/docs/keybindings.mdx
@@ -4,8 +4,8 @@ id: "keybindings"
title: "Key Bindings"
---
-import { Kbd, KbdChord } from "@site/src/components/kbd.tsx";
-import { PlatformProvider, PlatformSelectorButton } from "@site/src/components/platformcontext.tsx";
+import { Kbd, KbdChord } from "@site/src/components/kbd";
+import { PlatformProvider, PlatformSelectorButton } from "@site/src/components/platformcontext";
@@ -26,6 +26,7 @@ Chords are shown with a + between the keys. You have 2 seconds to hit the 2nd ch
| ------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
| | Open a new tab |
| | Open a new block (defaults to a terminal block with the same connection and working directory). Switch to launcher using `app:defaultnewblock` setting |
+| | Toggle WaveAI panel visibility |
| | Split horizontally, open a new block to the right |
| | Split vertically, open a new block below |
| | Split vertically, open a new block above |
@@ -39,12 +40,13 @@ Chords are shown with a + between the keys. You have 2 seconds to hit the 2nd ch
| | Open the "connection" switcher |
| | Refocus the current block (useful if the block has lost input focus) |
| | Show block numbers |
+| | Focus WaveAI input |
| | Switch to block number |
| | Move left, right, up, down between blocks |
| | Replace the current block with a launcher block |
| | Switch to tab number |
-| | Switch tab left |
-| | Switch tab right |
+| / | Switch tab left |
+| / | Switch tab right |
| | Switch to workspace number |
| | Refresh the UI |
| | Toggle terminal multi-input mode |
diff --git a/frontend/app/element/quicktips.tsx b/frontend/app/element/quicktips.tsx
index 6dcc1275eb..ac97ea72a5 100644
--- a/frontend/app/element/quicktips.tsx
+++ b/frontend/app/element/quicktips.tsx
@@ -168,6 +168,26 @@ const QuickTips = () => {
+
+
);
diff --git a/frontend/app/onboarding/onboarding.tsx b/frontend/app/onboarding/onboarding.tsx
index 3a629de4e8..2c7ff6a4d0 100644
--- a/frontend/app/onboarding/onboarding.tsx
+++ b/frontend/app/onboarding/onboarding.tsx
@@ -12,7 +12,6 @@ import { OverlayScrollbarsComponent } from "overlayscrollbars-react";
import { useEffect, useRef, useState } from "react";
import { debounce } from "throttle-debounce";
-import { QuickTips } from "@/app/element/quicktips";
import { OnboardingFeatures } from "@/app/onboarding/onboarding-features";
import { atoms, globalStore } from "@/app/store/global";
import { modalsModel } from "@/app/store/modalmodel";
@@ -26,7 +25,7 @@ import { atom, PrimitiveAtom, useAtom, useAtomValue, useSetAtom } from "jotai";
// init -> (telemetry enabled) -> features
// init -> (telemetry disabled) -> notelemetrystar -> features
-type PageName = "init" | "notelemetrystar" | "features" | "quicktips";
+type PageName = "init" | "notelemetrystar" | "features";
const pageNameAtom: PrimitiveAtom = atom("init");
@@ -212,42 +211,6 @@ const FeaturesPage = () => {
return ;
};
-const QuickTipsPage = ({ isCompact }: { isCompact: boolean }) => {
- const [tosOpen, setTosOpen] = useAtom(modalsModel.tosOpen);
-
- const handleGetStarted = () => {
- setTosOpen(false);
- };
-
- return (
-
-
-
-
-
- Icons and Keybindings
-
-
-
-
-
-
-
-
- );
-};
-
const OnboardingModal = () => {
const modalRef = useRef(null);
const [pageName, setPageName] = useAtom(pageNameAtom);
@@ -304,9 +267,6 @@ const OnboardingModal = () => {
case "features":
pageComp = ;
break;
- case "quicktips":
- pageComp = ;
- break;
}
if (pageComp == null) {
return null;
diff --git a/frontend/app/workspace/widgets.tsx b/frontend/app/workspace/widgets.tsx
index 35880b7924..b56471c4d5 100644
--- a/frontend/app/workspace/widgets.tsx
+++ b/frontend/app/workspace/widgets.tsx
@@ -90,6 +90,7 @@ const Widgets = memo(() => {
view: "tips",
},
},
+ magnified: true,
};
const showHelp = fullConfig?.settings?.["widget:showhelp"] ?? true;
const widgets = sortByDisplayOrder(fullConfig?.widgets);
From 5e9020f5dbccda8877fe9e7ae2ac22c82921543d Mon Sep 17 00:00:00 2001
From: sawka
Date: Mon, 13 Oct 2025 15:12:12 -0700
Subject: [PATCH 12/16] big update to "quicktips"
---
frontend/app/element/quicktips.tsx | 419 +++++++++++++++++++----------
1 file changed, 283 insertions(+), 136 deletions(-)
diff --git a/frontend/app/element/quicktips.tsx b/frontend/app/element/quicktips.tsx
index ac97ea72a5..868c1b125e 100644
--- a/frontend/app/element/quicktips.tsx
+++ b/frontend/app/element/quicktips.tsx
@@ -3,187 +3,334 @@
import { MagnifyIcon } from "@/app/element/magnify";
import { PLATFORM, PlatformMacOS } from "@/util/platformutil";
+import { cn } from "@/util/util";
const KeyCap = ({ children }: { children: React.ReactNode }) => {
return (
-