feat(login): /login opens the provider picker with green connected badges

/login previously fired the resolved method's flow immediately — there was
no way to see providers or their status. It now lands on the provider
picker; already-connected providers show a green 'connected' badge in the
key column.

Shell: initialize() probes stored credentials once (primary session flag,
auth.json oauth/<provider> scopes, resolved platform keys env>auth.json>
config) and stamps _meta.connected on each advertised method
(connected_method_ids + stamp_connected_meta, pure and unit-tested).
Display state only — never an authorization input.

TUI:
- PendingMenuItem::connected() reads the badge from method meta; the
  picker renders it green (accent_success), replacing the shortcut hint.
- New Action::OpenLoginPicker: /login shows the picker; mid-session it
  stashes the view like dispatch_login, and starts no flow by itself.
- Mid-session picker: last row reads 'Cancel' and dispatches CancelLogin
  (clicking it must not exit the app); Esc also returns to the session.
- After a successful login, the just-authenticated method is stamped
  connected in the TUI's advertised-methods copy (auth_in_flight_method →
  AuthComplete), so a later /login shows the badge without re-initialize.

Verified: kigi-shell 5256 + kigi-tui 6870 tests green, clippy clean.
This commit is contained in:
ZacharyZhang-NY committed 2026-07-22 16:14:42 -04:00
1 parent 1e57cd9225
commit fc7c2a1b9b
11 files changed
+384 -15

No files matched your search

@@ -506,3 +506,72 @@ fn login_with_unknown_method_fails_closed() {
app.auth_state
);
}
/// `/login` opens the provider picker (welcome `Pending`) instead of
/// auto-starting a flow — and from inside a session it stashes the view
/// (so Esc/q/Cancel return to it) without emitting any auth effect.
#[test]
fn open_login_picker_shows_picker_and_stashes_view() {
let mut app = test_app_with_agent();
assert_eq!(app.active_view, ActiveView::Agent(AgentId(0)));
let effects = dispatch(Action::OpenLoginPicker, &mut app);
assert!(effects.is_empty(), "the picker itself starts nothing");
assert_eq!(app.active_view, ActiveView::Welcome);
assert_eq!(app.auth_return_view, Some(ActiveView::Agent(AgentId(0))));
assert!(matches!(app.auth_state, AuthState::Pending { error: None }));
}
/// A successful login stamps `_meta.connected` on the method that just
/// authenticated, so a later `/login` picker shows its green badge without
/// re-initializing the shell.
#[test]
fn auth_complete_marks_the_authenticated_method_connected() {
use crate::app::app_view::pending_menu_items;
let mut app = test_app();
app.auth_state = AuthState::Pending { error: None };
app.auth_methods = kigi_shell::agent::auth_method::build_auth_methods(
kigi_shell::agent::auth_method::AuthMethodsBuildInputs {
has_external_api_key: false,
has_cached_token: false,
login_label: None,
},
)
.methods;
dispatch(
Action::LoginWith(acp::AuthMethodId::new("xai-grok")),
&mut app,
);
let seq = match &app.auth_state {
AuthState::Authenticating { request_seq, .. } => *request_seq,
other => panic!("expected Authenticating, got {other:?}"),
};
dispatch(
Action::TaskComplete(TaskResult::AuthComplete {
request_seq: seq,
meta: None,
}),
&mut app,
);
let items = pending_menu_items(&app.auth_methods, None);
let grok = items
.iter()
.find(|i| i.label().starts_with("xAI Grok"))
.expect("grok row");
assert!(
grok.connected(&app.auth_methods),
"the just-authenticated method must show as connected"
);
let kimi = items
.iter()
.find(|i| i.label().starts_with("Kimi Code"))
.expect("kimi row");
assert!(
!kimi.connected(&app.auth_methods),
"other methods must stay unmarked"
);
}