Program status and agents

TL;DR: Any program that speaks OSC 7501 shows status in Fut without a plugin. Follow progress, messages, completion, and errors in the normal project tree and Agents sidebar. Claude Code, Codex, and Pi emit OSC 7501 natively; the agent plugins are legacy fallbacks for older versions, and screen detection is the last resort. Automation can use fut --json agent list, agent prompt, agent wait, and agent read without changing client focus.

OSC 7501: the default

Fut implements revision 0.4 of the Program Status Protocol. A program writes status to its terminal, for example:

printf '\e]7501;state=working:app=demo:progress=40\e\\'

The states are idle, working, done, blocked, and error. blocked can include kind=permission, question, or auth. working and blocked may include an integer progress=0 through 100; omitting it means indeterminate progress. app is a short stable program identifier. title and msg are base64-encoded, single-line UTF-8 plain text, not markup. Here is the shell function from the specification:

status() {
  printf '\e]7501;state=%s:msg=%s\e\\' "$1" "$(printf '%s' "$2" | base64 | tr -d '\n')"
}
status working "Syncing photos"
rsync -a ~/Photos backup:/photos && status done "Photos synced" || status error "rsync failed"

A report replaces the entire record at its id; no id addresses the root. Ids such as build/test form subtrees. Omitted fields are removed, except that an omitted app is displayed using its nearest ancestor’s app. Send state=clear:id=build to remove that record and its children, or state=clear to remove every record. Fut supports up to 256 records per terminal, evicting the least recently updated when full.

Records belong to the terminal, not the screen. A new shell prompt (OSC 133 A) or terminal-process exit removes transient working and blocked records. Observed foreground-program exit removes only that reporter’s transient records, preserving unrelated work. Done and error records survive until replaced or cleared. A full reset (RIS) clears all records, while a soft reset (DECSTR) and screen switching do not.

Programs can query support with OSC 7501 ; ? ST. Fut replies:

OSC 7501 ; ?:states=idle,working,done,blocked,error:kinds=permission,question,auth ST

The reply uses the query’s terminator (ESC \\ or BEL) and never includes record contents. Queries are optional: programs may report immediately. Fut rejects reports over the protocol’s bounds and text containing control characters. Displayed messages remain untrusted plain text.

Legacy OSC 9;4 progress

Programs that only emit ConEmu-style progress (OSC 9 ; 4 ; state ; percent ST, as emitted by tools such as cargo’s term.progress.term-integration) still show progress in Fut. Fut maps it onto the root record:

OSC 9;4 state Root record
1 set working with the percentage, or indeterminate without one
3 indeterminate working, indeterminate
4 paused working, keeping the last percentage when none is given
2 error error
0 remove removes the record that OSC 9;4 created

Paused maps to working, not blocked: a paused bar does not mean the program is waiting for the user, and blocked would raise attention.

This mapping is always on and deliberately weak. Its records report activity.source as {"type": "detection", "agent": "progress", "rule": "osc_9_4"}, so like screen detection they never make a terminal an agent in agent list, agent get, or the Agents projections; they drive the normal status, tab and workspace progress, and error attention. Explicit OSC 7501 and CLI reports take precedence: they replace the inferred record, OSC 9;4 is ignored while an explicit reporter owns the terminal, and 9;4;0 only removes what OSC 9;4 created. Once a program sends any OSC 7501 report, Fut ignores OSC 9;4 in that terminal until that program exits or a full reset (RIS), so legacy progress emitted alongside the real protocol cannot overwrite a blocked kind or message. A new prompt or a soft reset does not end this. The program is the foreground process that sent the report, as for its records; when the terminal’s own shell reports, only RIS ends it, and a report Fut reads only after its program has already exited counts as the shell’s. Sending state=clear does not make a program the reporter, so an attached Fut client forwarding its status (including fut --machine over SSH) releases the terminal when it exits. Progress after that exit treats a surviving done or error root like any new root report does: it replaces it, and result children stay. A new prompt or the exit of the foreground program that sent OSC 9;4 ends inferred working progress; an inferred error stays, like any error, until 9;4;0 or new progress replaces it.

Programs running inside Fut receive scoped FUT_SESSION_ID, FUT_WORKSPACE_ID, FUT_TAB_ID, FUT_PANE_ID, and FUT_TERMINAL_ID environment variables. These use Fut’s compact, reversible 23-character form; CLI ID arguments also accept canonical UUIDs from structured output. Resolve the terminal’s current live ancestry, including the pane’s current agent activity, with:

fut --json context

FUT_TERMINAL_ID is stable. The ancestor variables describe the spawn location and may be stale after a pane move; context resolves their current replacements from a fresh daemon snapshot.

Look up any session, workspace, tab, pane, or terminal outside that inherited context by its compact ID or canonical UUID without changing visual focus:

fut --json get ID

Print Fut’s bundled agent skill with:

fut agent skill

The printed SKILL.md is bundled with the binary, so its instructions match the installed Fut release.

Forward status to the outer terminal

An attached Fut client reports its own rollup to the terminal it runs in, using the same protocol. A terminal that supports OSC 7501, such as Ghostty or another Fut, then shows what every Fut terminal is doing. Running Fut inside a Fut terminal (including fut --machine over SSH) shows the inner Fut as one program with a child per busy terminal:

root          app=fut   state=blocked  msg="proj/main/claude: Apply changes?"
fPq...Yw      app=claude-code  state=blocked  title="proj/main/claude"
fX2...0Q      app=cargo        state=working  progress=40  title="proj/main/build"

Legacy plugin fallbacks

Current releases of Claude Code (2.1.295 and later), Codex, and Pi (1.1.0 and later) emit OSC 7501 themselves, so they show status in Fut with nothing installed. Fut still ships first-party lifecycle plugins as legacy fallbacks for older agent versions:

The plugins call fut agent report, so they work only inside Fut. They do not reach Fut from inside tmux, which runs outside the Fut terminal’s process tree and also drops OSC 7501. All feed the same status records as native OSC 7501, so installing a plugin alongside a native reporter is harmless but unnecessary. One exception remains: Codex does not report turn completion over OSC 7501, so its documented notify command setting is still the way to get completion attention. Screen-based Codex detection exists as a lower-priority fallback; explicit OSC or CLI reports take precedence.

Control integrated agents

The historical agent command names also cover non-agent programs such as build tools that report OSC 7501. List and inspect explicitly reporting terminals without changing focus:

fut --json agent list
fut --json agent get TERMINAL_ID

Each listed agent includes an unread boolean, and the list result includes unread_count for status bars and other external observers:

fut --json agent list | jq -r '.result.unread_count'

Blocked, done, and error transitions become unread daemon-wide. A done record waits while any record in the same terminal is still working, so an agent whose sub-tasks are running stays working until the last one settles or clears. Rendering that terminal in any attached client marks its observed attention read for every client and later CLI calls; a newer transition remains unread. Reading a done record quiets its completion indicator without changing its stored state. JSON includes activity.records, with app, title, message, kind, progress, and revision metadata, plus explicit activity.source provenance.

Submit a prompt as one atomic paste-and-Enter operation. Targets are always explicit, and a currently working agent is rejected with agent_busy:

fut --json agent prompt TERMINAL_ID 'review the failing test'
fut --json agent prompt TERMINAL_ID 'review the failing test' --wait --timeout 2m
fut --json agent prompt TERMINAL_ID --stdin < detailed-prompt.md
fut --json agent wait TERMINAL_ID --timeout 30s

Use --stdin instead of the text argument for multiline prompts or automation that should not pass prompt contents through shell argument parsing. Prompt text is read to EOF and submitted with the same atomic paste-and-Enter behavior.

prompt --wait captures the current lifecycle revision, then requires a fresh working transition before it accepts a later done, blocked, error, or idle state. Blocked and error are successful structured outcomes, not command transport failures. Standalone agent wait returns an already settled agent immediately or waits for a currently working one. Stable failures include not_an_agent, agent_busy, agent_timeout, agent_events_lagged, agent_exited, terminal_exited, and input_backlog_full, which means the program has stopped reading its input.

An older stored result does not settle a new turn: waits correlate fresh work with its own record. An integration that exits or clears its active report while the terminal stays alive returns agent_exited; terminal process death remains terminal_exited.

Read bounded terminal output together with the agent’s current state and available flag:

fut --json agent read TERMINAL_ID
fut --json agent read TERMINAL_ID --source recent-unwrapped --lines 200

available means the reporting terminal is open and no record is currently working. Blocked agents remain available for a follow-up prompt when no other record is still working.

Interactive agent projections

The global Agents dialog (Ctrl-b a) and Agents sidebars list only live, explicitly reporting terminals, including non-agent OSC programs. The default right sidebar uses the focused session and stays undocked when that projection is empty. Configure an Agents component on either side with scope = "tab", "workspace", "session", or "global". Tab, workspace, and session scope use fresh live focus ancestry when available and otherwise fall back to the selected IDs; global scope needs no focus anchor. Any closing agent-row ancestor excludes the row, and detection-only activity does not qualify. Global rows navigate directly across sessions by pane ID. The Notifications dialog remains separate and tracks daemon-wide unread blocked, done, or error attention, including attention inferred by best-effort screen detection. Rows show the program app, title, message, blocked kind, and determinate percentage when available. Status text is plain text and cannot inject presentation tokens or styles.

Terminal output

Read one terminal without attaching or changing another client’s focus:

fut --json terminal read TERMINAL_ID
fut --json terminal read TERMINAL_ID --source recent --lines 200
fut --json terminal read TERMINAL_ID --source recent-unwrapped --lines 200
fut terminal read TERMINAL_ID --ansi

