Skip to main content

Pilot

A cross-project view of every agent run: grouped by project, ordered by what needs you, with attention bands, park, snooze, and a demand chip per project.

Reviewed

What Pilot Is

Once you are delegating across more than one project, the question stops being what is this agent doing and becomes which of the twenty things I started actually needs me right now. Nothing in a per-project view answers that. You end up switching projects to check, which is the expensive part.

Pilot is the answer to that question. It is a read-across list of every agent run in every project and every scratch workspace, including runs whose view has been evicted from memory, grouped by workspace and ordered by what needs you. The unit is the run (one agent terminal in one worktree), not the project. A project-level count tells you where to go; a run tells you what is stuck.

In the interface the feature presents itself as "View all agents": that is the name on the keyboard shortcut, on the action in the launcher, and on the palette's own header, which reads All agents. Pilot is the internal name for the surface, and the one this documentation uses because it is shorter and unambiguous. Both refer to the same dialog.

Opening it

  • Option-Command-O Control-Alt-O Control-Alt-O toggles it from anywhere.
  • The action palette, under View all agents.
  • The All agents button in the footer of the project switcher, one step further out than the switcher itself: the switcher picks a project, this shows every project at once.

Nothing about the dialog persists. Not whether it was open, not the search query, not the filter segment. Every opening starts from the same place, so what you see is always the current state of the fleet rather than a view you configured last week and forgot about.

Fleet vs Pilot

Fleet and Pilot both deal with many agents at once and are easy to confuse. They answer different questions and sit on opposite sides of the read/write line.

  • Fleet is a write surface inside one project. You arm a set of terminals, and from then on your keystrokes and bulk actions fan out to all of them. It is a mode you enter.
  • Pilot is a read-across view over every project. It never sends anything to an agent. The only things you can do from it are open a run, park it, or filter the list. It is a dialog you glance at.

The overlap is that both speak the same state vocabulary, which comes from the agent state model and is also what drives worktree card badges and the sidebar's quick filters.

Attention Bands

Every run lands in exactly one of eight bands, ordered worst first. A run that has stopped and may not restart on input outranks one merely asking a question, which outranks finished work, which outranks work still in flight. Everything below that is context rather than a demand.

BandLabel on the rowGlyphColorArises from
blockedBlockedProhibition circleDangerWaiting with an error as the reason
needs-youWaitingHollow circleAmberWaiting on your input
reviewReady for reviewCheck circleBlueCompleted and not yet acknowledged
runningWorking / DirectingSpinner / interacting circleGreenWorking or directing
doneFinishedCheck circleNeutralCompleted at or before the workspace's completion watermark
parkedParkedPause circleNeutralYou parked it
snoozedSnoozedDashed circleNeutralYou snoozed it and the snooze is still live
idleIdle / ExitedHollow circle / exited circleNeutralAnything else

A demand is exactly the first three bands: Blocked, Waiting, and Ready for review. That is the load-bearing idea of the whole surface. Every count, every chip, and every footer sentence on this page is computed from those three and nothing else. A working agent is not a demand: it is doing its job. A finished-and-acknowledged run is not a demand: you already saw it. A parked or snoozed run is not a demand because you said so.

Two precedence rules follow from that. A park beats every state, including blocked. Parking is you saying "this one does not need me until further notice", and a model that second-guesses that promise the moment something looks urgent is a model you have to keep re-checking, the exact cost parking exists to remove. And a run carrying both a park and a snooze reads as parked: the stronger, indefinite decision wins.

Color is reserved for the three demand bands plus green for working. Shape always carries the state as a second channel, so the list is readable without relying on hue, and the state is in the row's accessible name either way. The vocabulary deliberately matches the worktree sidebar's quick filters: one state with two names across two surfaces is a vocabulary you have to learn twice.

Reading a Row

A row is one line, about 32px tall, in this order: state glyph, the agent's brand mark, the run title, the park note if there is one, the quiet cue if there is one, and a right-aligned age in a tabular column so every row's age starts at the same offset.

Two things are deliberately not drawn. The status word, because the glyph already says it. And the worktree label, because it is a scratch workspace's UUID as often as it is a branch name, and it was charging the truncating title for width to say nothing it needed. Both still ride the row's accessible name and both are still searchable.

A parked row is aged from the park, not from the underlying state transition. Otherwise parking a run that had been waiting three hours would immediately read "Parked · 3h", which answers a question the row is no longer about.

The quiet cue

A run in the working band that has produced no output for more than ten minutes picks up an amber quiet 12m beside its age. A working glyph next to a long silence is the one combination the age column cannot express on its own: without it, "working" and "working but wedged" render identically.

The silence is anchored on the later of the run's last output and its entry into the current working stint, so a run resumed after twenty quiet minutes of waiting does not inherit that silence and get stamped quiet 20m before it has had a chance to make a sound. The snapshot polls every five seconds, so the crossing shows up within one poll. Screen readers hear "quiet for 12m".

Grouping and Order

