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;