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:
- Ratatui TUI — Terminal rendering and event handling
- Async IPC Bridge — Non-blocking communication with Mycelium
- 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
- Render — Draw to terminal buffer (60 FPS)
- Handle key — Process keyboard input
- 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>>,
}
- Send request with unique ID
- Store
oneshot::Senderin tracker - When response arrives, look up ID
- 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 commands —
s(spawn),d(delete),l(logs),o(open) - Modal interface — Press
sfor spawn dialog,Escto cancel - Visual feedback — Status indicators, real-time updates, color coding
- Keyboard-first — Everything accessible via keyboard
Key Bindings
| Key | Action | Context |
|---|---|---|
↑ / ↓ | Navigate list | Colony list |
Enter | View detail | Colony list |
s | Spawn colony | Colony list |
d | Delete colony | Colony list / detail |
l | Toggle logs | Colony detail |
o | Open in browser | Colony detail |
r | Refresh | Any |
q | Quit | Any |
? | Help | Any |
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:
- User opens logs for colony
abc123 - Stem GETs
http://localhost:8000/api/colonies/abc123/logs/stream - Mycelium subscribes to RingLogger
- Sends log events as SSE:
data: {"timestamp": "...", "level": "info", "message": "..."} - 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:
- Stem connects to
~/.colony/mycelium.sock - Sends
Pingto verify Mycelium responds - Maintains persistent connection
- 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)
Set RUST_LOG=debug before starting Stem for verbose logging. Useful when diagnosing IPC issues.
When to Use Stem vs Bloom
| Scenario | Use This |
|---|---|
| SSH session, no GUI | Stem |
| Local development | Bloom (richer previews) |
| Quick status check | Stem (faster startup) |
| Multi-window workflows | Bloom (Phase 2) |
| Log analysis | Bloom (better filtering) |
| CI/CD automation | Neither (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
- Configuration Reference — Understanding
colony.tomland service configuration - Networking Deep Dive — How components communicate
- Contributing Guide — Contributing to Stem development