From 883c5e72baedb03aa556df23d2c31024be3daed0 Mon Sep 17 00:00:00 2001 From: Sawyer Date: Fri, 18 Sep 2026 00:06:41 -0700 Subject: [PATCH] feat(web): chip agent picker with search and Create agent; scoped New Agent drawer (CL-8477) --- apps/web/src/pages/new-workbench-picker.tsx | 197 ++++++++++++++++++-- 1 file changed, 181 insertions(+), 16 deletions(-) diff --git a/apps/web/src/pages/new-workbench-picker.tsx b/apps/web/src/pages/new-workbench-picker.tsx index a153edac85..1abb90cef2 100644 --- a/apps/web/src/pages/new-workbench-picker.tsx +++ b/apps/web/src/pages/new-workbench-picker.tsx @@ -3,6 +3,7 @@ // empty room and add people and agents as you go. import { Button, toast } from "@corbits/react-ui"; +import { useDismissablePopover } from "@corbits/react-ui/hooks/use-dismissable-popover"; import { PaperPlaneRight } from "@/lib/icons"; import { CHAT_STRINGS, WorkbenchLoadingState } from "@/chat"; import { isMyraAgent, listChatAgents } from "@/chat/threads-api"; @@ -16,6 +17,7 @@ import { createWorkbench, WorkbenchCreateError } from "../workbench-create"; import { useNavigate } from "../navigation"; import { StageTopBar } from "../shell/stage-top-bar"; import { workbenchPath } from "../workbench-path"; +import { CreateAgentPanel } from "./create-agent-panel"; const GENERIC_CREATE_FAILURE = "Something went wrong creating this workbench. Try again."; @@ -33,6 +35,7 @@ export function describeWorkbenchCreateFailure(cause: unknown, refId?: string): } const PROMPT_PLACEHOLDER = "What do you want your Workbench to do?"; +const AGENT_LISTBOX_ID = "new-workbench-agent-listbox"; /** A room's name is its opening ask, trimmed — an empty room is just * "Workbench" until it is renamed. */ @@ -47,8 +50,22 @@ export function NewWorkbenchPickerRoute() { const { selectedTenantId } = useBench(); const [prompt, setPrompt] = useState(""); const [selectedAgentIds, setSelectedAgentIds] = useState([]); + const [agentPickerOpen, setAgentPickerOpen] = useState(false); + const [createAgentOpen, setCreateAgentOpen] = useState(false); + const [agentQuery, setAgentQuery] = useState(""); + const [activeAgentIndex, setActiveAgentIndex] = useState(0); const promptRef = useRef(null); + const { + rootRef: agentPickerRootRef, + triggerRef: agentPickerTriggerRef, + close: dismissAgentPicker, + } = useDismissablePopover({ + closeOnEscape: false, + open: agentPickerOpen, + onOpenChange: setAgentPickerOpen, + }); + const agentsQuery = useQuery({ queryKey: ["bench-agents", selectedTenantId], queryFn: () => listChatAgents(selectedTenantId!), @@ -58,6 +75,13 @@ export function NewWorkbenchPickerRoute() { () => (agentsQuery.data ?? []).filter((agent) => !isMyraAgent(agent)), [agentsQuery.data], ); + const filteredAgents = useMemo(() => { + const query = agentQuery.trim().toLocaleLowerCase(); + return query === "" + ? pickableAgents + : pickableAgents.filter((agent) => agent.name.toLocaleLowerCase().includes(query)); + }, [pickableAgents, agentQuery]); + const selectedAgents = pickableAgents.filter((agent) => selectedAgentIds.includes(agent.id)); function toggleAgent(id: string) { setSelectedAgentIds((current) => @@ -65,6 +89,18 @@ export function NewWorkbenchPickerRoute() { ); } + function closeAgentPicker() { + setAgentQuery(""); + setActiveAgentIndex(0); + dismissAgentPicker(); + } + + function openAgentPicker() { + setAgentPickerOpen(true); + setAgentQuery(""); + setActiveAgentIndex(0); + } + const create = useMutation({ mutationFn: ({ benchTenantId, @@ -174,6 +210,136 @@ export function NewWorkbenchPickerRoute() {
+
+ In the room + {selectedAgents.map((agent) => ( + + {agent.name} + + + ))} + {agentsQuery.isLoading ? ( + Loading agents… + ) : agentsQuery.isError ? ( + + Couldn't load agents. You can still start without one. + + ) : ( +
+ + {agentPickerOpen ? ( +
+ { + setAgentQuery(event.target.value); + setActiveAgentIndex(0); + }} + onKeyDown={(event) => { + if (event.key === "ArrowDown") { + event.preventDefault(); + setActiveAgentIndex((current) => + Math.min(current + 1, filteredAgents.length - 1), + ); + } + if (event.key === "ArrowUp") { + event.preventDefault(); + setActiveAgentIndex((current) => Math.max(current - 1, 0)); + } + if (event.key === "Enter") { + // Implicit form submission would otherwise + // fire on a miss, creating a workbench the + // operator did not ask for. + event.preventDefault(); + const activeAgent = filteredAgents[activeAgentIndex]; + if (activeAgent !== undefined) { + toggleAgent(activeAgent.id); + } + } + if (event.key === "Escape") { + event.preventDefault(); + closeAgentPicker(); + } + }} + /> + {filteredAgents.length === 0 ? ( +

+ {pickableAgents.length === 0 + ? "No agents are available yet." + : "No agents match that search."} +

+ ) : ( +
+ {filteredAgents.map((agent, index) => { + const selected = selectedAgentIds.includes(agent.id); + return ( + + ); + })} +
+ )} + +
+ ) : null} +
+ )} +
+ {selectedTenantId !== null && createAgentOpen ? ( + { + void queryClient.invalidateQueries({ + queryKey: ["bench-agents", selectedTenantId], + }); + setSelectedAgentIds((current) => [...current, deployment.definitionAssetId]); + promptRef.current?.focus(); + }} + /> + ) : null} ); }