F3: Kimi inference pipeline + full grok cloud-surface excision

Sampler / inference (PRD F3):
- kimi_compat.rs: single adaptation point for the Kimi chat/completions
  dialect (thinking-field mapping, model_id stripping, empty-content
  tool-call message fix, stream_options.include_usage), with kimi-cli
  source citations
- Rate-limit handling reworked for Kimi/Moonshot semantics; UA kigi/{version}
- /models replaces the xAI models-v2 endpoint everywhere; idle model
  refresh carries X-Msh-* device headers only (X-XAI-Token-Auth and
  x-grok-client-mode/CLIENT_MODE_HEADER machinery deleted)

Cloud-surface excision (PRD §5, zero-egress):
- remote/ conversations lane, cli-chat-proxy-types crate, prod/ dir,
  share command, credit bar: deleted (single local session lane;
  paginate() replaces merge_and_paginate)
- Subscription/tier gate stack deleted end-to-end: AppView
  gate/tier/team/ZDR fields, app/subscription.rs watch loop,
  dispatch/billing.rs paywall + SuperGrok upsell, free-usage-exhausted
  chain, tier-restricted commands, GateInfo, RemoteSettings gate fields,
  SettingsUpdateNotification gate fields
- /privacy + coding-data-sharing setting deleted (backed by a dead xAI
  RPC; Kigi is zero-egress — nothing to share or retain remotely)

Auth UX correctness (user-reported):
- Device-flow fixtures now mirror the live Kimi payload shape
  (https://www.kimi.com/code/authorize_device?user_code=..., verified
  against auth.kimi.com); the fabricated auth.kimi.com/device?code=...
  URLs are gone
- open_browser_detached is a no-op under cfg(test): unit tests drove
  wiremock fixture URLs into the real browser (root cause of the
  "garbage mock link" ABCD-1234 tabs)
- Welcome/pager-minimal rebrand: Grok Build -> Kigi, grok.com ->
  kimi.com, "Sign in to Grok" -> "Sign in to Kimi"