Groups are workspaces (projects and scratch workspaces alike) ordered by workspace MRU: most recently opened first, then name, then id to settle ties into a stable total order. Severity never lifts one project above another. A blocked agent in a project you left an hour ago does not outrank the one you are working in now, and because the order keys on when you last opened a workspace rather than on activity inside it, the list does not reshuffle between openings you did not cause. Spatial memory is worth more here than a marginally better ranking.

Rows within a group are ordered worst band first, then oldest first. Oldest-first is the anti-starvation rule: a stream of fresh completions must never bury the run that has been stuck for forty minutes, which is the exact failure that teaches you to stop trusting the top of the list. Parked rows order by oldest park, for the same reason they are aged from the park.

Group headers are structure, not options. Arrow keys walk agent rows and never stop on a heading. A header carries the workspace's tile, its name, a muted Current marker if it is the workspace you are already in, and the demand chip.

The demand chip

A workspace header holding any demand shows a count plus the worst band's glyph, in that band's hue. Five headers then answer "which project is on fire" without reading a single row. The chip is recomputed rather than inherited when a search or a filter narrows the group, so a header can never announce a blocked run that the query has already filtered out from under it.

Order holds under the pointer

The ranking is live: a run that blocks moves up while you are looking at it. But the order freezes from the first pointer movement over the list, and releases again when the pointer leaves, the keyboard takes over, or the window is backgrounded. A row cannot move out from under a click.

Search and Filters

Typing filters immediately. The query matches the run title, the worktree label, the agent name (the brand is drawn as an icon, but "codex" is a plausible thing to type) and the park note. Your own words about a run are often the string you remember it by. A workspace name matching admits every row in that workspace.

Matches stay grouped rather than flattening into one relevance-ranked list, and band order is preserved: demoting a blocked agent because a fresher one matched the query better would defeat the point of the surface. That is exactly why matching is gated on match quality rather than a bare subsequence test: with the ranking held fixed, a weak match is not sorted away from a strong one, it is interleaved with it, so quality has to be settled at the match or not at all.

Under the search box is a five-segment radiogroup, with All as the null option:

SegmentBands it admits
AllEverything
Waitingblocked, needs-you
Workingrunning
Finishedreview, done
Parkedparked

Points worth knowing about that table:

  • idle and snoozed appear in no segment except All. Both are things you have already decided are not news.
  • Waiting deliberately excludes review, so the footer's demand count and the Waiting segment cannot disagree about what is outstanding. Waiting is "an agent is stopped and cannot continue without you"; a completed run waiting to be looked at is a different errand.
  • Counts are computed over the query-intersected population before the segment applies. Counting afterwards would give every segment the length of the list already on screen, which tells you nothing you cannot see.
  • A segment is hued only while it actually holds a demand: Finished holding three acknowledged runs and Finished holding three unreviewed ones are the same number and opposite situations. Working is the exception: it is always green, because green already means working everywhere else in the app.
  • Selection follows focus, so arrowing across the bar applies each filter as you reach it.

The search box and the filter bar intersect with AND, so "show me the blocked agents in this repo" is one question rather than two.

Keyboard Model

Physical focus stays in the search box, which drives the list through aria-activedescendant.

KeyAction
Option-Command-O Control-Alt-O Control-Alt-O Toggle Pilot ("View all agents")
TypingFilters the list; focus stays in the search box
/ Move between agent rows; workspace headers are skipped
Home / EndFirst / last row while the search box is empty; otherwise they are ordinary caret keys
TabMove real focus from the search box to the active filter segment
on the filter barMove between segments; selection follows focus. Home / End jump to the first / last segment
EnterOpen the highlighted run, switching workspace first if it lives elsewhere
Alt+Enter ()Park the highlighted run, or edit its park if it is already parked
EscClose Pilot, or return to the list while the park editor is open

The footer hints mirror this: Open and Park / Edit park. Both disappear while focus sits on a filter segment or the list is empty, because a footer promising a key that visibly does nothing is worse than no footer. The park hint also disappears when the snapshot is not live, since a park is validated against a healthy snapshot before it is accepted.

Parking a Run

Parking is intent, not state. The agent underneath keeps doing exactly what it was doing; nothing is paused, interrupted, or signalled. What changes is how every attention surface treats the run: it drops to the parked band, stops counting toward any demand count or chip, and leaves the Waiting segment.

The point is that a deliberate wait stops reading as something that needs you. "This one is blocked on the migration finishing in the other repo" is a decision you made once; without a way to record it, that run re-asks you the same question every time you open the list, and the list stops being trustworthy.

The editor is a mode of the Pilot dialog rather than a second dialog stacked on it. It has two fields:

  • A Note, up to 500 characters, prompted with "Why is this parked? (optional)". It shows on the parked row, truncated to at most 45% of the row width so it shares space with the title rather than starving it, and it is carried in full in the row's accessible name and in the search index.
  • A Park until radiogroup. Its first option is "I unpark it myself" which is an indefinite park with no release condition. The remaining options are the other runs in the fleet: pick one and it becomes the park's gate. A run cannot gate its own park.

