docs / react

React

React hooks for connection management and an xterm.js terminal component. The fastest way to build a terminal UI.

react/src/useWalkieTalkie.ts

Install

terminal
npm install @walkie-talkie/react @xterm/xterm @xterm/addon-fit
Peer dependencies: This package requires react >= 18. The @xterm/xterm and @xterm/addon-fit packages are optional peer dependencies — only needed if you use the TerminalView component.

Quick Start

App.tsx
import { useWalkieTalkie } from '@walkie-talkie/react';
import { TerminalView } from '@walkie-talkie/react';
import '@xterm/xterm/css/xterm.css';

export default function App() {
  const wt = useWalkieTalkie();

  if (wt.connectionState !== 'connected') {
    return (
      <button onClick={() => wt.connect('http://localhost:3456', 'your-token')}>
        Connect
      </button>
    );
  }

  return (
    <div style={{ height: '100vh' }}>
      <button onClick={() => wt.createTerminal(80, 24)}>
        New Terminal
      </button>

      {wt.terminals.map((term) => (
        <TerminalView
          key={term.id}
          terminalId={term.id}
          isActive={true}
          onInput={(data) => wt.sendInput(term.id, data)}
          onResize={(cols, rows) => wt.resizeTerminal(term.id, cols, rows)}
          registerOutput={(handler) => wt.registerOutputHandler(term.id, handler)}
        />
      ))}
    </div>
  );
}

useWalkieTalkie

react/src/useWalkieTalkie.ts

The main hook. Manages the WebSocket connection, terminal list, and output buffering. Call it once at the top of your app.

const {
  // State
  connectionState,     // ConnectionState
  terminals,           // TerminalInfo[]
  isResuming,          // boolean

  // Connection
  connect,             // (serverUrl: string, token: string) => void
  resumeSession,       // (serverUrl: string, sessionId: string) => void
  disconnect,          // () => void

  // Terminal management
  createTerminal,      // (cols: number, rows: number) => void
  sendInput,           // (terminalId: string, data: string) => void
  resizeTerminal,      // (terminalId: string, cols: number, rows: number) => void
  killTerminal,        // (terminalId: string) => void
  listTerminals,       // () => void

  // Output
  registerOutputHandler,  // (terminalId: string, handler: (data: string) => void) => unsubscribe
} = useWalkieTalkie();

Connection

connecting.ts
// Fresh connection with a token
wt.connect('http://192.168.1.50:3456', 'abcd-ef01-2345-6789');

// Resume a previous session (no token needed)
wt.resumeSession('http://192.168.1.50:3456', 'session-uuid');

// Check state
if (wt.connectionState === 'connected') {
  // Ready to create terminals
}

// Disconnect (clears saved session)
wt.disconnect();

Terminal Lifecycle

terminals.ts
// Create a terminal (80 cols x 24 rows)
wt.createTerminal(80, 24);

// The terminals array updates reactively
wt.terminals.forEach((term) => {
  console.log(term.id, term.shell, term.pid);
});

// Send input (keystrokes, commands)
wt.sendInput(terminalId, 'echo hello\n');

// Resize when the container changes
wt.resizeTerminal(terminalId, 120, 40);

// Kill a terminal
wt.killTerminal(terminalId);

Output Handling

output.ts
// Register a handler — receives live output AND replays the buffer
const unsubscribe = wt.registerOutputHandler(terminalId, (data) => {
  // data is a string of terminal output
  // Write it to an xterm.js instance, a textarea, etc.
  terminal.write(data);
});

// When done, unsubscribe
unsubscribe();
Output buffering: The hook buffers up to 100KB of output per terminal. When you call registerOutputHandler, the handler immediately receives the full buffer — so terminals restore their scrollback on re-mount.

TerminalView

react/src/TerminalView.tsx

A React component that renders an xterm.js terminal. Handles fit-to-container, resize events, and input/output wiring.

