Skip to content
Merged
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
1 change: 1 addition & 0 deletions crates/tui/src/commands/core.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ pub fn clear(app: &mut App) -> CommandResult {
app.queued_draft = None;
app.session.total_tokens = 0;
app.session.total_conversation_tokens = 0;
app.session.reset_token_breakdown();
app.session.session_cost = 0.0;
app.session.session_cost_cny = 0.0;
app.session.subagent_cost = 0.0;
Expand Down
2 changes: 2 additions & 0 deletions crates/tui/src/commands/session.rs
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,8 @@ pub fn load(app: &mut App, path: Option<&str>) -> CommandResult {
app.workspace.clone_from(&session.metadata.workspace);
app.session.total_tokens = u32::try_from(session.metadata.total_tokens).unwrap_or(u32::MAX);
app.session.total_conversation_tokens = app.session.total_tokens;
// Accumulated token breakdown is per-runtime-session; zero on load.
app.session.reset_token_breakdown();
app.session.session_cost = 0.0;
app.session.session_cost_cny = 0.0;
app.session.subagent_cost = 0.0;
Expand Down
20 changes: 20 additions & 0 deletions crates/tui/src/commands/status.rs
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,26 @@ fn format_status(app: &App) -> String {
&token_count(app.session.last_completion_tokens),
);
push_row(&mut out, "Cache hit/miss:", &cache_summary(app));
push_row(
&mut out,
"Session input:",
&app.session.total_input_tokens.to_string(),
);
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);
push_row(
&mut out,
"Session output:",
&app.session.total_output_tokens.to_string(),
);
push_row(
&mut out,
"Total tokens:",
Expand Down
7 changes: 7 additions & 0 deletions crates/tui/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -683,6 +683,8 @@ pub enum StatusItem {
LastToolElapsed,
/// Remaining rate-limit budget (placeholder until wired).
RateLimit,
/// Session token usage: input / cache-hit / output.
Tokens,
}

impl StatusItem {
Expand All @@ -701,6 +703,7 @@ impl StatusItem {
StatusItem::Agents,
StatusItem::ReasoningReplay,
StatusItem::Cache,
StatusItem::Tokens,
]
}

Expand All @@ -721,6 +724,7 @@ impl StatusItem {
StatusItem::GitBranch => "git_branch",
StatusItem::LastToolElapsed => "last_tool_elapsed",
StatusItem::RateLimit => "rate_limit",
StatusItem::Tokens => "tokens",
}
}

Expand All @@ -741,6 +745,7 @@ impl StatusItem {
StatusItem::GitBranch => "Git branch",
StatusItem::LastToolElapsed => "Last tool elapsed",
StatusItem::RateLimit => "Rate-limit remaining",
StatusItem::Tokens => "Session tokens",
}
}

Expand All @@ -762,6 +767,7 @@ impl StatusItem {
StatusItem::GitBranch => "current workspace branch",
StatusItem::LastToolElapsed => "ms of the most recent tool call (placeholder)",
StatusItem::RateLimit => "remaining requests in the budget (placeholder)",
StatusItem::Tokens => "input / cache-hit / output token totals",
}
}

Expand All @@ -782,6 +788,7 @@ impl StatusItem {
StatusItem::GitBranch,
StatusItem::LastToolElapsed,
StatusItem::RateLimit,
StatusItem::Tokens,
]
}

Expand Down
3 changes: 3 additions & 0 deletions crates/tui/src/config_ui.rs
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,7 @@ pub enum StatusItemValue {
GitBranch,
LastToolElapsed,
RateLimit,
Tokens,
}

