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,331 @@
|
||||
use super::*;
|
||||
|
||||
/// Cached `mcp.push_server_status` flag resolution.
|
||||
///
|
||||
/// Resolution mirrors the `mcp.liveness_watchers` flag pattern but
|
||||
/// only stacks the env+default layers — the pager process does not load
|
||||
/// `config.toml` / `requirements.toml`. The first call performs one
|
||||
/// `BoolFlag::env` read; every subsequent call is a pure
|
||||
/// `OnceLock::get` (single atomic load). Default `true`; set
|
||||
/// `KIGI_MCP_PUSH_SERVER_STATUS=0` to disable.
|
||||
pub(super) fn push_server_status_enabled() -> bool {
|
||||
use std::sync::OnceLock;
|
||||
static ENABLED: OnceLock<bool> = OnceLock::new();
|
||||
*ENABLED.get_or_init(|| {
|
||||
kigi_shell::util::config::resolve_mcp_push_server_status(
|
||||
/* requirements */ None, /* user */ None, /* managed */ None,
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
pub(super) fn handle_mcp_init_progress(notif: &acp::ExtNotification, app: &mut AppView) -> bool {
|
||||
#[derive(serde::Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct Payload {
|
||||
total: u32,
|
||||
connected: u32,
|
||||
#[serde(default)]
|
||||
session_id: Option<String>,
|
||||
}
|
||||
let Ok(payload) = serde_json::from_str::<Payload>(notif.params.get()) else {
|
||||
return false;
|
||||
};
|
||||
let Some((is_active, agent)) = mcp_target_agent(app, payload.session_id.as_deref()) else {
|
||||
return false;
|
||||
};
|
||||
if let Some(ref mut progress) = agent.mcp_init_progress {
|
||||
progress.total = payload.total;
|
||||
progress.connected = payload.connected;
|
||||
} else {
|
||||
agent.mcp_init_progress = Some(super::super::agent_view::McpInitProgress {
|
||||
total: payload.total,
|
||||
connected: payload.connected,
|
||||
started_at: std::time::Instant::now(),
|
||||
});
|
||||
}
|
||||
is_active
|
||||
}
|
||||
|
||||
/// Handle `x.ai/mcp/tools_changed` and `x.ai/mcp_initialized`.
|
||||
///
|
||||
/// Routing rules (verified against the four shell emit sites in
|
||||
/// `kigi-shell/src/session/acp_session.rs` — toggle-tool ~L6661,
|
||||
/// `emit_mcp_tools_changed_notifications` ~L8997 and post-handshake
|
||||
/// ~L10156, plus `mcp_initialized` ~L10157):
|
||||
///
|
||||
/// 1. Try `notif.params.sessionId`. All `tools_changed` emit
|
||||
/// sites carry `sessionId` (the typed
|
||||
/// [`kigi_shell::extensions::mcp::McpToolsChanged`] struct), and
|
||||
/// `mcp_initialized` already carried it. So the sessionId branch
|
||||
/// is the primary path for current builds.
|
||||
///
|
||||
/// 2. Older shells / forward-compat (**`tools_changed` only**): a
|
||||
/// payload with no `sessionId` falls back to
|
||||
/// `app.active_view`. Older shells emit `tools_changed` as
|
||||
/// `{serverName, tools}` with no `sessionId`; the fallback keeps
|
||||
/// those in-flight payloads working. The `mcp_initialized`
|
||||
/// variant does NOT need this fallback — its emitter already
|
||||
/// carries `sessionId`, so
|
||||
/// the sessionId branch (step 1) is the only matched-build path
|
||||
/// for `mcp_initialized`.
|
||||
///
|
||||
/// 3. When the owning agent has an open extensions modal, schedules
|
||||
/// a debounced [`Effect::FetchMcpsList`] coalesced **per-agent**
|
||||
/// (see [`agent_has_pending_mcps_fetch`]). A pending fetch
|
||||
/// on agent A does NOT drop a notification for agent B.
|
||||
///
|
||||
/// Always clears `mcp_init_progress` on the `mcp_initialized` variant.
|
||||
pub(super) fn handle_mcp_tools_changed(notif: &acp::ExtNotification, app: &mut AppView) -> bool {
|
||||
let method = notif.method.as_ref();
|
||||
|
||||
// Both `x.ai/mcp_initialized` and (newer shell)
|
||||
// `x.ai/mcp/tools_changed` carry `sessionId`. Route by it so a
|
||||
// background agent's notification updates *its* state — not
|
||||
// whichever agent is foregrounded. Unknown and subagent (child)
|
||||
// sessions are dropped; a missing sessionId falls back to the
|
||||
// active agent (legacy shells).
|
||||
#[derive(serde::Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct Payload {
|
||||
#[serde(default)]
|
||||
session_id: Option<String>,
|
||||
}
|
||||
let session_id = serde_json::from_str::<Payload>(notif.params.get())
|
||||
.ok()
|
||||
.and_then(|p| p.session_id);
|
||||
let target: Option<(bool, AgentId)> = match session_id.as_deref() {
|
||||
Some(sid) => {
|
||||
let sid = acp::SessionId::new(sid);
|
||||
match find_session_match(app, &sid) {
|
||||
// Subagent (child) sessions don't own the top-level MCP
|
||||
// modal / connecting indicator — drop them.
|
||||
Some(SessionMatch::Child(_)) => None,
|
||||
Some(matched) => {
|
||||
let id = matched.agent_id();
|
||||
Some((is_matched_agent_active(app, id), id))
|
||||
}
|
||||
None => None,
|
||||
}
|
||||
}
|
||||
None => match app.active_view {
|
||||
ActiveView::Agent(id) => Some((true, id)),
|
||||
_ => None,
|
||||
},
|
||||
};
|
||||
let Some((is_active, id)) = target else {
|
||||
return false;
|
||||
};
|
||||
|
||||
let mut redraw = false;
|
||||
|
||||
// `mcp_initialized` clears the matched agent's connecting indicator.
|
||||
if method == "x.ai/mcp_initialized"
|
||||
&& let Some(agent) = app.agents.get_mut(&id)
|
||||
&& agent.mcp_init_progress.take().is_some()
|
||||
{
|
||||
redraw |= is_active;
|
||||
}
|
||||
|
||||
// Modal refresh: schedule a debounced refetch for the OWNING agent
|
||||
// (routed by sessionId — was active_view), per-agent coalesced.
|
||||
let modal_open = app
|
||||
.agents
|
||||
.get(&id)
|
||||
.is_some_and(|a| a.extensions_modal.is_some());
|
||||
if modal_open
|
||||
&& !agent_has_pending_mcps_fetch(app, id)
|
||||
&& let Some(session_id) = app
|
||||
.agents
|
||||
.get(&id)
|
||||
.and_then(|a| a.session.session_id.clone())
|
||||
{
|
||||
app.pending_effects.push(Effect::FetchMcpsList {
|
||||
agent_id: id,
|
||||
session_id,
|
||||
cache: true,
|
||||
});
|
||||
redraw |= is_active;
|
||||
}
|
||||
redraw
|
||||
}
|
||||
|
||||
/// Per-agent coalescing test for [`Effect::FetchMcpsList`].
|
||||
/// An earlier approach used `matches!(e, FetchMcpsList { .. })`
|
||||
/// which collapsed across agents — a pending fetch on agent A would
|
||||
/// drop the push for agent B. Now we key on `agent_id` so each
|
||||
/// agent's refetch is independently debounced.
|
||||
pub(super) fn agent_has_pending_mcps_fetch(app: &AppView, agent_id: AgentId) -> bool {
|
||||
app.pending_effects.iter().any(|e| {
|
||||
matches!(
|
||||
e,
|
||||
Effect::FetchMcpsList { agent_id: a, .. } if *a == agent_id
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
/// Handle `x.ai/mcp/server_status`.
|
||||
///
|
||||
/// Routes by the notification's `sessionId` via
|
||||
/// [`find_session_match`] — the matched agent's extensions modal is
|
||||
/// patched in-place via [`crate::views::mcps_modal::patch_server_row`]
|
||||
/// using the per-row delta, avoiding the full `mcp/list` round trip
|
||||
/// the legacy `tools_changed` debounced refetch path requires.
|
||||
///
|
||||
/// No-ops when:
|
||||
/// - the `sessionId` does not match any known agent (drop),
|
||||
/// - the matched agent has no extensions modal open (cheap path —
|
||||
/// the next `/mcps` open will pull fresh data anyway),
|
||||
/// - the modal's `mcps_data` is not yet `Loaded` (Loading / Error
|
||||
/// states would produce incoherent patches; the in-flight fetch
|
||||
/// will land a consistent snapshot shortly),
|
||||
/// - the named server is not present in the cached `servers` vec
|
||||
/// ([`patch_server_row`] silently returns).
|
||||
///
|
||||
/// Re-uses the shell's canonical wire types
|
||||
/// ([`kigi_shell::extensions::mcp::McpServerStatusPayload`] +
|
||||
/// [`kigi_shell::extensions::mcp::McpServerStatus`]) instead of
|
||||
/// re-declaring a parallel pager enum. Later variants (e.g.
|
||||
/// `RestartSucceeded` / `RestartFailed`) ride through automatically
|
||||
/// without a pager code change.
|
||||
///
|
||||
/// `status` is **not** `serde(default)`; a malformed
|
||||
/// payload falls into the `tracing::warn!` arm rather than silently
|
||||
/// re-painting the row red.
|
||||
///
|
||||
/// `tools` is decoded loosely as
|
||||
/// `Option<serde_json::Value>` so a future non-array shape doesn't
|
||||
/// drop the entire push — `status` still applies, and `tools` is
|
||||
/// silently skipped (warn-logged) on shape mismatch.
|
||||
///
|
||||
/// Returns `true` (request redraw) only when the row mutation
|
||||
/// happened AND the matched agent is the currently active view.
|
||||
pub(super) fn handle_mcp_server_status(notif: &acp::ExtNotification, app: &mut AppView) -> bool {
|
||||
use crate::views::extensions_modal::TabDataState;
|
||||
use crate::views::mcps_modal::{McpServerDisplayStatus, McpToolDetail, patch_server_row};
|
||||
use kigi_shell::extensions::mcp::{McpServerStatus, McpServerStatusPayload, McpToolEntry};
|
||||
|
||||
let Ok(payload) = serde_json::from_str::<McpServerStatusPayload>(notif.params.get()) else {
|
||||
tracing::warn!(
|
||||
"Failed to parse x.ai/mcp/server_status: {}",
|
||||
¬if.params.get()
|
||||
[..crate::render::line_utils::floor_char_boundary(notif.params.get(), 100)]
|
||||
);
|
||||
return false;
|
||||
};
|
||||
|
||||
let session_id = acp::SessionId::new(payload.session_id);
|
||||
let Some(matched) = find_session_match(app, &session_id) else {
|
||||
return false;
|
||||
};
|
||||
let id = matched.agent_id();
|
||||
let is_active = is_matched_agent_active(app, id);
|
||||
let Some(agent) = app.agents.get_mut(&id) else {
|
||||
return false;
|
||||
};
|
||||
// Cheap path: modal closed. Drop the push — the next `/mcps`
|
||||
// open will fetch a fresh full list.
|
||||
let Some(modal) = agent.extensions_modal.as_mut() else {
|
||||
return false;
|
||||
};
|
||||
// Cheap path: list still loading / errored. Patching would
|
||||
// produce incoherent state; the in-flight fetch will land
|
||||
// a consistent snapshot momentarily.
|
||||
let TabDataState::Loaded(ref mut servers) = modal.mcps_data else {
|
||||
return false;
|
||||
};
|
||||
let display_status = match payload.status {
|
||||
McpServerStatus::Ready => McpServerDisplayStatus::Ready,
|
||||
McpServerStatus::Initializing => McpServerDisplayStatus::Initializing,
|
||||
McpServerStatus::Unavailable => McpServerDisplayStatus::Unavailable,
|
||||
McpServerStatus::NeedsAuth => McpServerDisplayStatus::NeedsAuth,
|
||||
};
|
||||
// Decode `tools` loosely. Shell types this as
|
||||
// `Option<serde_json::Value>` (always `null` today;
|
||||
// reserved). If the value is present but not an array of
|
||||
// `McpToolEntry`-isomorphic objects we drop ONLY the tools
|
||||
// update and still apply the status — the previous strict
|
||||
// typing would have dropped the whole push on any shape
|
||||
// mismatch.
|
||||
let new_tools = payload.tools.and_then(|raw| {
|
||||
match serde_json::from_value::<Vec<McpToolEntry>>(raw) {
|
||||
Ok(entries) => Some(
|
||||
entries
|
||||
.into_iter()
|
||||
.map(|t| McpToolDetail {
|
||||
name: t.name,
|
||||
display_name: t.display_name,
|
||||
description: t.description,
|
||||
enabled: t.enabled,
|
||||
})
|
||||
.collect::<Vec<_>>(),
|
||||
),
|
||||
Err(e) => {
|
||||
tracing::warn!(
|
||||
server = %payload.name,
|
||||
error = %e,
|
||||
"x.ai/mcp/server_status: tools field present but not Vec<McpToolEntry>; status still applied"
|
||||
);
|
||||
None
|
||||
}
|
||||
}
|
||||
});
|
||||
let mutated = patch_server_row(servers, &payload.name, display_status, new_tools);
|
||||
mutated && is_active
|
||||
}
|
||||
|
||||
/// Handle `x.ai/mcp/servers_updated`.
|
||||
///
|
||||
/// Emitted by the shell from `MvpAgent` on managed-config resolve and
|
||||
/// on config reload (`crates/codegen/kigi-shell/src/agent/mvp_agent.rs`
|
||||
/// → `notify_servers_updated`). The shell's
|
||||
/// `McpServersUpdated` wire shape (`{ mcpServers: [...] }`) is
|
||||
/// intentionally session-agnostic by design
|
||||
/// An attempt to route by
|
||||
/// `sessionId` therefore always fell back to `app.active_view` and
|
||||
/// re-created the multi-agent bug.
|
||||
///
|
||||
/// Routing now correctly broadcasts: every agent with an open
|
||||
/// extensions modal gets a per-agent debounced [`Effect::FetchMcpsList`].
|
||||
/// Per-agent coalescing keeps a second push from displacing an
|
||||
/// in-flight fetch on the same agent. Agents without an open modal
|
||||
/// drop the push (cheap path).
|
||||
pub(super) fn handle_mcp_servers_updated(_notif: &acp::ExtNotification, app: &mut AppView) -> bool {
|
||||
// `_notif` is intentionally unread. The shell's
|
||||
// `McpServersUpdated` payload is `{ mcpServers: [...] }` with no
|
||||
// `sessionId` (the protocol forbids extending it), so there is
|
||||
// nothing in the notification body the broadcast model needs.
|
||||
// Do NOT "fix" this back to per-session routing without
|
||||
// re-reading the rustdoc above.
|
||||
//
|
||||
// Snapshot (agent_id, session_id, modal_open) up front so the
|
||||
// mutable `pending_effects` borrow can proceed without
|
||||
// aliasing `app.agents`.
|
||||
let targets: Vec<(AgentId, acp::SessionId)> = app
|
||||
.agents
|
||||
.iter()
|
||||
.filter_map(|(id, agent)| {
|
||||
if agent.extensions_modal.is_some() {
|
||||
agent.session.session_id.clone().map(|sid| (*id, sid))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
if targets.is_empty() {
|
||||
return false;
|
||||
}
|
||||
let mut redraw = false;
|
||||
for (id, session_id) in targets {
|
||||
if agent_has_pending_mcps_fetch(app, id) {
|
||||
continue;
|
||||
}
|
||||
let is_active = is_matched_agent_active(app, id);
|
||||
app.pending_effects.push(Effect::FetchMcpsList {
|
||||
agent_id: id,
|
||||
session_id,
|
||||
cache: true,
|
||||
});
|
||||
redraw |= is_active;
|
||||
}
|
||||
redraw
|
||||
}
|
||||
Reference in new issue
Block a user