fancy-term
@particle-academy/fancy-term
Human+ Terminal — a controlled, themeable <Terminal> over xterm.js with hooks and an MCP-bridgeable surface agents read + drive.
$
npm install @particle-academy/fancy-termAuthoring surface
Humans compose it
Controlled value + onChange, JSON-friendly props, terse defaults — so people (and agents) build it fast.
Inhabited surface
Agents drive it over MCP
A bridge in @particle-academy/agent-integrations exposes typed tools — agents read and mutate state through stable handles, never DOM scraping.
terminal_readterminal_writeterminal_run
Why
A terminal is where humans and agents do real work — run a command, watch the output, decide what's next. Most apps embed
xterm.js raw: an uncontrolled DOM widget that an embedded agent can only read by scraping pixels and can only drive by faking keystrokes. That breaks the moment the layout shifts. fancy-term makes the terminal a proper Human+ surface — controlled so the host owns the buffer, themeable so it's sexy by default, and bridgeable so an agent reads the visible buffer and writes input through a stable handle, never the DOM. And because running a command is irreversible, it carries the trust-but-verify affordance: an agent proposes a command and a human confirms before it runs.What
A React
<Terminal> wrapping xterm.js. Output is a controlled buffer — pass output and the component writes only the appended delta as it grows (so you can stream command output straight from React state, e.g. via fancy-query's useFancyStream); onData forwards the user's keystrokes upstream. It carries a stable data-fancy-terminal handle plus a ref exposing a TerminalHandle — write / writeln / clear / reset / fit / focus / getBuffer() (the visible text an agent "sees") / getSelection() / .xterm (escape hatch). Three hooks ship the headless engine layer: useTerminal (create/own the xterm instance), useTerminalFit (ResizeObserver auto-fit, guarding the hidden-tab / late-mount 0×0 trap), and useTerminalSession (bind to a streamed PTY / command backend). xterm + @xterm/addon-fit are peer dependencies — the wrapper stays zero-runtime-dep, the same posture as fancy-echarts over ECharts — and a Fancy dark theme (drawn from the react-fancy Tailwind v4 tokens) is the default.How
Install
npm i @particle-academy/fancy-term @xterm/xterm @xterm/addon-fit and import the xterm stylesheet once: import "@xterm/xterm/css/xterm.css";. The parent needs a height (the terminal fits its container): <div style={{ height: 360 }}><Terminal output={out} onData={(d) => backend.send(d)} /></div>. Drive it from React state via output, or imperatively through the ref. Bind a backend with useTerminalSession({ transport: { send, subscribe } }) → <Terminal output={session.output} onData={session.sendData} />. To make it inhabitable, register the bridge from @particle-academy/agent-integrations: registerTerminalBridge(server, { adapter }) exposes terminal_read / terminal_write / terminal_run; turn on pendingMode and destructive commands are staged for a human to confirm (terminal_confirm / terminal_reject). See the xterm.js API docs for the underlying terminal options and addon ecosystem.Components
1 component · click any tile for a full demo, source, and install snippet.
