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,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 = (
<Allotment onDragEnd={handleInnerPaneResize}>
<Allotment.Pane
visible={isMainPanelVisible}
priority={LayoutPriority.High}
minSize={300}
// Main panel content
const mainPanelContent = isCreateMode ? (
<CreateChatBoxContainer />
) : (
<FileNavigationProvider
viewFileInChanges={handleViewFileInChanges}
diffPaths={diffPaths}
>
<LogNavigationProvider
viewProcessInPanel={handleViewProcessInPanel}
viewToolContentInPanel={handleViewToolContentInPanel}
>
<div className="h-full overflow-hidden">
{isCreateMode ? (
<CreateChatBoxContainer />
) : (
<FileNavigationProvider
viewFileInChanges={handleViewFileInChanges}
diffPaths={diffPaths}
>
<LogNavigationProvider
viewProcessInPanel={handleViewProcessInPanel}
viewToolContentInPanel={handleViewToolContentInPanel}
>
<WorkspacesMainContainer
selectedWorkspace={selectedWorkspace ?? null}
selectedSession={selectedSession}
sessions={sessions}
onSelectSession={selectSession}
isLoading={isLoading}
isNewSessionMode={isNewSessionMode}
onStartNewSession={startNewSession}
onViewCode={handleToggleChangesMode}
diffStats={diffStats}
/>
</LogNavigationProvider>
</FileNavigationProvider>
)}
</div>
</Allotment.Pane>
<Allotment.Pane
minSize={300}
preferredSize={changesPanelWidth}
visible={isRightMainPanelVisible}
>
<div className="h-full overflow-hidden">
{isChangesMode && (
<ChangesPanelContainer
diffs={realDiffs}
selectedFilePath={selectedFilePath}
onFileInViewChange={setFileInView}
projectId={selectedWorkspaceTask?.project_id}
attemptId={selectedWorkspace?.id}
/>
)}
{isLogsMode && (
<LogsContentContainer
content={logsPanelContent}
searchQuery={logSearchQuery}
currentMatchIndex={logCurrentMatchIdx}
onMatchIndicesChange={setLogMatchIndices}
/>
)}
{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>
<WorkspacesMainContainer
selectedWorkspace={selectedWorkspace ?? null}
selectedSession={selectedSession}
sessions={sessions}
onSelectSession={selectSession}
isLoading={isLoading}
isNewSessionMode={isNewSessionMode}
onStartNewSession={startNewSession}
onViewCode={handleToggleChangesMode}
diffStats={diffStats}
/>
</LogNavigationProvider>
</FileNavigationProvider>
);
// Wrap inner Allotment with providers
// Right main panel content (Changes/Logs/Preview)
const rightMainPanelContent = (
<>
{isChangesMode && (
<ChangesPanelContainer
diffs={realDiffs}
selectedFilePath={selectedFilePath}
onFileInViewChange={setFileInView}
projectId={selectedWorkspaceTask?.project_id}
attemptId={selectedWorkspace?.id}
/>
)}
{isLogsMode && (
<LogsContentContainer
content={logsPanelContent}
searchQuery={logSearchQuery}
currentMatchIndex={logCurrentMatchIdx}
onMatchIndicesChange={setLogMatchIndices}
/>
)}
{isPreviewMode && (
<PreviewBrowserContainer attemptId={selectedWorkspace?.id} />
)}
</>
);
// 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}`