diff --git a/apps/web/src/components/RightPanelTabs.test.tsx b/apps/web/src/components/RightPanelTabs.test.tsx index dfaaf171c429..d88a6daba261 100644 --- a/apps/web/src/components/RightPanelTabs.test.tsx +++ b/apps/web/src/components/RightPanelTabs.test.tsx @@ -5,6 +5,7 @@ import { describe, expect, it } from "vite-plus/test"; import { RightPanelTabs, + agentsWorkingLabel, resolvePullRequestTabLink, shouldOpenDefaultBrowserProfileFromMenuClick, surfaceShortcutActionForKey, @@ -20,6 +21,14 @@ describe("browser profile submenu", () => { }); }); +describe("agentsWorkingLabel", () => { + it("pluralizes the count of agents currently working", () => { + expect(agentsWorkingLabel(1)).toBe("1 agent working"); + expect(agentsWorkingLabel(2)).toBe("2 agents working"); + expect(agentsWorkingLabel(0)).toBe("0 agents working"); + }); +}); + function shortcutEvent( key: string, overrides: Partial[1]> = {}, diff --git a/apps/web/src/components/RightPanelTabs.tsx b/apps/web/src/components/RightPanelTabs.tsx index 001342e48262..cb38f5044b43 100644 --- a/apps/web/src/components/RightPanelTabs.tsx +++ b/apps/web/src/components/RightPanelTabs.tsx @@ -45,6 +45,8 @@ import type { RightPanelSurface } from "~/rightPanelStore"; import { cn } from "~/lib/utils"; import { readLocalApi } from "~/localApi"; import { Button } from "~/components/ui/button"; +// Reuses the local shadcn Badge: https://ui.shadcn.com/docs/components/base/badge +import { Badge } from "~/components/ui/badge"; import { AndroidIcon, AppleIcon } from "~/components/Icons"; import { Tooltip, TooltipPopup, TooltipTrigger } from "~/components/ui/tooltip"; import { Kbd } from "~/components/ui/kbd"; @@ -132,7 +134,11 @@ interface RightPanelTabsProps { agentsAvailable: boolean; deviceAvailable: boolean; pullRequestStatusSeeds?: Readonly>; - /** Running + waiting subagents; badges the Agents card in the empty state. */ + /** + * Running + waiting subagents; badges the Agents tab, the Agents item in the + * add-surface menu, and the Agents card in the empty state so the count on + * the panel toggle is traceable to the surface it belongs to. + */ liveAgentCount: number; children: ReactNode; } @@ -277,6 +283,18 @@ export function surfaceShortcutTargetsTypingContext( ); } +export function agentsWorkingLabel(count: number): string { + return `${count} ${count === 1 ? "agent" : "agents"} working`; +} + +function AgentCountPill({ count, className }: { count: number; className?: string }) { + return ( + + {count} + + ); +} + function DisabledReasonTooltip(props: { reason: string; trigger: ReactElement }) { return ( @@ -291,6 +309,7 @@ function SurfaceMenuItem(props: { disabledReason?: string; shortcut: string; onClick: () => void; + "aria-label"?: string | undefined; children: ReactNode; }) { const item = ( @@ -299,6 +318,7 @@ function SurfaceMenuItem(props: { onClick={props.onClick} disabled={!props.available} aria-keyshortcuts={props.shortcut} + aria-label={props["aria-label"]} > {props.children} {props.shortcut} @@ -486,12 +506,7 @@ function RightPanelEmptyState(props: { {action.badgeCount > 0 ? ( - - {action.badgeCount} - + ) : null} ); @@ -533,6 +548,11 @@ function RightPanelEmptyState(props: { } /> @@ -1335,10 +1387,18 @@ export function RightPanelTabs(props: RightPanelTabsProps) { available={action.available} disabledReason={action.disabledReason} shortcut={action.shortcut} + aria-label={ + action.badgeCount > 0 + ? `${action.label}, ${agentsWorkingLabel(action.badgeCount)}` + : undefined + } onClick={action.onClick} > {action.label} + {action.badgeCount > 0 ? ( + + ) : null} ); })} diff --git a/apps/web/src/components/ui/badge.tsx b/apps/web/src/components/ui/badge.tsx index db688707fb91..645735689e04 100644 --- a/apps/web/src/components/ui/badge.tsx +++ b/apps/web/src/components/ui/badge.tsx @@ -15,6 +15,7 @@ const badgeVariants = cva( }, variants: { size: { + count: "h-3.5 min-w-3.5 rounded-full border-0 px-1 text-[9px] font-semibold tabular-nums", control: "h-7 min-w-7 rounded-[var(--control-radius)] px-[calc(--spacing(2)-1px)] text-sm sm:h-6 sm:min-w-6 sm:text-xs", default: @@ -29,6 +30,7 @@ const badgeVariants = cva( destructive: "bg-destructive text-white [button&,a&]:hover:bg-destructive/90", error: "bg-destructive/8 text-destructive-foreground dark:bg-destructive/16", info: "bg-info/8 text-info-foreground dark:bg-info/16", + infoSolid: "bg-info-solid text-white", outline: "border-input bg-background text-foreground dark:bg-input/32 [button&,a&]:hover:bg-accent/50 dark:[button&,a&]:hover:bg-input/48", secondary: "bg-secondary text-secondary-foreground [button&,a&]:hover:bg-secondary/90", diff --git a/apps/web/src/index.css b/apps/web/src/index.css index 9caf42ca2d6c..cdfe24b5a3ad 100644 --- a/apps/web/src/index.css +++ b/apps/web/src/index.css @@ -218,6 +218,7 @@ html[data-mobile-composer-route-transition="true"]::view-transition-old(t3-mobil --color-diff-deletion-foreground: var(--diff-deletion-foreground); --color-info-foreground: var(--info-foreground); --color-info: var(--info); + --color-info-solid: var(--info-solid); --color-destructive-foreground: var(--destructive-foreground); --color-ring: var(--ring); --color-input: var(--contrast-input); @@ -1062,6 +1063,7 @@ html[data-mobile-composer-route-transition="true"]::view-transition-old(t3-mobil --ring: var(--primary); --destructive-foreground: var(--error-foreground); --info: var(--color-blue-500); + --info-solid: var(--color-blue-600); --info-foreground: var(--color-blue-700); --success: var(--color-emerald-500); --success-foreground: var(--color-emerald-700);