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:
@@ -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",
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -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}`
|
||||
|
||||
Reference in New Issue
Block a user