Skip to content

Latest commit

 

History

History
265 lines (221 loc) · 85.3 KB

DESIGN.md

File metadata and controls

265 lines (221 loc) · 85.3 KB
name Chopin
description A calm shared workspace for repository-connected documents.
colors
petrol petrol-hover petrol-active page ground inset selected ink secondary-ink tertiary-ink destructive success warning merged chat-divider
oklch(0.50006 0.08514 210.06)
oklch(0.42914 0.07315 210.634)
oklch(0.37546 0.06382 210.435)
oklch(1 0 0)
oklch(0.96623 0.0039 95)
oklch(0.97857 0.00264 95)
oklch(0.94216 0.00393 95)
oklch(0.15908 0.00637 95)
oklch(0.39036 0.01026 95)
oklch(0.48124 0.01278 95)
oklch(0.588 0.17178 24.175)
oklch(0.51958 0.10966 145.072)
oklch(0.59898 0.12586 74.986)
oklch(0.47500 0.17500 301)
rgb(0 0 0 / 9%)
typography
document-title section-heading subheading document-body chrome compact small-metadata
fontFamily fontSize fontWeight lineHeight
"Lora", Georgia, serif
var(--text-2xl)
400
1.15
fontFamily fontSize fontWeight lineHeight
"Lora", Georgia, serif
var(--text-xl)
400
1.25
fontFamily fontSize fontWeight lineHeight
"Lora", Georgia, serif
var(--text-lg)
400
1.4
fontFamily fontSize fontWeight lineHeight
"Inter Variable", "Apple Color Emoji", "Segoe UI Emoji", "Noto Color Emoji", ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif
var(--text-base)
400
1.6
fontFamily fontSize fontWeight lineHeight
"Inter Variable", "Apple Color Emoji", "Segoe UI Emoji", "Noto Color Emoji", ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif
var(--text-sm)
400
1.5
fontFamily fontSize fontWeight lineHeight
"Inter Variable", "Apple Color Emoji", "Segoe UI Emoji", "Noto Color Emoji", ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif
var(--text-xs)
400
1.35
fontFamily fontSize fontWeight lineHeight
"Inter Variable", "Apple Color Emoji", "Segoe UI Emoji", "Noto Color Emoji", ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif
var(--text-2xs)
400
1.35
rounded
sm md lg xl full
0.25rem
0.375rem
0.5rem
0.625rem
9999px
spacing
half base one-and-half two three four six eight
0.125rem
0.25rem
0.375rem
0.5rem
0.75rem
1rem
1.5rem
2rem
components
button-primary button-secondary button-ghost field document navigation-current
backgroundColor textColor rounded height padding
{colors.petrol}
{colors.page}
{rounded.md}
2rem
0 0.75rem
backgroundColor textColor rounded height padding
{colors.selected}
{colors.ink}
{rounded.md}
2rem
0 0.75rem
backgroundColor textColor rounded height padding
transparent
{colors.secondary-ink}
{rounded.md}
2rem
0 0.75rem
backgroundColor textColor rounded
{colors.page}
{colors.ink}
{rounded.md}
backgroundColor textColor
{colors.page}
{colors.ink}
backgroundColor textColor
{colors.page}
{colors.ink}

Design System: Chopin

Overview

Creative North Star: "A Working Document"

Chopin feels calm, precise, and collaborative. The authored document is the visual center; navigation, conversation, and controls support the work without competing with it. The interface is compact enough for sustained use and gives long-form prose room to breathe.

Key Characteristics:

  • A white document surface sits on a warm olive ground.
  • Petrol marks actions, links, focus, and presence; semantic colors identify outcomes.
  • Controls are quiet, legible, and purposeful. Depth gives layers subtle separation.

The implementation is the authority for values: ../../packages/visuals/theme.css owns shared tokens, while src/theme.css, src/navigation.css, and ../../packages/editor/src/styles.css apply them. The frontmatter records the most reused values; it is not a replacement for those source files. scripts/check-design-record.ts compares the structured subset against the shared theme, including explicit aliases. Two component bindings to shared tokens are checked against their owning selectors: Chat's divider in .workspace-frame .workspace-chat-panel, and prose leading in .plan .plan-content. Their shared owners are --color-divider and --document-line-height. Document leading is deliberately 1.6 while the paired base UI token remains 1.5. The sidecar's color values, shadows, and motion entries are checked too; generated dates and illustrative tonal ramps do not establish authority. See the design contract guide for the exact scope and change workflow.

Colors

Primary

  • Petrol is the single interaction accent for primary actions, links, focus, and presence. Its darker states respond to hover and press.

Neutral

  • White page carries the authored document and raised controls.
  • Warm olive ground frames the workspace. Lighter inset and selected roles separate nearby surfaces without adding another accent.
  • Ink has primary, secondary, tertiary, and quaternary text roles; use the role that preserves the intended hierarchy and legibility.

Success, warning, and destructive colors communicate state. Their full surface, graphic, icon, and text pairs remain defined in the shared theme.

The destructive button red carries white text at 4.5:1 at rest; its hover and pressed colors are each one step deeper, distinct from it and from each other, at 5.2:1 and 6.0:1.

The Contrast Rule. Chopin meets WCAG 2.2 AA everywhere, in light and dark: 4.5:1 for text (placeholders, links, labels, timestamps, conversation metadata, loading and tool status, audit-page labels and code tokens included) and 3:1 for large text and for the non-text parts people need to see (icons in controls, the edges of fields, the composer, checkboxes and radios, status dots and focus rings). Disabled controls are exempt, as WCAG exempts them; nothing else is. Hierarchy comes from the order of the ink roles, size and weight, never from falling below AA. On 11 November 2026 the maintainer brought the former deliberate exceptions to AA, each by the smallest step that reaches it: quaternary ink (gray-500) to #727068, a settled research handoff's fade from 45% to 94%, the code and diff palette darkened in pierre-light's own hues (packages/editor/src/widgets/code-theme.ts), and the destructive red to #cf4747 with its hover and pressed colors one step deeper each. e2e/design/contrast.e2e.ts walks every audit plate in both themes and fails any pair below its threshold; a new role that cannot reach AA is a design decision recorded here, not an exception added to the walk.

The One Accent Rule. New action and focus treatments use the established petrol roles. New raw color values require a change to the shared theme, not a local copy.

Typography

Inter Variable is used for interface text and document prose. Document headings H1–H3 use Lora at weight 400 through --font-document-heading. H4 uses Inter at weight 700 and keeps the body-size token. The mono stack is reserved for code and technical content. The shared theme defines a fluid modular scale across 360–1440px viewports. Its base is 15–16px and its ratio grows from 1.12 to 1.14. Each role uses base × ratio^step at each endpoint and a CSS clamp() between them. The seven named roles are small metadata --text-2xs (step −2.5, 11.3–11.5px), compact --text-xs (step −2, 12–12.3px), common interface --text-sm (step −1, 13.4–14px), document body --text-base (step 0, 15–16px), subheading --text-lg (step +2), section heading --text-xl (step +3, 21.1–23.7px), and title --text-2xl (step +5, 26.4–30.8px). The editor gives prose looser leading than the paired UI token and forwards the fluid tokens through MDXEditor's fixed variable scope.

The document header has two approved fixed interface roles: --text-document-control (14px) for its view tabs and --text-build-blocker (13px) for the investigation pill. Build plan retains the common interface text. Its controls and tabs use 3px vertical and 8px horizontal padding; tabs have a 2px gap and 7px outer inset. The title bar remains 40px, with a 28px expand control, a 12px title inset after its divider, and equal expand and presence insets derived from the bar’s inner height, excluding its hairline. Blocker pills are 23px tall with 8px horizontal padding, a 14px icon and a 3px gap.

The Fluid Type Rule. Use a named text utility or --text-* token for every font size. The CI type-scale check rejects raw CSS font sizes, arbitrary Tailwind font sizes, and literal inline font sizes. Add a role in the shared theme and update its modular-scale test when the existing roles cannot serve the need. Inline code inherits the size of surrounding prose or a heading.

Interface copy

Keep labels and tooltips concise and in sentence case. Capitalize the first word and proper names; preserve handles and keyboard shortcuts as written. Use helper text only when people need it to act or recover.

A control is named by one hover text: the shared tooltip, or a copied element's own (the TooltipIconButton on a Chat code block), never both, and never the browser's native title beside or after it, a pressed control's included.

A person is named by their GitHub handle exactly as GitHub has it, the way Slack shows a username: never capitalized, with its digits, underscores and hyphens (lavaman131, mona_acme, Hubot-Bot), in Chat rows, reactions, threads, comments, sources, History and presence alike; the reader is You where a surface says so. A demo collaborator keeps its roster name. The agent is chopin, the name @chopin addresses, wherever it takes part, acts or is addressed: its Chat row and avatar, the @ menu, notes, sources, History, the research handoff, busy labels (chopin is working), controls and their names (Ask chopin, Send to chopin), notices, refusals and the lines the server writes in Chat (ana stopped chopin and paused its workflows.), all from PLANNER_NAME in @chopin/protocol/planner-name. A sentence that begins with it, or with a handle, begins in lower case, and sentenceCase leaves it so. Only the product keeps its capital: the logo, page and window titles, settings and help prose about the product (Chopin plans and builds on your machines, Your Chopin session has ended), and a connection to the server (Chopin isn't connected). @chopin and an older @Chopin address it alike. What chopin writes follows the same rule: every model whose words a person reads (the Planner and its background writers, reaction replies, research and History's summaries) is told it in NAMES_AS_WRITTEN (apps/server/src/agent/identity.ts).

Never separate inline metadata with a middle dot or bullet. Separate the pieces with layout (sibling elements with a gap, weight, tertiary color, or a line break), or cut the metadata; scripts/no-middot.test.ts enforces this.

Layout

The workspace places Chat on the left and the document on the right when space permits. Chat sits directly on the ground with no bar of its own; the document is a raised sheet that rises to the top margin and carries a 40px title bar above a compact views bar. The title bar holds the expand control, the project and title with rename and the document actions menu, then status and presence, separated by full-height dividers. The views bar below it holds Document, Decisions, and Build on the paper, with no rule beneath, and at its end a quiet History (clock) button. History slides in from the right under the title bar, over the views bar and the document, so nothing beneath reflows, and slides back out on close. Its title and close button stay put while the list scrolls under the shared scroll-edge fade. Each editing session is one line: face, who, a one-line summary of what it did (written by a small model when History opens and shown as a breathing skeleton until it arrives, or made edits when no Planner can write it), and a terse age (now, 2m, 3h, 4d, 2w, then the date), with space between rows growing with the time between them. Lingering on a row for a moment shows the full line and the exact time just left of it. Contents needs no button: one quiet tick per heading sits in the document's right margin, the first level with the title, darker for the section being read. Hovering or focusing a tick peeks at that section beside it (its heading and opening lines, in a hairline card with a light shadow that follows the pointer down the ticks), and choosing one scrolls there. The ticks follow edits and an earlier version on show. Comment markers own the lane beside the prose, so the ticks appear only when the right margin can hold both, as on an expanded or wide sheet; History hides them while open. Choosing an earlier session replaces the document in place with a read-only copy, and the views bar gives its place to the version's own: Current (back to the live document), whose edits, the exact time, and for writers a small Restore. Restoring brings back that version's prose as a new edit by the person who pressed it, while decision, questionnaire and research blocks stay as they are now, since their records own them. The newest session is the live document. Expanding the document only widens the sheet: Chat collapses, the title stays, and a Chat button in the same place restores it. As the sheet narrows, the title truncates first, then status labels fold to glyphs, then presence keeps one face and a count. A child document zooms in place of its root: the title bar shows Back, the root as a crumb, and the child's own title and actions, and the views fold away because a child has none. Compact layouts and phones show the path in a slim stack bar instead. Document, Decisions, and Build switch the main pane; Build is available on parent documents. Tabs never trigger actions: Build stays off until the plan is built, and only the header's Build plan starts a build. Build plan sits left of the people here, inert with its reason in a tooltip until it can run. Required investigations replace it with a coloured capsule showing how many remain; open decisions never do, since Build goes ahead on the agent's best judgment and lists them as assumed. Hover or click reveals their titles; each whole row opens its specific card, with one hover surface and a decorative arrow. After the press the same slot becomes a quiet capsule (glyph and words such as Building 3 of 8, Built, 3 changes to build) that opens Build, with at most one small outline action (Retry, Build). One shared glyph (progress ring, status dot, clock or warning) appears identically in the capsule and on the Build tab; hovering either lights the other, and on the first press the glyph travels to the tab unless reduced motion is on. On a phone the capsule keeps only its glyph and Build's headline carries the words. Local investigation proposals, approval controls, and results appear in the document flow. The Projects sidebar slides over the document without resizing it; on narrower screens, panels take their own view. The document uses a maximum prose measure of 40.625rem (650px), with 3.75rem (60px) of top padding and at least 3.5rem (56px) of padding on each side. At browser widths of 600px or less, the horizontal padding becomes 1.5rem (24px). On a phone (a primary touch pointer with a width of 600px or less, or a height of 500px or less, so landscape phones count), the workspace drops its card and the raised sheet: panes run full-bleed with the same 1.5rem (24px) gutters, so margin comment markers keep their lane, under a translucent top bar that tucks away while reading down, above a floating bottom tab bar in the thumb zone. Landscape phones use a shorter top bar and a one-line tab bar. Destinations slide sideways in tab order, and crossfade under reduced motion. Images and diagrams can extend beyond the prose measure within those gutters; a table wider than the measure grows evenly about the column's centre, up to 1.5 times the prose measure (60.9375rem, about 975px) or the gutters, whichever is narrower, and scrolls in its own lane beyond that. On a phone (a coarse pointer on a small screen), when the document is narrower than 40rem, a wide table fades whichever side still hides columns, code wraps with a hanging indent instead of scrolling, and a Mermaid diagram below its readability floor offers Expand diagram under its scroll lane, which opens it in a full-screen sheet at its authored size. A narrow document alone does not trigger these: narrow desktop windows keep the scroll lanes. Off a phone, a Mermaid diagram too wide to read at the frame's width opens at its start and offers Fit diagram to width under its scroll lane, which scales its whole shape to the frame until Actual size returns it, so the cut edge never reads as a crop. Two settings belong to the document, as Notion's do, so every reader sees them the moment a writer changes them. Full width, at the end of the document actions menu below its actions, lets every block, prose included, take the sheet's whole width inside its gutters, with tables free to grow to the same edges; the Decisions and Build views keep their measure, and a phone, whose column is already the screen's, neither offers nor applies it. Turning it on or off lays the page out once, then the column's edges glide to their new places while the old lines fade out and the new ones in (--panel-open-dur on --motion-move), keeping the block at the top of the reader's view where it was; reduced motion snaps. Wrap lines, among a code block's controls, wraps that block's long lines with the hanging indent a phone uses instead of scrolling them, and gives the controls a band of their own above the code so nothing hides under them; it is written into the fence as wrap.

Spacing starts from a 4px unit and uses the measured steps shown in the frontmatter. App chrome is dense; prose has larger margins and trailing space so the caret remains comfortable near the end of a document. Layout responds to both viewport and document container width. Safe-area insets and larger coarse-pointer targets are accounted for in the web styles.

Elevation & Depth

The system is mostly flat. Tonal separation and passive edges distinguish adjacent panes; subtle resting, raised, and overlay shadows distinguish surfaces that actually sit above others. In the split workspace the document panel is the only raised sheet; Chat has no surface of its own. The composer on the ground is a message box: the page surface with a hairline and no shadow.

Shapes

Rectangular controls use a restrained four-step radius scale: small for grips and toolbar cells, medium for buttons and fields, large for popovers, and extra large for the document page. Badges and radio controls use the full radius. Passive structure uses a light edge; popovers and buttons use the stronger control edge; fields, checkboxes, radios and the composer use the control boundary, which meets 3:1 on every ground because their edge is how they are found. The Chat pane has its own quiet divider color.