visible reads the canonical bottom viewport. recent selects the last N physical rows and preserves soft wraps; recent-unwrapped selects the same physical window and joins its soft-wrapped rows. Historical sources default to 200 rows and accept at most 2,000. starts_mid_logical_line is true when an unwrapped window begins inside a logical line, and truncated reports that older physical rows were omitted. Reads inspect at most 250,000 cells and return at most 1 MiB; Fut returns a typed error instead of splitting UTF-8 or silently byte-truncating output. --ansi preserves terminal styling.

Wait for current or future plain-text output with one daemon-side deadline:

fut --json terminal wait-output TERMINAL_ID --literal 'ready' --timeout 30s
fut --json terminal wait-output TERMINAL_ID --regex 'done [0-9]+' --timeout 2m

Waits subscribe before their initial output check and then react to terminal updates; callers do not need polling loops. Durations use ms, s, or m and range from 1 ms to 1 hour. Match ranges are UTF-8 byte offsets into the returned text and always land on character boundaries. Literal and regex patterns are limited to 4 KiB.

The alternate screen supports only --source visible; recent history and unwrapping return alternate_screen. Other stable failures include invalid_regex, output_timeout, terminal_exited, invalid_output_rows, and output_too_large.

Codex screen detection

When the foreground process is Codex and no explicit reporter currently owns its status, Fut infers idle, working, and blocked state from the canonical live bottom viewport. Client scrollback does not affect detection. Resource JSON keeps inferred provenance explicit under activity.source with type detection, including the matched rule. OSC and CLI reports remain authoritative while their reporting process is active.

When testing this fallback, uninstall or disable existing Fut Codex lifecycle plugins first. A plugin report intentionally takes precedence and prevents the screen detector from affecting state. Inference stops and clears when Codex is no longer the foreground process.

A detected transition from working to idle stores done and creates the same daemon-wide unread completion attention as an explicit done report.

Set FUT_AGENT_DETECTION_LOG=1 on the Fut daemon to print each Codex process, command line, matched rule, state, and quoted canonical screen to daemon stderr. This diagnostic can include terminal content and should only be enabled while debugging.

Report status with the CLI fallback

Report semantic state from an integration with:

fut agent report working --source codex --agent-session-id SESSION --progress 40 --app codex
fut agent report blocked --kind permission --message 'Apply these changes?'
fut agent report completed --message 'Changes ready for review'
fut agent report idle
fut agent report exited

The legacy verbs map onto records: idle, working, and blocked retain their names, completed stores done, and exited clears the addressed record and its descendants (all records when no --id is given). Use --id for a child record, --app for a stable program identifier, --title for a short label, --message for plain decoded text, --kind for a blocked reason, and --progress for a percentage. CLI reports replace whole records just like OSC reports; repeat any fields that should remain. --turn-id is accepted and ignored for older integrations, not stored or used for synchronization.

The terminal defaults to FUT_TERMINAL_ID; outside that environment pass --terminal-id. fut terminal report remains a compatibility alias and accepts the same fields. Reports using inherited terminal context are owned by the reporting command’s live process branch, so the integration exits when that agent process ends even if it cannot send a final exited report. Automatic exit cleanup drops only that reporter’s transient records, preserving unrelated work, native replacements, and done/error results.

Done, blocked, and error transitions create daemon-wide unread attention. A changed blocked kind, title, or message also marks a new request unread; progress-only updates do not. Use Ctrl-b u to list waiting terminals and Ctrl-b Ctrl-b to jump to the next one. A terminal that both rings the bell and reports attention is listed and counted once, under its stronger kind, and clearing it clears both. Rendering a terminal marks its current attention as read for every attached client and later CLI calls.

Program status (OSC 7501)

Any program can report its own state by writing the Program Status Protocol escape sequence to its terminal. No Fut CLI or socket is needed, so it also works over SSH and inside containers:

status() {
  printf '\033]7501;state=%s:app=%s\033\\' "$1" "$2"
}
status working rsync
rsync -a ~/Photos backup:/photos && status done rsync || status error rsync

Fut answers the OSC 7501 ; ? support query and applies root-record reports like lifecycle reports: working, blocked, and idle map directly, done and error become completion attention, and clear ends the reported activity. app becomes the integration source. Working, blocked, and idle reports belong to the foreground job and end when it exits; done and error remain until seen. Child records (reports with an id) roll up into the root’s status, progress shows in the tab.progress and workspace.progress tokens, and kind and msg appear in the Agents sidebar and dialog.

Event stream

Outside tools can subscribe to Fut’s state changes instead of polling:

fut events

Each line is versioned JSON with the complete resource snapshot — sessions, workspaces, tabs, panes, and program status records. The first line is the current state; every later line is the state after a change. The stream ends when the daemon exits.