v1.5.0: Inline subagent panels, Bedrock compaction fixes, context window debug view
New features: - Inline subagent panels with expand/collapse and click-to-toggle - /context slash command to inspect bedrock_message_history - Child-to-parent question routing with auto-answer for subagents - Subagent token usage and cost merging into parent conversation - Randomized session-colored user avatar silhouettes Bug fixes: - Bedrock: remove orphaned tool_results after compaction - Bedrock: self-healing exchange lookup for out-of-order streaming - Bedrock: append continuation prompt when conversation ends with assistant - Duration sanity check rejects epoch-time artifacts from session restore - Cache hit rate calculation uses actual total_input_tokens - Hide "Time to first token" when value is zero Improvements: - Demote verbose bedrock-debug logs to debug/trace levels - Bedrock tool usage counting falls back to action counting - Remove logout menu item from workspace menu 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
6f54e2cb30
commit
f278e53b7e
@@ -0,0 +1,179 @@
|
||||
use crate::ai::bedrock::convert::{ContentPart, ConversationMessage, MessageContent, MessageRole};
|
||||
use crate::appearance::Appearance;
|
||||
use crate::ui_components::blended_colors;
|
||||
use galaxyui::{
|
||||
elements::{
|
||||
Container, CornerRadius, CrossAxisAlignment, Flex, ParentElement, Radius, Text,
|
||||
},
|
||||
AppContext, Element, Entity, SingletonEntity, TypedActionView, View, ViewContext,
|
||||
};
|
||||
|
||||
pub struct ContextWindowView {
|
||||
messages: Vec<ConversationMessage>,
|
||||
}
|
||||
|
||||
impl ContextWindowView {
|
||||
pub fn new(messages: Vec<ConversationMessage>) -> Self {
|
||||
Self { messages }
|
||||
}
|
||||
}
|
||||
|
||||
impl View for ContextWindowView {
|
||||
fn ui_name() -> &'static str {
|
||||
"ContextWindowView"
|
||||
}
|
||||
|
||||
fn render(&self, app: &AppContext) -> Box<dyn Element> {
|
||||
let appearance = Appearance::as_ref(app);
|
||||
let theme = appearance.theme();
|
||||
let font_size = appearance.ui_font_size();
|
||||
let text_color = blended_colors::text_main(theme, theme.surface_2());
|
||||
let label_color = blended_colors::text_sub(theme, theme.surface_2());
|
||||
|
||||
let mut column = Flex::column()
|
||||
.with_cross_axis_alignment(CrossAxisAlignment::Stretch)
|
||||
.with_spacing(6.0);
|
||||
|
||||
// Header
|
||||
let total_chars: usize = self
|
||||
.messages
|
||||
.iter()
|
||||
.map(|m| match &m.content {
|
||||
MessageContent::Text(t) => t.len(),
|
||||
MessageContent::ToolUse { input, .. } => input.to_string().len(),
|
||||
MessageContent::ToolResult { content, .. } => content.len(),
|
||||
MessageContent::MultiPart(parts) => parts
|
||||
.iter()
|
||||
.map(|p| match p {
|
||||
ContentPart::Text(t) => t.len(),
|
||||
ContentPart::ToolUse { input, .. } => input.to_string().len(),
|
||||
ContentPart::ToolResult { content, .. } => content.len(),
|
||||
})
|
||||
.sum(),
|
||||
})
|
||||
.sum();
|
||||
let estimated_tokens = total_chars / 4;
|
||||
|
||||
let header_text = format!(
|
||||
"Context Window: {} messages, ~{} tokens est.",
|
||||
self.messages.len(),
|
||||
format_tokens(estimated_tokens as u32),
|
||||
);
|
||||
column = column.with_child(
|
||||
Text::new(
|
||||
header_text,
|
||||
appearance.ui_font_family(),
|
||||
font_size + 1.0,
|
||||
)
|
||||
.with_color(label_color)
|
||||
.finish(),
|
||||
);
|
||||
|
||||
// Messages — show full content, no truncation.
|
||||
// The parent blocklist handles scrolling.
|
||||
for (i, msg) in self.messages.iter().enumerate() {
|
||||
let role_str = match msg.role {
|
||||
MessageRole::User => "USER",
|
||||
MessageRole::Assistant => "ASST",
|
||||
};
|
||||
|
||||
// Role header
|
||||
let header_line = format!("--- [{}] Message #{} ---", role_str, i);
|
||||
column = column.with_child(
|
||||
Text::new(header_line, appearance.ui_font_family(), font_size)
|
||||
.with_color(label_color)
|
||||
.soft_wrap(true)
|
||||
.finish(),
|
||||
);
|
||||
|
||||
// Full content
|
||||
let content_text = match &msg.content {
|
||||
MessageContent::Text(t) => t.clone(),
|
||||
MessageContent::ToolUse { name, tool_use_id, input } => {
|
||||
format!(
|
||||
"[ToolUse] name={}, id={}\ninput={}",
|
||||
name, tool_use_id, input
|
||||
)
|
||||
}
|
||||
MessageContent::ToolResult {
|
||||
tool_use_id,
|
||||
content,
|
||||
is_error,
|
||||
} => {
|
||||
format!(
|
||||
"[ToolResult] id={}, error={}\n{}",
|
||||
tool_use_id, is_error, content
|
||||
)
|
||||
}
|
||||
MessageContent::MultiPart(parts) => {
|
||||
let mut out = String::new();
|
||||
for (pi, p) in parts.iter().enumerate() {
|
||||
match p {
|
||||
ContentPart::Text(t) => {
|
||||
out.push_str(&format!("[Part {} Text] {}\n", pi, t));
|
||||
}
|
||||
ContentPart::ToolUse { name, tool_use_id, input } => {
|
||||
out.push_str(&format!(
|
||||
"[Part {} ToolUse] name={}, id={}, input={}\n",
|
||||
pi, name, tool_use_id, input
|
||||
));
|
||||
}
|
||||
ContentPart::ToolResult { tool_use_id, content, is_error } => {
|
||||
out.push_str(&format!(
|
||||
"[Part {} ToolResult] id={}, error={}\n{}\n",
|
||||
pi, tool_use_id, is_error, content
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
};
|
||||
|
||||
column = column.with_child(
|
||||
Text::new(content_text, appearance.ui_font_family(), font_size)
|
||||
.with_color(text_color)
|
||||
.soft_wrap(true)
|
||||
.finish(),
|
||||
);
|
||||
}
|
||||
|
||||
if self.messages.is_empty() {
|
||||
column = column.with_child(
|
||||
Text::new(
|
||||
"(empty - no messages in bedrock history)".to_string(),
|
||||
appearance.ui_font_family(),
|
||||
font_size,
|
||||
)
|
||||
.with_color(label_color)
|
||||
.finish(),
|
||||
);
|
||||
}
|
||||
|
||||
Container::new(column.finish())
|
||||
.with_uniform_padding(12.0)
|
||||
.with_background(theme.surface_2())
|
||||
.with_corner_radius(CornerRadius::with_all(Radius::Pixels(8.0)))
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl Entity for ContextWindowView {
|
||||
type Event = ();
|
||||
}
|
||||
|
||||
impl TypedActionView for ContextWindowView {
|
||||
type Action = ();
|
||||
|
||||
fn handle_action(&mut self, _action: &Self::Action, _ctx: &mut ViewContext<Self>) {}
|
||||
}
|
||||
|
||||
fn format_tokens(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}")
|
||||
}
|
||||
}
|
||||
@@ -41,6 +41,8 @@ pub struct ConversationUsageInfo {
|
||||
pub total_cache_read_tokens: u32,
|
||||
/// Cumulative cache write tokens (session total).
|
||||
pub total_cache_write_tokens: u32,
|
||||
/// Cumulative total input tokens across all requests (session total).
|
||||
pub total_input_tokens: u32,
|
||||
}
|
||||
|
||||
/// Timing information for the last set of agent responses
|
||||
@@ -278,15 +280,15 @@ impl ConversationUsageView {
|
||||
));
|
||||
}
|
||||
|
||||
// Cache hit rate
|
||||
let cache_miss = self.usage_info.current_context_tokens
|
||||
.saturating_sub(self.usage_info.total_cache_read_tokens);
|
||||
let total_input = self.usage_info.total_cache_read_tokens + cache_miss;
|
||||
if total_input > 0 {
|
||||
let hit_rate = (self.usage_info.total_cache_read_tokens as f32 / total_input as f32) * 100.0;
|
||||
// Cache hit rate: proportion of total input tokens served from cache
|
||||
let total_input = self.usage_info.total_input_tokens;
|
||||
if total_input > 0 && self.usage_info.total_cache_read_tokens > 0 {
|
||||
let hit_rate = (self.usage_info.total_cache_read_tokens as f32
|
||||
/ total_input as f32)
|
||||
* 100.0;
|
||||
labels.push(render_label_text("Cache hit rate", appearance));
|
||||
values.push(render_value_text(
|
||||
format!("{:.0}%", hit_rate),
|
||||
format!("{:.0}%", hit_rate.min(100.0)),
|
||||
appearance,
|
||||
));
|
||||
}
|
||||
@@ -410,14 +412,16 @@ impl ConversationUsageView {
|
||||
));
|
||||
values.push(render_section_header("".to_string(), appearance));
|
||||
|
||||
labels.push(render_label_text("Time to first token", appearance));
|
||||
values.push(render_value_text(
|
||||
format!(
|
||||
"{:.1} seconds",
|
||||
timing.time_to_first_token_ms as f64 / 1000.0
|
||||
),
|
||||
appearance,
|
||||
));
|
||||
if timing.time_to_first_token_ms > 0 {
|
||||
labels.push(render_label_text("Time to first token", appearance));
|
||||
values.push(render_value_text(
|
||||
format!(
|
||||
"{:.1} seconds",
|
||||
timing.time_to_first_token_ms as f64 / 1000.0
|
||||
),
|
||||
appearance,
|
||||
));
|
||||
}
|
||||
|
||||
labels.push(render_label_text("Total agent response time", appearance));
|
||||
values.push(render_value_text(
|
||||
|
||||
@@ -2,6 +2,7 @@ use galaxy_core::ui::theme::{Fill, GalaxyTheme};
|
||||
use galaxy_core::ui::Icon;
|
||||
use galaxyui::Element;
|
||||
|
||||
pub mod context_window_view;
|
||||
pub mod conversation_usage_view;
|
||||
|
||||
pub fn icon_for_context_window_usage(context_window_usage: f32) -> Icon {
|
||||
|
||||
Reference in New Issue
Block a user