Skip to content

@adecore/agents-react ​

React views for agent chats: the thread, the composer, approvals and questions, the agent CLIs and their accounts, and what they cost. It runs in the page and talks to a host of chats, such as @adecore/agents, over the requests and events of @adecore/agent-contracts. It starts no process and registers no channel.

tsx
import { Timeline } from '@adecore/agents-react/chat/ui/Timeline';
import { Composer } from '@adecore/agents-react/chat/ui/Composer';
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>
    );
}

The demos on these pages run on a host in memory: two CLIs, three accounts, a chat two turns in, usage for the last months. Send a message and it answers a word at a time; mention a check or a test and it asks for permission first. Open the helper agent's row to read its own conversation. Nothing leaves the page.

What is in it ​

How it fits together ​

your app        setChatHost(...)          words, storage, files, navigation
  |
scope           ChatScopeContext          one per host of chats
  |               transport               requests and events
  |               ChatClient              writes them into the stores
  |
views           Timeline, Composer, ...   read the stores of their scope

The views read a chat from a store, never from the wire. A ChatClient fills that store from a ChatTransport, the one object that knows how to reach the host. A ChatScope bundles the two with an id and puts them in context, so an app that talks to two hosts renders a scope for each and their threads never mix. What only the app can decide, such as where a file opens or whether a stop needs a confirmation, it hands over once with setChatHost.

Entry points ​

There is no root import. Every module has its own subpath, @adecore/agents-react/<path>, without an extension; the module reference lists all of them. DiffPool, EditDiff and UnifiedDiff are default exports, everything else is named. Two entries are assets: @adecore/agents-react/theme.css and the words under @adecore/agents-react/locales/<language>/<namespace>.json.

Where to go next ​

  • Getting started installs the package, its CSS and its words, and renders a first chat.
  • Host adapters lists what setChatHost takes and what each part does without it.
  • Chat client and state covers opening, sending, reconnecting and reading the stores.
  • Persistence covers drafts, preferences and their storage keys.
  • Testing shows a host in memory like the one these demos use.

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