Composites
Chat
A virtualized controlled conversation with tail-follow, unread behavior, in-place edit, and branch versions.
Starts when it scrolls into view
NORD FROST · DARK · RADIUS 6/10 · BASE 16PX · SHADOW
Code
Cut from the gallery story this page runs. The states above change what the demo is given rather than how it is built, so they share this code.
let mut chat = Chat::new("gallery-chat", prompt, window, cx);
chat.set_welcome(
Some(
ChatWelcome::new("What should we look into?")
.description(
"Ask about suppliers, pricing, or delivery risk. Suggestions send \
immediately; the composer stays yours.",
)
.suggestions([
Suggestion::new("compare", "Compare supplier prices"),
Suggestion::new("risk", "Explain this week's delivery risk"),
Suggestion::new("draft", "Draft the order confirmations"),
]),
),
cx,
);
chat
let subscription = cx.subscribe_in(
&chat,
window,
|this, chat, event: &ChatEvent, window, cx| {
this.last_event = format!("{event:?}").into();
let prompt = chat.read(cx).prompt_bar().clone();
match event {
ChatEvent::SuggestionSelected { suggestion_id } => {
let draft = match suggestion_id.as_ref() {
"compare" => "Compare supplier prices",
"risk" => "Explain this week's delivery risk",
_ => "Draft the order confirmations",
};
prompt.update(cx, |prompt, cx| {
prompt.set_draft(draft, window, cx);
});
this.show_welcome = false;
}
ChatEvent::BranchSelected { message_id, index } => {
if message_id.as_ref() == "latest-question" {
this.question_version = *index;
this.question_text = None;
this.answer = None;
}
}
ChatEvent::EditSubmitted { message_id, text } => {
if message_id.as_ref() == "latest-question" {
this.question_text = Some(text.clone());
this.answer = None;
}
}
ChatEvent::EditCancelled { .. } | ChatEvent::AttachmentActivated { .. } => {}
ChatEvent::Prompt(PromptBarEvent::ModelChanged { model_id, .. }) => {
prompt.update(cx, |prompt, cx| {
prompt.set_selected_model(model_id.clone(), cx);
});
}
ChatEvent::Prompt(PromptBarEvent::Submit { .. }) => {
prompt.update(cx, |prompt, cx| {
prompt.set_progress(ProgressState::Running, cx);
});
}
ChatEvent::Prompt(PromptBarEvent::CancelRequested { .. }) => {
prompt.update(cx, |prompt, cx| {
prompt.set_progress(ProgressState::Complete, cx);
});
}
ChatEvent::Prompt(_)
| ChatEvent::RetryRequested { .. }
| ChatEvent::FollowUpSelected { .. }
| ChatEvent::CitationActivated { .. }
| ChatEvent::SourceActivated { .. }
| ChatEvent::MessageCopied { .. }
| ChatEvent::RegenerateRequested { .. }
| ChatEvent::EditRequested { .. }
| ChatEvent::FeedbackSubmitted { .. }
| ChatEvent::JumpedToLatest => {}
}
cx.notify();
},
);
Events and ownership
- Who owns the state
- Chat renders state supplied by the application; it does not own durable work.
- What it emits
- Interactive intent is reported through the typed ChatEvent contract and stable application IDs.
- What it means
- A virtualized controlled conversation with tail-follow, unread behavior, in-place edit, and branch versions.
- Overflow and motion
- Growing content remains reachable in a bounded vertical surface; reduced motion preserves a useful state.
Event types
- ChatEvent
What the demo does and does not show
The demo above is the same Rust as the native component, compiled to WebAssembly and drawn on a WebGPU canvas — not a mock-up, a recording, or a re-implementation. What it paints is what the component paints: layout, colour from the live theme, and motion.
The live browser specimen requires WebGPU; the native component remains the authoritative runtime.
A canvas cannot stand in for the platform, so treat everything that goes through the operating system as unproven here: keyboard handling and shortcuts, screen reader semantics, text input and IME, the clipboard, file drops, and the frame rate you would get on your own GPU. Judge those in a native build. If your browser has no WebGPU, the frame says so rather than showing an empty box.