npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@particle-academy/fancy-term

v0.3.0

Published

Human+ Terminal for React — a controlled, themeable <Terminal> wrapping xterm.js, with hooks and an MCP-bridgeable surface so embedded agents read the buffer, write input, and run commands without DOM-scraping.

Readme

@particle-academy/fancy-term

Human+ Terminal for React — a controlled, themeable <Terminal> wrapping xterm.js, with hooks and an MCP-bridgeable surface so embedded agents read the buffer, write input, and run commands without DOM-scraping.

Like every Fancy UI component it serves two surfaces at once:

  • Authoring — terse and controlled (output + onData), JSON-friendly props (rows/cols, theme tokens, initial buffer), a stable data-fancy-terminal handle, and a ref exposing the full TerminalHandle.
  • Inhabited — that same handle is what an MCP bridge drives, so an agent reads the visible buffer and writes input through stable affordances, never the DOM.

Sexy by default via a Fancy dark theme drawn from the react-fancy Tailwind v4 tokens.

Status: 0.2.0. <Terminal> + useTerminal / useTerminalFit / useTerminalSession are in place, plus shell / profile switching (the <ShellSwitcher> component, controlled shells / activeShell props, and the session hook's switchShell). The registerTerminalBridge MCP bridge (terminal_read / terminal_write / terminal_run / terminal_set_shell) and the trust‑but‑verify staged-command affordance ship in @particle-academy/agent-integrations.

Install

npm install @particle-academy/fancy-term @xterm/xterm @xterm/addon-fit

react, xterm, and the fit addon are peer dependencies — the wrapper itself is zero-runtime-dep (the same posture as fancy-echarts over ECharts). Import the xterm stylesheet once in your app:

import "@xterm/xterm/css/xterm.css";

<Terminal>

The parent needs a height — the terminal fits its container (like any xterm surface); a 0‑height parent collapses it.

import { Terminal } from "@particle-academy/fancy-term";

function Console() {
  const [out, setOut] = useState("$ ");
  return (
    <div style={{ height: 360 }}>
      <Terminal output={out} onData={(d) => backend.send(d)} />
    </div>
  );
}

output is a controlled buffer: the component writes only the appended delta as it grows (replacing it wholesale resets + rewrites), so you can drive the terminal straight from React state — e.g. streaming command output via fancy-query's useFancyStream.

| prop | type | notes | |---|---|---| | output | string | controlled buffer (delta-appended) | | onData | (data: string) => void | user keystrokes / paste → your PTY/command backend | | onResize | (size) => void | cols/rows changed | | theme | TerminalTheme | xterm color theme; omit for the Fancy dark theme | | rows / cols | number | fixed grid; omit + keep fit to size from the container | | fit | boolean | auto-fit via the fit addon + ResizeObserver (default true) | | readOnly | boolean | block stdin (display-only) | | cursorBlink / cursorStyle | | "block" \| "underline" \| "bar" | | fontFamily / fontSize / scrollback | | | | initialOutput | string | written once on mount (uncontrolled use) | | shells | ShellProfile[] | the shells / profiles the host offers (see Switching shells) | | activeShell | string | controlled selected shell id (omit for uncontrolled) | | onShellChange | (id, profile) => void | fired when the user / setShell switches | | showShellBar | boolean | render the <ShellSwitcher> toolbar above the surface (default false) | | clipboard | boolean | enable the copy chord + paste interceptor (default true) | | onPaste | (payload) => void \| boolean | every paste; receives { text, files, images } — handle pasted images here. Return false to suppress the native text paste | | contextMenu | false \| Item[] \| (ctx, defaults) => Item[] | the right-click selection menu (see Clipboard & context menu) |

The ref exposes a TerminalHandle:

const term = useRef<TerminalHandle>(null);
// term.current.write / writeln / clear / reset / fit / focus
// term.current.getBuffer()      → the visible buffer as text (what an agent "sees")
// term.current.getSelection()   → current selection
// term.current.copySelection()  → copy the selection to the system clipboard (Promise<boolean>)
// term.current.paste(text?)     → paste text (or the system clipboard) into the terminal
// term.current.selectAll() / clearSelection()
// term.current.setShell("pwsh") → switch the active shell (fires onShellChange)
// term.current.getShell()       → the active shell id
// term.current.xterm            → the raw xterm.js instance (escape hatch)

Clipboard & context menu

Copy — select text, then Ctrl+Shift+C (Windows/Linux) or Cmd+C (macOS, with a selection) copies to the system clipboard. Plain Ctrl+C is never intercepted — it stays SIGINT.

Paste — text pastes natively. Images can't render in a shell, so a pasted image is handed to you via onPaste to upload / feed an agent / write a path:

<Terminal
  output={out}
  onData={(d) => backend.send(d)}
  onPaste={({ text, images }) => {
    for (const img of images) upload(img).then((url) => backend.send(url));
    // return false here to also suppress the native text paste
  }}
/>

Context menu — right-click shows Copy / Paste / Select all / Clear by default. Customize it with the contextMenu prop:

// disable it
<Terminal contextMenu={false} … />

// add to the defaults (the function gets the default items)
<Terminal
  contextMenu={(ctx, defaults) => [
    ...defaults,
    { id: "sep", separator: true },
    { id: "send-agent", label: "Send selection to agent", icon: "🤖",
      disabled: !ctx.hasSelection, onSelect: (c) => agent.send(c.selection) },
  ]}
  …
/>

Each item is { id, label?, icon?, disabled?, separator?, onSelect?(ctx) }. Set clipboard={false} to turn off the copy chord + image-paste interception entirely.

Switching shells

fancy-term is a frontend wrapper — it never spawns a shell. It owns the selected-shell state, the UI, and the change events; the host reacts by reconnecting its PTY / command backend to the chosen profile. That separation keeps the wrapper portable across any backend.

A ShellProfile is fully JSON-friendly (an agent can emit one):

interface ShellProfile {
  id: string;        // stable key, e.g. "powershell"
  label: string;     // display, e.g. "PowerShell"
  icon?: string;     // optional short glyph / emoji / single char
  command?: string;  // host hint, e.g. "pwsh" / "cmd.exe"
  args?: string[];   // host hint
  cwd?: string;      // host hint
}

command / args / cwd are host hints only — fancy-term passes the choice along; your backend decides what to launch. Spread the built-in presets and filter to what your host actually offers:

import { BUILTIN_SHELLS } from "@particle-academy/fancy-term";
// cmd · powershell · pwsh · git-bash · bash · zsh
const shells = BUILTIN_SHELLS.filter((s) => ["pwsh", "git-bash"].includes(s.id));

On the <Terminal> (controlled)

const [shell, setShell] = useState("pwsh");

<div style={{ height: 360 }}>
  <Terminal
    output={out}
    onData={(d) => backend.send(d)}
    shells={shells}
    activeShell={shell}
    onShellChange={(id) => {
      setShell(id);
      backend.reconnect(id); // host reconnects its PTY to the chosen shell
    }}
    showShellBar           // opt-in: render the built-in switcher toolbar
  />
</div>

The root carries data-fancy-terminal-shell="<id>" so an agent (or an MCP bridge) can read the active shell without DOM-guessing. Omit activeShell for an uncontrolled terminal that tracks the selection internally.

Standalone <ShellSwitcher>

Place the picker anywhere — it's a self-contained, accessible (Arrow / Enter / Esc), Fancy-dark-themed dropdown with zero third-party deps:

import { ShellSwitcher } from "@particle-academy/fancy-term";

<ShellSwitcher
  shells={shells}
  value={shell}
  onChange={(id, profile) => backend.reconnect(profile)}
/>

Stable handles: the root carries data-fancy-shell-switcher, each option carries data-shell-id.

In the session hook

useTerminalSession accepts a shell and exposes switchShell(id). Switching resets the buffer and calls the transport's optional connect(shell) so the host can (re)wire its backend. Transports that don't care about shells just omit connect — fully backward compatible.

const session = useTerminalSession({
  shell: "pwsh",
  transport: {
    send: (d) => backend.send(d),
    subscribe: (onChunk) => backend.onOutput(onChunk),
    connect: (shell) => backend.connect(shell), // (re)connect to the chosen shell
  },
});

// later — reset + reconnect to a new shell:
session.switchShell("git-bash");
<Terminal output={session.output} onData={session.sendData} />

Hooks

// Headless engine — the terminal without the component shell.
const handle = useTerminal(containerRef, { theme, onData });

// Auto-fit on resize, guarding the hidden-tab / late-mount 0×0 case.
useTerminalFit(containerRef, () => handle.fit());

// Bind to a streamed backend (PTY / SSH / command runner).
const session = useTerminalSession({
  transport: {
    send: (d) => echo.private(`pty.${id}`).whisper("stdin", { d }),
    subscribe: (onChunk) => {
      echo.private(`pty.${id}`).listen(".stdout", (e) => onChunk(e.chunk));
      return () => echo.leave(`pty.${id}`);
    },
  },
});
<Terminal output={session.output} onData={session.sendData} />

Human+ contract

<Terminal> is controlled (value/onData, activeShell/onShellChange), carries stable handles (data-fancy-terminal + data-fancy-terminal-shell, data-fancy-shell-switcher / data-shell-id, plus the ref API), takes JSON-friendly props (including ShellProfile[]), and is bridgeableregisterTerminalBridge (in agent-integrations) maps terminal_read / terminal_write / terminal_run / terminal_set_shell onto the handle, wraps mutations so every write broadcasts AgentActivity, and supports a staged "agent proposes → human confirms" mode for destructive commands.

License

MIT


⭐ Star Fancy UI

If this package is useful to you, a quick ⭐ on the repo really helps us build a better kit. Thank you!