pub fn parse_mode(arg: Option<&str>) -> Result<ConfigUiMode, String> {
Expand Down Expand Up @@ -996,6 +997,7 @@ impl From<StatusItem> for StatusItemValue {
StatusItem::GitBranch => Self::GitBranch,
StatusItem::LastToolElapsed => Self::LastToolElapsed,
StatusItem::RateLimit => Self::RateLimit,
StatusItem::Tokens => Self::Tokens,
}
}
}
Expand All @@ -1016,6 +1018,7 @@ impl From<StatusItemValue> for StatusItem {
StatusItemValue::GitBranch => Self::GitBranch,
StatusItemValue::LastToolElapsed => Self::LastToolElapsed,
StatusItemValue::RateLimit => Self::RateLimit,
StatusItemValue::Tokens => Self::Tokens,
}
}
}
Expand Down
19 changes: 19 additions & 0 deletions crates/tui/src/tui/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1005,6 +1005,11 @@ pub struct SessionState {
pub last_reasoning_replay_tokens: Option<u32>,
pub total_tokens: u32,
pub total_conversation_tokens: u32,
/// Accumulated token breakdown for the session.
pub total_input_tokens: u32,
pub total_cache_hit_tokens: u32,
pub total_cache_miss_tokens: u32,
pub total_output_tokens: u32,
Comment on lines +1009 to +1012
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;
    }
}

pub turn_cache_history: VecDeque<TurnCacheRecord>,
pub last_cache_inspection: Option<PromptInspection>,
}
Expand All @@ -1026,12 +1031,26 @@ impl Default for SessionState {
last_reasoning_replay_tokens: None,
total_tokens: 0,
total_conversation_tokens: 0,
total_input_tokens: 0,
total_cache_hit_tokens: 0,
total_cache_miss_tokens: 0,
total_output_tokens: 0,
turn_cache_history: VecDeque::new(),
last_cache_inspection: None,
}
}
}

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;
}
}

/// Evidence collected during a turn for the post-turn receipt.
#[derive(Debug, Clone)]
pub struct ToolEvidence {
Expand Down
22 changes: 22 additions & 0 deletions crates/tui/src/tui/footer_ui.rs
Original file line number Diff line number Diff line change
Expand Up @@ -518,6 +518,7 @@ pub(crate) fn render_footer_from(
S::ContextPercent => footer_context_percent_spans(app),
S::GitBranch => footer_git_branch_spans(app),
S::LastToolElapsed | S::RateLimit => Vec::new(),
S::Tokens => footer_session_tokens_spans(app),
_ => continue,
};
if chip.is_empty() {
Expand Down Expand Up @@ -593,6 +594,27 @@ pub(crate) fn should_show_footer_cost(displayed_cost: f64) -> bool {
displayed_cost.is_finite() && displayed_cost > 0.0
}

/// Session token-usage chip for the footer right cluster.
///
/// Renders the accumulated input / cache-hit / output token breakdown
/// since the current runtime session started (not persisted across
/// restarts). Returns empty when no tokens have been recorded yet.
pub(crate) fn footer_session_tokens_spans(app: &App) -> Vec<Span<'static>> {
let session = &app.session;
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))]
}

/// Test-only helper retained as a parity reference for `FooterWidget`'s
/// auxiliary-span composition. Production rendering is performed by the
/// widget itself; the existing footer parity tests still exercise this
Expand Down
24 changes: 24 additions & 0 deletions crates/tui/src/tui/ui.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1434,6 +1434,28 @@ async fn run_event_loop(
.session
.total_conversation_tokens
.saturating_add(turn_tokens);
app.session.total_input_tokens = app
.session
.total_input_tokens
.saturating_add(usage.input_tokens);
app.session.total_output_tokens = app
.session
.total_output_tokens
.saturating_add(usage.output_tokens);
// Only accumulate cache telemetry when reported.
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);
}
app.session.last_prompt_tokens = Some(usage.input_tokens);
app.session.last_completion_tokens = Some(usage.output_tokens);
app.session.last_prompt_cache_hit_tokens = usage.prompt_cache_hit_tokens;
Expand Down Expand Up @@ -6606,6 +6628,8 @@ fn apply_loaded_session(app: &mut App, config: &Config, session: &SavedSession)
app.session.last_prompt_cache_hit_tokens = None;
app.session.last_prompt_cache_miss_tokens = None;
app.session.last_reasoning_replay_tokens = None;
// Accumulated token breakdown is per-runtime-session; reset on load.
app.session.reset_token_breakdown();
app.session.turn_cache_history.clear();
Comment on lines +6631 to 6633
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 👍 / 👎.

app.current_session_id = Some(session.metadata.id.clone());
app.session_artifacts = session.artifacts.clone();
Comment thread
greptile-apps[bot] marked this conversation as resolved.
Expand Down