From 35b82fdef35eef5031378b91e7a7acbb8de0e0ed Mon Sep 17 00:00:00 2001 From: Brooklyn Nicholson Date: Sun, 9 Aug 2026 03:23:08 -0500 Subject: [PATCH] feat(desktop): drag sidebar rows by the title, not just the grabber MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The grabber in the lead column was the only way to reorder a session or a project, and it only appears on hover — a 14px target for the whole gesture. The row's title is the obvious thing to grab, so put the sortable listeners on the row shell. For a session that means two drags share one press, since the title already starts the drag into the layout. They need no arbitration: each declines outside its own region. Over the sidebar only the reorder has a target (the session drop denies — side chrome hosts no main tile); over the tree only the session drop does (no sortable row there). Whichever the release lands on is the one that commits. Rows exclude their own controls through one data-row-actions selector, now owned by SidebarRowShell instead of restated per row. --- apps/desktop/src/app/chat/sidebar/chrome.tsx | 10 +++++++-- .../chat/sidebar/projects/overview-row.tsx | 13 ++++++++++++ .../src/app/chat/sidebar/session-row.tsx | 21 ++++++++++++------- 3 files changed, 35 insertions(+), 9 deletions(-) diff --git a/apps/desktop/src/app/chat/sidebar/chrome.tsx b/apps/desktop/src/app/chat/sidebar/chrome.tsx index 8e2da487829ac..d931c323de8d6 100644 --- a/apps/desktop/src/app/chat/sidebar/chrome.tsx +++ b/apps/desktop/src/app/chat/sidebar/chrome.tsx @@ -55,7 +55,9 @@ export function SidebarDateDivider({ className, label, ...props }: React.Compone ) } -/** Outer grid — sole owner of row height. */ +/** Outer grid — sole owner of row height. The trailing `actions` slot is + * marked `data-row-actions` so a row-wide drag gesture can exclude it with + * one selector: it holds real controls, never grab surface. */ export function SidebarRowShell({ actions, children, @@ -65,7 +67,11 @@ export function SidebarRowShell({ return (
{children} - {actions ?
{actions}
: null} + {actions ? ( +
+ {actions} +
+ ) : null}
) } diff --git a/apps/desktop/src/app/chat/sidebar/projects/overview-row.tsx b/apps/desktop/src/app/chat/sidebar/projects/overview-row.tsx index 210d861617dfa..aedd2e6e09e10 100644 --- a/apps/desktop/src/app/chat/sidebar/projects/overview-row.tsx +++ b/apps/desktop/src/app/chat/sidebar/projects/overview-row.tsx @@ -125,6 +125,18 @@ export function ProjectOverviewRow({ } className={cn('group/workspace', dragging && 'cursor-grabbing bg-(--ui-sidebar-surface-background)')} + // The label is grab surface too, not just the lead's grabber — same + // listeners, minus the controls that keep their own gestures. A project + // row has no rival drag (its title navigates on CLICK), so the sortable + // owns the press outright. + {...dragHandleProps} + onPointerDown={event => { + if ((event.target as HTMLElement).closest('[data-reorder-handle], [data-row-actions]')) { + return + } + + dragHandleProps?.onPointerDown?.(event) + }} ref={rowRef} > @@ -141,6 +153,7 @@ export function ProjectOverviewRow({