- Link ↵
+
+
+ {task.title}
+
+
+ {task.slug}
+ {status ? (
+ <>
+ ·
+ {status.type}
+ >
+ ) : null}
+
+
+
+ ↵
);
diff --git a/apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/PromptGroup/components/CompareBaseBranchPicker/CompareBaseBranchPicker.tsx b/apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/PromptGroup/components/CompareBaseBranchPicker/CompareBaseBranchPicker.tsx
index e3516c94da2..daff520fa33 100644
--- a/apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/PromptGroup/components/CompareBaseBranchPicker/CompareBaseBranchPicker.tsx
+++ b/apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/PromptGroup/components/CompareBaseBranchPicker/CompareBaseBranchPicker.tsx
@@ -7,19 +7,17 @@ import {
} from "@superset/ui/command";
import { Popover, PopoverContent, PopoverTrigger } from "@superset/ui/popover";
import { Tabs, TabsList, TabsTrigger } from "@superset/ui/tabs";
-import {
- Tooltip,
- TooltipContent,
- TooltipProvider,
- TooltipTrigger,
-} from "@superset/ui/tooltip";
import { useEffect, useRef, useState } from "react";
-import { GoGitBranch } from "react-icons/go";
+import { GoGitBranch, GoGlobe } from "react-icons/go";
import { HiCheck, HiChevronUpDown } from "react-icons/hi2";
+import { LuFolderOpen } from "react-icons/lu";
+import { PLATFORM } from "renderer/hotkeys";
import { formatRelativeTime } from "renderer/lib/formatRelativeTime";
import type { BranchFilter, BranchRow } from "../../../hooks/useBranchContext";
import { FormPickerTrigger } from "../FormPickerTrigger";
+const MOD_KEY = PLATFORM === "mac" ? "⌘" : "Ctrl";
+
interface CompareBaseBranchPickerProps {
effectiveCompareBaseBranch: string | null;
defaultBranch: string | null | undefined;
@@ -37,13 +35,9 @@ interface CompareBaseBranchPickerProps {
branchName: string,
source: "local" | "remote-tracking",
) => void;
- onCheckoutBranch: (branchName: string) => void;
- onOpenExisting: (branchName: string) => void;
- // Authoritative (cloud-synced) answer to "does a workspace row exist for
- // this branch on this host?". Computed from the v2Workspaces collection
- // so it stays in sync with soft-deletes. Trumps any server-side
- // `hasWorkspace` snapshot, which can be stale after deletion.
- hasWorkspaceForBranch: (branchName: string) => boolean;
+ // Server's workspaces.create resolves between open-tracked, adopt-foreign-
+ // worktree, and fresh-create — the picker doesn't decide.
+ onOpenWorkspace: (branchName: string) => void;
}
export function CompareBaseBranchPicker({
@@ -60,11 +54,11 @@ export function CompareBaseBranchPicker({
hasNextPage,
onLoadMore,
onSelectCompareBaseBranch,
- onCheckoutBranch,
- onOpenExisting,
- hasWorkspaceForBranch,
+ onOpenWorkspace,
}: CompareBaseBranchPickerProps) {
const [open, setOpen] = useState(false);
+ // Mirror cmdk's selected row so Mod+Enter can resolve it without DOM lookup.
+ const [selectedValue, setSelectedValue] = useState("");
const sentinelRef = useRef(null);
useEffect(() => {
@@ -113,20 +107,39 @@ export function CompareBaseBranchPicker({
{isBranchesLoading && branches.length === 0 ? (
- ) : (
+ ) : effectiveCompareBaseBranch ? (
- {effectiveCompareBaseBranch || "..."}
+ {effectiveCompareBaseBranch}
+
+ ) : (
+
+ Select base branch…
)}
event.stopPropagation()}
>
-
+ {
+ // cmdk leaves focus on the input, so the per-row button is
+ // pointer-only — Mod+Enter is the keyboard path to it.
+ if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) {
+ if (!selectedValue) return;
+ e.preventDefault();
+ e.stopPropagation();
+ onOpenWorkspace(selectedValue);
+ setOpen(false);
+ }
+ }}
+ >
-
-
- {!isBranchesLoading && branches.length === 0 && (
- No branches found
- )}
- {branches.map((branch) => {
- const isRemoteOnly = branch.isRemote && !branch.isLocal;
- const isWorktree = Boolean(branch.worktreePath);
- return (
- {
- // Carry the row's locality through so the server doesn't
- // re-resolve and risk picking a stale cached remote ref.
- onSelectCompareBaseBranch(
- branch.name,
- branch.isLocal ? "local" : "remote-tracking",
- );
- setOpen(false);
- }}
- className="group h-11 flex items-center justify-between gap-3 px-3"
- >
-
-
-
- {branch.name}
-
-
- {branch.name === defaultBranch && (
-
- default
-
- )}
- {isRemoteOnly && (
-
- remote
-
- )}
- {isWorktree && (
-
- worktree
-
- )}
-
+
+ {!isBranchesLoading && branches.length === 0 && (
+ No branches found
+ )}
+ {branches.map((branch) => {
+ const isRemoteOnly = branch.isRemote && !branch.isLocal;
+ const isWorktree = Boolean(branch.worktreePath);
+ return (
+ {
+ // Carry the row's locality through so the server doesn't
+ // re-resolve and risk picking a stale cached remote ref.
+ onSelectCompareBaseBranch(
+ branch.name,
+ branch.isLocal ? "local" : "remote-tracking",
+ );
+ setOpen(false);
+ }}
+ className="group items-start gap-3 rounded-md px-2.5 py-2"
+ >
+ {isWorktree ? (
+
+ ) : isRemoteOnly ? (
+
+ ) : (
+
+ )}
+
+
+ {branch.name}
-
+
{branch.lastCommitDate > 0 && (
-
+
{formatRelativeTime(branch.lastCommitDate * 1000)}
)}
- {isWorktree ? (
- (() => {
- // Authoritative check against the cloud-synced
- // collection — a `server hasWorkspace:true` row
- // may be stale after a delete.
- const hasWorkspace = hasWorkspaceForBranch(
- branch.name,
- );
- return (
-
- );
- })()
- ) : branch.isCheckedOut ? (
-
-
- {/*
- Use aria-disabled, NOT the native `disabled` attribute.
- Native disabled buttons don't fire pointer events, so the
- Tooltip never sees hover/focus and never opens — defeating
- the purpose of explaining why the button is unavailable.
- */}
-
-
-
- Already checked out in another worktree
-
-
- ) : (
-
+ {branch.name === defaultBranch && (
+ <>
+ ·
+ default
+ >
)}
- {effectiveCompareBaseBranch === branch.name && (
-
+ {isRemoteOnly && (
+ <>
+ ·
+ remote
+ >
+ )}
+ {isWorktree && (
+ <>
+ ·
+ worktree
+ >
)}
-
- );
- })}
- {hasNextPage && (
-
- {isFetchingNextPage ? "Loading more..." : ""}
-
- )}
-
-
+
+
+
+ {effectiveCompareBaseBranch === branch.name && (
+
+ )}
+
+
+ );
+ })}
+ {hasNextPage && (
+
+ {isFetchingNextPage ? "Loading more..." : ""}
+
+ )}
+
diff --git a/apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/PromptGroup/components/GitHubIssueLinkCommand/GitHubIssueLinkCommand.tsx b/apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/PromptGroup/components/GitHubIssueLinkCommand/GitHubIssueLinkCommand.tsx
index 49254278085..167b0e71835 100644
--- a/apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/PromptGroup/components/GitHubIssueLinkCommand/GitHubIssueLinkCommand.tsx
+++ b/apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/PromptGroup/components/GitHubIssueLinkCommand/GitHubIssueLinkCommand.tsx
@@ -129,7 +129,7 @@ export function GitHubIssueLinkCommand({
{tooltipLabel}
event.stopPropagation()}
@@ -153,7 +153,7 @@ export function GitHubIssueLinkCommand({
Show closed
-
+
{searchResults.length === 0 && (
{isLoading ? (
@@ -191,28 +191,37 @@ export function GitHubIssueLinkCommand({
: "Open issues"
}
>
- {searchResults.map((issue) => (
- handleSelect(issue)}
- className="group"
- >
-
-
- #{issue.issueNumber}
-
-
- {issue.title}
-
-
- Link ↵
-
-
- ))}
+ {searchResults.map((issue) => {
+ const state = normalizeIssueState(issue.state);
+ return (
+ handleSelect(issue)}
+ className="group items-start gap-3 rounded-md px-2.5 py-2"
+ >
+
+
+
+ {issue.title}
+
+
+
+ #{issue.issueNumber}
+
+ ·
+ {state}
+
+
+
+ ↵
+
+
+ );
+ })}
)}
diff --git a/apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/PromptGroup/components/PRLinkCommand/PRLinkCommand.tsx b/apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/PromptGroup/components/PRLinkCommand/PRLinkCommand.tsx
index 17c377fdded..8e1651f1a07 100644
--- a/apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/PromptGroup/components/PRLinkCommand/PRLinkCommand.tsx
+++ b/apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/PromptGroup/components/PRLinkCommand/PRLinkCommand.tsx
@@ -131,7 +131,7 @@ export function PRLinkCommand({
{tooltipLabel}
event.stopPropagation()}
@@ -155,7 +155,7 @@ export function PRLinkCommand({
Show closed
-
+
{pullRequests.length === 0 && (
{isLoading ? (
@@ -193,28 +193,35 @@ export function PRLinkCommand({
: "Open PRs"
}
>
- {pullRequests.map((pr) => (
- handleSelect(pr)}
- className="group"
- >
-
-
- #{pr.prNumber}
-
-
- {pr.title}
-
-
- Link ↵
-
-
- ))}
+ {pullRequests.map((pr) => {
+ const state = normalizeState(pr.state, pr.isDraft) as PRState;
+ return (
+ handleSelect(pr)}
+ className="group items-start gap-3 rounded-md px-2.5 py-2"
+ >
+
+
+
+ {pr.title}
+
+
+ #{pr.prNumber}
+ ·
+ {state}
+
+
+
+ ↵
+
+
+ );
+ })}
)}
diff --git a/apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/PromptGroup/hooks/useBranchPickerController/useBranchPickerController.ts b/apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/PromptGroup/hooks/useBranchPickerController/useBranchPickerController.ts
index b474d420d94..8488165d893 100644
--- a/apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/PromptGroup/hooks/useBranchPickerController/useBranchPickerController.ts
+++ b/apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/PromptGroup/hooks/useBranchPickerController/useBranchPickerController.ts
@@ -1,8 +1,6 @@
import { toast } from "@superset/ui/sonner";
-import { useLiveQuery } from "@tanstack/react-db";
import { useNavigate } from "@tanstack/react-router";
-import { useCallback, useMemo, useState } from "react";
-import { useCollections } from "renderer/routes/_authenticated/providers/CollectionsProvider";
+import { useCallback, useState } from "react";
import { useLocalHostService } from "renderer/routes/_authenticated/providers/LocalHostServiceProvider";
import { useWorkspaceCreates } from "renderer/stores/workspace-creates";
import type { BaseBranchSource } from "../../../../../DashboardNewWorkspaceDraftContext";
@@ -27,12 +25,7 @@ export interface UseBranchPickerControllerArgs {
closeModal: () => void;
}
-/**
- * Owns all state + handlers for the branch picker: the search/filter inputs,
- * the branch-context query, the host-id resolution that gates Open/Create
- * dispatch, and the per-row action callbacks. Returns a single `pickerProps`
- * object ready to spread into ``.
- */
+/** Returns a `pickerProps` object ready to spread into ``. */
export function useBranchPickerController(args: UseBranchPickerControllerArgs) {
const {
projectId,
@@ -44,12 +37,11 @@ export function useBranchPickerController(args: UseBranchPickerControllerArgs) {
} = args;
const navigate = useNavigate();
- const collections = useCollections();
const { machineId } = useLocalHostService();
const { submit } = useWorkspaceCreates();
- // `null` means "local active machine" — pin to the device's own machineId
- // so workspace lookups (which key by hostId) resolve against the right host.
+ // `null` hostId means "local active machine"; pin to the device's machineId
+ // so workspace lookups (keyed by hostId) hit the right host.
const resolvedHostId = hostId ?? machineId;
const [branchSearch, setBranchSearch] = useState("");
@@ -67,44 +59,19 @@ export function useBranchPickerController(args: UseBranchPickerControllerArgs) {
const effectiveCompareBaseBranch = baseBranch || defaultBranch || null;
- // Authoritative "does a workspace already exist for this (project, branch,
- // host)?" — driven by the cloud-synced collection rather than the server's
- // per-row hasWorkspace snapshot, which can be stale after a delete.
- const { data: projectWorkspaces } = useLiveQuery(
- (q) => q.from({ workspaces: collections.v2Workspaces }),
- [collections],
- );
-
- const workspaceByBranch = useMemo(() => {
- const map = new Map();
- if (!projectId || !projectWorkspaces || !resolvedHostId) return map;
- for (const w of projectWorkspaces) {
- if (
- w.projectId === projectId &&
- w.hostId === resolvedHostId &&
- w.branch
- ) {
- map.set(w.branch, w.id);
- }
- }
- return map;
- }, [projectId, projectWorkspaces, resolvedHostId]);
-
- const hasWorkspaceForBranch = useCallback(
- (name: string) => workspaceByBranch.has(name),
- [workspaceByBranch],
- );
-
- // Picker actions bypass the modal's submit, so they don't get the
- // `resolveNames` pass — fall back to the branch name when the user hasn't
- // typed a workspace name.
+ // Picker actions bypass the modal's submit pipeline (and its `resolveNames`
+ // pass), so we mirror its branch-name fallback here.
const resolveActionWorkspaceName = useCallback(
(branchName: string) => typedWorkspaceName.trim() || branchName,
[typedWorkspaceName],
);
- const onCheckoutBranch = useCallback(
- (branchName: string) => {
+ // Server's `workspaces.create` resolves all three cases (open tracked,
+ // adopt foreign worktree, fresh create). Await + navigate to the canonical
+ // id — the existing-row and adoption paths can return an id different from
+ // the optimistic snapshot id, which would otherwise 404.
+ const onOpenWorkspace = useCallback(
+ async (branchName: string) => {
if (!projectId) {
toast.error("Select a project first");
return;
@@ -113,19 +80,28 @@ export function useBranchPickerController(args: UseBranchPickerControllerArgs) {
toast.error("No active host");
return;
}
- const workspaceId = crypto.randomUUID();
+ const snapshotId = crypto.randomUUID();
const workspaceName = resolveActionWorkspaceName(branchName);
closeModal();
- void navigate({ to: `/v2-workspace/${workspaceId}` as string });
- void submit({
+ const result = await submit({
hostId: resolvedHostId,
snapshot: {
- id: workspaceId,
+ id: snapshotId,
projectId,
name: workspaceName,
branch: branchName,
},
});
+ if (result.ok) {
+ void navigate({
+ to: "/v2-workspace/$workspaceId",
+ params: { workspaceId: result.workspaceId },
+ });
+ } else {
+ // `submit` records the failure for the in-flight tracker but
+ // doesn't toast; surface it here so the closed modal isn't silent.
+ toast.error(result.error || "Failed to open workspace");
+ }
},
[
projectId,
@@ -137,22 +113,6 @@ export function useBranchPickerController(args: UseBranchPickerControllerArgs) {
],
);
- const onOpenExisting = useCallback(
- (branchName: string) => {
- const workspaceId = workspaceByBranch.get(branchName);
- if (!workspaceId) {
- toast.error("Could not find existing workspace for this branch");
- return;
- }
- closeModal();
- void navigate({
- to: "/v2-workspace/$workspaceId",
- params: { workspaceId },
- });
- },
- [workspaceByBranch, closeModal, navigate],
- );
-
const onSelectCompareBaseBranch = useCallback(
(branch: string, source: BaseBranchSource) => {
onBaseBranchChange(branch, source);
@@ -178,9 +138,7 @@ export function useBranchPickerController(args: UseBranchPickerControllerArgs) {
hasNextPage: hasNextPage ?? false,
onLoadMore,
onSelectCompareBaseBranch,
- onCheckoutBranch,
- onOpenExisting,
- hasWorkspaceForBranch,
+ onOpenWorkspace,
};
return { pickerProps };