Skip to content

Timeline ​

The thread of one chat, with the composer docked under it. It reads the chat from the store of the scope it is rendered in, so it needs a ChatScopeContext and a chat that was opened there.

tsx
import { Timeline } from '@adecore/agents-react/chat/ui/Timeline';
tsx
import { useEffect } from 'react';
import { AgentIcon } from '@adecore/agents-react/agents/AgentIcon';
import { useSubagentTrail } from '@adecore/agents-react/chat/subagent-view';
import { AccountPill } from '@adecore/agents-react/chat/ui/AccountPill';
import { Composer } from '@adecore/agents-react/chat/ui/Composer';
import { LimitPill } from '@adecore/agents-react/chat/ui/LimitState';
import { SubagentBreadcrumb, SubagentTitleCrumb } from '@adecore/agents-react/chat/ui/SubagentControls';
import { SubagentTimeline } from '@adecore/agents-react/chat/ui/SubagentTimeline';
import { Timeline } from '@adecore/agents-react/chat/ui/Timeline';
import { useChatScope } from '@adecore/agents-react/scope';
import { useChatRow } from '@adecore/agents-react/state/chats';
import { AgentDemo } from '../shared/agents.tsx';
import { DEMO_CHAT } from '../shared/agents-host.ts';

function Chat() {
    const scope = useChatScope();
    const info = useChatRow(DEMO_CHAT, (row) => row?.info);
    const { trail } = useSubagentTrail(DEMO_CHAT);
    const open = trail.at(-1);

    useEffect(() => {
        void scope.chats.open(DEMO_CHAT, {});
        return () => void scope.chats.detach(DEMO_CHAT);
    }, [scope]);

    return (
        <div className="flex h-[640px] w-full flex-col overflow-hidden rounded-lg border border-border bg-surface">
            <header className="flex h-10 shrink-0 items-center gap-2 border-b border-border px-3">
                <AgentIcon kind={info?.provider ?? 'claude'} />
                <SubagentTitleCrumb chatId={DEMO_CHAT} className="text-sm font-medium">
                    <span className="truncate text-sm font-medium">Retry for fetchJson</span>
                </SubagentTitleCrumb>
                <SubagentBreadcrumb chatId={DEMO_CHAT} />
                <span className="ml-auto flex items-center gap-1.5">
                    <LimitPill chatId={DEMO_CHAT} />
                    <AccountPill chatId={DEMO_CHAT} />
                </span>
            </header>
            <Timeline
                chatId={DEMO_CHAT}
                overlay={open && <SubagentTimeline key={open.toolUseId} chatId={DEMO_CHAT} toolUseId={open.toolUseId} />}
                composer={
                    info && (
                        <Composer
                            chatId={DEMO_CHAT}
                            info={info}
                            focused={false}
                            disabled={false}
                            providerFixed={false}
                            onSend={(text, extras) => void scope.chats.send(DEMO_CHAT, text, extras)}
                            onRetarget={(provider, selection) => scope.chats.retarget(DEMO_CHAT, provider, selection)}
                        />
                    )
                }
            />
        </div>
    );
}

/* Send a message that mentions a check to see an approval; any other message gets a reply. */
export default function ChatDemo() {
    return (
        <AgentDemo>
            <Chat />
        </AgentDemo>
    );
}
tsx
<Timeline chatId={chatId} composer={<Composer chatId={chatId} info={info} {...composerProps} />} />

The timeline fills its parent and scrolls on its own; every flex parent down to it needs min-h-0, or it has no height to measure. A chat with nothing in it shows which model answers and the folder it works in, or a greeting when the host's useWelcome says so.

What it draws ​

The items become rows. Tool calls in a row fold into one line ("Read 2 files"), a finished turn folds behind how long it worked and what it did, the files a turn changed gather in one card under its reply, and a subagent's own work hangs under its row. A page an agent published stands above its reply when the host draws visuals. A tool the package has no word for still draws, with a wrench and the first string of its input.

Only the rows on screen are drawn. The thread follows new text while it is scrolled to the end and stops once a person scrolls up or opens a fold. Near the top it loads the page before the oldest it holds and keeps the row being read in place. Once a thread has a few messages, a scrubber along its edge marks every message and bookmark.

A message has its actions under it on hover: bookmark, fork (only with a host fork) and copy. A right click adds copying the message, its code or its Markdown, and selecting text in a reply offers to quote it into the composer. A bookmark marks its message with a line across the thread.

Props ​

PropType
chatIdstringThe chat in the scope.
composerReactNodeDocked at the bottom of the frame, over the end of the thread.
overlayReactNodeStands in the thread's place, down to the composer. The thread keeps its place underneath and the composer stays usable.

The overlay is where a subagent's conversation goes. A row's open button sets the chat's trail (useSubagentTrail); draw a SubagentTimeline for its last step, as the demo above does:

