diff --git a/frontend/app/workspace/workspace-layout-model.ts b/frontend/app/workspace/workspace-layout-model.ts index eb7065f90c..52eedcd3be 100644 --- a/frontend/app/workspace/workspace-layout-model.ts +++ b/frontend/app/workspace/workspace-layout-model.ts @@ -52,6 +52,7 @@ class WorkspaceLayoutModel { private aiPanelWidth: number | null; private vtabWidth: number; private vtabVisible: boolean; + private tabBarOnRight: boolean; private transitionTimeoutRef: NodeJS.Timeout | null = null; private focusTimeoutRef: NodeJS.Timeout | null = null; private debouncedPersistAIWidth: () => void; @@ -71,6 +72,7 @@ class WorkspaceLayoutModel { this.aiPanelWidth = null; this.vtabWidth = VTabBar_DefaultWidth; this.vtabVisible = false; + this.tabBarOnRight = false; this.panelVisibleAtom = jotai.atom(false); this.widgetsSidebarVisibleAtom = jotai.atom( (get) => @@ -157,8 +159,9 @@ class WorkspaceLayoutModel { this.vtabWidth = savedVTabWidth; } const tabBarPosition = globalStore.get(getSettingsKeyAtom("app:tabbar")) ?? "top"; - const showLeftTabBar = tabBarPosition === "left" && !isBuilderWindow(); - this.vtabVisible = showLeftTabBar; + const showVTabBar = (tabBarPosition === "left" || tabBarPosition === "right") && !isBuilderWindow(); + this.vtabVisible = showVTabBar; + this.tabBarOnRight = tabBarPosition === "right" && !isBuilderWindow(); } catch (e) { console.warn("Failed to initialize from tab meta:", e); } @@ -224,7 +227,8 @@ class WorkspaceLayoutModel { handleOuterPanelLayout(sizes: number[]): void { if (this.inResize) return; const windowWidth = window.innerWidth; - const newLeftGroupPx = (sizes[0] / 100) * windowWidth; + const sideGroupPct = this.tabBarOnRight ? sizes[1] : sizes[0]; + const newLeftGroupPx = (sideGroupPct / 100) * windowWidth; if (this.vtabVisible && this.aiPanelVisible) { // vtab stays constant, aipanel absorbs the change @@ -251,7 +255,8 @@ class WorkspaceLayoutModel { const aiW = this.getResolvedAIWidth(windowWidth); const leftGroupW = vtabW + aiW; - const newVTabW = (sizes[0] / 100) * leftGroupW; + const vtabPct = this.tabBarOnRight ? sizes[1] : sizes[0]; + const newVTabW = (vtabPct / 100) * leftGroupW; const clampedVTab = clampVTabWidth(newVTabW); const newAIW = clampAIPanelWidth(leftGroupW - clampedVTab, windowWidth); @@ -289,7 +294,8 @@ class WorkspaceLayoutModel { aiPanelWrapperRef: HTMLDivElement, vtabPanelRef?: ImperativePanelHandle, vtabPanelWrapperRef?: HTMLDivElement, - showLeftTabBar?: boolean + showVTabBar?: boolean, + tabBarOnRight?: boolean ): void { this.aiPanelRef = aiPanelRef; this.vtabPanelRef = vtabPanelRef ?? null; @@ -298,7 +304,8 @@ class WorkspaceLayoutModel { this.panelContainerRef = panelContainerRef; this.aiPanelWrapperRef = aiPanelWrapperRef; this.vtabPanelWrapperRef = vtabPanelWrapperRef ?? null; - this.vtabVisible = showLeftTabBar ?? false; + this.vtabVisible = showVTabBar ?? false; + this.tabBarOnRight = tabBarOnRight ?? false; this.syncPanelCollapse(); this.commitLayouts(window.innerWidth); } @@ -360,14 +367,14 @@ class WorkspaceLayoutModel { // ---- Initial percentage helpers (used by workspace.tsx for defaultSize) ---- - getLeftGroupInitialPercentage(windowWidth: number, showLeftTabBar: boolean): number { - const vtabW = showLeftTabBar && !isBuilderWindow() ? this.getResolvedVTabWidth() : 0; + getLeftGroupInitialPercentage(windowWidth: number, showVTabBar: boolean): number { + const vtabW = showVTabBar && !isBuilderWindow() ? this.getResolvedVTabWidth() : 0; const aiW = this.aiPanelVisible ? this.getResolvedAIWidth(windowWidth) : 0; return ((vtabW + aiW) / windowWidth) * 100; } - getInnerVTabInitialPercentage(windowWidth: number, showLeftTabBar: boolean): number { - if (!showLeftTabBar || isBuilderWindow()) return 0; + getInnerVTabInitialPercentage(windowWidth: number, showVTabBar: boolean): number { + if (!showVTabBar || isBuilderWindow()) return 0; const vtabW = this.getResolvedVTabWidth(); const aiW = this.aiPanelVisible ? this.getResolvedAIWidth(windowWidth) : 0; const total = vtabW + aiW; @@ -375,8 +382,8 @@ class WorkspaceLayoutModel { return (vtabW / total) * 100; } - getInnerAIPanelInitialPercentage(windowWidth: number, showLeftTabBar: boolean): number { - const vtabW = showLeftTabBar && !isBuilderWindow() ? this.getResolvedVTabWidth() : 0; + getInnerAIPanelInitialPercentage(windowWidth: number, showVTabBar: boolean): number { + const vtabW = showVTabBar && !isBuilderWindow() ? this.getResolvedVTabWidth() : 0; const aiW = this.aiPanelVisible ? this.getResolvedAIWidth(windowWidth) : 0; const total = vtabW + aiW; if (total === 0) return 50; @@ -426,9 +433,11 @@ class WorkspaceLayoutModel { } } - setShowLeftTabBar(showLeftTabBar: boolean): void { - if (this.vtabVisible === showLeftTabBar) return; - this.vtabVisible = showLeftTabBar; + setShowVTabBar(showVTabBar: boolean, tabBarOnRight?: boolean): void { + const newRight = tabBarOnRight ?? this.tabBarOnRight; + if (this.vtabVisible === showVTabBar && this.tabBarOnRight === newRight) return; + this.vtabVisible = showVTabBar; + this.tabBarOnRight = newRight; this.enableTransitions(250); this.syncPanelCollapse(); this.commitLayouts(window.innerWidth); diff --git a/frontend/app/workspace/workspace.tsx b/frontend/app/workspace/workspace.tsx index 08278a4eed..c3ec6fb8fb 100644 --- a/frontend/app/workspace/workspace.tsx +++ b/frontend/app/workspace/workspace.tsx @@ -45,12 +45,15 @@ const WorkspaceElem = memo(() => { const ws = useAtomValue(atoms.workspace); const tabBarPosition = useAtomValue(getSettingsKeyAtom("app:tabbar")) ?? "top"; const showLeftTabBar = tabBarPosition === "left"; + const showRightTabBar = tabBarPosition === "right"; + const showVTabBar = showLeftTabBar || showRightTabBar; + const tabBarOnRight = showRightTabBar; const aiPanelVisible = useAtomValue(workspaceLayoutModel.panelVisibleAtom); const widgetsSidebarVisible = useAtomValue(workspaceLayoutModel.widgetsSidebarVisibleAtom); const windowWidth = window.innerWidth; - const leftGroupInitialPct = workspaceLayoutModel.getLeftGroupInitialPercentage(windowWidth, showLeftTabBar); - const innerVTabInitialPct = workspaceLayoutModel.getInnerVTabInitialPercentage(windowWidth, showLeftTabBar); - const innerAIPanelInitialPct = workspaceLayoutModel.getInnerAIPanelInitialPercentage(windowWidth, showLeftTabBar); + const leftGroupInitialPct = workspaceLayoutModel.getLeftGroupInitialPercentage(windowWidth, showVTabBar); + const innerVTabInitialPct = workspaceLayoutModel.getInnerVTabInitialPercentage(windowWidth, showVTabBar); + const innerAIPanelInitialPct = workspaceLayoutModel.getInnerAIPanelInitialPercentage(windowWidth, showVTabBar); const outerPanelGroupRef = useRef(null); const innerPanelGroupRef = useRef(null); const aiPanelRef = useRef(null); @@ -59,8 +62,8 @@ const WorkspaceElem = memo(() => { const aiPanelWrapperRef = useRef(null); const vtabPanelWrapperRef = useRef(null); - // showLeftTabBar is passed as a seed value only; subsequent changes are handled by setShowLeftTabBar below. - // Do NOT add showLeftTabBar as a dep here — re-registering refs on config changes would redundantly re-run commitLayouts. + // showVTabBar / tabBarOnRight are passed as seed values only; subsequent changes flow through setShowVTabBar below. + // Do NOT add them as deps here — re-registering refs on config changes would redundantly re-run commitLayouts. useEffect(() => { if ( aiPanelRef.current && @@ -77,7 +80,8 @@ const WorkspaceElem = memo(() => { aiPanelWrapperRef.current, vtabPanelRef.current ?? undefined, vtabPanelWrapperRef.current ?? undefined, - showLeftTabBar + showVTabBar, + tabBarOnRight ); } }, []); @@ -93,8 +97,8 @@ const WorkspaceElem = memo(() => { }, []); useEffect(() => { - workspaceLayoutModel.setShowLeftTabBar(showLeftTabBar); - }, [showLeftTabBar]); + workspaceLayoutModel.setShowVTabBar(showVTabBar, tabBarOnRight); + }, [showVTabBar, tabBarOnRight]); useEffect(() => { const handleFocus = () => workspaceLayoutModel.syncVTabWidthFromMeta(); @@ -102,15 +106,25 @@ const WorkspaceElem = memo(() => { return () => window.removeEventListener("focus", handleFocus); }, []); - const innerHandleVisible = showLeftTabBar && aiPanelVisible; + const innerHandleVisible = showVTabBar && aiPanelVisible; const innerHandleClass = `bg-transparent hover:bg-zinc-500/20 transition-colors ${innerHandleVisible ? "w-0.5" : "w-0 pointer-events-none"}`; - const outerHandleVisible = showLeftTabBar || aiPanelVisible; + const outerHandleVisible = showVTabBar || aiPanelVisible; const outerHandleClass = `bg-transparent hover:bg-zinc-500/20 transition-colors ${outerHandleVisible ? "w-0.5" : "w-0 pointer-events-none"}`; + // When tabBarOnRight, mirror panel ordering so vtab sits at the outer-right edge + // and content moves to the leftmost panel. The defaultSize percentages stay the + // same; only the `order` prop flips, which is what react-resizable-panels uses + // to determine left-to-right placement. + const sideGroupOrder = tabBarOnRight ? 1 : 0; + const contentOrder = tabBarOnRight ? 0 : 1; + const vtabOrder = tabBarOnRight ? 1 : 0; + const aiPanelOrder = tabBarOnRight ? 0 : 1; + const aiWrapperPaddingClass = tabBarOnRight ? "pl-0.5" : "pr-0.5"; + return (
- {!(showLeftTabBar && isMacOS()) && } - {showLeftTabBar && isMacOS() && } + {!(showVTabBar && isMacOS()) && } + {showVTabBar && isMacOS() && }
{ onLayout={workspaceLayoutModel.handleOuterPanelLayout} ref={outerPanelGroupRef} > - + { ref={vtabPanelRef} collapsible defaultSize={innerVTabInitialPct} - order={0} + order={vtabOrder} className="overflow-hidden" >
- {showLeftTabBar && } + {showVTabBar && }
@@ -140,12 +154,12 @@ const WorkspaceElem = memo(() => { ref={aiPanelRef} collapsible defaultSize={innerAIPanelInitialPct} - order={1} + order={aiPanelOrder} className="overflow-hidden" >
{tabId !== "" && }
@@ -153,12 +167,12 @@ const WorkspaceElem = memo(() => {
- + {tabId === "" ? ( No Active Tab ) : (
- + {widgetsSidebarVisible && }
)} diff --git a/pkg/wconfig/settingsconfig.go b/pkg/wconfig/settingsconfig.go index 67118b1670..2f22e60bc5 100644 --- a/pkg/wconfig/settingsconfig.go +++ b/pkg/wconfig/settingsconfig.go @@ -68,7 +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"` + AppTabBar string `json:"app:tabbar,omitempty" jsonschema:"enum=top,enum=left,enum=right"` FeatureWaveAppBuilder bool `json:"feature:waveappbuilder,omitempty"` diff --git a/schema/settings.json b/schema/settings.json index f341a0f365..46f0a16a85 100644 --- a/schema/settings.json +++ b/schema/settings.json @@ -47,7 +47,8 @@ "type": "string", "enum": [ "top", - "left" + "left", + "right" ] }, "feature:waveappbuilder": {