Files
Kigi-CLI/crates/codegen/kigi-tui/src/app/display_refresh_startup.rs
T
ZacharyZhang-NY d6c20fc13f 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).
2026-07-17 05:31:01 -04:00

194 lines
6.8 KiB
Rust

//! Display-refresh probe + motion cadence at TUI startup.
//!
//! Owns env cadence knobs, fail-closed probe planning (sync only when auto can
//! change a clock), paint-clock resolution, and the terminal + display-refresh
//! telemetry `spawn_blocking`. Keeps the event loop free of this policy glue.
use std::time::Duration;
use kigi_shell::util::config::{
DISPLAY_REFRESH_DEFAULT_CADENCE_MS, MotionCadence, RemoteSettings, resolve_display_refresh,
resolve_motion_cadence,
};
use toml::Value as TomlValue;
/// Inclusive bounds for motion cadence env knobs (`KIGI_MIN_DRAW_MS`,
/// `KIGI_SCROLL_CADENCE_MS`).
const CADENCE_ENV_MIN_MS: u64 = 1;
const CADENCE_ENV_MAX_MS: u64 = 100;
/// Paint clocks resolved once at startup (fixed for the event-loop lifetime).
#[derive(Debug, Clone, Copy)]
pub struct MotionClocks {
pub min_draw_interval: Duration,
pub scroll_cadence: Duration,
}
/// Pure parse for cadence ms: trim/empty/invalid → `default_ms`, clamp 1..=100.
fn parse_cadence_ms(raw: Option<&str>, default_ms: u64) -> u64 {
raw.and_then(|v| {
let t = v.trim();
if t.is_empty() {
None
} else {
t.parse::<u64>().ok()
}
})
.unwrap_or(default_ms)
.clamp(CADENCE_ENV_MIN_MS, CADENCE_ENV_MAX_MS)
}
/// Read a cadence env knob: `(set, ms)`. `set` is true when the var is present
/// (empty/invalid still counts as set → `default_ms` after clamp 1..=100).
fn cadence_ms_from_env(name: &str, default_ms: u64) -> (bool, u64) {
match std::env::var(name) {
Ok(raw) => (true, parse_cadence_ms(Some(&raw), default_ms)),
Err(_) => (false, default_ms),
}
}
/// How to obtain the display-refresh probe for telemetry (and optionally cadence).
enum ProbePlan {
/// Kill switch: no FFI; emit skipped/disabled.
Disabled,
/// Auto-cadence needs Hz before paint clocks pin — probe on the main path.
Sync(crate::host::DisplayRefreshProbeResult),
/// Telemetry-only: probe in the background task (default path).
Async,
}
struct StartupTel {
plan: ProbePlan,
auto_cadence_enabled: bool,
cadence: MotionCadence,
}
/// Resolve policy, pin motion clocks, and spawn terminal + display-refresh
/// telemetry off the first-paint path.
pub fn start(
requirements: Option<&TomlValue>,
user: Option<&TomlValue>,
managed: Option<&TomlValue>,
remote: Option<&RemoteSettings>,
) -> MotionClocks {
let policy = resolve_display_refresh(requirements, user, managed, remote);
let default_cadence_ms = DISPLAY_REFRESH_DEFAULT_CADENCE_MS;
let (min_draw_env_set, min_draw_env_ms) =
cadence_ms_from_env("KIGI_MIN_DRAW_MS", default_cadence_ms);
let (scroll_env_set, scroll_env_ms) =
cadence_ms_from_env("KIGI_SCROLL_CADENCE_MS", default_cadence_ms);
let both_env_cadence = min_draw_env_set && scroll_env_set;
let need_probe_for_cadence =
policy.probe_enabled && policy.auto_cadence_enabled && !both_env_cadence;
let plan = if !policy.probe_enabled {
ProbePlan::Disabled
} else if need_probe_for_cadence {
ProbePlan::Sync(crate::host::probe_display_refresh())
} else {
ProbePlan::Async
};
let probe_hz = match &plan {
ProbePlan::Sync(p) => p.hz,
ProbePlan::Disabled | ProbePlan::Async => None,
};
let cadence = resolve_motion_cadence(
&policy,
probe_hz,
min_draw_env_set.then_some(min_draw_env_ms),
scroll_env_set.then_some(scroll_env_ms),
);
let clocks = MotionClocks {
min_draw_interval: Duration::from_millis(cadence.min_draw_ms),
scroll_cadence: Duration::from_millis(cadence.scroll_ms),
};
let tel = StartupTel {
plan,
auto_cadence_enabled: policy.auto_cadence_enabled,
cadence,
};
spawn_terminal_and_display_refresh_telemetry(tel);
clocks
}
fn spawn_terminal_and_display_refresh_telemetry(tel: StartupTel) {
tokio::task::spawn_blocking(move || {
let t = crate::terminal::terminal_context().diagnostics_snapshot();
let _span = tracing::info_span!(
"terminal.detect",
terminal.brand = %t.brand,
terminal.multiplexer = %t.multiplexer,
terminal.is_ssh = t.is_ssh,
terminal.is_byobu = t.is_byobu,
terminal.tmux_version = %t.tmux_version,
terminal.term_var = %t.term_var,
terminal.xtversion = %t.xtversion,
)
.entered();
tracing::info!("terminal environment detected");
let (outcome, hz, source, skip_reason, duration_ms) = match tel.plan {
ProbePlan::Disabled => ("skipped", None, "none".into(), "disabled".into(), 0_u64),
ProbePlan::Sync(p) => (
p.outcome(),
p.hz,
p.source.to_string(),
p.skip_reason.to_string(),
p.duration_ms,
),
ProbePlan::Async => {
let p = crate::host::probe_display_refresh();
(
p.outcome(),
p.hz,
p.source.to_string(),
p.skip_reason.to_string(),
p.duration_ms,
)
}
};
let c = tel.cadence;
// OTLP path: numerics as i64 (u32/u64 fall back to strings and get dropped).
let duration_ms_i = duration_ms as i64;
let min_draw_i = c.min_draw_ms as i64;
let scroll_i = c.scroll_ms as i64;
let hz_i = hz.map(i64::from);
tracing::info!(
outcome,
hz = hz_i,
source = %source,
skip_reason = %skip_reason,
duration_ms = duration_ms_i,
auto_cadence_enabled = tel.auto_cadence_enabled,
auto_cadence_applied = c.auto_applied,
effective_min_draw_ms = min_draw_i,
effective_scroll_cadence_ms = scroll_i,
auto_cadence_reason = c.reason,
"display refresh probed"
);
});
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_cadence_ms_matrix() {
assert_eq!(parse_cadence_ms(None, 16), 16, "unset → default");
assert_eq!(parse_cadence_ms(Some(""), 16), 16, "empty → default");
assert_eq!(parse_cadence_ms(Some(" "), 16), 16, "whitespace → default");
assert_eq!(parse_cadence_ms(Some("nope"), 16), 16, "invalid → default");
assert_eq!(parse_cadence_ms(Some("0"), 16), 1, "below min → clamp 1");
assert_eq!(
parse_cadence_ms(Some("101"), 16),
100,
"above max → clamp 100"
);
assert_eq!(parse_cadence_ms(Some("8"), 16), 8, "in range");
assert_eq!(parse_cadence_ms(Some(" 8 "), 16), 8, "trim + in range");
assert_eq!(parse_cadence_ms(Some("1"), 16), 1, "min inclusive");
assert_eq!(parse_cadence_ms(Some("100"), 16), 100, "max inclusive");
}
}