Skip to content

feat(desktop): modifier+click in file tree — external editor / new tab - #3398

Merged
saddlepaddle merged 3 commits into
mainfrom
can-we-wire-up-meta-left-click-to-open-file-in-file-tree-in-external-editor-metashiftleft-click-or-j
Apr 13, 2026
Merged

saddlepaddle merged 3 commits into
mainfrom
can-we-wire-up-meta-left-click-to-open-file-in-file-tree-in-external-editor-metashiftleft-click-or-j

Conversation

@saddlepaddle

@saddlepaddle saddlepaddle commented Apr 13, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

  • Meta/Ctrl+click a file or folder in the file tree → opens in the configured external editor (Finder fallback if none set)
  • Shift+click a file → opens it in a new internal tab
  • Plain click / double-click / keyboard behavior unchanged

Wired up in both v1 (FileTreeItem, FileSearchResultItem) and v2 (WorkspaceFilesTreeItem) file trees — v2 had no modifier-key handling at all before, so Meta+click on a remote-host workspace would otherwise call local electron IPC against a path that doesn't exist on the user's machine. v2's handleOpenInEditor guards on hostMachineId === machineId (same pattern as layout.tsx and V2WorkspaceOpenInButton) and toasts "only supported on local workspaces" otherwise.

v2 also gains a real "open in new tab" pathway — openFilePane now accepts openInNewTab? and calls store.addTab(...) when true.

Test plan

  • v1 workspace: plain click a file → previews; Shift+click → new tab; Meta+click a file → external editor; Meta+click a folder → folder opens in editor/Finder
  • v2 local workspace: same four gestures work
  • v2 remote workspace: Meta+click shows "Opening in editor is only supported on local workspaces" toast; plain and Shift+click still work
  • Search results list (v1) mirrors the file-row behavior
  • Cmd+Enter on a keyboard-focused file still opens a new tab (regression check)

Summary by cubic

Add modifier-clicks to the desktop file tree: Meta/Ctrl+click opens items in the external app, and Shift+click opens a file in a new tab. Works in v1 and v2; v2 respects the per-project “Open In” setting and handles remote workspaces safely.

  • New Features

    • Meta/Ctrl+click opens files and folders via electronTrpcClient.external.openInApp; v2 resolves the app from v2SidebarProjects.defaultOpenInApp (fallback finder) and toasts on remote hosts.
    • Shift+click opens a new tab through openFilePane(filePath, openInNewTab?); plain click, double-click, and keyboard shortcuts are unchanged.
  • Bug Fixes

    • v2: defer the local/remote check until the host query resolves; Meta/Ctrl+click before it settles no-ops instead of showing a misleading toast.
    • New-tab titles use a separator-safe split, so Windows paths show only the filename.

Written for commit 444a897. Summary will update on new commits.

Summary by CodeRabbit

  • New Features
    • Shift-click opens files in a new tab; Meta/Ctrl-click opens files in the external editor.
    • File selection across workspace sidebar, file tree and search results supports the "open in new tab" option.
    • New "open in editor" action attempts to open files in the workspace's external app and shows an error when not permitted.

…w tab

- Meta/Ctrl+click: open in external editor (works on files and folders)
- Shift+click: open file in a new internal tab
- Plain click: unchanged (preview pane / toggle folder)

Adds the behavior to both v1 (FileTreeItem, FileSearchResultItem) and v2
(WorkspaceFilesTreeItem) file trees. v2 guards the external-editor path
with an isLocalWorkspace check so remote hosts toast instead of silently
calling local electron IPC against a path that does not exist.
@coderabbitai

coderabbitai Bot commented Apr 13, 2026 •

Copy link
Copy Markdown
Contributor
📝 Walkthrough

Walkthrough

Added an optional openInNewTab flag to file-selection callbacks and introduced onOpenInEditor to open files in external editors. Click handlers now interpret modifiers (shift → open in new tab, meta/ctrl → open in editor). FilesTab verifies workspace locality and calls an Electron RPC to open external apps.

