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).
This commit is contained in:
commit
d6c20fc13f
2612 files changed
+1353757
No files matched your search
@@ -0,0 +1,444 @@
|
||||
#![cfg_attr(rustfmt, rustfmt::skip)]
|
||||
use super::*;
|
||||
|
||||
/// Regression: a machine-wide `x.ai/models/update` broadcast
|
||||
/// carries each model's static catalog-default effort (`high`), not the
|
||||
/// session's chosen `xhigh`, and must not clobber the per-session choice.
|
||||
#[test]
|
||||
fn models_update_preserves_user_reasoning_effort() {
|
||||
use kigi_shell::sampling::types::ReasoningEffort;
|
||||
let mut app = make_app_with_agent("sess-1");
|
||||
|
||||
let agent = app.agents.get_mut(&AgentId(0)).unwrap();
|
||||
let id = acp::ModelId::new(std::sync::Arc::from("reason-model"));
|
||||
let mut info = make_model_info("reason-model");
|
||||
info.meta = serde_json::json!({
|
||||
"supportsReasoningEffort": true,
|
||||
"reasoningEffort": "high",
|
||||
})
|
||||
.as_object()
|
||||
.cloned();
|
||||
agent.session.models.available.insert(id.clone(), info);
|
||||
agent
|
||||
.session
|
||||
.models
|
||||
.set_current(id, Some(ReasoningEffort::Xhigh));
|
||||
assert_eq!(
|
||||
agent.session.models.reasoning_effort,
|
||||
Some(ReasoningEffort::Xhigh)
|
||||
);
|
||||
|
||||
let notif = make_reasoning_models_update_notif("reason-model", "high");
|
||||
assert!(handle_models_update(¬if, &mut app));
|
||||
|
||||
let agent = app.agents.get(&AgentId(0)).unwrap();
|
||||
assert_eq!(
|
||||
agent.session.models.reasoning_effort,
|
||||
Some(ReasoningEffort::Xhigh),
|
||||
"models/update broadcast must not clobber a user-set per-session effort"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn models_update_preserves_active_agent_model() {
|
||||
let mut app = make_app_with_agent("sess-1");
|
||||
|
||||
let agent = app.agents.get_mut(&AgentId(0)).unwrap();
|
||||
let id_3 = acp::ModelId::new(std::sync::Arc::from("grok-3"));
|
||||
agent
|
||||
.session
|
||||
.models
|
||||
.available
|
||||
.insert(id_3.clone(), make_model_info("grok-3"));
|
||||
agent.session.models.current = Some(id_3);
|
||||
|
||||
let notif = make_models_update_notif("grok-4", &["grok-3", "grok-4"]);
|
||||
handle_models_update(¬if, &mut app);
|
||||
|
||||
assert_eq!(
|
||||
app.models.current.as_ref().map(|id| id.0.as_ref()),
|
||||
Some("grok-3"),
|
||||
"app.models.current must preserve active agent's model, not remote settings default"
|
||||
);
|
||||
|
||||
let agent = app.agents.get(&AgentId(0)).unwrap();
|
||||
assert_eq!(
|
||||
agent
|
||||
.session
|
||||
.models
|
||||
.current
|
||||
.as_ref()
|
||||
.map(|id| id.0.as_ref()),
|
||||
Some("grok-3"),
|
||||
"agent's per-session model must be preserved"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn models_update_uses_shell_default_when_agent_model_removed() {
|
||||
let mut app = make_app_with_agent("sess-1");
|
||||
|
||||
let agent = app.agents.get_mut(&AgentId(0)).unwrap();
|
||||
let id_3 = acp::ModelId::new(std::sync::Arc::from("grok-3"));
|
||||
agent
|
||||
.session
|
||||
.models
|
||||
.available
|
||||
.insert(id_3.clone(), make_model_info("grok-3"));
|
||||
agent.session.models.current = Some(id_3);
|
||||
|
||||
// grok-3 removed from catalog.
|
||||
let notif = make_models_update_notif("grok-4.3", &["grok-4.3", "grok-4.5"]);
|
||||
handle_models_update(¬if, &mut app);
|
||||
|
||||
assert_eq!(
|
||||
app.models.current.as_ref().map(|id| id.0.as_ref()),
|
||||
Some("grok-4.3"),
|
||||
"app.models.current must use shell default when agent model removed"
|
||||
);
|
||||
|
||||
let agent = app.agents.get(&AgentId(0)).unwrap();
|
||||
assert_eq!(
|
||||
agent
|
||||
.session
|
||||
.models
|
||||
.current
|
||||
.as_ref()
|
||||
.map(|id| id.0.as_ref()),
|
||||
Some("grok-4.3"),
|
||||
"agent must fall back to shell default when its model is removed"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn models_update_without_active_agent_uses_shell_default() {
|
||||
let (tx, _rx) = tokio::sync::mpsc::unbounded_channel();
|
||||
let mut app = AppView::new(tx, ModelState::default(), Vec::new());
|
||||
|
||||
let notif = make_models_update_notif("grok-4", &["grok-3", "grok-4"]);
|
||||
handle_models_update(¬if, &mut app);
|
||||
|
||||
assert_eq!(
|
||||
app.models.current.as_ref().map(|id| id.0.as_ref()),
|
||||
Some("grok-4"),
|
||||
"without an active agent, shell default must be used"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn models_update_noop_when_agent_matches_shell_default() {
|
||||
let mut app = make_app_with_agent("sess-1");
|
||||
|
||||
let agent = app.agents.get_mut(&AgentId(0)).unwrap();
|
||||
let id_4 = acp::ModelId::new(std::sync::Arc::from("grok-4"));
|
||||
agent
|
||||
.session
|
||||
.models
|
||||
.available
|
||||
.insert(id_4.clone(), make_model_info("grok-4"));
|
||||
agent.session.models.current = Some(id_4);
|
||||
|
||||
let notif = make_models_update_notif("grok-4", &["grok-3", "grok-4"]);
|
||||
handle_models_update(¬if, &mut app);
|
||||
|
||||
assert_eq!(
|
||||
app.models.current.as_ref().map(|id| id.0.as_ref()),
|
||||
Some("grok-4"),
|
||||
"app.models.current must be grok-4 when agent and shell agree"
|
||||
);
|
||||
let agent = app.agents.get(&AgentId(0)).unwrap();
|
||||
assert_eq!(
|
||||
agent
|
||||
.session
|
||||
.models
|
||||
.current
|
||||
.as_ref()
|
||||
.map(|id| id.0.as_ref()),
|
||||
Some("grok-4"),
|
||||
"agent model must remain grok-4"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn models_update_non_active_agent_uses_shell_fallback_not_active_model() {
|
||||
let mut app = make_app_with_agent("sess-A");
|
||||
insert_agent(&mut app, AgentId(1), Some("sess-B"));
|
||||
|
||||
{
|
||||
let agent_a = app.agents.get_mut(&AgentId(0)).unwrap();
|
||||
let id_3 = acp::ModelId::new(std::sync::Arc::from("grok-3"));
|
||||
agent_a
|
||||
.session
|
||||
.models
|
||||
.available
|
||||
.insert(id_3.clone(), make_model_info("grok-3"));
|
||||
agent_a.session.models.current = Some(id_3);
|
||||
}
|
||||
|
||||
{
|
||||
let agent_b = app.agents.get_mut(&AgentId(1)).unwrap();
|
||||
let id_5 = acp::ModelId::new(std::sync::Arc::from("grok-4.5"));
|
||||
agent_b
|
||||
.session
|
||||
.models
|
||||
.available
|
||||
.insert(id_5.clone(), make_model_info("grok-4.5"));
|
||||
agent_b.session.models.current = Some(id_5);
|
||||
}
|
||||
|
||||
// grok-5 removed from catalog.
|
||||
let notif = make_models_update_notif("grok-4", &["grok-3", "grok-4"]);
|
||||
handle_models_update(¬if, &mut app);
|
||||
|
||||
assert_eq!(
|
||||
app.models.current.as_ref().map(|id| id.0.as_ref()),
|
||||
Some("grok-3"),
|
||||
);
|
||||
let agent_a = app.agents.get(&AgentId(0)).unwrap();
|
||||
assert_eq!(
|
||||
agent_a
|
||||
.session
|
||||
.models
|
||||
.current
|
||||
.as_ref()
|
||||
.map(|id| id.0.as_ref()),
|
||||
Some("grok-3"),
|
||||
"agent A's model must be preserved"
|
||||
);
|
||||
|
||||
// B's grok-5 was removed — must fall back to shell's grok-4, not A's grok-3.
|
||||
let agent_b = app.agents.get(&AgentId(1)).unwrap();
|
||||
assert_eq!(
|
||||
agent_b
|
||||
.session
|
||||
.models
|
||||
.current
|
||||
.as_ref()
|
||||
.map(|id| id.0.as_ref()),
|
||||
Some("grok-4"),
|
||||
"inactive agent must fall back to shell default, not active agent's model"
|
||||
);
|
||||
}
|
||||
|
||||
/// A follower client (no in-flight switch of its own) receives the
|
||||
/// leader's `ModelChanged` broadcast and silently mirrors the new model
|
||||
/// into its local state — no scrollback entry, no toast, just enough
|
||||
/// state for the status bar / `/model` dropdown to render correctly.
|
||||
#[test]
|
||||
fn model_changed_updates_state_silently_on_follower() {
|
||||
let mut app = make_app_with_agent("sess-1");
|
||||
let agent = app.agents.get_mut(&AgentId(0)).unwrap();
|
||||
seed_models(agent, "grok-3", &["grok-3", "grok-4"]);
|
||||
let scrollback_before = agent.scrollback.len();
|
||||
// Follower: no local switch in flight.
|
||||
assert!(!agent.session.model_switch_pending);
|
||||
|
||||
let notif = model_changed_ext("sess-1", "grok-4", None);
|
||||
let changed = handle_ext_notification(¬if, &mut app);
|
||||
assert!(
|
||||
changed,
|
||||
"follower's state changed → handler must request a redraw"
|
||||
);
|
||||
|
||||
let agent = app.agents.get(&AgentId(0)).unwrap();
|
||||
assert_eq!(
|
||||
agent
|
||||
.session
|
||||
.models
|
||||
.current
|
||||
.as_ref()
|
||||
.map(|id| id.0.as_ref()),
|
||||
Some("grok-4"),
|
||||
"follower must mirror the remote switch into its local model state",
|
||||
);
|
||||
assert_eq!(
|
||||
agent.scrollback.len(),
|
||||
scrollback_before,
|
||||
"follower must NOT push a 'Switched to' scrollback entry — that is \
|
||||
the invoking client's job (SwitchModelComplete owns the system message)"
|
||||
);
|
||||
assert!(
|
||||
!agent.session.model_switch_pending,
|
||||
"follower's pending flag must stay false (no local switch was issued)"
|
||||
);
|
||||
}
|
||||
|
||||
/// A live remote `ModelChanged` (leader-mode fan-out from another client)
|
||||
/// must apply even when this client already has a local
|
||||
/// `user_model_preference` — otherwise the status bar desyncs from the
|
||||
/// gateway session. Preference is updated to track the new live model.
|
||||
/// (History-replay silent-revert is suppressed on the shell side via
|
||||
/// `ReconnectState::user_selected_model`, not by permanently blocking
|
||||
/// remote ModelChanged here.)
|
||||
#[test]
|
||||
fn model_changed_applies_and_updates_user_model_preference() {
|
||||
let mut app = make_app_with_agent("sess-1");
|
||||
let agent = app.agents.get_mut(&AgentId(0)).unwrap();
|
||||
seed_models(agent, "heavy", &["auto", "heavy"]);
|
||||
agent.session.user_model_preference =
|
||||
Some(acp::ModelId::new(std::sync::Arc::from("heavy")));
|
||||
assert!(!agent.session.model_switch_pending);
|
||||
|
||||
let notif = model_changed_ext("sess-1", "auto", None);
|
||||
let changed = handle_ext_notification(¬if, &mut app);
|
||||
assert!(
|
||||
changed,
|
||||
"remote live ModelChanged must apply despite prior local preference"
|
||||
);
|
||||
|
||||
let agent = app.agents.get(&AgentId(0)).unwrap();
|
||||
assert_eq!(
|
||||
agent
|
||||
.session
|
||||
.models
|
||||
.current
|
||||
.as_ref()
|
||||
.map(|id| id.0.as_ref()),
|
||||
Some("auto"),
|
||||
"selector must mirror the remote switch"
|
||||
);
|
||||
assert_eq!(
|
||||
agent
|
||||
.session
|
||||
.user_model_preference
|
||||
.as_ref()
|
||||
.map(|id| id.0.as_ref()),
|
||||
Some("auto"),
|
||||
"preference must track the applied remote switch"
|
||||
);
|
||||
}
|
||||
|
||||
/// The invoking client is also a subscriber to its own session and so
|
||||
/// receives the broadcast it triggered. Its in-flight
|
||||
/// `SetSessionModelResponse` is the authority for its local state +
|
||||
/// the single "Switched to X" scrollback entry, so the broadcast handler
|
||||
/// must be a no-op here — gated on `model_switch_pending == true`.
|
||||
///
|
||||
/// Concretely we verify the broadcast does NOT touch
|
||||
/// `models.current` (preserving the pre-response snapshot) — that
|
||||
/// snapshot is what `SwitchModelComplete`'s `unchanged` check compares
|
||||
/// against to decide whether to render the "Switched to X" message. If
|
||||
/// the broadcast optimistically updated state here, the response
|
||||
/// handler would see `prev == new`, mark it unchanged, and suppress the
|
||||
/// user-facing message entirely.
|
||||
#[test]
|
||||
fn model_changed_skipped_when_local_switch_in_flight() {
|
||||
let mut app = make_app_with_agent("sess-1");
|
||||
let agent = app.agents.get_mut(&AgentId(0)).unwrap();
|
||||
seed_models(agent, "grok-3", &["grok-3", "grok-4"]);
|
||||
// Invoker: a local switch is in flight (set by Action::SwitchModel /
|
||||
// set_default_model before the SetSessionModelRequest is sent).
|
||||
agent.session.model_switch_pending = true;
|
||||
let scrollback_before = agent.scrollback.len();
|
||||
|
||||
let notif = model_changed_ext("sess-1", "grok-4", None);
|
||||
let changed = handle_ext_notification(¬if, &mut app);
|
||||
assert!(
|
||||
!changed,
|
||||
"broadcast must be a no-op while local switch is pending"
|
||||
);
|
||||
|
||||
let agent = app.agents.get(&AgentId(0)).unwrap();
|
||||
assert_eq!(
|
||||
agent
|
||||
.session
|
||||
.models
|
||||
.current
|
||||
.as_ref()
|
||||
.map(|id| id.0.as_ref()),
|
||||
Some("grok-3"),
|
||||
"models.current must stay at the pre-response snapshot — \
|
||||
SwitchModelComplete owns the final apply + system message"
|
||||
);
|
||||
assert_eq!(
|
||||
agent.scrollback.len(),
|
||||
scrollback_before,
|
||||
"broadcast must not push any scrollback entry on the invoker"
|
||||
);
|
||||
assert!(
|
||||
agent.session.model_switch_pending,
|
||||
"pending flag must remain set until SwitchModelComplete arrives"
|
||||
);
|
||||
}
|
||||
|
||||
/// A `ModelChanged` broadcast carrying a model id the local catalog
|
||||
/// doesn't know about must be dropped — applying it would render an
|
||||
/// unresolvable id in the status bar and desync the `/model` dropdown.
|
||||
/// This can happen when leader and a follower client briefly disagree
|
||||
/// on the model catalog (etag drift, custom-model config skew).
|
||||
#[test]
|
||||
fn model_changed_dropped_when_model_unknown_to_catalog() {
|
||||
let mut app = make_app_with_agent("sess-1");
|
||||
let agent = app.agents.get_mut(&AgentId(0)).unwrap();
|
||||
seed_models(agent, "grok-3", &["grok-3", "grok-4"]);
|
||||
|
||||
let notif = model_changed_ext("sess-1", "grok-99-unknown", None);
|
||||
let changed = handle_ext_notification(¬if, &mut app);
|
||||
assert!(
|
||||
!changed,
|
||||
"unknown model must NOT trigger a redraw — no state changed"
|
||||
);
|
||||
|
||||
let agent = app.agents.get(&AgentId(0)).unwrap();
|
||||
assert_eq!(
|
||||
agent
|
||||
.session
|
||||
.models
|
||||
.current
|
||||
.as_ref()
|
||||
.map(|id| id.0.as_ref()),
|
||||
Some("grok-3"),
|
||||
"models.current must stay on the previously-known model"
|
||||
);
|
||||
}
|
||||
|
||||
/// `reasoning_effort` round-trips through the broadcast: the follower
|
||||
/// applies it alongside the model id so the prompt header / status bar
|
||||
/// show the right effort without waiting for a subsequent
|
||||
/// `x.ai/models/update`.
|
||||
#[test]
|
||||
fn model_changed_applies_reasoning_effort_on_follower() {
|
||||
use kigi_shell::sampling::types::ReasoningEffort;
|
||||
let mut app = make_app_with_agent("sess-1");
|
||||
let agent = app.agents.get_mut(&AgentId(0)).unwrap();
|
||||
seed_models(agent, "grok-3", &["grok-3", "grok-4"]);
|
||||
|
||||
let notif = model_changed_ext("sess-1", "grok-4", Some("high"));
|
||||
assert!(handle_ext_notification(¬if, &mut app));
|
||||
|
||||
let agent = app.agents.get(&AgentId(0)).unwrap();
|
||||
assert_eq!(
|
||||
agent.session.models.reasoning_effort,
|
||||
Some(ReasoningEffort::High),
|
||||
"follower must mirror the broadcast's reasoning_effort"
|
||||
);
|
||||
}
|
||||
|
||||
/// `ModelChanged` for a session this client doesn't own / hasn't loaded
|
||||
/// must be dropped — `find_session_match` returns `None`. The bug-flavored
|
||||
/// version of this would be: leader-mode A switches model on session X
|
||||
/// (which this client never opened) and we accidentally apply the change
|
||||
/// to the active agent.
|
||||
#[test]
|
||||
fn model_changed_dropped_for_unknown_session_id() {
|
||||
let mut app = make_app_with_agent("sess-1");
|
||||
let agent = app.agents.get_mut(&AgentId(0)).unwrap();
|
||||
seed_models(agent, "grok-3", &["grok-3", "grok-4"]);
|
||||
|
||||
let notif = model_changed_ext("sess-OTHER", "grok-4", None);
|
||||
let changed = handle_ext_notification(¬if, &mut app);
|
||||
assert!(!changed);
|
||||
|
||||
let agent = app.agents.get(&AgentId(0)).unwrap();
|
||||
assert_eq!(
|
||||
agent
|
||||
.session
|
||||
.models
|
||||
.current
|
||||
.as_ref()
|
||||
.map(|id| id.0.as_ref()),
|
||||
Some("grok-3"),
|
||||
"unrelated-session broadcast must not touch this agent's model"
|
||||
);
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user