Skip to content

[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

Description

@mattbkiv

Environment

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

const interactivePrompt = useAppStore(
  (s) => s.agentStatusByPaneKey[paneKey]?.interactivePrompt ?? null
)

NativeChatView.tsx imports NativeChatInteractiveCard and nothing else — there is no transcript-derived path anywhere in the desktop view.

Mobile resolves from two sources, in mobile/src/session/use-mobile-native-chat-prompts.ts:

const askFromStatus = useMemo(
  () => parseAskFromStatus(status?.interactivePrompt, status?.toolName), ...)
const askFromMessages = useMemo(
  () => (askFromStatus ? null : extractPendingAsk(messages)), ...)
return { ..., ask: enabled ? (askFromStatus ?? askFromMessages) : null }

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:

  1. 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.
  2. Carry the full hook payload over the headless relay (server-side). Ensure the fix(agent-status): sync HTTP-hook status to remote clients on headless servers #11212 projection propagates interactivePrompt and toolName, not just live working/idle state, so the live path works as designed.

Local modifications (disclosure)

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.

Related

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions