Skip to content

[Bug] claude-native: switch-dialog confirm and slash-command delivery are unverified — swallowed Enters wedge the pane or silently diverge effort/model state #4246

Description

@joncarter1

[Bug] claude-native: switch-dialog confirm and slash-command delivery are unverified — swallowed Enters wedge the pane or silently diverge effort/model state

Summary

Two delivery gaps remain in the claude-native tmux bridge after #4074's dialog-confirm watch, both with the same root cause: a keystroke is sent into the Claude Code pane and assumed to have landed, with no verification. Under a busy repaint (the same load #4074 measured delaying the dialog render ~1.9s), the TUI drops keystrokes.

Gap 1 — the confirm Enter itself. _confirm_tui_dialog polls for the dialog by title, then presses Enter once. If that Enter is dropped, the dialog stays parked, the composer never returns, and every later delivery fails the readiness gate:

Error · execution · RuntimeError
inner executor error: Claude Code terminal did not become ready within 30.0s
(input prompt never rendered in 193 polls, 0 empty captures). The message was not delivered.
...
   Change effort level?
   Your next response will be slower and use more tokens
   ❯ 1. Yes, switch to high
     2. No, go back

(observed live: a web-UI effort switch mid-conversation, pane wedged on the dialog, every subsequent message failing after 30s)

Gap 2 — the command's own submit. inject_slash_command types /effort high and presses Enter blind — no commit-poll, no submit verification, unlike its sibling inject_user_message which gained both from prior production bugs (#2373 family). A swallowed submit Enter leaves the command drafted in the composer: the session row persists the new effort (the PATCH succeeded) while the pane keeps running the old one — silent state divergence — and the next injection's C-u clears the drafted command, destroying the evidence.

Reproduction

  1. Start a claude-native session from the web UI; get a long turn running (sub-agents, heavy streaming).
  2. Switch effort in the web picker mid-turn.
  3. With unlucky repaint timing, either the confirm Enter or the submit Enter is dropped → wedge (gap 1, loud) or divergence (gap 2, silent).

Fix (PR attached)

Verify every keystroke stage, mirroring inject_user_message's existing contract: wait for the typed command to render, verify the submit took (retry while the command verifiably remains), and after a matched-hint dialog Enter, verify the dialog closed (retry while it verifiably remains; torn captures never end a retry). Fail-soft to today's behavior when the draft can't be identified; fail loud (503) when the command verifiably never submitted. The slower (verified) injector also requires giving the /compact forward the TUI budget effort/model already use — with the default 5s budget, a timeout falls through to AP-side compaction on top of the terminal's still-running /compact.

Also rides along: CLAUDE_CODE_DISABLE_FEEDBACK_SURVEY=1 in the wrapped-pane env — Claude Code's in-TUI surveys ("How is Claude doing this session?") render only in the pane, where web-driven sessions can't see or answer them. Same decision as agy's survey disable (#1494 / #1501); unlike agy's, Claude's survey breaks nothing (it doesn't collide with any pane detector) — this is noise removal.

Deliberately deferred follow-ups

  • Bounded dialog-appearance window: the confirm watch gives the dialog _CONFIRM_DIALOG_TIMEOUT_S (4s) to render, bounded by the server's 5s best-effort runner forward. A dialog rendering later than that still wedges. Structural fix: the runner answers the effort_change/model_change POST immediately and confirms the dialog in a background task with a ~30s budget. Changes the handler's semantics, so it deserves its own change.
  • Pre-parked dialog: inject_slash_command has no readiness gate; a dialog already on the pane when a command arrives eats the keystrokes (the command's submit Enter accepts the stale dialog — recovery-ish, but the new command is lost).
  • Sibling harnesses (found scanning for this bug class, not addressed here):
    • kimi: inject_approval_keystroke treats a torn (empty) capture as "prompt already answered" and silently drops a web permission verdict (kimi_native_bridge.py, marker check).
    • cursor: switch_model_via_picker can blind-Enter a literal /model <id> into the composer as a chat message when the picker never renders within its poll window.
    • goose/kimi/hermes: inject_user_message has commit-polling but no submit verification — a swallowed Enter silently loses the message.
    • antigravity: interaction verdict digits are typed without checking the TUI prompt is still on screen (cursor's yolo path guards exactly this).

Environment

  • omnigent main, Claude Code 2.1.220, macOS (Darwin 24.6.0), claude-native web-driven sessions

Metadata

Metadata

Assignees

Labels

BugSomething isn't workingP2-mediumPriority: bug with workaround, important feature requestcomp:harness-t1Highest-usage harnessestriagedIssue has been triaged by the botvalidated:reproducedRepro agent validated the issue can be reproduced

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions