Implementation Plan: Agent prompt-first creation & template chips
Companion to spec.md. This is the how. Reuse-first: every prompt-first primitive already exists and is used by Missions/Ideas//new — we wire them onto /agents, add one new chips component, fix the wizard, and land the backend catalog service.
A. Frontend — prompt-first /agents page
Files
apps/web/src/app/[locale]/(dashboard)/agents/page.tsx(server) — additionally fetch the template catalog and pass it down:const [agentsResult, templates] = await Promise.all([agentsAPI.list({ limit: 50 }).catch(() => ({ data: [], meta: { total: 0, limit: 50, offset: 0 } })),listAstTemplates('agent').catch(() => []), // NFR-2: defensive]);return <AgentsList agents={agentsResult.data} templates={templates} />;apps/web/src/components/agents/AgentsList.tsx(client) — becomes the prompt-first page client:- Add
PromptComposer(model:IdeasPageClient.tsx) withplaceholderExamples= the agent placeholders fromNewPageClient. submit()mirrorsNewPageClient's agent path:startFromPrompt(description, { intent: 'Agent', attachments })thenrouter.push(ROUTES.DASHBOARD_AGENT_NEW). Min-length guard< 10.chipsBelow={<AgentTemplateChips … />}(new component, §B).- Move the
+ New Agentbutton out ofPageHeader.actions; render theOrblock ++ Create Agent Manuallybelow the composer. - Keep the existing cards grid + empty-state untouched below.
- Auto-collapse the chat panel on mount (
useChatPanel().setOpen(false)) likeNewPageClient, so the prompt gets the column on first land.
- Add
Or block — match the Works treatment. A thin presentational block: a centered Or rule (flex with before:/after: borders) and the + Create Agent Manually Button (href={ROUTES.DASHBOARD_AGENT_NEW}, variant="secondary", Plus icon). Keep it inline in AgentsList (small) rather than a new shared component, unless the Works CreationBlockTrio already exposes a reusable Or — if so, reuse it.
B. Frontend — AgentTemplateChips (new component)
apps/web/src/components/agents/AgentTemplateChips.tsx (client).
Props
interface AgentTemplateChipsProps {
templates: AstTemplateEntry[]; // repo/fallback catalog
userTemplates?: AstTemplateEntry[]; // "Your templates" (FR-16/29)
onPick: (tpl: AstTemplateEntry) => void; // seed the prompt (FR-13)
}
Behaviour
- Build a
PromptChip[]array:[{ value: VIEW_ALL, label: t('chips.viewAll'), Icon: LayoutGrid }, ...templates.map(tpl => ({ value: tpl.slug, label: tpl.title, Icon: resolveIcon(tpl.iconName) }))]. - Render
<PromptChipsRow chips={chips} value={expanded ? VIEW_ALL : null} onChange={onChange} />. onChange(v): ifv === VIEW_ALL→ toggleexpanded; else ifv→onPick(bySlug(v))(do not persist selection — value staysnullso no sticky highlight).- When
expanded, render the catalog panel: two<section>s ("All templates", "Your templates") each a card grid. Card click →onPick(tpl)+ collapse (default), with a secondary "Open in wizard" link to/agents/new?from=<slug>.Esccollapses. - Icon resolution: a small map from the curated lucide names used in
agent-templates.ts(ClipboardList,Code2,BookOpen, plus the role icons we add —Crown/Briefcasefor CEO,Cpu/Wrenchfor CTO, etc.). FallbackBot.
Chip seed semantics (spec Q1 default): in onPick, the parent (AgentsList) sets the composer value: empty → tpl.description (or "${tpl.title} — ${tpl.description}"); non-empty → prepend "${tpl.title} — ". Then focus the input (PromptComposer exposes inputId; focus by id).
C. Frontend — catalog client helper + global cache
apps/web/src/lib/api/agent-templates.ts:
- Keep the stable
AstTemplateEntryshape andlistAstTemplates(entity)signature (FR-28). - Extend the built-in fallback
AGENT_TEMPLATESwith the named roles the operator listed — additive, keep PM/Coder/Researcher: addCEO,CTO,Lead Engineer,Copywriter,Sales,Brand Specialist(each with slug, title, description, category, iconName, tags). This guarantees the chips are populated today (E2) before the repo lands. - Add a module-level cache (FR-30):
const cache = new Map<AstTemplateEntityType, { at: number; data: AstTemplateEntry[] }>()with a short client TTL;listAstTemplateschecks it first. (Server reads go through the service'scache_entries.) - Leave the documented swap-to-server-action path (per the existing file comment) — when the endpoint lands (§D) the fallback becomes the catch path.
D. Backend — AgentTemplateService + endpoint (ADR-011)
Service packages/agent/src/services/agent-template.service.ts (or the agents module if one exists):
listTemplates(entity='agent'): Promise<AstTemplateEntry[]>.- Resolve source:
EVER_WORKS_AGENTS_PATH(local clone) if set, elsegit clone --depth 1 --branch $EVER_WORKS_AGENTS_REF(github.com/ever-works/agents)into a temp dir (reuseisomorphic-git/existing git facade if it fits; otherwise a thin clone util). Use the official client / existing git util — no hand-rolled HTTP (NN #22). - Parse each top-level folder: read
agent.ymlfortitle/description/category/icon/tags; validate against a Zod schema; skip + warn on invalid folders. - Cache the parsed list in
cache_entrieskeyedagent-templates:agent:<ref>with 1h TTL. Cache-first read; clone only on miss. - Resilience (NFR-3): timeout the clone; on failure return
[](controller layer or client falls back).
Endpoint apps/api/src/... — GET /agent-templates?entity=agent → AstTemplateEntry[]. @Public() read is fine (catalog is public). Thin controller → service.
Web wiring — a server action listAstTemplatesFromCatalogAction(entity) that serverFetch('/agent-templates?entity=…'); listAstTemplates calls it and falls back to the built-in list on error (keeps the client bundle clean per the existing file's webpack note).
Repo creation — ever-works/agents may not exist yet. Creating/seeding it is a shared-state action requiring operator approval (spec Q4). The service + fallback are designed so the feature ships and works regardless; populating the repo flips chips from fallback to live with no code change.
E. Wizard — NewAgentDialog.tsx
Bug fix (FR-20/21/22) — minimal, surgical:
- Add an inline helper under the scope list / above the buttons when
!canAdvance && !pinned: e.g.t('pickParentHint', { scope })("Pick a {scope} to continue, or choose Workspace scope"). - Add
title={canAdvance ? undefined : t('nextDisabledReason')}to the NextButtonso the disabled state is explained on hover/focus. - Do not touch the
canAdvancehappy path fortenant. - (The empty-scope hint already exists at
:245; we make its consequence — disabled Next — legible.)
Optional template step (FR-23/24/25) — extend the step machine:
- Change
stepfrom1 | 2to a union'template' | 'scope' | 'details'. Initial:pinned ? 'details' : 'template'. Keep?from=<slug>→ set template +'details'(current behaviour preserved). - New prop
templates?: AstTemplateEntry[]passed from/agents/newpage (server-fetchlistAstTemplates('agent')). templatestep: a card grid of templates + a prominent "Start from scratch" button. Selecting a template →setTemplateSlug, prefillname/title, →'scope'. "Start from scratch" →'scope'.- Back navigation:
details → scope → template. - Guard: if
templatesis empty, skip the template step entirely (initial step becomes'scope') so we never show an empty step.
/agents/new/page.tsx — add templates={await listAstTemplates('agent').catch(() => [])} to the <NewAgentDialog> props (alongside the existing missions/works/ideas fetch).
F. i18n
Add keys under dashboard.agentsPage (e.g. promptLabel, orDivider, createManually, chips.viewAll, catalog.allTemplates, catalog.yourTemplates, catalog.yourTemplatesEmpty) and dashboard.agentsPage.newDialog (templateStepTitle, startFromScratch, pickParentHint, nextDisabledReason). Relabel newAgent is kept (still used as the manual button text) and add createManually = "Create Agent Manually". Write to all 21 apps/web/messages/*.json (English placeholder where untranslated — NFR-4). Script the injection with a small node merge.
G. Tests
- Unit:
AgentTemplateChips(renders View All first; chip click callsonPick; View All toggles panel). Co-locate*.unit.spec.tsxlike neighbours. - Unit:
NewAgentDialog— Workspace happy path still advances; Mission+empty shows hint and Next disabled withtitle; template step pre-fills + advances;pinnedskips template step. - Unit/
agent-templates.spec.ts: fallback includes the named roles;listAstTemplates('agent')returns them; cache hit path. - Backend:
AgentTemplateServicecache-hit / clone-miss / invalid-folder-skip / unreachable→[] (mock git + cache). - E2E (Playwright) smoke:
/agentsrenders composer + chips;+ Create Agent Manuallyroutes to wizard. (Match existing agents e2e if present.)
H. Sequencing / PRs
- PR 1 (frontend, no backend dep): §A + §B + §C(fallback+cache) + §E + §F + §G(frontend). Delivers the full visible feature off the extended fallback catalog. Self-contained, reviewable.
- PR 2 (backend): §D —
AgentTemplateService+ endpoint + server-action swap + §G(backend). Flips chips to live repo data. Depends on operator creating/seedingever-works/agents(Q4) for non-fallback content, but ships safely either way.
Both target develop. Migrations only if "Your templates" needs a column (FR-29) — default v1 derives from existing Agents, so likely none.