From 2d2ee02ce33704442ed0ab5dad08413289213734 Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Thu, 7 May 2026 18:38:14 +0700 Subject: [PATCH] Gate workspace open targets by daemon locality --- .../workspace-open-in-editor-button.tsx | 25 +++++++++---- .../workspace/workspace-open-targets.test.ts | 36 +++++++++++++++++++ .../workspace/workspace-open-targets.ts | 13 +++++++ 3 files changed, 68 insertions(+), 6 deletions(-) create mode 100644 packages/app/src/screens/workspace/workspace-open-targets.test.ts create mode 100644 packages/app/src/screens/workspace/workspace-open-targets.ts diff --git a/packages/app/src/screens/workspace/workspace-open-in-editor-button.tsx b/packages/app/src/screens/workspace/workspace-open-in-editor-button.tsx index 06745aff4..24f29dbf8 100644 --- a/packages/app/src/screens/workspace/workspace-open-in-editor-button.tsx +++ b/packages/app/src/screens/workspace/workspace-open-in-editor-button.tsx @@ -20,6 +20,7 @@ import { } from "@/components/ui/dropdown-menu"; import { useToast } from "@/contexts/toast-context"; import { useCheckoutStatusQuery } from "@/hooks/use-checkout-status-query"; +import { useIsLocalDaemon } from "@/hooks/use-is-local-daemon"; import { useHostRuntimeClient, useHostRuntimeIsConnected } from "@/runtime/host-runtime"; import { resolvePreferredEditorId, usePreferredEditor } from "@/hooks/use-preferred-editor"; import { buildGitHubBranchTreeUrl } from "@/utils/github-repo-url"; @@ -27,6 +28,7 @@ import { openExternalUrl } from "@/utils/open-external-url"; import { isAbsolutePath } from "@/utils/path"; import { isWeb } from "@/constants/platform"; import type { Theme } from "@/styles/theme"; +import { filterTargetsForDaemonLocation } from "./workspace-open-targets"; interface WorkspaceOpenInEditorButtonProps { serverId: string; @@ -38,6 +40,7 @@ interface OpenTarget { id: string; label: string; icon: ReactElement; + requiresLocalDaemon: boolean; onOpen: () => Promise | void; } @@ -82,14 +85,16 @@ export function WorkspaceOpenInEditorButton({ const toast = useToast(); const client = useHostRuntimeClient(serverId); const isConnected = useHostRuntimeIsConnected(serverId); + const isLocalDaemon = useIsLocalDaemon(serverId); const { preferredEditorId, updatePreferredEditor } = usePreferredEditor(); - const shouldLoadTargets = + const shouldQueryWorkspace = isWeb && Boolean(client && isConnected) && cwd.trim().length > 0 && isAbsolutePath(cwd); + const shouldLoadEditorTargets = shouldQueryWorkspace && isLocalDaemon; const availableEditorsQuery = useQuery({ queryKey: ["available-editors", serverId], - enabled: shouldLoadTargets, + enabled: shouldLoadEditorTargets, staleTime: 60_000, retry: false, queryFn: async () => { @@ -112,7 +117,7 @@ export function WorkspaceOpenInEditorButton({ const { status: checkoutStatus } = useCheckoutStatusQuery({ serverId, - cwd: shouldLoadTargets ? cwd : "", + cwd: shouldQueryWorkspace ? cwd : "", }); const editorTargets = useMemo( @@ -121,6 +126,7 @@ export function WorkspaceOpenInEditorButton({ id: editor.id, label: editor.label, icon: , + requiresLocalDaemon: true, onOpen: async () => { if (!client) { throw new Error("Host is not connected"); @@ -149,13 +155,20 @@ export function WorkspaceOpenInEditorButton({ id: "github", label: "GitHub", icon: , + requiresLocalDaemon: false, onOpen: () => openExternalUrl(url), }; }, [checkoutStatus]); const targets = useMemo( - () => (githubTarget ? [...editorTargets, githubTarget] : editorTargets), - [editorTargets, githubTarget], + () => + filterTargetsForDaemonLocation( + githubTarget ? [...editorTargets, githubTarget] : editorTargets, + { + isLocalDaemon, + }, + ), + [editorTargets, githubTarget, isLocalDaemon], ); const targetIds = useMemo(() => targets.map((target) => target.id), [targets]); @@ -210,7 +223,7 @@ export function WorkspaceOpenInEditorButton({ } }, [primaryOption, handleOpenTarget]); - if (!shouldLoadTargets || !primaryOption || targets.length === 0) { + if (!shouldQueryWorkspace || !primaryOption || targets.length === 0) { return null; } diff --git a/packages/app/src/screens/workspace/workspace-open-targets.test.ts b/packages/app/src/screens/workspace/workspace-open-targets.test.ts new file mode 100644 index 000000000..568307739 --- /dev/null +++ b/packages/app/src/screens/workspace/workspace-open-targets.test.ts @@ -0,0 +1,36 @@ +import { describe, expect, it } from "vitest"; +import { filterTargetsForDaemonLocation } from "./workspace-open-targets"; + +describe("filterTargetsForDaemonLocation", () => { + const targets = [ + { id: "cursor", requiresLocalDaemon: true }, + { id: "vscode", requiresLocalDaemon: true }, + { id: "github", requiresLocalDaemon: false }, + ]; + + it("keeps local app targets and URL targets for the local daemon", () => { + expect(filterTargetsForDaemonLocation(targets, { isLocalDaemon: true })).toEqual(targets); + }); + + it("hides local app targets for a remote daemon", () => { + expect(filterTargetsForDaemonLocation(targets, { isLocalDaemon: false })).toEqual([ + { id: "github", requiresLocalDaemon: false }, + ]); + }); + + it("preserves target order after filtering", () => { + expect( + filterTargetsForDaemonLocation( + [ + { id: "github", requiresLocalDaemon: false }, + { id: "finder", requiresLocalDaemon: true }, + { id: "docs", requiresLocalDaemon: false }, + ], + { isLocalDaemon: false }, + ), + ).toEqual([ + { id: "github", requiresLocalDaemon: false }, + { id: "docs", requiresLocalDaemon: false }, + ]); + }); +}); diff --git a/packages/app/src/screens/workspace/workspace-open-targets.ts b/packages/app/src/screens/workspace/workspace-open-targets.ts new file mode 100644 index 000000000..52efe5914 --- /dev/null +++ b/packages/app/src/screens/workspace/workspace-open-targets.ts @@ -0,0 +1,13 @@ +export interface WorkspaceOpenTargetAvailability { + requiresLocalDaemon: boolean; +} + +export function filterTargetsForDaemonLocation( + targets: readonly Target[], + input: { isLocalDaemon: boolean }, +): Target[] { + if (input.isLocalDaemon) { + return [...targets]; + } + return targets.filter((target) => !target.requiresLocalDaemon); +}