← Docs · Reference

Stem Terminal Interface

Deep dive into Stem's Rust + Ratatui architecture, IPC communication, and keyboard-driven UX.

Stem is Colony’s terminal interface. Rust + Ratatui. Fast. Keyboard-driven. Works over SSH.

Architecture Overview

Stem is composed of three key parts:

  1. Ratatui TUI — Terminal rendering and event handling
  2. Async IPC Bridge — Non-blocking communication with Mycelium
  3. Component System — Modular UI components inspired by Lazygit
┌─────────────────────────────────────────┐
│            Stem (Rust)                  │
│  ┌─────────────────────────────────┐    │
│  │  Ratatui TUI (render loop)      │    │
│  │  - Keyboard events              │    │
│  │  - Component tree               │    │
│  │  - Terminal drawing             │    │
│  └─────────────────────────────────┘    │
│              ↕                          │
│  ┌─────────────────────────────────┐    │
│  │  Async IPC Bridge (Tokio)      │    │
│  │  - Non-blocking UDS transport   │    │
│  │  - Request/response correlation │    │
│  │  - SSE log streaming            │    │
│  └─────────────────────────────────┘    │
└─────────────────────────────────────────┘
              ↕ UDS + Protobuf
┌─────────────────────────────────────────┐
│         Mycelium (Gleam/OTP)            │
│  - Colony management                    │
│  - Actor supervision                    │
│  - RingLogger (ETS)                     │
└─────────────────────────────────────────┘

Rust + Ratatui

Ratatui renders to a virtual terminal buffer. Same library as gitui and bottom.

Why Ratatui:

  • Fast — Renders in <50ms, even with thousands of log lines
  • Cross-platform — Linux, macOS, Windows (via crossterm)
  • Immediate mode — Redraw entire UI every frame. No state sync bugs.
  • Rich widgets — Tables, lists, paragraphs, charts. Built-in.

Why Rust:

  • Performance — You can’t build a responsive TUI in Python
  • Memory safety — No segfaults in long-running sessions
  • Ecosystem — Tokio for async I/O, prost for Protobuf

Component Architecture

Each UI section is a struct implementing the Component trait:

pub trait Component {
    fn render(&mut self, frame: &mut Frame, area: Rect);
    fn handle_key(&mut self, key: KeyEvent) -> ComponentResult;
    fn update(&mut self, ctx: &Context);
}

Core components:

  • ColonyList — Scrollable list with status indicators
  • ColonyDetail — Logs, services, actions
  • StatusBar — Key bindings and connection status
  • LogViewer — Real-time streaming with auto-scroll

Component Lifecycle

  1. Render — Draw to terminal buffer (60 FPS)
  2. Handle key — Process keyboard input
  3. Update — React to external state changes

UI rendering (sync) stays independent from I/O (async).

Async IPC Bridge

Stem’s IPC bridge runs on Tokio. Communicates with Mycelium over Unix Domain Sockets. Protobuf-encoded.

Transport Protocol

Length-prefixed Protobuf frames:

┌───────────────┬────────────────────┐
│ Length (u32)  │ Protobuf Message   │
│ Big-endian    │ Binary-encoded     │
└───────────────┴────────────────────┘

Example exchange:

Stem → Mycelium: ListColoniesRequest
Mycelium → Stem: ListColoniesResponse {
  colonies: [
    { id: "abc123", name: "my-app", status: "Running" }
  ]
}

Request/Response Correlation

Multiple requests in-flight simultaneously. Correlated by request ID:

struct RequestTracker {
    pending: HashMap<RequestId, oneshot::Sender<Response>>,
}
  1. Send request with unique ID
  2. Store oneshot::Sender in tracker
  3. When response arrives, look up ID
  4. Awaiter gets response, updates UI

Stem can issue parallel requests (list colonies + fetch logs) without blocking.

Lazygit-Style UX

Inspired by Lazygit.

Design principles:

  • Single-letter commandss (spawn), d (delete), l (logs), o (open)
  • Modal interface — Press s for spawn dialog, Esc to cancel
  • Visual feedback — Status indicators, real-time updates, color coding
  • Keyboard-first — Everything accessible via keyboard

Key Bindings

KeyActionContext
/ Navigate listColony list
EnterView detailColony list
sSpawn colonyColony list
dDelete colonyColony list / detail
lToggle logsColony detail
oOpen in browserColony detail
rRefreshAny
qQuitAny
?HelpAny
Vim Users

Stem also supports j/k for navigation and / for search (planned).

Log Streaming (SSE)

Stem uses Server-Sent Events (SSE) for real-time logs.

Flow:

  1. User opens logs for colony abc123
  2. Stem GETs http://localhost:8000/api/colonies/abc123/logs/stream
  3. Mycelium subscribes to RingLogger
  4. Sends log events as SSE: data: {"timestamp": "...", "level": "info", "message": "..."}
  5. Stem parses and appends to LogViewer

SSE format:

data: {"timestamp":"2026-02-16T10:30:45Z","level":"info","message":"Service started"}

data: {"timestamp":"2026-02-16T10:30:46Z","level":"error","message":"Connection failed"}

Auto-scroll keeps latest logs visible. Manual scroll overrides when you navigate up.

UDS Communication

Stem talks to Mycelium over a Unix Domain Socket at ~/.colony/mycelium.sock.

Why UDS instead of HTTP:

  • Lower latency — No TCP/IP overhead
  • Security — Filesystem permissions control access
  • Reliability — No port conflicts

Connection lifecycle:

  1. Stem connects to ~/.colony/mycelium.sock
  2. Sends Ping to verify Mycelium responds
  3. Maintains persistent connection
  4. If dropped, shows “Disconnected” and retries

Log File Location

Stem logs to ~/.cache/colony/stem.log (not stderr). Keeps the TUI clean.

View logs:

tail -f ~/.cache/colony/stem.log

Log rotation:

Rotates at 10MB. Keeps 3 files:

  • stem.log (current)
  • stem.log.1 (previous)
  • stem.log.2 (oldest)
Debugging

Set RUST_LOG=debug before starting Stem for verbose logging. Useful when diagnosing IPC issues.

When to Use Stem vs Bloom

ScenarioUse This
SSH session, no GUIStem
Local developmentBloom (richer previews)
Quick status checkStem (faster startup)
Multi-window workflowsBloom (Phase 2)
Log analysisBloom (better filtering)
CI/CD automationNeither (HTTP API)

Stem’s for headless environments and keyboard-driven workflows. Bloom’s for rich previews, terminal emulation, and collaboration.

Building and Running

From the repository root:

# Build
cargo build --release --bin stem

# Run
./target/release/stem

# Or via cargo
cargo run --bin stem

Dependencies (handled by Nix):

  • Rust 1.83+
  • Tokio (async runtime)
  • Ratatui (TUI framework)
  • prost (Protobuf codegen)
  • crossterm (terminal backend)

Troubleshooting

Stem Shows “Disconnected”

Mycelium is not running or socket path is wrong. Verify:

# Check Mycelium is running
curl http://localhost:8000/health

# Check socket exists
ls -la ~/.colony/mycelium.sock

Garbled Terminal Output

Terminal size too small or incompatible. Try:

# Reset terminal
reset

# Ensure minimum size (80x24)
resize

Logs Not Streaming

SSE connection failed. Check Mycelium logs and network connectivity:

curl http://localhost:8000/api/colonies/{id}/logs/stream

Next Steps