Skip to content

fix(web): make plan sidebar resizable like diff panel#2205

Closed
jonathanperis wants to merge 15 commits intopingdotgg:mainfrom
jonathanperis:fix/plan-sidebar-resizable
Closed

fix(web): make plan sidebar resizable like diff panel#2205
jonathanperis wants to merge 15 commits intopingdotgg:mainfrom
jonathanperis:fix/plan-sidebar-resizable

Conversation

@jonathanperis
Copy link
Copy Markdown

@jonathanperis jonathanperis commented Apr 19, 2026

What Changed

The plan sidebar panel was rendered as a plain `

` with a hardcoded `w-[340px]` width inside `ChatView`. It could not be resized by the user.

This PR adds drag-to-resize support, a max-width constraint, and localStorage width persistence — matching the behavior already present on the diff panel.

Changes across 5 files:

  • `rightPanelLayout.ts` — adds `PLAN_INLINE_*` constants, `COMPOSER_COMPACT_MIN_LEFT_CONTROLS_WIDTH_PX`, and `createComposerWidthValidator(cssVarName)` factory (shared by both panels)
  • `PlanSidebar.tsx` — removes hardcoded `w-[340px] shrink-0 border-l`; width/border controlled by the parent wrapper
  • `hooks/useResizeHandle.ts` — new hook encapsulating pointer capture, RAF-batched width updates, body style management, click suppression, localStorage persistence with viewport-aware restore validation, and three cleanup paths: (1) `isDisabled` effect for sheet-mode flip mid-drag, (2) `setWrapperRef(null)` for programmatic sidebar close mid-drag, (3) React unmount effect; uses `useMemo` for the width validator
  • `ChatView.tsx` — replaces ~90 lines of inline resize logic with `useResizeHandle({ cssVarName, storageKey, minWidth, isDisabled: shouldUsePlanSidebarSheet })`
  • `routes/_chat.$environmentId.$threadId.tsx` — removes duplicate `COMPOSER_COMPACT_MIN_LEFT_CONTROLS_WIDTH_PX` constant; replaces `shouldAcceptInlineSidebarWidth` inline body with `useMemo(() => createComposerWidthValidator("--sidebar-width"), [])`

Why a plain div instead of `SidebarProvider > Sidebar`?

The `Sidebar` component uses `position: fixed; right: 0; z-10` hardcoded in its JSX. When both the diff and plan panels are open simultaneously, two `fixed` elements at `right: 0` overlap. The plain flex div stays in normal document flow — it renders to the left of the diff panel's fixed container with no overlap.

Default width: `clamp(20rem, 30vw, 28rem)` (~340px). Minimum: 320px. The `shouldAcceptWidth` validator rejects resize attempts that would overflow the chat composer or drop it below its minimum usable width — same behaviour as the diff panel. Width persists under `localStorage` key `chat_plan_sidebar_width`. Sheet/mobile behaviour (≤1180px) is unchanged.

Why

The diff panel has had resize support since it was introduced. The plan sidebar did not, despite being the same visual pattern. Users working with large plans benefit from being able to widen the panel to read content without scrolling.

UI Changes

2026-04-19.15-37-07.mp4

Checklist

  • This PR is small and focused
  • I explained what changed and why
  • I included before/after screenshots for any UI changes
  • I included a video for animation/interaction changes

[!NOTE]

Make plan inline sidebar resizable with persistent width

  • Adds a draggable left-edge resize handle to the plan sidebar in ChatView.tsx, mirroring the existing diff panel behavior.
  • Extracts resize logic into a new useResizeHandle hook that manages pointer capture, RAF-batched width updates, minimum width clamping, and localStorage persistence via a CSS custom property.
  • Adds createComposerWidthValidator to rightPanelLayout.ts as a shared utility that prevents resize widths from breaking the chat composer layout; the diff panel now uses this instead of its own inline implementation.
  • PlanSidebar.tsx no longer sets its own fixed width or border in sidebar mode — sizing is now controlled by the parent wrapper.
  • Behavioral Change: plan sidebar width now defaults to clamp(20rem,30vw,28rem) and persists in localStorage under chat_plan_sidebar_width; resize is disabled when the sheet layout is active.

Macroscope summarized 71731f2.


[!NOTE]
Medium Risk
Moderate UI risk: adds pointer-driven resizing, DOM measurement, and localStorage persistence in the core chat view, which could regress layout or leave global body styles stuck if edge cases are missed.

Overview
Makes the inline plan sidebar in ChatView draggable-resizable (defaulting to a CSS-var width and persisting to localStorage), including a left-edge resize handle and disabling the interaction when the mobile/sheet layout is active.

Extracts the resize interaction into a new useResizeHandle hook (pointer capture, RAF-batched updates, cleanup, click suppression) and centralizes the “don’t break the composer” width-acceptance logic via createComposerWidthValidator in rightPanelLayout, which the diff panel route now uses instead of its duplicated inline validator. PlanSidebar no longer hardcodes its own sidebar width/border, relying on the parent wrapper for sizing.

