M0: compilable skeleton — Kigi 0.1.0 fork surgery

Hard fork of xai-org/grok-build (Apache-2.0) re-targeted as Kigi, an
unofficial Kimi Code CLI community build.

Rename & identity
- 72 xai-*/xai-grok-* crates -> kigi-* (explicit: xai-grok-pager-bin ->
  kigi-bin [binary `kigi`], xai-grok-pager -> kigi-tui; rest mechanical);
  ptyctl, ptyctl-cli, third_party/ unchanged; proto package
  xai.grok.tools.v1 -> kigi.tools.v1
- Config home ~/.kigi (KIGI_SHARE_DIR override), env prefix GROK_* ->
  KIGI_*, `kigi --version` carries the unofficial-community-build notice
- clap identity, help text, startup banner, prompt templates rebranded
  (templates re-encrypted)

Deletions (PRD removal list #5/#6/#7/#9/#10)
- voice input (xai-grok-voice) and all TUI wiring
- telemetry: Mixpanel client, external OTel stream, Sentry, OTLP layers,
  trace/GCS/S3 upload queues (kigi-file-utils halved), workspace upload
  module & dc_log, heap-profile uploader, auth-diagnostics uploader,
  session-analytics halves of feedback; local zero-egress observability
  preserved in new kigi-log crate (unified log, --debug firehose,
  subsystem file logs, opt-in instrumentation)
- announcements (crate, remote-settings fields, TUI surfaces)
- plugin marketplace (crate, sources/browse/CTA/extensions-modal tab);
  direct plugin install/uninstall/update via kigi-agent git_install kept
- relay/gateway/assets endpoints and features (agent relay, headless
  relay transport, gateway bridge, LeaderEnvUrls); leader IPC socket now
  ~/.kigi/leader.sock + KIGI_LEADER_SOCKET, no ws-url derivation
- functional types rehomed instead of deleted: PermissionMode ->
  kigi-config-types, McpInitStrategy -> kigi-mcp, PrCreationSource ->
  session signals, TerminalDiagnostics -> kigi-pager-render, agent_id ->
  shell util

Endpoints
- kigi-env rewritten: single production KigiEndpoints {coding_api_base_url
  https://api.kimi.com/coding/v1 (KIGI_CODE_BASE_URL), oauth_host
  https://auth.kimi.com (KIGI_OAUTH_HOST), update_base_url (GitHub
  Releases API), upgrade_page_url}; GrokBuildEnvironment enum deleted

Toolchain & workspace hygiene
- Rust 1.97.0 pinned; edition 2024; full cargo update; git2 hoisted to
  workspace at 0.21 (Option->Result API migration), quick-xml 0.41
- Root Cargo.toml hand-maintained (PRD §8.1): version 0.1.0 inherited by
  all members, members sorted, unused deps pruned
- cargo-deny advisories gate (deny.toml with documented transitive
  exceptions); CI workflow (check/clippy/fmt/deny/test, macOS+Linux)
- cross-crate test seams re-gated behind `test-support` cargo feature;
  insta snapshot baselines renamed to the kigi_tui prefix
- clippy --workspace --all-targets: zero warnings; fmt clean

Fixes surfaced by the port
- updater probe/installer divergence (bin/kigi vs bin/grok symlink set)
- idle model-metadata refresh dead under KIGI_CODE_BASE_URL override
  (new is_effective_coding_endpoint_url, loopback+override aware)
- macOS symlinked-TMPDIR fixture canonicalization (foreign_sessions,
  fast-worktree); RSS measurement tests serialized via serial_test

Docs & legal (Apache §4)
- NOTICE added (upstream attribution + change statement); THIRD-PARTY
  notices sustained; kigi-tools ported-code notices extended; README,
  CONTRIBUTING, SECURITY, AGENTS.md rewritten

