diff --git a/src/renderer/src/components/sidebar/WorktreeTitleInlineRename.lifecycle.test.tsx b/src/renderer/src/components/sidebar/WorktreeTitleInlineRename.lifecycle.test.tsx index 3042f97b8b7..362d44891c6 100644 --- a/src/renderer/src/components/sidebar/WorktreeTitleInlineRename.lifecycle.test.tsx +++ b/src/renderer/src/components/sidebar/WorktreeTitleInlineRename.lifecycle.test.tsx @@ -26,8 +26,6 @@ describe('WorktreeTitleInlineRename lifecycle', () => { root = null container?.remove() container = null - document.documentElement.removeAttribute('data-worktree-title-rename-cursor-lock') - document.getElementById('worktree-title-rename-cursor-lock-style')?.remove() }) it('reports editing=false if it unmounts while renaming', () => { @@ -117,7 +115,8 @@ describe('WorktreeTitleInlineRename lifecycle', () => { setSelectionRange.mockRestore() }) - it('selects the full title when hovercard field mode starts editing', () => { + it('places the caret at the end when hovercard field mode starts editing', () => { + const displayName = 'Feature workspace' const select = vi.spyOn(HTMLInputElement.prototype, 'select') const setSelectionRange = vi.spyOn(HTMLInputElement.prototype, 'setSelectionRange') const nextContainer = document.createElement('div') @@ -127,7 +126,7 @@ describe('WorktreeTitleInlineRename lifecycle', () => { act(() => { root?.render( @@ -141,8 +140,11 @@ describe('WorktreeTitleInlineRename lifecycle', () => { }) const input = nextContainer.querySelector('[data-worktree-title-rename-input]') - expect(select).toHaveBeenCalledTimes(1) - expect(setSelectionRange).not.toHaveBeenCalled() + // Why: the hovercard field opens with the caret at the end (no selection) so a + // pointer parked over the title never flips between the I-beam and the macOS + // drag-selection arrow. + expect(select).not.toHaveBeenCalled() + expect(setSelectionRange).toHaveBeenCalledWith(displayName.length, displayName.length) expect(input?.className).toContain('select-text') expect(input?.className).not.toContain('select-none') @@ -150,52 +152,6 @@ describe('WorktreeTitleInlineRename lifecycle', () => { setSelectionRange.mockRestore() }) - it('locks the document cursor only while the hovercard field editor is active', () => { - const nextContainer = document.createElement('div') - container = nextContainer - root = createRoot(nextContainer) - - act(() => { - root?.render( - - ) - }) - - expect(document.documentElement.hasAttribute('data-worktree-title-rename-cursor-lock')).toBe( - false - ) - - act(() => { - nextContainer - .querySelector('[data-worktree-title-inline-rename]') - ?.dispatchEvent(new MouseEvent('dblclick', { bubbles: true, cancelable: true })) - }) - - expect(document.documentElement.hasAttribute('data-worktree-title-rename-cursor-lock')).toBe( - true - ) - expect( - document.getElementById('worktree-title-rename-cursor-lock-style')?.textContent - ).toContain('cursor: text !important') - - act(() => { - nextContainer - .querySelector('[data-worktree-title-rename-input]') - ?.dispatchEvent( - new KeyboardEvent('keydown', { key: 'Escape', bubbles: true, cancelable: true }) - ) - }) - - expect(document.documentElement.hasAttribute('data-worktree-title-rename-cursor-lock')).toBe( - false - ) - expect(document.getElementById('worktree-title-rename-cursor-lock-style')).toBeNull() - }) - it('renders wrapped read text before mounting the hovercard field input', () => { const nextContainer = document.createElement('div') container = nextContainer diff --git a/src/renderer/src/components/sidebar/WorktreeTitleInlineRename.tsx b/src/renderer/src/components/sidebar/WorktreeTitleInlineRename.tsx index 8c5de69426f..e20312da6a9 100644 --- a/src/renderer/src/components/sidebar/WorktreeTitleInlineRename.tsx +++ b/src/renderer/src/components/sidebar/WorktreeTitleInlineRename.tsx @@ -1,4 +1,4 @@ -import React, { useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react' +import React, { useCallback, useEffect, useRef, useState } from 'react' import { LoaderCircle } from 'lucide-react' import { toast } from 'sonner' import { Input } from '@/components/ui/input' @@ -25,50 +25,6 @@ export function isWorktreeTitleTruncated( return element.scrollWidth > element.clientWidth } -const FIELD_RENAME_CURSOR_LOCK_ATTR = 'data-worktree-title-rename-cursor-lock' -const FIELD_RENAME_CURSOR_LOCK_STYLE_ID = 'worktree-title-rename-cursor-lock-style' -const useCursorLockEffect = typeof window === 'undefined' ? useEffect : useLayoutEffect - -let fieldRenameCursorLockCount = 0 - -function acquireFieldRenameCursorLock(): () => void { - if (typeof document === 'undefined') { - return () => {} - } - - fieldRenameCursorLockCount += 1 - document.documentElement.setAttribute(FIELD_RENAME_CURSOR_LOCK_ATTR, '') - - if (!document.getElementById(FIELD_RENAME_CURSOR_LOCK_STYLE_ID)) { - const style = document.createElement('style') - style.id = FIELD_RENAME_CURSOR_LOCK_STYLE_ID - // Why: Chromium can leave the native cursor stale after a stationary pointer - // swaps from wrapped text to an input. Lock cursor only while field editing. - style.textContent = ` -html[${FIELD_RENAME_CURSOR_LOCK_ATTR}], -html[${FIELD_RENAME_CURSOR_LOCK_ATTR}] body, -html[${FIELD_RENAME_CURSOR_LOCK_ATTR}] body * { - cursor: text !important; -} -` - document.head.append(style) - } - - let released = false - return () => { - if (released) { - return - } - released = true - fieldRenameCursorLockCount = Math.max(0, fieldRenameCursorLockCount - 1) - if (fieldRenameCursorLockCount > 0) { - return - } - document.documentElement.removeAttribute(FIELD_RENAME_CURSOR_LOCK_ATTR) - document.getElementById(FIELD_RENAME_CURSOR_LOCK_STYLE_ID)?.remove() - } -} - type WorktreeTitleInlineRenameProps = { displayName: string disabled?: boolean @@ -87,8 +43,9 @@ type WorktreeTitleInlineRenameProps = { // onBeginEditingConsumed so the request fires exactly once. beginEditing?: boolean onBeginEditingConsumed?: () => void - // Why: parent-triggered rename can place the caret without selecting, while - // double-click rename keeps the native one-keystroke replacement behavior. + // Why: the inline text row selects all for a one-keystroke replace; the hovercard + // field defaults to caret-at-end. A pointer parked over a fresh selection makes + // macOS flip between the I-beam and the drag-selection arrow. Callers can override. selectOnFocus?: boolean } @@ -107,7 +64,7 @@ export function WorktreeTitleInlineRename({ onRename, beginEditing = false, onBeginEditingConsumed, - selectOnFocus = true + selectOnFocus = editingPresentation !== 'field' }: WorktreeTitleInlineRenameProps): React.JSX.Element { const editingRef = useRef(false) const savingRef = useRef(false) @@ -185,13 +142,6 @@ export function WorktreeTitleInlineRename({ const savingInputClassName = editingPresentation === 'field' ? 'pr-6' : 'pr-4' const savingSpinnerClassName = editingPresentation === 'field' ? 'right-1.5' : 'right-0' - useCursorLockEffect(() => { - if (!editing || editingPresentation !== 'field') { - return - } - return acquireFieldRenameCursorLock() - }, [editing, editingPresentation]) - const setEditingMode = useCallback( (nextEditing: boolean) => { if (editingRef.current === nextEditing) {