diff --git a/site/src/pages/AgentsPage/AgentsSidebar.stories.tsx b/site/src/pages/AgentsPage/AgentsSidebar.stories.tsx index 54184d7e83..a2c8bb1eee 100644 --- a/site/src/pages/AgentsPage/AgentsSidebar.stories.tsx +++ b/site/src/pages/AgentsPage/AgentsSidebar.stories.tsx @@ -59,6 +59,7 @@ const meta: Meta = { modelOptions: defaultModelOptions, modelConfigs: defaultModelConfigs, onArchiveAgent: fn(), + onArchiveAndDeleteWorkspace: fn(), onNewAgent: fn(), isCreating: false, }, @@ -323,3 +324,156 @@ export const ActiveChatAncestryExpanded: Story = { ).toHaveAttribute("aria-expanded", "true"); }, }; + +const todayTimestamp = new Date().toISOString(); + +export const ArchivedAgentsCollapsed: Story = { + args: { + chats: [ + buildChat({ + id: "active-1", + title: "Active agent one", + updated_at: todayTimestamp, + }), + buildChat({ + id: "active-2", + title: "Active agent two", + updated_at: todayTimestamp, + }), + buildChat({ + id: "archived-1", + title: "Archived agent one", + archived: true, + }), + buildChat({ + id: "archived-2", + title: "Archived agent two", + archived: true, + }), + ], + }, + parameters: { + reactRouter: reactRouterParameters({ + location: { path: "/agents" }, + routing: agentsRouting, + }), + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + await waitFor(() => { + expect(canvas.getByText("Active agent one")).toBeInTheDocument(); + expect(canvas.getByText("Active agent two")).toBeInTheDocument(); + expect(canvas.getByText("Archived (2)")).toBeInTheDocument(); + }); + expect(canvas.queryByText("Archived agent one")).not.toBeInTheDocument(); + expect(canvas.queryByText("Archived agent two")).not.toBeInTheDocument(); + }, +}; + +export const ArchivedAgentsExpanded: Story = { + args: { + chats: [ + buildChat({ + id: "active-1", + title: "Active agent one", + updated_at: todayTimestamp, + }), + buildChat({ + id: "active-2", + title: "Active agent two", + updated_at: todayTimestamp, + }), + buildChat({ + id: "archived-1", + title: "Archived agent one", + archived: true, + }), + buildChat({ + id: "archived-2", + title: "Archived agent two", + archived: true, + }), + ], + }, + parameters: { + reactRouter: reactRouterParameters({ + location: { path: "/agents" }, + routing: agentsRouting, + }), + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + await waitFor(() => { + expect(canvas.getByText("Archived (2)")).toBeInTheDocument(); + }); + await userEvent.click(canvas.getByText("Archived (2)")); + await waitFor(() => { + expect(canvas.getByText("Archived agent one")).toBeInTheDocument(); + expect(canvas.getByText("Archived agent two")).toBeInTheDocument(); + }); + }, +}; + +export const ArchivedAgentsSearchAutoExpands: Story = { + args: { + chats: [ + buildChat({ + id: "active-task", + title: "Active task", + updated_at: todayTimestamp, + }), + buildChat({ + id: "old-archived", + title: "Old archived task", + archived: true, + }), + ], + }, + parameters: { + reactRouter: reactRouterParameters({ + location: { path: "/agents" }, + routing: agentsRouting, + }), + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + await userEvent.type( + canvas.getByPlaceholderText("Search agents..."), + "archived", + ); + await waitFor(() => { + expect(canvas.getByText("Old archived task")).toBeInTheDocument(); + }); + }, +}; + +export const NoArchivedSection: Story = { + args: { + chats: [ + buildChat({ + id: "chat-a", + title: "First active agent", + updated_at: todayTimestamp, + }), + buildChat({ + id: "chat-b", + title: "Second active agent", + updated_at: todayTimestamp, + }), + ], + }, + parameters: { + reactRouter: reactRouterParameters({ + location: { path: "/agents" }, + routing: agentsRouting, + }), + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + await waitFor(() => { + expect(canvas.getByText("First active agent")).toBeInTheDocument(); + expect(canvas.getByText("Second active agent")).toBeInTheDocument(); + }); + expect(canvas.queryByText(/^Archived \(/)).not.toBeInTheDocument(); + }, +}; diff --git a/site/src/pages/AgentsPage/AgentsSidebar.tsx b/site/src/pages/AgentsPage/AgentsSidebar.tsx index 13002bf5bd..bae9dbbfba 100644 --- a/site/src/pages/AgentsPage/AgentsSidebar.tsx +++ b/site/src/pages/AgentsPage/AgentsSidebar.tsx @@ -7,6 +7,11 @@ import type { import { ErrorAlert } from "components/Alert/ErrorAlert"; import type { ModelSelectorOption } from "components/ai-elements"; import { Button } from "components/Button/Button"; +import { + Collapsible, + CollapsibleContent, + CollapsibleTrigger, +} from "components/Collapsible/Collapsible"; import { DropdownMenu, DropdownMenuContent, @@ -537,6 +542,7 @@ export const AgentsSidebar: FC = (props) => { const [search, setSearch] = useState(""); const normalizedSearch = search.trim().toLowerCase(); const [expandedById, setExpandedById] = useState>({}); + const [isArchivedExpanded, setIsArchivedExpanded] = useState(false); const chatTree = useMemo(() => buildChatTree(chats), [chats]); const chatById = useMemo(() => { @@ -555,6 +561,24 @@ export const AgentsSidebar: FC = (props) => { () => chatTree.rootIds.filter((chatID) => visibleChatIDs.has(chatID)), [chatTree.rootIds, visibleChatIDs], ); + const activeRootIDs = useMemo( + () => + visibleRootIDs.filter((id) => { + const chat = chatById.get(id); + return chat && !chat.archived; + }), + [visibleRootIDs, chatById], + ); + const archivedRootIDs = useMemo( + () => + visibleRootIDs.filter((id) => { + const chat = chatById.get(id); + return chat?.archived; + }), + [visibleRootIDs, chatById], + ); + const effectiveArchivedExpanded = + normalizedSearch && archivedRootIDs.length > 0 ? true : isArchivedExpanded; // Auto-expand ancestors of the active chat so it's always visible. useEffect(() => { @@ -704,37 +728,76 @@ export const AgentsSidebar: FC = (props) => { ) : ( - {visibleRootIDs.length === 0 ? ( + {activeRootIDs.length === 0 && archivedRootIDs.length === 0 ? (
{normalizedSearch ? "No matching agents" : "No agents yet"}
) : ( - TIME_GROUPS.map((group) => { - const groupChats = visibleRootIDs - .map((id) => chatById.get(id)) - .filter( - (chat): chat is Chat => - chat !== undefined && - getTimeGroup(chat.updated_at) === group, - ); - if (groupChats.length === 0) return null; - return ( -
-
- {group} -
-
- {groupChats.map((chat) => ( - - ))} -
+
+ {activeRootIDs.length > 0 && ( +
+ {TIME_GROUPS.map((group) => { + const groupChats = activeRootIDs + .map((id) => chatById.get(id)) + .filter( + (chat): chat is Chat => + chat !== undefined && + getTimeGroup(chat.updated_at) === group, + ); + if (groupChats.length === 0) return null; + return ( +
+
+ {group} +
+
+ {groupChats.map((chat) => ( + + ))} +
+
+ ); + })}
- ); - }) + )} + {archivedRootIDs.length > 0 && ( + + +
+ Archived ({archivedRootIDs.length}) + {effectiveArchivedExpanded ? ( + + ) : ( + + )} +
+
+ +
+ {archivedRootIDs.map((id) => { + const chat = chatById.get(id); + if (!chat) return null; + return ( + + ); + })} +
+
+
+ )}{" "} +
)} )}