Changes

Cohort / File(s) Summary
Sidebar / Top-level Props
apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/WorkspaceSidebar.tsx
Extended WorkspaceSidebarProps.onSelectFile to (absolutePath: string, openInNewTab?: boolean) => void and forwarded it to FilesTab.
FilesTab & Live-query / RPC
apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/components/FilesTab/FilesTab.tsx
Updated onSelectFile signature; added onOpenInEditor; live-queries workspace/host to determine locality and default app; implemented handleOpenInEditor that calls electronTrpcClient.external.openInApp.mutate(...) and surfaces errors via toast.
Workspace Tree Item
apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/components/FilesTab/components/WorkspaceFilesTreeItem/WorkspaceFilesTreeItem.tsx
Added onOpenInEditor prop; extended onSelectFile to accept openInNewTab; click handler branches on modifiers: meta/ctrl → onOpenInEditor, shift → onSelectFile(path, true), default → onSelectFile(path); folders still toggle via onToggleDirectory.
Main Workspace Tree & Search Items
apps/desktop/src/renderer/screens/main/components/WorkspaceView/RightSidebar/FilesView/components/FileTreeItem/FileTreeItem.tsx, apps/desktop/src/renderer/screens/main/components/WorkspaceView/RightSidebar/FilesView/components/FileSearchResultItem/FileSearchResultItem.tsx
Adjusted click handlers: meta/ctrl now invokes onOpenInEditor(...) before expand/collapse; shift-click invokes onActivate(..., true) to open in a new tab; default activation unchanged.
Page-level Tab Logic
apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/page.tsx
openFilePane accepts openInNewTab?: boolean; when truthy creates a new tab via state.addTab with a single file pane (editor mode) and returns; otherwise preserves previous pane-opening logic.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant TreeItem as FilesTab/TreeItem
    participant LocalQuery as useLiveQuery\n(v2Workspaces & v2Hosts)
    participant ElectronRPC as Electron tRPC\nexternal.openInApp
    participant Toast as Toast Notifier

    User->>TreeItem: Click file (meta/ctrl)
    TreeItem->>LocalQuery: Query workspace locality & default app
    LocalQuery-->>TreeItem: { isLocal, defaultOpenInApp }
    alt isLocal
        TreeItem->>ElectronRPC: mutate({ path, app, cwd, projectId })
        ElectronRPC-->>TreeItem: success / error
        alt error
            TreeItem->>Toast: show error
        else success
            TreeItem-->>User: editor opened
        end
    else notLocal
        TreeItem->>Toast: show "workspace not local" error
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 I nibble keys and hop with glee,

Shift makes a new tab bloom for me.
Meta/Ctrl ring the editor bell,
I asked the host, then called the shell.
Hooray — files open, quick as can be!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately reflects the main change: adding modifier+click functionality (Meta/Ctrl for external editor, Shift for new tab) to the file tree.
Description check ✅ Passed The PR description is comprehensive and includes all required template sections with detailed context, test plan, and implementation notes.

✏️ 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 can-we-wire-up-meta-left-click-to-open-file-in-file-tree-in-external-editor-metashiftleft-click-or-j

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.

@greptile-apps

greptile-apps Bot commented Apr 13, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds modifier-key click handling to both the v1 (FileTreeItem, FileSearchResultItem) and v2 (WorkspaceFilesTreeItem) file trees: Meta/Ctrl+click opens a file or folder in the configured external editor, and Shift+click opens a file in a new internal tab. Plain click, double-click, and keyboard behavior are unchanged.

Key additions:

  • WorkspaceFilesTreeItem gains onOpenInEditor prop and a three-branch click handler (meta → editor, shift → new tab, default → preview/toggle)
  • FilesTab introduces handleOpenInEditor with a local-workspace guard, backed by a new useLiveQuery join
  • openFilePane in page.tsx gains an openInNewTab? flag that calls store.addTab(...) directly
  • v1 FileTreeItem reorders its handler so Meta/Ctrl fires before the folder-toggle branch (intentional)

