Production-ready React components and a framework-agnostic core for building Agent Client Protocol (ACP) clients.
Try the live demo · Quick start · Architecture · 中文
Build a complete agent workbench instead of assembling another chat box. The library includes multi-agent and multi-session orchestration, streaming, tool calls, permissions, plans, file browsing, diffs, skills, settings, and desktop-ready transports.
- Complete workbench — Start with
WorkbenchShell, then replace or extend individual views as your product grows. - Real ACP lifecycle — Handle sessions, streaming updates, tool calls, permissions, authentication, and usage through one typed data layer.
- Host and framework independent — Use the React UI on the web or in Tauri, or use
@acp-components/corewith Vue, Svelte, Solid, or vanilla TypeScript.
Run the interactive browser demo with the default local ACP agent:
pnpm install
pnpm dev:allOpen http://localhost:5173. This starts both the WebSocket bridge and the Vite demo; see Web Demo to use Codex, Claude, or another ACP agent.
- Multi-Agent — Connect to multiple ACP agents simultaneously, each with independent transport, capabilities, and session management. User-added agents are persisted to
storage('agents')and restored on next launch; built-in (host-supplied) agents are never user-removable - Multi-Workspace — Organize sessions by working directory (cwd); switch between workspaces seamlessly. Workspace list is persisted to
storage('workspaces')via the built-in<PlatformWorkspacesAuto>driver - Multi-Session — Run and view multiple sessions side-by-side in resizable split panes; each session has fully isolated state (messages, streaming, tool calls, permissions). Per-session chunk batching keeps concurrent streams independent
- Framework-Agnostic Core — Zustand vanilla stores with zero React dependency; works with Vue, Svelte, Solid, or vanilla JS
- Multi-Transport — Stdio, HTTP, WebSocket, and custom transports per agent. Stdio spawn capability is supplied by the host via
platform.process.createStdioTransport(a web host that cannot spawn a child process simply omits it); ships with a Tauri IPC example - Unified Workbench Shell —
WorkbenchShellwires theSidebar(top nav buttons + switchable body + footer) to a main area that swaps views by nav item: Sessions →SessionView, Skills →SkillView, New Session, Settings, plus host-injected views - Rich UI Components — SessionView (split session panes + Files side panel with FileTree & FileViewer), SkillView, SettingsView, chat view (with round grouping + inline permission prompt), diff view, plan view, thought view, command palette, login dialog, dropdown, select, status bar, and more — 25+ components
- File Tree & File Viewer — Per-workspace file tree (lazy expand, optional live watch) and an in-panel Monaco-backed file viewer with syntax highlighting and reveal-line; both driven zero-config by
platform.fs - Skills Catalog —
SkillViewfetches each connected agent's skill catalog live vialistSkills(), grouped by scope (user-level vs per-project cwd) - Settings Surface — Full-page
SettingsViewwith Appearance (theme) and Agents management panels; extensible by appending toSETTINGS_SECTIONS - Streaming UX — Real-time content and thought streaming (per-session chunk batching) with animated indicators, live tool call status, and token usage tracking
- Session Management — Full CRUD: create, load, switch, close, delete, fork, refresh, and load-more — scoped by workspace and agent; split panes let you watch several at once
- Tool Call Visualization — Track agent tool invocations with status, input/output, file locations, and diffs
- Authentication — Built-in auth flow with
LoginDialogcomponent, env_var and terminal-based auth methods, and programmaticauthenticate/authenticateWithEnvactions - Permission Handling — Promise-based per-session permission flow with an inline prompt (rendered inside
ChatView) for approving or rejecting tool call requests; auto-rejected on session close or agent disconnect - Theming — Dark and light themes via CSS custom properties (
--acp-*design tokens); runtime switching throughuseSettings().setTheme(); extensible viadata-acp-themeattribute - Internationalization — Built-in i18n (en-US, zh-CN) via i18next with locale auto-detection sourced from the host
Platform.system.getLocale() - Desktop Ready — Includes Tauri and stdio transport examples for native desktop applications
| Package | Description |
|---|---|
| @acp-components/core | Framework-agnostic: multi-agent transport layer, AcpClient, vanilla Zustand stores (workspace + agent + session + file-tree + file-viewer + skill), and imperative actions |
| @acp-components/react | React bindings: context providers (AcpContext / Platform / Settings / I18n), hooks (useSyncExternalStore), and 25+ UI components |
pnpm add @acp-components/core @acp-components/reactPeer dependencies: react (^18 || ^19), react-dom (^18 || ^19). monaco-editor is an optional peer dependency — only required if you use the built-in FileViewer.
The simplest setup mirrors the web demo: a <PlatformProvider> for host-native capabilities, an <I18nProvider> for translations, and an <AcpProvider> that manages agent connections. Inside, WorkbenchShell renders the entire layout — sidebar nav plus a main area that swaps between Sessions / Skills / New Session / Settings views.
import ReactDOM from 'react-dom/client';
import {
I18nProvider,
PlatformProvider,
AcpProvider,
WorkbenchShell,
PermissionPrompt,
LoginDialog,
useAcpStore,
} from '@acp-components/react';
// createWebPlatform is a host-side factory; the demo ships one in
// examples/demo/src/webPlatform.ts. Implement your own for a custom host.
import { createWebPlatform } from './webPlatform';
function AppInner() {
const activeSessionId = useAcpStore((s) => s.activeSessionId);
return (
<>
<WorkbenchShell sessionId={activeSessionId} />
<LoginDialog />
</>
);
}
function App() {
return (
<PlatformProvider platform={createWebPlatform()}>
<I18nProvider>
<AcpProvider
agents={[
{
id: 'main',
name: 'Main Agent',
transport: { type: 'websocket', url: 'ws://127.0.0.1:3100' },
},
]}
theme="dark"
>
<AppInner />
</AcpProvider>
</I18nProvider>
</PlatformProvider>
);
}
ReactDOM.createRoot(document.getElementById('root')!).render(<App />);WorkbenchShell owns the active-view state and the built-in nav (New Session, Skills). It defaults to the New Session view on first launch and flips back to the session view as soon as a session becomes active. Directory picking inside SessionList is driven by usePlatform().dialogs?.openFilePicker() — no onBrowse prop needed.
Connect to multiple agents in different modes simultaneously:
<AcpProvider
agents={[
{
id: 'craft',
name: 'Craft Agent',
transport: { type: 'websocket', url: 'ws://127.0.0.1:3100' },
},
{
id: 'ask',
name: 'Ask Agent',
transport: { type: 'stdio', command: 'opencode', args: ['acp', '--mode', 'ask'] },
},
]}
theme="dark"
>
<App />
</AcpProvider>A
{ type: 'stdio' }config requires the host to supplyplatform.process.createStdioTransport(so the process can actually be spawned). A web host that omitsprocesswill see stdio configs fail fast at connect time — so in the browser, usewebsocketagainst the bridge server instead.
Each agent in the agents array gets its own transport configuration:
// Stdio — spawn an agent process directly (Electron / Tauri / Node.js desktop).
// Requires platform.process.createStdioTransport on the host.
{
id: 'desktop-agent',
name: 'Desktop',
transport: { type: 'stdio', command: 'opencode', args: ['acp'] },
}
// HTTP — connect via HTTP POST
{
id: 'http-agent',
name: 'HTTP',
transport: { type: 'http', url: 'http://localhost:8080/acp', headers: { 'Authorization': 'Bearer token' } },
}
// WebSocket — connect to a bridge server (browser environments)
{
id: 'ws-agent',
name: 'WebSocket',
transport: { type: 'websocket', url: 'ws://127.0.0.1:3100' },
}
// Custom — provide your own AcpTransport implementation
{
id: 'custom-agent',
name: 'Custom',
transport: { type: 'custom', transport: myCustomTransport },
}| Component | Description |
|---|---|
AcpProvider |
Top-level provider: connects to the built-in agent set in parallel, manages agent lifecycle, persists user-added agents to storage('agents'), wires session updates to stores, exposes SettingsContext (runtime theme), and renders a loading spinner until the built-in agents are ready. Props: agents, theme (initial), children |
WorkbenchShell |
Orchestrates the whole layout: a Sidebar (top nav + switchable body + footer) on the left, and a main area that swaps views by active nav item (Sessions → SessionView, Skills → SkillView, New Session → NewSessionView, Settings → SettingsView, plus host-injected navItems). Props: sessionId, navItems, sidebarWidth, panelWidth, … |
Workbench |
Low-level three-panel layout (sidebar, main, panel) using CSS Grid — WorkbenchShell builds on top of this |
Sidebar |
Pure renderer: full-width icon+text nav buttons + a SessionList body + a SettingsMenu footer. Props: activeView, onActiveViewChange, navItems, onSelectSession, onOpenSettings |
SessionView |
Active-session surface: SessionPanes (split-able chat columns) on the left, a resizable SessionPanel with the built-in Files tab (FileTree + opened-FileViewer, two columns) on the right, plus host-injected tabs. Props: sessionId, tabs, activeTabId, panelWidth, showFilesTab, … |
SessionPanes |
Multi-session split view: renders one or more ChatView columns side-by-side with percentage-based resize handles. Click the split icon to duplicate a pane; close a pane to remove it. The active pane tracks activeSessionId. Props: sessionId, onNavigateFile, headerExtras |
SessionPanel |
Right-side panel with a tab bar (built-in Files + host-injected tabs) and a resizable FileTree / FileViewer two-column layout. Props: cwd, tabs, activeTabId, panelWidth, showFilesTab, … |
SessionList |
Sidebar body: workspaces grouped by directory, sessions flattened across agents and sorted newest-first, with add workspace / create / select / fork / delete actions |
ChatView |
Main chat area: groups messages into user/agent rounds, renders plan, usage bar, config panel, and inline PermissionPrompt. Props: sessionId, onNavigateFile, showHeader |
MessageBubble |
Renders message parts (content blocks, thought blocks, tool calls) with Markdown via react-markdown |
Markdown |
Reusable Markdown renderer with syntax-highlighted code blocks and GFM support |
ChatComposer |
Text input with slash-command palette integration and send / cancel controls |
StreamingIndicator |
Animated typing indicator shown during agent streaming |
ToolCallCard |
Displays tool call name, status, input/output, file locations |
ThoughtView |
Collapsible view for agent reasoning / thinking content |
PlanView |
Displays the agent's plan entries during streaming |
DiffView |
Side-by-side diff viewer for file changes |
PermissionPrompt |
Inline prompt for approving / rejecting tool permission requests |
LoginDialog |
Modal for agent authentication: supports env_var and terminal-based auth methods, env var form input, 5-minute timeout |
ConnectionStatus |
Per-agent connection state indicator with agent name and version |
UsageBar |
Token usage progress bar showing context window consumption |
SessionConfigPanel |
Dropdown for session configuration options |
CommandPalette |
Slash-command palette for available agent commands |
FileTree |
Per-workspace lazy file tree with expand/collapse, refresh, and reveal-on-open. Props: cwd, onSelectFile, … |
FileViewer |
In-panel file viewer with Monaco syntax highlighting and revealLine. Props: entries, activePath, onCloseFile, … |
SkillView |
Skill catalog fetched live from each connected agent's listSkills(), grouped by scope, with search. Props: onSelect, showSearch, emptyText, … |
NewSessionView |
Landing/composer screen for starting a new session. Props: onSubmitted, … |
SettingsView |
Full-page settings surface (Appearance + Agents panels). Props: activeSection, className |
SettingsMenu |
Footer dropdown in the sidebar for opening settings / theme / version info |
Select |
Styled select control with options and option groups |
Dropdown |
Composable dropdown primitives (trigger / content / section / item / submenu) |
ResizeHandle |
Draggable resize handle used by resizable panels |
PlatformProvider |
Injects the host Platform and auto-mounts <PlatformWorkspacesAuto>, <PlatformFileTreeAuto>, and <PlatformFileViewerAuto> (disable individually with autoWorkspaces / autoFileTree / autoFileViewer) |
| Hook | Description |
|---|---|
useAcpProvider(opts) |
Creates and manages the multi-agent ACP provider lifecycle (connect all agents → initialize → ready) |
useAcpStore(selector) |
Subscribe to the global acpStore (Zustand vanilla store via useSyncExternalStore) |
useSessionStore(sessionId, selector) |
Subscribe to per-session sessionStore |
useSessions() |
Session CRUD: list all sessions across workspaces, create, select, close, refresh; returns global activeSessionId |
useWorkspaces() |
Workspace CRUD: list, add, remove workspaces (persistence handled by <PlatformWorkspacesAuto>) |
useSessionMessages(sessionId) |
Messages for one session |
useSessionIsStreaming(sessionId) |
Streaming state for one session |
useSessionPlan(sessionId) |
Plan entries for one session |
useSessionAvailableCommands(sessionId) |
Available commands for one session |
useSessionPendingToolCalls(sessionId) |
Pending tool calls for one session |
useSessionPendingPermissions(sessionId) |
Pending permission requests for one session |
useSessionConfigOptions(sessionId) |
Config options for one session |
useSessionUsage(sessionId) |
Token usage for one session |
usePrompt(sessionId) |
send(blocks) and cancel() for sending / canceling prompts (auto-resolves the correct agent client) |
useToolCalls(sessionId) |
Pending and completed tool calls for a session |
usePermission(sessionId) |
Current permission request with respond(optionId) and deny() actions |
useConnectionStatus(agentId) |
Per-agent connection status, agent info (name, version) |
useAllAgentStatuses() |
Aggregate status across all agents: individual statuses plus overall status |
useFileTree(cwd) |
Per-workspace file-tree state and actions (expand / collapse / refresh / reveal) |
useFileViewer() |
Opened-file entries, active path, open / close / reveal actions |
useSkills() |
Skill catalog grouped by agent + scope, read from the global skillStore |
useExtensions() |
Extension method / notification callbacks (onExtMethod / onExtNotification) |
useResizable(opts) |
Generic resizable-pane state (width, dragging, handlers) |
useAcpContext() |
Raw access to getClient(agentId), agents list, addAgent / removeAgent, builtinAgentIds, isReady |
usePlatform() |
Access the host Platform (orthogonal to AcpContext) |
useSettings() |
Runtime theme via theme / setTheme() (backed by SettingsContext) |
useI18n() |
Access to t() translation function and i18n instance |
The component library uses CSS custom properties as a design-token contract. All component styles reference only --acp-* variables — no hardcoded color values.
Two built-in themes via data-acp-theme:
"dark"— Dark theme (default): deep navy background with accent highlights"light"— Light theme: cool white / blue-gray surfaces with color accents
The theme prop on <AcpProvider> sets the initial theme; switch it at runtime through useSettings().setTheme(), which syncs data-acp-theme onto <body> so portaled components (Select, dropdowns, …) inherit the variables.
Create custom themes by overriding the variables:
[data-acp-theme='my-theme'] {
--acp-color-bg-primary: #ffffff;
--acp-color-accent: #ff6b6b;
/* ... override all needed variables */
}import { useSettings } from '@acp-components/react';
// in a component rendered inside <AcpProvider>
const { theme, setTheme } = useSettings();
setTheme('my-theme');Built-in i18n via i18next. Locale is auto-detected from the host Platform.system.getLocale() (web: navigator.language; desktop: OS locale) with a localStorage override fallback, defaulting to en-US.
import { I18nProvider } from '@acp-components/react';
<I18nProvider
defaultLocale="zh-CN"
customLocales={{
'ja-JP': {
'composer.placeholder': 'メッセージを入力...',
'permission.title': '権限が必要です',
},
}}
>
<App />
</I18nProvider>Use the useI18n() hook for language switching:
const { t, i18n } = useI18n();
i18n.changeLanguage('zh-CN'); // switch to ChinesePlatform (defined in @acp-components/react, re-exported from the package) is an environment-agnostic native-capability contract, orthogonal to AcpContext. UI components consume it via usePlatform() and never touch host-native APIs (window.prompt, localStorage, @tauri-apps/plugin-*, …) directly. Each host provides its own implementation — reference factories: createWebPlatform() (examples/demo/src/webPlatform.ts) and createTauriPlatform() (examples/tauri/src/tauriPlatform.ts).
Capability is expressed by slice / method presence — callers guard with ?.:
| Slice | Members | Required |
|---|---|---|
storage |
storage(name?) → async KV store (workspaces, agents, i18n all depend on it) |
✅ always |
fs |
readDirectory, readFileContent, writeFileContent?, watchFileTree? |
optional |
dialogs |
openLink, openFilePicker, notify |
optional |
clipboard |
writeText, readText? |
optional |
openExternalEditor |
(path, line?) => void — delegates file opening to the host, bypassing the built-in FileViewer |
optional |
updater |
state(), check(), install() |
optional |
system |
getLocale?, onLocaleChanged?, restart?, exportLogs? |
optional |
process |
createStdioTransport(opts) — supplies the stdio spawn capability so { type: 'stdio' } agent configs can connect |
optional (desktop) |
<PlatformProvider platform={instance}> should wrap the whole tree, above <I18nProvider> and <AcpProvider>. By default it auto-mounts three zero-config drivers: <PlatformWorkspacesAuto> (workspace list ↔ storage('workspaces')), <PlatformFileTreeAuto> (drives fileTreeStore from fs.readDirectory / fs.watchFileTree), and <PlatformFileViewerAuto> (wires fs.readFileContent / openExternalEditor to fileViewerStore). Disable each with autoWorkspaces={false} / autoFileTree={false} / autoFileViewer={false} to wire your own.
Platformowns native capabilities; which agent to spawn (command / args / env) stays plain data onAgentConfig.transport. The two concerns are orthogonal — the same pattern asfs.readDirectory(capability) being separate from the workspacecwd(data) it is called with.
The @acp-components/core package has zero React dependency. You can use it with any framework:
import { acpStore, sessionStore, fileTreeStore, fileViewerStore, skillStore, createAcpProvider, sendPrompt } from '@acp-components/core';
// 1. Create multi-agent provider. `stdioFactory` is the host spawn capability
// (e.g. a child-process transport). Pass `null` on a host that cannot spawn.
const provider = createAcpProvider(
{
agents: [
{ id: 'main', name: 'Main', transport: { type: 'stdio', command: 'opencode', args: ['acp'] } },
],
},
stdioFactory,
);
// 2. Wait for ready
provider.subscribe(() => {
if (provider.ready) {
console.log('All agents connected!');
}
});
// 3. Read from vanilla stores
acpStore.getState().workspaces; // workspace state tree
acpStore.getState().agents; // agent connection statuses
fileTreeStore.getState(); // per-workspace file-tree state
skillStore.getState(); // skills catalog
acpStore.subscribe((state) => { }); // watch for changes
// 4. Use actions (need to provide client and agentId)
const client = provider.getClient('main');
await sendPrompt(client!, sessionId, blocks);
// 5. Add/remove agents dynamically
await provider.addAgent({ id: 'analyze', name: 'Analyze', transport: { type: 'websocket', url: 'ws://...' } });
await provider.removeAgent('analyze');
// 6. Teardown
provider.destroy();- Node.js >= 18
- pnpm
- An ACP-compatible agent (e.g., opencode with
acpsubcommand)
# Install dependencies
pnpm install
# Build all packages
pnpm build
# Build individual packages
pnpm build:core
pnpm build:react
# Run tests
pnpm test
# Lint
pnpm lintLocal development uses the WebSocket-to-stdio bridge as its built-in Local Agent. Start the bridge and demo together:
pnpm dev:allAlternatively, run pnpm dev:server and pnpm dev in separate terminals. Open http://localhost:5173; development mode automatically connects Local Agent at ws://127.0.0.1:3100 and enables the bridge-backed Files panel.
Choose which local ACP agent the bridge spawns with the existing scripts:
pnpm dev:server
# Or use Codex agent instead of opencode
pnpm dev:server-codex
# Or use the Claude agent
pnpm dev:server-claude
You can also open Settings > Agents and add other WebSocket or HTTP agents. User-added agents are persisted in browser storage.
The GitHub Pages build has no built-in agent and intentionally omits Platform.fs. Users may add a reachable WebSocket or HTTP agent, but the online page does not expose any bridge host's file system; the Files panel and client-side file navigation are hidden automatically.
pnpm dev:tauri # Development mode
pnpm build:tauri # Production build| Variable | Default | Description |
|---|---|---|
ACP_PORT |
3100 |
WebSocket server port |
ACP_HOST |
127.0.0.1 |
WebSocket server host |
ACP_AGENT |
opencode |
Agent command to spawn |
ACP_AGENT_ARGS |
acp |
Arguments passed to the agent |
Implement the AcpTransport interface to add any communication layer:
import type { AcpTransport, Stream } from '@acp-components/core';
class MyCustomTransport implements AcpTransport {
async connect(): Promise<Stream> { /* ... */ }
disconnect(): void { /* ... */ }
onClose?: (handler: () => void) => () => void;
onError?: (handler: (err: Error) => void) => () => void;
}
<AcpProvider agents={[{
id: 'custom',
name: 'Custom Agent',
transport: { type: 'custom', transport: new MyCustomTransport() },
}]}>Real-world examples: Tauri IPC, Electron IPC, Chrome Extension messaging, iframe postMessage.
Implement the Platform interface and inject it via <PlatformProvider platform={instance}>. Provide whichever slices your host backs; storage is the only always-required one. See createWebPlatform() / createTauriPlatform() for reference.
Agents can be added or removed at runtime. The addAgent / removeAgent exposed via useAcpContext() are persistence-wrapped — they persist user-added agents to storage('agents') and restore them on next launch. Built-in agents (from the agents prop) are host-supplied and cannot be removed:
const { addAgent, removeAgent } = useAcpContext();
// Add a new agent mid-session (persisted)
await addAgent({
id: 'new-agent',
name: 'New Agent',
transport: { type: 'stdio', command: 'my-agent', args: ['acp'] },
});
// Remove a user-added agent (cleans up its sessions automatically; refused for built-ins)
await removeAgent('new-agent');Programmatically manage workspaces:
const { addWorkspace, removeWorkspace, workspaces } = useWorkspaces();
// Add a workspace
addWorkspace('/path/to/project');
// List all workspaces
workspaces.forEach(ws => console.log(ws.cwd, ws.sessions.size));Inject your own nav items + main-area views into WorkbenchShell, and your own side-panel tabs into SessionView:
<WorkbenchShell
sessionId={activeSessionId}
navItems={[
{
id: 'terminal',
label: 'Terminal',
icon: <TerminalIcon />,
content: <TerminalView />,
},
]}
/><SessionView
sessionId={activeSessionId}
tabs={[{ id: 'terminal', label: 'Terminal', content: <TerminalView /> }]}
/>| Layer | Technology |
|---|---|
| Protocol | @agentclientprotocol/sdk (ACP TypeScript SDK) |
| State Management | Zustand v5 (vanilla store, no React dependency) |
| UI Framework | React 18 / 19 |
| Code Editor | Monaco (optional peer dep, used by FileViewer) |
| Internationalization | i18next + react-i18next |
| Markdown Rendering | react-markdown + remark-gfm |
| Icons | @ant-design/icons |
| Styling | SCSS Modules + CSS Custom Properties |
| Build Tool | Vite 6 (library mode) |
| Type System | TypeScript 5.6 (strict mode) |
| Testing | Vitest + @testing-library/react + jsdom |
| Package Manager | pnpm (workspace monorepo) |

