Skip to main content

Program Status Protocol

The Program Status Protocol (OSC 7501) is a novel terminal escape sequence protocol that lets a TUI/CLI program tell the host terminal what it is doing: idle, working, waiting on you, finished, or failed, and why. Rune understands it in every terminal, so a build, a deploy, or a coding agent you left running in another tab or workspace can tell you when it needs you, without you watching it.

The protocol is a generic terminal protocol, published in the Rex documentation. It describes state only and leaves presentation to the terminal. This guide covers what Rune does with each report, and the choices Rune makes where the specification leaves them to the terminal.

In Rune​

When a program running in a Rune terminal reports its status, Rune shows it in four places:

  • On the tab. A working tab's icon spins and an animation plays over its label. Blocked, finished, and failed tabs take an icon and color for that state. See Tabs.
  • On the workspace bar. Each workspace tab shows the most pressing status among its terminals, so you can tell which workspace needs you without switching to it. See The workspace bar.
  • As a notification. Every new message comes through as a notification, and a tab out of sight that becomes blocked, fails, or finishes raises one too. See Notifications.
  • In the status inbox. :programstatus lists every reported status across your workspaces, most pressing first, and jumps to the one you pick. See The status inbox.

Rune's own features report through the same model, and extensions can report the status of their tabs through the SDK.

Try it​

With runectl installed (pkg install runectl in the console), play one of the examples from the specification in a Rune terminal and watch the tab and the workspace bar:

runectl status demo --scenario deploy

To give any command a status without changing it, wrap it. The tab spins while it runs, then turns green or red, and Rune notifies you if you have moved on:

runectl status wrap -- make test

Nothing about the protocol needs runectl. This shell function, from the specification, writes the sequence directly:

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"

Reports travel in the terminal's output, so they reach Rune from SSH workspaces, from rune:// workspaces on the network, from inside containers, and through an ssh session you start in a Rune terminal.

What reports status​

SourceWhat it reports
Any program that writes OSC 7501Whatever it reports.
Claude Code v2.1.295 or laterWorking, waiting for your input, or done, with no setup.
runectl statusAny state from a script, a hook, or your shell, and runectl status wrap for any command.
Rune Agent chatsWorking while the agent thinks and runs tools, with what it is doing as the title. Blocked when it asks you a question or for permission. Done with the first line of its reply, or failed with the reason. Sub-agents and skills are records beneath the chat.
Rune console commandsEach command is a record: working while it runs, with a percentage when it reports progress (package and model downloads do), then done or failed. Interrupting a command drops its record.
Rune's permission promptsBlocked on permission, as app rune, until you answer the prompt.
ExtensionsThe status of the tabs they own, through the Go SDK.

How Rune shows it​

Tabs​

A tab's status comes from the terminal or extension behind it. With the shipped configuration:

StateTab iconTab name
workingA spinner, in cyanThe animations.active_content_tab effect plays over it
blocked? for permission and question, @ for auth, in yellowUnchanged
doneA check mark, in greenUnchanged
errorx, in redUnchanged
idleThe tab's own icon, in grayUnchanged

A program that reported idle is at rest and waiting on you, which is why its icon is colored until you see the tab; a tab whose program never reported anything is left alone. A blocked report without a kind takes the permission icon.

progress is kept with the status. Set terminal.status.content_tab.progress to true to split working tab labels at the reported percentage, painting the completed part with working_done_attr. The inbox always shows it as a progress bar. Every icon, color, and effect is configurable; see Configuration.

The workspace bar​

Each workspace tab on the bar shows the most pressing status in its workspace, drawn from every tab and from terminals that are not tabs, such as floating ! commands and minimized tasks. The order is:

  1. blocked, because the program cannot continue without you.
  2. error.
  3. done.
  4. working.
  5. A reported idle, which outranks a terminal that reported nothing.

With the shipped configuration the workspace's name turns yellow when blocked, red on error, and green once done. While the workspace is working, the animations.active_workspace_tab effect plays over its name, and a reported percentage fills the name in cyan from the left, so the tab reads as filling up as the work completes. The effect stops as soon as anything in the workspace is blocked, failed, or finished, so it never hides a workspace that needs you.

Notifications​

Rune raises a notification, in the workspace the status came from, in two cases:

  • A new message. Every time a record's msg is new or changes, Rune notifies it right away, whether or not you are looking at the tab, since nothing else shows the message. Report a message once per event, not with every report.
  • A state worth knowing about. When a terminal you are not looking at enters one of the states in terminal.status.notify (blocked, error, and done by default), Rune names the state, the kind of a blocked status, and the record's title. These notifications are rate limited to one per tab every terminal.status.notify_interval (2s by default), and the latest state wins. A message already announced in that state is not repeated.

