From 417a49bc1b542434af12188ea69f23946c3834a0 Mon Sep 17 00:00:00 2001 From: Jinjing <6427696+AmethystLiang@users.noreply.github.com> Date: Mon, 23 Mar 2026 23:06:44 -0700 Subject: [PATCH 1/4] feat: unify worktree meta editing into single dialog with double-click support (#76) Merge the separate "Link Issue" and "Edit Comment" modals into a single "Edit Worktree Details" dialog. Add double-click on worktree cards to open the unified editor. Context menu items now focus the relevant field. Co-authored-by: Claude Opus 4.6 --- .../src/components/sidebar/WorktreeCard.tsx | 10 ++ .../sidebar/WorktreeContextMenu.tsx | 18 +++- .../components/sidebar/WorktreeMetaDialog.tsx | 91 +++++++++++-------- src/renderer/src/store/slices/ui.ts | 7 +- 4 files changed, 78 insertions(+), 48 deletions(-) diff --git a/src/renderer/src/components/sidebar/WorktreeCard.tsx b/src/renderer/src/components/sidebar/WorktreeCard.tsx index 06aceba32e8..2e78a271a80 100644 --- a/src/renderer/src/components/sidebar/WorktreeCard.tsx +++ b/src/renderer/src/components/sidebar/WorktreeCard.tsx @@ -75,6 +75,7 @@ const WorktreeCard = React.memo(function WorktreeCard({ isActive }: WorktreeCardProps) { const setActiveWorktree = useAppStore((s) => s.setActiveWorktree) + const openModal = useAppStore((s) => s.openModal) const updateWorktreeMeta = useAppStore((s) => s.updateWorktreeMeta) const fetchPRForBranch = useAppStore((s) => s.fetchPRForBranch) const fetchIssue = useAppStore((s) => s.fetchIssue) @@ -150,6 +151,14 @@ const WorktreeCard = React.memo(function WorktreeCard({ setActiveWorktree(worktree.id) }, [worktree.id, setActiveWorktree]) + const handleDoubleClick = useCallback(() => { + openModal('edit-meta', { + worktreeId: worktree.id, + currentIssue: worktree.linkedIssue, + currentComment: worktree.comment + }) + }, [worktree.id, worktree.linkedIssue, worktree.comment, openModal]) + const handleToggleUnreadQuick = useCallback( (event: React.MouseEvent) => { event.preventDefault() @@ -172,6 +181,7 @@ const WorktreeCard = React.memo(function WorktreeCard({ isDeleting && 'opacity-70' )} onClick={handleClick} + onDoubleClick={handleDoubleClick} aria-busy={isDeleting} > {isDeleting && ( diff --git a/src/renderer/src/components/sidebar/WorktreeContextMenu.tsx b/src/renderer/src/components/sidebar/WorktreeContextMenu.tsx index 165705e5066..e319d04225e 100644 --- a/src/renderer/src/components/sidebar/WorktreeContextMenu.tsx +++ b/src/renderer/src/components/sidebar/WorktreeContextMenu.tsx @@ -48,12 +48,22 @@ const WorktreeContextMenu = React.memo(function WorktreeContextMenu({ worktree, }, [worktree.id, worktree.isUnread, updateWorktreeMeta]) const handleLinkIssue = useCallback(() => { - openModal('link-issue', { worktreeId: worktree.id, currentIssue: worktree.linkedIssue }) - }, [worktree.id, worktree.linkedIssue, openModal]) + openModal('edit-meta', { + worktreeId: worktree.id, + currentIssue: worktree.linkedIssue, + currentComment: worktree.comment, + focus: 'issue' + }) + }, [worktree.id, worktree.linkedIssue, worktree.comment, openModal]) const handleComment = useCallback(() => { - openModal('edit-comment', { worktreeId: worktree.id, currentComment: worktree.comment }) - }, [worktree.id, worktree.comment, openModal]) + openModal('edit-meta', { + worktreeId: worktree.id, + currentIssue: worktree.linkedIssue, + currentComment: worktree.comment, + focus: 'comment' + }) + }, [worktree.id, worktree.linkedIssue, worktree.comment, openModal]) const handleCloseTerminals = useCallback(async () => { await shutdownWorktreeTerminals(worktree.id) diff --git a/src/renderer/src/components/sidebar/WorktreeMetaDialog.tsx b/src/renderer/src/components/sidebar/WorktreeMetaDialog.tsx index 8b4889f89e4..27db62c14d1 100644 --- a/src/renderer/src/components/sidebar/WorktreeMetaDialog.tsx +++ b/src/renderer/src/components/sidebar/WorktreeMetaDialog.tsx @@ -11,6 +11,7 @@ import { import { Button } from '@/components/ui/button' import { Input } from '@/components/ui/input' import { parseGitHubIssueOrPRNumber } from '@/lib/github-links' +import type { WorktreeMeta } from '../../../../shared/types' const WorktreeMetaDialog = React.memo(function WorktreeMetaDialog() { const activeModal = useAppStore((s) => s.activeModal) @@ -18,20 +19,21 @@ const WorktreeMetaDialog = React.memo(function WorktreeMetaDialog() { const closeModal = useAppStore((s) => s.closeModal) const updateWorktreeMeta = useAppStore((s) => s.updateWorktreeMeta) - const isLinkIssue = activeModal === 'link-issue' - const isEditComment = activeModal === 'edit-comment' - const isOpen = isLinkIssue || isEditComment + const isEditMeta = activeModal === 'edit-meta' + const isOpen = isEditMeta const worktreeId = typeof modalData.worktreeId === 'string' ? modalData.worktreeId : '' 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 [issueInput, setIssueInput] = useState('') const [commentInput, setCommentInput] = useState('') const [saving, setSaving] = useState(false) + const issueInputRef = useRef(null) const textareaRef = useRef(null) const prevIsOpenRef = useRef(false) if (isOpen && !prevIsOpenRef.current) { @@ -50,20 +52,17 @@ const WorktreeMetaDialog = React.memo(function WorktreeMetaDialog() { }, []) useEffect(() => { - if (isEditComment) { + if (isEditMeta) { autoResize() } - }, [isEditComment, commentInput, autoResize]) + }, [isEditMeta, commentInput, autoResize]) const canSave = useMemo(() => { if (!worktreeId) { return false } - if (isLinkIssue) { - return issueInput.trim() === '' || parseGitHubIssueOrPRNumber(issueInput) !== null - } - return true - }, [worktreeId, isLinkIssue, issueInput]) + return issueInput.trim() === '' || parseGitHubIssueOrPRNumber(issueInput) !== null + }, [worktreeId, issueInput]) const handleOpenChange = useCallback( (open: boolean) => { @@ -80,28 +79,24 @@ const WorktreeMetaDialog = React.memo(function WorktreeMetaDialog() { } setSaving(true) try { - if (isLinkIssue) { - const trimmed = issueInput.trim() - const linkedIssueNumber = parseGitHubIssueOrPRNumber(trimmed) - if (trimmed === '' || linkedIssueNumber !== null) { - await updateWorktreeMeta(worktreeId, { linkedIssue: linkedIssueNumber }) - } - } else if (isEditComment) { - await updateWorktreeMeta(worktreeId, { comment: commentInput.trim() }) + const trimmedIssue = issueInput.trim() + const linkedIssueNumber = parseGitHubIssueOrPRNumber(trimmedIssue) + const finalLinkedIssue = + trimmedIssue === '' ? null : linkedIssueNumber !== null ? linkedIssueNumber : undefined + + const updates: Partial = { + comment: commentInput.trim() } + if (finalLinkedIssue !== undefined) { + updates.linkedIssue = finalLinkedIssue + } + + await updateWorktreeMeta(worktreeId, updates) closeModal() } finally { setSaving(false) } - }, [ - worktreeId, - isLinkIssue, - isEditComment, - issueInput, - commentInput, - updateWorktreeMeta, - closeModal - ]) + }, [worktreeId, issueInput, commentInput, updateWorktreeMeta, closeModal]) const handleCommentKeyDown = useCallback( (e: React.KeyboardEvent) => { @@ -113,35 +108,52 @@ const WorktreeMetaDialog = React.memo(function WorktreeMetaDialog() { [handleSave] ) + const handleIssueKeyDown = useCallback( + (e: React.KeyboardEvent) => { + if (e.key === 'Enter') { + e.preventDefault() + handleSave() + } + }, + [handleSave] + ) + return ( - e.preventDefault()}> + { + e.preventDefault() + if (focusField === 'issue') { + issueInputRef.current?.focus() + } else { + textareaRef.current?.focus() + } + }} + > - - {isLinkIssue ? 'Link GH Issue/PR' : 'Edit Comment'} - + Edit Worktree Details - {isLinkIssue - ? 'Add an issue/PR number or URL to link this worktree. Leave blank to remove the link.' - : 'Add or edit notes for this worktree.'} + Edit the GitHub issue link and notes for this worktree. - {isLinkIssue ? ( +
setIssueInput(e.target.value)} + onKeyDown={handleIssueKeyDown} placeholder="Issue/PR # or GitHub URL" className="h-8 text-xs" - autoFocus />

- Paste an issue or PR URL, or enter a number. + Paste an issue or PR URL, or enter a number. Leave blank to remove the link.

- ) : ( +