Components

  • Buttons: Primary, secondary, outline, ghost, and destructive tiers (outline is a white button with a quiet 12% edge, for a single action inside a card) share one shape and compact text treatment. Resting outlines use at most 15% opacity, including primary and danger treatments; secondary buttons keep the lighter 7% passive edge. Sizes follow shadcn's Nova scale: btn-xs 24px, btn-sm 28px, btn-md 32px and btn-lg 36px, with icon-only btn-icon-xs (24px) and btn-icon (28px). Extra-small and small buttons use the extra-small text role, a 4px icon gap and the medium radius; medium and large use the small text role, a 6px gap and the large radius. Every button chooses a size. Hover and pressed states change the relevant color role. Keyboard focus shows the brand focus ring; a pointer press hides it on buttons and menus, leaves the clicked Chat composer only its quiet brand edge, and other text fields keep theirs.
  • Recovery actions: Use btn-outline-danger on danger surfaces and icon-danger for error indicators. Hover and pressed states stay within the danger palette.
  • Fields and selections: White fields use the medium radius, control boundary, and subtle resting shadow. Invalid fields use the destructive role. Disabled controls use neutral fill and muted text.
  • Navigation: The current location uses a white surface and primary ink. Other items stay quieter and reveal their affordance on hover or focus. A navigation item that leads to a full page discloses progressively: a click opens a popover with the essential information and the most common choice (Machines lists the machines you can use, yours and those teammates share, sorted together newest first, and sets your default), plus a link to the full page. A document's own machine, which plans and builds it, is the exception: an icon-only Local or Cloud status button left of Build plan, never in the Chat composer. It is where the state of the document's Planner lives, as presence does in Slack: Chat never draws it. It says nothing while the Planner is attached: the dot becomes the connecting dots on a small capsule while the machine is being connected to, the warning role where it cannot be, a muted grey while the Planner is away, and no dot at all where no machine is connected, the same bare tertiary icon as a button with no machine, never the machine's own success green. The button's name and tooltip say it in words that speak of the machine (Connecting to Build box, Can't connect to Build box., Build box is offline. Last seen 10:31 AM. for a machine that is away (Your work is kept. where no time is known), Build box stepped away. Your work is kept until 10:31 AM., No machine is connected.), and a click explains it at the top of its menu: one sentence, or, where there is something to fix (it can't be connected to, or is offline), the fix-it steps (see Machine fix-it steps). Moving the document to another machine is one state: Connecting to Box B from the click, on the button, chopin's presence and the live region (said once), until it attaches (then nothing) or can't (the can't-start state), with nothing between, never the machine just left, an idle moment or a return; the server's word names the machine it starts, so no second read of the list is waited on. chopin's own mark carries the same presence where Slack shows an app's: a small dot on its corner in Chat's rows, its ephemeral row and its row of the @ menu, ringed in the ground behind it: the success green while its machine is attached, petrol breathing slowly by opacity alone while it is connected to (still under reduced motion), the warning role where it can't be, and the muted grey while it is away or there is none; the @ row's name says it in words (Chopin, agent, Away). Nothing is drawn where the document has no remote Planner. Connecting and no machine yet are said only once they have lasted as long as a lost connection does before the page speaks (1.5 s), the last known state staying until then, so a restart's passing states are never shown; a state clearing goes at once, and nothing says it did. Through a lost connection the last known state stays until the machine's own word arrives, however long that takes: the no machine a restarted server tells first is its placeholder, not news. The name is the document's own machine's (the one the button names), or whose machine it is by login, never another machine's, and nothing is said while the list of machines describes another account's Planner than the page's connection does, or another machine than the document's, even one of the person's own, or was read before the connection's last word (the page asks again; until that answer has come the status says nothing of the machine, however long that takes, and is never what it said of an older word. The state it last showed is remembered through the gap only so that the same state, naming the same machine, returns at once instead of waiting out its grace again, and a screen reader is not told it again; any other state that arrives after the gap is drawn by its own rules, so a machine being connected to or no machine yet draws nothing until it has lasted 1.5 s, and what was shown before the gap is never drawn in its place: a document moved from Build box to Studio does not read Build box is offline for a moment before Connecting to Studio). Status words are plain and name none of Chopin's own terms, such as the Planner, attaching or Decisions, nor Chopin itself; status-copy.test.tsx keeps it so, and keeps the Planner, attaching, holding and @chopin out of every other word a person reads, the busy labels (chopin is working), a run card's notes (It carries on when the machine reconnects), the composer's picker hints and every refusal included. Decision stays Chopin's word for its cards and its tab; only text about a machine and its connection leaves it out.
  • Menus and popovers: Every menu follows shadcn's Nova menu. The panel has 4px padding, the large radius, and a hairline ring with the raised shadow. Items are 28px: 4px by 6px padding, a 6px icon gap, the medium radius and the small text role. Group labels use the extra-small text role in medium weight and tertiary ink with the item's inset. Dividers are 1px, run through the panel's padding to both edges and keep 4px above and below. A selected item is tinted with the selected surface, never marked with a check. A setting that turns on and off (the document's Full width) is a checkbox row with a small switch at its end, petrol when on and the control edge when off; the row is not tinted, and choosing it leaves the menu open so the change is seen behind it. Secondary detail sits right-aligned in tertiary ink on the same line.
  • Composer menus and assistant-ui: The @ people and the / commands are assistant-ui's Composer menu, copied unmodified into src/components (ComposerMenu, its Mentions row ComposerPersonItem and its Slash commands row ComposerCommandItem): a 288px floating panel with 16px corners and 6px padding, rows of a round badge, a name and a tiny mono tag. A copy keeps its own classes, sizes and radii (one generated layer gives its roots Tailwind's scale, never Chopin's control, pill or menu tokens) and takes only Chopin's colours, through the shadcn colour variables in src/assistant-ui.css; Adding an assistant-ui element says how, and a copy is never restyled here. A recorded patch adds an extension point and changes no class or size: a person's own picture in the Mentions badge, with a tag that can be any word. Chopin's own state drives the menus through props (mentions.ts, commands.ts), not the element's useMentionMatches and applyMention, because the draft keeps its caret and its references. The # menu of the document's parts and the other documents is still Chopin's own, in the shared menu recipe, until its element is copied. THIRD_PARTY_NOTICES.md names the source, its revision and the MIT notice, and nothing of assistant-ui's runtime is used.
  • Commands and mentions: Typing / as the whole draft opens assistant-ui's Slash commands list over the composer: Chat's own command (/research) and the commands the document's agent reports (a skill, a prompt template, a workflow, any other kind the agent named), in the order reported, one row each: the icon of its kind, /name, a line of description that gives way to a narrow screen, and ↵ on the row Enter runs. It filters as you type, by the name or by what follows a skill's prefix, and says nothing when nothing is left. Enter or Tab chooses; an agent's command becomes one chip that opens the draft, and the words typed after it are what it is run with. @ is for people, as in Slack: assistant-ui's Mentions list offers chopin first, tagged agent, with its mark in a circle, then the people who can read the document, tagged human, each with their GitHub picture in the badge, a rounded square as a person is everywhere else: those here first, then the others the server lists (the repository's collaborators who can read it, or, where GitHub will not list them, the document's creator and authors), then those who have spoken in Chat, filtered by the login typed, and nothing else. # and the Mention docs button (which puts a # at the caret) are for the document: its sections (tagged with their level), Decisions, research cards and its repository's files, each with an icon and a tag that says what it is, above the other documents, filtered by the words typed, which may go on past a space or a slash; once they do, the menu shows only while something matches, so a #12 in a sentence holds no empty menu open. The document's parts arrive a moment after the other documents and a stale answer is never offered. No heading divides either list, since each row's tag says what it is. In all three menus Enter or Tab chooses the highlighted row and the draft keeps the focus; Shift+Tab, and Tab with no menu open, move the focus on. Choosing chopin or a person writes @login, drawn as a mention, and every @login that names chopin or someone who can read the document is drawn as one wherever it is, in the draft as it is typed and in the sent message alike (message-mentions.ts), while one that names no one stays words; choosing a part or a document is a chip, the same chip and the same reference whichever way it was reached. A chip is the one treatment for every reference the draft can hold except a document's link: the transcript's tint (--color-brand-wash, brand ink, the small radius, medium weight, no underline), whole in every edit (deleting at its edge or inside it takes all of it, a selection that reaches into one takes the rest, a caret placed inside one moves to its end before typing), and drawn the same, as a chip and never a link, once the message is sent. Both lists are the shared composer menu with its tinted active row, their region scrolls and is reachable by keyboard, and on a phone they meet both edges of the composer like the model picker. Neither says anything of the machine: a Planner that reports no commands simply offers Chat's own.
  • Composer box and send and stop: The composer is a message box in the shape of Slack's: a rounded rectangle (the large radius, a 1px control boundary that darkens to tertiary ink on focus and turns destructive when a send fails, the page's surface) holding, top to bottom, a formatting toolbar, Chopin's own field and a row of controls. Every size is Chopin's control size (28px ghost buttons with Slack's 18px glyphs, a 44px target that grows only on touch) and every colour a token. The toolbar sits on the inset ground under a hairline: bold, italic and strikethrough, a link, a numbered and a bulleted list, code and a code block, each formatting the selection in place, as Slack's does, and shown pressed while the caret is in its format, none taking the draft's focus or selection; each names its key in its tooltip. The draft renders what it holds as it will read (bold, italic and struck words, links, lists, inline code on the inset ground and code blocks in a hairline box), never the marks: it is Lexical, the editor the documents use, loaded beside the page's code rather than in it. Slack's keys format too (⌘ or Ctrl with B, I, Shift+X, Shift+C, Shift+7, Shift+8 and Alt+Shift+C; ⌘K links chosen words and is search otherwise; ⌘U is refused, Markdown having no underline), and so do Slack's typed marks as they close (*bold*, _italic_, ~strike~, `code`, and -, 1. or ``` at the start of a line). A link's address is asked in one row over the box (Link, Save, and Remove for a link already there), with https:// put before a bare host. Enter sends; Shift+Enter breaks the line, makes the next item in a list (and on an empty item leaves it) and the next line in a code block (and on an empty last line leaves it). A rich paste keeps its formatting and a plain one is words. What is sent is the Markdown the formatting means (Chat messages are Markdown), the same Markdown a person typing its marks would send, and a draft read back from Markdown (a reload, a refused send) is drawn the same way; Markdown the draft would write differently (a quote, a table, `*` emphasis) stays as the words it was written in. It gives way on a phone and where the pane is narrow, and the Aa button in the row turns it off and on; the choice is kept for each person in their browser. The row has `+` (attach, where the server takes files), Mention docs, Aa, an @ that starts a mention at the caret, the sparkle button that asks Chopin, the microphone (where speech can be turned into words) and the model pill on the left, and Resume and the send control on the right. The send control is one primary rounded-square button with a paper plane, driven by Chopin's state (`send-stop.ts`): while the Planner is idle it is Send, disabled while the draft is empty; while the Planner runs with an empty draft it is Stop (a square) and stops it, under the permission rules Stop has always had, so someone who may not stop it sees a disabled Send, never a Stop they cannot press; while it runs and the draft has words it is Send, which posts the message exactly as Enter does (a message sent meanwhile is posted at once, the composer says nothing of waiting, and one that mentions chopin steers the turn that is running), and it is Stop again once the draft is sent or cleared. The accessible name and tooltip always say what the icon shows (Send message, Stop generating), and one polite live region says once when it changes, never for the tokens of a reply. There is no chevron beside it, because Chopin has no send options to put there. The emoji button has no state to draw yet and is not drawn, and there is no context-window ring, in this composer or a thread's, even where a harness reports token usage; where no files can be taken or no speech recognized, neither `+` nor the microphone is. Resume is its own ghost button beside Send.
  • Asking chopin: Who a message goes to is in its words, as in Slack: a message that mentions @chopin (in any case, anywhere outside a reference) asks chopin, and one without goes to the team. The composer draws no other state for it: no chip, no mode, no second placeholder. The sparkle button in the row (Ask chopin, its tooltip naming the chord) and Command+J or Control+J (listed in the shortcuts sheet, from the one registry of chords) write @chopin and a space into the draft, the same mention the @ menu writes and the words a person could type: at the caret when the field has it (the button keeps the field's focus and caret), otherwise at the start, set apart by a space on each side, with the caret after it. A draft that already asks chopin gets no second mention; the button only puts the caret after the first one (or at the end, for a command). The button is never spent and has no pressed state, since the mention in the words already says who the message is for. The mention is words: Backspace takes it out like any others, and nothing stays on after a message is sent. A draft kept from before, whose prepared send asked chopin without naming it, comes back with the mention it went with. The empty draft says Message the team, or @chopin to ask. Where the document has no Planner there is no button, and the chord does nothing.
  • Model and reasoning control: In the composer's toolbar, left of the other controls, assistant-ui's own model pill (ComposerModelTrigger, a 32px ghost pill with a chevron and no icon, copied unmodified) names the model that will run and, in tertiary ink, the reasoning level somebody chose for it. It opens assistant-ui's Model selector, also copied, over the composer: a search field, the models grouped by provider with a one-line description under each name (Default. leads the description of the harness's own), a check at the chosen one, and, only for a model that has levels, the Thinking row (Default first, called Model default when a level of the model would be taken for it (Default in any case), then the levels exactly as the harness reports them, xhigh as xhigh and High beside high as two, the reported string being what a turn carries; the interface-copy rule's sentence case applies to what Chopin writes, not to a value a harness reports). A level is a setting, so choosing one leaves the popover open, while choosing a model closes it and returns focus to the pill; Escape, Tab out or a pointer elsewhere closes it, and the keys are the element's own. The pill gives way to an ellipsis rather than push the other controls, on a phone as on a desktop, and the level stays in its accessible name and the open list. Nothing is drawn where the document's harness offers no choice or the person cannot write. Where a client withdrew its models, or an older one took its place, the picker is gone but a choice somebody saved has its words in the composer's notice until Chat records the fallback; while no client is attached, or one that offered its models has yet to report them, the notice says nothing of it until a turn has really fallen back, and a document nobody chose a model for never has one. A choice the harness no longer offers says what runs instead, in plain words with none of Chopin's own terms, in the trigger's name, in the popover and, before a turn records it in Chat, in the composer's notice. A turn that could not read the choice at all, the catalog or the stored choice, never runs the default silently: Chat says once, and every composer's notice says, This document's model choice couldn't be read, so chopin used its default model., until a turn can read it again. These words are the composer's own notice, not a banner: a cause of their own, drawn beneath the connection banner when the machine's banner is drawn (the machine went after the turn), and giving way, as the composer's other notices do, to the room's own connection banner, a refused send and a failed reply, and back with the connection.
  • Files and dictation in the composer: Images and text files and speech are the composer's own state, in hooks (useFiles, useDictation) that the surfaces take by props, so nothing that draws a chip or the microphone keeps state of its own. They fill the box's slots from assistant-ui's own elements, copied and recorded: the + at the start of the row (Add files, one of the 28px ghost buttons, with a hidden picker) attaches; the files wait above the text, under the toolbar, as assistant-ui's staged chips (ComposerAttachments and ComposerAttachmentChip of its Composer) drawn the way Slack's composer draws them, from outside (chat/media/media.css), 8px apart on the page's surface with a 1px hairline and 12px corners: an image is a 64px square of its picture, with Slack's ring of how far it has uploaded over it (the petrol arc on a hairline circle) and its name and percent kept for a screen reader; any other file is a card 64px tall, as tall as an image's square, of its type's 40px coloured icon (see Chat files), its name in bold and its size, or the percent with a 2px bar along its foot while it uploads; each has its remove button, a 20px ink circle, on its top-end corner, shown on hover or focus and always on a touch screen; a file that cannot go keeps the card's layout, whatever it is, so its reason has room. An image too large to send whole (over CHAT_ATTACHMENT_MAX_BYTES, or more than 40 million pixels) is made smaller in the browser before it uploads, at most 4096 pixels on its longest side, then narrower until it fits (a JPEG stays a JPEG and anything else becomes a WebP), so it is never refused for its size, and the viewer says so; the microphone is assistant-ui's own round voice button (ComposerVoiceButton, 32px, ink and a square while it listens) in the row after the sparkle, and while it listens the element's voice (ComposerVoice: the pulsing dot, the waveform, the time so far, Transcribing while a recording is worked on) sits above the text where the files do; a sent message draws any image attachments through Chat's image elements (see Chat images) and its other files as Chat files. A paste into the field and a drop anywhere on the box add files too, the box's edge taking the accent and saying Drop files to add them while a drag is over it; where the server takes none, there is no +, and the composer is as it was. Three things the chips have no place for are recorded extension points: the id of the file (two pasted screenshots are both image.png), the picture of an image in the box of the icon, and what sits beside the status, which is Chopin's own icon buttons to try an upload again and to remove a file that did not go. The chip is the one place a reason is said: a file the browser or the server refuses (a type, a size, a count or a total) is the chip in its error state, its reason in the danger text, with no way to try again and the send held back until it is removed, and a screen reader hears it once; a file the document's model cannot read is shown the same way while the message is for Chopin and is fine again when it is for the team, and + says so in its own name and description and offers text in its picker. Images go only to a model that reads them. Mention docs, which + used to be, is the document glyph beside it. A message sent before its files have uploaded says Sending once your files have uploaded and goes by itself; if one fails it stays in the composer with its text; a message of only files is sent by the round button, which stops Chopin only when there is nothing to send. Dictation exists only where the browser recognizes speech on the device or the deployment set up transcription, and never takes the field's focus; what is being said is drawn in the tertiary ink beside the caret and is never part of the draft, and what is final is written at the caret as typing would. Where motion is reduced the dot and the shimmer hold still. Why it stopped is the composer's own strip, an alert with a Dismiss, and the polite live region says only that it started and stopped. Words about either use none of Chopin's own terms, and composer.test.tsx keeps it so.
  • Chat threads, context usage and compaction: Any delivered Chat message can open a side thread (Reply in thread). On desktop split layouts, opening a thread splits the Chat area the way Slack splits a channel and a thread: the thread panel takes roughly 40% of the combined Chat and Thread width (360–560px) to the right of main Chat (at least 420px), separated by a 1px divider and its own draggable resize handle (Resize thread). The document pane yields width first down to 480px; when the window cannot hold all three panes at their minimums, the document folds into a compact top strip keeping its title, presence and a Document button that closes the thread and restores the sheet, while Chat and Thread share the space cleanly. On phones and compact viewports, a thread opens as a full-height sheet with Back to Chat; pressing Escape or closing returns focus to the message control or summary row that opened it. In main Chat, Reply in thread is a button of the message's toolbar (see Message toolbar), the root message's row carries the selected wash while its thread is open, and once a thread has replies it shows an inline summary button: up to three replier faces in first-reply order, N replies in petrol, and the relative age (Last reply 2m ago, giving way to View thread › on hover or focus). Inside the thread panel, a quiet provenance line (Forked from Chat at 3:24 PM. chopin remembers everything up to here.) leads the root message, a hairline N replies divider separates the replies, and the thread reuses the main composer through a single boundary with an Also send to Chat checkbox underneath. A harness's token usage is not drawn: neither composer has a context ring (ComposerContext, still in the copied elements-composer, is not used), and elements-context-breakdown appears only on the design-audit plate. When a session compacts context, a quiet inline spinner (Compacting earlier messages…) gives way to an expandable hairline divider (Earlier messages were summarized) built on Unfold with message and saved-token counts as spaced siblings.
  • Machine fix-it steps: One component (fix-steps.tsx, its words from machine-fix.ts), drawn at the top of the machine button's menu and under How to fix in chopin's ephemeral row, and loaded only once a machine has something to fix. A bold title in the base role (Can't connect to Build box), one plain sentence of why in secondary ink (the machine's reported line in words: A program it needs can't be found on it., It runs an older chopin that can't say why it stops., else It stops each time before it connects.), then numbered steps, a quiet tabular number in the gutter beside one action each in primary ink, a command never in the sentence but alone under it in a well like Chat's code blocks (the inset surface, a hairline, the large radius, monospace in the extra-small role, wrapping at a space or within a long word, never wider than the menu) with a 24px ghost Copy button that turns to a success check for two seconds and says Copied to a screen reader. Only the viewer's own machine is told to run something (On Build box, see why it stops: chopin status); another's is Ask @mona to run this on their machine:. The last step says nothing needs pressing (Wait a moment. with It connects again by itself once it's fixed. in secondary ink under it), and, where the viewer may choose, Or choose another machine below. (in the note, Choose machine opens the menu). A hairline sets apart the foot: Show details at its start unfolds, under it, the machine's own words and when it was last tried (Last tried at 6:29 PM.), exactly as reported, in quiet monospace; at its end, Learn more in petrol. How often it was tried is never said, here or anywhere a person reads: a machine is connecting, connected or can't connect. No alarm colour: nothing is lost and Chopin keeps trying. Inside the menu every control is one of its items, so the arrow keys reach them, and on a phone's sheet they work without closing it. The menu is a little wider (23rem) while it holds them.
  • Message delivery: Nothing a person sends to Chopin is held for later, so nothing about a message waits, is queued or carries a clock or ticks. A person's own message to the document's machine enters Chat at once as an ordinary message and the composer empties; while a turn runs it is given to that turn and reads as any other message, where it was sent among the Planner's own lines. While only this browser has it, because its own connection is not back or a starting machine is still attaching, it says Sending in the tertiary ink at the line's end, faded in after a moment so that a send the server answers at once never shows it; once the server has it there is no mark. When the machine is away, still reconnecting in its hand-over window (Still connecting to @mona's machine, so this message wasn't sent. Send it again in a moment.), or unattached, or when the running turn cannot take a message (chopin can't take messages while it works on this machine. Stop it to ask now.), and the composer holds no newer draft, the typed words return to the draft (with the one button offering Stop while Chopin works, then Send once stopped), so nothing typed is lost, and chopin says why to this person alone, as Slack's ephemeral messages do: the last row of the transcript, under a quiet Only visible to you with a small lock over the text column, is chopin's ordinary row (its mark and presence, its name, its tag and the time) with the server's sentence in the message role and, where the machine has something to fix, How to fix in petrol after it, which unfolds the fix-it steps. Nothing about it is an error or an alert: the composer keeps its own edge and it is said politely, once. It stays until chopin takes a message or another refusal takes its place, and goes with the page. When a newer draft is already in the composer, or another refusal turns the message down, the message says so on itself: a warning glyph, Couldn't send. in the destructive ink with the reason in plain words (for a machine that is away: @mona's machine is offline, so this message wasn't sent. Send it again when it's back.), and Try again and Delete in its menu (see Message toolbar), which act on this browser alone. While the page's offline notice shows, both give way to it and return with the connection.
  • Message rows: Chat lays a message out as Slack does (c-message_kit__gutter), in Chopin's colours: a 36px rounded-square avatar (a person's face, or Chopin's mark for the Planner) 8px from the text, the name in bold with the time in the extra-small role and tertiary ink beside it, and the text under it in the base role (15px on about 22px, as Slack's c-message_kit__message). Consecutive messages from one person are grouped: only the first shows the avatar and name, and each after it shows its time in the avatar's gutter while its row is shown. A shown row is shaded across the pane's whole width in the hover surface, square, reaching over the name line of the first message in a group; a message whose thread is open keeps its row in the selected wash, and a message jumped to is washed in the brand wash in the same square, full-width row, which then fades. Every one of these reaches the transcript's very edges, in Chat and in a thread, because the transcript draws its own scrollbar over its rows, as Slack's message pane does, and sets no lane aside for one: the system's bar is hidden on every engine (forced colours keep it), and a 6px round thumb inset in a 10px lane that stops 6px short of each end, in the strongest scrollbar ink (4:1 or more on the ground and every shade and wash), shows while the pane is scrolled or pointed at, darkens to the tertiary ink under the pointer and while dragged, and sits over the rows' end, clear of the toolbar 12px in; keyboard scrolling, anchoring and screen readers are the scroller's own. Above its first row the transcript keeps 20px more for that row's toolbar, which stands over the row, so at the top of a Chat the first message's bar, its emoji and their shadow are whole. Who speaks is said the way Slack says an app is one. A person has their GitHub picture and no tag. Everyone else is an app with a 36px rounded-square app avatar and, between the name and the time, a tag of its kind, after Slack's APP (c-app_badge): the word in the extra-small role, bold capitals, secondary ink, 4px inside a box of ink at a tenth with the small radius, so it reads the same on a shaded row. The Planner is Agent, named chopin, with Chopin's mark; a research child is Subagent, named Research, with a sparkle on the brand wash, in its own row when it says it is ready or could not finish and when its report card arrives; each tool call is Tool, beside its name on its line. What an app attaches to its message (the Planner's tool calls once opened, the handoff to research and the research workers' progress) is drawn as Slack's attachment (c-message_attachment): a 4px rounded bar in the edge colour down its start edge, petrol while the work runs, and the content 12px beside it. The handoff and the workers are assistant-ui's AgentHandoff and SubagentList, copied unmodified and recorded, filling the attachment's width.
  • Message toolbar: Every Chat message, a person's or the Planner's, has one floating box after Slack's (c-message_actions__container and c-message_actions__group): absolutely placed at the row's top right, 12px in from the row's right edge and 16px above its top, over the row's edge, so it never moves the text or changes the row's height. Nothing is set aside for it: every line of a message wraps only at the text column's end, and while the bar shows it may cover the end of the first line, or of the lines beside it on a message taller than the pane, where it is kept in view, as Slack's does. It is a rounded square (8px, --radius-lg) of the page's surface with a hairline ring in the edge colour and the strong resting shadow, 2px inside, holding 32px square buttons with 4px corners, Slack's 20px icons and no gap between them, each greyed in the hover surface under the pointer and named by the shared dark tooltip above it. This bar and the reaction pills are the deliberate exceptions to the no-shadow, no-bordered-box rule. It holds, in order, three quick reactions (✅ Looks right, ❓ Explain this, 👎 Not quite, each named and with tooltips by those words and a toggle with aria-pressed), Add reaction, Reply in thread (where the message can have a thread: it opens the thread, or starts it) and a vertical three-dots button, Message actions, and nothing else. Only one row shows its bar and shade at a time: the row whose menu or picker is open, else the hovered row, else the row with keyboard focus. The action row (MessageActions, elements-message-actions with elements-surfaces) and the overflow menu (DropdownMenu, shadcn's dropdown-menu on Base UI Menu) are copied from their registries into src/components (THIRD_PARTY_NOTICES.md and src/components/sources.json) and carry only a recorded extension-point patch (message-actions.tsx.patch) for Chopin's items, menu trigger and copy-to-check icon swap; Chopin's .chat-message-toolbar class on the copied row gives it Slack's box, and the menu keeps its registry geometry. The menu offers Copy (the stored Markdown, character for character), Copy as plain text (what the page shows: Markdown syntax gone, tables, strikethrough and task lists rendered as text, a link as its text, list items on their own lines, @handle and the Planner address as drawn), then Try again and Delete for a message the server refused. Copying swaps the menu item's copy icon for a check (use-copy-to-clipboard, MIT; see THIRD_PARTY_NOTICES.md) and its label for Copied, the menu closes when that ends and says Copy again, and a polite live region says Copied once. The bar shows on hover, on keyboard focus (one tab stop, arrow keys inside, a toolbar) and while its menu or the emoji picker is open; on a touch screen it comes from a long press, sits wholly above the message in a lane of its own so it never covers its text, and keeps its 32px visible buttons with 44px invisible ::after touch targets. Add reaction opens Slack's emoji picker (see Emoji), and the picker takes one emoji typed or pasted, kept exactly as written. Reactions sit under the message as Slack's pills (c-reaction): 24px tall with round ends, 6px inside, the emoji and its count 2px apart, on the inset fill with a hairline ring in the edge colour; your own is in the accent's wash and ring with its count in bold, and a pointer on the row finds Slack's add-reaction pill (c-reaction_add, 34px wide) after them, a shortcut to the toolbar's Add reaction that a keyboard and a screen reader reach there instead. Who reacted is in the tooltip and name; pressing a pill toggles yours. They are stored per message and per person, live for everyone in the room, and shown at once as pending (dimmed) when only this browser has the message or the connection is down, then sent when it can be. The server takes one extended grapheme cluster of at most 64 bytes as an emoji and 20 different reactions from a person to a message; one it turns down is not shown, and the message says Couldn't react and why, in a polite status line that goes at the person's next reaction.
  • Markdown text: Read-only Markdown (Chat messages, a decision card's text and the phrases in it) is drawn by MarkdownText, copied from assistant-ui's registry (@assistant-ui/markdown-text, with tooltip-icon-button, use-copy-to-clipboard and shadcn's tooltip) and recorded in src/components/sources.json. Because markdown-text has no standalone registry entry and Chat has no assistant-ui runtime, a recorded patch (markdown-text.tsx.patch) forwards props, sets data-slot="markdown-text", and exposes CodeHeader's button slot and defaultComponents, while tsconfig.json and vite.config.ts map @assistant-ui/react and @assistant-ui/react-markdown to Chopin's props-driven react-markdown seam (src/chat/assistant-ui/markdown-primitive.tsx). Its elements keep upstream's aui-md-* Tailwind classes, sizes, borders and radii on the generated geometry layer and take only Chopin's colours through src/assistant-ui.css; a decision card opts out of that root slot so its prose and diagram controls keep the document's fluid scale and focus ring. A surface passes only what differs: Chat draws a heading as a line of text and links open in a new tab; a card reads no tables, strikethrough or autolinks except in an option's own visual, draws a Mermaid fence with the document's renderer through componentsByLanguage, and loads only the document's own images. Chat's messages also read GitHub's tables, strikethrough (two tildes) and task lists, whose boxes are named Task; footnotes and bare addresses stay plain text there, as they always were. A fenced block in Chat is Slack's code block in Chopin's colours, restyled from outside the copy by Chat's own unlayered rules (src/chat/markdown.css) as Chat's images are: one box the full width of the message's text, a hairline in the edge colour, the large radius and the inset ground, its one slim header row (upstream's CodeHeader, 4px above and beside its 24px TooltipIconButton, no band or rule under it) holding the language at the start in the extra-small role and tertiary ink and the copy control (Copy code, then for three seconds Copied with a check, or Couldn't copy with a warning glyph in the danger icon role, which a touch screen shows without a tooltip) at the end, then the code at Chopin's code text role (the mono stack, the small text role on 1.5, tabs of two), padded 12px, whose long lines scroll inside the block in a lane the keyboard reaches, on a phone too. A block whose fence names a language is coloured with the document's own highlighter (@chopin/editor/code-tokens, Shiki through @pierre/diffs, loaded with the first such block), each token in one of Chopin's roles at 4.5:1 or more on the inset ground: keywords in petrol ink, functions and types in the merged plum, strings in the success ink, numbers and constants in the warning ink, comments and punctuation in the tertiary ink, a diff's added and removed lines in the success and destructive inks; a block that names none, or names plain text, stays as written, and a block that streams in colours only the lines it gains. A card's code has none of this. A table scrolls inside its own lane. Raw HTML never reaches the page as markup: Chat drops it and a card shows it as the text it was written as. The collaborative editor is not drawn this way. A document shown read-only (an earlier version) stays in the editor's own read-only view, because it carries decisions, diagrams, comments and tables the dialect defines and MarkdownText does not.
  • Touch targets: A control keeps its visible box on a touch screen. Where its box is under 44px (--control-target-coarse) an invisible ::after of that size grows around it, so a finger just outside the control still reaches it, and the control never gets bigger, taller or spaced differently to make room for one. This applies only when the primary pointer is coarse ((pointer: coarse), mirrored as data-plan-coarse-pointer): a touch-screen laptop used with a mouse keeps the desktop's sizes. Stacked menu rows keep their own 28px, and a row that follows another stands 44px from it (one margin, which is the height the rows no longer take themselves) so the menu is as tall as before and neighbouring targets meet without overlapping. A control that clips its own overflow cannot grow a target past its edge. Copied assistant-ui elements follow the same rule through the generated layer.
  • Cursors: The cursor says what a thing does, as Slack's does. Whatever a click acts on and is not text entry shows the pointer: buttons and icon buttons, links, menu and list rows, tabs, toggles and checkboxes with their labels, chips and reaction pills, toolbar buttons, the machine button, and the copied assistant-ui and shadcn elements (shadcn's menu and command rows ask for the arrow, and Chopin gives them the pointer by their data-slot, never by editing a copy). A picture that opens the viewer shows the zoom. Editable text shows the text cursor (a field computes text; the draft and the document keep the browser's auto, which is the I-beam over editable text), and static text keeps the browser's own cursor, never the pointer. A disabled control keeps the arrow (not-allowed only where a copy already asks for it). What drags shows the open hand (grab) at rest, a divider the resize cursor (col-resize), an image's corner its diagonal; while it is dragged the whole page shows the drag's cursor (grabbing for a grip), so it does not flicker to the text cursor or the arrow when the pointer outruns the handle: holdDragCursor lays one transparent layer carrying it over the window for exactly as long as the drag, because a rule giving every element the cursor restyles the whole page when it switches on. The rules are src/cursors.css, in the base layer so a component's own cursor wins; it loads with the tooltips, before anything is drawn, rather than in the entry stylesheet. e2e/design/cursors.e2e.ts and e2e/cursors.e2e.ts read the cursor under the mouse.
  • Emoji: Every emoji is the platform's own colour emoji, drawn as text and never as an image, as Slack's native emoji setting draws them. The text stack lists Apple Color Emoji, Segoe UI Emoji and Noto Color Emoji right after Inter (--font-sans), so a letter is always Inter's and an emoji never falls to a generic family that draws it as a black outline (Linux's sans-serif is DejaVu Sans); where only emoji are drawn (a reaction pill, the toolbar's quick reactions, the picker) --font-emoji puts those fonts first, then the text stack. In a message an emoji is larger than the words (the xl role beside the base, Slack's 22px beside 15px), on their baseline and without making the line taller, and a message of nothing but emoji, up to Slack's 23, draws them in the 2xl role (Slack's jumbomoji); a character with a text form written without the emoji selector (©, a plain ❤) stays text, and code keeps its characters. The emoji picker is Slack's in Chopin's colours: a 360 by 420 popover at its button (a sheet along the bottom edge on a phone, up to 480px tall) with the popover's radius, hairline and overlay shadow, appearing with a short fade that reduced motion leaves out; a search field with a magnifier, focused on open, that filters by name, keyword and Slack's short name and says No emoji found; a row of nine category tabs in Chopin's icons (Frequently used, Smileys and people, Animals and nature, Food and drink, Activities, Travel and places, Objects, Symbols, Flags) whose petrol bar follows the scroll and which scroll to their section, smoothly unless motion is reduced; sticky bold headings over nine columns of emoji in the xl role (Slack's 22px) in square cells with the medium radius, greyed under the pointer or the keyboard; and a footer that shows the emoji under the pointer or the focus in the 2xl role with its Slack name (:+1:, :wave::skin-tone-4:) beside the hand that opens the six skin tones, whose choice applies to the grid and is kept in the browser. Frequently used comes first: two rows of the emoji this person reacts with most, the latest among equals, kept in their browser, filled from Slack's own first picks (👍 ✅ 👀 🙌 🎉 😂 🙏 ❤️ 🔥). The keyboard reaches everything: Down from the field enters the grid, arrows move by column across sections and Up from the top row returns to the field, Enter picks, Left and Right move between the tabs, Tab moves through the field, the tabs, the grid and the hand and closes the picker past either end, and Escape closes it and returns focus to Add reaction. The catalogue (emojibase-data, MIT, with iamcal's names, which Slack uses) loads the first time a picker opens.
  • Icons: Chat has one icon language, Chopin's own (packages/icons): line icons on an 18-unit grid with a 1.5 stroke and round caps and joins, drawn at 18px in the composer's 28px buttons and at Slack's 20px in the message toolbar's 32px ones. The icons assistant-ui's and shadcn's copies import from lucide-react are drawn with Chopin's own through src/chat/assistant-ui/lucide.tsx, which tsconfig.json maps that package to, so the copies keep their bytes and classes and no lucide artwork ships. Icon size is contextual: the container chooses it, not the call site. 16px is the default for menus, medium and large buttons, icon buttons and standalone interface icons. 14px sits beside small text: badges, pills, capsules and small buttons. 12px is for compact controls: extra-small buttons and the compact sidebar Search, disclosure, add-project, document-creation and document-action controls, which keep 24px targets. Icons in one row share one size and one column.
  • Badges and status graphics: Neutral, success, warning, and danger use paired semantic surface, icon, graphic, and text roles. A badge combines an icon and label in a pill. Merged (plum) is the fifth quad, reserved for finished GitHub work: a merged pull request or a completed issue, as GitHub colours them.
  • GitHub reference pills: A pull request or issue link in a document is a tinted capsule over the link itself: the state surface mixed half with the page, a hairline at 12% of the state icon colour, a 14px state glyph, the authored title in primary ink and a never-truncated #number in tertiary ink. Open is success, merged and completed are merged, closed is danger, and draft, not planned, loading and no access stay neutral.
  • Document: The authored page is white, readable, and wider only where content requires it. Headings, lists, tables, code, and callouts retain document semantics inside the editor, and edit as Notion's do: the / menu leads with the basic blocks (Text, Heading 1 to 3, Bulleted and Numbered list, Quote, Divider) before the blocks only it makes; Markdown shortcuts and --- make blocks as they are typed; Mod+Shift+ArrowUp and ArrowDown move the block holding the caret, Mod+D copies it, and Mod+E makes inline code. A code block is edited where it is read, as Notion's is: one surface, its own text coloured in place by the shared highlighter (Chat's inks, painted with CSS Custom Highlights, never written into the editor), with a code editor's keys inside (Enter is always a line, Tab and Shift+Tab indent every selected line, Mod+A selects its code first) and a document's at its edges (Escape, Mod+Enter, and the arrows at its end where nothing follows leave it; Backspace in an empty one makes it a paragraph; ``` with a language and Enter or Space makes one). Long lines scroll in the block's own lane until Wrap lines; for a reader who cannot edit, that lane takes keyboard focus. Only a patch keeps a drawn preview beside its source. A code block's controls sit in its top-end corner: Wrap lines, a diagram's or patch's source toggle and Copy code come on hover or focus beside its language (always shown on touch). Copy code is a Chat code block's control in the document's ghost icon button: it copies the block's source exactly as written, for readers too, then says Copied with a check, or Couldn't copy with a warning glyph in the danger icon role, for three seconds.
  • Document images: Click opens a full-screen preview with zoom and close controls. Hover or keyboard focus reveals metadata editing and corner resize handles. Resizing preserves the image ratio, shares the chosen width with collaborators, and remains bounded by the document or column on narrow screens.
  • Chat images: Every image in a Chat message — an agent turn's image part, one written in Markdown, or an image a person attached and sent — is drawn one way, the way Slack draws images, through assistant-ui's copied Image and ImageGallery elements (recorded in components/sources.json with their extension-point patches; see docs/assistant-ui.md and THIRD_PARTY_NOTICES.md), restyled from outside by Chopin's own unlayered rules (chat/image.css) to Slack's proportions in Chopin's colours, and driven by Chopin's state through props with none of assistant-ui's runtime. A single image is a block under the message: its name (the alt text, an attachment's file name, a missing alt reading Image or Generated image, with a small Generated label and sparkle where the Planner made it) on a line of its own in the compact role and tertiary ink 4px above it, as Slack's file meta line is, whole however long; then the picture in a frame with a 1px hairline (--color-button-outline-edge) and 8px corners, fitted whole inside Slack's 480 by 360 and never drawn larger than it is; then any caption in tertiary ink. Its shape is held from the first paint where it is known (stated, or read from a data address's own bytes, turned a quarter where the file says so), so nothing moves when it arrives; one whose shape is not known yet holds Slack's quiet 360 by 240 box on the inset ground, whose pulsing glyph holds still under reduced motion. An upload is read from the server's copies at Slack's thumbnail widths (?w=480, or the narrowest of 360 to 1024 that fills the box on the screen it is on), never as its whole original; another address is drawn as written. The image's own toolbar sits on it, 10px into its top-right corner, as Slack's does, and the message row's toolbar stays on the row: the page's surface with a hairline, a 2px inset, 8px corners and the resting shadow, holding 32px icon buttons with 4px corners for Open image, Copy image (a check for a moment), Download image, and, for an address on another site, Open image in a new tab; it fades in over 100ms on hover or keyboard focus, is held back while the image loads, and stands beside an image narrower than itself rather than over it. Pressing the picture opens the viewer. An address on another site is always drawn from its own address: once a pointer or focus reaches the image, the page probes it once without credentials or referrer to learn whether its host lets a page read its bytes; until then, and where the host does not, copy reads Copy link and download becomes an Open image link in a new tab without opener or referrer; where it does, copy and download work on the bytes and say Couldn't copy image or Couldn't download image if the read then fails. Chopin's server never fetches an address a person wrote. A path on this site that is not an upload is read without credentials and drawn only if a raster image answers. Several images of one message (from an agent turn, sent attachments, or written together in Markdown side by side or in consecutive image-only paragraphs) are Slack's grid, at most 480px wide with 4px between tiles of 8px corners on the inset ground: two side by side as squares, three as one tall tile beside two stacked squares, four or more as two rows of two whose fourth says +N for the rest, which the viewer holds. Each tile is a button named Open image: <alt> drawn from the copy for its size. An image that cannot load is Slack's placeholder in the box the image would have taken, never smaller than 160 by 96 so it can say so: the inset ground, a 24px off-image glyph and a plain sentence, Couldn't load image for a broken address or a non-raster answer, Image can't be shown for an address Chat never loads (sign-in and sign-out paths, cross-document uploads or attachments, SVG, script), with the image's name on its line above; it is one role="img" named by the reason and the name (Image can't be shown: Bar chart of weekly signups) and has no toolbar. A tile that cannot load says the same in its own square. No image is ever said to be too large: an image too large to store or send is stored as a smaller copy of itself (see Files and dictation in the composer and Storage). A turn's image that could not be stored for another reason appears inline in order as a refusal line with an off-image glyph, spanning the grid's columns when among tiles. Reference images (![alt][name]) resolve from their first definition, Markdown image titles become captions, and linked images lift out of their link so pressing the picture opens the viewer while an image-only link keeps an accessible name from the alt text or URL.
  • Chat files: A file sent with a message is Slack's file card: assistant-ui's received-files tile (MessageAttachments, copied, with a recorded patch for a file's own icon, its type and its actions) restyled from outside (chat/media/media.css) to a card at most 400px wide and 64px tall, on the page's surface with a 1px hairline and 12px corners and 12px padding, stacked 8px apart. It holds a 40px rounded square in the colour of its type with the type's 24px glyph from Chopin's icons in the page's colour (PDF the danger graphic, a document petrol, a spreadsheet success green, slides warning amber, code secondary ink, text the icon grey, an archive warning ink, an image plum, a video deep plum, audio lime, anything else tertiary ink; the type is read from the name, since Chat keeps every text file as plain text), the name in bold primary ink, and under it the type in words (PDF, CSV, TypeScript, Markdown, File) and the size, as spaced siblings in tertiary ink. Pressing the card downloads the file under its own name. On hover or focus its own toolbar, like an image's, fades in at its end: Download and Copy link; on a touch screen it is always there.
  • Chat image viewer: All showable images of one message share one viewer, Slack's full-screen image viewer on Chopin's ink ground (--color-text-primary, its words in the page's colour): ImageGallery's copied <Dialog>, with a recorded header slot, restyled to fill the window with no corners and no growing entrance. Along its top, who sent the message (their face, their name in bold), when, and the image's name, and, where its sender's browser sent a smaller copy, Shown smaller. The original was 22.7 MB, 12000 × 8000.; at its top end Copy image, Download image (of the stored file, never a copy) and Close. The image is centred between the top and the caption, drawn from the largest copy (1024px). Round translucent Previous image and Next image buttons sit halfway down the window's edges (at its foot on a phone), an arrow with nowhere to go not drawn, and ArrowLeft and ArrowRight step too; Image M of N is said once per step. Focus is trapped and wraps, Escape or pressing the dark ground closes it, and focus returns to what opened it. Reduced motion shows it at once.
  • Image lightbox: Document images and decision-card visuals open the editor's full-screen preview on the ink ground, with zoom and close controls at the top end and, when several images are present, a footer with Previous, alt text, caption, count and Next, focus trapping and return, and reduced-motion support. Chat images use Chat's own viewer instead (see Chat image viewer).
  • Decision decks: Adjacent decisions stack as a deck. The top card is live; up to four cards behind it show as blank, inert edges, each nearer one showing more of itself. A capsule pager under the stack (previous, the top card's place, next) and Left and Right arrows page through the deck and stop at either end. Next drops the top card a short way as it fades and the card behind rises into its place with its content fading in; previous slides it back. Answered decisions settle to the back and keep an open card's full layout, with the chosen option selected. Moves animate transform and opacity, plus a clip and the leaving card's height so a taller card never covers the pager. Reduced motion crossfades without movement. Deck order is each reader's own. On desktop, Decisions shows open decisions as the same deck when two or more wait, with resolved decisions listed below in an accordion that opens by default; phones keep the single-pane list and save bar. A blank document whose Planner asks decisions opens Decisions once per visit, unless the reader is editing the document.
  • Unfold: UnfoldSection uses Base UI Accordion for plain disclosure; card previews use the shared Unfold primitives. Both use the Chevron icon from the icon system. Native details and summary elements are not used. A button with aria-expanded and aria-controls folds a mounted panel open through its grid row while the content fades and tips down from its top edge, with the shared ease-out: card previews take the linger duration, plain accordions the accordion duration. A second press reverses from the current frame. Closed content is inert and hidden once the fold ends. Reduced motion snaps the height and keeps a short crossfade. A card preview never replaces the card's own open action.
  • Offline bar and failure banner: One family adapted from assistant-ui's standalone ConnectionState and ErrorState elements (see THIRD_PARTY_NOTICES.md), restyled to Chopin's roles. Connection and machine state stay quiet, as Slack's do: the page's connection is drawn only once its socket has been lost for ten seconds (OFFLINE_BAR_GRACE), so a drop that recovers sooner shows nothing, and the machine is never drawn in Chat at all. The offline bar is one thin line (32px, the extra-small text role, a 16px icon) across the top of Chat's transcript, resting over it so no row moves, or at the top of the document while Chat is not on screen; on a phone it rests just under the top bar. Connecting… Your changes are kept. is the neutral pair with the connecting dots; You're offline. Your changes are kept. is the warning pair with a btn-xs outline Reconnect, offered once automatic attempts have failed four times, which by ten seconds they usually have. Catching up and coming back draw nothing: the bar fades out (opacity alone, 120 ms) and nothing takes its place; a restart the page is waiting out in silence shows nothing. A screen reader still hears each phase once, the return included (Back. The reply kept going. 3 new messages), and the machine's own states, from the page's one polite live region. Edits that could not be saved are a data-safety notice of their own and stay drawn. The failure banner is the composer's attached top strip: the common interface text role, the extra-small role for reasons, btn-xs outline buttons, 16px icons and the semantic surface, icon and text pairs. It is the danger pair with Retry for a reply that ended without finishing (a model or harness error, a timeout, an agent that could not be reached, a tool it needed), in a machine's plain words with the reason exactly as reported; one that was refused has no button, and Retry runs the same message's turn again (chat:retry): nothing new is sent, the failed attempt (its partial reply and the failure line) leaves the transcript for everyone in the room, the banner reads Retrying until the turn starts, and a Retry the server refuses keeps the banner with the refusal's words as its reason. A message chopin could not take is its ephemeral row, not this banner (see Message delivery). A dropped link is never the failure's to tell, and the failed reply's own line leaves the transcript while the banner tells it. Both enter once, with a fade and a short slide (the bar's entrance is --duration-base with the smooth ease-out); reduced motion shows them at once and takes them away at once. Neither is a live region of its own, except that a failure alerts once.
  • Connecting: One indicator, ConnectingIndicator, wherever a machine or the room's connection is being connected to: the offline bar's icon while it reconnects, and the machine button's badge while its Planner starts. It is three round petrol dots drawn by CSS in the 16px icon box (a capsule cut from the surface on the button's corner, with the dots a little smaller) that brighten in turn from left to right, the calm of Slack's typing dots rather than a spinner: only opacity moves, over 1.2 seconds with the shared in-out ease, so nothing turns, grows or shifts. Under reduced motion the dots hold still, stepped from faint to full, an ellipsis that still reads as under way. It is decoration, hidden from assistive technology; the words beside it, the button's name and the page's one live region say what is connecting. A spinner remains for work that is not a connection, such as a reply being retried.

Do's and Don'ts

Do:

  • Do use shared color, type, radius, spacing, shadow, and motion tokens when extending the interface.
  • Do use the established semantic color pairs for status and feedback.
  • Do keep the focus ring keyboard-only for buttons, menus, and the Chat composer (a click leaves it a quiet brand edge), and always visible on other text fields. Give a ring room to draw: clip only the axis that must clip.
  • Do slide the inline Projects sidebar and split Chat the same way for their shortcuts as for a click, so the space a pane takes or gives back is always visible; reduced motion and a layout change settle them at once. Other keyboard-driven changes (the Projects drawer, menus, dialogs, document views, compact panes, and count or icon feedback) still settle immediately.
  • Do check new controls in the development design audit alongside their default, focus, disabled, and error states.

Don't:

  • Don't introduce a raw color, font size, or radius in a component when an established token serves that role.
  • Don't create a second local definition of a shared token or shadow treatment.
  • Don't join inline metadata with a middle dot, bullet, or a swapped-in glyph such as a slash or pipe; use spacing between elements, or drop the metadata.
  • Don't give a menu or popover its own padding, item height, radius or divider; build it from the shared menu parts.
  • Don't pick an icon size per call site when its container already sets one.
  • Don't add a navigation item that jumps straight to a full page when a popover could answer the common question first.

Shared component recipes

Important visual dimensions and colors belong to the shared theme. Use finite component variants for density, size, typography and state; avoid selector overrides that reach into another component or anonymous child. Browser geometry remains measured at runtime.

  • Count owns quiet, actionable and control appearances. Quiet sidebar counts use --color-count-quiet (gray-50); selected control counts use the page surface. Control counts use an 18px minimum square and allow wider values without clipping. Counted buttons use the shared 4px control inset.
  • IconButton owns normal and compact sizes, default and compact glyphs, and ghost or nested hover surfaces. Compact sidebar buttons are 24px with a 12px glyph; the common icon catalog also includes micro, default and larger roles. Coarse pointer targets use --control-target-coarse.
  • MenuItem owns the Nova item: shared height, padding, gap and radius tokens, with normal and compact density now identical. Destructive text and icons inherit the same semantic ink. MenuSeparator owns the edge-to-edge divider and its spacing.
  • Button utilities own their icon size through --btn-icon-size; a button's glyph never sets its own size.
  • EmptyState owns circular tinted icon frames, title and description type roles, copy measure, icon gap and optional framed presentation. Decisions and Chat use its compact density.
  • Document measure, gutters, top inset, leading and decision marker size are shared document roles. Marker positioning measures the rendered disc, so token changes cannot desynchronize the visual size and geometry.
  • Panel headers and the document title split control use shared heights and insets. Breadcrumb text uses medium weight; the split label and compact action produce an 8px text-to-chevron gap. Project names in document breadcrumbs are plain text; document title and menu controls share a single hover surface.

SeeCode uses a fixed canvas typography catalog in packages/diagrams/src/core/tokens.mjs: node labels 14px, subtitles 12px, tags 10px and edge labels 12px. Measurement and SVG drawing variables derive from the same catalog, so changing type updates boxes and routing together. These are coordinate-system roles; interface controls retain the fluid UI scale. Architecture graphs may use a narrower derived layout, targeting the smaller of 1100px or the document lane, while preserving saved source, node identities, connections and playback. Fitting has a readability floor; dense diagrams keep native scrolling and zoom controls below the diagram.

InlineNotice reserves space in the page flow for recoverable failures, using the shared semantic surface, icon and text roles. Navigation retry controls align within it; notices never cover document content. A lost connection is one notice, the offline bar, described under Components: it rests over the top of Chat's transcript while Chat is on screen, and at the top of the document's content (or above the panes where Chat and the document take turns) while it is not, never in both. The page has no second offline notice, and nothing in Chat speaks of the machine's own connection. Its grace period is counted only while the page can be looked at: a hidden tab's timers run once a second and a frozen tab's not at all, so such a tab says nothing of being offline however long its connection takes to come back, and counts the grace whole from when it is shown, thawed or restored while the loss lasts; a banner already shown stays. The phase is decided once, above Chat and the document, so switching between them neither restarts the grace period nor announces it again; on a phone the document's bar floats just under the top bar, and its height is added to the panes' top inset so it never covers the first of their content. A workflow run card and a message that could not be sent say nothing of their machine or the connection while the bar shows: a run whose machine had dropped reads as it last reported, with no disconnected, no note and a plain Pause, under a still neutral ring, and the words come back with the connection. A server's refusal of a send is told again, in the same words, once the connection returns.