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).
187 lines
7.1 KiB
Rust
187 lines
7.1 KiB
Rust
//! Layer 2a: Screen state tracking via `alacritty_terminal` (ptyctl).
|
|
//!
|
|
//! Parses raw PTY output through a headless terminal emulator and provides
|
|
//! queries for what the user would see on screen.
|
|
|
|
use ptyctl::styled::StyledLine;
|
|
use ptyctl::term::{ScreenOpts, ScreenOutput, SessionListener, Terminal};
|
|
|
|
/// Tracks the virtual terminal screen state by feeding raw PTY output
|
|
/// through an `alacritty_terminal`-based headless terminal (via ptyctl).
|
|
pub struct ScreenTracker {
|
|
terminal: Terminal,
|
|
/// Receives terminal-generated replies (cursor-position reports, device
|
|
/// attributes, color queries, …) the emulator emits while parsing input.
|
|
/// Drained by [`ScreenTracker::drain_responses`] so the harness can forward
|
|
/// them back to the child (real terminals answer these automatically).
|
|
pty_write_rx: tokio::sync::mpsc::UnboundedReceiver<Vec<u8>>,
|
|
}
|
|
|
|
impl ScreenTracker {
|
|
/// Create a new tracker for a terminal with the given dimensions.
|
|
pub fn new(rows: u16, cols: u16) -> Self {
|
|
let (tx, rx) = tokio::sync::mpsc::unbounded_channel();
|
|
let listener = SessionListener::new(tx);
|
|
Self {
|
|
terminal: Terminal::new(cols, rows, listener),
|
|
pty_write_rx: rx,
|
|
}
|
|
}
|
|
|
|
/// Feed raw PTY output bytes into the terminal emulator.
|
|
pub fn feed(&mut self, bytes: &[u8]) {
|
|
self.terminal.feed(bytes);
|
|
}
|
|
|
|
/// Drain any terminal-generated replies queued while parsing fed input
|
|
/// (cursor-position reports answering `ESC[6n`, device attributes, color
|
|
/// queries, …), concatenated in order. Empty when nothing was queued.
|
|
///
|
|
/// These MUST be written back to the PTY or programs that probe the
|
|
/// terminal will hang or time out — most relevant here, the inline
|
|
/// viewport's startup cursor-position query that minimal mode depends on
|
|
/// (a timeout there downgrades `--minimal` to full-screen inline). A real
|
|
/// terminal answers automatically; the harness forwards these in
|
|
/// [`crate::PtyHarness::update`] when response forwarding is enabled.
|
|
pub fn drain_responses(&mut self) -> Vec<u8> {
|
|
let mut out = Vec::new();
|
|
while let Ok(bytes) = self.pty_write_rx.try_recv() {
|
|
out.extend_from_slice(&bytes);
|
|
}
|
|
out
|
|
}
|
|
|
|
/// Return structured screen contents (no escape codes).
|
|
pub fn output(&self) -> ScreenOutput {
|
|
self.terminal.screen_content(&ScreenOpts::default())
|
|
}
|
|
|
|
/// Return the full text contents of the screen (no escape codes).
|
|
pub fn contents(&self) -> String {
|
|
self.output().lines.join("\n")
|
|
}
|
|
|
|
/// Check whether the screen contains the given text substring.
|
|
pub fn contains(&self, text: &str) -> bool {
|
|
self.contents().contains(text)
|
|
}
|
|
|
|
/// Return the current cursor position as `(row, col)` (0-indexed, matching
|
|
/// the original vt100 convention used by existing tests).
|
|
pub fn cursor_position(&self) -> (u16, u16) {
|
|
let pos = self.terminal.cursor_position();
|
|
// ptyctl cursor is 1-indexed; the harness API is 0-indexed.
|
|
(
|
|
(pos.row as u16).saturating_sub(1),
|
|
(pos.col as u16).saturating_sub(1),
|
|
)
|
|
}
|
|
|
|
/// Resize the virtual terminal to new dimensions.
|
|
pub fn resize(&mut self, rows: u16, cols: u16) {
|
|
self.terminal.resize(cols, rows);
|
|
}
|
|
|
|
/// Return the full screen with style information for visual artifacts.
|
|
pub fn styled(&self) -> Vec<StyledLine> {
|
|
self.terminal.screen_styled(&ScreenOpts::default())
|
|
}
|
|
|
|
/// Render the current screen as an HTML document.
|
|
pub fn html(&self) -> String {
|
|
self.terminal.screen_html(&ScreenOpts::default())
|
|
}
|
|
|
|
/// Access the underlying ptyctl `Terminal` for advanced queries
|
|
/// (styled output, scrollback, terminal modes, etc.).
|
|
pub fn terminal(&self) -> &Terminal {
|
|
&self.terminal
|
|
}
|
|
|
|
/// Number of lines in the terminal's scrollback history — content that has
|
|
/// scrolled *above* the visible screen. This is where minimal mode's
|
|
/// committed conversation blocks land (printed via `insert_before`).
|
|
pub fn scrollback_count(&self) -> usize {
|
|
self.terminal.scrollback_count()
|
|
}
|
|
|
|
/// The full scrollback history as text, oldest line first.
|
|
pub fn scrollback_text(&self) -> String {
|
|
let n = self.terminal.scrollback_count();
|
|
self.terminal
|
|
.scrollback_lines(n)
|
|
.into_iter()
|
|
.map(|l| l.text)
|
|
.collect::<Vec<_>>()
|
|
.join("\n")
|
|
}
|
|
|
|
/// Scrollback history plus the visible screen, joined oldest→newest:
|
|
/// everything a user could see by scrolling up. Minimal-mode committed
|
|
/// content may be in either region depending on how much has accumulated,
|
|
/// so assertions on committed output should use this.
|
|
pub fn full_text(&self) -> String {
|
|
let sb = self.scrollback_text();
|
|
let screen = self.contents();
|
|
if sb.is_empty() {
|
|
screen
|
|
} else {
|
|
format!("{sb}\n{screen}")
|
|
}
|
|
}
|
|
|
|
/// Whether scrollback + visible screen contains `text`.
|
|
pub fn full_contains(&self, text: &str) -> bool {
|
|
self.full_text().contains(text)
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
/// Lines pushed above a small screen must be readable via the scrollback
|
|
/// helpers — the property minimal-mode e2e tests rely on to assert that a
|
|
/// committed block reached native scrollback.
|
|
#[test]
|
|
fn scrolled_off_lines_are_captured_by_scrollback_helpers() {
|
|
// 3-row screen; print 8 numbered lines so the first ones scroll off.
|
|
let mut s = ScreenTracker::new(3, 20);
|
|
for i in 1..=8 {
|
|
s.feed(format!("line{i}\r\n").as_bytes());
|
|
}
|
|
// The earliest lines are no longer on the visible screen…
|
|
assert!(
|
|
!s.contains("line1"),
|
|
"line1 should have scrolled off-screen"
|
|
);
|
|
// …but they are in scrollback, and full_text sees everything.
|
|
assert!(s.scrollback_count() >= 5, "expected scrolled-off history");
|
|
assert!(s.scrollback_text().contains("line1"));
|
|
assert!(s.full_contains("line1"));
|
|
assert!(s.full_contains("line8"));
|
|
}
|
|
|
|
/// A DSR cursor-position query (`ESC[6n`) must produce a forwardable reply
|
|
/// (a CPR `ESC[<row>;<col>R`) — the mechanism minimal-mode tests rely on so
|
|
/// the inline viewport's startup cursor query completes. Without forwarding,
|
|
/// `--minimal` silently downgrades to full-screen inline.
|
|
#[test]
|
|
fn drain_responses_answers_cursor_position_query() {
|
|
let mut s = ScreenTracker::new(24, 80);
|
|
// Nothing queued before any query is fed.
|
|
assert!(s.drain_responses().is_empty());
|
|
|
|
s.feed(b"\x1b[6n");
|
|
let reply = s.drain_responses();
|
|
assert!(
|
|
reply.starts_with(b"\x1b[") && reply.ends_with(b"R"),
|
|
"expected a cursor-position report, got {:?}",
|
|
String::from_utf8_lossy(&reply)
|
|
);
|
|
|
|
// Drained exactly once — no duplicate delivery on the next call.
|
|
assert!(s.drain_responses().is_empty());
|
|
}
|
|
}
|