Files
Kigi-CLI/crates/codegen/kigi-tui/src/project_picker/mod.rs
T
ZacharyZhang-NY 6f31415ed6 §9 acceptance: grep-zero sweep — every internal x.ai/grok identifier renamed
The PRD's first acceptance gate now holds: grep -RinE '\bx\.ai\b|grok'
crates/ --include='*.rs' → 0 matches (exempt: NOTICE and third-party
license archives, README provenance, and the required 'Based on Grok
Build Open Source' attribution, now sourced from version_attribution.txt).

Wire-visible renames (both sides in this repo, changed in lockstep):
- Auth method id 'grok.com' → 'kimi-code' (AuthMethodKind::KimiCode).
- Every x.ai/* and _x.ai/* ACP ext method and meta key → kigi/* /
  _kigi/* (~200 names; grokShell → kigiShell). Session-file replay keeps
  a read-side alias for the legacy '_x.ai/session/update' method so
  existing updates.jsonl histories load; writes emit only the new name
  (both directions test-pinned).
- Agent types grok-build* → kigi* with a documented legacy-prefix alias
  at resolution time so persisted sessions keep resolving.
- ToolNamespace/BuiltinAgentName GrokBuild* → Kigi* (wire snake_case
  kigi/kigi_concise/kigi_hashline; schema regenerated); grok_build
  implementation dirs renamed to kigi*.
- x-grok-* headers → x-kigi-*, __GROK_* sentinels → __KIGI_*, themes
  grokday/groknight → kigiday/kiginight (old persisted values fall back
  to the default theme), web_fetch allowlist xAI hosts → kimi.com +
  moonshot platforms, changelog CDN → this repo, grok-build changelog
  archives deleted.
- BYOK default endpoint removed: [endpoints] api_base_url is now truly
  optional with NO default — consumers fail fast with the flag name when
  unset (no silent x.ai egress). Mock harnesses inject it explicitly.
- System-prompt identity fixed: 'released by xAI' → 'an unofficial
  community CLI for Kimi' (template + regenerated encrypted form).

Also repaired pre-existing grok-era test debt found by the sweep: the
stale trace_classify default-model pin, the grok-pager UA label test,
pty-harness stale-binary reuse and non-hermetic moonshot routing (a PTY
test could previously reach the real api.moonshot.cn), and the outdated
oauth fixture scope key.

Gates: §9 grep 0; fmt clean; workspace check/clippy 0/0 (-D warnings);
FULL cargo test --workspace: 234 suites, 21,961 passed, 0 failed;
deny advisories ok.
2026-07-18 02:48:46 -04:00

132 lines
4.3 KiB
Rust

//! Project picker: select a project directory on first prompt from a non-project dir.
pub mod detection {
pub use kigi_file_utils::workspace_classifier::is_project_dir;
}
pub mod sources;
use std::path::{Path, PathBuf};
use kigi_tools::implementations::kigi::ask_user_question::{Question, QuestionOption};
/// `resolved_paths` is index-aligned with the leading `question.options`.
/// The trailing "Don't ask me again" option at `dont_ask_index` has no
/// corresponding path (selecting it continues in the current directory).
pub struct ProjectQuestion {
pub question: Question,
pub resolved_paths: Vec<PathBuf>,
/// Option index of the "Don't ask me again" entry.
pub dont_ask_index: usize,
}
const MAX_RECENT_DIRS: usize = 5;
pub fn build_project_question(
recent_dirs: &[(PathBuf, chrono::DateTime<chrono::Utc>)],
cwd: &Path,
) -> ProjectQuestion {
let mut options = Vec::new();
let mut resolved_paths = Vec::new();
// First option: continue in the current directory.
let is_home = dirs::home_dir().is_some_and(|h| h == cwd);
let cwd_name = if is_home {
"~"
} else {
cwd.file_name()
.and_then(|n| n.to_str())
.unwrap_or("current directory")
};
options.push(QuestionOption {
label: format!("{cwd_name} (current)"),
description: sources::display_path(cwd),
preview: None,
id: None,
});
resolved_paths.push(cwd.to_path_buf());
// Recent project directories.
for (path, ts) in recent_dirs
.iter()
.filter(|(p, _)| p != cwd)
.take(MAX_RECENT_DIRS)
{
let raw_name = path.file_name().and_then(|n| n.to_str()).unwrap_or("?");
let name = crate::render::line_utils::truncate_str(raw_name, 22);
options.push(QuestionOption {
label: name,
description: format!(
"{} ({})",
sources::display_path(path),
crate::views::session_title::format_relative_time(
(chrono::Utc::now() - *ts).to_std().unwrap_or_default()
),
),
preview: None,
id: None,
});
resolved_paths.push(path.clone());
}
// Kept out of `resolved_paths` so the path options stay index-aligned.
let dont_ask_index = options.len();
options.push(QuestionOption {
label: "Don't ask me again".to_string(),
description: "Always start in the current directory (reset in config.toml)".to_string(),
preview: None,
id: None,
});
ProjectQuestion {
question: Question {
question: "Run Kigi in a project directory?\n\n\
This gives Kigi full context of your codebase for better results."
.into(),
id: None,
options,
multi_select: Some(false),
},
resolved_paths,
dont_ask_index,
}
}
#[cfg(test)]
mod tests {
use super::*;
use chrono::Utc;
#[test]
fn no_recent_dirs_returns_only_cwd() {
let pq = build_project_question(&[], Path::new("/home/user"));
assert_eq!(pq.resolved_paths.len(), 1);
assert_eq!(pq.resolved_paths[0], PathBuf::from("/home/user"));
}
#[test]
fn recent_dirs_index_aligned_with_options() {
let now = Utc::now();
let recent = vec![
(PathBuf::from("/projects/alpha"), now),
(PathBuf::from("/projects/beta"), now),
];
let pq = build_project_question(&recent, Path::new("/home/user"));
// Options carry one extra trailing "Don't ask me again" entry beyond
// the index-aligned path options.
assert_eq!(pq.question.options.len(), pq.resolved_paths.len() + 1);
assert_eq!(pq.resolved_paths[0], PathBuf::from("/home/user"));
assert_eq!(pq.resolved_paths[1], PathBuf::from("/projects/alpha"));
assert_eq!(pq.resolved_paths[2], PathBuf::from("/projects/beta"));
}
#[test]
fn dont_ask_option_is_last_and_excluded_from_paths() {
let pq = build_project_question(&[], Path::new("/home/user"));
assert_eq!(pq.dont_ask_index, pq.resolved_paths.len());
assert_eq!(pq.dont_ask_index, pq.question.options.len() - 1);
assert_eq!(
pq.question.options[pq.dont_ask_index].label,
"Don't ask me again"
);
}
}