Out of scope for M0 (tracked): Kimi auth/inference (M1), search/fetch,
command parity, config import (M2), Computer Hub excision & final
brand-token sweep (M2), distribution & self-update rewrite (M3).
This commit is contained in:
2026-07-17 05:31:01 -04:00
commit d6c20fc13f
2612 changed files with 1353757 additions and 0 deletions
@@ -0,0 +1,434 @@
//! Login, logout, account switching, and auth-code submission dispatchers.
use super::ctx::{restore_auth_return_view, show_welcome};
use super::queue::maybe_drain_queue;
use super::router::dispatch;
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::scrollback::block::RenderBlock;
use crate::scrollback::blocks::SessionEvent;
// ---------------------------------------------------------------------------
// Auth dispatch
// ---------------------------------------------------------------------------
/// `/logout` -- ask the shell to clear auth, then return to the login screen.
pub(super) fn dispatch_logout(_app: &mut AppView) -> Vec<Effect> {
vec![Effect::Logout]
}
/// Ensure `login_method_id` is populated from stored auth methods.
/// On the eager-auth path (cached token), login_method_id is never set
/// because the user skipped the login screen.
///
/// Does **not** invent `grok.com` when no interactive method is advertised
/// (e.g. `preferred_method=api_key` with no key — empty `auth_methods`).
/// Callers already surface "No login method available" when this leaves
/// `login_method_id` unset.
pub(super) fn ensure_login_method(app: &mut AppView) {
if app.login_method_id.is_some() {
return;
}
let (label, method_id, start_mode) =
crate::acp::find_interactive_login_method(&app.auth_methods);
if let Some(id) = method_id {
app.login_label = label;
app.login_method_id = Some(id);
app.auth_start_mode = match start_mode {
crate::acp::AuthStartMode::Pending => AuthMode::Pending,
crate::acp::AuthStartMode::Command => AuthMode::Command,
};
}
// No interactive method: leave login_method_id unset (fail-closed).
}
/// Error when no interactive login method is available (empty auth_methods,
/// e.g. `preferred_method=api_key` with no credentials). Prefer the shell's
/// pin-unavailable copy when the list is empty.
fn no_login_method_error(app: &AppView) -> String {
if app.auth_methods.is_empty() {
kigi_shell::agent::auth_method::PREFERRED_API_KEY_UNAVAILABLE.to_string()
} else {
"No login method available".to_string()
}
}
/// Log out, then start a new login flow in a single sequential task.
pub(super) fn dispatch_switch_account(app: &mut AppView) -> Vec<Effect> {
ensure_login_method(app);
let Some(method_id) = app.login_method_id.clone() else {
app.auth_state = AuthState::Pending {
error: Some(no_login_method_error(app)),
};
return vec![];
};
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: app.auth_start_mode,
};
vec![
Effect::SwitchAccount {
request_seq,
method_id,
use_oauth: app.auth_use_oauth,
},
Effect::PollAuthUrl { request_seq },
]
}
/// Scan the trailing run of session-event / system blocks for a
/// [`SessionEvent::ReAuthRequired`] prompt. Used by the `PromptResponse`
/// handler to suppress the redundant "Turn failed" block after a 401 — the
/// re-auth prompt is pushed by the `RetryState` handler, which runs first.
pub(super) fn scrollback_has_recent_reauth_prompt(
scrollback: &crate::scrollback::state::ScrollbackState,
) -> bool {
use crate::scrollback::block::RenderBlock;
for idx in (0..scrollback.len()).rev() {
match scrollback.entry(idx).map(|e| &e.block) {
Some(RenderBlock::SessionEvent(ev)) => {
if matches!(ev.event, SessionEvent::ReAuthRequired) {
return true;
}
}
// Tolerate interleaved system messages in the trailing run.
Some(RenderBlock::System(_)) => {}
// Stop at the first substantive block: any re-auth prompt for
// this turn lives in the trailing events pushed just before the
// PromptResponse arrived.
_ => break,
}
}
false
}
/// True if the trailing run of session/system blocks contains a terminal
/// context-overflow block ([`SessionEvent::ContextTooLarge`] or `CompactionFailed`).
/// Lets `PromptResponse` suppress the redundant `TurnFailed`, mirroring reauth.
pub(super) fn scrollback_has_recent_context_too_large(
scrollback: &crate::scrollback::state::ScrollbackState,
) -> bool {
use crate::scrollback::block::RenderBlock;
for idx in (0..scrollback.len()).rev() {
match scrollback.entry(idx).map(|e| &e.block) {
Some(RenderBlock::SessionEvent(ev)) => {
if matches!(
ev.event,
SessionEvent::ContextTooLarge | SessionEvent::CompactionFailed { .. }
) {
return true;
}
}
// Tolerate interleaved system messages in the trailing run.
Some(RenderBlock::System(_)) => {}
// Stop at the first substantive block.
_ => break,
}
}
false
}
/// Strip the trailing run of auth-error blocks — the `ReAuthRequired`
/// prompt plus any stale `RetryFailed` / `TurnFailed` — from an agent's
/// scrollback. Called after a successful mid-session re-auth so the prompt
/// disappears once the user returns to the session. Mirrors the
/// credit-limit upsell's stale-block strip.
pub(super) fn strip_trailing_auth_error_blocks(agent: &mut AgentView) {
use crate::scrollback::block::RenderBlock;
let mut to_remove = Vec::new();
for idx in (0..agent.scrollback.len()).rev() {
match agent.scrollback.entry(idx).map(|e| &e.block) {
Some(RenderBlock::SessionEvent(ev))
if matches!(
&ev.event,
SessionEvent::ReAuthRequired
| SessionEvent::RetryFailed { .. }
| SessionEvent::TurnFailed { .. }
) =>
{
to_remove.push(idx);
}
// Skip over other trailing session-event / system blocks.
Some(RenderBlock::SessionEvent(_) | RenderBlock::System(_)) => continue,
// Stop at the first substantive block.
_ => break,
}
}
for idx in to_remove {
agent.scrollback.remove_from(idx);
}
}
/// Start an interactive login flow. Triggered by pressing 'l' on the
/// welcome screen or by the `/login` slash command.
///
/// When invoked mid-session (the active view is an agent/dashboard rather
/// than the welcome screen), the auth UI — including the external auth
/// provider's sign-in URL and status — is only rendered by the welcome
/// view. We therefore stash the caller's view in `auth_return_view` and
/// switch to `Welcome` so the flow is actually visible; the prior view is
/// 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> {
ensure_login_method(app);
let Some(method_id) = app.login_method_id.clone() else {
app.auth_state = AuthState::Pending {
error: Some(no_login_method_error(app)),
};
return vec![];
};
// Surface the auth UI when triggered from inside a session. `show_welcome`
// resets ephemeral state here, covering the AuthComplete / cancel-login
// fallbacks too (`auth_return_view` is only ever set here).
if !matches!(app.active_view, ActiveView::Welcome) {
app.auth_return_view = Some(app.active_view);
show_welcome(app);
}
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: app.auth_start_mode,
};
vec![
Effect::Authenticate {
request_seq,
method_id,
use_oauth: app.auth_use_oauth,
force_interactive: true,
},
Effect::PollAuthUrl { request_seq },
]
}
/// Cancel a login that was started from inside a session and restore the
/// caller's view. Only meaningful when `auth_return_view` is set (a
/// mid-session `/login` or 401 re-auth prompt). Any in-flight auth task is
/// left to finish in the background — its `AuthComplete`/`AuthFailed`
/// result is ignored because we move `auth_state` out of `Authenticating`
/// here (the request-seq/state guard in those handlers drops stale results)
/// and bump the seq so a fresh login does not collide.
pub(super) fn dispatch_cancel_login(app: &mut AppView) -> Vec<Effect> {
let Some(return_view) = app.auth_return_view.take() else {
return vec![];
};
app.next_auth_request_seq += 1;
app.auth_state = AuthState::Done;
app.auth_show_raw_url = false;
app.auth_code_input.clear();
restore_auth_return_view(app, return_view);
// The user bailed out of re-auth — drop stashed prompts and strip the
// stale re-auth prompt from scrollback (on all agents: the login may
// have been started from the dashboard). Clearing the stash alone is
// not enough: a leftover `ReAuthRequired` block would let a later
// `PromptResponse` re-detect it via `scrollback_has_recent_reauth_prompt`
// and re-stash the prompt, so a subsequent unrelated login could
// silently resubmit it. Mirrors the strip in the `AuthComplete` path.
for agent in app.agents.values_mut() {
agent.reauth_stashed_prompt = None;
strip_trailing_auth_error_blocks(agent);
}
vec![]
}
/// User submitted a manually-pasted auth token in loopback mode.
pub(super) fn dispatch_submit_auth_code(app: &mut AppView, code: String) -> Vec<Effect> {
let request_seq = match &app.auth_state {
AuthState::Authenticating { request_seq, .. } => *request_seq,
_ => return vec![],
};
vec![Effect::SubmitAuthCode { request_seq, code }]
}
// TaskResult handlers.
pub(super) fn handle_auth_complete(
app: &mut AppView,
request_seq: u64,
meta: Option<serde_json::Value>,
) -> Vec<Effect> {
if let AuthState::Authenticating {
request_seq: current_seq,
..
} = &app.auth_state
&& *current_seq == request_seq
{
if let Some(meta_val) = meta.as_ref()
&& let Ok(auth_meta) =
serde_json::from_value::<kigi_shell::auth::AuthMeta>(meta_val.clone())
{
app.apply_auth_meta(&auth_meta);
}
app.auth_state = AuthState::Done;
app.auth_show_raw_url = false;
app.welcome_prompt_focused = !app.is_access_blocked();
app.auth_code_input.clear();
// Mid-session re-auth (`/login` or a 401 prompt): restore the
// view the user was on instead of running the startup
// load-session flow. The session state lives in `app.agents`,
// independent of `active_view`, so it is preserved across the
// auth detour.
if let Some(return_view) = app.auth_return_view.take() {
restore_auth_return_view(app, return_view);
// Mid-session re-auth returns to the existing session, NOT
// the startup flow, so discard any deferred startup stash
// (e.g. an incidental `Ctrl+N` pressed during /login that the
// chokepoint deferred) rather than leaving it to fire later.
clear_startup_actions(app);
// Re-auth succeeded — hide the now-stale re-auth prompt
// (and any trailing error blocks) so the user returns to
// a clean session. Mirrors the credit-limit upsell's
// stale-block strip.
// Auth is global, so handle every agent (the login may
// have been started from the dashboard, not the agent
// that 401'd).
let mut retry_effects = Vec::new();
for agent in app.agents.values_mut() {
strip_trailing_auth_error_blocks(agent);
// Auto-resubmit the prompt that failed on the expired
// login so the user doesn't have to retype it. The
// user couldn't have queued another prompt during the
// auth detour, so a plain front-enqueue + drain is safe.
if let Some(prompt) = agent.reauth_stashed_prompt.take() {
agent.scrollback.push_block(RenderBlock::system(
"Re-authenticated. Retrying\u{2026}".to_string(),
));
agent.session.enqueue_in_flight_prompt_front(prompt);
retry_effects.extend(maybe_drain_queue(agent));
}
}
let mut effects = dispatch(Action::RequestBundleStatus, app);
if app.usage_visible {
effects.push(Effect::FetchAppBilling);
}
effects.extend(retry_effects);
return effects;
}
// status only; shell auto-syncs post-auth
let mut effects = dispatch(Action::RequestBundleStatus, app);
// Start auto-checking subscription if gated.
// Check immediately (don't wait 5s) then schedule the timer.
if !app.has_access() {
app.paywall_check_started = Some(std::time::Instant::now());
effects.push(Effect::CheckSubscription { verify: None });
effects.push(Effect::SchedulePaywallCheck);
}
// Fetch billing so the welcome screen can show a credit warning.
if app.usage_visible {
effects.push(Effect::FetchAppBilling);
}
// Fetch changelog (mirrors startup path for interactive login).
effects.push(Effect::FetchChangelog);
// ZDR-blocked users stay on the welcome screen — discard any
// deferred startup (they cannot start a session).
if app.is_zdr_blocked() {
clear_startup_actions(app);
return effects;
}
// Replay deferred session startup once BOTH gates are open. Auth
// is now Done, so `session_startup_allowed()` here means "is trust
// also resolved?" -- if trust is still Pending its question renders
// next and its answer drains instead. Same predicate the trust
// handlers use, so the deferred startup runs exactly once after
// whichever gate resolves last.
if app.session_startup_allowed() {
effects.extend(drain_startup_actions(app));
}
return effects;
}
vec![]
}
pub(super) fn handle_auth_url_ready(
app: &mut AppView,
request_seq: u64,
auth_url: Option<String>,
external: bool,
mode: Option<String>,
) -> Vec<Effect> {
if let AuthState::Authenticating {
request_seq: current_seq,
auth_url: current_url,
mode: current_mode,
..
} = &mut app.auth_state
&& *current_seq == request_seq
{
*current_url = auth_url;
// Prefer `mode`; fall back to `external` for older agents. An
// old-agent device login lands on Loopback (harmless paste box;
// the background poll still completes).
*current_mode = match mode.as_deref() {
Some("device") => AuthMode::Device,
Some("command") => AuthMode::Command,
Some("loopback") => AuthMode::Loopback,
_ if external => AuthMode::Command,
_ => AuthMode::Loopback,
};
}
vec![]
}
pub(super) fn handle_mcp_auth_trigger_done(
app: &mut AppView,
agent_id: AgentId,
server_name: String,
result: Result<(), String>,
) -> Vec<Effect> {
let Some(agent) = app.agents.get_mut(&agent_id) else {
return vec![];
};
if let Some(ref mut modal) = agent.extensions_modal {
modal.pending_action = None;
modal.pending_entry_index = None;
if let Err(e) = result {
// String-match heuristic: directive vs name-embedded vs generic.
// Brittle if the shell ever quotes a name shape that doesn't
// match `server_name` here — replace with a structured
// discriminator on McpAuthTriggerResponse if that happens.
let msg = if e.starts_with("To authenticate") {
format!("{server_name}: {e}")
} else if e.contains(&server_name) {
format!("Auth failed: {e}")
} else {
format!("{server_name} auth failed: {e}")
};
modal.modal_message = Some(crate::views::extensions_modal::ModalMessage::Error(msg));
return vec![];
}
}
// No toast on success: the row transition from the FetchMcpsList
// refresh below is the confirmation.
let Some(session_id) = agent.session.session_id.clone() else {
return vec![];
};
vec![Effect::FetchMcpsList {
agent_id,
session_id,
cache: false,
}]
}