Skip to content
Merged
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
33 changes: 28 additions & 5 deletions site/src/pages/AgentsPage/components/ChatActionsMenuItems.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {
ArchiveIcon,
ArchiveRestoreIcon,
GitForkIcon,
PinIcon,
PinOffIcon,
SquarePenIcon,
Expand Down Expand Up @@ -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;
Expand All @@ -58,6 +62,9 @@ export const ChatActionsMenuItems: FC<ChatActionsMenuItemsProps> = ({
isChildChat,
hasWorkspace,
isArchiving = false,
subagentCount = 0,
isSubagentsExpanded = false,
onToggleSubagents,
onPinAgent,
onUnpinAgent,
onArchiveAgent,
Expand All @@ -67,10 +74,20 @@ export const ChatActionsMenuItems: FC<ChatActionsMenuItemsProps> = ({
Item,
Separator,
}) => {
const showSubagentsToggle = Boolean(onToggleSubagents) && subagentCount > 0;
const showPinAction =
!isArchived && !isChildChat && Boolean(onPinAgent && onUnpinAgent);
const showArchiveActions = !isArchived && !isChildChat;

const subagentToggle = showSubagentsToggle ? (
<Item onSelect={onToggleSubagents}>
<GitForkIcon className="size-3.5 rotate-180" />
{isSubagentsExpanded
? "Hide subagents"
: `Show subagents (${subagentCount})`}
</Item>
) : null;

return (
<>
{showPinAction && (
Expand All @@ -90,10 +107,13 @@ export const ChatActionsMenuItems: FC<ChatActionsMenuItemsProps> = ({
)}
{isArchived ? (
!isChildChat && (
<Item disabled={isArchiving} onSelect={onUnarchiveAgent}>
<ArchiveRestoreIcon className="size-3.5" />
Unarchive agent
</Item>
<>
<Item disabled={isArchiving} onSelect={onUnarchiveAgent}>
<ArchiveRestoreIcon className="size-3.5" />
Unarchive agent
</Item>
{subagentToggle}
</>
)
) : (
<>
Expand All @@ -103,9 +123,12 @@ export const ChatActionsMenuItems: FC<ChatActionsMenuItemsProps> = ({
Rename chat
</Item>
)}
{subagentToggle}
{showArchiveActions && (
<>
{(onOpenRenameDialog || showPinAction) && <Separator />}
{(onOpenRenameDialog || showPinAction || showSubagentsToggle) && (
<Separator />
)}
<Item
className="text-content-destructive focus:text-content-destructive"
disabled={isArchiving}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2007,6 +2007,86 @@ export const AgentWithWorkspaceMenuFull: Story = {
},
};

// A collapsed parent chat exposes a "Show subagents (N)" action in its
// actions menu; selecting it expands the children and the label flips to
// "Hide subagents". Leaf chats never show the toggle.
export const SubagentsMenuToggle: Story = {
args: {
chats: [
buildChat({
id: "root-subagents",
title: "Parent with subagents",
workspace_id: "workspace-1",
updated_at: recentTimestamp,
children: [
buildChat({
id: "subagent-1",
title: "Subagent one",
parent_chat_id: "root-subagents",
root_chat_id: "root-subagents",
}),
buildChat({
id: "subagent-2",
title: "Subagent two",
parent_chat_id: "root-subagents",
root_chat_id: "root-subagents",
}),
buildChat({
id: "subagent-3",
title: "Subagent three",
parent_chat_id: "root-subagents",
root_chat_id: "root-subagents",
}),
],
}),
],
},
parameters: {
reactRouter: reactRouterParameters({
// Route to the parent (not a child) so the tree starts collapsed and
// the menu reads "Show subagents (3)".
location: {
path: "/agents/root-subagents",
pathParams: { agentId: "root-subagents" },
},
routing: agentsRouting,
}),
},
play: async ({ canvasElement }) => {
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: [
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {
BotIcon,
ChevronDownIcon,
ChevronRightIcon,
EllipsisVerticalIcon,
Expand Down Expand Up @@ -150,6 +151,9 @@ export const ChatTreeNode: FC<ChatTreeNodeProps> = ({ 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),
Expand All @@ -174,8 +178,6 @@ export const ChatTreeNode: FC<ChatTreeNodeProps> = ({ 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",
)}
>
<div
Expand Down Expand Up @@ -240,6 +242,17 @@ export const ChatTreeNode: FC<ChatTreeNodeProps> = ({ chat, isChildNode }) => {
)}
</div>
<div className="flex min-w-0 items-center gap-1.5">
{hasChildren && (
<span
className="inline-flex shrink-0 items-center gap-0.5 text-[13px] leading-4 tabular-nums text-content-secondary"
title={`${childIDs.length} ${
childIDs.length === 1 ? "subagent" : "subagents"
}`}
>
{childIDs.length}
<BotIcon className="size-3.5" aria-hidden="true" />
</span>
)}
{hasLinkedDiffStatus && hasLineStats && (
<span
className="inline-flex shrink-0 items-center gap-0.5 text-[13px] leading-4 tabular-nums"
Expand Down Expand Up @@ -349,7 +362,7 @@ export const ChatTreeNode: FC<ChatTreeNodeProps> = ({ chat, isChildNode }) => {
</ContextMenu>

{hasChildren && isExpanded && (
<div className="relative ml-4 flex flex-col border-l border-border-default/60 pl-2.5">
<div className="relative ml-4 flex flex-col pl-2.5">
{childIDs.map((childID) => {
const childChat = chatById.get(childID);
if (!childChat) return null;
Expand Down
Loading