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
+309
View File
@@ -0,0 +1,309 @@
use crate::error::MermaidError;
use crate::theme::MermaidTheme;
use std::f64::consts::PI;
/// Mermaid 11.12.2 default pie chart colors (from the default theme).
/// pie1 = primaryColor (#ECECFF), pie2 = secondaryColor (#ffffde),
/// pie3pie12 computed via adjust/darken on primary/secondary/tertiary.
const MERMAID_PIE_COLORS: [&str; 12] = [
"#ECECFF", // pie1 primaryColor
"#ffffde", // pie2 secondaryColor
"hsl(80, 100%, 56.2745098039%)", // pie3 adjust(tertiaryColor, l:-40)
"hsl(240, 60%, 86.2745098039%)", // pie4 adjust(primaryColor, l:-10)
"hsl(120, 100%, 66.2745098039%)", // pie5 adjust(secondaryColor, l:-30)
"hsl(80, 100%, 76.2745098039%)", // pie6 adjust(tertiaryColor, l:-20)
"hsl(300, 60%, 76.2745098039%)", // pie7 adjust(primaryColor, h:60, l:-20)
"hsl(180, 60%, 56.2745098039%)", // pie8 adjust(primaryColor, h:-60, l:-40)
"hsl(0, 60%, 56.2745098039%)", // pie9 adjust(primaryColor, h:120, l:-40)
"hsl(300, 60%, 56.2745098039%)", // pie10 adjust(primaryColor, h:60, l:-40)
"hsl(150, 60%, 56.2745098039%)", // pie11 adjust(primaryColor, h:-90, l:-40)
"hsl(0, 60%, 66.2745098039%)", // pie12 adjust(primaryColor, h:120, l:-30)
];
// Mermaid 11.12.2 pie chart constants (from pieRenderer.ts and default config).
const PIE_HEIGHT: f64 = 450.0;
const PIE_WIDTH: f64 = 450.0;
const MARGIN: f64 = 40.0;
const RADIUS: f64 = (PIE_WIDTH / 2.0) - MARGIN; // 185
const OUTER_STROKE_WIDTH: f64 = 2.0;
const OUTER_RADIUS: f64 = RADIUS + OUTER_STROKE_WIDTH / 2.0; // 186
const TEXT_POSITION: f64 = 0.75;
const LEGEND_RECT_SIZE: f64 = 18.0;
const LEGEND_SPACING: f64 = 4.0;
const FONT_FAMILY: &str = "trebuchet ms,verdana,arial,sans-serif";
pub fn render_pie_diagram_to_svg(
mermaid_source: &str,
_theme: &MermaidTheme,
) -> Result<String, MermaidError> {
let chart = parse_pie_diagram(mermaid_source)?;
let total: f64 = chart.slices.iter().map(|s| s.value).sum();
if total <= 0.0 {
return Err(MermaidError::ParseError {
line: 1,
message: "Pie diagram total must be > 0".to_string(),
});
}
// Filter slices ≥1% and sort descending by value (matches d3.pie() default).
let mut slices: Vec<&PieSlice> = chart
.slices
.iter()
.filter(|s| s.value / total * 100.0 >= 1.0)
.collect();
slices.sort_by(|a, b| b.value.partial_cmp(&a.value).unwrap());
// All slices for the legend (unfiltered, original order).
let all_slices: Vec<&PieSlice> = chart.slices.iter().collect();
// Center of the pie in the translated group coordinate system is (0, 0).
let cx = PIE_WIDTH / 2.0;
let cy = PIE_HEIGHT / 2.0;
// Estimate legend text width (rough: 10px per char at 17px font).
let longest_label_len = all_slices
.iter()
.map(|s| {
if chart.show_data {
format!("{} [{}]", s.label, s.value).len()
} else {
s.label.len()
}
})
.max()
.unwrap_or(0);
let legend_text_width = longest_label_len as f64 * 10.0;
let total_width = PIE_WIDTH + MARGIN + LEGEND_RECT_SIZE + LEGEND_SPACING + legend_text_width;
let mut svg = String::new();
// Mermaid uses a CSS style block for pie chart classes.
svg.push_str(&format!(
"<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 {total_width:.4} {PIE_HEIGHT}\" \
style=\"max-width: {total_width:.3}px; background-color: white;\" \
role=\"graphics-document document\" aria-roledescription=\"pie\">"
));
// Inline CSS matching Mermaid 11.12.2 pieStyles.
svg.push_str(&format!(
"<style>\
.pieCircle{{stroke:black;stroke-width:2px;opacity:0.7;}}\
.pieOuterCircle{{stroke:black;stroke-width:2px;fill:none;}}\
.pieTitleText{{text-anchor:middle;font-size:25px;fill:black;font-family:{FONT_FAMILY};}}\
.slice{{font-family:{FONT_FAMILY};fill:#333;font-size:17px;}}\
.legend text{{fill:black;font-family:{FONT_FAMILY};font-size:17px;}}\
</style>"
));
// Group translated to pie center (matches mermaid: translate(pieWidth/2, height/2)).
svg.push_str(&format!("<g transform=\"translate({cx},{cy})\">"));
// Outer circle.
svg.push_str(&format!(
"<circle cx=\"0\" cy=\"0\" r=\"{OUTER_RADIUS}\" class=\"pieOuterCircle\"/>"
));
// Draw pie slices.
// d3.pie() default: startAngle=0 (12 o'clock), endAngle=2π, clockwise.
// In SVG with translate to center, angle 0 points up (-y).
let mut angle = -PI / 2.0;
for (idx, slice) in slices.iter().enumerate() {
let pct = (slice.value / total * 100.0).round() as i64;
if pct == 0 {
continue;
}
let frac = slice.value / total;
let sweep = frac * 2.0 * PI;
let next = angle + sweep;
let (x0, y0) = polar(0.0, 0.0, RADIUS, angle);
let (x1, y1) = polar(0.0, 0.0, RADIUS, next);
let large_arc = if sweep > PI { 1 } else { 0 };
let fill = MERMAID_PIE_COLORS[idx % MERMAID_PIE_COLORS.len()];
// Slice path.
svg.push_str(&format!(
"<path d=\"M0,0L{x0:.3},{y0:.3}A{RADIUS},{RADIUS},0,{large_arc},1,{x1:.3},{y1:.3}Z\" \
fill=\"{fill}\" class=\"pieCircle\"/>"
));
// Percentage label inside the slice at textPosition (0.75) of radius.
let label_r = RADIUS * TEXT_POSITION;
let mid = angle + sweep / 2.0;
let (lx, ly) = polar(0.0, 0.0, label_r, mid);
svg.push_str(&format!(
"<text transform=\"translate({lx:.3},{ly:.3})\" class=\"slice\" \
style=\"text-anchor: middle;\">{pct}%</text>"
));
angle = next;
}
// Title (positioned above the pie).
if let Some(title) = &chart.title {
let title_y = -((PIE_HEIGHT - 50.0) / 2.0);
svg.push_str(&format!(
"<text x=\"0\" y=\"{title_y:.0}\" class=\"pieTitleText\">{}</text>",
escape_xml(title)
));
}
// Legend (to the right of the pie).
let legend_h = LEGEND_RECT_SIZE + LEGEND_SPACING;
let legend_offset = legend_h * all_slices.len() as f64 / 2.0;
let legend_x = 12.0 * LEGEND_RECT_SIZE; // 216
// Build a color map that assigns colors to labels in the same order as the
// sorted/filtered slices (matching d3.scaleOrdinal behavior).
let mut color_map: Vec<(&str, &str)> = Vec::new();
for (idx, slice) in slices.iter().enumerate() {
color_map.push((
&slice.label,
MERMAID_PIE_COLORS[idx % MERMAID_PIE_COLORS.len()],
));
}
for (legend_idx, slice) in all_slices.iter().enumerate() {
let vert = legend_idx as f64 * legend_h - legend_offset;
let color = color_map
.iter()
.find(|(label, _)| *label == slice.label)
.map(|(_, c)| *c)
.unwrap_or(MERMAID_PIE_COLORS[legend_idx % MERMAID_PIE_COLORS.len()]);
svg.push_str(&format!(
"<g class=\"legend\" transform=\"translate({legend_x},{vert})\">"
));
svg.push_str(&format!(
"<rect width=\"{LEGEND_RECT_SIZE}\" height=\"{LEGEND_RECT_SIZE}\" \
style=\"fill: {color}; stroke: {color};\"/>"
));
let label_text = if chart.show_data {
format!("{} [{}]", slice.label, slice.value)
} else {
slice.label.clone()
};
let text_x = LEGEND_RECT_SIZE + LEGEND_SPACING;
let text_y = LEGEND_RECT_SIZE - LEGEND_SPACING;
svg.push_str(&format!(
"<text x=\"{text_x}\" y=\"{text_y}\">{}</text>",
escape_xml(&label_text)
));
svg.push_str("</g>");
}
svg.push_str("</g>"); // close main group
svg.push_str("</svg>");
Ok(svg)
}
#[derive(Debug, Clone)]
struct PieChart {
title: Option<String>,
show_data: bool,
slices: Vec<PieSlice>,
}
#[derive(Debug, Clone)]
struct PieSlice {
label: String,
value: f64,
}
fn parse_pie_diagram(input: &str) -> Result<PieChart, MermaidError> {
let lines: Vec<&str> = input.lines().collect();
let mut i = 0_usize;
let mut show_data = false;
while i < lines.len() {
let line = lines[i].trim();
if line.is_empty() || line.starts_with("%%") {
i += 1;
continue;
}
let mut tokens = line.split_whitespace();
let first = tokens.next().unwrap_or("");
if first == "pie" {
show_data = tokens.any(|t| t == "showData");
i += 1;
break;
}
return Err(MermaidError::ParseError {
line: i + 1,
message: "Expected 'pie' declaration".to_string(),
});
}
let mut title: Option<String> = None;
let mut slices: Vec<PieSlice> = Vec::new();
while i < lines.len() {
let raw = lines[i];
let line = raw.trim();
let line_no = i + 1;
i += 1;
if line.is_empty() || line.starts_with("%%") {
continue;
}
if let Some(rest) = line.strip_prefix("title ") {
let t = rest.trim();
if !t.is_empty() {
title = Some(t.to_string());
}
continue;
}
let Some((label_raw, value_raw)) = line.split_once(':') else {
return Err(MermaidError::ParseError {
line: line_no,
message: format!("Invalid pie slice: {line}"),
});
};
let mut label = label_raw.trim().to_string();
if let Some(stripped) = label.strip_prefix('"').and_then(|s| s.strip_suffix('"')) {
label = stripped.to_string();
}
let value_str = value_raw.trim();
let value: f64 = value_str.parse().map_err(|_| MermaidError::ParseError {
line: line_no,
message: format!("Invalid pie value: {value_str}"),
})?;
slices.push(PieSlice { label, value });
}
if slices.is_empty() {
return Err(MermaidError::ParseError {
line: 1,
message: "Pie diagram requires at least one slice".to_string(),
});
}
Ok(PieChart {
title,
show_data,
slices,
})
}
fn polar(cx: f64, cy: f64, r: f64, angle: f64) -> (f64, f64) {
(cx + r * angle.cos(), cy + r * angle.sin())
}
fn escape_xml(s: &str) -> String {
s.replace('&', "&amp;")
.replace('<', "&lt;")
.replace('>', "&gt;")
.replace('"', "&quot;")
.replace('\'', "&apos;")
}