Repository navigation
Description
Bug
Relaunch Workroom while an agent (e.g. Claude Code) is mid-turn in a persistent pane. The pane reattaches with its screen intact, but the busy indicator shows idle. It stays idle until the agent next sends OSC 9;4, and Claude Code only sends that again at the end of the turn.
Cause
The busy indicator comes only from OSC 9;4 (GHOSTTY_ACTION_PROGRESS_REPORT → GhosttySurfaceView.handleProgressReport → TerminalSessions.updateProgress), and that state lives in app memory per surface. A reattaching client gets wr-agent's shadow replay, and the replay carries no OSC 9;4 state. libghostty-vt's formatter re-synthesises screen and modes, not progress. So after a relaunch the new surface starts idle.
Affects reattach after an app relaunch, for local and remote/boxd sessions. A pane re-attaching within the same app run keeps its surface and its state, so it isn't affected.
Repro (wr-agent level)
Fixture launches can't reproduce this: they never use persistent sessions (TerminalPersistentSessionPolicy.swift:25, && !isFixture). Use wr-agent on a throwaway socket instead (keep the path short, or serve fails with path must be shorter than SUN_LEN):
- Client A attaches a new session and runs
printf '\033]9;4;3;\033\\'; echo WORKING; sleep 60. - Kill client A. The session stays alive, detached.
- Client B attaches to the same session.
Result with the release wr-agent (protocol 7, terminal-state yes) on 2026-10-06: client A received a real ESC ] 9;4;3. Client B's replay (5693 bytes) had the screen text but no ESC ] 9;4 byte sequence; its only OSC was OSC 4. Match on the ESC byte, not on ]9;4: the echoed command line contains ]9;4 as text. Full write-up: #358 (comment "Repro: the busy state is lost on reattach").
Fix
- In wr-agent, register
GHOSTTY_TERMINAL_OPT_PROGRESS_REPORTon the shadow terminal (vcs/crates/wr-agent/src/terminal.rs; the pinned libghostty-vt already has it) and keep the last report per session. REMOVE clears it. - On attach, send that state to the client. Recommended: add the last OSC 9;4 escape to the end of the replay, after the screen. The client's existing 9;4 path then restores the spinner and the safety timer with no Swift change, locally and remotely. A 9;4 report isn't a query, so the replay's query-stripping shouldn't touch it; confirm that.
- Don't put it in the persisted screen record (
record(),--screens). After a host reboot the program is gone, so a stale "working" would only pin the spinner.
Tests
- In wr-agent (next to
tests/attach_survives_detach.rs): send 9;4 SET, detach, reattach, and check that the replay contains the realESC ] 9;4bytes. Send SET, then REMOVE, then reattach, and check it doesn't. Break the fix and confirm the first test fails. - Check that
record()output has no 9;4.
Related
#358 (OSC 7501 program status) will later replace the single 9;4 value with a per-id record model. Keep this fix small; #358 builds on it.
🤖 Left by Claude Code
Activity
fix(wr-agent): replay the busy state to a reattaching client (#359)
docs(remote-workrooms): record the busy state surviving a reattach (#359