Disclosure: this write-up was drafted with an LLM from my own notes, code,
and design decisions, then reviewed and published by me. The project, the
engineering calls described below, and the code excerpts are my own work on
agent-console.
When you have one coding agent, a terminal tab is a perfectly good interface.
When you have five or ten of them across several projects, the problem changes. You are no longer having one conversation. You are managing work in different states: one session is still running, one needs approval, one has failed, and another finished ten minutes ago in a tab hidden behind the rest.
I wanted to solve one specific problem: get a cross-session view without changing how Codex and Claude Code are used.
The boundary: do not rebuild the provider UI
Agent Console does not implement a unified chat frontend. It scans persisted provider sessions, groups them by workspace, reduces them to working, waiting, idle, and failed, and invokes the provider's own resume flow. Once inside a session you are still using the native Codex or Claude Code terminal UI.
This boundary matters. A unified frontend looks tidy, but it lags behind provider features and creates a second compatibility layer for resume behaviour, keybindings, approval flows, and rendering. The layer above the native UI — discovery, state, alerts, search, navigation — is small enough to keep correct.
The Rust side is deliberately boring: ratatui for rendering, portable-pty for child processes, vt100 for terminal emulation in retained panes, rusqlite for state, and no async runtime at all. The whole thing is one synchronous loop plus threads that own PTYs.
What I learned building it
1. Discovery is parsing someone else's private format
Neither provider offers an API for "list my sessions". Both persist transcripts, so discovery means reading ~/.codex/sessions/**/rollout-*.jsonl and ~/.claude/projects/**/<uuid>.jsonl and inferring structure from records that were never meant to be a public interface.
That is fine as an observation channel and terrible as a contract. Every provider release can move a field. The mitigation was to stop branching on the provider anywhere except one table:
pub struct ProviderAdapter {
pub kind: AgentKind,
/// True when this file is one of the provider's session transcripts.
pub accepts: fn(&Path) -> bool,
/// Parse one accepted transcript. `Ok(None)` means "not a usable session".
pub parse: fn(&Path) -> io::Result<Option<Session>>,
/// Optional enrichment applied to the provider's parsed sessions.
pub enrich: Option<fn(&Path, &mut [Session])>,
}
Discovery then walks the table instead of matching on an enum in five places:
for adapter in providers::enabled() {
let root = paths.root(adapter.kind);
let files = provider_files(root, adapter.accepts);
let mut parsed = parse_cached_files(files, adapter, cache);
if let Some(enrich) = adapter.enrich {
enrich(root, &mut parsed);
}
sessions.extend(parsed);
}
Plain fn pointers rather than Box<dyn Fn> keep the table a const, which means adding a provider is one entry and no allocation. AGENT_CONSOLE_PROVIDERS=codex narrows the same table at runtime — useful now that both vendors are shipping their own multi-session views and one side may become redundant.
The failure mode I cared about most: a typo in that variable must not silently produce an empty dashboard. An unrecognised value logs the reason and keeps every provider enabled.
2. Owning a PTY means deciding who dies with the TUI
The obvious design is to spawn agents from the TUI process. Then closing the dashboard kills every agent, which is exactly the behaviour that makes people keep twelve terminal tabs open instead.
So managed PTYs live in a separate long-lived daemon. Closing or crashing the TUI detaches; a new TUI reconnects and replays a bounded tail. No tmux dependency, and no forced git worktrees — sessions run in place.
Running in place means two dashboards could resume the same provider session and corrupt one transcript, so each provider session ID is guarded by a cross-process lease with owner information, safe refusal, and an explicit force-takeover. This is the part where Rust helped least and process hygiene helped most: the type system says nothing about a second process on the same machine.
3. Let the model summarise, not decide
working, waiting, and failed come from provider hook events and process state, reduced deterministically with a fixed precedence. A model is used only to compress a long conversation into a glanceable task summary, it runs through the session's own provider outside the coding conversation, and it can be turned off entirely with AGENT_CONSOLE_SUMMARIZER=off.
A related detail that only shows up in practice: an alert is a state transition observed while the runtime is live, not a state. A session that is already waiting when the dashboard starts is not news. Getting that distinction wrong makes the alert list useless within a day.
4. Terminal emulation is where the time goes
pty.rs is 6,600 lines — by far the largest module, and larger than discovery, state, and the whole UI combined. Retained scrollback per pane, mouse reporting for providers that request it, alternate-screen handling, selection and clipboard, resize, and reconnect replay each look small and are not.
Two concrete lessons. Managed Codex sessions run with --no-alt-screen so the workspace pane can retain and scroll the transcript in every state. And only three Ctrl chords are actually free once Codex and Claude Code have taken theirs — Ctrl-\, Ctrl-^, Ctrl-Q — so everything else has to be forwarded untouched to the child.
Current state
v0.0.12, supporting Codex and Claude Code, with packages for macOS, Linux, and Windows and signed/notarised macOS binaries.
cargo install agent-console
agent-console doctor # checks providers, hooks, clipboard, daemon, permissions
agent-console
Licensed MIT OR Apache-2.0. Source: https://github.com/buhuipao/agent-console
It is still early. The questions I care about are more fundamental than adding ten provider logos: is the state trustworthy, do alerts actually reduce tab switching, do shells stay attached to the right workspace, and is native resume smooth enough?
If you run several coding agents every day — what should a cross-session control layer show you, and what should it never take over?

Top comments (0)