From 336778611d1a61090b2620f11026d950cd70bd70 Mon Sep 17 00:00:00 2001 From: Kiet Ho Date: Thu, 7 May 2026 19:55:03 -0700 Subject: [PATCH 1/8] feat(desktop): roomier v2 workspace pickers + clearer branch actions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Widen the PR/issue/task pickers from 320px to 440px and switch each row to a Linear-style two-line layout (title on top, monospace id + state below) so long titles aren't clipped. Branch picker matches the new width and gets a "Select base branch…" placeholder when no base is set. For each branch row, surface exactly one action that names what it does: "Open workspace" when a tracked workspace exists, "Create workspace" otherwise. Drops the misleading "Create"/"Check out" pairing and the disabled-tooltip state — every row is now actionable. --- .../IssueLinkCommand/IssueLinkCommand.tsx | 48 ++-- .../CompareBaseBranchPicker.tsx | 216 ++++++++---------- .../GitHubIssueLinkCommand.tsx | 57 +++-- .../PRLinkCommand/PRLinkCommand.tsx | 55 +++-- 4 files changed, 186 insertions(+), 190 deletions(-) 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..b605cb5923d 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,12 +7,6 @@ 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 { HiCheck, HiChevronUpDown } from "react-icons/hi2"; @@ -113,16 +107,20 @@ export function CompareBaseBranchPicker({ {isBranchesLoading && branches.length === 0 ? ( - ) : ( + ) : effectiveCompareBaseBranch ? ( - {effectiveCompareBaseBranch || "..."} + {effectiveCompareBaseBranch} + + ) : ( + + Select base branch… )} event.stopPropagation()} > @@ -146,134 +144,106 @@ export function CompareBaseBranchPicker({ - - - {!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 h-11 flex items-center justify-between gap-3 px-3" + > + + + + {branch.name} - - {branch.lastCommitDate > 0 && ( - - {formatRelativeTime(branch.lastCommitDate * 1000)} + + {branch.name === defaultBranch && ( + + default + + )} + {isRemoteOnly && ( + + remote + + )} + {isWorktree && ( + + worktree )} - {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.lastCommitDate > 0 && ( + + {formatRelativeTime(branch.lastCommitDate * 1000)} + + )} + {(() => { + // Authoritative check against the cloud-synced + // collection — a server `hasWorkspace:true` row + // may be stale after a delete. + const canOpen = hasWorkspaceForBranch(branch.name); + return ( + + {canOpen ? ( - ); - })() - ) : 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 - - - ) : ( - - )} - {effectiveCompareBaseBranch === branch.name && ( - - )} - - - ); - })} - {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} + +
+ + ↵ + +
+ ); + })} )}
From 4e937e10d1ff0e8af973e915632890560f535bf2 Mon Sep 17 00:00:00 2001 From: Kiet Ho Date: Thu, 7 May 2026 19:58:32 -0700 Subject: [PATCH 2/8] feat(desktop): two-line branch rows to match v2 picker layout MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Branch picker now mirrors the PR/Issue/Task two-line item layout: top row is the branch name in mono, bottom row is the meta strip (relative commit time · default · remote · worktree). Action button and the selected-base check moved to the right column. --- .../CompareBaseBranchPicker.tsx | 53 ++++++++++--------- 1 file changed, 28 insertions(+), 25 deletions(-) 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 b605cb5923d..31f24fec89f 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 @@ -164,48 +164,51 @@ export function CompareBaseBranchPicker({ ); setOpen(false); }} - className="group h-11 flex items-center justify-between gap-3 px-3" + className="group items-start gap-3 rounded-md px-2.5 py-2" > - - - + +
+ {branch.name} - - {branch.name === defaultBranch && ( - - default + + {branch.lastCommitDate > 0 && ( + + {formatRelativeTime(branch.lastCommitDate * 1000)} )} + {branch.name === defaultBranch && ( + <> + · + default + + )} {isRemoteOnly && ( - - remote - + <> + · + remote + )} {isWorktree && ( - - worktree - + <> + · + worktree + )} - - - {branch.lastCommitDate > 0 && ( - - {formatRelativeTime(branch.lastCommitDate * 1000)} - - )} +
+ {(() => { // Authoritative check against the cloud-synced // collection — a server `hasWorkspace:true` row // may be stale after a delete. const canOpen = hasWorkspaceForBranch(branch.name); return ( - + {canOpen ? ( + ) : isForeignWorktree ? ( + ) : ( - ) : isForeignWorktree ? ( - - ) : ( - - )} - - ); - })()} + + + {effectiveCompareBaseBranch === branch.name && ( )} 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 9dba73b4479..10fc5a17038 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"; @@ -29,9 +27,9 @@ export interface UseBranchPickerControllerArgs { /** * 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 ``. + * the branch-context query, the host-id resolution that gates dispatch, and + * the per-row action callbacks. Returns a single `pickerProps` object ready + * to spread into ``. */ export function useBranchPickerController(args: UseBranchPickerControllerArgs) { const { @@ -44,7 +42,6 @@ export function useBranchPickerController(args: UseBranchPickerControllerArgs) { } = args; const navigate = useNavigate(); - const collections = useCollections(); const { machineId } = useLocalHostService(); const { submit } = useWorkspaceCreates(); @@ -67,34 +64,6 @@ 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. @@ -103,64 +72,15 @@ export function useBranchPickerController(args: UseBranchPickerControllerArgs) { [typedWorkspaceName], ); - const onCheckoutBranch = useCallback( - (branchName: string) => { - if (!projectId) { - toast.error("Select a project first"); - return; - } - if (!resolvedHostId) { - toast.error("No active host"); - return; - } - const workspaceId = crypto.randomUUID(); - const workspaceName = resolveActionWorkspaceName(branchName); - closeModal(); - void navigate({ to: `/v2-workspace/${workspaceId}` as string }); - void submit({ - hostId: resolvedHostId, - snapshot: { - id: workspaceId, - projectId, - name: workspaceName, - branch: branchName, - }, - }); - }, - [ - projectId, - resolvedHostId, - resolveActionWorkspaceName, - submit, - closeModal, - navigate, - ], - ); - - 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], - ); - - // Foreign worktree = git knows about the worktree (`branch.worktreePath` is - // set) but no v2 workspace row exists for it. The server's workspaces.create - // procedure already adopts in this case; the only client-side wrinkle is - // that adoption returns a *different* canonical id than our optimistic - // snapshot id, so we have to await the submit and navigate to the resolved - // id rather than the random UUID. (onCheckoutBranch's fast-path nav would - // land the user on a 404.) - const onAdoptForeignWorktree = useCallback( + // Single "go to a workspace for this branch" path. The server's + // `workspaces.create` already covers all three cases: + // - Tracked workspace exists → returns canonical row (alreadyExists) + // - Foreign worktree, no row yet → adopts via adoptExistingWorktree + // - No worktree at all → fresh `git worktree add` + // Awaiting the result + navigating to the canonical id avoids the 404 you'd + // hit by optimistically navigating to the snapshot id (server can return a + // different id for the existing-row + adoption paths). + const onOpenWorkspace = useCallback( async (branchName: string) => { if (!projectId) { toast.error("Select a project first"); @@ -224,10 +144,7 @@ export function useBranchPickerController(args: UseBranchPickerControllerArgs) { hasNextPage: hasNextPage ?? false, onLoadMore, onSelectCompareBaseBranch, - onCheckoutBranch, - onOpenExisting, - onAdoptForeignWorktree, - hasWorkspaceForBranch, + onOpenWorkspace, }; return { pickerProps }; From a7cf01be7329903d9da327272f3b089b3d753099 Mon Sep 17 00:00:00 2001 From: Kiet Ho Date: Thu, 7 May 2026 21:17:20 -0700 Subject: [PATCH 7/8] fix(desktop): keyboard activation + error toast for branch picker MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two CodeRabbit findings: 1. The "Open workspace" button was unreachable via keyboard. cmdk leaves DOM focus on the search input during arrow-key navigation, so the per-row button gated on group-focus-within never showed and could not be clicked. Make the button visible on the cmdk-active row (`group-data-[selected=true]:inline-flex`) and wire Mod+Enter to fire onOpenWorkspace for the selected branch. Plain Enter still sets the compare base. 2. onOpenWorkspace closed the modal silently on submit failure. The in-flight tracker stores the error but does not toast — surface it here so a failed open isn't invisible. Adds a `MOD↵` keyboard hint to the button so the shortcut is discoverable. --- .../CompareBaseBranchPicker.tsx | 32 +++++++++++++++++-- .../useBranchPickerController.ts | 6 ++++ 2 files changed, 36 insertions(+), 2 deletions(-) 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 e8a77035083..170f11a1e66 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 @@ -11,10 +11,13 @@ import { useEffect, useRef, useState } from "react"; 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; @@ -55,6 +58,10 @@ export function CompareBaseBranchPicker({ onOpenWorkspace, }: CompareBaseBranchPickerProps) { const [open, setOpen] = useState(false); + // Mirror cmdk's selected-row value so a Mod+Enter keydown can resolve it + // to the branch name without traversing DOM. Without this, keyboard users + // have no path to `onOpenWorkspace`. + const [selectedValue, setSelectedValue] = useState(""); const sentinelRef = useRef(null); useEffect(() => { @@ -120,7 +127,24 @@ export function CompareBaseBranchPicker({ align="start" onWheel={(event) => event.stopPropagation()} > - + { + // Keyboard parity with the hover-only "Open workspace" button. + // cmdk arrow-keys leave focus on the input, so the per-row + // button is unreachable; Mod+Enter on the active row routes to + // the same handler. + if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) { + if (!selectedValue) return; + e.preventDefault(); + e.stopPropagation(); + onOpenWorkspace(selectedValue); + setOpen(false); + } + }} + > - + {effectiveCompareBaseBranch === branch.name && ( 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 10fc5a17038..cc1c098511a 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 @@ -107,6 +107,12 @@ export function useBranchPickerController(args: UseBranchPickerControllerArgs) { to: "/v2-workspace/$workspaceId", params: { workspaceId: result.workspaceId }, }); + } else { + // `submit` tracks the failure via `markError`, but the in-flight + // manager doesn't toast — without this, a rejected open closes + // the modal silently and the user has no feedback that anything + // failed. + toast.error(result.error || "Failed to open workspace"); } }, [ From 7028914f762b7c450a6ffabc7f13359ea5057445 Mon Sep 17 00:00:00 2001 From: Kiet Ho Date: Thu, 7 May 2026 21:21:14 -0700 Subject: [PATCH 8/8] refactor(desktop): tighten branch-picker comments + collapse spans Deslop pass on the branch picker: - Trim verbose "why" comments down to the load-bearing lines. - Collapse the redundant span around the row's Open-workspace button (visibility classes move onto the button itself). - Align the compare-base check icon's hide-trigger with the button's show-trigger so they don't briefly overlap on cmdk-active rows. --- .../CompareBaseBranchPicker.tsx | 47 ++++++++----------- .../useBranchPickerController.ts | 34 +++++--------- 2 files changed, 31 insertions(+), 50 deletions(-) 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 170f11a1e66..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 @@ -35,9 +35,8 @@ interface CompareBaseBranchPickerProps { branchName: string, source: "local" | "remote-tracking", ) => void; - // Single unified action: "go to a workspace for this branch". The server's - // `workspaces.create` resolves which path to take (open tracked / adopt - // foreign worktree / create fresh) so the client doesn't have to. + // Server's workspaces.create resolves between open-tracked, adopt-foreign- + // worktree, and fresh-create — the picker doesn't decide. onOpenWorkspace: (branchName: string) => void; } @@ -58,9 +57,7 @@ export function CompareBaseBranchPicker({ onOpenWorkspace, }: CompareBaseBranchPickerProps) { const [open, setOpen] = useState(false); - // Mirror cmdk's selected-row value so a Mod+Enter keydown can resolve it - // to the branch name without traversing DOM. Without this, keyboard users - // have no path to `onOpenWorkspace`. + // Mirror cmdk's selected row so Mod+Enter can resolve it without DOM lookup. const [selectedValue, setSelectedValue] = useState(""); const sentinelRef = useRef(null); @@ -132,10 +129,8 @@ export function CompareBaseBranchPicker({ value={selectedValue} onValueChange={setSelectedValue} onKeyDown={(e) => { - // Keyboard parity with the hover-only "Open workspace" button. - // cmdk arrow-keys leave focus on the input, so the per-row - // button is unreachable; Mod+Enter on the active row routes to - // the same handler. + // 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(); @@ -224,24 +219,22 @@ export function CompareBaseBranchPicker({ - - - + {effectiveCompareBaseBranch === branch.name && ( - + )} 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 cc1c098511a..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 @@ -25,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 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, @@ -45,8 +40,8 @@ export function useBranchPickerController(args: UseBranchPickerControllerArgs) { 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(""); @@ -64,22 +59,17 @@ export function useBranchPickerController(args: UseBranchPickerControllerArgs) { const effectiveCompareBaseBranch = baseBranch || defaultBranch || null; - // 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], ); - // Single "go to a workspace for this branch" path. The server's - // `workspaces.create` already covers all three cases: - // - Tracked workspace exists → returns canonical row (alreadyExists) - // - Foreign worktree, no row yet → adopts via adoptExistingWorktree - // - No worktree at all → fresh `git worktree add` - // Awaiting the result + navigating to the canonical id avoids the 404 you'd - // hit by optimistically navigating to the snapshot id (server can return a - // different id for the existing-row + adoption paths). + // 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) { @@ -108,10 +98,8 @@ export function useBranchPickerController(args: UseBranchPickerControllerArgs) { params: { workspaceId: result.workspaceId }, }); } else { - // `submit` tracks the failure via `markError`, but the in-flight - // manager doesn't toast — without this, a rejected open closes - // the modal silently and the user has no feedback that anything - // failed. + // `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"); } },