Skip to content

fix(desktop): v2 file-open honors CMD+O editor choice#3674

Merged
Kitenite merged 4 commits into
mainfrom
in-v2-when-clicking-on-any-file-to-open-external-we-should-be-using-the-cmdo-choice-the-user-made
Apr 23, 2026
Merged

fix(desktop): v2 file-open honors CMD+O editor choice#3674
Kitenite merged 4 commits into
mainfrom
in-v2-when-clicking-on-any-file-to-open-external-we-should-be-using-the-cmdo-choice-the-user-made

Conversation

@Kitenite
Copy link
Copy Markdown
Collaborator

@Kitenite Kitenite commented Apr 23, 2026

Summary

  • v2 click-to-open-externally (FilesTab tree, DiffPane entries, terminal links, file pane header) now uses the editor you picked in the v2 Open In dropdown, not whatever the v1 global default happened to be. Root cause: server-side resolveDefaultEditor only consults v1 localDb tables, so it never saw the v2 choice (which lives client-side in tanstack-db v2SidebarProjects.defaultOpenInApp). The hook now forwards that choice to openFileInEditor as an explicit app override.
  • Fixes a silent path-resolution bug that produced doubled paths like <worktree>/apps/desktop/apps/desktop/... when relative diff paths were opened without a cwd: resolvePath now throws RelativePathWithoutCwdError instead of falling back to Electron's process.cwd(). The tRPC input field is renamed cwdworktreePath so the intent is explicit, and openInApp rejects non-absolute paths (it has no cwd context).
  • Extracted useV2ProjectDefaultApp(projectId) as the single source of truth for reading/writing the v2 preference — used by both V2OpenInMenuButton (write on successful open) and useOpenInExternalEditor (read on every file open).
  • DiffPane and FilesTab now route through useOpenInExternalEditor instead of calling the tRPC mutation directly, so every v2 file-open goes through one place that forwards worktreePath + the v2 app choice automatically.

Test plan

  • Pick Cursor in the v2 Open In dropdown → Cmd+click a file in the sidebar tree → opens in Cursor.
  • Switch to Zed in the v2 Open In dropdown → click a file in the Changes pane → opens in Zed.
  • Switch to VS Code → Cmd+O a terminal file link → opens in VS Code.
  • FilesTab "Open in Editor" context menu entry respects the current dropdown choice.
  • bun test apps/desktop/src/lib/trpc/routers/external/helpers.test.ts — 106 pass.
  • bun run typecheck — clean.

Summary by CodeRabbit

  • New Features

    • Allow specifying an alternate app when opening files externally.
    • Added a project-level "default open in" hook to manage persisted preferred apps.
  • Bug Fixes

    • Relative paths without an explicit workspace context now produce clear client-facing errors and are rejected.
    • Enforced absolute-path input when opening in-app; quoted/wrapped relative paths are treated consistently.
  • Refactor

    • Centralized external editor/open-file logic into workspace-scoped hooks and unified request field name (cwd → worktreePath).
  • Tests

    • Updated path-resolution tests to codify the new validation and allowed path forms.

Summary by cubic

v2 file-open now respects the CMD+O “Open In” choice across FilesTab, DiffPane, terminal links, and file headers. It adds strict path resolution with clear BAD_REQUEST errors and standardizes worktreePath across APIs.

  • Bug Fixes

    • Honor the v2 per-project editor by forwarding an explicit app to external.openFileInEditor.
    • Path validation: resolvePath throws RelativePathWithoutCwdError and withResolveGuard surfaces it as BAD_REQUEST; external.openInApp now requires an absolute path.
  • Refactors

    • Added useV2ProjectDefaultApp as the single source of truth; used by V2OpenInMenuButton and useOpenInExternalEditor.
    • Routed all v2 opens through useOpenInExternalEditor to auto-forward worktreePath and the v2 app.

Written for commit 8661702. Summary will update on new commits.

