Skip to content

Give the pending read up when a run ends, so the next key is not swallowed [patch] - #206

Merged
matt-edmondson merged 2 commits into
mainfrom
claude/tui-149-cancel-pending-read
Oct 9, 2026
Merged

matt-edmondson merged 2 commits into
mainfrom
claude/tui-149-cancel-pending-read

Conversation

@matt-edmondson

Copy link
Copy Markdown
Contributor

Fixes #149

What changed

  • IConsoleProvider.ReadInputAsync(CancellationToken): a new overload with a default implementation that calls the parameterless read. Existing providers compile and behave as before.
  • SpectreConsoleProvider implements the overload by polling Console.KeyAvailable every 15 ms and calling ReadKey only once a key is waiting. Cancelling the read therefore leaves no thread blocked in ReadKey. With redirected stdin, KeyAvailable throws the same InvalidOperationException as before, so the bounded-failure handling from End the run after repeated failed reads instead of spinning forever #177 still applies.
  • UIApplication.ProcessInputAsync reads with a token that is cancelled however the loop ends (Shutdown(), Ctrl+C, the caller's token, or an exception). It then waits up to 250 ms for the cancelled read to finish, so a cancellable provider has stopped reading before RunAsync returns. If a provider ignores the token, its unfinished read is carried into the next run rather than raced by a second read. A carried read that was cancelled is discarded and replaced.
  • InteractiveDemo: a cref to the method is now ReadInputAsync(), because it became ambiguous (CS0419).

The loop body is re-indented to sit inside the new try/finally. Apart from the read call and the cancelled-carry-over check, its logic is unchanged. git diff -w shows the real change.

Tests

  • UIApplicationPendingReadTests uses a fake provider whose keys go to the earliest waiting read, as with Console.ReadKey:
    • AShutdownLeavesNoReadOutstanding: no read is still waiting after a Shutdown()-ended run.
    • AKeyPressedInASecondRunReachesTheRoot: runs once with a provider that honours cancellation and once with one that ignores it. This is the issue's reproduction.
  • SpectreConsoleProviderTests: a cancellable read waits for an available key and then reads it, and cancelling a waiting read ends it without calling ReadKey.
  • With the loop restored to its old behaviour (reading with CancellationToken.None), all three UIApplicationPendingReadTests cases fail, and they pass with the fix. The full solution test run passes (261/261).

🤖 Generated with Claude Code

https://claude.ai/code/session_016rdMXULeUT13t6FCwNfocx


Generated by Claude Code

…lowed [patch]

RunAsync abandoned its in-flight read when Shutdown(), Ctrl+C or the
caller's token ended the loop, and SpectreConsoleProvider's read was an
uncancellable Console.ReadKey on a pool thread. That orphaned read took
the next key meant for the host, or raced a second RunAsync for every key.

- IConsoleProvider gains ReadInputAsync(CancellationToken), defaulting to
  the parameterless read so existing providers keep working
- SpectreConsoleProvider implements it by polling Console.KeyAvailable
  and reading only once a key is waiting, so cancelling it leaves no
  thread blocked in ReadKey
- The input loop cancels its read however it ends and gives it a moment
  to finish. A read its provider would not give up is carried into the
  next run instead of being raced by a second one

Fixes #149

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016rdMXULeUT13t6FCwNfocx
Comment thread TUI/Services/UIApplication.cs Fixed
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016rdMXULeUT13t6FCwNfocx
@sonarqubecloud

sonarqubecloud Bot commented Oct 8, 2026

Copy link
Copy Markdown

@matt-edmondson
matt-edmondson merged commit f889586 into main Oct 9, 2026
14 checks passed
@matt-edmondson
matt-edmondson deleted the claude/tui-149-cancel-pending-read branch October 9, 2026 08:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

RunAsync leaves a Console.ReadKey pending after shutdown, so the next keypress (in the host or in a second RunAsync) is swallowed

2 participants