fix(tui): make provider onboarding navigable and escapable (#4763) - #4765
Conversation
Provider onboarding could trap a returning user whose persisted route was xAI OAuth: the Step 3/4 backdrop rendered no list, the picker opened straight into the OAuth modal for the persisted route, Escape popped the whole modal back to that empty backdrop, and Ctrl+C was swallowed. - Open the onboarding picker on the navigable catalog. `new_for_onboarding` focuses the persisted route without jumping to key/OAuth entry, so the provider list is always seen before a secret is asked for. `/setup` and `/provider` keep the existing `new_for_setup` behavior. - Stop intercepting Escape on the picker's behalf. The picker's own per-stage Escape walks key/OAuth entry back to the list, and dismissal from the list still runs the non-mutating `back_from_provider_onboarding` transition via `ProviderPickerDismissed`, preserving missing-key-recovery semantics. - Route Ctrl+C to shutdown from any onboarding state, modal or not. - Complete provider onboarding after an external CLI grant is reused, the same as a submitted key or an applied route. Previously the picker closed on success and returned the user to the provider step they had just satisfied. - Retell the backdrop footer as what actually happens (Enter opens the provider list, Esc goes back, Ctrl+C quits) across all locales that carry the string. Closes #4763
|
Bugbot is not enabled for your account, so this pull request was not reviewed. Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs. |
|
Claude finished @Hmbown's task in 3m 15s —— View job Review: PR #4765 — provider onboarding trap fixTodo list
I read the diff against Correctness
Provider/model/route safety (EPIC #2608)No provider/model inference from prefix strings or freeform text anywhere in this diff. Routing is entirely driven by explicit Reuse & simplification
TestsSix new tests, all pinning real regressions rather than incidental behavior:
SecurityNo secret handling, shell/exec, or input-validation changes in this diff. The external-grant test writes synthetic credentials to a Nit (non-blocking)None worth flagging — this is a tightly scoped fix with the diff matching its description at every claim I checked. Note: I was unable to run |
Keep the returning-user provider recovery test aligned with the current truthful missing-key diagnostic while preserving the picker and config immutability assertions.
|
Claude finished @Hmbown's task in 2m 29s —— View job Review: PR #4765 — provider onboarding trap fix (update on synchronize)Todo list
What's new since the last reviewOnly one commit landed after my previous pass: No other files changed; Verifying the new assertion isn't just a rubber-stamp string matchI traced the actual message-construction path for this scenario rather than trusting the commit message:
So the new assertion matches the actual code path the picker now renders through for this scenario, not just a plausible-looking string. The old string ( No correctness, safety, or test-coverage issues found in this update. Note: I could not run |
Closes #4763.
The trap
A returning user on the xAI OAuth route hit a closed loop at startup: an
empty Step 3/4 backdrop → the OAuth modal → back to the empty backdrop.
Ctrl+C was swallowed, so the only exit was Esc-then-Ctrl+C.
What changed
1. The provider list is visible and navigable.
open_onboarding_provider_pickernow uses a new
ProviderPickerView::new_for_onboarding, which focuses thepersisted route but stays on
Stage::List. Key/OAuth entry is reached bypicking a row, never by opening straight into it.
/setupand/providerkeep the existing
new_for_setupjump-to-key-entry behavior.2. Esc backs out one stage.
ui.rsno longer intercepts Esc on thepicker's behalf. The picker already had correct per-stage Esc handling
(KeyEntry → List, ModelPick → KeyEntry, Confirm → ModelPick); it just never
saw the key. Dismissal from
Stage::ListemitsProviderPickerDismissed,whose handler already calls
back_from_provider_onboarding— so themissing-key-recovery semantics at
ui.rs:807are preserved on the finalpop, and the non-mutating "Esc during onboarding never touches config"
invariant still holds (no picker sub-stage Esc mutates config or emits).
3. Ctrl+C quits from anywhere. Key routing during onboarding now goes
through one pure function,
onboarding_key_route, which returnsQuitforCtrl+C in any onboarding state before any modal gets a chance to swallow it.
4. The backdrop tells the truth. The footer said "Press 0-9 to choose,
↑/↓ to move, Enter to continue" next to no list. It now reads "Enter opens
the provider list · Esc goes back · Ctrl+C quits", updated in all 7 locale
files that carry
OnboardProviderFooter(zh-Hant.jsondoes not define it).5. External CLI grant reuse completes onboarding. Found while verifying
the issue's open question about the
Epath.ProviderPickerExternalConsentConfirmedpersisted consent and switched the provider but never called
complete_provider_picker_onboarding, unlike its siblingsProviderPickerAppliedandProviderPickerSetupConfirmed. Since the pickercloses on
EmitAndClose, a successful grant leftonboarding == Providerwith an empty view stack — i.e. it dumped the user back onto the empty
backdrop. This is the second half of the reported loop.
Investigation: why
needs_api_keywas trueThis is working as designed — no fix needed, and I did not change it.
The reporter's
~/.grok/auth.jsonwas fresh (<24h) but that is not therelevant clock. The entry carries
expires_atroughly 6 hours after issue;the file I inspected showed a 6-hour access-token lifetime with a
refresh_tokensitting right beside it.credentials_valid(xai_oauth.rs:225) accepts a refresh token forCodewhale-owned storage, but for an external grant it requires
entry_access_token_is_fresh— an access token that has not expired. Thatasymmetry is deliberate and load-bearing: read-only external consent never
refreshes, rewrites, or makes network calls against another CLI's file. It
is pinned by the existing tests
expired_read_only_external_credentials_never_refresh_rewrite_or_networkand
disabled_external_grok_credentials_cause_zero_external_io.So: Grok CLI token expires after ~6h → external grant stops counting →
has_api_key_forfalse →needs_api_keytrue → onboarding opens. Correctbehavior; the defect was that onboarding was a trap, not that it opened.
I added
expired_external_grok_grant_reads_as_missing_key_despite_refresh_tokento pin this so the entry condition stays explainable, asserting both
directions (expired external grant → missing; fresh → ready).
Follow-up worth filing separately: this state deserves an explanation in
the UI ("your reused Grok CLI token expired — run
grok login, or useCodewhale-owned device login"). Right now the user gets a bare provider step
with no hint that a 6-hour timer is what moved. I left it out to keep this
PR scoped.
On the
Epath specifically (the issue's second open question): itcompletes onboarding only when the grant actually yields a working route.
With an already-expired token — the reporter's exact case — pressing
Ere-affirms consent that is already recorded,
switch_providerfails, andthe user lands back on the backdrop. So the workaround in the issue
("press E to reuse existing Grok CLI credentials") does not work for an
expired token; device login or
grok loginis required. The backdrop is atleast now honest and escapable in that case.
Tests
Six new tests, all in existing patterns:
onboarding_catalog_focuses_missing_provider_without_leaving_the_listStage::Listduring onboardingonboarding_escape_walks_key_entry_back_to_the_list_then_dismissesonboarding_ctrl_c_quits_even_with_the_provider_picker_on_the_view_stackonboarding_escape_is_routed_to_the_provider_picker_not_interceptedexternal_grant_reuse_completes_provider_onboardingexpired_external_grok_grant_reads_as_missing_key_despite_refresh_tokenVerification
PTY reproduction (
pty+pyte, 120×40) against the built binary with asynthetic HOME reproducing the reported config —
provider = "xai",auth_mode = "oauth", read-onlygrok_cliconsent, expired~/.grok/auth.json:popped the whole modal).
exact reported trap. PTY closes, process exits.
E):before this change
E→ empty Step 3/6 backdrop; after, onboardingadvances to Step 5/6.
Gates, all run on this branch:
cargo fmt --check— cleancargo clippy --workspace --all-features --locked -- -D warnings -A clippy::uninlined_format_args -A clippy::too_many_arguments -A clippy::unnecessary_map_or -A clippy::collapsible_if -A clippy::assertions_on_constants— cleancargo test -p codewhale-tui --bin codewhale-tui— 8146 passed, 0 failed, 4 ignored (8140 on main, +6 new)./scripts/release/check-versions.sh—workspace=0.9.1, npm=0.9.1, lockfile in syncNo version bumps, tag moves, or publishing.
🤖 Generated with Claude Code
https://claude.ai/code/session_01HniWoNaNt9K9pkxTrGx1zd