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,362 @@
//! Transient user feedback: toasts, ephemeral tips, mode-switch banners,
//! terminal-size notes, clipboard-copy feedback, and their tick timers.
use super::{
ActivePane, AgentView, CLIPBOARD_TOAST_DEBOUNCE_MS, MODE_BANNER_TOTAL_TICKS, PromptInputMode,
};
#[cfg(test)]
use super::{AgentPane, test_fixtures};
use crate::app::actions::Action;
use std::time::Instant;
impl AgentView {
/// Show a brief toast message (e.g., "Copied!").
///
/// Displayed for ~3 seconds (90 ticks at 30fps). Previous transient toast
/// is replaced; [`Self::sticky_toast`] is preserved and returns after this
/// expires or is dismissed.
pub fn show_toast(&mut self, msg: &str) {
let msg = crate::glyphs::legacy_glyph_fallback(msg).into_owned();
self.toast = Some((msg, 90));
}
/// Show an ephemeral tip in the banner row above the prompt, gated by the
/// app-level per-session `seen_counts` map (`AppView::tip_seen_counts`).
/// Returns true when the tip was newly shown (and the per-session count
/// incremented in place — never persisted to disk).
///
/// No-op while the row cannot paint (an occluding view — permission,
/// question, modal, subagent takeover, fullscreen viewer, `/gboom`, the
/// extensions/agents modals, the goal-detail overlay, or an open prompt
/// dropdown — a short terminal, the unknown size before the first draw, or a
/// pending re-measure right after a resize event) so counts, TTL, and
/// telemetry never burn on an invisible tip.
pub fn show_ephemeral_tip(
&mut self,
tip: crate::tips::EphemeralTip,
seen_counts: &mut std::collections::HashMap<&'static str, u32>,
) -> bool {
if !self.ephemeral_tip_renderable(self.last_terminal_size.1) {
return false;
}
self.ephemeral_tip.show(tip, seen_counts)
}
/// Whether the tip row could paint right now (last drawn size). Lets
/// app-level triggers skip work that the show gate would refuse anyway.
pub(crate) fn ephemeral_tip_can_render(&self) -> bool {
self.ephemeral_tip_renderable(self.last_terminal_size.1)
}
/// Single definition of the agent-level eligibility for the clipboard-image
/// tip: the tip row can paint, no image chips are already attached, and the
/// current model accepts image input.
pub(crate) fn clipboard_image_tip_eligible(&self) -> bool {
self.ephemeral_tip_can_render()
&& self.prompt.images.is_empty()
&& self.session.models.current_model_accepts_images()
}
/// One-shot undo-tip show signal from the last `PromptWidget::handle_key`,
/// routed as an action so dispatch can reach `app.tip_seen_counts`. Fires
/// only on a qualifying wipe (set exclusively on `PromptEvent::Edited`).
pub(super) fn take_prompt_tip_signal(&mut self) -> Option<Action> {
// Undo (wipe-to-empty) takes precedence; the wipe and the typed-keyword
// nudge are mutually exclusive on a single keypress, so one
// `Option<Action>` suffices. The plan nudge is suppressed when already
// in plan mode (the optimistic read), while the turn is busy, or in a
// special prompt input mode (bash/feedback/remember).
if self.prompt.take_undo_tip_fire() {
return Some(Action::ShowUndoTip);
}
let in_plan = self.plan_mode_pending.unwrap_or(self.plan_mode_active);
if self.prompt.take_plan_nudge_fire()
&& !in_plan
&& self.session.state.is_idle()
&& self.prompt_input_mode == PromptInputMode::Normal
{
return Some(Action::ShowPlanNudge);
}
None
}
/// Whether the ephemeral tip needs tick / animation this frame.
/// Ambient tips freeze under EVERY occluder (permission ask, modal,
/// dropdown): their TTL burns only while the row can paint, so an
/// occluder pauses rather than expires them off-screen.
pub(crate) fn ephemeral_tip_needs_tick(&self) -> bool {
self.ephemeral_tip.is_active()
&& (!self.ephemeral_tip.active_is_ambient() || self.ephemeral_tip_can_render())
}
/// Advance tip TTL only when the tip is allowed to tick (see
/// [`Self::ephemeral_tip_needs_tick`]).
pub(crate) fn tick_ephemeral_tip(&mut self) -> bool {
// Word-select tip lifecycle: any prompt divergence since the tip was
// shown (typed, pasted, dropped — every edit path funnels into the
// prompt text) retires it, and the snapshot drops once the tip is
// gone for any reason. A visible tip is always ticking (it arms the
// metronome), so the sweep runs within a frame of the edit.
if self.ephemeral_tip.current_key() == Some(crate::tips::word_select::WORD_SELECT_TIP_KEY) {
if self.word_select_tip_prompt_snapshot.as_deref() != Some(self.prompt.text()) {
self.ephemeral_tip
.clear(crate::tips::word_select::WORD_SELECT_TIP_KEY);
self.word_select_tip_prompt_snapshot = None;
return true;
}
} else if self.word_select_tip_prompt_snapshot.is_some() {
self.word_select_tip_prompt_snapshot = None;
}
if !self.ephemeral_tip_needs_tick() {
return false;
}
self.ephemeral_tip.tick()
}
/// Unified visibility for the ephemeral tip row: no occluding view, a
/// tall-enough screen, and no resize since the height was measured. Shared
/// by the show gate and the draw path (reserve + paint), so a view opening
/// over an already-shown tip also stops the row's reservation until it
/// closes.
///
/// Most occluders leave an edit-contextual tip active with TTL still
/// burning (tip may repaint on close). AMBIENT tips freeze under any
/// occluder: paint yields **and** [`Self::tick_ephemeral_tip`] freezes TTL
/// so a long-lived occluder cannot burn the tip off-screen or keep
/// `needs_animation` hot.
///
/// An occluder is anything that, later in the same frame, keeps the banner
/// row from reaching the user. The transient mode-switch banner and the
/// inline `/btw` panel are deliberately NOT occluders: the banner owns the
/// slot ~2 s while the tip's TTL ticks, and `/btw` has its own layout slot
/// above the banner.
///
/// Drift warning: banner-covering views are also enumerated in two sibling
/// hand-maintained lists — the pre-overlay inline-media clear in `draw` and
/// the per-frame `frame_occluder_rects` (dropdowns + goal detail). A new
/// banner-covering view must be added here too.
pub(super) fn ephemeral_tip_renderable(&self, screen_height: u16) -> bool {
let occluded = !self.permission_queue.is_empty()
|| self.question_view.is_some()
|| self.active_modal.is_some()
// Subagent fullscreen takeover: draw early-returns into
// draw_subagent_fullscreen and never paints the parent banner.
|| self.active_subagent.is_some()
// Fullscreen viewers render after the banner paints: image/video/
// block dim the whole region down to the shortcuts row (banner
// included). line_viewer's overlay stops at turn_status.y when a
// turn status shows, so it does NOT always cover the banner — kept
// anyway as a safe over-refusal (the gate cannot know layout
// heights, and a tip during viewer reading is unwanted regardless).
|| self.line_viewer.is_some()
|| self.image_viewer.is_some()
|| self.video_viewer.is_some()
|| self.block_viewer.is_some()
// /gboom dims the same down-to-shortcuts region as the video viewer.
|| self.gboom.is_some()
// Extensions/agents modals are centered popups (render_modal_window)
// that capture all input and early-return out of draw; distinct
// from active_modal. persona_detail only renders atop the agents
// modal. A tip could at most peek beside the modal, so refuse.
|| self.extensions_modal.is_some()
|| self.agents_modal.is_some()
// Goal-detail is a vertically-centered overlay painted after the
// tip; its box only reaches the banner row for tall/content-rich
// goals, but kept unconditional as a safe over-refusal (like the
// modals and line_viewer) since a tip during goal reading is
// unwanted regardless.
|| (self.show_goal_detail && self.goal_state.is_some())
// Prompt dropdowns (@/slash/completion/history) render in the
// row directly above the prompt — the banner row — clearing it.
|| self.prompt.any_dropdown_open();
!self.terminal_size_stale && crate::tips::tip_row_renderable(occluded, screen_height)
}
/// Draw-path re-measure: record the size of the rect this view painted
/// into, invalidating Kitty image IDs when it changed (terminals clear
/// GPU data on resize), and mark the measurement fresh again.
///
/// Only draw calls this — the rect can be smaller than the terminal
/// (dashboard overlay header band/popup, dev tracing split), so a
/// resize event must NOT write an extrapolated size here; it flags
/// staleness via `note_terminal_resize` instead and the next draw
/// re-measures.
pub(crate) fn note_terminal_size(&mut self, size: (u16, u16)) {
if self.last_terminal_size != (0, 0) && self.last_terminal_size != size {
self.inline_media_ids.clear();
self.inline_media_iterm_emitted.clear();
crate::terminal::overlay::reset_owner();
}
self.last_terminal_size = size;
self.terminal_size_stale = false;
}
/// Event-path resize note: the terminal changed size, so the height in
/// `last_terminal_size` no longer describes what this view can paint —
/// chrome (dashboard overlay header/popup, dev tracing split) means the
/// view's rect is not derivable from the event's full-terminal size.
/// The ephemeral-tip show gate refuses until the next draw re-measures;
/// resize draws are debounced (`RESIZE_DEBOUNCE`), so that window is a
/// frame's worth of events, and a refusal burns nothing.
pub(crate) fn note_terminal_resize(&mut self) {
self.terminal_size_stale = true;
}
/// Set or clear the sticky status banner (process-wide indicators should
/// use [`Self::set_sticky_toast_recursive`] on every agent view).
pub fn set_sticky_toast(&mut self, msg: Option<&str>) {
self.sticky_toast = msg.map(|m| crate::glyphs::legacy_glyph_fallback(m).into_owned());
}
/// Propagate sticky status to this view and every nested subagent view.
pub fn set_sticky_toast_recursive(&mut self, msg: Option<&str>) {
self.set_sticky_toast(msg);
for child in self.subagent_views.values_mut() {
child.set_sticky_toast_recursive(msg);
}
}
/// Show a toast with an explicit tick duration.
pub fn show_toast_ticks(&mut self, msg: &str, ticks: u8) {
let msg = crate::glyphs::legacy_glyph_fallback(msg).into_owned();
self.toast = Some((msg, ticks));
}
/// Message currently drawn in the toast slot: transient wins while active,
/// otherwise sticky status (if any).
pub(super) fn active_toast_message(&self) -> Option<&str> {
if let Some((ref msg, _)) = self.toast {
return Some(msg.as_str());
}
let sticky = self.sticky_toast.as_deref()?;
// The mouse-off banner advertises how to re-enable. `Ctrl+R` only works
// from scrollback, so when the prompt is focused show the
// `/toggle-mouse-reporting` command instead (it toggles from any pane).
// Storage keeps the scrollback form; swap the displayed text here.
if sticky == crate::app::MOUSE_OFF_HINT_SCROLLBACK && self.active_pane == ActivePane::Prompt
{
return Some(crate::app::MOUSE_OFF_HINT_PROMPT);
}
Some(sticky)
}
/// Show a transient "Switched to mode: ..." banner above the prompt.
///
/// Triggered on Shift+Tab mode cycles.
/// Renders at full visibility for 2 s, then fades out over the final 0.3 s.
pub fn show_mode_switch_banner(&mut self, mode_name: &str) {
let msg = format!("Switched to mode: {}", mode_name);
self.mode_switch_banner = Some((msg, MODE_BANNER_TOTAL_TICKS));
}
/// Tick the mode-switch banner timer. Returns true if redraw needed
/// (active or just expired).
pub fn tick_mode_banner(&mut self) -> bool {
if let Some((_, ref mut remaining)) = self.mode_switch_banner {
if *remaining == 0 {
self.mode_switch_banner = None;
return true;
}
*remaining = remaining.saturating_sub(1);
return true; // redraw to advance fade
}
false
}
/// Copy text to clipboard and show the result toast.
pub fn copy_to_clipboard(&mut self, text: &str) -> bool {
let r = crate::clipboard::copy_text(text);
self.show_toast_ticks(r.message, r.ticks);
r.success
}
/// Like [`copy_to_clipboard`] but debounces the toast to prevent
/// rapid flickering during quick word/line selections.
pub(super) fn copy_to_clipboard_debounced(&mut self, text: &str) {
let now = Instant::now();
let too_soon = self
.last_clipboard_toast_at
.is_some_and(|t| now.duration_since(t).as_millis() < CLIPBOARD_TOAST_DEBOUNCE_MS);
if too_soon {
// Still copy, just skip the toast.
let _ = crate::clipboard::copy_text(text);
return;
}
self.last_clipboard_toast_at = Some(now);
self.copy_to_clipboard(text);
}
/// Returns `true` if the terminal can render pixel images. Shows a
/// toast and returns `false` when no graphics protocol is available.
pub(crate) fn guard_image_support(&mut self) -> bool {
if crate::terminal::image::detect_graphics_protocol().supports_images() {
return true;
}
let msg = match crate::terminal::terminal_context().graphics_protocol_skip_reason() {
Some("tmux") => "Inline images disabled within tmux.",
_ => "Image rendering not supported in this terminal",
};
self.show_toast_ticks(msg, 60);
false
}
/// Tick the transient toast timer. Call once per animation tick.
/// Returns true if the transient toast was removed (needs redraw so a
/// sticky banner can reappear).
pub fn tick_toast(&mut self) -> bool {
if let Some((_, ref mut remaining)) = self.toast {
if *remaining == 0 {
self.toast = None;
return true;
}
*remaining = remaining.saturating_sub(1);
}
false
}
/// Tick the extensions modal's transient result notice. Returns true if it
/// just expired (needs a redraw to erase the badge / status line).
pub fn tick_extensions_result_notice(&mut self) -> bool {
self.extensions_modal
.as_mut()
.is_some_and(|m| m.tick_result_notice())
}
}
#[cfg(test)]
mod mouse_off_banner_tests {
use super::test_fixtures::make_running_agent;
use super::*;
#[test]
fn mouse_off_banner_key_swaps_with_focused_pane() {
let mut view = make_running_agent();
view.set_sticky_toast(Some(crate::app::MOUSE_OFF_HINT_SCROLLBACK));
// Scrollback focus: Ctrl+R works there, so advertise it.
view.active_pane = AgentPane::Scrollback;
assert_eq!(
view.active_toast_message(),
Some(crate::app::MOUSE_OFF_HINT_SCROLLBACK)
);
// Prompt focus: the toggle chord is scrollback-only, so advertise the command.
view.active_pane = AgentPane::Prompt;
assert_eq!(
view.active_toast_message(),
Some(crate::app::MOUSE_OFF_HINT_PROMPT)
);
// A transient toast still wins over the sticky banner, regardless of pane.
view.show_toast("Copied!");
assert_eq!(view.active_toast_message(), Some("Copied!"));
}
#[test]
fn non_mouse_sticky_banner_is_not_swapped() {
let mut view = make_running_agent();
view.set_sticky_toast(Some("Reconnecting"));
view.active_pane = AgentPane::Prompt;
assert_eq!(view.active_toast_message(), Some("Reconnecting"));
}
}