feat: add environment sections, visual enhancements, enhanced tools/beads, and /clear detection

The feature layer that builds on the new infrastructure modules. Adds
4 new environment-aware sections, rewrites the tools/beads/turns sections,
introduces gradient sparklines and block-style context bars, and wires
/clear detection into the main binary.

New sections (4):
  cloud_profile — Shows active cloud provider profile from env vars
    ($AWS_PROFILE, $CLOUDSDK_CORE_PROJECT, $AZURE_SUBSCRIPTION_ID).
    Provider-specific coloring (AWS orange, GCP blue, Azure blue).

  k8s_context — Parses kubeconfig for current-context and namespace.
    Minimal YAML scanning (no yaml dependency). 30s TTL cache.
    Shows "context/namespace" with split coloring.

  python_env — Detects active virtualenv ($VIRTUAL_ENV) or conda
    ($CONDA_DEFAULT_ENV, excluding "base"). Shows just the env name.

  toolchain — Detects Rust (rust-toolchain.toml) and Node.js (.nvmrc,
    .node-version) versions. Compares expected vs actual ($RUSTUP_TOOLCHAIN,
    $NODE_VERSION) and highlights mismatches in yellow.

Tools section rewrite:
  Progressive disclosure based on terminal width:
    - Narrow: just the count ("245")
    - Medium: count + last tool name ("245 tools (Bash)")
    - Wide: per-tool color-coded breakdown ("245 tools (Bash: 84/Read: 35/...)")
  Adaptive width budgeting: breakdown reduces tool count until it fits
  within 1/3 of terminal width. Color palette priority: config > terminal
  ANSI palette (via OSC 4) > built-in Dracula palette.

Beads section rewrite:
  Switched from `br ready --json` to `br stats --json` to show all
  statuses. Now renders multi-status breakdown: "3 ready 1 wip 2 open"
  with per-status visibility toggles in config.

Turns section:
  Falls back to transcript-derived turn count when cost.total_turns is
  absent. Requires at least one data source to render (vanishes when
  no session data exists at all).

Visual enhancements:
  trend.rs:
    - append_delta(): tracks rate-of-change (delta between cumulative
      samples) so sparklines show burn intensity, not monotonic growth
    - sparkline(): now renders exactly `width` chars with left-padding
      for missing data. Baseline (space) vs flatline (lowest bar) chars.
    - sparkline_colored(): per-character gradient coloring via colorgrad,
      returns (raw, ansi) tuple for layout compatibility.

  context_bar.rs:
    - Block style: Unicode full-block fill + light-shade empty chars
    - Per-character green->yellow->red gradient for block style
    - Classic style preserved (= and - chars) with single threshold color
    - Configurable fill_char/empty_char overrides

  context_trend + cost_trend:
    Switched to append_delta for rate-based sparklines. Gradient coloring
    with green->yellow->red via sparkline_colored().

  format.rs:
    Background color support via resolve_background(). Accepts named
    colors, hex, and palette refs. Applied as ANSI bg wrap around section
    output, preserving foreground colors.

  layout/mod.rs:
    - Separator styles: text (default), powerline (Nerd Font), arrow
      (Unicode), none (spaces). Powerline auto-falls-back to arrow when
      glyphs disabled.
    - Placeholder support: when an enabled section returns None (no data),
      substitutes a configurable placeholder character (default: box-draw)
      to maintain layout stability during justify/flex.

Section refinements:
  cost, cost_velocity, token_velocity, duration, tokens_raw — now show
  zero/baseline values instead of hiding entirely. This prevents layout
  jumps when sessions start or after /clear.

  context_usage — uses current_usage fields (input_tokens +
  cache_creation + cache_read) for precise token counts instead of
  percentage-derived estimates. Shows one decimal place on percentage.

  metrics.rs — prefers total_api_duration_ms over total_duration_ms for
  velocity calculations (active processing vs wall clock with idle time).
  Cache efficiency now divides by total input (not just cache tokens).

Config additions (config.rs):
  SeparatorStyle enum (text/powerline/arrow/none), BarStyle enum
  (classic/block), gradient toggle on trends + context_bar, background
  and placeholder on SectionBase, tools breakdown config (show_breakdown,
  top_n, palette), 4 new section structs.

Main binary (/clear detection + wiring):
  detect_clear() — watches for significant context usage drops (>15%
  to <5%, >20pp drop) to identify /clear. On detection: saves transcript
  offset so derived stats only count post-clear entries, flushes trend
  caches for fresh sparklines.

  resolve_transcript_stats() — cached transcript parsing with 5s TTL,
  respects clear offset, skipped when cost already has tool counts.

  resolve_terminal_palette() — cached palette detection with 1h TTL.

  Debug: CLAUDE_STATUSLINE_DEBUG env var dumps raw input JSON to
  /tmp/claude-statusline-input.json. dump-state now includes input data.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Taylor Eernisse
2026-02-09 23:42:34 -05:00
parent e0c4a0fa9a
commit c03b0b1bd7
23 changed files with 1264 additions and 92 deletions

View File

@@ -1,23 +1,51 @@
use crate::color;
use crate::config::BarStyle;
use crate::section::{RenderContext, SectionOutput};
/// Render context bar at a given bar_width. Called both at initial render
/// and during flex expansion (with wider bar_width).
pub fn render_at_width(ctx: &RenderContext, bar_width: u16) -> Option<SectionOutput> {
let pct = ctx.input.context_window.as_ref()?.used_percentage?;
let pct = ctx
.input
.context_window
.as_ref()?
.used_percentage
.unwrap_or(0.0);
let pct_int = pct.round() as u16;
let filled = (u32::from(pct_int) * u32::from(bar_width) / 100) as usize;
let empty = bar_width as usize - filled;
let bar = "=".repeat(filled) + &"-".repeat(empty);
let raw = format!("[{bar}] {pct_int}%");
let cfg = &ctx.config.sections.context_bar;
let thresh = &cfg.thresholds;
let thresh = &ctx.config.sections.context_bar.thresholds;
let color_code = threshold_color(pct, thresh);
// Determine characters based on bar_style
let (fill_ch, empty_ch) = match cfg.bar_style {
BarStyle::Block => {
let f = cfg.fill_char.as_deref().unwrap_or("\u{2588}");
let e = cfg.empty_char.as_deref().unwrap_or("\u{2591}");
(f, e)
}
BarStyle::Classic => {
let f = cfg.fill_char.as_deref().unwrap_or("=");
let e = cfg.empty_char.as_deref().unwrap_or("-");
(f, e)
}
};
// Build raw string (always plain, no ANSI)
let bar_raw = fill_ch.repeat(filled) + &empty_ch.repeat(empty);
let raw = format!("[{bar_raw}] {pct_int}%");
let ansi = if ctx.color_enabled {
format!("{color_code}[{bar}] {pct_int}%{}", color::RESET)
if cfg.gradient && matches!(cfg.bar_style, BarStyle::Block) {
// Per-character gradient coloring
render_gradient_bar(filled, empty, bar_width, fill_ch, empty_ch, pct_int)
} else {
// Single threshold color for entire bar
let color_code = threshold_color(pct, thresh);
format!("{color_code}[{bar_raw}] {pct_int}%{}", color::RESET)
}
} else {
raw.clone()
};
@@ -25,6 +53,41 @@ pub fn render_at_width(ctx: &RenderContext, bar_width: u16) -> Option<SectionOut
Some(SectionOutput { raw, ansi })
}
/// Build a gradient-colored context bar. Each filled char gets a color
/// from a green→yellow→red gradient based on its position.
fn render_gradient_bar(
filled: usize,
empty: usize,
bar_width: u16,
fill_ch: &str,
empty_ch: &str,
pct_int: u16,
) -> String {
let grad = color::make_gradient(&["#50fa7b", "#f1fa8c", "#ff5555"]);
let bw = bar_width as f32;
let mut result = String::with_capacity(filled * 20 + empty * 10 + 20);
result.push('[');
for i in 0..filled {
let t = if bw > 1.0 { i as f32 / (bw - 1.0) } else { 0.0 };
result.push_str(&color::sample_fg(&grad, t));
result.push_str(fill_ch);
}
if empty > 0 {
result.push_str(color::DIM);
for _ in 0..empty {
result.push_str(empty_ch);
}
}
result.push_str(color::RESET);
result.push_str(&format!("] {pct_int}%"));
result
}
pub fn render(ctx: &RenderContext) -> Option<SectionOutput> {
if !ctx.config.sections.context_bar.base.enabled {
return None;