From 2da0aaae618e9d21b2a3e5bd8489060fa35b8265 Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Thu, 29 Oct 2026 14:31:41 -0700 Subject: [PATCH] perf(sidebar): stop restyling the whole document on every sidebar-resize frame (#36071) * perf(sidebar): scope live sidebar width to the drawers that read it Writing --workspace-sidebar-live-width on :root every drag frame restyled the whole document. Only the workspace board and agent dashboard sheets read it, so write it to their SheetContent elements instead; a newly mounted drawer picks up the current live width so opening mid-drag stays aligned. * perf(sidebar): keep the live-width registry across HMR and stop it inheriting into drawer subtrees - Registrations survive module re-evaluation; Radix keeps open drawers' refs stable, so they never re-register. - @property inherits:false: only the drawer element itself reads the width, so its subtree no longer restyles per drag frame. - Refresh the plugin-panel theme comment, which still described root writes during drags. --- src/renderer/src/assets/main.css | 6 +++ .../dashboard/AgentDashboardDrawer.tsx | 2 + .../use-plugin-panel-theme-revision.ts | 2 +- .../sidebar/WorkspaceKanbanSheet.tsx | 2 + src/renderer/src/components/sidebar/index.tsx | 7 +--- .../sidebar/sidebar-live-width.test.ts | 28 +++++++++++++ .../components/sidebar/sidebar-live-width.ts | 41 +++++++++++++++++++ 7 files changed, 82 insertions(+), 6 deletions(-) create mode 100644 src/renderer/src/components/sidebar/sidebar-live-width.test.ts create mode 100644 src/renderer/src/components/sidebar/sidebar-live-width.ts diff --git a/src/renderer/src/assets/main.css b/src/renderer/src/assets/main.css index 082584ee5f2..d6780be096f 100644 --- a/src/renderer/src/assets/main.css +++ b/src/renderer/src/assets/main.css @@ -168,6 +168,12 @@ --radius-4xl: calc(var(--radius) * 2.6); } +/* Why: only the drawer that carries it reads the live sidebar width; don't restyle its subtree per drag frame. */ +@property --workspace-sidebar-live-width { + syntax: '*'; + inherits: false; +} + :root { --app-font-family: 'Geist', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; --font-sans: var(--app-font-family); diff --git a/src/renderer/src/components/dashboard/AgentDashboardDrawer.tsx b/src/renderer/src/components/dashboard/AgentDashboardDrawer.tsx index 665295c835c..0a5f78e8dc8 100644 --- a/src/renderer/src/components/dashboard/AgentDashboardDrawer.tsx +++ b/src/renderer/src/components/dashboard/AgentDashboardDrawer.tsx @@ -18,6 +18,7 @@ import { AgentDashboardSettingsMenu } from './AgentDashboardSettingsMenu' import { useLiveDashboardSnapshot } from './useLiveDashboardSnapshot' import { useAgentDashboardTelemetry } from '../dashboard-popout/use-agent-dashboard-telemetry' import { translate } from '@/i18n/i18n' +import { trackSidebarLiveWidth } from '@/components/sidebar/sidebar-live-width' // Nested interactive overlays own Escape before the companion sheet; exclude the sheet’s own role=dialog marker. const AGENT_BOARD_ESCAPE_BLOCKING_OVERLAY_SELECTOR = [ @@ -213,6 +214,7 @@ export function AgentDashboardDrawer({ (null) useEffect(() => { snapshotRef.current ??= readPanelThemeSnapshot() diff --git a/src/renderer/src/components/sidebar/WorkspaceKanbanSheet.tsx b/src/renderer/src/components/sidebar/WorkspaceKanbanSheet.tsx index 3f26904e216..a9d4b513f70 100644 --- a/src/renderer/src/components/sidebar/WorkspaceKanbanSheet.tsx +++ b/src/renderer/src/components/sidebar/WorkspaceKanbanSheet.tsx @@ -2,6 +2,7 @@ import React from 'react' import { Sheet, SheetContent } from '@/components/ui/sheet' import { isWorkspaceBoardKeepOpenTarget } from './use-workspace-kanban-outside-dismiss' import { STATUS_BAR_RESERVE_HEIGHT, WORKSPACE_TOP_CHROME_HEIGHT } from './workspace-chrome-metrics' +import { trackSidebarLiveWidth } from './sidebar-live-width' type WorkspaceKanbanSheetProps = { boardRef: React.RefObject @@ -68,6 +69,7 @@ export default function WorkspaceKanbanSheet({ { - document.documentElement.style.setProperty('--workspace-sidebar-live-width', `${width}px`) - }, []) - const repoCount = repos.length const previousRepoCountRef = React.useRef(repoCount) useEffect(() => { @@ -146,7 +143,7 @@ function Sidebar({ maxWidth: MAX_WIDTH, deltaSign: 1, setWidth: setSidebarWidth, - onDraftWidthChange: setLiveSidebarWidth + onDraftWidthChange: setSidebarLiveWidth }) useWorkspaceRevealBodyRedirect(sidebarOpen && sidebarBody === 'agents') diff --git a/src/renderer/src/components/sidebar/sidebar-live-width.test.ts b/src/renderer/src/components/sidebar/sidebar-live-width.test.ts new file mode 100644 index 00000000000..d2c968c799d --- /dev/null +++ b/src/renderer/src/components/sidebar/sidebar-live-width.test.ts @@ -0,0 +1,28 @@ +// @vitest-environment happy-dom + +import { afterEach, describe, expect, it } from 'vitest' +import { setSidebarLiveWidth, trackSidebarLiveWidth } from './sidebar-live-width' + +const read = (el: HTMLElement): string => + el.style.getPropertyValue('--workspace-sidebar-live-width') + +describe('sidebar live width', () => { + const cleanups: (() => void)[] = [] + afterEach(() => cleanups.splice(0).forEach((cleanup) => cleanup())) + + it('scopes drag width to mounted drawers instead of the document root', () => { + setSidebarLiveWidth(300) + const drawer = document.createElement('div') + cleanups.push(trackSidebarLiveWidth(drawer)!) + // A drawer opened mid-drag starts at the live edge, not the persisted width. + expect(read(drawer)).toBe('300px') + + setSidebarLiveWidth(312) + expect(read(drawer)).toBe('312px') + expect(read(document.documentElement)).toBe('') + + cleanups.pop()!() + setSidebarLiveWidth(330) + expect(read(drawer)).toBe('312px') + }) +}) diff --git a/src/renderer/src/components/sidebar/sidebar-live-width.ts b/src/renderer/src/components/sidebar/sidebar-live-width.ts new file mode 100644 index 00000000000..40672641fcf --- /dev/null +++ b/src/renderer/src/components/sidebar/sidebar-live-width.ts @@ -0,0 +1,41 @@ +const LIVE_WIDTH_PROPERTY = '--workspace-sidebar-live-width' + +type SidebarLiveWidthState = { width: string | null; targets: Set } +type SidebarLiveWidthHotData = { sidebarLiveWidth?: SidebarLiveWidthState } + +// Why: Radix keeps open drawers' refs stable across HMR, so registrations must survive module re-evaluation. +function sharedState(hotData?: SidebarLiveWidthHotData): SidebarLiveWidthState { + const state = hotData?.sidebarLiveWidth ?? { width: null, targets: new Set() } + if (hotData) { + hotData.sidebarLiveWidth = state + } + return state +} + +const state = sharedState(import.meta.hot?.data) + +// Why: a custom property on :root restyles the whole document every drag frame; only the drawers read it. +export function setSidebarLiveWidth(width: number): void { + const next = `${width}px` + if (next === state.width) { + return + } + state.width = next + for (const target of state.targets) { + target.style.setProperty(LIVE_WIDTH_PROPERTY, next) + } +} + +// Ref callback: applies the current width on mount so a drawer opened mid-drag starts at the sidebar edge. +export function trackSidebarLiveWidth(element: HTMLElement | null): (() => void) | undefined { + if (!element) { + return undefined + } + if (state.width !== null) { + element.style.setProperty(LIVE_WIDTH_PROPERTY, state.width) + } + state.targets.add(element) + return () => { + state.targets.delete(element) + } +}