From 1e65a86f71c07546420907f5e290cb006871fbc3 Mon Sep 17 00:00:00 2001 From: Eric Wang Date: Wed, 29 Jul 2026 04:10:13 -0700 Subject: [PATCH 1/3] The trace chip + the advisor's true edge MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two taste fixes for the two-line render. A session traced by cctrace (deva --trace, or cctrace directly) now wears a dim [cctrace:PORT] chip on the LEFT, next to path and branch: being recorded is session identity, so it lives in the structure lane — red stays reserved for pressure. Detection takes the strongest signal available: the trace env cctrace exports into its child (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 (older cctrace) resolve the port through the live-instance registry — session id first, then project path, then only-live-capture; tombstones never lend their port. No resolvable port still shows a bare [cctrace]. The advisor row anchored on term_width - 5, but under a pipe `tput cols` answers a flat 80 whatever the terminal is — line 1 overflowed the phantom edge while the advice dangled mid-line beneath it. The row now anchors on line 1's ACTUAL rendered width (line1_cols, recorded by format_output), so the second line's right edge meets the first's even when the width guess is wrong. Width detection itself got honest too: the controlling tty (stty size --- statusline.sh | 98 +++++++++++++++++++++++++++++--- t/helpers.bash | 8 ++- t/statusline.bats | 142 ++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 239 insertions(+), 9 deletions(-) diff --git a/statusline.sh b/statusline.sh index 53e1e66..fc3aa3e 100755 --- a/statusline.sh +++ b/statusline.sh @@ -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/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 @@ -3099,6 +3115,58 @@ 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 +# (/instances/*.json, a documented contract) is matched by this +# session's id, then by project path, then by being the only live capture. +# 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:-}" --arg cwd "${current_dir:-}" ' + [ .[] | select(type == "object" and .endedAt == null) ] as $live | + (($live | map(select(.sessionId == $sid and $sid != ""))) + + ($live | map(select(.projectPath == $cwd and $cwd != ""))) + + (if ($live | length) == 1 then $live else [] end)) + | first.port // empty' 2>/dev/null) + 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. @@ -3678,8 +3746,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) @@ -3690,11 +3764,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") @@ -3703,16 +3779,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 @@ -3723,15 +3802,18 @@ 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))}…" @@ -3739,7 +3821,7 @@ if [ "$advisor_display_mode" != "off" ] && [ -n "${usage_data:-}" ]; then 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" diff --git a/t/helpers.bash b/t/helpers.bash index 4e35b7a..f8516f4 100644 --- a/t/helpers.bash +++ b/t/helpers.bash @@ -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) @@ -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")" diff --git a/t/statusline.bats b/t/statusline.bats index 22e9188..ee11204 100644 --- a/t/statusline.bats +++ b/t/statusline.bats @@ -2934,3 +2934,145 @@ _write_ppw_fixture() { # dir ppw [ "$result" = "a 5h[8%] free" ] rm -rf "$tmpdir" } + +# --- cctrace trace chip (left cluster) --- + +@test "build_trace_component: silent without any trace signal" { + result=$(stdin_session_id="s1" current_dir="/t"; build_trace_component) + [ -z "$result" ] +} + +@test "build_trace_component: CCTRACE_SERVER_PORT renders the chip directly" { + result=$( (CCTRACE_SERVER_PORT=9317; build_trace_component) ) + plain=$(strip_ansi "$result") + [ "$plain" = " [cctrace:9317]" ] +} + +@test "build_trace_component: registry match by session id resolves the port" { + tmpdir=$(mktemp -d) + mkdir -p "$tmpdir/instances" + printf '{"id":"r1","pid":1,"port":9319,"project":"p","projectPath":"/elsewhere","logFile":"","mode":"mitm","startedAt":"2026-01-01T00:00:00Z","sessionId":"sid-match"}' \ + > "$tmpdir/instances/r1.json" + result=$( (DEVA_TRACE=1 CCTRACE_DATA_DIR="$tmpdir" stdin_session_id="sid-match" current_dir="/t"; build_trace_component) ) + plain=$(strip_ansi "$result") + [ "$plain" = " [cctrace:9319]" ] + rm -rf "$tmpdir" +} + +@test "build_trace_component: tombstoned runs never lend their port (bare chip)" { + tmpdir=$(mktemp -d) + mkdir -p "$tmpdir/instances" + printf '{"id":"r1","pid":1,"port":9319,"project":"p","projectPath":"/t","logFile":"","mode":"mitm","startedAt":"2026-01-01T00:00:00Z","sessionId":"sid-1","endedAt":"2026-01-01T01:00:00Z"}' \ + > "$tmpdir/instances/r1.json" + result=$( (DEVA_TRACE=1 CCTRACE_DATA_DIR="$tmpdir" stdin_session_id="sid-1" current_dir="/t"; build_trace_component) ) + plain=$(strip_ansi "$result") + [ "$plain" = " [cctrace]" ] + rm -rf "$tmpdir" +} + +@test "build_trace_component: NODE_EXTRA_CA_CERTS marker derives the registry dir" { + base=$(mktemp -d) + tmpdir="$base/cctrace" + mkdir -p "$tmpdir/mitm" "$tmpdir/instances" + touch "$tmpdir/mitm/ca-cert.pem" + printf '{"id":"r1","pid":1,"port":9321,"project":"p","projectPath":"/proj","logFile":"","mode":"mitm","startedAt":"2026-01-01T00:00:00Z","sessionId":"sid-x"}' \ + > "$tmpdir/instances/r1.json" + result=$( (NODE_EXTRA_CA_CERTS="$tmpdir/mitm/ca-cert.pem" stdin_session_id="sid-x" current_dir="/t"; build_trace_component) ) + plain=$(strip_ansi "$result") + [ "$plain" = " [cctrace:9321]" ] + rm -rf "$base" +} + +@test "build_trace_component: project-path fallback when the session id is not registered yet" { + tmpdir=$(mktemp -d) + mkdir -p "$tmpdir/instances" + printf '{"id":"r1","pid":1,"port":9320,"project":"p","projectPath":"/my/proj","logFile":"","mode":"mitm","startedAt":"2026-01-01T00:00:00Z"}' \ + > "$tmpdir/instances/r1.json" + printf '{"id":"r2","pid":2,"port":9325,"project":"q","projectPath":"/other","logFile":"","mode":"mitm","startedAt":"2026-01-01T00:00:00Z"}' \ + > "$tmpdir/instances/r2.json" + result=$( (DEVA_TRACE=1 CCTRACE_DATA_DIR="$tmpdir" stdin_session_id="unseen" current_dir="/my/proj"; build_trace_component) ) + plain=$(strip_ansi "$result") + [ "$plain" = " [cctrace:9320]" ] + rm -rf "$tmpdir" +} + +@test "build_trace_component: two live captures with no match stay honest (bare chip)" { + tmpdir=$(mktemp -d) + mkdir -p "$tmpdir/instances" + printf '{"id":"r1","pid":1,"port":9320,"project":"p","projectPath":"/a","logFile":"","mode":"mitm","startedAt":"2026-01-01T00:00:00Z"}' \ + > "$tmpdir/instances/r1.json" + printf '{"id":"r2","pid":2,"port":9325,"project":"q","projectPath":"/b","logFile":"","mode":"mitm","startedAt":"2026-01-01T00:00:00Z"}' \ + > "$tmpdir/instances/r2.json" + result=$( (DEVA_TRACE=1 CCTRACE_DATA_DIR="$tmpdir" stdin_session_id="unseen" current_dir="/c"; build_trace_component) ) + plain=$(strip_ansi "$result") + [ "$plain" = " [cctrace]" ] + rm -rf "$tmpdir" +} + +@test "build_trace_component: a lone live capture is claimed without a match" { + tmpdir=$(mktemp -d) + mkdir -p "$tmpdir/instances" + printf '{"id":"r1","pid":1,"port":9322,"project":"p","projectPath":"/a","logFile":"","mode":"mitm","startedAt":"2026-01-01T00:00:00Z"}' \ + > "$tmpdir/instances/r1.json" + result=$( (DEVA_TRACE=1 CCTRACE_DATA_DIR="$tmpdir" stdin_session_id="unseen" current_dir="/c"; build_trace_component) ) + plain=$(strip_ansi "$result") + [ "$plain" = " [cctrace:9322]" ] + rm -rf "$tmpdir" +} + +@test "integration: traced session wears the chip on the left, next to path" { + tmpdir=$(mktemp -d) + mkdir -p "$tmpdir/.claude" + out=$(echo '{"session_id":"trace-int","model":{"id":"claude-opus-4-6","display_name":"Opus"},"cwd":"/t/proj","workspace":{"current_dir":"/t/proj"},"version":"2.1.174","cost":{"total_cost_usd":0}}' \ + | HOME="$tmpdir" CCTRACE_SERVER_PORT=9317 bash "$SCRIPT_DIR/statusline.sh" --test) + plain=$(strip_ansi "$out") + [[ "$plain" == "proj [cctrace:9317]"* ]] + rm -rf "$tmpdir" +} + +@test "integration: untraced session has no chip" { + tmpdir=$(mktemp -d) + mkdir -p "$tmpdir/.claude" + out=$(echo '{"session_id":"trace-int2","model":{"id":"claude-opus-4-6","display_name":"Opus"},"cwd":"/t/proj","workspace":{"current_dir":"/t/proj"},"version":"2.1.174","cost":{"total_cost_usd":0}}' \ + | HOME="$tmpdir" bash "$SCRIPT_DIR/statusline.sh" --test) + plain=$(strip_ansi "$out") + [[ "$plain" != *"cctrace"* ]] + rm -rf "$tmpdir" +} + +# --- advisor row alignment (second line meets line 1's right edge) --- + +@test "integration: advisor row right-aligns to line 1's actual edge" { + tmpdir=$(mktemp -d) + mkdir -p "$tmpdir/.claude/statusline" + printf '{"claudeAiOauth":{"accessToken":"tok"}}' > "$tmpdir/.claude/.credentials.json" + # 85% only 2h into the 5h window: pace-hot, the advisor projects the cap. + reset_5h=$(date -u -d '+3 hours' '+%Y-%m-%dT%H:%M:%SZ') + printf '{"five_hour":{"utilization":85,"resets_at":"%s"},"seven_day":{"utilization":10},"fetched_at":%s}' \ + "$reset_5h" "$(date +%s)" > "$tmpdir/.claude/statusline/usage.cache" + out=$(echo '{"session_id":"align-test","model":{"id":"claude-opus-4-6","display_name":"Opus"},"cwd":"/t/proj","workspace":{"current_dir":"/t/proj"},"version":"2.1.174","cost":{"total_cost_usd":0}}' \ + | HOME="$tmpdir" COLUMNS=110 bash "$SCRIPT_DIR/statusline.sh" --test) + line1=$(strip_ansi "$(printf '%s\n' "$out" | sed -n 1p)") + line2=$(strip_ansi "$(printf '%s\n' "$out" | sed -n 2p)") + [ -n "$line2" ] + [ "${#line1}" -eq "${#line2}" ] + rm -rf "$tmpdir" +} + +@test "integration: advisor row still meets the edge when the width guess is low" { + tmpdir=$(mktemp -d) + mkdir -p "$tmpdir/.claude/statusline" + printf '{"claudeAiOauth":{"accessToken":"tok"}}' > "$tmpdir/.claude/.credentials.json" + reset_5h=$(date -u -d '+3 hours' '+%Y-%m-%dT%H:%M:%SZ') + printf '{"five_hour":{"utilization":85,"resets_at":"%s"},"seven_day":{"utilization":10},"fetched_at":%s}' \ + "$reset_5h" "$(date +%s)" > "$tmpdir/.claude/statusline/usage.cache" + # A phantom 40-col terminal: line 1 overflows it (padding clamps to 6); + # the advisor must anchor on line 1's real edge, not the phantom one. + out=$(echo '{"session_id":"align-low","model":{"id":"claude-opus-4-6","display_name":"Opus"},"cwd":"/t/proj","workspace":{"current_dir":"/t/proj"},"version":"2.1.174","cost":{"total_cost_usd":0}}' \ + | HOME="$tmpdir" COLUMNS=40 bash "$SCRIPT_DIR/statusline.sh" --test) + line1=$(strip_ansi "$(printf '%s\n' "$out" | sed -n 1p)") + line2=$(strip_ansi "$(printf '%s\n' "$out" | sed -n 2p)") + [ -n "$line2" ] + [ "${#line1}" -eq "${#line2}" ] + rm -rf "$tmpdir" +} From 06d3df9ed8b853f15e48cc35fecb3f794c603de8 Mon Sep 17 00:00:00 2001 From: Eric Wang Date: Wed, 29 Jul 2026 04:10:13 -0700 Subject: [PATCH 2/3] v0.23.0: trace chip / advisor-edge release surfaces Co-Authored-By: Claude Fable 5 --- CHANGELOG.md | 25 +++++++++++++++++++++++++ README.md | 1 + 2 files changed, 26 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 27ee9a4..612d11c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,30 @@ # 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, then project path, then by being the only live +capture. 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 Date: Wed, 29 Jul 2026 04:16:40 -0700 Subject: [PATCH 3/3] Trace chip: the registry speaks sid8, and fallbacks demand a heartbeat MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Live testing against this repo's own traced dev session caught two registry realities the first cut missed. cctrace REDACTS session ids before they land on disk ("c3a6e0f3-****-…"), so an exact-id match never fires against a real registry — match on the sid8 prefix, the same join key cctrace's own UI uses. And crashed captures leave non-tombstoned "live" entries behind for up to a day, so the project- path and only-live fallbacks now trust heartbeat-fresh files only (<2min against a 30s heartbeat); the sid8 match keeps trusting any non-tombstone entry — if OUR capture died, this session's proxy died with it. Tests 346 -> 348: the sid test now stores a redacted id, plus stale-entry coverage both ways (sid8 claims its own stale entry; fallbacks refuse a stranger's). Co-Authored-By: Claude Fable 5 --- CHANGELOG.md | 10 ++++++---- README.md | 2 +- statusline.sh | 27 +++++++++++++++++++++------ t/statusline.bats | 36 +++++++++++++++++++++++++++++++++--- 4 files changed, 61 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 612d11c..8aa9c71 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,9 +11,11 @@ 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, then project path, then by being the only live -capture. No resolvable port still shows a bare `[cctrace]`: "recorded" -matters even portless. +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 @@ -23,7 +25,7 @@ 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 /instances/*.json, a documented contract) is matched by this # session's id, then by project path, then by being the only live capture. -# No match still shows the bare chip: "recorded" matters even portless. +# 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="" @@ -3149,12 +3157,19 @@ build_trace_component() { fi reg="$reg/instances" if [ -d "$reg" ]; then - port=$(cat "$reg"/*.json 2>/dev/null | jq -rs --arg sid "${stdin_session_id:-}" --arg cwd "${current_dir:-}" ' - [ .[] | select(type == "object" and .endedAt == null) ] as $live | - (($live | map(select(.sessionId == $sid and $sid != ""))) + - ($live | map(select(.projectPath == $cwd and $cwd != ""))) + - (if ($live | length) == 1 then $live else [] end)) + 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 diff --git a/t/statusline.bats b/t/statusline.bats index ee11204..782cfa4 100644 --- a/t/statusline.bats +++ b/t/statusline.bats @@ -2948,17 +2948,47 @@ _write_ppw_fixture() { # dir ppw [ "$plain" = " [cctrace:9317]" ] } -@test "build_trace_component: registry match by session id resolves the port" { +@test "build_trace_component: registry stores the sid REDACTED — sid8 prefix still matches" { + # cctrace's capture-time redaction masks session ids past the first 8 hex + # before anything lands on disk; the sid8 prefix is the join key. tmpdir=$(mktemp -d) mkdir -p "$tmpdir/instances" - printf '{"id":"r1","pid":1,"port":9319,"project":"p","projectPath":"/elsewhere","logFile":"","mode":"mitm","startedAt":"2026-01-01T00:00:00Z","sessionId":"sid-match"}' \ + printf '{"id":"r1","pid":1,"port":9319,"project":"p","projectPath":"/elsewhere","logFile":"","mode":"mitm","startedAt":"2026-01-01T00:00:00Z","sessionId":"c3a6e0f3-****-****-****-************"}' \ > "$tmpdir/instances/r1.json" - result=$( (DEVA_TRACE=1 CCTRACE_DATA_DIR="$tmpdir" stdin_session_id="sid-match" current_dir="/t"; build_trace_component) ) + result=$( (DEVA_TRACE=1 CCTRACE_DATA_DIR="$tmpdir" stdin_session_id="c3a6e0f3-871b-4047-a282-60ca3d2244e6" current_dir="/t"; build_trace_component) ) plain=$(strip_ansi "$result") [ "$plain" = " [cctrace:9319]" ] rm -rf "$tmpdir" } +@test "build_trace_component: sid8 claims its own entry even when the heartbeat looks stale" { + # If OUR capture died the session's proxy died with it — an identity + # match never needs freshness. + tmpdir=$(mktemp -d) + mkdir -p "$tmpdir/instances" + printf '{"id":"r1","pid":1,"port":9319,"project":"p","projectPath":"/elsewhere","logFile":"","mode":"mitm","startedAt":"2026-01-01T00:00:00Z","sessionId":"c3a6e0f3-****-****-****-************"}' \ + > "$tmpdir/instances/r1.json" + touch -d '10 minutes ago' "$tmpdir/instances/r1.json" + result=$( (DEVA_TRACE=1 CCTRACE_DATA_DIR="$tmpdir" stdin_session_id="c3a6e0f3-871b-4047-a282-60ca3d2244e6" current_dir="/t"; build_trace_component) ) + plain=$(strip_ansi "$result") + [ "$plain" = " [cctrace:9319]" ] + rm -rf "$tmpdir" +} + +@test "build_trace_component: a stale live entry never lends its port via fallback" { + # Crashed captures leave non-tombstoned entries behind for up to a day; + # the path/only-live fallbacks trust heartbeat-fresh files only. + tmpdir=$(mktemp -d) + mkdir -p "$tmpdir/instances" + printf '{"id":"r1","pid":1,"port":9319,"project":"p","projectPath":"/my/proj","logFile":"","mode":"mitm","startedAt":"2026-01-01T00:00:00Z"}' \ + > "$tmpdir/instances/r1.json" + touch -d '10 minutes ago' "$tmpdir/instances/r1.json" + result=$( (DEVA_TRACE=1 CCTRACE_DATA_DIR="$tmpdir" stdin_session_id="unseen-sid-123" current_dir="/my/proj"; build_trace_component) ) + plain=$(strip_ansi "$result") + [ "$plain" = " [cctrace]" ] + rm -rf "$tmpdir" +} + @test "build_trace_component: tombstoned runs never lend their port (bare chip)" { tmpdir=$(mktemp -d) mkdir -p "$tmpdir/instances"