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).
219 lines
7.3 KiB
Rust
219 lines
7.3 KiB
Rust
//! Differential test: the anstyle-parse-based `split_into_line_segments`
|
|
//! against a reference copy of the previous termwiz-based implementation.
|
|
//!
|
|
//! Production code uses `anstyle-parse` for ANSI line splitting. This test
|
|
//! embeds the prior termwiz-based splitter as a reference and asserts
|
|
//! identical observable output so the rewrite cannot drift. `termwiz` is a
|
|
//! dev-dependency only (powers this test; not linked into shipped binaries).
|
|
//!
|
|
//! Inputs are restricted to what the production splitter actually sees:
|
|
//! complete escape sequences (the old implementation `debug_assert`ed on
|
|
//! trailing incomplete ones) and no raw C1 controls encoded as UTF-8 (termwiz
|
|
//! maps e.g. U+0085 to a control action while VTE prints it; that corner was
|
|
//! unspecified before and is not exercised by terminal output we render).
|
|
|
|
use kigi_ratatui_inline::split_into_line_segments;
|
|
|
|
// ─── Reference: the previous termwiz-based implementation, verbatim ────────
|
|
|
|
struct RefSegment<'a> {
|
|
content: &'a str,
|
|
ends_with_crlf: bool,
|
|
}
|
|
|
|
fn reference_split<'a>(input: &'a str, term_width: usize) -> Vec<RefSegment<'a>> {
|
|
use termwiz::escape::{Action, ControlCode, parser::Parser};
|
|
use unicode_width::UnicodeWidthChar as _;
|
|
|
|
let mut parser = Parser::new();
|
|
let mut remaining_bytes = input.as_bytes();
|
|
|
|
let mut segments = Vec::<RefSegment>::new();
|
|
let mut segment_start = 0_usize;
|
|
let mut segment_end = 0_usize;
|
|
let mut visual_width = 0_usize;
|
|
let mut has_visual = false;
|
|
let mut prev_is_cr = false;
|
|
|
|
macro_rules! push_segment {
|
|
($end:expr, $crlf:expr) => {
|
|
#[allow(unused_assignments)]
|
|
{
|
|
segments.push(RefSegment {
|
|
content: &input[segment_start..$end],
|
|
ends_with_crlf: $crlf,
|
|
});
|
|
visual_width = 0;
|
|
has_visual = false;
|
|
}
|
|
};
|
|
}
|
|
|
|
while let Some((ansi_action, consumed)) = parser.parse_first(remaining_bytes) {
|
|
remaining_bytes = &remaining_bytes[consumed..];
|
|
let mut is_cr = false;
|
|
|
|
match ansi_action {
|
|
Action::Control(ControlCode::LineFeed) => {
|
|
push_segment!(segment_end - usize::from(prev_is_cr), true);
|
|
segment_end += consumed;
|
|
segment_start = segment_end;
|
|
}
|
|
Action::Control(ControlCode::CarriageReturn) => {
|
|
segment_end += consumed;
|
|
visual_width = 0;
|
|
is_cr = true;
|
|
}
|
|
Action::Print(ch) => {
|
|
let char_width = ch.width().unwrap_or(0);
|
|
let new_width = visual_width + char_width;
|
|
if new_width > term_width && has_visual {
|
|
push_segment!(segment_end, false);
|
|
segment_start = segment_end;
|
|
segment_end += consumed;
|
|
visual_width = char_width;
|
|
has_visual = true;
|
|
if char_width > term_width {
|
|
push_segment!(segment_end, false);
|
|
segment_start = segment_end;
|
|
}
|
|
} else {
|
|
segment_end += consumed;
|
|
visual_width = new_width;
|
|
has_visual = true;
|
|
}
|
|
}
|
|
Action::PrintString(_) => unreachable!(),
|
|
_ => {
|
|
segment_end += consumed;
|
|
}
|
|
}
|
|
|
|
prev_is_cr = is_cr;
|
|
}
|
|
|
|
assert!(remaining_bytes.is_empty(), "{remaining_bytes:?}");
|
|
|
|
if segment_end > segment_start {
|
|
let input_start = input.as_ptr();
|
|
if let Some(last) = segments.last_mut() {
|
|
let last_start = last.content.as_ptr();
|
|
let last_end = unsafe { last_start.add(last.content.len()) };
|
|
if !last.ends_with_crlf && !has_visual {
|
|
assert_eq!(segment_start, (last_end as usize - input_start as usize));
|
|
let last_offset = last_start as usize - input_start as usize;
|
|
last.content = &input[last_offset..];
|
|
} else {
|
|
push_segment!(segment_end, false);
|
|
}
|
|
} else {
|
|
push_segment!(segment_end, false);
|
|
}
|
|
}
|
|
|
|
segments
|
|
}
|
|
|
|
// ─── Comparison harness ─────────────────────────────────────────────────────
|
|
|
|
#[track_caller]
|
|
fn assert_same(input: &str, widths: &[usize]) {
|
|
for &width in widths {
|
|
let actual = split_into_line_segments(input, width);
|
|
let expected = reference_split(input, width);
|
|
let actual_view: Vec<(&str, bool)> = actual
|
|
.iter()
|
|
.map(|s| (s.content, s.ends_with_crlf))
|
|
.collect();
|
|
let expected_view: Vec<(&str, bool)> = expected
|
|
.iter()
|
|
.map(|s| (s.content, s.ends_with_crlf))
|
|
.collect();
|
|
assert_eq!(
|
|
actual_view, expected_view,
|
|
"divergence for width {width}, input: {input:?}"
|
|
);
|
|
}
|
|
}
|
|
|
|
const WIDTHS: &[usize] = &[1, 2, 3, 5, 8, 10, 20, 80, 200];
|
|
|
|
#[test]
|
|
fn corpus_matches_reference() {
|
|
let corpus: &[&str] = &[
|
|
"",
|
|
"hello",
|
|
"hello world, this is a longer line that will wrap several times",
|
|
"line1\nline2\nline3",
|
|
"line1\r\nline2\r\n",
|
|
"12345\r67",
|
|
"\r\r\n\n\r",
|
|
"😊😊😊 emoji wall 😊😊😊",
|
|
"hello 你好 混合 width",
|
|
"\x1b[31mred\x1b[0m plain \x1b[1;32;44mstyled\x1b[m",
|
|
"\x1b[31m\x1b[1m\x1b[4mnested styles no text\x1b[0m",
|
|
"12345678\x1b[0m90",
|
|
"text\x1b]8;;https://example.com\x07link\x1b]8;;\x07 after",
|
|
"osc title\x1b]0;window title\x07body",
|
|
"cursor \x1b[2Amoves \x1b[10;20H everywhere",
|
|
"tab\tand\x08backspace and \x07bell",
|
|
"\x1b[38;5;196mext colors\x1b[38;2;10;20;30m truecolor\x1b[0m",
|
|
"interrupted \x1b[3\nmid-sequence",
|
|
"\x1b[31m\nstyle then newline",
|
|
"trailing style 12345678\x1b[0m",
|
|
"\x1b(Bcharset\x1b)0 escapes",
|
|
"zero\u{200b}width\u{fe0f}chars",
|
|
"combining a\u{0301}e\u{0301} accents",
|
|
];
|
|
for input in corpus {
|
|
assert_same(input, WIDTHS);
|
|
}
|
|
}
|
|
|
|
/// Deterministic pseudo-random ANSI soup (xorshift, no extra deps).
|
|
#[test]
|
|
fn randomized_ansi_soup_matches_reference() {
|
|
let mut state = 0x243F_6A88_85A3_08D3_u64; // seed: pi digits
|
|
|
|
let mut next = move || {
|
|
state ^= state << 13;
|
|
state ^= state >> 7;
|
|
state ^= state << 17;
|
|
state
|
|
};
|
|
|
|
const PIECES: &[&str] = &[
|
|
"word",
|
|
"a",
|
|
"longer-token",
|
|
" ",
|
|
" ",
|
|
"\n",
|
|
"\r",
|
|
"\r\n",
|
|
"😊",
|
|
"你好",
|
|
"é",
|
|
"\u{200b}",
|
|
"\x1b[31m",
|
|
"\x1b[0m",
|
|
"\x1b[1;44;38;5;10m",
|
|
"\x1b[2K",
|
|
"\x1b[10D",
|
|
"\x1b]0;title\x07",
|
|
"\x1b]8;;http://x\x07",
|
|
"\t",
|
|
"\x07",
|
|
];
|
|
|
|
for _ in 0..2000 {
|
|
let mut input = String::new();
|
|
let len = (next() % 30) as usize;
|
|
for _ in 0..len {
|
|
input.push_str(PIECES[(next() % PIECES.len() as u64) as usize]);
|
|
}
|
|
let width = 1 + (next() % 40) as usize;
|
|
assert_same(&input, &[width]);
|
|
}
|
|
}
|