Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 24 additions & 15 deletions frontend/app/workspace/workspace-layout-model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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) =>
Expand Down Expand Up @@ -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);
}
Expand Down Expand Up @@ -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
Expand All @@ -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);

Expand Down Expand Up @@ -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;
Expand All @@ -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);
}
Expand Down Expand Up @@ -360,23 +367,23 @@ 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;
if (total === 0) return 50;
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;
Expand Down Expand Up @@ -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);
Expand Down
52 changes: 33 additions & 19 deletions frontend/app/workspace/workspace.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<ImperativePanelGroupHandle>(null);
const innerPanelGroupRef = useRef<ImperativePanelGroupHandle>(null);
const aiPanelRef = useRef<ImperativePanelHandle>(null);
Expand All @@ -59,8 +62,8 @@ const WorkspaceElem = memo(() => {
const aiPanelWrapperRef = useRef<HTMLDivElement>(null);
const vtabPanelWrapperRef = useRef<HTMLDivElement>(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 &&
Expand All @@ -77,7 +80,8 @@ const WorkspaceElem = memo(() => {
aiPanelWrapperRef.current,
vtabPanelRef.current ?? undefined,
vtabPanelWrapperRef.current ?? undefined,
showLeftTabBar
showVTabBar,
tabBarOnRight
);
}
}, []);
Expand All @@ -93,32 +97,42 @@ const WorkspaceElem = memo(() => {
}, []);

useEffect(() => {
workspaceLayoutModel.setShowLeftTabBar(showLeftTabBar);
}, [showLeftTabBar]);
workspaceLayoutModel.setShowVTabBar(showVTabBar, tabBarOnRight);
}, [showVTabBar, tabBarOnRight]);

useEffect(() => {
const handleFocus = () => workspaceLayoutModel.syncVTabWidthFromMeta();
window.addEventListener("focus", handleFocus);
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 (
<div className="flex flex-col w-full flex-grow overflow-hidden">
{!(showLeftTabBar && isMacOS()) && <TabBar key={ws.oid} workspace={ws} noTabs={showLeftTabBar} />}
{showLeftTabBar && isMacOS() && <MacOSTabBarSpacer />}
{!(showVTabBar && isMacOS()) && <TabBar key={ws.oid} workspace={ws} noTabs={showVTabBar} />}
{showVTabBar && isMacOS() && <MacOSTabBarSpacer />}
<div ref={panelContainerRef} className="flex flex-row flex-grow overflow-hidden">
<ErrorBoundary key={tabId}>
<PanelGroup
direction="horizontal"
onLayout={workspaceLayoutModel.handleOuterPanelLayout}
ref={outerPanelGroupRef}
>
<Panel order={0} defaultSize={leftGroupInitialPct} className="overflow-hidden">
<Panel order={sideGroupOrder} defaultSize={leftGroupInitialPct} className="overflow-hidden">
<PanelGroup
direction="horizontal"
onLayout={workspaceLayoutModel.handleInnerPanelLayout}
Expand All @@ -128,37 +142,37 @@ const WorkspaceElem = memo(() => {
ref={vtabPanelRef}
collapsible
defaultSize={innerVTabInitialPct}
order={0}
order={vtabOrder}
className="overflow-hidden"
>
<div ref={vtabPanelWrapperRef} className="w-full h-full">
{showLeftTabBar && <VTabBar workspace={ws} />}
{showVTabBar && <VTabBar workspace={ws} />}
</div>
</Panel>
<PanelResizeHandle className={innerHandleClass} />
<Panel
ref={aiPanelRef}
collapsible
defaultSize={innerAIPanelInitialPct}
order={1}
order={aiPanelOrder}
className="overflow-hidden"
>
<div
ref={aiPanelWrapperRef}
className={`w-full h-full pr-0.5 ${aiPanelVisible ? "" : "opacity-0"}`}
className={`w-full h-full ${aiWrapperPaddingClass} ${aiPanelVisible ? "" : "opacity-0"}`}
>
{tabId !== "" && <AIPanel roundTopLeft={showLeftTabBar} />}
</div>
</Panel>
</PanelGroup>
</Panel>
<PanelResizeHandle className={outerHandleClass} />
<Panel order={1} defaultSize={100 - leftGroupInitialPct}>
<Panel order={contentOrder} defaultSize={100 - leftGroupInitialPct}>
{tabId === "" ? (
<CenteredDiv>No Active Tab</CenteredDiv>
) : (
<div className="flex flex-row h-full">
<TabContent key={tabId} tabId={tabId} noTopPadding={showLeftTabBar && isMacOS()} />
<TabContent key={tabId} tabId={tabId} noTopPadding={showVTabBar && isMacOS()} />
{widgetsSidebarVisible && <Widgets />}
</div>
)}
Expand Down
2 changes: 1 addition & 1 deletion pkg/wconfig/settingsconfig.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`

Expand Down
3 changes: 2 additions & 1 deletion schema/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,8 @@
"type": "string",
"enum": [
"top",
"left"
"left",
"right"
]
},
"feature:waveappbuilder": {
Expand Down