Skip to content
Star on GitHub
WorkspaceAgent builder
Style
Accent
Sound

Built by DesignMe, free to use under the MIT licence.

Expert design and dev support for startups and enterprises, from Astro sites to React apps and AI products.

Book a call

Agent builder

Describe an agent in a sentence and it drafts the rest: who it is, what it may do, what it knows, when it runs. Test it, save it, publish it. Free and MIT licensed, as HTML, a typed React component or an Astro component, in two styles with light and dark mode.

$npx designme add agent-builder
Identity
Instructions
Model and effort
ToolsWhat it may do in each
Knowledge
Trigger
When someone messages it
Not saved
TestTry it before it goes live

Draft the agent, then press Test to send it a sample email.

01

Install

Add it to your project with the CLI.

HTML, CSS and JavaScript

$npx designme add agent-builder

React

$npx designme add agent-builder --react

Astro

$npx designme add agent-builder --astro

The CLI copies the source into your project, so you own it and can change anything. The HTML version lands in ./designme with the shared core and a demo.html. The React version lands in src/components/designme and needs React 19; it works in Next.js, Vite and Astro islands. The Astro version lands in src/components/designme too, with the same markup, CSS and script as the HTML version. See Astro.

shadcn CLI, React

$npx shadcn@latest add https://designme.dev/r/shadcn/agent-builder.json

shadcn CLI, Astro

$npx shadcn@latest add https://designme.dev/r/shadcn/astro/agent-builder.json

In a project set up for shadcn/ui, its CLI adds the same files to your components folder, under designme/, with the shared core. To give the components your shadcn or Tailwind v4 theme, add the theme bridge.

02

About

What it is for and what it does.

