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:
@@ -0,0 +1,330 @@
|
||||
//! Scroll-test support: timed wheel-burst drivers and viewport-marker
|
||||
//! helpers. Scroll tests import via `use super::scroll::*;` alongside
|
||||
//! `use super::common::*;`.
|
||||
//!
|
||||
//! Frame capture convention: use the harness's live parser — `reset_timing()`
|
||||
//! before the interaction, `frame_timings()` / `frame_count()` after the
|
||||
//! drain (the `renders_on_action.rs` pattern). Frame COUNT and per-frame
|
||||
//! CHARS are byte-deterministic and CI-assertable; DURATIONS are not — they
|
||||
//! are load-sensitive, and under the no-drain drivers below the burst's
|
||||
//! chunks are parsed during the post-burst `update()`, so durations reflect
|
||||
//! drain pacing rather than real frame timing.
|
||||
|
||||
use std::time::Duration;
|
||||
|
||||
use kigi_pager_pty_harness::PtyHarness;
|
||||
pub(crate) use kigi_pager_pty_harness::{SGR_SCROLL_DOWN, SGR_SCROLL_UP};
|
||||
|
||||
use super::common::{
|
||||
ContentController, DEFAULT_COLS, DEFAULT_ROWS, MOCK_RESPONSE_SENTINEL, PROMPT,
|
||||
WELCOME_SCREEN_SENTINEL, WELCOME_TIMEOUT, locate_screen_text, pager_binary, sgr_mouse,
|
||||
};
|
||||
|
||||
/// Wheel-report position, 0-based (row,col): inside the scrollback pane at
|
||||
/// the default 50×120 PTY. Encodes to the same bytes as the raw constants in
|
||||
/// `resize_preserves_scroll_position.rs` (wire `40;12`).
|
||||
pub(crate) const WHEEL_ROW: u16 = 11;
|
||||
|
||||
pub(crate) const WHEEL_COL: u16 = 39;
|
||||
|
||||
/// Emit one SGR (DECSET 1006) wheel press report per entry of `btns`
|
||||
/// ([`SGR_SCROLL_UP`] / [`SGR_SCROLL_DOWN`]; a mixed slice drives direction
|
||||
/// reversals mid-stream) at 0-based (row,col) via [`sgr_mouse`], sleeping
|
||||
/// `interval` host-side (`std::thread::sleep`) BETWEEN writes — never before
|
||||
/// the first or after the last.
|
||||
///
|
||||
/// The pager classifies a scroll stream from inter-event ARRIVAL timing
|
||||
/// (constants in pager `src/input/mouse.rs`), and the harness terminal
|
||||
/// (`TERM=xterm-256color`, `TERM_PROGRAM` stripped → `TerminalName::Unknown`)
|
||||
/// resolves to `events_per_tick = 3`. On such ept≥2 terminals a stream
|
||||
/// promotes to Wheel only when the first 3 reports land within 12ms of the
|
||||
/// stream's start (`DEFAULT_WHEEL_TICK_DETECT_MAX_MS`); anything slower never
|
||||
/// promotes and finalizes as Trackpad once a >80ms `STREAM_GAP` closes the
|
||||
/// stream. So under this harness: `Duration::ZERO` (back-to-back reports)
|
||||
/// drives wheel clicks, while a long run of spaced reports — even 40ms apart —
|
||||
/// is a trackpad stream. "Slow interval = wheel" holds only on ept=1 brands
|
||||
/// (iTerm2 / WezTerm / VS Code embeds, via `trackpad_detect_max_interval_ms`
|
||||
/// 30–60ms), and 8ms is the acceleration fast band (`ACCEL_INTERVAL_FAST_MS`),
|
||||
/// not a classifier threshold.
|
||||
///
|
||||
/// `interval` only LOWER-BOUNDS each gap: sleep plus scheduler jitter can
|
||||
/// stretch any gap under CI load (a nominal 6ms past the 12ms promotion
|
||||
/// window, say). Tests asserting classification-dependent behavior must
|
||||
/// tolerate or detect stretched gaps; prefer assertions that hold under
|
||||
/// either classification (scrolled at all, ≥1 frame, ≤1 frame per event).
|
||||
/// Deliberately dumb: no draining, no assertions — callers `update()`
|
||||
/// afterwards and assert on screen state / frame captures.
|
||||
///
|
||||
/// Harness mirror: the scroll-matrix runner's gesture-replay loop
|
||||
/// (`kigi-pager-pty-harness/src/scroll_matrix/runner.rs`) ports this
|
||||
/// send loop onto `WheelStep` tables — apply fixes there too.
|
||||
pub(crate) fn send_wheel_sequence(
|
||||
h: &mut PtyHarness,
|
||||
btns: &[u16],
|
||||
row: u16,
|
||||
col: u16,
|
||||
interval: Duration,
|
||||
) {
|
||||
for (i, &btn) in btns.iter().enumerate() {
|
||||
if i > 0 && !interval.is_zero() {
|
||||
std::thread::sleep(interval);
|
||||
}
|
||||
h.inject_keys(sgr_mouse(btn, row, col, 'M').as_bytes())
|
||||
.expect("inject SGR wheel report");
|
||||
}
|
||||
}
|
||||
|
||||
/// [`send_wheel_sequence`] for the common case: `count` same-direction reports.
|
||||
pub(crate) fn send_wheel_burst(
|
||||
h: &mut PtyHarness,
|
||||
btn: u16,
|
||||
count: usize,
|
||||
row: u16,
|
||||
col: u16,
|
||||
interval: Duration,
|
||||
) {
|
||||
let btns = vec![btn; count];
|
||||
send_wheel_sequence(h, &btns, row, col, interval);
|
||||
}
|
||||
|
||||
/// Prefix shared by [`marker_line`] and the top-down screen parse in
|
||||
/// [`topmost_visible_marker`].
|
||||
const MARKER_PREFIX: &str = "MARKER-";
|
||||
|
||||
/// Unique numbered marker line `n` (`MARKER-0042`). Zero-padded so every
|
||||
/// marker is the same width and no marker is a substring of another within a
|
||||
/// [`marker_response`] transcript.
|
||||
pub(crate) fn marker_line(n: usize) -> String {
|
||||
format!("{MARKER_PREFIX}{n:04}")
|
||||
}
|
||||
|
||||
/// A response of `count` numbered marker lines inside a fenced code block, so
|
||||
/// markdown renders exactly one row per marker with no soft-wrap reflow (same
|
||||
/// trick as `tall_response` in `common.rs`). Feed to `content.set_response(..)`;
|
||||
/// scroll tests then assert viewport movement via [`marker_screen_row`] /
|
||||
/// [`topmost_visible_marker`] deltas instead of fragile absolute positions.
|
||||
pub(crate) fn marker_response(sentinel: &str, count: usize) -> String {
|
||||
let mut s = String::with_capacity(count * 16 + 64);
|
||||
s.push_str("```\n");
|
||||
s.push_str(sentinel);
|
||||
s.push('\n');
|
||||
for i in 0..count {
|
||||
s.push_str(&marker_line(i));
|
||||
s.push('\n');
|
||||
}
|
||||
s.push_str("```\n");
|
||||
s
|
||||
}
|
||||
|
||||
/// Current screen row (0-based) of `marker`, or `None` when it is off-screen.
|
||||
pub(crate) fn marker_screen_row(h: &PtyHarness, marker: &str) -> Option<u16> {
|
||||
locate_screen_text(&h.screen_contents(), marker).map(|(row, _)| row)
|
||||
}
|
||||
|
||||
/// Index of the topmost marker on screen: a single top-down pass parsing the
|
||||
/// first `MARKER-nnnn` occurrence (`None` when no marker is visible; malformed
|
||||
/// hits are skipped). Scrolling UP strictly decreases it by the number of rows
|
||||
/// scrolled — the primitive for "viewport moved by K rows / did not jump"
|
||||
/// assertions.
|
||||
pub(crate) fn topmost_visible_marker(h: &PtyHarness) -> Option<usize> {
|
||||
h.screen_contents().lines().find_map(|line| {
|
||||
let digits_at = line.find(MARKER_PREFIX)? + MARKER_PREFIX.len();
|
||||
line.get(digits_at..digits_at + 4)?.parse().ok()
|
||||
})
|
||||
}
|
||||
|
||||
/// Spawn the pager over a `marker_count`-marker transcript and drive it to
|
||||
/// the primed scroll-test state (the shared preamble of every wheel pacing
|
||||
/// test, per the `drive_to_scrollback_with_turn` precedent): response fully
|
||||
/// streamed, viewport bottom-pinned with the first marker off-screen-top,
|
||||
/// scrollback focused via Tab (Esc would silently arm the rewind picker,
|
||||
/// whose ~800ms expiry redraw lands a non-burst frame in the capture
|
||||
/// window; wheel reports are position-routed regardless of focus), then
|
||||
/// quiesced and `reset_timing()` applied — counted frames come only from
|
||||
/// what the caller does next.
|
||||
///
|
||||
/// Returns the harness, the content controller (keep it alive: it owns the
|
||||
/// mock server), and the topmost visible marker index as the movement
|
||||
/// baseline. Panics with the screen contents on any setup violation.
|
||||
///
|
||||
/// Destructure the controller as `_content`, never `_` — a `_` binding drops
|
||||
/// it immediately, killing the mock server mid-test and surfacing as a
|
||||
/// confusing 60s stream timeout instead of an obvious failure.
|
||||
///
|
||||
/// Harness mirror: `spawn_settled_marker_session` in
|
||||
/// `kigi-pager-pty-harness/src/scroll_matrix/session.rs` is a port of
|
||||
/// this preamble — fixes must flow both ways.
|
||||
pub(crate) async fn spawn_bottom_pinned_marker_scrollback(
|
||||
marker_count: usize,
|
||||
) -> (PtyHarness, ContentController, usize) {
|
||||
spawn_bottom_pinned_marker_scrollback_with_env(marker_count, &[]).await
|
||||
}
|
||||
|
||||
/// [`spawn_bottom_pinned_marker_scrollback`] with extra pager env vars
|
||||
/// (e.g. `KIGI_SCROLL_SPEED`) appended to the controller's env.
|
||||
pub(crate) async fn spawn_bottom_pinned_marker_scrollback_with_env(
|
||||
marker_count: usize,
|
||||
extra_env: &[(&str, &str)],
|
||||
) -> (PtyHarness, ContentController, usize) {
|
||||
let content = ContentController::start().await.expect("start content");
|
||||
content.set_response(marker_response(MOCK_RESPONSE_SENTINEL, marker_count));
|
||||
|
||||
let binary = pager_binary().expect("resolve pager binary");
|
||||
// spawn_with_content minus the fixed env: content env + the caller's.
|
||||
let content_env = content.env_for_pager();
|
||||
let mut env: Vec<(&str, &str)> = content_env
|
||||
.iter()
|
||||
.map(|(k, v)| (k.as_str(), v.as_str()))
|
||||
.collect();
|
||||
env.extend_from_slice(extra_env);
|
||||
let mut harness = PtyHarness::new(&binary, DEFAULT_ROWS, DEFAULT_COLS, &[], &env)
|
||||
.expect("spawn pager with content");
|
||||
|
||||
harness
|
||||
.wait_for_text(WELCOME_SCREEN_SENTINEL, WELCOME_TIMEOUT)
|
||||
.expect("welcome text");
|
||||
harness
|
||||
.inject_keys(format!("{PROMPT}\r").as_bytes())
|
||||
.expect("submit prompt");
|
||||
// The LAST marker is the last thing streamed → the whole transcript is in.
|
||||
harness
|
||||
.wait_for_text(&marker_line(marker_count - 1), Duration::from_secs(60))
|
||||
.expect("response finished streaming");
|
||||
harness.update(Duration::from_millis(500));
|
||||
|
||||
// Setup guards: bottom-pinned after the stream, the first marker must be
|
||||
// off-screen-top (the transcript overflows the viewport) and some later
|
||||
// marker visible — otherwise there is nothing to scroll into view.
|
||||
assert!(
|
||||
marker_screen_row(&harness, &marker_line(0)).is_none(),
|
||||
"setup: {} already visible → transcript not taller than the screen\nscreen:\n{}",
|
||||
marker_line(0),
|
||||
harness.screen_contents()
|
||||
);
|
||||
let top_before = topmost_visible_marker(&harness).unwrap_or_else(|| {
|
||||
panic!(
|
||||
"setup: no marker visible after the stream\nscreen:\n{}",
|
||||
harness.screen_contents()
|
||||
)
|
||||
});
|
||||
|
||||
harness.inject_keys(b"\t").expect("focus scrollback (tab)");
|
||||
harness.update(Duration::from_millis(500));
|
||||
// Footer wait is best-effort, as in drive_to_scrollback_with_turn.
|
||||
let _ = harness.wait_for_text("Space:prompt", Duration::from_secs(5));
|
||||
|
||||
// Quiesce, then live-parser watermark (renders_on_action.rs pattern).
|
||||
harness.update(Duration::from_millis(300));
|
||||
harness.reset_timing();
|
||||
|
||||
(harness, content, top_before)
|
||||
}
|
||||
|
||||
/// End marker of a [`spawn_streaming_marker_turn`] stream: the final streamed
|
||||
/// word. The spawn guards pin it off-screen while the tail is in flight;
|
||||
/// bottom-pinned follow renders it once the viewport tracks the stream to
|
||||
/// its end — the witness for both "stream still in flight" and "viewport
|
||||
/// followed the live bottom" assertions.
|
||||
pub(crate) const STREAM_END_SENTINEL: &str = "STREAMDONE";
|
||||
|
||||
/// Turn text for [`spawn_streaming_marker_turn`]: the whole fenced
|
||||
/// `marker_count`-marker block rides the FIRST delta (the mock splits deltas
|
||||
/// on single spaces and the block contains none), so scrollable content
|
||||
/// exists immediately; the space-separated `tail_words` tail then streams
|
||||
/// word-by-word, ending in [`STREAM_END_SENTINEL`].
|
||||
fn streaming_marker_turn_text(marker_count: usize, tail_words: usize) -> String {
|
||||
let mut s = marker_response(MOCK_RESPONSE_SENTINEL, marker_count);
|
||||
for i in 0..tail_words {
|
||||
s.push_str(&format!("TAIL-{i:04} "));
|
||||
}
|
||||
s.push_str(STREAM_END_SENTINEL);
|
||||
s
|
||||
}
|
||||
|
||||
/// Spawn the pager onto a turn that is STILL STREAMING and provably cannot
|
||||
/// complete (the shared preamble of the mid-stream wheel tests): the marker
|
||||
/// block arrives on the first delta, the tail keeps deltas in flight at
|
||||
/// `chunk_delay` per SSE event, and the mock's completion gate
|
||||
/// (`hold_agent_completions`) holds the turn's terminal event — every
|
||||
/// assertion against the returned harness is mid-turn by construction until
|
||||
/// the caller releases the gate. `extra_env` is appended to the controller's
|
||||
/// pager env (e.g. the forced-wheel pricing vars). Ends after the setup
|
||||
/// guards; the caller owns all wheel activity, the gate release, and the
|
||||
/// completion wait.
|
||||
///
|
||||
/// Returns the harness, the content controller (it owns the mock server AND
|
||||
/// the held gate — callers must eventually call `release_agent_completions()`
|
||||
/// for the turn to finish), and the topmost visible marker index as the
|
||||
/// movement baseline, taken bottom-pinned in follow mode with the first
|
||||
/// marker off-screen-top and [`STREAM_END_SENTINEL`] not yet on screen
|
||||
/// (guards panic with the screen contents otherwise).
|
||||
///
|
||||
/// Destructure the controller into a live binding (`content` / `_content`),
|
||||
/// never `_` — a `_` binding drops it immediately, killing the mock server
|
||||
/// mid-test and surfacing as a confusing stream timeout.
|
||||
///
|
||||
/// Harness mirror: `spawn_streaming_marker_session` in
|
||||
/// `kigi-pager-pty-harness/src/scroll_matrix/session.rs` is a port of
|
||||
/// this preamble — fixes must flow both ways.
|
||||
pub(crate) async fn spawn_streaming_marker_turn(
|
||||
marker_count: usize,
|
||||
tail_words: usize,
|
||||
chunk_delay: Duration,
|
||||
extra_env: &[(&str, &str)],
|
||||
) -> (PtyHarness, ContentController, usize) {
|
||||
let content = ContentController::start().await.expect("start content");
|
||||
content.set_chunk_delay(Some(chunk_delay));
|
||||
content.hold_agent_completions();
|
||||
// set_turns (not set_response): only agent turns ride the completion
|
||||
// gate; aux title/classifier requests fall through to echo untouched.
|
||||
content.set_turns([streaming_marker_turn_text(marker_count, tail_words)]);
|
||||
|
||||
let binary = pager_binary().expect("resolve pager binary");
|
||||
// spawn_with_content minus the fixed env: content env + the caller's.
|
||||
let content_env = content.env_for_pager();
|
||||
let mut env: Vec<(&str, &str)> = content_env
|
||||
.iter()
|
||||
.map(|(k, v)| (k.as_str(), v.as_str()))
|
||||
.collect();
|
||||
env.extend_from_slice(extra_env);
|
||||
let mut harness = PtyHarness::new(&binary, DEFAULT_ROWS, DEFAULT_COLS, &[], &env)
|
||||
.expect("spawn pager with content");
|
||||
|
||||
harness
|
||||
.wait_for_text(WELCOME_SCREEN_SENTINEL, WELCOME_TIMEOUT)
|
||||
.expect("welcome text");
|
||||
harness
|
||||
.inject_keys(format!("{PROMPT}\r").as_bytes())
|
||||
.expect("submit prompt");
|
||||
// The last marker rides the first delta, so this waits only for the
|
||||
// stream to START, not to finish — the tail is still streaming after.
|
||||
harness
|
||||
.wait_for_text(&marker_line(marker_count - 1), Duration::from_secs(30))
|
||||
.expect("marker block streamed");
|
||||
harness.update(Duration::from_millis(300));
|
||||
|
||||
// Setup guards, all taken while bottom-pinned in follow mode:
|
||||
// the transcript overflows the viewport (something to scroll to)…
|
||||
assert!(
|
||||
marker_screen_row(&harness, &marker_line(0)).is_none(),
|
||||
"setup: {} already visible → transcript not taller than the screen\nscreen:\n{}",
|
||||
marker_line(0),
|
||||
harness.screen_contents()
|
||||
);
|
||||
// …and the last chunk has NOT streamed yet (bottom-pinned follow shows
|
||||
// the newest content, so a finished stream would render the sentinel).
|
||||
assert!(
|
||||
!harness.contains_text(STREAM_END_SENTINEL),
|
||||
"setup: {STREAM_END_SENTINEL} already on screen — the paced tail finished \
|
||||
before the test's wheel activity could overlap the stream\nscreen:\n{}",
|
||||
harness.screen_contents()
|
||||
);
|
||||
let top_before = topmost_visible_marker(&harness).unwrap_or_else(|| {
|
||||
panic!(
|
||||
"setup: no marker visible mid-stream\nscreen:\n{}",
|
||||
harness.screen_contents()
|
||||
)
|
||||
});
|
||||
|
||||
(harness, content, top_before)
|
||||
}
|
||||
Reference in New Issue
Block a user