npx astro add moonarcA button that copies text to the clipboard: the icon turns into a check, the label says "Copied", and both come back after the ambient duration.
A conversation with an AI agent, replayed in the page: the reader's message, the agent's reply with its tool calls folded into <details> (the call and its result in the monospace face), a request for approval that is approved, and the final answer, typed, with a button that copies it; a prompt box at the foot as a demo form. The server HTML is the end state. The replay is CSS on one clock, started when the chat comes into view, held by a native checkbox drawn as a Pause button and by a pointer, and done in 8 to 12 s. Several conversations can be keyed by slug, and a small internal script picks one from the address (?task=refactor); the default renders without it.
Sample conversation
An imaginary agent on an imaginary repository. Open a call to read what it returned.
You
Fix the failing checkout test in apps/web
Sample agent
One assertion fails: the total keeps a fraction of a cent. I will find where it is computed.
read_log(job 4182)0.4 sFAIL apps/web/cart.test.ts
total > rounds to cents
expected 23.99, received 23.988grep("* 1.2" apps/web/src)0.2 sapps/web/src/cart/total.ts:3 return sum * 1.2;run_tests(apps/web)38 sTests 48 passed (48)
Time 37.9 sSample agent
Push the change to fix/checkout-rounding and open a pull request?
Approved
Sample agent
Done. The total now rounds to cents after tax, and all 48 tests in apps/web pass. The pull request is open for review.
You
Update the lockfile and fix what breaks
Sample agent
Three packages have new minor releases. I will update them one at a time and run the suite after each.
outdated()1.1 sdate-lib 3.2.0 → 3.4.1
http-kit 8.0.2 → 8.1.0
ui-icons 1.9.0 → 1.12.0install(date-lib@3.4.1)6.2 slockfile updated, 1 package changedrun_tests(--changed)52 sTests 211 passed (211)Sample agent
Commit the three updates to deps/2026-09 and open a pull request?
Approved
Sample agent
Updated three packages. One test needed a new date format, which I changed; the other 211 passed as they were.
You
Explain why main is red
Sample agent
The last merge changed a shared type. I will compare the two runs.
list_runs(main, 2)0.3 s#4190 failed 3 min ago
#4189 passed 41 min agodiff_runs(4189, 4190)0.8 snew failure: api/orders.test.ts
Order.total is string, expected numberSample agent
Open an issue for the author of the merge with this explanation?
Approved
Sample agent
Main is red because a merge 40 minutes ago made Order.total a string. One test in api/orders expects a number. Reverting that line or parsing it fixes the build.
Live, from the library itself, at full size: scroll, hover, navigate away and back. The demo is never gated.
Variants · each row writes the attribute the prop writes; the section's own CSS answers
JavaScript of its own
0 B
Uses Reveal and the shared runtime (1.9 kB raw, once per site) and CopyButton and PromptInputSubmitOnEnter. With those included: 4.1 kB raw.
With tasks: 314 B raw of its own on Reveal and the runtime and CopyButton and PromptInputSubmitOnEnter, measured on its own fixture. The default above ships none of it.
CSS 32.6 kB raw including the base tokens. Measured from a production build, and again in CI for every change that can move it.
Browser support
Chrome 116 · not Firefox · Safari 18; needs has, display-animation, clipboard. Elsewhere: without :has() the chat plays once and cannot be held; where display does not animate (Firefox) the answer still types in turn through the font-size in the same keyframes; it ends on the whole conversation either way.
The demo above is never gated; the source is. With a key in components.json (see account), the registry serves it like any other item:
npx shadcn@latest add @moonarc-pro/chat-demo---
import ChatDemo from '@/components/moonarc/pro/ChatDemo.astro';
import chats from '../content/chats.json';
---
<!-- /demo/?task=update-deps shows that conversation; the first is the default, and what renders without JavaScript -->
<ChatDemo
level={1}
headline="Watch the agent work"
names={{ user: 'You', agent: 'Agent' }}
chats={chats}
action="/demo/"
/>Without a key the registry answers 401 with an upgrade message. Everything this section composes is free: reveal, spinner, typewriter, copy-button, prompt-input.
| Prop | Type | Default | Description |
|---|---|---|---|
chats | { slug, prompt, reply, calls, ask, answer }[] | none | The conversations, up to six. prompt: the reader's message. reply: the agent's first words. calls: one to four { name, args?, took?, result }, each folded into a <details> with its result. ask: the request for approval. answer: the final answer, typed, with a copy button. slug keys it in the address. |
active | string | none | The slug of the conversation shown by default; the first unless set. |
param | string | 'task' | The query parameter that picks a conversation (/demo/?task=refactor). With more than one conversation this adds a small internal script, measured on its own; an unknown slug keeps the default. |
names | { user?, agent? } | You · Agent | Who is speaking: every message is a list item that starts with its speaker's name for screen readers, and the agent's name is shown in the chat's bar. |
mark | ImageMetadata | string | none | The agent's mark beside its name. Unset, its initial on the accent. |
headline | string | none | The headline above the chat. The headline slot wins over it. |
eyebrow | string | none | Small label above the headline. The eyebrow slot wins over it. |
copy | string | none | The paragraph under the headline. The copy slot wins over it. |
labels | { log?, working?, running?, done?, approve?, approved?, copy?, copied?, placeholder?, prompt?, send? } | Conversation · Working · Running · Done · Approve · Approved · Copy the answer · Copied · Ask a follow-up | The words the chat prints around the conversation: the list's name, the working line, the status, the approve before and after it is pressed, the copy button's name (it says what it copies) and its done face, and the prompt box's placeholder, name and send button. |
pauseLabel | string | 'Pause' | The pause control's label: a checkbox drawn as a small button, checked while the chat is held. |
input | boolean | true | The prompt box at the foot. |
action | string | '/demo/' | Where the foot's form goes: a demo page by default. |
method | 'get' | 'post' | 'get' | The foot's form method. |
name | string | none | The foot's textarea name. Unset, it has none and the form sends no text. |
level | 1 | 2 | 2 | Heading level of the headline: 2 under the page's h1, 1 when the section opens the page. |
tone | 'surface' | 'muted' | 'ink' | 'brand' | unset (paints nothing) | The ground: the page surface, a muted band, an always-dark ink panel, or a deep brand panel. With theme.css the chat's panel takes the tone's card; on brand what is live is marked in the text colour. |
density | 'compact' | 'cozy' | 'roomy' | 'cozy' | Block padding and the gap above the chat ×0.8 / ×1 / ×1.25 through --ma-density. |
The section reads the theme contract (shadcn's variable names with the base tokens as fallback), so a pasted theme or one --ma-brand recolours it. Its class names are the override surface and stay stable; a named slot replaces the matching prop with your own markup.
.ma-chat-demo.ma-chat-demo__agent.ma-chat-demo__answer.ma-chat-demo__approve.ma-chat-demo__approved.ma-chat-demo__args.ma-chat-demo__ask.ma-chat-demo__ask-text.ma-chat-demo__bar.ma-chat-demo__bars.ma-chat-demo__beat.ma-chat-demo__bubble.ma-chat-demo__call.ma-chat-demo__calls.ma-chat-demo__chat.ma-chat-demo__chev.ma-chat-demo__copy.ma-chat-demo__copy-wrap.ma-chat-demo__cue.ma-chat-demo__decision.ma-chat-demo__details.ma-chat-demo__done.ma-chat-demo__eyebrow.ma-chat-demo__fn.ma-chat-demo__foot.ma-chat-demo__head.ma-chat-demo__headline.ma-chat-demo__inner.ma-chat-demo__log.ma-chat-demo__mark.ma-chat-demo__msg.ma-chat-demo__out.ma-chat-demo__panel.ma-chat-demo__pause.ma-chat-demo__pause-control.ma-chat-demo__pause-icon.ma-chat-demo__pip.ma-chat-demo__play.ma-chat-demo__room.ma-chat-demo__running.ma-chat-demo__spinner.ma-chat-demo__stack.ma-chat-demo__status.ma-chat-demo__still.ma-chat-demo__summary.ma-chat-demo__text.ma-chat-demo__took.ma-chat-demo__typed.ma-chat-demo__working.ma-in.ma-type__char
slot="eyebrow"slot="headline"slot="copy"
The whole conversation at once: every message there, the calls folded, the approval given, the answer whole and its copy button shown. Nothing travels and the Pause control is not drawn.
ClientRouterCSS only; the copy button, the prompt box's Enter and the conversation picker rebind through the shared runtime. A new page replays the chat when it comes into view.
Replaces: Chat demo (Vercel AI Elements) · AI chat mockup (Magic UI). See the migration table.
npx astro add moonarcA button that copies text to the clipboard: the icon turns into a check, the label says "Copied", and both come back after the ambient duration.
grows with the text · Enter sends · ⇧Enter breaks the line
The AI prompt box: a form with a textarea that grows with its content through field-sizing, a model select, an attachment button that is a real file input, and a send button that dims until something is typed and spins while the form carries data-pending.
Types a string one character at a time in any font, with a caret that stays solid while typing and blinks once idle.
Sample run
An imaginary agent on an imaginary repository. The lanes fill in order; under reduced motion, or without JavaScript, they are filled from the start.
Task Fix the failing checkout test in apps/web
Done
read_log(job 4182)done0.4 sgrep("toBe(19.99)" apps/web)done0.2 srun_tests(apps/web)done38 sexport function total(items: Item[]) { const sum = items.reduce(add, 0); return sum * 1.2; const gross = sum * 1.2; return Math.round(gross * 100) / 100;}Maya Kim
Ready to merge. The suite passes and the change is two lines.
Approved
Checkout totals round to cents
An AI agent's run, replayed in the page: the task at the top, then four lanes that fill in order (the plan ticks off, tool calls spin and finish with their time, a diff writes in line by line, a person is asked to approve and the approve reads Approved), then the result with its numbers.
A hero whose entry point is a prompt box: a small label, a short headline, a lead, a large PromptInput with Enter to send, a status line and example tasks.
Also: view transitions · how costs are measured · browser support · accessibility policy · five-minute setup · MCP for agents · this page as Markdown