fix: make splitter state key position-aware to prevent shared panel widths (#9525)

## Summary

Fix right-side sidebar panels and left-side panels sharing the same
PrimeVue Splitter state key, causing them to incorrectly apply each
other's saved widths.

## Changes

- **What**: Make `sidebarStateKey` position-aware by including
`sidebarLocation` and offside panel visibility in the localStorage key

## Problem

When sidebar location is set to **right**, all panels (both the
right-side sidebar like Job History and left-side panels like Workflow
overview) share a single PrimeVue Splitter `state-key`
(`unified-sidebar`). PrimeVue persists panel widths to localStorage
using this key, so any resize on one side gets applied to the other.

### AS-IS (before fix)

The `sidebarStateKey` is computed without any awareness of panel
position:

```typescript
// Always returns 'unified-sidebar' (when unified width enabled)
// or the active tab id — regardless of sidebar location or offside panel state
const sidebarStateKey = computed(() => {
  return unifiedWidth.value
    ? 'unified-sidebar'
    : (activeSidebarTabId.value ?? 'default-sidebar')
})
```

This produces a **single localStorage key** for all layout
configurations. The result:

1. Set sidebar to **right**, open **Job History** → resize it smaller →
saved to `unified-sidebar`
2. Open **Workflow overview** (appears on the left as an offside panel)
→ loads the same `unified-sidebar` key → gets the Job History width
applied to a completely different panel position
3. Both panels open simultaneously share the same persisted width, even
though they are on opposite sides of the screen

This is exactly the behavior shown in the [issue
screenshots](https://github.com/Comfy-Org/ComfyUI_frontend/issues/9440):
pulling the Workflow overview smaller also changes Job History to that
same size, and vice versa.

### TO-BE (after fix)

The `sidebarStateKey` now includes `sidebarLocation` (`left`/`right`)
and whether the offside panel is visible:

```typescript
const sidebarTabKey = computed(() => {
  return unifiedWidth.value
    ? 'unified-sidebar'
    : (activeSidebarTabId.value ?? 'default-sidebar')
})

const sidebarStateKey = computed(() => {
  const base = sidebarTabKey.value
  const suffix = showOffsideSplitter.value ? '-with-offside' : ''
  return `${base}-${sidebarLocation.value}${suffix}`
})
```

This produces **distinct localStorage keys** per layout configuration:
| Layout | Key |
|--------|-----|
| Sidebar left, no offside | `unified-sidebar-left` |
| Sidebar left, right panel open | `unified-sidebar-left-with-offside` |
| Sidebar right, no offside | `unified-sidebar-right` |
| Sidebar right, left panel open | `unified-sidebar-right-with-offside`
|

Each configuration now persists and restores its own panel sizes
independently, so resizing Job History on the right no longer affects
Workflow overview on the left.

## Review Focus

- The offside suffix (`-with-offside`) is necessary because the Splitter
transitions from a 2-panel layout (sidebar + center) to a 3-panel layout
(sidebar + center + offside) — these are fundamentally different panel
configurations and should not share persisted sizes.

Fixes #9440

## Screenshots (if applicable)

See issue for reproduction screenshots:
https://github.com/Comfy-Org/ComfyUI_frontend/issues/9440

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Dante
2026-03-27 08:31:56 +09:00
committed by GitHub
parent 3e197b5c57
commit ff263fced0
2 changed files with 209 additions and 9 deletions

View File

@@ -18,15 +18,20 @@
<Splitter
:key="splitterRefreshKey"
class="pointer-events-none flex-1 overflow-hidden border-none bg-transparent"
:state-key="isSelectMode ? 'builder-splitter' : sidebarStateKey"
:state-key="
isSelectMode
? sidebarLocation === 'left'
? 'builder-splitter'
: 'builder-splitter-right'
: sidebarStateKey
"
state-storage="local"
@resizestart="onResizestart"
@resizeend="normalizeSavedSizes"
>
<!-- First panel: sidebar when left, properties when right -->
<SplitterPanel
v-if="
!focusMode && (sidebarLocation === 'left' || showOffsideSplitter)
"
v-if="firstPanelVisible"
:class="
sidebarLocation === 'left'
? cn(
@@ -56,7 +61,7 @@
</SplitterPanel>
<!-- Main panel (always present) -->
<SplitterPanel :size="CENTER_PANEL_SIZE" class="flex flex-col">
<SplitterPanel :size="centerPanelDefaultSize" class="flex flex-col">
<slot name="topmenu" :sidebar-panel-visible />
<Splitter
@@ -86,9 +91,7 @@
<!-- Last panel: properties when left, sidebar when right -->
<SplitterPanel
v-if="
!focusMode && (sidebarLocation === 'right' || showOffsideSplitter)
"
v-if="lastPanelVisible"
:class="
sidebarLocation === 'right'
? cn(
@@ -167,13 +170,52 @@ const sidebarPanelVisible = computed(
() => activeSidebarTab.value !== null && !isBuilderMode.value
)
const sidebarStateKey = computed(() => {
const firstPanelVisible = computed(
() =>
!focusMode.value &&
(sidebarLocation.value === 'left' || showOffsideSplitter.value)
)
const lastPanelVisible = computed(
() =>
!focusMode.value &&
(sidebarLocation.value === 'right' || showOffsideSplitter.value)
)
/**
* When both side panels are visible, reduce center panel default size so that
* initial sizes sum to 100%. This prevents PrimeVue Splitter from saving an
* inconsistent panelSizes array (where untouched panel values are prop-based
* while resized panels are pixel-derived), which caused one panel's width to
* drift when the other was resized.
*
* Uses runtime visibility (not just mount state) because the sidebar panel can
* be mounted but hidden via display:none when no tab is active.
*/
const bothSidePanelsVisible = computed(
() =>
!focusMode.value && sidebarPanelVisible.value && showOffsideSplitter.value
)
const centerPanelDefaultSize = computed(() =>
bothSidePanelsVisible.value ? 100 - 2 * SIDE_PANEL_SIZE : CENTER_PANEL_SIZE
)
const sidebarTabKey = computed(() => {
return unifiedWidth.value
? 'unified-sidebar'
: // When no tab is active, use a default key to maintain state
(activeSidebarTabId.value ?? 'default-sidebar')
})
const sidebarStateKey = computed(() => {
const base = sidebarTabKey.value
if (sidebarLocation.value === 'left' && !showOffsideSplitter.value) {
return base
}
const suffix = showOffsideSplitter.value ? '-with-offside' : ''
return `${base}-${sidebarLocation.value}${suffix}`
})
/**
* Avoid triggering default behaviors during drag-and-drop, such as text selection.
*/
@@ -181,6 +223,42 @@ function onResizestart({ originalEvent: event }: SplitterResizeStartEvent) {
event.preventDefault()
}
/**
* Normalize persisted panel sizes to sum to 100% after each resize.
*
* PrimeVue Splitter only updates the two panels adjacent to the dragged gutter,
* leaving the third panel at its initial prop value. Because that prop value
* doesn't account for CSS min-width or gutter offsets, the saved array can sum
* to more than 100%, causing the untouched panel's width to drift on restore.
*/
function normalizeSavedSizes() {
const stateKey = isSelectMode.value
? sidebarLocation.value === 'left'
? 'builder-splitter'
: 'builder-splitter-right'
: sidebarStateKey.value
const raw = localStorage.getItem(stateKey)
if (!raw) return
try {
const parsed: unknown = JSON.parse(raw)
if (
!Array.isArray(parsed) ||
parsed.length === 0 ||
parsed.some((s) => typeof s !== 'number' || !Number.isFinite(s))
) {
return
}
const sum = parsed.reduce((a, b) => a + b, 0)
if (sum <= 0 || Math.abs(sum - 100) <= 0.5) return
localStorage.setItem(
stateKey,
JSON.stringify(parsed.map((s) => (s / sum) * 100))
)
} catch {
return
}
}
/*
* Force refresh the splitter when right panel visibility or sidebar location changes
* to recalculate the width and panel order