feat: add worktree rename (display name) (#90)

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Neil
2026-03-24 19:03:15 -07:00
committed by GitHub
co-authored by Claude Opus 4.6
parent 556f31abcd
commit c7eeef4e8c
3 changed files with 69 additions and 7 deletions
@@ -166,10 +166,11 @@ const WorktreeCard = React.memo(function WorktreeCard({
const handleDoubleClick = useCallback(() => {
openModal('edit-meta', {
worktreeId: worktree.id,
currentDisplayName: worktree.displayName,
currentIssue: worktree.linkedIssue,
currentComment: worktree.comment
})
}, [worktree.id, worktree.linkedIssue, worktree.comment, openModal])
}, [worktree.id, worktree.displayName, worktree.linkedIssue, worktree.comment, openModal])
const handleToggleUnreadQuick = useCallback(
(event: React.MouseEvent<HTMLButtonElement>) => {
@@ -6,7 +6,17 @@ import {
DropdownMenuSeparator,
DropdownMenuTrigger
} from '@/components/ui/dropdown-menu'
import { FolderOpen, Copy, Bell, BellOff, Link, MessageSquare, XCircle, Trash2 } from 'lucide-react'
import {
FolderOpen,
Copy,
Bell,
BellOff,
Link,
MessageSquare,
Pencil,
XCircle,
Trash2
} from 'lucide-react'
import { useAppStore } from '@/store'
import type { Worktree } from '../../../../shared/types'
@@ -47,23 +57,35 @@ const WorktreeContextMenu = React.memo(function WorktreeContextMenu({ worktree,
updateWorktreeMeta(worktree.id, { isUnread: !worktree.isUnread })
}, [worktree.id, worktree.isUnread, updateWorktreeMeta])
const handleRename = useCallback(() => {
openModal('edit-meta', {
worktreeId: worktree.id,
currentDisplayName: worktree.displayName,
currentIssue: worktree.linkedIssue,
currentComment: worktree.comment,
focus: 'displayName'
})
}, [worktree.id, worktree.displayName, worktree.linkedIssue, worktree.comment, openModal])
const handleLinkIssue = useCallback(() => {
openModal('edit-meta', {
worktreeId: worktree.id,
currentDisplayName: worktree.displayName,
currentIssue: worktree.linkedIssue,
currentComment: worktree.comment,
focus: 'issue'
})
}, [worktree.id, worktree.linkedIssue, worktree.comment, openModal])
}, [worktree.id, worktree.displayName, worktree.linkedIssue, worktree.comment, openModal])
const handleComment = useCallback(() => {
openModal('edit-meta', {
worktreeId: worktree.id,
currentDisplayName: worktree.displayName,
currentIssue: worktree.linkedIssue,
currentComment: worktree.comment,
focus: 'comment'
})
}, [worktree.id, worktree.linkedIssue, worktree.comment, openModal])
}, [worktree.id, worktree.displayName, worktree.linkedIssue, worktree.comment, openModal])
const handleCloseTerminals = useCallback(async () => {
await shutdownWorktreeTerminals(worktree.id)
@@ -111,6 +133,10 @@ const WorktreeContextMenu = React.memo(function WorktreeContextMenu({ worktree,
Copy Path
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem onSelect={handleRename} disabled={isDeleting}>
<Pencil className="size-3.5" />
Rename
</DropdownMenuItem>
<DropdownMenuItem onSelect={handleToggleRead} disabled={isDeleting}>
{worktree.isUnread ? <BellOff className="size-3.5" /> : <Bell className="size-3.5" />}
{worktree.isUnread ? 'Mark Read' : 'Mark Unread'}
@@ -23,12 +23,15 @@ const WorktreeMetaDialog = React.memo(function WorktreeMetaDialog() {
const isOpen = isEditMeta
const worktreeId = typeof modalData.worktreeId === 'string' ? modalData.worktreeId : ''
const currentDisplayName =
typeof modalData.currentDisplayName === 'string' ? modalData.currentDisplayName : ''
const currentIssue =
typeof modalData.currentIssue === 'number' ? String(modalData.currentIssue) : ''
const currentComment =
typeof modalData.currentComment === 'string' ? modalData.currentComment : ''
const focusField = typeof modalData.focus === 'string' ? modalData.focus : 'comment'
const [displayNameInput, setDisplayNameInput] = useState('')
const [issueInput, setIssueInput] = useState('')
const [commentInput, setCommentInput] = useState('')
const [saving, setSaving] = useState(false)
@@ -36,7 +39,9 @@ const WorktreeMetaDialog = React.memo(function WorktreeMetaDialog() {
const issueInputRef = useRef<HTMLInputElement>(null)
const textareaRef = useRef<HTMLTextAreaElement>(null)
const prevIsOpenRef = useRef(false)
const displayNameInputRef = useRef<HTMLInputElement>(null)
if (isOpen && !prevIsOpenRef.current) {
setDisplayNameInput(currentDisplayName)
setIssueInput(currentIssue)
setCommentInput(currentComment)
}
@@ -84,8 +89,12 @@ const WorktreeMetaDialog = React.memo(function WorktreeMetaDialog() {
const finalLinkedIssue =
trimmedIssue === '' ? null : linkedIssueNumber !== null ? linkedIssueNumber : undefined
const trimmedDisplayName = displayNameInput.trim()
const updates: Partial<WorktreeMeta> = {
comment: commentInput.trim()
comment: commentInput.trim(),
...(trimmedDisplayName !== currentDisplayName && {
displayName: trimmedDisplayName || undefined
})
}
if (finalLinkedIssue !== undefined) {
updates.linkedIssue = finalLinkedIssue
@@ -96,7 +105,15 @@ const WorktreeMetaDialog = React.memo(function WorktreeMetaDialog() {
} finally {
setSaving(false)
}
}, [worktreeId, issueInput, commentInput, updateWorktreeMeta, closeModal])
}, [
worktreeId,
displayNameInput,
currentDisplayName,
issueInput,
commentInput,
updateWorktreeMeta,
closeModal
])
const handleCommentKeyDown = useCallback(
(e: React.KeyboardEvent<HTMLTextAreaElement>) => {
@@ -124,7 +141,9 @@ const WorktreeMetaDialog = React.memo(function WorktreeMetaDialog() {
className="max-w-md"
onOpenAutoFocus={(e) => {
e.preventDefault()
if (focusField === 'issue') {
if (focusField === 'displayName') {
displayNameInputRef.current?.focus()
} else if (focusField === 'issue') {
issueInputRef.current?.focus()
} else {
textareaRef.current?.focus()
@@ -139,6 +158,22 @@ const WorktreeMetaDialog = React.memo(function WorktreeMetaDialog() {
</DialogHeader>
<div className="space-y-4">
<div className="space-y-1">
<label className="text-[11px] font-medium text-muted-foreground">Display Name</label>
<Input
ref={displayNameInputRef}
value={displayNameInput}
onChange={(e) => setDisplayNameInput(e.target.value)}
onKeyDown={handleIssueKeyDown}
placeholder="Custom display name..."
className="h-8 text-xs"
/>
<p className="text-[10px] text-muted-foreground">
Only changes the name shown in the sidebar — the folder on disk stays the same. Leave
blank to use the branch or folder name.
</p>
</div>
<div className="space-y-1">
<label className="text-[11px] font-medium text-muted-foreground">GH Issue / PR</label>
<Input