UI kit
zidane-ui: the desktop app's design system and chat components as one React package.
zidane-ui is the design system behind the desktop app and this site, plus the app's chat components: session list, transcript and composer. The chat components read a ChatHost, an object of readable stores and optional actions. A fixture, an in-page agent, your server or zidane-server can be the host.
| Layer | Import | Contents |
|---|---|---|
| Design system | zidane-ui | Tokens, surfaces, palette engine, buttons, menus, popovers, dialogs, form controls, motion helpers |
| Chat | zidane-ui/chat | ChatProvider, SessionList, Transcript / HostTranscript, Composer / HostComposer, HostInteractions |
| Hosts | zidane-ui/chat/headless, zidane-ui/chat/testing, zidane-ui/adapters | React-free model (ChatHost, stores, transcript pipeline, createFixtureHost) and hosts backed by a real agent |
The design system works without the chat layer. The chat layer works without zidane-ui/adapters. Every import is listed in Entry points.
Install#
npm install zidane-ui react react-dom motion
npm install zidane # for every area except appearance, plus the adapters and the server helpers| Peer | Version | Needed for |
|---|---|---|
react, react-dom | ^19.3 | Everything |
motion | ^13.5 | The design system and the chat layer. Overlay, FloatingSurface, Lightbox and the transcript's Grow animate with it |
zidane | ^8 | Optional: the adapters, the server helpers and every area except appearance: chat, approvals, app, files, git, keybindings, machines, onboarding (the setup wizard), palette, session, settings, terminal, workspace. They import zidane/chat/pure, a browser-safe slice with no agent runtime |
unocss | >=66 | Optional, for zidane-ui/uno only |
Without zidane you can use the design system (zidane-ui, zidane-ui/code/editor, the highlight and patch-parse workers, zidane-ui/labels, zidane-ui/uno), the appearance area, and the headless and testing entries of files and terminal. Markdown (react-markdown, remark-gfm) and the composer editor (Tiptap) are regular dependencies.
Styling#
Pick one of the two routes below. Do not use both.
Prebuilt CSS#
Use this route when your app does not run UnoCSS. Import one stylesheet once: zidane-ui/core.css for the design system alone, zidane-ui/styles.css for everything (chat, setup wizard, editor, terminal). It holds every utility class and icon that those components use, then the kit styles. If your page has no reset, import the reset first. Without one, native button and list styles show through.
import 'zidane-ui/reset.css'
import 'zidane-ui/core.css' // or 'zidane-ui/styles.css'The prebuilt files sit in the cascade layers zidane-ui.reset, zidane-ui.utilities and zidane-ui.kit, so your own unlayered CSS overrides the kit without a specificity fight. They only carry the utilities the kit uses: to put token classes in your own markup, take the UnoCSS route or use the --zg-* variables.
UnoCSS#
Add the preset, scan the kit's files and import the kit stylesheet after the Uno utilities. Your markup can then use token classes (bg-zg-raised, text-zg-muted). crispPhosphor draws regular Phosphor icons with their bold outline, so they stay crisp at the kit's 10 to 14px.
// uno.config.ts
import { crispPhosphor, presetZidane } from 'zidane-ui/uno'
import { defineConfig, presetIcons, presetUno } from 'unocss'
export default defineConfig({
content: { filesystem: ['src/**/*.{ts,tsx}', 'node_modules/zidane-ui/dist/**/*.js'] },
presets: [presetIcons({ collections: { ph: () => import('@iconify-json/ph/icons.json').then(i => crispPhosphor(i.default)) } }), presetUno({ dark: { dark: '.dark' } }), presetZidane()],
})/* your stylesheet, after the Uno utilities */
@import 'zidane-ui/styles/index.css';
/* or, to put the kit in a layer of its own: */
@import 'zidane-ui/styles/index.css' layer(zidane-ui);Components paint from --zg-* CSS variables with dark defaults, so they render before you set a theme.
Extension icons#
Extensions name their icons as data (icon: { gui: 'i-ph-rocket' }), so no stylesheet built ahead of time has them all. Call watchRuntimeIcons once at startup, on either route. It adds the rule of each i-ph-* class the page shows without one, from the same Phosphor set. The set loads the first time an icon is missing, in a chunk of its own.
import { watchRuntimeIcons } from 'zidane-ui'
import { crispPhosphor } from 'zidane-ui/uno'
watchRuntimeIcons({ loadPhosphor: () => import('@iconify-json/ph/icons.json').then(m => crispPhosphor(m.default)) })Build your own agent UI#
The example below puts a sidebar, a transcript, question cards and a composer on one host. This fixture host echoes every prompt. You can swap in a real host without a change to the components.
import 'zidane-ui/styles.css'
import { ChatProvider, HostComposer, HostInteractions, HostTranscript, SessionList } from 'zidane-ui/chat'
import { createFixtureHost } from 'zidane-ui/chat/testing'
import { useState } from 'react'
const host = createFixtureHost(
{ sessions: [{ id: 's1', title: 'First session', status: 'idle', createdAt: Date.now(), updatedAt: Date.now() }] },
{ reply: prompt => [{ kind: 'markdown', text: `You said: ${prompt}` }] },
)
export function App() {
const [id, setId] = useState('s1')
return (
<ChatProvider host={host}>
<div style={{ display: 'grid', gridTemplateColumns: '260px 1fr', height: '100vh' }}>
<SessionList activeId={id} onSelect={setId} search />
<main style={{ display: 'flex', flexDirection: 'column', gap: 12, padding: 16, minWidth: 0 }}>
<div style={{ flex: 1, overflowY: 'auto' }}><HostTranscript sessionId={id} /></div>
<HostInteractions sessionId={id} />
<HostComposer sessionId={id} attachments />
</main>
</div>
</ChatProvider>
)
}| Component | Role |
|---|---|
ChatProvider | Puts the host (and label overrides) in context |
SessionList | Lists host.sessions. The row menu shows only the actions that the host implements |
HostTranscript | Renders host.transcript(id). Transcript takes a plain event array instead. |
HostInteractions | Questions and plans the agent is waiting on |
HostComposer | Sends through host.send, steers or queues during a run, shows model pickers when the host offers models |
The inline styles are there because the prebuilt stylesheet holds only the kit's own classes. With UnoCSS, use utility classes.
Pick a host#
A host runs the agent behind the components. Every host returns a ChatHost, so switching hosts leaves the components unchanged.
| Host | Import | Agent runs | Use for |
|---|---|---|---|
createFixtureHost | zidane-ui/chat/testing | Nowhere (static data, scripted reply) | Demos, tests, docs, design |
createRunHost | zidane-ui/adapters | Your run function, in the page | Code that already streams zidane events |
createWorkerHost + exposeRunHost | zidane-ui/adapters | Same, in a Web Worker | Keeping the main thread free |
createSseHost + createSseHandler | zidane-ui/adapters, zidane-ui/server | A zidane chat engine on your server, over HTTP and SSE | A product with its own users, auth and session store |
createServerHost | zidane-ui/adapters | A running zidane-server, over WebSocket | Sessions shared with the TUI and desktop clients of that server |
Connecting a host sets up each one and lists their limits. Server clients compares a browser with other zidane-server clients.
What a host offers#
To write your own host, implement ChatHost. Only transcript(id) is required. Each optional member that you leave out hides its control:
| Member | Without it |
|---|---|
transcript(sessionId) | Required: the transcript store of a session |
sessions.list | No session list |
sessions.create, rename, setArchived, setFavorite, remove | No New, Rename, Archive, Favorite or Delete control |
send | The composer cannot send, so it does not render |
interrupt | No Stop button, and Escape does nothing |
steer, followUp | Enter does nothing during a run. The draft stays until the run ends |
interactions.pending, answer | No question or plan cards |
interactions.cancel | No Dismiss on a card |
models | No model picker |
toolOutput | No "Load full output" on a clipped tool result |
cancelTool | No Stop on a running ! command |
Entry points#
| Import | Contents |
|---|---|
zidane-ui | Design system: components, motion helpers, hooks, code highlighting (@pierre/highlights, loaded on first use), theme engine, brand marks, labels |
zidane-ui/code/editor | Code editor, file view, file tree, find and diagnostics (@pierre/diffs) |
zidane-ui/code/highlight.worker | The highlighting worker module, for ?worker or new URL |
zidane-ui/terminal | Terminal surface (@tahul/ghostty-web, loaded on first use), and the terminal area: TerminalProvider, TerminalPanel, TerminalDrawer, TerminalToggle, TerminalPanelTab (guide) |
zidane-ui/terminal/headless | TerminalHost, createTerminalStore, scope keys, DEFAULT_TERMINAL_LABELS |
zidane-ui/terminal/testing | createFixtureTerminalHost and demo terminals |
zidane-ui/labels | DEFAULT_LABELS, LabelsProvider, useLabels |
zidane-ui/uno | presetZidane(): tokens as a UnoCSS preset |
zidane-ui/chat | Styled chat layer, including the three entries below |
zidane-ui/chat/transcript | Transcript rows, renderer slots (TranscriptRenderersProvider), actions (TranscriptActionsProvider) |
zidane-ui/chat/composer | Composer, model and reasoning pickers, mention suggestions, attachment helpers |
zidane-ui/chat/interactions | Question and plan cards |
zidane-ui/chat/headless | React-free model: ChatHost, stores, session-list model, transcript pipeline, labels, diff tree |
zidane-ui/chat/testing | createFixtureHost, DEMO_TRANSCRIPT, DEMO_TRANSCRIPT_ALL_KINDS |
zidane-ui/approvals | Tool approvals: ApprovalsProvider, ApprovalBar, ApprovalCard, FileEditApproval, SafelistPanel |
zidane-ui/approvals/headless | ApprovalsHost, the pending queue, the safelist store, DEFAULT_APPROVALS_LABELS |
zidane-ui/approvals/testing | createFixtureApprovalsHost and demo approvals |
zidane-ui/onboarding | The setup wizard: OnboardingProvider, OnboardingFlow, the eleven steps, useOnboarding, OnboardingStepFrame |
zidane-ui/onboarding/headless | OnboardingHost, createOnboardingController, DEFAULT_ONBOARDING_LABELS |
zidane-ui/onboarding/testing | createFixtureOnboardingHost and demo data |
zidane-ui/app | AppHostProvider (provides each area's host, ChatProvider included, in one wrap), useAppHost |
zidane-ui/app/headless | AppHost: one optional host per area (chat, onboarding) |
zidane-ui/app/testing | createFixtureAppHost: an AppHost of the areas' fixture hosts |
zidane-ui/adapters | Browser-safe hosts: createRunHost, createWorkerHost / exposeRunHost, createSseHost, createServerHost |
zidane-ui/server | Node and Bun only (browser builds fail to resolve it): createSseHandler, createLocalEnvironment |
zidane-ui/server/testing | Node and Bun only: createScriptedProvider, demoTools |
zidane-ui/core.css | Prebuilt, the design system alone: its utilities and icons, then its styles |
zidane-ui/styles.css | Prebuilt, everything: utilities, icons, kit stylesheet |
zidane-ui/styles/index.css | Kit stylesheet alone, for UnoCSS apps (styles/<part>.css for one part) |
zidane-ui/reset.css | Optional reset (Tailwind preflight from @unocss/reset) |
dist/ has one module per source file. Only the CSS has side effects, so bundlers keep only what you import. No module has 'use client'. If you use React Server Components, import the kit from your own client component.
Labels#
Every rendered string comes from one dictionary. Override chat strings with ChatProvider's labels prop and design-system strings (Copy, Confirm, Cancel, Close) with LabelsProvider. Function labels take their values as arguments, so translations control word order. A prop passed to a component wins over the dictionary.
import type { ChatHost } from 'zidane-ui/chat/headless'
import type { ReactNode } from 'react'
import { ChatProvider } from 'zidane-ui/chat'
import { LabelsProvider } from 'zidane-ui/labels'
export function French({ host, children }: { host: ChatHost, children: ReactNode }) {
return (
<LabelsProvider labels={{ confirm: 'Confirmer', cancel: 'Annuler', close: 'Fermer' }}>
<ChatProvider
host={host}
labels={{
newSession: 'Nouvelle session',
searchSessions: 'Rechercher',
noMatches: query => `Aucune session ne correspond à « ${query} »`,
composerPlaceholder: 'Écrivez à l’agent…',
}}
>
{children}
</ChatProvider>
</LabelsProvider>
)
}Full lists: DEFAULT_LABELS (zidane-ui/labels) and DEFAULT_CHAT_LABELS (zidane-ui/chat/headless).