A builder for a new agent that starts from one sentence. Type what the agent should do and press Draft: the agent fills in every section in turn (its name and purpose, its avatar's colour and shape, its instructions, the model and effort, what it may do in each tool, what it knows, and what starts it) and marks each one Drafted until someone changes it. The avatar is Dotto, a dot-matrix face on a dark screen in a bezel of the agent's colour, and it shows wherever the agent does. Beside the form, a test chat sends it a sample and shows what it would do. A bar at the foot saves it and publishes it, and it goes Live.

  • To set up an agent without filling in a long form from scratch.
  • When each tool needs its own permission: read only, ask first, or act on its own.
  • When people should try the agent on a sample before it goes live.
03

Usage

Import it in React and drive it from your own state, or place it in an Astro page.

React

import { AgentBuilder } from '@/components/designme/agent-builder/AgentBuilder';

React example

The demo above as a React component, 63 lines you can copy.

'use client';
// The animated preview from designme.dev, rebuilt from the React component: one sentence, the fields fill in (the avatar takes a
// colour and a shape), one test email while the avatar thinks, save, publish.
import { useRef, useState } from 'react';
import { AgentBuilder, type AgentBuilderProps, type AgentDraft, type AgentSection } from './AgentBuilder';
import { useStory, useWait } from '../lib/useScript';

const EMPTY: AgentDraft = { name: '', purpose: '', color: 'accent', instructions: '', model: 'Haiku 5', effort: 'med', trigger: 'chat', triggerDetail: 'When someone messages it',
  tools: [{ key: 'gmail', label: 'Gmail', brand: 'gmail', level: 'read' }, { key: 'linear', label: 'Linear', brand: 'linear', level: 'read' }, { key: 'slack', label: 'Slack', brand: 'slack', level: 'read' }], knowledge: [] };
const DRAFT: Record<AgentSection, Partial<AgentDraft>> = {
  id: { name: 'Support triage', purpose: 'Sorts incoming support emails and routes them to the right team' },
  ins: { instructions: 'Read each new email. Tag it Billing, Bug or How-to, set its priority, and route it to that team. Draft a reply for billing refunds but never send money without asking.' },
  model: { model: 'Sonnet 5', effort: 'med' },
  tools: { tools: [{ key: 'gmail', label: 'Gmail', brand: 'gmail', level: 'act' }, { key: 'linear', label: 'Linear', brand: 'linear', level: 'act' }, { key: 'slack', label: 'Slack', brand: 'slack', level: 'ask' }] },
  know: { knowledge: [{ label: 'Refund policy.pdf', icon: 'file-text' }, { label: 'Help centre', icon: 'globe' }, { label: 'Team routing.csv', icon: 'table' }] },
  trig: { trigger: 'event', triggerDetail: 'When an email arrives at support@' }
};
const TRIG = { chat: 'When someone messages it', sched: 'Every weekday at 9:00', event: 'When an email arrives at support@' };

export default function AgentBuilderDemo() {
  const ref = useRef<HTMLDivElement>(null);
  const wait = useWait();
  const [desc, setDesc] = useState('');
  const [value, setValue] = useState(EMPTY);
  const [drafting, setDrafting] = useState<AgentSection | null>(null);
  const [drafted, setDrafted] = useState<AgentSection[]>([]);
  const [status, setStatus] = useState<AgentBuilderProps['status']>('empty');
  const [publishing, setPublishing] = useState(false);
  const [test, setTest] = useState<AgentBuilderProps['test']>();
  const draft = async () => {
    for (const k of Object.keys(DRAFT) as AgentSection[]) {
      setDrafting(k); await wait(550); setValue(v => ({ ...v, ...DRAFT[k] }));
      // it names the agent, then picks its avatar's colour and shape
      if (k === 'id') { await wait(350); setValue(v => ({ ...v, color: 'violet' })); await wait(350); setValue(v => ({ ...v, shape: 'circle' })); }
      setDrafted(d => d.includes(k) ? d : [...d, k]);
    }
    setDrafting(null); setStatus('dirty');
  };
  const runTest = async () => {
    setTest({ email: { subject: 'I was charged twice for October', body: 'Hi, my card shows two charges of $48 on Oct 3. Can you refund one? Maya', from: '[email protected] · sample' }, reply: 'busy' });
    await wait(2000);
    setTest(t => t && { ...t, reply: { tags: [{ label: 'Billing' }, { label: 'High priority', tone: 'warn' }, { label: 'Routed to Billing' }], text: 'Duplicate charge of $48 on Oct 3. I drafted a refund reply and will ask before sending it, as the instructions say.' } });
  };
  const publish = async () => { setPublishing(true); await wait(800); setPublishing(false); setStatus('live'); };
  useStory(ref, async ({ wait, press, taken }) => {
    await wait(1000);
    const text = 'Triage support emails';
    taken(() => setDesc(''));
    for (let i = 1; i <= text.length; i++) { setDesc(text.slice(0, i)); await wait(60); }
    await wait(500); press('.dm-abx-askr .dm-pill');
    await wait(5000); press('.dm-abx-bar .dm-pill:first-of-type');
    await wait(4000); press('.dm-abx-bar .dm-pill:nth-of-type(2)');
    await wait(1000); press('.dm-abx-bar .dm-primary');
  });
  return (
    <div ref={ref} style={{ width: '100%' }}>
      <AgentBuilder value={value} description={desc} onDescriptionChange={setDesc} onDraft={() => { void draft(); }} drafting={drafting} drafted={drafted} status={status}
        onChange={(v, section) => { setValue({ ...v, triggerDetail: TRIG[v.trigger] }); setDrafted(d => d.filter(x => x !== section)); if (status !== 'empty') setStatus('dirty'); }}
        onSave={() => setStatus('saved')} onPublish={publish} publishing={publishing} onTest={() => { void runTest(); }} test={test} />
    </div>
  );
}

Astro

In a page in src/pages. It plays the demo above; change its agent-builder.js to show your own data. Attributes like class go on its root element.

---
import AgentBuilder from '../components/designme/agent-builder/AgentBuilder.astro';
---
<AgentBuilder />
<AgentBuilder data-style="soft" class="my-class" />
04

Props

Everything AgentBuilder takes.

AgentBuilderProps

PropTypeWhat it does
valuerequiredAgentDraft
onChangerequired(value: AgentDraft, section: AgentSection) => void
descriptionrequiredstringThe one sentence it starts from
onDescriptionChangerequired(text: string) => void
onDraftrequired() => voidAsks the agent to draft every field from the description
draftingAgentSection | nullThe section being drafted now, while drafting; the ones after it wait
draftedAgentSection[]Sections the agent drafted and nobody has changed since: they carry a Drafted tag
statusrequired'empty' | 'dirty' | 'saved' | 'live'empty: nothing drafted yet; dirty: unsaved changes; saved; live
onSaverequired() => void
onPublishrequired() => void | Promise<unknown>
publishingboolean
onTest() => void
test{ email: { subject: string; body: string; from?: string }; reply?: 'busy' | { tags: { label: string; tone?: 'warn' }[]; text: ReactNode } }The test chat: what went in, and the agent's reply, or 'busy' while it works (the avatar thinks meanwhile)
modelsstring[]
colors{ value: string; label: string; color: string }[]The avatar colours to pick from
classNamestring

AgentMascotProps

PropTypeWhat it does
colorstringAny CSS colour; the bezel takes it, the screen and the dots are mixed from it. Leave it out for the accent
shapeMascotShape
stateMascotStateidle blinks a row of dots; thinking moves the eyes cell by cell and runs a loading row. Still poses with reduced motion
sizenumberIn px; 24 by default
labelstringNames it for screen readers; without one it is decorative
classNamestring
05

Code

The full source, as the CLI installs it.

AgentBuilder.tsxReact · 235 lines
'use client';
import { useId, useRef, useState, type CSSProperties, type KeyboardEvent, type ReactNode, type Ref } from 'react';
import { Brand, type BrandName } from '../lib/brands';
import { cx } from '../lib/cx';
import { useIsoLayoutEffect } from '../lib/hooks';
import { Icon, type IconName } from '../lib/icons';
import { Seg, Spin, Swap, Tip } from '../lib/motion';
import '../designme.css';
import './agent-builder.css';

export type AgentSection = 'id' | 'ins' | 'model' | 'tools' | 'know' | 'trig';
export type ToolLevel = 'read' | 'ask' | 'act';
export type MascotShape = 'circle' | 'squircle';
export type MascotState = 'idle' | 'thinking';

/* ---------- Dotto, the agent's mascot ---------- */
// each outline as [bezel, screen], in a 64 by 64 box; a mascot draws only the one in use
const SHAPES: Record<MascotShape, [string, string]> = {
  circle: ['M4 32A28 28 0 1 0 60 32A28 28 0 1 0 4 32Z', 'M10.5 32A21.5 21.5 0 1 0 53.5 32A21.5 21.5 0 1 0 10.5 32Z'],
  squircle: ['M60 32C60 58.5 58.5 60 32 60S4 58.5 4 32 5.5 4 32 4 60 5.5 60 32Z', 'M53.5 32C53.5 52.3 52.3 53.5 32 53.5S10.5 52.3 10.5 32 11.7 10.5 32 10.5 53.5 11.7 53.5 32Z'],
};
const SHAPE_NAMES: Record<MascotShape, string> = { circle: 'Circle', squircle: 'Squircle' };
type Cell = [number, number];
const GRID: Cell[] = Array.from({ length: 25 }, (_, i) => [i % 5, Math.floor(i / 5)]);
const dots = (cells: Cell[], cls?: string) => cells.map(([x, y]) => <circle key={`${x}-${y}`} className={cls} cx={19 + x * 6.5} cy={19 + y * 6.5} r={cls ? 2.75 : 1.3} />);

export interface AgentMascotProps {
  /** Any CSS colour; the bezel takes it, the screen and the dots are mixed from it. Leave it out for the accent */
  color?: string;
  shape?: MascotShape;
  /** idle blinks a row of dots; thinking moves the eyes cell by cell and runs a loading row. Still poses with reduced motion */
  state?: MascotState;
  /** In px; 24 by default */
  size?: number;
  /** Names it for screen readers; without one it is decorative */
  label?: string;
  className?: string;
}

/** Dotto: a dark LED screen in a bezel of the agent's colour, with a 5 by 5 grid of dots drawing its eyes and smile. */
export function AgentMascot({ color, shape = 'squircle', state = 'idle', size, label, className }: AgentMascotProps) {
  const [bz, sc] = SHAPES[shape];
  const style = { ...(color ? { '--dm-m': color } : {}), ...(size ? { width: size, height: size } : {}) } as CSSProperties;
  return (
    <svg className={cx('dm-abx-mascot', className)} viewBox="0 0 64 64" data-shape={shape} data-state={state} style={style}
      {...(label ? { role: 'img', 'aria-label': label } : { 'aria-hidden': true })} focusable="false">
      <path className="dm-bz" d={bz} /><path className="dm-sc" d={sc} />
      <g className="dm-dg">{dots(GRID)}</g>
      <g className="dm-lk">{dots([[1, 1], [3, 1]], 'dm-on dm-tp')}{dots([[1, 2], [3, 2]], 'dm-on')}</g>
      <g className="dm-id">{dots([[0, 3], [4, 3], [1, 4], [2, 4], [3, 4]], 'dm-on')}</g>
      <g className="dm-th">{dots([[1, 4]], 'dm-on dm-ld dm-l1')}{dots([[2, 4]], 'dm-on dm-ld dm-l2')}{dots([[3, 4]], 'dm-on dm-ld dm-l3')}</g>
    </svg>
  );
}

/** The colours the builder offers: the accent, then four of the palette's. A colour the accent already is shows once */
export const AGENT_COLORS: { value: string; label: string; color: string }[] = [
  { value: 'accent', label: 'Accent', color: 'var(--dm-accent)' },
  { value: 'blue', label: 'Blue', color: '#116ff8' },
  { value: 'violet', label: 'Violet', color: '#7c4dff' },
  { value: 'orange', label: 'Orange', color: '#f76700' },
  { value: 'green', label: 'Green', color: '#00a876' }
];

/** A radio group: a click picks, the arrow keys move and pick (and wrap), Home and End go to the ends; only the picked one is
    in the tab order */
function Radios<T extends string>({ options, value, onChange, label, className, children, item, rootRef }: {
  options: { value: T; label: string; hidden?: boolean }[]; value: T; onChange: (v: T) => void; label: string; className?: string;
  children?: ReactNode; rootRef?: Ref<HTMLDivElement>; item: (o: { value: T; label: string }) => { className?: string; style?: CSSProperties; content?: ReactNode };
}) {
  const shown = options.filter(o => !o.hidden);
  const key = (e: KeyboardEvent<HTMLDivElement>) => {
    const i = shown.findIndex(o => o.value === value), d = ({ ArrowRight: 1, ArrowDown: 1, ArrowLeft: -1, ArrowUp: -1 } as Record<string, number>)[e.key];
    const to = d ? shown[(i + d + shown.length) % shown.length] : e.key === 'Home' ? shown[0] : e.key === 'End' ? shown[shown.length - 1] : null;
    if (!to) return;
    e.preventDefault(); onChange(to.value);
    (e.currentTarget.querySelector(`[data-v="${to.value}"]`) as HTMLElement | null)?.focus();
  };
  return (
    <div className={className} role="radiogroup" aria-label={label} onKeyDown={key} ref={rootRef}>
      {children}
      {shown.map(o => { const { className: c, style, content } = item(o);
        return <Tip key={o.value} label={o.label}><button type="button" role="radio" aria-checked={o.value === value} tabIndex={o.value === value ? 0 : -1} aria-label={o.label}
          data-v={o.value} className={c} style={style} onClick={() => onChange(o.value)}>{content}</button></Tip>; })}
    </div>
  );
}

export interface AgentDraft {
  name: string;
  purpose: string;
  /** The avatar's colour: one of `colors` by its value ('accent', 'blue', 'violet', 'orange', 'green' by default) */
  color: string;
  /** The avatar's outline. Leave it out for a squircle */
  shape?: MascotShape;
  instructions: string;
  model: string;
  effort: 'low' | 'med' | 'high';
  tools: { key: string; label: string; brand?: BrandName; level: ToolLevel }[];
  knowledge: { label: string; icon?: IconName }[];
  trigger: 'chat' | 'sched' | 'event';
  /** A line beside the trigger, like "When an email arrives at support@" */
  triggerDetail?: string;
}

export interface AgentBuilderProps {
  value: AgentDraft;
  onChange: (value: AgentDraft, section: AgentSection) => void;
  /** The one sentence it starts from */
  description: string;
  onDescriptionChange: (text: string) => void;
  /** Asks the agent to draft every field from the description */
  onDraft: () => void;
  /** The section being drafted now, while drafting; the ones after it wait */
  drafting?: AgentSection | null;
  /** Sections the agent drafted and nobody has changed since: they carry a Drafted tag */
  drafted?: AgentSection[];
  /** empty: nothing drafted yet; dirty: unsaved changes; saved; live */
  status: 'empty' | 'dirty' | 'saved' | 'live';
  onSave: () => void;
  onPublish: () => void | Promise<unknown>;
  publishing?: boolean;
  onTest?: () => void;
  /** The test chat: what went in, and the agent's reply, or 'busy' while it works (the avatar thinks meanwhile) */
  test?: { email: { subject: string; body: string; from?: string }; reply?: 'busy' | { tags: { label: string; tone?: 'warn' }[]; text: ReactNode } };
  models?: string[];
  /** The avatar colours to pick from */
  colors?: { value: string; label: string; color: string }[];
  className?: string;
}

const SECTIONS: AgentSection[] = ['id', 'ins', 'model', 'tools', 'know', 'trig'];
const TRIG = [{ value: 'chat' as const, label: 'Chat' }, { value: 'sched' as const, label: 'Schedule' }, { value: 'event' as const, label: 'Event' }];

/** Describe an agent in one sentence and the agent drafts the rest: identity (its avatar, Dotto, in a colour and shape you pick),
    instructions, model and effort, tools with a permission each, knowledge and trigger. A test chat beside it, and a bar to save and
    publish. */
export function AgentBuilder({ value: v, onChange, description, onDescriptionChange, onDraft, drafting = null, drafted = [], status, onSave, onPublish, publishing = false, onTest, test,
  models = ['Haiku 5', 'Sonnet 5', 'Opus 5.5'], colors = AGENT_COLORS, className }: AgentBuilderProps) {
  const errId = useId(), descId = useId();
  const ref = useRef<HTMLDivElement>(null), shpRef = useRef<HTMLDivElement>(null);
  // the colours the accent already is, read once it is placed
  const [dupes, setDupes] = useState<string[]>([]);
  useIsoLayoutEffect(() => {
    const el = ref.current; if (!el) return;
    const ink = (c: string) => { const t = document.createElement('i'); t.hidden = true; t.style.color = c; el.appendChild(t); const x = getComputedStyle(t).color; t.remove(); return x; };
    const acc = ink('var(--dm-accent)');
    setDupes(colors.filter(c => c.value !== 'accent' && ink(c.color) === acc).map(c => c.value));
  }, [colors]);
  const shape = v.shape ?? 'squircle';
  const pick = colors.find(c => c.value === v.color && !dupes.includes(c.value)) ?? colors[0];
  const mood: MascotState = test?.reply === 'busy' ? 'thinking' : 'idle';
  const dotto = (extra?: Partial<AgentMascotProps>) => <AgentMascot color={pick.color} shape={shape} state={mood} {...extra} />;
  // the shape switch's knob sits under the picked outline
  const [knob, setKnob] = useState<CSSProperties>({});
  useIsoLayoutEffect(() => {
    const el = shpRef.current; if (!el) return;
    const place = () => { const b = el.querySelector<HTMLElement>('[aria-checked="true"]'); if (b) setKnob({ left: b.offsetLeft, width: b.offsetWidth }); };
    place(); const ro = new ResizeObserver(place); ro.observe(el); return () => ro.disconnect();
  }, [shape]);
  const set = (section: AgentSection, patch: Partial<AgentDraft>) => onChange({ ...v, ...patch }, section);
  const at = drafting ? SECTIONS.indexOf(drafting) : -1;
  const sec = (k: AgentSection) => cx('dm-abx-s', at >= 0 && SECTIONS.indexOf(k) > at && 'dm-wait', drafting === k && 'dm-busy');
  const head = (k: AgentSection, label: string, extra?: ReactNode) => (
    <div className="dm-abx-sh"><span>{label}</span>{drafted.includes(k) && drafting !== k && <span className="dm-abx-dt dm-enter"><Icon name="spark" size="xs" />Drafted</span>}{extra}</div>
  );
  const bad = status !== 'empty' && !v.name.trim();
  return (
    <div data-dm="" ref={ref} className={cx('dm-abx dm-panel', className)} style={{ '--dm-m': pick.color } as CSSProperties}>
      <div className="dm-abx-g">
        <div className="dm-abx-f">
          <div className="dm-abx-ask"><label className="dm-abx-l" htmlFor={descId}>Describe your agent</label>
            <div className="dm-abx-askr"><input className="dm-abx-in" id={descId} autoComplete="off" placeholder="What should it do?" value={description} onChange={e => onDescriptionChange(e.target.value)} />
              <button type="button" className={cx('dm-pill dm-sm', status === 'empty' ? 'dm-primary' : 'dm-line')} disabled={!!drafting || !description.trim()} onClick={onDraft}>
                {drafting ? <><Spin />Drafting</> : status === 'empty' ? <><Icon name="spark" size="xs" />Draft</> : <><Icon name="refresh" size="xs" />Draft again</>}</button></div>
          </div>
          <div className={sec('id')} role="group" aria-label="Identity">{head('id', 'Identity')}
            <div className="dm-abx-idr">
              <span className="dm-abx-av" role="img" aria-label={`Avatar: ${pick.label}, ${SHAPE_NAMES[shape].toLowerCase()}`}>{dotto()}</span>
              <input className="dm-abx-in" aria-label="Name" placeholder="Name" autoComplete="off" value={v.name} aria-invalid={bad} aria-describedby={errId} onChange={e => set('id', { name: e.target.value })} />
              <div className="dm-abx-look">
                <Radios className="dm-abx-sw" label="Colour" value={pick.value} onChange={c => set('id', { color: c })}
                  options={colors.map(c => ({ value: c.value, label: c.label, hidden: dupes.includes(c.value) }))}
                  item={o => ({ style: { '--dm-s': colors.find(c => c.value === o.value)!.color } as CSSProperties })} />
                <Radios className="dm-seg2 dm-abx-shp" label="Shape" value={shape} onChange={s => set('id', { shape: s })} rootRef={shpRef}
                  options={(Object.keys(SHAPES) as MascotShape[]).map(s => ({ value: s, label: SHAPE_NAMES[s] }))}
                  item={o => ({ content: <svg viewBox="0 0 64 64" aria-hidden="true" focusable="false"><path d={SHAPES[o.value][0]} /></svg> })}>
                  <span className="dm-knob" style={knob} />
                </Radios>
              </div>
            </div>
            <span className="dm-abx-err" id={errId} hidden={!bad}><Icon name="alert" size="xs" />Give it a name to save it</span>
            <input className="dm-abx-in" aria-label="Purpose" placeholder="What it is for" autoComplete="off" value={v.purpose} onChange={e => set('id', { purpose: e.target.value })} />
          </div>
          <div className={sec('ins')} role="group" aria-label="Instructions">{head('ins', 'Instructions')}<textarea className="dm-abx-ta" rows={3} aria-label="Instructions" placeholder="How it should work" value={v.instructions} onChange={e => set('ins', { instructions: e.target.value })} /></div>
          <div className={sec('model')} role="group" aria-label="Model and effort">{head('model', 'Model and effort')}
            <div className="dm-abx-mr"><select className="dm-abx-in dm-abx-sel" aria-label="Model" value={v.model} onChange={e => set('model', { model: e.target.value })}>{models.map(m => <option key={m}>{m}</option>)}</select>
              <Seg className="dm-abx-eff" label="Effort" options={[{ value: 'low', label: 'Low' }, { value: 'med', label: 'Medium' }, { value: 'high', label: 'High' }]} value={v.effort} onChange={e => set('model', { effort: e })} /></div>
          </div>
          <div className={sec('tools')} role="group" aria-label="Tools">{head('tools', 'Tools', <span className="dm-muted">What it may do in each</span>)}
            <div className="dm-abx-tools">{v.tools.map(t => (
              <div key={t.key} className="dm-abx-tl">{t.brand ? <Brand name={t.brand} /> : <Icon name="bolt" />}<span className="dm-n">{t.label}</span>
                <Seg label={`${t.label}: what it may do`} options={[{ value: 'read', label: 'Read' }, { value: 'ask', label: 'Ask' }, { value: 'act', label: 'Act' }]} value={t.level}
                  onChange={l => set('tools', { tools: v.tools.map(x => x.key === t.key ? { ...x, level: l } : x) })} /></div>))}</div>
          </div>
          <div className={sec('know')} role="group" aria-label="Knowledge">{head('know', 'Knowledge')}
            <div className="dm-abx-kn">{v.knowledge.length ? v.knowledge.map(k => <span key={k.label} className="dm-tag">{k.icon && <Icon name={k.icon} size="xs" />}{k.label}</span>) : <span className="dm-muted" style={{ fontSize: 12 }}>Nothing yet</span>}</div>
          </div>
          <div className={sec('trig')} role="group" aria-label="Trigger">{head('trig', 'Trigger')}
            <div className="dm-abx-tr"><Seg className="dm-abx-trs" label="Starts on" options={TRIG} value={v.trigger} onChange={t => set('trig', { trigger: t })} />{v.triggerDetail && <Swap className="dm-muted dm-abx-trd">{v.triggerDetail}</Swap>}</div>
          </div>
          <div className="dm-abx-bar">
            <span className="dm-abx-st dm-muted">{status === 'live' ? <>{dotto()}<span className="dm-tag dm-ok">Live</span></> : status === 'saved' ? <><Icon name="check" size="xs" />Saved</> : status === 'dirty' ? (drafted.length ? 'Drafted, not saved' : 'Unsaved changes') : 'Not saved'}</span>
            <span className="dm-sp" />
            {onTest && <button type="button" className="dm-pill dm-line dm-sm" disabled={status === 'empty' || test?.reply === 'busy'} onClick={onTest}><Icon name="play" size="xs" />Test</button>}
            <button type="button" className="dm-pill dm-line dm-sm" disabled={status === 'empty' || status === 'saved' || status === 'live' || bad} onClick={onSave}>Save</button>
            <button type="button" className="dm-pill dm-primary dm-sm" disabled={status === 'empty' || status === 'live' || bad || publishing} onClick={() => { void onPublish(); }}>{publishing ? <><Spin />Publishing</> : status === 'live' ? 'Published' : 'Publish'}</button>
          </div>
        </div>
        <div className="dm-abx-c">
          <div className="dm-abx-ch">{dotto()}<div className="dm-abx-cht"><b>Test</b><Swap className="dm-muted dm-abx-cs">{!test ? 'Try it before it goes live' : test.reply === 'busy' || !test.reply ? 'Testing with a sample email' : 'The test passed'}</Swap></div></div>
          <div className="dm-abx-log" aria-live="polite">
            {!test ? <p className="dm-muted dm-abx-empty">Draft the agent, then press Test to send it a sample email.</p> : <>
              <div className="dm-abx-em dm-enter"><b>{test.email.subject}</b><span>{test.email.body}</span>{test.email.from && <span className="dm-muted">{test.email.from}</span>}</div>
              {test.reply === 'busy' ? <div className="dm-abx-rp dm-enter"><span className="dm-k">{dotto()}<span className="dm-shim">{v.name || 'The agent'} is reading it</span></span></div>
                : test.reply ? <div className="dm-abx-rp dm-enter"><span className="dm-k">{dotto()}{v.name || 'The agent'}</span>
                    <span className="dm-tags">{test.reply.tags.map(t => <span key={t.label} className={cx('dm-tag', t.tone === 'warn' && 'dm-warn')}>{t.label}</span>)}</span><span>{test.reply.text}</span></div> : null}
            </>}
          </div>
        </div>
      </div>
    </div>
  );
}
agent-builder.cssReact · 104 lines
/* Generated from designme.dev by scripts/sync.mjs. Edit the site, then sync. */
/* ---------- Agent builder ---------- */
/* a form on the left, a test chat on the right (under it on a narrow card). Every field the agent drafted carries a Drafted tag
   until someone changes it; a bar at the foot of the form saves, tests and publishes */
.dm-abx{overflow:hidden;container-type:inline-size}
.dm-abx-g{display:grid;grid-template-columns:minmax(0,1.3fr) minmax(0,1fr)}
.dm-abx-f{display:flex;flex-direction:column;gap:var(--dm-s-3);padding:var(--dm-s-4);min-width:0}
.dm-abx-l,.dm-abx-sh{font-size:12px;color:var(--dm-muted)}
.dm-abx-ask{display:flex;flex-direction:column;gap:6px;padding-bottom:var(--dm-s-3);border-bottom:1px solid color-mix(in srgb,var(--dm-line) 50%,transparent)}
.dm-abx-askr{display:flex;gap:var(--dm-s-2)}
.dm-abx-askr .dm-abx-in{flex:1}
.dm-abx-in,.dm-abx-ta{min-width:0;height:32px;padding:0 10px;border:0;border-radius:var(--dm-r-field);outline:0;background:var(--dm-surface);box-shadow:inset 0 0 0 1px color-mix(in srgb,var(--dm-field-line) 50%,transparent);color:var(--dm-fg);font:inherit;font-size:13px;transition:box-shadow var(--dm-t-fast)}
.dm-abx-ta{height:auto;min-height:76px;padding:var(--dm-s-2) 10px;resize:none;line-height:1.5;border-radius:var(--dm-r-box);field-sizing:content}
.dm-abx-in:focus,.dm-abx-ta:focus{box-shadow:inset 0 0 0 1px color-mix(in srgb,var(--dm-field-line-strong) 50%,transparent)}
.dm-abx-in[aria-invalid="true"]{box-shadow:inset 0 0 0 1px var(--dm-bad)}
.dm-abx-in::placeholder,.dm-abx-ta::placeholder{color:var(--dm-muted)}
@supports (corner-shape:squircle){.dm-abx-in{border-radius:var(--dm-r-field-sq)}.dm-abx-ta{border-radius:var(--dm-r-box-sq)}}
.dm-abx-sel{appearance:none;-webkit-appearance:none;padding-right:28px;background:var(--dm-surface) url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23888' stroke-width='2' stroke-linecap='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E") right 9px center/12px no-repeat;cursor:pointer}
.dm-abx-s{display:flex;flex-direction:column;gap:6px;transition:opacity .3s}
.dm-abx-sh{display:flex;align-items:center;gap:var(--dm-s-2);min-height:20px}
.dm-abx-sh>span:first-child{color:var(--dm-fg2);font-weight:500}
.dm-abx-sh .dm-muted{margin-left:auto;font-size:11.5px}
.dm-abx-s.dm-wait{opacity:.45}
.dm-abx-s.dm-busy .dm-abx-sh>span:first-child{animation:dm-shim 2.2s linear infinite;background:linear-gradient(90deg,var(--dm-fg2) 0%,var(--dm-fg2) 38%,var(--dm-faint) 50%,var(--dm-fg2) 62%,var(--dm-fg2) 100%);background-size:260% 100%;-webkit-background-clip:text;background-clip:text;color:transparent}
/* drafted by the agent, until someone edits it: the accent's tint with the accent as text */
.dm-abx-dt{display:inline-flex;align-items:center;gap:4px;height:18px;padding:0 6px;border-radius:var(--dm-r-tag);background:var(--dm-accent-bg);color:var(--dm-accent-fg,var(--dm-accent));font-size:11px;font-weight:500}
@supports (corner-shape:squircle){.dm-abx-dt{border-radius:var(--dm-r-tag-sq)}}
/* identity: the agent's avatar (Dotto) beside its name, with its colour and shape picked under the name */
.dm-abx-idr{display:grid;grid-template-columns:auto minmax(0,1fr);gap:var(--dm-s-2) var(--dm-s-3);align-items:center}
.dm-abx-av{grid-row:span 2;width:64px;height:64px;display:grid;place-items:center}
.dm-abx-av .dm-abx-mascot{width:64px;height:64px}
.dm-abx-look{display:flex;align-items:center;justify-content:space-between;gap:var(--dm-s-2) var(--dm-s-3);flex-wrap:wrap;min-width:0}
/* colour: swatches, a radio group; the one picked has a grey ring */
.dm-abx-sw{display:flex;gap:var(--dm-s-2)}
.dm-abx-sw button{width:20px;height:20px;border-radius:calc(6px * var(--dm-rk));background:var(--dm-s);box-shadow:inset 0 0 0 1px rgba(0,0,0,.08);transition:transform .15s var(--dm-ease),box-shadow var(--dm-t-fast)}
@media (hover:hover) and (pointer:fine){.dm-abx-sw button:hover{transform:scale(1.08)}}
.dm-abx-sw button[aria-checked="true"]{box-shadow:0 0 0 2px var(--dm-surface),0 0 0 3px color-mix(in srgb,var(--dm-pick-on) 70%,transparent)}
.dm-abx-sw button:focus-visible{outline-offset:4px;border-radius:calc(6px * var(--dm-rk))}
/* shape: a segmented switch of outlines, a radio group */
.dm-abx-shp button{width:30px;height:24px;padding:0;display:grid;place-items:center}
.dm-abx-shp button[aria-checked="true"]{color:var(--dm-fg)}
.dm-abx-shp svg{width:14px;height:14px;fill:none;stroke:currentColor;stroke-width:1.5;overflow:visible}
.dm-abx-shp svg path{vector-effect:non-scaling-stroke}
:root:not([data-style="soft"]) .dm-abx-sw button:not(:where([data-style="soft"],[data-style="soft"] *)),:root:not([data-style="soft"]) .dm-abx-sw button:focus-visible:not(:where([data-style="soft"],[data-style="soft"] *)){border-radius:0}
/* Dotto, the agent's mascot: a white screen (owner, 4 Nov: dark on a dark bezel was hard to see) in a bezel of the agent's colour (--dm-m, the accent unless one is picked), a 5 by 5
   grid of dots drawing its eyes and smile. Idle blinks the top row of dots; thinking moves the eyes cell by cell and runs a
   loading row. With reduced motion each state is a still pose */
.dm-abx-mascot{--dm-m-c:var(--dm-m,var(--dm-accent));--dm-m-ink:color-mix(in oklab,var(--dm-m-c) 18%,#0a0a10);--dm-m-glow:color-mix(in oklab,var(--dm-m-c) 82%,white);display:block;flex:none;width:24px;height:24px;overflow:visible}
.dm-abx-mascot .dm-bz{fill:var(--dm-m-c);transition:fill var(--dm-t-fast)}
.dm-abx-mascot .dm-sc{fill:#fff;transition:fill var(--dm-t-fast)}
.dm-abx-mascot .dm-dg{fill:var(--dm-m-c);opacity:.22}
/* the lit dots are the agent's colour taken darker, so they read on the white screen in every colour, lime included */
.dm-abx-mascot .dm-on{fill:color-mix(in oklab,var(--dm-m-c) 72%,#0a0a10)}
/* in dark mode the screen is black and the lit dots glow in the agent's colour */
:root[data-theme="dark"] .dm-abx-mascot .dm-sc{fill:#0b0b0e}
:root[data-theme="dark"] .dm-abx-mascot .dm-on{fill:var(--dm-m-glow)}
:root[data-theme="dark"] .dm-abx-mascot .dm-dg{opacity:.3}
@media (prefers-color-scheme:dark){:root:not([data-theme="light"]) .dm-abx-mascot .dm-sc{fill:#0b0b0e}:root:not([data-theme="light"]) .dm-abx-mascot .dm-on{fill:var(--dm-m-glow)}:root:not([data-theme="light"]) .dm-abx-mascot .dm-dg{opacity:.3}}
.dm-abx-mascot .dm-th,.dm-abx-mascot[data-state="thinking"] .dm-id{display:none}
.dm-abx-mascot[data-state="thinking"] .dm-th{display:inline}
.dm-abx-mascot[data-state="thinking"] .dm-lk{transform:translate(0,-6.5px)}
@media (prefers-reduced-motion:no-preference){
.dm-abx-mascot .dm-tp{animation:dm-abxblink 5.2s step-end infinite}
.dm-abx-mascot[data-state="thinking"] .dm-lk{animation:dm-abxlook 2.7s step-end infinite}
.dm-abx-mascot[data-state="thinking"] .dm-ld{animation:dm-abxload .96s step-end infinite}
.dm-abx-mascot[data-state="thinking"] .dm-l2{animation-delay:-.64s}
.dm-abx-mascot[data-state="thinking"] .dm-l3{animation-delay:-.32s}
}
@keyframes dm-abxblink{0%{opacity:1}90%{opacity:0}94%{opacity:1}}
@keyframes dm-abxlook{0%{transform:translate(0,-6.5px)}33.3%{transform:translate(6.5px,-6.5px)}66.6%{transform:translate(-6.5px,-6.5px)}}
@keyframes dm-abxload{0%{opacity:1}33.3%{opacity:.18}}
.dm-abx-err{display:flex;align-items:center;gap:6px;font-size:12px;color:var(--dm-bad)}
.dm-abx-mr{display:flex;gap:var(--dm-s-2);flex-wrap:wrap}
.dm-abx-mr .dm-abx-sel{flex:1;min-width:120px}
.dm-abx-eff button,.dm-abx-trs button,.dm-abx-tl .dm-seg2 button{font-size:12px;height:28px;padding:0 10px}
.dm-abx-tools{display:flex;flex-direction:column;gap:2px}
.dm-abx-tl{display:flex;align-items:center;gap:var(--dm-s-2);padding:2px 0;font-size:13px}
.dm-abx-tl .dm-brand{width:16px;height:16px}
.dm-abx-tl .dm-n{flex:1;min-width:0}
.dm-abx-tl .dm-seg2 button{height:24px;padding:0 9px;font-size:11.5px}
.dm-abx-kn{display:flex;flex-wrap:wrap;gap:6px}
.dm-abx-tr{display:flex;align-items:center;gap:var(--dm-s-2);flex-wrap:wrap}
.dm-abx-trd{font-size:12px}
.dm-abx-bar{display:flex;align-items:center;gap:6px;margin-top:var(--dm-s-1);padding-top:var(--dm-s-3);border-top:1px solid color-mix(in srgb,var(--dm-line) 50%,transparent)}
.dm-abx-st{font-size:12.5px;display:flex;align-items:center;gap:6px}
.dm-abx-c{display:flex;flex-direction:column;min-width:0;background:var(--dm-row);border-left:1px solid color-mix(in srgb,var(--dm-line) 50%,transparent)}
.dm-abx-ch{display:flex;align-items:center;gap:var(--dm-s-3);padding:var(--dm-s-4) var(--dm-s-4) var(--dm-s-2);font-size:13.5px;line-height:1.4}
.dm-abx-ch .dm-abx-mascot{width:32px;height:32px}
.dm-abx-cht{display:flex;flex-direction:column;min-width:0}
.dm-abx-ch b{font-weight:500}
.dm-abx-ch .dm-muted{font-size:12px}
.dm-abx-log{flex:1;display:flex;flex-direction:column;gap:var(--dm-s-3);padding:var(--dm-s-2) var(--dm-s-4) var(--dm-s-4)}
.dm-abx-empty{margin:auto 0;text-align:center;font-size:12.5px;line-height:1.5}
.dm-abx-em{align-self:flex-end;max-width:92%;display:flex;flex-direction:column;gap:2px;padding:9px var(--dm-s-3);border-radius:var(--dm-r-card);background:var(--dm-surface);box-shadow:inset 0 0 0 1px color-mix(in srgb,var(--dm-line) 50%,transparent);font-size:12.5px;line-height:1.5;color:var(--dm-fg2)}
.dm-abx-em b{font-weight:500;color:var(--dm-fg);font-size:13px}
.dm-abx-em .dm-muted{font-size:11.5px}
.dm-abx-rp{display:flex;flex-direction:column;gap:6px;font-size:13px;line-height:1.5}
.dm-abx-rp .dm-k{display:flex;align-items:center;gap:6px;font-size:12px;color:var(--dm-muted)}
.dm-abx-rp .dm-tags{display:flex;gap:4px;flex-wrap:wrap}
.dm-abx-sr{position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;clip-path:inset(50%);white-space:nowrap}
@container (max-width:600px){.dm-abx-g{grid-template-columns:minmax(0,1fr)}.dm-abx-c{border-left:0;border-top:1px solid color-mix(in srgb,var(--dm-line) 50%,transparent);min-height:220px}}
/* on a phone the state takes its own line over the buttons, so Live with its avatar never pushes Publish out of the card */
@container (max-width:340px){.dm-abx-bar{flex-wrap:wrap;row-gap:var(--dm-s-2)}.dm-abx-st{flex-basis:100%}}
AgentBuilder.astroAstro · 49 lines
---
// Generated from designme.dev by scripts/sync.mjs. Edit the site, then sync.
// Agent builder: Describe an agent in a sentence and it drafts the rest: who it is, what it may do, what it knows, when it runs. Test it, save it, publish it.
// The markup, CSS and script are designme.dev's own, run by the shared core in ../core, which mounts the component when it scrolls
// into view and plays its demo. To show your own data, change agent-builder.js. The markup is kept exactly as it is (is:raw).
import type { HTMLAttributes } from 'astro/types';
import '../core/designme.css';
import './agent-builder.css';

export interface Props extends HTMLAttributes<'div'> {
  /** "soft" draws it in the Soft style. Leave it out for Blueprint, or for the style set on <html> (data-style="soft") */
  'data-style'?: 'soft';
}
const { class: className, ...rest } = Astro.props;
---
<div class:list={['card', 'wide', className]} data-c="agent-builder" {...rest}>
  <div class="stage-in"><Fragment is:raw>
<div class="abx panel"><div class="abx-g">
  <div class="abx-f">
    <div class="abx-ask"><label class="abx-l" for="abx-desc">Describe your agent</label><div class="abx-askr"><input class="abx-in" id="abx-desc" autocomplete="off" placeholder="What should it do?"><button class="pill primary sm abx-go"><svg class="i xs"><use href="#i-spark"/></svg>Draft</button></div></div>
    <div class="abx-s" data-s="id" role="group" aria-label="Identity"><div class="abx-sh"><span>Identity</span></div>
      <div class="abx-idr"><span class="abx-av"></span><input class="abx-in abx-name" aria-label="Name" placeholder="Name" autocomplete="off">
        <div class="abx-look"><div class="abx-sw" role="radiogroup" aria-label="Colour"></div><div class="seg2 abx-shp" role="radiogroup" aria-label="Shape"></div></div></div>
      <span class="abx-err" hidden><svg class="i xs"><use href="#i-alert"/></svg>Give it a name to save it</span>
      <input class="abx-in abx-purp" aria-label="Purpose" placeholder="What it is for" autocomplete="off">
    </div>
    <div class="abx-s" data-s="ins" role="group" aria-label="Instructions"><div class="abx-sh"><span>Instructions</span></div><textarea class="abx-ta" rows="3" aria-label="Instructions" placeholder="How it should work"></textarea></div>
    <div class="abx-s" data-s="model" role="group" aria-label="Model and effort"><div class="abx-sh"><span>Model and effort</span></div>
      <div class="abx-mr"><select class="abx-in abx-sel" aria-label="Model"><option>Haiku 5</option><option>Sonnet 5</option><option>Opus 5.5</option></select><div class="seg2 abx-eff" role="group" aria-label="Effort"><button aria-pressed="false" data-v="low">Low</button><button aria-pressed="true" data-v="med">Medium</button><button aria-pressed="false" data-v="high">High</button></div></div>
    </div>
    <div class="abx-s" data-s="tools" role="group" aria-label="Tools"><div class="abx-sh"><span>Tools</span><span class="muted">What it may do in each</span></div><div class="abx-tools"></div></div>
    <div class="abx-s" data-s="know" role="group" aria-label="Knowledge"><div class="abx-sh"><span>Knowledge</span></div><div class="abx-kn"></div></div>
    <div class="abx-s" data-s="trig" role="group" aria-label="Trigger"><div class="abx-sh"><span>Trigger</span></div>
      <div class="abx-tr"><div class="seg2 abx-trs" role="group" aria-label="Starts on"><button aria-pressed="true" data-v="chat">Chat</button><button aria-pressed="false" data-v="sched">Schedule</button><button aria-pressed="false" data-v="event">Event</button></div><span class="muted abx-trd">When someone messages it</span></div>
    </div>
    <div class="abx-bar"><span class="abx-st muted">Not saved</span><span class="sp"></span><button class="pill line sm abx-test"><svg class="i xs"><use href="#i-play"/></svg>Test</button><button class="pill line sm abx-save">Save</button><button class="pill primary sm abx-pub">Publish</button></div>
  </div>
  <div class="abx-c">
    <div class="abx-ch"><div class="abx-cht"><b>Test</b><span class="muted abx-cs">Try it before it goes live</span></div></div>
    <div class="abx-log" aria-live="polite"><p class="muted abx-empty">Draft the agent, then press Test to send it a sample email.</p></div>
  </div>
  </div><p class="abx-sr" role="status"></p>
</div>
  </Fragment></div>
</div>
<script>
  import './agent-builder.js';
</script>
agent-builder.htmlHTML · 31 lines
<div class="card wide" data-c="agent-builder">
  <div class="stage-in">
    <div class="abx panel"><div class="abx-g">
      <div class="abx-f">
        <div class="abx-ask"><label class="abx-l" for="abx-desc">Describe your agent</label><div class="abx-askr"><input class="abx-in" id="abx-desc" autocomplete="off" placeholder="What should it do?"><button class="pill primary sm abx-go"><svg class="i xs"><use href="#i-spark"/></svg>Draft</button></div></div>
        <div class="abx-s" data-s="id" role="group" aria-label="Identity"><div class="abx-sh"><span>Identity</span></div>
          <div class="abx-idr"><span class="abx-av"></span><input class="abx-in abx-name" aria-label="Name" placeholder="Name" autocomplete="off">
            <div class="abx-look"><div class="abx-sw" role="radiogroup" aria-label="Colour"></div><div class="seg2 abx-shp" role="radiogroup" aria-label="Shape"></div></div></div>
          <span class="abx-err" hidden><svg class="i xs"><use href="#i-alert"/></svg>Give it a name to save it</span>
          <input class="abx-in abx-purp" aria-label="Purpose" placeholder="What it is for" autocomplete="off">
        </div>
        <div class="abx-s" data-s="ins" role="group" aria-label="Instructions"><div class="abx-sh"><span>Instructions</span></div><textarea class="abx-ta" rows="3" aria-label="Instructions" placeholder="How it should work"></textarea></div>
        <div class="abx-s" data-s="model" role="group" aria-label="Model and effort"><div class="abx-sh"><span>Model and effort</span></div>
          <div class="abx-mr"><select class="abx-in abx-sel" aria-label="Model"><option>Haiku 5</option><option>Sonnet 5</option><option>Opus 5.5</option></select><div class="seg2 abx-eff" role="group" aria-label="Effort"><button aria-pressed="false" data-v="low">Low</button><button aria-pressed="true" data-v="med">Medium</button><button aria-pressed="false" data-v="high">High</button></div></div>
        </div>
        <div class="abx-s" data-s="tools" role="group" aria-label="Tools"><div class="abx-sh"><span>Tools</span><span class="muted">What it may do in each</span></div><div class="abx-tools"></div></div>
        <div class="abx-s" data-s="know" role="group" aria-label="Knowledge"><div class="abx-sh"><span>Knowledge</span></div><div class="abx-kn"></div></div>
        <div class="abx-s" data-s="trig" role="group" aria-label="Trigger"><div class="abx-sh"><span>Trigger</span></div>
          <div class="abx-tr"><div class="seg2 abx-trs" role="group" aria-label="Starts on"><button aria-pressed="true" data-v="chat">Chat</button><button aria-pressed="false" data-v="sched">Schedule</button><button aria-pressed="false" data-v="event">Event</button></div><span class="muted abx-trd">When someone messages it</span></div>
        </div>
        <div class="abx-bar"><span class="abx-st muted">Not saved</span><span class="sp"></span><button class="pill line sm abx-test"><svg class="i xs"><use href="#i-play"/></svg>Test</button><button class="pill line sm abx-save">Save</button><button class="pill primary sm abx-pub">Publish</button></div>
      </div>
      <div class="abx-c">
        <div class="abx-ch"><div class="abx-cht"><b>Test</b><span class="muted abx-cs">Try it before it goes live</span></div></div>
        <div class="abx-log" aria-live="polite"><p class="muted abx-empty">Draft the agent, then press Test to send it a sample email.</p></div>
      </div>
      </div><p class="abx-sr" role="status"></p>
    </div>
  </div>
</div>
agent-builder.cssHTML · 103 lines
/* ---------- Agent builder ---------- */
/* a form on the left, a test chat on the right (under it on a narrow card). Every field the agent drafted carries a Drafted tag
   until someone changes it; a bar at the foot of the form saves, tests and publishes */
.abx{overflow:hidden;container-type:inline-size}
.abx-g{display:grid;grid-template-columns:minmax(0,1.3fr) minmax(0,1fr)}
.abx-f{display:flex;flex-direction:column;gap:var(--dm-s-3);padding:var(--dm-s-4);min-width:0}
.abx-l,.abx-sh{font-size:12px;color:var(--dm-muted)}
.abx-ask{display:flex;flex-direction:column;gap:6px;padding-bottom:var(--dm-s-3);border-bottom:1px solid color-mix(in srgb,var(--dm-line) 50%,transparent)}
.abx-askr{display:flex;gap:var(--dm-s-2)}
.abx-askr .abx-in{flex:1}
.abx-in,.abx-ta{min-width:0;height:32px;padding:0 10px;border:0;border-radius:var(--dm-r-field);outline:0;background:var(--dm-surface);box-shadow:inset 0 0 0 1px color-mix(in srgb,var(--dm-field-line) 50%,transparent);color:var(--dm-fg);font:inherit;font-size:13px;transition:box-shadow var(--dm-t-fast)}
.abx-ta{height:auto;min-height:76px;padding:var(--dm-s-2) 10px;resize:none;line-height:1.5;border-radius:var(--dm-r-box);field-sizing:content}
.abx-in:focus,.abx-ta:focus{box-shadow:inset 0 0 0 1px color-mix(in srgb,var(--dm-field-line-strong) 50%,transparent)}
.abx-in[aria-invalid="true"]{box-shadow:inset 0 0 0 1px var(--dm-bad)}
.abx-in::placeholder,.abx-ta::placeholder{color:var(--dm-muted)}
@supports (corner-shape:squircle){.abx-in{border-radius:var(--dm-r-field-sq)}.abx-ta{border-radius:var(--dm-r-box-sq)}}
.abx-sel{appearance:none;-webkit-appearance:none;padding-right:28px;background:var(--dm-surface) url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23888' stroke-width='2' stroke-linecap='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E") right 9px center/12px no-repeat;cursor:pointer}
.abx-s{display:flex;flex-direction:column;gap:6px;transition:opacity .3s}
.abx-sh{display:flex;align-items:center;gap:var(--dm-s-2);min-height:20px}
.abx-sh>span:first-child{color:var(--dm-fg2);font-weight:500}
.abx-sh .muted{margin-left:auto;font-size:11.5px}
.abx-s.wait{opacity:.45}
.abx-s.busy .abx-sh>span:first-child{animation:shim 2.2s linear infinite;background:linear-gradient(90deg,var(--dm-fg2) 0%,var(--dm-fg2) 38%,var(--dm-faint) 50%,var(--dm-fg2) 62%,var(--dm-fg2) 100%);background-size:260% 100%;-webkit-background-clip:text;background-clip:text;color:transparent}
/* drafted by the agent, until someone edits it: the accent's tint with the accent as text */
.abx-dt{display:inline-flex;align-items:center;gap:4px;height:18px;padding:0 6px;border-radius:var(--dm-r-tag);background:var(--dm-accent-bg);color:var(--dm-accent-fg,var(--dm-accent));font-size:11px;font-weight:500}
@supports (corner-shape:squircle){.abx-dt{border-radius:var(--dm-r-tag-sq)}}
/* identity: the agent's avatar (Dotto) beside its name, with its colour and shape picked under the name */
.abx-idr{display:grid;grid-template-columns:auto minmax(0,1fr);gap:var(--dm-s-2) var(--dm-s-3);align-items:center}
.abx-av{grid-row:span 2;width:64px;height:64px;display:grid;place-items:center}
.abx-av .abx-mascot{width:64px;height:64px}
.abx-look{display:flex;align-items:center;justify-content:space-between;gap:var(--dm-s-2) var(--dm-s-3);flex-wrap:wrap;min-width:0}
/* colour: swatches, a radio group; the one picked has a grey ring */
.abx-sw{display:flex;gap:var(--dm-s-2)}
.abx-sw button{width:20px;height:20px;border-radius:calc(6px * var(--dm-rk));background:var(--dm-s);box-shadow:inset 0 0 0 1px rgba(0,0,0,.08);transition:transform .15s var(--dm-ease),box-shadow var(--dm-t-fast)}
@media (hover:hover) and (pointer:fine){.abx-sw button:hover{transform:scale(1.08)}}
.abx-sw button[aria-checked="true"]{box-shadow:0 0 0 2px var(--dm-surface),0 0 0 3px color-mix(in srgb,var(--dm-pick-on) 70%,transparent)}
.abx-sw button:focus-visible{outline-offset:4px;border-radius:calc(6px * var(--dm-rk))}
/* shape: a segmented switch of outlines, a radio group */
.abx-shp button{width:30px;height:24px;padding:0;display:grid;place-items:center}
.abx-shp button[aria-checked="true"]{color:var(--dm-fg)}
.abx-shp svg{width:14px;height:14px;fill:none;stroke:currentColor;stroke-width:1.5;overflow:visible}
.abx-shp svg path{vector-effect:non-scaling-stroke}
:root:not([data-style="soft"]) .abx-sw button:not(:where([data-style="soft"],[data-style="soft"] *)),:root:not([data-style="soft"]) .abx-sw button:focus-visible:not(:where([data-style="soft"],[data-style="soft"] *)){border-radius:0}
/* Dotto, the agent's mascot: a white screen (owner, 4 Nov: dark on a dark bezel was hard to see) in a bezel of the agent's colour (--dm-m, the accent unless one is picked), a 5 by 5
   grid of dots drawing its eyes and smile. Idle blinks the top row of dots; thinking moves the eyes cell by cell and runs a
   loading row. With reduced motion each state is a still pose */
.abx-mascot{--dm-m-c:var(--dm-m,var(--dm-accent));--dm-m-ink:color-mix(in oklab,var(--dm-m-c) 18%,#0a0a10);--dm-m-glow:color-mix(in oklab,var(--dm-m-c) 82%,white);display:block;flex:none;width:24px;height:24px;overflow:visible}
.abx-mascot .bz{fill:var(--dm-m-c);transition:fill var(--dm-t-fast)}
.abx-mascot .sc{fill:#fff;transition:fill var(--dm-t-fast)}
.abx-mascot .dg{fill:var(--dm-m-c);opacity:.22}
/* the lit dots are the agent's colour taken darker, so they read on the white screen in every colour, lime included */
.abx-mascot .on{fill:color-mix(in oklab,var(--dm-m-c) 72%,#0a0a10)}
/* in dark mode the screen is black and the lit dots glow in the agent's colour */
:root[data-theme="dark"] .abx-mascot .sc{fill:#0b0b0e}
:root[data-theme="dark"] .abx-mascot .on{fill:var(--dm-m-glow)}
:root[data-theme="dark"] .abx-mascot .dg{opacity:.3}
@media (prefers-color-scheme:dark){:root:not([data-theme="light"]) .abx-mascot .sc{fill:#0b0b0e}:root:not([data-theme="light"]) .abx-mascot .on{fill:var(--dm-m-glow)}:root:not([data-theme="light"]) .abx-mascot .dg{opacity:.3}}
.abx-mascot .th,.abx-mascot[data-state="thinking"] .id{display:none}
.abx-mascot[data-state="thinking"] .th{display:inline}
.abx-mascot[data-state="thinking"] .lk{transform:translate(0,-6.5px)}
@media (prefers-reduced-motion:no-preference){
.abx-mascot .tp{animation:abxblink 5.2s step-end infinite}
.abx-mascot[data-state="thinking"] .lk{animation:abxlook 2.7s step-end infinite}
.abx-mascot[data-state="thinking"] .ld{animation:abxload .96s step-end infinite}
.abx-mascot[data-state="thinking"] .l2{animation-delay:-.64s}
.abx-mascot[data-state="thinking"] .l3{animation-delay:-.32s}
}
@keyframes abxblink{0%{opacity:1}90%{opacity:0}94%{opacity:1}}
@keyframes abxlook{0%{transform:translate(0,-6.5px)}33.3%{transform:translate(6.5px,-6.5px)}66.6%{transform:translate(-6.5px,-6.5px)}}
@keyframes abxload{0%{opacity:1}33.3%{opacity:.18}}
.abx-err{display:flex;align-items:center;gap:6px;font-size:12px;color:var(--dm-bad)}
.abx-mr{display:flex;gap:var(--dm-s-2);flex-wrap:wrap}
.abx-mr .abx-sel{flex:1;min-width:120px}
.abx-eff button,.abx-trs button,.abx-tl .seg2 button{font-size:12px;height:28px;padding:0 10px}
.abx-tools{display:flex;flex-direction:column;gap:2px}
.abx-tl{display:flex;align-items:center;gap:var(--dm-s-2);padding:2px 0;font-size:13px}
.abx-tl .brand{width:16px;height:16px}
.abx-tl .n{flex:1;min-width:0}
.abx-tl .seg2 button{height:24px;padding:0 9px;font-size:11.5px}
.abx-kn{display:flex;flex-wrap:wrap;gap:6px}
.abx-tr{display:flex;align-items:center;gap:var(--dm-s-2);flex-wrap:wrap}
.abx-trd{font-size:12px}
.abx-bar{display:flex;align-items:center;gap:6px;margin-top:var(--dm-s-1);padding-top:var(--dm-s-3);border-top:1px solid color-mix(in srgb,var(--dm-line) 50%,transparent)}
.abx-st{font-size:12.5px;display:flex;align-items:center;gap:6px}
.abx-c{display:flex;flex-direction:column;min-width:0;background:var(--dm-row);border-left:1px solid color-mix(in srgb,var(--dm-line) 50%,transparent)}
.abx-ch{display:flex;align-items:center;gap:var(--dm-s-3);padding:var(--dm-s-4) var(--dm-s-4) var(--dm-s-2);font-size:13.5px;line-height:1.4}
.abx-ch .abx-mascot{width:32px;height:32px}
.abx-cht{display:flex;flex-direction:column;min-width:0}
.abx-ch b{font-weight:500}
.abx-ch .muted{font-size:12px}
.abx-log{flex:1;display:flex;flex-direction:column;gap:var(--dm-s-3);padding:var(--dm-s-2) var(--dm-s-4) var(--dm-s-4)}
.abx-empty{margin:auto 0;text-align:center;font-size:12.5px;line-height:1.5}
.abx-em{align-self:flex-end;max-width:92%;display:flex;flex-direction:column;gap:2px;padding:9px var(--dm-s-3);border-radius:var(--dm-r-card);background:var(--dm-surface);box-shadow:inset 0 0 0 1px color-mix(in srgb,var(--dm-line) 50%,transparent);font-size:12.5px;line-height:1.5;color:var(--dm-fg2)}
.abx-em b{font-weight:500;color:var(--dm-fg);font-size:13px}
.abx-em .muted{font-size:11.5px}
.abx-rp{display:flex;flex-direction:column;gap:6px;font-size:13px;line-height:1.5}
.abx-rp .k{display:flex;align-items:center;gap:6px;font-size:12px;color:var(--dm-muted)}
.abx-rp .tags{display:flex;gap:4px;flex-wrap:wrap}
.abx-sr{position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;clip-path:inset(50%);white-space:nowrap}
@container (max-width:600px){.abx-g{grid-template-columns:minmax(0,1fr)}.abx-c{border-left:0;border-top:1px solid color-mix(in srgb,var(--dm-line) 50%,transparent);min-height:220px}}
/* on a phone the state takes its own line over the buttons, so Live with its avatar never pushes Publish out of the card */
@container (max-width:340px){.abx-bar{flex-wrap:wrap;row-gap:var(--dm-s-2)}.abx-st{flex-basis:100%}}
agent-builder.jsHTML · 127 lines
DM.def('agent-builder', (r, c) => {
  r.innerHTML = r.innerHTML.replace(/\{(spark|alert|play)\}/g, (m, k) => I(k, 'xs'));
  const $r = s => $(s, r);
  const root = $r('.abx'), desc = $r('#abx-desc'), go = $r('.abx-go'), name = $r('.abx-name'), purp = $r('.abx-purp'), ins = $r('.abx-ta'), sel = $r('.abx-sel'), err = $r('.abx-err');
  const tools = $r('.abx-tools'), kn = $r('.abx-kn'), trd = $r('.abx-trd'), st = $r('.abx-st'), test = $r('.abx-test'), save = $r('.abx-save'), pub = $r('.abx-pub'), log = $r('.abx-log'), cs = $r('.abx-cs'), sr = $r('.abx-sr');
  const S = k => $(`.abx-s[data-s="${k}"]`, r);
  // Dotto, the agent's avatar: a dark LED screen in a bezel of the agent's colour, a 5 by 5 grid of dots drawing its eyes and
  // smile. Each one carries only the outline in use; its colour is --m on the builder, so every Dotto in it changes at once
  const SHAPES = { circle:['M4 32A28 28 0 1 0 60 32A28 28 0 1 0 4 32Z', 'M10.5 32A21.5 21.5 0 1 0 53.5 32A21.5 21.5 0 1 0 10.5 32Z'], squircle:['M60 32C60 58.5 58.5 60 32 60S4 58.5 4 32 5.5 4 32 4 60 5.5 60 32Z', 'M53.5 32C53.5 52.3 52.3 53.5 32 53.5S10.5 52.3 10.5 32 11.7 10.5 32 10.5 53.5 11.7 53.5 32Z'] };
  const SHAPE_N = { circle:'Circle', squircle:'Squircle' };
  const COLS = [['accent', 'Accent', 'var(--dm-accent)'], ['blue', 'Blue', '#116ff8'], ['violet', 'Violet', '#7c4dff'], ['orange', 'Orange', '#f76700'], ['green', 'Green', '#00a876']];
  // it starts as an accent squircle (owner, 4 Nov: squircle and circle only)
  const look = { color:'accent', shape:'squircle' };
  let mood = 'idle';
  const dots = (cls, cells) => cells.map(([x, y]) => `<circle${cls ? ` class="${cls}"` : ''} cx="${19 + x * 6.5}" cy="${19 + y * 6.5}" r="${cls ? 2.75 : 1.3}"/>`).join('');
  const GRID = [...Array(25)].map((_, i) => [i % 5, i / 5 | 0]);
  const mascot = () => { const [bz, sc] = SHAPES[look.shape];
    return `<svg class="abx-mascot" viewBox="0 0 64 64" data-shape="${look.shape}" data-state="${mood}" aria-hidden="true" focusable="false"><path class="bz" d="${bz}"/><path class="sc" d="${sc}"/>`
      + `<g class="dg">${dots('', GRID)}</g><g class="lk">${dots('on tp', [[1, 1], [3, 1]])}${dots('on', [[1, 2], [3, 2]])}</g>`
      + `<g class="id">${dots('on', [[0, 3], [4, 3], [1, 4], [2, 4], [3, 4]])}</g><g class="th">${dots('on ld l1', [[1, 4]])}${dots('on ld l2', [[2, 4]])}${dots('on ld l3', [[3, 4]])}</g></svg>`; };
  const av = $r('.abx-av'), sw = $r('.abx-sw'), shp = $r('.abx-shp');
  av.innerHTML = mascot(); av.setAttribute('role', 'img');
  $r('.abx-ch').insertAdjacentHTML('afterbegin', mascot());
  // a change of colour, shape or mood reaches every Dotto in the builder: the avatar, the test chat's header and replies, Live
  function paint(){
    root.style.setProperty('--dm-m', COLS.find(x => x[0] === look.color)[2]);
    const [bz, sc] = SHAPES[look.shape];
    $$('.abx-mascot', r).forEach(m => { m.dataset.shape = look.shape; m.dataset.state = mood; $('.bz', m).setAttribute('d', bz); $('.sc', m).setAttribute('d', sc); });
    av.setAttribute('aria-label', `Avatar: ${COLS.find(x => x[0] === look.color)[1]}, ${SHAPE_N[look.shape].toLowerCase()}`);
  }
  // the pickers are radio groups: a click picks, the arrow keys move and pick (and wrap), Home and End go to the ends, and only
  // the picked one is in the tab order
  function radios(g, on){
    const all = () => $$('[role="radio"]', g), shown = () => all().filter(b => !b.hidden);
    const pick = (v, by) => { const t = all().find(b => b.dataset.v === v); if (!t || t.hidden) v = shown()[0].dataset.v;
      all().forEach(b => { const y = b.dataset.v === v; b.setAttribute('aria-checked', String(y)); b.tabIndex = y ? 0 : -1; }); on(v, by); };
    c.on(g, 'click', e => { const b = e.target.closest('[role="radio"]'); if (b) pick(b.dataset.v, true); });
    c.on(g, 'keydown', e => { const l = shown(), i = l.indexOf(document.activeElement), d = { ArrowRight:1, ArrowDown:1, ArrowLeft:-1, ArrowUp:-1 }[e.key];
      const to = d ? l[(i + d + l.length) % l.length] : e.key === 'Home' ? l[0] : e.key === 'End' ? l[l.length - 1] : null;
      if (to){ e.preventDefault(); to.focus(); pick(to.dataset.v, true); } });
    return v => pick(v, false);
  }
  sw.innerHTML = COLS.map(([v, n, x]) => `<button role="radio" aria-checked="false" tabindex="-1" aria-label="${n}" data-tip="${n}" data-v="${v}" style="--dm-s:${x}"></button>`).join('');
  // a colour the accent already is shows once, as the accent
  const ink = v => { const t = document.createElement('i'); t.hidden = true; t.style.color = v; root.appendChild(t); const x = getComputedStyle(t).color; t.remove(); return x; };
  const acc = ink('var(--dm-accent)');
  COLS.slice(1).forEach(([v, , x]) => { if (ink(x) === acc) $(`[data-v="${v}"]`, sw).hidden = true; });
  shp.innerHTML = Object.entries(SHAPE_N).map(([v, n]) => `<button role="radio" aria-checked="false" tabindex="-1" aria-label="${n}" data-tip="${n}" data-v="${v}"><svg viewBox="0 0 64 64" aria-hidden="true" focusable="false"><path d="${SHAPES[v][0]}"/></svg></button>`).join('');
  // the shape switch slides its knob like the other switches; its first placing is instant
  const knob = document.createElement('span'); knob.className = 'knob'; shp.prepend(knob);
  const place = () => { const b = $('[aria-checked="true"]', shp); if (b){ knob.style.left = b.offsetLeft + 'px'; knob.style.width = b.offsetWidth + 'px'; } };
  const setColor = radios(sw, (v, by) => { look.color = v; paint(); if (by) edited('id'); });
  const setShape = radios(shp, (v, by) => { look.shape = v; place(); paint(); if (by) edited('id'); });
  setColor(look.color); setShape(look.shape);
  requestAnimationFrame(() => { knob.style.transition = 'none'; place(); void knob.offsetWidth; knob.style.transition = ''; });
  new ResizeObserver(place).observe(shp);
  const think = on => { mood = on ? 'thinking' : 'idle'; paint(); };
  // tools, each with what the agent may do there: read, ask first, or act
  const T = [['gmail', 'Gmail'], ['linear', 'Linear'], ['slack', 'Slack']];
  tools.innerHTML = T.map(([k, n]) => `<div class="abx-tl" data-t="${k}">${BRAND[k]}<span class="n">${n}</span><div class="seg2" role="group" aria-label="${n}: what it may do"><button aria-pressed="true" data-v="read">Read</button><button aria-pressed="false" data-v="ask">Ask</button><button aria-pressed="false" data-v="act">Act</button></div></div>`).join('');
  const segs = {}; $$('.abx-tl', tools).forEach(t => segs[t.dataset.t] = seg($('.seg2', t), () => edited('tools')));
  const setSeg = (el, v) => { const b = $(`button[data-v="${v}"]`, el); $$('button', el).forEach(x => x.setAttribute('aria-pressed', x === b)); };
  const TRIG = { chat:'When someone messages it', sched:'Every weekday at 9:00', event:'When an email arrives at support@' };
  seg($r('.abx-trs'), v => { swap(trd, TRIG[v]); edited('trig'); });
  seg($r('.abx-eff'), () => edited('model'));
  kn.innerHTML = '<span class="muted" style="font-size:12px">Nothing yet</span>';
  // what the person has changed since it was drafted: the Drafted tag goes, and the form has unsaved changes
  const tag = k => $('.abx-dt', S(k));
  let state = 'empty';
  const status = (t, cls) => { st.innerHTML = t; st.className = 'abx-st ' + (cls || 'muted'); };
  function edited(k){ const t = tag(k); if (t) t.remove(); if (state !== 'empty'){ state = 'dirty'; status('Unsaved changes'); } valid(); }
  [[name, 'id'], [purp, 'id'], [ins, 'ins'], [sel, 'model']].forEach(([el, k]) => c.on(el, 'input', () => edited(k)));
  c.on(sel, 'change', () => edited('model'));
  // a name is needed to save or publish
  err.id = 'abx-err-' + Math.random().toString(36).slice(2, 8); name.setAttribute('aria-describedby', err.id);
  function valid(){ const ok = !!name.value.trim() || state === 'empty'; err.hidden = ok; name.setAttribute('aria-invalid', String(!ok)); save.disabled = !ok || state === 'empty' || state === 'saved' || state === 'live'; pub.disabled = !ok || state === 'empty' || state === 'live'; return ok; }
  valid(); test.disabled = true;
  // drafting: the agent fills each section in turn from the one sentence, and marks it Drafted
  const D = {
    // it names the agent, then picks its colour and its shape
    id:async () => { name.value = 'Support triage'; purp.value = 'Sorts incoming support emails and routes them to the right team'; await c.wait(350); setColor('violet'); await c.wait(350); setShape('circle'); },
    ins:() => { ins.value = 'Read each new email. Tag it Billing, Bug or How-to, set its priority, and route it to that team. Draft a reply for billing refunds but never send money without asking.'; },
    model:() => { sel.value = 'Sonnet 5'; setSeg($r('.abx-eff'), 'med'); },
    tools:() => { setSeg($('.seg2', $('[data-t="gmail"]', tools)), 'act'); setSeg($('.seg2', $('[data-t="linear"]', tools)), 'act'); setSeg($('.seg2', $('[data-t="slack"]', tools)), 'ask'); },
    know:() => { kn.innerHTML = `<span class="tag">${I('file-text','xs')}Refund policy.pdf</span><span class="tag">${I('globe','xs')}Help centre</span><span class="tag">${I('table','xs')}Team routing.csv</span>`; },
    trig:() => { $$('.abx-trs button', r).forEach(b => b.setAttribute('aria-pressed', b.dataset.v === 'event')); $r('.abx-trs').dispatchEvent(new Event('resize')); swap(trd, TRIG.event); }
  };
  let drafting = false;
  go.onclick = () => c.run(async () => {
    if (drafting) return; if (!desc.value.trim()){ desc.focus(); return; }
    drafting = true; go.disabled = true; go.innerHTML = '<span class="spin"></span>Drafting'; sr.textContent = 'Drafting the agent';
    const keys = Object.keys(D); keys.forEach(k => S(k).classList.add('wait'));
    for (const k of keys){ const s = S(k); s.classList.remove('wait'); s.classList.add('busy'); await c.wait(550); await D[k](); s.classList.remove('busy');
      if (!tag(k)){ const t = html(`<span class="abx-dt">${I('spark','xs')}Drafted</span>`); $('.abx-sh', s).insertBefore(t, $('.abx-sh .muted', s)); enter(t); } }
    // the trigger switch was redrawn from code, so its knob is placed again
    const tk = $('.abx-trs .knob', r), tb = $('.abx-trs [aria-pressed="true"]', r); if (tk && tb){ tk.style.left = tb.offsetLeft + 'px'; tk.style.width = tb.offsetWidth + 'px'; }
    $$('.abx-tl .seg2', tools).forEach(g => { const k = $('.knob', g), b = $('[aria-pressed="true"]', g); if (k && b){ k.style.left = b.offsetLeft + 'px'; k.style.width = b.offsetWidth + 'px'; } });
    const ek = $('.abx-eff .knob', r), eb = $('.abx-eff [aria-pressed="true"]', r); if (ek && eb){ ek.style.left = eb.offsetLeft + 'px'; ek.style.width = eb.offsetWidth + 'px'; }
    state = 'dirty'; status('Drafted, not saved'); go.disabled = false; go.className = 'pill line sm abx-go'; go.innerHTML = `${I('refresh','xs')}Draft again`; test.disabled = false; drafting = false; valid(); sr.textContent = 'Drafted. Check the fields, then test it';
  });
  // a test: a sample email goes in, and the agent says what it would do with it
  test.onclick = () => c.run(async () => {
    test.disabled = true; swap(cs, 'Testing with a sample email');
    log.innerHTML = `<div class="abx-em"><b>I was charged twice for October</b><span>Hi, my card shows two charges of $48 on Oct 3. Can you refund one? Maya</span><span class="muted">[email protected] · sample</span></div>`; enter(log.firstElementChild);
    await c.wait(700);
    // Dotto thinks while the agent replies
    think(true);
    const rp = html(`<div class="abx-rp"><span class="k">${mascot()}<span class="shim">Support triage is reading it</span></span></div>`); log.appendChild(rp); enter(rp);
    await c.wait(1300);
    think(false);
    rp.innerHTML = `<span class="k">${mascot()}Support triage</span><span class="tags"><span class="tag">Billing</span><span class="tag warn">High priority</span><span class="tag">Routed to Billing</span></span><span>Duplicate charge of $48 on Oct 3. I drafted a refund reply and will ask before sending it, as the instructions say.</span>`;
    enter(rp); swap(cs, 'The test passed'); test.disabled = false; sr.textContent = 'Test done: Billing, high priority, routed to Billing';
  });
  save.onclick = () => { if (!valid()) { name.focus(); return; } state = 'saved'; status(`${I('check','xs')}Saved`); sr.textContent = 'Saved'; valid(); };
  pub.onclick = () => c.run(async () => { if (!valid()) { name.focus(); return; } pub.innerHTML = '<span class="spin"></span>Publishing'; await c.wait(800); state = 'live'; status(`${mascot()}<span class="tag ok">Live</span>`, 'muted'); pub.textContent = 'Published'; valid(); sr.textContent = 'Published. The agent is live'; });
  // the preview's story: one sentence, the fields fill in, one test email, save, publish
  if (r === c.st) c.story(async s => {
    await s.beat(2);
    await s.point(desc, false); await s.type(desc, 'Triage support emails', { n:.12 }); await s.beat(1);
    await s.tap(go); s.away();
    await s.until(() => !drafting && state === 'dirty'); await s.beat(3);
    await s.tap(test); s.away(); await s.until(() => $('.abx-rp .tags', r)); await s.beat(4);
    await s.tap(save); await s.beat(2);
    await s.tap(pub); s.away(); await s.until(() => state === 'live'); await s.beat(6);
  });
});
06

Questions

What people ask about it.

Does it draft the fields itself?
No. onDraft is yours: ask your model for the fields, then set drafting to the section in hand and update value and drafted as each one arrives.
How do I know which section someone changed?
onChange gets the new value and the section it came from (id, ins, model, tools, know or trig), so you can drop that section from drafted.
What do Read, Ask and Act mean?
They are the permission the agent has in that tool: Read only looks, Ask asks a person before changing anything, and Act may change things on its own. The component shows the choice; enforcing it is up to your agent.
Can I use the avatar on its own?
In React, yes: AgentMascot comes from the same file as AgentBuilder, with color (any CSS colour, the accent if left out), shape (squircle or circle), state (idle or thinking), size in px and an optional label. In AgentDraft, color names one of the builder's colors and shape may be left out for the style's own. In the HTML and Astro versions the builder's script draws it.
07

More in Workspace

Navigation, search, settings, models and automations around the agent.

Automation canvasA trigger, a few agent steps, and a run you can watch flow through.
Canvas toolbarA floating toolbar for a workflow canvas: run, pick a tool, comment, fit and zoom.
Command paletteJump anywhere or hand a task to the agent from one box.
Memory panelWhat the agent remembers about your team, editable and removable.
Model compareRun one prompt on three models side by side and keep the best.
Agent settingsHow much the agent may do alone, with a save bar for changes.
Tools and connectionsThe tools the agent can reach, and how much it may do with each.
Knowledge sourcesThe files, sites and folders the agent knows, and how far each is indexed.
All 12 in Workspace →