fix(tui): route each OAuth picker row to its own provider, not kimi.com

Root cause: PendingMenuItem::Login carried only a label, so every provider
row collapsed to the id-less Action::Login, and dispatch_login resolved
the FIRST advertised interactive method — the Kimi device flow. Selecting
Grok/Claude/Copilot/Codex all opened kimi.com. (The shell side was already
correct: authenticate() dispatches each method id to its own OAuth flow.)

- PendingMenuItem::Login now carries the advertised method id (None only
  on the no-interactive-method fallback row).
- New Action::LoginWith(AuthMethodId); the picker dispatches it with the
  row's own id. Action::Login keeps its meaning (resolved/default method)
  for /login, auto-login, and re-auth.
- dispatch_login_with resolves the id against the advertised methods and
  FAILS CLOSED on an unknown id — no silent first-method fallback — then
  adopts the method's label and start mode.

Tests: picker rows pinned to their method ids; LoginWith(claude-pro-max)
must authenticate with claude-pro-max even when kimi-code was previously
resolved; unknown ids surface an error and start nothing.

Verified: kigi-tui 6867 tests green, clippy clean.
This commit is contained in:
2026-07-22 15:51:46 -04:00
parent c2d1067f0d
commit 1e57cd9225
5 changed files with 195 additions and 29 deletions
@@ -175,6 +175,44 @@ pub(super) fn strip_trailing_auth_error_blocks(agent: &mut AgentView) {
/// restored once auth completes or is cancelled. Without this, `/login`
/// with an external auth provider configured appeared to do nothing.
pub(super) fn dispatch_login(app: &mut AppView) -> Vec<Effect> {
dispatch_login_with(app, None)
}
/// Start an interactive login flow with an explicitly chosen method (a
/// provider row on the login picker). `None` keeps the historical behavior:
/// re-use the current method or resolve the first interactive one.
///
/// The explicit id is resolved against the shell-advertised `auth_methods`
/// and FAILS CLOSED when absent — silently falling back to the first method
/// is exactly the bug that sent every provider row to the Kimi flow.
pub(super) fn dispatch_login_with(
app: &mut AppView,
method_id: Option<agent_client_protocol::AuthMethodId>,
) -> Vec<Effect> {
if let Some(id) = method_id {
let Some(method) = app.auth_methods.iter().find(|m| *m.id() == id) else {
app.auth_state = AuthState::Pending {
error: Some(format!("Login method not available: {}", id.0)),
};
return vec![];
};
// Mirror `find_interactive_login_method`: external auth providers
// start in Command mode, everything else Pending (the mode firms up
// when the auth URL arrives).
let is_provider = method
.meta()
.as_ref()
.and_then(|v| v.get("external_provider"))
.and_then(|v| v.as_bool())
.unwrap_or(false);
app.login_label = Some(method.name().to_string());
app.login_method_id = Some(method.id().clone());
app.auth_start_mode = if is_provider {
AuthMode::Command
} else {
AuthMode::Pending
};
}
ensure_login_method(app);
let Some(method_id) = app.login_method_id.clone() else {
app.auth_state = AuthState::Pending {