One P1 concern: isLocalWorkspace in FilesTab.tsx is false while the live query is loading, causing a misleading error toast on Meta+click before data resolves.

Confidence Score: 4/5

Safe to merge after addressing the loading-state false-negative in handleOpenInEditor

One P1 UX bug (spurious toast during query loading window) needs a small guard fix; the rest of the feature is well-structured and correct

FilesTab.tsx — isLocalWorkspace evaluation needs a loading-state guard before the error toast is reachable

Important Files Changed

Filename Overview
apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/components/FilesTab/FilesTab.tsx Adds handleOpenInEditor with local-workspace guard, but isLocalWorkspace is false during the live query loading window causing spurious error toasts; also introduces a duplicate workspace-host live query.
apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/components/FilesTab/components/WorkspaceFilesTreeItem/WorkspaceFilesTreeItem.tsx Adds modifier-key click handling with correct priority ordering; delegation of onOpenInEditor is clean.
apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/page.tsx Extends openFilePane with openInNewTab flag; consistent with other addTab callsites.
apps/desktop/src/renderer/screens/main/components/WorkspaceView/RightSidebar/FilesView/components/FileTreeItem/FileTreeItem.tsx Reorders click-handler priority; Meta/Ctrl now fires before folder-toggle; Shift adds new-tab path.
apps/desktop/src/renderer/screens/main/components/WorkspaceView/RightSidebar/FilesView/components/FileSearchResultItem/FileSearchResultItem.tsx Adds Shift+click → new tab; mirrors FileTreeItem pattern correctly.

Reviews (1): Last reviewed commit: "feat(desktop): modifier+click in file tr..." | Re-trigger Greptile

Comment on lines +239 to +259
const isLocalWorkspace = workspacesWithHost[0]?.hostMachineId === machineId;

