Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
44 changes: 39 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,17 @@ interface ChatActionsMenuItemsProps {
readonly isChildChat: boolean;
readonly hasWorkspace: boolean;
readonly isArchiving?: boolean;
/**
* Subagent toggle: when a chat has subagent children, the menu offers a
* "Show subagents (N)" / "Hide subagents" action that expands or collapses
* the child rows in the sidebar tree. The action is only rendered when
* `onToggleSubagents` is provided and `subagentCount` is greater than zero,
* so call sites that render outside the tree (e.g. the chat top bar) omit
* these props and the item stays hidden.
*/
readonly subagentCount?: number;
readonly isSubagentsExpanded?: boolean;
readonly onToggleSubagents?: () => void;
Comment thread
tracyjohnsonux marked this conversation as resolved.
Outdated
readonly onPinAgent?: () => void;
readonly onUnpinAgent?: () => void;
readonly onArchiveAgent: () => void;
Expand All @@ -58,6 +70,9 @@ export const ChatActionsMenuItems: FC<ChatActionsMenuItemsProps> = ({
isChildChat,
hasWorkspace,
isArchiving = false,
subagentCount = 0,
isSubagentsExpanded = false,
onToggleSubagents,
onPinAgent,
onUnpinAgent,
onArchiveAgent,
Expand All @@ -67,10 +82,23 @@ export const ChatActionsMenuItems: FC<ChatActionsMenuItemsProps> = ({
Item,
Separator,
}) => {
const showSubagentsToggle = Boolean(onToggleSubagents) && subagentCount > 0;
const showPinAction =
!isArchived && !isChildChat && Boolean(onPinAgent && onUnpinAgent);
const showArchiveActions = !isArchived && !isChildChat;

// Grouped with pin/rename rather than shown as its own section: the toggle
// sits directly under "Rename chat" (or under "Unarchive agent" for an
// archived parent). Only rendered when the chat actually has subagents.
const subagentToggle = showSubagentsToggle ? (
Comment thread
tracyjohnsonux marked this conversation as resolved.
Outdated
<Item onSelect={onToggleSubagents}>
<GitForkIcon className="size-3.5 rotate-180" />
{isSubagentsExpanded
? "Hide subagents"
: `Show subagents (${subagentCount})`}
</Item>
) : null;

return (
<>
{showPinAction && (
Expand All @@ -90,10 +118,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 +134,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,12 @@ export const ChatTreeNode: FC<ChatTreeNodeProps> = ({ chat, isChildNode }) => {
isChildChat: isChildNode,
hasWorkspace: Boolean(workspaceId),
isArchiving,
// Subagents are the visible child chats; the menu toggle expands or
// collapses them so they no longer have to be discovered by hovering
// the status icon. subagentCount gates the item, so leaf chats hide it.
subagentCount: childIDs.length,
Comment thread
tracyjohnsonux marked this conversation as resolved.
Outdated
isSubagentsExpanded: isExpanded,
onToggleSubagents: () => toggleExpanded(chatID),
onPinAgent: () => onPinAgent(chat.id),
onUnpinAgent: () => onUnpinAgent(chat.id),
onArchiveAgent: () => onArchiveAgent(chat.id),
Expand All @@ -174,8 +181,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 +245,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 +365,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