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,332 @@
//! Popup dialog for creating a new worktree with an optional label.
use ratatui::buffer::Buffer;
use ratatui::layout::{Constraint, Flex, Layout, Rect};
use ratatui::style::{Modifier, Style};
use ratatui::text::{Line, Span};
use ratatui::widgets::Widget;
use unicode_segmentation::UnicodeSegmentation;
use unicode_width::UnicodeWidthStr;
use crate::app::app_view::NewWorktreeDialogState;
use crate::theme::Theme;
/// Minimum dialog width (fits title + empty input + hints comfortably).
const MIN_DIALOG_WIDTH: u16 = 50;
const DIALOG_HEIGHT: u16 = 5;
/// Left/right padding inside the border (`inner_x = dialog.x + 2`).
const INNER_PAD: u16 = 4;
const LABEL_PREFIX: &str = "Name (optional): ";
/// Render the new-worktree popup dialog centered on screen.
///
/// The dialog grows with the typed label (up to the available terminal
/// width) so long names stay fully visible. When the terminal itself is
/// too narrow for the full name, the input scrolls to keep the cursor
/// (end of the label) in view, with a leading `…` when scrolled.
pub fn render_new_worktree_dialog(area: Rect, buf: &mut Buffer, state: &NewWorktreeDialogState) {
let theme = Theme::current();
let dialog_width = dialog_width_for(area.width, &state.label_input);
if area.height < DIALOG_HEIGHT || area.width < 20 {
// Too small to render — draw a minimal "resize" hint so the user
// knows the dialog is still active and can press Esc to dismiss.
if area.height >= 1 && area.width >= 16 {
let hint = Line::from(Span::styled(
"[Esc] to close",
Style::default().fg(theme.gray_dim),
));
hint.render(Rect::new(area.x, area.y, area.width.min(16), 1), buf);
}
return;
}
let [_, dialog_h, _] = Layout::horizontal([
Constraint::Min(0),
Constraint::Length(dialog_width),
Constraint::Min(0),
])
.flex(Flex::Center)
.areas(area);
let [_, dialog, _] = Layout::vertical([
Constraint::Min(0),
Constraint::Length(DIALOG_HEIGHT),
Constraint::Min(0),
])
.flex(Flex::Center)
.areas(dialog_h);
// Draw background
let bg_style = Style::default().bg(theme.bg_dark);
for y in dialog.y..dialog.y + dialog.height {
for x in dialog.x..dialog.x + dialog.width {
if let Some(cell) = buf.cell_mut((x, y)) {
cell.set_char(' ');
cell.set_style(bg_style);
}
}
}
// Draw border
let border_style = Style::default().fg(theme.gray_dim).bg(theme.bg_dark);
// Top border
if let Some(cell) = buf.cell_mut((dialog.x, dialog.y)) {
cell.set_char('\u{256D}');
cell.set_style(border_style);
}
for x in dialog.x + 1..dialog.x + dialog.width - 1 {
if let Some(cell) = buf.cell_mut((x, dialog.y)) {
cell.set_char('\u{2500}');
cell.set_style(border_style);
}
}
if let Some(cell) = buf.cell_mut((dialog.x + dialog.width - 1, dialog.y)) {
cell.set_char('\u{256E}');
cell.set_style(border_style);
}
// Bottom border
let bottom = dialog.y + dialog.height - 1;
if let Some(cell) = buf.cell_mut((dialog.x, bottom)) {
cell.set_char('\u{2570}');
cell.set_style(border_style);
}
for x in dialog.x + 1..dialog.x + dialog.width - 1 {
if let Some(cell) = buf.cell_mut((x, bottom)) {
cell.set_char('\u{2500}');
cell.set_style(border_style);
}
}
if let Some(cell) = buf.cell_mut((dialog.x + dialog.width - 1, bottom)) {
cell.set_char('\u{256F}');
cell.set_style(border_style);
}
// Side borders
for y in dialog.y + 1..dialog.y + dialog.height - 1 {
if let Some(cell) = buf.cell_mut((dialog.x, y)) {
cell.set_char('\u{2502}');
cell.set_style(border_style);
}
if let Some(cell) = buf.cell_mut((dialog.x + dialog.width - 1, y)) {
cell.set_char('\u{2502}');
cell.set_style(border_style);
}
}
let inner_x = dialog.x + 2;
let inner_width = dialog.width.saturating_sub(INNER_PAD);
// Row 1: Title
let title = Line::from(Span::styled(
"New Worktree",
Style::default()
.fg(theme.text_primary)
.add_modifier(Modifier::BOLD),
));
title.render(Rect::new(inner_x, dialog.y + 1, inner_width, 1), buf);
// Row 2: Label input — grow with content; scroll when still too wide.
let prefix_w = LABEL_PREFIX.width() as u16;
let cursor_w = 1u16;
let input_budget = inner_width
.saturating_sub(prefix_w)
.saturating_sub(cursor_w) as usize;
let visible_input = visible_input_suffix(&state.label_input, input_budget);
let prefix_span = Span::styled(LABEL_PREFIX, Style::default().fg(theme.gray_bright));
let input_span = Span::styled(visible_input, Style::default().fg(theme.text_primary));
let cursor_span = Span::styled("\u{2588}", Style::default().fg(theme.accent_user));
let input_line = Line::from(vec![prefix_span, input_span, cursor_span]);
input_line.render(Rect::new(inner_x, dialog.y + 2, inner_width, 1), buf);
// Row 3: Hints
let hints = Line::from(vec![
Span::styled(
"enter",
Style::default()
.fg(theme.accent_user)
.add_modifier(Modifier::BOLD),
),
Span::styled(" = create ", Style::default().fg(theme.gray)),
Span::styled(
"esc",
Style::default()
.fg(theme.accent_user)
.add_modifier(Modifier::BOLD),
),
Span::styled(" = cancel", Style::default().fg(theme.gray)),
]);
hints.render(Rect::new(inner_x, dialog.y + 3, inner_width, 1), buf);
}
/// Dialog width that fits the typed label, clamped to the available area.
fn dialog_width_for(area_width: u16, label: &str) -> u16 {
let max_width = area_width.saturating_sub(4);
// prefix + label + block cursor + inner pad
let needed = (LABEL_PREFIX.width() + label.width() + 1 + INNER_PAD as usize) as u16;
needed.max(MIN_DIALOG_WIDTH).min(max_width)
}
/// Return the visible portion of `label` for an end-anchored input field.
///
/// When `label` fits in `budget` columns, returns it unchanged. Otherwise
/// returns a leading `…` plus the suffix that fits, so the cursor at the
/// end of the label stays visible while typing a long name.
///
/// Walks Unicode grapheme clusters (not scalar values) so combining marks
/// and ZWJ sequences are never split across the scroll boundary.
fn visible_input_suffix(label: &str, budget: usize) -> String {
if budget == 0 {
return String::new();
}
if label.width() <= budget {
return label.to_string();
}
if budget == 1 {
return "".to_string();
}
let suffix_budget = budget - 1; // reserve one column for leading …
let mut width = 0usize;
let mut start = label.len();
let graphemes: Vec<(usize, &str)> = label.grapheme_indices(true).collect();
for &(i, g) in graphemes.iter().rev() {
let cw = UnicodeWidthStr::width(g);
if width + cw > suffix_budget {
break;
}
width += cw;
start = i;
}
format!("{}", &label[start..])
}
#[cfg(test)]
mod tests {
use super::*;
use ratatui::buffer::Buffer;
use ratatui::layout::Rect;
fn render_to_text(area: Rect, label: &str) -> String {
let mut buf = Buffer::empty(area);
let state = NewWorktreeDialogState {
label_input: label.to_string(),
};
render_new_worktree_dialog(area, &mut buf, &state);
let mut lines = Vec::new();
for y in 0..area.height {
let mut row = String::new();
for x in 0..area.width {
row.push_str(buf[(x, y)].symbol());
}
lines.push(row);
}
lines.join("\n")
}
#[test]
fn empty_dialog_uses_minimum_width() {
assert_eq!(dialog_width_for(120, ""), MIN_DIALOG_WIDTH);
assert_eq!(dialog_width_for(40, ""), 36); // area.width - 4
}
#[test]
fn dialog_grows_with_long_label() {
let label = "a-very-long-worktree-name-that-exceeds-fifty";
let width = dialog_width_for(120, label);
assert!(
width > MIN_DIALOG_WIDTH,
"expected dialog wider than min for long label, got {width}"
);
// Full label + chrome must fit inside the grown dialog.
let inner = width.saturating_sub(INNER_PAD) as usize;
let needed = LABEL_PREFIX.width() + label.width() + 1;
assert!(
needed <= inner,
"grown dialog inner={inner} should fit needed={needed}"
);
}
#[test]
fn dialog_clamps_to_terminal_width() {
let label = "x".repeat(100);
let width = dialog_width_for(60, &label);
assert_eq!(width, 56); // 60 - 4
}
#[test]
fn visible_suffix_keeps_end_when_scrolled() {
let label = "abcdefghijklmnopqrstuvwxyz0123456789";
let visible = visible_input_suffix(label, 10);
assert!(
visible.starts_with('…'),
"expected leading ellipsis: {visible}"
);
assert!(
visible.ends_with("0123456789") || visible.ends_with("123456789"),
"expected end of label visible: {visible}"
);
assert_eq!(visible.width(), 10);
}
#[test]
fn visible_suffix_unchanged_when_fits() {
assert_eq!(visible_input_suffix("short", 20), "short");
}
#[test]
fn visible_suffix_does_not_split_grapheme_clusters() {
// "e" + combining acute (U+0301) is one grapheme; pad so we must scroll.
let cluster = "e\u{0301}";
let label = format!("{}{}", "x".repeat(20), cluster);
let visible = visible_input_suffix(&label, 8);
assert!(
visible.starts_with('…'),
"expected leading ellipsis: {visible}"
);
// Either the full cluster is present, or it was dropped as a unit —
// never a lone combining mark after the ellipsis.
let after_ellipsis = &visible[visible.char_indices().nth(1).map(|(i, _)| i).unwrap_or(0)..];
assert!(
!after_ellipsis.starts_with('\u{0301}'),
"must not start scrolled suffix on a combining mark: {visible:?}"
);
if after_ellipsis.contains('e') {
assert!(
after_ellipsis.contains(cluster),
"base 'e' must keep its combining mark: {visible:?}"
);
}
assert!(visible.width() <= 8, "width overflow: {visible:?}");
}
#[test]
fn long_name_fully_visible_on_wide_terminal() {
let area = Rect::new(0, 0, 100, 20);
let label = "biscuit-worktree-popup-long-name-fix";
let text = render_to_text(area, label);
assert!(
text.contains(label),
"full long name must be visible on a wide terminal:\n{text}"
);
assert!(text.contains("New Worktree"), "title missing:\n{text}");
}
#[test]
fn long_name_end_visible_on_narrow_terminal() {
// Terminal narrower than the full label — end (cursor side) must show.
let area = Rect::new(0, 0, 40, 12);
let label = "super-long-worktree-name-that-will-not-fit";
let text = render_to_text(area, label);
let tail = &label[label.len().saturating_sub(8)..];
assert!(
text.contains(tail),
"end of long name must remain visible when scrolled:\n{text}"
);
assert!(
text.contains('…') || text.contains(tail),
"expected scrolled indicator or tail:\n{text}"
);
}
}