Reviewed by Cursor Bugbot for commit 71731f2. Bugbot is set up for automated code reviews on this repo. Configure here.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 19, 2026

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: e5a13f46-9b60-4603-ab87-034fd5697859

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions github-actions Bot added size:M 30-99 changed lines (additions + deletions). vouch:unvouched PR author is not yet trusted in the VOUCHED list. labels Apr 19, 2026
Comment thread apps/web/src/components/ChatView.tsx Outdated
Comment thread apps/web/src/components/ChatView.tsx Outdated
@macroscopeapp
Copy link
Copy Markdown
Contributor

macroscopeapp Bot commented Apr 19, 2026

Approvability

Verdict: Needs human review

Introduces new user-facing capability: a resizable plan sidebar with localStorage persistence. While the implementation follows existing patterns (reusing logic from the diff panel) and is self-contained to frontend UI, it adds new interaction behavior that warrants human verification of the UX.

You can customize Macroscope's approvability policy. Learn more.

@github-actions github-actions Bot added size:L 100-499 changed lines (additions + deletions). and removed size:M 30-99 changed lines (additions + deletions). labels Apr 19, 2026
Comment thread apps/web/src/components/ChatView.tsx Outdated
Comment thread apps/web/src/components/ChatView.tsx Outdated
Comment thread apps/web/src/components/ChatView.tsx Outdated
Comment thread apps/web/src/hooks/useResizeHandle.ts Outdated
Comment thread apps/web/src/components/ChatView.tsx Outdated
Comment thread apps/web/src/hooks/useResizeHandle.ts
Copy link
Copy Markdown
Contributor

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 4b50396. Configure here.

Comment thread apps/web/src/hooks/useResizeHandle.ts
jonathanperis and others added 8 commits April 22, 2026 21:18
Wrap PlanSidebar in SidebarProvider + Sidebar with resizable config,
matching the diff panel pattern. Width is persisted to localStorage
via chat_plan_sidebar_width. Removes hardcoded w-[340px].

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add shouldAcceptWidth validator matching the diff panel — plan sidebar
now stops expanding when the composer would overflow or fall below its
minimum usable width.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The Sidebar component uses position:fixed;right:0 — when both the plan
and diff panels are open they stack on top of each other. Replace the
SidebarProvider/Sidebar/SidebarRail wrapper with a plain resizable div
that stays in the normal flex flow. Resize logic (pointer events,
shouldAcceptWidth constraint, localStorage persistence) ported directly
from SidebarRail.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ayout

`shouldAcceptPlanSidebarWidth` and `shouldAcceptInlineSidebarWidth` were
verbatim copies differing only in CSS variable name. Extract
`createComposerWidthValidator(cssVarName)` factory to `rightPanelLayout.ts`
and deduplicate `COMPOSER_COMPACT_MIN_LEFT_CONTROLS_WIDTH_PX` constant.

Addresses PR review comment r3107299548.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Moves the ~90-line plan sidebar resize interaction (pointer capture, RAF-batched
width updates, body style management, click suppression, localStorage persistence)
out of ChatView into a reusable useResizeHandle hook.

Also fixes a bug: when the media query flips to sheet mode while the user is
mid-drag, the handle div unmounts before onPointerUp/onPointerCancel fire, leaving
cursor:col-resize and user-select:none stuck on document.body. The isDisabled prop
watches shouldUsePlanSidebarSheet and calls stop() immediately on transition.

Addresses PR review comments r3107317194 and r3107319298.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ctions

useCallback(factory(), [deps]) calls the factory on every render;
useCallback only memoizes the result of its first argument expression,
not the expression itself. Use useMemo(() => factory(), [deps]) so the
factory is only invoked when deps change.

Fixes useResizeHandle and shouldAcceptInlineSidebarWidth. Also removes
the eslint-disable comment that was masking the issue.

Addresses PR review comment r3107361909.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Remove wrapperRef from useResizeHandle return value; consumers only
  need setWrapperRef (the callback ref). Fixes unused destructured
  variable in ChatView.

- In setWrapperRef, call stop() when node is null so that body styles
  (cursor: col-resize, user-select: none) are cleared immediately if
  the wrapper div unmounts mid-drag due to planSidebarOpen flipping
  false programmatically (e.g. auto-close from plan state changes).

Addresses PR review comments r3107527089 and r3107536395.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…rt on mount

A stale stored width could squeeze the chat composer below its minimum
usable width if the viewport is narrower than when the width was saved.
Pass the parsed value through shouldAcceptWidth before applying it to
the wrapper; fall back to the CSS default (clamp(...)) if rejected.

Addresses PR review comment r3107548991.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@jonathanperis jonathanperis force-pushed the fix/plan-sidebar-resizable branch from a1d378c to f8e76c8 Compare April 23, 2026 00:25
@vercel
Copy link
Copy Markdown

vercel Bot commented May 4, 2026

@jonathanperis is attempting to deploy a commit to the Ping Labs Team on Vercel.

A member of the Team first needs to authorize it.

@jonathanperis
Copy link
Copy Markdown
Author

Superseded by #2598: #2598

@jonathanperis jonathanperis deleted the fix/plan-sidebar-resizable branch May 8, 2026 14:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:L 100-499 changed lines (additions + deletions). vouch:unvouched PR author is not yet trusted in the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant