Hermes Switch UI — Terminal PTY Architecture

How keystrokes travel from xterm.js in the browser to a real OS shell and back

Legend Browser (frontend) Server (Node.js) Child process (Python) OS (external) Input flow Output flow Browser TanStack Start SPA xterm.js terminal FitAddon + WebLinksAddon resize · split · color rendering Terminal screen React component + tab bar SSE output stream /api/terminal-stream REST input APIs POST /api/terminal-input POST /api/terminal-resize POST /api/terminal-close Server (Node.js) TanStack Start server module terminal-sessions.ts session registry (Map) idle reap timer · EventEmitter API route handlers terminal-stream.ts (SSE) terminal-input.ts terminal-resize.ts · terminal-close.ts createTerminalSession() spawn child_process sendInput · resize · close stdio bridge child.stdin.write() → PTY input child.stdout → EventEmitter Python child process pty-helper.py pty-helper.py Python PTY bridge no node-pty native addon needed os.openpty() master/slave FDs TIOCSWINSZ resize OS kernel TTY layer TTY device /dev/pty* Shell process $SHELL • bash / zsh INPUT keystrokes key event POST /terminal-input stdin.write() write to master FD OUTPUT shell stdout read master FD stdout chunks SSE event stream xterm.write(data)

Browser — xterm.js

  • • xterm.js renders VT100/ANSI output
  • • FitAddon resizes canvas to container
  • • Keystrokes sent via POST /api/terminal-input
  • • Output arrives via SSE /api/terminal-stream

Server — terminal-sessions.ts

  • • Session registry as in-memory Map
  • • Spawns pty-helper.py via child_process
  • • EventEmitter relays stdout to SSE listeners
  • • Idle reap timer (5 min default) cleans up

pty-helper.py — Python PTY bridge

  • • Uses os.openpty() for real TTY master/slave
  • • Avoids node-pty native addon requirement
  • • Handles TIOCSWINSZ for terminal resize
  • • Copied to dist/ at build time