diff --git a/crates/tui/src/commands/debug.rs b/crates/tui/src/commands/debug.rs index 373c927f..b096b29f 100644 --- a/crates/tui/src/commands/debug.rs +++ b/crates/tui/src/commands/debug.rs @@ -74,7 +74,9 @@ pub fn tokens(app: &mut App) -> CommandResult { .replace("{total}", &app.session.total_tokens.to_string()) .replace( "{cost}", - &app.format_cost_amount_precise(app.session_cost_for_currency(app.cost_currency)), + &app.format_cost_amount_precise( + app.displayed_session_cost_for_currency(app.cost_currency), + ), ) .replace("{api_messages}", &message_count.to_string()) .replace("{chat_messages}", &chat_count.to_string()) @@ -84,9 +86,10 @@ pub fn tokens(app: &mut App) -> CommandResult { /// Show session cost breakdown pub fn cost(app: &mut App) -> CommandResult { + let total = app.displayed_session_cost_for_currency(app.cost_currency); let report = tr(app.ui_locale, MessageId::CmdCostReport).replace( "{cost}", - &app.format_cost_amount_precise(app.session_cost_for_currency(app.cost_currency)), + &app.format_cost_amount_precise(total), ); CommandResult::message(report) } diff --git a/crates/tui/src/commands/rename.rs b/crates/tui/src/commands/rename.rs index 705ec02f..e551cf61 100644 --- a/crates/tui/src/commands/rename.rs +++ b/crates/tui/src/commands/rename.rs @@ -58,6 +58,7 @@ fn rename_with_manager( u64::from(app.session.total_tokens), app.system_prompt.as_ref(), ); + app.sync_cost_to_metadata(&mut session.metadata); session.metadata.title = new_title.to_string(); match manager.save_session(&session) { diff --git a/crates/tui/src/commands/session.rs b/crates/tui/src/commands/session.rs index d6c9db6c..6f3a4257 100644 --- a/crates/tui/src/commands/session.rs +++ b/crates/tui/src/commands/session.rs @@ -28,6 +28,7 @@ pub fn save(app: &mut App, path: Option<&str>) -> CommandResult { app.system_prompt.as_ref(), Some(app.mode.label()), ); + app.sync_cost_to_metadata(&mut session.metadata); session.artifacts = app.session_artifacts.clone(); let sessions_dir = save_path diff --git a/crates/tui/src/main.rs b/crates/tui/src/main.rs index 54373758..5a84a625 100644 --- a/crates/tui/src/main.rs +++ b/crates/tui/src/main.rs @@ -3060,13 +3060,14 @@ fn fork_session(session_id: Option, last: bool, workspace: &Path) -> Res .system_prompt .as_ref() .map(|text| SystemPrompt::Text(text.clone())); - let forked = create_saved_session( + let mut forked = create_saved_session( &saved.messages, &saved.metadata.model, &saved.metadata.workspace, saved.metadata.total_tokens, system_prompt.as_ref(), ); + forked.metadata.copy_cost_from(&saved.metadata); manager.save_session(&forked)?; let source_title = saved.metadata.title.trim(); diff --git a/crates/tui/src/tui/app.rs b/crates/tui/src/tui/app.rs index 4a74c6f7..d0bf153c 100644 --- a/crates/tui/src/tui/app.rs +++ b/crates/tui/src/tui/app.rs @@ -1784,6 +1784,18 @@ impl App { self.refresh_displayed_cost_high_water(); } + /// Copy current session/subagent cost accumulators into session metadata + /// for persistence. + pub fn sync_cost_to_metadata(&self, metadata: &mut crate::session_manager::SessionMetadata) { + metadata.cost.session_cost_usd = self.session.session_cost; + metadata.cost.session_cost_cny = self.session.session_cost_cny; + metadata.cost.subagent_cost_usd = self.session.subagent_cost; + metadata.cost.subagent_cost_cny = self.session.subagent_cost_cny; + metadata.cost.displayed_cost_high_water_usd = self.session.displayed_cost_high_water; + metadata.cost.displayed_cost_high_water_cny = + self.session.displayed_cost_high_water_cny; + } + /// Recompute the displayed cost high-water mark. Called any time a cost /// counter is mutated; never decreases. pub fn refresh_displayed_cost_high_water(&mut self) { diff --git a/crates/tui/src/tui/session_picker.rs b/crates/tui/src/tui/session_picker.rs index 9fd520b1..77b86964 100644 --- a/crates/tui/src/tui/session_picker.rs +++ b/crates/tui/src/tui/session_picker.rs @@ -17,8 +17,8 @@ use unicode_width::{UnicodeWidthChar, UnicodeWidthStr}; use crate::palette; use crate::session_manager::{ - SavedSession, SessionManager, SessionMetadata, extract_title, extract_user_prompt, - strip_thinking_tags, + SavedSession, SessionCostSnapshot, SessionManager, SessionMetadata, extract_title, + extract_user_prompt, strip_thinking_tags, }; use crate::tui::views::{ModalKind, ModalView, ViewAction, ViewEvent}; @@ -801,7 +801,7 @@ mod tests { model: "deepseek-v4-pro".to_string(), workspace: std::path::PathBuf::from("/tmp"), mode: Some("agent".to_string()), - cost: crate::session_manager::SessionCostSnapshot::default(), + cost: SessionCostSnapshot::default(), } } diff --git a/crates/tui/src/tui/sidebar.rs b/crates/tui/src/tui/sidebar.rs index f2630f91..35b7b68d 100644 --- a/crates/tui/src/tui/sidebar.rs +++ b/crates/tui/src/tui/sidebar.rs @@ -1339,22 +1339,22 @@ fn render_context_panel(f: &mut Frame, area: Rect, app: &App) { ))); // ── Session cost ───────────────────────────────────────────── - let total_cost = app.displayed_session_cost_for_currency(app.cost_currency); + let displayed_total = 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); 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 (total_cost - real_total).abs() < COST_EQ_TOLERANCE { + let cost_line = if (displayed_total - real_total).abs() < COST_EQ_TOLERANCE { format!( "cost: {} (session {} + agents {})", - app.format_cost_amount(total_cost), + app.format_cost_amount(displayed_total), app.format_cost_amount(session_cost), app.format_cost_amount(agent_cost) ) } else { - format!("cost: {}", app.format_cost_amount(total_cost),) + format!("cost: {}", app.format_cost_amount(displayed_total)) }; lines.push(Line::from(Span::styled( cost_line, diff --git a/crates/tui/src/tui/ui.rs b/crates/tui/src/tui/ui.rs index 055ed03c..366c65f6 100644 --- a/crates/tui/src/tui/ui.rs +++ b/crates/tui/src/tui/ui.rs @@ -3481,6 +3481,7 @@ fn build_session_snapshot(app: &App, manager: &SessionManager) -> SavedSession { app.system_prompt.as_ref(), ); updated.metadata.mode = Some(app.mode.as_setting().to_string()); + app.sync_cost_to_metadata(&mut updated.metadata); updated.context_references = app.session_context_references.clone(); updated.artifacts = app.session_artifacts.clone(); updated @@ -3505,6 +3506,7 @@ fn build_session_snapshot(app: &App, manager: &SessionManager) -> SavedSession { Some(app.mode.as_setting()), ) }; + app.sync_cost_to_metadata(&mut session.metadata); session.context_references = app.session_context_references.clone(); session.artifacts = app.session_artifacts.clone(); session @@ -6874,10 +6876,10 @@ fn apply_loaded_session(app: &mut App, session: &SavedSession) -> bool { 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; - app.session.session_cost = 0.0; - app.session.session_cost_cny = 0.0; - app.session.subagent_cost = 0.0; - app.session.subagent_cost_cny = 0.0; + app.session.session_cost = session.metadata.cost.session_cost_usd; + app.session.session_cost_cny = session.metadata.cost.session_cost_cny; + app.session.subagent_cost = session.metadata.cost.subagent_cost_usd; + app.session.subagent_cost_cny = session.metadata.cost.subagent_cost_cny; app.session.subagent_cost_event_seqs.clear(); // Restore the high-water marks from persisted metadata so the // monotonic cost guarantee (#244) survives session restarts.