Replace per-block usage footer with persistent input status bar
Move token/cache/cost metrics from the expandable per-block usage button into a persistent status bar rendered in the agent input area. This gives always-visible feedback without requiring user interaction. - Add render_session_status_bar to agent input showing context %, cache hit rate, and cost - Persist cache/cost totals in ConversationUsageMetadata for session restore - Remove render_usage_button, ToggleIsUsageFooterExpanded action, and UsageFooterToggled event - Remove "Request tokens: --" debug block footer overlay - Fix has_footer() to return false so blocks no longer reserve phantom footer space (root cause of padding/margin issue) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
259244863b
commit
9e11c91a6e
@@ -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
|
||||
|
||||
@@ -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<String>,
|
||||
|
||||
/// 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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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<Box<dyn Elem
|
||||
flex.add_child(fork_button);
|
||||
}
|
||||
|
||||
flex.add_child(render_usage_button(props, app));
|
||||
|
||||
// Review changes button.
|
||||
if props.has_accepted_edits && !props.shared_session_status.is_viewer() {
|
||||
@@ -3214,155 +3209,6 @@ fn render_response_footer(props: Props, app: &AppContext) -> Option<Box<dyn Elem
|
||||
Some(flex.finish().with_content_item_spacing().finish())
|
||||
}
|
||||
|
||||
/// Renders the usage button that, on click, will expand & collapse the usage summary footer.
|
||||
fn render_usage_button(props: Props, app: &AppContext) -> Box<dyn Element> {
|
||||
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<V: View>(
|
||||
action_id: &AIAgentActionId,
|
||||
|
||||
Reference in New Issue
Block a user