Skip to content

feat(plan-sidebar): add drag-to-resize with localStorage persistence#1438

Closed
mia-riezebos wants to merge 2 commits intopingdotgg:mainfrom
mia-riezebos:main
Closed

feat(plan-sidebar): add drag-to-resize with localStorage persistence#1438
mia-riezebos wants to merge 2 commits intopingdotgg:mainfrom
mia-riezebos:main

Conversation

@mia-riezebos
Copy link
Copy Markdown

@mia-riezebos mia-riezebos commented Mar 26, 2026

What Changed

Adds drag-to-resize to the Plan sidebar and follows up with cleanup for drag styles if the sidebar unmounts mid-resize.

  • Replaced the fixed w-[340px] layout with a dynamic width driven by React state
  • Added a left-edge resize handle for PlanSidebar
  • Clamp width between 240px and 560px and persist it to localStorage
  • Clean up document.body cursor and user-select overrides if the sidebar unmounts during a drag

Why

The Plan sidebar was fixed at 340px with no way to adjust it for different screen sizes or content widths.

This implementation keeps the resize handle fully inside the Plan sidebar bounds so it does not steal pointer interactions from the chat area. That is the same class of interaction problem described in #958, but this PR does not close #958 because that issue is specifically about the diff sidebar / SidebarRail.

UI Changes

This is a drag-interaction change. The resize handle is invisible by default and shows a 2px border indicator on hover, consistent with the existing resize affordances.

Before:

Screen.Recording.2026-03-26.at.14.37.34_compressed.mp4

After:

Screen.Recording.2026-03-26.at.14.39.16_compressed.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

Low Risk
Low risk UI-only change confined to PlanSidebar that adds pointer-driven resizing and localStorage persistence; main risk is minor UX regressions (cursor/user-select leakage) during drag interactions.

Overview
Makes the Plan sidebar width user-resizable by replacing the fixed w-[340px] layout with a state-driven inline width, clamped between 240–560px.

Adds a left-edge pointer-capture resize handle and persists the final width to localStorage (Schema.Finite) on drag end, with unmount cleanup to avoid leaked cursor/user-select body styles.

Written by Cursor Bugbot for commit 6e60894. This will update automatically on new commits. Configure here.

Note

Add drag-to-resize with localStorage persistence to PlanSidebar

  • Adds a left-edge drag handle to PlanSidebar that lets users resize the sidebar between 240px and 560px (default 340px).
  • Width is persisted to localStorage under plan-sidebar-width using Schema.Finite validation and restored on load.
  • Pointer capture handles drag tracking; body cursor and user-select are mutated during drag and cleaned up on release or unmount.

<a href="https://app.macroscope.com\">Macroscope summarized 6e60894.

The Plan sidebar was hardcoded at 340px with no way to resize it.
Now it has a proper resize handle on its left edge — drag to widen
or narrow between 240–560px. Width persists across close/reopen
and page refresh via localStorage.

Follows the same pointer-capture pattern as ThreadTerminalDrawer:
pointerDown captures, pointerMove updates width in real-time,
pointerEnd persists to storage. The resize hit-area lives fully
inside the sidebar bounds so it never overlaps the chat scroll
viewport — a lesson learned from pingdotgg#958.

Closes pingdotgg#958

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@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 Mar 26, 2026
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 26, 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: 53f4047c-bd72-4a2a-ac6d-4645aa4f7368

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.

PlanSidebar is conditionally rendered — if the component unmounts
mid-drag (user closes the sidebar while resizing), the body cursor
and user-select overrides would leak permanently. Added a useEffect
cleanup that unconditionally removes them, matching SidebarRail's
existing cleanup pattern.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@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 Mar 26, 2026
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.

function readStoredWidth(): number {
const stored = getLocalStorageItem(PLAN_SIDEBAR_WIDTH_STORAGE_KEY, Schema.Finite);
return stored !== null ? clampSidebarWidth(stored) : PLAN_SIDEBAR_DEFAULT_WIDTH;
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Unhandled decode error in state initializer crashes component

Low Severity

readStoredWidth calls getLocalStorageItem without a try-catch. Since Schema.decodeSync throws on corrupted/invalid localStorage data, and this function runs inside a useState lazy initializer, a decode failure will crash the entire component render. The useLocalStorage hook wraps the same call in try-catch for this exact reason. In contrast, sidebar.tsx calls getLocalStorageItem inside a useEffect where a throw is less catastrophic. Here, it prevents the sidebar from mounting at all.

Fix in Cursor Fix in Web

@mia-riezebos
Copy link
Copy Markdown
Author

incorrect commit messages, retargeting this pr.

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.

Diff sidebar resize rail steals chat scroll/drag interactions at the chat boundary

1 participant