usage.tsx
import { TerminalView } from '@walkie-talkie/react';

<TerminalView
  terminalId={term.id}
  isActive={activeTab === term.id}
  onInput={(data) => wt.sendInput(term.id, data)}
  onResize={(cols, rows) => wt.resizeTerminal(term.id, cols, rows)}
  registerOutput={(handler) => wt.registerOutputHandler(term.id, handler)}
  fontSize={14}
  cursorBlink={true}
  theme={{
    background: '#0d1117',
    foreground: '#e6edf3',
    cursor: '#00d4aa',
  }}
/>

Props

PropTypeDefaultDescription
terminalIdstring-Unique terminal ID from the server
isActiveboolean-Whether this terminal is visible. Hidden terminals use display: none.
onInput(data: string) => void-Called when the user types
onResize(cols, rows) => void-Called when the terminal resizes
registerOutput(handler) => unsubscribe-Register an output handler. Must return unsubscribe function.
fontSizenumber14Font size in pixels
fontFamilystringSF Mono, Fira Code...CSS font-family string
lineHeightnumber1.2Line height multiplier
cursorBlinkbooleantrueWhether the cursor blinks
themeTerminalThemeDark themeFull xterm.js theme object

Default Theme

theme.ts
import { defaultTheme } from '@walkie-talkie/react';

// {
//   background: '#0d1117',
//   foreground: '#e6edf3',
//   cursor: '#00d4aa',
//   cursorAccent: '#0d1117',
//   selectionBackground: '#264f78',
//   black: '#484f58',
//   red: '#ff7b72',
//   green: '#3fb950',
//   yellow: '#d29922',
//   blue: '#58a6ff',
//   magenta: '#bc8cff',
//   cyan: '#39c5cf',
//   white: '#b1bac4',
//   brightBlack: '#6e7681',
//   brightRed: '#ffa198',
//   brightGreen: '#56d364',
//   brightYellow: '#e3b341',
//   brightBlue: '#79c0ff',
//   brightMagenta: '#d2a8ff',
//   brightCyan: '#56d4dd',
//   brightWhite: '#f0f6fc',
// }

Full Example: Tabbed Terminal App

TabbedTerminals.tsx
import { useState } from 'react';
import { useWalkieTalkie, TerminalView } from '@walkie-talkie/react';
import '@xterm/xterm/css/xterm.css';

export function TabbedTerminals({ serverUrl, token }: {
  serverUrl: string;
  token: string;
}) {
  const wt = useWalkieTalkie();
  const [activeId, setActiveId] = useState<string | null>(null);

  // Auto-connect on mount
  if (wt.connectionState === 'disconnected') {
    wt.connect(serverUrl, token);
  }

  if (wt.connectionState !== 'connected') {
    return <div>Connecting...</div>;
  }

  return (
    <div style={{ display: 'flex', flexDirection: 'column', height: '100vh' }}>
      {/* Tab bar */}
      <div style={{ display: 'flex', gap: 4, padding: 8 }}>
        {wt.terminals.map((t) => (
          <button
            key={t.id}
            onClick={() => setActiveId(t.id)}
            style={{ fontWeight: activeId === t.id ? 'bold' : 'normal' }}
          >
            {t.shell} #{t.pid}
          </button>
        ))}
        <button onClick={() => wt.createTerminal(80, 24)}>+ New</button>
      </div>

      {/* Terminal area */}
      <div style={{ flex: 1 }}>
        {wt.terminals.map((t) => (
          <TerminalView
            key={t.id}
            terminalId={t.id}
            isActive={activeId === t.id}
            onInput={(data) => wt.sendInput(t.id, data)}
            onResize={(cols, rows) => wt.resizeTerminal(t.id, cols, rows)}
            registerOutput={(handler) =>
              wt.registerOutputHandler(t.id, handler)
            }
          />
        ))}
      </div>
    </div>
  );
}