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,274 @@
//! Permission request selection, follow-up, cancellation, and queue draining.
use super::modes::set_yolo_mode;
use crate::app::actions::Effect;
use crate::app::agent_view::AgentView;
use crate::app::app_view::{ActiveView, AppView};
use agent_client_protocol as acp;
// ---------------------------------------------------------------------------
// Permission dispatch
// ---------------------------------------------------------------------------
/// Handle permission option selection (AllowOnce, AllowAlways, RejectAlways).
///
/// Pops the front request, sends the response, and handles queue transitions
/// (prompt restore on empty, prompt clear on next-front).
///
/// Special case for [`kigi_workspace::permission::ENABLE_ALWAYS_APPROVE_OPTION_ID`]:
/// when the user picks the prepended "Yes, and don't ask again for anything"
/// option, this dispatcher (a) sends the standard `Selected` response so the
/// in-flight request is allowed once (the shell's `map_selected_outcome`
/// resolves the id to `PromptOutcome::AllowOnce`), then (b) reuses the
/// existing `set_yolo_mode(true)` flow to flip the local YOLO state, drain
/// any remaining queued permissions, persist `[ui] permission_mode =
/// "always-approve"` to `~/.kigi/config.toml`, and fire the
/// `x.ai/yolo_mode_changed` ACP notification. See the option-id constant
/// doc-comment for the full client/shell split. Under a managed-policy
/// pin step (b) is refused with a toast — the request is still allowed once.
pub(super) fn dispatch_permission_select(
app: &mut AppView,
option_id: acp::PermissionOptionId,
) -> Vec<Effect> {
let ActiveView::Agent(id) = app.active_view else {
return vec![];
};
let Some(agent) = app.agents.get_mut(&id) else {
return vec![];
};
let Some(perm) = agent.permission_queue.pop_front() else {
return vec![];
};
// Detect the "enable always-approve mode" id BEFORE moving option_id
// into the response. Cheap str compare on the `Arc<str>` interior.
let enable_always_approve =
option_id.0.as_ref() == kigi_workspace::permission::ENABLE_ALWAYS_APPROVE_OPTION_ID;
// Remember the user's choice (by option kind) so the next prompt's cursor
// sticks to it. Allow-flavored choices only — a rejection must not steer a
// later prompt's cursor onto a reject row. Also skip the two options that
// aren't per-prompt choices:
// - the global always-approve (YOLO) option flips global auto-approve, so
// there will be no subsequent prompt to land on;
// - "allow all edits during this session" is edit-scoped (kind
// `AllowAlways`) — letting it stick would steer an unrelated later
// prompt onto its "always allow this command" row, escalating scope.
let steers_next_cursor = !enable_always_approve
&& option_id.0.as_ref() != kigi_workspace::permission::ALLOW_EDITS_SESSION_OPTION_ID;
if steers_next_cursor
&& let Some(kind) = perm
.options
.iter()
.find(|o| o.option_id == option_id)
.map(|o| o.kind)
&& matches!(
kind,
acp::PermissionOptionKind::AllowOnce | acp::PermissionOptionKind::AllowAlways
)
{
crate::appearance::permission_cursor::set_last_used_permission(
crate::appearance::permission_cursor::DefaultSelectedPermission::from_kind(&kind),
);
}
// Build response meta. MCP and bash flows are mutually exclusive at
// the per-request level; check MCP first because it owns the
// `allow-always-mcp` option id and the bash branch is the existing
// fallback.
let meta = if let Some(scope) = perm
.mcp_scope
.as_ref()
.filter(|_| option_id.0.as_ref() == "allow-always-mcp")
{
let selection = match scope.selected {
crate::views::permission_view::McpScope::Tool => {
kigi_workspace::permission::McpScopeSelection::Tool {
tool_name: scope.tool_name.clone(),
}
}
crate::views::permission_view::McpScope::Server => match &scope.server_prefix {
Some(prefix) => kigi_workspace::permission::McpScopeSelection::Server {
server: prefix.clone(),
},
// Defensive: render path should disable Server when no prefix.
None => kigi_workspace::permission::McpScopeSelection::Tool {
tool_name: scope.tool_name.clone(),
},
},
};
serde_json::to_value(selection)
.ok()
.and_then(|v| v.as_object().cloned())
} else if let Some(ref h) = perm.bash_highlights
&& perm.bash_selection_count > 0
{
let parts: Vec<String> = h.highlighted_words[..perm.bash_selection_count].to_vec();
serde_json::to_value(kigi_workspace::permission::BashCommandSelectedTerms {
command_parts: parts,
})
.ok()
.and_then(|v| v.as_object().cloned())
} else {
None
};
perm.request
.response_tx
.send(Ok(acp::RequestPermissionResponse::new(
acp::RequestPermissionOutcome::Selected(acp::SelectedPermissionOutcome::new(option_id)),
)
.meta(meta)))
.ok();
// Queue transition: restore prompt if queue is now empty, clear if next-front.
resolve_permission_queue_transition(agent);
// "Enable always-approve" side effect: flip YOLO + persist + notify.
// Reuses the existing `set_yolo_mode` pipeline so telemetry, queue
// drain, toast, modal refresh, config persistence, and ACP
// notification all flow through one well-tested code path.
//
// Idempotency: if YOLO is already on, the pager auto-approves in
// `handle_permission_request` before the panel is shown, so the
// user couldn't have selected this option. The `is_yolo()` guard
// is defensive — a redundant call would re-emit the toast and a
// duplicate `PersistPermissionMode` effect, but is otherwise safe.
if enable_always_approve {
let already_on = app
.agents
.get(&id)
.map(|a| a.session.is_yolo())
.unwrap_or(false);
if !already_on {
return set_yolo_mode(app, true);
}
}
vec![]
}
/// Handle permission followup message (RejectOnce with user-typed text).
pub(super) fn dispatch_permission_followup(app: &mut AppView, text: String) -> Vec<Effect> {
let ActiveView::Agent(id) = app.active_view else {
return vec![];
};
let Some(agent) = app.agents.get_mut(&id) else {
return vec![];
};
let Some(perm) = agent.permission_queue.pop_front() else {
return vec![];
};
// Find the RejectOnce option.
let option_id = perm
.options
.iter()
.find(|o| o.kind == acp::PermissionOptionKind::RejectOnce)
.map(|o| o.option_id.clone());
let Some(option_id) = option_id else {
// No RejectOnce option — cancel instead.
perm.request
.response_tx
.send(Ok(acp::RequestPermissionResponse::new(
acp::RequestPermissionOutcome::Cancelled,
)))
.ok();
resolve_permission_queue_transition(agent);
return vec![];
};
// Include followup message in meta.
let meta = if !text.trim().is_empty() {
serde_json::json!({
"followup_message": text,
})
.as_object()
.cloned()
} else {
None
};
perm.request
.response_tx
.send(Ok(acp::RequestPermissionResponse::new(
acp::RequestPermissionOutcome::Selected(acp::SelectedPermissionOutcome::new(option_id)),
)
.meta(meta)))
.ok();
resolve_permission_queue_transition(agent);
vec![]
}
/// Handle permission cancel (Ctrl-C / Esc — cancels front request only).
pub(super) fn dispatch_permission_cancel(app: &mut AppView) -> Vec<Effect> {
let ActiveView::Agent(id) = app.active_view else {
return vec![];
};
let Some(agent) = app.agents.get_mut(&id) else {
return vec![];
};
let Some(perm) = agent.permission_queue.pop_front() else {
return vec![];
};
perm.request
.response_tx
.send(Ok(acp::RequestPermissionResponse::new(
acp::RequestPermissionOutcome::Cancelled,
)))
.ok();
resolve_permission_queue_transition(agent);
vec![]
}
/// Drain all queued permission requests, sending `Cancelled` to each.
///
/// Called on turn-end and turn-cancel. After draining, restores the stashed
/// prompt text (if any). This is distinct from `dispatch_permission_cancel`
/// which cancels only the front request.
pub(super) fn drain_permission_queue(agent: &mut AgentView) {
agent.last_permission_click = None;
if agent.permission_queue.is_empty() {
return;
}
for perm in agent.permission_queue.drain(..) {
perm.request
.response_tx
.send(Ok(acp::RequestPermissionResponse::new(
acp::RequestPermissionOutcome::Cancelled,
)))
.ok();
}
// Queue is now empty — restore stashed prompt.
if let Some(stashed) = agent.permission_stashed_prompt.take() {
agent.prompt.restore(stashed);
}
}
/// Handle queue transition after resolving (select/followup/cancel) the front
/// permission request.
///
/// - Queue now empty → restore stashed prompt text.
/// - Queue still has items → clear prompt text (for next followup input)
/// and reset next front's focus to Options.
pub(crate) fn resolve_permission_queue_transition(agent: &mut AgentView) {
agent.last_permission_click = None;
if agent.permission_queue.is_empty() {
// Restore original prompt.
if let Some(stashed) = agent.permission_stashed_prompt.take() {
agent.prompt.restore(stashed);
}
} else {
// Clear any followup text from the just-resolved permission so it
// doesn't leak into the next permission's UI.
agent.prompt.set_text("");
// Reset next front's focus to Options.
if let Some(next) = agent.permission_queue.front_mut() {
next.focus = crate::views::permission_view::PermissionFocus::Options;
}
}
}