Screen recording of the program-status demo in Rex: hovering the background tab shows the root record Blocked while deploying, with the child records underneath (AP South at 70%, EU West blocked on the permission prompt, US East done). After approval, it shows Done · Deployed to 3 regions with the region records cleared.
feat: report program status over OSC 7501#20
Closes #18. Implements the whole program side of the Program Status Protocol (OSC 7501): every state and kind, child records, clear, and feature detection. Terminals such as Rex can then show whether an ashlar app, and each piece of work inside it, is idle, working, blocked, done, or in error.
API
tsx
| 1 | mount(App, { driver, statusApp: "deploy" }); // app for root reports; children inherit it |
| 2 | |
| 3 | useProgramStatus(() => ({ state: "working", msg })); // nearest record (root by default) |
| 4 | <StatusRecord id="eu-west" title="EU West"> // scope: reports inside go to eu-west |
| 5 | <ProgressBar value={p()} status /> // working + percentage |
| 6 | </StatusRecord> |
| 7 | <Spinner status /> // working, no percentage |
| 8 | |
| 9 | const eu = screen.status.record("eu-west"); // handles; ids checked once |
| 10 | eu.report({ state: "blocked", kind: "permission", msg: "Approve deploy?" }); |
| 11 | eu.clear(); // eu-west and its children |
| 12 | screen.status.clear(); // every record |
| 13 | await screen.status.detect(); // { states, kinds } | null, cached |
Behavior
- Spec rules in one place (
formatProgramStatus):kindonly withblocked, andprogressonly withworking/blocked, rounded and clamped to 0–100.msg/titleare base64 UTF-8 on one line (control characters become spaces), cut with an ellipsis at a UTF-8 boundary past 2048/192 bytes. Long error messages still reach the terminal; every report stays under the 4096-byte cap.- An invalid
state,kind, orappdrops the report:report()returns false and the hook warns. Ids follow the spec's grammar:record()throws on a bad one, and<StatusRecord>warns instead.
- Writes go straight to the terminal outside the frame diff, like OSC 52. Identical reports are written once per record. The dedup map is capped at the spec's 256 records, oldest first, matching terminal eviction.
- Several reporters on one record take turns. The latest report is shown; when its component goes away, the newest remaining report is shown again.
- Lifecycle: with no reporters left, a
working/blockedroot becomesidle, and a child record is cleared unless it'sdone/error.- This waits a microtask, so a component taking over the record in the same update reports first.
- Screen unmount, SIGINT, and SIGTERM do it synchronously for every record.
- Reports after unmount still write, so an app can leave
done/errorbehind before it exits.
- Detection sends
OSC 7501 ; ?thenCSI cand resolves when the device-attributes reply arrives, which always comes last, so no reply is left to leak into the shell.- It resolves null if only device attributes answer, after 1 s (an unref'd timer), without input, or after unmount, in which case nothing is sent.
- The reply's
states/kindslists default to the spec's fixed sets. - Reports are never held back waiting for it.
<StatusRecord>followsidchanges and settles the old record. Itsapp/title/msgfill in fields a report leaves undefined.
Left out
- OSC 9;4 progress. It shares the number 9 with iTerm2 notifications and garbles output in terminals that know only one of them. It could be an opt-in later.
- A
useStalledmapping: it isn't a state, and<Spinner status>already reportsworking.
Demos
bun --conditions=development demo/src/demos/program-status.tsx: a three-region deploy. The root isworking, each region is a child record with its progress, EU West blocks on a permission prompt (y/n), and the deploy endsdoneorerrorwith the regions cleared.bun --conditions=development demo/src/demos/program-status-states.tsx: set each state, kind, progress, and message by hand.- Both are in the
bun demomenu. In Rex, switch to another tab to see the indicators.
Verification
- Unit and screen tests cover:
- the formatter: bytes, text cutting, invalid values;
- the id grammar, including the 128-byte limit;
- subtree clear, and unmount/stop semantics, including idle children;
- detection: reply order, defaults, the DA1-first case, timeout, and after stop;
- the stdin reply path and SIGTERM, through a mounted screen with a stubbed TTY;
- StatusRecord paths, labels, and id changes;
- several reporters on one record;
- gap-free handoff.
- Rex 0.0.1793377904: both demos were driven with
rex send-keywhile recordingrex events --all --json. The event log showed the expectedprogram_status_changedandprogram_status_removedsequence for approve, deny, restart, quit, and cancel mid-deploy (regions cleared, then rootidle).
Independent of the Rex detection PR (#19).
Added 2 commits
Added 1 commit
- fix: share program status records and harden detectionddcbc6c