Skip to content

fix(web): retain right panel content on close

MacroscopeApp / Macroscope - UI Consistency failed Aug 16, 2026 in 13m 14s

UI Consistency: 2 issues found

Two consistency issues found in the changed UI code.

  • apps/web/src/components/ui/sidebar.tsx (line 284) — the sidebar collapse is retimed to 240ms + --motion-ease-drawer, but transitions driven by the same interaction were left at duration-200 ease-linear: SidebarGroupLabel (ui/sidebar.tsx:731) and the topbar transition-[padding-left] insets in components/usage/UsagePage.tsx:110,123 and components/settings/ProjectSettingsPanel.tsx:180,190. The same inset was updated in ChatView, NoActiveThreadState, routes/settings.tsx and routes/_chat.index.tsx, so those two pages now trail the sidebar edge. Suggested folding the transition into the shared COLLAPSED_SIDEBAR_TITLEBAR_INSET_CLASS constant.
  • apps/web/src/components/preview/PreviewPanelShell.tsx (line 112) — the new inner content column is pinned to widthpx while its clip parent is w-full of a host whose border-l sits inside the border box, so the right 1px of every inline panel surface is cropped and the content no longer reflows when max-w-full clamps the host.

Both findings were posted as inline review comments.

Details

Note

Your check run agent prompt is: .macroscope/check-run-agents/ui-consistency.md
More information about how Check Run Agents work can be found in our Help Center.

Prior-run findings verified as addressed at head 9202269: RightPanelResizeHandle is now rendered as a sibling of the overflow-hidden/[contain:layout_paint] wrapper (covered by the new PreviewPanelShell.test.ts case), and the persistent terminal drawer animates via a content-sized grid-rows-[1fr]/0fr wrapper rather than a wrapper pinned to the store height, so pointer-drag resizing is no longer clipped and the xterm element keeps its measured size while collapsed.

Checked and found no issue with:

  • floating-surface-motion / -resizable component-layer contract vs Tailwind utility layer ordering; the :is([data-*-style]) branch covers popups that carry the transition attributes themselves (menu, popover, select, tooltip) and the :has(> …) branch covers the wrapper-plus-child structure in combobox/autocomplete. [--floating-surface-duration:*] overrides and [data-instant] still win by specificity within the layer.
  • Dialog backdrop transition-all → transition-opacity: dialog-backdrop only declares static background/backdrop-filter, so nothing else was being transitioned.
  • Right-panel retain/exit state machine: RIGHT_PANEL_EXIT_DURATION_MS (200ms) matches the duration-200 shell transitions; panelLayoutControls keeps one absolute inset in both containers; selectSelectedRightPanelSurface already existed in rightPanelStore.ts and returns the selection while closed, which is what keeps the tab bar and retained content correct.
  • Launcher shortcut ownership: keyboardShortcutsEnabled gates both the capture-phase key handler and the data-surface-launcher-keys attribute read by ChatView's type-to-focus guard, so a hidden-but-mounted panel no longer claims letter keys; inert/aria-hidden prevent focus and AT exposure of the hidden panel and drawer.
  • No position: fixed descendants inside right-panel surfaces that the new [contain:layout_paint] wrapper could clip, and no overhanging absolute handles inside the terminal drawer (its resize strip is top-0, inside the aside).

🔍 Agent activity — 1 tool call