The buttons are Park (or Update park when re-parking), Unpark (only shown when the run is already parked), and Cancel. Enter commits from the note field and from the gate list.

How a gated park releases

Gate release is edge-triggered and time-fenced. A park gated on run B lifts when B transitions from busy (working or directing) to ready (idle, waiting, completed, or exited) with a timestamp strictly after the park was created.

Both halves matter. The edge requirement makes the common flow work without a race: "park A until B is free" is usually said while B is still waiting, just before you hand B the work whose completion the park is actually about. The time fence closes the other half: a busy-to-ready edge that happened before the park but was still in flight must not count as the gate coming free.

Ready is deliberately broader than "completed". Completion is pattern-detected from output, and most agents end a stint sitting in waiting and never emit a completion at all.

Note
If the gate never runs again, the park holds until you lift it by hand. That is the honest consequence of an edge-triggered rule, and it is stated rather than papered over with a timeout that would fire at an arbitrary moment.

Trashing a gate releases the parks it held, because a park that can no longer release must resurface rather than hide its run forever. Trashing the parked run does not unpark it: trash is restorable, and restoring a run should restore the intent recorded against it.

Park released

When a park lifts on its own you get a notification titled Park released, with a message naming the run and the reason ("its gate came free", or "its gate terminal closed") and your note quoted back. It carries an Open action that jumps straight to the run, switching workspace if needed. The parked run usually lives in a different project from the one you are working in, so the row reappearing in Pilot is not otherwise visible from where you are. See Notifications & Sound for how these surface and how to route them.

Parks are owned by the main process, so they survive a project's view being evicted from memory and they survive an app restart. Two bounds apply: at most 200 parked runs, and a park expires after 14 days. The expiry is a product decision rather than housekeeping: a park nobody has looked at in two weeks is stale intent, and honoring it silently forever hides a run behind a decision you no longer remember making.

Snoozing a Run

Snoozing is the shorter-lived cousin of parking. Right-click an agent terminal → Snooze, and pick one of four durations:

OptionEffect
15 minutesQuiet until 15 minutes from now
30 minutesQuiet until 30 minutes from now
6 hoursQuiet until 6 hours from now
Unlimited (until I interact)Quiet with no expiry: only your typing ends it

Wake now appears in the same menu once a run is snoozed. The menu only shows for a terminal Pilot can actually see, a live agent run it already tracks.

A snooze suppresses attention, not presence. A snoozed run that is working still counts as running everywhere it should; only the demand it makes on you is withdrawn.

Any typed input wakes it, including typing at an agent that is already working, which produces no state change at all. The duration you pick is a ceiling on how long the run can stay quiet, not the mechanism that ends the snooze. Most snoozes end because you came back and answered the agent.

Wake times are absolute wall-clock instants, so a machine that sleeps through the window wakes with the snooze already over. Expired snoozes are stripped before a row is built, so nothing in the interface needs a countdown or a clock. Snoozed runs also show on the project switcher's rows as a dashed dot and an "N snoozed" count. The same bounds as parks apply: 200 records, 14-day expiry.

Tip
Park or snooze? Park when the wait has a reason you want recorded and possibly a condition you want watched: it takes a note, it can be gated on another run, and it announces itself when it releases. Snooze when you simply do not want to think about this run for the next half hour. A park ends on a gate or by hand; a snooze ends on a clock or on your keystrokes.

This ladder is deliberately different from the notification inbox's snooze (1 hour, 4 hours, tomorrow, next week). An agent blocked on a question is a much shorter-lived thing than an inbox thread, and sub-hour defers are the common case for one and not the other.

The right side of the footer summarizes the fleet, counted by band rather than off the raw row total. When there is a demand it is a button ("2 agents need you") that applies the Waiting filter, isolating exactly the runs it just counted. With nothing waiting it degrades to plain text: "Ready for review", "Nothing needs you · 4 agents working", or "Nothing needs you · 12 agents".

Note the phrasing. "Needs you" is a sentence, not a state label: the state is called Waiting everywhere it is named as one.

When Pilot Cannot See the Fleet

The snapshot comes from the main process and can be late, unavailable, or stale. Pilot says which rather than guessing.

StateWhat you see
LoadingA skeleton shaped like a group heading and its rows, and, if it is still going after five seconds, "Still reading the fleet…"
Unavailable"Can't reach the agent host", with "Agents keep running. This reconnects on its own." There is no retry button, because the service already re-reads every few seconds: a control that does what the app is doing anyway is a promise you have to keep pressing.
StaleThe last known runs still render, behind "Can't reach the agent host, showing the last known state from 3m ago". The footer summary becomes "Last known: …" rather than claiming an all-clear it cannot vouch for.
Empty"Start an agent in any project", plus a Clear filter button when it was a segment or a query that produced the emptiness rather than an empty fleet.