Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,32 @@
# Changelog

## v0.23.0 — 2026-07-29 — the trace chip, and a second line that holds its edge

**New: cctrace trace chip.** A session whose wire is being captured by
[cctrace](https://github.com/thevibeworks/cctrace) (`deva --trace`, or
`cctrace` directly) now says so: `[cctrace:9317]` on the left, next to
path and branch — the port is the live trace UI on localhost. Session
identity, not pressure, so it's dim in the neutral lane. Detection takes
the strongest signal available: the trace env cctrace exports into the
traced process (`CCTRACE_SERVER_PORT`), the capture's CA plumbing
(`NODE_EXTRA_CA_CERTS` under a cctrace dir), or deva's `DEVA_TRACE=1`;
plumbing-only captures resolve the port through cctrace's live-instance
registry — by session id (sid8 prefix: the registry stores ids redacted
past the first 8 hex), then project path, then by being the only live
capture; the fallbacks trust heartbeat-fresh entries only, since crashed
runs leave "live" files behind. No resolvable port still shows a bare
`[cctrace]`: "recorded" matters even portless.

**Fixed: the advisor row right-aligns to line 1's actual edge.** The
statusline runs with stdout on a pipe, where `tput cols` answers a flat
80 no matter how wide the terminal is — line 1 overflowed the phantom
edge while the advisor anchored on it, leaving the advice dangling
mid-line under a much longer badge cluster. The advisor now anchors on
line 1's actual rendered width, so the second line's right edge meets
the first's whatever the width guess was. Width detection itself also
got honest: the controlling tty (`stty size </dev/tty`) and an inherited
`COLUMNS` both beat `tput`'s default. 348 tests (336 statusline + 12 installer).

## v0.22.0 — 2026-07-28 — the agent surface

