Skip to content

Commit eb00f7b

Browse files
committed
post: building occc
1 parent c5f2353 commit eb00f7b

1 file changed

Lines changed: 67 additions & 0 deletions

File tree

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
---
2+
title: "Building a Command Center for AI Agents"
3+
description: "I had a dozen AI agents running and no way to know which ones were stuck. So I built one."
4+
date: 2026-03-15
5+
tags: ["architecture", "tooling"]
6+
draft: false
7+
---
8+
9+
## The Problem
10+
11+
I run a lot of AI agents. On a busy day I might have eight or more OpenCode sessions going in separate tmux panes. Each one is working on something different. Some are humming along. Some are stuck waiting for me to approve a tool call. Some crashed ten minutes ago and I haven't noticed.
12+
13+
My workflow was: cycle through each tmux pane, glance at the bottom of the terminal, figure out the state, move on. This stops working past three or four agents. By the time I check the last one, the first might be stuck again.
14+
15+
I wanted a single screen that showed me everything. A GUI was out. I'm already in the terminal. I don't want to context-switch to a browser. So I built `occc` (OpenCode Command Center), a TUI in Rust using Ratatui. I built the whole thing in a day, with AI agents helping write it. Yes, I used AI agents to build a tool for monitoring AI agents. I'm aware of the recursion.
16+
17+
## Architecture
18+
19+
The system pulls from two data sources and merges them into a single view. A polling engine grabs updates on a background thread, and the main event loop handles rendering and keyboard input.
20+
21+
```mermaid
22+
graph LR
23+
DB[(opencode.db)] -->|sessions, messages| Poll
24+
Tmux[tmux panes] -->|pane content| Poll
25+
Poll[Polling engine\n500ms] -->|PollUpdate| Loop
26+
User -->|keypress| Loop
27+
Loop[Event loop] -->|render| TUI[Ratatui TUI]
28+
Loop -->|send-keys| Tmux
29+
```
30+
31+
The DB and tmux feed the polling engine, which pushes updates into the event loop. The event loop renders the TUI and can also send keystrokes back to tmux (for approving blocked agents).
32+
33+
## Two Data Sources
34+
35+
OpenCode stores all its session data in a SQLite database at `~/.local/share/opencode/opencode.db`. Messages, model info, tool calls, todos. It's a rich source of context for understanding what an agent has been doing.
36+
37+
But the DB has a blind spot. It doesn't reflect what's happening right now. If an agent is blocked on a permission prompt, the database still shows the last completed message. The session looks active when it's actually waiting for you.
38+
39+
The fix: also read the tmux pane content with `tmux capture-pane`. The pane shows exactly what's on screen at this moment. It's the live view.
40+
41+
Combined, the DB gives you context (what happened) and the pane gives you state (what's happening now). Neither one is enough on its own.
42+
43+
## Async Polling
44+
45+
The TUI has to stay responsive. If the main thread blocks on a database query or waits for eight `tmux capture-pane` calls to finish, the UI freezes. That's not acceptable.
46+
47+
Polling runs on a background thread at 500ms intervals (configurable with `--poll-interval`). It queries the DB, captures all the tmux panes, and sends a single update to the main thread through a channel. The main thread handles keyboard input and renders at ~60fps. It picks up new data whenever it arrives, but never waits for it.
48+
49+
I took this pattern from tmuxcc, one of three similar projects I studied before writing any code. The others were recon and ATM. Studying prior art before building saved me from a lot of wrong turns.
50+
51+
## Reading the Bottom of the Terminal
52+
53+
The hardest question was: how do you detect agent status without modifying OpenCode itself?
54+
55+
The answer is simple. Look at the last few lines of the terminal. OpenCode renders a status bar at the bottom of the pane. The status bar tells you everything.
56+
57+
If you see "esc interrupt", the agent is working. If you see "Confirm" and "Cancel", the agent is blocked on a permission prompt and needs your attention. If you see a `>` prompt, the agent is idle.
58+
59+
The detection logic scans the bottom five non-empty lines of pane content, bottom-up. It's simple, reliable, and version-independent. It mirrors what you'd do manually: glance at the bottom of the terminal and read the status bar.
60+
61+
No structured event parsing. No hooks. Just text matching on five lines of terminal output.
62+
63+
## Using It
64+
65+
I keep `occc` running in its own tmux pane now. One glance tells me which agents need attention. I press `a` to approve a blocked agent without leaving the pane. It's a small tool that removed a real friction point from my workflow.
66+
67+
Building it with AI agents was fitting. They wrote most of the boilerplate while I focused on the architecture and the polling design. The recursive irony wasn't lost on me, but mostly it just worked.

0 commit comments

Comments
 (0)