diff --git a/app/src/ai/agent/conversation.rs b/app/src/ai/agent/conversation.rs index b1bcb11f..84774455 100644 --- a/app/src/ai/agent/conversation.rs +++ b/app/src/ai/agent/conversation.rs @@ -470,6 +470,28 @@ impl AIConversation { let task_store = TaskStore::from_tasks(tasks_by_id, root_task_id); + let restored_token_usage = { + let mut map = HashMap::new(); + let cache_read = conversation_usage_metadata.total_cache_read_tokens; + let cache_write = conversation_usage_metadata.total_cache_write_tokens; + let cache_miss = conversation_usage_metadata.total_cache_miss_tokens; + let cost = conversation_usage_metadata.total_cost_cents; + if cache_read > 0 || cache_write > 0 || cache_miss > 0 || cost > 0.0 { + map.insert( + "restored".to_string(), + TokenUsage { + model_id: "restored".to_string(), + total_input: cache_miss, + output: 0, + input_cache_read: cache_read, + input_cache_write: cache_write, + cost_in_cents: cost, + }, + ); + } + map + }; + Ok(Self { id, is_viewing_shared_session: false, @@ -493,7 +515,7 @@ impl AIConversation { reverted_action_ids, dismissed_suggestion_ids: Default::default(), total_request_cost: RequestCost::new(0.), - total_token_usage_by_model: Default::default(), + total_token_usage_by_model: restored_token_usage, last_block_token_usage_by_model: Default::default(), optimistic_cli_subagent_subtask_id: None, fallback_display_title: None, @@ -1735,6 +1757,12 @@ impl AIConversation { last_block_entry.cost_in_cents += usage.cost_in_cents; } + // Sync accumulated cache/cost totals into persisted metadata + self.conversation_usage_metadata.total_cache_read_tokens = self.total_cache_read_tokens(); + self.conversation_usage_metadata.total_cache_write_tokens = self.total_cache_write_tokens(); + self.conversation_usage_metadata.total_cache_miss_tokens = self.cache_miss_tokens(); + self.conversation_usage_metadata.total_cost_cents = self.total_cost_cents(); + if let Some(request_cost) = request_cost { let credits_spent_for_last_block = self .conversation_usage_metadata diff --git a/app/src/ai/blocklist/block.rs b/app/src/ai/blocklist/block.rs index 2c91871b..271aa5ea 100644 --- a/app/src/ai/blocklist/block.rs +++ b/app/src/ai/blocklist/block.rs @@ -391,8 +391,6 @@ pub(super) struct AIBlockStateHandles { /// Mouse state handle for the fork conversation button fork_conversation_handle: MouseStateHandle, - /// Mouse state handle for the usage button - usage_button_handle: MouseStateHandle, /// Mouse state handles per citation. /// A given citation should only appear once per block. @@ -926,9 +924,6 @@ pub struct AIBlock { /// When set, CopyCommand will copy this specific command instead of all commands. last_right_clicked_command: Option, - /// Whether the usage summary footer is expanded. - is_usage_footer_expanded: bool, - /// Controller for reading/modifying `AgentView` state for this terminal pane (e.g. if there is /// an active agent view or not, which affects whether or not this block should be hidden). /// @@ -1354,7 +1349,6 @@ impl AIBlock { rewind_button, view_screenshot_buttons: Default::default(), last_right_clicked_command: None, - is_usage_footer_expanded: false, agent_view_controller, aws_bedrock_credentials_error_view: None, imported_comments: Default::default(), @@ -5591,12 +5585,6 @@ pub enum AIBlockEvent { /// commands and requested actions have been executed or cancelled). Finished, - /// Emitted when we want to show or hide the usage footer. - UsageFooterToggled { - conversation_id: AIConversationId, - is_expanded: bool, - }, - /// Emitted when the AI block requires user confirmation to execute. ActionBlockedOnUserConfirmation, @@ -5826,8 +5814,6 @@ pub enum AIBlockAction { CopyDebugId(String), /// Open Warp feedback documentation OpenFeedbackDocs, - /// Toggle the usage summary footer expansion state - ToggleIsUsageFooterExpanded, CommentExpanded { id: CommentId, }, @@ -6027,13 +6013,6 @@ impl TypedActionView for AIBlock { AIBlockAction::ToggleReferencesSection => { self.is_references_section_open = !self.is_references_section_open; } - AIBlockAction::ToggleIsUsageFooterExpanded => { - self.is_usage_footer_expanded = !self.is_usage_footer_expanded; - ctx.emit(AIBlockEvent::UsageFooterToggled { - conversation_id: self.client_ids.conversation_id, - is_expanded: self.is_usage_footer_expanded, - }); - } AIBlockAction::CommentExpanded { id } => { let Some(comment) = self.comment_states.get_mut(id) else { return; diff --git a/app/src/ai/blocklist/block/view_impl.rs b/app/src/ai/blocklist/block/view_impl.rs index 863e808d..483ab41d 100644 --- a/app/src/ai/blocklist/block/view_impl.rs +++ b/app/src/ai/blocklist/block/view_impl.rs @@ -1088,7 +1088,6 @@ impl View for AIBlock { has_accepted_edits, current_todo_list: self.current_todo_list(app), finish_reason: self.finish_reason.as_ref(), - is_usage_footer_expanded: self.is_usage_footer_expanded, shared_session_status: &shared_session_status, terminal_view_id: self.terminal_view_id, is_conversation_transcript_viewer, diff --git a/app/src/ai/blocklist/block/view_impl/output.rs b/app/src/ai/blocklist/block/view_impl/output.rs index 02a7f8d4..c3cc81f3 100644 --- a/app/src/ai/blocklist/block/view_impl/output.rs +++ b/app/src/ai/blocklist/block/view_impl/output.rs @@ -41,12 +41,9 @@ use galaxy_core::ui::theme::color::internal_colors; #[allow(unused_imports)] use galaxy_util::path::{common_path, CleanPathResult}; use galaxyui::elements::new_scrollable::SingleAxisConfig; -use galaxyui::elements::{ - ChildAnchor, NewScrollable, OffsetPositioning, ParentAnchor, ParentOffsetBounds, Stack, -}; +use galaxyui::elements::NewScrollable; use galaxyui::EntityId; use pathfinder_color::ColorU; -use pathfinder_geometry::vector::vec2f; use ui_components::{button, Component as _, Options as _}; use crate::ai::blocklist::block::{ @@ -130,7 +127,7 @@ use super::{ use galaxyui::{ elements::{ Align, Border, ChildView, ConstrainedBox, Container, CornerRadius, CrossAxisAlignment, - Empty, Expanded, Fill, Flex, FormattedTextElement, Hoverable, MainAxisAlignment, + Expanded, Fill, Flex, FormattedTextElement, Hoverable, MainAxisAlignment, MainAxisSize, ParentElement, Radius, Shrinkable, Text, Wrap, }, keymap::Keystroke, @@ -186,7 +183,6 @@ pub(crate) struct Props<'a> { pub(super) current_todo_list: Option<&'a AIAgentTodoList>, pub(super) has_accepted_edits: bool, pub(super) finish_reason: Option<&'a FinishReason>, - pub(super) is_usage_footer_expanded: bool, pub(super) shared_session_status: &'a SharedSessionStatus, pub(super) terminal_view_id: EntityId, pub(super) is_conversation_transcript_viewer: bool, @@ -3182,7 +3178,6 @@ fn render_response_footer(props: Props, app: &AppContext) -> Option Option Box { - let Some(conversation) = props.model.conversation(app) else { - return Empty::new().finish(); - }; - - let has_any_usage = conversation.total_tokens() > 0 - || conversation.total_cost_cents() > 0.0; - if !has_any_usage { - return Empty::new().finish(); - } - - let appearance = Appearance::as_ref(app); - let ui_builder = appearance.ui_builder().clone(); - - let expansion_icon = if props.is_usage_footer_expanded { - Icon::ChevronDown - } else { - Icon::ChevronRight - }; - - let context_usage = conversation.context_window_usage(); - let current_context = conversation.current_context_tokens(); - let cache_read = conversation.total_cache_read_tokens(); - let cache_write = conversation.total_cache_write_tokens(); - let total_input = conversation.total_input_tokens(); - let cost_cents = conversation.total_cost_cents(); - - let max_context: u32 = if context_usage > 0.0 { - (current_context as f32 / context_usage).round() as u32 - } else { - 200_000 - }; - let context_pct = context_usage * 100.0; - let cache_total_ops = cache_read + cache_write + total_input; - let cache_hit_pct = if cache_total_ops > 0 { - (cache_read as f64 / cache_total_ops as f64) * 100.0 - } else { - 0.0 - }; - - let usage_text = format!( - "Context: {:.1}% ({} / {}) | Cache Hit: {:.1}% (R: {}, W: {}, M: {}) | Cost: ${:.2}", - context_pct, - format_token_count(current_context), - format_token_count(max_context), - cache_hit_pct, - format_token_count(cache_read), - format_token_count(cache_write), - format_token_count(total_input), - cost_cents / 100.0, - ); - - let icon_size = icon_size(app); - let button_row = Flex::row() - .with_cross_axis_alignment(CrossAxisAlignment::Center) - .with_main_axis_size(MainAxisSize::Min) - .with_child( - Container::new( - Text::new_inline( - usage_text, - appearance.ui_font_family(), - appearance.monospace_font_size(), - ) - .with_color( - appearance - .theme() - .sub_text_color(appearance.theme().background()) - .into(), - ) - .with_selectable(false) - .finish(), - ) - .with_padding_top(2.) - .with_margin_left(4.) - .finish(), - ) - .with_child( - Container::new( - ConstrainedBox::new( - expansion_icon - .to_galaxyui_icon( - appearance - .theme() - .sub_text_color(appearance.theme().background()), - ) - .finish(), - ) - .with_width(icon_size) - .with_height(icon_size) - .finish(), - ) - .with_margin_top(1.) - .finish(), - ); - - Hoverable::new( - props.state_handles.usage_button_handle.clone(), - |mouse_state| { - let mut content = Container::new(button_row.finish()); - - if mouse_state.is_hovered() || mouse_state.is_clicked() { - let background = if mouse_state.is_clicked() { - appearance.theme().background() - } else { - blended_colors::neutral_4(appearance.theme()).into() - }; - - content = content - .with_background(background) - .with_corner_radius(CornerRadius::with_all(Radius::Pixels(4.))); - - let mut stack = Stack::new().with_child(content.finish()); - let tooltip = ui_builder - .tool_tip("Show usage details".to_string()) - .build() - .finish(); - stack.add_positioned_overlay_child( - tooltip, - OffsetPositioning::offset_from_parent( - vec2f(0., 8.), - ParentOffsetBounds::WindowByPosition, - ParentAnchor::BottomMiddle, - ChildAnchor::TopMiddle, - ), - ); - - stack.finish() - } else { - content.finish() - } - }, - ) - .on_click(|ctx, _, _| { - ctx.dispatch_typed_action(AIBlockAction::ToggleIsUsageFooterExpanded); - }) - .with_cursor(Cursor::PointingHand) - .finish() -} - -fn format_token_count(tokens: u32) -> String { - if tokens >= 1_000_000 { - format!("{:.1}M", tokens as f64 / 1_000_000.0) - } else if tokens >= 1_000 { - format!("{:.1}k", tokens as f64 / 1_000.0) - } else { - format!("{tokens}") - } -} pub fn action_icon( action_id: &AIAgentActionId, diff --git a/app/src/server/server_api/ai.rs b/app/src/server/server_api/ai.rs index 1346a9e9..72f26299 100644 --- a/app/src/server/server_api/ai.rs +++ b/app/src/server/server_api/ai.rs @@ -2292,6 +2292,10 @@ fn convert_usage_metadata( credits_spent_for_last_block: None, token_usage: vec![], tool_usage_metadata: Default::default(), + total_cache_read_tokens: 0, + total_cache_write_tokens: 0, + total_cache_miss_tokens: 0, + total_cost_cents: 0.0, } } diff --git a/app/src/terminal/block_list_element.rs b/app/src/terminal/block_list_element.rs index ed76c32a..c3938788 100644 --- a/app/src/terminal/block_list_element.rs +++ b/app/src/terminal/block_list_element.rs @@ -6,7 +6,7 @@ use crate::drive::settings::WarpDriveSettings; use crate::features::FeatureFlag; use crate::pane_group::SplitPaneState; use crate::settings::{ - AISettings, DebugSettings, EnforceMinimumContrast, PrivacySettings, TerminalSpacing, + AISettings, EnforceMinimumContrast, PrivacySettings, TerminalSpacing, }; use crate::terminal::alt_screen::{should_intercept_mouse, should_intercept_scroll}; use crate::terminal::block_list_viewport::AutoscrollBehavior; @@ -23,7 +23,6 @@ use crate::terminal::{grid_renderer, SizeInfo}; use crate::themes::theme::{Fill, GalaxyTheme}; use crate::ui_components::{self, icons as UIIcon}; use crate::util::color::Opacity; -use crate::BlocklistAIHistoryModel; use enum_iterator::Sequence; use galaxy_core::semantic_selection::SemanticSelection; use galaxy_core::ui::builder::UiBuilder; @@ -3547,38 +3546,6 @@ impl Element for BlockListElement { // references to the terminal model drop(viewport_iter); - if DebugSettings::as_ref(app).should_show_memory_stats() { - let history_model = BlocklistAIHistoryModel::as_ref(app); - for block_index in &visible_block_indices { - if let Some(block) = model.block_list().block_at(*block_index) { - if !block.has_footer() { - continue; - } - let request_tokens = block - .agent_view_visibility() - .agent_view_conversation_id() - .and_then(|conversation_id| history_model.conversation(&conversation_id)) - .map_or(0, |conversation| conversation.last_block_total_tokens()); - let text = if request_tokens > 0 { - format!("Request tokens: {request_tokens}") - } else { - "Request tokens: --".to_string() - }; - - let mut element = Text::new_inline(text, self.ui_font_family, self.font_size) - .with_style(Properties::default().weight(self.font_weight)) - .with_color( - self.warp_theme - .sub_text_color(self.warp_theme.background()) - .into(), - ) - .finish(); - - element.layout(constraint, ctx, app); - self.block_footer_elements.insert(*block_index, element); - } - } - } // Explicitly drop the terminal model mutex guard so that it can be freed up for other // threads diff --git a/app/src/terminal/input/agent.rs b/app/src/terminal/input/agent.rs index beea38f1..b7748fdc 100644 --- a/app/src/terminal/input/agent.rs +++ b/app/src/terminal/input/agent.rs @@ -30,11 +30,12 @@ use galaxyui::{ Align, AnchorPair, Border, ConstrainedBox, Container, CornerRadius, CrossAxisAlignment, DispatchEventResult, DropTarget, Element, EventHandler, Flex, Hoverable, MainAxisSize, OffsetPositioning, OffsetType, ParentElement, PositionedElementOffsetBounds, - PositioningAxis, Radius, SavePosition, Stack, XAxisAnchor, YAxisAnchor, + PositioningAxis, Radius, SavePosition, Stack, Text, XAxisAnchor, YAxisAnchor, }, presenter::ChildView, AppContext, SingletonEntity as _, }; +use pathfinder_color::ColorU; pub(super) const CLOUD_MODE_V2_MAX_WIDTH: f32 = 720.; @@ -156,6 +157,17 @@ impl Input { .finish(), ); + if let Some(conv_id) = self + .agent_view_controller + .as_ref(app) + .agent_view_state() + .active_conversation_id() + { + if let Some(status_bar) = render_session_status_bar(appearance, app, conv_id) { + column.add_child(status_bar); + } + } + stack.add_child(wrap_input_with_terminal_padding_and_focus_handler( self.is_active_session(app), column.finish(), @@ -598,6 +610,137 @@ impl Input { } } +fn format_token_count(tokens: u32) -> String { + if tokens >= 1_000_000 { + format!("{:.1}M", tokens as f64 / 1_000_000.0) + } else if tokens >= 1_000 { + format!("{:.1}k", tokens as f64 / 1_000.0) + } else { + format!("{tokens}") + } +} + +fn cache_hit_color(pct: f64, theme: &galaxy_core::ui::theme::GalaxyTheme) -> ColorU { + if pct >= 90.0 { + theme.ansi_fg_green() + } else if pct >= 50.0 { + theme.ansi_fg_yellow() + } else { + theme.ansi_fg_red() + } +} + +fn render_session_status_bar(appearance: &Appearance, app: &AppContext, conversation_id: crate::ai::agent::conversation::AIConversationId) -> Option> { + let (cache_read, cache_write, cache_miss, cost_cents, context_usage, current_context) = + if let Some(conversation) = BlocklistAIHistoryModel::as_ref(app).conversation(&conversation_id) { + ( + conversation.total_cache_read_tokens(), + conversation.total_cache_write_tokens(), + conversation.cache_miss_tokens(), + conversation.total_cost_cents(), + conversation.context_window_usage(), + conversation.current_context_tokens(), + ) + } else { + (0, 0, 0, 0.0, 0.0, 0) + }; + + let cache_total_ops = cache_read + cache_write + cache_miss; + let cache_hit_pct = if cache_total_ops > 0 { + (cache_read as f64 / cache_total_ops as f64) * 100.0 + } else { + 0.0 + }; + + let max_context: u32 = if context_usage > 0.0 { + (current_context as f32 / context_usage).round() as u32 + } else { + 200_000 + }; + let context_pct = context_usage * 100.0; + + let theme = appearance.theme(); + let font_family = appearance.ui_font_family(); + let font_size = appearance.monospace_font_size() - 1.0; + let dim_color: ColorU = theme.sub_text_color(theme.background()).into(); + let cache_color = cache_hit_color(cache_hit_pct, theme); + + let mut row = Flex::row() + .with_cross_axis_alignment(CrossAxisAlignment::Center) + .with_main_axis_size(MainAxisSize::Min); + + // Context: XX.X% (Xk / Xk) + let context_text = format!( + "\u{25a0} Ctx: {:.1}% ({}/{})", + context_pct, + format_token_count(current_context), + format_token_count(max_context), + ); + row.add_child( + Text::new_inline(context_text, font_family, font_size) + .with_color(dim_color) + .finish(), + ); + + // Separator + row.add_child( + Container::new( + Text::new_inline(" \u{2502} ".to_string(), font_family, font_size) + .with_color(dim_color) + .finish(), + ) + .finish(), + ); + + // Cache Hit: XX.X% (R: Xk W: Xk M: Xk) + let cache_label = format!("\u{25c6} Cache: {:.1}%", cache_hit_pct); + row.add_child( + Text::new_inline(cache_label, font_family, font_size) + .with_color(cache_color) + .finish(), + ); + + let cache_detail = format!( + " (R:{} W:{} M:{})", + format_token_count(cache_read), + format_token_count(cache_write), + format_token_count(cache_miss), + ); + row.add_child( + Text::new_inline(cache_detail, font_family, font_size) + .with_color(dim_color) + .finish(), + ); + + // Separator + row.add_child( + Container::new( + Text::new_inline(" \u{2502} ".to_string(), font_family, font_size) + .with_color(dim_color) + .finish(), + ) + .finish(), + ); + + // Cost: $X.XX + let cost_text = format!("\u{25b2} ${:.2}", cost_cents / 100.0); + row.add_child( + Text::new_inline(cost_text, font_family, font_size) + .with_color(theme.ansi_fg_green()) + .finish(), + ); + + Some( + Container::new(row.finish()) + .with_padding_left(12.) + .with_padding_right(12.) + .with_padding_top(2.) + .with_padding_bottom(2.) + .with_background(theme.background()) + .finish(), + ) +} + pub mod styles { use galaxy_core::ui::theme::GalaxyTheme; use pathfinder_color::ColorU; diff --git a/app/src/terminal/model/block.rs b/app/src/terminal/model/block.rs index 88f63fa9..1cd74619 100644 --- a/app/src/terminal/model/block.rs +++ b/app/src/terminal/model/block.rs @@ -2063,7 +2063,7 @@ impl Block { } pub fn has_footer(&self) -> bool { - self.show_memory_stats + false } pub fn footer_top_padding(&self) -> Lines { diff --git a/app/src/terminal/view.rs b/app/src/terminal/view.rs index a604e480..fe4beb39 100644 --- a/app/src/terminal/view.rs +++ b/app/src/terminal/view.rs @@ -87,9 +87,6 @@ use crate::ai::blocklist::block::cli_controller::{ CLISubagentController, CLISubagentEvent, UserTakeOverReason, }; use crate::ai::blocklist::block::status_bar::BlocklistAIStatusBarEvent; -use crate::ai::blocklist::usage::conversation_usage_view::{ - ConversationUsageInfo, ConversationUsageView, DisplayMode, TimingInfo, -}; use crate::ai::blocklist::{block_context_from_terminal_model, SlashCommandRequest}; use crate::ai::document::ai_document_model::{AIDocumentId, AIDocumentModel, AIDocumentVersion}; use crate::ai::loading::shimmering_warp_loading_text; @@ -2583,9 +2580,6 @@ pub struct TerminalView { /// the `insert_rich_content` helper function. rich_content_views: Vec, - /// Cached view ids for usage footers keyed by the AI block view id that owns them. - usage_footer_view_ids: HashMap, - /// View ID of the context window debug view, if visible. context_view_id: Option, @@ -4094,7 +4088,6 @@ impl TerminalView { block_filter_editor, active_filter_editor_block_index: None, rich_content_views: Vec::new(), - usage_footer_view_ids: Default::default(), context_view_id: None, settings_view_id: None, block_onboarding_active: false, @@ -5035,22 +5028,6 @@ impl TerminalView { self.hide_telemetry_banner_permanently(ctx); } - // Close any open usage footer(s) when a new AI block is added - if !self.usage_footer_view_ids.is_empty() { - let owner_block_ids: Vec = - self.usage_footer_view_ids.keys().copied().collect(); - for owner_id in &owner_block_ids { - if let Some(ai_block_handle) = self.ai_block_handle_by_view_id(*owner_id) { - ai_block_handle.update(ctx, |block, ctx| { - block.handle_action( - &AIBlockAction::ToggleIsUsageFooterExpanded, - ctx, - ); - }); - } - } - } - if self.ambient_agent_view_model.as_ref(ctx).is_ambient_agent() && self .model @@ -5599,156 +5576,8 @@ impl TerminalView { /// Handle the opening and closing of the usage footer. /// We insert the usage footer as a rich content view into the blocklist /// below the block that triggered the toggle event. - fn handle_usage_footer_toggled( - &mut self, - source_ai_block_view_id: EntityId, - conversation_id: AIConversationId, - is_expanded: bool, - ctx: &mut ViewContext, - ) { - // Close any existing usage footer for this specific AI block - if let Some(id) = self.usage_footer_view_ids.remove(&source_ai_block_view_id) { - let mut model = self.model.lock(); - model.block_list_mut().remove_rich_content(id); - drop(model); - self.rich_content_views.retain(|rc| rc.view_id() != id); - } - - if !is_expanded { - // If the goal was to close the usage footer block, we've done that above - ctx.notify(); - return; - } - - // Get the conversation from the history model - let Some(conversation) = - BlocklistAIHistoryModel::as_ref(ctx).conversation(&conversation_id) - else { - log::error!("Could not find conversation for usage footer"); - return; - }; - - let tool_usage = conversation.tool_usage_metadata(); - // For Bedrock conversations, the server doesn't send tool_usage_metadata. - // Fall back to counting actions from the conversation exchanges. - let tool_call_count = if tool_usage.total_tool_calls() > 0 { - tool_usage.total_tool_calls() - } else { - conversation.count_all_actions() as i32 - }; - let commands_executed = if tool_usage.run_command_stats.commands_executed > 0 { - tool_usage.run_command_stats.commands_executed - } else { - conversation.count_command_actions() as i32 - }; - let time_to_first_token_ms = conversation.time_to_first_token_for_last_user_query_ms(); - let total_agent_response_time_ms = - conversation.total_agent_response_time_since_last_user_query_ms(); - let wall_to_wall_response_time_ms = - conversation.wall_to_wall_response_time_since_last_query(); - - let token_usage_list = conversation.total_token_usage(); - let total_cache_read_tokens: u32 = token_usage_list.iter().map(|u| u.input_cache_read).sum(); - let total_cache_write_tokens: u32 = - token_usage_list.iter().map(|u| u.input_cache_write).sum(); - let total_input_tokens: u32 = token_usage_list.iter().map(|u| u.total_input).sum(); - let estimated_cost_cents: f32 = token_usage_list.iter().map(|u| u.cost_in_cents).sum(); - - let conversation_usage_info = ConversationUsageInfo { - tool_calls: tool_call_count, - models: conversation.token_usage().to_vec(), - context_window_usage: conversation.context_window_usage(), - files_changed: tool_usage.apply_file_diff_stats.files_changed, - lines_added: tool_usage.apply_file_diff_stats.lines_added, - lines_removed: tool_usage.apply_file_diff_stats.lines_removed, - commands_executed, - current_context_tokens: conversation.current_context_tokens(), - estimated_cost_cents, - total_cache_read_tokens, - total_cache_write_tokens, - total_input_tokens, - }; - - let timing_info = TimingInfo { - time_to_first_token_ms, - total_agent_response_time_ms, - wall_to_wall_response_time_ms, - }; - - // View to hold the usage footer. - let usage_view = ctx.add_view(|_| { - ConversationUsageView::new( - conversation_usage_info, - DisplayMode::Footer, - Some(timing_info), - MouseStateHandle::default(), - ) - }); - self.usage_footer_view_ids - .insert(source_ai_block_view_id, usage_view.id()); - - let agent_view_conversation_id = self - .agent_view_controller - .as_ref(ctx) - .agent_view_state() - .active_conversation_id(); - - let item = RichContentItem::new(None, usage_view.id(), agent_view_conversation_id, false); - - let mut model = self.model.lock(); - let inserted = model.block_list_mut().insert_rich_content_after_item( - RemovableBlocklistItem::RichContent(source_ai_block_view_id), - item, - ); - drop(model); - - if inserted { - self.rich_content_views.push( - RichContent::new(usage_view, agent_view_conversation_id) - .with_metadata(RichContentMetadata::UsageFooter), - ); - } else { - // Fallback: append usage block to the end of the blocklist - self.insert_rich_content( - None, - usage_view, - Some(RichContentMetadata::UsageFooter), - RichContentInsertionPosition::Append { - insert_below_long_running_block: true, - }, - ctx, - ); - } - - ctx.notify(); - } - - fn toggle_usage_footer(&mut self, ctx: &mut ViewContext) { - let conversation_id = self - .agent_view_controller - .as_ref(ctx) - .agent_view_state() - .active_conversation_id(); - - let Some(conversation_id) = conversation_id else { - return; - }; - - let last_ai_block_handle = self - .rich_content_views - .iter() - .rev() - .find_map(|rich_content| { - let ai_metadata = rich_content.ai_block_metadata()?; - (ai_metadata.conversation_id == conversation_id) - .then(|| ai_metadata.ai_block_handle.clone()) - }); - - if let Some(ai_block_handle) = last_ai_block_handle { - ai_block_handle.update(ctx, |block, ctx| { - block.handle_action(&AIBlockAction::ToggleIsUsageFooterExpanded, ctx); - }); - } + fn toggle_usage_footer(&mut self, _ctx: &mut ViewContext) { + // Usage footer has been replaced by the persistent status bar in the input area. } fn toggle_context_view(&mut self, ctx: &mut ViewContext) { @@ -13785,15 +13614,6 @@ impl TerminalView { true }); - // Close any open usage footers on blocks being removed to prevent them becoming orphaned - for (view_id, handle) in &blocks_to_remove { - if self.usage_footer_view_ids.contains_key(view_id) { - handle.update(ctx, |block, ctx| { - block.handle_action(&AIBlockAction::ToggleIsUsageFooterExpanded, ctx); - }); - } - } - blocks_to_remove.into_iter().for_each(|(view_id, handle)| { handle.update(ctx, |block, ctx| { block.cleanup_block(ctx); @@ -19039,12 +18859,6 @@ impl TerminalView { AIBlockEvent::CopiedEmptyText => { self.copy(ctx); } - AIBlockEvent::UsageFooterToggled { - conversation_id, - is_expanded, - } => { - self.handle_usage_footer_toggled(block.id(), *conversation_id, *is_expanded, ctx); - } AIBlockEvent::OpenSettings => { ctx.emit(Event::OpenSettings(SettingsSection::WarpAgent)); } diff --git a/crates/graphql/src/api/queries/get_conversation_usage.rs b/crates/graphql/src/api/queries/get_conversation_usage.rs index d1be64a7..7b5374c3 100644 --- a/crates/graphql/src/api/queries/get_conversation_usage.rs +++ b/crates/graphql/src/api/queries/get_conversation_usage.rs @@ -171,6 +171,10 @@ impl From<&ConversationUsageMetadata> for persistence::model::ConversationUsageM credits_spent_for_last_block: None, token_usage: convert_token_usage(&gql.warp_token_usage, &gql.byok_token_usage), tool_usage_metadata: (&gql.tool_usage_metadata).into(), + total_cache_read_tokens: 0, + total_cache_write_tokens: 0, + total_cache_miss_tokens: 0, + total_cost_cents: 0.0, } } } diff --git a/crates/persistence/src/model.rs b/crates/persistence/src/model.rs index f99220c6..885a9cfa 100644 --- a/crates/persistence/src/model.rs +++ b/crates/persistence/src/model.rs @@ -1304,6 +1304,14 @@ pub struct ConversationUsageMetadata { pub token_usage: Vec, #[serde(default)] pub tool_usage_metadata: ToolUsageMetadata, + #[serde(default)] + pub total_cache_read_tokens: u32, + #[serde(default)] + pub total_cache_write_tokens: u32, + #[serde(default)] + pub total_cache_miss_tokens: u32, + #[serde(default)] + pub total_cost_cents: f32, } impl ConversationUsageMetadata {