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 {...@@ -59,6 +59,7 @@ import {
59 useLinkedThreadPullRequest,59 useLinkedThreadPullRequest,
60} from "./ThreadStatusIndicators";60} from "./ThreadStatusIndicators";
61import { Button } from "./ui/button";61import { Button } from "./ui/button";
62import { ComposerControl } from "./chat/ComposerControl";
62import { Switch } from "./ui/switch";63import { Switch } from "./ui/switch";
63import { getVirtualizedScrollFadeClassName } from "./ui/scroll-area";64import { getVirtualizedScrollFadeClassName } from "./ui/scroll-area";
64import {65import {
...@@ -790,7 +791,7 @@ export function BranchToolbarBranchSelector({...@@ -790,7 +791,7 @@ export function BranchToolbarBranchSelector({
790 data-composer-context-control791 data-composer-context-control
791 >792 >
792 <ThreadPullRequestBadgeControl793 <ThreadPullRequestBadgeControl
793 variant="ghost"794 render={<ComposerControl size="xs" />}
794 badge={prBadge}795 badge={prBadge}
795 number={prNumber}796 number={prNumber}
796 url={prUrl}797 url={prUrl}
apps/web/src/components/Sidebar.tsx+2-2
...@@ -221,7 +221,7 @@ import {...@@ -221,7 +221,7 @@ import {
221} from "../providerInstances";221} from "../providerInstances";
222import { useThreadRunningTerminalIds } from "../state/terminalSessions";222import { useThreadRunningTerminalIds } from "../state/terminalSessions";
223import { stackedThreadToast, toastManager } from "./ui/toast";223import { stackedThreadToast, toastManager } from "./ui/toast";
224import { Button } from "./ui/button";224import { Button, InlineButton } from "./ui/button";
225import {225import {
226 Combobox,226 Combobox,
227 ComboboxEmpty,227 ComboboxEmpty,
...@@ -1510,7 +1510,7 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: {...@@ -1510,7 +1510,7 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: {
1510 const prBadge =1510 const prBadge =
1511 prBadgeShape?.kind === "stack" || pr || currentLinkedPr ? (1511 prBadgeShape?.kind === "stack" || pr || currentLinkedPr ? (
1512 <ThreadPullRequestBadgeControl1512 <ThreadPullRequestBadgeControl
1513 variant="underline"1513 render={<InlineButton />}
1514 badge={prBadgeShape}1514 badge={prBadgeShape}
1515 number={pr?.number ?? currentLinkedPr?.number}1515 number={pr?.number ?? currentLinkedPr?.number}
1516 url={pr?.url ?? currentLinkedPr?.url}1516 url={pr?.url ?? currentLinkedPr?.url}
apps/web/src/components/ThreadStatusIndicators.tsx+60-43
...@@ -15,8 +15,8 @@ import {...@@ -15,8 +15,8 @@ import {
15 type ThreadPullRequestBadge,15 type ThreadPullRequestBadge,
16} from "@t3tools/shared/threadPullRequests";16} from "@t3tools/shared/threadPullRequests";
17import { FolderGit2Icon, TerminalIcon } from "lucide-react";17import { FolderGit2Icon, TerminalIcon } from "lucide-react";
18import { useMemo, type MouseEvent } from "react";18import { useRender } from "@base-ui/react/use-render";
19import { Button, InlineButton } from "./ui/button";19import { useMemo, type MouseEvent, type ReactElement } from "react";
20import { cn } from "../lib/utils";20import { cn } from "../lib/utils";
21import { useEnvironment, usePrimaryEnvironmentId } from "../state/environments";21import { useEnvironment, usePrimaryEnvironmentId } from "../state/environments";
22import { EnvironmentMachineIcon } from "./EnvironmentMachineIcon";22import { EnvironmentMachineIcon } from "./EnvironmentMachineIcon";
...@@ -191,9 +191,14 @@ export function resolveThreadPullRequestBadgePresentation({...@@ -191,9 +191,14 @@ export function resolveThreadPullRequestBadgePresentation({
191 };191 };
192}192}
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 */
195export function ThreadPullRequestBadgeControl({200export function ThreadPullRequestBadgeControl({
196 variant,201 render,
197 badge,202 badge,
198 number,203 number,
199 url,204 url,
...@@ -201,7 +206,7 @@ export function ThreadPullRequestBadgeControl({...@@ -201,7 +206,7 @@ export function ThreadPullRequestBadgeControl({
201 onOpenStack,206 onOpenStack,
202 onOpenPullRequest,207 onOpenPullRequest,
203}: {208}: {
204 variant: "underline" | "ghost";209 render: ReactElement<{ render?: useRender.RenderProp }>;
205 badge: ThreadPullRequestBadge | null;210 badge: ThreadPullRequestBadge | null;
206 number?: number | undefined;211 number?: number | undefined;
207 url?: string | undefined;212 url?: string | undefined;
...@@ -211,53 +216,65 @@ export function ThreadPullRequestBadgeControl({...@@ -211,53 +216,65 @@ export function ThreadPullRequestBadgeControl({
211}) {216}) {
212 const presentation = resolveThreadPullRequestBadgePresentation({ badge, number, url, status });217 const presentation = resolveThreadPullRequestBadgePresentation({ badge, number, url, status });
213 if (presentation === null) return null;218 if (presentation === null) return null;
214 const isStack = badge?.kind === "stack";219 return (
215 const content = (220 <PullRequestBadge
216 <>221 render={render}
217 <presentation.Icon aria-hidden className="size-3 shrink-0" />222 presentation={presentation}
218 {presentation.text}223 isStack={badge?.kind === "stack"}
219 </>224 url={url}
225 onOpenStack={onOpenStack}
226 onOpenPullRequest={onOpenPullRequest}
227 />
220 );228 );
221 const linkProps = isStack229}
222 ? {230
223 onClick: (event: MouseEvent<HTMLElement>) => {231function PullRequestBadge({
224 event.preventDefault();232 render,
225 event.stopPropagation();233 presentation,
226 onOpenStack();234 isStack,
227 },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();
228 }251 }
229 : { onClick: onOpenPullRequest };252 : onOpenPullRequest;
230 const element = isStack ? (253 const element = isStack ? (
231 <button type="button" />254 <button type="button" />
232 ) : (255 ) : (
233 <a href={url} target="_blank" rel="noopener noreferrer" />256 <a href={url} target="_blank" rel="noopener noreferrer" />
234 );257 );
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 });
235 return (269 return (
236 <Tooltip>270 <Tooltip>
237 <TooltipTrigger271 <TooltipTrigger render={control}>
238 render={272 <span
239 variant === "ghost" ? (273 className={cn("contents font-normal text-xs tabular-nums", presentation.toneClassName)}
240 <Button274 >
241 render={element}275 <presentation.Icon aria-hidden className="size-3 shrink-0" />
242 variant="ghost"276 {presentation.text}
243 size="xs"277 </span>
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}
261 </TooltipTrigger>278 </TooltipTrigger>
262 <TooltipPopup side="top">{presentation.label}</TooltipPopup>279 <TooltipPopup side="top">{presentation.label}</TooltipPopup>
263 </Tooltip>280 </Tooltip>
scripts/lint-restyle-ceiling.ts+1-1
...@@ -8,7 +8,7 @@ import * as NodeURL from "node:url";...@@ -8,7 +8,7 @@ import * as NodeURL from "node:url";
8// components/ui exports are migrated to variants (see vite.config.ts). This gate keeps the8// components/ui exports are migrated to variants (see vite.config.ts). This gate keeps the
9// count from growing: CI fails when findings exceed the ceiling. Lower the ceiling when you9// count from growing: CI fails when findings exceed the ceiling. Lower the ceiling when you
10// migrate a file, and delete this script when the rule becomes an error.10// migrate a file, and delete this script when the rule becomes an error.
11export const RESTYLE_CEILING = 628;11export const RESTYLE_CEILING = 627;
1212
13const RULE = "shadcn(no-restyle)";13const RULE = "shadcn(no-restyle)";
1414