@psci-labs/chat-ui
v0.3.0
Published
React UI for the Claude Agent SDK chat surface — <ChatProvider>, useChat() hook, and the default tool renderers
Downloads
360
Readme
@psci-labs/chat-ui
React UI for the Claude Agent SDK chat surface — <ChatProvider>, useChat() hook, and the 16 default tool renderers, themed with Tailwind v4 + CSS variables.
pnpm add @psci-labs/chat-ui react react-dom tailwindcssReact 18 or 19 is supported; Tailwind v4 is required — the package ships a
styles.css with @theme tokens that only Tailwind v4's CSS-first config
understands.
v0.1.0 limitations
- Code blocks — code, command output, and search results render as plain
monospaced
<pre>. Shiki-backed syntax highlighting is a planned post-0.1.0 enhancement. - AskUser —
freeform+yes-noare fully supported.single-select/multi-selectmodes degrade to freeform (the options are listed above the textarea). Full radio / checkbox UI lands once the runtime exposes an HTTP endpoint for browser-side tool responses. - Plan-mode renderer is interactive on
ExitPlanModewhen the tool is complete and the call is part of the latest message. Three buttons (Accept & Continue, Keep Planning, Accept & Start Fresh) send canned user-message prompts viauseChat().send/clearContext— they do NOT callrespondToTool(the SDK's plan-mode tools fire-and-complete with no pending response). For historical plans the renderer falls back to a read-only display.
Message rendering
Assistant text and thinking parts render as GitHub-Flavored Markdown by default (bold, italics, lists, blockquotes, tables, links, headings). Fenced code blocks are routed through the same Shiki-backed <CodeBlock> used elsewhere in the UI. External links open in a new tab with rel="noreferrer noopener". User input is left as plain text — markdown in user messages is rarely intentional and renders verbatim.
Minimal usage
import { ChatProvider, Composer, Conversation, defaultToolRenderers } from '@psci-labs/chat-ui';
import '@psci-labs/chat-ui/styles.css';
<ChatProvider
endpoint="/api/chat"
threadId={threadId}
renderers={{
...defaultToolRenderers,
// Bash: MyCustomBashRenderer,
// 'mcp__sharepoint__list_files': SharePointFiles,
}}
>
<Conversation />
<Composer />
</ChatProvider>;<ChatProvider> loads history on mount, opens an SSE stream when the user
sends a message, and exposes send / cancel / clearContext via
useChat(). The wire format matches @psci-labs/chat-runtime 1:1 — no
runtime configuration knobs needed.
The runtime returns 409 if a stream is already open for the thread, so the
<Composer> auto-disables while status === 'streaming' to keep the UI
in step with that contract.
Tool renderer registry
Renderers are keyed on tool name strings ('Read', 'Bash',
'mcp__sharepoint__list_files', ...), not Claude-SDK types. This is
deliberate — a future agent harness (opencode, pi) plugs in without
changing the renderer contract.
import type { ToolRenderer } from '@psci-labs/chat-ui';
const MyBashRenderer: ToolRenderer = ({ toolCall, isLatest }) => (
<pre>{JSON.stringify(toolCall.input, null, 2)}</pre>
);Renderers receive an optional isLatest prop on ToolRendererProps — true
when the tool call belongs to the last message in the conversation. Used by
the default plan-mode renderer to gate interactive controls on historical
plans; custom renderers can consume it the same way.
Anything not in the registry falls through to DefaultToolRenderer, which
prints the tool name, raw input, and result. Useful for new MCP tools you
haven't built a renderer for yet.
Apps that want to dispatch manually (e.g. wrap each renderer in a custom
card) can use the useToolRenderer(name) escape hatch hook to look up the
renderer for a given name without going through <ToolCall>.
Tailwind v4 setup
The package ships a CSS file with @theme tokens. Consumers run it through
their own Tailwind v4 build:
/* app's main CSS entry */
@import 'tailwindcss';
@import '@psci-labs/chat-ui/styles.css';
@source "../node_modules/@psci-labs/chat-ui/dist/**/*.{js,cjs}";
/* Optional: override any token to re-theme the chat surface. Last `@theme` wins. */
@theme {
--color-chat-user-bg: oklch(0.6 0.18 30);
}Tailwind v4 is declared as a peerDependency — the package will not work in apps that have not adopted Tailwind v4.
Available CSS variables
The @theme block exposes:
- Base palette —
--color-background,--color-foreground,--color-card,--color-card-foreground,--color-popover,--color-popover-foreground,--color-primary,--color-primary-foreground,--color-secondary,--color-secondary-foreground,--color-muted,--color-muted-foreground,--color-accent,--color-accent-foreground,--color-destructive,--color-destructive-foreground,--color-border,--color-input,--color-ring - Chat surface —
--color-chat-user-bg/-fg,--color-chat-assistant-bg/-fg,--color-chat-mention-bg/-fg,--color-chat-toolcall-bg/-fg,--color-chat-checkpoint-bg/-fg - Radii —
--radius,--radius-sm,--radius-md,--radius-lg
Dark mode kicks in under either .dark or [data-theme="dark"] — both selectors are honored so the package matches whichever theme-toggle convention the consumer already uses.
cn() utility
Re-exported so customer apps writing custom tool renderers can compose classes the same way the built-in renderers do:
import { cn } from '@psci-labs/chat-ui';
<div className={cn('px-4 py-2', isActive && 'bg-primary')} />;Internally it's just twMerge(clsx(...)). If a consumer already has its own
cn() they prefer, they can keep using it — the built-in renderers don't
care which one their callers use.
