fix(tui): move tolerance to module-level const, add total helpers

Promote COST_EQ_TOLERANCE from a function-local const to a module-level
constant in sidebar.rs.

Add SessionCostSnapshot::total_usd() and total_cny() helpers that
encapsulate session+subagent cost summation, used during session restore.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

# Conflicts:
#	crates/tui/src/session_manager.rs
#	crates/tui/src/tui/sidebar.rs
#	crates/tui/src/tui/ui.rs
This commit is contained in:
lbcheng
2026-05-08 20:24:11 +08:00
committed by Hunter Bown
parent a617e3af18
commit 9f2b8a5f14
3 changed files with 67 additions and 3 deletions
+46
View File
@@ -122,6 +122,52 @@ pub struct SessionMetadata {
/// Optional mode label (agent/plan/etc.)
#[serde(default)]
pub mode: Option<String>,
/// Accumulated cost data for persisted billing and high-water mark.
#[serde(default)]
pub cost: SessionCostSnapshot,
}
/// Cost and high-water-mark fields persisted with each session.
#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize)]
pub struct SessionCostSnapshot {
/// Accumulated parent-turn session cost in USD.
#[serde(default)]
pub session_cost_usd: f64,
/// Accumulated parent-turn session cost in CNY.
#[serde(default)]
pub session_cost_cny: f64,
/// Accumulated sub-agent/background LLM cost in USD.
#[serde(default)]
pub subagent_cost_usd: f64,
/// Accumulated sub-agent/background LLM cost in CNY.
#[serde(default)]
pub subagent_cost_cny: f64,
/// Max-ever displayed session+subagent cost in USD (preserves #244
/// monotonic guarantee across session restarts).
#[serde(default)]
pub displayed_cost_high_water_usd: f64,
/// Max-ever displayed session+subagent cost in CNY.
#[serde(default)]
pub displayed_cost_high_water_cny: f64,
}
impl SessionCostSnapshot {
/// Session + subagent cost in USD.
pub fn total_usd(&self) -> f64 {
self.session_cost_usd + self.subagent_cost_usd
}
/// Session + subagent cost in CNY.
pub fn total_cny(&self) -> f64 {
self.session_cost_cny + self.subagent_cost_cny
}
}
impl SessionMetadata {
/// Copy cost fields from another metadata (used when forking a session).
pub fn copy_cost_from(&mut self, other: &SessionMetadata) {
self.cost = other.cost;
}
}
/// A saved session containing full conversation history
+10 -1
View File
@@ -26,6 +26,11 @@ use super::history::{HistoryCell, ToolCell, ToolStatus};
use super::subagent_routing::active_fanout_counts;
use super::ui::truncate_line_to_width;
/// Tolerance for floating-point cost comparison in the sidebar breakdown.
/// Must be large enough that accumulated f64 error across hundreds of turns
/// does not prematurely hide the session+agents breakdown.
const COST_EQ_TOLERANCE: f64 = 1e-6;
pub fn render_sidebar(f: &mut Frame, area: Rect, app: &App) {
if area.width < 24 || area.height < 8 {
// Paint a styled block over the area so stale cells from a previous
@@ -674,7 +679,11 @@ fn render_context_panel(f: &mut Frame, area: Rect, app: &App) {
let total_cost = app.displayed_session_cost_for_currency(app.cost_currency);
let session_cost = app.session_cost_for_currency(app.cost_currency);
let agent_cost = app.subagent_cost_for_currency(app.cost_currency);
lines.push(Line::from(Span::styled(
let real_total = session_cost + agent_cost;
// Only show the additive breakdown when it matches the displayed
// total; when the high-water mark is in effect (post-reconciliation),
// the breakdown would not sum to the displayed value (#244).
let cost_line = if (displayed_total - real_total).abs() < COST_EQ_TOLERANCE {
format!(
"cost: {} (session {} + agents {})",
app.format_cost_amount(total_cost),
+11 -2
View File
@@ -6256,8 +6256,17 @@ fn apply_loaded_session(app: &mut App, session: &SavedSession) -> bool {
app.session.subagent_cost = 0.0;
app.session.subagent_cost_cny = 0.0;
app.session.subagent_cost_event_seqs.clear();
app.session.displayed_cost_high_water = 0.0;
app.session.displayed_cost_high_water_cny = 0.0;
// Restore the high-water marks from persisted metadata so the
// monotonic cost guarantee (#244) survives session restarts.
// Take the max with the current totals — old sessions without
// persisted high-water fields deserialise to 0.0 and fall back to
// the restored total with no regression.
let total_restored_usd = session.metadata.cost.total_usd();
let total_restored_cny = session.metadata.cost.total_cny();
app.session.displayed_cost_high_water =
session.metadata.cost.displayed_cost_high_water_usd.max(total_restored_usd);
app.session.displayed_cost_high_water_cny =
session.metadata.cost.displayed_cost_high_water_cny.max(total_restored_cny);
app.session.last_prompt_tokens = None;
app.session.last_completion_tokens = None;
app.session.last_prompt_cache_hit_tokens = None;