const handleOpenInEditor = useCallback(
(absolutePath: string) => {
if (!isLocalWorkspace) {
toast.error("Opening in editor is only supported on local workspaces");
return;
}
electronTrpcClient.external.openFileInEditor
.mutate({
path: absolutePath,
cwd: rootPath || undefined,
projectId,
})
.catch((err) => {
toast.error("Couldn't open file", {
description: err instanceof Error ? err.message : String(err),
});
});
},
[isLocalWorkspace, rootPath, projectId],

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 False-negative isLocalWorkspace during loading shows spurious error toast

workspacesWithHost defaults to [] (via = []) while the live query is still resolving. During that window workspacesWithHost[0]?.hostMachineId is undefined, so undefined === machineId evaluates to false — making isLocalWorkspace always false until data arrives. Because handleOpenInEditor immediately toasts an error when !isLocalWorkspace, a user who Meta+clicks a file in a local workspace before the query resolves will see the misleading "Opening in editor is only supported on local workspaces" toast.

In contrast, V2WorkspaceOpenInButton silently hides the button while loading (Boolean(workspace) && ...), so there's no visible error path during the loading window. Here the file-tree rows are already rendered and clickable, so the guard needs to be explicit.

Consider distinguishing the "not yet loaded" state from the "loaded but remote" state:

const hasLoaded = workspacesWithHost.length > 0;
const isLocalWorkspace =
  hasLoaded && workspacesWithHost[0]?.hostMachineId === machineId;

const handleOpenInEditor = useCallback(
  (absolutePath: string) => {
    if (!hasLoaded) return; // Still initialising — silently abort
    if (!isLocalWorkspace) {
      toast.error("Opening in editor is only supported on local workspaces");
      return;
    }
    // ...
  },
  [hasLoaded, isLocalWorkspace, rootPath, projectId],
);

const state = store.getState();
if (openInNewTab) {
state.addTab({
titleOverride: filePath.split("/").pop(),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 pop() can return undefined for edge-case paths

String.prototype.split("/").pop() returns undefined when the resulting array is empty. Since CreateTabInput.titleOverride is typed as string | undefined, TypeScript accepts this and runtime behavior is fine (tab gets no title). A fallback makes the intent clearer:

Suggested change
titleOverride: filePath.split("/").pop(),
titleOverride: filePath.split("/").pop() ?? filePath,

Comment on lines +224 to +239
const collections = useCollections();
const { machineId } = useLocalHostService();
const { data: workspacesWithHost = [] } = useLiveQuery(
(q) =>
q
.from({ workspaces: collections.v2Workspaces })
.leftJoin({ hosts: collections.v2Hosts }, ({ workspaces, hosts }) =>
eq(workspaces.hostId, hosts.id),
)
.where(({ workspaces }) => eq(workspaces.id, workspaceId))
.select(({ hosts }) => ({
hostMachineId: hosts?.machineId ?? null,
})),
[collections, workspaceId],
);
const isLocalWorkspace = workspacesWithHost[0]?.hostMachineId === machineId;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 Duplicated workspace-host live query

layout.tsx and V2WorkspaceOpenInButton already resolve hostMachineId and compute isLocal for this same workspace. Adding a third independent live query here spreads the join logic further and adds unnecessary reactive overhead. Consider lifting isLocalWorkspace from the layout as a prop (WorkspaceSidebar → FilesTab), or exposing it via shared context.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

…idebarProjects

Align with the per-project v2 "Open In" pattern established in #3393:
resolve the editor from collections.v2SidebarProjects.defaultOpenInApp
(falling back to finder) and call electronTrpc.external.openInApp —
instead of openFileInEditor, which does v1-era backend resolution off
the projects.defaultApp column that v2 doesn't write to.

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

2 issues found across 6 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/page.tsx">

<violation number="1" location="apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/page.tsx:111">
P2: Deriving the tab title with `split("/")` is not cross-platform and breaks basename extraction for Windows paths. Use a separator-safe approach.

(Based on your team's feedback about using cross-platform path utilities instead of manual split.) [FEEDBACK_USED]</violation>
</file>

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

<violation number="1" location="apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/components/FilesTab/FilesTab.tsx:239">
P2: `isLocalWorkspace` is `false` while the live query is still resolving (the default `[]` makes `workspacesWithHost[0]?.hostMachineId` evaluate to `undefined`). A user who Meta+clicks a file before the query completes will see the misleading "only supported on local workspaces" toast even on a local workspace. Distinguish "not yet loaded" from "loaded but remote" — e.g. gate `handleOpenInEditor` on `workspacesWithHost.length > 0` so it silently no-ops until data arrives.</violation>
</file>

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

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 the current code and only fix it if needed.

Inline comments:
In
`@apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/`$workspaceId/components/WorkspaceSidebar/components/FilesTab/FilesTab.tsx:
- Around line 227-267: The issue is that useLiveQuery results are defaulted to
[] which hides the loading state and causes isLocalWorkspace and
resolvedOpenInApp to be derived incorrectly; remove the default [] for
workspacesWithHost and sidebarProjectRows and add explicit undefined checks
before using them (e.g., don't compute isLocalWorkspace or resolvedOpenInApp
until workspacesWithHost and sidebarProjectRows are not undefined), update
handleOpenInEditor to guard early if those query results are undefined (show a
loading/disabled state or no-op) and ensure useCallback dependencies include any
values used for the guards (workspacesWithHost, sidebarProjectRows, machineId,
projectId) so the derived behavior updates once queries resolve.
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: d858e7b4-b1a0-4fcb-808b-d5e46c04428d

📥 Commits

Reviewing files that changed from the base of the PR and between 967a8f6 and a82beec.

📒 Files selected for processing (1)
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/components/FilesTab/FilesTab.tsx

Comment on lines +227 to +267
const { data: workspacesWithHost = [] } = useLiveQuery(
(q) =>
q
.from({ workspaces: collections.v2Workspaces })
.leftJoin({ hosts: collections.v2Hosts }, ({ workspaces, hosts }) =>
eq(workspaces.hostId, hosts.id),
)
.where(({ workspaces }) => eq(workspaces.id, workspaceId))
.select(({ hosts }) => ({
hostMachineId: hosts?.machineId ?? null,
})),
[collections, workspaceId],
);
const isLocalWorkspace = workspacesWithHost[0]?.hostMachineId === machineId;

const { data: sidebarProjectRows = [] } = useLiveQuery(
(q) =>
q
.from({ sp: collections.v2SidebarProjects })
.where(({ sp }) => eq(sp.projectId, projectId ?? ""))
.select(({ sp }) => ({ defaultOpenInApp: sp.defaultOpenInApp })),
[collections, projectId],
);
const resolvedOpenInApp: ExternalApp =
(sidebarProjectRows[0]?.defaultOpenInApp as ExternalApp | null) ?? "finder";

const handleOpenInEditor = useCallback(
(absolutePath: string) => {
if (!isLocalWorkspace) {
toast.error("Opening in editor is only supported on local workspaces");
return;
}
electronTrpcClient.external.openInApp
.mutate({ path: absolutePath, app: resolvedOpenInApp })
.catch((err) => {
toast.error("Couldn't open file", {
description: err instanceof Error ? err.message : String(err),
});
});
},
[isLocalWorkspace, resolvedOpenInApp],

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🌐 Web query:

For the version of @tanstack/react-dbused in this repository, canuseLiveQueryexposedataasundefined before the first result is available?

💡 Result:

Yes, useLiveQuery can expose data as undefined before the first result is available.

Citations:


🏁 Script executed:

# Check the version of `@tanstack/react-db` in the repo
fd -t f "package.json" | head -1 | xargs cat | jq '.dependencies, .devDependencies' | grep -A 2 -B 2 "tanstack/react-db"

Repository: superset-sh/superset

Length of output: 46


🏁 Script executed:

# Search for useLiveQuery usages in the codebase to understand patterns
rg "useLiveQuery" --type tsx --type ts -B 2 -A 8 | head -100

Repository: superset-sh/superset

Length of output: 91


🏁 Script executed:

# Check the specific file to see the full context of handleOpenInEditor usage
cat -n apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/\$workspaceId/components/WorkspaceSidebar/components/FilesTab/FilesTab.tsx | sed -n '227,280p'

Repository: superset-sh/superset

Length of output: 2229


🏁 Script executed:

# Find and check package.json
find . -name "package.json" -type f -exec grep -l "tanstack/react-db" {} \; | head -1 | xargs cat | grep -A 1 "tanstack/react-db"

Repository: superset-sh/superset

Length of output: 190


🏁 Script executed:

# Search for useLiveQuery usages with proper type flags
rg "useLiveQuery" -A 5 | head -150

Repository: superset-sh/superset

Length of output: 17996


🏁 Script executed:

# Check if there are existing tri-state patterns for loading in the codebase
rg "data\s*=" -A 2 | grep -E "(undefined|null)" | head -50

Repository: superset-sh/superset

Length of output: 2149


🏁 Script executed:

# Check if FilesTab.tsx uses isLoading flag from useLiveQuery
grep -n "isLoading\|isFetching\|isLoading\|isLoading" apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/\$workspaceId/components/WorkspaceSidebar/components/FilesTab/FilesTab.tsx | head -20

Repository: superset-sh/superset

Length of output: 98


🏁 Script executed:

# Check how other similar components handle loading state - look at AccountSettings pattern more closely
sed -n '1,50p' apps/desktop/src/renderer/routes/_authenticated/settings/account/components/AccountSettings/AccountSettings.tsx

Repository: superset-sh/superset

Length of output: 1759


🏁 Script executed:

# Verify the handleOpenInEditor is called directly from UI or through a guarded callback
grep -n "handleOpenInEditor\|onOpenInEditor" apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/\$workspaceId/components/WorkspaceSidebar/components/FilesTab/FilesTab.tsx | head -20

Repository: superset-sh/superset

Length of output: 316


Check loading state before deriving locality and app configuration.

useLiveQuery initially returns data: undefined before the first result arrives. The current code defaults to [], which conflates loading with empty-result, causing isLocalWorkspace to evaluate as false and resolvedOpenInApp to default to "finder" during mount. A quick modifier-click right after the component mounts will incorrectly show the remote-workspace toast for a local workspace, or open Finder before the configured editor preference loads.

Remove the default [] values and check for undefined before using the results:

Suggested fix
- const { data: workspacesWithHost = [] } = useLiveQuery(
+ const { data: workspacesWithHost } = useLiveQuery(
    ...
  );
- const isLocalWorkspace = workspacesWithHost[0]?.hostMachineId === machineId;
+ const isLocalWorkspace =
+   workspacesWithHost == null
+     ? undefined
+     : workspacesWithHost[0]?.hostMachineId === machineId;

- const { data: sidebarProjectRows = [] } = useLiveQuery(
+ const { data: sidebarProjectRows } = useLiveQuery(
    ...
  );
- const resolvedOpenInApp: ExternalApp =
-   (sidebarProjectRows[0]?.defaultOpenInApp as ExternalApp | null) ?? "finder";
+ const resolvedOpenInApp: ExternalApp | undefined =
+   sidebarProjectRows == null
+     ? undefined
+     : ((sidebarProjectRows[0]?.defaultOpenInApp as ExternalApp | null) ??
+         "finder");

  const handleOpenInEditor = useCallback(
    (absolutePath: string) => {
+     if (isLocalWorkspace == null || resolvedOpenInApp == null) return;
      if (!isLocalWorkspace) {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const { data: workspacesWithHost = [] } = useLiveQuery(
(q) =>
q
.from({ workspaces: collections.v2Workspaces })
.leftJoin({ hosts: collections.v2Hosts }, ({ workspaces, hosts }) =>
eq(workspaces.hostId, hosts.id),
)
.where(({ workspaces }) => eq(workspaces.id, workspaceId))
.select(({ hosts }) => ({
hostMachineId: hosts?.machineId ?? null,
})),
[collections, workspaceId],
);
const isLocalWorkspace = workspacesWithHost[0]?.hostMachineId === machineId;
const { data: sidebarProjectRows = [] } = useLiveQuery(
(q) =>
q
.from({ sp: collections.v2SidebarProjects })
.where(({ sp }) => eq(sp.projectId, projectId ?? ""))
.select(({ sp }) => ({ defaultOpenInApp: sp.defaultOpenInApp })),
[collections, projectId],
);
const resolvedOpenInApp: ExternalApp =
(sidebarProjectRows[0]?.defaultOpenInApp as ExternalApp | null) ?? "finder";
const handleOpenInEditor = useCallback(
(absolutePath: string) => {
if (!isLocalWorkspace) {
toast.error("Opening in editor is only supported on local workspaces");
return;
}
electronTrpcClient.external.openInApp
.mutate({ path: absolutePath, app: resolvedOpenInApp })
.catch((err) => {
toast.error("Couldn't open file", {
description: err instanceof Error ? err.message : String(err),
});
});
},
[isLocalWorkspace, resolvedOpenInApp],
const { data: workspacesWithHost } = useLiveQuery(
(q) =>
q
.from({ workspaces: collections.v2Workspaces })
.leftJoin({ hosts: collections.v2Hosts }, ({ workspaces, hosts }) =>
eq(workspaces.hostId, hosts.id),
)
.where(({ workspaces }) => eq(workspaces.id, workspaceId))
.select(({ hosts }) => ({
hostMachineId: hosts?.machineId ?? null,
})),
[collections, workspaceId],
);
const isLocalWorkspace =
workspacesWithHost == null
? undefined
: workspacesWithHost[0]?.hostMachineId === machineId;
const { data: sidebarProjectRows } = useLiveQuery(
(q) =>
q
.from({ sp: collections.v2SidebarProjects })
.where(({ sp }) => eq(sp.projectId, projectId ?? ""))
.select(({ sp }) => ({ defaultOpenInApp: sp.defaultOpenInApp })),
[collections, projectId],
);
const resolvedOpenInApp: ExternalApp | undefined =
sidebarProjectRows == null
? undefined
: ((sidebarProjectRows[0]?.defaultOpenInApp as ExternalApp | null) ??
"finder");
const handleOpenInEditor = useCallback(
(absolutePath: string) => {
if (isLocalWorkspace == null || resolvedOpenInApp == null) return;
if (!isLocalWorkspace) {
toast.error("Opening in editor is only supported on local workspaces");
return;
}
electronTrpcClient.external.openInApp
.mutate({ path: absolutePath, app: resolvedOpenInApp })
.catch((err) => {
toast.error("Couldn't open file", {
description: err instanceof Error ? err.message : String(err),
});
});
},
[isLocalWorkspace, resolvedOpenInApp],
🤖 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 227 - 267, The issue is that useLiveQuery results are defaulted to
[] which hides the loading state and causes isLocalWorkspace and
resolvedOpenInApp to be derived incorrectly; remove the default [] for
workspacesWithHost and sidebarProjectRows and add explicit undefined checks
before using them (e.g., don't compute isLocalWorkspace or resolvedOpenInApp
until workspacesWithHost and sidebarProjectRows are not undefined), update
handleOpenInEditor to guard early if those query results are undefined (show a
loading/disabled state or no-op) and ensure useCallback dependencies include any
values used for the guards (workspacesWithHost, sidebarProjectRows, machineId,
projectId) so the derived behavior updates once queries resolve.

- Gate the isLocal check on the host live query actually resolving, so
  a meta+click before the collections query settles silently no-ops
  instead of toasting a misleading "only supported on local workspaces"
  error (cubic/greptile/coderabbit).
- Use a separator-safe split when deriving the new-tab title from a file
  path so Windows host paths (backslash-separated) don't produce a full
  path as the tab title (cubic).

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

♻️ Duplicate comments (1)
apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/components/FilesTab/FilesTab.tsx (1)

227-251: ⚠️ Potential issue | 🟡 Minor

Don’t collapse unresolved live-query state into Finder/remote fallbacks.

hostMachineId is explicitly nullable here, and sidebarProjectRows = [] turns “still loading” into “no preference configured”. A quick modifier-click can still show the remote-workspace toast while the host join is unresolved, or open Finder before defaultOpenInApp has loaded.

Suggested fix
- const { data: workspacesWithHost = [] } = useLiveQuery(
+ const { data: workspacesWithHost } = useLiveQuery(
    ...
  );
- const workspaceHost = workspacesWithHost[0];
+ const workspaceHost = workspacesWithHost?.[0];

- const { data: sidebarProjectRows = [] } = useLiveQuery(
+ const { data: sidebarProjectRows } = useLiveQuery(
    ...
  );
- const resolvedOpenInApp: ExternalApp =
-   (sidebarProjectRows[0]?.defaultOpenInApp as ExternalApp | null) ?? "finder";
+ const resolvedOpenInApp: ExternalApp | undefined =
+   sidebarProjectRows == null
+     ? undefined
+     : ((sidebarProjectRows[0]?.defaultOpenInApp as ExternalApp | null) ??
+         "finder");

  const handleOpenInEditor = useCallback(
    (absolutePath: string) => {
-     if (!workspaceHost) return;
-     if (workspaceHost.hostMachineId !== machineId) {
+     if (!workspaceHost || workspaceHost.hostMachineId == null) return;
+     if (resolvedOpenInApp == null) return;
+     if (workspaceHost.hostMachineId !== machineId) {
        toast.error("Opening in editor is only supported on local workspaces");
        return;
      }

Also applies to: 253-268

🤖 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 227 - 251, The live-query results are being collapsed into
fallbacks too early; stop treating an unresolved query as an empty result by
checking for undefined before mapping to fallback values. In the useLiveQuery
for workspacesWithHost and for sidebarProjectRows, keep the raw hook result
(e.g. workspacesWithHost may be undefined while loading) and only derive
workspaceHost.hostMachineId and resolvedOpenInApp when the corresponding data
array is present and has an element; reference useLiveQuery, workspacesWithHost,
workspaceHost, hostMachineId, sidebarProjectRows, defaultOpenInApp, and
resolvedOpenInApp and ensure you only apply the "finder" fallback after
confirming sidebarProjectRows is loaded (not when it's undefined).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In
`@apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/`$workspaceId/components/WorkspaceSidebar/components/FilesTab/FilesTab.tsx:
- Around line 227-251: The live-query results are being collapsed into fallbacks
too early; stop treating an unresolved query as an empty result by checking for
undefined before mapping to fallback values. In the useLiveQuery for
workspacesWithHost and for sidebarProjectRows, keep the raw hook result (e.g.
workspacesWithHost may be undefined while loading) and only derive
workspaceHost.hostMachineId and resolvedOpenInApp when the corresponding data
array is present and has an element; reference useLiveQuery, workspacesWithHost,
workspaceHost, hostMachineId, sidebarProjectRows, defaultOpenInApp, and
resolvedOpenInApp and ensure you only apply the "finder" fallback after
confirming sidebarProjectRows is loaded (not when it's undefined).

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: e5ce086a-acd5-405f-9425-ae5d32762481

📥 Commits

Reviewing files that changed from the base of the PR and between a82beec and 444a897.

📒 Files selected for processing (2)
  • 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/page.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/page.tsx

@saddlepaddle
saddlepaddle merged commit 8f4480b into main Apr 13, 2026
7 checks passed
@github-actions

Copy link
Copy Markdown
Contributor

🧹 Preview Cleanup Complete

The following preview resources have been cleaned up:

  • ✅ Neon database branch
  • ⚠️ Electric Fly.io app

Thank you for your contribution! 🎉

MocA-Love pushed a commit to MocA-Love/superset that referenced this pull request Apr 13, 2026
ParadevAI#3398)

* feat(desktop): modifier+click in file tree opens external editor / new tab

- Meta/Ctrl+click: open in external editor (works on files and folders)
- Shift+click: open file in a new internal tab
- Plain click: unchanged (preview pane / toggle folder)

Adds the behavior to both v1 (FileTreeItem, FileSearchResultItem) and v2
(WorkspaceFilesTreeItem) file trees. v2 guards the external-editor path
with an isLocalWorkspace check so remote hosts toast instead of silently
calling local electron IPC against a path that does not exist.

* fix(desktop): v2 file-tree meta+click reads defaultOpenInApp from v2SidebarProjects

Align with the per-project v2 "Open In" pattern established in ParadevAI#3393:
resolve the editor from collections.v2SidebarProjects.defaultOpenInApp
(falling back to finder) and call electronTrpc.external.openInApp —
instead of openFileInEditor, which does v1-era backend resolution off
the projects.defaultApp column that v2 doesn't write to.

* fix(desktop): address PR review on v2 file-tree meta+click

- Gate the isLocal check on the host live query actually resolving, so
  a meta+click before the collections query settles silently no-ops
  instead of toasting a misleading "only supported on local workspaces"
  error (cubic/greptile/coderabbit).
- Use a separator-safe split when deriving the new-tab title from a file
  path so Windows host paths (backslash-separated) don't produce a full
  path as the tab title (cubic).
@Kitenite
Kitenite deleted the can-we-wire-up-meta-left-click-to-open-file-in-file-tree-in-external-editor-metashiftleft-click-or-j branch April 13, 2026 16:35
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