diff --git a/apps/desktop/src/renderer/components/Chat/ChatInterface/components/IssueLinkCommand/IssueLinkCommand.tsx b/apps/desktop/src/renderer/components/Chat/ChatInterface/components/IssueLinkCommand/IssueLinkCommand.tsx index 4bcf7860b64..031b283717b 100644 --- a/apps/desktop/src/renderer/components/Chat/ChatInterface/components/IssueLinkCommand/IssueLinkCommand.tsx +++ b/apps/desktop/src/renderer/components/Chat/ChatInterface/components/IssueLinkCommand/IssueLinkCommand.tsx @@ -155,7 +155,7 @@ export function IssueLinkCommand({ {tooltipLabel} event.stopPropagation()} @@ -179,7 +179,7 @@ export function IssueLinkCommand({ Show closed - + {filteredTasks.length === 0 && ( {showClosed ? "No issues found." : "No open issues found."} @@ -211,25 +211,35 @@ export function IssueLinkCommand({ task.externalUrl ?? undefined, ) } - className="group" + className="group items-start gap-3 rounded-md px-2.5 py-2" > - {status ? ( - - ) : ( - - )} - - {task.slug} + + {status ? ( + + ) : ( + + )} - - {task.title} - - - 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 };