protocols: relay OSC 7501 program status as experimental support #67
Labels
No labels
Area/ci
Area/cli
Area/client
Area/client-core
Area/daemon
Area/deps
Area/docs
Area/grid
Area/manual
Area/nix
Area/protocol
Area/pty
Area/render
Area/shaping
Area/skills
Area/transport
Area/vt
Compat/Breaking
Kind/Bug
Kind/Documentation
Kind/Enhancement
Kind/Feature
Kind/Performance
Kind/Security
Kind/Testing
perf-regression-accepted
Reviewed
Confirmed
Reviewed
Duplicate
Reviewed
Invalid
Reviewed
Won't Fix
Status
Blocked
Status
Need More Info
Status
Upstream
No milestone
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
felis-terminal/felis#67
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
OSC 7501, the Program Status Protocol, lets a program tell the terminal it is idle, working, blocked on the user, done, or failed, and why. Claude Code, Codex and Terraform already emit it, and libghostty-vt parses it (ghostty-org/ghostty#14560). felis has no reliable way today to tell that a TUI agent in a session is waiting on the user:
sessions send --waitrelies on OSC 133D, which a TUI never emits.felis should support it as experimental, following the notification relay posture (
docs/explanation/protocols/notifications.md): decode the reports, keep the records in the daemon, expose them on IPC, draw nothing (no window flash, no attention hint). "Experimental" is a documentation label only, naming the spec revision implemented (0.3 at the time of writing). It does not relax the IPC/CLI contract. Instead the shape below is built so that following a spec revision, or withdrawing support, needs no breaking change.Layering
kindis a table change. Admitting a newstatealso needs a lifetime decision in the daemon.OSC 7501 ; ? STreply, and emits typed events for reports and RIS. It stores no records (same split as libghostty).Decoder contract
All checks finish before any record changes:
id). An unknown key is ignored (dropped, not passed through). For a repeated key, the last one wins.kind, an out-of-range or misplacedprogress, a misplacedkind, or anappoutside its charset is treated as absent.state, or a presentidthat fails the grammar, is ignored. It never falls back to the root record.msg/title. The 4096-byte sequence cap is the decoder's job, sinceOSC_BUFFER_LIMITis 8192.Records and lifetime
k=s) OSC 133A, on whichever screen is active, drops working and blocked records. The same happens on PTY EOF once the final output is parsed (the session itself lingers in post-exit grace). Idle, done and error are kept.?is always answered, with the same posture as the static OSC 99p=?reply. OSC 9;4 stays consumed-without-effect and is not mapped to the root record.Public shape
Record (protobuf and JSON), every field optional:
{id, state, kind, progress, app, title, msg}.id""is the root record.appis the raw stored value; ancestor inheritance is left to the consumer.msg/titleare decoded UTF-8. Bidi disarming is the job of whoever displays them.stateandkindare open strings, not enums. Today's values are an additive vocabulary (theerror.kindrule): consumers treat an unknown value as unknown. The decoder still admits only spec values, so following a spec revision changes the decoder table only, with no IPC change.Snapshot:
sessions info/list --format jsongain an optionalprogram_status: [record…].Stream: a dedicated Stream-class verb (working name
felis sessions status-subscribe [--session <id>]) plus a bridge op. It is separate fromnotifications subscribeand has its own broadcast ring. Items carry full state per session, not deltas, the same shape cmux settled on:recordsis authoritative.changedis a hint for consumers that react to transitions.cause(report,clear,prompt,exit,evict,reset,resync) andeventare additive vocabularies, and a later event never changes whatrecordsmeans.Listing::Heldincluded). It then writes the ack: aSubscribed { filter: ResolvedId?, sessions }frame, sono_match/ambiguousare typed. Finally it asks each captured task for acause: resyncitem. A task that is already shutting down answers withgone.subscribedstarts a new state epoch and the consumer discards its previous map. The initial picture is complete once astatusorgonehas arrived for every id insubscribed.sessions.lagitem, then the clean terminal, and the consumer re-subscribes. The daemon does no resnapshot or syntheticgone.goneis the session's last item, published bysuperviseafter the session task returns, whether normally or by panic. Every roster exit ends that task.endon daemon stop, afterlag, or after the filtered session'sgone.countcounts thestatusandgoneitems, excluding the ack andlag. Exit0.no_match/ambiguous(1),daemon_unreachable/daemon_lost(2),unsupported.Wire: a new message family under a minor bump, send-gated by the effective minor. The same minor adds
StreamErrorReason::Unsupported, mapped tounsupported.Retirement path:
?and stop decoding.program_status.unsupported.Tests
k=sexcluded) and on PTY EOF.Docs
docs/*/protocols/.cli.mdandipc.md.skills/felis.Not in this issue
--until done|blocked|erroror similar) for agents waiting on TUI agents.Pstcapability, once the spec stabilizes. Ghostty also holds it back for now.