diff --git a/apps/web/src/components/BranchToolbarBranchSelector.tsx b/apps/web/src/components/BranchToolbarBranchSelector.tsx index 72391f714fc..b7c5bd1cab8 100644 --- a/apps/web/src/components/BranchToolbarBranchSelector.tsx +++ b/apps/web/src/components/BranchToolbarBranchSelector.tsx @@ -16,6 +16,7 @@ import { import { useComposerDraftStore, type DraftId } from "../composerDraftStore"; import { readEnvironmentApi } from "../environmentApi"; +import { useOpenPrLink } from "../lib/openPullRequestLink"; import { useVcsStatus } from "../lib/vcsStatusState"; import { useVcsRefs, vcsRefManager } from "../lib/vcsRefState"; import { newCommandId } from "../lib/utils"; @@ -32,6 +33,11 @@ import { resolveEffectiveEnvMode, shouldIncludeBranchPickerItem, } from "./BranchToolbar.logic"; +import { + ChangeRequestStatusIcon, + prStatusIndicator, + resolveThreadPr, +} from "./ThreadStatusIndicators"; import { Button } from "./ui/button"; import { Combobox, @@ -44,6 +50,7 @@ import { ComboboxTrigger, } from "./ui/combobox"; import { stackedThreadToast, toastManager } from "./ui/toast"; +import { Tooltip, TooltipPopup, TooltipTrigger } from "./ui/tooltip"; interface BranchToolbarBranchSelectorProps { className?: string; @@ -514,6 +521,16 @@ export function BranchToolbarBranchSelector({ resolvedActiveBranch, }); + // PR pill shown next to the branch selector when the active branch has one. + const branchPr = resolveThreadPr(resolvedActiveBranch, branchStatusQuery.data ?? null); + const branchPrStatus = prStatusIndicator(branchPr, branchStatusQuery.data?.sourceControlProvider); + // Action-oriented tooltip (the pill opens the PR), distinct from the sidebar's + // state-description tooltip. + const branchPrTooltip = branchPr + ? `Open ${sourceControlPresentation.terminology.singular} #${branchPr.number} (${branchPr.state}) in browser` + : ""; + const openPrLink = useOpenPrLink(); + function renderPickerItem(itemValue: string, index: number) { if (checkoutPullRequestItemValue && itemValue === checkoutPullRequestItemValue) { return ( @@ -610,15 +627,38 @@ export function BranchToolbarBranchSelector({ open={isBranchMenuOpen} value={resolvedActiveBranch} > - } - className={cn("min-w-0 text-muted-foreground/70 hover:text-foreground/80", className)} - disabled={isInitialBranchesLoadPending || isBranchActionPending} - > - - {triggerLabel} - - +
+ {branchPr && branchPrStatus ? ( + + openPrLink(event, branchPrStatus.url)} + className={cn( + "inline-flex shrink-0 items-center gap-0.5 rounded px-1 py-0.5 text-[11px] font-medium tabular-nums transition-colors hover:bg-muted/60", + branchPrStatus.colorClass, + )} + /> + } + > + + #{branchPr.number} + + {branchPrTooltip} + + ) : null} + } + className="min-w-0 text-muted-foreground/70 hover:text-foreground/80" + disabled={isInitialBranchesLoadPending || isBranchActionPending} + > + + {triggerLabel} + + +
diff --git a/apps/web/src/components/Sidebar.tsx b/apps/web/src/components/Sidebar.tsx index dc5acaaadc7..e6471f4d763 100644 --- a/apps/web/src/components/Sidebar.tsx +++ b/apps/web/src/components/Sidebar.tsx @@ -63,6 +63,7 @@ import { import { usePrimaryEnvironmentId } from "../environments/primary"; import { isElectron } from "../env"; import { APP_STAGE_LABEL, APP_VERSION } from "../branding"; +import { useOpenPrLink } from "../lib/openPullRequestLink"; import { isTerminalFocused } from "../lib/terminalFocus"; import { isMacPlatform, newCommandId } from "../lib/utils"; import { @@ -1011,29 +1012,7 @@ const SidebarProjectItem = memo(function SidebarProjectItem(props: SidebarProjec ); }, }); - const openPrLink = useCallback((event: React.MouseEvent, prUrl: string) => { - event.preventDefault(); - event.stopPropagation(); - - const api = readLocalApi(); - if (!api) { - toastManager.add({ - type: "error", - title: "Link opening is unavailable.", - }); - return; - } - - void api.shell.openExternal(prUrl).catch((error) => { - toastManager.add( - stackedThreadToast({ - type: "error", - title: "Unable to open pull request link", - description: error instanceof Error ? error.message : "An error occurred.", - }), - ); - }); - }, []); + const openPrLink = useOpenPrLink(); const sidebarThreads = useStore( useShallow( useMemo( diff --git a/apps/web/src/lib/openPullRequestLink.ts b/apps/web/src/lib/openPullRequestLink.ts new file mode 100644 index 00000000000..899e5c38c58 --- /dev/null +++ b/apps/web/src/lib/openPullRequestLink.ts @@ -0,0 +1,37 @@ +import { type MouseEvent, useCallback } from "react"; + +import { stackedThreadToast, toastManager } from "../components/ui/toast"; +import { readLocalApi } from "../localApi"; + +/** + * Returns a click handler that opens a pull request URL in the system browser. + * + * Stops event propagation/default so activating the link does not also trigger + * an enclosing row or trigger (e.g. opening the branch dropdown), and surfaces a + * toast when the local API is unavailable or the open fails. + */ +export function useOpenPrLink() { + return useCallback((event: MouseEvent, prUrl: string) => { + event.preventDefault(); + event.stopPropagation(); + + const api = readLocalApi(); + if (!api) { + toastManager.add({ + type: "error", + title: "Link opening is unavailable.", + }); + return; + } + + void api.shell.openExternal(prUrl).catch((error) => { + toastManager.add( + stackedThreadToast({ + type: "error", + title: "Unable to open pull request link", + description: error instanceof Error ? error.message : "An error occurred.", + }), + ); + }); + }, []); +}