Skip to content

Busy indicator shows idle after reattaching to a session mid-turn #359

Closed

Description

@joelmoss

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):

  1. Client A attaches a new session and runs printf '\033]9;4;3;\033\\'; echo WORKING; sleep 60.
  2. Kill client A. The session stays alive, detached.
  3. 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

  1. In wr-agent, register GHOSTTY_TERMINAL_OPT_PROGRESS_REPORT on 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.
  2. 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.
  3. 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 real ESC ] 9;4 bytes. 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

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions