Component reference
AssistantView
A complete default Assistant surface.
Preview
Complete Assistant composition
The same Assistant renders tools and tasks by default and keeps messages, draft, attachments, activity, questions and safe Markdown.
export function agentAssistantDemo(): Child {
const messages = signal<readonly AssistantMessage[]>([
turn("ask-check", "Check the shared components and tell me what still needs a decision."),
{
id: "agent-run",
role: "assistant",
author: "Assistant",
status: "streaming",
parts: [
{
id: "summary",
kind: "text",
text: "Checking shared components. **Public progress summary**: The file check is in progress, the independent review finished executing, and its public reply is still pending.\n\n`<script>alert(1)</script>` is description text. Raw HTML is not executed either: <img src=x onerror=alert(1)>.",
},
{
id: "inspect-tool",
kind: "tool",
icon: "file-text",
label: "Read component files",
summary: "packages/meowtail/src/chat/long-component-file-name.tsx",
state: "running",
statusLabel: "Receiving output",
input: payload,
output: resultLines,
outputSummary: "Returned 24 records; the full result can be expanded.",
maxLines: 3,
defaultOpen: true,
canCancel: true,
},
{
id: "delegate-task",
kind: "task",
title: "Independent keyboard review",
state: "success",
statusLabel: "Succeeded · Public reply not received",
executionText: "Execution returned; 5 / 5 items complete.",
replyText: "Waiting for the public reply does not mean execution failed.",
progress: { value: 5, max: 5, label: "Execution check 5 / 5 items" },
},
{
id: "retry-tool",
kind: "tool",
icon: "database",
label: "Read index",
state: "error",
statusLabel: "Local simulated read failed",
input: "index.json",
output: "Previous confirmed data is retained.",
canRetry: true,
},
{ id: "files", kind: "attachments", items: [attachment] },
{ id: "references", kind: "text", text: "Artifacts and sources provided by the host" },
],
},
]);
const draft = signal<AssistantDraft>({
text: "Continue checking composition in a narrow container.",
attachments: [],
});
const questions = signal<readonly AssistantQuestion[]>([
{
id: "scope",
title: "Which areas should be checked next?",
state: "open",
description: "Waiting for your answer. No reply does not mean execution failed.",
options: [
{ value: "tools", label: "Tools and tasks" },
{ value: "composer", label: "Input and attachments" },
],
},
]);
let alive = true;
const timers = new Set<ReturnType<typeof setTimeout>>();
const patchTool = (
id: string,
patch: Partial<Extract<AssistantMessage["parts"][number], { kind: "tool" }>>,
) => {
messages.value = messages.value.map((message) => ({
...message,
parts: message.parts.map((part) =>
part.kind === "tool" && part.id === id ? { ...part, ...patch } : part,
),
}));
};
return (
<Stack
style={{ height: "min(1000px, calc(100dvh - 48px))", minHeight: "420px" }}
ref={() => () => {
alive = false;
timers.forEach(clearTimeout);
}}
>
<Inline>
<Button
size="small"
variant="outline"
onClick={() => {
const part = messages.value
.flatMap((message) => message.parts)
.find((item) => item.id === "inspect-tool");
if (part?.kind !== "tool") return;
patchTool("inspect-tool", {
output: `${part.output ?? ""}\nNew output: A text update <img src=x onerror=alert(1)> stays inert.`,
outputSummary: "Appended one local result; expansion and focus are unchanged.",
});
}}
>
Append tool output
</Button>
</Inline>
<AssistantView
title="Agent workflow"
description="Shared default composition · Local data, not connected to an Agent runtime"
messages={messages}
draft={draft}
onDraftChange={(next) => {
draft.value = next;
}}
onSubmit={async (snapshot) => {
messages.value = [
...messages.value,
{
id: `user-${messages.value.length}`,
role: "user",
author: "You",
parts: [{ id: "text", kind: "text", text: snapshot.text }],
},
];
return { accepted: true };
}}
activity={{
title: "Check shared components",
statusText: "File check in progress",
tone: "processing",
}}
work={[
{
id: "running",
title: "File check",
statusText: "Receiving output",
tone: "processing",
executionText: "Running",
},
{
id: "queued",
title: "Organize the report",
statusText: "Queued · Not started yet",
tone: "idle",
executionText: "Waiting for scheduling",
},
{
id: "returned",
title: "Independent keyboard review",
statusText: "Succeeded · Waiting for public reply",
tone: "success",
executionText: "Execution complete",
replyText: "Public reply not received yet",
},
]}
questions={questions}
onAnswer={(id, answer) => {
questions.value = questions.value.map((question) =>
question.id === id
? {
...question,
state: "answered",
feedback: `The host received the scope choice: ${answer}`,
}
: question,
);
}}
onMessageAction={() => {}}
onToolAction={(_messageId, partId, action) => {
if (action === "cancel") {
patchTool(partId, {
state: "cancelled",
statusLabel: "The user canceled further checks",
canCancel: false,
});
return;
}
patchTool(partId, {
state: "running",
statusLabel: "Retrying the local simulation",
retryDisabled: true,
});
const timer = setTimeout(() => {
timers.delete(timer);
if (alive)
patchTool(partId, {
state: "success",
statusLabel: "Local simulation retry succeeded",
canRetry: false,
output: "Index read completed in this local fixture.",
});
}, 650);
timers.add(timer);
}}
renderPart={(part) =>
part.id === "references" ? (
<Stack gap="compact">
<ResourceLink
title="Component review artifact"
detail="Example entry point · Uses a local anchor only"
href="#agent-reference"
/>
<ChatSources
items={[
{
title: "Meowtail source",
url: "https://github.com/lidessen/meowland",
description: "An explicitly provided example source; not retrieved by the UI.",
},
]}
/>
<span id="agent-reference">
Artifacts, references and attachments are all provided by the host.
</span>
</Stack>
) : undefined
}
/>
</Stack>
);
}Installation
bun add meowtail
Usage
import { AssistantView } from "meowtail";Examples
The other stories that show this component.
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),
}),
}),
}),
],
});
}API reference
Data, permissions, network and business lifetime stay with the host.
Props
| Prop | Type | Default |
|---|---|---|
title | string | – |
description | string | – |
messagesRequired | MaybeSignal<readonly AssistantMessage[]> | – |
readStateFirst loading, empty, and refresh failure with retained messages are separate states. | MaybeSignal<"ready" | "loading" | "error"> | – |
readFeedback | MaybeSignal<string> | – |
activity | MaybeSignal<AssistantActivity | undefined> | – |
work | MaybeSignal<readonly AssistantWork[]> | – |
questions | MaybeSignal<readonly AssistantQuestion[]> | – |
draftSupplying draft makes the host its sole owner, even for a plain value. | MaybeSignal<AssistantDraft> | – |
defaultDraft | AssistantDraft | – |
readOnly | boolean | – |
onDraftChange | (draft: AssistantDraft) => void | – |
onSubmitOnly an explicit accepted:true may clear an unchanged submitted snapshot. | (draft: AssistantDraft) => { accepted: boolean } | Promise<{ accepted: boolean }> | – |
onAttachHost attachment ingestion. Return normalized entries or update controlled draft. No upload is implicit. | ( files: readonly File[], ) => readonly AssistantAttachment[] | void | Promise<readonly AssistantAttachment[] | void> | – |
allowAttachmentsDisable local file selection when the host supports only already-normalized attachments. | boolean | – |
disabled | MaybeSignal<boolean> | – |
disabledReason | MaybeSignal<string> | – |
models | MaybeSignal<readonly AssistantChoice[]> | – |
model | MaybeSignal<string> | – |
onModelChange | (value: string) => void | – |
reasoningOptions | MaybeSignal<readonly AssistantChoice[]> | – |
reasoning | MaybeSignal<string> | – |
onReasoningChange | (value: string) => void | – |
onAnswer | (questionId: string, answer: string) => void | Promise<void> | – |
onDismissQuestion | (questionId: string) => void | – |
onToolActionAn explicit host intent. The host owns availability, execution and subsequent state. | (messageId: string, partId: string, action: "retry" | "cancel") => void | – |
onMessageActionFeedback and regeneration intents on a finished reply; supplying it shows those actions. Copy is built in. | (messageId: string, action: AssistantMessageAction) => void | – |
historyThe column of the reader's own messages over the transcript; on by default. | boolean | – |
onRefreshWork | (workId: string) => void | – |
onLoadOlder | () => void | – |
loadingOlder | MaybeSignal<boolean> | – |
renderPartReturn undefined to keep the default renderer for this one part. | (part: AssistantMessagePart, message: AssistantMessage) => Child | undefined | – |
renderWorkDetail | (work: AssistantWork) => Child | undefined | – |
composerTrailing | Child | – |
headerActions | Child | – |
labels | Partial<AssistantLabels> | – |
Also takes className, children and the attributes and events of its root element.
DOM parts
rootlayoutmessagesviewportcontentlatestcomposerfeedbackemptyactivityactivityHeaderactivityBodysectionworkDetailstatushistoryhistoryItemhistoryTrigger
Registry
The component is registered as assistant-view in the meowtail component registry.