This commit is contained in:
2026-07-17 16:05:51 -04:00
parent fe1f885bb3
commit ea0ce9d15f
231 changed files with 4730 additions and 26358 deletions
+6 -156
View File
@@ -595,10 +595,6 @@ pub(crate) async fn run(
.as_ref()
.and_then(|s| s.show_resolved_model)
.unwrap_or(true);
app.sharing_enabled = remote_settings
.as_ref()
.and_then(|s| s.sharing_enabled)
.unwrap_or(false);
app.session_picker_grouped = std::env::var("KIGI_SESSION_PICKER_GROUPED")
.ok()
.and_then(|v| match v.as_str() {
@@ -681,7 +677,7 @@ pub(crate) async fn run(
// else: auth_state defaults to Done (already authenticated eagerly)
// Effects stashed until after the initial render, so the user sees the
// welcome/auth UI right away.
let mut post_render_effects = if needs_interactive_login {
let post_render_effects = if needs_interactive_login {
if connection.auth_methods.is_empty() {
app.auth_state = super::app_view::AuthState::Pending {
error: Some("No login method available".to_string()),
@@ -711,21 +707,6 @@ pub(crate) async fn run(
}
}
// Fallback: prefetch may have gate info the shell's AuthMeta missed.
// Errs on the side of blocking if stale.
if app.gate.is_none()
&& let Some(rs) = remote_settings.as_ref()
{
app.gate = AppView::gate_from_settings(rs);
}
// Re-impose the startup gate through the chokepoint: cached auth meta
// and the settings prefetch are both possibly stale, so a consumer
// session's gate is deferred for live verification before first paint.
if let Some(gate) = app.gate.take() {
post_render_effects.extend(app.impose_gate(gate));
}
// Load config layers once, resolve tips and feature flags.
let requirements = kigi_shell::config::load_merged_requirements();
let user_config = kigi_shell::config::load_from_disk().ok();
@@ -756,17 +737,6 @@ pub(crate) async fn run(
);
}
app.zdr_access_enabled = kigi_shell::util::config::resolve_zdr_access_enabled(
requirements.as_ref(),
user_config.as_ref(),
managed_config.as_ref(),
remote_settings.as_ref(),
);
app.subscription_watch_interval_secs = remote_settings
.as_ref()
.and_then(|rs| rs.subscription_watch_interval_secs);
// Full layered resolve (env/requirements/remote may beat plain `[ui]`).
crate::appearance::cache::set_show_thinking_blocks(
kigi_shell::util::config::resolve_show_thinking_blocks(
@@ -796,14 +766,6 @@ pub(crate) async fn run(
.value,
);
app.usage_billing_redirect_url = remote_settings
.as_ref()
.and_then(|s| s.usage_billing_redirect_url.clone());
if app.is_access_blocked() {
app.welcome_prompt_focused = false;
}
{
use kigi_shell::util::config::resolve_tips;
@@ -1124,20 +1086,6 @@ pub(crate) async fn run(
// iteration so it is popped on every close path.
let mut gboom_keyboard_pushed = false;
const BILLING_POLL_INTERVAL: Duration = Duration::from_secs(30);
let mut billing_poll_at: Option<Instant> = None;
const GATE_POLL_INTERVAL: Duration = Duration::from_secs(30);
let mut gate_poll_at: Option<Instant> = None;
// Free→paid subscription watch (see `app::subscription`).
let mut subscription_watch_at: Option<Instant> = if app.subscription_watch_wanted() {
app.subscription_watch_interval()
.map(|iv| Instant::now() + iv)
} else {
None
};
// Leader-mode roster poll (FleetView dashboard). Only fires while the
// dashboard is open AND we're connected via a leader. Armed to fire
// immediately at loop start so an already-open dashboard refreshes
@@ -1168,22 +1116,12 @@ pub(crate) async fn run(
if process_effects(effs, &mut tasks, &mut app, &progress_tx) {
return Ok(make_run_result(&app));
}
// Fetch billing early so the welcome screen can show a credit warning.
if app.usage_visible {
let effs = vec![super::actions::Effect::FetchAppBilling];
if process_effects(effs, &mut tasks, &mut app, &progress_tx) {
return Ok(make_run_result(&app));
}
}
// Fetch changelog off the render path so the welcome screen
// can display bullets and /release-notes uses the cached result.
let effs = vec![super::actions::Effect::FetchChangelog];
if process_effects(effs, &mut tasks, &mut app, &progress_tx) {
return Ok(make_run_result(&app));
}
if !app.has_access() {
gate_poll_at = Some(Instant::now() + GATE_POLL_INTERVAL);
}
}
if !post_render_effects.is_empty()
@@ -1276,16 +1214,14 @@ pub(crate) async fn run(
app.draw(terminal);
}
// Initial prompt from the CLI positional (`grok "fix the bug"`). When
// Initial prompt from the CLI positional (`kigi "fix the bug"`). When
// already authenticated, hand it to the shared dispatcher helper (same
// `NewSession`/`SendPrompt` path the welcome screen uses). ZDR-blocked
// accounts cannot start a session, so drop the prompt — this mirrors the
// deferred post-login path, which clears the startup prompt for ZDR-blocked
// accounts. When not yet authenticated, stash it for `AuthComplete`.
// `NewSession`/`SendPrompt` path the welcome screen uses). When not yet
// authenticated, stash it for `AuthComplete`.
if let Some(initial_prompt) = args.initial_prompt() {
if !app.session_startup_allowed() {
app.deferred_startup.prompt = Some(initial_prompt.to_string());
} else if !app.is_zdr_blocked() {
} else {
let effs = dispatch::dispatch_initial_prompt(&mut app, initial_prompt.to_string());
if process_effects(effs, &mut tasks, &mut app, &progress_tx) {
return Ok(make_run_result(&app));
@@ -1322,10 +1258,7 @@ pub(crate) async fn run(
// empty one so the user lands directly at the prompt. Unauthenticated /
// ZDR-blocked startup stays on Welcome, where `crate::minimal::live` shows
// a sign-in hint instead of a blank region.
if term_state.screen_mode.is_minimal()
&& matches!(app.active_view, ActiveView::Welcome)
&& !app.is_zdr_blocked()
{
if term_state.screen_mode.is_minimal() && matches!(app.active_view, ActiveView::Welcome) {
if app.session_startup_allowed() {
// Already authenticated + trusted: open the empty session now so the
// user lands directly at the prompt.
@@ -1456,15 +1389,6 @@ pub(crate) async fn run(
roster_poll_at = Some(Instant::now());
}
// (Re-)arm the subscription watch on the dormant→wanted transition
// and after each fired tick.
if subscription_watch_at.is_none()
&& app.subscription_watch_wanted()
&& let Some(iv) = app.subscription_watch_interval()
{
subscription_watch_at = Some(Instant::now() + iv);
}
// Future that sleeps until the next animation tick, or waits forever if none.
let animation_tick = async {
match animation_tick_at {
@@ -1511,27 +1435,6 @@ pub(crate) async fn run(
}
};
let billing_poll = async {
match billing_poll_at {
Some(at) => sleep_until(at).await,
None => std::future::pending().await,
}
};
let gate_poll = async {
match gate_poll_at {
Some(at) => sleep_until(at).await,
None => std::future::pending().await,
}
};
let subscription_watch = async {
match subscription_watch_at {
Some(at) => sleep_until(at).await,
None => std::future::pending().await,
}
};
let roster_poll = async {
match roster_poll_at {
Some(at) => sleep_until(at).await,
@@ -1629,18 +1532,6 @@ pub(crate) async fn run(
schedule_tick(&mut animation_tick_at, &app, tick_interval);
resize_debounce_at = None;
// Schedule/clear poll timers.
if app.billing_poll_wanted && billing_poll_at.is_none() {
billing_poll_at = Some(Instant::now() + BILLING_POLL_INTERVAL);
} else if !app.billing_poll_wanted {
billing_poll_at = None;
}
if !app.has_access() && gate_poll_at.is_none() {
gate_poll_at = Some(Instant::now() + GATE_POLL_INTERVAL);
} else if app.has_access() {
gate_poll_at = None;
}
app.draw(terminal);
last_draw_at = Instant::now();
draw_scheduled_at = None;
@@ -1810,41 +1701,6 @@ pub(crate) async fn run(
schedule_tick(&mut animation_tick_at, &app, tick_interval);
}
_ = billing_poll => {
billing_poll_at = None;
if let ActiveView::Agent(id) = app.active_view {
let effs = vec![Effect::FetchBilling {
agent_id: id,
silent: true,
}];
if process_effects(effs, &mut tasks, &mut app, &progress_tx) {
break;
}
}
if app.billing_poll_wanted {
billing_poll_at = Some(Instant::now() + BILLING_POLL_INTERVAL);
}
}
_ = gate_poll => {
gate_poll_at = None;
let effs = vec![Effect::RefreshGate];
if process_effects(effs, &mut tasks, &mut app, &progress_tx) {
break;
}
if !app.has_access() {
gate_poll_at = Some(Instant::now() + GATE_POLL_INTERVAL);
}
}
_ = subscription_watch => {
subscription_watch_at = None;
let effs = app.fire_subscription_check("watch");
if process_effects(effs, &mut tasks, &mut app, &progress_tx) {
break;
}
}
_ = roster_poll => {
roster_poll_at = None;
// Only poll while the dashboard is open. When it is not active
@@ -2519,12 +2375,6 @@ async fn drain_and_process(
{
crate::clipboard::prewarm_image_probe();
}
// The user may have just subscribed in the browser and
// tabbed back.
let effs = app.fire_subscription_check("focus");
if process_effects(effs, tasks, app, progress_tx) {
return true;
}
// Restore Prompt on refocus: needs-input overlay always, else idle non-vim.
match app.active_view {
ActiveView::Agent(id) => {