A program running in a Programa terminal can report what it is doing with the Program Status Protocol, an OSC 7501 escape sequence. Programa shows the result in the sidebar, posts notifications, and exposes it on the socket.
printf '\e]7501;state=working:progress=40:app=cargo\e\\'
printf '\e]7501;state=done:msg=%s\e\\' "$(printf 'Build finished' | base64)"
printf '\e]7501;state=clear\e\\'Pairs are separated by : and split on the first =. Unknown keys are ignored. If a key
repeats, the last value wins.
| Key | Meaning |
|---|---|
state |
Required. idle, working, done, blocked, error, or clear. |
id |
The record this report is about. Without it the report is about the program itself (the root record). A / makes a child: deploy/us-east is a child of deploy. Up to 8 segments of 1-32 characters from A-Za-z0-9_.+-, 128 bytes in total. |
kind |
Only read when state=blocked: permission, question, or auth. |
progress |
Only read when state is working or blocked: an integer from 0 to 100. |
app |
A stable name for the program, 1-32 characters from A-Za-z0-9_.+-. A child without app uses its nearest ancestor's. |
title, msg |
Standard base64 text, padding optional. At most 192 and 2048 decoded bytes, valid UTF-8, no control characters. |
A report replaces its record completely. A report with an invalid id, invalid text or an unknown
state is discarded whole. state=clear removes the record named by id and every descendant
(a removes a/b, not ab); without id it removes all records. Each terminal holds at most
256 records: a report that would create the 257th is discarded, and updates to existing ids
still apply. Records are not saved with the session.
- A shell prompt (shell integration) removes
workingandblockedrecords at every depth.done,errorandidlesurvive prompts. - A full terminal reset (
printf '\ec') removes all records. - Closing the surface removes its records. A program that exits without a shell prompt being printed keeps its record until the next report, a clear, or a reset.
The root record drives the terminal's own status. Child records show up as helpers.
-
Helpers: every child record appears in the workspace's helpers popover (the "N helpers running" line on the sidebar row), with its
titleas the name,appunder it, a progress bar whenprogressis set, andmsgas the report preview.doneanderrorchildren fold under Finished; Clear removes them from the list and from the terminal's records. Child records never reach the socket:agent.task.listandworkspace.listcount only helpers reported there. -
A child whose last id segment is a helper's id from
agent.task.start(as 32 hex digits, since a segment holds at most 32 characters) updates that helper's state and progress instead of adding a row. Onlyworkingandblockedapply; the socket report finishes the helper. -
When a child record enters
blockedon a surface you are not looking at, Programa posts one notification naming the program'sapp("A deployer task needs your input"), at most one per 10 seconds per surface. Itstitleandmsgstay out of it, for the same reason as below. -
Sidebar:
workingandblockedshow as before;doneanderrorshow a check and a stop icon. A blocked or working surface in the same workspace outranks them. -
Notifications: when the root record enters
blocked,doneorerroron a surface you are not looking at, Programa posts one notification, at most one per 10 seconds per surface. The notification title is the Programa tab title and the body names the state. The program'stitleandmsgstay out of it, becausenotification.listreturns notification text over the socket and the spec forbids revealing record contents back to programs. Workspaces with a hook-managed agent are skipped, since the hooks notify. There is no setting for this. -
Socket:
surface.listandsystem.treereturnprogram_status, andsurface.waitacceptsprogram_state. Record text and child ids are never returned. Seedocs/socket-api.md. -
agent_stateprojects the root record:workingandblockedmap to themselves, andidle,doneanderrormap toidle. The surface'sagent_state_sourcereads"program".
OSC 7501 outranks hooks, and hooks outrank screen inference.
- A program report always applies.
- While the root record is
workingorblocked, hook and inferred reports are ignored. - While the root record is
idle,doneorerror, a hook report is accepted only if it saysworkingorblocked. Then the hook owns the surface'sagent_state, and the record stays visible inprogram_status. - Screen inference does not run on a surface that a program or a hook owns.
- While the program owns the surface, a hook or inferred clear is ignored in every state.
surface.report_agent_state rejects source: "program".
The diagram shows a report for a surface whose agent_state the program owns:
flowchart TD
R[Report arrives] --> S{Source}
S -->|OSC 7501 program| A[Always applies]
S -->|hook or screen inference| Q{Root record state}
Q -->|working or blocked| X[Ignored, hook clear too]
Q -->|idle, done, error| H{Hook says working or blocked?}
H -->|yes| O[Hook owns agent_state]
H -->|no| X
The Claude Code, Codex and OpenCode hooks write OSC 7501 to the terminal as well as reporting to
the socket, so the report also reaches a terminal where the socket is out of reach, such as an SSH
session. The Claude Code hooks also report each subagent as the child record
agents/<helper id>: working from SubagentStart and each of its tool calls, blocked when it
asks for permission or input, and done with the first line of its report. Its title is the task
description Claude gave it, or its agent type. They emit only when PROGRAMA_SURFACE_ID is set, and they skip emission inside tmux
(TMUX is set), where the sequence would need passthrough and can reach the wrong pane.
OSC 9;4 progress is a separate feature and is not mapped to program status.