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:
@@ -0,0 +1,490 @@
|
||||
use agent_client_protocol as acp;
|
||||
use kigi_acp_lib::AcpResult;
|
||||
|
||||
pub use kigi_tools::implementations::grok_build::exit_plan_mode::{
|
||||
ExitPlanModeExtRequest, ExitPlanModeExtResponse,
|
||||
};
|
||||
|
||||
use crate::views::prompt_widget::StashedPrompt;
|
||||
|
||||
/// Placeholder body for the plan-approval preview when `exit_plan_mode` parks
|
||||
/// with no plan content (missing/empty `plan.md`, or a whitespace-only body).
|
||||
///
|
||||
/// Must be non-empty after trim so `LineViewerState::open_markdown_content`
|
||||
/// accepts it — empty bodies are rejected there.
|
||||
pub const EMPTY_PLAN_PLACEHOLDER: &str = "\
|
||||
# No plan written yet
|
||||
|
||||
The agent exited plan mode without writing a plan.
|
||||
|
||||
- **Approve** — leave plan mode and start implementing
|
||||
- **Request changes** — send the agent back to planning
|
||||
- **Quit** — abandon and turn plan mode off
|
||||
";
|
||||
|
||||
/// Status-line label while plan approval is parked.
|
||||
///
|
||||
/// Empty plans use an active decision prompt instead of "Waiting…", so the
|
||||
/// UI doesn't look stuck when there is no preview body to open.
|
||||
pub fn plan_approval_status_label(has_plan: bool) -> &'static str {
|
||||
if has_plan {
|
||||
"Waiting on plan approval"
|
||||
} else {
|
||||
"No plan written — approve or request changes"
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum PlanApprovalFocus {
|
||||
Preview,
|
||||
Prompt,
|
||||
Commenting,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum PlanReviewSource {
|
||||
Inline,
|
||||
FileBacked,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PlanComment {
|
||||
pub id: u64,
|
||||
pub line_range: std::ops::Range<usize>,
|
||||
pub text: String,
|
||||
}
|
||||
|
||||
pub struct PlanApprovalViewState {
|
||||
pub tool_call_id: String,
|
||||
pub has_plan: bool,
|
||||
pub plan_content: Option<String>,
|
||||
pub source: PlanReviewSource,
|
||||
pub stashed_prompt: StashedPrompt,
|
||||
pub response_tx: Option<tokio::sync::oneshot::Sender<AcpResult<acp::ExtResponse>>>,
|
||||
|
||||
pub focus: PlanApprovalFocus,
|
||||
pub comments: Vec<PlanComment>,
|
||||
pub next_comment_id: u64,
|
||||
pub editing_comment_id: Option<u64>,
|
||||
pub commenting_range: Option<std::ops::Range<usize>>,
|
||||
|
||||
pub stashed_feedback_prompt: Option<StashedPrompt>,
|
||||
}
|
||||
|
||||
impl PlanApprovalViewState {
|
||||
pub fn new(
|
||||
request: ExitPlanModeExtRequest,
|
||||
stashed_prompt: StashedPrompt,
|
||||
response_tx: tokio::sync::oneshot::Sender<AcpResult<acp::ExtResponse>>,
|
||||
) -> Self {
|
||||
Self::with_source(
|
||||
request,
|
||||
PlanReviewSource::Inline,
|
||||
stashed_prompt,
|
||||
response_tx,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn with_source(
|
||||
request: ExitPlanModeExtRequest,
|
||||
source: PlanReviewSource,
|
||||
stashed_prompt: StashedPrompt,
|
||||
response_tx: tokio::sync::oneshot::Sender<AcpResult<acp::ExtResponse>>,
|
||||
) -> Self {
|
||||
let plan_content = request.plan_content.filter(|s| !s.trim().is_empty());
|
||||
let has_plan = plan_content.is_some();
|
||||
Self {
|
||||
tool_call_id: request.tool_call_id,
|
||||
has_plan,
|
||||
plan_content,
|
||||
source,
|
||||
stashed_prompt,
|
||||
response_tx: Some(response_tx),
|
||||
focus: PlanApprovalFocus::Preview,
|
||||
comments: Vec::new(),
|
||||
next_comment_id: 0,
|
||||
editing_comment_id: None,
|
||||
commenting_range: None,
|
||||
stashed_feedback_prompt: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn format_feedback(&self, freeform: Option<&str>) -> String {
|
||||
let mut parts: Vec<String> = self
|
||||
.comments
|
||||
.iter()
|
||||
.map(|comment| match self.source {
|
||||
PlanReviewSource::Inline => {
|
||||
let label = if comment.line_range.len() == 1 {
|
||||
format!("Proposed plan line {}:", comment.line_range.start)
|
||||
} else {
|
||||
format!(
|
||||
"Proposed plan lines {}-{}:",
|
||||
comment.line_range.start,
|
||||
comment.line_range.end - 1
|
||||
)
|
||||
};
|
||||
let snippets =
|
||||
inline_plan_snippets(self.plan_content.as_deref(), &comment.line_range);
|
||||
format!("{label}\n{snippets}\n\nComment:\n{}", comment.text)
|
||||
}
|
||||
PlanReviewSource::FileBacked => format_file_backed_plan_comment(comment),
|
||||
})
|
||||
.collect();
|
||||
|
||||
if let Some(text) = freeform
|
||||
&& !text.trim().is_empty()
|
||||
{
|
||||
let text = match (self.source, self.comments.is_empty()) {
|
||||
(PlanReviewSource::Inline, false) => format!("Additional feedback:\n{text}"),
|
||||
_ => text.to_owned(),
|
||||
};
|
||||
parts.push(text);
|
||||
}
|
||||
|
||||
parts.join("\n\n")
|
||||
}
|
||||
}
|
||||
|
||||
pub fn send_exit_plan_response(
|
||||
tx: tokio::sync::oneshot::Sender<AcpResult<acp::ExtResponse>>,
|
||||
outcome: &str,
|
||||
feedback: Option<String>,
|
||||
) {
|
||||
let feedback = feedback.filter(|f| !f.trim().is_empty());
|
||||
let resp = ExitPlanModeExtResponse {
|
||||
outcome: outcome.into(),
|
||||
feedback,
|
||||
};
|
||||
let raw = serde_json::value::to_raw_value(&resp)
|
||||
.expect("ExitPlanModeExtResponse serialization should not fail");
|
||||
tx.send(Ok(acp::ExtResponse::new(raw.into()))).ok();
|
||||
}
|
||||
|
||||
fn send_ext_response(
|
||||
tx: &mut Option<tokio::sync::oneshot::Sender<AcpResult<acp::ExtResponse>>>,
|
||||
outcome: &str,
|
||||
feedback: Option<String>,
|
||||
) -> bool {
|
||||
let Some(tx) = tx.take() else {
|
||||
return false;
|
||||
};
|
||||
send_exit_plan_response(tx, outcome, feedback);
|
||||
true
|
||||
}
|
||||
|
||||
impl PlanApprovalViewState {
|
||||
pub fn send_approved(&mut self) -> bool {
|
||||
send_ext_response(&mut self.response_tx, "approved", None)
|
||||
}
|
||||
|
||||
pub fn send_abandoned(&mut self) -> bool {
|
||||
send_ext_response(&mut self.response_tx, "abandoned", None)
|
||||
}
|
||||
|
||||
pub fn send_cancelled(&mut self, feedback: Option<String>) -> bool {
|
||||
send_ext_response(&mut self.response_tx, "cancelled", feedback)
|
||||
}
|
||||
|
||||
pub fn send_stale_cancel(&mut self) -> bool {
|
||||
self.send_cancelled(None)
|
||||
}
|
||||
}
|
||||
|
||||
fn format_file_backed_plan_comment(comment: &PlanComment) -> String {
|
||||
let range = if comment.line_range.len() == 1 {
|
||||
format!("@plan.md:{}", comment.line_range.start)
|
||||
} else {
|
||||
format!(
|
||||
"@plan.md:{}-{}",
|
||||
comment.line_range.start,
|
||||
comment.line_range.end - 1
|
||||
)
|
||||
};
|
||||
format!("{range}\n{}", comment.text)
|
||||
}
|
||||
|
||||
pub(crate) fn inline_plan_snippets(
|
||||
plan_content: Option<&str>,
|
||||
range: &std::ops::Range<usize>,
|
||||
) -> String {
|
||||
let Some(plan_content) = plan_content else {
|
||||
return "> [plan content unavailable]".to_owned();
|
||||
};
|
||||
let lines: Vec<&str> = plan_content.lines().collect();
|
||||
if range.start == 0 || range.start >= range.end || range.start > lines.len() {
|
||||
return "> [selected lines unavailable]".to_owned();
|
||||
}
|
||||
|
||||
let end = range.end.saturating_sub(1).min(lines.len());
|
||||
if end < range.start {
|
||||
return "> [selected lines unavailable]".to_owned();
|
||||
}
|
||||
|
||||
lines[range.start - 1..end]
|
||||
.iter()
|
||||
.map(|line| format!("> {line}"))
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n")
|
||||
}
|
||||
|
||||
pub(crate) fn format_plan_comments(comments: &[PlanComment], plan_content: Option<&str>) -> String {
|
||||
comments
|
||||
.iter()
|
||||
.map(|comment| {
|
||||
let label = if comment.line_range.len() == 1 {
|
||||
format!("Proposed plan line {}:", comment.line_range.start)
|
||||
} else {
|
||||
format!(
|
||||
"Proposed plan lines {}-{}:",
|
||||
comment.line_range.start,
|
||||
comment.line_range.end - 1
|
||||
)
|
||||
};
|
||||
let snippets = inline_plan_snippets(plan_content, &comment.line_range);
|
||||
format!("{label}\n{snippets}\n\nComment:\n{}", comment.text)
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n\n")
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn make_test_state() -> (
|
||||
PlanApprovalViewState,
|
||||
tokio::sync::oneshot::Receiver<AcpResult<acp::ExtResponse>>,
|
||||
) {
|
||||
let (tx, rx) = tokio::sync::oneshot::channel();
|
||||
let request = ExitPlanModeExtRequest {
|
||||
session_id: "test-session".into(),
|
||||
tool_call_id: "call_123".into(),
|
||||
plan_content: Some("# Plan\n\n## Step 1\nDo something".into()),
|
||||
};
|
||||
let state = PlanApprovalViewState::new(
|
||||
request,
|
||||
StashedPrompt {
|
||||
text: "stashed text".into(),
|
||||
cursor: 0,
|
||||
images: Vec::new(),
|
||||
chip_elements: Vec::new(),
|
||||
image_counter: 0,
|
||||
image_undo_stash: Vec::new(),
|
||||
},
|
||||
tx,
|
||||
);
|
||||
(state, rx)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_send_approved() {
|
||||
let (mut state, mut rx) = make_test_state();
|
||||
assert!(state.send_approved());
|
||||
let resp = rx.try_recv().expect("should receive response");
|
||||
let raw = resp.expect("should be Ok");
|
||||
let parsed: serde_json::Value =
|
||||
serde_json::from_str(raw.0.get()).expect("should be valid JSON");
|
||||
assert_eq!(parsed["outcome"], "approved");
|
||||
assert!(parsed.get("feedback").is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_send_cancelled_with_feedback() {
|
||||
let (mut state, mut rx) = make_test_state();
|
||||
assert!(state.send_cancelled(Some("fix auth flow".into())));
|
||||
let resp = rx.try_recv().expect("should receive response");
|
||||
let raw = resp.expect("should be Ok");
|
||||
let parsed: serde_json::Value =
|
||||
serde_json::from_str(raw.0.get()).expect("should be valid JSON");
|
||||
assert_eq!(parsed["outcome"], "cancelled");
|
||||
assert_eq!(parsed["feedback"], "fix auth flow");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_send_cancelled_without_feedback() {
|
||||
let (mut state, mut rx) = make_test_state();
|
||||
assert!(state.send_cancelled(None));
|
||||
let resp = rx.try_recv().expect("should receive response");
|
||||
let raw = resp.expect("should be Ok");
|
||||
let parsed: serde_json::Value =
|
||||
serde_json::from_str(raw.0.get()).expect("should be valid JSON");
|
||||
assert_eq!(parsed["outcome"], "cancelled");
|
||||
assert!(parsed.get("feedback").is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_send_cancelled_empty_feedback_is_none() {
|
||||
let (mut state, mut rx) = make_test_state();
|
||||
assert!(state.send_cancelled(Some(" ".into())));
|
||||
let resp = rx.try_recv().expect("should receive response");
|
||||
let raw = resp.expect("should be Ok");
|
||||
let parsed: serde_json::Value =
|
||||
serde_json::from_str(raw.0.get()).expect("should be valid JSON");
|
||||
assert_eq!(parsed["outcome"], "cancelled");
|
||||
assert!(parsed.get("feedback").is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_send_stale_cancel() {
|
||||
let (mut state, mut rx) = make_test_state();
|
||||
assert!(state.send_stale_cancel());
|
||||
let resp = rx.try_recv().expect("should receive response");
|
||||
let raw = resp.expect("should be Ok");
|
||||
let parsed: serde_json::Value =
|
||||
serde_json::from_str(raw.0.get()).expect("should be valid JSON");
|
||||
assert_eq!(parsed["outcome"], "cancelled");
|
||||
assert!(parsed.get("feedback").is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_double_send_returns_false() {
|
||||
let (mut state, _rx) = make_test_state();
|
||||
assert!(state.send_approved());
|
||||
assert!(!state.send_approved());
|
||||
assert!(!state.send_cancelled(None));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_constructor_defaults() {
|
||||
let (state, _rx) = make_test_state();
|
||||
assert_eq!(state.tool_call_id, "call_123");
|
||||
assert!(state.has_plan);
|
||||
assert_eq!(
|
||||
state.plan_content.as_deref(),
|
||||
Some("# Plan\n\n## Step 1\nDo something")
|
||||
);
|
||||
assert_eq!(state.source, PlanReviewSource::Inline);
|
||||
assert_eq!(state.stashed_prompt.text, "stashed text");
|
||||
assert!(state.response_tx.is_some());
|
||||
assert_eq!(state.focus, PlanApprovalFocus::Preview);
|
||||
assert!(state.comments.is_empty());
|
||||
assert_eq!(state.next_comment_id, 0);
|
||||
assert!(state.editing_comment_id.is_none());
|
||||
assert!(state.commenting_range.is_none());
|
||||
assert!(state.stashed_feedback_prompt.is_none());
|
||||
}
|
||||
|
||||
fn make_empty_plan_state() -> (
|
||||
PlanApprovalViewState,
|
||||
tokio::sync::oneshot::Receiver<AcpResult<acp::ExtResponse>>,
|
||||
) {
|
||||
let (tx, rx) = tokio::sync::oneshot::channel();
|
||||
let request = ExitPlanModeExtRequest {
|
||||
session_id: "test-session".into(),
|
||||
tool_call_id: "call_456".into(),
|
||||
plan_content: None,
|
||||
};
|
||||
let state = PlanApprovalViewState::new(
|
||||
request,
|
||||
StashedPrompt {
|
||||
text: "stashed".into(),
|
||||
cursor: 0,
|
||||
images: Vec::new(),
|
||||
chip_elements: Vec::new(),
|
||||
image_counter: 0,
|
||||
image_undo_stash: Vec::new(),
|
||||
},
|
||||
tx,
|
||||
);
|
||||
(state, rx)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_empty_plan_has_plan_false() {
|
||||
let (state, _rx) = make_empty_plan_state();
|
||||
assert!(!state.has_plan);
|
||||
assert!(state.plan_content.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn plan_approval_status_label_distinguishes_empty() {
|
||||
assert_eq!(plan_approval_status_label(true), "Waiting on plan approval");
|
||||
assert_eq!(
|
||||
plan_approval_status_label(false),
|
||||
"No plan written — approve or request changes"
|
||||
);
|
||||
// Placeholder must be non-empty so the line viewer accepts it.
|
||||
assert!(!EMPTY_PLAN_PLACEHOLDER.trim().is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_empty_plan_whitespace_only() {
|
||||
let (tx, _rx) = tokio::sync::oneshot::channel();
|
||||
let request = ExitPlanModeExtRequest {
|
||||
session_id: "test-session".into(),
|
||||
tool_call_id: "call_789".into(),
|
||||
plan_content: Some(" \n\n ".into()),
|
||||
};
|
||||
let state = PlanApprovalViewState::new(
|
||||
request,
|
||||
StashedPrompt {
|
||||
text: "stashed".into(),
|
||||
cursor: 0,
|
||||
images: Vec::new(),
|
||||
chip_elements: Vec::new(),
|
||||
image_counter: 0,
|
||||
image_undo_stash: Vec::new(),
|
||||
},
|
||||
tx,
|
||||
);
|
||||
assert!(!state.has_plan);
|
||||
assert!(state.plan_content.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn inline_plan_feedback_quotes_selected_line_snippets() {
|
||||
let (mut state, _rx) = make_test_state();
|
||||
state.plan_content = Some("alpha\nbravo\ncharlie\ndelta".into());
|
||||
state.comments.push(PlanComment {
|
||||
id: 0,
|
||||
line_range: 2..3,
|
||||
text: "rewrite this".into(),
|
||||
});
|
||||
state.comments.push(PlanComment {
|
||||
id: 1,
|
||||
line_range: 3..5,
|
||||
text: "combine these".into(),
|
||||
});
|
||||
|
||||
let feedback = state.format_feedback(Some("overall note"));
|
||||
|
||||
assert_eq!(
|
||||
feedback,
|
||||
"Proposed plan line 2:\n> bravo\n\nComment:\nrewrite this\n\nProposed plan lines 3-4:\n> charlie\n> delta\n\nComment:\ncombine these\n\nAdditional feedback:\noverall note"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn inline_plan_feedback_handles_out_of_range_lines() {
|
||||
let (mut state, _rx) = make_test_state();
|
||||
state.plan_content = Some("alpha".into());
|
||||
state.comments.push(PlanComment {
|
||||
id: 0,
|
||||
line_range: 9..10,
|
||||
text: "where is this".into(),
|
||||
});
|
||||
|
||||
assert_eq!(
|
||||
state.format_feedback(None),
|
||||
"Proposed plan line 9:\n> [selected lines unavailable]\n\nComment:\nwhere is this"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn file_backed_plan_feedback_keeps_plan_md_references() {
|
||||
let (mut state, _rx) = make_test_state();
|
||||
state.source = PlanReviewSource::FileBacked;
|
||||
state.plan_content = Some("alpha\nbravo".into());
|
||||
state.comments.push(PlanComment {
|
||||
id: 0,
|
||||
line_range: 1..3,
|
||||
text: "keep file ref".into(),
|
||||
});
|
||||
|
||||
assert_eq!(
|
||||
state.format_feedback(Some("freeform")),
|
||||
"@plan.md:1-2\nkeep file ref\n\nfreeform"
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user