[Bug]: Claude AskUserQuestion renders as a plain tool call in the desktop Chat UI on a paired headless Remote Orca Server — no question card, answerable only in the terminal · Issue #11761 · stablyai/orca · GitHub
You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
[Bug]: Claude AskUserQuestion renders as a plain tool call in the desktop Chat UI on a paired headless Remote Orca Server — no question card, answerable only in the terminal #11761
Server: headless orca serve (orca-ide --serve --serve-port 6768 --pairing-address <tailnet-ip>) on an Ubuntu 26.04 LTS Hetzner VPS, v1.4.162, reachable tailnet-only
Client: Orca desktop on Windows, paired to that server
Agent: Claude Code, running in a workspace on the server
View: native Chat UI (not terminal view)
The desktop host's shell (PowerShell vs WSL) is not in this path — the agent runs on the remote Linux host and the desktop is only a viewer. This is a different topology from the WSL Chat UI reports (#10326, #10523, #11327).
Symptom
When Claude calls AskUserQuestion, the desktop Chat UI renders it as an ordinary collapsed tool call in the message stream. No question card, no clickable options. The session reads as still working, so you wait on an agent that is actually blocked on you. The question is only discoverable by expanding the tool-call details, and only answerable by switching to the terminal view, where the TUI prompt has been waiting the whole time.
Root cause — two independent layers
1. The desktop chat has no transcript fallback for the ask prompt; mobile has one
src/renderer/src/components/native-chat/NativeChatInteractiveCard.tsx derives the card from exactly one source:
extractPendingAsk lives in src/shared/native-chat-ask.ts — cross-platform code — but is imported only by mobile. Its own docstring describes the resilience the desktop therefore lacks:
Transcript replay parses each tool-call through the same registered-parser + canonical-shape fallback as live status, so a question tool that rendered live cannot vanish from pending state after a reconnect/replay.
Consequence: on desktop, if the live status never arrives, the card cannot render at all — even though the pending AskUserQuestion tool call is sitting right there in the transcript the view has already parsed and rendered.
2. HTTP-hook status does not reach paired desktop clients on a headless server (#11210)
Claude reports status over the HTTP hook Orca installs into ~/.claude/settings.json (src/main/claude/hook-settings.ts, PreToolUse among others), and src/shared/agent-hook-listener.ts is what stamps interactivePrompt when the tool is AskUserQuestion.
That is precisely the transport #11210 documents as broken under headless --serve:
applyNormalizedStatus only updates the in-process cache, writes last-status.json, and sends agentStatus:set over Electron IPC to the local renderer. On a headless server there is no renderer, so the IPC is a no-op. […] session.tabs's agent-status projection (getFreshRetainedAgentStatusForMobileTab) only reads the OSC 9999 retained store, not the agent-hook cache.
#11210 was filed for OpenCode's missing activity dots, but the broken path is shared by every HTTP-hook agent, and Claude is one of them.
Verified on the affected server:
~/.claude/settings.json contains the Orca hook entries invoking ~/.orca/agent-hooks/claude-hook.sh for PreToolUse / PostToolUse / PermissionRequest / Stop / SubagentStart etc.
~/.config/orca/agent-hooks/last-status.json is written live with agentType: "claude" rows carrying state and toolName — so server-side capture works.
PR #11212 fixes the projection for working/idle state. It is not clear from that diff whether interactivePrompt and toolName are carried through to paired clients, which is what the question card needs.
Now confirmed (see the follow-up comment below): polling that cache at 200 ms during a live AskUserQuestion captured 188 consecutive samples over 6m35s, every one carrying state: "waiting", toolName: "AskUserQuestion" and the full untruncated interactivePrompt JSON — while the paired desktop client showed only a collapsed tool call. Server-side capture is correct; the defect is in delivery to paired clients and in the absence of a desktop fallback.
Why this is worse than a missing activity dot
A missing dot is cosmetic. A missing question card means the agent is blocked on human input with no signal in the view the user is looking at — the session reads as working. On a remote server this is the ordinary case, not an edge case, and the workaround (switch to terminal view) defeats the purpose of the Chat UI.
Expected
The question card renders in the desktop Chat UI whenever Claude has an unanswered AskUserQuestion, whether the agent runs locally or on a paired headless remote server, and answering from the card works without switching to terminal view.
Proposed fix
Two parts, ideally both:
Desktop transcript fallback (client-side, robust). Give the desktop view the same askFromStatus ?? extractPendingAsk(messages) resolution mobile already has. The shared helper exists and is already covered by tests; this survives any relay gap, not only this one, and needs no server change.
The server has one local patch, applied because the AppImage AppRun unconditionally injects --no-sandbox, which crashes ELECTRON_RUN_AS_NODE mode on a userns-restricted host. The patch replaces the orca-ide CLI wrapper on $PATH so it execs the extracted app directly. It does not modify the application bundle, and the running serve process is the stock AppImage. Mentioning it for completeness only — it is not plausibly related to chat rendering.
Confirmed: the server captures everything the card needs — the gap is delivery, not capture
Closing the one open question from the report. I polled ~/.config/orca/agent-hooks/last-status.json at 200 ms while an AskUserQuestion was live, since that file retains only the newest status per pane and is therefore useless to read after the fact.
Result: 188 consecutive samples over 6m35s (12:05:03 → 12:11:38), every one of them carrying the complete prompt.
{
"state": "waiting",
"toolName": "AskUserQuestion",
"hasInteractivePrompt": true,
"interactivePrompt": "{\"questions\":[{\"question\":\"Given that AskUserQuestion is currently invisible in the desktop Chat UI on this VPS, how should I handle questions for the rest of our sessions here?\",\"header\":\"Workaround\",\"options\":[{\"label\":\"Ask in plain chat text, skip the tool (Recommended)\", ..."pane": "08607fd9-31c2-4898…"
}
So on the headless --serve host the hook pipeline is working exactly as designed: state: "waiting", the correct toolName, and the full untruncated interactivePrompt JSON — the three fields parseAskFromStatus(status.interactivePrompt, status.toolName) needs to build the card.
Meanwhile the paired Windows desktop client showed the call as an ordinary collapsed tool call in the Chat UI, with the question answerable only after switching to terminal view.
That isolates the defect precisely. Server-side capture: correct. Client-side parser: correct, and already shared. What is missing is (1) the transport that would carry interactivePrompt / toolName from the headless server's agent-hook cache to a paired client's agentStatusByPaneKey, and (2) any desktop fallback for when it doesn't arrive.
Point (2) is worth restating in light of this: the pending AskUserQuestion tool call was in the transcript the desktop Chat UI had already parsed and rendered — it drew it as a tool-call block. extractPendingAsk(messages) would have found it there with no server change whatsoever. Mobile does exactly that. The desktop simply never asks.
Correction to an earlier draft of this comment
An earlier version of this comment inferred something about the waiting state not clearing at answer time. That inference was unsound and I have removed it: the sampling window closed at 12:11:39, while the tool call did not return an answer until 12:33:44 — 22 minutes later. The window therefore says nothing about clearing behaviour, only that the prompt was continuously present and correct on the server for as long as it was observed.
What the window does establish stands unchanged: for its full duration the question was genuinely pending and the server held state: "waiting", the correct toolName, and the complete interactivePrompt.
This is worse than a hidden question: the invisible card silently converts the user's next message into an answer
Reporting a consequence we hit twice while investigating this issue. It escalates the severity well beyond "the card doesn't render".
What happened
Two AskUserQuestion calls in this session returned answers that the user never gave. Both selected option index 0 — the one labelled "(Recommended)". The agent then acted on both as if they were decisions.
Transcript evidence (Claude Code 2.1.220):
tool call
answer returned
user's next real message
Q1
11:04:16Z
11:07:22Z
11:09:58Z (2m36s later)
Q2
12:05:02Z
12:33:44Z
12:38:41Z (4m57s later)
Both answer records carry type: user, userType: "external", entrypoint: "cli" — so the answer arrived through the TUI, indistinguishable from a real keypress. Nothing synthesised it upstream. And no user turn exists in the transcript at either answer timestamp: the user's typed text was consumed, not delivered.
Mechanism
The user's own explanation, which the source supports:
The question card, when it renders, replaces the composer — that is the stated purpose of onShowingQuestionChange in NativeChatInteractiveCard.tsx: "Reports whether a question card is on screen so the view can replace the composer with it (the card's free-text row is the answer input)."
Because of this bug the card never renders, so the composer stays live over a pane whose TUI is sitting on a question selector.
A composer send writes into that PTY. Per this repo's own note on STA-1860 (native-chat-agent-support.ts), Claude's selector "ignores typed label text (pasting "Blue" + Enter commits the highlighted FIRST option)".
So typing any ordinary message and pressing Enter commits option 1 and discards the message.
#11511 is the same event observed from the Chat UI side: a composer send the agent never accepts leaves a permanent optimistic "sent" bubble, because the only thing that retires it is a matching transcript turn that will never exist.
Why this deserves more weight than a rendering defect
A hidden question wastes time. A hidden selector under a live composer fabricates consent: the user's next message becomes an affirmative answer to a question they never saw, and it lands on the highlighted default — which, by convention, is usually the option the agent recommended. It then reads downstream as a considered decision. In our case the agent reported one of these back to the user as "you chose X"; the user had chosen nothing.
For anyone using agents to gate real actions behind AskUserQuestion, that is a correctness problem, not a UX one.
Suggested handling
The fix already proposed in this issue resolves it — render the card and the composer is replaced, closing the hole. Two defensive additions worth considering independently:
Never write a composer send into a pane with a known-pending question tool. The pending state is derivable from the transcript via extractPendingAsk even when live status is unavailable, which is exactly the fallback this issue asks for.
Do not treat a bare Enter as an answer commit when the selector received text it discarded — an ignored-text commit is indistinguishable from a deliberate default selection, which is what makes the failure silent.
Two clarifications that sharpen the above, both from the user who ran the test.
1. During this particular run, nothing appeared in the chat pane at all. Not the question, and not even the tool-call log for it, nor the two Bash calls that preceded it. Everything materialised at once only after canary-7734 was typed. My step 2 above said the user was "seeing only a collapsed tool call" — that was true of the original incident but not of this test run, and I have left the original text above rather than silently rewriting it.
That behaviour matches #11265 (paired remote terminals stall with live TCP sockets while bytes sit unconsumed in the client's receive queue; any read-only RPC drains them). The environments line up closely: Ubuntu 26.04 LTS remote host, Tailscale WebSocket to orca serve --port 6768. Typing into the composer appears to act as the same kind of wake-up as orca status does there.
2. The 8h37m pending window was simply the user not replying — they were working in other sessions — not an artefact of any Orca behaviour. The conclusion it supports is unchanged and is worth restating precisely: nothing auto-committed the question during those eight hours; the commit coincided with the user's keystroke.
Why this makes the report stronger, not weaker
The two failures are separable, and both leave the composer live over a pending selector:
Stream state
What the user sees
Issue
Flowing
The tool call, rendered as an ordinary collapsed block — no question card
this issue
Stalled
Nothing whatsoever, until a local action drains the queue
In the first state the user has a weak signal they are unlikely to interpret correctly. In the second they have none at all. In both, the next thing they type is silently converted into an answer to a question they never saw. The stall does not cause the fabricated consent — the missing card does, by leaving the composer in place — but it removes the last cue that anything is pending.
Activity
mattbkiv commentedon Jul 31, 2026
Confirmed: the server captures everything the card needs — the gap is delivery, not capture
Closing the one open question from the report. I polled
~/.config/orca/agent-hooks/last-status.jsonat 200 ms while anAskUserQuestionwas live, since that file retains only the newest status per pane and is therefore useless to read after the fact.Result: 188 consecutive samples over 6m35s (12:05:03 → 12:11:38), every one of them carrying the complete prompt.
{ "state": "waiting", "toolName": "AskUserQuestion", "hasInteractivePrompt": true, "interactivePrompt": "{\"questions\":[{\"question\":\"Given that AskUserQuestion is currently invisible in the desktop Chat UI on this VPS, how should I handle questions for the rest of our sessions here?\",\"header\":\"Workaround\",\"options\":[{\"label\":\"Ask in plain chat text, skip the tool (Recommended)\", ... "pane": "08607fd9-31c2-4898…" }So on the headless
--servehost the hook pipeline is working exactly as designed:state: "waiting", the correcttoolName, and the full untruncatedinteractivePromptJSON — the three fieldsparseAskFromStatus(status.interactivePrompt, status.toolName)needs to build the card.Meanwhile the paired Windows desktop client showed the call as an ordinary collapsed tool call in the Chat UI, with the question answerable only after switching to terminal view.
That isolates the defect precisely. Server-side capture: correct. Client-side parser: correct, and already shared. What is missing is (1) the transport that would carry
interactivePrompt/toolNamefrom the headless server's agent-hook cache to a paired client'sagentStatusByPaneKey, and (2) any desktop fallback for when it doesn't arrive.Point (2) is worth restating in light of this: the pending
AskUserQuestiontool call was in the transcript the desktop Chat UI had already parsed and rendered — it drew it as a tool-call block.extractPendingAsk(messages)would have found it there with no server change whatsoever. Mobile does exactly that. The desktop simply never asks.Correction to an earlier draft of this comment
An earlier version of this comment inferred something about the
waitingstate not clearing at answer time. That inference was unsound and I have removed it: the sampling window closed at 12:11:39, while the tool call did not return an answer until 12:33:44 — 22 minutes later. The window therefore says nothing about clearing behaviour, only that the prompt was continuously present and correct on the server for as long as it was observed.What the window does establish stands unchanged: for its full duration the question was genuinely pending and the server held
state: "waiting", the correcttoolName, and the completeinteractivePrompt.mattbkiv commentedon Jul 31, 2026
This is worse than a hidden question: the invisible card silently converts the user's next message into an answer
Reporting a consequence we hit twice while investigating this issue. It escalates the severity well beyond "the card doesn't render".
What happened
Two
AskUserQuestioncalls in this session returned answers that the user never gave. Both selected option index 0 — the one labelled "(Recommended)". The agent then acted on both as if they were decisions.Transcript evidence (Claude Code
2.1.220):Both answer records carry
type: user,userType: "external",entrypoint: "cli"— so the answer arrived through the TUI, indistinguishable from a real keypress. Nothing synthesised it upstream. And no user turn exists in the transcript at either answer timestamp: the user's typed text was consumed, not delivered.Mechanism
The user's own explanation, which the source supports:
onShowingQuestionChangeinNativeChatInteractiveCard.tsx: "Reports whether a question card is on screen so the view can replace the composer with it (the card's free-text row is the answer input)."native-chat-agent-support.ts), Claude's selector "ignores typed label text (pasting "Blue" + Enter commits the highlighted FIRST option)".#11511 is the same event observed from the Chat UI side: a composer send the agent never accepts leaves a permanent optimistic "sent" bubble, because the only thing that retires it is a matching transcript turn that will never exist.
Why this deserves more weight than a rendering defect
A hidden question wastes time. A hidden selector under a live composer fabricates consent: the user's next message becomes an affirmative answer to a question they never saw, and it lands on the highlighted default — which, by convention, is usually the option the agent recommended. It then reads downstream as a considered decision. In our case the agent reported one of these back to the user as "you chose X"; the user had chosen nothing.
For anyone using agents to gate real actions behind
AskUserQuestion, that is a correctness problem, not a UX one.Suggested handling
The fix already proposed in this issue resolves it — render the card and the composer is replaced, closing the hole. Two defensive additions worth considering independently:
extractPendingAskeven when live status is unavailable, which is exactly the fallback this issue asks for.mattbkiv commentedon Jul 31, 2026
Correction and refinement (from the reporter)
Two clarifications that sharpen the above, both from the user who ran the test.
1. During this particular run, nothing appeared in the chat pane at all. Not the question, and not even the tool-call log for it, nor the two
Bashcalls that preceded it. Everything materialised at once only aftercanary-7734was typed. My step 2 above said the user was "seeing only a collapsed tool call" — that was true of the original incident but not of this test run, and I have left the original text above rather than silently rewriting it.That behaviour matches #11265 (paired remote terminals stall with live TCP sockets while bytes sit unconsumed in the client's receive queue; any read-only RPC drains them). The environments line up closely: Ubuntu 26.04 LTS remote host, Tailscale WebSocket to
orca serve --port 6768. Typing into the composer appears to act as the same kind of wake-up asorca statusdoes there.2. The 8h37m pending window was simply the user not replying — they were working in other sessions — not an artefact of any Orca behaviour. The conclusion it supports is unchanged and is worth restating precisely: nothing auto-committed the question during those eight hours; the commit coincided with the user's keystroke.
Why this makes the report stronger, not weaker
The two failures are separable, and both leave the composer live over a pending selector:
In the first state the user has a weak signal they are unlikely to interpret correctly. In the second they have none at all. In both, the next thing they type is silently converted into an answer to a question they never saw. The stall does not cause the fabricated consent — the missing card does, by leaving the composer in place — but it removes the last cue that anything is pending.
test(native-chat): pin the AskUserQuestion card render under real Ele…
Show Claude's AskUserQuestion card in desktop Chat when the agent run…
fix(agent-status): stop an idle title retiring a pane pending a human…
fix(agent-status): stop an idle title retiring a pane pending a human…
fix(agent-status): stop an idle title retiring a pane pending a human…
fix(native-chat): retire an ask the transcript shows already resolved
fix(native-chat): keep a resolved-ask verdict across turn boundaries
Show Claude's AskUserQuestion card in desktop Chat when the agent run…
fix(agent-status): stop an idle title retiring a pane pending a human…