FORK

Messages

Text, widgets, attachments, and input patterns — chat and terminal side by side

Message Anatomy

Ironclad rule — applies to every message in both chat and terminal, regardless of sender.

1.
Attachments — 0..N media (horizontal carousel) and/or files (vertical list)
optional
Folds — 0..N tool activity (metadata between attachments and text, not a content slot)
optional
2.
Text — markdown or plain text body
optional
3.
Widget — 0..1 rich card: link preview, changeset, activity, interactive card, artifact, steps, or options
optional, max 1
Widget types
  • .chat-link-preview/Link preview card (icon, title, description, domain)
  • .term-changeset/Commit summary (hash, message, files)
  • .term-activity/Activity / progress card (title, bar, badge)
  • .chat-widget/Interactive preview — map, dashboard, poll
  • .attach-artifact--lg/Large artifact pill (created app, deployed project)
  • .term-steps/Step progress list (pending → active → done)
  • .term-options/Option picker (numbered choices)
Rules
  • Order is fixed: attachments → text → widget. Top to bottom. Never rearranged.
  • Attachments are always above the text. Media thumbnails scroll horizontally. File pills stack vertically (max 320px). Media first, file pills after.
  • Text is markdown (rendered as prose) or plain text. One text block per message. Large code fences from markdown may render as standalone .term-block cards outside the bubble for readability — they are still text content and do not consume the widget slot. A message can have both a code block and a widget.
  • Widget is at most one per message. Any type from the widget list above. Placed below text. An artifact and a changeset cannot coexist in one message — both are widgets, split into two messages.
  • Artifact class naming: artifact pills reuse .chat-attach-files / .attach-artifact--lg CSS classes for pill styling, but in the data model they are widgets (widget: .artifact(...), NOT attachments[]). Render artifacts in the widget slot (after text), not the attachment slot (before text). The CSS class name is a styling convenience — it does not reflect data model position.
  • Minimum content: a message must have at least one of the three parts. Empty messages are invalid.
  • Media bubble modifier: .chat-msg-bubble--media is used for both media-only and media+caption cases. Without text: image fills the entire bubble edge-to-edge, 0 padding. With caption text: image edge-to-edge at top, caption below with padding. The CSS distinguishes via child presence (:has(p) / :last-child) — native platforms must branch explicitly based on whether a caption exists.
  • Chat bubble content: for all senders, attachments + text are inside the bubble. Widget is always a sibling below the bubble. The bubble is the content container — its visual style changes per sender (transparent for agent, systemFill for peer, chatBubbleUserBackground for outgoing), but the content structure is identical.
  • Chat agent: bubble has transparent background and minimal padding (space2 vertical, 0 horizontal). Media scrolls full width (agent bubble is 100% wide so carousel bleeds to edges). The bubble view exists for layout but is visually invisible. Agent messages use 100% width (not 85%) — the only sender type exempt from the 85% max. On Web, the CSS reference flattens agent attachments as siblings for convenience — the visual result is identical since the bubble is transparent. Native implementations should keep attachments inside the bubble container.
  • Chat peer/user: attachments + text inside bubble. Widget as sibling below. Outgoing (user) widgets right-align with the bubble; incoming (peer) widgets left-align. Same content structure as agent — only bubble styling differs.
  • Bubble width: determined by the widest child — media, text, or file pills — capped at max bubble width (85% for peer/user, 100% for agent). The bubble grows to fit its widest content, never wider than the cap. Widgets are always outside the bubble and size independently — but align to the same edge as the bubble (right for outgoing, left for incoming). Widgets without an explicit max-width (changeset, activity, steps, options) should not exceed the max bubble width for that sender type (85% for peer/user, 100% for agent) — this applies whether or not a bubble is present in the message.
  • Terminal: inline flow — attach-row → text lines/prose → card.
  • Folds (tool activity): inline metadata — not attachments, text, or widgets. Folds sit between attachments and text: the full chat order is attachments → folds → text → widget. Folds stack tight (0 gap between folds, 0 gap from last fold to text). Unlimited count. They don't occupy any of the three anatomy slots.
  • Terminal anatomy differs from chat. Terminal has no "message" boundary — output is a continuous stream. Fixed order: prompt → folds → steps → prose → changeset / activity → artifact. Folds and steps appear before prose because terminal output is chronological: the agent reads files (fold), plans work (steps), then writes output (prose) and commits (changeset / activity). Chat anatomy (attachments → text → widget) applies to discrete messages — the "max 1 widget per message" rule applies to chat only. Terminal flow applies to continuous output blocks between prompts — a single terminal response can contain both a changeset and an artifact because they are separate output events in the stream, not coexisting widgets in one message.
  • Bubble tails (standalone): outgoing = tail at bottom-right (chatTailRadius). Peer = tail at top-left. Agent = no tail (transparent bubble, no visible corners). Tail is a single corner set to chatTailRadius (6px) while all other corners use chatBubbleRadius (18px).
  • Capsule variant: single-line peer or user messages with no attachments can use capsule shape (chatCapsuleRadius, 24px). Messages with attachments inside the bubble are multi-content — always use regular bubble. Text-only + widget (widget is outside) can use capsule if the text fits on one line. Not limited to grouped messages. If text exceeds max bubble width, fall back to regular bubble — never truncate. Capsule eligibility is a code decision: the rendering layer must measure text width before rendering and choose capsule vs regular bubble — CSS cannot auto-switch. Capsule tails follow the same rules as regular bubbles (outgoing = bottom-right, peer = top-left). Agent messages should not use capsule — the transparent bubble makes the capsule radius invisible, and the only effect (white-space: nowrap) provides no value when agent width is 100%.
  • Agent grouping: agent messages never group. Each agent message is standalone with its own name label and potential folds/widgets. Grouping applies only to user and peer messages (same sender, <60s apart). First in group: shows name label (no timestamp). Subsequent: name and timestamp hidden. Last in group: timestamp restored. Avatar: layout-preserved but visually hidden (visibility: hidden) on grouped messages — maintains horizontal alignment with the first message's visible avatar.
  • Outgoing widgets: widgets outside the bubble always use their own default styling — they do NOT inherit outgoing blue color scheme. Each widget type defines its own background (tertiarySystemFill for changeset/activity/options, secondarySystemGroupedBackground for link preview/widget card). Only content inside the bubble adapts to outgoing colors.
Chat
Agent
attachments
Here's the dashboard with updated colors.
2:00 PM
Terminal
anatomy
$build dashboard

Dashboard deployed with updated colors.

DashboardReady
live
Platform notes
  • iOS Message model: attachments: [Attachment], text: AttributedString?, widget: Widget?. Render in VStack in this exact order. Agent bubble: UIView with .clear background — do not remove the view, it anchors text layout. VoiceOver should not announce it as a separate container.
  • Android Message data class: attachments: List<Attachment>, text: AnnotatedString?, widget: Widget?. Column composable, top to bottom.
  • Web Message container: flex column. Slots render conditionally. .chat-msg-attachments.chat-msg-bubble.chat-widget / .chat-link-preview / .term-changeset. Outgoing: align-items: flex-end — all children (bubble + widgets) right-align. The CSS reference uses .chat-demo as a wrapper for chat context overrides — native implementations apply these values directly (see table below).
  • All This order is non-negotiable. Every message, every sender, every context. Violations break scanning patterns.
  • All 85% max-width applies to the message row container (not the bubble). All children — bubble and widgets — are constrained by the row. Agent messages force width: 100% (not just max-width — the row does not shrink to content). Peer/user rows are max-width: 85% and shrink to fit their widest child.
  • All Incoming bubble cascade: all incoming messages (agent + peer) start with transparent background and minimal padding (space2 / 0). Peer then overrides to systemFill + standard padding (space12 / space16). On native, apply incoming defaults first, then peer overrides — do not treat agent and peer as independent branches. If the code checks "incoming" and returns early, peer never gets its fill.
  • All Outgoing widget color isolation: apply chatBubbleUserBackground and chatBubbleUserText to the bubble view only, never to the message container. Widgets are siblings of the bubble and must retain their own default color scheme (each widget type defines its own background). If outgoing color is applied at the container level, widgets inherit white text on non-blue backgrounds.
  • All Terminal components in chat context use different font and radius values. The CSS reference uses .chat-demo as the context selector for these overrides — this is a reference-page artifact. In production, native platforms must apply these overrides based on their own context detection (e.g. a chat view controller vs terminal view controller). Web production code should use a comparable wrapper class. Values:
ComponentTerminalChat
Fold summaryfontMono / typeCodefontBody / caption1
StepsfontMono / typeCodefontBody / caption1
OptionsfontMono / typeCodefontBody / caption1
ChangesetfontMono / typeCodefontBody / caption1
Activity cardfontMono / typeCodefontBody / caption1
Pill (attach-artifact)fontMono / r0fontBody / r10
Changeset radiusradius12radius14
Activity radiusradius12radius14
Standalone code block (.term-block)radius10radius14
Inline pre (inside prose in bubble)radius14 (incoming/peer — concentric with bubble) / radius6 (outgoing — tighter inset)
Option radius0radius10

Attachment Rules

How media and file attachments render inside messages — depends on count, text presence, and sender type.

Chat
JK
Julia Kim

Two screenshots.

Agent
Config files attached.
Terminal
attachments
$fork deploy
config.yaml
✓ Deployed
Single Media
Paddingnone — image edge-to-edge
Bubble widthdriven by image aspect ratio
Max width280px (layout constant, not a theme token — hardcode on all platforms), capped by 85% bubble max
With texttext below image, space4 top + space12 horizontal + 0 bottom (bubble's own padding-bottom: space8 provides bottom clearance). Caption is subordinate to image — narrower horizontal padding than bubble text.
Without textimage IS the bubble
Multiple Media
Layouthorizontal scroll
Thumb width70% of bubble (peek 30% of next)
Paddingspace8 all sides (uniform padding around carousel)
Gapspace8 between thumbs (inside bubble). Agent flat carousel: space4 (tighter fit at full width).
Default ratio4:3 when unknown, override per-item when available
Min height120px
Max height400px (clip with expand affordance)
Scrollbarhidden (all platforms)
File Pills (no preview)
Layoutvertical list
Container max320px
Pill widthfit-content (agent flat) / stretch (inside bubble)
Pill radiusradius10 (chat) / radius20 (large artifact) / 0 (terminal)
FontfontBody (chat) / fontMono (terminal)
Agent (flat)
Media scrollfull chat width (bleed)
Filesfit-content (flat); full width inside bubble
Paddingnone — not inside bubble
Rules
  • Single media, no text: image fills the bubble. No padding. Bubble shape = image shape (clipped by border-radius). This is the "photo message" pattern.
  • Single media + text: image still edge-to-edge at top. Text below with horizontal padding. Bubble width driven by media width (280px default), capped at 85%.
  • Multiple media: horizontal scroll. Each thumb is 70% of bubble width so 30% of the next item peeks — the visual cue that scrolling is available.
  • File pills: always vertical list, below media (if any), above text. Container max 320px. Inside bubble: pills stretch to bubble width. Agent (flat): pills shrink to content. Rounded (radius10).
  • Agent: no bubble — media and files are flat siblings. Media carousel bleeds to full chat width. Pills and artifacts shrink to content width. Media-only agent message (no text, no widget) = just the media, no visible bubble wrapper.
  • Mixed media + files: media carousel first, file list second, text third. Always this order (anatomy rule).
  • Terminal geometry: terminal pills, thumbs, and option rows are square-cornered (0 radius). Chat uses rounded corners (radius10 for pills, radius20 for large artifacts) concentric with bubbles. This split is intentional — terminal surfaces are utilitarian and grid-aligned; chat surfaces are organic and conversational.
Platform notes
  • iOS Single media: AsyncImage with .clipShape() matching bubble shape. Multiple: UICollectionView with paging-like scroll (70% width cells).
  • Android Single media: AsyncImage with Modifier.clip(). Multiple: LazyRow with Arrangement.spacedBy(4.dp), item width = 0.7f * maxWidth.
  • Web Single: width: 100% inside bubble. Multiple: flex-shrink: 0; width: 70% per thumb, overflow-x: auto on container.
  • All Peek pattern is critical — if the user can't see the next item, they won't know to scroll. 70% is the sweet spot: enough to show the current image, enough peek to hint at more.

Messages

Chat
Today, 2:14 PM
Agent
Here's what I found. The results look promising — want me to dig deeper?
2:14 PM
Yes, can you also check the latest data from the API? I want to compare both sources.
2:15 PM
JK
Julia Kim
Hey! The new token architecture looks really clean.
2:16 PM
Thanks!
The warm neutrals are great
Let's sync tomorrow
2:16 PM
JK
Julia Kim
The spacing system feels really solid.
JK
The Braun influence is spot on
JK
Love it
2:17 PM
Agent
On it. Pulling from both endpoints now.
2:17 PM
Terminal
session — multi-user
~/project$fork info
FORK Design System v1.0
Tokens: 127 active
@julia~/project$fork list --tokens spacing
space4 4px
space8 8px
space16 16px
space24 24px
4 tokens shown
~/project$fork audit
✓ Audit complete — 0 errors
@julia~/project$looks good!
$
Chat Bubble
FontFigtree 17px (callout)
Paddingspace12 × space16 (12px × 16px)
RadiuschatBubbleRadius (18px)
TailchatTailRadius (6px)
Max width85% peer/user · 100% agent — of message list width (after safe area insets and list padding)
Outgoing bgchatBubbleUserBackground — #1478CC (both themes)
Peer bgsystemFill
Agenttransparent bg, 100% width, padding space2 × 0, no avatar, no tail
Avatar32px · avatar--sm (peers only, see Components)
Terminal Output
FontIBM Plex Mono · typeCode (14px)
Line height1.7
stdoutsecondaryLabel
MutedtertiaryLabel
Prompt symboltint
Peer usersystemBlue
Grouping
TriggerSame sender, <60s (user + peer only — agent never groups)
Spacing4px between grouped
First in groupname visible, timestamp hidden. Avatar visible.
Subsequentname + timestamp hidden. Avatar: layout-preserved but visually hidden (visibility: hidden) — maintains horizontal alignment.
Last in groupname hidden, timestamp restored. Avatar: layout-preserved but visually hidden.
Outgoing — firstTL: bubble (18) · TR: bubble (18) · BL: bubble (18) · BR: tail (6). Same as standalone.
Outgoing — middleTL: bubble (18) · TR: tail (6) · BL: bubble (18) · BR: tail (6). Both right corners are tail.
Outgoing — lastTL: bubble (18) · TR: tail (6) · BL: bubble (18) · BR: bubble (18). Tail up = TR only.
Peer — firstTL: bubble (18) · TR: bubble (18) · BL: tail (6) · BR: bubble (18). Tail shifts down from TL.
Peer — middleTL: tail (6) · TR: bubble (18) · BL: tail (6) · BR: bubble (18). Both left corners are tail.
Peer — lastTL: tail (6) · TR: bubble (18) · BL: bubble (18) · BR: bubble (18). Same as standalone.
Capsule Variant
TriggerSingle-line peer or user message with no attachments. Messages with attachments in the bubble are multi-content — always regular bubble. Agent should not use capsule — transparent bubble makes radius invisible.
RadiuschatCapsuleRadius (24px)
TailSame corner rules as regular bubble but with capsuleRadius (24px) instead of bubbleRadius (18px) for non-tail corners
Grouped capsuleSubstitute chatCapsuleRadius (24) for chatBubbleRadius (18) in every non-tail corner from the Grouping spec. Tail corners remain chatTailRadius (6). Example — outgoing capsule middle: TL=24, TR=6, BL=24, BR=6.
OverflowIf text exceeds max bubble width → fall back to regular bubble. Never truncate. This is a code decision: measure text width before rendering, choose capsule vs regular. CSS cannot auto-switch.
Message Spacing
Default gapspace16 (16px)
Grouped gapspace4 (4px)
System msgspace16 (same as default)
Sender changespace16 (no extra space)
Between folds0 (folds stack tight, each has space4 internal padding)
Fold → bubble0 (last fold flows directly into bubble text)
Bubble → widgetspace4 (4px) when widget follows a bubble. space8 (8px) when widget has no preceding bubble (e.g. attachment-only message with widget)
Fold → widgetspace8 (8px) — same as no-bubble case. When a message has folds but no text/bubble, treat the widget as having no preceding bubble.
System Message
FontfontBody · caption1 (13px)
ColorsecondaryLabel
Alignmentcenter
Backgroundtransparent
Paddingspace4 × space16
Max width100% (not 85%)
Contenttimestamps, membership events, session boundaries — never user-generated
When to use
  • .chat-msg--incoming/Agent = transparent bubble (no fill, space2 / 0 padding — bubble view exists for text layout but is visually invisible). Peer = filled bubble (systemFill). The visual distinction tells users AI vs human.
  • .chat-msg--outgoing/User messages. Blue bubble, right-aligned. Widgets right-align with bubble. Capsule for single-line.
  • .term-prompt-user/Peer username prefix in terminal. Uses systemBlue — a distinct token from chatBubbleUserBackground. Both signal human presence (as opposed to agent/system) but are intentionally different colors: outgoing blue for "me", systemBlue for "peer".
  • Standalone tails: outgoing = bottom-right corner at chatTailRadius (6px). Peer = top-left corner. Agent = no tail (transparent bubble).
  • Grouped tails: tails face inside the group. Outgoing: first = same as standalone (BR=tail). Middle = both right corners are tail (TR=tail, BR=tail). Last = tail moves up (TR=tail, BR→full). Peer: first ≠ standalone — tail moves from TL to BL (TL→full, BL=tail). Middle = both left corners are tail (TL=tail, BL=tail). Last = same as standalone (TL=tail, BL→full). All non-tail corners stay at chatBubbleRadius (or chatCapsuleRadius for capsules). See Grouping spec card for complete per-corner values.
Platform notes
  • iOS Chat: dedicated outgoing-bubble color (not UIColor.systemBlue) — dark mode needs a darker blue for white-text contrast. Do NOT use tint. Agent bubble: UIView with .clear background — keep the view for layout, remove it from VoiceOver. Terminal peer: same mono font, systemBlue for username.
  • Android Chat: per-corner radii via RoundedCornerShape. Terminal: AnnotatedString with colored spans for usernames.
  • Web Chat: flex column with align-items: flex-end on outgoing messages — bubble and widgets both right-align. Terminal: .term-prompt-user is inline before path.
  • All Any user can send any content type — prose, media, widgets, code. The bubble/prompt wrapper changes, not the content inside.
  • All Terminal line-height is 1.7 (not 1.6 from the type scale) — taller leading keeps dense monospace output scannable. This is an intentional terminal-specific override.
  • All Capsule eligibility is a code decision. Requirements: (1) peer or user sender, (2) text-only content — no attachments inside the bubble, (3) text fits within max bubble width on a single line. If any condition fails, fall back to regular bubble (chatBubbleRadius). A text-only message with a widget (outside the bubble) can use capsule. CSS white-space: nowrap prevents wrapping but cannot auto-switch shape — the rendering layer must branch.
  • All Media bubble modifier (.chat-msg-bubble--media) covers two cases: media-only (0 padding) and media+caption (padding below image for text). On Web, CSS :has() selectors handle both. On native, explicitly branch based on whether a caption exists — this is not a single layout path.
  • All Media carousel gap: space4 for agent flat carousel (full-width, tighter fit), space8 for peer/user bubbled scroll (spaced for peek pattern). This is an intentional split — agent carousel bleeds to edges and needs compact spacing.

Rich Text

Chat
Agent

Here's a summary:

  • Revenue is up 12% QoQ
  • Retention improved from 68% to 74%
  • API latency dropped to 42ms
The biggest driver was the onboarding redesign.
3:10 PM

My notes on the spacing system:

  1. Related items → space4space8
  2. Unrelated → space24space48
3:11 PM
JK
Julia Kim

Agreed! Also worth noting:

  • The density budget concept is great
  • Min card padding space24
3:12 PM
Agent

When a rounded rectangle is nested inside another, the inner radius must be smaller than the outer

Terminal
mono prose
$explain the spacing system

Spacing Tokens

The spacing system uses a geometric scale based on multiples of 4px.

Scale

  • space4 — Tight grouping
  • space8 — Default gap
  • space16 — Section padding
  • space24 — Group separation
Whitespace is content — generous padding signals confidence.
streaming
$what is concentric corner radius?

When a rounded rectangle is nested inside another, the inner radius must be smaller than the outer by the distance between them.

Chat Prose
FontFigtree 17px (callout)
Inside bubbletransparent bg, 0 padding reset
Any senderagent, user, peer — all can send prose
Streaming.is-streaming → solid tint cursor via ::after
Terminal Mono Prose
FontIBM Plex Mono · typeCode (14px)
BodysecondaryLabel
Strongweight 600 · label
H1700 · uppercase · 0.08em tracking
Streaming.is-streaming → solid tint cursor via ::after
When to use
  • .prose/Rendered markdown inside chat bubbles — lists, blockquotes, bold, inline code. Resets container padding/background to transparent.
  • .term-prose/Mono prose in terminal — headings via weight + uppercase, body via secondaryLabel. Hierarchy through weight, not typeface.
  • .is-streaming/Active generation — appends a tint block cursor via ::after. Remove class when stream completes.
  • Any sender can send rich text — agent, peer, user. Same prose styles in all contexts.
Platform notes
  • iOS Chat: AttributedString(markdown:) with Figtree. Terminal: IBM Plex Mono throughout, bold=600.
  • Android Chat: AnnotatedString with markdown parser, Satoshi body font. Terminal: IBM Plex Mono throughout, bold=600.
  • Web Chat: .prose resets inside bubble. Terminal: .term-prose inherits var(--fontMono).
  • All Chat = mixed fonts (Figtree + Plex code). Terminal = mono only. Hierarchy via weight, not typeface.

Code & Blocks

Chat
Agent

Here's the resolver:

function resolveToken(name) {
  const v = getComputedStyle(root)
    .getPropertyValue('--' + name);
  return v || 'undefined';
}
Agent
Here's the diff:
@@ styles.css @@
- --tintOrange: #E86420;
+ --tintOrange: #F07030;
7:16 PM

What about this?

const tint = resolveToken('tint');
Terminal
syntax + blocks
// Token resolver
function resolveToken(name) {
const v = getComputedStyle(root).getPropertyValue('--' + name);
return v || 'undefined';
}
~/project$fork diff styles.css
@@ -93,6 +93,7 @@ :root {
--tint: #E86420;
- --tintOrange: #E86420;
+ --tintOrange: #F07030;
+ --tintSunrise: #FF8C42;
2 additions, 1 deletion
ANSI COLORS (TERMINAL ONLY)
0Black
1Red
2Green
3Yellow
4Blue
5Magenta
6Cyan
7White
Syntax → System Colors
.term-kwsystemPink
.term-strsystemGreen
.term-fnsystemBlue
.term-commenttertiaryLabel
Standalone Block (.term-block)
Positiontext content (rendered from markdown code fences). Visually outside bubble for readability, but semantically part of the text slot — does NOT consume the widget slot. A message can have code blocks AND a widget.
BackgroundtertiarySystemBackground
Border1px solid border
Radiusradius14 (chat, all senders) / radius10 (terminal)
Inline Code Block (pre inside prose)
Positioninside bubble, within .prose container
BackgroundtertiarySystemBackground (incoming/peer) / rgba(0,0,0,0.38) (outgoing — dark inset)
Border1px solid border (incoming/peer) / none (outgoing)
Radiusradius14 (incoming/peer — concentric with bubble) / radius6 (outgoing — tighter inset inside blue bubble)
Diff Colors
.term-diff-addsystemGreen + 6% fill
.term-diff-delsystemRed + 6% fill
.term-diff-hunksystemBlue · bold
Outgoing Syntax (on blue)
Surfacergba(0,0,0,0.38) — dark inset inside blue bubble
.term-kw#E0A0FF (muted pink)
.term-fn#7DD3FC (muted blue)
.term-str#86EFAC (muted green)
.term-param#FDE68A (muted yellow)
.term-oprgba(255,255,255,0.7)
.term-punct / .term-commentrgba(255,255,255,0.45)
ThemeFixed values — do not vary by light/dark. The dark inset surface provides consistent contrast in both themes.
ANSI → System Tokens
0 BlackansiBlack
1 RedsystemRed
2 GreensystemGreen
3 YellowsystemYellow
4 BluesystemBlue
5 MagentasystemPink
6 CyantintTeal
7 WhiteansiWhite
When to use
  • .term-kw / .term-str / .term-fn/Syntax highlighting — reuses system colors (pink, green, blue). Same classes in chat pre code and terminal .term-line.
  • .term-block/Fenced container for diffs and multi-line code output. tertiarySystemBackground + border. Radius: radius14 in chat (all senders), radius10 in terminal.
  • .term-diff-add / .term-diff-del / .term-diff-hunk/Diff lines — green add, red delete, blue hunk header. Semantic color + 6% fill background.
  • .term-ansi-*/ANSI color classes — terminal only. Map standard 8 + 8 bright colors to system palette.
Platform notes
  • Web Chat: syntax spans inside .prose pre code. Terminal: syntax spans inside .term-line. Same .term-* classes in both.
  • iOS Syntax: NSAttributedString with color spans. Diff: tinted UIColor backgrounds at 6% opacity.
  • Android Syntax: AnnotatedString with SpanStyle(color = ...). Diff: Modifier.background() with 6% alpha.
  • All Code is always Plex Mono regardless of context. Zero new tokens for syntax — all existing system colors. ANSI is terminal-only.

Status & Thinking

Chat
JK
Julia Kim
Agent
Terminal
status
$fork build
ℹ Compiling 42 modules...
⚠ Deprecated token --oldFill
✕ Build failed: unresolved token
$fork build --fix
✓ Build complete — 0 errors
$fork deploy
Uploading
73%
Badges: Success Error Warning Info
thinking
$explain concentric corner radius
Chat Indicators
Peer typing3 dots · 6px · bounce 1.4s
Agent thinkingblock cursor · tint · 1s step blink
Terminal Status
ErrorsystemRed
Warningwarning
Successsuccess
InfosystemBlue
Terminal Thinking
Cursorblock · tint · 1s step blink
Progress bar4px · tint fill · 0.4s easeFluent
When to use
  • .chat-typing/Peer typing — standard 3-dot bounce. Show when a human user is composing.
  • .chat-thinking/Agent thinking — blinking block cursor (same as terminal). Replaced by output when response begins.
  • .term-thinking/Terminal thinking — blinking block cursor. Same cursor component in both contexts.
  • .term-line--stderr / --success / --warning / --info/Semantic status lines. Color = meaning: systemRed=error, success=success, warning=warning, systemBlue=info.
  • .term-progress/Inline progress bar for long-running operations. Title + track + percentage.
Platform notes
  • iOS Chat peer typing: UIView.animate with bounce. Agent thinking: CABasicAnimation for cursor blink (same as terminal).
  • Android Chat peer: InfiniteTransition with staggered delays. Agent thinking: animateFloatAsState for cursor opacity.
  • Web Chat peer: @keyframes chat-typing. Agent + terminal thinking: shared .cursor--block with CSS steps(2) blink, respects prefers-reduced-motion.
  • All Two distinct indicators: peer dots (human composing) and block cursor (AI/terminal thinking). Cursor is shared between chat agent and terminal.

Tool Activity

Chat
Agent
Read styles.css done
5254 lines
Edit styles.css:95 done
+ --tintCoral: #E05248;
Added --tintCoral to both themes.
3:10 PM
JK
Julia Kim
Lint styles.css 2 warnings
⚠ Unused token --oldFill
⚠ Unused token --legacyBg
Found some unused tokens
3:11 PM
Test 42 specs passed
✓ All 42 tests passed
Tests are green
3:12 PM
Terminal
tool activity
$add a coral tint variant
Read styles.css done
5254 lines
Edit styles.css:95 done
+ --tintCoral: #E05248;

Added --tintCoral to both themes.

Fold
Element<details class="term-fold">
Summary fontfontMono · typeCode (terminal) / fontBody · caption1 (chat)
Body fontfontMono · typeCode (always — code output)
Min-height32px (full-width row — tap target met by width)
Defaultcollapsed
Body indent2ch
Fold gap0 — folds stack tight (each has space4 internal padding top/bottom)
Fold → text0 — last fold flows directly into bubble/prose (no extra spacing)
When to use
  • .term-fold/Collapsible block for tool calls, verbose output, logs. Collapsed by default — user reads response first, opens on demand.
  • .term-fold-tool/Tool name (Read, Edit, Scaffold). SecondaryLabel to recede from the target path.
  • .term-badge/Status badge right-aligned in fold summary. Success/error for quick scan without expanding.
  • Any sender can share tool results — agent, peer, or user. Same fold structure in all contexts.
Platform notes
  • iOS DisclosureGroup with custom label. Chevron: SF Symbol chevron.right with rotation animation.
  • Android AnimatedVisibility with custom header row. Chevron: animateFloatAsState rotation.
  • Web Native <details> for free accessibility + keyboard support. Chevron via CSS ::before rotation.
  • All Default collapsed. Any sender can share tool results. Body uses mono font for code output regardless of chat/terminal context.

Steps

Chat
Agent
Deploying:
Run tests
Build
Push
Health check
JK
Julia Kim
My review checklist
Read PR
Run locally
Contrast check — failed
My progress
Design
Implement
Ship
Terminal
steps
$deploy to production
Run tests
Build
Push
Health check
@julia$review PR #42
Read PR
Run locally
Contrast check — failed
Step States
FontfontMono · typeCode (terminal) / fontBody · caption1 (chat)
Pending○ tertiaryLabel
Active◉ tint / label
Done✓ success / secondaryLabel
Error✕ destructive
Max widthmax bubble width for that sender type (85% for peer/user, 100% for agent) — applies whether or not a bubble is present in the message
When to use
  • .term-steps/Multi-step process with visible progress. Each step transitions pending → active → done (or error).
  • .term-step--active/Currently executing. Tint icon + full-contrast text draw attention.
  • .term-step--pending/Not yet started. Tertiary color recedes.
  • Always show all steps — don't hide pending ones. Any sender can share step progress.
Platform notes
  • iOS VStack(alignment: .leading, spacing: 2). Icons: SF Symbols circle, circle.inset.filled, checkmark, xmark.
  • Android Column(verticalArrangement = Arrangement.spacedBy(2.dp)). Material icons for states.
  • Web Icons via CSS ::before — Unicode (○, ◉, ✓, ✕). No icon font needed.
  • All Steps update in-place as task progresses. Always show all steps — don't hide pending ones.

Options

Chat
Agent
What kind of project?
1Blank
2Landing page
3Dashboard
JK
Julia Kim
Which tint for the new feature?
ACoral — warm, energetic
BIndigo — calm, focused
Vote on release timing:
1Ship Monday
2Wait for QA
3Staged rollout
Terminal
options
$create a new project
Choose a template:
1Blank — empty project
2Landing page
3Dashboard
@julia$which tint?
ACoral — warm
BIndigo — calm
Option Item
FontfontMono · typeCode (terminal) / fontBody · caption1 (chat)
Min height44px
BackgroundtertiarySystemFill
Radiusradius10 (chat) / 0 (terminal)
Selected1px solid tint
CollapsedAfter selection: entire picker removed from the widget slot. Chosen option is rendered as plain text in a new outgoing message (regular bubble, not styled as an option row). The original message retains its text but loses the widget.
Max widthmax bubble width for that sender type (85% for peer/user, 100% for agent) — applies whether or not a bubble is present
Options Prompt
Element.term-options-prompt
Font (terminal)fontMono · typeCode
Font (chat)fontBody · footnote · 600 — only if options appear without a preceding bubble (rare). Normally the bubble text serves as the prompt and .term-options-prompt is omitted.
VisibilityTerminal: always present — terminal has no bubble, so the options prompt provides the question context. Chat: omitted when bubble text serves as prompt (typical); shown only when options widget appears without a preceding bubble.
When to use
  • .term-options/Numbered choice list. Present all options at once — never paginate.
  • .term-option--selected/User's selection — tint border. Only one selected at a time.
  • .term-option-key/Shortcut key (1, 2, A, B). Left-aligned, secondary color.
  • Any sender can present options — agent asks questions, peer proposes polls, user creates votes.
Platform notes
  • iOS VStack of Button elements. Selected: .border(tint, width: 1).
  • Android Column of Surface with onClick. Selected: BorderStroke(1.dp, tint).
  • Web Keyboard: number keys quick-select, arrow keys navigate, Enter confirms. Focus ring on :focus-visible.
  • All After selection, picker collapses and chosen option appears as regular text. Any sender can present options — agent, peer, or user.

Changeset

Chat
Agent
Done! Here's the commit:
a3f8c1d#4
Add dark theme tokens
styles.cssscripts.js
JK
Julia Kim
Pushed a fix
b7e2d09#5
Fix contrast ratio
styles.css
Terminal
changesets
$add dark mode
a3f8c1d#4
Add dark theme tokens
styles.cssscripts.js
@julia$fix contrast
b7e2d09#5
Fix contrast ratio
styles.css
Changeset
Radiusradius14 (chat) / radius12 (terminal)
Paddingspace12 × space16
BackgroundtertiarySystemFill
FontfontMono (terminal) / fontBody (chat)
Commit IDtint · caption2 · 600
Messagelabel · caption1 · 600
SequencetertiaryLabel · caption2 · right-aligned
FilessecondaryLabel · caption2
Max widthmax bubble width for that sender type (85% for peer/user, 100% for agent) — applies whether or not a bubble is present
When to use
  • .term-changeset/Commit summary card. Shows icon, short hash, sequence number, message, and file list.
  • .term-changeset-id/Short hash in tint — clickable to view full diff.
  • .term-changeset-seq/Sequence number (e.g. #4) — tertiaryLabel, right-aligned. Tracks order within session.
  • .term-changeset-icon/Commit type icon — use a full radial icon (gear/sun) for agent-generated commits, a simpler vertical icon for peer/user pushes. Icon is decorative; the semantic info is in the hash and message.
  • Any sender can share changesets — agent commits, peer pushes, user references.
Platform notes
  • iOS VStack with mono font throughout. Tap navigates to diff viewer.
  • Android Column inside Surface(shape = RoundedCornerShape(12.dp)). Mono font.
  • Web .term-changeset base radius12; .chat-demo .term-changeset overrides to radius14. Same component, context-aware radius.
  • All Terminal: mono font throughout. Chat: fontBody (caption1) for readability — only fold bodies retain mono. Radius14 in chat (concentric with bubble), radius12 in terminal.

Card

Chat
Agent
Live preview:
JK
Julia Kim
Meetup location
Here's the poll
Terminal
no equivalent
$show dashboard

Dashboard: analytics.fork.site

Status: live · 3 widgets active

Open in browser: analytics.fork.site

Terminal has no interactive preview card. Rich content renders as prose with links. For progress/status, use .term-activity (see Activity Card section).

Widget (Interactive Preview)
BackgroundsecondarySystemGroupedBackground
Border1px solid border
Radiusradius14
Max width320px
Preview height160px · tertiarySystemBackground
Footer paddingspace12
NamefontBody · caption1 · 600
MetafontMono · overline · tertiaryLabel
HovertranslateY(-1px) + shadowSmall
When to use
  • .chat-widget/Interactive preview card — dashboard, map, poll. One per message (widget slot).
  • .chat-widget-preview/160px visual area with tertiarySystemBackground. Hosts live content or placeholder icon.
  • .chat-widget-meta/Overline status — LIVE, TAP TO OPEN, count. Mono font, tertiaryLabel.
  • Any sender can share cards — agent presents results, peer shares locations, user posts polls.
Platform notes
  • iOS Card: UIView with clipsToBounds at radius14. Preview: embedded WKWebView or UIView snapshot.
  • Android Card: Surface(shape = RoundedCornerShape(14.dp)). Preview: AndroidView or composable snapshot.
  • Web .chat-widget with overflow: hidden + radius14. Hover: translateY(-1px) + shadowSmall.
  • All Tap opens full-screen view. Max width 320px. Chat-only component — terminal has no interactive preview card. Terminal renders rich content as prose with links; for progress/status use .term-activity (separate component, see Activity Card).

Activity Card

Chat
Agent
Building your site:
PortfolioInstalling...
35%
JK
Julia Kim
My deploy is done
API ServiceReady
live
Running tests
Test suite42 / 128
33%
Terminal
activity
$create landing page
Landing PageReady
live
@julia$deploy api
API ServiceReady
live
Activity Card
Radiusradius14 (chat) / radius12 (terminal)
Paddingspace12 × space16
BackgroundtertiarySystemFill
FontfontMono (terminal) / fontBody (chat)
Icon32px · radius6 · tint bg · onLight
Titlelabel · caption1 · 600
StatussecondaryLabel · caption1
Trackspace4 height · quaternarySystemFill · tint bar
Bar animation0.4s easeFluent (longer than default 0.25s — smooth progress increments)
Max widthmax bubble width for that sender type (85% for peer/user, 100% for agent) — applies whether or not a bubble is present
When to use
  • .term-activity/Progress card for long-running tasks — builds, deploys, installs. Shared component in chat and terminal.
  • .term-activity-bar/Tint-colored progress fill. Width set via inline style (JS-driven). Animates with easeFluent.
  • .term-badge/Status badge (live, error, etc.) replaces percentage label when task completes.
  • Any sender can share activity — agent builds, peer deploys, user runs tests.
Platform notes
  • iOS Card: HStack with icon + body + badge. Progress: ProgressView with custom tint style.
  • Android Card: Row composable. Progress: LinearProgressIndicator with animateFloatAsState.
  • Web .term-activity flex row. Bar width via inline style, transitions with easeFluent 0.4s.
  • All Terminal: mono font throughout. Chat: fontBody (caption1) for readability. Radius14 in chat (concentric with bubble), radius12 in terminal. Icon optional. Progress updates in-place — no re-render flicker.

Artifact

Large artifact pill for primary outputs — created apps, deployed projects. 56×56 icon, 72px pill.

Chat
Agent
Your dashboard is live.
2:24 PM
JK
Julia Kim
Check out my app
2:25 PM
Terminal
artifact
$deploy dashboard
✓ Deployed
@julia$share portfolio
Artifact Pill — Large
Modifier.attach-artifact--lg
Pill height72px
Icon56 × 56, radius14
SVG glyph24 × 24
Namesubheadline (16px), 600
Metacaption1 (13px)
Chat pill radiusradius20 pill, radius14 icon — padding is 8px, icon radius optically adjusted from strict concentric (12) to 14 for app-icon alignment
Terminal pill0 (square, per terminal geometry)
When to use
  • .attach-artifact--lg/Large artifact pill with 56×56 icon. Use when the agent creates an application, deploys a project, or generates a primary output artifact.
  • Not for: regular file references (config.yaml, schema.sql) — use default .attach-artifact.
  • Icon color: tint (default) for agent-created. Custom color via inline style for peer/external apps.
Platform notes
  • iOS Data model: message.widget = .artifact(name:, url:, badge:) — NOT in message.attachments. Icon shape: UISmoothCornerPath (continuous corner) at radius14 to match native app icon. Accepts UIImage or SF Symbol at 24pt.
  • Android Data model: message.widget = Widget.Artifact(...) — NOT in message.attachments. Icon shape: RoundedCornerShape(14.dp). Accepts ImageVector or Painter at 24dp.
  • Web Modifier .attach-artifact--lg overrides --pill-h and icon sizing via CSS custom properties. No JS needed.
  • All Artifact is a widget — it occupies the widget slot (below text), max one per message. Uses .chat-attach-files CSS markup for pill styling, but in the data model it is widget: .artifact(...), NOT part of attachments[]. An artifact and a changeset cannot coexist in one message (both are widgets — split into two messages). Tap opens the artifact. Badge optional (live, draft, error).

Audio Message

Voice message player — waveform visualization with play/pause and duration. Widget type: sits outside bubble in the widget slot (max 1 per message).

Chat
0:42
2:30 PM
JK
Julia Kim
0:27
2:28 PM
AL
Alex Lee
Meeting notes from today
1:15
2:26 PM
Terminal
audio
@julia$send voice
0:18
$play recording.m4a
2:04
Chat Audio
SurfacetertiarySystemFill · radius14
Paddingspace8 × space12
Play button36 × 36, radiusFull, tint bg, onLight icon 16 × 16
Waveform height28px
Bar width2px (fixed)
Bar distributionspace-between (auto gap)
Bar radius1px
UnplayedtertiaryLabel
Playedtint (.is-played)
DurationfontMono · caption2 (12px) · secondaryLabel
Inner gapspace8
Min width200px
After bubblespace4 gap (widget sibling rule)
After namespace8 gap (no preceding bubble)
Terminal Audio
SurfacetertiarySystemFill · radius12
Paddingspace8 × space12
Play button32 × 32 (compact)
Waveform height24px
Icon14 × 14
When to use
  • .chat-audio/Voice message widget in chat. Sits in the widget slot — outside and below the bubble (or standalone when voice-only).
  • .term-audio/Audio player in terminal context. Standalone block with tertiarySystemFill background.
  • .is-played/Applied to each bar that has been played. Progress fills left-to-right as playback advances.
  • States: not-played (play icon, all bars uncolored, total duration), playing (pause icon, bars fill left-to-right, elapsed time), played (play icon, all bars colored, total duration).
  • With text: text goes in the bubble above, audio widget below — standard widget pattern (like link preview after text).
  • Widget slot: audio occupies the widget slot. Cannot coexist with other widgets (link preview, changeset, artifact) in the same message.
  • Not for: audio files (music, podcasts, sound effects) — use file pill with audio icon. Not for transcription — that's text content.
Platform notes
  • iOS Playback: AVAudioPlayer. Waveform: CAShapeLayer with bar paths or custom UIView.draw(_:). Play/pause: UIButton with spring animation on state change. Extend hit area to 44pt via point(inside:with:) override or contentEdgeInsets.
  • Android Playback: MediaPlayer or ExoPlayer. Waveform: Canvas.drawRoundRect() per bar in custom View, or Compose Canvas composable. Play button: IconButton(modifier = Modifier.size(44.dp)) for touch target.
  • Web Playback: HTMLAudioElement API. Waveform bars: <span> elements with --h custom property. Toggle .is-played per bar on timeupdate event. No audio analysis needed at render — waveform data comes from server.
  • All Waveform data = peak amplitudes sampled from audio file (typically 25–40 samples, normalized 0–100%). Play button effective tap target ≥ 44pt — 36px button + 12px widget padding = 48px touchable area. Data model: message.widget = .audio(url, duration, waveform) — NOT in attachments[]. Occupies widget slot (max 1 per message).

File Pills

Chat
Agent
Here are the deployment artifacts.
3:15 PM
JK
Julia Kim

Shared the docs and schema.

3:16 PM

Here's my mockup and the source file.

3:17 PM
Terminal
attachments
$fork deploy
✓ Done — 3 artifacts
config.yaml
deploy.shscripts/
@julia$share docs
API DocsMarkdown
schema.sqldb/
Chat Attachments (in bubble)
1 mediafills bubble, no padding, width = 280px (layout constant)
N mediahorizontal scroll, 70% width per thumb (peek next)
Filesvertical list · container max 320px · pills stretch to bubble width (capped by the smaller of 320px or actual bubble width)
Pill height48px (--pill-h) — same in chat and terminal
Pill namecaption1 (13px) · 600 weight (regular pill) / subheadline (16px) · 600 (large artifact)
Pill radiusradius10 (chat) / radius20 (large artifact) / 0 (terminal). Concentric formula: innerRadius = outerRadius − gap, where gap = padding from container edge to nested element edge. Regular pill: bubble(18) − 8 = pill(10). Large artifact: pill padding is 8px, strict concentric = 20 − 8 = 12, but icon uses radius14 (optical adjustment for app-icon alignment).
Ordermedia → files → text (anatomy rule)
Terminal Attachments
Mediahorizontal row · aspect-ratio driven
Fileshorizontal waterfall · .attach-pills
Pill height48px (--pill-h)
Pill radius0 (square — terminal geometry)
Ordermedia first → pills last
When to use
  • .chat-attach-media/Media carousel. Agent: full-width flat. Peer/user: inside bubble.
  • .chat-attach-files/Vertical file pill list. Max 320px. Radius10 pills with fontBody.
  • .chat-msg-bubble--media/Single media bubble. Image edge-to-edge, text below with padding.
  • .chat-msg-bubble--media-scroll/Multi-media bubble. 70% thumb width, peek 30% of next.
  • Agent flat: media + files not inside bubble, scroll full chat width.
  • Terminal: horizontal waterfall in .attach-row. Files are compact pills in 2-row columns.
Platform notes
  • iOS Chat: PHPickerViewController for media. File pills: self-sizing UICollectionViewCell. Terminal: UICollectionView horizontal flow.
  • Android Chat: ActivityResultContracts.GetContent(). Pills: AssistChip. Terminal: LazyRow.
  • Web Chat: hidden <input type="file">. overflow-x: auto + hidden scrollbar on carousel.
  • All Any sender can attach any file type. Unknown types = file pill. Thumb size differs: chat = 280px, terminal = 48px (--pill-h).

Media Thumbnails

Chat
Agent
Here are the charts you requested.
2:20 PM
2:21 PM
16:9

Check this screenshot.

2:21 PM
JK
Julia Kim

Three screenshots from the review.

2:23 PM
Agent
Generating...
2:22 PM
JK
Julia Kim
screenshot

Check the layout on mobile.

2:23 PM
Terminal
attachments
$fork deploy
config.yaml
deploy.shscripts/
$generate hero image
Generating...
Chat Media
Radiusradius14 (flat agent). Single inside bubble: 0 (edge-to-edge, clipped by bubble overflow). Multi-scroll inside bubble: radius6 (per-thumb rounding with padding).
Max width280px (layout constant — CSS uses var(--chat-media-w, 280px) for override flexibility, but 280px is the default on all platforms)
Border1px solid border
Default ratio4:3 when unknown, override per-item when available
Min height120px
Max height400px (clip with expand affordance)
Terminal Attachments
Pill height48px (--pill-h)
Gapspace8
Inputwraps, max 2 rows
Generatingopacity pulse 2s
Generating Placeholder
Aspect ratio16:9 default
Min height120px
BackgroundtertiarySystemFill
Animationopacity 0.5–1.0, 2s easeFluent infinite
Label"Generating..." · caption2 · tertiaryLabel
When to use
  • .chat-msg-bubble--media/Single media in peer/user bubble — image edge-to-edge, no padding. With text: image top, text below with padding.
  • .chat-msg-bubble--media-scroll/Multiple media — horizontal scroll, 70% width per thumb (peek pattern). Inside peer/user bubble.
  • .attach-thumb--generating/Shimmer placeholder while media is being generated. Opacity pulse animation, "Generating..." label.
  • .attach-row--input/Terminal input attachments — compact wrapping layout. Media thumb + pill waterfall in 2-row columns.
  • .attach-row--lg/Terminal output attachments — taller layout. 3-row column wrap for pills when media is present.
Platform notes
  • iOS Media bubble: AsyncImage with .clipShape() matching bubble. Generating: .opacity animation with shimmer gradient.
  • Android Media: AsyncImage with Modifier.clip(). Scroll: LazyRow with 70% width items.
  • Web Media bubble: overflow: hidden on bubble clips image to border-radius. Generating: CSS @keyframes opacity pulse.
  • All Aspect ratio is preserved — never crop or stretch media. If aspect ratio is unknown at load time, render a 4:3 placeholder; once dimensions resolve, animate to actual ratio (easeFluent 0.25s). Generating state must be visually distinct from loaded state.

Composer & Prompt

Chat
EMPTY
WITH TEXT
WITH ATTACHMENTS
Terminal
fork — ~/project · interactive
# Click here and start typing
~/project$echo "Hello, FORK"
Hello, FORK
~/project$
minimal
~$
main
logs
build
~/project$
Chat Composer
ShaperadiusFull (capsule)
BackgroundsystemFill
Input fontsubheadline (16px)
SendchatBubbleUserBackground · 40pt visual circle inside 44pt tap area (2pt inset each side)
Thumb size64pt square · radius10
Terminal Prompt & Chrome
FontIBM Plex Mono · typeCode
Symboltint
Window radiusradius14
Surface bgsecondarySystemBackground
Tab activesystemFill · label
When to use
  • .chat-compose/Chat input bar — capsule shape when single-line, softens to radius20 when content grows (attachments or multi-line).
  • .chat-compose-attachments/Horizontal thumbnail strip above input. Scrollable, hidden scrollbar. Separator line between thumbs and text input.
  • .chat-compose-send/Send action — chatBubbleUserBackground filled circle. Disabled (38% opacity) when input is empty.
  • .chat-compose-attach/Attach action — systemFill circle, outside capsule. Opens file picker.
  • .term-prompt/Terminal input — path + tint symbol + text. Prompt is inline, not a separate input area.
  • .window-tabs/Tab bar in terminal titlebar for multiple sessions. Active tab: systemFill + label color.
Platform notes
  • iOS Composer: UITextView with auto-growing height. Send: UIButton with outgoing-bubble color fill (not systemBlue — see Chat Bubbles notes). Safe area: input sits above home indicator via safeAreaLayoutGuide.bottomAnchor.
  • Android Composer: BasicTextField with Modifier.heightIn(min = 44.dp). Send: IconButton. Bottom inset: WindowInsets.ime + WindowInsets.navigationBars.
  • Web Composer: <textarea> with JS auto-resize. Terminal: keydown handler on .term-surface[tabindex]. Bottom safe area: env(safe-area-inset-bottom).
  • All Input must stay above keyboard on mobile. Composer min tap target: 44pt. Terminal cursor blinks at 1s via steps(2), pauses while typing.

Full Session

Comprehensive showcase — every component in context. Note: this demo intentionally exceeds the density budget to show all widget types in a single flow. In production, a single message should have at most 1 widget (anatomy rule).

Chat
Today, 3:00 PM
Build me a weather dashboard
3:00 PM
Agent

I'll build it. Choose a style:

1Rich — cards, charts
2Minimal
3:00 PM
Rich style with a map widget.
3:01 PM
Agent
Scaffold weather-dash/ done
12 files
Write api.js done
87 lines

Done! Here's your API key setup:

const KEY = 'your-key';
f4a1b2c#1
Initial weather dashboard
api.jsdashboard.html
3:02 PM
Agent
Dashboard is live
3:02 PM
Julia Kim joined
JK
Julia Kim
screenshot

Colors need adjusting — hot should be coral.

3:05 PM
On it
Fixing now
3:06 PM
Agent
Terminal
fork — ~/weather-dash
$build a weather dashboard
design-spec.md
Read design-spec.md done
48 lines
Scaffold weather-dash/ done
12 files
Scaffold
API
UI
Deploy

Done! API key: const KEY = 'your-key';

f4a1b2c#1
Initial weather dashboard
api.jsdashboard.html
@julia~/weather-dash$fix the temperature colors
Edit styles.css:14 done
- --tempHot: #FF6B35;
+ --tempHot: #E05248;
✓ Colors updated
$add wind speed overlay
Session Rules
Max widgets/msg1 (anatomy rule)
Foldsinline metadata, not widgets — unlimited
Virtualize at>500 items
Terminal flowprompt → folds → steps → prose → changeset / activity → artifact
Chat orderattachments → folds → text → widget
When to use
  • Chat anatomy per message: attachments → folds → text → widget. At most 1 widget. Folds are inline metadata, not widgets. Text + changeset = OK. Text + widget = OK. Text + changeset + widget = split into 2 messages.
  • Terminal flow: prompt → folds → steps → prose → changeset / activity → artifact. All in one continuous stream. No message boundary — output flows naturally.
  • Peer in chat: media inside bubble (single = edge-to-edge). Peer in terminal: @julia username prefix.
  • Grouping: consecutive user or peer messages from the same sender (<60s) group (tight spacing, shared timestamp on last). Agent never groups.
Platform notes
  • iOS Chat: UICollectionView with self-sizing cells. Terminal: NSAttributedString-backed scroll view.
  • Android Chat: LazyColumn with reverseLayout = true. Terminal: LazyColumn with auto-scroll.
  • Web Chat: flex column, overflow-y: auto. Terminal: .term-surface with scrollIntoView.
  • All Virtualize long sessions (>500 items). Input stays above keyboard on mobile.