affectUI › Components › Agents

Collaboration Stream

What agents actually say to each other – a quiet chat where every message shows who it is from and who it is for, handoffs and questions for you tagged, files as chips that open a preview, a card with details under every message, and a composer to name agents with @ or answer a question.

Part of affectUI Pro. Install: npx shadcn@latest add https://www.affectui.com/r/collaboration-stream.json · Import from @/components/ui/collaboration-stream

When to use

  • Showing what the agents of a run say to each other – not just who started whom, but the actual messages, handoffs and questions
  • Multi-agent runs where you step in: a question for you is tagged, and you answer it or name an agent with @ right from the composer
  • Live runs, where messages come in while the agents work and the stream follows the newest one

When not to use

  • Who started whom, at a glance – use Agent Tree
  • What every agent did over time, side by side – use Agent Lanes
  • A chat between one person and one assistant – use Chat Composer with your own message list
  • Requests that wait for a person across many runs – use Agent Inbox

Usage

import { CollaborationStream, type StreamAgent, type StreamMessage } from "@/components/ui/collaboration-stream";

const agents: StreamAgent[] = [
  { id: "orch", name: "Orchestrator", model: "Opus 5.5" },
  { id: "research", name: "Researcher", model: "Gemini 3.8 Flash" },
];

const messages: StreamMessage[] = [
  { id: "m1", from: "you", to: "orch", kind: "ask", text: "Launch the new pricing page by Friday.", at: "14:02" },
  { id: "m2", from: "orch", to: "research", kind: "spawn", text: "Find out what the others charge per seat." },
  { id: "m3", from: "research", to: "orch", kind: "share", text: "Linear $10, Notion $12, Figma $15 per seat.", at: "14:04",
    replyTo: "m2", attachments: [{ label: "pricing-table.csv" }] },
];

export function RunChat() {
  return <CollaborationStream agents={agents} messages={messages} task="Launch the new pricing page" live />;
}

Props

NameTypeDefaultDescription
agentsStreamAgent[]{ id, name, model?, avatar? } – every agent gets its own pixel mark and color in this order; avatar swaps the mark for a logo
messagesStreamMessage[]{ id, from, to, kind, text, at?, replyTo?, typing?, attachments?, details? } – in the order they were sent; from and to are agent ids, "you" or (to) "all"
kind"ask" | "reply" | "handoff" | "share" | "question" | "spawn"Per message – a handoff is tagged, a question to you says Needs you, a spawn is a quiet line "X started Y"
replyTostringPer message – the id it answers; it shows as a quote unless that message is right above, and a click on the quote jumps there
typingbooleanPer message – still being written: Writing… shimmers and the sender's mark twinkles
attachmentsStreamAttachment[]Per message – { label, kind?, preview?, meta? } as small white chips; a click opens the preview window. kind is told from the label when left out: "…/" a folder, .csv a table
detailsStreamDetailsPer message – { facts?, items? } shown in a card under the message when it's clicked
taskstringWhat the run is about – part of the stream's label
livebooleanfalseThe run is still going – a Live dot, and the stream follows new messages unless you scrolled up
selectedstring | nullControlled id of the picked message – its details open, the rest fades
onSelectedChange(id: string | null) => voidA message was picked or let go
focusAgentstring | nullControlled id of the agent whose messages stay – the tabs on top
onFocusAgentChange(id: string | null) => voidA tab was picked
onSend({ text, to, replyTo? }) => voidShows the composer. to holds the agents named with @ or the one answered, else ["all"]
placeholderstring"Message the agents – @ to name one"The composer's placeholder
logosRecord<string, ReactNode>Names that get their logo in the text, e.g. { Notion: <NotionLogo /> }
maxHeightnumber520The stream scrolls past this height in px – with a composer it's the fixed height, so the composer never moves
titlestring"Agent chat"Label of the stream for assistive tech
classNamestringExtra class on the root

Keyboard

  • Tab: The tabs are one stop, the stream another; then the focused message's Reply and chips, then the composer
  • ArrowLeft / ArrowRight: Moves between the agent tabs; Home and End jump to the first and the last
  • ArrowUp / ArrowDown: Moves between the messages; Home and End jump to the first and the last
  • Enter / Space: Picks the focused message and opens its details, or lets it go
  • Esc: Lets go of the picked message or agent; in the composer it drops the reply, in the preview it closes it
  • @: In the composer: opens the agents – type to filter, arrows to move, Enter or Tab to name one
  • Enter: In the composer: sends – Shift+Enter starts a new line

Accessibility

  • The stream is a log, announced politely while live; every message is labelled with who wrote it, to whom, when and what
  • Roving focus – only one message is in the tab order, so the stream is a single stop
  • The agent tabs are toggle buttons with aria-pressed in a labelled toolbar – one tab stop, arrows move; a fade at an edge shows there are more
  • Only the focused message's Reply and chips are in the tab order, so a long stream doesn't fill it with buttons
  • The @ menu is a listbox tied to the field with aria-expanded; the preview is a modal dialog that keeps focus inside and hands it back to its chip
  • Needs you and Handoff are words, not just colors; pixel marks and logos are hidden from assistive tech

Responsive

  • Built on a container query – bubbles hug their words up to 560px and wrap on narrow screens
  • The tabs scroll sideways when there are more agents than room
  • The preview window fits the component, its content scrolls
  • Dark mode built in: switches when an ancestor (usually <html>) has the class dark or data-theme="dark"

Requirements

  • React 19

Related

  • Agent Tree – Who started whom – an orchestrator and its sub-agents as a live tree, with status, cost and time on every card, costs rolling up each branch, and branches that fold to one line
  • Agent Lanes – Where every agent is right now – a live timeline with a lane per agent, tasks growing into “now”, hand-offs drawn between agents and approvals right on the block
  • Agent Inbox – All your background agent tasks in one calm list – live clocks and status tags, filter tabs with rolling counts, rows that unfold to the last steps with Approve, Reject, Retry or Open, and glide to their new place
  • Chat Composer – Prompt composer with model picker, voice input, app connections and approval request
  • Thinking State – AI working panel with a live timer, searched sources and thinking steps that appear one by one