[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
- Start a claude-native session from the web UI; get a long turn running (sub-agents, heavy streaming).
- Switch effort in the web picker mid-turn.
- 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
[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_dialogpolls 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:(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_commandtypes/effort highand presses Enter blind — no commit-poll, no submit verification, unlike its siblinginject_user_messagewhich 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'sC-uclears the drafted command, destroying the evidence.Reproduction
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/compactforward 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=1in 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
_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 theeffort_change/model_changePOST immediately and confirms the dialog in a background task with a ~30s budget. Changes the handler's semantics, so it deserves its own change.inject_slash_commandhas 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).inject_approval_keystroketreats a torn (empty) capture as "prompt already answered" and silently drops a web permission verdict (kimi_native_bridge.py, marker check).switch_model_via_pickercan blind-Enter a literal/model <id>into the composer as a chat message when the picker never renders within its poll window.inject_user_messagehas commit-polling but no submit verification — a swallowed Enter silently loses the message.Environment