From 4c65b42ee274cd293efd965363e4b06d07bc9f4e Mon Sep 17 00:00:00 2001
From: Jinjing <6427696+AmethystLiang@users.noreply.github.com>
Date: Wed, 29 Jul 2026 17:03:58 -0700
Subject: [PATCH] fix(sidebar): move project header grab cursor to title
surface only (#11435)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* fix(sidebar): move project header grab cursor to title surface only
Prevent grab cursor appearing over action buttons (…, +, chevron) which
should show cursor-pointer, not the reorder hand.
- Grab cursor scoped to icon + label surface only
- Row retains data-repo-header-drag-handle for indent/padding drag targets
- Actions excluded via [data-repo-header-actions] selector
- Add lockstep test to keep action selectors synchronized
* fix(sidebar): share project header action selector across drag contracts
Address Greptile feedback: drop the format-sensitive regex lockstep parse and
import one shared REPO_HEADER_ACTION_SELECTOR for repo and group headers.
---
.../sidebar/ProjectHeaderActions.test.tsx | 4 +
.../sidebar/ProjectHeaderActions.tsx | 5 +-
.../src/components/sidebar/WorktreeList.tsx | 82 ++++++-----
.../sidebar/project-group-header-dom.test.ts | 25 ++++
.../project-group-header-drag-contract.ts | 8 +-
.../project-group-header-drag-start.test.ts | 135 ++++++++++++++++++
.../sidebar/project-group-header-drag.test.ts | 54 +++++++
...ct-header-action-selector-lockstep.test.ts | 25 ++++
.../sidebar/project-header-drag-contract.ts | 5 +-
.../sidebar/project-header-drag-start.test.ts | 68 +++++++++
.../sidebar/project-header-drag.test.ts | 13 ++
11 files changed, 380 insertions(+), 44 deletions(-)
create mode 100644 src/renderer/src/components/sidebar/project-group-header-drag.test.ts
create mode 100644 src/renderer/src/components/sidebar/project-header-action-selector-lockstep.test.ts
diff --git a/src/renderer/src/components/sidebar/ProjectHeaderActions.test.tsx b/src/renderer/src/components/sidebar/ProjectHeaderActions.test.tsx
index dc41dfef2..eab571774 100644
--- a/src/renderer/src/components/sidebar/ProjectHeaderActions.test.tsx
+++ b/src/renderer/src/components/sidebar/ProjectHeaderActions.test.tsx
@@ -35,5 +35,9 @@ describe('ProjectHeaderActions', () => {
expect(actions?.className).toContain('group-hover:pointer-events-auto')
expect(actions?.className).toContain('has-[:focus-visible]:pointer-events-auto')
expect(actions?.className).toContain('has-[button[data-state=open]]:pointer-events-auto')
+ // Why: title drag surface is cursor-grab; actions must override so hovering
+ // … / + never shows the reorder hand.
+ expect(actions?.className).toContain('cursor-pointer')
+ expect(actions?.className).toContain('self-stretch')
})
})
diff --git a/src/renderer/src/components/sidebar/ProjectHeaderActions.tsx b/src/renderer/src/components/sidebar/ProjectHeaderActions.tsx
index 681f3cb21..158d0e1c2 100644
--- a/src/renderer/src/components/sidebar/ProjectHeaderActions.tsx
+++ b/src/renderer/src/components/sidebar/ProjectHeaderActions.tsx
@@ -3,8 +3,11 @@ import { cn } from '@/lib/utils'
// Why: desktop hover actions should not permanently reserve project-title width;
// touch devices keep them in normal flow because there is no hover reveal.
+// Why: title surface uses cursor-grab; force pointer here so … / + / chevron
+// (and gaps) never show the reorder hand. self-stretch fills the row height in
+// flow so the gutter beside buttons is still an action hit target, not a drag arm.
export const PROJECT_HEADER_ACTIONS_CLASS_NAME = cn(
- 'flex shrink-0 items-center gap-0.5',
+ 'flex shrink-0 cursor-pointer items-center gap-0.5 self-stretch',
'can-hover:absolute can-hover:right-1 can-hover:top-1/2 can-hover:z-10 can-hover:-translate-y-1/2',
'can-hover:rounded-md can-hover:bg-worktree-sidebar can-hover:pl-1',
'can-hover:pointer-events-none can-hover:opacity-0 can-hover:transition-opacity',
diff --git a/src/renderer/src/components/sidebar/WorktreeList.tsx b/src/renderer/src/components/sidebar/WorktreeList.tsx
index f0db0c6fb..2abe607a1 100644
--- a/src/renderer/src/components/sidebar/WorktreeList.tsx
+++ b/src/renderer/src/components/sidebar/WorktreeList.tsx
@@ -4254,6 +4254,8 @@ const VirtualizedWorktreeViewport = React.memo(function VirtualizedWorktreeViewp
? repoHeaderSectionEndByRepoId.get(projectIdForHeader)
: undefined
}
+ // Why: row keeps handle attrs so indent/padding still arms drag; grab
+ // cursor lives only on the title surface so … / + never inherit it.
data-repo-header-drag-handle={isDraggableRepoHeader ? '' : undefined}
data-project-group-header-id={projectGroupIdForHeader}
data-project-group-header-index={projectGroupHeaderIndex}
@@ -4270,10 +4272,10 @@ const VirtualizedWorktreeViewport = React.memo(function VirtualizedWorktreeViewp
data-workspace-status={headerWorkspaceStatus ?? undefined}
data-workspace-pin-drop-target={isPinnedHeader ? '' : undefined}
className={cn(
+ // Why: no row-level grab — only the title surface below shows the hand;
+ // actions use cursor-pointer so … / + never look reorderable.
'group relative flex h-7 w-full items-center gap-1.5 pr-2 text-left transition-all',
- isDraggableRepoHeader || isDraggableProjectGroupHeader
- ? 'cursor-grab active:cursor-grabbing'
- : 'cursor-pointer',
+ !(isDraggableRepoHeader || isDraggableProjectGroupHeader) && 'cursor-pointer',
highlightedRevealRowKey === row.key &&
'rounded-md bg-worktree-sidebar-accent ring-1 ring-worktree-sidebar-ring/50',
(isDraggingThis || isDraggingThisProjectGroup) &&
@@ -4330,39 +4332,48 @@ const VirtualizedWorktreeViewport = React.memo(function VirtualizedWorktreeViewp
}
}}
>
- {row.icon ? (
-
- {row.repo ? (
-
- ) : (
-
- )}
-
- ) : null}
-
-
-
-
- {row.label}
+ {/* Why: grab cursor on icon+title only. Row still has handle attrs so
+ indent/padding can arm drag; actions are excluded via data-repo-header-actions.
+ self-stretch fills h-7 so grab matches the full title column height. */}
+