v2 click-to-open-externally (FilesTab tree, DiffPane entries, terminal
links, file pane header) was always hitting Cursor regardless of the
editor the user picked in the v2 Open In dropdown. Root cause: the
server-side `resolveDefaultEditor` only consults v1 localDb tables, so
it never saw the v2 choice (which lives client-side in tanstack-db
`v2SidebarProjects.defaultOpenInApp`). The hook now forwards that
choice to `openFileInEditor` as an explicit `app` override.

Also fixes a silent path-resolution bug that produced doubled paths
like `<worktree>/apps/desktop/apps/desktop/...` when relative diff
paths were opened without a cwd: `resolvePath` now throws
`RelativePathWithoutCwdError` instead of falling back to Electron's
`process.cwd()`, and the tRPC input field is renamed `cwd` →
`worktreePath` so the intent is explicit.

Extracted `useV2ProjectDefaultApp(projectId)` as the single source of
truth for reading/writing the v2 preference — used by both
`V2OpenInMenuButton` (write on open) and `useOpenInExternalEditor`
(read on open).
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 23, 2026

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 36778b99-bc68-4423-b3eb-dba1d4c5c9a3

📥 Commits

Reviewing files that changed from the base of the PR and between 61788bd and 8661702.

⛔ Files ignored due to path filters (1)
  • bun.lock is excluded by !**/*.lock
📒 Files selected for processing (1)
  • apps/desktop/src/lib/trpc/routers/external/helpers.test.ts

📝 Walkthrough

Walkthrough

Enforces explicit handling of relative paths by throwing RelativePathWithoutCwdError when no cwd/worktreePath is provided, maps that error to a TRPC BAD_REQUEST via withResolveGuard, renames cwd → worktreePath across UI/trpc calls, and adds a v2 project default-app hook plus related wiring.

Changes

Cohort / File(s) Summary
Core path helpers & router
apps/desktop/src/lib/trpc/routers/external/helpers.ts, apps/desktop/src/lib/trpc/routers/external/helpers.test.ts, apps/desktop/src/lib/trpc/routers/external/index.ts
Adds RelativePathWithoutCwdError; resolvePath now throws for relative paths when cwd/worktreePath is absent; tests updated; adds withResolveGuard to convert that error into a TRPC BAD_REQUEST; router threads/renames cwdworktreePath.
V2 project default-app hook
apps/desktop/src/renderer/routes/_authenticated/hooks/useV2ProjectDefaultApp/index.ts, .../useV2ProjectDefaultApp/useV2ProjectDefaultApp.ts
Adds useV2ProjectDefaultApp(projectId) and re-export index; hook reads/stores a project's default "open in" app and exposes a memoized setter that ensures project appears in the sidebar before updating.
Open-in-editor wiring / hooks
apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/.../useOpenInExternalEditor/useOpenInExternalEditor.ts, apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/.../V2OpenInMenuButton/V2OpenInMenuButton.tsx, apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/.../usePaneRegistry/components/DiffPane/DiffPane.tsx
Introduces/uses useOpenInExternalEditor(workspaceId); forwards worktreePath, projectId, and optional app override to TRPC openFileInEditor; components refactored to consume the hook and useV2ProjectDefaultApp.
Rename: cwd → worktreePath across UI
apps/desktop/src/renderer/.../usePathActions.ts, .../FilesView.tsx, .../FileItem.tsx, .../FileDiffSection.tsx, .../FileTreeItem.tsx, .../FileSearchResultItem.tsx
Replaces cwd parameter with worktreePath in hook signatures, calls, and mutation payloads; updates docs/comments and dependency lists accordingly.
Minor UI export
apps/desktop/src/renderer/routes/_authenticated/hooks/useV2ProjectDefaultApp/index.ts
Adds a re-export for the new hook.

Sequence Diagram(s)

sequenceDiagram
    actor User
    participant Component
    participant Hook
    participant Router
    participant Guard
    participant Helper

    User->>Component: request open file (path)
    Component->>Hook: invoke open(path, line, column)
    Hook->>Router: electronTrpcClient.external.openFileInEditor(path, worktreePath?, app?)
    Router->>Guard: withResolveGuard -> validate resolvePath
    Guard->>Helper: resolvePath(path, worktreePath)
    alt resolved path is relative AND worktreePath missing
        Helper-->>Guard: throw RelativePathWithoutCwdError
        Guard-->>Router: convert to TRPCError(BAD_REQUEST)
        Router-->>Component: error
        Component-->>User: show validation error
    else path valid
        Helper-->>Guard: return resolved path
        Guard-->>Router: continue
        Router-->>Hook: success
        Hook-->>Component: success
        Component-->>User: file opened
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 I hop through paths both near and far,
I nibble cwd until it's clear,
Worktree named, guards standing by,
No secret relative paths slip by,
A rabbit cheers: explicit, dear!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 28.57% 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 The title clearly summarizes the main change: v2 file-open now honors the CMD+O editor choice, addressing the core bug fix in this PR.
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 The PR description comprehensively covers changes, root causes, refactoring, and includes a detailed test plan with specific manual and automated test cases.

✏️ 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 in-v2-when-clicking-on-any-file-to-open-external-we-should-be-using-the-cmdo-choice-the-user-made

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.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/desktop/src/lib/trpc/routers/external/index.ts (1)

148-187: ⚠️ Potential issue | 🟡 Minor

All identified openInApp callers already pass absolute paths—the validation is safe.

The new isAbsolute guard (line 161) is purely additive. Inspection of all 6 call sites—ClickablePath, OpenInButton, usePathActions, and others—confirms they all pass paths explicitly named or documented as absolute (absolutePath prop, project.mainRepoPath from API, currentPath from server state). Terminal link detection separately uses the host-side statPath endpoint which resolves relative/tilde paths before returning validated absolute paths.

The optional refactor to enforce at the zod layer remains a reasonable improvement for cleaner error semantics, but the current runtime guard is correct and non-breaking.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/src/lib/trpc/routers/external/index.ts` around lines 148 - 187,
Move the absolute-path validation into the procedure's Zod input schema so
invalid input is rejected before the resolver runs: update the z.object for
openInApp to use z.string().refine(nodePath.isAbsolute, { message: "openInApp
requires an absolute path" }) for the path field (reference: openInApp,
publicProcedure, z.object, nodePath.isAbsolute), then remove the runtime
TRPCError isAbsolute check inside the mutation (or keep it as a redundant guard)
and ensure the error message matches the Zod validation for consistent
semantics.
🧹 Nitpick comments (3)
apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/components/FilesTab/FilesTab.tsx (1)

224-231: Nit: handleOpenInEditor is now a pure passthrough.

Since openInExternalEditor already has a stable identity and accepts (path, opts?), you can drop handleOpenInEditor and pass openInExternalEditor directly as onOpenInEditor (the prop's (absolutePath) => void signature is structurally compatible because opts is optional).

♻️ Proposed simplification
 	const openInExternalEditor = useOpenInExternalEditor(workspaceId);
-
-	const handleOpenInEditor = useCallback(
-		(absolutePath: string) => {
-			openInExternalEditor(absolutePath);
-		},
-		[openInExternalEditor],
-	);

Then replace onOpenInEditor={handleOpenInEditor} at line 596 with onOpenInEditor={openInExternalEditor}.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/`$workspaceId/components/WorkspaceSidebar/components/FilesTab/FilesTab.tsx
around lines 224 - 231, Remove the unnecessary passthrough callback
handleOpenInEditor and pass the hook result directly: stop declaring
handleOpenInEditor and instead use the stable openInExternalEditor returned from
useOpenInExternalEditor(workspaceId) as the onOpenInEditor prop (replace
onOpenInEditor={handleOpenInEditor} with onOpenInEditor={openInExternalEditor});
openInExternalEditor already has a compatible signature ((path, opts?) => void)
so no wrapper is needed.
apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/useOpenInExternalEditor/useOpenInExternalEditor.ts (1)

45-68: Minor: workspaceHost identity churn may invalidate the returned callback.

workspaceHost is a live-query object whose identity typically changes on every collection update. Depending on it (rather than workspaceHost?.hostMachineId) makes openInExternalEditor unstable, which can defeat React.memo on consumers like DiffFileEntry that receive it as onOpenFile.

♻️ Proposed fix
-	const workspaceHost = workspacesWithHost[0];
-	const projectId = workspaceHost?.projectId ?? undefined;
+	const workspaceHost = workspacesWithHost[0];
+	const hostMachineId = workspaceHost?.hostMachineId ?? null;
+	const projectId = workspaceHost?.projectId ?? undefined;
@@
-			if (workspaceHost?.hostMachineId !== machineId) {
+			if (hostMachineId !== machineId) {
 				toast.error("Can't open remote workspace paths in an external editor");
 				return;
 			}
@@
-		[workspaceHost, machineId, projectId, worktreePath, v2PreferredApp],
+		[hostMachineId, machineId, projectId, worktreePath, v2PreferredApp],
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/`$workspaceId/hooks/useOpenInExternalEditor/useOpenInExternalEditor.ts
around lines 45 - 68, The returned callback from useOpenInExternalEditor is
unstable because it depends on the whole live-query object workspaceHost; change
it to depend only on the stable primitive workspaceHost?.hostMachineId (e.g.,
capture const hostMachineId = workspaceHost?.hostMachineId and use that in the
closure and dependency array) so openInExternalEditor (used by consumers like
DiffFileEntry) remains stable; update the dependency list of the useCallback in
useOpenInExternalEditor to include hostMachineId instead of workspaceHost while
keeping other deps (machineId, projectId, worktreePath, v2PreferredApp).
apps/desktop/src/lib/trpc/routers/external/helpers.test.ts (1)

591-621: Optional: new describe block partially duplicates existing coverage.

Absolute / ~ / file:// / "relative with cwd" are already covered by the earlier describe blocks in this file. The one new case worth keeping is the wrapped/quoted relative-path throw. Consolidating (or dropping the redundant sub-tests) would keep the suite tighter; leaving as-is is fine too since the explicit "guards against process.cwd() fallback" grouping does add documentation value.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/src/lib/trpc/routers/external/helpers.test.ts` around lines 591
- 621, The new "resolvePath guards against process.cwd() fallback" describe
block duplicates coverage already present for absolute, ~-prefixed, file://, and
"relative with cwd" cases; remove the redundant tests and keep only the unique
wrapped/quoted relative-path case to reduce duplication. Update the describe
block (or collapse it into an earlier describe) so it only contains the test
that asserts resolvePath('"apps/desktop/src/index.ts"') throws
RelativePathWithoutCwdError (retain the existing test name "throws for a
wrapped/quoted relative path with no cwd" and keep references to resolvePath and
RelativePathWithoutCwdError).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@apps/desktop/src/lib/trpc/routers/external/index.ts`:
- Around line 148-187: Move the absolute-path validation into the procedure's
Zod input schema so invalid input is rejected before the resolver runs: update
the z.object for openInApp to use z.string().refine(nodePath.isAbsolute, {
message: "openInApp requires an absolute path" }) for the path field (reference:
openInApp, publicProcedure, z.object, nodePath.isAbsolute), then remove the
runtime TRPCError isAbsolute check inside the mutation (or keep it as a
redundant guard) and ensure the error message matches the Zod validation for
consistent semantics.

---

Nitpick comments:
In `@apps/desktop/src/lib/trpc/routers/external/helpers.test.ts`:
- Around line 591-621: The new "resolvePath guards against process.cwd()
fallback" describe block duplicates coverage already present for absolute,
~-prefixed, file://, and "relative with cwd" cases; remove the redundant tests
and keep only the unique wrapped/quoted relative-path case to reduce
duplication. Update the describe block (or collapse it into an earlier describe)
so it only contains the test that asserts
resolvePath('"apps/desktop/src/index.ts"') throws RelativePathWithoutCwdError
(retain the existing test name "throws for a wrapped/quoted relative path with
no cwd" and keep references to resolvePath and RelativePathWithoutCwdError).

In
`@apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/`$workspaceId/components/WorkspaceSidebar/components/FilesTab/FilesTab.tsx:
- Around line 224-231: Remove the unnecessary passthrough callback
handleOpenInEditor and pass the hook result directly: stop declaring
handleOpenInEditor and instead use the stable openInExternalEditor returned from
useOpenInExternalEditor(workspaceId) as the onOpenInEditor prop (replace
onOpenInEditor={handleOpenInEditor} with onOpenInEditor={openInExternalEditor});
openInExternalEditor already has a compatible signature ((path, opts?) => void)
so no wrapper is needed.

In
`@apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/`$workspaceId/hooks/useOpenInExternalEditor/useOpenInExternalEditor.ts:
- Around line 45-68: The returned callback from useOpenInExternalEditor is
unstable because it depends on the whole live-query object workspaceHost; change
it to depend only on the stable primitive workspaceHost?.hostMachineId (e.g.,
capture const hostMachineId = workspaceHost?.hostMachineId and use that in the
closure and dependency array) so openInExternalEditor (used by consumers like
DiffFileEntry) remains stable; update the dependency list of the useCallback in
useOpenInExternalEditor to include hostMachineId instead of workspaceHost while
keeping other deps (machineId, projectId, worktreePath, v2PreferredApp).

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 5df920a4-753b-4e71-9938-766085e2ae1c

📥 Commits

Reviewing files that changed from the base of the PR and between 0791b0b and 7a8dc8d.

⛔ Files ignored due to path filters (1)
  • bun.lock is excluded by !**/*.lock