tsx
const { trail } = useSubagentTrail(chatId);
const open = trail.at(-1);

<Timeline chatId={chatId} composer={composer} overlay={open && <SubagentTimeline key={open.toolUseId} chatId={chatId} toolUseId={open.toolUseId} />} />;

Streaming ​

useStreaming() on the host picks how a reply appears: a word at a time with a fade (words), a Markdown block at a time (blocks), or all at once when it is done (whole). A code fence that is still open is highlighted line by line as it grows. Only the row of the growing reply redraws for a word; see reading the stores.

Find ​

The thread has no find of its own. The host's useTimelineFind(options) gets the rows, the refs of the frame, the thread and the scroller, and callbacks to open a fold and scroll to a row, and answers a TimelineFind: the bar to draw over the thread, the rows of the hits and the current one. FindReveal and useOpenForFind let a fold open for a hit inside it.

Rows ​

Row draws one TimelineRow, and the components it picks from are exported for a thread of your own: UserRow, AssistantRow, ThinkingRow, ReportRow, ReplyHeader, NoteRow, AgentTurnRow, CompactionRow, ApprovalHistoryRow and QuestionHistoryRow in chat/ui/rows/MessageRows; WorkRow, WorkLiveRow, WorkGroupRow, WorkingRow, RunningFor, TurnFoldRow, ChangedFilesRow and ToggleLine in chat/ui/rows/WorkRows; SubagentRow, SubagentBranchRow, TaskRow, WorkflowRow, ForksRow and VisualRow in files of their own. deriveTimelineRows(items, options) turns items into rows the way the timeline does.

tsx
import { useState } from 'react';
import type { ChatToolItem } from '@adecore/agent-contracts';
import { summarizeGroup } from '@adecore/agents-react/chat/logic/timeline';
import { AgentTurnRow, CompactionRow, NoteRow, ReportRow } from '@adecore/agents-react/chat/ui/rows/MessageRows';
import { WorkGroupRow, WorkingRow, WorkRow } from '@adecore/agents-react/chat/ui/rows/WorkRows';
import { ForksRow } from '@adecore/agents-react/chat/ui/rows/ForksRow';
import { TaskRow } from '@adecore/agents-react/chat/ui/rows/TaskRow';
import { WorkflowRow } from '@adecore/agents-react/chat/ui/rows/WorkflowRow';
import { NOW, chatItems, taskRows, workflowTool } from '../shared/agents-data.ts';
import { AgentDemo } from '../shared/agents.tsx';
import { DEMO_CHAT, demoChats, forkChats } from '../shared/agents-host.ts';

const tools = chatItems().filter((item): item is ChatToolItem => item.kind === 'tool');
const reads = tools.filter((tool) => tool.name === 'Read' || tool.name === 'Grep');
const workflow = workflowTool();
const tasks = taskRows();

function Rows() {
    const [expanded, setExpanded] = useState(false);
    const [openTask, setOpenTask] = useState<string | null>(null);

    return (
        <div className="chat-thread flex w-full max-w-2xl flex-col gap-3">
            <WorkRow tool={tools.find((tool) => tool.name === 'Bash')!} />
            <WorkGroupRow tools={reads} summary={summarizeGroup(reads)} expanded={expanded} onToggle={() => setExpanded(!expanded)} />
            <WorkingRow startedAt={NOW - 14_000} />
            <WorkflowRow chatId={DEMO_CHAT} tool={workflow} workflow={workflow.workflow!} />
            <div>
                {tasks.map(({ item, task }) => (
                    <TaskRow
                        key={item.id}
                        item={item}
                        task={task}
                        open={openTask === item.id}
                        onPress={() => setOpenTask(openTask === item.id ? null : item.id)}
                    />
                ))}
            </div>
            <ForksRow chatId={DEMO_CHAT} turnId="turn-1" />
            <NoteRow id="note-info" level="info" text="The chat moved to the account Work." />
            <NoteRow id="note-warning" level="warning" text="The CLI restarted and resumed the conversation." />
            <NoteRow id="note-error" level="error" text="The CLI exited with code 1." />
            <CompactionRow preTokens={96_300} />
            <AgentTurnRow label="The helper agent finished" />
            <ReportRow id="report" text="Three callers: `barometer.ts` and `thermometer.ts` catch errors, `station.ts` does not." />
        </div>
    );
}

export default function RowsDemo() {
    return (
        <AgentDemo chats={() => ({ ...demoChats(), ...forkChats() })}>
            <Rows />
        </AgentDemo>
    );
}

The menus and markers are exported too: TimelineMenuPopup (the right click menu), MessageActions, BookmarkMarker, BookmarkSubmenu, BookmarkMenuItems, QuoteButton and Scrubber. Prefer the whole Timeline unless your app owns the grouping and the scrolling.

Licenses and third-party notices are listed in each package.