Composer
Anatomy
The multi-state prompt dock for an agent session view. Server-rendered as a plain <form> wrapping a <textarea rows="2">, so it submits with JavaScript disabled — auto-grow, ⌘↵-to-send and voice capture are a JS layer the consumer adds on top.
Four states share one anchor line (the dock never moves): Ready (the 84px resting dock), Growing (focused, ringed, grows then scrolls), Executing (a turn is running; the field stays writable, and an Interrupt hollow-destructive appears), and Asleep (a 46px dashed bar whose "type to wake" field is a real single-row textarea, plus a Wake submit).
maud_ui::primitives::composer
Props
Generated from public Rust fields. Defaults are evaluated from the implementation.
| Prop | Type | Default | Description |
|---|---|---|---|
state | State | Ready | Which state to render. |
action | String | "" | <form action> — where the message posts. |
method | String | "post" | <form method> — defaults to post. |
field_name | String | "message" | <textarea name> — defaults to message. |
placeholder | String | "" | Placeholder shown when the field is empty. |
value | String | "" | Pre-filled field content — the in-progress draft. |
chips | Vec<Chip> | [] | Leading mono chips (model, auto-accept, attachments). |
control_chips | Vec<Markup> | [] | Interactive chips — each Markup is a FORM CONTROL (typically a <select name=…>) rendered inside the chip row, wrapped in .mui-composer__chip--control so it takes the chip treatment. Controls live inside the composer's form, so they submit with the message and work with JavaScript disabled. Rendered after chips. |
trailing | Vec<Markup> | [] | Trailing action slot — icon actions (retry, attach, …) rendered in the actions row between the spacer and the primary action. |
show_voice | bool | false | Show the voice-capture button before the primary action. |
flat | bool | false | Render the send button flat and chromeless: a transparent field with no border, the hairline ring carried by shadow alone (a dock sitting on a raised panel surface that brings its own ground). |
icon_only_send | bool | false | Icon-only send button — a 26px circle; the label stays in the DOM, visually hidden (.mui-sr-only), so screen readers keep it. Pair with a visible glyph drawn by the consumer's CSS or an icon slot. |
secondary_label | Option<String> | None | Optional secondary action label — renders a hollow-destructive button (the Interrupt affordance) before the primary. Only meaningful while executing. |
secondary_action | Option<String> | None | <form action> for the secondary button. When set, the button is a real submit targeting a sibling form (HTML5 form attribute), so the action works with JavaScript disabled. When unset the button renders type="button" and needs a consumer JS layer. |
primary_label | String | "Send" | Primary button label — Send, Queue, … |
primary_kbd | Option<String> | None | Optional kbd hint shown inside the primary button (⌘↵). |
status | Option<Markup> | None | Optional mono status line rendered below the field — the "beside" data (turn count, context, spend, worktree). 11px, muted. |
Examples
Ready — the 84px dock
The resting state. Welded to the bottom of the pane; the operator's hands never hunt for it.
Growing — focused, ringed, grows then scrolls
Two rows minimum; grows to a max then scrolls internally. The transcript loses the height, never the composer.
Executing — writable, not disabled
A turn is running, but the field stays writable so a follow-up can be queued. Interrupt sits beside the primary as a hollow-destructive.
Surface — a second way to write, in the same card
An alternate input surface (voice capture, say) takes the text area's place while it is shown; the text area stays in the form and comes back when the surface hides.
Asleep — 46px, dashed, same anchor line
The session is paused. A mono state tag and a neutral Wake button; still the same anchor line.
Usage and composition
Import
use maud_ui::primitives::composer::{self, Props, State, Chip};
Example
use maud::html;
use maud_ui::primitives::composer::{self, Props, State, Chip};
composer::render(Props {
state: State::Ready,
action: "/session/42/turn".into(),
placeholder: "Message refactor-auth-middleware…".into(),
chips: vec![Chip::new("sonnet-4.6"), Chip::new("auto-accept: off"), Chip::new("+ attach")],
show_voice: true,
primary_label: "Send".into(),
primary_kbd: Some("⌘↵".into()),
status: Some(html! { span { "12 turns · 41.2k ctx · $1.84" } span { "⇧↵ newline" } }),
..Default::default()
})
State enum
| Value | Description |
|---|---|
| Ready | The 84px resting dock. |
| Growing | Focused; field ringed, grows to a cap then scrolls internally. |
| Executing | A turn is running; field writable, Interrupt shown. |
| Asleep | 46px dashed bar: mono state tag, a real single-row textarea ("type to wake" is literal — it submits with JS off), and a Wake button. |
Chip
Chip::new(label) for a plain mono chip; Chip::accent(label) for the accent-tinted "carries a payload" treatment (attachments, e.g. user.rs ×); Chip::with_icon(label, icon) for a chip with a leading icon — plus trailing_chevron: Some(markup) for the caret of a menu chip.
Geometry knobs
--mui-composer-max— the field'smax-width(default67.5rem, the 1080px code measure).--mui-composer-max-h— the height the field grows to before scrolling (default40vh).
Progressive enhancement
The writable states are a real form: with JS off, the textarea submits and the primary button posts. Auto-grow, ⌘↵, esc-to-interrupt and voice are all additive.
CSS classes
mui-composer(root<div>) + one ofmui-composer--ready/--growing/--executing/--asleep; the form inside ismui-composer__form, the secondary targetmui-composer__secondary.mui-composer__field,__input,__actions,__chip(+--accent),__voice,__send,__kbd,__interrupt,__status.- Asleep:
mui-composer__sleepbar,__state-tag,__input--sleep,__wake.
Related
Turn progress (the strip above the executing composer), Segmented control, Textarea, Kbd.
Accessibility
Supply meaningful labels, choose heading levels for the surrounding page, and preserve native link and form behavior. Keyboard focus follows document order. Motion honors reduced-motion preferences.