diff --git a/web/oss/src/components/AgentChatSlice/AgentConversation.tsx b/web/oss/src/components/AgentChatSlice/AgentConversation.tsx index 3ebd6526ac..d6c2868e43 100644 --- a/web/oss/src/components/AgentChatSlice/AgentConversation.tsx +++ b/web/oss/src/components/AgentChatSlice/AgentConversation.tsx @@ -21,10 +21,7 @@ import { filesDrawerOpenAtomFamily, filesDrawerStagedAtomFamily, } from "@/oss/components/Drives/SessionFilesDrawer" -import {TEMPLATE_STRIP_MODE} from "@/oss/components/pages/agent-home/assets/constants" import {openTraceDrawerAtom} from "@/oss/components/SharedDrawers/TraceDrawer/store/traceDrawerStore" -import {STRIP_COPY} from "@/oss/components/TemplateStrip/assets/constants" -import CopiedToast from "@/oss/components/TemplateStrip/components/CopiedToast" import {describeAccepted} from "./assets/attachments" import {CONTENT_VISIBILITY_ENABLED} from "./assets/conversationLayout" @@ -727,14 +724,6 @@ const AgentConversation = ({ /> - - {TEMPLATE_STRIP_MODE ? ( - onboardingChat.setCopiedToastOpen(false)} - /> - ) : null} ) diff --git a/web/oss/src/components/AgentChatSlice/components/AgentComposerDock.tsx b/web/oss/src/components/AgentChatSlice/components/AgentComposerDock.tsx index cd68b54a64..e24f3d8e3a 100644 --- a/web/oss/src/components/AgentChatSlice/components/AgentComposerDock.tsx +++ b/web/oss/src/components/AgentChatSlice/components/AgentComposerDock.tsx @@ -125,7 +125,6 @@ const AgentComposerDock = ({ streamIdeBubble, ideHandoffActive, handleStartOver, - handleCodingAgentCopy, showBareOnboardingHero, } = onboardingChat const { @@ -377,7 +376,6 @@ const AgentComposerDock = ({ // (shared component), with the one-click copy + toast handoff. ) : ( diff --git a/web/oss/src/components/AgentChatSlice/hooks/useOnboardingChat.ts b/web/oss/src/components/AgentChatSlice/hooks/useOnboardingChat.ts index 5dd6506c91..b8b5485620 100644 --- a/web/oss/src/components/AgentChatSlice/hooks/useOnboardingChat.ts +++ b/web/oss/src/components/AgentChatSlice/hooks/useOnboardingChat.ts @@ -4,7 +4,6 @@ import {workflowMolecule} from "@agenta/entities/workflow" import {generateId} from "@agenta/shared/utils" import {type RichChatInputHandle} from "@agenta/ui/rich-chat-input" import {type UIMessage} from "ai" -import {App} from "antd" import {useAtomValue} from "jotai" import { @@ -18,7 +17,6 @@ import { } from "@/oss/components/pages/agent-home/assets/onboardingAnalytics" import {type AgentTemplate} from "@/oss/components/pages/agent-home/assets/templates" import {useOptionalOnboardingContext} from "@/oss/components/pages/agent-home/PlaygroundOnboarding/OnboardingContext" -import {buildCodingAgentClipboard} from "@/oss/components/TemplateStrip/assets/codingAgentClipboard" import {useTemplateProvenance} from "@/oss/components/TemplateStrip/hooks/useTemplateProvenance" import {usePostHogAg} from "@/oss/lib/helpers/analytics/hooks/usePostHogAg" @@ -52,7 +50,6 @@ export const useOnboardingChat = ({ // eases in a beat later (see `chromeRevealed`) so it doesn't move the composer during the send. const chromeHidden = !!onboarding && !onboarding.chromeRevealed const onboardingPosthog = usePostHogAg() - const {message: appMessage} = App.useApp() // ── Template strip (TEMPLATE_STRIP_MODE) ───────────────────────────────── // One provenance instance per panel, shared by the onboarding hero strip (S5) and the @@ -76,7 +73,6 @@ export const useOnboardingChat = ({ const revisionVersion = revisionQuery.data?.version const isFreshAgentRevision = !revisionQuery.isPending && typeof revisionVersion === "number" && revisionVersion <= 1 - const [copiedToastOpen, setCopiedToastOpen] = useState(false) const handleStripPick = useCallback( (template: AgentTemplate) => { stripProvenance.pick(template) @@ -94,20 +90,6 @@ export const useOnboardingChat = ({ }, [stripProvenance.pick, onboardingPosthog, onboardingActive], ) - const handleCodingAgentCopy = useCallback(async () => { - const text = richInputRef.current?.getMarkdown().trim() ?? "" - try { - await navigator.clipboard.writeText(buildCodingAgentClipboard(text)) - setCopiedToastOpen(true) - } catch { - appMessage.error("Couldn't copy — copy it manually") - return - } - captureFirstAgentIntent(onboardingPosthog, { - source: "composer", - properties: {action: "coding_agent_copy", message: truncateForCapture(text)}, - }) - }, [appMessage, onboardingPosthog]) // Optimistic first turn: the description the user submitted with "Create agent", shown as a sent // user message + assistant loading placeholder DURING commit + until the real conversation takes @@ -264,9 +246,6 @@ export const useOnboardingChat = ({ selectedTemplateKey: stripProvenance.selectedTemplateKey, handleStripPick, isFreshAgentRevision, - copiedToastOpen, - setCopiedToastOpen, - handleCodingAgentCopy, pendingFirstTurn, pendingFirstMessage, handleCreateAgent, diff --git a/web/oss/src/components/TemplateStrip/assets/codingAgentClipboard.test.ts b/web/oss/src/components/TemplateStrip/assets/codingAgentClipboard.test.ts deleted file mode 100644 index 7d825a7c5a..0000000000 --- a/web/oss/src/components/TemplateStrip/assets/codingAgentClipboard.test.ts +++ /dev/null @@ -1,25 +0,0 @@ -import {describe, expect, it} from "vitest" - -import {buildCodingAgentClipboard} from "./codingAgentClipboard" - -describe("buildCodingAgentClipboard", () => { - it("joins the install command, the instruction line, and the text", () => { - expect(buildCodingAgentClipboard("Review my PRs")).toBe( - "npx skills add Agenta-AI/agenta-skills\n\n" + - "Then use the Agenta skills to create an agent that does the following:\n\n" + - "Review my PRs", - ) - }) - - it("trims the text", () => { - expect(buildCodingAgentClipboard(" hello \n")).toMatch(/following:\n\nhello$/) - }) - - it("falls back to the placeholder when empty", () => { - expect(buildCodingAgentClipboard("")).toMatch(/following:\n\n$/) - }) - - it("falls back to the placeholder when whitespace-only", () => { - expect(buildCodingAgentClipboard(" \n\t")).toMatch(/$/) - }) -}) diff --git a/web/oss/src/components/TemplateStrip/assets/codingAgentClipboard.ts b/web/oss/src/components/TemplateStrip/assets/codingAgentClipboard.ts deleted file mode 100644 index 1d60f67e2f..0000000000 --- a/web/oss/src/components/TemplateStrip/assets/codingAgentClipboard.ts +++ /dev/null @@ -1,7 +0,0 @@ -import {CODING_AGENT_INSTALL} from "./constants" - -/** Clipboard payload for the "Use my coding agent" handoff (owner-authoritative shape). */ -export const buildCodingAgentClipboard = (text: string): string => - `${CODING_AGENT_INSTALL}\n\n` + - `Then use the Agenta skills to create an agent that does the following:\n\n` + - `${text.trim() || ""}` diff --git a/web/oss/src/components/TemplateStrip/assets/constants.ts b/web/oss/src/components/TemplateStrip/assets/constants.ts index 87dbf5fa17..eb365e994d 100644 --- a/web/oss/src/components/TemplateStrip/assets/constants.ts +++ b/web/oss/src/components/TemplateStrip/assets/constants.ts @@ -6,19 +6,12 @@ export const STRIP_COPY = { showAgain: "show again", hideMenuItem: "Don't show again", fromTemplate: "From template:", - useCodingAgent: "Use my coding agent", createAgent: "Create agent", creatingAgent: "Creating agent", - copiedToast: "Copied — paste into Claude Code, Cursor, Codex, or any coding agent", // Single source for every "describe an agent" composer (home hero + playground onboarding). describeAgentPlaceholder: "e.g. Watch our #support channel, triage each thread by urgency, and route it to the right owner — ask me before closing anything.", } as const -/** Owner-specified install command (differs from the flag-off IDE_INSTALL_COMMAND on purpose). */ -export const CODING_AGENT_INSTALL = "npx skills add Agenta-AI/agenta-skills" - /** Owner-specified key (matches the design prototype; intentionally not "agenta:"-prefixed). */ export const STRIP_HIDDEN_STORAGE_KEY = "agenta-tpl-strip-hidden" - -export const TOAST_DISMISS_MS = 2600 diff --git a/web/oss/src/components/TemplateStrip/components/AgentIntentActions.tsx b/web/oss/src/components/TemplateStrip/components/AgentIntentActions.tsx index 295da0406a..5165470cd9 100644 --- a/web/oss/src/components/TemplateStrip/components/AgentIntentActions.tsx +++ b/web/oss/src/components/TemplateStrip/components/AgentIntentActions.tsx @@ -1,4 +1,4 @@ -import {ArrowRight, Terminal} from "@phosphor-icons/react" +import {ArrowRight} from "@phosphor-icons/react" import {Button} from "antd" import {STRIP_COPY} from "../assets/constants" @@ -6,8 +6,6 @@ import {STRIP_COPY} from "../assets/constants" interface AgentIntentActionsProps { /** Create the agent from the composer's current text. */ onCreate: () => void - /** Copy the coding-agent install command + current text to the clipboard. */ - onCodingAgentCopy: () => void /** Create is in flight (e.g. committing the ephemeral) — spins the button and swaps its label. */ loading?: boolean } @@ -16,11 +14,8 @@ interface AgentIntentActionsProps { * The trailing action cluster every "describe an agent" composer shares (home hero and the * playground onboarding composer), so the surfaces can't drift apart again. */ -const AgentIntentActions = ({onCreate, onCodingAgentCopy, loading}: AgentIntentActionsProps) => ( +const AgentIntentActions = ({onCreate, loading}: AgentIntentActionsProps) => (
-
diff --git a/web/oss/src/components/pages/agent-home/StripHome.tsx b/web/oss/src/components/pages/agent-home/StripHome.tsx index 55c8ac7cc2..9fa13e7072 100644 --- a/web/oss/src/components/pages/agent-home/StripHome.tsx +++ b/web/oss/src/components/pages/agent-home/StripHome.tsx @@ -6,7 +6,7 @@ import {pageContentWidthClass} from "@agenta/ui/components/page-width" import {PanelScroll, PanelSurface} from "@agenta/ui/components/presentational" import type {RichChatInputHandle} from "@agenta/ui/rich-chat-input" import {ArrowLeftIcon} from "@phosphor-icons/react" -import {App, Typography} from "antd" +import {Typography} from "antd" import clsx from "clsx" import {useAtomValue, useSetAtom} from "jotai" import Link from "next/link" @@ -16,18 +16,13 @@ import NewAgentButton from "@/oss/components/NewAgentButton" import NextTriggersSection from "@/oss/components/NextTriggers" import {agentsWorkflowsAtom, agentsWorkflowsLoadingAtom} from "@/oss/components/pages/agents/store" import TemplateStrip from "@/oss/components/TemplateStrip" -import {buildCodingAgentClipboard} from "@/oss/components/TemplateStrip/assets/codingAgentClipboard" -import {STRIP_COPY} from "@/oss/components/TemplateStrip/assets/constants" -import CopiedToast from "@/oss/components/TemplateStrip/components/CopiedToast" import StripComposer from "@/oss/components/TemplateStrip/components/StripComposer" import {useTemplateProvenance} from "@/oss/components/TemplateStrip/hooks/useTemplateProvenance" import UsageSummary from "@/oss/components/UsageSummary" import useURL from "@/oss/hooks/useURL" -import {usePostHogAg} from "@/oss/lib/helpers/analytics/hooks/usePostHogAg" import {layoutFullHeightRequestAtom} from "@/oss/state/layout/fullHeight" import {HERO, RETURNING_HERO} from "./assets/constants" -import {captureFirstAgentIntent, truncateForCapture} from "./assets/onboardingAnalytics" import {AGENT_TEMPLATES, type AgentTemplate} from "./assets/templates" import HomeAutomationsSection from "./components/HomeAutomationsSection" import HomeSessionsSection from "./components/HomeSessionsSection" @@ -35,6 +30,7 @@ import HomeTaskComposer from "./components/HomeTaskComposer" import YourAgentsTable from "./components/YourAgentsTable" import {useAgentHomeActions} from "./hooks/useAgentHomeActions" import {useAgentHomeVariants} from "./hooks/useAgentHomeVariants" +import {useCreateAgentFromTemplate} from "./hooks/useCreateAgentFromTemplate" /** * The strip-era home layout (TEMPLATE_STRIP_MODE on): hero + composer (chip-docked) + @@ -50,11 +46,8 @@ const StripHome: React.FC = () => { // Home creates, navigates to the playground, and auto-sends (owner decision). const {onCreate} = useAgentHomeActions(composerRef, {autoSendSeed: true}) const {firstRunOverride, creatingAgent} = useAgentHomeVariants() - const posthog = usePostHogAg() const {baseAppURL} = useURL() const router = useRouter() - const {message} = App.useApp() - const [toastOpen, setToastOpen] = useState(false) // Create is a multi-step async round-trip; on success we navigate away, so we keep the // spinner running (only reset on failure) rather than flashing the label back mid-navigation. const [loading, setLoading] = useState(false) @@ -70,6 +63,14 @@ const StripHome: React.FC = () => { // A returning user gets there via `?new=1` rather than through the task composer. const firstRun = isFirstRun || creatingAgent + const templateParam = Array.isArray(router.query.template) + ? router.query.template[0] + : router.query.template + // "Blank agent" from the New agent menu asks for one thing: describe it. The templates below + // were the answer to a question this path already declined, so the composer stands alone and + // centres. First run and `?template=` still show them — there the strip is the offer. + const blankCreate = creatingAgent && !templateParam + // Only the centred first-run document wants the layout's bounded frame. The workspace scrolls // with the page, so it must NOT ask — see `layoutFullHeightRequestAtom`. useEffect(() => { @@ -85,6 +86,9 @@ const StripHome: React.FC = () => { }, }) + // A card here IS the create action — no composer step, no second confirmation. + const {createFromTemplate, pendingKey} = useCreateAgentFromTemplate("create") + const handlePick = useCallback( (template: AgentTemplate) => { // On the workspace home the composer runs tasks, so a pick here has no composer to @@ -93,28 +97,14 @@ const StripHome: React.FC = () => { void router.push(`${baseAppURL}?new=1&template=${template.key}`) return } - provenance.pick(template) - captureFirstAgentIntent(posthog, { - source: "template", - properties: { - template: template.name, - templateId: template.key, - templateCategory: template.category, - mode: "strip", - surface: "home", - }, - intentValue: template.category || template.name, - }) + void createFromTemplate(template) }, - [firstRun, router, baseAppURL, provenance.pick, posthog], + [firstRun, router, baseAppURL, createFromTemplate], ) // Seed once PER TEMPLATE KEY: a boolean guard blocked every template after the first, // because this surface stays mounted across ?template= navigations. const seededTemplate = useRef(null) - const templateParam = Array.isArray(router.query.template) - ? router.query.template[0] - : router.query.template useEffect(() => { if (!templateParam) { seededTemplate.current = null @@ -137,21 +127,6 @@ const StripHome: React.FC = () => { [loading, onCreate, provenance.resolveTemplateName], ) - const handleCodingAgentCopy = useCallback(async () => { - const text = composerRef.current?.getMarkdown().trim() ?? "" - try { - await navigator.clipboard.writeText(buildCodingAgentClipboard(text)) - setToastOpen(true) - } catch { - message.error("Couldn't copy — copy it manually") - return - } - captureFirstAgentIntent(posthog, { - source: "composer", - properties: {action: "coding_agent_copy", message: truncateForCapture(text)}, - }) - }, [message, posthog]) - return ( {/* First run stays a centered document in the layout's bounded frame — one question, @@ -171,7 +146,9 @@ const StripHome: React.FC = () => {
{ {
{firstRun ? ( - + blankCreate ? null : ( + + ) ) : ( // EXPERIMENT: the columns' contents are swapped. What's in flight takes // the wide column under the composer; the templates strip and the agents @@ -315,12 +294,6 @@ const StripHome: React.FC = () => { ) : null} - - setToastOpen(false)} - />
) } diff --git a/web/oss/src/components/pages/agent-home/components/TemplateDetail/index.tsx b/web/oss/src/components/pages/agent-home/components/TemplateDetail/index.tsx index 3182734730..45a18702a2 100644 --- a/web/oss/src/components/pages/agent-home/components/TemplateDetail/index.tsx +++ b/web/oss/src/components/pages/agent-home/components/TemplateDetail/index.tsx @@ -4,12 +4,12 @@ import {ArrowLeftIcon, ArrowRightIcon, CheckCircleIcon, LightningIcon} from "@ph import {Button, Empty, Tag, Tooltip} from "antd" import clsx from "clsx" import Link from "next/link" -import {useRouter} from "next/router" import Markdown from "@/oss/components/AgentChatSlice/assets/markdown" import useURL from "@/oss/hooks/useURL" import {AGENT_TEMPLATES, PROVIDERS} from "../../assets/templates" +import {useCreateAgentFromTemplate} from "../../hooks/useCreateAgentFromTemplate" const SectionLabel = ({children}: {children: React.ReactNode}) => (

@@ -26,8 +26,8 @@ const SectionLabel = ({children}: {children: React.ReactNode}) => ( * so they are absent rather than invented. */ const TemplateDetail = ({templateKey}: {templateKey: string}) => { - const router = useRouter() const {baseAppURL} = useURL() + const {createFromTemplate, pendingKey} = useCreateAgentFromTemplate("template_detail") const template = AGENT_TEMPLATES.find((entry) => entry.key === templateKey) if (!template) { @@ -69,15 +69,16 @@ const TemplateDetail = ({templateKey}: {templateKey: string}) => {

+ {/* Creates the agent and lands in its playground — no bounce back through the + create surface to press the same button again. */} diff --git a/web/oss/src/components/pages/agent-home/components/TemplatesGallery/TemplateSection.tsx b/web/oss/src/components/pages/agent-home/components/TemplatesGallery/TemplateSection.tsx index 56fd7c101f..88935bf5c2 100644 --- a/web/oss/src/components/pages/agent-home/components/TemplatesGallery/TemplateSection.tsx +++ b/web/oss/src/components/pages/agent-home/components/TemplatesGallery/TemplateSection.tsx @@ -5,10 +5,17 @@ interface TemplateSectionProps { category: string templates: AgentTemplate[] onSelectTemplate: (template: AgentTemplate) => void + /** Key of the template whose agent is being created (spins that card, dims the rest). */ + pendingTemplateKey?: string | null } /** One category block in the gallery: uppercase header + per-section count, then a 3-col card grid. */ -const TemplateSection = ({category, templates, onSelectTemplate}: TemplateSectionProps) => { +const TemplateSection = ({ + category, + templates, + onSelectTemplate, + pendingTemplateKey, +}: TemplateSectionProps) => { if (templates.length === 0) return null return ( @@ -30,6 +37,8 @@ const TemplateSection = ({category, templates, onSelectTemplate}: TemplateSectio key={template.key} template={template} onSelect={onSelectTemplate} + loading={template.key === pendingTemplateKey} + disabled={!!pendingTemplateKey && template.key !== pendingTemplateKey} /> ))} diff --git a/web/oss/src/components/pages/agent-home/components/TemplatesGallery/index.tsx b/web/oss/src/components/pages/agent-home/components/TemplatesGallery/index.tsx index e69d103318..8cb14f7629 100644 --- a/web/oss/src/components/pages/agent-home/components/TemplatesGallery/index.tsx +++ b/web/oss/src/components/pages/agent-home/components/TemplatesGallery/index.tsx @@ -3,13 +3,11 @@ import {useCallback, useDeferredValue, useEffect, useMemo, useState} from "react import {type SectionRailItem} from "@agenta/entity-ui" import {PageLayout} from "@agenta/ui" import {pageContentWidthClass} from "@agenta/ui/components/page-width" -import {App, Input, Typography} from "antd" +import {Input, Typography} from "antd" import clsx from "clsx" import {Search} from "lucide-react" import {useRouter} from "next/router" -import useURL from "@/oss/hooks/useURL" - import {TEMPLATES_GALLERY} from "../../assets/constants" import { AGENT_TEMPLATES, @@ -19,7 +17,7 @@ import { templateCategories, type AgentTemplate, } from "../../assets/templates" -import TemplateSetupDrawer, {type TemplateSetupResult} from "../TemplateSetupDrawer" +import {useCreateAgentFromTemplate} from "../../hooks/useCreateAgentFromTemplate" import TemplateSection from "./TemplateSection" @@ -33,8 +31,6 @@ const matchesQuery = (template: AgentTemplate, query: string) => { /** Full templates gallery — category rail + sectioned card grid, reached from Home's "Browse all". */ const TemplatesGalleryPage = () => { const router = useRouter() - const {message} = App.useApp() - const {baseAppURL} = useURL() const categories = useMemo(() => templateCategories(), []) const [active, setActive] = useState(ALL_TEMPLATES_CATEGORY) @@ -80,24 +76,13 @@ const TemplatesGalleryPage = () => { [router], ) - // Template card click: builder mode → straight to a seeded playground; else open the setup - // drawer. Gated by NEXT_PUBLIC_AGENT_TEMPLATE_BUILDER. - const [setupTemplate, setSetupTemplate] = useState(null) - // A card opens the template rather than creating from it: the detail page is where you find - // out what it needs before committing, which is the point of having one. + // A card IS the create action: picking a template mints the agent and opens its playground, + // rather than routing through a detail page and a second "Use this template" click. The detail + // page stays reachable by URL for anyone who wants to read what a template does first. + const {createFromTemplate, pendingKey} = useCreateAgentFromTemplate("gallery") const handleSelectTemplate = useCallback( - (template: AgentTemplate) => - void router.push(`${baseAppURL}/agent-templates/${template.key}`), - [router, baseAppURL], - ) - - // TODO(Phase B): create the ephemeral draft from the template + open the playground. - const handleTemplateCreate = useCallback( - ({template, name}: TemplateSetupResult) => { - setSetupTemplate(null) - message.info(`Create "${name}" from ${template.name} — wiring in the next phase`) - }, - [message], + (template: AgentTemplate) => void createFromTemplate(template), + [createFromTemplate], ) // Sections to render: All → every present category; otherwise just the active one. @@ -188,19 +173,13 @@ const TemplatesGalleryPage = () => { category={section.category} templates={section.templates} onSelectTemplate={handleSelectTemplate} + pendingTemplateKey={pendingKey} /> ))} )} - - setSetupTemplate(null)} - onCreate={handleTemplateCreate} - /> ) } diff --git a/web/oss/src/components/pages/agent-home/components/TemplatesSection/TemplateCard.tsx b/web/oss/src/components/pages/agent-home/components/TemplatesSection/TemplateCard.tsx index 88f7975c19..aee6a1b342 100644 --- a/web/oss/src/components/pages/agent-home/components/TemplatesSection/TemplateCard.tsx +++ b/web/oss/src/components/pages/agent-home/components/TemplatesSection/TemplateCard.tsx @@ -1,3 +1,5 @@ +import {CircleNotchIcon} from "@phosphor-icons/react" + import {templateProviderSlugs, type AgentTemplate} from "../../assets/templates" import ProviderMarks from "./ProviderMarks" @@ -5,6 +7,10 @@ import ProviderMarks from "./ProviderMarks" interface TemplateCardProps { template: AgentTemplate onSelect: (template: AgentTemplate) => void + /** This card's agent is being created — swaps the monogram for a spinner and blocks re-clicks. */ + loading?: boolean + /** Another card's create is in flight. */ + disabled?: boolean } /** @@ -15,19 +21,28 @@ interface TemplateCardProps { * design's "1.2k uses" has no telemetry behind it — the footer carries what the template actually * declares (its tools and when it fires) instead of an invented popularity number. */ -const TemplateCard = ({template, onSelect}: TemplateCardProps) => { +const TemplateCard = ({template, onSelect, loading, disabled}: TemplateCardProps) => { + const busy = loading || disabled return (