diff --git a/frontend/app/workspace/widgets.tsx b/frontend/app/workspace/widgets.tsx index 39d3237766..482cfcefce 100644 --- a/frontend/app/workspace/widgets.tsx +++ b/frontend/app/workspace/widgets.tsx @@ -401,7 +401,7 @@ const Widgets = memo(() => { <>
{mode === "supercompact" ? ( @@ -504,7 +504,7 @@ const Widgets = memo(() => {
{widgets?.map((data, idx) => ( diff --git a/frontend/app/workspace/workspace-layout-model.ts b/frontend/app/workspace/workspace-layout-model.ts index 725c9a17b5..9403bcdb36 100644 --- a/frontend/app/workspace/workspace-layout-model.ts +++ b/frontend/app/workspace/workspace-layout-model.ts @@ -20,6 +20,10 @@ const AIPANEL_DEFAULTWIDTHRATIO = 0.33; const AIPANEL_MINWIDTH = 300; const AIPANEL_MAXWIDTHRATIO = 0.66; +const WIDGETS_DEFAULTWIDTH = 48; +const WIDGETS_MINWIDTH = 48; +const WIDGETS_MAXWIDTH = 300; + class WorkspaceLayoutModel { private static instance: WorkspaceLayoutModel | null = null; @@ -27,10 +31,14 @@ class WorkspaceLayoutModel { panelGroupRef: ImperativePanelGroupHandle | null; panelContainerRef: HTMLDivElement | null; aiPanelWrapperRef: HTMLDivElement | null; + innerPanelGroupRef: ImperativePanelGroupHandle | null; inResize: boolean; // prevents recursive setLayout calls (setLayout triggers onLayout which calls setLayout) + inInnerResize: boolean; private aiPanelVisible: boolean; private aiPanelWidth: number | null; + private widgetsWidth: number | null; private debouncedPersistWidth: (width: number) => void; + private debouncedPersistWidgetsWidth: (width: number) => void; private initialized: boolean = false; private transitionTimeoutRef: NodeJS.Timeout | null = null; private focusTimeoutRef: NodeJS.Timeout | null = null; @@ -41,13 +49,17 @@ class WorkspaceLayoutModel { this.panelGroupRef = null; this.panelContainerRef = null; this.aiPanelWrapperRef = null; + this.innerPanelGroupRef = null; this.inResize = false; + this.inInnerResize = false; this.aiPanelVisible = false; this.aiPanelWidth = null; + this.widgetsWidth = null; this.panelVisibleAtom = jotai.atom(this.aiPanelVisible); this.handleWindowResize = this.handleWindowResize.bind(this); this.handlePanelLayout = this.handlePanelLayout.bind(this); + this.handleInnerPanelLayout = this.handleInnerPanelLayout.bind(this); this.debouncedPersistWidth = debounce((width: number) => { try { @@ -59,6 +71,17 @@ class WorkspaceLayoutModel { console.warn("Failed to persist panel width:", e); } }, 300); + + this.debouncedPersistWidgetsWidth = debounce((width: number) => { + try { + RpcApi.SetMetaCommand(TabRpcClient, { + oref: WOS.makeORef("tab", this.getTabId()), + meta: { "widgets:width": width }, + }); + } catch (e) { + console.warn("Failed to persist widgets width:", e); + } + }, 300); } static getInstance(): WorkspaceLayoutModel { @@ -75,6 +98,7 @@ class WorkspaceLayoutModel { try { const savedVisible = globalStore.get(this.getPanelOpenAtom()); const savedWidth = globalStore.get(this.getPanelWidthAtom()); + const savedWidgetsWidth = globalStore.get(this.getWidgetsWidthAtom()); if (savedVisible != null) { this.aiPanelVisible = savedVisible; @@ -83,6 +107,9 @@ class WorkspaceLayoutModel { if (savedWidth != null) { this.aiPanelWidth = savedWidth; } + if (savedWidgetsWidth != null) { + this.widgetsWidth = savedWidgetsWidth; + } } catch (e) { console.warn("Failed to initialize from tab meta:", e); } @@ -102,6 +129,11 @@ class WorkspaceLayoutModel { return getOrefMetaKeyAtom(tabORef, "waveai:panelwidth"); } + private getWidgetsWidthAtom(): jotai.Atom { + const tabORef = WOS.makeORef("tab", this.getTabId()); + return getOrefMetaKeyAtom(tabORef, "widgets:width"); + } + registerRefs( aiPanelRef: ImperativePanelHandle, panelGroupRef: ImperativePanelGroupHandle, @@ -116,6 +148,11 @@ class WorkspaceLayoutModel { this.updateWrapperWidth(); } + registerInnerPanelGroupRef(ref: ImperativePanelGroupHandle): void { + this.innerPanelGroupRef = ref; + this.syncInnerPanelLayout(); + } + updateWrapperWidth(): void { if (!this.aiPanelWrapperRef) { return; @@ -160,6 +197,7 @@ class WorkspaceLayoutModel { this.panelGroupRef.setLayout(layout); this.inResize = false; this.updateWrapperWidth(); + this.syncInnerPanelLayout(); } handlePanelLayout(sizes: number[]): void { @@ -297,6 +335,88 @@ class WorkspaceLayoutModel { const clampedWidth = this.getClampedAIPanelWidth(width, windowWidth); this.setAIPanelWidth(clampedWidth); } + + getWidgetsWidth(): number { + this.initializeFromTabMeta(); + if (this.widgetsWidth == null) { + this.widgetsWidth = WIDGETS_DEFAULTWIDTH; + } + return this.widgetsWidth; + } + + setWidgetsWidth(width: number): void { + this.widgetsWidth = width; + this.debouncedPersistWidgetsWidth(width); + } + + getClampedWidgetsWidth(width: number): number { + return Math.max(WIDGETS_MINWIDTH, Math.min(width, WIDGETS_MAXWIDTH)); + } + + getWidgetsPercentage(containerWidth: number): number { + if (containerWidth <= 0) { + return 5; + } + const widgetsWidth = this.getWidgetsWidth(); + const clamped = this.getClampedWidgetsWidth(widgetsWidth); + return (clamped / containerWidth) * 100; + } + + getWidgetsMinPercentage(containerWidth: number): number { + if (containerWidth <= 0) { + return 3; + } + return (WIDGETS_MINWIDTH / containerWidth) * 100; + } + + getWidgetsMaxPercentage(containerWidth: number): number { + if (containerWidth <= 0) { + return 30; + } + return (WIDGETS_MAXWIDTH / containerWidth) * 100; + } + + getInnerContainerWidth(): number { + // The inner panel group's container width is the main content panel width + // which is the window width minus the AI panel width (if visible) + const windowWidth = window.innerWidth; + if (this.getAIPanelVisible()) { + const aiWidth = this.getClampedAIPanelWidth(this.getAIPanelWidth(), windowWidth); + return windowWidth - aiWidth; + } + return windowWidth; + } + + syncInnerPanelLayout(): void { + if (!this.innerPanelGroupRef) { + return; + } + const containerWidth = this.getInnerContainerWidth(); + const widgetsPercentage = this.getWidgetsPercentage(containerWidth); + const mainPercentage = 100 - widgetsPercentage; + this.inInnerResize = true; + this.innerPanelGroupRef.setLayout([mainPercentage, widgetsPercentage]); + this.inInnerResize = false; + } + + handleInnerPanelLayout(sizes: number[]): void { + if (this.inInnerResize) { + return; + } + if (!this.innerPanelGroupRef) { + return; + } + const containerWidth = this.getInnerContainerWidth(); + const widgetsPixelWidth = (sizes[1] / 100) * containerWidth; + const clampedWidth = this.getClampedWidgetsWidth(widgetsPixelWidth); + this.setWidgetsWidth(clampedWidth); + + const newWidgetsPercentage = (clampedWidth / containerWidth) * 100; + const mainPercentage = 100 - newWidgetsPercentage; + this.inInnerResize = true; + this.innerPanelGroupRef.setLayout([mainPercentage, newWidgetsPercentage]); + this.inInnerResize = false; + } } export { WorkspaceLayoutModel }; diff --git a/frontend/app/workspace/workspace.tsx b/frontend/app/workspace/workspace.tsx index fb1d78668f..35f2155a90 100644 --- a/frontend/app/workspace/workspace.tsx +++ b/frontend/app/workspace/workspace.tsx @@ -20,6 +20,45 @@ import { PanelResizeHandle, } from "react-resizable-panels"; +const InnerContent = memo(({ tabId }: { tabId: string }) => { + const workspaceLayoutModel = WorkspaceLayoutModel.getInstance(); + const innerContainerWidth = workspaceLayoutModel.getInnerContainerWidth(); + const initialWidgetsPercentage = workspaceLayoutModel.getWidgetsPercentage(innerContainerWidth); + const innerPanelGroupRef = useRef(null); + + useEffect(() => { + if (innerPanelGroupRef.current) { + workspaceLayoutModel.registerInnerPanelGroupRef(innerPanelGroupRef.current); + } + }, []); + + const widgetsMinSize = workspaceLayoutModel.getWidgetsMinPercentage(innerContainerWidth); + const widgetsMaxSize = workspaceLayoutModel.getWidgetsMaxPercentage(innerContainerWidth); + + return ( + + + + + + + + + + ); +}); + +InnerContent.displayName = "InnerContent"; + const WorkspaceElem = memo(() => { const workspaceLayoutModel = WorkspaceLayoutModel.getInstance(); const tabId = useAtomValue(atoms.staticTabId); @@ -77,10 +116,7 @@ const WorkspaceElem = memo(() => { {tabId === "" ? ( No Active Tab ) : ( -
- - -
+ )} diff --git a/frontend/types/gotypes.d.ts b/frontend/types/gotypes.d.ts index dada3a248b..3fe30ec0e3 100644 --- a/frontend/types/gotypes.d.ts +++ b/frontend/types/gotypes.d.ts @@ -1081,6 +1081,7 @@ declare global { "waveai:model"?: string; "waveai:chatid"?: string; "waveai:widgetcontext"?: boolean; + "widgets:width"?: number; "term:*"?: boolean; "term:fontsize"?: number; "term:fontfamily"?: string; diff --git a/pkg/waveobj/metaconsts.go b/pkg/waveobj/metaconsts.go index c1383ee32c..7cc95b59d7 100644 --- a/pkg/waveobj/metaconsts.go +++ b/pkg/waveobj/metaconsts.go @@ -102,6 +102,8 @@ const ( MetaKey_WaveAiChatId = "waveai:chatid" MetaKey_WaveAiWidgetContext = "waveai:widgetcontext" + MetaKey_WidgetsWidth = "widgets:width" + MetaKey_TermClear = "term:*" MetaKey_TermFontSize = "term:fontsize" MetaKey_TermFontFamily = "term:fontfamily" diff --git a/pkg/waveobj/wtypemeta.go b/pkg/waveobj/wtypemeta.go index 73fcf52fd7..30c673dc8f 100644 --- a/pkg/waveobj/wtypemeta.go +++ b/pkg/waveobj/wtypemeta.go @@ -106,6 +106,8 @@ type MetaTSType struct { WaveAiChatId string `json:"waveai:chatid,omitempty"` WaveAiWidgetContext *bool `json:"waveai:widgetcontext,omitempty"` // default is true + WidgetsWidth int `json:"widgets:width,omitempty"` + TermClear bool `json:"term:*,omitempty"` TermFontSize int `json:"term:fontsize,omitempty"` TermFontFamily string `json:"term:fontfamily,omitempty"`