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) {