The notification level follows the state: warning for blocked, error for error, success for done, and info otherwise. Each notification starts with the name of the tab it came from, followed by the reporting app when that adds anything. A terminal that is not a tab is named after its app, or terminal when it reported none.

A tab keeps one notification per state: a new message or state notification replaces the one the tab last raised in the same state, so a program reporting step after step shows its latest step only. The working notification stays open, with the tab's progress, until the tab leaves working, is closed, or you dismiss it; a later message brings it back. The others close on their own.

A notification in a workspace you are not looking at stays open until you switch to it and adds to the badge on its workspace tab. See Knowing when a worktree needs you.

The status inbox​

programstatus opens a floating window with a sidebar on the left and a live preview of the selected program on the right. Everything in it updates while it is open. The window sits at the top of the screen and takes at least 60% of its width, growing and shrinking downwards as the preview changes size.

The sidebar lists every tab and terminal that reported a status, across your workspaces, most pressing first, then by workspace and tab. Each shows:

  • the name of its workspace, as on the workspace bar, in gray, or silver once selected or under the mouse;
  • the state's icon and the tab's name, styled like the tab itself;
  • beneath, the state followed by the reporting app, in the state's color;
  • beneath that, while working with a reported progress, a progress bar with the percentage on its right, green as far as it is done and gray past that. See program_status to draw it with other characters.

A tab stays listed until you close it, even once you have seen what it reported: it then reads idle, followed by the app it last reported. A terminal that is not a tab leaves the list once it holds no record: once you have seen what it reported, or once it closes.

When the programs don't all fit, the sidebar scrolls to keep the selected one in sight: moving past the last program shown scrolls down, and past the first scrolls up. The mouse wheel scrolls the sidebar one program at a time without moving the selection, which comes back in sight as soon as it moves. An arrow at the bottom of the sidebar shows there are more programs below; its row stays empty once you reach the last one, so the list doesn't shift.

KeyAction
j / <down> / <ctrl-j> / <ctrl-n>Next program
k / <up> / <ctrl-k> / <ctrl-p>Previous program
ClickSelect a program
Mouse wheel over the sidebarScroll the programs
<enter>Switch to the program's workspace and show it
<delete> / <backspace>Close the program as :tabclose would: a tab or window is closed, a task stopped, and the companion terminal hidden. A working or blocked program, or a file with unwritten changes, asks first
<esc> / q / <ctrl-c>Close the inbox

Tabs that never reported a status are not listed, and the inbox is not bound to a key by default. Bind it like any other command under command.key_bindings.

Several records​

A program can report several records at once, one per id, to describe a tree of tasks. This is the deploy example from the specification:

OSC 7501 ; state=working:app=deploy:msg=RGVwbG95aW5nIHYyLjQuMQ== ST # Deploying v2.4.1
OSC 7501 ; state=working:id=us-east:title=VVMgRWFzdA==:progress=40:msg=UHVzaGluZyBpbWFnZQ== ST # US East / Pushing image
OSC 7501 ; state=blocked:kind=permission:id=eu-west:title=RVUgV2VzdA==:msg=QXBwcm92ZSBkZXBsb3kgdG8gZXUtd2VzdCAocHJvZHVjdGlvbik/ ST # EU West / Approve deploy to eu-west (production)?

The tab shows the most pressing record, in the same order as the workspace bar, so it shows blocked while eu-west waits for approval even though the root record still says working. When two records share the top state, the most recently updated one wins, and the inbox lists the tab once, in that state. A record without an app takes it from its nearest ancestor that has one, which is how the region records above report deploy.

When a status goes away​

Records live with the terminal, not with a screen. Rune ties their lifetime to events it already sees, and never asks a program to repeat a report to keep it alive.

StateRune drops it when
working, blockedThe terminal's process exits, or the shell starts a new prompt and marks it with OSC 133 A.
idleEither of the above, or you see it, as for done and error.
done, errorYou see it: you focus the tab or the window showing the terminal, such as a floating ! command or a task, press a key in its tab, or it is reported on what you are already looking at.

In every state a record also goes away when the program reports another one with the same id, when the program reports clear for it or one of its ancestors, and when its terminal closes. A full reset (ESC c) removes every record. Switching between the primary and alternate screen, and a soft reset, leave records alone.

A done or error record survives the process exit and the next prompt, so a program that exits as soon as it finishes should report one of them right before exiting, and leave it for you to find.

tip

