Skip to content

Commit c888699

Browse files
teknium1nickdlkk
authored andcommitted
fix(acp): run /steer as a regular prompt on idle sessions (NousResearch#18258)
When a user types /steer <text> on an ACP session that isn't actively running a turn (and there's no interrupted-prompt salvage available), _cmd_steer silently appended to state.queued_prompts and replied "No active turn — queued for the next turn". That looks identical to /queue output even though the user never typed /queue — @EddyLeeKhane reported this as "/steer never works, gets queued instead". Rewrite the payload to a plain user prompt before the slash-intercept fires, matching the gateway's idle-/steer fallthrough in gateway/run.py ~L4898.
1 parent 48d78e6 commit c888699

2 files changed

Lines changed: 42 additions & 8 deletions

File tree

acp_adapter/server.py

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -669,24 +669,38 @@ async def prompt(
669669
if not has_content:
670670
return PromptResponse(stop_reason="end_turn")
671671

672-
# Zed currently interrupts an active ACP request before delivering a
673-
# follow-up slash command. If that follow-up is /steer, there may be no
674-
# live AIAgent left to steer by the time this method runs. Salvage that
675-
# UX by replaying the interrupted prompt with the steer text attached as
676-
# explicit correction/guidance.
672+
# /steer on an idle session has no in-flight tool call to inject into.
673+
# Rewrite it so the payload runs as a normal user prompt, matching the
674+
# gateway's behavior (gateway/run.py ~L4898). Two sub-cases:
675+
# 1. Zed-interrupt salvage — a prior prompt was cancelled by the
676+
# client right before /steer arrived; replay it with the steer
677+
# text attached as explicit correction/guidance so the user's
678+
# in-flight work isn't lost.
679+
# 2. Plain idle — no prior work to salvage; just run the steer
680+
# payload as a regular prompt. Without this, _cmd_steer would
681+
# silently append to state.queued_prompts and respond with
682+
# "No active turn — queued for the next turn", which looks like
683+
# /queue even though the user never typed /queue.
677684
if isinstance(user_content, str) and user_text.startswith("/steer"):
678685
steer_text = user_text.split(maxsplit=1)[1].strip() if len(user_text.split(maxsplit=1)) > 1 else ""
679686
interrupted_prompt = ""
687+
rewrite_idle = False
680688
with state.runtime_lock:
681-
if not state.is_running and steer_text and state.interrupted_prompt_text:
682-
interrupted_prompt = state.interrupted_prompt_text
683-
state.interrupted_prompt_text = ""
689+
if not state.is_running and steer_text:
690+
if state.interrupted_prompt_text:
691+
interrupted_prompt = state.interrupted_prompt_text
692+
state.interrupted_prompt_text = ""
693+
else:
694+
rewrite_idle = True
684695
if interrupted_prompt:
685696
user_text = (
686697
f"{interrupted_prompt}\n\n"
687698
f"User correction/guidance after interrupt: {steer_text}"
688699
)
689700
user_content = user_text
701+
elif rewrite_idle:
702+
user_text = steer_text
703+
user_content = steer_text
690704

691705
# Intercept slash commands — handle locally without calling the LLM.
692706
# Slash commands are text-only; if the client included images/resources,

tests/acp_adapter/test_acp_commands.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,26 @@ async def test_acp_steer_after_zed_interrupt_replays_interrupted_prompt_with_gui
9999
assert state.interrupted_prompt_text == ""
100100

101101

102+
@pytest.mark.asyncio
103+
async def test_acp_steer_on_idle_session_runs_as_regular_prompt():
104+
# /steer on an idle session (no running turn, nothing to salvage) should
105+
# run the steer payload as a normal user prompt — NOT silently append it
106+
# to state.queued_prompts. Without this, users on Zed / other ACP clients
107+
# see their /steer turn into "queued for the next turn" when they never
108+
# typed /queue. Matches gateway/run.py ~L4898 idle-/steer behavior.
109+
acp_agent, state, fake, _conn = make_agent_and_state()
110+
111+
response = await acp_agent.prompt(
112+
session_id=state.session_id,
113+
prompt=[TextContentBlock(type="text", text="/steer summarize the README")],
114+
)
115+
116+
assert response.stop_reason == "end_turn"
117+
assert fake.steers == []
118+
assert fake.runs == ["summarize the README"]
119+
assert state.queued_prompts == []
120+
121+
102122
@pytest.mark.asyncio
103123
async def test_acp_queue_slash_command_adds_next_turn_without_running_now():
104124
acp_agent, state, fake, _conn = make_agent_and_state()

0 commit comments

Comments
 (0)