Files
Kigi-CLI/crates/codegen/kigi-shell/src/agent/mvp_agent/code_nav.rs
T
ZacharyZhang-NY d6c20fc13f 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).
2026-07-17 05:31:01 -04:00

262 lines
11 KiB
Rust

//! Code-navigation eligibility gating and codebase-index management for [`MvpAgent`].
//! Co-located child of `mvp_agent` (`use super::*`).
use super::*;
impl MvpAgent {
/// Parse the `x.ai/codeNavigation.enabled` capability from an initialize
/// request. Returns `false` if the field is absent or not `true`.
pub(crate) fn parse_code_nav_capability(init: &acp::InitializeRequest) -> bool {
init.client_capabilities
.meta
.as_ref()
.and_then(|m| m.get("x.ai/codeNavigation"))
.and_then(|v| v.get("enabled"))
.and_then(|v| v.as_bool())
.unwrap_or(false)
}
/// Start (or reuse) the codebase index for an eligible code-nav request.
///
/// Returns `Some((handle, was_newly_started))` on success or `None` when
/// config/git-root checks prevent starting. The bool is the authoritative
/// "first spawn vs reuse" signal threaded up from `CodebaseIndexManager`.
///
/// This is the narrow `pub(crate)` entry point for lazy index startup
/// from `extensions/code_nav.rs`. Callers must verify eligibility with
/// [`code_nav_eligibility_for_request`] before calling this.
pub(crate) fn start_codebase_index_for_code_nav(
&self,
session_id: Option<&acp::SessionId>,
cwd: &std::path::Path,
) -> Option<(std::sync::Arc<kigi_codebase_graph::IndexManagerHandle>, bool)> {
let (handle, was_newly_started) = self.resolve_codebase_index(cwd)?;
// Pin the index to the requesting session so the Weak in
// CodebaseIndexManager doesn't orphan it immediately.
if let Some(sid) = session_id {
self.session_index_claims
.borrow_mut()
.insert(sid.clone(), std::sync::Arc::clone(&handle));
}
Some((handle, was_newly_started))
}
/// Core eligibility check — pure function that accepts explicit client
/// context rather than reading global agent state.
///
/// This is the single place that applies all four gates. Call it via
/// [`code_nav_eligibility_for_request`] (leader-mode safe) or
/// [`code_nav_eligibility`] (global state, non-leader use only).
pub(super) fn code_nav_eligibility_inner(
&self,
cwd: &std::path::Path,
client_type: ClientType,
code_nav_enabled: bool,
) -> Result<(), CodeNavEligibility> {
use crate::agent::config::CodebaseIndexingSetting;
// Gate 1: client type
if !matches!(client_type, ClientType::GrokWeb) {
tracing::info!(
client_type = ?client_type,
gate = "client_type",
skip_reason = "client_not_web",
"code-nav eligibility check: skipping (client type not eligible)"
);
return Err(CodeNavEligibility::ClientNotWeb);
}
// Gate 2: capability advertised
if !code_nav_enabled {
tracing::info!(
gate = "capability",
skip_reason = "capability_not_advertised",
"code-nav eligibility check: skipping (x.ai/codeNavigation.enabled not advertised)"
);
return Err(CodeNavEligibility::CapabilityNotAdvertised);
}
// Gate 3: config
let setting = self.cfg.borrow().features.codebase_indexing.clone();
if let CodebaseIndexingSetting::Enabled(false) = &setting {
tracing::info!(
gate = "config",
skip_reason = "disabled_by_config",
"code-nav eligibility check: skipping (codebase_indexing disabled in config)"
);
return Err(CodeNavEligibility::DisabledByConfig);
}
// Gate 4: git root / config globs
let git_root = kigi_workspace::session::git::find_git_root_from_path(cwd).ok();
match &setting {
CodebaseIndexingSetting::Enabled(true) => {
if git_root.is_none() {
tracing::info!(
cwd = %cwd.display(),
gate = "git_root",
skip_reason = "not_git_repo",
"code-nav eligibility check: skipping (not inside a git repo)"
);
return Err(CodeNavEligibility::NotGitRepo);
}
}
CodebaseIndexingSetting::Patterns(_) => {
let check_path = git_root.as_deref().unwrap_or(cwd);
if !setting.should_index(check_path) {
tracing::info!(
cwd = %cwd.display(),
gate = "config_globs",
skip_reason = "disabled_by_config",
"code-nav eligibility check: skipping (not matched by config globs)"
);
return Err(CodeNavEligibility::DisabledByConfig);
}
}
CodebaseIndexingSetting::Enabled(false) => {} // handled above
}
Ok(())
}
/// Check eligibility using per-session context (leader-mode safe).
///
/// When `session_id` is provided, reads the session's own client type
/// and code-nav capability — the values that were in effect when that
/// specific client created the session. This is correct in leader mode
/// where multiple clients share one agent process and `initialize()` is
/// called once per connection; the global fields on `MvpAgent` reflect
/// only the **last** client to call `initialize()`.
///
/// Falls back to global agent state when no session_id is given.
pub fn code_nav_eligibility_for_request(
&self,
session_id: Option<&acp::SessionId>,
cwd: &std::path::Path,
) -> Result<(), CodeNavEligibility> {
let session_id = match session_id {
Some(sid) => sid,
// No session_id: per-client capability cannot be determined without a
// session. Reject with SessionRequired rather than fall back to shared
// global state. Callers must provide sessionId for x.ai/code/* requests.
None => return Err(CodeNavEligibility::SessionRequired),
};
let sessions = self.sessions.borrow();
let (client_type, code_nav_enabled) = if let Some(handle) = sessions.get(session_id) {
let ct = crate::http::client_type_from_origin(handle.origin_client.as_ref());
(ct, handle.code_nav_enabled)
} else {
// Session not found (evicted/unknown): reject rather than silently
// falling back to shared global state — that would reintroduce the
// last-client-wins bug for stale session IDs in leader mode.
return Err(CodeNavEligibility::SessionRequired);
};
drop(sessions);
self.code_nav_eligibility_inner(cwd, client_type, code_nav_enabled)
}
/// Check eligibility using the stored initialize_request context.
///
/// **Not safe in leader mode** — reads the last `initialize()` call's
/// client_type and capability. Prefer [`code_nav_eligibility_for_request`]
/// when a session_id is available.
pub fn code_nav_eligibility(&self, cwd: &std::path::Path) -> Result<(), CodeNavEligibility> {
let client_type = *self.client_type.borrow();
let code_nav_enabled = self.code_nav_enabled.get();
self.code_nav_eligibility_inner(cwd, client_type, code_nav_enabled)
}
/// Resolve and get-or-create the codebase index for `cwd`, applying config
/// and git-root eligibility checks.
///
/// Returns `Some((handle, was_newly_started))` when an index is available,
/// `None` when config or git-root checks rule it out. The bool is the
/// authoritative "was this a first spawn?" signal from the manager.
pub(super) fn resolve_codebase_index(
&self,
cwd: &std::path::Path,
) -> Option<(std::sync::Arc<kigi_codebase_graph::IndexManagerHandle>, bool)> {
use crate::agent::config::CodebaseIndexingSetting;
let setting = self.cfg.borrow().features.codebase_indexing.clone();
let git_root = kigi_workspace::session::git::find_git_root_from_path(cwd).ok();
match (&setting, &git_root) {
(CodebaseIndexingSetting::Enabled(false), _) => {
tracing::info!(
cwd = %cwd.display(),
skip_reason = "disabled_by_config",
"code-nav: skipping index creation (disabled in config)"
);
return None;
}
(CodebaseIndexingSetting::Enabled(true), None) => {
tracing::info!(
cwd = %cwd.display(),
skip_reason = "not_git_repo",
"code-nav: skipping index creation (not inside a git repo)"
);
return None;
}
(CodebaseIndexingSetting::Patterns(_), _) => {
let check_path = git_root.as_deref().unwrap_or(cwd);
if !setting.should_index(check_path) {
tracing::info!(
cwd = %cwd.display(),
skip_reason = "disabled_by_config",
"code-nav: skipping index creation (not matched by config globs)"
);
return None;
}
}
(CodebaseIndexingSetting::Enabled(true), Some(_)) => {}
}
let target = git_root.unwrap_or_else(|| cwd.to_path_buf());
// get_or_create returns the authoritative (handle, was_newly_started) pair.
// Log only on actual first spawn so reuse requests are not misleadingly
// labelled as "starting".
let (handle, was_newly_started) = self.get_or_create_codebase_index(target.clone());
if was_newly_started {
tracing::info!(
cwd = %cwd.display(),
index_target = %target.display(),
event = "index_first_spawn",
"code-nav: first lazy spawn of codebase index"
);
}
Some((handle, was_newly_started))
}
pub(super) fn indexed_roots_for(&self, cwd: &std::path::Path) -> Vec<String> {
if self.get_codebase_index(cwd).is_some() {
return vec![cwd.to_string_lossy().into_owned()];
}
if let Ok(git_root) = kigi_workspace::session::git::find_git_root_from_path(cwd)
&& self.get_codebase_index(&git_root).is_some()
{
return vec![git_root.to_string_lossy().into_owned()];
}
Vec::new()
}
/// Returns `(handle, was_newly_started)` — the bool is the authoritative
/// "did this call spawn a new actor?" bit from `CodebaseIndexManager::get_or_create`.
pub(super) fn get_or_create_codebase_index(
&self,
cwd: PathBuf,
) -> (std::sync::Arc<kigi_codebase_graph::IndexManagerHandle>, bool) {
self.codebase_indexes.lock().get_or_create(cwd)
}
/// Get an existing codebase index for the given cwd.
/// Returns None if no index exists for this cwd.
pub fn get_codebase_index(
&self,
cwd: &std::path::Path,
) -> Option<std::sync::Arc<kigi_codebase_graph::IndexManagerHandle>> {
self.codebase_indexes.lock().get(cwd)
}
}