📒 Files selected for processing (15)
  • apps/desktop/src/lib/trpc/routers/external/helpers.test.ts
  • apps/desktop/src/lib/trpc/routers/external/helpers.ts
  • apps/desktop/src/lib/trpc/routers/external/index.ts
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/components/V2OpenInMenuButton/V2OpenInMenuButton.tsx
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/components/FilesTab/FilesTab.tsx
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/useOpenInExternalEditor/useOpenInExternalEditor.ts
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/DiffPane/DiffPane.tsx
  • apps/desktop/src/renderer/routes/_authenticated/hooks/useV2ProjectDefaultApp/index.ts
  • apps/desktop/src/renderer/routes/_authenticated/hooks/useV2ProjectDefaultApp/useV2ProjectDefaultApp.ts
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/ChangesContent/components/FileDiffSection/FileDiffSection.tsx
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/RightSidebar/ChangesView/components/FileItem/FileItem.tsx
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/RightSidebar/ChangesView/hooks/usePathActions.ts
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/RightSidebar/FilesView/FilesView.tsx
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/RightSidebar/FilesView/components/FileSearchResultItem/FileSearchResultItem.tsx
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/RightSidebar/FilesView/components/FileTreeItem/FileTreeItem.tsx

@greptile-apps
Copy link
Copy Markdown

