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:
2026-07-17 05:31:01 -04:00
commit d6c20fc13f
2612 changed files with 1353757 additions and 0 deletions
@@ -0,0 +1,298 @@
//! Layout cache for the list pane.
//!
//! Tracks per-item heights and prefix sums so that scroll-position ↔ item-index
//! conversions are fast. Two variants:
//!
//! - [`FixedHeight`] — all items have height 1 (NoWrap mode). Everything is O(1).
//! - [`Variable`] — items have different heights (Wrap mode). Uses a prefix-sum
//! vec for O(log n) position lookups.
/// Wrap mode for the list pane.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum WrapMode {
/// Soft-wrap lines at viewport width. Variable height per item.
/// Requires full layout cache.
Wrap,
/// No wrapping — each item is exactly 1 visual line, truncated with `…`.
/// Layout is trivial O(1).
NoWrap,
}
/// Layout cache — an enum to support the fixed-height fast path.
#[derive(Debug, Clone)]
pub enum ListLayoutCache {
/// All items are height 1 (NoWrap mode). No allocation needed.
FixedHeight {
/// Number of items (= total height in visual lines).
count: usize,
},
/// Variable-height items (Wrap mode).
Variable {
/// Width at which heights were computed.
width: u16,
/// Per-item heights (indexed by *visible* index when filtered).
heights: Vec<u16>,
/// Prefix sums: `prefix_sums[i]` = sum of `heights[0..i]`.
///
/// Length = `heights.len() + 1`. `prefix_sums[0] = 0`.
/// `prefix_sums[n] = total_height`.
prefix_sums: Vec<usize>,
},
}
impl ListLayoutCache {
// -----------------------------------------------------------------------
// Constructors
// -----------------------------------------------------------------------
/// Create a fixed-height cache for `count` items (all height 1).
pub fn fixed(count: usize) -> Self {
Self::FixedHeight { count }
}
/// Build a variable-height cache from an iterator of per-item heights.
pub fn from_heights(width: u16, heights: impl IntoIterator<Item = u16>) -> Self {
let heights: Vec<u16> = heights.into_iter().collect();
let mut prefix_sums = Vec::with_capacity(heights.len() + 1);
prefix_sums.push(0);
for &h in &heights {
let prev = *prefix_sums.last().unwrap();
prefix_sums.push(prev + h as usize);
}
Self::Variable {
width,
heights,
prefix_sums,
}
}
/// Extend an existing `Variable` cache with additional item heights.
///
/// Used for **incremental append**: when new items arrive, we compute
/// heights only for the new items and extend the prefix-sum array.
///
/// Panics if `self` is `FixedHeight` — caller must ensure the mode matches.
pub fn extend_heights(&mut self, new_heights: impl IntoIterator<Item = u16>) {
match self {
Self::Variable {
heights,
prefix_sums,
..
} => {
for h in new_heights {
let prev = *prefix_sums.last().unwrap();
prefix_sums.push(prev + h as usize);
heights.push(h);
}
}
Self::FixedHeight { .. } => {
panic!("extend_heights called on FixedHeight cache");
}
}
}
// -----------------------------------------------------------------------
// Queries
// -----------------------------------------------------------------------
/// Total height in visual lines.
pub fn total_height(&self) -> usize {
match self {
Self::FixedHeight { count } => *count,
Self::Variable { prefix_sums, .. } => *prefix_sums.last().unwrap_or(&0),
}
}
/// Number of items in the cache.
pub fn item_count(&self) -> usize {
match self {
Self::FixedHeight { count } => *count,
Self::Variable { heights, .. } => heights.len(),
}
}
/// Virtual-y position (in visual lines from top) of item at `idx`.
///
/// For `FixedHeight`, this is just `idx`.
pub fn virtual_y(&self, idx: usize) -> usize {
match self {
Self::FixedHeight { .. } => idx,
Self::Variable { prefix_sums, .. } => prefix_sums.get(idx).copied().unwrap_or(0),
}
}
/// Height of item at `idx` in visual lines.
pub fn item_height(&self, idx: usize) -> u16 {
match self {
Self::FixedHeight { .. } => 1,
Self::Variable { heights, .. } => heights.get(idx).copied().unwrap_or(1),
}
}
/// Find the item index whose virtual-y range contains `y`.
///
/// For `FixedHeight`, this is just `y` (clamped to `count - 1`).
/// For `Variable`, binary search on prefix sums — O(log n).
///
/// Returns `None` if the cache is empty.
pub fn item_at_y(&self, y: usize) -> Option<usize> {
match self {
Self::FixedHeight { count } => {
if *count == 0 {
None
} else {
Some(y.min(*count - 1))
}
}
Self::Variable { prefix_sums, .. } => {
if prefix_sums.len() <= 1 {
return None; // empty
}
// Binary search: find the largest i such that prefix_sums[i] <= y.
// partition_point returns the first index where prefix_sums[i] > y,
// so we subtract 1.
let pos = prefix_sums.partition_point(|&s| s <= y);
let idx = pos.saturating_sub(1);
// Clamp to valid item range
let max_idx = prefix_sums.len() - 2; // last valid item index
Some(idx.min(max_idx))
}
}
}
/// Width at which this cache was computed (only meaningful for `Variable`).
pub fn cached_width(&self) -> Option<u16> {
match self {
Self::FixedHeight { .. } => None,
Self::Variable { width, .. } => Some(*width),
}
}
}
// ===========================================================================
// Tests
// ===========================================================================
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn fixed_height_basics() {
let cache = ListLayoutCache::fixed(5);
assert_eq!(cache.total_height(), 5);
assert_eq!(cache.item_count(), 5);
assert_eq!(cache.virtual_y(0), 0);
assert_eq!(cache.virtual_y(3), 3);
assert_eq!(cache.item_height(0), 1);
assert_eq!(cache.item_height(4), 1);
assert_eq!(cache.item_at_y(0), Some(0));
assert_eq!(cache.item_at_y(4), Some(4));
// Clamped
assert_eq!(cache.item_at_y(100), Some(4));
}
#[test]
fn fixed_height_empty() {
let cache = ListLayoutCache::fixed(0);
assert_eq!(cache.total_height(), 0);
assert_eq!(cache.item_count(), 0);
assert_eq!(cache.item_at_y(0), None);
}
#[test]
fn variable_height_basics() {
// Items with heights: 3, 1, 2, 4
let cache = ListLayoutCache::from_heights(80, vec![3, 1, 2, 4]);
assert_eq!(cache.total_height(), 10);
assert_eq!(cache.item_count(), 4);
// virtual_y positions: 0, 3, 4, 6
assert_eq!(cache.virtual_y(0), 0);
assert_eq!(cache.virtual_y(1), 3);
assert_eq!(cache.virtual_y(2), 4);
assert_eq!(cache.virtual_y(3), 6);
assert_eq!(cache.item_height(0), 3);
assert_eq!(cache.item_height(1), 1);
assert_eq!(cache.item_height(2), 2);
assert_eq!(cache.item_height(3), 4);
}
#[test]
fn variable_height_item_at_y() {
// Items with heights: 3, 1, 2, 4 → prefix_sums: [0, 3, 4, 6, 10]
let cache = ListLayoutCache::from_heights(80, vec![3, 1, 2, 4]);
// y=0,1,2 → item 0
assert_eq!(cache.item_at_y(0), Some(0));
assert_eq!(cache.item_at_y(1), Some(0));
assert_eq!(cache.item_at_y(2), Some(0));
// y=3 → item 1
assert_eq!(cache.item_at_y(3), Some(1));
// y=4,5 → item 2
assert_eq!(cache.item_at_y(4), Some(2));
assert_eq!(cache.item_at_y(5), Some(2));
// y=6,7,8,9 → item 3
assert_eq!(cache.item_at_y(6), Some(3));
assert_eq!(cache.item_at_y(9), Some(3));
// y=10+ → clamped to item 3
assert_eq!(cache.item_at_y(10), Some(3));
assert_eq!(cache.item_at_y(100), Some(3));
}
#[test]
fn variable_height_empty() {
let cache = ListLayoutCache::from_heights(80, Vec::<u16>::new());
assert_eq!(cache.total_height(), 0);
assert_eq!(cache.item_count(), 0);
assert_eq!(cache.item_at_y(0), None);
}
#[test]
fn variable_height_single_item() {
let cache = ListLayoutCache::from_heights(80, vec![5]);
assert_eq!(cache.total_height(), 5);
assert_eq!(cache.item_count(), 1);
assert_eq!(cache.virtual_y(0), 0);
assert_eq!(cache.item_at_y(0), Some(0));
assert_eq!(cache.item_at_y(4), Some(0));
assert_eq!(cache.item_at_y(5), Some(0)); // clamped
}
#[test]
fn cached_width() {
let fixed = ListLayoutCache::fixed(5);
assert_eq!(fixed.cached_width(), None);
let var = ListLayoutCache::from_heights(120, vec![1, 2]);
assert_eq!(var.cached_width(), Some(120));
}
#[test]
fn extend_heights_appends() {
let mut cache = ListLayoutCache::from_heights(80, vec![3, 1]);
assert_eq!(cache.item_count(), 2);
assert_eq!(cache.total_height(), 4);
cache.extend_heights(vec![2, 4]);
assert_eq!(cache.item_count(), 4);
assert_eq!(cache.total_height(), 10);
// Prefix sums: [0, 3, 4, 6, 10]
assert_eq!(cache.virtual_y(0), 0);
assert_eq!(cache.virtual_y(1), 3);
assert_eq!(cache.virtual_y(2), 4);
assert_eq!(cache.virtual_y(3), 6);
assert_eq!(cache.item_height(2), 2);
assert_eq!(cache.item_height(3), 4);
}
#[test]
fn extend_heights_empty_iter_is_noop() {
let mut cache = ListLayoutCache::from_heights(80, vec![3, 1]);
cache.extend_heights(Vec::<u16>::new());
assert_eq!(cache.item_count(), 2);
assert_eq!(cache.total_height(), 4);
}
}
@@ -0,0 +1,316 @@
//! Generic scrollable list pane widget.
//!
//! `ListPaneState` + `ListPane<T>` provide a reusable, scrollable, selectable
//! list component. The state is non-generic and owns only scroll/selection/layout
//! data; item data lives in an external model and is borrowed via
//! [`ListPaneState::prepare_layout`].
//!
//! Designed for three concrete use cases:
//! - **Tracing pane** (100K+ entries, append-only, NoWrap, follow mode)
//! - **Todo pane** (<10 items, random mutations, Wrap)
//! - **Background task pane** (<10 items, random mutations, NoWrap)
mod layout;
mod render;
mod state;
pub use crate::search::QueryKind;
pub use layout::{ListLayoutCache, WrapMode};
pub use render::ListPane;
pub use state::{
FilterMatcher, InputBarMode, ListFilter, ListMatcher, ListPaneConfig, ListPaneState, MatchMode,
};
use ratatui::buffer::Buffer;
use ratatui::layout::Rect;
use ratatui::style::Color;
use ratatui::text::Line;
// ---------------------------------------------------------------------------
// ListPaneStyle — configurable colors for the framework's post-pass overlays
// ---------------------------------------------------------------------------
/// Visual style configuration for a `ListPane`.
///
/// Controls colors for selection highlighting, input bar, and other
/// framework-level overlays. Match highlights use style inversion
/// (REVERSED modifier) and don't need configurable colors.
///
/// Items do **not** need to know about these — the framework applies them
/// as post-passes after each item renders.
#[derive(Debug, Clone, Copy)]
pub struct ListPaneStyle {
/// Background color for the selected item row (cursor line).
pub selection_bg: Color,
/// Background color for the visual selection range (not the cursor line).
/// Slightly distinct from cursor bg, distinguishing range from cursor.
pub visual_select_bg: Color,
/// Background color for the input bar (search/filter).
pub input_bar_bg: Color,
/// Foreground color for the prompt prefix (`/`, `f>`).
pub input_bar_prompt_fg: Color,
/// Foreground color for the typed query text.
pub input_bar_text_fg: Color,
/// Scrollbar track background color.
pub scrollbar_bg: Color,
/// Scrollbar thumb foreground color.
pub scrollbar_fg: Color,
/// Corner indicator color (▲ ▼ for scroll position hints).
pub indicator_fg: Color,
/// Follow mode indicator color (▶ in bottom-right when following).
/// Distinct from `indicator_fg` so it's visible against content.
pub follow_indicator_fg: Color,
/// "Copied!" toast foreground color.
pub toast_fg: Color,
/// When true, the cursor line uses `visual_select_bg` when inside a
/// visual selection (uniform range appearance). The cursor is then
/// distinguished only by the `prefix_cursor` style, not by background.
///
/// When false (default), the cursor line always uses `selection_bg`,
/// even within a visual selection.
pub uniform_visual_bg: bool,
/// When false, the right-corner scroll indicators (▲/▼) are suppressed.
/// Used by panes that draw their own scroll affordance (e.g. the tasks
/// pane draws the same ▲/▼ centered on dedicated rows). Defaults to `true`.
pub show_corner_indicators: bool,
}
impl Default for ListPaneStyle {
fn default() -> Self {
let theme = crate::theme::Theme::current();
Self {
// Palette defaults — sourced from theme to ensure quantization.
selection_bg: theme.bg_highlight,
visual_select_bg: theme.bg_visual,
input_bar_bg: theme.bg_base,
input_bar_prompt_fg: theme.command,
input_bar_text_fg: theme.text_secondary,
scrollbar_bg: theme.bg_base,
scrollbar_fg: theme.scrollbar_fg,
indicator_fg: theme.gray,
follow_indicator_fg: theme.command,
toast_fg: theme.accent_user,
uniform_visual_bg: false,
show_corner_indicators: true,
}
}
}
// ---------------------------------------------------------------------------
// ListItem trait
// ---------------------------------------------------------------------------
/// Trait that items in a `ListPane` must implement.
///
/// Items are owned by the **model** (not the view). The view borrows them
/// through `&[T]` in [`ListPaneState::prepare_layout`] and
/// [`ListPane::new`].
///
/// ## Rendering: two modes
///
/// **Content-based (preferred):** implement [`content()`] and optionally
/// [`prefix()`]. The framework handles wrapping, truncation, highlighting,
/// and selection overlays automatically. This is the right choice for most
/// items.
///
/// **Custom rendering (escape hatch):** override [`render()`] to paint
/// directly into a buffer. Use this only when the content/prefix model
/// doesn't fit (e.g. diff hunks with side-by-side layout). You must also
/// override [`desired_height()`] when using custom rendering.
///
/// Items that implement [`content()`] (non-empty Line) get framework
/// rendering; the default [`render()`] and [`desired_height()`] are derived
/// automatically. Items that override [`render()`] bypass the framework.
pub trait ListItem {
// =======================================================================
// Content-based API (preferred)
// =======================================================================
/// The styled content to display — one logical line of text.
///
/// The framework handles wrapping (Wrap mode) and truncation (NoWrap mode)
/// based on this content. Return a reference to a stored `Line`.
///
/// Default returns an empty `Line` (signals "use custom `render()`").
fn content(&self) -> &Line<'_> {
static EMPTY: std::sync::LazyLock<Line<'static>> = std::sync::LazyLock::new(Line::default);
&EMPTY
}
/// Optional prefix column (checkbox, spinner, timestamp, etc.).
///
/// Rendered in a fixed-width column at the left edge of the item.
/// In Wrap mode, continuation lines are indented by the prefix width.
///
/// Returned by value since prefixes are small and often constructed
/// dynamically (spinner frame, elapsed timer, checkbox toggle).
fn prefix(&self) -> Option<Line<'_>> {
None
}
/// Optional prefix for items in the visual selection range (not the cursor line).
///
/// Called instead of `prefix()` when the item is in the visual selection
/// range but NOT the cursor line. Default falls back to `prefix()`.
fn prefix_in_selection(&self) -> Option<Line<'_>> {
self.prefix()
}
/// Optional prefix for the cursor line (the focused/active item).
///
/// Called instead of `prefix()` when the item is the cursor line.
/// Default falls back to `prefix()`.
fn prefix_cursor(&self) -> Option<Line<'_>> {
self.prefix()
}
/// Optional full-width background color for this item.
///
/// When `Some(color)`, the framework fills the entire item row(s) with
/// this background color before rendering content. Used for code blocks
/// in markdown viewers.
fn background(&self) -> Option<Color> {
None
}
// =======================================================================
// Custom rendering API (escape hatch)
// =======================================================================
/// Render this item into the given area.
///
/// Override this **only** when the content/prefix model doesn't fit.
/// When using the content-based API, leave this as the default (no-op).
///
/// The framework calls this only when `content()` returns an empty Line.
fn render(&self, _area: Rect, _buf: &mut Buffer, _selected: bool, _focused: bool) {}
/// Height in visual lines at the given `width` when soft-wrapping.
///
/// In `NoWrap` mode the pane ignores this and uses height = 1.
/// Must be ≥ 1.
///
/// Default implementation computes from [`content()`] and [`prefix()`].
/// Override only when using custom [`render()`].
fn desired_height(&self, width: u16) -> u16 {
if width == 0 {
return 1;
}
let prefix_w = self.prefix().map(|p| line_display_width(&p)).unwrap_or(0);
let content_w = line_display_width(self.content());
if content_w == 0 {
return 1;
}
let text_area = (width as usize).saturating_sub(prefix_w);
if text_area == 0 {
return 1;
}
// Use actual word-wrap line count via textwrap (not character-count
// division). The cheap ceil(chars/width) estimate underestimates
// because word-aware wrapping produces more lines when words can't
// fit at line boundaries.
//
// We use textwrap::wrap directly (cheap — just computes break
// positions) rather than word_wrap_line (expensive — builds styled
// Lines). Uses the same FirstFit options as the rendering pipeline.
let flat: String = self
.content()
.spans
.iter()
.map(|s| s.content.as_ref())
.collect();
let opts = textwrap::Options::new(text_area)
.wrap_algorithm(textwrap::WrapAlgorithm::FirstFit)
.break_words(true);
(textwrap::wrap(&flat, opts).len() as u16).max(1)
}
// =======================================================================
// Identity & behavior
// =======================================================================
/// Stable identity that survives insertions, removals, and reordering.
///
/// Must be unique within the list. Used so that selection state persists
/// across mutations without index arithmetic.
fn stable_id(&self) -> u64;
/// Whether this item can be selected. Return `false` for separator rows.
fn is_selectable(&self) -> bool {
true
}
/// Source line number for goto-line (`:N`) navigation.
///
/// When items have a meaningful source line number (e.g., file viewer
/// lines), return `Some(n)` so goto-line targets the correct item even
/// when the visual index differs (e.g., interleaved comment lines).
/// Return `None` (default) to use the visual index.
fn goto_line_number(&self) -> Option<usize> {
None
}
/// Whether this item needs periodic tick updates (e.g. elapsed timer).
fn needs_tick(&self) -> bool {
false
}
// =======================================================================
// Search / filter
// =======================================================================
/// Plain text for search/filter matching.
///
/// The framework calls `regex.is_match(item.search_text())` during
/// filtering and `regex.find_iter(item.search_text())` for highlight
/// rendering. Byte offsets in this string correspond to the text
/// content rendered starting at column [`search_text_col_offset`].
///
/// Default returns `""` (item not searchable/filterable).
fn search_text(&self) -> &str {
""
}
/// Column offset where `search_text()` content begins in the rendered output.
///
/// The framework uses this to position match highlights correctly.
///
/// Default derives from [`prefix()`] display width. Override only
/// when using custom [`render()`] with a non-standard layout.
fn search_text_col_offset(&self) -> u16 {
self.prefix()
.map(|p| line_display_width(&p) as u16)
.unwrap_or(0)
}
/// Text to copy when `y` is pressed.
///
/// Default extracts plain text from `content()`. Override for items
/// that use custom `render()` with empty `content()`.
fn copy_text(&self) -> String {
self.content()
.spans
.iter()
.map(|s| s.content.as_ref())
.collect()
}
}
/// Compute the display width of a ratatui `Line` (sum of span display widths).
pub(crate) fn line_display_width(line: &Line<'_>) -> usize {
line.spans
.iter()
.map(|s| unicode_width::UnicodeWidthStr::width(s.content.as_ref()))
.sum()
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff