Turn status: moon-phase spinner for Waiting for response

The active-turn spinner next to "Waiting for response…" now cycles
the moon phases 🌑🌒🌓🌔🌕🌖🌗🌘 — the official kimi-cli's lunation
animation (rich's `moon` spinner) — matching the welcome-screen moon
logo. New glyphs::moon_spinner_frames() with the same legacy-ConHost
ASCII fallback as every other spinner set; emoji frames are 2 columns
and the status row already measures the rendered frame, so layout
adapts. The ambient "Starting session…" row and other spinners stay
braille.

Verified: kigi-tui lib 6621 + pager-render 963 tests green; real-binary
PTY e2e waiting_for_model_label_shows_before_first_token passes with
the new spinner rendering in a live vt.
This commit is contained in:
2026-07-18 12:00:52 -04:00
parent 0dc98a34a6
commit 82ebcc38b7
2 changed files with 38 additions and 8 deletions
+34 -4
View File
@@ -218,10 +218,12 @@ pub fn diamond_hollow_char() -> char {
/// 1-column ASCII spinner (`|`, `/`, `-`, `\`) on legacy ConHost. /// 1-column ASCII spinner (`|`, `/`, `-`, `\`) on legacy ConHost.
/// ///
/// The U+2800 Braille Patterns block is not part of CP437 and renders as /// The U+2800 Braille Patterns block is not part of CP437 and renders as
/// tofu on the legacy console raster font, so the turn-status line, the /// tofu on the legacy console raster font, so the starting-session row,
/// MCP-connecting chip, the image-viewer loader, and the `/btw` overlay /// the MCP-connecting chip, the image-viewer loader, and the `/btw`
/// all fall back to the classic ASCII spinner there. Every frame in both /// overlay all fall back to the classic ASCII spinner there. Every frame
/// sets is exactly 1 column so the surrounding layout never shifts. /// in both sets is exactly 1 column so the surrounding layout never
/// shifts. (The turn-status "Waiting for response…" line uses
/// [`moon_spinner_frames`] instead.)
pub fn braille_spinner_frames() -> &'static [&'static str] { pub fn braille_spinner_frames() -> &'static [&'static str] {
const FANCY: &[&str] = &[ const FANCY: &[&str] = &[
"\u{280b}", "\u{2819}", "\u{2839}", "\u{2838}", "\u{283c}", "\u{2834}", "\u{2826}", "\u{280b}", "\u{2819}", "\u{2839}", "\u{2838}", "\u{283c}", "\u{2834}", "\u{2826}",
@@ -235,6 +237,34 @@ pub fn braille_spinner_frames() -> &'static [&'static str] {
} }
} }
/// Moon-phase spinner frames (`🌑🌒🌓🌔🌕🌖🌗🌘`) normally; the ASCII
/// spinner on legacy ConHost.
///
/// The turn-status "Waiting for response…" line reuses the official
/// kimi-cli's lunation animation (rich's `moon` spinner) — one full
/// lunation per cycle, matching the welcome-screen moon logo. Emoji
/// frames are 2 columns wide; the caller measures the rendered frame
/// (`spinner_str.width()`) so the layout adapts. Legacy ConHost's raster
/// font has no emoji, so it falls back to the 1-column ASCII spinner.
pub fn moon_spinner_frames() -> &'static [&'static str] {
const FANCY: &[&str] = &[
"\u{1f311}",
"\u{1f312}",
"\u{1f313}",
"\u{1f314}",
"\u{1f315}",
"\u{1f316}",
"\u{1f317}",
"\u{1f318}",
];
const FALLBACK: &[&str] = &["|", "/", "-", "\\"];
if is_legacy_windows_console() {
FALLBACK
} else {
FANCY
}
}
/// Pulsing dot progress-spinner frames (`⋅ : ⸬ ⁙`) normally; a quiet /// Pulsing dot progress-spinner frames (`⋅ : ⸬ ⁙`) normally; a quiet
/// 1-column dot cycle (`.`, `:`, `·`) on legacy ConHost. /// 1-column dot cycle (`.`, `:`, `·`) on legacy ConHost.
/// ///
@@ -351,13 +351,13 @@ pub fn render_turn_status(
// ── Build components ── // ── Build components ──
// While a tool is blocked on a permission prompt or `ask_user_question`, // While a tool is blocked on a permission prompt or `ask_user_question`,
// swap the running braille spinner for a pulsing `◆`. Same animation // swap the spinning moon for a pulsing `◆`. Same animation shape the
// shape the drain-blocked and plan-approval indicators already use, // drain-blocked and plan-approval indicators already use, so every
// so every "your turn" status reads with one consistent visual cue. // "your turn" status reads with one consistent visual cue.
let spinner_str = if is_pending_user_input { let spinner_str = if is_pending_user_input {
format!("{} ", crate::glyphs::diamond_filled()) format!("{} ", crate::glyphs::diamond_filled())
} else { } else {
let frames = crate::glyphs::braille_spinner_frames(); let frames = crate::glyphs::moon_spinner_frames();
let frame_idx = (tick / SPINNER_DIVISOR) as usize % frames.len(); let frame_idx = (tick / SPINNER_DIVISOR) as usize % frames.len();
format!("{} ", frames[frame_idx]) format!("{} ", frames[frame_idx])
}; };