💭 I'm checking whether the previous issues with PreviewPanelShell's overflow clipping and ChatView's terminal drawer height were fixed in the current commit. The diff shows a new test for keeping the resize handle outside the panel content clip, which suggests that was addressed, but I need to verify if the overflow property is still causing problems. The resize handle issue looks resolved—it's now positioned outside the clipping wrapper as a sibling element. Now I'm examining the ChatView terminal drawer implementation to see how it's handling the grid-rows approach instead of a fixed height. I'm working through how the grid layout resolves row sizing when the wrapper uses grid-rows-[1fr] with overflow-hidden during a resize drag—specifically whether the 1fr value expands to fill the grid container or collapses to the content's max-content when the container itself is shrink-0 in a flex column. Looks like the changes addressed the earlier issues with clipping and grid layout. Now I'm reviewing the current diff to spot any new problems, particularly checking whether the transition duration constant of 200ms aligns with what's actually being used in the PreviewPanelShell and the right panel container in ChatView. The sheet implementation uses 240ms instead, and I need to verify whether retainClosedRightPanelContent is even relevant for the sheet path. I'm also noticing that rightPanelMaximized is now computed differently using !shouldUseRightPanelSheet rather than the previous canMaximizeRightPanel logic, and there's a removed call to setMaximizedRightPanelThreadKey(null) that I should trace to understand the context. The RightPanelTabs component is remounting based on visibility state with a conditional key, which might be intentional for resetting launcher state but worth confirming. Finally, I'm checking the CSS layer ordering in index.css where .floating-surface-motion is defined in the components layer, but Tailwind utilities like duration-* would override it since utilities come after components. Now I'm looking at the selector specificity for the floating surface motion styles and whether the removed Tailwind data-starting-style utilities are causing conflicts with the new CSS approach. The floating-surface-motion class handles the reduced motion case by setting scale to 1, which disables the scale animation. The tooltip's data-instant attribute uses a duration override that wins through specificity, but I need to verify whether that attribute actually sits on the popup element that has the floating-surface-motion class. Looking at the menu component, it sets a duration of 150ms, which is fine. But I'm noticing the menu popup previously had no transitions defined in its classes — they might have been in the dropdown-glass style from index.css, so I should check whether the new approach is accidentally applying transitions twice. I'm noticing that floating-surface-motion is applied to the SelectPrimitive.Popup element, and Base UI does add the transition data attributes (data-starting-style and data-ending-style) to the Popup. This means the select popup now has animation classes where it previously had none. The question is whether the Popup itself needs a transition property defined for the exit animation to work, since Base UI waits for the transition on whichever element has the data-ending-style attribute. The :has(> ...) selector variant likely handles cases where the transition attribute lives on a child element rather than the Popup itself. I should verify where the data attributes actually get applied in menu.tsx and popover.tsx. There's also a potential concern about whether the :has(> :is(...)) selector will work properly with Tailwind and Lightning CSS, though it should pass through fine as plain CSS in the @layer components. The scale: 0.98 and opacity: 0 in the components layer should coexist fine with any Tailwind data-starting-style utilities since those would take precedence anyway. Now I need to look at the bigger behavioral changes in ChatView — specifically the rightPanelMaximized state, the removal of setMaximizedRightPanelThreadKey(null) at line 3401, the panelLayoutControls, and how RightPanelTabs visibility is being spread across the component. I need to locate where the right panel container gets rendered around lines 6680-6740 and understand its collapse animation behavior. I should also verify that the selectSelectedRightPanelSurface selector is properly defined in the store, since it's being imported but I want to confirm it actually exists. When the panel is closed but not maximized, RightPanelTabs renders with visibility hidden and zero width, which works fine. However, when closing a maximized panel, rightPanelMaximized stays true for 200ms during the fade-out animation, causing the chat column to collapse to zero width at the same time—so both elements are shrinking simultaneously instead of sequentially. After the 200ms delay, rightPanelMaximized flips to false and the chat column snaps back to full width instantly, which creates an abrupt appearance with no animation. The panelLayoutControls also remount during this transition, jumping from an absolute position to the header, which is a minor layout shift. The empty state has a focus-on-mount ref that calls focus when the component mounts, but since the panel stays mounted with visibility toggled, the empty state renders while hidden. Looking at the key prop, it forces a remount whenever visibility changes, which triggers the focus handler both when the panel opens and closes—stealing focus unintentionally when it should only happen on open. When the panel closes, the empty state remounts due to the key change, and focus() gets called on an element inside an inert container. Since inert elements can't receive focus, the call becomes a no-op and focus stays where it is, which is probably fine. But I need to check whether the empty state is actually rendered by RightPanelTabs itself or if it's part of the retained content during the 200ms close animation. I'm realizing the RightPanelTabs component stays mounted even when closed in inline mode, and it conditionally renders either an empty state or children based on whether activeSurfaceId is null. The key insight is that activeSurfaceId comes from a new selector that returns the selected surface's ID regardless of the panel's open state, whereas the old selector would return null when closed—so now when the panel is closed but a surface is selected, the children still render. There's a potential issue with the empty state calling focus() on mount while the panel is inert and hidden, which wouldn't actually move focus. The focusOnMount behavior is intended when the panel becomes visible, but I need to check whether toggling visibility causes a remount and whether inert is properly scoped to inline mode only. I'm also tracking down where data-surface-launcher-keys is used and whether setting it to undefined when hidden prevents duplicates as intended. Now I'm examining the sidebar styling — specifically the offcanvas collapse state where the sidebar translates off-canvas with opacity changes. The data-collapsible attribute is only set when collapsed, so the group selector should only apply in that state. I'm also checking whether adding scale transforms to buttons, especially popover triggers, creates any containing block issues that could affect layout or stacking context. There's a potential conflict with transition utilities: the new transition-[scale,box-shadow] on buttons replaces the previous transition-shadow, and if call sites add their own transition-colors, Tailwind's cascade means the later class in the CSS file wins rather than source order. This is the same conflict that existed before, though, and tailwind-merge would handle it by removing the base transition in favor of the override. For the dialog backdrop, I need to check whether changing from transition-all to transition-opacity loses any animations — specifically whether the backdrop-filter property (like blur) was being transitioned. Looking at the CSS, the starting and ending states only differ in opacity, so transition-all was effectively just transitioning opacity anyway. The dialog popup also switches from ease-in-out to a custom easing variable with motion-reduce support. For the Sheet popup, it uses transition-[opacity,translate] with motion-reduce disabling all transitions, but I'm wondering if Base UI's Sheet component relies on those transitions for its internal behavior. Looking at the Collapsible component, it also gets motion-reduce:transition-none, which means the height jumps instantly under reduced motion since the transition is removed but the height still changes from the starting style. Now examining the floating-surface-motion layer... I need to verify where the .floating-surface-motion class is actually placed in components like menu and combobox to ensure the :has selector targets the right element with the transition attributes—if the class ends up on the wrong level, the enter/exit animations could break silently. The wrapper span holds the class while the inner Popup receives data-starting-style and data-ending-style attributes, which means the :has(> ...) selector correctly targets direct children. I'm noticing a concrete issue though: the wrapper previously had transition-[scale,opacity] but lacked the data-starting-style utilities that would actually define those initial values. Looking at the grep results, there's no global CSS rule for [data-starting-style] in index.css beyond the new ones, so the previous implementation probably relied on Base UI's own styling. The PR's intent is to add animation where there wasn't any before, which seems fine. The MenuPopup width-defaulting contract stays unchanged, and the base class now includes [--floating-surface-duration:150ms] without interfering with the width detection regex since that only inspects the consumer's className. I should quickly verify the autocomplete popup structure and confirm the class placement relative to the data attributes. I'm working through how the Popover popup receives the data-starting-style attribute and how it interacts with the Viewport's transition states. The selector .floating-surface-motion:has(> :is([data-ending-style], [data-starting-style])) targets the popup when its child (the Viewport) has elements with those transition attributes. I need to clarify whether Base UI applies data-starting-style directly to the Viewport itself, and how the opacity animation works with the data-current and data-ending-style combination. The direct child of the popup is the Viewport, which doesn't itself receive the starting/ending style attributes — those are applied to the descendant panels inside it. Since the :has(> ...) selector only matches direct children, the popup won't trigger the fade-out rule based on its Viewport's contents. I should verify this holds for other cases like Tooltip and MenuPopup to ensure the selector doesn't accidentally match popups with direct children that have these transition attributes. The specificity works out because data-instant sets transition-duration to 0ms in the same layer, overriding the custom property value. Since Base UI applies data-instant to the popup element itself, the instant behavior should work correctly. Let me check the tooltip structure to see how the popup and positioner are organized. I'm noticing a potential conflict with the tooltip popup's styling—the animation utilities that were previously defined as Tailwind classes have moved into the components layer, but the popup's default opacity when open is already set to 1, so that should be fine. The real issue is that the floating surface motion resizable element has a transition property that includes width, height, opacity, and scale, which could interact unexpectedly with the tooltip's own transitions. Looking at the CSS cascade, the .floating-surface-motion rule sets the duration and timing, then .floating-surface-motion-resizable comes later and overrides the property—since they're both in the components layer with equal specificity, the later rule wins, which is correct. The :is([data-ending-style], [data-starting-style]) selector sets opacity to 0 and scale to 0.98 in the components layer, but Tailwind utilities like opacity-* live in a higher layer, so there shouldn't be a conflict there. For the dropdown-glass popups, that's a utility in Tailwind that only handles background and backdrop-filter without touching opacity, so that's safe. The critical check is whether Base UI applies data-starting-style to the Popup element as a direct child—it does for combobox popups, which means the :has(> :is(...)) selector should work as intended. But there's a potential issue with the exit animation: the transition is on the parent span, not on the popup element itself, and Base UI observes the popup element's transition to know when the exit animation finishes. If Base UI unmounts the popup immediately because it has no transition, the exit animation on the parent would be cut off before it completes—though this is speculative since I'm not entirely sure how Base UI's useAnimationsFinished hook traverses the subtree. The real question is whether this counts as a concrete regression worth reporting. It's not a regression from previous behavior since there was no exit animation before, but it is an inconsistency: menus and popovers would get the exit fade while combobox and autocomplete wouldn't. That feels like a consistency issue rather than a hard violation, and without runtime evidence it's risky to speculate about Base UI's internals. I should focus on stronger, more concrete findings instead. The unlayered CSS wins over Tailwind utilities, which is fine for this single-use case. The translate property replacement is consistent with Tailwind v4's approach. For the terminal drawer, @starting-style triggers the entry animation when the drawer transitions from hidden to visible, but since it's now always mounted with visibility toggled, I need to check whether that animation fires correctly on state changes rather than just initial mount. I'm also verifying browser support for animating grid-template-rows between 0fr and 1fr values — Chrome 107+, Firefox 116+, and Safari 16+ all support this interpolation, so that should be fine. The real question is whether 1fr resolves properly in an indefinite-height container when transitioning from 0fr. In an auto-height grid, a single 1fr row gets sized to its content (the fr unit treats the row as minmax(auto, ...) with flex resolved against the content), so the collapse animation from 1fr to 0fr works as expected. But I need to think through what happens during drag resize when the terminal drawer's aside has its own explicit height style applied. Now I'm checking whether the visible prop is properly threaded through PersistentThreadTerminalPanel down to ThreadTerminalPanel and ultimately to ThreadTerminalDrawer. The panel receives visible={rightPanelOpen} from ChatView, and I need to verify that this visibility state is actually being passed through and respected in the right-panel terminal surface. I'm tracing through how the visible prop flows into ThreadTerminalDrawer — it looks like the diff adds visible={visible} at line 1167, but I need to check what was happening before this change. The previous version of PersistentThreadTerminalPanel didn't seem to pass a visible prop at all, so ThreadTerminalDrawer was getting it hardcoded or not receiving it in panel mode. Since the typecheck passes, visible is probably optional with a sensible default. When visible=false during the panel's exit animation, the drawer stays mounted but keyboard shortcuts disable, which is the intended behavior for a closed-but-retained panel. The preview surface also gets visible={rightPanelOpen}, so during exit the webview may detach or hide immediately, leaving an empty panel for the animation. Now I'm thinking about the RightPanelEmptyState remount behavior and what actually needs flagging here. u call focus() on a non-focusable element, focus doesn't move. I'm checking for other issues now — aria-hidden and inert are both applied when the panel's hidden, which is redundant but harmless. The visibility styling uses width, translate, and opacity transitions together, and the inner content has a fixed width with shrink-0 while the outer container constrains to max-w-full with overflow-hidden and layout containment. Looking at the height constraints: the clip wrapper uses h-full min-h-0 inside a flex column host that also has h-full min-h-0, and the content column inside gets h-full as well. The contain: layout paint on the clip wrapper creates a new formatting context, so I'm checking whether there are any absolutely-positioned descendants that might be affected by that containment. Actually, I'm realizing a potential issue with the width: the host is width: ${width}px with border-l, so its content box is actually width - 1px due to the border. But the inner content div is explicitly set to width: ${width}px, which means it's 1px wider than the clip area and gets clipped on the right side by the overflow-hidden. So the real issue is that when the host gets constrained by max-w-full in a flex context, the content stays at its fixed width and clips rather than reflowing to fit the narrower space. but the inner content no longer reflows into the clamped host. The fixed inner width was added to prevent content from squashing during the collapse animation, but a better approach might be using min-width or only pinning the width during the animation itself. This looks like a concrete issue: the inner content width duplicates the host width but ignores the max-w-full clamp and the 1px border, making the content 1px wider than its clip boundary. Let me verify how max-w-full actually behaves on the host when the parent is the flex row. So the panel's max-w-full equals 100% of its container, and when the width exceeds that container, the host gets clamped to the container width while the clip wrapper becomes container minus 1. This means content gets cut off—the tab bar buttons and right padding disappear—and the content doesn't shrink anymore, whereas previously the flex children would have shrunk to fit. This happens on window resize when the viewport narrows, the maxWidth gets recomputed, and I need to check whether useResizableWidth is actually clamping the stored width value. The hook's clamp should pick this up automatically, so width follows the clamp. With the floor at Math.max(PREVIEW_PANEL_MIN_WIDTH, ...), on very narrow rows (less than 720px), maxWidth becomes 360 while the row might only be 500px total, so the host width of 360 fits fine. To exceed the container, I'd need the container itself to be less than 360—which is very narrow and probably triggers shouldUseRightPanelSheet on narrow screens. The max-w-full clamp mainly kicks in at edge cases, though the host being shrink-0 in the flex row means if both columns can't fit, the sibling chat column shrinks instead. The 1px content clip on the right edge is a real issue—the panel sits at the far right of the app, so losing that 1px of right padding or scrollbar is nearly invisible visually, but it does mean right-anchored elements like the tab bar's padding or scrollbars get cut off slightly. I think the cleaner fix is to make sure the inner content div respects the full available width by either putting the border on the outer wrapper, subtracting the border width from the content's width calculation, or applying the panel width directly to the inner div so the host stays at full width. So the 1px overflow is a real clipping regression, even if minor. Given that this file was already flagged for clipping issues before and the check defaults to failure, it might not be worth the nitpick. Let me look at the sidebar changes instead since those could have more widespread impact. The sidebar uses fixed positioning with hidden/flex breakpoints, and when collapsed it shifts off-canvas with negative left positioning, zero opacity, and a translate transform. Since it's completely off-screen, the opacity and translate are mostly visual polish, though I should note that translate on a fixed element creates a containing block for children, and opacity below 1 establishes a new stacking context—but that's only during the collapsed state when it's hidden anyway. For the button styling, there's an active state that scales down slightly, and I'm using an arbitrary transition property that targets both scale and box-shadow. The scale property is animatable, so that works fine. With motion-reduce, it falls back to just transitioning the box-shadow. The duration gets merged properly by tailwind-merge at call sites. One thing to watch: when buttons render as menu triggers and scale on active, the scaled button might misalign with an anchored menu. The issue is that menus open on pointerdown while the trigger is scaled to 0.97, so the popup positions itself against the shrunk button rect. Since floating-ui's ResizeObserver doesn't detect transform changes, the popup stays positioned to that scaled rect even after the button returns to normal size on release — creating a sub-pixel offset that's barely noticeable but technically there. Now I'm checking the dialog backdrop CSS to see if switching from transition-all to transition-opacity makes sense. The dialog backdrop's blur is static, so using transition-all doesn't actually animate anything—transition-opacity would be equivalent. I notice the sheet backdrop has motion-reduce applied but the dialog backdrop doesn't, which is a minor inconsistency. The opacity fade itself is fine under reduced motion since the dialog keeps that transition enabled, so this seems like an intentional design choice rather than an oversight. Now I'm looking at where that setMaximizedRightPanelThreadKey(null) call was removed from—need to check what function that was in and understand the context around it. I'm checking whether canMaximizeRightPanel is still referenced elsewhere in the codebase, since TypeScript's noUnusedLocals would catch it if it's orphaned. For the sheet rendering, the activeSurfaceId is correctly set to the selected right panel surface when the panel is open, so the active and selected states align. There's a semantic shift happening where the inline panel's activeSurfaceId now pulls from selectedRightPanelSurface instead of its previous source. I see that selectSelectedRightPanelSurface is already defined in rightPanelStore, so that's not something this PR added. Now I need to check how the maximize toggle works around line 3528 where canMaximizeRightPanel is used, especially since the panel can now be maximized while closed. When the user closes the maximized panel, there's now a 200ms delay before the maximized state resets, which means the chat column stays collapsed during that window. The key question is whether the panel actually animates out during this time, or if the maximized state needs to toggle first for the animation to work. React 18 batches separate setTimeout callbacks as distinct tasks, so they trigger two renders—between them the panel is hidden but content retained, which is fine since both are invisible. When the user switches threads, the maximized state clears immediately. But there's a real issue: the layout controls can jump position 200ms after closing a maximized panel, since they're rendered in different branches depending on the maximized state, whereas closing a non-maximized panel moves them immediately as before. I'm noticing the panelLayoutControls uses absolute positioning in both layout states, staying at the same offset so it doesn't shift between containers. The row container is always relatively positioned, but the header's positioning changes depending on whether we're in Electron mode — it's relative in Electron but not in non-Electron, which means the absolutely positioned controls resolve against different ancestors depending on the mode. The RightPanelMaximizeControl only renders when the right panel is open and we're not using the sheet layout, so it disappears during the fade-out animation of a maximized panel closing. Looking at the actual rendering now, I'm seeing a potential issue with the PreviewPanelShell's inner content width — it's set to the full width while the clip wrapper is 1px narrower due to the left border, which could be clipping the right edge of all right-panel content. During the collapse animation, the host width animates down to zero while the content stays at its original width, getting progressively clipped. For the chat column, overflow-hidden is applied instead of just overflow-x-hidden, which adds vertical clipping. This is likely to prevent content from spilling during the maximize collapse when the width goes to zero, but I need to verify whether the terminal drawers inside the chat column have any intentional vertical overflow that this would affect. The overflow behavior change is minor—previously the chat column could scroll vertically if content overflowed, but now with overflow: hidden it clips instead, though this shouldn't happen in practice with the flex layout. Now I'm looking at another issue: activeThreadTerminalMounted has self-referential logic where it reads currentThreadIds from the state updater parameter. Now I'm realizing the real problem: keeping the drawer mounted with zero height means the xterm terminal inside stays in the DOM with a 0-height container, which can cause the fit addon to compute dimensions from that zero height and resize the PTY to just 1 row—a classic bug that would reflow all the shell output. Before, when inactive threads were hidden with display:none, the fit addon would bail out when it detected zero dimensions, but now with the grid-rows approach it stays mounted and tries to fit. The terminal's height stays at 300px even when clipped, so xterm dimensions don't get reset and ResizeObserver-based fit works fine. For width, same story. Now looking at the inert attribute approach — React 19 supports it as a boolean, so they can pass inert={!visible ? true : undefined} to conditionally disable the element, and the React version here is likely 19 given the use() hook in the combobox. The drawer wrapper combines pointer-events-none with inert when hidden, plus aria-hidden for good measure. With reduced motion enabled, the hide transition becomes instant but the element stays in the layout with zero-height rows. For the entry animation, they're using a @starting-style rule at the top level that sets the initial state with zero-height rows, zero opacity, and a slight downward translate. Since this rule is unlayered, it takes precedence over Tailwind's translate utility, which is fine. The opacity also starts at zero in the @starting-style block. Now looking at another candidate approach... The pill container is conditionally rendered, so @starting-style handles the entry animation, but when it's removed there's no exit animation — asymmetric but acceptable. There's a potential issue though: @starting-style blocks are at the top level of the stylesheet, while the reduced-motion variant is nested inside a media query, which should work fine. I'm noticing a conflict with the floating-surface-motion component-layer class versus Tailwind utilities — if consumers pass transition, duration, opacity, or scale utilities through className, they could clash with what's already defined. The bigger concern is whether any consumers are relying on data-starting-style or data-ending-style attributes, or if they're passing transition-none expecting it to override the popup's base transitions. I should search for these patterns in the codebase to see if removing the base transitions breaks anything. I'm noticing that toast.tsx at line 742 still has the old inline transition pattern on a dropdown popup, which duplicates what the PR is trying to standardize with floating-surface-motion. It's a consistency issue, but since the file wasn't the focus of this PR, I'll leave it alone rather than scope creep into unrelated cleanup. Now I need to verify something important about the popover implementation— specifically how Base UI's PopoverPrimitive.Viewport interacts with the data-starting-style and data-ending-style attributes. The Viewport's className uses descendant selectors with these attributes, and it also sets data-instant:transition-none on itself, which suggests Base UI applies transition-status attributes to the Viewport too. If that's the case, swapping popover content could cause the entire popup to fade out—a visible regression for multi-page popovers. I should check Base UI's Popover.Viewport implementation to understand whether it applies these attributes and how the rendered children inherit them. I'm checking Base UI's documentation for how the Viewport handles transition attributes — it looks like the Viewport itself carries data-transitioning rather than the starting/ending style attributes, which are applied to its children instead. That should keep things safe from unintended blinking during content swaps. The strongest issue I'm seeing so far is the PreviewPanelShell's 1px border and width mismatch, so let me dig deeper into how severe that actually is. So in non-inline modes there's no left border, so no width mismatch. But inline non-maximized mode has that border, creating a 1px discrepancy where the content box ends up narrower than the fixed inner width, causing the right edge to clip by 1px and shift the panel content. The persisted width measurement also gets thrown off since the resize handle sets the width but the visible content is actually 1px less. It's a real geometry regression from the recent changes, though I'm weighing whether it's significant enough to fail the check. The handle sits at the panel's left edge, so the width calculation is straightforward. Given that a 1px clip is a real bug even if minor, I could fix it by sizing the inner div with a calculated width that accounts for the border, or by moving the panel width to a CSS variable. The cleanest approach might be to apply the width directly to the inner content div and let the outer host animate through a different mechanism, or shift the border from the host to the clipping wrapper instead. so the host's content box is actually W-1 pixels wide. The simplest fix is probably to set the inner element's width to calc(${width}px - 1px) to account for the border, or define the panel width as a CSS variable and use that in the calculation. Before the change, flex children fit within that W-1 space; now they're getting the full W, so content is 1px wider and gets clipped on the right. It's subtle—the right padding of elements like the diff columns and tab bar effectively shrinks by 1px—but it's a real geometric regression. The fix is straightforward enough that I think it's worth flagging as a finding. Let me verify I'm not overlooking something by checking how the ThreadTerminalDrawer interacts with the layout. The wrapper has shrink-0 with grid, and the chat column above it is flex min-h-0 flex-col overflow-hidden, so the drawer stays at its content height while the messages area shrinks to fill the remaining space. That part looks correct, but I need to trace through whether the drawer's sizing depends on being a flex item in a specific way. The grid uses grid-rows-[1fr], which in an auto-height container behaves like fit-content — the row expands to fit its content. So the wrapper's height is determined by the drawer's content. Now I'm wondering if adding overflow-hidden to the inner wrapper would clip the resize handle at the top edge of the drawer, especially if that handle has a negative offset overhang like the preview panel does. The handle positioning and grid row sizing are working correctly—as the aside height changes during a resize drag, the grid row expands to match, keeping the handle visible. But I'm realizing the transition applied to grid-template-rows might cause a delay in that responsiveness. The specified value stays 1fr in both states, so technically no transition should fire since the CSS property itself isn't changing—only the computed used size is. But browsers like Chrome actually do interpolate grid-template-rows transitions on the used values, which is why the 0fr to 1fr trick works smoothly. So if the specified value remains 1fr, the transition might not trigger at all, or it could create a mismatch between the instant height change and the animated grid row. The specified values stay interpolable (0fr to 1fr), so transitions only trigger on style changes, not content layout shifts—keeping drag smooth. When the terminal opens, the grid animates from 0fr to 1fr nicely. With the hidden state using grid-rows-[0fr], the wrapper collapses to zero height while the inner aside stays clipped at 300px, preserving xterm. Now I'm checking browser support for animating grid-template-rows—Chrome and Firefox have solid support, but Safari's handling of fr value interpolation is less clear to me. Since this is an Electron app with Chromium, the animation will work fine there, and if Safari doesn't animate it smoothly, the drawer would just snap closed instead of sliding, which is acceptable degradation. Let me search the codebase to see if anything else depends on that .hidden class I'm removing from the drawer wrapper. I also notice they've switched to using inert for hidden drawers instead of display:none, which properly prevents focus on inactive thread content. But now hidden drawers are rendered at zero height with their content clipped by overflow, so they're not painted but still take up layout space — and with multiple hidden drawers stacking up to the MAX_HIDDEN limit, that could add some overhead. There's a potential performance concern too: xterm instances in hidden threads might now render continuously since they're technically visible in the DOM, though xterm.js likely has optimizations like IntersectionObserver checks to skip rendering when elements aren't actually visible. I'll skip diving into that for now. The accessibility looks solid with aria-hidden and inert both applied to hidden drawers. Now I'm checking the CSS approach with @starting-style and the data-hidden attribute... I'm checking whether focusOnMount works correctly when the panel starts closed with no surfaces — the RightPanelEmptyState mounts on page load while hidden inside an inert host, and calling focus() on a node with tabIndex={0} might get blocked if inert is properly supported. The mobile sheet mode doesn't apply here since it only renders when open, but there's something else to consider about this setup. Looking at how inert gets applied: it's only set when isInline && !visible, and RightPanelTabs receives the visible prop from ChatView as rightPanelOpen. In inline mode, inert applies correctly. The _chat.pull-requests.tsx file also uses inline mode without an explicit visible prop, so it defaults to true and skips the inert — that's fine and unchanged. When the panel is closed on first load, focus() on an inert element does nothing per spec, which Chrome implements correctly. If the user opens the panel via keyboard shortcut, the remount triggers focus to move to the launcher, same as before. Now I need to think through what happens when the panel closes while focus is already inside it. I'm checking whether other places with padding-left transitions were missed—if some topbars animate at 240ms with the new easing while others stayed at 200ms linear, they'd desync during sidebar toggles, creating an inconsistency the PR introduced. Let me search for any remaining instances of those transition patterns. I'm noticing there are still several animation timing mismatches across the codebase — the topbars in ProjectSettingsPanel.tsx and UsagePage.tsx are stuck at 200ms with linear easing for the sidebar toggle padding animation, while SidebarGroupLabel has the same timing issue, causing its margin and opacity to fall out of sync with the sidebar's new 240ms drawer easing. The sidebar width animation at 240ms with drawer easing now desynchronizes from the titlebar padding-left animation at 200ms linear on the Usage and ProjectSettings pages — they're both in scope but not in the diff, so I can only report this inconsistency in the summary rather than as inline comments. Actually, I can work around this by commenting on an adjacent changed line in the sidebar file itself, noting that the two unupdated headers and SidebarGroupLabel still use the old 200ms timing and create the visible desync. Since the PR already updated four of six such headers, flagging the two missed ones is directly aligned with the intent of the change, not unrelated cleanup. The real question is whether this is a symptom of a deeper issue—six call sites repeating the same hardcoded duration suggests this should be a shared token or variant rather than a literal value scattered across the codebase. That would be a more durable fix than manually updating each call site. The fix is to consolidate the transition styles into the shared COLLAPSED_SIDEBAR_TITLEBAR_INSET_CLASS constant so all six consumers automatically stay synchronized instead of each maintaining their own transition separately. I should anchor my comment on a changed line in the diff—NoActiveThreadState.tsx line 13 is a good target since it's a small file with a clear change. I'll point out that two call sites were missed and suggest moving the timing into the shared constant to prevent future drift. Now for the second finding about the border: the clip wrapper's content width is actually the host width minus 1px due to the border-left, while the inner div is fixed at the full width, which means 1px gets clipped on the right. The resize handle positioning remains correct since it's still outside the clip wrapper. The overflow-hidden on the clip wrapper enforces this clipping behavior. But this creates a potential issue: if panel surfaces have absolutely-positioned elements that overhang their bounds—like internal split handles with negative left/right offsets, floating controls in the preview panel, or drag regions—those would now get clipped where they previously wouldn't have been. I need to search for any -left-1 or -right-1 positioning within nested surfaces that might be affected by this change. The RightPanelResizeHandle is the only element overhanging outside the clip boundary, which is what I want. I'm considering whether panel content like the preview's inline mini-player or resize handles within the DiffPanel might also overflow, but I don't think they do. On the 1px discrepancy, I'm fairly certain it's a real regression that I can trace back to the diff—the host element has a left border and box-border sizing, which accounts for the measurement difference. But I already reviewed this file and it was corrected properly last time. Adding a 1px complaint feels nitpicky, especially given the guidelines about reporting only concrete violations from changed lines and not failing on optional aesthetic preferences. A 1px clip is technically a geometry regression, not just aesthetics, but I'm questioning whether it's worth flagging. There's also the question of whether the panel's content being 1px wider than the visible area could trigger a horizontal scrollbar, though the overflow-hidden clip prevents that anyway. I'm thinking about a different angle on the wrapper's contain: layout_paint property—the paint containment also clips fixed-position descendants. If any panel content renders fixed-positioned elements like an image lightbox or floating widget within the DiffPanel, those would be affected. Let me search through the panel components to see if any of them use fixed positioning directly. There's no fixed-position issue, so the 1px discrepancy is the main finding. Let me verify once more whether the border-l on the host creates a compensating element in the clip wrapper — the wrapper is w-full, which means it's 100% of the host's content box width, so if the host has a 1px left border, the content box is actually width minus 1, making the clip wrapper that same reduced width while the inner content tries to fill the original width. During the collapse animation, the inner content stays fixed at full width while the host shrinks to 0. The clip wrapper is left-anchored and moves right as the host narrows (since it's the last flex item, its left edge shifts right while the right edge stays fixed at the viewport). The content inside is anchored to the clip's left edge, so it slides right with the panel and gets progressively cropped on the right side by the shrinking width — combined with the translate and opacity changes, this is the intended effect. Now I'm deciding whether to report the 1px issue — the check has already been reviewed once and the author addressed the previous findings. The stronger framing is that the inner content width duplicates the host width and ignores both the 1px border and the max-w-full clamp that the existing test protects. This is a real geometry-ownership problem: when the row clamps the host, the surface content doesn't reflow and gets silently cropped instead. The fix could be to size the inner div from the same box as the clip (using w-full normally or calc(width - 1px)), or use min-w- semantics. But if they set the inner to w-full, they lose the "no reflow during collapse" property, which is the whole point. A more precise approach would be to move the border from the host to the inner content div, so the host's content box equals the width directly. The correct solution is to keep the border on the host and set the inner width to calc(${width}px - 1px) to account for the border-box. I'll suggest this in the comment, framing it as: the inner is pinned to the full width while its clip is width minus 1px because the host's border is inside the border-box, so the right 1px gets cropped—the fix is to subtract the border from the inner width or move the border onto the inner column. The anchor point is line 112 in PreviewPanelShell.tsx where the width style is being set. I'm confident about the border-box math here, so this is a real finding worth reporting as a minor issue alongside the main timing inconsistency. Now I'm verifying the animation desync more carefully—the sidebar gap animates its width over 240ms with drawer easing, while the topbar padding-left on those pages animates over 200ms with linear timing, creating a visible 40ms mismatch before the sidebar finishes expanding. I also notice SidebarGroupLabel in the sidebar component animates margin and opacity over 200ms linear when collapsing to icon mode, which now desynchronizes with the sidebar's 240ms width animation. This is another regression introduced by the PR changes. I'm deciding whether to anchor both findings in a single comment or split them across two—one on the sidebar file itself and another on the topbar component. I'm debating whether comment A is worth the overhead—the 40ms label desync is minor compared to the broader timing mismatch pattern in B. Since GitHub inline comments are file-specific, I can't easily merge them across sidebar.tsx and NoActiveThreadState.tsx. I'll post B and C as separate comments, then reference A's file location within B's body to keep the feedback cohesive. Actually, I could anchor one unified comment on sidebar.tsx at the line where the shared constant was changed, covering all the desynchronized transitions across SidebarGroupLabel, UsagePage headers, and ProjectSettingsPanel headers. That's cleaner—one comment naming the root cause and the fix needed. rrced with COLLAPSED_SIDEBAR_TITLEBAR_INSET_CLASS, so consolidating the transition timing into that constant would prevent the six locations from getting out of sync. For the preview panel, the inner column's fixed width gets clipped by its parent's full-width border box, leaving the right edge cropped by 1px — subtracting the border width from the column keeps it from reflowing while avoiding that crop. Adjusting the style property... I'm going to post the review with those two inline comments. Let me double-check the first comment against the "don't demand unrelated cleanup" rule — the PR retimed the shared sidebar animation, and both UsagePage and ProjectSettingsPanel are direct consumers that render the same collapse class, so they're legitimately affected by this timing change. I should verify those pages are actually live routes and not deprecated. Both pages render the sidebar headers, confirming they're active workspace pages. I'll frame this as a suggestion and post it.

1. modify_pr — Reviewed the motion/visibility refactor against the shared component system. Both findings from the previous run look addressed (the resize handle now sits outside the panel content clip, and the terminal drawer animates with a content-sized grid row instead of a pinned store height). Two new consistency issues below.


Agent Credits: 451 credits