Skip to content

feat: session token breakdown in footer and /status#2152

Merged
Hmbown merged 2 commits into
Hmbown:mainfrom
encyc:feat/session-token-breakdown-v2
May 26, 2026
Merged

feat: session token breakdown in footer and /status#2152
Hmbown merged 2 commits into
Hmbown:mainfrom
encyc:feat/session-token-breakdown-v2

Conversation

@encyc
Copy link
Copy Markdown

@encyc encyc commented May 26, 2026

Summary

Re-submit of #1666, rebased onto current main (v0.8.45) after the CodeWhale rebrand.

Add accumulated session token tracking with input / cache-hit / output breakdown:

  • SessionState: new total_input_tokens, total_cache_hit_tokens, total_cache_miss_tokens, total_output_tokens fields
  • Turn outcome handler: accumulate per-turn token breakdown from usage struct
  • StatusItem::Tokens: new footer chip, enabled by default
  • Footer chip: compact format — 12K in · 8.1K cch · 2.5K out
  • /status command: expanded with session input / cache / output rows
  • /clear and /load: reset accumulated breakdown (per-runtime-session, not persisted)

Verified

  • cargo fmt --check
  • cargo check
  • cargo clippy ✓ (no new warnings)
  • cargo test --workspace --all-features — 3329 passed; 2 pre-existing failures unrelated to this change

Greptile Summary

This PR adds accumulated session token tracking (input / cache-hit / cache-miss / output) to the TUI footer chip and /status command. It is a rebase of #1666 onto the CodeWhale-branded main.

  • SessionState gains four u32 accumulator fields and a reset_token_breakdown helper that is called consistently from /clear, /load, and apply_loaded_session, keeping all token-reset paths in sync.
  • TurnOutcome handler accumulates per-turn input and output tokens unconditionally; cache telemetry is gated on prompt_cache_hit_tokens being Some, which avoids double-counting when the provider doesn't report cache data.
  • Footer chip (StatusItem::Tokens, enabled by default) renders a compact N in · N cch · N out label; the chip is hidden until the first turn completes. The /status command gets three new rows showing the same breakdown in raw counts.

Confidence Score: 5/5

Safe to merge; all accumulation, reset, and display paths are correctly wired and symmetric.

The accumulation logic is straightforward: four counters incremented per turn, reset in all three load/clear paths, displayed in the footer chip and /status. The cache-telemetry guard (if let Some(hit_tokens)) correctly prevents double-counting when the provider omits cache fields. Config enum and UI mappings are complete with no missing arms. The only gaps are missing test assertions for the new /status rows.

The status_report_includes_runtime_fields test in crates/tui/src/commands/status.rs would benefit from asserting the new Session input/cache/output rows.

Important Files Changed

Filename Overview
crates/tui/src/tui/app.rs Adds four u32 accumulator fields and a reset_token_breakdown helper to SessionState; defaults are zero and the Default impl is updated correctly.
crates/tui/src/tui/ui.rs Accumulates per-turn input/output/cache-hit/cache-miss tokens in the TurnOutcome handler; cache telemetry is gated on prompt_cache_hit_tokens being Some, avoiding the double-count bug. Also calls reset_token_breakdown inside apply_loaded_session.
crates/tui/src/tui/footer_ui.rs Adds footer_session_tokens_spans and wires it into the Tokens chip slot; chip is hidden when both input and output are zero, consistent with other optional chips.
crates/tui/src/commands/status.rs Adds three new rows to /status output (Session input/cache/output); existing test does not assert the new rows, so a regression there would go undetected.
crates/tui/src/commands/core.rs Calls reset_token_breakdown in the /clear handler alongside the existing token-counter resets; correctly symmetric with the load and session-select paths.
crates/tui/src/commands/session.rs Calls reset_token_breakdown in the /load handler; comment correctly explains that the breakdown is per-runtime-session, not persisted.
crates/tui/src/config.rs Adds StatusItem::Tokens to the enum and all five required match arms (key, label, description, defaults, all); no arms are missing.
crates/tui/src/config_ui.rs Mirrors StatusItem::Tokens into StatusItemValue with both From impls updated symmetrically.

Sequence Diagram

sequenceDiagram
    participant Engine
    participant ui.rs as run_event_loop
    participant SessionState
    participant FooterUI as footer_ui.rs
    participant StatusCmd as /status

    Engine->>ui.rs: "TurnOutcome { usage }"
    ui.rs->>SessionState: "total_input_tokens += usage.input_tokens"
    ui.rs->>SessionState: "total_output_tokens += usage.output_tokens"
    alt prompt_cache_hit_tokens is Some(hit)
        ui.rs->>SessionState: "total_cache_hit_tokens += hit"
        ui.rs->>SessionState: "total_cache_miss_tokens += miss (or input-hit)"
    end

    Note over ui.rs,SessionState: /clear or /load or apply_loaded_session
    ui.rs->>SessionState: reset_token_breakdown()

    SessionState-->>FooterUI: total_input/cache_hit/output_tokens
    FooterUI-->>FooterUI: render N in · N cch · N out chip

    SessionState-->>StatusCmd: total_input/cache_hit/cache_miss/output_tokens
    StatusCmd-->>StatusCmd: render Session input/cache/output rows
Loading

Fix All in Codex Fix All in Claude Code Fix All in Cursor

Reviews (2): Last reviewed commit: "fix: address review feedback — current_s..." | Re-trigger Greptile

Add accumulated session token tracking with input / cache-hit / output
breakdown. Rebased from PR Hmbown#1666 onto post-rebrand main (v0.8.45).

Changes:
- SessionState: new total_input_tokens, total_cache_hit_tokens,
  total_cache_miss_tokens, total_output_tokens fields
- Turn outcome handler: accumulate per-turn token breakdown
- StatusItem::Tokens: new footer chip, enabled by default
- Footer chip: "12K in · 8.1K cch · 2.5K out" format
- /status: expanded with session input/cache/output rows
- /clear and /load: reset accumulated breakdown
Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces tracking and displaying of accumulated session token usage (input, cache hit, cache miss, and output tokens) across the TUI application. Feedback focuses on refactoring the repeated token-resetting logic into a helper method on SessionState to keep the code DRY. Additionally, improvements are suggested to handle models/providers that do not support cache telemetry, ensuring the UI (footer and status view) does not display misleading zero-values or redundant cache metrics when telemetry is unavailable.

Comment thread crates/tui/src/tui/app.rs
Comment on lines +1009 to +1012
pub total_input_tokens: u32,
pub total_cache_hit_tokens: u32,
pub total_cache_miss_tokens: u32,
pub total_output_tokens: u32,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

To avoid duplicating the resetting of the accumulated token breakdown fields across multiple commands and UI event loops, consider adding a helper method on SessionState.

For example:

impl SessionState {
    /// Reset the accumulated token breakdown fields to zero.
    pub fn reset_token_breakdown(&mut self) {
        self.total_input_tokens = 0;
        self.total_cache_hit_tokens = 0;
        self.total_cache_miss_tokens = 0;
        self.total_output_tokens = 0;
    }
}

Comment thread crates/tui/src/commands/core.rs Outdated
Comment on lines +58 to +61
app.session.total_input_tokens = 0;
app.session.total_cache_hit_tokens = 0;
app.session.total_cache_miss_tokens = 0;
app.session.total_output_tokens = 0;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Use the newly introduced reset_token_breakdown helper method on SessionState to reduce boilerplate and keep the code DRY.

Suggested change
app.session.total_input_tokens = 0;
app.session.total_cache_hit_tokens = 0;
app.session.total_cache_miss_tokens = 0;
app.session.total_output_tokens = 0;
app.session.reset_token_breakdown();

Comment thread crates/tui/src/commands/session.rs Outdated
Comment on lines +178 to +181
app.session.total_input_tokens = 0;
app.session.total_cache_hit_tokens = 0;
app.session.total_cache_miss_tokens = 0;
app.session.total_output_tokens = 0;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Use the reset_token_breakdown helper method on SessionState here as well to simplify the session loading logic.

Suggested change
app.session.total_input_tokens = 0;
app.session.total_cache_hit_tokens = 0;
app.session.total_cache_miss_tokens = 0;
app.session.total_output_tokens = 0;
app.session.reset_token_breakdown();

Comment thread crates/tui/src/tui/ui.rs Outdated
Comment on lines +6631 to +6634
app.session.total_input_tokens = 0;
app.session.total_cache_hit_tokens = 0;
app.session.total_cache_miss_tokens = 0;
app.session.total_output_tokens = 0;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Use the reset_token_breakdown helper method on SessionState here to keep the session loading logic consistent and clean.

    app.session.reset_token_breakdown();

Comment thread crates/tui/src/tui/ui.rs Outdated
Comment on lines +1445 to +1457
app.session.total_cache_hit_tokens = app
.session
.total_cache_hit_tokens
.saturating_add(usage.prompt_cache_hit_tokens.unwrap_or(0));
let cache_miss = usage.prompt_cache_miss_tokens.unwrap_or_else(|| {
usage
.input_tokens
.saturating_sub(usage.prompt_cache_hit_tokens.unwrap_or(0))
});
app.session.total_cache_miss_tokens = app
.session
.total_cache_miss_tokens
.saturating_add(cache_miss);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

If the active model or provider does not support cache telemetry, prompt_cache_hit_tokens will be None. In this case, defaulting to 0 and subtracting it from input_tokens falsely attributes all input tokens as cache misses. This leads to misleading metrics (e.g., showing 100% cache misses for unsupported models).

