Skip to content

feat(navigator): Files / Changes 行に右クリックメニューでファイルパス copy を追加#640

Merged
miyaoka merged 12 commits into
mainfrom
feat/file-context-menu-copy-path
May 26, 2026
Merged

feat(navigator): Files / Changes 行に右クリックメニューでファイルパス copy を追加#640
miyaoka merged 12 commits into
mainfrom
feat/file-context-menu-copy-path

Conversation

@miyaoka

@miyaoka miyaoka commented May 26, 2026

Copy link
Copy Markdown
Owner

概要

Filer (Files) / Changes パネルの行に右クリック / control+click でメニューを追加し、ファイルやフォルダの絶対パスを clipboard に copy できるようにする。snapshot / commit 選択中は {commit hash}\n{絶対パス} の 2 行形式で copy する。

背景

これまで「いま見ているファイルやフォルダの絶対パスを別ツールに貼りたい」「過去 commit のあるファイル参照を hash 付きで控えたい」という日常的な copy 要求に応える手段がなかった。Filer / Changes はどちらも active worktree の行を表示しているので、両方の行に統一的な右クリック経路を追加するのが自然な置き場所になる。

設計判断の過程で実機検証と設計レビューを通じて見つかった構造的制約、そのリファクタの経緯:

  • light-dismiss 回避: popover="auto" を contextmenu 同サイクル内で showPopover すると mousedown が light-dismiss を予約し、続く mouseup で即閉じる ( whatwg/html issue 10905 )。setTimeout(0) / requestAnimationFrame 等の defer は WebKit (WebPage) では light-dismiss を抜けないことが実機で判明したため、pointerup capture listener を NavigatorPane の setup 直下に常設し、子から bubble する contextmenu event を pending ref に積んで次の pointerup で showPopover する経路にした
  • macOS control+click 対応: WebKit は control+click を button=0 + click event として dispatch する ( webkit bugzilla 52174 ) ため、contextmenu と通常 click の両方が発火する。click handler 側で event.ctrlKey 早期 return することで folder の展開 / file の select 経路を抑止し、contextmenu 経路のみが動くようにした
  • 依存方向の整理: 右クリックメニューを src/shared/ に置こうとすると isolateModules lint に引っかかるため、features/navigator/ に閉じ込め、子 pane (FilerPane / ChangesPane / TreeItem) は contextMenu event を bubble で navigator まで上げる構造に倒した。子は payload 型のみ type-only import で受け、runtime 依存は navigator → 子の 1 方向に保つ
  • race の排除: defer 中に worktree や commit 選択が切り替わる race を排除するため dircommitHash右クリック時に snapshot して popover context に焼き付け、menu 側は live store を読み直さない
  • Filer / Changes 対称性: commit hash 解決は useGitGraphStore.contextMenuHash の SSOT に集約 (range mode は undefined、UNCOMMITTED_HASH も undefined、それ以外で selectedHash)。両 pane で同 file の copy 結果が非対称にならないよう統一

変更内容

新規モジュール ( features/navigator/ )

  • useFileContextMenu.tsusePopover<{ dir, relPath, commitHash?, x?, y? }>() の module singleton。FileContextMenuPayload (子から bubble する event payload 型) を SSOT として export。docstring に light-dismiss 回避の不変条件 ( setTimeout / rAF / queueMicrotask 不可、{ capture: true } 外すな、event.button で filter するな、pointerdown で reset するな ) を集約
  • FileContextMenu.vue — Copy file path 1 アクションのポップオーバー。context の dir (右クリック時 snapshot) と relPathjoinAbsRel で結合した絶対パスを clipboard に書く。commitHash 指定時は {hash}\n{abs path} 形式

NavigatorPane

  • 末尾に <FileContextMenu /> を 1 個マウント (singleton state)
  • useEventListener(window, "pointerup", ..., { capture: true }) を setup 直下で 1 度だけ登録 (effect scope 連動で auto cleanup)
  • pendingOpen ref に dir / hash を snapshot で積み、pointerup で消化 + null 化 (連打時は最後の右クリックだけが menu を開く)
  • anchorEl.isConnected で defer 中 unmount race をガード

