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 <louis@bloop.ai>
This commit is contained in:
Theo Browne
2026-01-14 03:04:11 -08:00
committed by GitHub
parent 5c95368ebf
commit cd579ca791
4 changed files with 158 additions and 247 deletions

View File

@@ -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",

View File

@@ -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<AllotmentHandle>(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 (
<Allotment vertical onDragEnd={handleFileTreeResize} proportionalLayout>
<Allotment.Pane minSize={200} preferredSize={fileTreeHeight}>
<div className="flex flex-col h-full">
<div className="flex-[7] min-h-0 overflow-hidden">
<FileTreeContainer
key={selectedWorkspace?.id}
workspaceId={selectedWorkspace?.id}
@@ -649,16 +631,16 @@ export function WorkspacesLayout() {
setExpanded(`diff:${path}`, true);
}}
/>
</Allotment.Pane>
<Allotment.Pane minSize={200}>
</div>
<div className="flex-[3] min-h-0 overflow-hidden">
<GitPanelContainer
selectedWorkspace={selectedWorkspace}
repos={repos}
repoInfos={repoInfos}
onBranchNameChange={handleBranchNameChange}
/>
</Allotment.Pane>
</Allotment>
</div>
</div>
);
}
@@ -670,8 +652,8 @@ export function WorkspacesLayout() {
? logsPanelContent.processId
: null;
return (
<Allotment vertical onDragEnd={handleFileTreeResize} proportionalLayout>
<Allotment.Pane minSize={200} preferredSize={fileTreeHeight}>
<div className="flex flex-col h-full">
<div className="flex-[7] min-h-0 overflow-hidden">
<ProcessListContainer
selectedProcessId={selectedProcessId}
onSelectProcess={handleViewProcessInPanel}
@@ -683,38 +665,38 @@ export function WorkspacesLayout() {
onPrevMatch={handleLogPrevMatch}
onNextMatch={handleLogNextMatch}
/>
</Allotment.Pane>
<Allotment.Pane minSize={200}>
</div>
<div className="flex-[3] min-h-0 overflow-hidden">
<GitPanelContainer
selectedWorkspace={selectedWorkspace}
repos={repos}
repoInfos={repoInfos}
onBranchNameChange={handleBranchNameChange}
/>
</Allotment.Pane>
</Allotment>
</div>
</div>
);
}
if (isPreviewMode) {
// In preview mode, split git panel vertically: preview controls on top, git on bottom
return (
<Allotment vertical onDragEnd={handleFileTreeResize} proportionalLayout>
<Allotment.Pane minSize={200} preferredSize={fileTreeHeight}>
<div className="flex flex-col h-full">
<div className="flex-[7] min-h-0 overflow-hidden">
<PreviewControlsContainer
attemptId={selectedWorkspace?.id}
onViewProcessInPanel={handleViewProcessInPanel}
/>
</Allotment.Pane>
<Allotment.Pane minSize={200}>
</div>
<div className="flex-[3] min-h-0 overflow-hidden">
<GitPanelContainer
selectedWorkspace={selectedWorkspace}
repos={repos}
repoInfos={repoInfos}
onBranchNameChange={handleBranchNameChange}
/>
</Allotment.Pane>
</Allotment>
</div>
</div>
);
}
@@ -744,46 +726,10 @@ 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 = (
<Allotment onDragEnd={handleInnerPaneResize}>
<Allotment.Pane
visible={isMainPanelVisible}
priority={LayoutPriority.High}
minSize={300}
>
<div className="h-full overflow-hidden">
{isCreateMode ? (
// Main panel content
const mainPanelContent = isCreateMode ? (
<CreateChatBoxContainer />
) : (
<FileNavigationProvider
@@ -807,16 +753,11 @@ export function WorkspacesLayout() {
/>
</LogNavigationProvider>
</FileNavigationProvider>
)}
</div>
</Allotment.Pane>
);
<Allotment.Pane
minSize={300}
preferredSize={changesPanelWidth}
visible={isRightMainPanelVisible}
>
<div className="h-full overflow-hidden">
// Right main panel content (Changes/Logs/Preview)
const rightMainPanelContent = (
<>
{isChangesMode && (
<ChangesPanelContainer
diffs={realDiffs}
@@ -837,30 +778,67 @@ export function WorkspacesLayout() {
{isPreviewMode && (
<PreviewBrowserContainer attemptId={selectedWorkspace?.id} />
)}
</div>
</Allotment.Pane>
<Allotment.Pane
minSize={300}
preferredSize={gitPanelWidth}
maxSize={600}
visible={isGitPanelVisible}
>
<div className="h-full overflow-hidden">
{renderRightPanelContent()}
</div>
</Allotment.Pane>
</Allotment>
</>
);
// Wrap inner Allotment with providers
// Inner layout with main, changes/logs, git panel
const innerLayout = (
<div className="flex h-full">
{/* Resizable area for main + right panels */}
<Group
orientation="horizontal"
className="flex-1 min-w-0 h-full"
defaultLayout={defaultLayout()}
onLayoutChange={onLayoutChange}
>
{/* Main panel (chat area) */}
{isMainPanelVisible && (
<Panel
id="left-main"
minSize={20}
className="min-w-0 h-full overflow-hidden"
>
{mainPanelContent}
</Panel>
)}
{/* Resize handle between main and right panels */}
{isMainPanelVisible && isRightMainPanelVisible && (
<Separator
id="main-separator"
className="w-1 bg-transparent hover:bg-brand/50 transition-colors cursor-col-resize"
/>
)}
{/* Right main panel (Changes/Logs/Preview) */}
{isRightMainPanelVisible && (
<Panel
id="right-main"
minSize={20}
className="min-w-0 h-full overflow-hidden"
>
{rightMainPanelContent}
</Panel>
)}
</Group>
{/* Git panel (right sidebar) - fixed width, not resizable */}
{isGitPanelVisible && (
<div className="w-[300px] shrink-0 h-full overflow-hidden">
{renderRightPanelContent()}
</div>
)}
</div>
);
// Wrap inner layout with providers
const wrappedInnerContent = isCreateMode ? (
<CreateModeProvider
initialProjectId={lastWorkspaceTask?.project_id}
initialRepos={lastWorkspaceRepos}
>
<ReviewProvider attemptId={selectedWorkspace?.id}>
{innerAllotment}
{innerLayout}
</ReviewProvider>
</CreateModeProvider>
) : (
@@ -870,32 +848,23 @@ export function WorkspacesLayout() {
sessionId={selectedSessionId}
>
<ReviewProvider attemptId={selectedWorkspace?.id}>
{innerAllotment}
{innerLayout}
</ReviewProvider>
</ExecutionProcessesProvider>
);
return (
<Allotment
ref={allotmentRef}
className="flex-1 min-h-0"
onDragEnd={handleOuterPaneResize}
>
{/* Sidebar pane - OUTSIDE providers, won't remount on workspace switch */}
<Allotment.Pane
minSize={300}
preferredSize={sidebarWidth}
maxSize={600}
visible={isSidebarVisible}
>
<div className="h-full overflow-hidden">{renderSidebar()}</div>
</Allotment.Pane>
<div className="flex flex-1 min-h-0">
{/* Sidebar - OUTSIDE providers, won't remount on workspace switch */}
{isSidebarVisible && (
<div className="w-[300px] shrink-0 h-full overflow-hidden">
{renderSidebar()}
</div>
)}
{/* Container for provider-wrapped inner content */}
<Allotment.Pane priority={LayoutPriority.High}>
{wrappedInnerContent}
</Allotment.Pane>
</Allotment>
<div className="flex-1 min-w-0 h-full">{wrappedInnerContent}</div>
</div>
);
};

View File

@@ -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}`

51
pnpm-lock.yaml generated
View File

@@ -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: {}