Prevent WorkspacesSidebar re-render on workspace switch (Vibe Kanban) (#1973)

* The refactoring is complete. Here's a summary of what was changed:

## Summary

**File modified:** `vibe-kanban/frontend/src/components/ui-new/containers/WorkspacesLayout.tsx`

**Change:** Restructured `renderContent()` so that `WorkspacesSidebar` is rendered **outside** the `ExecutionProcessesProvider`.

### Before:
```tsx
<ExecutionProcessesProvider key={`${selectedWorkspace?.id}-${selectedSessionId}`}>
  <ReviewProvider>
    <Allotment>
      <Allotment.Pane>{renderSidebar()}</Allotment.Pane>  // Inside provider
      <Allotment.Pane>...</Allotment.Pane>
      <Allotment.Pane>...</Allotment.Pane>
      <Allotment.Pane>...</Allotment.Pane>
    </Allotment>
  </ReviewProvider>
</ExecutionProcessesProvider>
```

### After:
```tsx
<Allotment>
  <Allotment.Pane>{renderSidebar()}</Allotment.Pane>  // OUTSIDE provider - won't remount

  <ExecutionProcessesProvider key={`${selectedWorkspace?.id}-${selectedSessionId}`}>
    <ReviewProvider>
      <>
        <Allotment.Pane>...</Allotment.Pane>
        <Allotment.Pane>...</Allotment.Pane>
        <Allotment.Pane>...</Allotment.Pane>
      </>
    </ReviewProvider>
  </ExecutionProcessesProvider>
</Allotment>
```

**Result:** When switching workspaces, the `ExecutionProcessesProvider`'s key changes, which unmounts/remounts only panes 2-4. The sidebar pane stays mounted and won't re-render unnecessarily.

* The implementation is complete. Here's a summary of what was changed:

## Summary

**File modified:** `vibe-kanban/frontend/src/components/ui-new/containers/WorkspacesLayout.tsx`

### Changes:

1. **Created nested Allotments structure:**
   - **Outer Allotment** (2 panes): Sidebar + Content container
   - **Inner Allotment** (3 panes): Main + Changes/Logs + Git Panel

2. **Separated resize handlers:**
   - `handleOuterPaneResize`: Handles sidebar width only
   - `handleInnerPaneResize`: Handles main, changes/logs, and git panel widths

3. **Removed old `handlePaneResize`** which was designed for the flat 4-pane structure

### Result:
```
Outer Allotment
├── Sidebar Pane (OUTSIDE providers - won't remount)
└── Content Pane
    └── ExecutionProcessesProvider (keyed)
        └── ReviewProvider
            └── Inner Allotment
                ├── Main Pane
                ├── Changes/Logs Pane
                └── Git Panel Pane
```

Now when switching workspaces:
- The `ExecutionProcessesProvider` with its key will remount the inner Allotment (ensuring clean state)
- The sidebar stays mounted because it's outside the keyed provider
- Layout looks the same as before (sidebar on left, 3 panes on right)
This commit is contained in:
Louis Knight-Webb
2026-01-12 17:14:48 +00:00
committed by GitHub
parent 2c474d689c
commit 572e9706e5

View File

@@ -495,26 +495,6 @@ export function WorkspacesLayout() {
[setFileTreeHeight] [setFileTreeHeight]
); );
// Handle pane resize end
const handlePaneResize = useCallback(
(sizes: number[]) => {
// sizes[0] = sidebar, sizes[1] = main, sizes[2] = changes/logs panel, sizes[3] = git panel
if (sizes[0] !== undefined) setSidebarWidth(sizes[0]);
if (sizes[3] !== undefined) setGitPanelWidth(sizes[3]);
const total = sizes.reduce((sum, s) => sum + (s ?? 0), 0);
if (total > 0) {
// Store changes/logs panel as percentage of TOTAL container width
const centerPaneWidth = sizes[2];
if (centerPaneWidth !== undefined) {
const percent = Math.round((centerPaneWidth / total) * 100);
setChangesPanelWidth(`${percent}%`);
}
}
},
[setSidebarWidth, setGitPanelWidth, setChangesPanelWidth]
);
// Navigate to logs panel and select a specific process // Navigate to logs panel and select a specific process
const handleViewProcessInPanel = useCallback( const handleViewProcessInPanel = useCallback(
(processId: string) => { (processId: string) => {
@@ -698,23 +678,39 @@ 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) // Render layout content (create mode or workspace mode)
const renderContent = () => { const renderContent = () => {
const allotmentContent = ( // Inner Allotment with panes 2-4 (main, changes/logs, git panel)
<Allotment const innerAllotment = (
ref={allotmentRef} <Allotment onDragEnd={handleInnerPaneResize}>
className="flex-1 min-h-0"
onDragEnd={handlePaneResize}
>
<Allotment.Pane
minSize={300}
preferredSize={sidebarWidth}
maxSize={600}
visible={isSidebarVisible}
>
<div className="h-full overflow-hidden">{renderSidebar()}</div>
</Allotment.Pane>
<Allotment.Pane <Allotment.Pane
visible={isMainPanelVisible} visible={isMainPanelVisible}
priority={LayoutPriority.High} priority={LayoutPriority.High}
@@ -791,30 +787,50 @@ export function WorkspacesLayout() {
</Allotment> </Allotment>
); );
if (isCreateMode) { // Wrap inner Allotment with providers
return ( const wrappedInnerContent = isCreateMode ? (
<CreateModeProvider <CreateModeProvider
initialProjectId={lastWorkspaceTask?.project_id} initialProjectId={lastWorkspaceTask?.project_id}
initialRepos={lastWorkspaceRepos} initialRepos={lastWorkspaceRepos}
> >
<ReviewProvider attemptId={selectedWorkspace?.id}> <ReviewProvider attemptId={selectedWorkspace?.id}>
{allotmentContent} {innerAllotment}
</ReviewProvider> </ReviewProvider>
</CreateModeProvider> </CreateModeProvider>
); ) : (
}
return (
<ExecutionProcessesProvider <ExecutionProcessesProvider
key={`${selectedWorkspace?.id}-${selectedSessionId}`} key={`${selectedWorkspace?.id}-${selectedSessionId}`}
attemptId={selectedWorkspace?.id} attemptId={selectedWorkspace?.id}
sessionId={selectedSessionId} sessionId={selectedSessionId}
> >
<ReviewProvider attemptId={selectedWorkspace?.id}> <ReviewProvider attemptId={selectedWorkspace?.id}>
{allotmentContent} {innerAllotment}
</ReviewProvider> </ReviewProvider>
</ExecutionProcessesProvider> </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>
{/* Container for provider-wrapped inner content */}
<Allotment.Pane priority={LayoutPriority.High}>
{wrappedInnerContent}
</Allotment.Pane>
</Allotment>
);
}; };
return ( return (