tui: don't let a dead host-key prompt hold the keyboard - #4
Merged
Conversation
Sync to a remote endpoint and the TUI could stop responding: arrows did
nothing, the panes never moved, and q did not quit. Ctrl-C was the only way
out.
The prompt owns the keyboard while it is up, which is right — a fingerprint
should not be dismissable by mashing keys. What made it dangerous is that it
could outlive the connection that asked. Point a pane at a host DiskPush has
not seen and take fifteen seconds to compare the fingerprint: ssh2's
readyTimeout fires, connect rejects with "Timed out while waiting for
handshake", the pane shows that error — and the question stays on screen with
nobody behind it. Every key then goes to a `decide` whose promise no one
awaits, and because that branch only ever returned true, q was swallowed with
the rest. The app looked frozen because it was.
Two changes, and neither loosens the prompt:
- session() clears the question in a finally. Whoever asked is gone, so the
question goes with them.
- q quits from inside the prompt. Everything else is still deliberately
swallowed; a dialog you cannot leave is worse than one you can.
Found by driving the real TUI over a pty against a real sshd, which is also
what confirms the fix: before, TAB/j/h changed nothing and q hung; after,
the panes move and it exits cleanly. The two tests that pin this fail without
the change; the one asserting y/n still answer and other keys are still
swallowed passes either way, which is the point of it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y5jnkZKX4AdPgBMzMosxE7
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.
Reported as: "once it syncs i can't get back to previous screen in tui" — after a sync to a remote endpoint (
dev:me).What was happening
The host-key prompt owns the keyboard while it is up, which is correct — a fingerprint should not be dismissable by mashing keys. What made that dangerous is that it could outlive the connection that asked.
Point a pane at a host DiskPush has not seen, then take fifteen seconds to actually compare the fingerprint. ssh2's
readyTimeout(connectTimeoutSeconds, 15s) fires,SshSession.connectrejects with "Timed out while waiting for handshake", the pane shows that error — andthis.hostKeyis never cleared. The question stays on screen with nobody behind it.From then on every key lands in this branch:
decideresolves a promise nobody awaits, so answering does nothing useful — and because the branch only ever returnstrue,qnever quits. Arrows do nothing, panes never move, and Ctrl-C is the only way out. The app looks frozen because it is.It reads as a sync bug because the post-sync reload is what re-enters
session()and raises the question again.The fix
Two changes, neither of which loosens the prompt:
session()clears the question in afinally. Whoever asked is gone, so the question goes with them. On the success path this is already null (decideclears it), so it is a no-op there.qquits from inside the prompt. Everything else is still deliberately swallowed — an arrow must not leak through to the panes behind it — but a dialog that can trap you in the app is worse than one you can leave, andqis the quit key everywhere else.How it was found and confirmed
By driving the real TUI over a pty against a real sshd, counting redraws (every frame starts with
ESC[2J) and reading the last frame back. That is what showed the tell: the pane said "Timed out while waiting for handshake" and the "Unknown host" box was still drawn underneath it.Same harness, before and after:
j/hafter the failed connectqapps/cli/src/tui/host-key-prompt.test.tsadds 3 tests. The two that pin the fix fail without it; the third — y/n still answer, other keys still swallowed — passes either way, which is what makes it a regression test rather than a restatement.Full suite: 238 tests, all passing.
🤖 Generated with Claude Code
https://claude.ai/code/session_01Y5jnkZKX4AdPgBMzMosxE7