Files
Kigi-CLI/crates/codegen/kigi-shell/src/session/user_message.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

200 lines
6.9 KiB
Rust

use std::path::Path;
use kigi_workspace::session::git::VcsKind;
// Re-export from kigi-chat-state — canonical definition lives there.
pub use kigi_chat_state::compaction_utils::extract_user_query;
/// Wraps the user query properly
pub fn user_query(user_message: String) -> String {
format!(
r#"<user_query>
{user_message}
</user_query>"#
)
}
/// Environment info for constructing the `<user_info>` block.
///
/// When `None`, values are read from the local machine. When `Some`,
/// the provided values are used (e.g. from a remote workspace via
/// `workspace.info` RPC).
pub struct UserInfoOverride {
pub os: String,
pub shell: String,
pub cwd: String,
}
/// Minimal user message prefix for fast-start / headless contexts.
///
/// Intentionally excludes workspace snapshot and git status.
/// When `override_info` is provided, uses remote workspace info instead
/// of local machine introspection.
pub fn construct_user_message_minimal(
working_directory: &Path,
override_info: Option<&UserInfoOverride>,
) -> String {
let local_shell;
let (os, shell, cwd) = match override_info {
Some(info) => (info.os.as_str(), info.shell.as_str(), info.cwd.clone()),
None => {
local_shell = resolve_shell_display();
(
std::env::consts::OS,
local_shell.as_str(),
working_directory.to_string_lossy().to_string(),
)
}
};
// Local-timezone date, captured when the prefix is built. Re-stamped on
// compaction and on resume (build_user_message_prefix), so it stays current
// across long sessions.
let today = chrono::Local::now().format("%Y-%m-%d");
format!(
r#"<user_info>
OS Version: {os}
Shell: {shell}
Workspace Path: {cwd}
Today's date: {today}
Note: Prefer using relative paths over absolute paths as tool call args when possible.
</user_info>"#,
)
}
/// Resolve a display string for the user's shell.
///
/// Unix: full path from `$SHELL` (e.g. `/bin/zsh`).
/// Windows: detected via `detect_windows_shell` cascade
/// (pwsh > powershell.exe > Git Bash > cmd.exe).
fn resolve_shell_display() -> String {
#[cfg(unix)]
{
std::env::var("SHELL").unwrap_or_else(|_| "/bin/sh".to_string())
}
#[cfg(not(unix))]
{
kigi_config::shell::detect_windows_shell()
.name()
.to_string()
}
}
pub fn format_vcs_status_block(status: &str, vcs_kind: VcsKind) -> String {
let (tag, description) = if vcs_kind.is_jj() {
(
"jj_status",
"This is the Jujutsu (jj) status at the start of the conversation. This is a \
jj-managed repository \u{2014} use `jj` commands instead of `git`. There is no staging \
area; all changes are part of the working-copy commit (@). Use `jj describe` to \
set commit messages and `jj new` to finalize changes.",
)
} else {
(
"git_status",
"This is the git status at the start of the conversation. Note that this status \
is a snapshot in time, and will not update during the conversation.",
)
};
format!("\n\n<{tag}>\n{description}\n{status}\n</{tag}>\n")
}
/// Compute the VCS status block (without the `<user_info>` wrapper).
pub async fn compute_vcs_status_block(
working_directory: &Path,
vcs_kind: VcsKind,
) -> Option<String> {
use kigi_workspace::file_system::{git_status, jj_status};
if matches!(vcs_kind, VcsKind::None) {
return None;
}
let _timer = crate::instrumentation_timer!("session.user_prefix.vcs_status");
let timeout = std::time::Duration::from_secs(2);
let result = if vcs_kind.is_jj() {
tokio::time::timeout(timeout, jj_status(working_directory)).await
} else {
tokio::time::timeout(timeout, git_status(working_directory)).await
};
match result {
Ok(Ok(status)) => Some(format_vcs_status_block(&status, vcs_kind)),
Ok(Err(e)) => {
tracing::warn!("user prefix VCS status failed: {e}");
None
}
Err(_) => {
tracing::warn!(vcs = ?vcs_kind, "user prefix VCS status timed out after 2s");
None
}
}
}
/// Full user message prefix: `<user_info>` + VCS status.
///
/// When `override_info` is provided, uses remote workspace info and
/// `vcs_status_override` (pre-fetched from the remote workspace) instead
/// of local introspection.
pub async fn construct_user_message(
working_directory: &Path,
vcs_kind: VcsKind,
override_info: Option<&UserInfoOverride>,
vcs_status_override: Option<String>,
) -> String {
let cwd = working_directory.to_string_lossy().to_string();
let vcs_block = if let Some(status) = vcs_status_override {
Some(format_vcs_status_block(&status, vcs_kind))
} else {
let (block, elapsed_ms) = crate::timed!({
let mut timer = crate::instrumentation_timer!("session.user_prefix");
timer.with_field("cwd", cwd.as_str());
compute_vcs_status_block(working_directory, vcs_kind).await
});
tracing::debug!(elapsed_ms = elapsed_ms as u64, "startup: user_prefix");
block
};
let mut user_info = construct_user_message_minimal(working_directory, override_info);
if let Some(vcs) = vcs_block {
user_info.push_str(&vcs);
}
user_info
}
// Tests for extract_user_query now live in kigi_chat_state::compaction_utils.
#[cfg(test)]
mod tests {
use super::*;
use kigi_workspace::file_system::FsError;
/// Verify that construct_user_message completes within the 2s git_status
/// timeout even when pointed at a non-existent directory (git commands
/// fail instantly → no timeout path exercised, but validates the happy
/// path doesn't regress).
#[tokio::test]
async fn construct_user_message_returns_without_git_status_on_bad_dir() {
let dir = std::path::Path::new("/tmp/nonexistent-kigi-test-dir");
let msg = construct_user_message(dir, VcsKind::Git, None, None).await;
// user_info block is always present
assert!(
msg.contains("<user_info>"),
"must contain user_info section"
);
// git_status block should be absent (git commands fail on non-repo)
assert!(
!msg.contains("<git_status>"),
"must not contain git_status for non-repo directory"
);
}
#[test]
fn git_status_error_omits_block() {
// Simulate what construct_user_message does when git_status returns Err
let git_status_res: Result<String, FsError> = Err(FsError::Other("timed out".into()));
let mut user_info = "<user_info>test</user_info>".to_string();
if let Ok(git_status) = git_status_res {
user_info = format!("{user_info}\n<git_status>{git_status}</git_status>");
}
assert!(!user_info.contains("<git_status>"));
}
}