greptile-apps Bot commented Apr 23, 2026

Greptile Summary

This PR fixes two root-cause bugs in v2 file-open flows and does a clean refactor of the related code paths. The core changes are:

  • v2 editor preference forwarded to server: useOpenInExternalEditor now reads the v2 per-project app choice (via the new useV2ProjectDefaultApp hook) and passes it as an explicit app override to openFileInEditor. The server uses input.app ?? resolveDefaultEditor(...), so v2 projects use the client-side tanstack-db preference while v1 projects continue using the localDb-backed resolveDefaultEditor.
  • Doubled-path bug fixed: resolvePath now throws RelativePathWithoutCwdError instead of falling back to process.cwd(), eliminating the silent path doubling. withResolveGuard converts this into a tRPC BAD_REQUEST with a clear message.
  • cwdworktreePath rename: All call sites use the more semantically precise worktreePath.
  • useV2ProjectDefaultApp extracted: Single hook for reading/writing the v2 open-in preference replaces duplicated tanstack-db query logic.
  • DiffPane and FilesTab simplified: Both components delegate to useOpenInExternalEditor instead of re-implementing logic inline.

Confidence Score: 4/5

PR is safe to merge; the core fix is correct and well-tested, with one P1 UX regression in the loading-state guard that should be addressed.

