From 85e4f0c2e6749eb503a808d2f61609ab833a48d0 Mon Sep 17 00:00:00 2001 From: TheIcarusWings <10465470+TheIcarusWings@users.noreply.github.com> Date: Fri, 12 Jun 2026 22:42:21 +0100 Subject: [PATCH 1/2] feat(composer): show clickable PR pill next to branch selector Render a small PR pill (icon + #number) to the left of the branch selector in the bottom composer bar when the active branch has a pull request. The pill is colored by PR state (open=emerald, merged=violet, closed=zinc) via the shared prStatusIndicator, and clicking it opens the PR in the system browser without opening the branch dropdown. The tooltip is action-oriented ("Open pull request #N (state) in browser") and uses the provider's terminology, distinct from the sidebar's state-description tooltip. --- .../BranchToolbarBranchSelector.tsx | 84 +++++++++++++++++-- 1 file changed, 75 insertions(+), 9 deletions(-) diff --git a/apps/web/src/components/BranchToolbarBranchSelector.tsx b/apps/web/src/components/BranchToolbarBranchSelector.tsx index 72391f714fc..cd1114f6753 100644 --- a/apps/web/src/components/BranchToolbarBranchSelector.tsx +++ b/apps/web/src/components/BranchToolbarBranchSelector.tsx @@ -3,6 +3,7 @@ import type { EnvironmentId, VcsRef, ThreadId } from "@t3tools/contracts"; import { LegendList, type LegendListRef } from "@legendapp/list/react"; import { ChevronDownIcon, GitBranchIcon, SearchIcon } from "lucide-react"; import { + type MouseEvent, useCallback, useDeferredValue, useEffect, @@ -16,6 +17,7 @@ import { import { useComposerDraftStore, type DraftId } from "../composerDraftStore"; import { readEnvironmentApi } from "../environmentApi"; +import { readLocalApi } from "../localApi"; import { useVcsStatus } from "../lib/vcsStatusState"; import { useVcsRefs, vcsRefManager } from "../lib/vcsRefState"; import { newCommandId } from "../lib/utils"; @@ -32,6 +34,11 @@ import { resolveEffectiveEnvMode, shouldIncludeBranchPickerItem, } from "./BranchToolbar.logic"; +import { + ChangeRequestStatusIcon, + prStatusIndicator, + resolveThreadPr, +} from "./ThreadStatusIndicators"; import { Button } from "./ui/button"; import { Combobox, @@ -44,6 +51,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 +522,41 @@ 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 = 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.", + }), + ); + }); + }, []); + function renderPickerItem(itemValue: string, index: number) { if (checkoutPullRequestItemValue && itemValue === checkoutPullRequestItemValue) { return ( @@ -610,15 +653,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} + + +
From 7c713b91f291bc10a7228946685d810a355a6549 Mon Sep 17 00:00:00 2001 From: TheIcarusWings <10465470+TheIcarusWings@users.noreply.github.com> Date: Sun, 14 Jun 2026 20:50:01 +0100 Subject: [PATCH 2/2] refactor(composer): extract shared useOpenPrLink hook Address review feedback: the openPrLink handler was duplicated verbatim between Sidebar and the composer branch selector. Extract it into a shared lib/openPullRequestLink hook and use it from both call sites. --- .../BranchToolbarBranchSelector.tsx | 32 ++-------------- apps/web/src/components/Sidebar.tsx | 25 +------------ apps/web/src/lib/openPullRequestLink.ts | 37 +++++++++++++++++++ 3 files changed, 42 insertions(+), 52 deletions(-) create mode 100644 apps/web/src/lib/openPullRequestLink.ts diff --git a/apps/web/src/components/BranchToolbarBranchSelector.tsx b/apps/web/src/components/BranchToolbarBranchSelector.tsx index cd1114f6753..b7c5bd1cab8 100644 --- a/apps/web/src/components/BranchToolbarBranchSelector.tsx +++ b/apps/web/src/components/BranchToolbarBranchSelector.tsx @@ -3,7 +3,6 @@ import type { EnvironmentId, VcsRef, ThreadId } from "@t3tools/contracts"; import { LegendList, type LegendListRef } from "@legendapp/list/react"; import { ChevronDownIcon, GitBranchIcon, SearchIcon } from "lucide-react"; import { - type MouseEvent, useCallback, useDeferredValue, useEffect, @@ -17,7 +16,7 @@ import { import { useComposerDraftStore, type DraftId } from "../composerDraftStore"; import { readEnvironmentApi } from "../environmentApi"; -import { readLocalApi } from "../localApi"; +import { useOpenPrLink } from "../lib/openPullRequestLink"; import { useVcsStatus } from "../lib/vcsStatusState"; import { useVcsRefs, vcsRefManager } from "../lib/vcsRefState"; import { newCommandId } from "../lib/utils"; @@ -524,38 +523,13 @@ export function BranchToolbarBranchSelector({ // 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, - ); + 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 = 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.", - }), - ); - }); - }, []); + const openPrLink = useOpenPrLink(); function renderPickerItem(itemValue: string, index: number) { if (checkoutPullRequestItemValue && itemValue === checkoutPullRequestItemValue) { 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.", + }), + ); + }); + }, []); +}