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/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/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/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 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"; 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/element/quicktips.tsx b/frontend/app/element/quicktips.tsx index 6dcc1275eb..868c1b125e 100644 --- a/frontend/app/element/quicktips.tsx +++ b/frontend/app/element/quicktips.tsx @@ -3,170 +3,337 @@ 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 +
+
-
More Tips
-
- - - - Right click the tabs to change backgrounds or rename. +
+
+
+ wsh commands
-
- - - - Click the gear in the web view to set your homepage +
+
+ + > + wsh view + [filename|url] + +
Preview files, directories, or web URLs
+
+
+ + > + wsh edit + [filename] + +
Edit config and code files
+
-
- - - - Click the gear in the terminal to set your terminal theme and font size +
+ +
+
+
+ More Tips +
+
+
+ + + + + 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 + +
-
Need More Help?
-
- - - -
- +
+ + 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]} diff --git a/frontend/app/modals/modalregistry.tsx b/frontend/app/modals/modalregistry.tsx index b624380f26..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 { OnboardingModal } from "@/app/onboarding/onboarding"; import { AboutModal } from "./about"; -import { TosModal } from "./tos"; 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 ae9e57fca4..a0b2620a1b 100644 --- a/frontend/app/modals/modalsrenderer.tsx +++ b/frontend/app/modals/modalsrenderer.tsx @@ -1,12 +1,12 @@ // Copyright 2025, Command Line Inc. // SPDX-License-Identifier: Apache-2.0 +import { OnboardingModal } from "@/app/onboarding/onboarding"; import { atoms, globalStore } from "@/store/global"; import { modalsModel } from "@/store/modalmodel"; 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); @@ -20,7 +20,7 @@ const ModalsRenderer = () => { } } if (tosOpen) { - rtn.push(); + rtn.push(); } useEffect(() => { if (!clientData.tosagreed) { 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-command.tsx b/frontend/app/onboarding/onboarding-command.tsx new file mode 100644 index 0000000000..de9bbe274b --- /dev/null +++ b/frontend/app/onboarding/onboarding-command.tsx @@ -0,0 +1,132 @@ +// Copyright 2025, Command Line Inc. +// SPDX-License-Identifier: Apache-2.0 + +import { useCallback, useLayoutEffect, useState } from "react"; +import { FakeBlock } from "./onboarding-layout"; + +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); + const [isComplete, setIsComplete] = useState(false); + + useLayoutEffect(() => { + let charIndex = 0; + const typeInterval = setInterval(() => { + if (charIndex < command.length) { + setDisplayedText(command.slice(0, charIndex + 1)); + charIndex++; + } else { + clearInterval(typeInterval); + setIsComplete(true); + setShowCursor(false); + if (onComplete) { + onComplete(); + } + } + }, typeIntervalMs); + + const cursorInterval = setInterval(() => { + setShowCursor((prev) => !prev); + }, 500); + + return () => { + clearInterval(typeInterval); + clearInterval(cursorInterval); + }; + }, [command, typeIntervalMs, onComplete]); + + return ( +
          + > + + {displayedText} + {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 ( + + + + ); +}; + +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 ( + + + + ); +}; diff --git a/frontend/app/onboarding/onboarding-features.tsx b/frontend/app/onboarding/onboarding-features.tsx new file mode 100644 index 0000000000..794a82b791 --- /dev/null +++ b/frontend/app/onboarding/onboarding-features.tsx @@ -0,0 +1,351 @@ +// 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 { RpcApi } from "@/app/store/wshclientapi"; +import { TabRpcClient } from "@/app/store/wshrpcutil"; +import { isMacOS } from "@/util/platformutil"; +import { useEffect, useState } from "react"; +import { FakeChat } from "./fakechat"; +import { EditBashrcCommand, ViewLogoCommand, ViewShortcutsCommand } from "./onboarding-command"; +import { FakeLayout } from "./onboarding-layout"; + +type FeaturePageName = "waveai" | "magnify" | "files"; + +const OnboardingFooter = ({ + currentStep, + totalSteps, + onNext, + onPrev, + onSkip, +}: { + currentStep: number; + totalSteps: number; + onNext: () => void; + onPrev?: () => void; + onSkip?: () => void; +}) => { + const isLastStep = currentStep === totalSteps; + const buttonText = isLastStep ? "Get Started" : "Next"; + + return ( +
          +
          + {currentStep > 1 && onPrev && ( + + )} + + {currentStep} of {totalSteps} + +
          +
          + +
          + {!isLastStep && onSkip && ( + + )} +
          + ); +}; + +const WaveAIPage = ({ onNext, onSkip }: { onNext: () => void; onSkip: () => void }) => { + const isMac = isMacOS(); + 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 ( +
          +
          +
          + +
          +
          Wave AI
          +
          +
          +
          +
          +
          + + 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 +

          +
          + + +
          +
          +
          +
          +
          +
          + +
          +
          +
          + +
          + ); +}; + +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 ( +
          +
          +
          + +
          +
          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 +

          + +
          +
          +
          +
          + +
          +
          + +
          + ); +}; + +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 handleFireClick = () => { + setFireClicked(!fireClicked); + if (!fireClicked) { + RpcApi.RecordTEventCommand(TabRpcClient, { + event: "onboarding:fire", + props: { + "onboarding:feature": "wsh", + }, + }); + } + }; + + const commands = [ + (onComplete: () => void) => , + (onComplete: () => void) => , + (onComplete: () => void) => , + ]; + + const handleCommandComplete = () => { + setTimeout(() => { + setCommandIndex((prev) => (prev + 1) % commands.length); + setKey((prev) => prev + 1); + }, 2500); + }; + + return ( +
          +
          +
          + +
          +
          Viewing/Editing Files
          +
          +
          +
          +
          +
          +

          + 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. +

          + + +
          +
          +
          +
          +
          + {commands[commandIndex](handleCommandComplete)} +
          +
          + +
          + ); +}; + +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"); + } else if (currentPage === "magnify") { + setCurrentPage("files"); + } + }; + + const handlePrev = () => { + if (currentPage === "magnify") { + setCurrentPage("waveai"); + } else if (currentPage === "files") { + setCurrentPage("magnify"); + } + }; + + const handleSkip = () => { + RpcApi.RecordTEventCommand(TabRpcClient, { + event: "onboarding:skip", + props: {}, + }); + 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-layout.tsx b/frontend/app/onboarding/onboarding-layout.tsx new file mode 100644 index 0000000000..057aef0fc2 --- /dev/null +++ b/frontend/app/onboarding/onboarding-layout.tsx @@ -0,0 +1,165 @@ +// Copyright 2025, Command Line Inc. +// SPDX-License-Identifier: Apache-2.0 + +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"; + +export type FakeBlockProps = { + icon: string; + name: string; + highlighted?: boolean; + className?: string; + markdown?: string; + imgsrc?: string; + editorText?: string; +}; + +export const FakeBlock = ({ icon, name, highlighted, className, markdown, imgsrc, editorText }: FakeBlockProps) => { + return ( +
          +
          + + {name} + + + + +
          +
          + {editorText ? ( +
          + +
          + ) : imgsrc ? ( + {name} + ) : markdown ? ( +
          + +
          + ) : ( + + )} +
          +
          + ); +}; + +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 && ( + <> +
          +
          + +
          + + )} +
          + ); +}; diff --git a/frontend/app/modals/tos.tsx b/frontend/app/onboarding/onboarding.tsx similarity index 67% rename from frontend/app/modals/tos.tsx rename to frontend/app/onboarding/onboarding.tsx index a55760e1e0..350e11c241 100644 --- a/frontend/app/modals/tos.tsx +++ b/frontend/app/onboarding/onboarding.tsx @@ -4,31 +4,45 @@ 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, globalRefocus } 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 "./modal"; -import { QuickTips } from "@/app/element/quicktips"; -import { atoms } from "@/app/store/global"; +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"; +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) -> features +// init -> (telemetry disabled) -> notelemetrystar -> features -const ModalPage1 = ({ isCompact }: { isCompact: boolean }) => { +type PageName = "init" | "notelemetrystar" | "features"; + +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 ? "features" : "notelemetrystar"); }; const setTelemetry = (value: boolean) => { @@ -129,35 +143,57 @@ const ModalPage1 = ({ isCompact }: { isCompact: boolean }) => { ); }; -const ModalPage2 = ({ isCompact }: { isCompact: boolean }) => { - const [tosOpen, setTosOpen] = useAtom(modalsModel.tosOpen); +const NoTelemetryStarPage = ({ isCompact }: { isCompact: boolean }) => { + const setPageName = useSetAtom(pageNameAtom); - const handleGetStarted = () => { - setTosOpen(false); + 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("features"); + }; + + const handleMaybeLater = async () => { + const clientId = globalStore.get(atoms.clientId); + await RpcApi.SetMetaCommand(TabRpcClient, { + oref: WOS.makeORef("client", clientId), + meta: { "onboarding:githubstar": false }, + }); + setPageName("features"); }; return (
          -
          -
          +
          +
          -
          Icons and Keybindings
          +
          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. +

          +
          -
          -
          - +
          @@ -165,9 +201,22 @@ const ModalPage2 = ({ isCompact }: { isCompact: boolean }) => { ); }; -const TosModal = () => { +const FeaturesPage = () => { + const [tosOpen, setTosOpen] = useAtom(modalsModel.tosOpen); + + const handleComplete = () => { + setTosOpen(false); + setTimeout(() => { + globalRefocus(); + }, 10); + }; + + return ; +}; + +const OnboardingModal = () => { 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 +236,10 @@ const TosModal = () => { useEffect(() => { if (clientData.tosagreed) { - setPageNum(2); + setPageName("features"); } return () => { - setPageNum(1); + setPageName("init"); }; }, []); @@ -203,13 +252,23 @@ const TosModal = () => { }; }, []); + useEffect(() => { + disableGlobalKeybindings(); + return () => { + enableGlobalKeybindings(); + }; + }, []); + 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 "features": + pageComp = ; break; } if (pageComp == null) { @@ -217,14 +276,15 @@ const TosModal = () => { } const paddingClass = isCompact ? "!py-3 !px-[30px]" : "!p-[30px]"; + const widthClass = pageName === "features" ? "w-[800px]" : "w-[560px]"; return ( - +
          {pageComp}
          ); }; -TosModal.displayName = "TosModal"; +OnboardingModal.displayName = "OnboardingModal"; -export { TosModal }; +export { OnboardingModal }; 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, 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); 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; + } +} diff --git a/frontend/types/gotypes.d.ts b/frontend/types/gotypes.d.ts index 14ea692ffa..65f4d42950 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; }; @@ -933,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; @@ -949,6 +951,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/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"` 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..ed1e6b2b47 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) } @@ -225,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 {