feat(sidebar): pin chats within projects#3913
Conversation
Persist pinned thread keys per logical project, keep pinned chats above the project's normal sort order, and separate pinned chats with a compact divider.
|
Important Review skippedAuto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
Track row hover and focus directly instead of relying on nested group selectors, and hide row actions while a thread drag is active.
Render the pinned divider during active drags when all threads are in one group, allowing the first pin and last unpin gestures to cross a measurable boundary.
ApprovabilityVerdict: Needs human review New feature introducing thread pinning with drag-and-drop, new UI components, and state management. Additionally, 3 medium-severity bugs have been flagged in review comments identifying potential correctness issues. You can customize Macroscope's approvability policy. Learn more. |
Prevent drag activation from interactive controls, preserve mobile row metadata beside actions, and keep scoped active threads visible in collapsed previews.
Overlay the temporary divider when only pinned or unpinned threads exist so mounting it during a drag does not shift the active row.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using high effort and found 3 potential issues.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 02c19ff. Configure here.
| scopedThreadKey(scopeThreadRef(thread.environmentId, thread.id)), | ||
| ), | ||
| ) | ||
| : threadPreview.hiddenThreads; |
There was a problem hiding this comment.
Stale preview hides active thread
Medium Severity
The useMemo for renderedThreads omits activeRouteThreadKey from its dependencies. When a project is expanded and its thread list is folded, this causes the sidebar to display a stale thread preview, potentially hiding the active thread or showing a previously active one.
Reviewed by Cursor Bugbot for commit 02c19ff. Configure here.
| <SidebarMenuSubItem | ||
| ref={nodeRef} | ||
| aria-hidden="true" | ||
| className={`flex h-3 w-full items-center gap-1.5 px-2 transition-colors before:h-px before:flex-1 after:h-px after:flex-1 ${overlaysThreadList ? "absolute inset-x-0 z-10" : ""} ${colorClass}`} |
There was a problem hiding this comment.
Overlay divider clips on last unpin
Medium Severity
The SidebarPinnedDivider is absolutely positioned with inset-x-0 but no top or bottom when either the pinned or regular thread list is empty. This misplaces its hit target at the top of the list and can hide the divider, leading to incorrect unpinning behavior during drag-and-drop.
Reviewed by Cursor Bugbot for commit 02c19ff. Configure here.
| hiddenThreads: threads.filter((thread) => !visibleThreadIds.has(thread.id)), | ||
| visibleThreads: threads.filter((thread) => visibleThreadIds.has(thread.id)), | ||
| hiddenThreads: threads.filter((thread) => !visibleThreadIds.has(getThreadId(thread))), | ||
| visibleThreads: threads.filter((thread) => visibleThreadIds.has(getThreadId(thread))), |
There was a problem hiding this comment.
Spurious Show more with empty hidden
Medium Severity
When getVisibleThreadsForProject pulls the active thread into the folded preview, it still returns hasHiddenThreads: true even if hiddenThreads is empty. Wiring that into the sidebar makes hasOverflowingThreads stay true whenever total threads exceed the preview limit, so “Show more” can appear after every below-fold chat is already visible—common once pinned rows fill the preview and the active unpinned chat is force-included.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit 02c19ff. Configure here.


What Changed
Why
Important chats should stay easy to reach without being pinned globally across unrelated projects. Project-scoped pinning keeps each sidebar group predictable while preserving the existing thread sort order for unpinned chats.
The hover control provides a quick direct action, while divider-crossing drag behavior makes bulk sidebar organization feel natural.
UI Changes
Interaction demo:
Screen.Recording.2026-07-12.at.11.51.13.PM.mp4
Checklist
Note
Low Risk
Client-only UI and localStorage preferences; no server or auth changes. Drag-and-drop and preview logic are the main regression surfaces.
Overview
Adds project-scoped thread pinning in the sidebar: pinned chats stay at the top of each project’s list (after the existing sort), with state in
pinnedThreadKeysByProjectpersisted viauiStateStore.Users can pin/unpin from hover actions on each row and by dragging a thread across a pin divider (
dnd-kit); divider visibility and pin vs unpin intent use new helpers inSidebar.logic.ts. Rows split into pinned and regular sections with aSidebarPinnedDividerand safer drag activation (blocked on buttons/inputs).getVisibleThreadsForProjectnow accepts an optionalgetThreadIdso folded previews work with scoped thread keys in grouped projects. Thread row UX shifts to explicit hover/focus row actions (pin + archive) andshouldHideThreadMetainstead of pure CSS group-hover for metadata.Reviewed by Cursor Bugbot for commit 02c19ff. Bugbot is set up for automated code reviews on this repo. Configure here.
Note
Add pin-to-top support for chat threads within sidebar projects
uiStateStoreaspinnedThreadKeysByProject, persisted across sessions.resolveThreadPinCrossingAction.prioritizeThreadsByPinnedKeys, preserving relative order within each group.shouldBlockThreadDragActivation,shouldHideThreadMeta,shouldShowPinnedThreadDivider) manage drag safety, metadata visibility, and divider rendering.Macroscope summarized 02c19ff.