Rune does not install shell integration. If your shell does not mark its prompts with OSC 133, a program that dies without reporting again leaves its working or blocked record until the terminal closes. Run runectl status clear to remove it, or start the program under runectl status wrap, which always reports how the command ended.

Protocol support​

Rune implements the whole specification. Where it leaves the choice to the terminal, Rune's behavior is the one described here.

Reports​

OSC 7501 ; pairs ST report
OSC 7501 ; ? ST feature detection

The body is a list of key=value pairs separated by :. Rune accepts both terminators, ESC \ and BEL. It follows the parsing rules exactly:

  • Whitespace around keys and values is removed.
  • A malformed pair (no =, an empty key, or a byte outside the value set) is skipped, and the rest of the report still applies.
  • Unknown keys are ignored, and a repeated key keeps its last value.
  • A report without state, with a state Rune does not know, or with a malformed id is ignored.
  • msg and title are standard base64 of UTF-8 text, with optional padding.
KeyValuesRune's handling
stateidle, working, done, blocked, error, clearRequired. clear removes the addressed record and every record beneath it, or every record when there is no id.
id/-separated segments of [A-Za-z0-9_.+-]Absent addresses the root record. Each report replaces its record completely.
kindpermission, question, authRead with blocked only. Picks the tab icon and is named in notifications. An unknown value is treated as absent.
progress0 to 100Read with working and blocked only. Absent or invalid means indeterminate.
app[A-Za-z0-9_.+-], up to 32 bytesNames the program in notifications and the inbox. Inherited from the nearest ancestor. An invalid value is treated as absent.
titlebase64 textThe record's label in notifications.
msgbase64 textNotified whenever it changes.

Feature detection​

Rune answers the query OSC 7501 ; ? ST with OSC 7501 ; ? ST, using the terminator the query used. The reply lists no states or kinds, which means exactly the five states and three kinds of the specification. Programs may also report without asking first.

runectl status query sends the query, followed by a primary device attributes request so it never waits for long, and exits with status 0 when the terminal supports the protocol and 1 when it does not.

Limits​

Rune enforces every limit of the specification. A report that breaks one is discarded whole, before any record changes.

ItemLimit
Whole sequence, OSC through ST4096 bytes
Key length16 bytes
msg2732 bytes encoded, 2048 decoded
title256 bytes encoded, 192 decoded
app32 bytes
id128 bytes in all, 32 per segment, 8 levels deep
Records per terminal256. A report that would create one more removes the least recently updated record.

Security​

Everything in a report is untrusted input, so Rune:

  • Discards a report whose msg or title is not valid UTF-8 or contains a control character, including newline and tab.
  • Strips zero-width and bidirectional formatting characters from msg and title before showing them anywhere, so a program cannot disguise what a notification says.
  • Never treats msg or title as markup.
  • Never writes anything from a report back to the terminal. The feature detection reply is fixed, and there is no way to read records.
  • Raises each notification in the workspace the status came from, prefixed with the tab's name, and lists every status in the inbox under its workspace and tab.
  • Rate limits the notifications a state change raises, per tab.

Relationship to other sequences​

SequenceIn Rune
OSC 9;4 (progress)Not read, and not mapped to a status. A program may send both without either affecting the other.
OSC 9, OSC 99, OSC 777 (desktop notifications)Not shown. Report a msg instead, which Rune notifies, or use runectl notify.
OSC 133 (shell integration)Rune reads A, the start of a prompt, to drop working, blocked, and idle records. The other marks are ignored.
OSC 0 and OSC 2 (window title)Set the tab name while terminal.dynamic_tab_name is on, which it is by default. Independent of the status.
BELHighlights the tab of an unfocused window with terminal.needs_attention_attr. Independent of the status.

Report from an extension​

Extensions report the status of the tabs they own through the Go SDK, without writing escape sequences. The model is the same: WindowManager.SetTabStatus replaces the record a browserapi.TabStatus addresses on the tab identified by its URI.

err := wm.SetTabStatus(uri, browserapi.TabStatus{
ID: "index",
State: browserapi.TabWorking,
Progress: 40,
Title: "Indexing",
})
  • State is one of TabIdle, TabWorking, TabDone, TabBlocked, TabError, or TabClear, and Kind one of TabKindNone, TabKindPermission, TabKindQuestion, or TabKindAuth.
  • Set Progress to -1 when there is no percentage: its zero value means 0%.
  • Rune fills in app with the extension's id.
  • Title and Message follow the protocol's limits, and a report that breaks one, or carries a control character, fails with an InvalidArgument error.
  • TabDone, TabError and TabIdle clear once you see the tab, and every record goes away when the tab closes.

Rune Agent reports its chats this way.

Configuration​