The root-cause fix (forwarding v2PreferredApp to server, throwing on relative-without-cwd) is logically sound and backed by new tests. The refactor cleanly eliminates duplicated logic. The one concrete regression — showing a misleading toast when host data hasn't loaded yet — is P1 but affects only a short window and tanstack-db is a synchronous local store so the likelihood is low.

apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/useOpenInExternalEditor/useOpenInExternalEditor.ts — loading-state guard change (lines 49-52)

Important Files Changed

Filename Overview
apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/useOpenInExternalEditor/useOpenInExternalEditor.ts Central v2 file-open hook — correctly forwards worktreePath and v2PreferredApp to the server, but the workspaceHost loading guard was simplified in a way that shows a misleading toast on local workspaces before host data resolves; also adds a second workspace query that may be redundant.
apps/desktop/src/lib/trpc/routers/external/helpers.ts Introduces RelativePathWithoutCwdError and makes resolvePath throw it instead of silently falling back to process.cwd() — clean, well-documented safety improvement.
apps/desktop/src/lib/trpc/routers/external/index.ts Renames cwdworktreePath, adds app override to openFileInEditor, adds withResolveGuard for clear BAD_REQUEST errors, and adds an early absolute-path check to openInApp — all correct.
apps/desktop/src/renderer/routes/_authenticated/hooks/useV2ProjectDefaultApp/useV2ProjectDefaultApp.ts New hook — clean single source of truth for v2 per-project open-in app preference; correctly queries v2SidebarProjects and guards setApp when projectId is undefined.
apps/desktop/src/lib/trpc/routers/external/helpers.test.ts Test coverage for the new RelativePathWithoutCwdError and all edge cases (wrapped paths, ~-prefix, file:// URLs, relative with cwd) — comprehensive.
apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/components/V2OpenInMenuButton/V2OpenInMenuButton.tsx Refactored to delegate to useV2ProjectDefaultApp — removes duplicated tanstack-db query logic, no functional change.
apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/components/FilesTab/FilesTab.tsx Removes duplicated host-locality check and open-in-app logic in favour of useOpenInExternalEditor — significant simplification.
apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/DiffPane/DiffPane.tsx Removes inline openFile callback and delegates to useOpenInExternalEditor; old if (!worktreePath) return guard now handled server-side via withResolveGuard.
apps/desktop/src/renderer/screens/main/components/WorkspaceView/RightSidebar/ChangesView/hooks/usePathActions.ts Rename-only change: cwdworktreePath in props interface and internal usage — mechanical, correct.

Comments Outside Diff (1)

  1. apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/useOpenInExternalEditor/useOpenInExternalEditor.ts, line 49-52 (link)

    P1 Misleading error toast when host data is still loading

    When workspaceHost is undefined (the useLiveQuery hasn't resolved yet), workspaceHost?.hostMachineId evaluates to undefined. Since undefined !== machineId is always true, the user gets a "Can't open remote workspace paths in an external editor" toast even on a fully local workspace — simply because the tanstack-db query hadn't returned yet.

    The prior code handled these two cases explicitly:

    // old: silent early-return while loading, toast only if actually remote
    if (!workspaceHost || workspaceHost.hostMachineId !== machineId) {
        if (workspaceHost && workspaceHost.hostMachineId !== machineId) {
            toast.error("Opening in editor is only supported on local workspaces");
        }
        return;
    }

    The new version collapses both states into a single branch that always fires the toast:

    // new: always toasts, even when workspaceHost is undefined
    if (workspaceHost?.hostMachineId !== machineId) {
        toast.error("Can't open remote workspace paths in an external editor");
        return;
    }

    Restoring the two-case check would fix it:

    if (!workspaceHost || workspaceHost.hostMachineId !== machineId) {
        if (workspaceHost && workspaceHost.hostMachineId !== machineId) {
            toast.error("Can't open remote workspace paths in an external editor");
        }
        return;
    }
    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/useOpenInExternalEditor/useOpenInExternalEditor.ts
    Line: 49-52
    
    Comment:
    **Misleading error toast when host data is still loading**
    
    When `workspaceHost` is `undefined` (the `useLiveQuery` hasn't resolved yet), `workspaceHost?.hostMachineId` evaluates to `undefined`. Since `undefined !== machineId` is always `true`, the user gets a "Can't open remote workspace paths in an external editor" toast even on a fully local workspace — simply because the tanstack-db query hadn't returned yet.
    
    The prior code handled these two cases explicitly:
    ```ts
    // old: silent early-return while loading, toast only if actually remote
    if (!workspaceHost || workspaceHost.hostMachineId !== machineId) {
        if (workspaceHost && workspaceHost.hostMachineId !== machineId) {
            toast.error("Opening in editor is only supported on local workspaces");
        }
        return;
    }
    ```
    
    The new version collapses both states into a single branch that always fires the toast:
    ```ts
    // new: always toasts, even when workspaceHost is undefined
    if (workspaceHost?.hostMachineId !== machineId) {
        toast.error("Can't open remote workspace paths in an external editor");
        return;
    }
    ```
    
    Restoring the two-case check would fix it:
    ```ts
    if (!workspaceHost || workspaceHost.hostMachineId !== machineId) {
        if (workspaceHost && workspaceHost.hostMachineId !== machineId) {
            toast.error("Can't open remote workspace paths in an external editor");
        }
        return;
    }
    ```
    
    How can I resolve this? If you propose a fix, please make it concise.
Prompt To Fix All With AI
This is a comment left during a code review.
Path: apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/useOpenInExternalEditor/useOpenInExternalEditor.ts
Line: 49-52

Comment:
**Misleading error toast when host data is still loading**

When `workspaceHost` is `undefined` (the `useLiveQuery` hasn't resolved yet), `workspaceHost?.hostMachineId` evaluates to `undefined`. Since `undefined !== machineId` is always `true`, the user gets a "Can't open remote workspace paths in an external editor" toast even on a fully local workspace — simply because the tanstack-db query hadn't returned yet.

The prior code handled these two cases explicitly:
```ts
// old: silent early-return while loading, toast only if actually remote
if (!workspaceHost || workspaceHost.hostMachineId !== machineId) {
    if (workspaceHost && workspaceHost.hostMachineId !== machineId) {
        toast.error("Opening in editor is only supported on local workspaces");
    }
    return;
}
```

The new version collapses both states into a single branch that always fires the toast:
```ts
// new: always toasts, even when workspaceHost is undefined
if (workspaceHost?.hostMachineId !== machineId) {
    toast.error("Can't open remote workspace paths in an external editor");
    return;
}
```

Restoring the two-case check would fix it:
```ts
if (!workspaceHost || workspaceHost.hostMachineId !== machineId) {
    if (workspaceHost && workspaceHost.hostMachineId !== machineId) {
        toast.error("Can't open remote workspace paths in an external editor");
    }
    return;
}
```

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/useOpenInExternalEditor/useOpenInExternalEditor.ts
Line: 40-43

Comment:
**Redundant workspace query alongside existing `useLiveQuery`**

The hook already queries `v2Workspaces` via `useLiveQuery` to get `projectId`. Adding a second `workspaceTrpc.workspace.get.useQuery` purely to read `worktreePath` means every component that calls `useOpenInExternalEditor` now fires two separate workspace lookups.

If `worktreePath` is a field in the `v2Workspaces` collection, it could be selected in the existing `useLiveQuery` instead:
```ts
.select(({ workspaces, hosts }) => ({
    hostMachineId: hosts?.machineId ?? null,
    projectId: workspaces.projectId ?? null,
    worktreePath: workspaces.worktreePath ?? null,
})),
```

This would eliminate the tRPC round-trip and keep the callback's dependencies driven by a single reactive source.

How can I resolve this? If you propose a fix, please make it concise.

Reviews (1): Last reviewed commit: "docs(desktop): mention worktreePath not ..." | Re-trigger Greptile

Comment on lines +40 to +43
const workspaceQuery = workspaceTrpc.workspace.get.useQuery({
id: workspaceId,
});
const worktreePath = workspaceQuery.data?.worktreePath ?? undefined;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Redundant workspace query alongside existing useLiveQuery

The hook already queries v2Workspaces via useLiveQuery to get projectId. Adding a second workspaceTrpc.workspace.get.useQuery purely to read worktreePath means every component that calls useOpenInExternalEditor now fires two separate workspace lookups.

If worktreePath is a field in the v2Workspaces collection, it could be selected in the existing useLiveQuery instead:

.select(({ workspaces, hosts }) => ({
    hostMachineId: hosts?.machineId ?? null,
    projectId: workspaces.projectId ?? null,
    worktreePath: workspaces.worktreePath ?? null,
})),

This would eliminate the tRPC round-trip and keep the callback's dependencies driven by a single reactive source.

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/useOpenInExternalEditor/useOpenInExternalEditor.ts
Line: 40-43

Comment:
**Redundant workspace query alongside existing `useLiveQuery`**

The hook already queries `v2Workspaces` via `useLiveQuery` to get `projectId`. Adding a second `workspaceTrpc.workspace.get.useQuery` purely to read `worktreePath` means every component that calls `useOpenInExternalEditor` now fires two separate workspace lookups.

If `worktreePath` is a field in the `v2Workspaces` collection, it could be selected in the existing `useLiveQuery` instead:
```ts
.select(({ workspaces, hosts }) => ({
    hostMachineId: hosts?.machineId ?? null,
    projectId: workspaces.projectId ?? null,
    worktreePath: workspaces.worktreePath ?? null,
})),
```

This would eliminate the tRPC round-trip and keep the callback's dependencies driven by a single reactive source.

How can I resolve this? If you propose a fix, please make it concise.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

SelectV2Workspace is inferred from the Drizzle schema in packages/db/src/schema/schema.ts which does not include worktreePath — v2 worktree paths are computed on the host-service side and returned via workspaceTrpc.workspace.get, they are not replicated into the tanstack-db collection. The second query is the canonical source (used throughout v2: page.tsx, FilesTab, WorkspaceDiff, useWorkspaceChatController, etc.), and react-query dedupes the network call across callsites by query key.

Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

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

No issues found across 16 files

…n-any-file-to-open-external-we-should-be-using-the-cmdo-choice-the-user-made

# Conflicts:
#	bun.lock
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

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

1 issue found across 16 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/DiffPane/DiffPane.tsx">

<violation number="1" location="apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/DiffPane/DiffPane.tsx:56">
P2: Keep the worktreePath guard before opening diff files. This wiring can call the external-editor mutation with `worktreePath: undefined`, which now throws for relative diff paths.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

},
[worktreePath, projectId],
);
const openInExternalEditor = useOpenInExternalEditor(workspaceId);
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot Apr 23, 2026

Choose a reason for hiding this comment

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

P2: Keep the worktreePath guard before opening diff files. This wiring can call the external-editor mutation with worktreePath: undefined, which now throws for relative diff paths.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/DiffPane/DiffPane.tsx, line 56:

<comment>Keep the worktreePath guard before opening diff files. This wiring can call the external-editor mutation with `worktreePath: undefined`, which now throws for relative diff paths.</comment>

<file context>
@@ -55,24 +53,7 @@ export function DiffPane({ context, workspaceId }: DiffPaneProps) {
-		},
-		[worktreePath, projectId],
-	);
+	const openInExternalEditor = useOpenInExternalEditor(workspaceId);
 
 	// O(1) collapsed lookup per child instead of Array.includes.
</file context>
Fix with Cubic

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Apr 23, 2026

🚀 Preview Deployment

🔗 Preview Links

Service Status Link
Neon Database (Neon) View Branch
Vercel API (Vercel) Open Preview
Vercel Web (Vercel) Open Preview
Vercel Marketing (Vercel) Open Preview
Vercel Admin (Vercel) Open Preview
Vercel Docs (Vercel) Open Preview

Preview updates automatically with new commits

@Kitenite Kitenite merged commit ae5cd60 into main Apr 23, 2026
7 of 8 checks passed
@Kitenite Kitenite deleted the in-v2-when-clicking-on-any-file-to-open-external-we-should-be-using-the-cmdo-choice-the-user-made branch April 23, 2026 16:04
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