Repository navigation
Description
Blocked by: a Lakr233/libghostty-spm release built from Ghostty a4aacd918 or later, so both engine pins can move together. ghostty-org/ghostty#14560 merged 2026-10-06. Updated by Claude Code at the owner's request, 2026-10-09.
Problem
The pane busy indicator only knows "working" or "idle", from OSC 9;4 (GHOSTTY_ACTION_PROGRESS_REPORT, macapp/WorkroomApp/Core/GhosttyRuntimeAdapter.swift). Claude Code emits it once at start and end; Codex never emits it. Workroom can't tell a pane that is working from one that is waiting on the user (permission prompt, question, login), which matters most when several agents run side by side.
Upstream
OSC 7501, the program status protocol (spec) lets a program report state=idle|working|done|blocked|error|clear, plus kind=permission|question|auth for blocked, app=, a base64 msg=, a progress percentage and hierarchical ids.
ghostty#14560 adds it to libghostty-vt only, as a GHOSTTY_TERMINAL_OPT_PROGRAM_STATUS callback in include/ghostty/vt/terminal.h. It does not touch the apprt: there is no GHOSTTY_ACTION_* in include/ghostty.h, and src/termio/stream_handler.zig ignores .program_status. Bumping GhosttyKit in the macapp therefore gains nothing.
Approach
wr-agent already links libghostty-vt (feature terminal-state, always enabled for app builds by macapp/scripts/build-agent.sh), so the daemon is the natural place to consume it:
- Once #14560 merges, bump the Ghostty checkout used by
scripts/build-ghostty-vt.sh. - Register the program-status callback in wr-agent. The library keeps no records: wr-agent keeps one record per id per session and applies the spec's lifetime rules (a full reset reports a clear of every record).
- Carry the current state to the app over the existing session protocol.
- Show it in the sidebar/tab strip: at least a "needs you" badge for
blocked(withkindandmsgin the tooltip), and done/error.
Benefits of doing it daemon-side: works for local and remote/boxd hosts alike, and the state survives client reattach because wr-agent holds the record.
Notes
- The terminal only answers the support query (
OSC 7501 ; ?) while the callback is set. Programs that probe first stay silent until wr-agent registers it. app=could later replace some of the argv0 / foreground-pgid agent detection, for programs that send it.- Consider whether
blockedshould count as busy for the boxd keep-awake heartbeat (Far-side lifecycle shim: keep a busy box awake through the provider (boxd first) #257). - Emitter support (Claude Code, Codex, Terraform, Homebrew) is proof-of-concept per the announcement; check what has actually shipped before building UI.
- The C API may change while #14560 is a draft.
Estimate: about a day once upstream merges.
🤖 Left by Claude Code
Activity
joelmoss commentedon Oct 6, 2026
Emitter check: Claude Code does not emit OSC 7501 yet
Checked 2026-10-06 against Claude Code 2.1.292, the current npm
latest(stableis 2.1.285), by reading the shipped binary (~/.local/share/claude/versions/2.1.292). Builds 2.1.289–2.1.291 are the same.7501appears 3 times in the binary, all inside hashes or unrelated constants.kind=permissionandprogram_statusdon't appear at all.${bg.ITERM2};${aSt.PROGRESS};…, behind theterminalProgressBarEnabledsetting, default on). So a 7501 emitter would show up here, and a string search alone wouldn't have found 9;4 either.TAB_STATUS. Claude Code does emitOSC 21337 ; indicator=<#rgb>;status=<text>;status-color=<#rgb>(it can also parse it). There is also ashowStatusInTerminalTabsetting; I haven't checked whether it controls this sequence. This is a per-tab status line Claude Code ships today. It may be worth handling as an interim signal before 7501 lands, but libghostty 1.x doesn't surface it, so wr-agent would have to parse it.The proof-of-concept Claude Code integration mentioned in the OSC 7501 announcement hasn't shipped in a release. Before building UI for
blocked, recheck the latest Claude Code for7501in thatbg={…}table.🤖 Left by Claude Code
joelmoss commentedon Oct 6, 2026
Design notes from the Rex docs
Source: Rex: Program status. Rex is a terminal from Superlogical that already consumes OSC 7501. These rules apply directly to wr-agent's record store.
A), dropworkingandblockedrecords; droppingidleis optional.doneanderrorrecords survive both events, so the user can still find them. Programs are told to reportdone/errorjust before they exit.app/titleevery time. A report with nostateis ignored.build/testis a child ofbuild). A program can beidleat its prompt while one of its workers isblocked, so a "needs you" badge has to roll up the whole tree, not only the root record.GHOSTTY_ACTION_PROGRESS_REPORT, or via wr-agent). The indicator then upgrades by itself when 7501 arrives, and most of this issue no longer has to wait on ghostty#14560.OSC 7501 ; ? STfollowed byCSI c(DA1). If the DA1 reply arrives first, the program treats 7501 as unsupported.Open question: who answers the support query?
The macapp's GhosttyKit app layer ignores 7501, so it won't answer
?, but it will answer DA1. That means wr-agent's libghostty-vt has to answer?itself and write the reply to the PTY before the client's DA1 reply comes back over the session socket. Check two things:If either goes wrong, programs that check first will decide 7501 isn't supported and stay silent.
Correction to my earlier comment
Rex calls OSC 21337 iTerm2's earlier take on the same idea. (The form Claude Code emits is
key=value, not JSON.)🤖 Left by Claude Code
joelmoss commentedon Oct 6, 2026
Answer: wr-agent's shadow terminal writes nothing back to the PTY today
Checked on
feat/boxd-in-app@ 827db07 against the pinned libghostty-vt (~/.cache/workroom/ghostty-vt/src-c4e16970a).ShadowTerminal::new(vcs/crates/wr-agent/src/terminal.rs) sets exactly one option,GHOSTTY_TERMINAL_OPT_CONTINUATION_MAX_BYTES. It's the onlyghostty_terminal_setcall inwr-agent/src.GHOSTTY_TERMINAL_OPT_WRITE_PTY. Perterminal.h: "By default, sequences that require output (queries, device status reports) are silently ignored." So every query reply the shadow would produce (DA1, DSR, DECRQM, colours…) is dropped. Only the attached client answers queries.So today nothing answers
OSC 7501 ; ?. Even after ghostty#14560 lands, registering the program-status callback alone won't make programs that check first start reporting. wr-agent also needs aWRITE_PTYcallback.What the fix needs
WRITE_PTYto the 7501 reply only. Pass through bytes starting withESC ] 7501 ;and drop the rest. Forwarding every reply would double-answer DA1/DSR/colour queries alongside the client. The duplicate replies would land in the program's input as garbage, the same failure class as the replay-buffer query leak fixed earlier.shadow.writeruns in the PTY reader (session.rs~L1484) while holding both theattachedlock and the shadow lock. A PTY write that blocks there would stall the drain for every client. Queue the bytes in the callback and write them after the locks are released.shadow.write(screen)~L1063) and a paint at ~L2864. Replies triggered during those must not reach the PTY.Ordering is fine as built
The reader feeds the shadow before it captures the client list and forwards the bytes (
session.rs~L1484–1492). So wr-agent's reply to?is generated before the client has even received theCSI cthat follows it, and the client's DA1 answer can't overtake it. This holds for remote/boxd sessions too, since the shadow sits next to the PTY. Still worth one integration test once 7501 exists.Also: OSC 9;4 is available to wr-agent today
The pinned libghostty-vt already exposes
GHOSTTY_TERMINAL_OPT_PROGRESS_REPORT. wr-agent can own the record model and the 9;4-to-root-record mapping now, for local and remote sessions alike, without waiting on #14560.Separate gap
With no client attached, nobody answers DA1 or any other query. A program started in a detached session that checks for 7501 waits for its DA1 timeout. That's out of scope here.
🤖 Left by Claude Code
joelmoss commentedon Oct 6, 2026
Scope for
ready-for-agentThis label covers only the work that doesn't need ghostty#14560. Build it against the libghostty-vt already pinned (
src-c4e16970a).In scope now:
GHOSTTY_TERMINAL_OPT_PROGRESS_REPORT(OSC 9;4) on the shadow terminal, and keep a per-session program-status record model shaped for OSC 7501: statesidle|working|done|blocked|error, hierarchical ids, and the lifetime rules from the Rex notes above. Today OSC 9;4 maps onto the root record.A): dropworking/blocked, keepdone/error. 9;4's REMOVE state counts as idle.GHOSTTY_ACTION_PROGRESS_REPORTpath working for sessions that aren't daemon-backed.working.Out of scope, still blocked on ghostty#14560:
WRITE_PTYreply toOSC 7501 ; ?(see the comment above);blocked/kind"needs you" badge UI;app=-based agent identity.When #14560 merges, move the remaining work back to
needs-triageor split it into its own issue.🤖 Left by Claude Code
joelmoss commentedon Oct 6, 2026
Repro: the busy state is lost on reattach (confirmed at the wr-agent level)
Why not the fixture app: fixture launches never use persistent sessions.
TerminalPersistentSessionPolicy.swift:25is(isAvailable || hasExistingSession) && !isRunCommand && !isFixture, so a fixture pane can't reattach. A non-fixture launch would share the Dev bundle's real session store and agent, so I didn't use one.What I ran instead: the release
wr-agent(protocol 7,terminal-state yes) on a throwaway socket in/tmp, withSHELL=/bin/sh, a throwawayHOME, and a Python PTY as the client:printf '\033]9;4;3;\033\\'; echo WORKING; sleep 60. That's the same indeterminate "working" OSC 9;4 Claude Code sends at the start of a turn, followed by a long-running "turn".listshows itdetached, runningsleep).ESC ] 9;4bytes\x1b]9;4;3)The only OSC in B's replay is OSC 4 (palette). A first check falsely matched
]9;4against the echoed command text, so the result above is checked against the real ESC byte. Client A's row shows the check can fail.What the app does with that (code reading, not driven in the GUI): the busy indicator comes only from
GHOSTTY_ACTION_PROGRESS_REPORT(GhosttySurfaceView.onProgressReport, thenTerminalSessions.updateProgress), and that state lives in app memory per surface. After an app relaunch, a reattached pane starts with no busy state, and the replay can't restore it. So a Claude Code turn in progress at relaunch shows idle until Claude Code next sends 9;4, which is the end of the turn.Impact: this affects reattach after an app relaunch (local and remote/boxd). It doesn't affect a pane that re-attaches within the same app run, since that surface keeps its state.
Fix this supports: the ready-now scope above. wr-agent records the 9;4 state (
GHOSTTY_TERMINAL_OPT_PROGRESS_REPORT) and sends it to the client on attach. Either synthesise a 9;4 escape into the replay, or add a protocol field. Synthesising follows the existing pattern of re-synthesising state on attach. The protocol field fits better with the OSC 7501 record model later.🤖 Left by Claude Code
joelmoss commentedon Oct 6, 2026
Scope update
The reattach bug is split out into #359 (
ready-for-agent): wr-agent records the last OSC 9;4 report and replays it on attach. That was the only user-visible gain in this issue's "ready now" scope.The rest of the ready-now work was only a record model shaped for OSC 7501, which has no user-visible gain until OSC 7501 actually reaches us. So this issue goes back to
needs-triageand stays blocked on ghostty#14560 and on Claude Code (or another agent) shipping OSC 7501. #359's 9;4 state is what the record model will replace.🤖 Left by Claude Code
13 remaining items
docs: track upstream program status for non-daemon panes (#358 deferral)
feat(macapp): follow program status from the agent and roll it up per…
feat(wr-agent): keep OSC 7501 program status and publish it on its ow…
feat(macapp): show when a terminal is waiting on you, or finished its…
fix(wr-agent): never stall a pty on a subscribe reply, and keep the p…
feat(macapp): alert when a terminal needs you, count it, and go to it…
docs: program status, the marks, alerts and ⇧⌘M, and ADR 0050
fix(macapp): alert for a terminal swapped out for a diff tab, and sto…
test: replay Claude Code's recorded program status sequence without C…
test(macapp): drive program status end to end through a real agent in…
fix(macapp): name a "needs you" alert by the terminal and its workroo…
test(macapp): close program status test gaps from the plan
fix(macapp): name a "needs you" alert's place once, not in title and …