Gate workspace open targets by daemon locality

This commit is contained in:
Mohamed Boudra
2026-05-07 18:38:14 +07:00
parent 4d4fbf7257
commit 2d2ee02ce3
3 changed files with 68 additions and 6 deletions
@@ -20,6 +20,7 @@ import {
} from "@/components/ui/dropdown-menu"; } from "@/components/ui/dropdown-menu";
import { useToast } from "@/contexts/toast-context"; import { useToast } from "@/contexts/toast-context";
import { useCheckoutStatusQuery } from "@/hooks/use-checkout-status-query"; import { useCheckoutStatusQuery } from "@/hooks/use-checkout-status-query";
import { useIsLocalDaemon } from "@/hooks/use-is-local-daemon";
import { useHostRuntimeClient, useHostRuntimeIsConnected } from "@/runtime/host-runtime"; import { useHostRuntimeClient, useHostRuntimeIsConnected } from "@/runtime/host-runtime";
import { resolvePreferredEditorId, usePreferredEditor } from "@/hooks/use-preferred-editor"; import { resolvePreferredEditorId, usePreferredEditor } from "@/hooks/use-preferred-editor";
import { buildGitHubBranchTreeUrl } from "@/utils/github-repo-url"; import { buildGitHubBranchTreeUrl } from "@/utils/github-repo-url";
@@ -27,6 +28,7 @@ import { openExternalUrl } from "@/utils/open-external-url";
import { isAbsolutePath } from "@/utils/path"; import { isAbsolutePath } from "@/utils/path";
import { isWeb } from "@/constants/platform"; import { isWeb } from "@/constants/platform";
import type { Theme } from "@/styles/theme"; import type { Theme } from "@/styles/theme";
import { filterTargetsForDaemonLocation } from "./workspace-open-targets";
interface WorkspaceOpenInEditorButtonProps { interface WorkspaceOpenInEditorButtonProps {
serverId: string; serverId: string;
@@ -38,6 +40,7 @@ interface OpenTarget {
id: string; id: string;
label: string; label: string;
icon: ReactElement; icon: ReactElement;
requiresLocalDaemon: boolean;
onOpen: () => Promise<void> | void; onOpen: () => Promise<void> | void;
} }
@@ -82,14 +85,16 @@ export function WorkspaceOpenInEditorButton({
const toast = useToast(); const toast = useToast();
const client = useHostRuntimeClient(serverId); const client = useHostRuntimeClient(serverId);
const isConnected = useHostRuntimeIsConnected(serverId); const isConnected = useHostRuntimeIsConnected(serverId);
const isLocalDaemon = useIsLocalDaemon(serverId);
const { preferredEditorId, updatePreferredEditor } = usePreferredEditor(); const { preferredEditorId, updatePreferredEditor } = usePreferredEditor();
const shouldLoadTargets = const shouldQueryWorkspace =
isWeb && Boolean(client && isConnected) && cwd.trim().length > 0 && isAbsolutePath(cwd); isWeb && Boolean(client && isConnected) && cwd.trim().length > 0 && isAbsolutePath(cwd);
const shouldLoadEditorTargets = shouldQueryWorkspace && isLocalDaemon;
const availableEditorsQuery = useQuery<EditorTargetDescriptorPayload[]>({ const availableEditorsQuery = useQuery<EditorTargetDescriptorPayload[]>({
queryKey: ["available-editors", serverId], queryKey: ["available-editors", serverId],
enabled: shouldLoadTargets, enabled: shouldLoadEditorTargets,
staleTime: 60_000, staleTime: 60_000,
retry: false, retry: false,
queryFn: async () => { queryFn: async () => {
@@ -112,7 +117,7 @@ export function WorkspaceOpenInEditorButton({
const { status: checkoutStatus } = useCheckoutStatusQuery({ const { status: checkoutStatus } = useCheckoutStatusQuery({
serverId, serverId,
cwd: shouldLoadTargets ? cwd : "", cwd: shouldQueryWorkspace ? cwd : "",
}); });
const editorTargets = useMemo<OpenTarget[]>( const editorTargets = useMemo<OpenTarget[]>(
@@ -121,6 +126,7 @@ export function WorkspaceOpenInEditorButton({
id: editor.id, id: editor.id,
label: editor.label, label: editor.label,
icon: <ThemedEditorAppIcon editorId={editor.id} size={16} uniProps={mutedColorMapping} />, icon: <ThemedEditorAppIcon editorId={editor.id} size={16} uniProps={mutedColorMapping} />,
requiresLocalDaemon: true,
onOpen: async () => { onOpen: async () => {
if (!client) { if (!client) {
throw new Error("Host is not connected"); throw new Error("Host is not connected");
@@ -149,13 +155,20 @@ export function WorkspaceOpenInEditorButton({
id: "github", id: "github",
label: "GitHub", label: "GitHub",
icon: <ThemedGitHubIcon size={16} uniProps={mutedColorMapping} />, icon: <ThemedGitHubIcon size={16} uniProps={mutedColorMapping} />,
requiresLocalDaemon: false,
onOpen: () => openExternalUrl(url), onOpen: () => openExternalUrl(url),
}; };
}, [checkoutStatus]); }, [checkoutStatus]);
const targets = useMemo( 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]); const targetIds = useMemo(() => targets.map((target) => target.id), [targets]);
@@ -210,7 +223,7 @@ export function WorkspaceOpenInEditorButton({
} }
}, [primaryOption, handleOpenTarget]); }, [primaryOption, handleOpenTarget]);
if (!shouldLoadTargets || !primaryOption || targets.length === 0) { if (!shouldQueryWorkspace || !primaryOption || targets.length === 0) {
return null; return null;
} }
@@ -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 },
]);
});
});
@@ -0,0 +1,13 @@
export interface WorkspaceOpenTargetAvailability {
requiresLocalDaemon: boolean;
}
export function filterTargetsForDaemonLocation<Target extends WorkspaceOpenTargetAvailability>(
targets: readonly Target[],
input: { isLocalDaemon: boolean },
): Target[] {
if (input.isLocalDaemon) {
return [...targets];
}
return targets.filter((target) => !target.requiresLocalDaemon);
}