authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-09-22 19:00:08-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-09-22 19:00:08-07:00
log6975efd3dd52c95dd978ff26f591139d8c5c2503
treedd9f1971fd3faac50faa01153567b007b977973f
parent2eaff08241a51dadfd9ef5ad53cba74e6d0fceb4
signature Signed by PGP key B5690EEEBB952194

fix(web): the pull request badge reads at the meta size again (#13175)

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

4 files changed, 65 insertions(+), 47 deletions(-)

apps/web/src/components/BranchToolbarBranchSelector.tsx+2-1
......@@ -59,6 +59,7 @@ import {
5959 useLinkedThreadPullRequest,
6060} from "./ThreadStatusIndicators";
6161import { Button } from "./ui/button";
62import { ComposerControl } from "./chat/ComposerControl";
6263import { Switch } from "./ui/switch";
6364import { getVirtualizedScrollFadeClassName } from "./ui/scroll-area";
6465import {
......@@ -790,7 +791,7 @@ export function BranchToolbarBranchSelector({
790791 data-composer-context-control
791792 >
792793 <ThreadPullRequestBadgeControl
793 variant="ghost"
794 render={<ComposerControl size="xs" />}
794795 badge={prBadge}
795796 number={prNumber}
796797 url={prUrl}
apps/web/src/components/Sidebar.tsx+2-2
......@@ -221,7 +221,7 @@ import {
221221} from "../providerInstances";
222222import { useThreadRunningTerminalIds } from "../state/terminalSessions";
223223import { stackedThreadToast, toastManager } from "./ui/toast";
224import { Button } from "./ui/button";
224import { Button, InlineButton } from "./ui/button";
225225import {
226226 Combobox,
227227 ComboboxEmpty,
......@@ -1510,7 +1510,7 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: {
15101510 const prBadge =
15111511 prBadgeShape?.kind === "stack" || pr || currentLinkedPr ? (
15121512 <ThreadPullRequestBadgeControl
1513 variant="underline"
1513 render={<InlineButton />}
15141514 badge={prBadgeShape}
15151515 number={pr?.number ?? currentLinkedPr?.number}
15161516 url={pr?.url ?? currentLinkedPr?.url}
apps/web/src/components/ThreadStatusIndicators.tsx+60-43
......@@ -15,8 +15,8 @@ import {
1515 type ThreadPullRequestBadge,
1616} from "@t3tools/shared/threadPullRequests";
1717import { FolderGit2Icon, TerminalIcon } from "lucide-react";
18import { useMemo, type MouseEvent } from "react";
19import { Button, InlineButton } from "./ui/button";
18import { useRender } from "@base-ui/react/use-render";
19import { useMemo, type MouseEvent, type ReactElement } from "react";
2020import { cn } from "../lib/utils";
2121import { useEnvironment, usePrimaryEnvironmentId } from "../state/environments";
2222import { EnvironmentMachineIcon } from "./EnvironmentMachineIcon";
......@@ -191,9 +191,14 @@ export function resolveThreadPullRequestBadgePresentation({
191191 };
192192}
193193
194/** The complete linked-PR control shared by the sidebar and composer footer. */
194/**
195 * The linked-PR badge shared by the sidebar and composer footer. The badge owns what it shows:
196 * the state glyph and number at the meta size, in the state's color. The caller owns the control
197 * it sits in through `render` (an inline link in a sidebar row, a toolbar control in the
198 * composer), and the badge fills in the link or stack button behavior.
199 */
195200export function ThreadPullRequestBadgeControl({
196 variant,
201 render,
197202 badge,
198203 number,
199204 url,
......@@ -201,7 +206,7 @@ export function ThreadPullRequestBadgeControl({
201206 onOpenStack,
202207 onOpenPullRequest,
203208}: {
204 variant: "underline" | "ghost";
209 render: ReactElement<{ render?: useRender.RenderProp }>;
205210 badge: ThreadPullRequestBadge | null;
206211 number?: number | undefined;
207212 url?: string | undefined;
......@@ -211,53 +216,65 @@ export function ThreadPullRequestBadgeControl({
211216}) {
212217 const presentation = resolveThreadPullRequestBadgePresentation({ badge, number, url, status });
213218 if (presentation === null) return null;
214 const isStack = badge?.kind === "stack";
215 const content = (
216 <>
217 <presentation.Icon aria-hidden className="size-3 shrink-0" />
218 {presentation.text}
219 </>
219 return (
220 <PullRequestBadge
221 render={render}
222 presentation={presentation}
223 isStack={badge?.kind === "stack"}
224 url={url}
225 onOpenStack={onOpenStack}
226 onOpenPullRequest={onOpenPullRequest}
227 />
220228 );
221 const linkProps = isStack
222 ? {
223 onClick: (event: MouseEvent<HTMLElement>) => {
224 event.preventDefault();
225 event.stopPropagation();
226 onOpenStack();
227 },
229}
230
231function PullRequestBadge({
232 render,
233 presentation,
234 isStack,
235 url,
236 onOpenStack,
237 onOpenPullRequest,
238}: {
239 render: ReactElement<{ render?: useRender.RenderProp }>;
240 presentation: NonNullable<ReturnType<typeof resolveThreadPullRequestBadgePresentation>>;
241 isStack: boolean;
242 url: string | undefined;
243 onOpenStack: () => void;
244 onOpenPullRequest: (event: MouseEvent<HTMLElement>) => void;
245}) {
246 const onClick = isStack
247 ? (event: MouseEvent<HTMLElement>) => {
248 event.preventDefault();
249 event.stopPropagation();
250 onOpenStack();
228251 }
229 : { onClick: onOpenPullRequest };
252 : onOpenPullRequest;
230253 const element = isStack ? (
231254 <button type="button" />
232255 ) : (
233256 <a href={url} target="_blank" rel="noopener noreferrer" />
234257 );
258 // The caller's control (InlineButton, ComposerControl) renders as the link or stack button
259 // through its own render prop; useRender merges the badge's behavior into it.
260 const control = useRender({
261 render,
262 props: {
263 render: element,
264 "aria-label": presentation.label,
265 onPointerDown: (event: MouseEvent<HTMLElement>) => event.stopPropagation(),
266 onClick,
267 },
268 });
235269 return (
236270 <Tooltip>
237 <TooltipTrigger
238 render={
239 variant === "ghost" ? (
240 <Button
241 render={element}
242 variant="ghost"
243 size="xs"
244 className={presentation.toneClassName}
245 aria-label={presentation.label}
246 onPointerDown={(event) => event.stopPropagation()}
247 {...linkProps}
248 />
249 ) : (
250 <InlineButton
251 render={element}
252 className={presentation.toneClassName}
253 aria-label={presentation.label}
254 onPointerDown={(event) => event.stopPropagation()}
255 {...linkProps}
256 />
257 )
258 }
259 >
260 {content}
271 <TooltipTrigger render={control}>
272 <span
273 className={cn("contents font-normal text-xs tabular-nums", presentation.toneClassName)}
274 >
275 <presentation.Icon aria-hidden className="size-3 shrink-0" />
276 {presentation.text}
277 </span>
261278 </TooltipTrigger>
262279 <TooltipPopup side="top">{presentation.label}</TooltipPopup>
263280 </Tooltip>
scripts/lint-restyle-ceiling.ts+1-1
......@@ -8,7 +8,7 @@ import * as NodeURL from "node:url";
88// components/ui exports are migrated to variants (see vite.config.ts). This gate keeps the
99// count from growing: CI fails when findings exceed the ceiling. Lower the ceiling when you
1010// migrate a file, and delete this script when the rule becomes an error.
11export const RESTYLE_CEILING = 628;
11export const RESTYLE_CEILING = 627;
1212
1313const RULE = "shadcn(no-restyle)";
1414