authorgravatar for me@t3.ggTheo Browne <me@t3.gg> 2026-08-08 19:20:06-04:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-08-08 16:20:06-07:00
logc2f8cb7ca1576afd70294b98dfe1de9be17aacae
tree123db90d8b341c6193b38ca055763a732e897f09
parent89ee692bf0436505d008c1d70215e70836eba4e2
signature Signed by PGP key B5690EEEBB952194

feat(web): show how many subagents are running at a glance (#5745)

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>

3 files changed, 48 insertions(+), 3 deletions(-)

apps/web/src/components/ChatView.tsx+7
...@@ -5913,6 +5913,11 @@ function ChatViewContent(props: ChatViewProps) {...@@ -5913,6 +5913,11 @@ function ChatViewContent(props: ChatViewProps) {
5913 rightPanelAvailable={activeProject !== null}5913 rightPanelAvailable={activeProject !== null}
5914 rightPanelOpen={rightPanelOpen}5914 rightPanelOpen={rightPanelOpen}
5915 rightPanelShortcutLabel={shortcutLabelForCommand(keybindings, "rightPanel.toggle")}5915 rightPanelShortcutLabel={shortcutLabelForCommand(keybindings, "rightPanel.toggle")}
5916 // Suppressed while the Agents surface is visible: the roster itself is
5917 // on screen, so the toggle badge would be pointing at nothing.
5918 liveAgentCount={
5919 rightPanelOpen && activeRightPanelSurface?.kind === "agents" ? 0 : agentPanelModel.liveCount
5920 }
5916 onToggleTerminal={toggleTerminalVisibility}5921 onToggleTerminal={toggleTerminalVisibility}
5917 onToggleRightPanel={toggleRightPanel}5922 onToggleRightPanel={toggleRightPanel}
5918 />5923 />
...@@ -6419,6 +6424,7 @@ function ChatViewContent(props: ChatViewProps) {...@@ -6419,6 +6424,7 @@ function ChatViewContent(props: ChatViewProps) {
6419 browserAvailable={isPreviewSupportedInRuntime()}6424 browserAvailable={isPreviewSupportedInRuntime()}
6420 diffAvailable={isServerThread && isGitRepo}6425 diffAvailable={isServerThread && isGitRepo}
6421 filesAvailable={activeProject !== null}6426 filesAvailable={activeProject !== null}
6427 liveAgentCount={agentPanelModel.liveCount}
6422 >6428 >
6423 {rightPanelContent}6429 {rightPanelContent}
6424 </RightPanelTabs>6430 </RightPanelTabs>
...@@ -6447,6 +6453,7 @@ function ChatViewContent(props: ChatViewProps) {...@@ -6447,6 +6453,7 @@ function ChatViewContent(props: ChatViewProps) {
6447 browserAvailable={isPreviewSupportedInRuntime()}6453 browserAvailable={isPreviewSupportedInRuntime()}
6448 diffAvailable={isServerThread && isGitRepo}6454 diffAvailable={isServerThread && isGitRepo}
6449 filesAvailable={activeProject !== null}6455 filesAvailable={activeProject !== null}
6456 liveAgentCount={agentPanelModel.liveCount}
6450 >6457 >
6451 {rightPanelContent}6458 {rightPanelContent}
6452 </RightPanelTabs>6459 </RightPanelTabs>
apps/web/src/components/RightPanelTabs.tsx+20-1
...@@ -48,6 +48,8 @@ interface RightPanelTabsProps {...@@ -48,6 +48,8 @@ interface RightPanelTabsProps {
48 browserAvailable: boolean;48 browserAvailable: boolean;
49 diffAvailable: boolean;49 diffAvailable: boolean;
50 filesAvailable: boolean;50 filesAvailable: boolean;
51 /** Running + waiting subagents; badges the Agents card in the empty state. */
52 liveAgentCount: number;
51 children: ReactNode;53 children: ReactNode;
52}54}
5355
...@@ -96,6 +98,7 @@ function RightPanelEmptyState(props: {...@@ -96,6 +98,7 @@ function RightPanelEmptyState(props: {
96 browserAvailable: boolean;98 browserAvailable: boolean;
97 diffAvailable: boolean;99 diffAvailable: boolean;
98 filesAvailable: boolean;100 filesAvailable: boolean;
101 liveAgentCount: number;
99}) {102}) {
100 const actions = [103 const actions = [
101 {104 {
...@@ -105,6 +108,7 @@ function RightPanelEmptyState(props: {...@@ -105,6 +108,7 @@ function RightPanelEmptyState(props: {
105 available: props.browserAvailable,108 available: props.browserAvailable,
106 disabledReason: SURFACE_DISABLED_REASONS.browser,109 disabledReason: SURFACE_DISABLED_REASONS.browser,
107 onClick: props.onAddBrowser,110 onClick: props.onAddBrowser,
111 badgeCount: 0,
108 },112 },
109 {113 {
110 label: "Terminal",114 label: "Terminal",
...@@ -113,6 +117,7 @@ function RightPanelEmptyState(props: {...@@ -113,6 +117,7 @@ function RightPanelEmptyState(props: {
113 available: true,117 available: true,
114 disabledReason: null,118 disabledReason: null,
115 onClick: props.onAddTerminal,119 onClick: props.onAddTerminal,
120 badgeCount: 0,
116 },121 },
117 {122 {
118 label: "Files",123 label: "Files",
...@@ -121,6 +126,7 @@ function RightPanelEmptyState(props: {...@@ -121,6 +126,7 @@ function RightPanelEmptyState(props: {
121 available: props.filesAvailable,126 available: props.filesAvailable,
122 disabledReason: SURFACE_DISABLED_REASONS.files,127 disabledReason: SURFACE_DISABLED_REASONS.files,
123 onClick: props.onAddFiles,128 onClick: props.onAddFiles,
129 badgeCount: 0,
124 },130 },
125 {131 {
126 label: "Diff",132 label: "Diff",
...@@ -129,6 +135,7 @@ function RightPanelEmptyState(props: {...@@ -129,6 +135,7 @@ function RightPanelEmptyState(props: {
129 available: props.diffAvailable,135 available: props.diffAvailable,
130 disabledReason: SURFACE_DISABLED_REASONS.diff,136 disabledReason: SURFACE_DISABLED_REASONS.diff,
131 onClick: props.onAddDiff,137 onClick: props.onAddDiff,
138 badgeCount: 0,
132 },139 },
133 {140 {
134 label: "Agents",141 label: "Agents",
...@@ -137,6 +144,7 @@ function RightPanelEmptyState(props: {...@@ -137,6 +144,7 @@ function RightPanelEmptyState(props: {
137 available: true,144 available: true,
138 disabledReason: null,145 disabledReason: null,
139 onClick: props.onAddAgents,146 onClick: props.onAddAgents,
147 badgeCount: props.liveAgentCount,
140 },148 },
141 ] as const;149 ] as const;
142150
...@@ -154,7 +162,17 @@ function RightPanelEmptyState(props: {...@@ -154,7 +162,17 @@ function RightPanelEmptyState(props: {
154 const Icon = action.icon;162 const Icon = action.icon;
155 const content = (163 const content = (
156 <>164 <>
157 <Icon className="mb-3 size-5" />165 <span className="relative mb-3 inline-flex">
166 <Icon className="size-5" />
167 {action.badgeCount > 0 ? (
168 <span
169 aria-hidden
170 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-[9px] font-semibold tabular-nums text-white"
171 >
172 {action.badgeCount}
173 </span>
174 ) : null}
175 </span>
158 <span className="text-sm font-medium">{action.label}</span>176 <span className="text-sm font-medium">{action.label}</span>
159 <span className="mt-1 text-xs leading-relaxed text-muted-foreground">177 <span className="mt-1 text-xs leading-relaxed text-muted-foreground">
160 {action.description}178 {action.description}
...@@ -498,6 +516,7 @@ export function RightPanelTabs(props: RightPanelTabsProps) {...@@ -498,6 +516,7 @@ export function RightPanelTabs(props: RightPanelTabsProps) {
498 browserAvailable={props.browserAvailable}516 browserAvailable={props.browserAvailable}
499 diffAvailable={props.diffAvailable}517 diffAvailable={props.diffAvailable}
500 filesAvailable={props.filesAvailable}518 filesAvailable={props.filesAvailable}
519 liveAgentCount={props.liveAgentCount}
501 />520 />
502 ) : (521 ) : (
503 props.children522 props.children
apps/web/src/components/chat/PanelLayoutControls.tsx+21-2
...@@ -11,6 +11,8 @@ interface PanelLayoutControlsProps {...@@ -11,6 +11,8 @@ interface PanelLayoutControlsProps {
11 rightPanelAvailable: boolean;11 rightPanelAvailable: boolean;
12 rightPanelOpen: boolean;12 rightPanelOpen: boolean;
13 rightPanelShortcutLabel: string | null;13 rightPanelShortcutLabel: string | null;
14 /** Running + waiting subagents in this thread; badges the right panel toggle. */
15 liveAgentCount: number;
14 onToggleTerminal: () => void;16 onToggleTerminal: () => void;
15 onToggleRightPanel: () => void;17 onToggleRightPanel: () => void;
16}18}
...@@ -22,6 +24,7 @@ export const PanelLayoutControls = memo(function PanelLayoutControls({...@@ -22,6 +24,7 @@ export const PanelLayoutControls = memo(function PanelLayoutControls({
22 rightPanelAvailable,24 rightPanelAvailable,
23 rightPanelOpen,25 rightPanelOpen,
24 rightPanelShortcutLabel,26 rightPanelShortcutLabel,
27 liveAgentCount,
25 onToggleTerminal,28 onToggleTerminal,
26 onToggleRightPanel,29 onToggleRightPanel,
27}: PanelLayoutControlsProps) {30}: PanelLayoutControlsProps) {
...@@ -59,18 +62,34 @@ export const PanelLayoutControls = memo(function PanelLayoutControls({...@@ -59,18 +62,34 @@ export const PanelLayoutControls = memo(function PanelLayoutControls({
59 className="shrink-0 [-webkit-app-region:no-drag]"62 className="shrink-0 [-webkit-app-region:no-drag]"
60 pressed={rightPanelOpen}63 pressed={rightPanelOpen}
61 onPressedChange={onToggleRightPanel}64 onPressedChange={onToggleRightPanel}
62 aria-label="Toggle right panel"65 aria-label={
66 liveAgentCount > 0
67 ? `Toggle right panel, ${liveAgentCount} ${liveAgentCount === 1 ? "agent" : "agents"} working`
68 : "Toggle right panel"
69 }
63 variant="ghost"70 variant="ghost"
64 size="sm"71 size="sm"
65 disabled={!rightPanelAvailable}72 disabled={!rightPanelAvailable}
66 >73 >
67 <PanelRightIcon className="size-3.5" />74 <PanelRightIcon className="size-3.5" />
75 {liveAgentCount > 0 ? (
76 <span
77 aria-hidden
78 className="absolute -top-1 -right-1 flex h-3.5 min-w-3.5 items-center justify-center rounded-full bg-info px-1 text-[9px] font-semibold tabular-nums text-white"
79 >
80 {liveAgentCount}
81 </span>
82 ) : null}
68 </Toggle>83 </Toggle>
69 }84 }
70 />85 />
71 <TooltipPopup side="bottom">86 <TooltipPopup side="bottom">
72 {rightPanelAvailable87 {rightPanelAvailable
73 ? `Toggle right panel${rightPanelShortcutLabel ? ` (${rightPanelShortcutLabel})` : ""}`88 ? `Toggle right panel${rightPanelShortcutLabel ? ` (${rightPanelShortcutLabel})` : ""}${
89 liveAgentCount > 0
90 ? ` · ${liveAgentCount} ${liveAgentCount === 1 ? "agent" : "agents"} working`
91 : ""
92 }`
74 : "Right panel is unavailable"}93 : "Right panel is unavailable"}
75 </TooltipPopup>94 </TooltipPopup>
76 </Tooltip>95 </Tooltip>