Component reference
assistant-composer
Shown below inside the stories that use it. Its props and DOM parts follow.
Preview
assistant-composer has no story of its own; this one draws it as part of AssistantView.
Complete Assistant
Messages, safe rich text, process, draft, model selection, attachments, activity, work details and questions use one public component.
export function assistantDemo(): Child {
let next = 20;
const messages = signal<readonly AssistantMessage[]>(
Array.from({ length: 10 }, (_, index) => ({
id: `message-${index}`,
role: index % 2 ? "assistant" : "user",
author: index % 2 ? "Assistant" : "You",
timestamp: `10:${String(index).padStart(2, "0")}`,
parts: [
{
id: "text",
kind: "text",
text:
index % 2
? "The default Assistant keeps **messages, process and actions** on one reading axis.\n\nYou can read earlier messages while new results arrive. This example uses local view models, with no product API or execution runtime."
: "Review the shared Assistant layout and preserve my reading position.",
},
...(index === 7
? [
{
id: "process",
kind: "process",
title: "Review process",
state: "success",
statusText: "3 steps",
steps: [
{
id: "read",
title: "Read the layout",
description: "Checked the shared component contract and the reading region.",
statusText: "Reviewed",
tone: "success",
},
{
id: "check",
title: "Check interaction",
description: "Keep draft and focus while opening activity details.",
statusText: "Returned",
tone: "returned",
},
{
id: "decide",
title: "Wait for a decision",
description: "A returned execution does not imply the goal is complete.",
statusText: "Needs your answer",
tone: "warning",
},
],
} as const,
]
: []),
...(index === 9
? [
{
id: "context-note",
kind: "text",
text: "A local part override uses the same render tree.",
} as const,
]
: []),
],
})),
);
const draft = signal<AssistantDraft>({
text: "Keep this draft while reviewing the activity panel.",
attachments: [],
});
const model = signal("balanced");
const readState = signal<"ready" | "loading" | "error">("ready");
const disabled = signal(false);
const disabledReason = signal("");
const work = signal<readonly AssistantWork[]>([
{
id: "review",
title: "Review the reading experience",
statusText: "Waiting for review",
tone: "returned",
executionText: "Execution returned",
replyText: "Public reply not yet posted",
detail:
"## Review notes\n\nThe execution returned its findings. The goal is still open and the host has not published the final answer.",
steps: [
{
id: "note",
title: "Preserve accurate status",
description: "No execution state is inferred by the UI.",
statusText: "Returned",
tone: "returned",
},
],
},
{
id: "queued",
title: "Check attachment previews",
statusText: "Requested; waiting for execution",
tone: "idle",
executionText: "Pending",
summary: "This item is pending, with no evidence that it is running.",
},
{
id: "running",
title: "Prepare an independent example",
statusText: "Rendering the public component",
tone: "processing",
executionText: "Running",
summary: "The host explicitly reports an active execution.",
},
]);
const questions = signal<readonly AssistantQuestion[]>([
{
id: "layout",
title: "Which reading layout should we use?",
description: "You can select an option or write a different answer.",
state: "open",
options: [
{ value: "Single reading column", label: "Single reading column" },
{ value: "Reading column with activity", label: "Reading column with activity" },
],
},
{ id: "review-time", title: "When should we review the result?", state: "open" },
]);
const append = () => {
messages.value = [
...messages.value,
{
id: `message-${next++}`,
role: "assistant",
author: "Assistant",
parts: [
{
id: "text",
kind: "text",
text: "A new result arrived. Your current reading position should remain stable until you return to the latest message.",
},
],
},
];
};
return h(Stack, {
style: { height: "min(900px, calc(100dvh - 48px))", minHeight: "360px" },
children: [
h(Inline, {
wrap: true,
children: [
h(Button, {
variant: "outline",
size: "small",
onClick: append,
children: "Append result",
}),
h(Button, {
variant: "outline",
size: "small",
onClick: () => {
const last = messages.value.at(-1)!;
messages.value = [
...messages.value.slice(0, -1),
{
...last,
status: "streaming",
parts: [
{
id: "text",
kind: "text",
text: `${last.parts[0]?.kind === "text" ? last.parts[0].text : ""} Further streamed text keeps the existing message instance.`,
},
],
},
];
},
children: "Stream text",
}),
h(Button, {
variant: "quiet",
size: "small",
onClick: () => {
readState.value = readState.value === "error" ? "ready" : "error";
},
children: "Toggle read failure",
}),
h(Button, {
variant: "quiet",
size: "small",
onClick: () => {
disabled.value = !disabled.value;
disabledReason.value = disabled.value ? "The host has disabled input." : "";
},
children: "Toggle disabled",
}),
],
}),
h("div", {
style: { flex: "1", minHeight: 0 },
children: h(DemoContext.Provider, {
value: "Ancestor context retained",
children: h(AssistantView, {
title: "Shared Assistant",
description: "A complete public UI · local fixture data",
messages,
draft,
onDraftChange: (nextDraft: AssistantDraft) => {
draft.value = nextDraft;
},
readState,
disabled,
disabledReason,
models: [
{ value: "balanced", label: "Balanced model" },
{ value: "fast", label: "Fast model" },
],
model,
onModelChange: (value: string) => {
model.value = value;
},
activity: {
title: "Reviewing the UI",
statusText: "Checking the shared composition",
tone: "processing",
description: "The host owns all activity facts.",
},
work,
questions,
onLoadOlder: () => {
messages.value = [
{
id: `older-${next++}`,
role: "system",
author: "Earlier context",
parts: [
{
id: "text",
kind: "text",
text: "An older message was prepended. The first visible existing message should stay at its reading offset.",
},
],
},
...messages.value,
];
},
onSubmit: async (snapshot: AssistantDraft) => {
await new Promise((resolve) => setTimeout(resolve, 600));
messages.value = [
...messages.value,
{
id: `message-${next++}`,
role: "user",
author: "You",
parts: [
{ id: "text", kind: "text", text: snapshot.text },
...(snapshot.attachments.length
? [{ id: "files", kind: "attachments", items: snapshot.attachments } as const]
: []),
],
},
];
return { accepted: true };
},
onAnswer: async (id: string, answer: string) => {
questions.value = questions.value.map((question) =>
question.id === id
? { ...question, state: "answered", feedback: `Host accepted: ${answer}` }
: question,
);
},
onDismissQuestion: (id: string) => {
questions.value = questions.value.filter((question) => question.id !== id);
},
onRefreshWork: (id: string) => {
work.value = work.value.map((item) =>
item.id === id
? {
...item,
readState: "error",
feedback:
"The fixture refresh failed; previously read details remain visible.",
}
: item,
);
},
renderPart: (part) => (part.id === "context-note" ? h(ContextNote, null) : undefined),
}),
}),
}),
],
});
}Installation
bun add meowtail
API reference
Props
This component documents no props of its own.
DOM parts
rootchoicesfeedbackfile
Registry
The component is registered as assistant-composer in the meowtail component registry.