Chat
The components of an agent chat, shipped as their own package.
npm install @textui/chat
They are built on @textui/core and @textui/widgets, and on nothing that talks to a host.
A conversation is drawn from data the caller already has, so a client that speaks AHP, or anything else, maps its own records onto these shapes and the components never learn where a session came from.
import { ChatComposer, ChatTranscript } from '@textui/chat';
import type { Block } from '@textui/chat';
import { Column } from '@textui/widgets';
const blocks: Block[] = [
{ kind: 'said', id: 's1', turnId: 't1', text: 'Rename the package.' },
{ kind: 'prose', id: 'p1', turnId: 't1', content: 'Done. Three files changed.', streaming: false },
];
<Column flex={1}>
<ChatTranscript blocks={blocks} expanded={{}} onToggle={() => {}} flex={1} />
<ChatComposer value="" onChange={() => {}} onSubmit={() => {}} />
</Column>
The conversation
ChatTranscript is the whole thing: a list of Blocks in a Feed, which owns the viewport, the cursor and the tail it follows.
Each kind of block draws as its own component.
A thing said is a ChatBubble - a speaker’s glyph and name over a Gutter that runs down the left of the answer.
Prose still arriving is StreamingText, and what the agent was thinking is a ReasoningBlock folded to one row.
What the agent did is a ToolCallRow, which opens onto the input and the output.
A turn is not a block: an agent turn is a header, some prose, a reasoning fold and a row per tool call, and each of those folds and selects on its own. Whoever owns the turns turns them into these.
The composer
ChatComposer is the field, the slash and path menus above it, and the control rows under it.
The field is TextArea from the catalog; what is here is what enter means while a turn is running, which completions the draft has earned, and how much room a short terminal leaves for the menu.
ComposerBar is the row of chips under the field.
Everything on it is a current value rather than a label - which harness, which model, how much it may do before it asks, where it runs - and each chip is one command’s argument, asked through openPicker above the chip that asked.
The mark in front of a value comes from settingIcon and valueIcon, which are hints rather than decisions: an unrecognised setting keeps every word the host gave it.
CommandList is the menu itself, extracted so a caller with a different list of commands does not have to re-derive the column arithmetic.
Waiting on a person
ChatHitl is the block that means the agent is stopped.
It is the only thing on the screen that is waiting on the reader, so it takes the focus and is answerable without leaving the keyboard.
A tool confirmation and a request in prose are two different things and it renders both.
ChatInputStatus is the row under it saying what came of the answer.
Sessions
SessionList is the catalogue, two lines to a row, and ConnectionBadge says which host it came from.
ChatSessionHead is what a conversation is, drawn as the first thing inside the transcript so it costs nothing after the first screen.
SessionDetails is the pane where the identifiers are read in full and copied.
Changes
FileDiff is one file out of a changeset with both sides lined up, and diffLines works out which lines those were.
Its own shapes
Every component takes a shape of this package’s own: a ChatToolCall, a ChatSession whose status is already a word, a tone and a glyph, a ChatPendingInput with its questions.
They are all on one page with the functions that read them: Chat data.
Nothing is read from the store. The markdown switch, the draft answers of a question and the row under the composer are all props, and whoever mounts the components holds them wherever it likes.
See also
- Feed - the viewport under the transcript
- MarkdownView - how a message is laid out
- Focus - the scopes the waiting block and the composer use