[BUG] claude-agent-acp: streaming loop only exits on session_state_changed: idle, leaving Zed stop button stuck after every turn

Resolved 💬 2 comments Opened May 10, 2026 by puffthemagicgreendragon Closed May 10, 2026

Affected package: @agentclientprotocol/claude-agent-acp (verified on v0.31.4 and v0.33.1)
Affected client: Zed (any recent version using the ACP integration)

Summary

The streaming loop in acp-agent.js exits the user-turn only on session_state_changed: { state: "idle" }. The result message — which is the actual semantic end of a Claude Code turn — falls through to a bare break; and the loop keeps waiting. When idle lags or never arrives, session.promptRunning stays true indefinitely.

Symptom (in Zed)

  • Stop button stays active after Claude finishes responding.
  • User has to click Stop manually before sending the next message (clicking Stop forces the stream closed, which finally lets the loop exit).
  • Sound alert (Zed's AcpThreadEvent::Stopped) is delayed by the same mechanism — it fires on stream close, not on result.

Repro

  1. Use Zed with the Claude Code ACP agent.
  2. Send any prompt — especially one that launches background tasks.
  3. After Claude's response finishes streaming, observe that the stop button stays active in the chat input.

Root cause

In node_modules/@agentclientprotocol/claude-agent-acp/dist/acp-agent.js, case "result": ends with break; instead of returning. The only path that returns and closes the prompt is case "session_state_changed": when message.state === "idle". If idle never arrives, the loop hangs forever.

Proposed fix

At the end of case "result": in acp-agent.js, replace the trailing break; with:

For v0.33+ (which has the isTaskNotification flag for autonomous task-notification followups):

if (!isTaskNotification) {
    return { stopReason, usage: sessionUsage(session) };
}
break;

The !isTaskNotification guard prevents an autonomous followup result from terminating the user-turn early.

For v0.31 and earlier (no isTaskNotification):

return { stopReason, usage: sessionUsage(session) };

result is the semantic end of the turn — the dependency on session_state_changed: idle is sloppy. Same return shape as the idle handler already uses.

Why this matters

Every Zed + Claude Code user hits this. It reads like a Zed bug to the user ("Zed's stop button is broken") but the fix lives in the Anthropic-published bridge.

View original on GitHub ↗

This issue has 2 comments on GitHub. Read the full discussion on GitHub ↗