Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions apps/web/src/components/RightPanelTabs.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { describe, expect, it } from "vite-plus/test";

import {
RightPanelTabs,
agentsWorkingLabel,
resolvePullRequestTabLink,
shouldOpenDefaultBrowserProfileFromMenuClick,
surfaceShortcutActionForKey,
Expand All @@ -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<Parameters<typeof surfaceShortcutActionForKey>[1]> = {},
Expand Down
74 changes: 67 additions & 7 deletions apps/web/src/components/RightPanelTabs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -132,7 +134,11 @@ interface RightPanelTabsProps {
agentsAvailable: boolean;
deviceAvailable: boolean;
pullRequestStatusSeeds?: Readonly<Record<string, PullRequestTabStatusSeed>>;
/** 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;
}
Expand Down Expand Up @@ -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 (
<Badge aria-hidden size="count" variant="infoSolid" className={className}>
{count}
</Badge>
);
}

function DisabledReasonTooltip(props: { reason: string; trigger: ReactElement }) {
return (
<Tooltip>
Expand All @@ -291,6 +309,7 @@ function SurfaceMenuItem(props: {
disabledReason?: string;
shortcut: string;
onClick: () => void;
"aria-label"?: string | undefined;
children: ReactNode;
}) {
const item = (
Expand All @@ -299,6 +318,7 @@ function SurfaceMenuItem(props: {
onClick={props.onClick}
disabled={!props.available}
aria-keyshortcuts={props.shortcut}
aria-label={props["aria-label"]}
>
{props.children}
<MenuShortcut>{props.shortcut}</MenuShortcut>
Expand Down Expand Up @@ -486,12 +506,7 @@ function RightPanelEmptyState(props: {
<span className="relative inline-flex shrink-0">
<Icon className={iconClassName} />
{action.badgeCount > 0 ? (
<span
aria-hidden
className="absolute -top-1.5 -right-2 flex h-3.5 min-w-3.5 items-center justify-center rounded-full bg-info px-1 text-3xs font-semibold tabular-nums text-white"
>
{action.badgeCount}
</span>
<AgentCountPill count={action.badgeCount} className="absolute -top-1.5 -right-2" />
) : null}
</span>
);
Expand Down Expand Up @@ -533,6 +548,11 @@ function RightPanelEmptyState(props: {
<button
type="button"
onClick={action.onClick}
aria-label={
action.badgeCount > 0
? `${action.label}, ${agentsWorkingLabel(action.badgeCount)}`
: undefined
}
className={cn(
"flex h-8 w-full cursor-pointer items-center gap-2.5 rounded-(--control-radius) px-2.5 text-left text-sm transition-colors group-hover:bg-accent/60",
isHighlighted(action) && "bg-accent/60",
Expand Down Expand Up @@ -587,8 +607,14 @@ function RightPanelEmptyState(props: {
reason={action.disabledReason}
trigger={
<div
role="button"
tabIndex={0}
aria-disabled="true"
aria-label={
action.badgeCount > 0
? `${action.label}, ${agentsWorkingLabel(action.badgeCount)}`
: undefined
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
className="flex h-8 w-full cursor-default items-center gap-2.5 rounded-(--control-radius) px-2.5 text-left text-sm opacity-50"
>
{actionIcon(action, "size-4")}
Expand Down Expand Up @@ -858,6 +884,10 @@ export function RightPanelTabs(props: RightPanelTabsProps) {
});
}, []);

const agentsSurfaceActive = props.surfaces.some(
(surface) => surface.kind === "agents" && surface.id === props.activeSurfaceId,
);

const scrollTabs = useCallback((direction: -1 | 1) => {
const viewport = tabScrollViewport(tabListRef.current);
if (!viewport) return;
Expand All @@ -876,6 +906,7 @@ export function RightPanelTabs(props: RightPanelTabsProps) {
available: props.browserAvailable,
disabledReason: SURFACE_DISABLED_REASONS.browser,
onClick: props.onAddBrowser,
badgeCount: 0,
},
{
label: "Terminal",
Expand All @@ -884,6 +915,7 @@ export function RightPanelTabs(props: RightPanelTabsProps) {
available: props.terminalAvailable,
disabledReason: SURFACE_DISABLED_REASONS.terminal,
onClick: props.onAddTerminal,
badgeCount: 0,
},
{
label: "Files",
Expand All @@ -892,6 +924,7 @@ export function RightPanelTabs(props: RightPanelTabsProps) {
available: props.filesAvailable,
disabledReason: SURFACE_DISABLED_REASONS.files,
onClick: props.onAddFiles,
badgeCount: 0,
},
{
label: "Diff",
Expand All @@ -900,6 +933,7 @@ export function RightPanelTabs(props: RightPanelTabsProps) {
available: props.diffAvailable,
disabledReason: SURFACE_DISABLED_REASONS.diff,
onClick: props.onAddDiff,
badgeCount: 0,
},
{
label: "Pull request",
Expand All @@ -908,6 +942,7 @@ export function RightPanelTabs(props: RightPanelTabsProps) {
available: props.pullRequestAvailable,
disabledReason: SURFACE_DISABLED_REASONS.pullRequest,
onClick: props.onAddPullRequest,
badgeCount: 0,
},
{
label: "Linked pull requests",
Expand All @@ -916,6 +951,7 @@ export function RightPanelTabs(props: RightPanelTabsProps) {
available: props.pullRequestsAvailable,
disabledReason: SURFACE_DISABLED_REASONS.pullRequests,
onClick: props.onAddPullRequests,
badgeCount: 0,
},
{
label: "Agents",
Expand All @@ -924,6 +960,9 @@ export function RightPanelTabs(props: RightPanelTabsProps) {
available: props.agentsAvailable,
disabledReason: SURFACE_DISABLED_REASONS.agents,
onClick: props.onAddAgents,
// Same suppression as the toggle and tab badges: while the Agents
// roster is on screen the count would be pointing at nothing.
badgeCount: agentsSurfaceActive ? 0 : props.liveAgentCount,
},
{
label: "Device",
Expand All @@ -932,6 +971,7 @@ export function RightPanelTabs(props: RightPanelTabsProps) {
available: props.deviceAvailable,
disabledReason: SURFACE_DISABLED_REASONS.device,
onClick: props.onAddDevice,
badgeCount: 0,
},
] as const;

Expand Down Expand Up @@ -1133,6 +1173,10 @@ export function RightPanelTabs(props: RightPanelTabsProps) {
const pending = props.pendingSurfaceIds.has(surface.id);
const title = surfaceTitle(surface, props.previewSessions, props.terminalLabelsById);
const previewTabId = previewTabIdOf(surface, props.previewSessions);
// Mirrors the toggle badge suppression: while the Agents roster
// itself is on screen the count would be pointing at nothing.
const agentBadgeCount =
surface.kind === "agents" && !active ? props.liveAgentCount : 0;
// Desktop state is keyed by the session id, but desktop actions
// must be addressed with the runtime id.
const audio = tabAudioState(
Expand Down Expand Up @@ -1236,9 +1280,17 @@ export function RightPanelTabs(props: RightPanelTabsProps) {
setRenamingDevice(surface.id);
}}
className="cursor-pointer flex min-w-0 items-center"
aria-label={
agentBadgeCount > 0
? `${title}, ${agentsWorkingLabel(agentBadgeCount)}`
: undefined
}
onClick={() => props.onActivate(surface)}
>
<span className="truncate">{title}</span>
{agentBadgeCount > 0 ? (
<AgentCountPill count={agentBadgeCount} className="ml-1 shrink-0" />
) : null}
</button>
}
/>
Expand Down Expand Up @@ -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}
>
<Icon />
{action.label}
{action.badgeCount > 0 ? (
<AgentCountPill count={action.badgeCount} />
) : null}
</SurfaceMenuItem>
);
})}
Expand Down
2 changes: 2 additions & 0 deletions apps/web/src/components/ui/badge.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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",
Expand Down
2 changes: 2 additions & 0 deletions apps/web/src/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand Down
Loading