Gate workspace open targets by daemon locality
This commit is contained in:
@@ -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);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user