Only accumulate cache hit and miss tokens if prompt_cache_hit_tokens is actually reported (Some).

                        if let Some(hit_tokens) = usage.prompt_cache_hit_tokens {
                            app.session.total_cache_hit_tokens = app
                                .session
                                .total_cache_hit_tokens
                                .saturating_add(hit_tokens);
                            let cache_miss = usage.prompt_cache_miss_tokens.unwrap_or_else(|| {
                                usage.input_tokens.saturating_sub(hit_tokens)
                            });
                            app.session.total_cache_miss_tokens = app
                                .session
                                .total_cache_miss_tokens
                                .saturating_add(cache_miss);
                        }

Comment thread crates/tui/src/tui/footer_ui.rs Outdated
Comment on lines +604 to +613
if session.total_input_tokens == 0 && session.total_output_tokens == 0 {
return Vec::new();
}
let in_str = format_token_count_compact(u64::from(session.total_input_tokens));
let cache_str = format_token_count_compact(u64::from(session.total_cache_hit_tokens));
let out_str = format_token_count_compact(u64::from(session.total_output_tokens));
vec![Span::styled(
format!("{in_str} in · {cache_str} cch · {out_str} out"),
Style::default().fg(palette::TEXT_MUTED),
)]
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

If the active model does not support cache telemetry, total_cache_hit_tokens and total_cache_miss_tokens will both be 0. Displaying 0 cch in the footer is redundant and clutters the UI for unsupported models. Consider omitting the cch chip entirely when no cache telemetry has been recorded.

    if session.total_input_tokens == 0 && session.total_output_tokens == 0 {
        return Vec::new();
    }
    let in_str = format_token_count_compact(u64::from(session.total_input_tokens));
    let out_str = format_token_count_compact(u64::from(session.total_output_tokens));
    let text = if session.total_cache_hit_tokens == 0 && session.total_cache_miss_tokens == 0 {
        format!("{in_str} in · {out_str} out")
    } else {
        let cache_str = format_token_count_compact(u64::from(session.total_cache_hit_tokens));
        format!("{in_str} in · {cache_str} cch · {out_str} out")
    };
    vec![Span::styled(
        text,
        Style::default().fg(palette::TEXT_MUTED),
    )]

Comment thread crates/tui/src/commands/status.rs Outdated
Comment on lines +72 to +79
push_row(
&mut out,
"Session cache:",
&format!(
"{} hit / {} miss",
app.session.total_cache_hit_tokens, app.session.total_cache_miss_tokens
),
);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

For models/providers that do not support cache telemetry, showing 0 hit / 0 miss is less clear than explicitly stating not reported (similar to how cache_summary handles it). Consider showing not reported when both hit and miss counts are zero.

Suggested change
push_row(
&mut out,
"Session cache:",
&format!(
"{} hit / {} miss",
app.session.total_cache_hit_tokens, app.session.total_cache_miss_tokens
),
);
let session_cache = if app.session.total_cache_hit_tokens == 0 && app.session.total_cache_miss_tokens == 0 {
"not reported".to_string()
} else {
format!(
"{} hit / {} miss",
app.session.total_cache_hit_tokens, app.session.total_cache_miss_tokens
)
};
push_row(
&mut out,
"Session cache:",
&session_cache,
);

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 6cde1c74f3

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread crates/tui/src/tui/ui.rs
Comment on lines +6630 to 6635
// Accumulated token breakdown is per-runtime-session; reset on load.
app.session.total_input_tokens = 0;
app.session.total_cache_hit_tokens = 0;
app.session.total_cache_miss_tokens = 0;
app.session.total_output_tokens = 0;
app.session.turn_cache_history.clear();
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Restore current session ID when applying a loaded session

apply_loaded_session no longer assigns app.current_session_id, so after resuming/loading through this path the app keeps None or a stale previous ID. That breaks session scoping: startup queue restore compares against current_session_id, and later saves/syncs use this field to decide which session to update, so loading session B after working in session A can cause updates to be written to A (or a new ID) instead of B.

Useful? React with 👍 / 👎.

Comment thread crates/tui/src/tui/ui.rs
Comment thread crates/tui/src/tui/ui.rs Outdated
…Y helper

- Restore app.current_session_id assignment accidentally dropped in
  apply_loaded_session during rebase (P1: breaks startup-resume and
  session-sync paths)
- Guard cache-hit/miss accumulation behind is_some() so providers
  that omit cache telemetry don't inflate miss totals
- Extract SessionState::reset_token_breakdown() to avoid duplicating
  the four-field reset in core/session/ui call sites
- Hide the "cch" segment from the footer token chip when no cache
  data has been recorded
- Show "not reported" in /status session-cache row instead of
  "0 hit / 0 miss" when no cache telemetry is available
@Hmbown Hmbown merged commit 0611b86 into Hmbown:main May 26, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants