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) => ( -
        1. +
        2. ), 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 ( +
          + + {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} + +
          + +
          + {!isLastStep && onSkip && ( + + )} +
          + ); +}; + +const WaveAIPage = ({ onNext, onSkip }: { onNext: () => void; onSkip: () => void }) => { + return ( +
          +
          +
          + +
          +
          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. +

          +
          +
          +
          +
          +
          +
          +
          + +
          + ); +}; + +const MagnifyBlocksPage = ({ onNext, onSkip }: { onNext: () => void; onSkip: () => void }) => { + const isMac = isMacOS(); + const shortcutKey = isMac ? "⌘" : "Alt"; + const [fireClicked, setFireClicked] = useState(false); + + return ( +
          +
          +
          + +
          +
          Magnify Blocks
          +
          +
          +
          +
          {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 ( +
          +
          +
          {userPrompt}
          +
          +
          + ); +}); + +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 + +
          + + +
          +
          + ); +}); + +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 ? ( + {name} + ) : 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 && ( + + )} + + {currentStep} of {totalSteps} + +
          - +
          ); }; -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 ? ( {name} ) : 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 = ({
          {currentStep > 1 && onPrev && ( - )} @@ -66,6 +65,18 @@ const WaveAIPage = ({ onNext, onSkip }: { onNext: () => void; onSkip: () => void const shortcutKey = isMac ? "⌘-Shift-A" : "Alt-Shift-A"; const [fireClicked, setFireClicked] = useState(false); + const handleFireClick = () => { + setFireClicked(!fireClicked); + if (!fireClicked) { + RpcApi.RecordTEventCommand(TabRpcClient, { + event: "onboarding:fire", + props: { + "onboarding:feature": "waveai", + }, + }); + } + }; + return (
          @@ -111,11 +122,7 @@ const WaveAIPage = ({ onNext, onSkip }: { onNext: () => void; onSkip: () => void

          - setFireClicked(!fireClicked)} - /> +
          @@ -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 (
          @@ -163,7 +190,7 @@ const MagnifyBlocksPage = ({ onNext, onSkip, onPrev }: { onNext: () => void; onS

          A quick {shortcutKey}-M to magnify and another {shortcutKey}-M to unmagnify

          - setFireClicked(!fireClicked)} /> +
          @@ -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 ( -
+
{children}
); }; -const IconBox = ({ children }: { children: React.ReactNode }) => { +const IconBox = ({ children, variant = "accent" }: { children: React.ReactNode; variant?: "accent" | "secondary" }) => { + const colorClasses = + variant === "secondary" + ? "text-secondary bg-white/5 border-white/10 [&_svg]:fill-secondary [&_svg_#arrow1]:fill-primary [&_svg_#arrow2]:fill-primary" + : "text-accent-400 bg-accent-400/10 border-accent-400/20 [&_svg]:fill-accent-400 [&_svg_#arrow1]:fill-accent-400 [&_svg_#arrow2]:fill-accent-400"; + return ( -
+
{children}
); }; const KeyBinding = ({ keyDecl }: { keyDecl: string }) => { - const parts = keyDecl.split(":"); - const elems: React.ReactNode[] = []; - for (let part of parts) { - if (part === "Cmd") { - if (PLATFORM === PlatformMacOS) { - elems.push(⌘ Cmd); - } else { - elems.push(Alt); + const chordParts = keyDecl.split("+"); + const chordElems: React.ReactNode[] = []; + + for (let chordIdx = 0; chordIdx < chordParts.length; chordIdx++) { + const parts = chordParts[chordIdx].trim().split(":"); + const elems: React.ReactNode[] = []; + + for (let part of parts) { + if (part === "Cmd") { + if (PLATFORM === PlatformMacOS) { + elems.push(⌘ Cmd); + } else { + elems.push(Alt); + } + continue; } - continue; - } - if (part == "Ctrl") { - elems.push(^ Ctrl); - continue; - } - if (part == "Shift") { - elems.push(⇧ Shift); - continue; - } - if (part == "Arrows") { - elems.push(←); - elems.push(β†’); - elems.push(↑); - elems.push(↓); - continue; + if (part == "Ctrl") { + elems.push(^ Ctrl); + continue; + } + if (part == "Shift") { + elems.push(⇧ Shift); + continue; + } + if (part == "Arrows") { + elems.push(←); + elems.push(β†’); + elems.push(↑); + elems.push(↓); + continue; + } + if (part == "Digit") { + elems.push(Number (1-9)); + continue; + } + if (part == "[" || part == "]") { + elems.push({part}); + continue; + } + elems.push({part.toUpperCase()}); } - if (part == "Digit") { - elems.push(Number (1-9)); - continue; + + chordElems.push( +
+ {elems} +
+ ); + + if (chordIdx < chordParts.length - 1) { + chordElems.push( + + + + + ); } - elems.push({part.toUpperCase()}); } - return
{elems}
; + + return
{chordElems}
; }; const QuickTips = () => { return ( -
-
-
Header Icons
-
- - - - Magnify a Block +
+
+
+
+ Header Icons
-
- - - - Connect to a remote server - -
-
- - - - Block Settings +
+
+ + + +
+ Magnify a Block + +
+
+
+ + + +
+ Connect to a remote server + +
+
+
+ + + + Block Settings +
+
+ + + +
+ Close Block + +
+
-
- - - - Close Block +
+ +
+
+
+ Important Keybindings
-
Important Keybindings
+
+
+
+ Main Keybindings +
+
+ New Tab + +
+
+ New Terminal Block + +
+
+ Open Wave AI Panel + +
+
-
- - New Tab -
-
- - New Terminal Block -
-
- - Navigate Between Blocks -
-
- - Focus Nth Block -
-
- - Switch To Nth Tab -
-
- - Open Wave AI Panel -
-
- - Focus Wave AI -
+
+
+ Tab Switching ({PLATFORM === PlatformMacOS ? "Cmd" : "Alt"}) +
+
+ Switch To Nth Tab + +
+
+ Previous Tab + +
+
+ Next Tab + +
+
-
wsh commands
-
-
- wsh view [filename|url] -
- Run this command in the terminal to preview a file, directory, or web URL. +
+
+ Block Navigation (Ctrl-Shift) +
+
+ Navigate Between Blocks + +
+
+ Focus Nth Block + +
+
+ Focus Wave AI +
+ +
+
+ Split Blocks +
+
+ Split Right + +
+
+ Split Below + +
+
+ Split in Direction + +
+
+
+
+ +
+
+
+ wsh commands
+
+
+ + > + wsh view + [filename|url] + +
Preview files, directories, or web URLs
+
+
+ + > + wsh edit + [filename] + +
Edit config and code files
+
+
+
-
More Tips
-
- - - - Right click the tabs to change backgrounds or rename. +
+
+
+ More Tips
-
- - - - Click the gear in the web view to set your homepage +
+
+ + + + + Tabs - Right click any tab to change backgrounds or rename. + +
+
+ + + + + Web View - Click the gear in the web view to set your homepage + +
+
+ + + + + Terminal - Click the gear in the terminal to set your terminal theme and font size + +
-
- - - - Click the gear in the terminal to set your terminal theme and font size +
+ +
+
+
+ Need More Help?
-
Need More Help?
-
- - - -
- + -
- - - - -
- - - - -
- - - -
- + From 1c38dedad7f9fc6a0ac9d80a2efd800edce64f18 Mon Sep 17 00:00:00 2001 From: sawka Date: Mon, 13 Oct 2025 16:16:30 -0700 Subject: [PATCH 13/16] new Wave AI docs page --- docs/docs/waveai.mdx | 82 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100644 docs/docs/waveai.mdx diff --git a/docs/docs/waveai.mdx b/docs/docs/waveai.mdx new file mode 100644 index 0000000000..6e3463d8bd --- /dev/null +++ b/docs/docs/waveai.mdx @@ -0,0 +1,82 @@ +--- +sidebar_position: 3.4 +id: "waveai" +title: "Wave AI" +--- + +import { Kbd } from "@site/src/components/kbd"; +import { PlatformProvider, PlatformSelectorButton } from "@site/src/components/platformcontext"; + + + + + +

+Context-aware terminal assistant with access to terminal output, widgets, and filesystem. + +## Keyboard Shortcuts + +| Shortcut | Action | +|----------|--------| +| | Toggle AI panel | +| | Focus AI input | +| | Clear chat / start new | +| | Send message | +| | New line | + +## Widget Context Toggle + +Controls AI's access to your workspace: + +**ON**: AI can read terminal output, capture widget screenshots, access files/directories (with approval), navigate web widgets, and use custom widget tools. Use for debugging, code analysis, and workspace tasks. + +**OFF**: AI only sees your messages and attached files. Standard chat mode for general questions. + +## File Attachments + +Drag files onto the AI panel to attach: + +| Type | Formats | Size Limit | Notes | +|------|---------|------------|-------| +| Images | JPEG, PNG, GIF, WebP, SVG | 10 MB | Auto-resized to 4096px max, converted to WebP | +| PDFs | `.pdf` | 5 MB | Text extraction for analysis | +| Text/Code | `.js`, `.ts`, `.py`, `.go`, `.md`, `.json`, `.yaml`, etc. | 200 KB | All common languages and configs | + +## AI Tools (Widget Context Enabled) + +### Terminal +- **Read Terminal Output**: Fetches scrollback from terminal widgets, supports line ranges + +### File System +- **Read Files**: Reads text files with line range support (requires approval) +- **List Directories**: Returns file info, sizes, permissions, timestamps (requires approval) + +### Web +- **Navigate Web**: Changes URLs in web browser widgets + +### All Widgets +- **Capture Screenshots**: Takes screenshots of any widget for visual analysis + +:::warning Security +File system operations require explicit approval. You control all file access. +::: + +## Privacy + +- Messages are proxied through the Wave Cloud AI service (powered by OpenAI) +- Wave does not store your chats, attachments, or use them for training +- Usage counters included in anonymous telemetry +- File access requires explicit approval + +:::info Under Active Development +Wave AI is in active beta with included AI credits while we refine the experience. BYOK will be available once we've stabilized core features and gathered feedback on what works best. Share feedback in our [Discord](https://discord.gg/XfvZ334gwU). + +**Coming Soon:** +- **CLI Integration**: Send files and chat prompts directly from the command line +- **Remote File Access**: Read files on SSH-connected systems +- **Command Execution**: Run terminal commands with approval +- **File Editing**: Modify files with approval or open in editor widgets +- **Web Content**: Extract text from web pages (currently screenshots only) +::: + +
\ No newline at end of file From aac6f0908bbfc9baaf600aafba7f0ecc23fd6ee9 Mon Sep 17 00:00:00 2001 From: sawka Date: Mon, 13 Oct 2025 16:16:44 -0700 Subject: [PATCH 14/16] update imports in other mdx files --- docs/docs/gettingstarted.mdx | 4 ++-- docs/docs/index.mdx | 2 +- docs/docs/tabs.mdx | 6 +++--- docs/docs/widgets.mdx | 4 ++-- docs/docs/wsh-reference.mdx | 4 ++-- 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/docs/docs/gettingstarted.mdx b/docs/docs/gettingstarted.mdx index 47e8c02734..cffcdbf083 100644 --- a/docs/docs/gettingstarted.mdx +++ b/docs/docs/gettingstarted.mdx @@ -4,8 +4,8 @@ id: "gettingstarted" title: "Getting Started" --- -import { PlatformProvider, PlatformSelectorButton, PlatformItem } from "@site/src/components/platformcontext.tsx"; -import { Kbd } from "@site/src/components/kbd.tsx"; +import { PlatformProvider, PlatformSelectorButton, PlatformItem } from "@site/src/components/platformcontext"; +import { Kbd } from "@site/src/components/kbd"; Wave Terminal is a modern terminal that includes graphical capabilities like web browsing, file previews, and AI assistance alongside traditional terminal features. This guide will help you get started. diff --git a/docs/docs/index.mdx b/docs/docs/index.mdx index ce97b6be9b..1c5ee7f3f8 100644 --- a/docs/docs/index.mdx +++ b/docs/docs/index.mdx @@ -6,7 +6,7 @@ hide_title: true hide_table_of_contents: true --- -import { Card, CardGroup } from "@site/src/components/card.tsx"; +import { Card, CardGroup } from "@site/src/components/card"; # Welcome to Wave Terminal diff --git a/docs/docs/tabs.mdx b/docs/docs/tabs.mdx index 76fe743ead..354089be4c 100644 --- a/docs/docs/tabs.mdx +++ b/docs/docs/tabs.mdx @@ -1,11 +1,11 @@ --- -sidebar_position: 3.2 +sidebar_position: 3.25 id: "tabs" title: "Tabs" --- -import { PlatformProvider, PlatformSelectorButton } from "@site/src/components/platformcontext.tsx"; -import { Kbd } from "@site/src/components/kbd.tsx"; +import { PlatformProvider, PlatformSelectorButton } from "@site/src/components/platformcontext"; +import { Kbd } from "@site/src/components/kbd"; diff --git a/docs/docs/widgets.mdx b/docs/docs/widgets.mdx index 9a83f75855..d8795ca4e9 100644 --- a/docs/docs/widgets.mdx +++ b/docs/docs/widgets.mdx @@ -4,8 +4,8 @@ id: "widgets" title: "Widgets" --- -import { Kbd } from "@site/src/components/kbd.tsx"; -import { PlatformProvider, PlatformSelectorButton } from "@site/src/components/platformcontext.tsx"; +import { Kbd } from "@site/src/components/kbd"; +import { PlatformProvider, PlatformSelectorButton } from "@site/src/components/platformcontext"; diff --git a/docs/docs/wsh-reference.mdx b/docs/docs/wsh-reference.mdx index 18eacc106a..0903cde982 100644 --- a/docs/docs/wsh-reference.mdx +++ b/docs/docs/wsh-reference.mdx @@ -4,8 +4,8 @@ id: "wsh-reference" title: "wsh reference" --- -import { Kbd } from "@site/src/components/kbd.tsx"; -import { PlatformProvider, PlatformSelectorButton } from "@site/src/components/platformcontext.tsx"; +import { Kbd } from "@site/src/components/kbd"; +import { PlatformProvider, PlatformSelectorButton } from "@site/src/components/platformcontext"; From fa480cd9ee530024924296d2665ef1dcf133abcd Mon Sep 17 00:00:00 2001 From: sawka Date: Mon, 13 Oct 2025 16:22:59 -0700 Subject: [PATCH 15/16] minor updates --- frontend/app/onboarding/onboarding-command.tsx | 8 +++++--- frontend/app/onboarding/onboarding-layout.tsx | 4 ++-- pkg/wcore/workspace.go | 2 +- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/frontend/app/onboarding/onboarding-command.tsx b/frontend/app/onboarding/onboarding-command.tsx index 67019a02f9..de9bbe274b 100644 --- a/frontend/app/onboarding/onboarding-command.tsx +++ b/frontend/app/onboarding/onboarding-command.tsx @@ -52,7 +52,9 @@ export const CommandReveal = ({ > {displayedText} - {showCursorProp && !isComplete && showCursor && } + {showCursorProp && !isComplete && showCursor && ( + + )}
); @@ -105,7 +107,7 @@ Use Ctrl-Shift + Number (1-9) to focus a specific block by its position.`; export const ViewLogoCommand = ({ onComplete }: { onComplete?: () => void }) => { return ( - + ); }; @@ -127,4 +129,4 @@ export PATH="$HOME/.local/bin:$PATH"`; ); -}; \ No newline at end of file +}; diff --git a/frontend/app/onboarding/onboarding-layout.tsx b/frontend/app/onboarding/onboarding-layout.tsx index 7acfc0b5ce..057aef0fc2 100644 --- a/frontend/app/onboarding/onboarding-layout.tsx +++ b/frontend/app/onboarding/onboarding-layout.tsx @@ -152,7 +152,7 @@ export const FakeLayout = () => {
@@ -162,4 +162,4 @@ export const FakeLayout = () => { )}
); -}; \ No newline at end of file +}; diff --git a/pkg/wcore/workspace.go b/pkg/wcore/workspace.go index fa430c70dd..ed1e6b2b47 100644 --- a/pkg/wcore/workspace.go +++ b/pkg/wcore/workspace.go @@ -219,7 +219,7 @@ func CreateTab(ctx context.Context, workspaceId string, tabName string, activate } } - // No need to apply an initial layout for the initial launch, since the starter layout will get applied after TOS modal dismissal + // No need to apply an initial layout for the initial launch, since the starter layout will get applied after onboarding modal dismissal if !isInitialLaunch { err = ApplyPortableLayout(ctx, tab.OID, GetNewTabLayout(), true) if err != nil { From 82f96965de7bb05b240d9933c19f0f94022c184e Mon Sep 17 00:00:00 2001 From: sawka Date: Mon, 13 Oct 2025 16:39:13 -0700 Subject: [PATCH 16/16] add globalrefocus after existing the onboarding.... --- frontend/app/onboarding/onboarding.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/frontend/app/onboarding/onboarding.tsx b/frontend/app/onboarding/onboarding.tsx index 2c7ff6a4d0..350e11c241 100644 --- a/frontend/app/onboarding/onboarding.tsx +++ b/frontend/app/onboarding/onboarding.tsx @@ -5,7 +5,7 @@ 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 { disableGlobalKeybindings, enableGlobalKeybindings, globalRefocus } from "@/app/store/keymodel"; import { WorkspaceLayoutModel } from "@/app/workspace/workspace-layout-model"; import * as services from "@/store/services"; import { OverlayScrollbarsComponent } from "overlayscrollbars-react"; @@ -206,6 +206,9 @@ const FeaturesPage = () => { const handleComplete = () => { setTosOpen(false); + setTimeout(() => { + globalRefocus(); + }, 10); }; return ;