Program status is configured under terminal.status. The activity effects that play over working tabs are configured separately, under animations.active_content_tab and animations.active_workspace_tab; see Tab animations. The status inbox is configured under program_status.

KeyDefaultEffect
enabledtruefalse drops the status icons and colors from tabs and the workspace bar, and raises no status notifications. The activity effects have their own enabled switch under animations.
icons.permission, icons.question, icons.auth?, ?, @The icon of a blocked tab by kind.
icons.done, icons.errora check mark, xThe icon of a finished or failed tab.
spinner⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏The frames a working tab's icon cycles through.
spinner_fps10Spinner frames per second.
content_tabsee belowHow content tabs (files, terminals, agent chats) show their status.
workspace_tabsee belowHow workspace tabs on the bar show the status of their workspace.
notify[blocked, error, done]The states that raise a notification when a tab out of sight enters them. working is also accepted.
notify_interval2sThe minimum time between two state notifications for the same tab.

content_tab and workspace_tab take the same keys. Colors left at default keep the tab's own:

Keycontent_tabworkspace_tabEffect
progressfalsetrueSplit a working label at the reported percentage.
working_done_attrdefaultcyanThe completed part of a split label.
blocked_attr, done_attr, error_attrdefaultyellow, green, redThe name in that state.
idle_icon_attr, working_icon_attrgray, cyandefaultThe icon of a tab that reported idle, or the spinner.
blocked_icon_attr, done_icon_attr, error_icon_attryellow, green, reddefaultThe icon in that state.

For example, to be notified only when a program needs you or fails, and to color the names of finished and failed content tabs:

terminal:
status:
notify: [blocked, error]
content_tab:
done_attr: {fg: green}
error_attr: {fg: red}

Configuring the inbox​

The status inbox is configured under the top-level program_status. Colors left at default keep those of what they are drawn over.

KeyDefaultEffect
selected_attrgray backgroundStyles the whole selected program. Its colors win over those of the program's name and state.
hover_attrboldStyles the whole program under the mouse, as selected_attr does. On the selected program, the selection's colors win.
marker▌The marker drawn down the left of the selected program.
marker_attrdefaultStyles the marker. The colors it leaves at default are the foreground of browser.focus_tab_highlight_attr and the background of the selection.
workspace_attrgrayThe name of the workspace above each program's name. A workspace without a name takes no row.
workspace_highlight_attrsilverThe name of the workspace of the program selected or under the mouse, which gray would blend into the selection. Its colors, like workspace_attr's, win over selected_attr's and hover_attr's.
progress_bar🬋🬋🬋🬋The four characters a progress bar is drawn with, in order: its left end, what is done, what is not, and its right end, as in the console's ╢░_╟. Each must be one cell wide.
progress_attrdefaultBeneath the whole bar. A background set here stays over the selection's.
progress_done_attrgreenWhat is done of the bar.
progress_remaining_attrgrayWhat is not done of the bar.
progress_remaining_selected_attrsilverWhat is not done of the selected program's bar, which the gray above would blend into the selection.
progress_label_attrdefaultThe percentage on the bar's right.
more_below\uf063 (Nerd Font arrow down)The arrow drawn below the programs when there are more out of sight below.
more_below_attrdefaultStyles that arrow.
divider_attrgrayThe line between the programs and the preview.
placeholder_attrgrayThe text shown when no program reported a status, or the selected one has nothing to preview.

A bar's ends are colored as the part of the bar they stand in, so a single character repeated four times draws a bar that only its colors split. The default, 🬋, fills the middle third of a cell; Rune's GUI draws it itself, while a terminal running Rune draws it from its own fonts.

For example, to draw progress bars as the console does, on a dark track, and to highlight the program under the mouse:

program_status:
progress_bar: "╢░_╟"
progress_attr: {bg: "#303030"}
progress_remaining_selected_attr: {fg: gray}
hover_attr: {bg: "#3a3a3a"}

Troubleshooting​

Nothing shows when a program reports. Run runectl status query in the same terminal. If it reports the protocol unsupported while you are inside tmux, screen, or another multiplexer, the multiplexer is answering instead of Rune and does not forward the reports: run the program directly in a Rune terminal. Otherwise, check that terminal.status.enabled is not false.

A status never goes away. A working or blocked record whose program died needs the shell's next prompt to be marked with OSC 133, or a clear; see When a status goes away. A done or error record stays until you see it: focus the tab or the window showing the terminal, or choose it in the status inbox.

Too many notifications. Remove states from terminal.status.notify, or raise terminal.status.notify_interval. Messages of the same state replace one another rather than stack up.

Ask Rune Agent