From c7eeef4e8cb8f94fed3c6e4922ab185a1f300d34 Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Tue, 24 Mar 2026 19:03:15 -0700 Subject: [PATCH] feat: add worktree rename (display name) (#90) Co-authored-by: Claude Opus 4.6 --- .../src/components/sidebar/WorktreeCard.tsx | 3 +- .../sidebar/WorktreeContextMenu.tsx | 32 +++++++++++++-- .../components/sidebar/WorktreeMetaDialog.tsx | 41 +++++++++++++++++-- 3 files changed, 69 insertions(+), 7 deletions(-) diff --git a/src/renderer/src/components/sidebar/WorktreeCard.tsx b/src/renderer/src/components/sidebar/WorktreeCard.tsx index 4590551adb5..89bcfb6feb5 100644 --- a/src/renderer/src/components/sidebar/WorktreeCard.tsx +++ b/src/renderer/src/components/sidebar/WorktreeCard.tsx @@ -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) => { diff --git a/src/renderer/src/components/sidebar/WorktreeContextMenu.tsx b/src/renderer/src/components/sidebar/WorktreeContextMenu.tsx index 30773d8edbc..124a68a0a8f 100644 --- a/src/renderer/src/components/sidebar/WorktreeContextMenu.tsx +++ b/src/renderer/src/components/sidebar/WorktreeContextMenu.tsx @@ -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 + + + Rename + {worktree.isUnread ? : } {worktree.isUnread ? 'Mark Read' : 'Mark Unread'} diff --git a/src/renderer/src/components/sidebar/WorktreeMetaDialog.tsx b/src/renderer/src/components/sidebar/WorktreeMetaDialog.tsx index 27db62c14d1..da759939b72 100644 --- a/src/renderer/src/components/sidebar/WorktreeMetaDialog.tsx +++ b/src/renderer/src/components/sidebar/WorktreeMetaDialog.tsx @@ -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(null) const textareaRef = useRef(null) const prevIsOpenRef = useRef(false) + const displayNameInputRef = useRef(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 = { - 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) => { @@ -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() {
+
+ + setDisplayNameInput(e.target.value)} + onKeyDown={handleIssueKeyDown} + placeholder="Custom display name..." + className="h-8 text-xs" + /> +

+ Only changes the name shown in the sidebar — the folder on disk stays the same. Leave + blank to use the branch or folder name. +

+
+