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:
commit
d6c20fc13f
2612 files changed
+1353757
No files matched your search
@@ -0,0 +1,438 @@
|
||||
//! Selection box rendering for v3 pager.
|
||||
//!
|
||||
//! The `SelectionBox` is computed by components (like ScrollbackPane) and rendered
|
||||
//! by the frame, allowing selection boxes to span component boundaries.
|
||||
|
||||
use ratatui::buffer::Buffer;
|
||||
use ratatui::layout::Rect;
|
||||
use ratatui::style::Style;
|
||||
|
||||
use crate::render::osc8::LinkOverlay;
|
||||
use crate::scrollback::text_selection::ResolvedSelectionModel;
|
||||
use crate::theme::Theme;
|
||||
|
||||
/// Box drawing characters for selection border.
|
||||
mod border_chars {
|
||||
pub const TOP_LEFT: char = '┌';
|
||||
pub const TOP_RIGHT: char = '┐';
|
||||
pub const BOTTOM_LEFT: char = '└';
|
||||
pub const BOTTOM_RIGHT: char = '┘';
|
||||
pub const VERTICAL: char = '│';
|
||||
/// Dashed vertical - used on edge rows when clipped to indicate continuation.
|
||||
pub const VERTICAL_DASHED: char = '┆';
|
||||
}
|
||||
|
||||
/// A selection box that can be drawn around a selected block.
|
||||
///
|
||||
/// The box consists of:
|
||||
/// - Side borders (│) on the left and right edges of `inner_area`
|
||||
/// - Top corners (┌┐) one row above `inner_area` (if `!top_clipped`)
|
||||
/// - Bottom corners (└┘) one row below `inner_area` (if `!bottom_clipped`)
|
||||
///
|
||||
/// This struct is returned by components (like ScrollbackPane) and rendered
|
||||
/// by the frame, allowing selection boxes to span component boundaries.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SelectionBox {
|
||||
/// The inner area surrounded by the selection border.
|
||||
pub inner_area: Rect,
|
||||
/// True if the block has rows clipped at top (scrolled out of view).
|
||||
pub top_clipped: bool,
|
||||
/// True if the block has rows clipped at bottom.
|
||||
pub bottom_clipped: bool,
|
||||
/// Style for the border (typically just fg color).
|
||||
pub style: Style,
|
||||
/// Whether to render a close control replacing the top-right corner.
|
||||
pub closable: bool,
|
||||
/// Whether the close control is currently hovered.
|
||||
pub close_hovered: bool,
|
||||
/// Optional close label; `None` uses default `✗`.
|
||||
pub close_label: Option<&'static str>,
|
||||
}
|
||||
|
||||
/// Output from render that needs post-processing.
|
||||
///
|
||||
/// Render returns this instead of mutating state, keeping render pure.
|
||||
/// The caller is responsible for rendering these elements after the main pass.
|
||||
///
|
||||
/// # Example
|
||||
/// ```ignore
|
||||
/// let output = pane.render_with_scratch(area, buf, &state, &mut scratch);
|
||||
///
|
||||
/// // Post-render pass
|
||||
/// if let Some(sel) = output.selection_box {
|
||||
/// sel.render(buf);
|
||||
/// }
|
||||
/// ```
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct RenderOutput {
|
||||
/// Selection box to render around the selected entry.
|
||||
/// Rendered after main content so it can span component boundaries.
|
||||
pub selection_box: Option<SelectionBox>,
|
||||
/// Scroll info for scrollbar rendering.
|
||||
/// Viewport uses this to render the scrollbar at the correct position.
|
||||
pub scroll_info: Option<ScrollInfo>,
|
||||
/// Screen area of the individual selected entry (within a group).
|
||||
/// Used by agent_view to position inline buttons on the correct row.
|
||||
pub selected_entry_area: Option<Rect>,
|
||||
/// Per-frame resolved selection metadata for visible content.
|
||||
pub selection_model: ResolvedSelectionModel,
|
||||
/// OSC 8 link overlay for post-flush emission.
|
||||
pub link_overlay: LinkOverlay,
|
||||
/// Inline media to render via post-flush escape sequences.
|
||||
pub inline_media: Vec<crate::scrollback::render::InlineMediaPlacement>,
|
||||
/// Mermaid diagram affordance rows to paint + register click hit-rects for.
|
||||
pub diagram_affordances: Vec<crate::scrollback::render::DiagramAffordancePlacement>,
|
||||
}
|
||||
|
||||
/// Scroll information for scrollbar rendering.
|
||||
#[derive(Debug, Clone, Copy, Default)]
|
||||
pub struct ScrollInfo {
|
||||
/// Current scroll offset (lines from top). `usize`: tall sessions exceed
|
||||
/// `u16::MAX`.
|
||||
pub scroll_offset: usize,
|
||||
/// Visible viewport height (lines). Stays `u16` (a terminal is never that tall).
|
||||
pub viewport_height: u16,
|
||||
/// Total content height (lines). `usize` for the same reason as `scroll_offset`.
|
||||
pub total_height: usize,
|
||||
}
|
||||
|
||||
impl RenderOutput {
|
||||
/// Create empty render output.
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
/// Create render output with a selection box.
|
||||
pub fn with_selection_box(selection_box: SelectionBox) -> Self {
|
||||
Self {
|
||||
selection_box: Some(selection_box),
|
||||
scroll_info: None,
|
||||
selected_entry_area: None,
|
||||
selection_model: ResolvedSelectionModel::default(),
|
||||
link_overlay: Default::default(),
|
||||
inline_media: Vec::new(),
|
||||
diagram_affordances: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Add scroll info to the output.
|
||||
pub fn with_scroll_info(mut self, scroll_info: ScrollInfo) -> Self {
|
||||
self.scroll_info = Some(scroll_info);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl SelectionBox {
|
||||
/// Create a new selection box with the given inner area and style.
|
||||
pub fn new(inner_area: Rect, style: Style) -> Self {
|
||||
Self {
|
||||
inner_area,
|
||||
top_clipped: false,
|
||||
bottom_clipped: false,
|
||||
style,
|
||||
closable: false,
|
||||
close_hovered: false,
|
||||
close_label: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Set whether the top is clipped (no top corners).
|
||||
pub fn with_top_clipped(mut self, clipped: bool) -> Self {
|
||||
self.top_clipped = clipped;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set whether the bottom is clipped (no bottom corners).
|
||||
pub fn with_bottom_clipped(mut self, clipped: bool) -> Self {
|
||||
self.bottom_clipped = clipped;
|
||||
self
|
||||
}
|
||||
|
||||
/// Enable a close control replacing the top-right corner `┐` (default: `✗`).
|
||||
///
|
||||
/// Normal state: same color as the border. Hovered: bright white.
|
||||
pub fn with_closable(mut self, closable: bool, hovered: bool) -> Self {
|
||||
self.closable = closable;
|
||||
self.close_hovered = hovered;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set close control label (`Some` implies closable).
|
||||
pub fn with_close_label(mut self, label: Option<&'static str>) -> Self {
|
||||
self.close_label = label;
|
||||
if label.is_some() {
|
||||
self.closable = true;
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
/// Hit-test rect for the close control, if it would be rendered.
|
||||
///
|
||||
/// Pure computation — does not touch the buffer. Use for mouse hit-testing.
|
||||
/// Returns `None` if not closable, top is clipped, or no room.
|
||||
pub fn close_button_rect(&self) -> Option<Rect> {
|
||||
if !self.closable || self.top_clipped || self.inner_area.y == 0 {
|
||||
return None;
|
||||
}
|
||||
let label_w = self
|
||||
.close_label
|
||||
.map(|s| s.chars().count() as u16)
|
||||
.unwrap_or(1)
|
||||
.max(1);
|
||||
let right_x = self.inner_area.x + self.inner_area.width.saturating_sub(1);
|
||||
let x = right_x.saturating_sub(label_w.saturating_sub(1));
|
||||
Some(Rect {
|
||||
x,
|
||||
y: self.inner_area.y - 1,
|
||||
width: label_w,
|
||||
height: 1,
|
||||
})
|
||||
}
|
||||
|
||||
/// Render the selection box to the buffer.
|
||||
///
|
||||
/// Draws:
|
||||
/// - Side borders (│) on left and right edges of inner_area
|
||||
/// - Dashed borders (┆) on edge rows when clipped, to indicate continuation
|
||||
/// - Top corners (┌┐) at inner_area.y - 1 if !top_clipped and y > 0
|
||||
/// - Bottom corners (└┘) at inner_area.y + height if !bottom_clipped
|
||||
/// - Close button (✗) left of ┐ if enabled
|
||||
pub fn render(&self, buf: &mut Buffer) {
|
||||
let area = self.inner_area;
|
||||
if area.width == 0 || area.height == 0 {
|
||||
return;
|
||||
}
|
||||
|
||||
let left_x = area.x;
|
||||
let right_x = area.x + area.width.saturating_sub(1);
|
||||
let y_top = area.y;
|
||||
let y_bottom = area.y + area.height.saturating_sub(1);
|
||||
|
||||
// Draw side borders
|
||||
for y in y_top..=y_bottom {
|
||||
let is_first_row = y == y_top;
|
||||
let is_last_row = y == y_bottom;
|
||||
let use_dashed =
|
||||
(is_first_row && self.top_clipped) || (is_last_row && self.bottom_clipped);
|
||||
|
||||
let vert_char = if use_dashed {
|
||||
border_chars::VERTICAL_DASHED
|
||||
} else {
|
||||
border_chars::VERTICAL
|
||||
};
|
||||
|
||||
if let Some(cell) = buf.cell_mut((left_x, y)) {
|
||||
cell.set_char(vert_char).set_style(self.style);
|
||||
}
|
||||
if let Some(cell) = buf.cell_mut((right_x, y)) {
|
||||
cell.set_char(vert_char).set_style(self.style);
|
||||
}
|
||||
}
|
||||
|
||||
// Draw top corners (if not clipped and there's room)
|
||||
if !self.top_clipped && y_top > 0 {
|
||||
let corner_y = y_top - 1;
|
||||
if let Some(cell) = buf.cell_mut((left_x, corner_y)) {
|
||||
cell.set_char(border_chars::TOP_LEFT).set_style(self.style);
|
||||
}
|
||||
// Close control replaces ┐, or draw normal corner
|
||||
if let Some(close_rect) = self.close_button_rect() {
|
||||
let style = if self.close_hovered {
|
||||
Style::default().fg(Theme::current().text_primary)
|
||||
} else {
|
||||
self.style
|
||||
};
|
||||
if let Some(label) = self.close_label {
|
||||
use crate::render::SafeBuf;
|
||||
buf.set_string_safe(close_rect.x, close_rect.y, label, style);
|
||||
} else if let Some(cell) = buf.cell_mut((close_rect.x, close_rect.y)) {
|
||||
cell.set_symbol(crate::glyphs::ballot_x()).set_style(style);
|
||||
}
|
||||
} else if let Some(cell) = buf.cell_mut((right_x, corner_y)) {
|
||||
cell.set_char(border_chars::TOP_RIGHT).set_style(self.style);
|
||||
}
|
||||
}
|
||||
|
||||
// Draw bottom corners (if not clipped)
|
||||
if !self.bottom_clipped {
|
||||
let corner_y = y_bottom + 1;
|
||||
if let Some(cell) = buf.cell_mut((left_x, corner_y)) {
|
||||
cell.set_char(border_chars::BOTTOM_LEFT)
|
||||
.set_style(self.style);
|
||||
}
|
||||
if let Some(cell) = buf.cell_mut((right_x, corner_y)) {
|
||||
cell.set_char(border_chars::BOTTOM_RIGHT)
|
||||
.set_style(self.style);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_selection_box_render() {
|
||||
let mut buf = Buffer::empty(Rect::new(0, 0, 10, 10));
|
||||
|
||||
let selection = SelectionBox::new(Rect::new(0, 2, 10, 4), Style::default());
|
||||
|
||||
selection.render(&mut buf);
|
||||
|
||||
// Check top corners at y=1 (inner_area.y - 1)
|
||||
assert_eq!(buf.cell((0, 1)).unwrap().symbol(), "┌");
|
||||
assert_eq!(buf.cell((9, 1)).unwrap().symbol(), "┐");
|
||||
|
||||
// Check side borders at y=2..=5 (all solid, not clipped)
|
||||
for y in 2..=5 {
|
||||
assert_eq!(buf.cell((0, y)).unwrap().symbol(), "│");
|
||||
assert_eq!(buf.cell((9, y)).unwrap().symbol(), "│");
|
||||
}
|
||||
|
||||
// Check bottom corners at y=6 (inner_area.y + height)
|
||||
assert_eq!(buf.cell((0, 6)).unwrap().symbol(), "└");
|
||||
assert_eq!(buf.cell((9, 6)).unwrap().symbol(), "┘");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_selection_box_top_clipped() {
|
||||
let mut buf = Buffer::empty(Rect::new(0, 0, 10, 10));
|
||||
|
||||
let selection =
|
||||
SelectionBox::new(Rect::new(0, 2, 10, 4), Style::default()).with_top_clipped(true);
|
||||
|
||||
selection.render(&mut buf);
|
||||
|
||||
// Top corners should NOT be drawn
|
||||
assert_ne!(buf.cell((0, 1)).unwrap().symbol(), "┌");
|
||||
assert_ne!(buf.cell((9, 1)).unwrap().symbol(), "┐");
|
||||
|
||||
// First row (y=2) should have DASHED borders
|
||||
assert_eq!(buf.cell((0, 2)).unwrap().symbol(), "┆");
|
||||
assert_eq!(buf.cell((9, 2)).unwrap().symbol(), "┆");
|
||||
|
||||
// Middle rows should have solid borders
|
||||
for y in 3..=5 {
|
||||
assert_eq!(buf.cell((0, y)).unwrap().symbol(), "│");
|
||||
assert_eq!(buf.cell((9, y)).unwrap().symbol(), "│");
|
||||
}
|
||||
|
||||
// Bottom corners should be drawn
|
||||
assert_eq!(buf.cell((0, 6)).unwrap().symbol(), "└");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_selection_box_bottom_clipped() {
|
||||
let mut buf = Buffer::empty(Rect::new(0, 0, 10, 10));
|
||||
|
||||
let selection =
|
||||
SelectionBox::new(Rect::new(0, 2, 10, 4), Style::default()).with_bottom_clipped(true);
|
||||
|
||||
selection.render(&mut buf);
|
||||
|
||||
// Top corners should be drawn
|
||||
assert_eq!(buf.cell((0, 1)).unwrap().symbol(), "┌");
|
||||
assert_eq!(buf.cell((9, 1)).unwrap().symbol(), "┐");
|
||||
|
||||
// First rows should have solid borders
|
||||
for y in 2..=4 {
|
||||
assert_eq!(buf.cell((0, y)).unwrap().symbol(), "│");
|
||||
assert_eq!(buf.cell((9, y)).unwrap().symbol(), "│");
|
||||
}
|
||||
|
||||
// Last row (y=5) should have DASHED borders
|
||||
assert_eq!(buf.cell((0, 5)).unwrap().symbol(), "┆");
|
||||
assert_eq!(buf.cell((9, 5)).unwrap().symbol(), "┆");
|
||||
|
||||
// Bottom corners should NOT be drawn
|
||||
assert_ne!(buf.cell((0, 6)).unwrap().symbol(), "└");
|
||||
assert_ne!(buf.cell((9, 6)).unwrap().symbol(), "┘");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_selection_box_both_clipped() {
|
||||
let mut buf = Buffer::empty(Rect::new(0, 0, 10, 10));
|
||||
|
||||
let selection = SelectionBox::new(Rect::new(0, 2, 10, 4), Style::default())
|
||||
.with_top_clipped(true)
|
||||
.with_bottom_clipped(true);
|
||||
|
||||
selection.render(&mut buf);
|
||||
|
||||
// No corners should be drawn
|
||||
assert_ne!(buf.cell((0, 1)).unwrap().symbol(), "┌");
|
||||
assert_ne!(buf.cell((0, 6)).unwrap().symbol(), "└");
|
||||
|
||||
// First row (y=2) should have DASHED borders
|
||||
assert_eq!(buf.cell((0, 2)).unwrap().symbol(), "┆");
|
||||
assert_eq!(buf.cell((9, 2)).unwrap().symbol(), "┆");
|
||||
|
||||
// Middle rows should have solid borders
|
||||
for y in 3..=4 {
|
||||
assert_eq!(buf.cell((0, y)).unwrap().symbol(), "│");
|
||||
assert_eq!(buf.cell((9, y)).unwrap().symbol(), "│");
|
||||
}
|
||||
|
||||
// Last row (y=5) should have DASHED borders
|
||||
assert_eq!(buf.cell((0, 5)).unwrap().symbol(), "┆");
|
||||
assert_eq!(buf.cell((9, 5)).unwrap().symbol(), "┆");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_selection_box_single_row_both_clipped() {
|
||||
// Edge case: only 1 row visible, both ends clipped
|
||||
let mut buf = Buffer::empty(Rect::new(0, 0, 10, 10));
|
||||
|
||||
let selection = SelectionBox::new(Rect::new(0, 3, 10, 1), Style::default())
|
||||
.with_top_clipped(true)
|
||||
.with_bottom_clipped(true);
|
||||
|
||||
selection.render(&mut buf);
|
||||
|
||||
// The single row should have DASHED borders (first row = last row, both clipped)
|
||||
assert_eq!(buf.cell((0, 3)).unwrap().symbol(), "┆");
|
||||
assert_eq!(buf.cell((9, 3)).unwrap().symbol(), "┆");
|
||||
|
||||
// No corners
|
||||
assert_ne!(buf.cell((0, 2)).unwrap().symbol(), "┌");
|
||||
assert_ne!(buf.cell((0, 4)).unwrap().symbol(), "└");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_selection_box_single_row_top_clipped_only() {
|
||||
// Edge case: only 1 row visible, only top clipped
|
||||
let mut buf = Buffer::empty(Rect::new(0, 0, 10, 10));
|
||||
|
||||
let selection =
|
||||
SelectionBox::new(Rect::new(0, 3, 10, 1), Style::default()).with_top_clipped(true);
|
||||
|
||||
selection.render(&mut buf);
|
||||
|
||||
// The single row should have DASHED borders (it's first row and top_clipped)
|
||||
assert_eq!(buf.cell((0, 3)).unwrap().symbol(), "┆");
|
||||
assert_eq!(buf.cell((9, 3)).unwrap().symbol(), "┆");
|
||||
|
||||
// Bottom corners should be drawn
|
||||
assert_eq!(buf.cell((0, 4)).unwrap().symbol(), "└");
|
||||
assert_eq!(buf.cell((9, 4)).unwrap().symbol(), "┘");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_selection_box_at_top_edge() {
|
||||
let mut buf = Buffer::empty(Rect::new(0, 0, 10, 10));
|
||||
|
||||
// Selection at y=0 (no room for top corners even if not clipped)
|
||||
let selection = SelectionBox::new(Rect::new(0, 0, 10, 4), Style::default());
|
||||
|
||||
selection.render(&mut buf);
|
||||
|
||||
// Side borders at y=0..=3 (all solid, not clipped)
|
||||
for y in 0..=3 {
|
||||
assert_eq!(buf.cell((0, y)).unwrap().symbol(), "│");
|
||||
}
|
||||
|
||||
// Bottom corners at y=4
|
||||
assert_eq!(buf.cell((0, 4)).unwrap().symbol(), "└");
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user