A keyboard-first terminal manager for AI coding agent sessions: Claude Code, pi, Codex, or any other CLI agent. Agents run in roster's own private tmux server; roster tracks whether each one is working, needs input, is idle or stopped, and whether you've seen its latest turn.
Claude Code is tracked through its hooks, pi through a small extension roster loads into it, Codex through hooks it hands each run it starts; any other CLI agent can be added as a generic agent, tracked by its screen.
The agents in the recording are a scripted stand-in for Claude Code (demo/).
Requires tmux ≥ 3.2, an agent on PATH (Claude Code, pi, Codex, or any CLI agent you
configure), and to build it a recent stable Rust
(rustup).
cargo install --git https://github.com/XobSod/agent-roster # roster into ~/.cargo/binFrom a clone:
cargo install --path . # the same, from the checkout
cargo build --release # or only build it: ./target/release/roster
cargo build # a debug build, quicker to build, slower to run: ./target/debug/rosterInstalling puts only the roster binary in place. What else roster needs it makes
as it runs, in ~/.local/state/agent-roster/: its database, its tmux config, and
the extension it loads into the pi sessions it starts.
To scroll back through an agent's history while attached, also set up scrolling back, and if you run roster inside your own tmux, the passthrough there.
Run roster for the TUI: sessions on the left, the selected agent's screen on the right.
One runs at a time (a second one says where the first is); roster commands work
alongside it.
Navigate with h j k l (or arrows; the list wraps around); Enter attaches (or folds a
heading); in an agent, a line at the top says where you are, like the preview's header
(project, the session's name if you gave it one, and its title), and Ctrl+Q comes back
and marks it read. / filters the list as you type, fuzzily (atrw finds "Auth
rework"; 'word matches exactly): Enter keeps the filter, Esc clears it. ? shows
every key, and q quits (in the projects and server views it goes back, like Esc).
Everything else is in three menus, which pop up their keys:
| Menu | Keys |
|---|---|
s session |
n new · c new chat (no project needed) · e edit · s start · x stop · r restart · u read/unread · t new title · o note · a adopt a conversation · d archive / restore · D delete for good |
p project |
n new · e edit · o note · f show only this project · d remove · l list (all projects; there these work without p) |
v view |
p all projects · t tmux server and database · s settings · u usage |
Sessions sit on shelves by attention. At the top what waits for you: Needs input
(questions, finished turns you haven't seen), Idle and Stopped; at the bottom
what you've handed off or finished: Working (dimmed) and Archived (folded;
Enter restores one, s D deletes it for good). Within a shelf it's first come,
first served (configurable). group_by = "project" puts them under each project
instead, archived ones still on their shelf. Coming back from an agent you gave a task, the cursor
stays where it was, on the next one waiting; after only reading, it stays on that
agent. A stopped or archived session's preview says what it was: when it stopped or
was archived, its project, title, note and last prompts, and whether starting it
resumes its conversation. For a stopped one whose agent exited with an error, it
also shows how the agent's screen ended (roster show prints all of it). A shelf's
or a project's heading previews its sessions at a glance: what each is waiting for
and since when, its last prompt, and its note's line.
New and edit open a form (Tab/↑↓ move, ←→ change, Enter submits). Projects
carry a default agent, model and effort for their sessions, and each agent its own
(model and effort in its settings), used when neither the session nor its project
picks one; "default" leaves it to that. A new session can also get a first prompt,
typed into the agent once it's ready (Codex gets it on its command line). s c starts a chat, a
session that needs no project: chats share one directory in a built-in project.
A Claude, pi or Codex conversation started outside roster (plain claude, pi or
codex, another
session manager) can be adopted with s a: pick it (typing filters, Tab shows those
outside your projects), and it continues as a session here, in its project or a new one
for its directory. It goes on with the model and effort it last ran on (as far as its
agent recorded them), the defaults (the project's, if the project runs the same agent,
else the agent's), or ones you pick in the form (--defaults, --model, --effort on
the CLI). Close it where it's open first, or adopt a copy and leave the original alone.
Roster can tell when one is open in Claude, but not in pi or Codex.
Each session can have a note, what's left to do in it, say. s o opens it under the
preview's header, the agent's screen still below, in an editor with vim's keys: normal,
insert and visual mode, motions, C, r (rx ticks a box, . the next), ciw and
the like, xp and ddp, / search, u, . (no counts). :w saves; :wq, :x or ZZ save and close; :q closes unless there are
unsaved changes, which :q! and ZQ leave. A note saved meanwhile by something else
(roster note, an agent using it) isn't saved over: :w! (:wq!, :x!) does that;
in $EDITOR, what you wrote is kept in its file instead, and roster says where. Esc doesn't close it, so pressing it once
too often is harmless; escape = "jk" under [notes] makes jk typed quickly leave
insert mode too. mm pins the cursor's line (✎; again there unpins it): above the
agent's screen the preview shows the pinned line and the one below (lines under
[notes] shows more, or only the one), so a to-do list
shows where you are in it (with no pin, the note's first lines). The pin follows its
line as you edit. Ctrl+G hands the note to $VISUAL or $EDITOR and back; with
editor = "external" under [notes], s o opens it there. A note stays through
stops, restarts and archiving, and goes only when its session is deleted for good;
✎ marks sessions with one.
Each project can have a note too, for what outlives its sessions: a backlog, what to
remember there. p o opens it, in the same editor (in the projects view, o). Its
pinned lines show beside those of a session's note, on the left, by ▤ where the
session's are by ✎ (on a narrow preview, above them); a note that isn't there takes
no room. With a session selected, p o opens beside its note: in the editor [ shows
the project's and ] the session's, as the preview has them, left and right. Each
keeps what you changed while the other shows, as vim's buffers do: :wa saves both,
:wqa (:xa) saves both and closes, :qa! closes leaving both; :q, :wq and :x
don't close while the other has unsaved changes. In the editor too, the pinned line is
marked ▤ in the project's note. ▤ after a project's name marks it having a note,
on cards and headings and in the projects view. It goes when the project is removed.
v u shows what's left of your Claude and Codex accounts' limits: the 5-hour and the
weekly window, each as a bar, full while none of it is used and shrinking as you use
it, with a mark where as much of the window's time is left (a bar ending short of it
is used faster than the window lasts), and when it starts over. Beside each, its
pace: ▼ slow (used slower than its time goes, so there's more to use), ◆ on pace
(within 5 points of its time), ▲ fast (at this rate it runs out before it starts
over), or ✕ used up; the bar and what's left are blue, grey or red to match.
The sidebar's last line has the same, briefly (claude 69%▲ 48%◆ codex 93%▼ 87%▼, the 5-hour window first; the agents' marks in place of their
names where those don't fit), dim when old. Roster asks
Claude.ai's usage service with Claude's login (through curl; the login is Claude's
to renew, so once it expires roster waits for Claude to), and Codex's own server
(codex app-server): at the start, every 5 minutes while the figures show, and when
the view opens; Enter there asks again. Codex also writes them into its rollouts as it
works, which roster reads as they change. pi and other agents say nothing of the kind.
To the right of the limits (h/l move along the tabs), what the agents spent in the
last 24 hours, 7, 30 or 90 days, from every conversation they keep on this machine,
started in roster or not: tokens of each kind (input, read from the cache, written to
it, output) and what they'd cost at the API's prices, by agent, with a chart of when,
and by model. Prices are roster's own table, from pi's model catalog; a model it has no
price for shows as unpriced, and pi's own costs count as pi says them. Roster keeps it in
its database, by the hour, so it stays once a conversation is gone: Claude deletes its
after 30 days (cleanupPeriodDays), and you may delete them yourself.
It goes back as far as the agents' files did when roster first read them.
Everything is also available from the CLI:
roster project add myproj ~/src/myproj --model opus --effort high
roster project ls # directories, sessions, defaults
roster new myproj --name api --attach # start Claude and go into it; Ctrl+Q comes back
roster new myproj --prompt "fix the failing tests" # typed in once Claude is ready
roster chat --prompt "explain tmux copy mode" # no project needed: a chat
roster edit api --model sonnet # applies on the next start
roster edit api --title "Auth rework" # your title; --title "" follows Claude's again
roster retitle api # a fresh title from its agent, from the recent prompts
roster info api # created, prompts, time worked, context, tokens, cost, its note
roster note api # print its note; --edit opens it in $EDITOR
roster note api --append "- [ ] check the fork path" # --set replaces it ("-": from stdin), --clear removes it
roster note api --pin 3 # the line the preview shows, with the one below; --unpin
roster note --project myproj --append "- [ ] retry on 429" # a project's note; the same options
roster note api --project # api's project's; inside an agent, --project alone is its own's
roster adopt --list # Claude, pi and Codex conversations started elsewhere (--all: not only in your projects)
roster adopt 3f2a --name api # continue one here; --fork adopts a copy instead
roster project edit myproj --effort default
roster ls # ● marks sessions with a turn you haven't seen, ✎ those with a note
roster ls --project myproj --sort attention # like the TUI's shelves
roster ls auth rework # only those matching (fuzzy, like / in the TUI)
roster show api # print its current screen
roster attach api # going in and out with Ctrl+Q marks it read
roster stop api && roster start api # start resumes the conversation
roster archive api && roster restore api # archived sessions keep their conversation
roster rm api # delete an archived session for good (its conversation can be adopted again)
roster events --follow # what the agents' hooks report
roster usage # what your Claude and Codex accounts have used of their limits
roster tokens --days 7 # what the agents spent: tokens by kind, and their cost at API prices
roster doctor # roster's tmux server and database, checked; --fix removes orphans
roster config # every setting: its value, yours or the default, what it does
roster config set agents.pi.title_model claude-haiku-4-5 # one changed in config.toml, the rest kept
roster config unset fit_preview # back to its default
roster config --defaults # a config.toml with every setting, commented out
roster config --edit # config.toml in $EDITOR (from those defaults if there's none)Sessions are addressed as project/name, name, or an id prefix. roster --help
lists every command.
All files are optional and live in ~/.config/agent-roster/:
config.toml: the settings view (v s) shows every setting with its value and what it does, and changes them: Enter (on/off, the next choice, a value typed, or for a key the next one pressed),←→(for a model, through the agent's; Tab completes one being typed),aadds a key,dputs the default back,nadds an agent,xtakes an agent's settings out,eopens the file in$EDITOR. A change keeps the rest of the file as it is, comments included (taking a setting out takes the comment just above it too), and goes through a symlink (a config kept in dotfiles stays there). Roster also reads the file again when it changes, after an editor or agit pull, say. The settings:-
default_agent: the agent new sessions get unless their project names one (defaultclaude;"pi","codex", or any agent under[agents]). On a machine where it isn't installed, Claude. -
shell: what sources the env files (default$SHELLif POSIX-like). -
chats_dir: where chats run (default inside roster's state directory). -
status_line:falsedrops the line at the top while you're attached (it takes a row from the agent). It's set as you attach, so a title the agent picks meanwhile shows from the next attach. Turning it on or off changes sessions' height by a row on their next attach. -
sidebar_usage:falsedrops the sidebar's last line, what your Claude and Codex accounts have used of their limits (v ustill shows it). -
group_by:"attention"(default), the session list on shelves by what waits for you, or"project", under each project's heading. -
[notes]:editor, wheres oandp oopen a note:"builtin"(default), roster's own editor with vim's keys, or"external",$VISUALor$EDITOR;escape, keys that leave insert mode when typed quickly, as vim'sinoremap jk <Esc>("jk", say; none by default), held back meanwhile;escape_ms, how quickly: at most this many milliseconds from one to the next (default 300). Typed slower, they're just text.lines, how many of each note's lines the preview shows, from the pinned one (default 2, at most 10). -
fit_preview: agents drawing full screen (in the terminal's alternate screen, as Claude Code's fullscreen renderer does) are sized to the preview while you're not attached, so it shows all of their screen;"never"turns that off, also per agent under[agents.<name>]. Agents drawing on the normal screen, Claude's classic renderer among them, keep your terminal's size: resizing would garble their history in tmux. -
[order]: per shelf (needs_input,idle,stopped,working,archived),"oldest"or"newest"first; by default first come, first served, and the archive newest first. -
[keys]: every TUI key. Each takes one key, a list, or""for none. A key is a character (j,D,/),C-x(Ctrl),M-x(Alt), or one ofEnter,Esc,Tab,BackTab,Space,Backspace,Delete,Insert,Up,Down,Left,Right,PageUp,PageDown,Home,End,F1–F12. A menu's keys act only inside it, so they may repeat other keys; within a menu, and among the top-level keys and menu openers, a key is used once. The defaults:[keys] detach = "C-q" # back to roster while attached (a tmux key name, with C- or M- unless an F key) up = ["k", "Up"] down = ["j", "Down"] # the list wraps around collapse = ["h", "Left"] # fold expand = ["l", "Right"] # unfold confirm = "Enter" # attach, fold or unfold a heading, restore when archived search = "/" # filter the list quit = ["q", "C-c"] # in the projects and server views: back help = "?" [keys.session] # the session menu: `menu` opens it, the rest act in it menu = "s" new = "n" chat = "c" # a new chat, in no project of its own edit = "e" start = "s" stop = "x" restart = "r" archive = "d" # or restore an archived one delete = "D" # an archived one, for good toggle_read = "u" retitle = "t" # a title generated from the recent prompts note = "o" # the session's note, to read and write adopt = "a" # a conversation started elsewhere [keys.project] menu = "p" new = "n" edit = "e" remove = "d" filter = "f" # show only this project's sessions list = "l" # the projects view note = "o" # the project's note, to read and write [keys.view] menu = "v" projects = "p" # the projects view server = "t" # the tmux server and the database settings = "s" # every setting, to see and change usage = "u" # what the agents' accounts have used of their limits
-
[agents.claude],[agents.pi]and[agents.codex]:command,args(added to every start; for pi on the normal screen, so tmux's copy mode scrolls its history,args = ["--tui-mode", "regular"]),modelandeffort, what new sessions get unless they or their project pick one (default: the agent's own, from its own settings, which roster doesn't change), andtitle_model, the model a generated title (s t) is made with (default:model; a small one, such ashaiku, is quicker). pi names no task itself, so roster has it make a title after a conversation's first turn;auto_title = falseturns that off. Any other agent goes in its own table and is then offered for new sessions:[agents.aider] # runs `aider`; or set command = "..." args = ["--no-auto-commits"] idle_after = 4 # seconds of a still screen that count as idle
Such agents are tracked by their screen and can't resume, so roster asks before starting one again.
-
env: sourced before every agent start, soTOKEN="$(token-cmd)"gets a fresh value each time. A command that fails stops the start, its error left on the session's screen. Leave outexport: every value is exported anyway, andexport TOKEN="$(token-cmd)"hides the command's failure, so the agent starts withTOKENempty. A.agent-roster.envin a project's directory is sourced after it.tmux.conf: additions to roster's tmux, such as the scrolling setup below. By default every key except Ctrl+Q goes to the agent.
State lives in ~/.local/state/agent-roster/. Uninstall: remove the binary and both
directories, and tmux -L agent-roster kill-server.
Roster starts pi with an extension of its own,
-e ~/.local/state/agent-roster/pi/agent-roster.ts, for its sessions only; your pi
settings and extensions stay as they are. The extension reports pi's state to roster:
working, idle, needs input while a dialog (an extension asking permission, say) holds
up a turn. A session's model is anything pi's --model takes (forms offer the ones
pi --list-models lists), its effort pi's thinking level, and its title one pi makes
after the first turn (with title_model), or the name you give its session (/name).
/new starts a new conversation in the same session; starting it again resumes the
last one (--session-id). The statistics come from pi's session file, cost included,
with the context window pi reports for the model. Its conversations started elsewhere
can be adopted, from ~/.pi/agent/sessions (or where PI_CODING_AGENT_DIR or
PI_CODING_AGENT_SESSION_DIR say); a copy is forked with --fork.
Roster starts Codex with hooks of its own, given to
each run as -c hooks.<Event>=[…] arguments, for its sessions only; your ~/.codex
config and hooks stay as they are. Codex runs a hook only once it's trusted, so the
first time, it asks you to review roster's (and again if roster moves, since the
command is part of what's trusted): trust them, or roster can't follow it. They report
its state: the tools it calls say working, a question it asks (PermissionRequest)
needs input, the end of a turn idle. Each run also gets --no-daemon, so its tools
run in the session's environment, your env files' included, rather than a shared
Codex server's.
Codex says nothing through its hooks before its first turn, so a session's first
prompt goes on its command line, as Codex's own, instead of being typed in: whatever
Codex shows first (a folder to trust, an update to install) is yours to answer. It
names its conversation itself, so roster learns it, and the file it's kept in, from the
first turn's reports; starting a session again resumes it by id (codex resume <id>),
and if Codex can't find it, it says so rather than starting another. Until Codex has reported it (with its hooks not trusted, it never does), roster doesn't know it, so starting the session again asks before beginning a new one. A session's model
is anything Codex takes as -c model= (forms offer what codex debug models lists,
each with the efforts it takes), its effort the reasoning level as
-c model_reasoning_effort= (none … ultra). Codex names each conversation itself,
from its first prompt, and that name is the session's title (as Claude's terminal title
is Claude's); a title made on request (s t, with title_model) is made by
codex exec, which keeps nothing.
The statistics come from its rollout, under ~/.codex/sessions (or wherever
CODEX_HOME says): prompts, tokens, the context window Codex counts, and the lines its edits changed. What a
subscription costs it doesn't say, so neither does roster info. Its conversations
started elsewhere can be adopted by their id; a copy is forked with codex fork. Codex
doesn't say which conversation a running Codex has open, so roster can only warn you
to close it first.
An agent's history lives in roster's tmux, which by default has no prefix and passes
every key except Ctrl+Q to the agent. To scroll back, give it a prefix and copy mode:
copy examples/tmux.conf to ~/.config/agent-roster/tmux.conf.
That example makes C-a roster's prefix, and this README uses C-a for it from
here on. It's only the example's choice: any key works. If you pick another, put it in
place of C-a in that file, in the snippet below, and wherever this README says C-a.
Then, while attached, C-a [ (or C-a Enter) opens copy mode with vi keys (k/j,
C-u/C-d, / to search, q to leave), and C-a C-a sends a literal C-a to the
agent. Roster's tmux re-reads the file the next time roster starts (the TUI, or any
roster command), even with agents running.
Claude Code's fullscreen renderer (/tui fullscreen) keeps the conversation itself
rather than in tmux's history, so copy mode doesn't see it there: scroll with its own
keys, PgUp/PgDn, or Ctrl+O for the transcript, which scrolls like less
(j/k, Ctrl+U/Ctrl+D, g/G, / to search) and where [ writes the whole
conversation into tmux's history, for copy mode. Its docs say the mouse wheel needs
tmux's mouse mode: set -g mouse on in roster's tmux.conf.
pi and Codex draw full screen by default, the same way. For copy mode, run them on
the normal screen instead, from their next start, in config.toml:
[agents.pi]
args = ["--tui-mode", "regular"]
[agents.codex]
args = ["--no-alt-screen"]Codex then writes each message into tmux's history once it's finished; the one being
written, and the prompt, stay below until then. alternate_screen = "never" under
[tui] in Codex's own ~/.codex/config.toml does the same outside roster too.
Roster works inside your own tmux unchanged: its agents run on its own tmux server, with its own small config, so your prefix and bindings stay out of the agents' way and its sessions never show up in your session list.
Two prefixes are involved here: yours, written <prefix> below, and roster's,
C-a with the setup from Scrolling back. Three things to know:
Scrolling back. Copy mode in your tmux shows your pane's older output (what ran before roster), never the agent's history: that's in roster's tmux. So:
-
If your prefix differs from roster's (yours is
C-b, say), roster'sC-a [in a roster pane already reaches roster's tmux. Nothing else to do. -
If they're the same key, your tmux takes it first: press it twice (
C-a C-a [), or add this passthrough to your~/.tmux.conf(examples/outer-tmux.conf). In a pane running roster, your usual<prefix> [and<prefix> Enterthen send roster'sC-a [/C-a Enter, opening copy mode on the agent's history; in every other pane they're your normal copy mode. It works with any prefix of yours; theC-ain it is roster's:bind [ if -F '#{m:*roster,#{pane_current_command}}' { send-keys C-a [ } { copy-mode } bind Enter if -F '#{m:*roster,#{pane_current_command}}' { send-keys C-a Enter } { copy-mode }
While attached, your tmux reports the pane's command as
roster(the attach runs in roster's process group), which is what the snippet matches.
Copying. y in roster's copy mode sends the selection to the terminal (OSC 52);
your tmux passes it on only with set -s set-clipboard on (its default, external,
drops it). Or copy straight to a clipboard tool from roster's tmux.conf:
bind -T copy-mode-vi y send -X copy-pipe-and-cancel 'clip.exe' # WSL; wl-copy, pbcopyKeys. Keys your tmux binds without a prefix are taken by your tmux before they
reach roster, as in any pane: vim-tmux-navigator's C-h/j/k/l, say. Keep Ctrl+Q free
in your tmux, or change roster's back key (detach under [keys] in config.toml).
For Shift+Enter (a newline in Claude), a binding in your tmux such as
bind -n S-Enter send-keys C-j sends it on as Ctrl+J, which Claude also takes as a
newline.
cargo test # unit tests, plus end-to-end tests that need tmuxROSTER_TMUX_SOCKET=<name> (and XDG_CONFIG_HOME/XDG_STATE_HOME) keep a
development build apart from your real sessions.
demo/render.sh records the GIF above from demo/tapes/ with
VHS in Docker, in a home of its own, with
demo/env/fake-claude as every agent.
