From b0e10402c805675b2fe1002179c3a56aeb66f87c Mon Sep 17 00:00:00 2001 From: Danielle Maywood Date: Thu, 5 Mar 2026 11:38:45 +0000 Subject: [PATCH] feat(site): searchable workspace selector in agent chat input (#22656) --- .../pages/AgentsPage/AgentsPage.stories.tsx | 125 ++++++++++++++---- site/src/pages/AgentsPage/AgentsPage.tsx | 81 +++++++----- 2 files changed, 145 insertions(+), 61 deletions(-) diff --git a/site/src/pages/AgentsPage/AgentsPage.stories.tsx b/site/src/pages/AgentsPage/AgentsPage.stories.tsx index 35d80c3aaf..c6a2f81958 100644 --- a/site/src/pages/AgentsPage/AgentsPage.stories.tsx +++ b/site/src/pages/AgentsPage/AgentsPage.stories.tsx @@ -55,34 +55,36 @@ type Story = StoryObj; export const Default: Story = {}; +const mockWorkspaces = [ + { + ...MockWorkspace, + id: "ws-1", + name: "my-project", + owner_name: "johndoe", + owner_id: "user-1", + }, + { + ...MockWorkspace, + id: "ws-2", + name: "my-project", + owner_name: "janedoe", + owner_id: "user-2", + }, + { + ...MockWorkspace, + id: "ws-3", + name: "backend-api", + owner_name: "johndoe", + owner_id: "user-1", + }, +]; + export const WithWorkspaces: Story = { beforeEach: () => { localStorage.clear(); spyOn(API, "getWorkspaces").mockResolvedValue({ - workspaces: [ - { - ...MockWorkspace, - id: "ws-1", - name: "my-project", - owner_name: "johndoe", - owner_id: "user-1", - }, - { - ...MockWorkspace, - id: "ws-2", - name: "my-project", - owner_name: "janedoe", - owner_id: "user-2", - }, - { - ...MockWorkspace, - id: "ws-3", - name: "backend-api", - owner_name: "johndoe", - owner_id: "user-1", - }, - ], - count: 3, + workspaces: mockWorkspaces, + count: mockWorkspaces.length, }); }, play: async ({ canvasElement }) => { @@ -92,8 +94,79 @@ export const WithWorkspaces: Story = { expect(trigger).toBeEnabled(); }); await userEvent.click(canvas.getByText("Workspace").closest("button")!); - // Wait for the portalled dropdown to appear so Chromatic captures it. - await within(canvasElement.ownerDocument.body).findByRole("listbox"); + // Wait for the portalled combobox dropdown to appear so Chromatic + // captures it. + await within(canvasElement.ownerDocument.body).findByRole("dialog"); + }, +}; + +export const SearchWorkspaces: Story = { + beforeEach: () => { + localStorage.clear(); + spyOn(API, "getWorkspaces").mockResolvedValue({ + workspaces: mockWorkspaces, + count: mockWorkspaces.length, + }); + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + await waitFor(() => { + const trigger = canvas.getByText("Workspace").closest("button")!; + expect(trigger).toBeEnabled(); + }); + await userEvent.click(canvas.getByText("Workspace").closest("button")!); + + const body = within(canvasElement.ownerDocument.body); + await body.findByRole("dialog"); + + // Type in the search input to filter workspaces. + const searchInput = body.getByPlaceholderText("Search workspaces..."); + await userEvent.type(searchInput, "backend"); + + // Only the matching workspace should remain visible. + await waitFor(() => { + const options = body.getAllByRole("option"); + // "Auto-create Workspace" is filtered out, only + // "johndoe/backend-api" matches. + expect(options).toHaveLength(1); + expect(options[0]).toHaveTextContent("johndoe/backend-api"); + }); + }, +}; + +export const SelectWorkspaceViaSearch: Story = { + beforeEach: () => { + localStorage.clear(); + spyOn(API, "getWorkspaces").mockResolvedValue({ + workspaces: mockWorkspaces, + count: mockWorkspaces.length, + }); + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + await waitFor(() => { + const trigger = canvas.getByText("Workspace").closest("button")!; + expect(trigger).toBeEnabled(); + }); + await userEvent.click(canvas.getByText("Workspace").closest("button")!); + + const body = within(canvasElement.ownerDocument.body); + await body.findByRole("dialog"); + + // Search for "janedoe" and select the result. + const searchInput = body.getByPlaceholderText("Search workspaces..."); + await userEvent.type(searchInput, "janedoe"); + + await waitFor(() => { + expect(body.getAllByRole("option")).toHaveLength(1); + }); + + await userEvent.click(body.getByRole("option", { name: /janedoe/ })); + + // The trigger should now show the selected workspace. + await waitFor(() => { + expect(canvas.getByText("janedoe/my-project")).toBeInTheDocument(); + }); }, }; diff --git a/site/src/pages/AgentsPage/AgentsPage.tsx b/site/src/pages/AgentsPage/AgentsPage.tsx index bb852d4ac7..47c98d2e6a 100644 --- a/site/src/pages/AgentsPage/AgentsPage.tsx +++ b/site/src/pages/AgentsPage/AgentsPage.tsx @@ -15,17 +15,20 @@ import { import { workspaces } from "api/queries/workspaces"; import type * as TypesGen from "api/typesGenerated"; import { ErrorAlert } from "components/Alert/ErrorAlert"; +import { ChevronDownIcon } from "components/AnimatedIcons/ChevronDown"; import type { ModelSelectorOption } from "components/ai-elements"; import { Button } from "components/Button/Button"; +import { + Combobox, + ComboboxContent, + ComboboxEmpty, + ComboboxInput, + ComboboxItem, + ComboboxList, + ComboboxTrigger, +} from "components/Combobox/Combobox"; import { ExternalImage } from "components/ExternalImage/ExternalImage"; import { CoderIcon } from "components/Icons/CoderIcon"; -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from "components/Select/Select"; import { useAuthenticated } from "hooks"; import { MonitorIcon, PanelLeftIcon } from "lucide-react"; import { useDashboard } from "modules/dashboard/useDashboard"; @@ -698,7 +701,7 @@ export const AgentsEmptyState: FC = ({ useState(initialSystemPrompt); const [systemPromptDraft, setSystemPromptDraft] = useState(initialSystemPrompt); - const workspacesQuery = useQuery(workspaces({ q: "owner:me", limit: 50 })); + const workspacesQuery = useQuery(workspaces({ q: "owner:me", limit: 0 })); const [selectedWorkspaceId, setSelectedWorkspaceId] = useState( () => { if (typeof window === "undefined") return null; @@ -850,38 +853,46 @@ export const AgentsEmptyState: FC = ({ inputStatusText={inputStatusText} modelCatalogStatusMessage={modelCatalogStatusMessage} leftActions={ - + + + + Auto-create Workspace + + {workspaceOptions.map((workspace) => ( + + {workspace.owner_name}/{workspace.name} + + ))} + + No workspaces found + + } />