Back

On this page

  • The bet: let the model be the parser
  • Process per widget
  • One trait, many widgets
  • The event loop
  • Set it up
  • Where it stands
Back

Apex

March 1, 2026 · 4 min read
GitHub

Claude Code is text in, text out. That's fine until you ask it something visual. "Show me my CPU." "Let me browse this folder." "What changed in git?" It answers with a paragraph you then have to parse in your head.

Apex is my fix for that. It's an AI-native workspace, written in Rust, that lets Claude open live, interactive terminal widgets in your tmux panes. You say what you want, Claude spawns a real dashboard you drive with the keyboard. No mouse, no separate app, no leaving the terminal.

The bet: let the model be the parser

The interesting decision here is what Apex does not do. There's no natural-language parsing anywhere in it. No intent classifier, no keyword matching. Apex just exposes four tools over MCP and lets Claude decide when to call them:

  • spawn a widget
  • update a running widget
  • query a widget's state
  • close a widget

That's the whole surface. When you say "keep an eye on my CPU," Claude maps that to "spawn a system_monitor widget" on its own. The model is the router. Apex just gives it hands.

The flip side of trusting the model is stopping it from overreacting, so the server tells it plainly when to sit still:

plain text
Apex: AI-native TUI workspace. Spawn rich terminal widgets in tmux panes. IMPORTANT: Only use Apex tools when the user EXPLICITLY asks to show a widget, display something visually, or interact with an existing widget. For normal conversations and questions, respond normally without any Apex tools.

Prompt design as flow control. It keeps coming up in agent tooling.

Process per widget

Apex is three Rust crates with a clean split. One is the MCP server. One renders a single widget. One holds the shared protocol between them.

The load-bearing choice is that every widget is its own process, living in its own tmux pane. That sidesteps the pain of many widgets fighting over one terminal's screen, and it lets tmux own the layout and focus. Apex just asks tmux to split, and the window manager everyone already runs does the rest.

Spawning a widget is, concretely, shelling out to tmux to split a pane and grabbing the id of the new one.

The server and each widget then talk over a per-widget Unix socket, one JSON line each way.

One trait, many widgets

Adding a widget is a small job because everything routes through a single Widget trait, so the app loop never has to know which widget it's holding. Each widget implements the same few methods: render itself, handle a keypress, handle an IPC message, tick, answer a state query, and an optional post_render.

The set so far includes a system monitor, a git dashboard, a syntax-highlighted file browser, an image viewer, and more. That last method, post_render, exists for one reason: the TUI library can't emit the escape codes for inline images, so image widgets draw their pixels straight to the terminal after the normal render pass.

The event loop

Each widget runs a small loop in the Elm style. Three sources feed one channel, a keypress, a timer tick, or an IPC message, and the loop handles one event per frame.

Set it up

Want to try it? You'll need Rust, tmux (3.3+ for images), and the Claude Code CLI.

Clone and build:

shell
git clone https://github.com/harsh7z/apex.git
cd apex
cargo build --release

Point Claude Code at the MCP server:

shell
claude mcp add --transport stdio --scope user apex ./target/release/apex-mcp

One tmux tweak so widgets can render images, add to ~/.tmux.conf:

shell
set -g allow-passthrough on

Then start a tmux session, run claude, and ask for something visual, "show me my CPU" or "open a file browser at ~/projects". Claude spawns the widget.

Folding keyboard input, timers, and cross-process messages into one stream is what keeps the widget code simple. It never reasons about where an event came from, only what to do with it.

Where it stands

Apex is a real, working system: the full MCP surface, tmux integration, a dozen widgets, and a self-cleaning lifecycle that sweeps stale sockets on startup and kills its own pane on quit. It's also an early, personal project. The interactive update and query path over the socket still has rough edges I'm hardening, and the test coverage is thin.

I'm writing about it because the architecture is the point. Process per widget in tmux, the model as the intent router, and a uniform trait over a stack of widgets. The lesson: the model is a better parser than anything I'd hand-write, and tmux is a better window manager than anything I'd embed. Apex mostly just gets out of the way of both.

EmailGitHub
@hvaptl