diff --git a/apps/web/src/components/BranchToolbarBranchSelector.tsx b/apps/web/src/components/BranchToolbarBranchSelector.tsx
index 3bab1315ee8b..9ca02389b192 100644
--- a/apps/web/src/components/BranchToolbarBranchSelector.tsx
+++ b/apps/web/src/components/BranchToolbarBranchSelector.tsx
@@ -59,6 +59,7 @@ import {
useLinkedThreadPullRequest,
} from "./ThreadStatusIndicators";
import { Button } from "./ui/button";
+import { ComposerControl } from "./chat/ComposerControl";
import { Switch } from "./ui/switch";
import { getVirtualizedScrollFadeClassName } from "./ui/scroll-area";
import {
@@ -790,7 +791,7 @@ export function BranchToolbarBranchSelector({
data-composer-context-control
>
}
badge={prBadge}
number={prNumber}
url={prUrl}
diff --git a/apps/web/src/components/Sidebar.tsx b/apps/web/src/components/Sidebar.tsx
index 95f0e062d8e0..3ec0f981d386 100644
--- a/apps/web/src/components/Sidebar.tsx
+++ b/apps/web/src/components/Sidebar.tsx
@@ -221,7 +221,7 @@ import {
} from "../providerInstances";
import { useThreadRunningTerminalIds } from "../state/terminalSessions";
import { stackedThreadToast, toastManager } from "./ui/toast";
-import { Button } from "./ui/button";
+import { Button, InlineButton } from "./ui/button";
import {
Combobox,
ComboboxEmpty,
@@ -1510,7 +1510,7 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: {
const prBadge =
prBadgeShape?.kind === "stack" || pr || currentLinkedPr ? (
}
badge={prBadgeShape}
number={pr?.number ?? currentLinkedPr?.number}
url={pr?.url ?? currentLinkedPr?.url}
diff --git a/apps/web/src/components/ThreadStatusIndicators.tsx b/apps/web/src/components/ThreadStatusIndicators.tsx
index 8b27b74fd0a5..29020f4975a3 100644
--- a/apps/web/src/components/ThreadStatusIndicators.tsx
+++ b/apps/web/src/components/ThreadStatusIndicators.tsx
@@ -15,8 +15,8 @@ import {
type ThreadPullRequestBadge,
} from "@t3tools/shared/threadPullRequests";
import { FolderGit2Icon, TerminalIcon } from "lucide-react";
-import { useMemo, type MouseEvent } from "react";
-import { Button, InlineButton } from "./ui/button";
+import { useRender } from "@base-ui/react/use-render";
+import { useMemo, type MouseEvent, type ReactElement } from "react";
import { cn } from "../lib/utils";
import { useEnvironment, usePrimaryEnvironmentId } from "../state/environments";
import { EnvironmentMachineIcon } from "./EnvironmentMachineIcon";
@@ -191,9 +191,14 @@ export function resolveThreadPullRequestBadgePresentation({
};
}
-/** The complete linked-PR control shared by the sidebar and composer footer. */
+/**
+ * The linked-PR badge shared by the sidebar and composer footer. The badge owns what it shows:
+ * the state glyph and number at the meta size, in the state's color. The caller owns the control
+ * it sits in through `render` (an inline link in a sidebar row, a toolbar control in the
+ * composer), and the badge fills in the link or stack button behavior.
+ */
export function ThreadPullRequestBadgeControl({
- variant,
+ render,
badge,
number,
url,
@@ -201,7 +206,7 @@ export function ThreadPullRequestBadgeControl({
onOpenStack,
onOpenPullRequest,
}: {
- variant: "underline" | "ghost";
+ render: ReactElement<{ render?: useRender.RenderProp }>;
badge: ThreadPullRequestBadge | null;
number?: number | undefined;
url?: string | undefined;
@@ -211,53 +216,65 @@ export function ThreadPullRequestBadgeControl({
}) {
const presentation = resolveThreadPullRequestBadgePresentation({ badge, number, url, status });
if (presentation === null) return null;
- const isStack = badge?.kind === "stack";
- const content = (
- <>
-
- {presentation.text}
- >
+ return (
+
);
- const linkProps = isStack
- ? {
- onClick: (event: MouseEvent) => {
- event.preventDefault();
- event.stopPropagation();
- onOpenStack();
- },
+}
+
+function PullRequestBadge({
+ render,
+ presentation,
+ isStack,
+ url,
+ onOpenStack,
+ onOpenPullRequest,
+}: {
+ render: ReactElement<{ render?: useRender.RenderProp }>;
+ presentation: NonNullable>;
+ isStack: boolean;
+ url: string | undefined;
+ onOpenStack: () => void;
+ onOpenPullRequest: (event: MouseEvent) => void;
+}) {
+ const onClick = isStack
+ ? (event: MouseEvent) => {
+ event.preventDefault();
+ event.stopPropagation();
+ onOpenStack();
}
- : { onClick: onOpenPullRequest };
+ : onOpenPullRequest;
const element = isStack ? (
) : (
);
+ // The caller's control (InlineButton, ComposerControl) renders as the link or stack button
+ // through its own render prop; useRender merges the badge's behavior into it.
+ const control = useRender({
+ render,
+ props: {
+ render: element,
+ "aria-label": presentation.label,
+ onPointerDown: (event: MouseEvent) => event.stopPropagation(),
+ onClick,
+ },
+ });
return (
- event.stopPropagation()}
- {...linkProps}
- />
- ) : (
- event.stopPropagation()}
- {...linkProps}
- />
- )
- }
- >
- {content}
+
+
+
+ {presentation.text}
+
{presentation.label}
diff --git a/scripts/lint-restyle-ceiling.ts b/scripts/lint-restyle-ceiling.ts
index 06429c1b2e20..0c1028d44833 100644
--- a/scripts/lint-restyle-ceiling.ts
+++ b/scripts/lint-restyle-ceiling.ts
@@ -8,7 +8,7 @@ import * as NodeURL from "node:url";
// components/ui exports are migrated to variants (see vite.config.ts). This gate keeps the
// count from growing: CI fails when findings exceed the ceiling. Lower the ceiling when you
// migrate a file, and delete this script when the rule becomes an error.
-export const RESTYLE_CEILING = 628;
+export const RESTYLE_CEILING = 627;
const RULE = "shadcn(no-restyle)";