Filer

  • FileTreeItem.vue: 行 button に @contextmenu を追加し、inertLeaf (submodule / snapshot symlink) 以外で preventDefault + emit。toggle 内で event.ctrlKey 早期 return (control+click で folder 展開を抑止)
  • FilerPane.vue: 配下から bubble してくる contextMenu を NavigatorPane に bubble
  • select-none で右クリック時のテキスト選択を抑制

Changes

  • ChangesTreeItem.vue: file leaf / folder どちらも menu 対象。folder は chain 圧縮の最深 fullPath ( displayPath ) を relPath に。onClick 内で event.ctrlKey 早期 return
  • ChangesPane.vue: contextMenu を NavigatorPane に bubble
  • select-none でテキスト選択抑制

Git graph store

  • useGitGraphStore.contextMenuHash (computed) を追加。「右クリックで copy する commit hash」の SSOT。range mode は undefined、UNCOMMITTED_HASH も undefined、それ以外で selectedHash

Changes tree

  • ChangesTreeNode.folderdisplayPath (chain 圧縮の最深 folder fullPath) を追加。chain 圧縮された .github/workflows のような行でも 1 つの実体 path に決まるため menu 対象にできる
  • changesTree.test.ts 新規追加 — displayPath の境界 5 ケース (圧縮なし / 1 段 / 多段 / file 混在 / root 直下) と throw 契約 3 ケース (重複 / / 末尾 / / 空 path) を覆う

Worktree pathUtils

  • joinAbsRel(dir, relPath) を追加。絶対 dir + worktree 相対 path → 絶対 path の SSOT
  • dir 末尾 / (Swift URL.path 経由) を defensive strip、dir === "/" の root ケースも safe に処理
  • pathUtils.test.ts に境界テスト 4 件追加

Type / 依存方向

  • FileContextMenuPayload は navigator が SSOT として export、子 pane は import type で参照 (type-only import なので runtime 依存は navigator → 子の 1 方向で閉じる)

確認事項

  • Files / Changes のファイル行を右クリック → menu がカーソル位置に開く
  • Filer の directory 行 / Changes の folder 行を右クリック → menu が開き、Copy file path で folder の絶対 path が copy される
  • working tree 中で Copy file path → 絶対パスのみ copy される
  • git-graph で過去 commit を選んだ状態で Filer / Changes 双方から Copy → {hash}\n{abs path} が copy される
  • range mode (shift+click) で Changes から Copy → path のみ copy される (multi-commit を単一 hash で代表しない)
  • 右クリック時にテキスト選択が発生しない
  • macOS control+click でも menu が開く (button=0 経路、bugzilla 52174 対応)
  • macOS control+click で folder が開閉しない (click 経路は ctrlKey で早期 return)
  • submodule / snapshot symlink を右クリック → gozd menu が開かず OS 標準 menu が出る
  • 右クリック直後に worktree 切替ショートカット → menu が右クリック時点の dir / hash で開く (snapshot 化が効いていること)

