From b3f05a23fc0525467753f0c839216ba700863d9e Mon Sep 17 00:00:00 2001 From: TJ Date: Tue, 18 Aug 2026 13:25:42 -0700 Subject: [PATCH] feat(site/src/pages/AgentsPage): surface subagents in the chat sidebar (#28234) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## What Makes subagents more discoverable in the agent chat sidebar, without disturbing the existing status icon or layout. - **Parent chat rows show a subagent count** with a bot icon (e.g. `3 🤖`) at the start of the metadata line, next to the diff stats. Only shown when a chat has subagents; uses lucide `BotIcon`, the same icon this codebase already uses to denote agents. Passive indicator (the leading status icon, timestamp, and kebab are untouched). - **The chat actions menu gains a "Show subagents (N)" / "Hide subagents" toggle**, grouped directly under "Rename chat" in both the kebab (⋮) and the right-click context menu. It expands/collapses the row's subagents and is only rendered when the chat has subagents. Expansion still uses the existing sidebar expand state, so the hover chevron, the menu toggle, and the count all stay in sync. ## Notes / decisions - Subagent children are capped at depth 1, so the count and toggle only appear on parent chats. - The menu toggle lives in the shared `ChatActionsMenuItems`, so it appears in both the kebab and the right-click menu; the top-bar kebab intentionally does not pass the props, so it stays hidden there. - The on-row `N 🤖` indicator is currently passive (clicking the row opens the chat as before). Can be made a click-to-expand control if wanted. ## Testing Validated in Storybook (`ChatsSidebar` stories) with 0 TypeScript errors. Added a `SubagentsMenuToggle` story with a `play` function asserting the label swaps between "Show subagents (3)" and "Hide subagents" and that children expand. Existing `ChatsSidebar.test.tsx` does not assert menu contents and is unaffected. --- *This PR was generated by Coder Agents on behalf of @tracyjohnsonux.* --- .../components/ChatActionsMenuItems.tsx | 33 ++++++-- .../ChatsSidebar/ChatsSidebar.stories.tsx | 80 +++++++++++++++++++ .../ChatsSidebar/tree/ChatTreeNode.tsx | 19 ++++- 3 files changed, 124 insertions(+), 8 deletions(-) diff --git a/site/src/pages/AgentsPage/components/ChatActionsMenuItems.tsx b/site/src/pages/AgentsPage/components/ChatActionsMenuItems.tsx index 84b35e4208..7cc0055150 100644 --- a/site/src/pages/AgentsPage/components/ChatActionsMenuItems.tsx +++ b/site/src/pages/AgentsPage/components/ChatActionsMenuItems.tsx @@ -1,6 +1,7 @@ import { ArchiveIcon, ArchiveRestoreIcon, + GitForkIcon, PinIcon, PinOffIcon, SquarePenIcon, @@ -41,6 +42,9 @@ interface ChatActionsMenuItemsProps { readonly isChildChat: boolean; readonly hasWorkspace: boolean; readonly isArchiving?: boolean; + readonly subagentCount?: number; + readonly isSubagentsExpanded?: boolean; + readonly onToggleSubagents?: () => void; readonly onPinAgent?: () => void; readonly onUnpinAgent?: () => void; readonly onArchiveAgent: () => void; @@ -58,6 +62,9 @@ export const ChatActionsMenuItems: FC = ({ isChildChat, hasWorkspace, isArchiving = false, + subagentCount = 0, + isSubagentsExpanded = false, + onToggleSubagents, onPinAgent, onUnpinAgent, onArchiveAgent, @@ -67,10 +74,20 @@ export const ChatActionsMenuItems: FC = ({ Item, Separator, }) => { + const showSubagentsToggle = Boolean(onToggleSubagents) && subagentCount > 0; const showPinAction = !isArchived && !isChildChat && Boolean(onPinAgent && onUnpinAgent); const showArchiveActions = !isArchived && !isChildChat; + const subagentToggle = showSubagentsToggle ? ( + + + {isSubagentsExpanded + ? "Hide subagents" + : `Show subagents (${subagentCount})`} + + ) : null; + return ( <> {showPinAction && ( @@ -90,10 +107,13 @@ export const ChatActionsMenuItems: FC = ({ )} {isArchived ? ( !isChildChat && ( - - - Unarchive agent - + <> + + + Unarchive agent + + {subagentToggle} + ) ) : ( <> @@ -103,9 +123,12 @@ export const ChatActionsMenuItems: FC = ({ Rename chat )} + {subagentToggle} {showArchiveActions && ( <> - {(onOpenRenameDialog || showPinAction) && } + {(onOpenRenameDialog || showPinAction || showSubagentsToggle) && ( + + )} { + const canvas = within(canvasElement); + await waitFor(() => { + expect(canvas.getByText("Parent with subagents")).toBeInTheDocument(); + }); + // Collapsed by default: children are not rendered yet. + expect(canvas.queryByText("Subagent one")).not.toBeInTheDocument(); + + const trigger = canvas.getByLabelText( + "Open actions for Parent with subagents", + ); + await userEvent.click(trigger); + const body = within(document.body); + await waitFor(() => { + expect(body.getByText("Show subagents (3)")).toBeInTheDocument(); + }); + + // Selecting the toggle closes the menu and expands the children. + await userEvent.click(body.getByText("Show subagents (3)")); + await waitFor(() => { + expect(canvas.getByText("Subagent one")).toBeInTheDocument(); + }); + + // Reopening the menu now offers the inverse action. + await userEvent.click( + canvas.getByLabelText("Open actions for Parent with subagents"), + ); + await waitFor(() => { + expect( + within(document.body).getByText("Hide subagents"), + ).toBeInTheDocument(); + }); + }, +}; + export const ArchivedChildChatRowHasNoActionsMenu: Story = { args: { chats: [ diff --git a/site/src/pages/AgentsPage/components/ChatsSidebar/tree/ChatTreeNode.tsx b/site/src/pages/AgentsPage/components/ChatsSidebar/tree/ChatTreeNode.tsx index be0480ca49..04ed47cc7a 100644 --- a/site/src/pages/AgentsPage/components/ChatsSidebar/tree/ChatTreeNode.tsx +++ b/site/src/pages/AgentsPage/components/ChatsSidebar/tree/ChatTreeNode.tsx @@ -1,4 +1,5 @@ import { + BotIcon, ChevronDownIcon, ChevronRightIcon, EllipsisVerticalIcon, @@ -150,6 +151,9 @@ export const ChatTreeNode: FC = ({ chat, isChildNode }) => { isChildChat: isChildNode, hasWorkspace: Boolean(workspaceId), isArchiving, + subagentCount: childIDs.length, + isSubagentsExpanded: isExpanded, + onToggleSubagents: () => toggleExpanded(chatID), onPinAgent: () => onPinAgent(chat.id), onUnpinAgent: () => onUnpinAgent(chat.id), onArchiveAgent: () => onArchiveAgent(chat.id), @@ -174,8 +178,6 @@ export const ChatTreeNode: FC = ({ chat, isChildNode }) => { "group relative flex min-w-0 select-none [@media(pointer:coarse)]:[-webkit-touch-callout:none] items-start gap-1.5 rounded-md pl-1 pr-1.5 text-content-secondary", "transition-none [@media(hover:hover)]:hover:bg-surface-tertiary/50 [@media(hover:hover)]:hover:text-content-primary has-[[data-state=open]]:bg-surface-tertiary", "has-[[aria-current=page]]:bg-surface-quaternary/25 has-[[aria-current=page]]:text-content-primary [@media(hover:hover)]:has-[[aria-current=page]]:hover:bg-surface-quaternary/50", - isChildNode && - "before:absolute before:-left-2.5 before:top-[17px] before:h-px before:w-2.5 before:bg-border-default/70", )} >
= ({ chat, isChildNode }) => { )}
+ {hasChildren && ( + + {childIDs.length} + + )} {hasLinkedDiffStatus && hasLineStats && ( = ({ chat, isChildNode }) => { {hasChildren && isExpanded && ( -
+
{childIDs.map((childID) => { const childChat = chatById.get(childID); if (!childChat) return null;