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.
132 lines
4.3 KiB
Rust
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"
|
|
);
|
|
}
|
|
}
|