diff --git a/desktop/src/features/agents/ui/CreateAgentStartDialog.tsx b/desktop/src/features/agents/ui/CreateAgentStartDialog.tsx index a32d2d760..05f8af9a8 100644 --- a/desktop/src/features/agents/ui/CreateAgentStartDialog.tsx +++ b/desktop/src/features/agents/ui/CreateAgentStartDialog.tsx @@ -1,21 +1,40 @@ import { FileUp, Plus } from "lucide-react"; +import * as React from "react"; -import { ProfileAvatar } from "@/features/profile/ui/ProfileAvatar"; import { useAgentTemplatesQuery } from "@/features/agents/hooks"; +import { ProfileAvatar } from "@/features/profile/ui/ProfileAvatar"; import type { AgentTemplate } from "@/shared/api/types"; -import { - Dialog, - DialogContent, - DialogDescription, - DialogHeader, - DialogTitle, -} from "@/shared/ui/dialog"; import { useFileImportZone } from "@/shared/hooks/useFileImportZone"; +import { cn } from "@/shared/lib/cn"; +import { Button } from "@/shared/ui/button"; +import { ChooserDialogContent } from "@/shared/ui/chooser-dialog-content"; +import { Dialog } from "@/shared/ui/dialog"; +import { Markdown } from "@/shared/ui/markdown"; +import { Skeleton } from "@/shared/ui/skeleton"; + +const agentInstructionMarkdownClassName = [ + "mt-3 leading-6 text-muted-foreground [&_blockquote]:!text-muted-foreground [&_code]:!text-muted-foreground [&_li]:text-muted-foreground [&_ol]:text-muted-foreground [&_p]:text-muted-foreground [&_strong]:text-muted-foreground [&_td]:text-muted-foreground [&_ul]:text-muted-foreground", + "[&>h1]:!text-sm [&>h1]:!font-semibold [&>h1]:!leading-6 [&>h1]:!tracking-normal [&>h1]:!text-foreground", + "[&>h2]:!text-sm [&>h2]:!font-semibold [&>h2]:!leading-6 [&>h2]:!tracking-normal [&>h2]:!text-foreground", + "[&>h3]:!text-sm [&>h3]:!font-semibold [&>h3]:!leading-6 [&>h3]:!tracking-normal [&>h3]:!text-foreground", + "[&>h4]:!text-sm [&>h4]:!font-semibold [&>h4]:!leading-6 [&>h4]:!tracking-normal [&>h4]:!text-foreground", + "[&>h5]:!text-sm [&>h5]:!font-semibold [&>h5]:!leading-6 [&>h5]:!tracking-normal [&>h5]:!text-foreground", + "[&>h6]:!text-sm [&>h6]:!font-semibold [&>h6]:!leading-6 [&>h6]:!tracking-normal [&>h6]:!text-foreground", +].join(" "); + +const sidebarRowClassName = + "flex w-full items-center gap-2 rounded-lg px-4 py-1.5 text-left transition-[background-color,color,box-shadow] focus:outline-hidden focus-visible:ring-2 focus-visible:ring-sidebar-ring/50 focus-visible:ring-offset-2 focus-visible:ring-offset-sidebar"; + +const sidebarRowIdleClassName = + "text-sidebar-foreground/70 hover:bg-sidebar-accent hover:text-sidebar-accent-foreground"; /** * Step 1 of the Create Agent wizard: choose a starting point — a blank - * agent, a built-in starter template, or an imported agent file. Selecting - * anything hands off to the details step (CreateAgentDialog) prefilled. + * agent, a built-in starter template, or an imported agent file. Uses the + * catalog chooser layout: a sidebar list on the left and a template detail + * pane on the right. Blank/import act immediately; picking a template + * previews it and "Use template" hands off to the details step + * (CreateAgentDialog) prefilled. */ export function CreateAgentStartDialog({ open, @@ -30,8 +49,12 @@ export function CreateAgentStartDialog({ onPickBlank: () => void; onPickTemplate: (template: AgentTemplate) => void; }) { + const contentRef = React.useRef(null); const templatesQuery = useAgentTemplatesQuery({ enabled: open }); const templates = templatesQuery.data ?? []; + const isLoading = templatesQuery.isLoading; + const error = + templatesQuery.error instanceof Error ? templatesQuery.error : null; const { fileInputRef, isDragOver, @@ -40,130 +63,318 @@ export function CreateAgentStartDialog({ openFilePicker, } = useFileImportZone({ onImportFile }); + const [selectedTemplateId, setSelectedTemplateId] = React.useState< + string | null + >(null); + const selectedTemplate = React.useMemo(() => { + if (templates.length === 0) { + return null; + } + + return ( + templates.find((template) => template.id === selectedTemplateId) ?? + templates[0] + ); + }, [templates, selectedTemplateId]); + + React.useEffect(() => { + if (!open) { + return; + } + + if (templates.length === 0) { + setSelectedTemplateId(null); + return; + } + + setSelectedTemplateId((current) => + current && templates.some((template) => template.id === current) + ? current + : templates[0].id, + ); + }, [open, templates]); + + const handleUseTemplate = () => { + if (!selectedTemplate) { + return; + } + + onPickTemplate(selectedTemplate); + }; + return ( - -
- {isDragOver ? ( -
-

- Drop an agent file to import -

-
- ) : null} + { + event.preventDefault(); + contentRef.current?.focus(); + }} + ref={contentRef} + scrollAreaClassName="flex min-h-0 overflow-hidden px-0" + scrollAreaTestId="create-agent-start-dialog-body" + tabIndex={-1} + title="New agent" + {...dropHandlers} + > + {isDragOver ? ( +
+

+ Drop an agent file to import +

+
+ ) : null} - - New agent - - Choose a starting point. You can adjust everything in the next - step. - - - -
-
- + - -
+ +
-
-

+

Templates

-
- {templates.map((template) => ( - - ))} -
+ + {isLoading ? : null} + + {!isLoading && templates.length > 0 ? ( +
+ {templates.map((template) => { + const isCurrent = template.id === selectedTemplate?.id; + + return ( + + ); + })} +
+ ) : null}
- +
+
+ {isLoading ? : null} + + {!isLoading && selectedTemplate ? ( + + ) : null} + + {!isLoading && templates.length === 0 && !error ? ( +
+
+

+ No templates available +

+

+ Start from a blank agent or import an agent file instead. +

+
+
+ ) : null} + + {error ? ( +

+ {error.message} +

+ ) : null} +
+ +
+ +
+
-
+ + +
); } -function TemplateCard({ - template, - onPick, -}: { - template: AgentTemplate; - onPick: (template: AgentTemplate) => void; -}) { - const firstLine = template.systemPrompt - .trim() - .split("\n") - .find((line) => line.trim().length > 0); - +function TemplateDetail({ template }: { template: AgentTemplate }) { return ( - + +
+

+ Agent instruction +

+ +
+ + ); +} + +function TemplateMetaGroup({ + items, +}: { + items: { label: string; value: string }[]; +}) { + return ( +
+
+ {items.map((item, index) => ( +
0 && + "border-t border-border/60 sm:border-t-0 sm:before:absolute sm:before:bottom-3 sm:before:left-0 sm:before:top-3 sm:before:w-px sm:before:bg-border/70", + )} + key={item.label} + > +

+ {item.label} +

+

+ {item.value} +

+
+ ))} +
+
+ ); +} + +function TemplateListSkeleton() { + return ( +
+ {["first", "second", "third", "fourth", "fifth"].map((key) => ( +
+ + +
+ ))} +
+ ); +} + +function TemplateDetailSkeleton() { + return ( +
+
+ + +
+
+ + + +
+ +
); } diff --git a/desktop/tests/e2e/agents.spec.ts b/desktop/tests/e2e/agents.spec.ts index fcefa05df..befc0c5cb 100644 --- a/desktop/tests/e2e/agents.spec.ts +++ b/desktop/tests/e2e/agents.spec.ts @@ -155,6 +155,7 @@ test("selecting a template prefills the create agent form", async ({ await openCreateAgentStart(page); await page.getByTestId("create-agent-template-builtin:fizz").click(); + await page.getByTestId("create-agent-start-use-template").click(); await expect(page.getByTestId("agent-name-input")).toHaveValue("Fizz"); await expect(page.getByTestId("agent-instructions-input")).toHaveValue( @@ -169,6 +170,7 @@ test("template submit creates an agent card in the grid", async ({ page }) => { await openCreateAgentStart(page); await page.getByTestId("create-agent-template-builtin:fizz").click(); + await page.getByTestId("create-agent-start-use-template").click(); await expect(page.getByTestId("agent-name-input")).toHaveValue("Fizz"); await expect(page.getByTestId("create-agent-submit")).toBeEnabled(); await page.getByTestId("create-agent-submit").click();