From cd579ca79153f76286a80476157deaa564d68db3 Mon Sep 17 00:00:00 2001 From: Theo Browne Date: Wed, 14 Jan 2026 03:04:11 -0800 Subject: [PATCH] Replace allotment with react-resizable-panels and simplify layout (#2029) * merge * switch out panel lib * fmt * remove allotment dep * bump lock * The position of the react resizable panel is lost when the page is reloaded in (vibe-kanban 6ac32e23) `WorkspacesLayout.tsx` , but should be stored in `useUiPreferencesStore.ts` `vibe-kanban/frontend/src/components/ui-new/containers/WorkspacesLayout.tsx` `vibe-kanban/frontend/src/stores/useUiPreferencesStore.ts` * remove redundant type * add dep --------- Co-authored-by: Louis Knight-Webb --- frontend/package.json | 1 - .../ui-new/containers/WorkspacesLayout.tsx | 341 ++++++++---------- frontend/src/stores/useUiPreferencesStore.ts | 12 +- pnpm-lock.yaml | 51 --- 4 files changed, 158 insertions(+), 247 deletions(-) diff --git a/frontend/package.json b/frontend/package.json index 3b5a9441..b0df0724 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -50,7 +50,6 @@ "@tanstack/react-query": "^5.85.5", "@uiw/react-codemirror": "^4.25.1", "@virtuoso.dev/message-list": "^1.13.3", - "allotment": "^1.20.5", "class-variance-authority": "^0.7.0", "click-to-react-component": "^1.1.2", "clsx": "^2.0.0", diff --git a/frontend/src/components/ui-new/containers/WorkspacesLayout.tsx b/frontend/src/components/ui-new/containers/WorkspacesLayout.tsx index 95826acf..802f1c0f 100644 --- a/frontend/src/components/ui-new/containers/WorkspacesLayout.tsx +++ b/frontend/src/components/ui-new/containers/WorkspacesLayout.tsx @@ -1,7 +1,5 @@ import { useState, useCallback, useMemo, useEffect, useRef } from 'react'; -import { useNavigate } from 'react-router-dom'; -import { Allotment, LayoutPriority, type AllotmentHandle } from 'allotment'; -import 'allotment/dist/style.css'; +import { Group, Layout, Panel, Separator } from 'react-resizable-panels'; import { useWorkspaceContext } from '@/contexts/WorkspaceContext'; import { useActions } from '@/contexts/ActionsContext'; import { ExecutionProcessesProvider } from '@/contexts/ExecutionProcessesContext'; @@ -39,9 +37,9 @@ import { useTask } from '@/hooks/useTask'; import { useAttemptRepo } from '@/hooks/useAttemptRepo'; import { useBranchStatus } from '@/hooks/useBranchStatus'; import { - usePaneSize, - useExpandedAll, PERSIST_KEYS, + useExpandedAll, + usePaneSize, } from '@/stores/useUiPreferencesStore'; import { useLayoutStore, @@ -53,6 +51,7 @@ import { useCommandBarShortcut } from '@/hooks/useCommandBarShortcut'; import { Actions } from '@/components/ui-new/actions'; import type { RepoAction } from '@/components/ui-new/primitives/RepoCard'; import type { Workspace, RepoWithTargetBranch, Merge } from 'shared/types'; +import { useNavigate } from 'react-router-dom'; // Container component for GitPanel that uses hooks requiring GitOperationsProvider interface GitPanelContainerProps { @@ -300,9 +299,29 @@ export function WorkspacesLayout() { setMainPanelVisible, } = useLayoutStore(); - // Derived state: right main panel (Changes/Logs/Preview) is visible + const [rightMainPanelSize, setRightMainPanelSize] = usePaneSize( + PERSIST_KEYS.rightMainPanel, + 50 + ); const isRightMainPanelVisible = useIsRightMainPanelVisible(); + const defaultLayout = (): Layout => { + let layout = { 'left-main': 50, 'right-main': 50 }; + if (typeof rightMainPanelSize === 'number') { + layout = { + 'left-main': 100 - rightMainPanelSize, + 'right-main': rightMainPanelSize, + }; + } + return layout; + }; + + const onLayoutChange = (layout: Layout) => { + if (isRightMainPanelVisible) { + setRightMainPanelSize(layout['right-main']); + } + }; + // === Auto-show Workspaces Guide on first visit === const WORKSPACES_GUIDE_ID = 'workspaces-guide'; const { @@ -493,17 +512,6 @@ export function WorkspacesLayout() { ); }, [logMatchIndices.length]); - // Ref to Allotment for programmatic control - const allotmentRef = useRef(null); - - // Reset Allotment sizes when right main panel becomes visible - // This re-applies preferredSize percentages based on current window size - useEffect(() => { - if (isRightMainPanelVisible && allotmentRef.current) { - allotmentRef.current.reset(); - } - }, [isRightMainPanelVisible]); - // Reset changes and logs mode when entering create mode useEffect(() => { if (isCreateMode) { @@ -535,32 +543,6 @@ export function WorkspacesLayout() { // Expanded state for file tree selection const { setExpanded } = useExpandedAll(); - // Persisted pane sizes - const [sidebarWidth, setSidebarWidth] = usePaneSize( - PERSIST_KEYS.sidebarWidth, - 300 - ); - const [gitPanelWidth, setGitPanelWidth] = usePaneSize( - PERSIST_KEYS.gitPanelWidth, - 300 - ); - const [changesPanelWidth, setChangesPanelWidth] = usePaneSize( - PERSIST_KEYS.changesPanelWidth, - '40%' - ); - const [fileTreeHeight, setFileTreeHeight] = usePaneSize( - PERSIST_KEYS.fileTreeHeight, - '70%' - ); - - // Handle file tree resize (vertical split within git panel) - const handleFileTreeResize = useCallback( - (sizes: number[]) => { - if (sizes[0] !== undefined) setFileTreeHeight(sizes[0]); - }, - [setFileTreeHeight] - ); - // Navigate to logs panel and select a specific process const handleViewProcessInPanel = useCallback( (processId: string) => { @@ -635,8 +617,8 @@ export function WorkspacesLayout() { if (isChangesMode) { // In changes mode, split git panel vertically: file tree on top, git on bottom return ( - - +
+
- - +
+
- - +
+
); } @@ -670,8 +652,8 @@ export function WorkspacesLayout() { ? logsPanelContent.processId : null; return ( - - +
+
- - +
+
- - +
+
); } if (isPreviewMode) { // In preview mode, split git panel vertically: preview controls on top, git on bottom return ( - - +
+
- - +
+
- - +
+
); } @@ -744,123 +726,119 @@ export function WorkspacesLayout() { /> ); - // Handle inner pane resize (main, changes/logs, git panel) - const handleInnerPaneResize = useCallback( - (sizes: number[]) => { - // sizes[0] = main (no persistence needed, uses LayoutPriority.High) - // sizes[1] = changes/logs panel - // sizes[2] = git panel - if (sizes[2] !== undefined) setGitPanelWidth(sizes[2]); - - const total = sizes.reduce((sum, s) => sum + (s ?? 0), 0); - if (total > 0) { - const centerPaneWidth = sizes[1]; - if (centerPaneWidth !== undefined) { - const percent = Math.round((centerPaneWidth / total) * 100); - setChangesPanelWidth(`${percent}%`); - } - } - }, - [setGitPanelWidth, setChangesPanelWidth] - ); - - // Handle outer pane resize (sidebar only) - const handleOuterPaneResize = useCallback( - (sizes: number[]) => { - if (sizes[0] !== undefined) setSidebarWidth(sizes[0]); - }, - [setSidebarWidth] - ); - // Render layout content (create mode or workspace mode) const renderContent = () => { - // Inner Allotment with panes 2-4 (main, changes/logs, git panel) - const innerAllotment = ( - - + ) : ( + + -
- {isCreateMode ? ( - - ) : ( - - - - - - )} -
-
- - -
- {isChangesMode && ( - - )} - {isLogsMode && ( - - )} - {isPreviewMode && ( - - )} -
-
- - -
- {renderRightPanelContent()} -
-
-
+ + + ); - // Wrap inner Allotment with providers + // Right main panel content (Changes/Logs/Preview) + const rightMainPanelContent = ( + <> + {isChangesMode && ( + + )} + {isLogsMode && ( + + )} + {isPreviewMode && ( + + )} + + ); + + // Inner layout with main, changes/logs, git panel + const innerLayout = ( +
+ {/* Resizable area for main + right panels */} + + {/* Main panel (chat area) */} + {isMainPanelVisible && ( + + {mainPanelContent} + + )} + + {/* Resize handle between main and right panels */} + {isMainPanelVisible && isRightMainPanelVisible && ( + + )} + + {/* Right main panel (Changes/Logs/Preview) */} + {isRightMainPanelVisible && ( + + {rightMainPanelContent} + + )} + + + {/* Git panel (right sidebar) - fixed width, not resizable */} + {isGitPanelVisible && ( +
+ {renderRightPanelContent()} +
+ )} +
+ ); + + // Wrap inner layout with providers const wrappedInnerContent = isCreateMode ? ( - {innerAllotment} + {innerLayout} ) : ( @@ -870,32 +848,23 @@ export function WorkspacesLayout() { sessionId={selectedSessionId} > - {innerAllotment} + {innerLayout} ); return ( - - {/* Sidebar pane - OUTSIDE providers, won't remount on workspace switch */} - -
{renderSidebar()}
-
+
+ {/* Sidebar - OUTSIDE providers, won't remount on workspace switch */} + {isSidebarVisible && ( +
+ {renderSidebar()} +
+ )} {/* Container for provider-wrapped inner content */} - - {wrappedInnerContent} - - +
{wrappedInnerContent}
+
); }; diff --git a/frontend/src/stores/useUiPreferencesStore.ts b/frontend/src/stores/useUiPreferencesStore.ts index 7d81a072..7882474c 100644 --- a/frontend/src/stores/useUiPreferencesStore.ts +++ b/frontend/src/stores/useUiPreferencesStore.ts @@ -32,11 +32,8 @@ export const PERSIST_KEYS = { contextBarPosition: 'context-bar-position', // GitHub comments toggle showGitHubComments: 'show-github-comments', - // Pane sizes - sidebarWidth: 'workspaces-sidebar-width', - gitPanelWidth: 'workspaces-git-panel-width', - changesPanelWidth: 'workspaces-changes-panel-width', - fileTreeHeight: 'workspaces-file-tree-height', + // Panel sizes + rightMainPanel: 'right-main-panel', // Dynamic keys (use helper functions) repoCard: (repoId: string) => `repo-card-${repoId}` as const, } as const; @@ -53,10 +50,7 @@ export type PersistKey = | typeof PERSIST_KEYS.changesSection | typeof PERSIST_KEYS.devServerSection | typeof PERSIST_KEYS.showGitHubComments - | typeof PERSIST_KEYS.sidebarWidth - | typeof PERSIST_KEYS.gitPanelWidth - | typeof PERSIST_KEYS.changesPanelWidth - | typeof PERSIST_KEYS.fileTreeHeight + | typeof PERSIST_KEYS.rightMainPanel | `repo-card-${string}` | `diff:${string}` | `edit:${string}` diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4b95284f..26ad1d0c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -122,9 +122,6 @@ importers: '@virtuoso.dev/message-list': specifier: ^1.13.3 version: 1.13.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - allotment: - specifier: ^1.20.5 - version: 1.20.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1) class-variance-authority: specifier: ^0.7.0 version: 0.7.1 @@ -2121,12 +2118,6 @@ packages: ajv@8.17.1: resolution: {integrity: sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==} - allotment@1.20.5: - resolution: {integrity: sha512-7i4NT7ieXEyAd5lBrXmE7WHz/e7hRuo97+j+TwrPE85ha6kyFURoc76nom0dWSZ1pTKVEAMJy/+f3/Isfu/41A==} - peerDependencies: - react: ^17.0.0 || ^18.0.0 || ^19.0.0 - react-dom: ^17.0.0 || ^18.0.0 || ^19.0.0 - ansi-regex@5.0.1: resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} engines: {node: '>=8'} @@ -2222,9 +2213,6 @@ packages: class-variance-authority@0.7.1: resolution: {integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==} - classnames@2.5.1: - resolution: {integrity: sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==} - click-to-react-component@1.1.2: resolution: {integrity: sha512-8e9xU2MTubMwrtqu66/FtVHnv4TD94svOwMLRhza54OsmZqwMsLkscnl6ecJ3GgJ8Rk74jbLHCxpoSaZrdClGw==} peerDependencies: @@ -2512,9 +2500,6 @@ packages: resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} engines: {node: '>=0.10.0'} - eventemitter3@5.0.1: - resolution: {integrity: sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==} - fancy-ansi@0.1.3: resolution: {integrity: sha512-tRQVTo5jjdSIiydqgzIIEZpKddzSsfGLsSVt6vWdjVm7fbvDTiQkyoPu6Z3dIPlAM4OZk0jP5jmTCX4G8WGgBw==} @@ -2825,12 +2810,6 @@ packages: lodash-es@4.17.21: resolution: {integrity: sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==} - lodash.clamp@4.0.3: - resolution: {integrity: sha512-HvzRFWjtcguTW7yd8NJBshuNaCa8aqNFtnswdT7f/cMd/1YKy5Zzoq4W/Oxvnx9l7aeY258uSdDfM793+eLsVg==} - - lodash.debounce@4.0.8: - resolution: {integrity: sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==} - lodash.merge@4.6.2: resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} @@ -3513,12 +3492,6 @@ packages: peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - usehooks-ts@3.1.1: - resolution: {integrity: sha512-I4diPp9Cq6ieSUH2wu+fDAVQO43xwtulo+fKEidHUwZPnYImbtkTjzIJYcDcJqxgmX31GVqNFURodvcgHcW0pA==} - engines: {node: '>=16.15.0'} - peerDependencies: - react: ^16.8.0 || ^17 || ^18 || ^19 || ^19.0.0-rc - util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} @@ -5613,17 +5586,6 @@ snapshots: json-schema-traverse: 1.0.0 require-from-string: 2.0.2 - allotment@1.20.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1): - dependencies: - classnames: 2.5.1 - eventemitter3: 5.0.1 - fast-deep-equal: 3.1.3 - lodash.clamp: 4.0.3 - lodash.debounce: 4.0.8 - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) - usehooks-ts: 3.1.1(react@18.3.1) - ansi-regex@5.0.1: {} ansi-regex@6.1.0: {} @@ -5718,8 +5680,6 @@ snapshots: dependencies: clsx: 2.1.1 - classnames@2.5.1: {} - click-to-react-component@1.1.2(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: '@floating-ui/react-dom-interactions': 0.3.1(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -6064,8 +6024,6 @@ snapshots: esutils@2.0.3: {} - eventemitter3@5.0.1: {} - fancy-ansi@0.1.3: dependencies: escape-html: 1.0.3 @@ -6344,10 +6302,6 @@ snapshots: lodash-es@4.17.21: {} - lodash.clamp@4.0.3: {} - - lodash.debounce@4.0.8: {} - lodash.merge@4.6.2: {} lodash@4.17.21: {} @@ -6960,11 +6914,6 @@ snapshots: dependencies: react: 18.3.1 - usehooks-ts@3.1.1(react@18.3.1): - dependencies: - lodash.debounce: 4.0.8 - react: 18.3.1 - util-deprecate@1.0.2: {} uuid@13.0.0: {}