Skip to content

feat(sidebar): group Copy path/branch/PR-MR URL in the worktree row menu - #12240

Open
nwparker wants to merge 2 commits into
mainfrom
nwparker/fix-6167-worktree-copy-submenu
Open

feat(sidebar): group Copy path/branch/PR-MR URL in the worktree row menu#12240
nwparker wants to merge 2 commits into
mainfrom
nwparker/fix-6167-worktree-copy-submenu

Conversation

@nwparker

@nwparker nwparker commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

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:

  • Copy Path — the worktree path, verbatim (Windows drive paths and SSH remote paths preserved; no path joining or separator assumptions)
  • Copy branch name — resolved via the existing getWorktreeGitIdentityDisplay, so detached HEAD and folder workspaces disable the item with a "No branch" hint instead of copying an empty string
  • Copy PR URL / Copy MR URL — provider-neutral. The label is driven by the resolved review's provider (gitlab → 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:

  • handleCopyPath previously fired a floating promise. A clipboard failure produced an unhandled rejection with zero user feedback. All three actions now route through announceClipboardWrite, which catches and surfaces a toast.
  • 12 new user-visible strings are routed through translate() and added to en.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 from repro-6167-worktree-copy-submenu.test.tsx; git show -M reports 94% similarity, the only delta being a 3-line docblock — every expect is byte-identical.)

BEFORE — source files reverted to their pre-fix content at ff09527c61, repro re-run:

 FAIL  ...WorktreeContextMenu.copy-actions.test.tsx > offers Copy Path and Copy branch name
AssertionError: expected 'cardWorkspaceUpdateMove to StatusCopy…' to contain 'Copy branch name'
Received: "cardWorkspaceUpdateMove to StatusCopy PathPinMark UnreadNew group from projectSet Parent Worktree...SleepDelete"

 FAIL  ...WorktreeContextMenu.copy-actions.test.tsx > offers Copy PR URL for a worktree with a linked pull request
AssertionError: expected 'cardWorkspaceUpdateMove to StatusCopy…' to contain 'Copy PR URL'

 FAIL  ...WorktreeContextMenu.copy-actions.test.tsx > wires a clipboard write per requested Copy action
AssertionError: expected 1 to be greater than or equal to 3

 Test Files  1 failed (1)
      Tests  3 failed (3)

AFTER — at d09b196be6:

$ npx vitest run --config config/vitest.config.ts \
    src/renderer/src/components/sidebar/WorktreeContextMenu.copy-actions.test.tsx

 Test Files  1 passed (1)
      Tests  3 passed (3)
   Duration  575ms

The e2e repro tests/e2e/repro-6167-worktree-copy-menu.spec.ts (asserts copyItems.length >= 3 against 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 by git checkout ff09527c61 -- src/ plus a full electron-vite build --mode e2e rebuild.

BEFORE — one loose Copy Path sitting between Open in and Pin:

before-worktree-context-menu.png

AFTER — labelled Copy section with all three actions. Copy PR URL is disabled with a Not linked hint because the e2e fixture repo has no linked review:

after-worktree-context-menu.png

AFTER, GitLab worktree — same group, third item reads Copy MR URL and is enabled. This is the provider-neutrality proof:

after-gitlab-mr-context-menu.png

Full-window context shots (1400×900)

BEFORE:

before-worktree-context-menu-full.png

AFTER:

after-worktree-context-menu-full.png

DOM label dumps taken alongside the screenshots:

copyItems in the live menu
before ["Copy Path"]
after ["Copy Path", "Copy branch name", "Copy PR URL\nNot linked"]
after (GitLab) ["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.

Command Result
vitest run src/renderer/src/components/sidebar/ 213 files / 1857 tests passed
vitest run src/renderer/src/components/right-sidebar/ 184 files / 1609 tests passed
vitest run src/renderer/src/i18n/ 9 files / 68 tests passed
vitest on the 14 dependent-module tests (WorktreeCard.*, WorktreeList.card-memo-stability, worktree-card-pr-display, …) 120 tests passed
tsc --noEmit -p config/tsconfig.tc.web.json exit 0, no diagnostics
oxlint on all changed files; oxlint --type-aware --deny-warnings src config tests exit 0
oxlint --config config/oxlint-react-doctor.json exit 0
check-max-lines-ratchet.mjs exit 0 — no new bypasses, no max-lines disable added
verify-localization-catalog.mjs / verify-localization-extraction.mjs / audit-localization-coverage.mjs --check all exit 0
check-reliability-gates.mjs exit 0 — 62 gates
electron-vite build --mode e2e exit 0
playwright tests/e2e/repro-6167-worktree-copy-menu.spec.ts 1 passed
playwright tests/e2e/worktree-lineage.spec.ts (only other spec that clicks this menu) 6 passed

Behaviour-preservation checks done by reading, not just running:

  • The new items sit inside the same !isMultiContext block that already held Copy Path, so multi-select behaviour is untouched.
  • All existing e2e menu selectors are name-based (getByRole('menuitem', { name: … })) — grep confirms no nth() indexing on menu items, so the inserted label and separator cannot shift anything.
  • No new useAppStore / Zustand subscriptions. check:zustand-selector-fanout passes: 2500 subscribers × 2000 unrelated writes, median 34.46 ms, 0 render invalidations.

Trade-offs and user-visible changes — disclosed

  1. "Copy Path" is now noisy. It previously wrote to the clipboard in total silence; it now raises a success toast (and, more importantly, a failure toast). This was deliberate — the old handler swallowed clipboard errors entirely — but it is a change to a pre-existing action's behaviour. No test asserted the old silence.
  2. Flat labelled section, not a nested submenu. The issue asked for a "Copy submenu". Radix portals DropdownMenuSubContent outside [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 labelled Copy section 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.
  3. Two items can be permanently disabled. On a folder workspace or an unlinked worktree, Copy branch name / Copy PR URL render greyed out with a reason chip rather than being hidden. Hiding them would make the e2e's >= 3 count unsatisfiable on a fixture worktree.
  4. WorktreeContextMenu.copy-actions.test.tsx:152 asserts by regexing the component's own source for >= 3 occurrences of writeClipboardText. That is brittle and non-behavioural, but it is the adjudicated repro and I did not weaken it. The real behavioural coverage lives in WorktreeContextMenu.copy-targets.test.tsx.
  5. Only en.json received the 12 new keys; es/ja/ko/zh did not. This matches the repo's existing state rather than introducing a gap — the neighbouring deleteWithDescendants key is also en-only, and verify-localization-catalog.mjs tolerates it (base f3e087ec06 already reported 147 missing per locale; HEAD reports 159, exactly +12). translate(key, fallback) supplies the English default, so nothing breaks.
  6. branchName={branch} is redundantgetWorktreeCopyTargets derives an identical value itself. It also bypasses the card's isFolder gate, 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: WorktreeCard passes two new props, and WorktreeContextMenu gains one useMemo plus two useCallbacks. Everything else (the label, the two new items, the toast, the clipboard IPC) lives inside DropdownMenuContent, which is wrapped in a Radix Portal with no forceMount — so it is unmounted when closed and costs exactly zero per row, paid once per right-click.

  • Pure projection cost: benchmarked a standalone replica at 500 rows — median 0.0179 ms / 500 rows = 0.04 µs per row (p95 0.0979 ms). WorktreeList is @tanstack/react-virtual-virtualized, so real cost is bounded by ~20–40 visible rows ≈ 1.6 µs per sidebar render. Below noise.
  • The one real risk — React.memo fan-out from the unmemoized review={hoverReview} prop — was investigated with a throwaway happy-dom test replicating the exact shape (memo'd child, inline children, unstable review literal): 2 parent renders → 2 child renders, identical with and without the prop. Delta = 0. The memo was already inert because children={cardBody} is a fresh element every render.
  • No added user-perceived latency. announceClipboardWrite is invoked via void, so onSelect returns immediately and Radix closes the menu without awaiting. The IPC round-trip only gates when the toast appears.
  • No new sync IPC, fs, git, timers, retries, or session-lifetime collections. Terminal and tab-switch paths untouched.
  • Bundle: GitBranch + Link2 from lucide-react (tree-shaken), sonner already 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 f3e087ec06d09b196be6: 8 files, +790/−3, renderer-only. No package.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 (no metaKey, no path separators, no OS branching; the existing Electron clipboard IPC has a browser fallback in web-preload-api.ts, so desktop and paired-web both work).

Three P2 follow-ups, all safe to land later:

  1. "Not linked" is shown for worktrees that are linked but not yet cached. getWorktreeCardPrDisplay returns a fallback display with a number but no url on cold start / offline / rate-limited / polling-disabled. worktree-copy-targets.ts:55 maps that to reviewUrl: null, so the row says "Not linked", which is factually wrong. Should say "Loading…" or derive the URL from repo.gitRemoteIdentity + number.
  2. PR/MR wording falls back to "PR" on GitLab repos with no linked review. worktree-copy-targets.ts:54 derives the label from a null review. The repo already has the provider-aware, localized answer — localizedHostedReviewCopy(resolveSupportedHostedReviewCopyProvider(repo?.gitRemoteIdentity?.provider)) — and repo is 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 existing getReviewLabel in worktree-review-helpers.tsx:7.
  3. review={hoverReview} is unmemoized (WorktreeCard.tsx:1936, from prDisplay at :535). Harmless today because the memo was already inert (see Perf), but memoizing prDisplay would benefit every other consumer and avoid re-arming this footgun.

Styling conforms: text-[11px] text-muted-foreground for trailing metadata is explicitly sanctioned at docs/STYLEGUIDE.md:220, the new DropdownMenuLabel copies the pre-existing "Workspace" label in the same file verbatim, and size-3.5 matches 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, tsc on 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, no e.metaKey hardcoding, 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.tsx and the GitLab screenshot above.

Made with Orca 🐋

nwparker and others added 2 commits August 2, 2026 21:30
…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.
@nwparker
nwparker requested a review from brennanb2025 as a code owner August 3, 2026 06:54
@coderabbitai

coderabbitai Bot commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

@nwparker, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 20 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

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 configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 30d6f836-a479-46bb-b714-0eb7cc4bb57c

📥 Commits

Reviewing files that changed from the base of the PR and between d48cac7 and d09b196.

📒 Files selected for processing (8)
  • src/renderer/src/components/sidebar/WorktreeCard.tsx
  • src/renderer/src/components/sidebar/WorktreeContextMenu.copy-actions.test.tsx
  • src/renderer/src/components/sidebar/WorktreeContextMenu.copy-targets.test.tsx
  • src/renderer/src/components/sidebar/WorktreeContextMenu.tsx
  • src/renderer/src/components/sidebar/worktree-copy-targets.test.ts
  • src/renderer/src/components/sidebar/worktree-copy-targets.ts
  • src/renderer/src/i18n/locales/en.json
  • tests/e2e/repro-6167-worktree-copy-menu.spec.ts

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature]: add more "Copy XXX" menu item

1 participant