**New: `skills/usage-insight` — teach Claude Code to read its own
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ Every component earns its place:
| Signal | Why it matters |
|--------|----------------|
| Path and branch | Know where Claude Code is writing. Neutral grey; a dirty branch brightens to white with a `*`. |
| Trace chip | `[cctrace:9317]` when the session's wire is being captured by [cctrace](https://github.com/thevibeworks/cctrace) (`deva --trace`, or `cctrace` directly) — the port is the live trace UI on localhost. Session identity, so it sits on the left with path and branch, dim (red stays reserved for pressure). Detected from the trace env cctrace exports into the traced process (`CCTRACE_SERVER_PORT`), from the capture's CA plumbing (`NODE_EXTRA_CA_CERTS` under a cctrace dir), or from `DEVA_TRACE=1`; when only the plumbing is visible (older cctrace) the port resolves through cctrace's live-instance registry, matched by session id (sid8 prefix — the registry stores ids redacted), then project path, then by being the only live capture, with the fallbacks trusting heartbeat-fresh entries only. A traced session with no resolvable port still shows a bare `[cctrace]` — "recorded" matters even portless. |
| Activity | Session diff without opening git. |
| Time and cost | Track long sessions. Hours format above 60m (`1h30m`). |
| Model | Abbreviated: `claude-opus-4-8` becomes `opus4.8`, `claude-fable-5` becomes `fabl5`, `claude-sonnet-5` becomes `sonnet5`. The `[1m]` tag marks a 1M-context session, detected from the window the CLI reports (`context_window_size`) — not the name — so it shows even when Claude Code strips the `[1m]` suffix (which it does since 2.1.173 whenever 1M is the default; Sonnet 5 joined Fable 5 on that path in 2.1.197). |
Expand Down
113 changes: 105 additions & 8 deletions statusline.sh
Original file line number Diff line number Diff line change
Expand Up @@ -451,7 +451,23 @@ get_context_limit() {

debug_log "PARSED INPUT: model=$model_display (id=$model_id) cwd=$current_dir cost=$cost_usd ctx_pct=$ctx_pct exceeds_200k=$exceeds_200k api_duration_ms=$api_duration_ms"

term_width=$(tput cols 2>/dev/null || echo 80)
# Terminal width, best signal first. The statusline runs with stdout on a
# pipe, where bare `tput cols` answers a flat 80 no matter how wide the real
# terminal is — the controlling tty (when we still have one) and an inherited
# COLUMNS both beat it. The advisor row additionally anchors on line 1's
# actual rendered width (see format_output), so a wrong width degrades the
# gap, never the alignment between the two rows.
term_width=""
if [ -e /dev/tty ]; then
term_width=$({ stty size </dev/tty; } 2>/dev/null | awk '{print $2}')
fi
if ! [ "$term_width" -gt 0 ] 2>/dev/null; then
term_width="${COLUMNS:-}"
fi
if ! [ "$term_width" -gt 0 ] 2>/dev/null; then
term_width=$(tput cols 2>/dev/null || echo 80)
fi
debug_log "TERM WIDTH: $term_width"

# Palette is organized in three lanes so a glance is unambiguous:
# STATUS (green/yellow/red) — pressure ONLY: quota, context, cache, premium
Expand Down Expand Up @@ -3099,6 +3115,73 @@ if [ -d "$current_dir/.git" ] || git -C "$current_dir" rev-parse --git-dir >/dev
fi
fi

# cctrace trace chip: is this session's wire being captured, and where does
# the live UI serve? Session identity, so it lives on the LEFT with path and
# branch (structure lane, dim — red stays reserved for pressure). Detection,
# strongest signal first:
# CCTRACE_SERVER_PORT / CCTRACE_TRACE_FILE cctrace >= 0.29 exports trace
# identity into the child env
# NODE_EXTRA_CA_CERTS under a cctrace dir older mitm captures leave only
# proxy plumbing behind
# DEVA_TRACE=1 deva --trace says so outright
# Without the port env (old cctrace), the live-instance registry
# (<data-dir>/instances/*.json, a documented contract) is matched by this
# session's id, then by project path, then by being the only live capture.
# The registry stores session ids REDACTED past the first 8 hex
# ("c3a6e0f3-****-…" — capture-time redaction, ids never land on disk in
# full), so the sid8 prefix is the join key — the same convention cctrace's
# own UI uses. The id match trusts any non-tombstone entry (if OUR capture
# died, this session's proxy died with it); the path/only-live fallbacks
# trust only heartbeat-fresh files (<2min, heartbeat is 30s) — crashed runs
# leave "live" entries behind for up to a day, and a stale port is worse
# than no port. No match still shows the bare chip: "recorded" matters
# even portless.
build_trace_component() {
local port="${CCTRACE_SERVER_PORT:-}"
local traced=""
[ -n "$port" ] && traced=1
[ -n "${CCTRACE_TRACE_FILE:-}" ] && traced=1
case "${NODE_EXTRA_CA_CERTS:-}" in
*/cctrace/*) traced=1 ;;
esac
[ "${DEVA_TRACE:-}" = "1" ] && traced=1
[ -n "$traced" ] || return 0

if [ -z "$port" ]; then
local reg="${CCTRACE_DATA_DIR:-}"
if [ -z "$reg" ]; then
case "${NODE_EXTRA_CA_CERTS:-}" in
*/cctrace/*) reg=$(dirname "$(dirname "$NODE_EXTRA_CA_CERTS")") ;;
*) reg="$CLAUDE_HOME/.local/share/cctrace" ;;
esac
fi
reg="$reg/instances"
if [ -d "$reg" ]; then
port=$(cat "$reg"/*.json 2>/dev/null | jq -rs --arg sid "${stdin_session_id:-}" '
[ .[] | select(type == "object" and .endedAt == null) ]
| map(select(($sid | length) >= 8 and
((.sessionId // "")[0:8] == $sid[0:8])))
| first.port // empty' 2>/dev/null)
if [ -z "$port" ]; then
port=$(find "$reg" -name '*.json' -newermt '-120 seconds' 2>/dev/null \
| xargs cat 2>/dev/null | jq -rs --arg cwd "${current_dir:-}" '
[ .[] | select(type == "object" and .endedAt == null) ] as $fresh |
(($fresh | map(select(.projectPath == $cwd and $cwd != ""))) +
(if ($fresh | length) == 1 then $fresh else [] end))
| first.port // empty' 2>/dev/null)
fi
fi
fi

if [ -n "$port" ]; then
echo " ${DIM}[cctrace:${port}]${RESET}"
else
echo " ${DIM}[cctrace]${RESET}"
fi
}

trace_info=$(build_trace_component)

# settings.json .model is only a fallback for the rare case stdin carries no
# model id — it's a static default a session can override, so it must not be
# read (let alone preferred) when stdin already tells us the running model.
Expand Down Expand Up @@ -3678,8 +3761,14 @@ for item in "${order_array[@]}"; do
esac
done

# Renders line 1 and records its actual width in line1_cols: the advisor row
# right-aligns to THAT edge, not to term_width — when the width guess is
# wrong (tput's flat 80 under a pipe) line 1 overflows its phantom edge, and
# anchoring line 2 on the same phantom left it dangling mid-line.
line1_cols=0

format_output() {
local path_part="${DIM}${display_path}${RESET}${git_info}"
local path_part="${DIM}${display_path}${RESET}${git_info}${trace_info}"
local stats_part="${right_parts}"

# Strip ANSI escapes for length calculation (portable: printf %b instead of echo -e)
Expand All @@ -3690,11 +3779,13 @@ format_output() {
"left-right")
local padding=$((term_width - ${#path_plain} - ${#stats_plain} - 5))
[ $padding -lt 6 ] && padding=6
line1_cols=$((${#path_plain} + padding + ${#stats_plain}))
printf "%b%*s%b\n" "$path_part" $padding "" "$stats_part"
;;
"right-left")
local padding=$((term_width - ${#path_plain} - ${#stats_plain} - 5))
[ $padding -lt 6 ] && padding=6
line1_cols=$((${#stats_plain} + padding + ${#path_plain}))
printf "%b%*s%b\n" "$stats_part" $padding "" "$path_part"
;;
"center")
Expand All @@ -3703,16 +3794,19 @@ format_output() {
local left_padding=$(((term_width - ${#path_plain}) / 2))
local right_padding=$((term_width - left_padding - ${#path_plain} - ${#stats_plain}))
[ $right_padding -lt 2 ] && right_padding=2
line1_cols=$((left_padding + ${#path_plain} + right_padding + ${#stats_plain}))
printf "%*s%b%*s%b\n" $left_padding "" "$path_part" $right_padding "" "$stats_part"
else
local padding=$((term_width - ${#path_plain} - ${#stats_plain} - 5))
[ $padding -lt 6 ] && padding=6
line1_cols=$((${#path_plain} + padding + ${#stats_plain}))
printf "%b%*s%b\n" "$path_part" $padding "" "$stats_part"
fi
;;
*)
local padding=$((term_width - ${#path_plain} - ${#stats_plain} - 5))
[ $padding -lt 6 ] && padding=6
line1_cols=$((${#path_plain} + padding + ${#stats_plain}))
printf "%b%*s%b\n" "$path_part" $padding "" "$stats_part"
;;
esac
Expand All @@ -3723,23 +3817,26 @@ format_output
# Advisor row: a second stdout line renders as its own row in Claude Code's
# status area, and printing nothing produces no row — so a quiet advisor
# costs zero height. Single-hue by design, which keeps truncation honest.
# Right-aligned to the same anchor the stats cluster ends at in format_output
# (term_width - 5), so the advice sits directly beneath the usage badges it
# interprets, stable across widths and message lengths; when the stats sit
# left (right-left alignment) the advisor stays left with them.
# Right-aligned to line 1's ACTUAL rendered edge (line1_cols, recorded by
# format_output) — not to term_width, which lies under a pipe (tput says 80,
# line 1 overflows it, and a term_width anchor left the advice dangling
# mid-line). Anchoring on the row above keeps the advice directly beneath
# the usage badges it interprets, whatever the width guess was; when the
# stats sit left (right-left alignment) the advisor stays left with them.
if [ "$advisor_display_mode" != "off" ] && [ -n "${usage_data:-}" ]; then
advisor_line=$(build_advisor_line "$usage_data" "$advisor_display_mode" "${model_id:-$model_display}")
if [ -n "$advisor_line" ]; then
advisor_plain=$(printf '%b' "$advisor_line" | sed 's/\x1b\[[0-9;]*m//g')
advisor_max=$((term_width - 1))
advisor_anchor=$((line1_cols > 0 ? line1_cols : term_width - 5))
advisor_max=$((advisor_anchor > term_width - 1 ? advisor_anchor : term_width - 1))
if [ "${#advisor_plain}" -gt "$advisor_max" ] 2>/dev/null && [ "$advisor_max" -gt 1 ]; then
advisor_color=$(printf '%s' "$advisor_line" | grep -o '^\\033\[[0-9;]*m' | head -1)
advisor_plain="${advisor_plain:0:$((advisor_max - 1))}…"
advisor_line="${advisor_color}${advisor_plain}${RESET}"
fi
advisor_pad=0
if [ "$alignment" != "right-left" ]; then
advisor_pad=$((term_width - 5 - ${#advisor_plain}))
advisor_pad=$((advisor_anchor - ${#advisor_plain}))
[ "$advisor_pad" -gt 0 ] 2>/dev/null || advisor_pad=0
fi
debug_log "ADVISOR: $advisor_plain"
Expand Down
8 changes: 7 additions & 1 deletion t/helpers.bash
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,12 @@ export STATUSLINE_NO_FETCH
# would add an account chip and rescope cache dirs in every integration run.
unset DEVA_AUTH_TAG DEVA_AUTH_METHOD DEVA_AUTH_DETAILS STATUSLINE_ACCOUNT ACCOUNT_TAG

# Trace identity must come from each test too: a test host running under
# `deva --trace` / cctrace (this repo's own dev loop, literally) carries the
# capture env and a live instance registry — leaking either in would put a
# [cctrace:PORT] chip into every integration render.
unset CCTRACE_SERVER_PORT CCTRACE_TRACE_FILE CCTRACE_INSTANCE_ID CCTRACE_DATA_DIR DEVA_TRACE NODE_EXTRA_CA_CERTS

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"

# Color constants (needed by functions)
Expand Down Expand Up @@ -64,7 +70,7 @@ debug_log() {
# Source individual functions by extracting them from statusline.sh.
# This is deliberate: we test the actual production code, not copies.
eval "$(awk '
/^(abbreviate_model_id|get_runtime_model|format_reset_relative|format_reset_absolute|get_reset_seconds|format_duration|should_show_extra|get_cache_health|infer_cache_ttl_class|build_cache_indicator|get_usage_color|get_seven_day_color|seven_day_elapsed|seven_day_pace|weekend_secs_ahead|get_adaptive_ttl|curl_ca_bundle|acquire_lock|reap_stale_lock|fetch_usage_for_session|merge_stdin_rate_limits|rotate_usage_log|build_seven_day_profile|seven_day_forecast|premium_band_level|abbrev_effort|effort_color|_epoch_from_ts|_fmt_epoch|render_bar|format_money_minor|oauth_token_expired|refresh_oauth_credentials_file|is_default_1m_family|get_context_limit|is_1m_model|rotate_debug_log|build_display_path|delta_flash|delta_flash_part|quota_bump_notice|record_fetch_error|fetch_error_remaining|fetch_error_badge|model_scope_abbrev|build_scoped_quota_display|build_usage_display|build_extra_usage_display|build_user_info|get_user_tier|build_advisor_line|build_advisor_fleet_hint|_seven_day_walk|forecast_pct_per_window|log_usage_snapshot|detect_session_boundary|run_usage_report|run_check|run_session_summary)\(\)/ { capture=1 }
/^(abbreviate_model_id|get_runtime_model|format_reset_relative|format_reset_absolute|get_reset_seconds|format_duration|should_show_extra|get_cache_health|infer_cache_ttl_class|build_cache_indicator|get_usage_color|get_seven_day_color|seven_day_elapsed|seven_day_pace|weekend_secs_ahead|get_adaptive_ttl|curl_ca_bundle|acquire_lock|reap_stale_lock|fetch_usage_for_session|merge_stdin_rate_limits|rotate_usage_log|build_seven_day_profile|seven_day_forecast|premium_band_level|abbrev_effort|effort_color|_epoch_from_ts|_fmt_epoch|render_bar|format_money_minor|oauth_token_expired|refresh_oauth_credentials_file|is_default_1m_family|get_context_limit|is_1m_model|rotate_debug_log|build_display_path|build_trace_component|delta_flash|delta_flash_part|quota_bump_notice|record_fetch_error|fetch_error_remaining|fetch_error_badge|model_scope_abbrev|build_scoped_quota_display|build_usage_display|build_extra_usage_display|build_user_info|get_user_tier|build_advisor_line|build_advisor_fleet_hint|_seven_day_walk|forecast_pct_per_window|log_usage_snapshot|detect_session_boundary|run_usage_report|run_check|run_session_summary)\(\)/ { capture=1 }
capture { print }
capture && /^}$/ { capture=0 }
' "$SCRIPT_DIR/statusline.sh")"
Expand Down
Loading
Loading