Skip to content
Merged
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
56 changes: 27 additions & 29 deletions frontend/app/workspace/workspace-layout-model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import * as WOS from "@/app/store/wos";
import { RpcApi } from "@/app/store/wshclientapi";
import { TabRpcClient } from "@/app/store/wshrpcutil";
import { getLayoutModelForStaticTab } from "@/layout/lib/layoutModelHooks";
import { atoms, getApi, getOrefMetaKeyAtom, recordTEvent, refocusNode } from "@/store/global";
import { atoms, getApi, getOrefMetaKeyAtom, getSettingsKeyAtom, recordTEvent, refocusNode } from "@/store/global";
import debug from "debug";
import * as jotai from "jotai";
import { debounce } from "lodash-es";
Expand Down Expand Up @@ -44,19 +44,18 @@ class WorkspaceLayoutModel {
innerPanelGroupRef: ImperativePanelGroupHandle | null;
panelContainerRef: HTMLDivElement | null;
aiPanelWrapperRef: HTMLDivElement | null;
vtabPanelWrapperRef: HTMLDivElement | null;
panelVisibleAtom: jotai.PrimitiveAtom<boolean>;
vtabVisibleAtom: jotai.PrimitiveAtom<boolean>;

private inResize: boolean;
private aiPanelVisible: boolean;
private aiPanelWidth: number | null;
private vtabWidth: number;
private vtabVisible: boolean;
private initialized: boolean = false;
private transitionTimeoutRef: NodeJS.Timeout | null = null;
private focusTimeoutRef: NodeJS.Timeout | null = null;
private debouncedPersistAIWidth: (width: number) => void;
private debouncedPersistVTabWidth: (width: number) => void;
private debouncedPersistAIWidth: () => void;
private debouncedPersistVTabWidth: () => void;

private constructor() {
this.aiPanelRef = null;
Expand All @@ -65,19 +64,23 @@ class WorkspaceLayoutModel {
this.innerPanelGroupRef = null;
this.panelContainerRef = null;
this.aiPanelWrapperRef = null;
this.vtabPanelWrapperRef = null;
this.inResize = false;
this.aiPanelVisible = false;
this.aiPanelWidth = null;
this.vtabWidth = VTabBar_DefaultWidth;
this.vtabVisible = false;
this.panelVisibleAtom = jotai.atom(false);
this.vtabVisibleAtom = jotai.atom(false);
this.initializeFromMeta();

this.handleWindowResize = this.handleWindowResize.bind(this);
this.handleOuterPanelLayout = this.handleOuterPanelLayout.bind(this);
this.handleInnerPanelLayout = this.handleInnerPanelLayout.bind(this);

this.debouncedPersistAIWidth = debounce((width: number) => {
this.debouncedPersistAIWidth = debounce(() => {
if (!this.aiPanelVisible) return;
const width = this.aiPanelWrapperRef?.offsetWidth;
if (width == null || width <= 0) return;
try {
RpcApi.SetMetaCommand(TabRpcClient, {
oref: WOS.makeORef("tab", this.getTabId()),
Expand All @@ -88,7 +91,10 @@ class WorkspaceLayoutModel {
}
}, 300);

this.debouncedPersistVTabWidth = debounce((width: number) => {
this.debouncedPersistVTabWidth = debounce(() => {
if (!this.vtabVisible) return;
const width = this.vtabPanelWrapperRef?.offsetWidth;
if (width == null || width <= 0) return;
try {
RpcApi.SetMetaCommand(TabRpcClient, {
oref: WOS.makeORef("workspace", this.getWorkspaceId()),
Expand Down Expand Up @@ -130,8 +136,6 @@ class WorkspaceLayoutModel {
}

private initializeFromMeta(): void {
if (this.initialized) return;
this.initialized = true;
try {
const savedVisible = globalStore.get(this.getPanelOpenAtom());
const savedAIWidth = globalStore.get(this.getPanelWidthAtom());
Expand All @@ -146,6 +150,9 @@ class WorkspaceLayoutModel {
if (savedVTabWidth != null && savedVTabWidth > 0) {
this.vtabWidth = savedVTabWidth;
}
const tabBarPosition = globalStore.get(getSettingsKeyAtom("app:tabbar")) ?? "top";
const showLeftTabBar = tabBarPosition === "left" && !isBuilderWindow();
this.vtabVisible = showLeftTabBar;
} catch (e) {
console.warn("Failed to initialize from tab meta:", e);
}
Expand All @@ -154,7 +161,6 @@ class WorkspaceLayoutModel {
// ---- Resolved width getters (always clamped) ----

private getResolvedAIWidth(windowWidth: number): number {
this.initializeFromMeta();
let w = this.aiPanelWidth;
if (w == null) {
w = Math.max(AIPanel_DefaultWidth, windowWidth * AIPanel_DefaultWidthRatio);
Expand All @@ -164,7 +170,6 @@ class WorkspaceLayoutModel {
}

private getResolvedVTabWidth(): number {
this.initializeFromMeta();
return clampVTabWidth(this.vtabWidth);
}

Expand Down Expand Up @@ -218,17 +223,14 @@ class WorkspaceLayoutModel {
if (this.vtabVisible && this.aiPanelVisible) {
// vtab stays constant, aipanel absorbs the change
const vtabW = this.getResolvedVTabWidth();
const newAIW = clampAIPanelWidth(newLeftGroupPx - vtabW, windowWidth);
this.aiPanelWidth = newAIW;
this.debouncedPersistAIWidth(newAIW);
this.aiPanelWidth = clampAIPanelWidth(newLeftGroupPx - vtabW, windowWidth);
this.debouncedPersistAIWidth();
} else if (this.vtabVisible) {
const clamped = clampVTabWidth(newLeftGroupPx);
this.vtabWidth = clamped;
this.debouncedPersistVTabWidth(clamped);
this.vtabWidth = clampVTabWidth(newLeftGroupPx);
this.debouncedPersistVTabWidth();
} else if (this.aiPanelVisible) {
const clamped = clampAIPanelWidth(newLeftGroupPx, windowWidth);
this.aiPanelWidth = clamped;
this.debouncedPersistAIWidth(clamped);
this.aiPanelWidth = clampAIPanelWidth(newLeftGroupPx, windowWidth);
this.debouncedPersistAIWidth();
}

this.commitLayouts(windowWidth);
Expand All @@ -249,11 +251,11 @@ class WorkspaceLayoutModel {

if (clampedVTab !== this.vtabWidth) {
this.vtabWidth = clampedVTab;
this.debouncedPersistVTabWidth(clampedVTab);
this.debouncedPersistVTabWidth();
}
if (newAIW !== this.aiPanelWidth) {
this.aiPanelWidth = newAIW;
this.debouncedPersistAIWidth(newAIW);
this.debouncedPersistAIWidth();
}

this.commitLayouts(windowWidth);
Expand All @@ -280,6 +282,7 @@ class WorkspaceLayoutModel {
panelContainerRef: HTMLDivElement,
aiPanelWrapperRef: HTMLDivElement,
vtabPanelRef?: ImperativePanelHandle,
vtabPanelWrapperRef?: HTMLDivElement,
showLeftTabBar?: boolean
): void {
this.aiPanelRef = aiPanelRef;
Expand All @@ -288,8 +291,8 @@ class WorkspaceLayoutModel {
this.innerPanelGroupRef = innerPanelGroupRef;
this.panelContainerRef = panelContainerRef;
this.aiPanelWrapperRef = aiPanelWrapperRef;
this.vtabPanelWrapperRef = vtabPanelWrapperRef ?? null;
this.vtabVisible = showLeftTabBar ?? false;
globalStore.set(this.vtabVisibleAtom, this.vtabVisible);
this.syncPanelCollapse();
this.commitLayouts(window.innerWidth);
}
Expand Down Expand Up @@ -342,7 +345,6 @@ class WorkspaceLayoutModel {
// ---- Public getters ----

getAIPanelVisible(): boolean {
this.initializeFromMeta();
return this.aiPanelVisible;
}

Expand All @@ -353,15 +355,13 @@ class WorkspaceLayoutModel {
// ---- Initial percentage helpers (used by workspace.tsx for defaultSize) ----

getLeftGroupInitialPercentage(windowWidth: number, showLeftTabBar: boolean): number {
this.initializeFromMeta();
const vtabW = showLeftTabBar && !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;
this.initializeFromMeta();
const vtabW = this.getResolvedVTabWidth();
const aiW = this.aiPanelVisible ? this.getResolvedAIWidth(windowWidth) : 0;
const total = vtabW + aiW;
Expand All @@ -370,7 +370,6 @@ class WorkspaceLayoutModel {
}

getInnerAIPanelInitialPercentage(windowWidth: number, showLeftTabBar: boolean): number {
this.initializeFromMeta();
const vtabW = showLeftTabBar && !isBuilderWindow() ? this.getResolvedVTabWidth() : 0;
const aiW = this.aiPanelVisible ? this.getResolvedAIWidth(windowWidth) : 0;
const total = vtabW + aiW;
Expand Down Expand Up @@ -424,7 +423,6 @@ class WorkspaceLayoutModel {
setShowLeftTabBar(showLeftTabBar: boolean): void {
if (this.vtabVisible === showLeftTabBar) return;
this.vtabVisible = showLeftTabBar;
globalStore.set(this.vtabVisibleAtom, showLeftTabBar);
this.enableTransitions(250);
this.syncPanelCollapse();
this.commitLayouts(window.innerWidth);
Expand Down
19 changes: 11 additions & 8 deletions frontend/app/workspace/workspace.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,6 @@ const WorkspaceElem = memo(() => {
const tabBarPosition = useAtomValue(getSettingsKeyAtom("app:tabbar")) ?? "top";
const showLeftTabBar = tabBarPosition === "left";
const aiPanelVisible = useAtomValue(workspaceLayoutModel.panelVisibleAtom);
const vtabVisible = useAtomValue(workspaceLayoutModel.vtabVisibleAtom);
const windowWidth = window.innerWidth;
const leftGroupInitialPct = workspaceLayoutModel.getLeftGroupInitialPercentage(windowWidth, showLeftTabBar);
const innerVTabInitialPct = workspaceLayoutModel.getInnerVTabInitialPercentage(windowWidth, showLeftTabBar);
Expand All @@ -57,6 +56,7 @@ const WorkspaceElem = memo(() => {
const vtabPanelRef = useRef<ImperativePanelHandle>(null);
const panelContainerRef = useRef<HTMLDivElement>(null);
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.
Expand All @@ -75,6 +75,7 @@ const WorkspaceElem = memo(() => {
panelContainerRef.current,
aiPanelWrapperRef.current,
vtabPanelRef.current ?? undefined,
vtabPanelWrapperRef.current ?? undefined,
showLeftTabBar
);
}
Expand All @@ -85,24 +86,24 @@ const WorkspaceElem = memo(() => {
getApi().setWaveAIOpen(isVisible);
}, []);

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

useEffect(() => {
window.addEventListener("resize", workspaceLayoutModel.handleWindowResize);
return () => window.removeEventListener("resize", workspaceLayoutModel.handleWindowResize);
}, []);

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

useEffect(() => {
const handleFocus = () => workspaceLayoutModel.syncVTabWidthFromMeta();
window.addEventListener("focus", handleFocus);
return () => window.removeEventListener("focus", handleFocus);
}, []);

const innerHandleVisible = vtabVisible && aiPanelVisible;
const innerHandleVisible = showLeftTabBar && 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;
const outerHandleVisible = showLeftTabBar || aiPanelVisible;
const outerHandleClass = `bg-transparent hover:bg-zinc-500/20 transition-colors ${outerHandleVisible ? "w-0.5" : "w-0 pointer-events-none"}`;

return (
Expand All @@ -129,7 +130,9 @@ const WorkspaceElem = memo(() => {
order={0}
className="overflow-hidden"
>
{showLeftTabBar && <VTabBar workspace={ws} />}
<div ref={vtabPanelWrapperRef} className="w-full h-full">
{showLeftTabBar && <VTabBar workspace={ws} />}
</div>
</Panel>
<PanelResizeHandle className={innerHandleClass} />
<Panel
Expand Down
Loading