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>
103 lines
2.9 KiB
Rust
103 lines
2.9 KiB
Rust
use crate::color;
|
|
use crate::section::{RenderContext, SectionOutput};
|
|
use crate::shell;
|
|
use std::time::Duration;
|
|
|
|
/// Cached format: "open:N,wip:N,ready:N,closed:N"
|
|
pub fn render(ctx: &RenderContext) -> Option<SectionOutput> {
|
|
if !ctx.config.sections.beads.base.enabled {
|
|
return None;
|
|
}
|
|
|
|
// Check if .beads/ exists in project dir
|
|
if !ctx.project_dir.join(".beads").is_dir() {
|
|
return None;
|
|
}
|
|
|
|
// --no-shell: serve stale cache only
|
|
if ctx.no_shell {
|
|
return render_from_summary(ctx, &ctx.cache.get_stale("beads_stats")?);
|
|
}
|
|
|
|
let ttl = Duration::from_secs(ctx.config.sections.beads.ttl);
|
|
|
|
let cached = ctx.cache.get("beads_stats", ttl);
|
|
let summary = cached.or_else(|| {
|
|
let out = ctx.shell_results.get("beads").cloned().unwrap_or_else(|| {
|
|
shell::exec_gated(
|
|
ctx.shell_config,
|
|
"br",
|
|
&["stats", "--json"],
|
|
Some(ctx.project_dir.to_str()?),
|
|
)
|
|
})?;
|
|
let summary = parse_stats(&out)?;
|
|
ctx.cache.set("beads_stats", &summary);
|
|
Some(summary)
|
|
})?;
|
|
|
|
render_from_summary(ctx, &summary)
|
|
}
|
|
|
|
/// Parse `br stats --json` output into "open:N,wip:N,ready:N,closed:N"
|
|
fn parse_stats(json: &str) -> Option<String> {
|
|
let v: serde_json::Value = serde_json::from_str(json).ok()?;
|
|
let s = v.get("summary")?;
|
|
let open = s.get("open_issues")?.as_u64().unwrap_or(0);
|
|
let wip = s.get("in_progress_issues")?.as_u64().unwrap_or(0);
|
|
let ready = s.get("ready_issues")?.as_u64().unwrap_or(0);
|
|
let closed = s.get("closed_issues")?.as_u64().unwrap_or(0);
|
|
Some(format!(
|
|
"open:{open},wip:{wip},ready:{ready},closed:{closed}"
|
|
))
|
|
}
|
|
|
|
fn render_from_summary(ctx: &RenderContext, summary: &str) -> Option<SectionOutput> {
|
|
let mut open = 0u64;
|
|
let mut wip = 0u64;
|
|
let mut ready = 0u64;
|
|
let mut closed = 0u64;
|
|
|
|
for part in summary.split(',') {
|
|
if let Some((key, val)) = part.split_once(':') {
|
|
let n: u64 = val.parse().unwrap_or(0);
|
|
match key {
|
|
"open" => open = n,
|
|
"wip" => wip = n,
|
|
"ready" => ready = n,
|
|
"closed" => closed = n,
|
|
_ => {}
|
|
}
|
|
}
|
|
}
|
|
|
|
let cfg = &ctx.config.sections.beads;
|
|
let mut parts: Vec<String> = Vec::new();
|
|
|
|
if cfg.show_ready_count && ready > 0 {
|
|
parts.push(format!("{ready} ready"));
|
|
}
|
|
if cfg.show_wip_count && wip > 0 {
|
|
parts.push(format!("{wip} wip"));
|
|
}
|
|
if cfg.show_open_count && open > 0 {
|
|
parts.push(format!("{open} open"));
|
|
}
|
|
if cfg.show_closed_count && closed > 0 {
|
|
parts.push(format!("{closed} done"));
|
|
}
|
|
|
|
if parts.is_empty() {
|
|
return None;
|
|
}
|
|
|
|
let raw = parts.join(" ");
|
|
let ansi = if ctx.color_enabled {
|
|
format!("{}{raw}{}", color::DIM, color::RESET)
|
|
} else {
|
|
raw.clone()
|
|
};
|
|
|
|
Some(SectionOutput { raw, ansi })
|
|
}
|