Skip to content

fix: render local-command-stdout messages instead of dropping them#649

Open
Wandering-Consciousness wants to merge 1 commit into
agentclientprotocol:mainfrom
spinsphere:fix/render-local-command-stdout-in-live-handler
Open

fix: render local-command-stdout messages instead of dropping them#649
Wandering-Consciousness wants to merge 1 commit into
agentclientprotocol:mainfrom
spinsphere:fix/render-local-command-stdout-in-live-handler

Conversation

@Wandering-Consciousness
Copy link
Copy Markdown

@Wandering-Consciousness Wandering-Consciousness commented May 12, 2026

AI disclaimer: I identified this bug after noticing that custom skills produced no output within the Zed Thread/Claude Agent UI. I used Claude to investigate the issue and develop the fix. I have manually verified the equivalent fix against an installed @zed-industries/claude-code-acp@0.16.1 (the deprecated package Zed currently ships): custom slash commands that previously produced empty threads now render the model's response correctly after Zed restart.

Summary

The live case "user" / "assistant" handler in src/acp-agent.ts drops every SDK message whose string content contains <local-command-stdout>. The branch was added to tolerate /compact's malformed output, but its side-effect is silencing every successful slash command invocation — custom user-defined skills (whose expanded bodies arrive wrapped in <command-*> + <local-command-stdout> markers) and built-in commands that emit textual output through these tags.

Symptom in ACP clients

User types /some-command (custom skill, or one of the affected built-ins). Thread renders the user's /some-command bubble at the top, then nothing — no expanded skill body, no model response. Under the hood the model has actually received the expansion and produced a turn, but the user message carrying the expansion never reaches the UI, and the cascade of subsequent assistant content gets visually decoupled.

In CLI Claude Code the same expansion is printed to the terminal, so the asymmetry is purely client-side.

Fix

This repo already ships stripLocalCommandMetadata (and its underlying stripMarkerTags helper) — they return null for marker-only payloads and the stripped prose otherwise. They are already wired into session replay/load (acp-agent.ts:1385) but not into the live handler.

The change wires the same helper into the live handler: strip the markers and route what remains through the existing toAcpNotifications path, preserving the message's role. Pure-marker payloads (e.g. /compact's) still no-op via the null branch, so the existing /compact works integration test in src/tests/acp-agent.test.ts continues to pass.

Why the previous behaviour was conservative-but-wrong

The inline comment notes the filter exists for /compact's SDK-side malformed output. That tradeoff swallows a much larger class of legitimate output (anything that uses <local-command-stdout> to wrap real prose) to avoid one specific command's noise. With stripLocalCommandMetadata available, the conservative behaviour can target exactly the empty-after-strip case (still null → no-op) without affecting messages that have real prose alongside markers — which is exactly the regression case the helper's tests were written to cover (see src/tests/acp-agent.test.ts:1145-1163, "in the original bug report the entire /model preamble and the user's real 'hi' prompt were concatenated into a single message. We want to strip the marker tags and preserve the real prose, not drop the whole message.").

The fix applies that same principle to the live handler, where the helper is most needed.

Refs

  • zed-industries/claude-code-acp#624 — "'Skill' Session Updates missing when invoked via slash command". The reporter correctly diagnoses the mechanism: "Claude just attaches the skill directly to context when the /command is used, which means the skill loader isn't used." The skill body arrives wrapped in markers and gets dropped by this filter.
  • zed-industries/claude-code-acp#642 — built-in slash commands (/usage, /status, /model, /memory, /permissions, /agents, /mcp) emit no output when invoked. Same root cause: their output arrives in messages tagged with <local-command-stdout> and is filtered out.

Both upstream issues remain open against the old repo zed-industries/claude-code-acp and have no comments / linked PRs yet. Cross-linking here so this PR can be the closing reference for both.

Test plan

  • npm run check (lint + format) — passes
  • npm run build (tsc) — passes
  • npx vitest run src/tests/acp-agent.test.ts -t "stripLocalCommandMetadata" — 8 passed
  • Existing /compact works integration test — needs a CLI environment to run; relies on the null branch preserving the no-op behaviour for marker-only payloads, which the helper tests already cover

🤖 Generated with Claude Code

The live "user"/"assistant" handler in acp-agent.ts drops every SDK
message whose string content contains <local-command-stdout>. The
branch was added to tolerate /compact's malformed output, but its
side-effect is silencing every successful slash command invocation —
custom user-defined skills (whose expanded bodies arrive wrapped in
<command-*> + <local-command-stdout> markers) and built-in commands
that emit textual output through these tags. From an ACP client the
symptom is: type `/foo`, get an empty thread; the model receives the
expansion and responds, but the user message containing the
expansion never reaches the UI.

The repo already ships `stripLocalCommandMetadata`, which returns
null for marker-only payloads and the stripped prose otherwise; it
is already wired into session replay/load (around line 1385) but
not into the live handler. Wire it in: strip the markers and route
what remains through the existing `toAcpNotifications` path,
preserving the message's role. Pure-marker payloads (e.g. /compact)
still no-op via the null path, so the existing /compact integration
test in src/tests/acp-agent.test.ts continues to pass.

Refs agentclientprotocol#624, agentclientprotocol#642.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant