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).
262 lines
9.4 KiB
Rust
262 lines
9.4 KiB
Rust
//! Minimal-mode plan-approval host (design PR10).
|
|
//!
|
|
//! The full TUI renders plan approval as a fullscreen line-viewer plus a live
|
|
//! feedback prompt. Minimal takes a simpler route: the **whole plan is committed
|
|
//! into native scrollback** as a normal conversation block (see
|
|
//! [`maybe_commit_plan`]), so it reads and scrolls exactly like the rest of the
|
|
//! transcript. The prompt-anchored live region then holds only the decision
|
|
//! controls — approve / revise / keep planning — plus the feedback input when
|
|
//! revising. Nothing of the plan body is drawn under the prompt.
|
|
//!
|
|
//! Input routing is unchanged: while `line_viewer.is_some()` the agent's input
|
|
//! handler already routes keys to `handle_line_viewer_key` (Preview focus:
|
|
//! `a` approve / `s`/`Tab` revise / `q` keep planning) and `handle_plan_feedback_key`
|
|
//! (Prompt focus: type feedback, `Enter` send, `Esc` back). Minimal keeps the
|
|
//! line viewer open (so those keys fire) but renders this compact controls strip
|
|
//! in place of the never-drawn fullscreen viewer.
|
|
|
|
use ratatui::buffer::Buffer;
|
|
use ratatui::layout::Rect;
|
|
use ratatui::style::{Color, Modifier, Style};
|
|
use ratatui::text::Span;
|
|
|
|
use kigi_tui::app::agent_view::AgentView;
|
|
use kigi_tui::app::app_view::{ActiveView, AppView};
|
|
use kigi_tui::minimal_api;
|
|
use kigi_tui::scrollback::block::RenderBlock;
|
|
use kigi_tui::theme::Theme;
|
|
use kigi_tui::views::plan_approval_view::PlanApprovalFocus;
|
|
use kigi_tui::views::prompt_widget::PromptStyle;
|
|
|
|
/// The active plan-approval focus, defaulting to `Preview`.
|
|
fn focus(agent: &AgentView) -> PlanApprovalFocus {
|
|
minimal_api::plan_approval_view(agent)
|
|
.map(|p| p.focus)
|
|
.unwrap_or(PlanApprovalFocus::Preview)
|
|
}
|
|
|
|
/// Scrollback notice when exit_plan_mode parks with no plan body.
|
|
///
|
|
/// Kept short and plain (no markdown chrome) so native scrollback reads cleanly
|
|
/// under minimal mode's chromeless commit path.
|
|
const EMPTY_PLAN_SCROLLBACK: &str = "\
|
|
No plan written yet.
|
|
|
|
Approve to leave plan mode and start implementing, request changes to send the \
|
|
agent back to planning, or quit to abandon.";
|
|
|
|
/// Controls-strip header for the parked plan-approval surface.
|
|
fn plan_header(has_plan: bool) -> &'static str {
|
|
if has_plan {
|
|
"Plan ready for review"
|
|
} else {
|
|
"No plan written yet"
|
|
}
|
|
}
|
|
|
|
/// Body committed into native scrollback for a parked plan approval.
|
|
fn plan_scrollback_body(plan_content: Option<&str>) -> String {
|
|
plan_content
|
|
.filter(|s| !s.trim().is_empty())
|
|
.map(str::to_owned)
|
|
.unwrap_or_else(|| EMPTY_PLAN_SCROLLBACK.to_owned())
|
|
}
|
|
|
|
/// Commit the active plan into native scrollback, once per plan (and once per
|
|
/// revision).
|
|
///
|
|
/// Minimal has no separate plan pane: the terminal's scrollback *is* the
|
|
/// history, so the plan is pushed as an ordinary finalized agent-message block
|
|
/// and printed into native scrollback by the normal commit pass — leaving only
|
|
/// the decision controls under the prompt. De-duplicated by the plan's
|
|
/// `tool_call_id`; a revised plan arrives as a fresh ExitPlanMode with a new id
|
|
/// and is committed as its own block. Empty / whitespace-only plans still commit
|
|
/// a short notice so the user sees *why* approval is parked (otherwise only the
|
|
/// controls strip appears and the session looks stuck).
|
|
///
|
|
/// NOTE (draw-path state mutation + replay durability): this pushes into
|
|
/// `ScrollbackState` from the render path — a deliberate exception, since the
|
|
/// plan block must enter the normal commit pipeline. The pushed block is
|
|
/// client-render state, not a server event: a resumed session will not replay
|
|
/// it, so post-reload `/transcript` shows the plan only through whatever the
|
|
/// agent itself messaged. Accepted for v1 (the live session — the mode's whole
|
|
/// surface — is consistent).
|
|
///
|
|
/// Call once per frame from [`crate::draw`], before the commit pass.
|
|
pub fn maybe_commit_plan(app: &mut AppView) {
|
|
let ActiveView::Agent(id) = &app.active_view else {
|
|
return;
|
|
};
|
|
let id = *id;
|
|
|
|
// Extract the plan (owned) under a short immutable borrow so the mutable
|
|
// scrollback push and the `minimal_state` read/write below don't overlap it.
|
|
let plan = app.agents.get(&id).and_then(|agent| {
|
|
minimal_api::plan_approval_view(agent).map(|pav| {
|
|
let content = plan_scrollback_body(pav.plan_content.as_deref());
|
|
(pav.tool_call_id.clone(), content)
|
|
})
|
|
});
|
|
let Some((tool_call_id, content)) = plan else {
|
|
return;
|
|
};
|
|
|
|
if minimal_api::minimal_committed_plan_id(app) == Some(tool_call_id.as_str()) {
|
|
return; // already emitted this plan
|
|
}
|
|
|
|
// Mark the plan as emitted only when the block was actually pushed: the
|
|
// agent borrow can't fail here (the plan was just extracted from it), but
|
|
// if it ever did, stamping the id anyway would treat the plan as committed
|
|
// while nothing ever reaches native scrollback.
|
|
if let Some(agent) = app.agents.get_mut(&id) {
|
|
agent
|
|
.scrollback
|
|
.push_block(RenderBlock::agent_message(content));
|
|
minimal_api::set_minimal_committed_plan_id(app, Some(tool_call_id));
|
|
}
|
|
}
|
|
|
|
/// Desired controls-strip height: header + controls + optional feedback input.
|
|
pub fn height(agent: &AgentView) -> u16 {
|
|
let input = if focus(agent) == PlanApprovalFocus::Prompt {
|
|
1
|
|
} else {
|
|
0
|
|
};
|
|
// header (1) + controls (1) + input (0/1)
|
|
2u16.saturating_add(input)
|
|
}
|
|
|
|
/// Render the compact plan-approval controls strip into `area`. The plan itself
|
|
/// lives in native scrollback ([`maybe_commit_plan`]); this only draws the
|
|
/// header, the decision hint, and — when revising — the feedback input. Returns
|
|
/// the text cursor when the feedback input is focused, else `None`.
|
|
pub fn render(
|
|
buf: &mut Buffer,
|
|
area: Rect,
|
|
agent: &mut AgentView,
|
|
theme: &Theme,
|
|
) -> Option<(u16, u16)> {
|
|
if area.height == 0 || area.width < 4 {
|
|
return None;
|
|
}
|
|
let foc = focus(agent);
|
|
let input_h: u16 = if foc == PlanApprovalFocus::Prompt {
|
|
1
|
|
} else {
|
|
0
|
|
};
|
|
|
|
// header (1) · controls (1) · input (0/1)
|
|
let controls_y = (area.y + area.height).saturating_sub(1 + input_h);
|
|
|
|
// ── header ──
|
|
let has_plan = minimal_api::plan_approval_view(agent)
|
|
.map(|p| p.has_plan)
|
|
.unwrap_or(false);
|
|
let header_style = Style::default()
|
|
.fg(theme.accent_user)
|
|
.bg(Color::Reset)
|
|
.add_modifier(Modifier::BOLD);
|
|
buf.set_style(
|
|
Rect { height: 1, ..area },
|
|
Style::default().bg(Color::Reset),
|
|
);
|
|
buf.set_span(
|
|
area.x,
|
|
area.y,
|
|
&Span::styled(plan_header(has_plan), header_style),
|
|
area.width,
|
|
);
|
|
|
|
// ── controls hint ──
|
|
let has_content = minimal_api::plan_approval_view(agent)
|
|
.map(|p| !p.comments.is_empty())
|
|
.unwrap_or(false)
|
|
|| !agent.prompt.text().trim().is_empty();
|
|
// Tab reopens the preview (including the empty-plan placeholder).
|
|
let hint = match foc {
|
|
PlanApprovalFocus::Prompt if has_content => {
|
|
"enter request changes \u{00b7} tab plan \u{00b7} esc back"
|
|
}
|
|
PlanApprovalFocus::Prompt => "enter approve \u{00b7} tab plan \u{00b7} esc back",
|
|
PlanApprovalFocus::Commenting => "enter save comment \u{00b7} esc cancel",
|
|
PlanApprovalFocus::Preview => "a approve \u{00b7} s revise \u{00b7} q keep planning",
|
|
};
|
|
let hint_style = theme.dim().bg(Color::Reset);
|
|
let controls_rect = Rect {
|
|
x: area.x,
|
|
y: controls_y,
|
|
width: area.width,
|
|
height: 1,
|
|
};
|
|
buf.set_style(controls_rect, hint_style);
|
|
buf.set_span(
|
|
area.x,
|
|
controls_y,
|
|
&Span::styled(hint, hint_style),
|
|
area.width,
|
|
);
|
|
|
|
// ── feedback input (revise mode) ──
|
|
if input_h > 0 {
|
|
let row = Rect {
|
|
x: area.x,
|
|
y: (area.y + area.height).saturating_sub(1),
|
|
width: area.width,
|
|
height: 1,
|
|
};
|
|
let style = input_style(theme);
|
|
buf.set_style(row, Style::default().bg(theme.bg_visual));
|
|
return agent.prompt.draw(buf, row, None, &style, None).cursor_pos;
|
|
}
|
|
None
|
|
}
|
|
|
|
/// Chromeless prompt style for the feedback editor (the modal supplies framing).
|
|
fn input_style(theme: &Theme) -> PromptStyle {
|
|
PromptStyle {
|
|
focused: true,
|
|
show_prefix: false,
|
|
vpad_top: 0,
|
|
compact: false,
|
|
chrome: false,
|
|
chrome_pad_left: 0,
|
|
chrome_pad_right: 0,
|
|
bg_override: Some(theme.bg_visual),
|
|
accent_color_override: None,
|
|
border_color_override: None,
|
|
prefix_override: None,
|
|
placeholder_override: None,
|
|
show_accent_line: false,
|
|
show_borders: false,
|
|
title: None,
|
|
image_preview: true,
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn empty_plan_header_is_explicit() {
|
|
assert_eq!(plan_header(true), "Plan ready for review");
|
|
assert_eq!(plan_header(false), "No plan written yet");
|
|
}
|
|
|
|
#[test]
|
|
fn empty_plan_scrollback_uses_notice_not_silence() {
|
|
let body = plan_scrollback_body(None);
|
|
assert!(body.contains("No plan written yet"));
|
|
assert!(body.contains("Approve"));
|
|
|
|
let whitespace = plan_scrollback_body(Some(" \n\t "));
|
|
assert_eq!(whitespace, body, "whitespace-only counts as empty");
|
|
|
|
let real = plan_scrollback_body(Some("# Plan\n- do it"));
|
|
assert_eq!(real, "# Plan\n- do it");
|
|
}
|
|
}
|