intent(navigator): copy file paths from Files / Changes panes — working tree as absolute path alone, snapshot / commit selection as "{hash}\n{abs path}"
decision(file-context-menu): module singleton usePopover shared by Filer / Changes, mounted once in NavigatorPane. relPath + active worktree dir join happens in the menu to avoid prop drilling
decision(changes): derive contextMenuHash from useGitGraphStore — workingTreeOnly / UNCOMMITTED_HASH fall back through compareHash so range-mode WT endpoints still copy a meaningful commit
rejected(shared): placement under src/shared/ blocked by isolateModules (shared→shared dep on popover/notification forbidden); placed under features/navigator/ since navigator already depends on filer + changes
learned(popover-auto-dismiss): right-click on popover="auto" closes immediately because mousedown reserves light-dismiss consumed on mouseup (whatwg/html#10905). delayed open via pointerup capture-once survives the cycle
@miyaoka miyaoka self-assigned this May 26, 2026
@coderabbitai

coderabbitai Bot commented May 26, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

このプルリクエストは、Filer および Changes ペイン内のファイルに対する右クリックコンテキストメニューを実装しています。新たに useFileContextMenu composable でポペオーバー状態を管理し、FileContextMenu コンポーネントで「Copy file path」アクションを提供します。ChangesPane では commit hash を条件分岐で算出し、ChangesTreeItem と FileTreeItem の両方でコンテキストメニューイベントを処理することで、worktree 相対パスおよび任意のコミットハッシュ情報をクリップボードにコピーできます。

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed PR タイトル「feat(navigator): Files / Changes 行に右クリックメニューでファイルパス copy を追加」は、変更の主要な機能(Files/Changes パネルのファイル行への右クリックメニューとコピー機能追加)を正確に表現している。
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description check ✅ Passed プルリクエストの説明は、ファイル行への右クリックコンテキストメニュー追加、ファイルパスのコピー機能、設計判断(light-dismiss回避、macOS対応など)、変更内容、確認事項など、実装内容全体に関連した詳細な説明がされている。

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/file-context-menu-copy-path

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 and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/renderer/src/features/filer/FileTreeItem.vue`:
- Around line 392-410: onContextMenu (and the equivalent in ChangesTreeItem.vue)
currently waits for a pointerup listener before calling openContextMenu which
can miss keyboard-triggered contextmenu events; change onContextMenu to keep the
pointerup listener path but also create a fallback that opens the menu on the
next animation frame or microtask when no pointerup arrives (call
openContextMenu without x/y so the menu positions itself from the anchor), and
ensure useEventListener returns a cleanup function you store so you can cancel
the pending pointerup listener when the fallback runs (and cancel any scheduled
fallback if pointerup fires first); reference the onContextMenu function,
openContextMenu call, and the useEventListener cleanup to implement this.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 98bf5e39-e814-4406-81ae-4cde99c35860

📥 Commits

Reviewing files that changed from the base of the PR and between 95bb7e7 and 238821e.

📒 Files selected for processing (7)
  • apps/renderer/src/features/changes/ChangesPane.vue
  • apps/renderer/src/features/changes/ChangesTreeItem.vue
  • apps/renderer/src/features/filer/FileTreeItem.vue
  • apps/renderer/src/features/navigator/FileContextMenu.vue
  • apps/renderer/src/features/navigator/NavigatorPane.vue
  • apps/renderer/src/features/navigator/index.ts
  • apps/renderer/src/features/navigator/useFileContextMenu.ts

Comment thread apps/renderer/src/features/filer/FileTreeItem.vue
miyaoka added 5 commits May 26, 2026 16:27
intent(navigator): apply review feedback — fix listener leak, input-source dependency, hash semantics in range mode, dependency direction inversion, missing inert leaf / folder guards
decision(navigator): hoist popover open to NavigatorPane, children (FilerPane / ChangesPane / TreeItem) only emit contextMenu and bubble up. removes navigator import from filer / changes (keeps navigator → child as 1-way dependency)
decision(navigator): defer showPopover via setTimeout(0) in NavigatorPane to escape the contextmenu mousedown / mouseup cycle (whatwg/html#10905). input-agnostic so keyboard contextmenu / programmatic dispatch also work, unlike the per-row useEventListener pointerup workaround which leaked on unmount
decision(changes): contextMenuHash returns undefined in range mode (compareHash !== null). representing multi-commit diffs with a single hash misleads users; single commit selection still returns selectedHash matching Filer's snapshotHash semantics
decision(filer): contextmenu on inertLeaf (submodule / snapshot symlink) falls through to OS menu — copying the working tree absolute path of a symlink / gitlink is misleading semantics
decision(changes): contextmenu on folder rows falls through to OS menu — gozd has no folder-targeted menu action, do not steal the OS menu
decision(filer): expose joinPath via the filer barrel and reuse it in FileContextMenu instead of inline `${dir}/${relPath}` to keep worktree-path joining as a single SSOT
rejected(shared-context-menu): placing useFileContextMenu in src/shared/ still requires shared→shared deps (popover / notification) blocked by isolateModules; navigator-owned + event-bubble pattern matches the existing SidebarPane / WorktreeMenu architecture instead
…ix timer lifecycle

intent(navigator): apply second-round review feedback — Filer / Changes copy hash asymmetry, joinPath responsibility overrun, directory row inconsistency, payload type duplication, anchor disconnect race, timer lifecycle leak
decision(git-graph): introduce useGitGraphStore.contextMenuHash as the single source of truth for "what hash to copy". range mode returns undefined (multi-commit diff cannot be represented by one hash). NavigatorPane reads it at open time; child panes no longer compute or pass commit hash
decision(navigator): export FileContextMenuPayload from useFileContextMenu.ts and have every emit site `import type` it. eliminates the 5-place duplicate definition while keeping the import as type-only (no runtime dependency back to navigator)
decision(navigator): pendingOpenTimers set + onScopeDispose clearTimeout, and `req.anchorEl.isConnected` guard inside the deferred callback. covers timer leak on unmount and dir-switch races where the anchor element is gone before showPopover fires. disconnected case writes a debug log instead of silent dropping
decision(filer): FileTreeItem early-returns for directory rows so the OS context menu shows instead of "Copy file path" with a directory path. mirrors the Changes folder-row behavior. inert leaf guard kept for submodule / snapshot symlink
decision(worktree): add joinAbsRel(dir, relPath) to features/worktree/pathUtils.ts as the SSOT for absolute-dir + worktree-relative joining. filerUtils.joinPath stays scoped to worktree-relative concatenation (`""` parent case is its defining contract). FileContextMenu uses joinAbsRel
rejected(payload-in-shared): putting FileContextMenuPayload in src/shared/popover mixes file-specific concerns into a generic popover module. type-only re-export from navigator keeps the type close to its only consumer (NavigatorPane) without runtime coupling
…outFn

intent(navigator): apply third-round review feedback — close worktree-switch race during defer, replace handwritten timer queue with useTimeoutFn, harden joinAbsRel against trailing-slash dir, sync NavigatorPane <doc> with current behavior
decision(navigator): snapshot worktreeStore.dir and gitGraphStore.contextMenuHash synchronously when contextMenu fires and freeze them into the popover context. defer-time / menu-show-time store reads were a structural race: switching worktrees mid-defer would compose the old relPath with the new dir. FileContextMenu now reads context.dir instead of the live store
decision(navigator): swap the handwritten pendingOpenTimers Set + onScopeDispose for VueUse useTimeoutFn(_, 0, {immediate:false}). gives scope-bound cleanup, start() cancels the prior pending (last right-click wins, matches popover singleton's openState overwrite semantics)
decision(worktree): joinAbsRel strips trailing slashes from dir and treats stripped "/" as root. Swift URL.path can emit trailing-slash absolute paths; the function now produces "/abs/dir/rel" / "/rel" / "/" deterministically. tests cover trailing slash, repeated slash, root-only, root + relPath, root + empty
decision(worktree): drop the "filer の joinPath とは責務が別" comparison from joinAbsRel docstring. the function name and contract describe the responsibility on their own; the cross-feature reference was leftover review context, not durable documentation
…extMenu

intent(navigator): apply fourth-round review feedback — remove duplicate spec text from FileContextMenu and NavigatorPane <doc> blocks, document intentional silence of pending-cancel and no-active-worktree paths
decision(navigator): make useFileContextMenu.ts docstring the single source of truth for popover semantics (defer / snapshot / disconnect guard / context shape). FileContextMenu.vue <doc> and NavigatorPane.vue <doc> now state only the component-local responsibility and point readers at the SSOT
decision(navigator): document why useTimeoutFn.start silently cancels prior pending — keeping user right-click bursts noise-free; matches popover singleton's last-write-wins openState semantics
decision(navigator): document why "no active worktree" path stays at notification.debug — FilerPane never renders tree items when dir is undefined ("waiting for open command..." instead), so this branch is defensive-only and not user-visible. info toast would conflate it with normal-state messaging
…irectory rows

intent(navigator): two user-reported regressions after applying reviewer guidance — menu instantly closing on mouse release (light-dismiss not avoided) and Filer directory rows no longer opening the menu
decision(navigator): drop useTimeoutFn / setTimeout(0) defer and go back to pointerup capture listener. WebKit (WebPage) does not let any task / microtask defer escape popover="auto" light-dismiss; only running showPopover after the next pointerup is consumed reliably keeps the menu open. verified on the initial commit (238821e) and confirmed regressed under setTimeout
decision(navigator): keep the pointerup listener at setup top-level via useEventListener (no { once: true }) and stash the request in a pending ref. setup-level registration binds it to the effect scope so it cleans up on unmount / HMR, addressing the per-row useEventListener leak that prompted the earlier refactor without re-introducing it
decision(filer): restore Filer directory rows as menu targets. directories carry a real filesystem path and copying the absolute path is meaningful. the prior symmetry with Changes folder rows was a false equivalence: Changes folder rows are chain-compressed presentation (e.g. `.github/workflows`) with no single canonical path, hence still excluded
decision(navigator): document the unbreakable invariant — `setTimeout(0)` / `requestAnimationFrame` / `queueMicrotask` are all confirmed unable to escape WebKit's popover light-dismiss; only the pointerup capture listener works. Shift+F10 / Apps key / programmatic dispatch produce no pointerup so the menu does not open via those paths; that responsibility is deferred to a future keybinding-system route rather than papered over with a defer trick

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/renderer/src/features/filer/FileTreeItem.vue`:
- Around line 392-416: The onContextMenu handler currently prevents default and
emits a custom contextMenu for directories as well as files; add a guard so only
file leaves trigger the custom menu: inside function onContextMenu, before
calling event.preventDefault() and emit("contextMenu", ...), check and return
early if the node is a directory (e.g. use an existing flag like isDirectory or
node.type === 'directory' — referenced symbols: onContextMenu, isInertLeaf,
emit("contextMenu"), props.path); apply the same directory-guard change to the
other context-menu handler mentioned (the handler at the other occurrence around
line 435) so directories fall through to the OS menu.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 530558d9-1d20-4f16-95d1-e81f39b708cc

📥 Commits

Reviewing files that changed from the base of the PR and between 238821e and f5fb06e.

📒 Files selected for processing (12)
  • apps/renderer/src/features/changes/ChangesPane.vue
  • apps/renderer/src/features/changes/ChangesTreeItem.vue
  • apps/renderer/src/features/filer/FileTreeItem.vue
  • apps/renderer/src/features/filer/FilerPane.vue
  • apps/renderer/src/features/git-graph/useGitGraphStore.ts
  • apps/renderer/src/features/navigator/FileContextMenu.vue
  • apps/renderer/src/features/navigator/NavigatorPane.vue
  • apps/renderer/src/features/navigator/index.ts
  • apps/renderer/src/features/navigator/useFileContextMenu.ts
  • apps/renderer/src/features/worktree/index.ts
  • apps/renderer/src/features/worktree/pathUtils.test.ts
  • apps/renderer/src/features/worktree/pathUtils.ts
✅ Files skipped from review due to trivial changes (1)
  • apps/renderer/src/features/worktree/index.ts

Comment thread apps/renderer/src/features/filer/FileTreeItem.vue
miyaoka added 6 commits May 26, 2026 17:54
…ight-click

intent(navigator): user-reported regression — Changes folder rows do not open the menu. user expects the menu on every row in Files / Changes, not just file leaves
decision(changes): expose displayPath (deepest folder fullPath after chain compression) on folder nodes and emit it as relPath on right-click. user clicks the row showing `.github/workflows`, the menu copies `.github/workflows` — the chain-compressed display always resolves to one concrete deepest folder, so the earlier "path is ambiguous" argument was wrong
decision(filer): drop the "Changes folder is excluded by responsibility" comment now that both sides include folder rows. asymmetry was a misread on the reviewer's part, not a real responsibility split
decision(navigator): pointerup capture handler filters event.button === 2 so only right-click releases consume the pending. left clicks / middle clicks elsewhere on screen no longer race-fire the menu while a pending is parked
decision(navigator): docstring of useFileContextMenu.ts gains a note that the light-dismiss invariant block is duplicated in NavigatorPane.vue. they must be updated together. the duplication is intentional (the NavigatorPane copy sits where the next implementer reads first), but the cross-link makes the contract explicit
intent(navigator): reviewer flagged that event.button === 2 filter breaks macOS WebKit control + click (bugzilla 52174 — control + click dispatches as button=0), and that the new displayPath path on Changes folder rows had no test coverage
decision(navigator): remove the event.button check from the pointerup capture listener. pending ref already acts as the "we just had a contextmenu" flag, consuming the next pointerup and nulling itself. multi-button race (right-click held while clicking elsewhere) is an accepted edge case, the realistic right-click → release path remains correct
decision(navigator): document the bug in the invariant block — "do not filter by event.button" added to the list alongside "do not defer with setTimeout / rAF" and "do not change capture / pointer phase"
decision(changes): add changesTree.test.ts covering displayPath for the five chain-compression boundaries — no compression, single-chain, multi-chain, file/folder mix interrupting the chain, single-file-only branch. displayPath drives the relPath used by Copy file path, so regressing the chain-walk produces incorrect copy contents without compile-time signal
intent(navigator): apply low-severity reviewer feedback (34/35/36) — defend pointerup-only design against pointerdown reset proposals, exercise buildChangesTree error contracts, group tests by responsibility
decision(navigator): expand the invariant block with why pointerdown reset cannot be added. right-click pointerdown happens before onFileContextMenu sets pending, so the reset target does not exist yet; pairing reset with subsequent left-click pointerdown then erases pending unexpectedly. keeps future refactors from chasing the same dead-end button-filter detour
decision(changes): add throw-contract tests (`a//b.ts`, `a/b.ts/`, `""`) so the invalid-path contract that ChangesPane catches via tryCatch + notify.error is exercised. previously only the happy paths existed
decision(changes): nest describe blocks (`buildChangesTree` > `displayPath` / `error contracts`) to keep future additions grouped by responsibility instead of flat
…g note

intent(navigator): apply low-severity reviewer feedback (37/38) — rewrite the pointerdown-reset prohibition so its logic reads in one direction, and record why buildChangesTree tests are grouped by responsibility
decision(navigator): restructure the docstring sentence so the "right-click sequence wouldn't break" observation comes first as a possible counter-argument, then the actual race (an unrelated pointerdown erasing pending) is the prohibition's load-bearing reason. removes the internal contradiction the reviewer pointed out
decision(changes): add a top-level describe comment explaining the nested grouping is for future responsibility-scoped additions (displaySegments / anchorPath etc.), so flattening back is recognized as a regression instead of an aesthetic preference
…not toggle

intent(navigator): user reported control+click on Filer/Changes folders toggles them open while the context menu opens. control+click on macOS is meant to be a context menu trigger only, not a click
decision(navigator): early-return from both toggle (Filer) and onClick (Changes) when event.ctrlKey is true. WebKit dispatches control+click as button=0 with a regular click event in addition to contextmenu, so both handlers fire; gating click on ctrlKey leaves contextmenu alone but stops the unwanted toggle/select
intent(navigator): apply low-severity reviewer feedback (39/41) — make the control+click rationale traceable and call out the macOS-only assumption baked into the ctrlKey gate
decision(navigator): cite webkit bugzilla 52174 in the click-handler comments so the "control+click is dispatched as button=0" claim is verifiable. matches the citation used in NavigatorPane's invariant block
decision(navigator): document that gozd is macOS-only (root CLAUDE.md) so ctrlKey === control+click is a safe identity here. note explicitly that cross-platform support would require an OS check before this early return, since Ctrl+Click on Linux/Windows carries different semantics
@miyaoka miyaoka merged commit 16c5b5b into main May 26, 2026
7 checks passed
@miyaoka miyaoka deleted the feat/file-context-menu-copy-path branch May 26, 2026 09:23
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.

1 participant