From 6cf17da943a733b92b4e1fa67326f5e9a231b4bd Mon Sep 17 00:00:00 2001 From: sawka Date: Fri, 13 Mar 2026 10:58:40 -0700 Subject: [PATCH 01/24] add app:tabbar into settings --- frontend/types/gotypes.d.ts | 1 + pkg/wconfig/defaultconfig/settings.json | 1 + pkg/wconfig/metaconsts.go | 1 + pkg/wconfig/settingsconfig.go | 1 + schema/settings.json | 7 +++++++ 5 files changed, 11 insertions(+) diff --git a/frontend/types/gotypes.d.ts b/frontend/types/gotypes.d.ts index ddcb4a63e7..9a65762d38 100644 --- a/frontend/types/gotypes.d.ts +++ b/frontend/types/gotypes.d.ts @@ -1305,6 +1305,7 @@ declare global { "app:disablectrlshiftarrows"?: boolean; "app:disablectrlshiftdisplay"?: boolean; "app:focusfollowscursor"?: string; + "app:tabbar"?: string; "feature:waveappbuilder"?: boolean; "ai:*"?: boolean; "ai:preset"?: string; diff --git a/pkg/wconfig/defaultconfig/settings.json b/pkg/wconfig/defaultconfig/settings.json index ab10987fc9..8ed6af7235 100644 --- a/pkg/wconfig/defaultconfig/settings.json +++ b/pkg/wconfig/defaultconfig/settings.json @@ -4,6 +4,7 @@ "ai:maxtokens": 4000, "ai:timeoutms": 60000, "app:defaultnewblock": "term", + "app:tabbar": "top", "app:confirmquit": true, "app:hideaibutton": false, "app:disablectrlshiftarrows": false, diff --git a/pkg/wconfig/metaconsts.go b/pkg/wconfig/metaconsts.go index 084dab1793..8195495ad2 100644 --- a/pkg/wconfig/metaconsts.go +++ b/pkg/wconfig/metaconsts.go @@ -17,6 +17,7 @@ const ( ConfigKey_AppDisableCtrlShiftArrows = "app:disablectrlshiftarrows" ConfigKey_AppDisableCtrlShiftDisplay = "app:disablectrlshiftdisplay" ConfigKey_AppFocusFollowsCursor = "app:focusfollowscursor" + ConfigKey_AppTabBar = "app:tabbar" ConfigKey_FeatureWaveAppBuilder = "feature:waveappbuilder" diff --git a/pkg/wconfig/settingsconfig.go b/pkg/wconfig/settingsconfig.go index 17aafa6685..b1b10d977f 100644 --- a/pkg/wconfig/settingsconfig.go +++ b/pkg/wconfig/settingsconfig.go @@ -68,6 +68,7 @@ type SettingsType struct { AppDisableCtrlShiftArrows bool `json:"app:disablectrlshiftarrows,omitempty"` AppDisableCtrlShiftDisplay bool `json:"app:disablectrlshiftdisplay,omitempty"` AppFocusFollowsCursor string `json:"app:focusfollowscursor,omitempty" jsonschema:"enum=off,enum=on,enum=term"` + AppTabBar string `json:"app:tabbar,omitempty" jsonschema:"enum=top,enum=left"` FeatureWaveAppBuilder bool `json:"feature:waveappbuilder,omitempty"` diff --git a/schema/settings.json b/schema/settings.json index 348c937dac..5213fed365 100644 --- a/schema/settings.json +++ b/schema/settings.json @@ -43,6 +43,13 @@ "term" ] }, + "app:tabbar": { + "type": "string", + "enum": [ + "top", + "left" + ] + }, "feature:waveappbuilder": { "type": "boolean" }, From 187c9c69a1f596d01d4c4c7db135027c444208e9 Mon Sep 17 00:00:00 2001 From: sawka Date: Fri, 13 Mar 2026 11:21:27 -0700 Subject: [PATCH 02/24] left tabs working, but needs UI cleanup --- frontend/app/tab/tabbar.tsx | 13 +++++++++---- frontend/app/tab/vtabbar.tsx | 2 +- frontend/app/workspace/workspace.tsx | 10 +++++++--- 3 files changed, 17 insertions(+), 8 deletions(-) diff --git a/frontend/app/tab/tabbar.tsx b/frontend/app/tab/tabbar.tsx index a5cbff3398..1d3939a37f 100644 --- a/frontend/app/tab/tabbar.tsx +++ b/frontend/app/tab/tabbar.tsx @@ -39,6 +39,7 @@ const OSOptions = { interface TabBarProps { workspace: Workspace; + noTabs?: boolean; } const WaveAIButton = memo(({ divRef }: { divRef?: React.RefObject }) => { @@ -152,7 +153,7 @@ function strArrayIsEqual(a: string[], b: string[]) { return true; } -const TabBar = memo(({ workspace }: TabBarProps) => { +const TabBar = memo(({ workspace, noTabs }: TabBarProps) => { const env = useWaveEnv(); const [tabIds, setTabIds] = useState([]); const [dragStartPositions, setDragStartPositions] = useState([]); @@ -680,8 +681,12 @@ const TabBar = memo(({ workspace }: TabBarProps) => {
-
- {tabIds.map((tabId, index) => { +
+ {!noTabs && tabIds.map((tabId, index) => { const isActive = activeTabId === tabId; const showDivider = index !== 0 && !isActive && index !== activeTabIndex + 1; return ( @@ -706,7 +711,7 @@ const TabBar = memo(({ workspace }: TabBarProps) => { diff --git a/frontend/util/util.ts b/frontend/util/util.ts index 2e5a3b5a13..8c2d330580 100644 --- a/frontend/util/util.ts +++ b/frontend/util/util.ts @@ -1,4 +1,4 @@ -// Copyright 2025, Command Line Inc. +// Copyright 2026, Command Line Inc. // SPDX-License-Identifier: Apache-2.0s import base64 from "base64-js"; @@ -108,7 +108,7 @@ function jsonDeepEqual(v1: any, v2: any): boolean { if (keys1.length !== keys2.length) { return false; } - for (let key of keys1) { + for (const key of keys1) { if (!jsonDeepEqual(v1[key], v2[key])) { return false; } From 64bf73f3975686a1cead20c35e3a54859612fb3a Mon Sep 17 00:00:00 2001 From: sawka Date: Fri, 13 Mar 2026 11:46:15 -0700 Subject: [PATCH 04/24] hover working --- frontend/app/tab/vtab.tsx | 14 ++++++-------- frontend/app/tab/vtabbar.tsx | 13 ++++++++++++- 2 files changed, 18 insertions(+), 9 deletions(-) diff --git a/frontend/app/tab/vtab.tsx b/frontend/app/tab/vtab.tsx index 7969001573..154f40716e 100644 --- a/frontend/app/tab/vtab.tsx +++ b/frontend/app/tab/vtab.tsx @@ -29,6 +29,7 @@ interface VTabProps { onDragOver: (event: React.DragEvent) => void; onDrop: (event: React.DragEvent) => void; onDragEnd: () => void; + onHoverChanged?: (isHovered: boolean) => void; } export function VTab({ @@ -44,6 +45,7 @@ export function VTab({ onDragOver, onDrop, onDragEnd, + onHoverChanged, }: VTabProps) { const [originalName, setOriginalName] = useState(tab.name); const [isEditable, setIsEditable] = useState(false); @@ -145,20 +147,16 @@ export function VTab({ onDragOver={onDragOver} onDrop={onDrop} onDragEnd={onDragEnd} + onMouseEnter={() => onHoverChanged?.(true)} + onMouseLeave={() => onHoverChanged?.(false)} className={cn( "group relative flex h-9 w-full cursor-pointer items-center pl-3 text-xs transition-colors select-none", "whitespace-nowrap", - active - ? "text-primary" - : isReordering - ? "text-secondary" - : "text-secondary hover:text-primary", + active ? "text-primary" : isReordering ? "text-secondary" : "text-secondary hover:text-primary", isDragging && "opacity-50" )} > - {active && ( -
- )} + {active &&
} {!active && !isReordering && (
)} diff --git a/frontend/app/tab/vtabbar.tsx b/frontend/app/tab/vtabbar.tsx index 73a496b44d..0f77c4b10b 100644 --- a/frontend/app/tab/vtabbar.tsx +++ b/frontend/app/tab/vtabbar.tsx @@ -47,6 +47,7 @@ interface VTabWrapperProps { onDragOver: (event: React.DragEvent) => void; onDrop: (event: React.DragEvent) => void; onDragEnd: () => void; + onHoverChanged: (isHovered: boolean) => void; } function VTabWrapper({ @@ -63,6 +64,7 @@ function VTabWrapper({ onDragOver, onDrop, onDragEnd, + onHoverChanged, }: VTabWrapperProps) { const env = useWaveEnv(); const [tabData] = env.wos.useWaveObjectValue(makeORef("tab", tabId)); @@ -101,6 +103,7 @@ function VTabWrapper({ onDragOver={onDragOver} onDrop={onDrop} onDragEnd={onDragEnd} + onHoverChanged={onHoverChanged} /> ); } @@ -116,6 +119,8 @@ export function VTabBar({ workspace, width, className }: VTabBarProps) { const [dropIndex, setDropIndex] = useState(null); const [dropLineTop, setDropLineTop] = useState(null); const [hoverResetVersion, setHoverResetVersion] = useState(0); + const [hoveredTabId, setHoveredTabId] = useState(null); + const [isNewTabHovered, setIsNewTabHovered] = useState(false); const dragSourceRef = useRef(null); const didResetHoverForDragRef = useRef(false); @@ -190,14 +195,17 @@ export function VTabBar({ workspace, width, className }: VTabBarProps) { > {orderedTabIds.map((tabId, index) => { const isActive = tabId === activeTabId; + const isHovered = tabId === hoveredTabId; + const isLast = index === orderedTabIds.length - 1; const nextTabId = orderedTabIds[index + 1]; const isNextActive = nextTabId === activeTabId; + const isNextHovered = nextTabId === hoveredTabId; return ( setHoveredTabId(isHovered ? tabId : null)} /> ); })} @@ -244,6 +253,8 @@ export function VTabBar({ workspace, width, className }: VTabBarProps) { type="button" className="group relative flex h-9 w-full shrink-0 cursor-pointer items-center gap-1.5 pl-3 pr-3 text-xs text-secondary/60 transition-colors hover:text-primary select-none" onClick={() => env.electron.createTab()} + onMouseEnter={() => setIsNewTabHovered(true)} + onMouseLeave={() => setIsNewTabHovered(false)} aria-label="New Tab" >
From 1dac092d008417471f68dbc30693dd89a6e0e792 Mon Sep 17 00:00:00 2001 From: sawka Date: Fri, 13 Mar 2026 12:18:39 -0700 Subject: [PATCH 05/24] update ui --- frontend/app/tab/vtab.tsx | 4 ++-- frontend/app/tab/vtabbar.tsx | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/frontend/app/tab/vtab.tsx b/frontend/app/tab/vtab.tsx index 154f40716e..a85139e238 100644 --- a/frontend/app/tab/vtab.tsx +++ b/frontend/app/tab/vtab.tsx @@ -156,9 +156,9 @@ export function VTab({ isDragging && "opacity-50" )} > - {active &&
} + {active &&
} {!active && !isReordering && ( -
+
)}
Date: Fri, 13 Mar 2026 13:22:39 -0700 Subject: [PATCH 06/24] remove width prop, fix ellipsis padding --- .gitignore | 1 + frontend/app/tab/vtab.tsx | 6 +++-- frontend/app/tab/vtabbar.tsx | 27 +++---------------- frontend/preview/previews/vtabbar.preview.tsx | 6 ++--- 4 files changed, 12 insertions(+), 28 deletions(-) diff --git a/.gitignore b/.gitignore index 161db5f191..a1c7240b5a 100644 --- a/.gitignore +++ b/.gitignore @@ -16,6 +16,7 @@ out/ make/ artifacts/ mikework/ +aiplans/ manifests/ .env out diff --git a/frontend/app/tab/vtab.tsx b/frontend/app/tab/vtab.tsx index a85139e238..09e344b4fd 100644 --- a/frontend/app/tab/vtab.tsx +++ b/frontend/app/tab/vtab.tsx @@ -156,7 +156,9 @@ export function VTab({ isDragging && "opacity-50" )} > - {active &&
} + {active && ( +
+ )} {!active && !isReordering && (
)} @@ -174,7 +176,7 @@ export function VTab({
400) { - return 400; - } - return width; -} - interface VTabWrapperProps { tabId: string; active: boolean; @@ -108,7 +94,7 @@ function VTabWrapper({ ); } -export function VTabBar({ workspace, width, className }: VTabBarProps) { +export function VTabBar({ workspace, className }: VTabBarProps) { const env = useWaveEnv(); const activeTabId = useAtomValue(env.atoms.staticTabId); const reinitVersion = useAtomValue(env.atoms.reinitVersion); @@ -134,8 +120,6 @@ export function VTabBar({ workspace, width, className }: VTabBarProps) { } }, [reinitVersion]); - const barWidth = useMemo(() => clampWidth(width), [width]); - const clearDragState = () => { if (dragSourceRef.current != null && !didResetHoverForDragRef.current) { didResetHoverForDragRef.current = true; @@ -170,11 +154,8 @@ export function VTabBar({ workspace, width, className }: VTabBarProps) { return (
setWidth(Number(event.target.value))} + onChange={(event) => setWidth(Math.max(100, Math.min(400, Number(event.target.value))))} className="w-full cursor-pointer" />

Drag tabs to reorder. Names, badges, and close buttons remain single-line.

-
- {workspace != null && } +
+ {workspace != null && }
); From ef0dd3280d3f964d7af980a16283e208d5236876 Mon Sep 17 00:00:00 2001 From: sawka Date: Fri, 13 Mar 2026 13:25:13 -0700 Subject: [PATCH 07/24] minor --- frontend/app/tab/vtabbar.tsx | 2 +- frontend/app/workspace/workspace.tsx | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/frontend/app/tab/vtabbar.tsx b/frontend/app/tab/vtabbar.tsx index e7a24e1f12..ef8a608193 100644 --- a/frontend/app/tab/vtabbar.tsx +++ b/frontend/app/tab/vtabbar.tsx @@ -232,7 +232,7 @@ export function VTabBar({ workspace, className }: VTabBarProps) { })} {dragTabId != null && dropIndex != null && dropLineTop != null && (
)}
+
); } diff --git a/frontend/app/tab/vtabbarenv.ts b/frontend/app/tab/vtabbarenv.ts index 48d6c61b82..75c665fb7b 100644 --- a/frontend/app/tab/vtabbarenv.ts +++ b/frontend/app/tab/vtabbarenv.ts @@ -19,6 +19,7 @@ export type VTabBarEnv = WaveEnvSubset<{ staticTabId: WaveEnv["atoms"]["staticTabId"]; fullConfigAtom: WaveEnv["atoms"]["fullConfigAtom"]; reinitVersion: WaveEnv["atoms"]["reinitVersion"]; + documentHasFocus: WaveEnv["atoms"]["documentHasFocus"]; }; wos: WaveEnv["wos"]; showContextMenu: WaveEnv["showContextMenu"]; From c3fd5ea50df5eaea4ca597b975fdb64b695cee0a Mon Sep 17 00:00:00 2001 From: sawka Date: Fri, 13 Mar 2026 15:33:45 -0700 Subject: [PATCH 16/24] scroll on drag --- frontend/app/tab/vtabbar.tsx | 56 ++++++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/frontend/app/tab/vtabbar.tsx b/frontend/app/tab/vtabbar.tsx index c60e0ad07e..a79f8a1d1e 100644 --- a/frontend/app/tab/vtabbar.tsx +++ b/frontend/app/tab/vtabbar.tsx @@ -124,6 +124,9 @@ export function VTabBar({ workspace, className }: VTabBarProps) { const dragSourceRef = useRef(null); const didResetHoverForDragRef = useRef(false); const scrollContainerRef = useRef(null); + const scrollAnimFrameRef = useRef(null); + const scrollDirectionRef = useRef(0); + const scrollSpeedRef = useRef(0); useEffect(() => { setOrderedTabIds(tabIds); @@ -143,7 +146,59 @@ export function VTabBar({ workspace, className }: VTabBarProps) { el?.scrollIntoView({ block: "nearest" }); }, [activeTabId, documentHasFocus]); + const stopScrollLoop = useCallback(() => { + if (scrollAnimFrameRef.current != null) { + cancelAnimationFrame(scrollAnimFrameRef.current); + scrollAnimFrameRef.current = null; + } + scrollDirectionRef.current = 0; + }, []); + + const startScrollLoop = useCallback(() => { + if (scrollAnimFrameRef.current != null) { + return; + } + const loop = () => { + const container = scrollContainerRef.current; + if (container == null || scrollDirectionRef.current === 0) { + scrollAnimFrameRef.current = null; + return; + } + container.scrollTop += scrollDirectionRef.current * scrollSpeedRef.current; + scrollAnimFrameRef.current = requestAnimationFrame(loop); + }; + scrollAnimFrameRef.current = requestAnimationFrame(loop); + }, []); + + const updateScrollFromDragY = useCallback( + (clientY: number) => { + const container = scrollContainerRef.current; + if (container == null) { + return; + } + const EdgeZone = 60; + const MaxScrollSpeed = 12; + const rect = container.getBoundingClientRect(); + const relY = clientY - rect.top; + const height = rect.height; + if (relY < EdgeZone) { + scrollDirectionRef.current = -1; + scrollSpeedRef.current = MaxScrollSpeed * (1 - relY / EdgeZone); + startScrollLoop(); + } else if (relY > height - EdgeZone) { + scrollDirectionRef.current = 1; + scrollSpeedRef.current = MaxScrollSpeed * (1 - (height - relY) / EdgeZone); + startScrollLoop(); + } else { + scrollDirectionRef.current = 0; + stopScrollLoop(); + } + }, + [startScrollLoop, stopScrollLoop] + ); + const clearDragState = () => { + stopScrollLoop(); if (dragSourceRef.current != null && !didResetHoverForDragRef.current) { didResetHoverForDragRef.current = true; setHoverResetVersion((version) => version + 1); @@ -185,6 +240,7 @@ export function VTabBar({ workspace, className }: VTabBarProps) { className="relative flex min-h-0 flex-1 flex-col overflow-y-auto" onDragOver={(event) => { event.preventDefault(); + updateScrollFromDragY(event.clientY); if (event.target === event.currentTarget) { setDropIndex(orderedTabIds.length); setDropLineTop(event.currentTarget.scrollHeight); From 2b81a882185df73ba9c7b5a86b1bbc212bc41787 Mon Sep 17 00:00:00 2001 From: sawka Date: Fri, 13 Mar 2026 15:41:45 -0700 Subject: [PATCH 17/24] fix document focus scroll --- frontend/app/tab/vtabbar.tsx | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/frontend/app/tab/vtabbar.tsx b/frontend/app/tab/vtabbar.tsx index a79f8a1d1e..fb78c10055 100644 --- a/frontend/app/tab/vtabbar.tsx +++ b/frontend/app/tab/vtabbar.tsx @@ -144,7 +144,15 @@ export function VTabBar({ workspace, className }: VTabBarProps) { } const el = scrollContainerRef.current.querySelector(`[data-tabid="${activeTabId}"]`); el?.scrollIntoView({ block: "nearest" }); - }, [activeTabId, documentHasFocus]); + }, [activeTabId]); + + useEffect(() => { + if (!documentHasFocus || activeTabId == null || scrollContainerRef.current == null) { + return; + } + const el = scrollContainerRef.current.querySelector(`[data-tabid="${activeTabId}"]`); + el?.scrollIntoView({ block: "nearest" }); + }, [documentHasFocus]); const stopScrollLoop = useCallback(() => { if (scrollAnimFrameRef.current != null) { From f61531d7191d7c7f9007a7bf9bd2022a287a74ca Mon Sep 17 00:00:00 2001 From: sawka Date: Fri, 13 Mar 2026 15:52:01 -0700 Subject: [PATCH 18/24] fix vtabbar width across the workspace. fix add tab button --- frontend/app/tab/vtabbar.tsx | 2 +- frontend/app/workspace/workspace-layout-model.ts | 8 ++++++++ frontend/app/workspace/workspace.tsx | 6 ++++++ 3 files changed, 15 insertions(+), 1 deletion(-) diff --git a/frontend/app/tab/vtabbar.tsx b/frontend/app/tab/vtabbar.tsx index fb78c10055..e3c7068068 100644 --- a/frontend/app/tab/vtabbar.tsx +++ b/frontend/app/tab/vtabbar.tsx @@ -245,7 +245,7 @@ export function VTabBar({ workspace, className }: VTabBarProps) { >
{ event.preventDefault(); updateScrollFromDragY(event.clientY); diff --git a/frontend/app/workspace/workspace-layout-model.ts b/frontend/app/workspace/workspace-layout-model.ts index d6a799e983..ac190b069d 100644 --- a/frontend/app/workspace/workspace-layout-model.ts +++ b/frontend/app/workspace/workspace-layout-model.ts @@ -265,6 +265,14 @@ class WorkspaceLayoutModel { // ---- Registration & sync ---- + syncVTabWidthFromMeta(): void { + const savedVTabWidth = globalStore.get(this.getVTabBarWidthAtom()); + if (savedVTabWidth != null && savedVTabWidth > 0 && savedVTabWidth !== this.vtabWidth) { + this.vtabWidth = savedVTabWidth; + this.commitLayouts(window.innerWidth); + } + } + registerRefs( aiPanelRef: ImperativePanelHandle, outerPanelGroupRef: ImperativePanelGroupHandle, diff --git a/frontend/app/workspace/workspace.tsx b/frontend/app/workspace/workspace.tsx index 3eb8df176f..6e72e2d658 100644 --- a/frontend/app/workspace/workspace.tsx +++ b/frontend/app/workspace/workspace.tsx @@ -74,6 +74,12 @@ const WorkspaceElem = memo(() => { return () => window.removeEventListener("resize", workspaceLayoutModel.handleWindowResize); }, []); + useEffect(() => { + const handleFocus = () => workspaceLayoutModel.syncVTabWidthFromMeta(); + window.addEventListener("focus", handleFocus); + return () => window.removeEventListener("focus", handleFocus); + }, []); + const innerHandleVisible = vtabVisible && aiPanelVisible; const innerHandleClass = `bg-transparent hover:bg-zinc-500/20 transition-colors ${innerHandleVisible ? "w-0.5" : "w-0 pointer-events-none"}`; const outerHandleVisible = vtabVisible || aiPanelVisible; From 9b28cf7a21f74465e55647176f65ff2d123dc31d Mon Sep 17 00:00:00 2001 From: sawka Date: Fri, 13 Mar 2026 16:15:30 -0700 Subject: [PATCH 19/24] change tab position from context menu --- frontend/app/tab/tab.tsx | 2 ++ frontend/app/tab/tabcontextmenu.ts | 16 ++++++++++++++++ frontend/app/tab/vtabbarenv.ts | 3 ++- 3 files changed, 20 insertions(+), 1 deletion(-) diff --git a/frontend/app/tab/tab.tsx b/frontend/app/tab/tab.tsx index 3ac494afeb..7b2aa6856e 100644 --- a/frontend/app/tab/tab.tsx +++ b/frontend/app/tab/tab.tsx @@ -19,6 +19,7 @@ import { buildTabContextMenu } from "./tabcontextmenu"; export type TabEnv = WaveEnvSubset<{ rpc: { ActivityCommand: WaveEnv["rpc"]["ActivityCommand"]; + SetConfigCommand: WaveEnv["rpc"]["SetConfigCommand"]; SetMetaCommand: WaveEnv["rpc"]["SetMetaCommand"]; UpdateTabNameCommand: WaveEnv["rpc"]["UpdateTabNameCommand"]; }; @@ -26,6 +27,7 @@ export type TabEnv = WaveEnvSubset<{ fullConfigAtom: WaveEnv["atoms"]["fullConfigAtom"]; }; wos: WaveEnv["wos"]; + getSettingsKeyAtom: WaveEnv["getSettingsKeyAtom"]; showContextMenu: WaveEnv["showContextMenu"]; }>; diff --git a/frontend/app/tab/tabcontextmenu.ts b/frontend/app/tab/tabcontextmenu.ts index 5ffb11b361..8cef7739fb 100644 --- a/frontend/app/tab/tabcontextmenu.ts +++ b/frontend/app/tab/tabcontextmenu.ts @@ -85,6 +85,22 @@ function buildTabContextMenu( } menu.push({ label: "Backgrounds", type: "submenu", submenu }, { type: "separator" }); } + const currentTabBar = globalStore.get(env.getSettingsKeyAtom("app:tabbar")) ?? "top"; + const tabBarSubmenu: ContextMenuItem[] = [ + { + label: "Top", + type: "checkbox", + checked: currentTabBar === "top", + click: () => fireAndForget(() => env.rpc.SetConfigCommand(TabRpcClient, { "app:tabbar": "top" })), + }, + { + label: "Left", + type: "checkbox", + checked: currentTabBar === "left", + click: () => fireAndForget(() => env.rpc.SetConfigCommand(TabRpcClient, { "app:tabbar": "left" })), + }, + ]; + menu.push({ label: "Tab Bar Position", type: "submenu", submenu: tabBarSubmenu }, { type: "separator" }); menu.push({ label: "Close Tab", click: () => onClose(null) }); return menu; } diff --git a/frontend/app/tab/vtabbarenv.ts b/frontend/app/tab/vtabbarenv.ts index 75c665fb7b..ac61fe95c6 100644 --- a/frontend/app/tab/vtabbarenv.ts +++ b/frontend/app/tab/vtabbarenv.ts @@ -13,6 +13,7 @@ export type VTabBarEnv = WaveEnvSubset<{ UpdateWorkspaceTabIdsCommand: WaveEnv["rpc"]["UpdateWorkspaceTabIdsCommand"]; UpdateTabNameCommand: WaveEnv["rpc"]["UpdateTabNameCommand"]; ActivityCommand: WaveEnv["rpc"]["ActivityCommand"]; + SetConfigCommand: WaveEnv["rpc"]["SetConfigCommand"]; SetMetaCommand: WaveEnv["rpc"]["SetMetaCommand"]; }; atoms: { @@ -23,7 +24,7 @@ export type VTabBarEnv = WaveEnvSubset<{ }; wos: WaveEnv["wos"]; showContextMenu: WaveEnv["showContextMenu"]; - getSettingsKeyAtom: SettingsKeyAtomFnType<"tab:confirmclose">; + getSettingsKeyAtom: SettingsKeyAtomFnType<"tab:confirmclose" | "app:tabbar">; mockSetWaveObj: WaveEnv["mockSetWaveObj"]; isWindows: WaveEnv["isWindows"]; isMacOS: WaveEnv["isMacOS"]; From ca8979b39655cfebe699d0769f93c1059d617541 Mon Sep 17 00:00:00 2001 From: sawka Date: Fri, 13 Mar 2026 16:16:12 -0700 Subject: [PATCH 20/24] update reservation for traffic lights width on macos tahoe --- frontend/app/store/wshclientapi.ts | 6 ++++ frontend/app/tab/tabbar.tsx | 57 ++++++++++++++++++------------ frontend/app/tab/tabbarenv.ts | 7 +++- frontend/util/platformutil.ts | 15 +++++++- frontend/wave.ts | 5 +++ pkg/wshrpc/wshclient/wshclient.go | 6 ++++ pkg/wshrpc/wshrpctypes.go | 1 + pkg/wshrpc/wshserver/wshserver.go | 4 +++ 8 files changed, 76 insertions(+), 25 deletions(-) diff --git a/frontend/app/store/wshclientapi.ts b/frontend/app/store/wshclientapi.ts index 6b9f4a72d4..d64f7f06b0 100644 --- a/frontend/app/store/wshclientapi.ts +++ b/frontend/app/store/wshclientapi.ts @@ -618,6 +618,12 @@ export class RpcApiType { return client.wshRpcCall("listalleditableapps", null, opts); } + // command "macosversion" [call] + MacOSVersionCommand(client: WshClient, opts?: RpcOpts): Promise { + if (this.mockClient) return this.mockClient.mockWshRpcCall(client, "macosversion", null, opts); + return client.wshRpcCall("macosversion", null, opts); + } + // command "makedraftfromlocal" [call] MakeDraftFromLocalCommand(client: WshClient, data: CommandMakeDraftFromLocalData, opts?: RpcOpts): Promise { if (this.mockClient) return this.mockClient.mockWshRpcCall(client, "makedraftfromlocal", data, opts); diff --git a/frontend/app/tab/tabbar.tsx b/frontend/app/tab/tabbar.tsx index 1d3939a37f..9bd34898f0 100644 --- a/frontend/app/tab/tabbar.tsx +++ b/frontend/app/tab/tabbar.tsx @@ -7,6 +7,7 @@ import { TabRpcClient } from "@/app/store/wshrpcutil"; import { useWaveEnv } from "@/app/waveenv/waveenv"; import { WorkspaceLayoutModel } from "@/app/workspace/workspace-layout-model"; import { deleteLayoutModelForTab } from "@/layout/index"; +import { isMacOSTahoeOrLater } from "@/util/platformutil"; import { fireAndForget } from "@/util/util"; import { useAtomValue } from "jotai"; import { OverlayScrollbars } from "overlayscrollbars"; @@ -20,6 +21,9 @@ import { WorkspaceSwitcher } from "./workspaceswitcher"; const TabDefaultWidth = 130; const TabMinWidth = 100; +const MacOSTrafficLightsWidth = 74; +const MacOSTahoeTrafficLightsWidth = 80; + const OSOptions = { overflow: { x: "scroll", @@ -636,10 +640,13 @@ const TabBar = memo(({ workspace, noTabs }: TabBarProps) => { // Calculate window drag left width based on platform and state let windowDragLeftWidth = 10; if (env.isMacOS() && !isFullScreen) { + const trafficLightsWidth = isMacOSTahoeOrLater() + ? MacOSTahoeTrafficLightsWidth + : MacOSTrafficLightsWidth; if (zoomFactor > 0) { - windowDragLeftWidth = 74 / zoomFactor; + windowDragLeftWidth = trafficLightsWidth / zoomFactor; } else { - windowDragLeftWidth = 74; + windowDragLeftWidth = trafficLightsWidth; } } @@ -684,28 +691,32 @@ const TabBar = memo(({ workspace, noTabs }: TabBarProps) => {
- {!noTabs && tabIds.map((tabId, index) => { - const isActive = activeTabId === tabId; - const showDivider = index !== 0 && !isActive && index !== activeTabIndex + 1; - return ( - handleSelectTab(tabId)} - active={isActive} - onDragStart={(event) => handleDragStart(event, tabId, tabRefs.current[index])} - onClose={(event) => handleCloseTab(event, tabId)} - onLoaded={() => handleTabLoaded(tabId)} - isDragging={draggingTab === tabId} - tabWidth={tabWidthRef.current} - isNew={tabId === newTabId} - /> - ); - })} + {!noTabs && + tabIds.map((tabId, index) => { + const isActive = activeTabId === tabId; + const showDivider = index !== 0 && !isActive && index !== activeTabIndex + 1; + return ( + handleSelectTab(tabId)} + active={isActive} + onDragStart={(event) => handleDragStart(event, tabId, tabRefs.current[index])} + onClose={(event) => handleCloseTab(event, tabId)} + onLoaded={() => handleTabLoaded(tabId)} + isDragging={draggingTab === tabId} + tabWidth={tabWidthRef.current} + isNew={tabId === newTabId} + /> + ); + })}