diff --git a/frontend/app/tab/vtabbar.tsx b/frontend/app/tab/vtabbar.tsx index ad558373dd..0634645789 100644 --- a/frontend/app/tab/vtabbar.tsx +++ b/frontend/app/tab/vtabbar.tsx @@ -1,20 +1,22 @@ // Copyright 2026, Command Line Inc. // SPDX-License-Identifier: Apache-2.0 -import { cn } from "@/util/util"; +import { getTabBadgeAtom } from "@/app/store/badge"; +import { makeORef } from "@/app/store/wos"; +import { TabRpcClient } from "@/app/store/wshrpcutil"; +import { useWaveEnv } from "@/app/waveenv/waveenv"; +import { validateCssColor } from "@/util/color-validator"; +import { cn, fireAndForget } from "@/util/util"; +import { useAtomValue } from "jotai"; import { useEffect, useMemo, useRef, useState } from "react"; import { VTab, VTabItem } from "./vtab"; +import { VTabBarEnv } from "./vtabbarenv"; export type { VTabItem } from "./vtab"; interface VTabBarProps { - tabs: VTabItem[]; - activeTabId?: string; + workspace: Workspace; width?: number; className?: string; - onSelectTab?: (tabId: string) => void; - onCloseTab?: (tabId: string) => void; - onRenameTab?: (tabId: string, newName: string) => void; - onReorderTabs?: (tabIds: string[]) => void; } function clampWidth(width?: number): number { @@ -30,8 +32,83 @@ function clampWidth(width?: number): number { return width; } -export function VTabBar({ tabs, activeTabId, width, className, onSelectTab, onCloseTab, onRenameTab, onReorderTabs }: VTabBarProps) { - const [orderedTabs, setOrderedTabs] = useState(tabs); +interface VTabWrapperProps { + tabId: string; + active: boolean; + isDragging: boolean; + isReordering: boolean; + hoverResetVersion: number; + index: number; + onSelect: () => void; + onClose: () => void; + onRename: (newName: string) => void; + onDragStart: (event: React.DragEvent) => void; + onDragOver: (event: React.DragEvent) => void; + onDrop: (event: React.DragEvent) => void; + onDragEnd: () => void; +} + +function VTabWrapper({ + tabId, + active, + isDragging, + isReordering, + hoverResetVersion, + onSelect, + onClose, + onRename, + onDragStart, + onDragOver, + onDrop, + onDragEnd, +}: VTabWrapperProps) { + const env = useWaveEnv(); + const [tabData] = env.wos.useWaveObjectValue(makeORef("tab", tabId)); + const badges = useAtomValue(getTabBadgeAtom(tabId, env)); + + const rawFlagColor = tabData?.meta?.["tab:flagcolor"]; + let flagColor: string | null = null; + if (rawFlagColor) { + try { + validateCssColor(rawFlagColor); + flagColor = rawFlagColor; + } catch { + flagColor = null; + } + } + + const tab: VTabItem = { + id: tabId, + name: tabData?.name ?? "", + badges, + flagColor, + }; + + return ( + + ); +} + +export function VTabBar({ workspace, width, className }: VTabBarProps) { + const env = useWaveEnv(); + const activeTabId = useAtomValue(env.atoms.staticTabId); + const reinitVersion = useAtomValue(env.atoms.reinitVersion); + const tabIds = workspace?.tabids ?? []; + + const [orderedTabIds, setOrderedTabIds] = useState(tabIds); const [dragTabId, setDragTabId] = useState(null); const [dropIndex, setDropIndex] = useState(null); const [dropLineTop, setDropLineTop] = useState(null); @@ -40,8 +117,14 @@ export function VTabBar({ tabs, activeTabId, width, className, onSelectTab, onCl const didResetHoverForDragRef = useRef(false); useEffect(() => { - setOrderedTabs(tabs); - }, [tabs]); + setOrderedTabIds(tabIds); + }, [workspace?.tabids]); + + useEffect(() => { + if (reinitVersion > 0) { + setOrderedTabIds(workspace?.tabids ?? []); + } + }, [reinitVersion]); const barWidth = useMemo(() => clampWidth(width), [width]); @@ -61,25 +144,28 @@ export function VTabBar({ tabs, activeTabId, width, className, onSelectTab, onCl if (sourceTabId == null) { return; } - const sourceIndex = orderedTabs.findIndex((tab) => tab.id === sourceTabId); + const sourceIndex = orderedTabIds.findIndex((id) => id === sourceTabId); if (sourceIndex === -1) { return; } - const boundedTargetIndex = Math.max(0, Math.min(targetIndex, orderedTabs.length)); + const boundedTargetIndex = Math.max(0, Math.min(targetIndex, orderedTabIds.length)); const adjustedTargetIndex = sourceIndex < boundedTargetIndex ? boundedTargetIndex - 1 : boundedTargetIndex; if (sourceIndex === adjustedTargetIndex) { return; } - const nextTabs = [...orderedTabs]; - const [movedTab] = nextTabs.splice(sourceIndex, 1); - nextTabs.splice(adjustedTargetIndex, 0, movedTab); - setOrderedTabs(nextTabs); - onReorderTabs?.(nextTabs.map((tab) => tab.id)); + const nextTabIds = [...orderedTabIds]; + const [movedId] = nextTabIds.splice(sourceIndex, 1); + nextTabIds.splice(adjustedTargetIndex, 0, movedId); + setOrderedTabIds(nextTabIds); + fireAndForget(() => env.rpc.UpdateWorkspaceTabIdsCommand(TabRpcClient, workspace.oid, nextTabIds)); }; return (
{ event.preventDefault(); if (event.target === event.currentTarget) { - setDropIndex(orderedTabs.length); + setDropIndex(orderedTabIds.length); setDropLineTop(event.currentTarget.scrollHeight); } }} @@ -99,22 +185,26 @@ export function VTabBar({ tabs, activeTabId, width, className, onSelectTab, onCl clearDragState(); }} > - {orderedTabs.map((tab, index) => ( - ( + onSelectTab?.(tab.id)} - onClose={onCloseTab ? () => onCloseTab(tab.id) : undefined} - onRename={onRenameTab ? (newName) => onRenameTab(tab.id, newName) : undefined} + hoverResetVersion={hoverResetVersion} + index={index} + onSelect={() => env.electron.setActiveTab(tabId)} + onClose={() => fireAndForget(() => env.electron.closeTab(workspace.oid, tabId, false))} + onRename={(newName) => + fireAndForget(() => env.rpc.UpdateTabNameCommand(TabRpcClient, tabId, newName)) + } onDragStart={(event) => { didResetHoverForDragRef.current = false; - dragSourceRef.current = tab.id; + dragSourceRef.current = tabId; event.dataTransfer.effectAllowed = "move"; - event.dataTransfer.setData("text/plain", tab.id); - setDragTabId(tab.id); + event.dataTransfer.setData("text/plain", tabId); + setDragTabId(tabId); setDropIndex(index); setDropLineTop(event.currentTarget.offsetTop); }} @@ -141,6 +231,15 @@ export function VTabBar({ tabs, activeTabId, width, className, onSelectTab, onCl onDragEnd={clearDragState} /> ))} + {dragTabId != null && dropIndex != null && dropLineTop != null && (
; + mockSetWaveObj: WaveEnv["mockSetWaveObj"]; + isWindows: WaveEnv["isWindows"]; + isMacOS: WaveEnv["isMacOS"]; +}>; diff --git a/frontend/preview/mock/tabbar-mock.tsx b/frontend/preview/mock/tabbar-mock.tsx new file mode 100644 index 0000000000..c4a811f6e4 --- /dev/null +++ b/frontend/preview/mock/tabbar-mock.tsx @@ -0,0 +1,173 @@ +// Copyright 2026, Command Line Inc. +// SPDX-License-Identifier: Apache-2.0 + +import { globalStore } from "@/app/store/jotaiStore"; +import { useWaveEnv, WaveEnv, WaveEnvContext } from "@/app/waveenv/waveenv"; +import { applyMockEnvOverrides, MockWaveEnv } from "@/preview/mock/mockwaveenv"; +import { PlatformMacOS } from "@/util/platformutil"; +import { atom } from "jotai"; +import React, { useMemo, useRef } from "react"; + +type PreviewTabEntry = { + tabId: string; + tabName: string; + badges?: Badge[] | null; + flagColor?: string | null; +}; + +function badgeBlockId(tabId: string, badgeId: string): string { + return `${tabId}-badge-${badgeId}`; +} + +function makeTabWaveObj(tab: PreviewTabEntry): Tab { + const blockids = (tab.badges ?? []).map((b) => badgeBlockId(tab.tabId, b.badgeid)); + return { + otype: "tab", + oid: tab.tabId, + version: 1, + name: tab.tabName, + blockids, + meta: tab.flagColor ? { "tab:flagcolor": tab.flagColor } : {}, + } as Tab; +} + +function makeMockBadgeEvents(): BadgeEvent[] { + const events: BadgeEvent[] = []; + for (const tab of TabBarMockTabs) { + for (const badge of tab.badges ?? []) { + events.push({ oref: `block:${badgeBlockId(tab.tabId, badge.badgeid)}`, badge }); + } + } + return events; +} + +export const TabBarMockWorkspaceId = "preview-workspace-1"; + +export const TabBarMockTabs: PreviewTabEntry[] = [ + { tabId: "preview-tab-1", tabName: "Terminal" }, + { + tabId: "preview-tab-2", + tabName: "Build Logs", + badges: [ + { + badgeid: "01958000-0000-7000-0000-000000000001", + icon: "triangle-exclamation", + color: "#f59e0b", + priority: 2, + }, + ], + }, + { + tabId: "preview-tab-3", + tabName: "Deploy", + badges: [ + { badgeid: "01958000-0000-7000-0000-000000000002", icon: "circle-check", color: "#4ade80", priority: 3 }, + ], + flagColor: "#429dff", + }, + { + tabId: "preview-tab-4", + tabName: "A Very Long Tab Name To Show Truncation", + badges: [ + { badgeid: "01958000-0000-7000-0000-000000000003", icon: "bell", color: "#f87171", priority: 2 }, + { badgeid: "01958000-0000-7000-0000-000000000004", icon: "circle-small", color: "#fbbf24", priority: 1 }, + ], + }, + { tabId: "preview-tab-5", tabName: "Wave AI" }, + { tabId: "preview-tab-6", tabName: "Preview", flagColor: "#bf55ec" }, +]; + +function makeMockWorkspace(tabIds: string[]): Workspace { + return { + otype: "workspace", + oid: TabBarMockWorkspaceId, + version: 1, + name: "Preview Workspace", + tabids: tabIds, + activetabid: tabIds[1] ?? tabIds[0] ?? "", + meta: {}, + } as Workspace; +} + +export function makeTabBarMockEnv( + baseEnv: WaveEnv, + envRef: React.RefObject, + platform: NodeJS.Platform +): MockWaveEnv { + const initialTabIds = TabBarMockTabs.map((t) => t.tabId); + const mockWaveObjs: Record = { + [`workspace:${TabBarMockWorkspaceId}`]: makeMockWorkspace(initialTabIds), + }; + for (const tab of TabBarMockTabs) { + mockWaveObjs[`tab:${tab.tabId}`] = makeTabWaveObj(tab); + } + const env = applyMockEnvOverrides(baseEnv, { + tabId: TabBarMockTabs[1].tabId, + platform, + mockWaveObjs, + atoms: { + workspaceId: atom(TabBarMockWorkspaceId), + staticTabId: atom(TabBarMockTabs[1].tabId), + }, + rpc: { + GetAllBadgesCommand: () => Promise.resolve(makeMockBadgeEvents()), + }, + electron: { + createTab: () => { + const e = envRef.current; + if (e == null) return; + const newTabId = `preview-tab-${crypto.randomUUID()}`; + e.mockSetWaveObj(`tab:${newTabId}`, { + otype: "tab", + oid: newTabId, + version: 1, + name: "New Tab", + blockids: [], + meta: {}, + } as Tab); + const ws = globalStore.get(e.wos.getWaveObjectAtom(`workspace:${TabBarMockWorkspaceId}`)); + e.mockSetWaveObj(`workspace:${TabBarMockWorkspaceId}`, { + ...ws, + tabids: [...(ws.tabids ?? []), newTabId], + }); + globalStore.set(e.atoms.staticTabId as any, newTabId); + }, + closeTab: (_workspaceId: string, tabId: string) => { + const e = envRef.current; + if (e == null) return Promise.resolve(false); + const ws = globalStore.get(e.wos.getWaveObjectAtom(`workspace:${TabBarMockWorkspaceId}`)); + const newTabIds = (ws.tabids ?? []).filter((id) => id !== tabId); + if (newTabIds.length === 0) { + return Promise.resolve(false); + } + e.mockSetWaveObj(`workspace:${TabBarMockWorkspaceId}`, { ...ws, tabids: newTabIds }); + if (globalStore.get(e.atoms.staticTabId) === tabId) { + globalStore.set(e.atoms.staticTabId as any, newTabIds[0]); + } + return Promise.resolve(true); + }, + setActiveTab: (tabId: string) => { + const e = envRef.current; + if (e == null) return; + globalStore.set(e.atoms.staticTabId as any, tabId); + }, + showWorkspaceAppMenu: () => { + console.log("[preview] showWorkspaceAppMenu"); + }, + }, + }); + envRef.current = env; + return env; +} + +type TabBarMockEnvProviderProps = { + children: React.ReactNode; +}; + +export function TabBarMockEnvProvider({ children }: TabBarMockEnvProviderProps) { + const baseEnv = useWaveEnv(); + const envRef = useRef(null); + const tabEnv = useMemo(() => makeTabBarMockEnv(baseEnv, envRef, PlatformMacOS), []); + return {children}; +} +TabBarMockEnvProvider.displayName = "TabBarMockEnvProvider"; diff --git a/frontend/preview/previews/tabbar.preview.tsx b/frontend/preview/previews/tabbar.preview.tsx index 104ef4f8a6..f2ba2234b7 100644 --- a/frontend/preview/previews/tabbar.preview.tsx +++ b/frontend/preview/previews/tabbar.preview.tsx @@ -2,171 +2,26 @@ // SPDX-License-Identifier: Apache-2.0 import { loadBadges, LoadBadgesEnv } from "@/app/store/badge"; -import { globalStore } from "@/app/store/jotaiStore"; import { TabBar } from "@/app/tab/tabbar"; import { TabBarEnv } from "@/app/tab/tabbarenv"; import { useWaveEnv, WaveEnvContext } from "@/app/waveenv/waveenv"; -import { applyMockEnvOverrides, MockWaveEnv } from "@/preview/mock/mockwaveenv"; +import { makeTabBarMockEnv, TabBarMockWorkspaceId } from "@/preview/mock/tabbar-mock"; +import { MockWaveEnv } from "@/preview/mock/mockwaveenv"; import { PlatformLinux, PlatformMacOS, PlatformWindows } from "@/util/platformutil"; -import { atom, useAtom, useAtomValue } from "jotai"; +import { useAtom, useAtomValue } from "jotai"; import { CSSProperties, useEffect, useMemo, useRef, useState } from "react"; -type PreviewTabEntry = { - tabId: string; - tabName: string; - badges?: Badge[] | null; - flagColor?: string | null; -}; - -function badgeBlockId(tabId: string, badgeId: string): string { - return `${tabId}-badge-${badgeId}`; -} - -function makeTabWaveObj(tab: PreviewTabEntry): Tab { - const blockids = (tab.badges ?? []).map((b) => badgeBlockId(tab.tabId, b.badgeid)); - return { - otype: "tab", - oid: tab.tabId, - version: 1, - name: tab.tabName, - blockids, - meta: tab.flagColor ? { "tab:flagcolor": tab.flagColor } : {}, - } as Tab; -} - -function makeMockBadgeEvents(): BadgeEvent[] { - const events: BadgeEvent[] = []; - for (const tab of InitialTabs) { - for (const badge of tab.badges ?? []) { - events.push({ oref: `block:${badgeBlockId(tab.tabId, badge.badgeid)}`, badge }); - } - } - return events; -} - -const MockWorkspaceId = "preview-workspace-1"; -const InitialTabs: PreviewTabEntry[] = [ - { tabId: "preview-tab-1", tabName: "Terminal" }, - { - tabId: "preview-tab-2", - tabName: "Build Logs", - badges: [ - { - badgeid: "01958000-0000-7000-0000-000000000001", - icon: "triangle-exclamation", - color: "#f59e0b", - priority: 2, - }, - ], - }, - { - tabId: "preview-tab-3", - tabName: "Deploy", - badges: [ - { badgeid: "01958000-0000-7000-0000-000000000002", icon: "circle-check", color: "#4ade80", priority: 3 }, - ], - flagColor: "#429dff", - }, - { - tabId: "preview-tab-4", - tabName: "A Very Long Tab Name To Show Truncation", - badges: [ - { badgeid: "01958000-0000-7000-0000-000000000003", icon: "bell", color: "#f87171", priority: 2 }, - { badgeid: "01958000-0000-7000-0000-000000000004", icon: "circle-small", color: "#fbbf24", priority: 1 }, - ], - }, - { tabId: "preview-tab-5", tabName: "Wave AI" }, - { tabId: "preview-tab-6", tabName: "Preview", flagColor: "#bf55ec" }, -]; - const MockConfigErrors: ConfigError[] = [ { file: "~/.waveterm/config.json", err: 'unknown preset "bg@aurora"' }, { file: "~/.waveterm/settings.json", err: "invalid color for tab theme" }, ]; -function makeMockWorkspace(tabIds: string[]): Workspace { - return { - otype: "workspace", - oid: MockWorkspaceId, - version: 1, - name: "Preview Workspace", - tabids: tabIds, - activetabid: tabIds[1] ?? tabIds[0] ?? "", - meta: {}, - } as Workspace; -} - export function TabBarPreview() { const baseEnv = useWaveEnv(); - const initialTabIds = InitialTabs.map((t) => t.tabId); const envRef = useRef(null); const [platform, setPlatform] = useState(PlatformMacOS); - const tabEnv = useMemo(() => { - const mockWaveObjs: Record = { - [`workspace:${MockWorkspaceId}`]: makeMockWorkspace(initialTabIds), - }; - for (const tab of InitialTabs) { - mockWaveObjs[`tab:${tab.tabId}`] = makeTabWaveObj(tab); - } - const env = applyMockEnvOverrides(baseEnv, { - tabId: InitialTabs[1].tabId, - platform, - mockWaveObjs, - atoms: { - workspaceId: atom(MockWorkspaceId), - staticTabId: atom(InitialTabs[1].tabId), - }, - rpc: { - GetAllBadgesCommand: () => Promise.resolve(makeMockBadgeEvents()), - }, - electron: { - createTab: () => { - const e = envRef.current; - if (e == null) return; - const newTabId = `preview-tab-${crypto.randomUUID()}`; - e.mockSetWaveObj(`tab:${newTabId}`, { - otype: "tab", - oid: newTabId, - version: 1, - name: "New Tab", - blockids: [], - meta: {}, - } as Tab); - const ws = globalStore.get(e.wos.getWaveObjectAtom(`workspace:${MockWorkspaceId}`)); - e.mockSetWaveObj(`workspace:${MockWorkspaceId}`, { - ...ws, - tabids: [...(ws.tabids ?? []), newTabId], - }); - globalStore.set(e.atoms.staticTabId as any, newTabId); - }, - closeTab: (_workspaceId: string, tabId: string) => { - const e = envRef.current; - if (e == null) return Promise.resolve(false); - const ws = globalStore.get(e.wos.getWaveObjectAtom(`workspace:${MockWorkspaceId}`)); - const newTabIds = (ws.tabids ?? []).filter((id) => id !== tabId); - if (newTabIds.length === 0) { - return Promise.resolve(false); - } - e.mockSetWaveObj(`workspace:${MockWorkspaceId}`, { ...ws, tabids: newTabIds }); - if (globalStore.get(e.atoms.staticTabId) === tabId) { - globalStore.set(e.atoms.staticTabId as any, newTabIds[0]); - } - return Promise.resolve(true); - }, - setActiveTab: (tabId: string) => { - const e = envRef.current; - if (e == null) return; - globalStore.set(e.atoms.staticTabId as any, tabId); - }, - showWorkspaceAppMenu: () => { - console.log("[preview] showWorkspaceAppMenu"); - }, - }, - }); - envRef.current = env; - return env; - }, [platform]); + const tabEnv = useMemo(() => makeTabBarMockEnv(baseEnv, envRef, platform), [platform]); return ( @@ -190,7 +45,7 @@ function TabBarPreviewInner({ platform, setPlatform }: TabBarPreviewInnerProps) const [zoomFactor, setZoomFactor] = useAtom(env.atoms.zoomFactorAtom); const [fullConfig, setFullConfig] = useAtom(env.atoms.fullConfigAtom); const [updaterStatus, setUpdaterStatus] = useAtom(env.atoms.updaterStatusAtom); - const workspace = useAtomValue(env.wos.getWaveObjectAtom(`workspace:${MockWorkspaceId}`)); + const workspace = useAtomValue(env.wos.getWaveObjectAtom(`workspace:${TabBarMockWorkspaceId}`)); useEffect(() => { loadBadges(loadBadgesEnv); diff --git a/frontend/preview/previews/vtabbar.preview.tsx b/frontend/preview/previews/vtabbar.preview.tsx index c4739f593d..a814291573 100644 --- a/frontend/preview/previews/vtabbar.preview.tsx +++ b/frontend/preview/previews/vtabbar.preview.tsx @@ -1,43 +1,36 @@ // Copyright 2026, Command Line Inc. // SPDX-License-Identifier: Apache-2.0 -import { VTabBar, VTabItem } from "@/app/tab/vtabbar"; -import { useState } from "react"; - -const InitialTabs: VTabItem[] = [ - { id: "vtab-1", name: "Terminal" }, - { - id: "vtab-2", - name: "Build Logs", - badges: [ - { badgeid: "01957000-0000-7000-0000-000000000001", icon: "bell", color: "#f59e0b", priority: 2 }, - { badgeid: "01957000-0000-7000-0000-000000000002", icon: "circle-small", color: "#4ade80", priority: 3 }, - ], - }, - { id: "vtab-3", name: "Deploy", flagColor: "#429DFF" }, - { id: "vtab-4", name: "Wave AI" }, - { - id: "vtab-5", - name: "A Very Long Tab Name To Show Truncation", - badges: [{ badgeid: "01957000-0000-7000-0000-000000000003", icon: "solid@terminal", color: "#fbbf24", priority: 3 }], - flagColor: "#BF55EC", - }, -]; +import { loadBadges, LoadBadgesEnv } from "@/app/store/badge"; +import { VTabBar } from "@/app/tab/vtabbar"; +import { VTabBarEnv } from "@/app/tab/vtabbarenv"; +import { useWaveEnv } from "@/app/waveenv/waveenv"; +import { TabBarMockEnvProvider, TabBarMockWorkspaceId } from "@/preview/mock/tabbar-mock"; +import { useAtomValue } from "jotai"; +import { useEffect, useState } from "react"; export function VTabBarPreview() { - const [tabs, setTabs] = useState(InitialTabs); - const [activeTabId, setActiveTabId] = useState(InitialTabs[0].id); const [width, setWidth] = useState(220); + return ( + + + + ); +} - const handleCloseTab = (tabId: string) => { - setTabs((prevTabs) => { - const nextTabs = prevTabs.filter((tab) => tab.id !== tabId); - if (activeTabId === tabId && nextTabs.length > 0) { - setActiveTabId(nextTabs[0].id); - } - return nextTabs; - }); - }; +type VTabBarPreviewInnerProps = { + width: number; + setWidth: (width: number) => void; +}; + +function VTabBarPreviewInner({ width, setWidth }: VTabBarPreviewInnerProps) { + const env = useWaveEnv(); + const loadBadgesEnv = useWaveEnv(); + const workspace = useAtomValue(env.wos.getWaveObjectAtom(`workspace:${TabBarMockWorkspaceId}`)); + + useEffect(() => { + loadBadges(loadBadgesEnv); + }, []); return (
@@ -56,25 +49,9 @@ export function VTabBarPreview() {

- { - setTabs((prevTabs) => - prevTabs.map((tab) => (tab.id === tabId ? { ...tab, name: newName } : tab)) - ); - }} - onReorderTabs={(tabIds) => { - setTabs((prevTabs) => { - const tabById = new Map(prevTabs.map((tab) => [tab.id, tab])); - return tabIds.map((tabId) => tabById.get(tabId)).filter((tab) => tab != null); - }); - }} - /> + {workspace != null && }
); } +VTabBarPreviewInner.displayName = "VTabBarPreviewInner"; diff --git a/package-lock.json b/package-lock.json index 99c2a025b4..ce697130cf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "waveterm", - "version": "0.14.2-beta.1", + "version": "0.14.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "waveterm", - "version": "0.14.2-beta.1", + "version": "0.14.2", "hasInstallScript": true, "license": "Apache-2.0", "workspaces": [