Login picker: offer all three platforms like the official CLI
The unauthenticated welcome screen previously offered only 'Login with
Kimi Code'. It now lists every interactive platform the shell
advertises, matching the official kimi-cli picker:
Kimi Code (OAuth)
Moonshot Open Platform (API key · moonshot.cn)
Moonshot Open Platform (API key · moonshot.ai)
Quit
- Shell: new ACP auth methods moonshot-cn / moonshot-ai (advertised
after kimi-code; the BYOK first-position invariant holds).
authenticate(moonshot-*) reloads keys from env>config, fails with an
actionable message when none is configured, validates the key against
GET {base}/models (401 → 'invalid API key' naming the console), then
swaps the fresh config in, triggers the model sync, and reports
auth_mode api_key so the pager treats it like other API-key logins.
Never session-based; keys never logged.
- Config: save_platform_api_key persists [platforms.<id>].api_key via an
atomic mode-preserving write under the config lock; refuses OAuth
platforms, blank keys, and unparseable files.
- TUI: the Pending welcome renders the picker rows from the advertised
methods (arrows/Enter/mouse; 'l' keeps selecting the OAuth row).
Choosing a Moonshot row opens a masked paste box ('Paste your Moonshot
API key (from platform.moonshot.cn)'); Esc returns to the picker,
Enter persists the key and authenticates in one sequential effect;
failures return to the picker with the error line, success lands on
the normal welcome. Startup eager-auth is unchanged: a key already in
the environment authenticates exactly as before, and single-method
shells keep the historical auto-device-flow.
Gates: workspace check/clippy 0/0; shell 4870 + tui 6620 lib tests
green; headless probe advertises [kimi-code, moonshot-cn, moonshot-ai]
for a fresh user and xai.api_key-first with a key configured.
This commit is contained in:
@@ -7,7 +7,7 @@ use super::session::lifecycle::{clear_startup_actions, drain_startup_actions};
|
||||
use crate::app::actions::{Action, Effect};
|
||||
use crate::app::agent::AgentId;
|
||||
use crate::app::agent_view::AgentView;
|
||||
use crate::app::app_view::{ActiveView, AppView, AuthMode, AuthState};
|
||||
use crate::app::app_view::{ActiveView, AppView, AuthMode, AuthState, PlatformLogin};
|
||||
use crate::scrollback::block::RenderBlock;
|
||||
use crate::scrollback::blocks::SessionEvent;
|
||||
|
||||
@@ -252,6 +252,74 @@ pub(super) fn dispatch_submit_auth_code(app: &mut AppView, code: String) -> Vec<
|
||||
vec![Effect::SubmitAuthCode { request_seq, code }]
|
||||
}
|
||||
|
||||
/// A Moonshot row was selected on the welcome login picker: switch the
|
||||
/// welcome screen into the API-key paste box for that platform.
|
||||
pub(super) fn dispatch_begin_platform_key_entry(
|
||||
app: &mut AppView,
|
||||
target: PlatformLogin,
|
||||
) -> Vec<Effect> {
|
||||
let request_seq = app.next_auth_request_seq;
|
||||
app.next_auth_request_seq += 1;
|
||||
app.auth_code_input.clear();
|
||||
app.auth_state = AuthState::Authenticating {
|
||||
request_seq,
|
||||
handle: None,
|
||||
auth_url: None,
|
||||
mode: AuthMode::ApiKeyEntry(target),
|
||||
};
|
||||
vec![]
|
||||
}
|
||||
|
||||
/// Esc in the API-key paste box: back to the login picker (no error line).
|
||||
/// Bumps the request seq so any stale in-flight auth result is dropped by
|
||||
/// the `AuthComplete`/`AuthFailed` guards.
|
||||
pub(super) fn dispatch_cancel_platform_key_entry(app: &mut AppView) -> Vec<Effect> {
|
||||
if !matches!(
|
||||
app.auth_state,
|
||||
AuthState::Authenticating {
|
||||
mode: AuthMode::ApiKeyEntry(_),
|
||||
..
|
||||
}
|
||||
) {
|
||||
return vec![];
|
||||
}
|
||||
app.next_auth_request_seq += 1;
|
||||
app.auth_code_input.clear();
|
||||
app.auth_state = AuthState::Pending { error: None };
|
||||
vec![]
|
||||
}
|
||||
|
||||
/// Enter with a non-empty key in the API-key paste box: persist the key to
|
||||
/// `[platforms.<id>]` in config.toml, then authenticate with the platform's
|
||||
/// method id (one sequential background task — see the effect handler).
|
||||
/// The screen shows the connecting state while the key is validated; a
|
||||
/// failure lands back on the picker with the error line (`AuthFailed`).
|
||||
pub(super) fn dispatch_submit_platform_api_key(app: &mut AppView, key: String) -> Vec<Effect> {
|
||||
let (request_seq, target) = match &app.auth_state {
|
||||
AuthState::Authenticating {
|
||||
request_seq,
|
||||
mode: AuthMode::ApiKeyEntry(target),
|
||||
..
|
||||
} => (*request_seq, *target),
|
||||
_ => return vec![],
|
||||
};
|
||||
let key = key.trim().to_string();
|
||||
if key.is_empty() {
|
||||
return vec![];
|
||||
}
|
||||
app.auth_state = AuthState::Authenticating {
|
||||
request_seq,
|
||||
handle: None,
|
||||
auth_url: None,
|
||||
mode: AuthMode::Pending,
|
||||
};
|
||||
vec![Effect::PersistPlatformApiKeyAndAuthenticate {
|
||||
request_seq,
|
||||
target,
|
||||
key,
|
||||
}]
|
||||
}
|
||||
|
||||
// TaskResult handlers.
|
||||
|
||||
pub(super) fn handle_auth_complete(
|
||||
|
||||
Reference in New Issue
Block a user