docs(comments): rewrite comments across all crates to the guidelines

Sweep every first-party crate source (1956 .rs files) to the project comment
guidelines: delete redundant restatements, decorative banners, change
narration, and end-of-line comments; keep and tighten the crucial ones
(invariants, bug rationale, SAFETY blocks, ported-source attribution).

No functional code changed. Every edit is proven comment-only against the
prior tree by a comment-stripping lexer (string/char/raw-string aware) plus a
separate doctest-fence check. Where removing a comment made rustfmt or clippy
want to re-lay-out adjacent code, the minimal triggering comment is restored so
code tokens stay byte-identical.

Gates green: cargo fmt --all --check (0 diffs), cargo check and cargo clippy
--workspace --all-targets (0 warnings).

Adds scripts/check_codegen_comment_guidelines.py — the enforcement gate for
these guidelines (flags banners, end-of-line comments, change narration, and
commented-out code).
This commit is contained in:
ZacharyZhang-NY committed 2026-07-23 16:55:39 -04:00
1 parent ff0fb56c67
commit a02b555e66
1458 files changed
+10729 -21750

No files matched your search

@@ -28,7 +28,7 @@ static DISMISS_TMP_NONCE: AtomicU64 = AtomicU64::new(0);
static REMOTE_CAMPAIGN_CACHE: RwLock<Vec<CampaignEntry>> = RwLock::new(Vec::new());
/// Seed the process-global remote campaign cache. A `None` settings value (e.g.
/// a failed fetch) is a no-op so it can't clobber a previously-seeded cache;
/// a failed fetch) is a no-op so it can't clobber an already-seeded cache;
/// `Some` with zero campaigns legitimately clears it (campaigns withdrawn).
pub fn set_remote_campaigns_from_settings(remote: Option<&RemoteSettings>) {
let Some(remote) = remote else {
@@ -74,8 +74,9 @@ fn dismiss_campaign_ids_at(
let _guard = DISMISS_LOCK.lock().unwrap_or_else(|p| p.into_inner());
let path = campaigns_state_path(home);
// Cross-process advisory lock over the read-modify-write: in leader mode
// several kigi processes share `$KIGI_SHARE_DIR`; the in-process mutex alone would
// let them lose-update the set. Best-effort; a lock failure still proceeds.
// several kigi processes share `$KIGI_SHARE_DIR`, and the in-process mutex
// alone would allow them to lose-update the set. Best-effort; a lock
// failure still proceeds.
let lock = std::fs::OpenOptions::new()
.create(true)
.truncate(false)
@@ -327,7 +328,7 @@ fn apply_campaign_fields(
pub fn sync_campaign_fields(cfg: &mut crate::agent::config::Config) {
let remote = remote_campaigns_from_settings(cfg.remote_settings.as_ref());
// Seed the process-global cache from the parse we already did (skip on `None`
// so a failed fetch can't clobber a previously-seeded cache).
// so a failed fetch can't clobber an already-seeded cache).
if cfg.remote_settings.is_some() {
set_remote_campaigns(remote.clone());
}
@@ -10,16 +10,16 @@ use toml::Value as TomlValue;
use toml::map::Map as TomlMap;
pub use kigi_mcp::oauth_config::{McpOAuthConfig, McpOAuthConfigMap};
// MCP server config value types extracted to `kigi-config-types` (config
// MCP server config value types live in `kigi-config-types` (config
// dependency inversion); re-exported so `crate::util::config::*` paths keep working.
pub use kigi_config_types::{McpJsonOAuthBlock, McpServerConfig, McpServerTransportConfig};
// Permission-policy value types likewise extracted; re-exported to keep paths stable.
// Permission-policy value types likewise live there; re-exported to keep paths stable.
pub use kigi_config_types::{
PatternMode, PermissionConfig, PermissionRule, RuleAction, ToolFilter,
};
// Relay-sync + MCP-config value types extracted; re-exported to keep paths stable.
// Relay-sync + MCP-config value types live there too; re-exported to keep paths stable.
pub use kigi_config_types::McpConfig;
// Worktree-pool config value type extracted; re-exported to keep paths stable.
// Worktree-pool config value type lives there too; re-exported to keep paths stable.
pub use kigi_config_types::PoolConfig;
/// TUI/CLI settings. Composed from typed section configs defined in `agent::config`.
@@ -62,7 +62,6 @@ pub fn get_mcp_server_config_with_project(
name: &str,
cwd: &std::path::Path,
) -> Option<McpServerConfig> {
// Check project-scoped configs from cwd (highest priority) to repo root
let project_configs = crate::config::find_project_configs(cwd);
for config_path in project_configs.iter().rev() {
if let Ok(root) = crate::config::load_config_file(config_path) {
@@ -73,7 +72,6 @@ pub fn get_mcp_server_config_with_project(
}
}
// Fall back to global config
get_mcp_server_config(name)
}
@@ -167,7 +165,6 @@ pub fn worktree_pool_from_toml(root: &TomlValue) -> PoolConfig {
if let TomlValue::Table(table) = root
&& let Some(pool_val) = table.get("worktree_pool")
{
// Try to deserialize the section; fall back to defaults on error
pool_val
.clone()
.try_into::<PoolConfig>()
@@ -635,9 +632,7 @@ fn parse_mcp_servers_from_toml(root: &TomlValue) -> IndexMap<String, McpServerCo
result
}
// ── .mcp.json support ────────────────────────────────────────────────
// `.mcp.json` discovery moved to `kigi-workspace` (client-side, shared with
// `.mcp.json` discovery lives in `kigi-workspace` (client-side, shared with
// the folder-trust gate); re-exported so `crate::util::config::*` paths keep working.
pub use kigi_workspace::project_config::{
MCP_JSON_FILENAME, find_mcp_json_files, mcp_json_candidate_paths,
@@ -666,7 +661,7 @@ pub(crate) fn load_mcp_json_servers_as_configs(
/// Like [`load_mcp_json_servers_as_configs`] but bypasses the import-marker
/// gate. Used by the `/import-claude` scanner so users can re-import items
/// they previously skipped, even after the runtime cutoff is active.
/// they had skipped, even after the runtime cutoff is active.
pub fn load_mcp_json_servers_as_configs_unfiltered(
cwd: &std::path::Path,
) -> IndexMap<String, McpServerConfig> {
@@ -777,7 +772,7 @@ pub(crate) fn load_claude_json_mcp_servers_as_configs(
/// Like [`load_claude_json_mcp_servers_as_configs`] but bypasses the
/// import-marker gate. Used by the `/import-claude` scanner so users can
/// re-import items they previously skipped, even after the runtime cutoff
/// re-import items they had skipped, even after the runtime cutoff
/// is active.
pub fn load_claude_json_mcp_servers_as_configs_unfiltered(
cwd: &std::path::Path,
@@ -986,7 +981,6 @@ fn load_claude_json_mcp_servers_from(
servers
}
/// Read and parse a JSON file. Returns `None` on I/O or parse errors (logged).
/// Env var carrying `--mcp-config-file` paths across the TUI → shell
/// process boundary (joined with the platform path separator). kimi-cli
/// parity (F6): each file uses the `.mcp.json` shape
@@ -1013,6 +1007,7 @@ pub(crate) fn load_cli_mcp_config_files_as_configs() -> IndexMap<String, McpServ
result
}
/// Read and parse a JSON file. Returns `None` on I/O or parse errors (logged).
pub(crate) fn read_mcp_json(path: &std::path::Path) -> Option<McpConfig> {
let content = std::fs::read_to_string(path)
.map_err(|e| {
@@ -1026,7 +1021,7 @@ pub(crate) fn read_mcp_json(path: &std::path::Path) -> Option<McpConfig> {
.ok()
}
/// Like `load_mcp_servers_with_project` but returns raw configs without filtering by `enabled`.
/// Like [`load_mcp_server_configs_with_project`] but returns raw configs without filtering by `enabled`.
fn load_all_mcp_configs(cwd: &std::path::Path) -> IndexMap<String, McpServerConfig> {
load_mcp_server_configs_with_project(cwd)
.into_iter()
@@ -1273,7 +1268,6 @@ mod tests {
));
}
/// Covers all canonical wire values plus the unknown/corrupt fallback.
#[test]
fn test_parse_mcp_servers_empty() {
let root = toml::from_str::<TomlValue>("").unwrap();
@@ -1305,7 +1299,6 @@ args = ["server.js"]
#[test]
fn test_use_leader_parsing_true() {
// Test that we can parse a config with use_leader = true
let toml_str = r#"
[cli]
use_leader = true
@@ -1316,7 +1309,6 @@ use_leader = true
#[test]
fn test_use_leader_parsing_false() {
// Test that we can parse a config with use_leader = false
let toml_str = r#"
[cli]
use_leader = false
@@ -1327,7 +1319,6 @@ use_leader = false
#[test]
fn test_use_leader_default_false() {
// Test that missing use_leader defaults to false
let toml_str = r#"
[cli]
auto_update = true
@@ -1338,7 +1329,6 @@ auto_update = true
#[test]
fn test_use_leader_no_cli_section() {
// Test with no cli section at all
let toml_str = r#"
[models]
default = "kigi-code-fast-1"
@@ -1348,7 +1338,6 @@ default = "kigi-code-fast-1"
let has_cli = table.get("cli").is_some();
assert!(!has_cli);
}
// use_leader_from_toml() should default to false when no cli section
assert!(!use_leader_from_toml(&root));
}
@@ -1392,10 +1381,8 @@ default = "kigi-code-fast-1"
assert_eq!(use_leader_from_toml_opt(&root), None);
}
// WorktreeType tests
#[test]
fn test_project_scoped_mcp_override_replaces_entirely() {
// Simulate global config with timeouts
let global_toml = r#"
[mcp_servers.linear]
command = "npx"
@@ -1407,7 +1394,6 @@ tool_timeout_sec = 60
let global_root = toml::from_str::<TomlValue>(global_toml).unwrap();
let global_servers = parse_mcp_servers_from_toml(&global_root);
// Simulate project config WITHOUT timeouts
let project_toml = r#"
[mcp_servers.linear]
command = "npx"
@@ -1417,17 +1403,14 @@ enabled = true
let project_root = toml::from_str::<TomlValue>(project_toml).unwrap();
let project_servers = parse_mcp_servers_from_toml(&project_root);
// Global config should have timeouts
let global_linear = global_servers.get("linear").unwrap();
assert_eq!(global_linear.startup_timeout_sec, Some(10));
assert_eq!(global_linear.tool_timeout_sec, Some(60));
// Project config should NOT have timeouts (defaults apply)
let project_linear = project_servers.get("linear").unwrap();
assert_eq!(project_linear.startup_timeout_sec, None);
assert_eq!(project_linear.tool_timeout_sec, None);
// Merge: project overrides global entirely
let mut merged: IndexMap<String, McpServerConfig> = IndexMap::new();
for (name, config) in &global_servers {
merged.insert(name.clone(), config.clone());
@@ -1436,7 +1419,6 @@ enabled = true
merged.insert(name.clone(), config.clone());
}
// After merge, the project config should have replaced the global one entirely
let merged_linear = merged.get("linear").unwrap();
assert_eq!(merged_linear.startup_timeout_sec, None);
assert_eq!(merged_linear.tool_timeout_sec, None);
@@ -1462,7 +1444,6 @@ enabled = true
let project_root = toml::from_str::<TomlValue>(project_toml).unwrap();
let project_servers = parse_mcp_servers_from_toml(&project_root);
// Merge: project adds new server
let mut merged: IndexMap<String, McpServerConfig> = IndexMap::new();
for (name, config) in &global_servers {
merged.insert(name.clone(), config.clone());
@@ -1488,7 +1469,6 @@ enabled = true
let global_servers = parse_mcp_servers_from_toml(&global_root);
assert!(global_servers.get("linear").unwrap().enabled);
// Project config disables the server
let project_toml = r#"
[mcp_servers.linear]
command = "npx"
@@ -1506,7 +1486,6 @@ enabled = false
merged.insert(name.clone(), config.clone());
}
// After merge, the server should be disabled by project config
assert!(!merged.get("linear").unwrap().enabled);
}
@@ -1557,7 +1536,6 @@ enabled = true
let global_root = toml::from_str::<TomlValue>(global_toml).unwrap();
let global_servers = parse_mcp_servers_from_toml(&global_root);
// Project only overrides linear
let project_toml = r#"
[mcp_servers.linear]
command = "npx"
@@ -1575,7 +1553,6 @@ enabled = false
merged.insert(name.clone(), config.clone());
}
// buildkite should be preserved from global config
assert_eq!(merged.len(), 2);
assert!(merged.get("buildkite").unwrap().enabled);
assert!(!merged.get("linear").unwrap().enabled);
@@ -1752,6 +1729,4 @@ enabled = false
]
);
}
// === merge_section tests ===
}
@@ -20,7 +20,7 @@ pub use load::*;
pub use mcp::*;
pub use permissions::*;
pub use persist::*;
// `remote` extracted to the `kigi-config-types` crate (dependency inversion);
// `remote` lives in the `kigi-config-types` crate (dependency inversion);
// re-exported so `crate::util::config::{RemoteSettings, GoalRoleModel}` keep working.
pub use kigi_config_types::{
CampaignOverride, ContextualHintsRemote, DisplayRefreshSettings, DoomLoopRecoverySettings,
@@ -267,7 +267,6 @@ pub fn load_require_plan_approval() -> bool {
}
/// Synchronously load the remote agent secret from the config file.
/// Looks for [remote] section with secret field.
///
/// Example config.toml:
/// ```toml
@@ -485,7 +484,7 @@ mod tests {
// Table-driven: (cli_yolo, cli_perm_mode, config_yolo, expected_yolo, description)
let cases: &[(bool, Option<&str>, bool, bool, &str)] = &[
// --- CLI --permission-mode present: it wins completely ---
// CLI --permission-mode present: it wins completely.
(
false,
Some("plan"),
@@ -558,7 +557,7 @@ mod tests {
true,
"bypass + --yolo still yolo",
),
// --- No --permission-mode: fall back to legacy --yolo then config ---
// No --permission-mode: fall back to legacy --yolo then config.
(true, None, false, true, "--yolo alone → yolo"),
(true, None, true, true, "--yolo + config yolo → yolo"),
(false, None, true, true, "no cli flags + config yolo → yolo"),
@@ -173,10 +173,6 @@ fn merge_ask_user_question_section(
}
}
/// Merge serialized fields of `value` into `table[key]`, preserving any
/// existing keys not present in the serialized output. This prevents
/// unmodeled fields (e.g. pager-written `show_timestamps`, `auto_dark_theme`)
/// from being silently dropped when `save_config` round-trips the struct.
/// Deep-merge `incoming` into `existing`: nested tables recurse; scalars replace.
fn merge_toml_tables(
existing: &mut TomlMap<String, TomlValue>,
@@ -194,6 +190,10 @@ fn merge_toml_tables(
}
}
/// Merge serialized fields of `value` into `table[key]`, preserving any
/// existing keys not present in the serialized output. This prevents
/// unmodeled fields (e.g. pager-written `show_timestamps`, `auto_dark_theme`)
/// from being silently dropped when `save_config` round-trips the struct.
fn merge_section<T: serde::Serialize>(
table: &mut TomlMap<String, TomlValue>,
key: &str,
@@ -344,8 +344,6 @@ mod tests {
assert!(!oauth.contains_key("plain"));
}
// -- Cursor MCP loading --
#[test]
fn merge_section_preserves_unmodeled_fields() {
let mut table = TomlMap::new();
@@ -479,20 +477,13 @@ mod tests {
#[test]
fn merge_section_session_default_does_not_leak_load_envrc() {
let mut table = TomlMap::new();
// The starting state: user has no [session] section on disk
// (managed config might set load_envrc = false; user TOML is
// silent on the matter).
assert!(table.get("session").is_none());
// Pager calls update_config to set a totally unrelated [ui]
// field. The closure exits with cfg.session ==
// SessionConfig::default() (both fields None). save_config
// then calls merge_section("session", &cfg.session).
let cfg = crate::agent::config::SessionConfig::default();
merge_section(&mut table, "session", &cfg);
// After the fix, [session] is either absent OR present-but-
// empty. Crucially, it must NOT contain `load_envrc`.
// [session] must be either absent or present-but-empty; it must
// NOT contain `load_envrc`.
if let Some(session) = table.get("session").and_then(|v| v.as_table()) {
assert!(
session.get("load_envrc").is_none(),
@@ -506,7 +497,6 @@ mod tests {
"default auto_compact_threshold_percent must not be serialized either"
);
}
// If the table is wholly absent or empty, that's also fine.
}
/// Companion to the above: when the user explicitly commits a
@@ -522,9 +512,7 @@ mod tests {
session.insert("load_envrc".into(), TomlValue::Boolean(false));
table.insert("session".into(), TomlValue::Table(session));
// User commits auto_compact_threshold_percent via the modal.
// The cfg.session has load_envrc: None (user never touched it)
// and auto_compact_threshold_percent: Some(70).
// load_envrc: None means the user never touched it.
let cfg = crate::agent::config::SessionConfig {
auto_compact_threshold_percent: Some(70),
load_envrc: None,
@@ -532,7 +520,6 @@ mod tests {
merge_section(&mut table, "session", &cfg);
let session = table.get("session").unwrap().as_table().unwrap();
// The user's commit landed.
assert_eq!(
session
.get("auto_compact_threshold_percent")
@@ -571,8 +558,6 @@ mod tests {
"explicit load_envrc = false on disk must load as Some(false), not None"
);
// Now round-trip through save: merge into a fresh table and
// verify load_envrc = false stays present.
let mut table = TomlMap::new();
merge_section(&mut table, "session", &cfg.session);
let session = table.get("session").unwrap().as_table().unwrap();
@@ -646,12 +631,10 @@ auto_light_theme = "kigiday"
#[test]
fn ui_config_hunk_tracker_mode_round_trips() {
// Parse from `[ui].hunk_tracker_mode`...
let root: TomlValue = toml::from_str("[ui]\nhunk_tracker_mode = \"off\"\n").unwrap();
let cfg = load_config_from_toml(&root);
assert_eq!(cfg.ui.hunk_tracker_mode.as_deref(), Some("off"));
// ...and serialize back through merge_section.
let mut table = root.as_table().unwrap().clone();
merge_section(&mut table, "ui", &cfg.ui);
let ui = table.get("ui").unwrap().as_table().unwrap();
@@ -739,7 +722,6 @@ auto_update = true
// User changes default model (unrelated to UI)
cfg.models.default = Some("kigi-4".to_string());
// Simulate save_config
let mut table = root.as_table().unwrap().clone();
merge_section(&mut table, "cli", &cfg.cli);
merge_section(&mut table, "models", &cfg.models);
@@ -779,7 +761,6 @@ auto_update = true
ui.insert("compact_mode".into(), TomlValue::Boolean(true));
table.insert("ui".into(), TomlValue::Table(ui));
// Revert both to false (their defaults)
let cfg = crate::agent::config::UiConfig::default();
merge_section(&mut table, "ui", &cfg);
@@ -973,8 +954,6 @@ auto_update = true
assert_eq!(cfg2.models.default.as_deref(), Some("kigi-persisted"));
}
// ── merge_section pin tests for CLI/session setters ──────────────────
//
// Pin the schema-level write shape: each setter writes to the correct
// TOML section, and `None` fields don't serialize (skip_serializing_if).
@@ -1095,7 +1074,7 @@ auto_update = true
);
}
// ── resolve_auto_compact_threshold_percent: precedence matrix ──────────
// resolve_auto_compact_threshold_percent: precedence matrix.
//
// Covers every boundary in the resolver chain:
// env > user [model.<id>] > user [session] > GB per-model > GB global > 85
@@ -1194,8 +1173,6 @@ auto_update = true
}
}
// ── Tier 6: default (all unset) ─────────────────────────────────
#[test]
fn all_unset_returns_default_85() {
let _g = EnvVarGuard::unset();
@@ -1213,8 +1190,6 @@ auto_update = true
);
}
// ── Tier 5: GB global ───────────────────────────────────────────
#[test]
fn gb_global_only() {
let _g = EnvVarGuard::unset();
@@ -1222,8 +1197,6 @@ auto_update = true
assert_eq!(resolve(&cfg, None), 40);
}
// ── Tier 4 > Tier 5: GB per-model beats GB global ───────────────
#[test]
fn gb_per_model_beats_gb_global() {
let _g = EnvVarGuard::unset();
@@ -1231,8 +1204,6 @@ auto_update = true
assert_eq!(resolve(&cfg, Some(90)), 90);
}
// ── Tier 3 > Tier 4: user global beats GB per-model ─────────────
#[test]
fn user_session_beats_gb_per_model() {
let _g = EnvVarGuard::unset();
@@ -1247,8 +1218,6 @@ auto_update = true
assert_eq!(resolve(&cfg, None), 75);
}
// ── Tier 2 > Tier 3: user per-model beats user global ───────────
#[test]
fn user_per_model_beats_user_session() {
let _g = EnvVarGuard::unset();
@@ -1277,8 +1246,6 @@ auto_update = true
assert_eq!(resolve(&cfg, Some(90)), 70);
}
// ── Tier 1: env wins over everything ────────────────────────────
#[test]
fn env_beats_user_per_model() {
let _g = EnvVarGuard::set("50");
@@ -1300,8 +1267,6 @@ auto_update = true
assert_eq!(resolve(&cfg, None), 100);
}
// ── Env-tier failure modes fall through ─────────────────────────
#[test]
fn env_out_of_range_high_falls_through() {
let _g = EnvVarGuard::set("101");
@@ -1330,8 +1295,6 @@ auto_update = true
assert_eq!(resolve(&cfg, None), DEFAULT_AUTO_COMPACT_THRESHOLD_PERCENT);
}
// ── Per-model entry for a DIFFERENT model must not match ────────
#[test]
fn user_per_model_for_other_model_does_not_match() {
let _g = EnvVarGuard::unset();
@@ -1362,8 +1325,6 @@ auto_update = true
assert_eq!(resolve(&cfg, None), 75);
}
// ── ModelInfo-less call still walks the rest of the chain ───────
#[test]
fn missing_model_info_falls_through_to_gb_global() {
let _g = EnvVarGuard::unset();
@@ -1374,8 +1335,6 @@ auto_update = true
);
}
// ── No remote_settings still works ──────────────────────────────
#[test]
fn no_remote_settings_falls_through_to_default() {
let _g = EnvVarGuard::unset();
@@ -1386,8 +1345,6 @@ auto_update = true
assert_eq!(resolve(&cfg, None), DEFAULT_AUTO_COMPACT_THRESHOLD_PERCENT);
}
// ── ConfigModelOverride should NOT collapse into ModelInfo ──────
//
// Regression guard. If `ConfigModelOverride::apply` ever starts
// merging `auto_compact_threshold_percent` into `ModelInfo`, the
// resolver would see user-per-model as GB-per-model and ordering
@@ -11,8 +11,7 @@ pub(crate) const ENV_AUTO_PERMISSION_MODE: &str = "KIGI_AUTO_PERMISSION_MODE";
#[cfg(test)]
pub(crate) static AUTO_PERMISSION_MODE_ENV_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
/// Extract the `[auto_mode] enabled` gate from one TOML layer (the local opt-in
/// that replaced `[features] auto_permission_mode`).
/// Extract the `[auto_mode] enabled` gate from one TOML layer.
fn auto_permission_mode_from_toml(v: Option<&TomlValue>) -> Option<bool> {
v?.get("auto_mode")?.get("enabled")?.as_bool()
}
@@ -134,9 +134,13 @@ mod compaction_wall_clock_budget_tests {
// Assumes KIGI_COMPACTION_WALL_CLOCK_SECS is unset in the test env.
#[test]
fn default_global_disable_and_no_clamp() {
assert_eq!(resolve(None), 300); // client default
assert_eq!(resolve(Some(450)), 450); // server global wins
assert_eq!(resolve(Some(0)), 0); // 0 explicitly disables (no clamp)
assert_eq!(resolve(Some(5)), 5); // low values pass through (warned, not clamped)
// client default
assert_eq!(resolve(None), 300);
// server global wins
assert_eq!(resolve(Some(450)), 450);
// 0 explicitly disables (no clamp)
assert_eq!(resolve(Some(0)), 0);
// low values pass through (warned, not clamped)
assert_eq!(resolve(Some(5)), 5);
}
}
@@ -258,18 +258,22 @@ mod mcp_startup_timeout_tests {
#[test]
fn precedence_requirements_env_config_remote_default() {
assert_eq!(r(None, None, None, None), DEFAULT_MCP_STARTUP_TIMEOUT_SECS);
assert_eq!(r(Some(5), Some(6), Some(7), Some(8)), 5); // requirements highest
assert_eq!(r(None, Some(6), Some(7), Some(8)), 6); // env
assert_eq!(r(None, None, Some(7), Some(8)), 7); // config
assert_eq!(r(None, None, None, Some(8)), 8); // remote
// requirements highest
assert_eq!(r(Some(5), Some(6), Some(7), Some(8)), 5);
// env
assert_eq!(r(None, Some(6), Some(7), Some(8)), 6);
// config
assert_eq!(r(None, None, Some(7), Some(8)), 7);
// remote
assert_eq!(r(None, None, None, Some(8)), 8);
}
}
// ── MCP max output bytes (inline tool-result cap) ───────────────────────────
// MCP max output bytes (inline tool-result cap).
//
// Full multi-tier resolve lives only here (shell can read config/requirements).
// Tools holds a single effective atomic: we resolve once on apply and push the
// result via `set_mcp_max_output_bytes` so free-function truncation sees it.
// Tools holds a single effective atomic: resolved once on apply and pushed via
// `set_mcp_max_output_bytes` so free-function truncation sees it.
/// Default MCP tool-result inline cap (bytes).
pub const DEFAULT_MAX_MCP_OUTPUT_BYTES: usize = kigi_tools::MCP_MAX_OUTPUT_BYTES;
@@ -318,7 +322,8 @@ pub fn resolve_max_mcp_output_bytes(remote: Option<u64>) -> usize {
resolve_max_mcp_output_bytes_precedence(
requirements,
kigi_tools::mcp_max_output_bytes_from_env(),
None, // project tier needs a cwd — see resolve_max_mcp_output_bytes_for_cwd
// project tier needs a cwd — see resolve_max_mcp_output_bytes_for_cwd
None,
config,
remote_usize,
)
@@ -400,11 +405,16 @@ mod max_mcp_output_bytes_tests {
r(None, None, None, None, None),
DEFAULT_MAX_MCP_OUTPUT_BYTES
);
assert_eq!(r(Some(1), Some(2), Some(9), Some(3), Some(4)), 1); // requirements highest
assert_eq!(r(None, Some(2), Some(9), Some(3), Some(4)), 2); // env beats project
assert_eq!(r(None, None, Some(9), Some(3), Some(4)), 9); // project beats user config
assert_eq!(r(None, None, None, Some(3), Some(4)), 3); // user config beats remote
assert_eq!(r(None, None, None, None, Some(4)), 4); // remote beats default
// requirements highest
assert_eq!(r(Some(1), Some(2), Some(9), Some(3), Some(4)), 1);
// env beats project
assert_eq!(r(None, Some(2), Some(9), Some(3), Some(4)), 2);
// project beats user config
assert_eq!(r(None, None, Some(9), Some(3), Some(4)), 9);
// user config beats remote
assert_eq!(r(None, None, None, Some(3), Some(4)), 3);
// remote beats default
assert_eq!(r(None, None, None, None, Some(4)), 4);
}
#[test]
@@ -440,11 +440,16 @@ mod ask_user_question_timeout_tests {
let d = kigi_tools::implementations::kigi::ask_user_question::RESPONSE_TIMEOUT.as_secs();
let r = resolve_ask_user_question_timeout_secs_from_tiers;
assert_eq!(r(None, None, None, None, None), d);
assert_eq!(r(Some(1), Some(2), Some(3), Some(4), Some(5)), 1); // requirements highest
assert_eq!(r(None, Some(2), Some(3), Some(4), Some(5)), 2); // env
assert_eq!(r(None, None, Some(3), Some(4), Some(5)), 3); // user config
assert_eq!(r(None, None, None, Some(4), Some(5)), 4); // managed
assert_eq!(r(None, None, None, None, Some(5)), 5); // remote
// requirements highest
assert_eq!(r(Some(1), Some(2), Some(3), Some(4), Some(5)), 1);
// env
assert_eq!(r(None, Some(2), Some(3), Some(4), Some(5)), 2);
// user config
assert_eq!(r(None, None, Some(3), Some(4), Some(5)), 3);
// managed
assert_eq!(r(None, None, None, Some(4), Some(5)), 4);
// remote
assert_eq!(r(None, None, None, None, Some(5)), 5);
}
#[test]
@@ -516,14 +521,17 @@ mod tests {
let req: TomlValue = toml::from_str("[toolset.bash]\nfind_bfs = true\n").unwrap();
let managed: TomlValue = toml::from_str("[toolset.bash]\ngrep_ugrep = false\n").unwrap();
let (find, grep) = resolve_search_tools_enabled(Some(&req), Some(&user), Some(&managed));
assert!(find); // requirements `true` beats user `false`
assert!(grep); // user `true` beats managed `false`
// requirements `true` beats user `false`
assert!(find);
// user `true` beats managed `false`
assert!(grep);
}
#[test]
fn resolve_search_tool_enabled_precedence() {
// args: disable, requirement, env, config, managed
assert!(resolve_search_tool_enabled(None, None, None, None, None)); // default on
// default on
assert!(resolve_search_tool_enabled(None, None, None, None, None));
// Org requirement wins outright — even over the user DISABLE kill-switch.
assert!(resolve_search_tool_enabled(
Some(true),
@@ -1,100 +1,82 @@
use super::persist::update_config;
use anyhow::Result;
// ---------------------------------------------------------------------------
// Settings helpers — typed disk-write wrappers for each setting.
// All route through `update_config` → `merge_section` → `save_config`.
// ---------------------------------------------------------------------------
// Settings helpers — typed disk-write wrappers for each setting. All route
// through `update_config` → `merge_section` → `save_config`.
/// Persist `[ui].compact_mode` via `update_config`.
pub async fn set_compact_mode(value: bool) -> Result<()> {
update_config(|cfg| cfg.ui.compact_mode = value).await
}
/// Persist `[ui].show_timestamps` via `update_config`. `UiConfig::show_timestamps`
/// is `Option<bool>` — pager-side `None` means "use default" — so we wrap.
/// `UiConfig::show_timestamps` is `Option<bool>` — pager-side `None` means
/// "use default" — so we wrap.
pub async fn set_show_timestamps(value: bool) -> Result<()> {
update_config(|cfg| cfg.ui.show_timestamps = Some(value)).await
}
/// Persist `[ui].show_timeline` via `update_config`. Same `Option<bool>`
/// shape as `show_timestamps`.
pub async fn set_show_timeline(value: bool) -> Result<()> {
update_config(|cfg| cfg.ui.show_timeline = Some(value)).await
}
/// Persist `[ui].simple_mode` via `update_config`. Same `Option<bool>`
/// shape as `show_timestamps`.
pub async fn set_simple_mode(value: bool) -> Result<()> {
update_config(|cfg| cfg.ui.simple_mode = Some(value)).await
}
/// Persist `[ui.contextual_hints].undo` via `update_config`. The nested struct
/// stays out of `config.toml` until a tip is toggled (`skip_serializing_if`).
/// The nested `contextual_hints` struct stays out of `config.toml` until a
/// tip is toggled (`skip_serializing_if`).
pub async fn set_contextual_hint_undo(value: bool) -> Result<()> {
update_config(|cfg| cfg.ui.contextual_hints.undo = Some(value)).await
}
/// Persist `[ui.contextual_hints].plan_mode` via `update_config`.
pub async fn set_contextual_hint_plan_mode(value: bool) -> Result<()> {
update_config(|cfg| cfg.ui.contextual_hints.plan_mode = Some(value)).await
}
/// Persist `[ui.contextual_hints].image_input` via `update_config`.
pub async fn set_contextual_hint_image_input(value: bool) -> Result<()> {
update_config(|cfg| cfg.ui.contextual_hints.image_input = Some(value)).await
}
/// Persist `[ui.contextual_hints].send_now` via `update_config`.
pub async fn set_contextual_hint_send_now(value: bool) -> Result<()> {
update_config(|cfg| cfg.ui.contextual_hints.send_now = Some(value)).await
}
/// Persist `[ui.contextual_hints].small_screen` via `update_config`.
pub async fn set_contextual_hint_small_screen(value: bool) -> Result<()> {
update_config(|cfg| cfg.ui.contextual_hints.small_screen = Some(value)).await
}
/// Persist `[ui.contextual_hints].word_select` via `update_config`.
pub async fn set_contextual_hint_word_select(value: bool) -> Result<()> {
update_config(|cfg| cfg.ui.contextual_hints.word_select = Some(value)).await
}
/// Persist `[ui].theme` via `update_config`. Caller must pass the
/// canonical theme name (`kiginight`, `tokyonight`, `auto`, etc.).
/// Caller must pass the canonical theme name (`kiginight`, `tokyonight`,
/// `auto`, etc.).
pub async fn set_theme(value: String) -> Result<()> {
update_config(|cfg| cfg.ui.theme = Some(value)).await
}
/// Persist `[ui].auto_dark_theme` via `update_config`. `UiConfig::auto_dark_theme`
/// is `Option<String>` (canonical theme name; `auto` is rejected by the
/// pager's `load_auto_theme_config` filter at read time to prevent
/// circular reference).
/// `auto` is rejected by the pager's `load_auto_theme_config` filter at read
/// time to prevent circular reference.
pub async fn set_auto_dark_theme(value: String) -> Result<()> {
update_config(|cfg| cfg.ui.auto_dark_theme = Some(value)).await
}
/// Persist `[ui].auto_light_theme` via `update_config`. Same shape as
/// [`set_auto_dark_theme`].
pub async fn set_auto_light_theme(value: String) -> Result<()> {
update_config(|cfg| cfg.ui.auto_light_theme = Some(value)).await
}
/// Maximum length (in bytes) accepted by [`set_default_model`].
/// Defense against callers bypassing catalog validation.
pub const MAX_DEFAULT_MODEL_LEN: usize = 256;
/// Persist `[models].default` and dismiss any active campaign nudging it (an
/// explicit user pick wins over the soft campaign default).
///
/// This is the only sanctioned writer of `models.default`; it routes through
/// [`super::campaigns::persist_models_default`] so a user pick always dismisses
/// an active campaign. Do not persist `models.default` via raw `update_config`,
/// or a campaign would keep overriding the user's choice.
/// [`super::campaigns::persist_models_default`] so an explicit user pick
/// always dismisses an active campaign nudging that default. Do not persist
/// `models.default` via raw `update_config`, or a campaign would keep
/// overriding the user's choice.
///
/// Caller must validate `value` against the model catalog first.
/// Empty string clears the field (falls back to remote/built-in default).
/// Length over [`MAX_DEFAULT_MODEL_LEN`] returns `Err`.
/// Length over [`MAX_DEFAULT_MODEL_LEN`] returns `Err` (checked in
/// `persist_models_default`).
pub async fn set_default_model(value: String) -> Result<()> {
super::campaigns::persist_models_default(
if value.is_empty() { None } else { Some(value) },
@@ -103,10 +85,8 @@ pub async fn set_default_model(value: String) -> Result<()> {
.await
}
/// Persist `[ui].fork_secondary_model` via `update_config`.
///
/// Caller must validate against the model catalog. Empty string
/// restores the built-in default. Length > [`MAX_DEFAULT_MODEL_LEN`] → `Err`.
/// restores the built-in default.
pub async fn set_fork_secondary_model(value: String) -> Result<()> {
if value.len() > MAX_DEFAULT_MODEL_LEN {
anyhow::bail!(
@@ -130,81 +110,64 @@ pub async fn set_fork_secondary_model(value: String) -> Result<()> {
const MAX_THOUGHTS_WIDTH_SHELL_MIN: i64 = 40;
const MAX_THOUGHTS_WIDTH_SHELL_MAX: i64 = 500;
/// Persist `[ui].max_thoughts_width` via `update_config`.
/// Defensively clamps to `[40, 500]` at the shell boundary.
pub async fn set_max_thoughts_width(value: i64) -> Result<()> {
let clamped = value.clamp(MAX_THOUGHTS_WIDTH_SHELL_MIN, MAX_THOUGHTS_WIDTH_SHELL_MAX) as u16;
update_config(|cfg| cfg.ui.max_thoughts_width = clamped).await
}
/// Persist `[ui].scroll_speed` via `update_config`.
/// Defensively clamps to `[1, 100]` at the shell boundary.
pub async fn set_scroll_speed(value: i64) -> Result<()> {
let clamped = value.clamp(1, 100) as u8;
update_config(|cfg| cfg.ui.scroll_speed = Some(clamped)).await
}
/// Persist `[ui].scroll_mode` (`auto` | `wheel` | `trackpad`) via `update_config`.
pub async fn set_scroll_mode(value: String) -> Result<()> {
update_config(|cfg| cfg.ui.scroll_mode = Some(value)).await
}
/// Persist `[ui].invert_scroll` via `update_config`.
pub async fn set_invert_scroll(value: bool) -> Result<()> {
update_config(|cfg| cfg.ui.invert_scroll = Some(value)).await
}
/// Persist `[ui.display_refresh].auto_cadence_enabled` via `update_config`.
/// Nested field only — does not replace the whole `display_refresh` object.
pub async fn set_display_refresh_auto_cadence(value: bool) -> Result<()> {
update_config(|cfg| cfg.ui.display_refresh.auto_cadence_enabled = Some(value)).await
}
/// Persist `[ui].scroll_lines` via `update_config`.
/// Defensively clamps to `[1, 10]` at the shell boundary.
pub async fn set_scroll_lines(value: i64) -> Result<()> {
let clamped = value.clamp(1, 10) as u8;
update_config(|cfg| cfg.ui.scroll_lines = Some(clamped)).await
}
/// Persist `[ui].vim_mode` via `update_config`.
pub async fn set_vim_mode(value: bool) -> Result<()> {
update_config(|cfg| cfg.ui.vim_mode = Some(value)).await
}
/// Persist `[ui].remember_tool_approvals` via `update_config`.
pub async fn set_remember_tool_approvals(value: bool) -> Result<()> {
update_config(|cfg| cfg.ui.remember_tool_approvals = Some(value)).await
}
/// Persist `[ui].show_thinking_blocks` via `update_config`.
pub async fn set_show_thinking_blocks(value: bool) -> Result<()> {
update_config(|cfg| cfg.ui.show_thinking_blocks = Some(value)).await
}
/// Persist `[ui].prompt_suggestions` via `update_config`.
pub async fn set_prompt_suggestions(value: bool) -> Result<()> {
update_config(|cfg| cfg.ui.prompt_suggestions = Some(value)).await
}
/// Persist `[toolset.ask_user_question].timeout_enabled` via `update_config`
/// (the user tier of the shell's tiered resolver; the effective value is
/// re-resolved at agent build).
/// The user tier of the shell's tiered resolver; the effective value is
/// re-resolved at agent build.
pub async fn set_ask_user_question_timeout_enabled(value: bool) -> Result<()> {
update_config(|cfg| cfg.ask_user_question.timeout_enabled = Some(value)).await
}
/// Persist `[ui].group_tool_verbs` via `update_config`.
pub async fn set_group_tool_verbs(value: bool) -> Result<()> {
update_config(|cfg| cfg.ui.group_tool_verbs = Some(value)).await
}
/// Persist `[ui].collapsed_edit_blocks` via `update_config`.
pub async fn set_collapsed_edit_blocks(value: bool) -> Result<()> {
update_config(|cfg| cfg.ui.collapsed_edit_blocks = Some(value)).await
}
/// Persist `[ui].keep_text_selection` (`flash` | `hold` | `word_select`).
/// Clears the legacy `selection_highlight_duration_ms` and the retired
/// `double_click_action` keys it supersedes so the two can never drift (one-shot
/// disk migration away from the legacy key on any Settings write).
@@ -217,43 +180,29 @@ pub async fn set_keep_text_selection(value: String) -> Result<()> {
.await
}
/// Persist `[ui].render_mermaid` via `update_config`. Value is one of the
/// canonical strings `auto` | `on` | `off`.
pub async fn set_render_mermaid(value: String) -> Result<()> {
update_config(|cfg| cfg.ui.render_mermaid = Some(value)).await
}
/// Persist `[ui].hunk_tracker_mode` via `update_config`. Value is one of the
/// canonical strings `agent_only` | `all_dirty` | `off`.
/// Restart-required: the mode is read once at connect time.
pub async fn set_hunk_tracker_mode(value: String) -> Result<()> {
update_config(|cfg| cfg.ui.hunk_tracker_mode = Some(value)).await
}
/// Persist `[ui].voice_capture_mode` via `update_config`. Value is one of the
/// canonical strings `toggle` | `hold`.
pub async fn set_voice_capture_mode(value: String) -> Result<()> {
update_config(|cfg| cfg.ui.voice_capture_mode = Some(value)).await
}
/// Persist `[ui].voice_stt_language` via `update_config`. Value is a canonical
/// language code from the settings catalog (`en`, `es`, …) or `auto` (system
/// locale, falling back to English).
/// `auto` means system locale, falling back to English.
pub async fn set_voice_stt_language(value: String) -> Result<()> {
update_config(|cfg| cfg.ui.voice_stt_language = Some(value)).await
}
/// Persist `[ui].default_selected_permission` via `update_config`. Value is
/// one of the canonical strings from `DEFAULT_SELECTED_PERMISSION_CHOICES`
/// (`default` | `allow_once` | `allow_always` | `reject`); `default` is the
/// "no preselection" sentinel.
/// `default` is the "no preselection" sentinel.
pub async fn set_default_selected_permission(value: String) -> Result<()> {
update_config(|cfg| cfg.ui.default_selected_permission = Some(value)).await
}
/// Persist `[ui].cancel_subagents_on_turn_cancel` via `update_config`.
/// Canonical values: `ask` (clear / prompt each time), `always_stop`,
/// `always_continue`.
pub async fn set_cancel_subagents_on_turn_cancel(value: String) -> Result<()> {
update_config(|cfg| {
cfg.ui.cancel_subagents_on_turn_cancel = if value == "ask" { None } else { Some(value) };
@@ -261,7 +210,6 @@ pub async fn set_cancel_subagents_on_turn_cancel(value: String) -> Result<()> {
.await
}
/// Persist `[ui].screen_mode` (`fullscreen` | `minimal`). Empty clears the key.
pub async fn set_screen_mode(value: String) -> Result<()> {
update_config(|cfg| {
cfg.ui.screen_mode = if value.is_empty() { None } else { Some(value) };
@@ -269,13 +217,11 @@ pub async fn set_screen_mode(value: String) -> Result<()> {
.await
}
/// Persist `[cli].show_tips` via `update_config`.
/// Restart-required: `resolve_tips` reads this once at startup.
pub async fn set_show_tips(value: bool) -> Result<()> {
update_config(|cfg| cfg.cli.show_tips = Some(value)).await
}
/// Persist `[cli].auto_update` via `update_config`.
/// Restart-required: auto-update check fires once on startup.
pub async fn set_auto_update(value: bool) -> Result<()> {
update_config(|cfg| cfg.cli.auto_update = Some(value)).await
@@ -85,7 +85,6 @@ pub fn resolve_tips(
let usr = user.and_then(tips_from_toml);
let mgd = managed.and_then(tips_from_toml);
// Priority: requirements > remote > user > managed.
merge_tips(req, usr, mgd, remote_tips)
}
@@ -193,7 +193,6 @@ default = "kigi-code-fast-1"
worktree_type = "invalid"
"#;
let root: TomlValue = toml::from_str(toml_str).unwrap();
// Invalid values should fall back to default
assert_eq!(worktree_type_from_toml(&root), WorktreeType::Linked);
}
@@ -297,8 +296,6 @@ worktree_type = "invalid"
);
}
// === restore_code config tests ===
#[test]
fn test_restore_code_from_toml_present_true() {
let root: TomlValue = toml::from_str("[cli]\nrestore_code = true").unwrap();
@@ -374,6 +371,4 @@ worktree_type = "invalid"
};
assert!(!resolve_restore_code(&root, Some(&remote)));
}
// === minimum_version tests ===
}
@@ -59,7 +59,6 @@ impl KigiAuthCredentials {
self.auth_manager = Some(am);
self
}
/// Return a reference to the internal `AuthManager`, if any.
pub fn auth_manager(&self) -> Option<&Arc<crate::auth::AuthManager>> {
self.auth_manager.as_ref()
}