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:
Ryan Ward
2026-06-03 00:17:34 -05:00
co-authored by Claude Opus 4.6
parent 259244863b
commit 9e11c91a6e
11 changed files with 195 additions and 403 deletions
+144 -1
View File
@@ -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<Box<dyn Element>> {
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;