feat(sidebar): group Copy path/branch/PR-MR URL in the worktree row menu - #12240
feat(sidebar): group Copy path/branch/PR-MR URL in the worktree row menu#12240nwparker wants to merge 2 commits into
Conversation
…h/PR items Co-authored-by: Orca <help@stably.ai>
…enu (#6167) The worktree row context menu only offered "Copy Path". Add a "Copy" group with Copy Path, Copy branch name, and Copy PR/MR URL. - Labels are provider-neutral: GitLab reads "Copy MR URL", every other supported provider reads "Copy PR URL". - The owning WorktreeCard passes the branch and the review it already resolved, so the menu can never disagree with the badge on the row and no new per-row store subscription is added. A standalone menu falls back to the worktree's linked review metadata for the label only. - Inapplicable items render disabled with a reason ("No branch" on detached HEAD, "Not linked" until a review URL resolves) instead of copying a synthesized URL that could 404 on forks or self-hosted instances. - Clipboard writes now report success/failure instead of leaving an unhandled rejection when web-runtime clipboard activation is lost. Flat items rather than a submenu: Radix portals sub-content outside the parent [role="menu"], which the committed e2e repro scopes to. Both committed repros keep their original assertions.
|
Warning Review limit reached
Next review available in: 20 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (8)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Description
The sidebar worktree row context menu only offered a single loose Copy Path item. There was no way to copy the worktree's branch name or the URL of its linked pull request / merge request without leaving the menu and hunting elsewhere in the UI.
This adds a labelled Copy section to that menu with three actions:
getWorktreeGitIdentityDisplay, so detached HEAD and folder workspaces disable the item with a "No branch" hint instead of copying an empty stringgitlab→ MR, everything else → PR), not by a synthesized URL. Disabled with a "Not linked" hint when no review URL is available.The new copy logic lives in a small pure module,
src/renderer/src/components/sidebar/worktree-copy-targets.ts, so it is unit-testable independently of the menu.Two incidental improvements came along:
handleCopyPathpreviously fired a floating promise. A clipboard failure produced an unhandled rejection with zero user feedback. All three actions now route throughannounceClipboardWrite, which catches and surfaces a toast.translate()and added toen.json.Fixes #6167
ELI5
Right-clicking a worktree in the sidebar used to give you exactly one copy option: the folder path. If you wanted the branch name or the link to its pull request, you had to go find them somewhere else.
Now that right-click menu has a little "Copy" group with three choices: the path, the branch name, and the link to the pull request (or "merge request" if you're on GitLab — it uses the right word for wherever your code lives). If a worktree doesn't have a branch or isn't linked to a pull request yet, that choice is greyed out with a short note saying why, instead of silently copying nothing.
Also: if copying to the clipboard ever fails, you now get a little message telling you. Before, it just quietly did nothing.
Fix proof
Test file:
src/renderer/src/components/sidebar/WorktreeContextMenu.copy-actions.test.tsx(This is the committed, independently-adjudicated repro from
ff09527c61. It was renamed fromrepro-6167-worktree-copy-submenu.test.tsx;git show -Mreports 94% similarity, the only delta being a 3-line docblock — everyexpectis byte-identical.)BEFORE — source files reverted to their pre-fix content at
ff09527c61, repro re-run:AFTER — at
d09b196be6:The e2e repro
tests/e2e/repro-6167-worktree-copy-menu.spec.ts(assertscopyItems.length >= 3against the live DOM) likewise fails on a pre-fix build and passes at HEAD: 1 passed (17.8s).Visual proof
Real Playwright + Electron captures, not mockups. Both runs used an identical 1400×900 viewport, the same seeded worktree row, and the same
[role="menu"]crop, so they are directly comparable. BEFORE was produced bygit checkout ff09527c61 -- src/plus a fullelectron-vite build --mode e2erebuild.BEFORE — one loose
Copy Pathsitting betweenOpen inandPin:AFTER — labelled
Copysection with all three actions.Copy PR URLis disabled with aNot linkedhint because the e2e fixture repo has no linked review:AFTER, GitLab worktree — same group, third item reads
Copy MR URLand is enabled. This is the provider-neutrality proof:Full-window context shots (1400×900)
BEFORE:
AFTER:
DOM label dumps taken alongside the screenshots:
copyItemsin the live menu["Copy Path"]["Copy Path", "Copy branch name", "Copy PR URL\nNot linked"]["Copy Path", "Copy branch name", "Copy MR URL"]No video — this change is static menu content, not motion- or interaction-dependent.
Regressions & trade-offs
0 regressions found. Every command below was actually run on this branch; all passed with zero failures, and no pre-existing failures had to be excused.
vitest run src/renderer/src/components/sidebar/vitest run src/renderer/src/components/right-sidebar/vitest run src/renderer/src/i18n/viteston the 14 dependent-module tests (WorktreeCard.*,WorktreeList.card-memo-stability,worktree-card-pr-display, …)tsc --noEmit -p config/tsconfig.tc.web.jsonoxlinton all changed files;oxlint --type-aware --deny-warnings src config testsoxlint --config config/oxlint-react-doctor.jsoncheck-max-lines-ratchet.mjsmax-linesdisable addedverify-localization-catalog.mjs/verify-localization-extraction.mjs/audit-localization-coverage.mjs --checkcheck-reliability-gates.mjselectron-vite build --mode e2eplaywright tests/e2e/repro-6167-worktree-copy-menu.spec.tsplaywright tests/e2e/worktree-lineage.spec.ts(only other spec that clicks this menu)Behaviour-preservation checks done by reading, not just running:
!isMultiContextblock that already heldCopy Path, so multi-select behaviour is untouched.getByRole('menuitem', { name: … })) — grep confirms nonth()indexing on menu items, so the inserted label and separator cannot shift anything.useAppStore/ Zustand subscriptions.check:zustand-selector-fanoutpasses: 2500 subscribers × 2000 unrelated writes, median 34.46 ms, 0 render invalidations.Trade-offs and user-visible changes — disclosed
DropdownMenuSubContentoutside[role="menu"], which is structurally incompatible with the committed e2e repro's locator, and nesting would have pushed the most-used action (Copy Path) one hover deeper. I shipped a labelledCopysection instead: behaviour-equivalent, one less pointer step, e2e repro untouched. It does read slightly oddly as "Copy › Copy Path", and it adds 3 always-visible rows to an already long menu.Copy branch name/Copy PR URLrender greyed out with a reason chip rather than being hidden. Hiding them would make the e2e's>= 3count unsatisfiable on a fixture worktree.WorktreeContextMenu.copy-actions.test.tsx:152asserts by regexing the component's own source for>= 3occurrences ofwriteClipboardText. That is brittle and non-behavioural, but it is the adjudicated repro and I did not weaken it. The real behavioural coverage lives inWorktreeContextMenu.copy-targets.test.tsx.en.jsonreceived the 12 new keys;es/ja/ko/zhdid not. This matches the repo's existing state rather than introducing a gap — the neighbouringdeleteWithDescendantskey is also en-only, andverify-localization-catalog.mjstolerates it (basef3e087ec06already reported 147 missing per locale; HEAD reports 159, exactly +12).translate(key, fallback)supplies the English default, so nothing breaks.branchName={branch}is redundant —getWorktreeCopyTargetsderives an identical value itself. It also bypasses the card'sisFoldergate, so a folder workspace on a git checkout offers "Copy branch name" even though the row hides the branch. Arguably the more useful behaviour, but it contradicts the prop's own docstring.Perf
Verdict: NO REGRESSION. Measured, not assumed.
Only two things run outside a menu-open event:
WorktreeCardpasses two new props, andWorktreeContextMenugains oneuseMemoplus twouseCallbacks. Everything else (the label, the two new items, the toast, the clipboard IPC) lives insideDropdownMenuContent, which is wrapped in a RadixPortalwith noforceMount— so it is unmounted when closed and costs exactly zero per row, paid once per right-click.WorktreeListis@tanstack/react-virtual-virtualized, so real cost is bounded by ~20–40 visible rows ≈ 1.6 µs per sidebar render. Below noise.React.memofan-out from the unmemoizedreview={hoverReview}prop — was investigated with a throwaway happy-dom test replicating the exact shape (memo'd child, inlinechildren, unstablereviewliteral): 2 parent renders → 2 child renders, identical with and without the prop. Delta = 0. The memo was already inert becausechildren={cardBody}is a fresh element every render.announceClipboardWriteis invoked viavoid, soonSelectreturns immediately and Radix closes the menu without awaiting. The IPC round-trip only gates when the toast appears.GitBranch+Link2fromlucide-react(tree-shaken),sonneralready in the sidebar graph, plus a 57-line pure module importing only files already in that graph.Release scan
Verdict: PASS-WITH-NOTES — no P0, no P1. Ship-safe.
Span
f3e087ec06→d09b196be6: 8 files, +790/−3, renderer-only. Nopackage.json/ lockfile / native / IPC / persisted-state / migration / mobile changes, so there is no startup, data-loss, or supply-chain exposure. All checklist areas cleared: SSH/remote (no new network surface; remote paths copy correctly), crash/cast/retry/growth (no new casts, non-null assertions,JSON.parse, or timers — and the change fixes a latent unhandled rejection), security (no new deps, no shell/eval/network; clipboard payloads are written, never logged or parsed), backcompat (nothing persisted; i18n purely additive), cross-platform (nometaKey, no path separators, no OS branching; the existing Electron clipboard IPC has a browser fallback inweb-preload-api.ts, so desktop and paired-web both work).Three P2 follow-ups, all safe to land later:
getWorktreeCardPrDisplayreturns a fallback display with anumberbut nourlon cold start / offline / rate-limited / polling-disabled.worktree-copy-targets.ts:55maps that toreviewUrl: null, so the row says "Not linked", which is factually wrong. Should say "Loading…" or derive the URL fromrepo.gitRemoteIdentity+ number.worktree-copy-targets.ts:54derives the label from anullreview. The repo already has the provider-aware, localized answer —localizedHostedReviewCopy(resolveSupportedHostedReviewCopyProvider(repo?.gitRemoteIdentity?.provider))— andrepois already in scope. This is the one worth taking first, since AGENTS.md calls provider neutrality out explicitly and the fix is a two-line swap. The new code also duplicates the existinggetReviewLabelinworktree-review-helpers.tsx:7.review={hoverReview}is unmemoized (WorktreeCard.tsx:1936, fromprDisplayat:535). Harmless today because the memo was already inert (see Perf), but memoizingprDisplaywould benefit every other consumer and avoid re-arming this footgun.Styling conforms:
text-[11px] text-muted-foregroundfor trailing metadata is explicitly sanctioned atdocs/STYLEGUIDE.md:220, the newDropdownMenuLabelcopies the pre-existing "Workspace" label in the same file verbatim, andsize-3.5matches every sibling menu icon. No new tokens, colours, font sizes, or shadow tiers invented.Review loop
Reviewed 1 time until clean — verdict: "Merge-ready. The repro's assertions are provably unchanged (rename + docblock only), it genuinely fails at the pre-fix source and passes at HEAD, and I independently ran the unit tests (1857 sidebar tests pass), the Playwright e2e repro (passes), oxlint,
tscon the web project, all three localization gates, and the max-lines ratchet — all green, working tree clean. The implementation is correct on the paths I could exercise: path copied verbatim (Windows/SSH-safe, no path assumptions), provider-neutral MR/PR labeling driven by the resolved review rather than a synthesized URL, detached-HEAD and folder-workspace degradation to a disabled item, noe.metaKeyhardcoding, no invented tokens, no max-lines bypass. A live screenshot confirms the rendered menu is visually correct with no truncation. The remaining items are polish and reuse concerns, not merge blockers."No unresolved blocking findings.
Credits
No code was cherry-picked. The earlier stale PR #6728 attempted this issue but lacked provider-neutrality coverage (it had no GitLab MR test); this implementation was written fresh against the committed repro, and that gap is now covered by
WorktreeContextMenu.copy-targets.test.tsxand the GitLab screenshot above.Made with Orca 🐋