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
+2
-188
@@ -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<RichContent>,
|
||||
|
||||
/// Cached view ids for usage footers keyed by the AI block view id that owns them.
|
||||
usage_footer_view_ids: HashMap<EntityId, EntityId>,
|
||||
|
||||
/// View ID of the context window debug view, if visible.
|
||||
context_view_id: Option<EntityId>,
|
||||
|
||||
@@ -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<EntityId> =
|
||||
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<Self>,
|
||||
) {
|
||||
// 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<Self>) {
|
||||
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<Self>) {
|
||||
// Usage footer has been replaced by the persistent status bar in the input area.
|
||||
}
|
||||
|
||||
fn toggle_context_view(&mut self, ctx: &mut ViewContext<Self>) {
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user