fix(tui): single-owner keyboard input and crash-safe terminal lifecycle - #817
Draft
judyks wants to merge 1 commit into
Draft
fix(tui): single-owner keyboard input and crash-safe terminal lifecycle#817judyks wants to merge 1 commit into
judyks wants to merge 1 commit into
Conversation
judyks
marked this pull request as draft
August 7, 2026 15:17
… and diagnostics visibility
Fixes the three highest-impact defect clusters in the `adr-bot` TUI, plus
adversarial-review and production-review hardening on top.
1. Unified keyboard input
Two threads previously read the same crossterm event queue — a nav-only
poller thread (alive through the project-confirm prompt) and the prompts'
own blocking readers. Whichever reader won got the key, so at the confirm
prompt 'y' (accept) could be consumed as "copy output" and Enter could be
silently dropped. Replaced both with a single input-hub thread that owns
event::read() for the whole run and routes by a shared flag: nav/quit/resize
during execution, verbatim forwarding to the active prompt otherwise.
Adjacent fixes delivered by the same architecture:
- Cancellation outside tailoring: quit keys abort the API fetch and its 503
backoff immediately, and take effect at stage boundaries after the
synchronous analysis/signals phases and before file writes. Previously
nothing before tailoring could be cancelled.
- Resize handling: prompt loops and review mode redraw immediately on
resize (previously discarded); the 503 backoff sleeps in 1-second slices
with a live countdown.
2. Crash-safe terminal lifecycle
- Panic hook (installed once, when TUI mode starts): restores the terminal
before delegating to the previous hook, so panic messages reach the
normal screen instead of being erased by teardown.
- try_setup_tui unwind: partial-failure branches undo exactly what they set
up — no more raw mode silently stuck on after a fallback to plain mode.
- Non-ASCII ADR-id panic: format_adr_diffs now truncates by character
instead of byte-slicing &id[..8].
3. Adversarial-review fixes
Two independent review passes over the diff surfaced and confirmed:
- Esc/fullscreen collision: the hint line advertises "Esc exit fullscreen",
but the exec route treated Esc as a quit key — losing a whole run to a
keypress the UI suggested. The renderer now mirrors fullscreen state to
the hub; Esc exits fullscreen when active, cancels otherwise.
- Sticky cancel latch: a quit key racing prompt entry latched cancellation
forever, killing the run *after* the user explicitly accepted the prompt
(reported as "API request failed: User cancelled"). The hub clears the
latch on the exec-to-prompt transition, and fetch_error_message now
reports a cancel as "Cancelled".
- Stale nav commands leaking into prompts/review (could overflow review
scroll arithmetic — debug-build panic): drained at prompt entry, plus
saturating_add.
- Drain-before-flip ordering in take_prompt_source (a fresh type-ahead key
could be discarded as stale).
- KeyEventKind::Release filtering (kitty protocol/Windows double-fire;
Repeat still scrolls).
- Plain-fallback hardening: a mid-run draw failure now stops the hub
thread and clears the TUI-active flag so dialoguer prompts never compete
with the hub.
- Test-flake fix: the tests mutating the global TUI_ACTIVE are serialized.
4. Diagnostics visibility
- Error paths no longer erase the transcript: the run wrapper holds the
TUI open ("press any key to close") on failure as well as success, so
subprocess stderr and API errors written to the log pane can actually be
read. UserCancelled skips the pause; Plain/Quiet never block.
- --verbose is visible in TUI mode: the four diagnostic sites previously
wrote through suspend(|| eprintln!(...)) — a screen flash with no
readable output. They now write to the log pane (TuiRenderer::eprintln,
which also routes correctly to stderr in plain mode, preserving the
documented stdout/stderr contract for scripted consumption).
5. ETXTBSY flake fix
Both claude-binary spawn sites in auth.rs now retry on ETXTBSY ("Text file
busy") — a transient fork/exec race that reddened CI intermittently and is
unrelated to the TUI changes above. Also covers the production case of the
claude binary being upgraded concurrently with an auth probe.
6. Production-hardening pass
- Hub-thread fail-safe: if the input thread exits for any reason other
than a requested stop (panic, or a future bug breaking the loop), it
sets the cancel signal and logs an error before dying. Raw mode eats
Ctrl+C's SIGINT while the TUI is live, so a silently dead input thread
previously meant no cancel path at all.
- Cancellation observability: tracing on the three decision points the hub
introduced (quit-key latch set, latch cleared by a prompt opening, and a
checkpoint consuming the cancel), so unexpected cancellations are
debuggable from the log file.
- Renderer-hub contract consolidated: the four separately-plumbed shared
handles (route, fullscreen, stop, channels) become one HubShared /
HubConnection handed over via a single connect_input_hub call, so the
wiring cannot be half-done.
Behavior changes to note:
- q/Q/Esc/Ctrl+C cancel the run during Environment/Analysis/Fetch and
write-prep (previously ignored there); Esc exits fullscreen first when
fullscreen is active. Cancellation during synchronous stages lands at
the next stage boundary; writes are never aborted mid-file.
- On failure in TUI mode, the run pauses for a keypress before closing so
the diagnostics stay readable.
Testing: ~50 new unit tests across the hub (routing matrix incl.
fullscreen/Release/latch semantics, wait_cancelled), renderer (resize in
all prompt loops, prompt-channel take/restore/stale-drain, panic-hook
restore, fullscreen mirror sync), pipeline (cancel_checkpoint, countdown
backoff, cancelled-fetch message), auth (ETXTBSY retry), and a first
co-located test module for adr_utils.rs. Full suite passes except
pre-existing run-as-root artifacts (verified identical on the base
commit). cargo fmt --check and cargo clippy -- -D warnings clean.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V9Rz9BcK1xb1jiGNER1FeU
judyks
force-pushed
the
tui-unified-input
branch
from
August 11, 2026 17:38
2ff6a2a to
515eb8f
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Fixes the three highest-impact defect clusters in the
adr-botTUI: keyboard input races, crash paths that corrupt the terminal, and failure paths that destroy the diagnostics the user needs — plus two rounds of hardening from adversarial and production review.1. Unified keyboard input
Two threads previously read the same crossterm event queue — a nav-only poller thread (alive through the project-confirm prompt) and the prompts' own blocking readers. Whichever reader won got the key, so at the confirm prompt
y(accept) could be consumed as "copy output" andEntersilently dropped. Now a single input-hub thread ownsevent::read()for the whole run and routes by a shared flag: nav/quit/resize during execution, verbatim forwarding to the active prompt otherwise.Adjacent fixes delivered by the same architecture:
2. Crash-safe terminal lifecycle
try_setup_tuiunwind: partial-failure branches undo exactly what they set up — no more raw mode silently stuck on after a fallback to plain mode.format_adr_diffsnow truncates by character instead of byte-slicing&id[..8].3. Adversarial-review fixes
Two independent review passes over the diff surfaced and confirmed:
fetch_error_messagenow reports a cancel as "Cancelled".saturating_add.take_prompt_source(a fresh type-ahead key could be discarded as stale).KeyEventKind::Releasefiltering (kitty protocol/Windows double-fire; Repeat still scrolls).TUI_ACTIVEare serialized.4. Diagnostics visibility
UserCancelledskips the pause; Plain/Quiet never block.--verboseis visible in TUI mode: the four diagnostic sites previously wrote throughsuspend(|| eprintln!(...))— a screen flash with no readable output. They now write to the log pane, and a dedicatedeprintlnkeeps the plain-mode stdout/stderr contract intact.5. ETXTBSY flake fix
Both
claude-binary spawn sites inauth.rsnow retry on ETXTBSY ("Text file busy") — a transient fork/exec race that reddened CI intermittently, unrelated to the TUI work. Also covers the production case of the binary being upgraded concurrently with an auth probe.6. Production-hardening pass
HubShared/HubConnectionhanded over via a singleconnect_input_hubcall, so the wiring cannot be half-done.Behavior changes to note
q/Q/Esc/Ctrl+Ccancel the run during Environment/Analysis/Fetch and write-prep (previously ignored there); Esc exits fullscreen first when fullscreen is active. Cancellation during synchronous stages lands at the next stage boundary; writes are never aborted mid-file.Testing
wait_cancelled), renderer (resize in all prompt loops, prompt-channel take/restore/stale-drain, panic-hook restore, fullscreen mirror sync), pipeline (cancel_checkpoint, countdown backoff, cancelled-fetch message), auth (ETXTBSY retry), and a first co-located test module foradr_utils.rs.cargo test --lib→ 3,139 passed, 13 failures — verified as a strict subset of the base commit's pre-existing run-as-root artifacts (CI runs unprivileged).cargo fmt --checkandcargo clippy -- -D warningsclean. CI was fully green on the pre-squash history, including Coverage Enforcement, across every push.Not in this PR (later items of the TUI plan)
A non-
--forcePTY e2e test exercising the interactive prompts end-to-end; exec-phase resize redraw during long synchronous stages (resize is handled in prompts/review/backoff; a stale frame can persist during CPU-bound analysis until the next draw); SIGINT dead zones between awaited spans (pre-existing).🤖 Generated with Claude Code
https://claude.ai/code/session_01V9Rz9BcK1xb1jiGNER1FeU