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:
Ryan Ward
2026-05-21 14:30:04 -05:00
co-authored by Claude Opus 4.6
parent 6f54e2cb30
commit f278e53b7e
22 changed files with 914 additions and 113 deletions
@@ -1,4 +1,6 @@
use crate::ai::agent::{AIAgentActionResultType, AIAgentActionType};
use crate::ai::blocklist::orchestration_events::OrchestrationEventService;
use crate::ai::blocklist::BlocklistAIHistoryModel;
use crate::ai::blocklist::BlocklistAIPermissions;
use ai::agent::action_result::{AskUserQuestionAnswerItem, AskUserQuestionResult};
use futures::{future::BoxFuture, FutureExt};
@@ -51,6 +53,52 @@ impl AskUserQuestionExecutor {
}
};
// For child agent conversations, route the question to the parent
// for silent auto-answer instead of presenting UI to the user.
if let Some(parent_conversation_id) = self.parent_conversation_id(input.conversation_id, ctx)
{
let question_text = questions
.iter()
.map(|q| q.question.clone())
.collect::<Vec<_>>()
.join("\n");
let options: Vec<String> = questions
.iter()
.filter_map(|q| q.multiple_choice_options())
.flatten()
.map(|o| o.label.clone())
.collect();
OrchestrationEventService::handle(ctx).update(ctx, |service, ctx| {
service.route_subagent_question_to_parent(
input.conversation_id,
parent_conversation_id,
question_text,
options,
0,
ctx,
);
});
// Wait for the parent's answer to arrive via the same channel
let receiver = self.result_rx.1.clone();
return ActionExecution::new_async(
async move { receiver.recv().await },
|result, _ctx| match result {
Ok(AskUserQuestionDecision::Completed(answers)) => {
AIAgentActionResultType::AskUserQuestion(
AskUserQuestionResult::Success { answers },
)
}
Ok(AskUserQuestionDecision::Cancelled) | Err(_) => {
AIAgentActionResultType::AskUserQuestion(
AskUserQuestionResult::Cancelled,
)
}
},
);
}
if self.should_autoexecute(input, ctx) {
let question_ids = questions
.iter()
@@ -80,6 +128,16 @@ impl AskUserQuestionExecutor {
)
}
fn parent_conversation_id(
&self,
conversation_id: crate::ai::agent::conversation::AIConversationId,
ctx: &ModelContext<Self>,
) -> Option<crate::ai::agent::conversation::AIConversationId> {
let history_model = BlocklistAIHistoryModel::as_ref(ctx);
let conversation = history_model.conversation(&conversation_id)?;
conversation.parent_conversation_id()
}
pub(super) fn preprocess_action(
&mut self,
_action: PreprocessActionInput,
@@ -3,9 +3,8 @@
//! Shows a collapsible panel with the subagent's status, a mini-transcript of
//! recent messages, and controls to expand to full view or cancel.
use galaxy_core::ui::theme::Fill;
use galaxyui::elements::{
ConstrainedBox, Container, CornerRadius, CrossAxisAlignment, Element, Empty, Flex,
ConstrainedBox, Container, CornerRadius, CrossAxisAlignment, Element, Empty, Flex, Hoverable,
MainAxisAlignment, MainAxisSize, MouseStateHandle, ParentElement, Radius, Shrinkable, Text,
};
use galaxyui::{AppContext, SingletonEntity};
@@ -13,6 +12,8 @@ use pathfinder_color::ColorU;
use warp_multi_agent_api as api;
use crate::ai::agent::conversation::{AIConversationId, ConversationStatus};
use crate::ai::agent::AIAgentActionId;
use crate::ai::blocklist::block::AIBlockAction;
use crate::ai::blocklist::inline_action::inline_action_header::{
ICON_MARGIN, INLINE_ACTION_HEADER_VERTICAL_PADDING, INLINE_ACTION_HORIZONTAL_PADDING,
};
@@ -51,6 +52,7 @@ impl SubagentPanelState {
/// Renders the inline subagent panel for a child conversation.
pub fn render_subagent_inline_panel(
state: &SubagentPanelState,
action_id: &AIAgentActionId,
app: &AppContext,
) -> Box<dyn Element> {
let appearance = Appearance::as_ref(app);
@@ -71,14 +73,28 @@ pub fn render_subagent_inline_panel(
let mut column = Flex::column().with_cross_axis_alignment(CrossAxisAlignment::Stretch);
// Header — always visible
column.add_child(render_panel_header(
&agent_name,
&status,
state,
panel_bg,
app,
));
// Header — always visible, click to toggle expand/collapse
let header_mouse_state = state.header_mouse_state.clone();
let toggle_action_id = action_id.clone();
let header_status = status.clone();
let header_expanded = state.is_expanded;
column.add_child(
Hoverable::new(header_mouse_state, move |_mouse_state| {
render_panel_header(
&agent_name,
&header_status,
header_expanded,
panel_bg,
app,
)
})
.on_click(move |ctx, _, _| {
ctx.dispatch_typed_action(AIBlockAction::ToggleSubagentPanel {
action_id: toggle_action_id.clone(),
});
})
.finish(),
);
// Body (mini-transcript) — only when expanded
if state.is_expanded {
@@ -106,7 +122,7 @@ pub fn render_subagent_inline_panel(
fn render_panel_header(
agent_name: &str,
status: &ConversationStatus,
state: &SubagentPanelState,
is_expanded: bool,
_background: ColorU,
app: &AppContext,
) -> Box<dyn Element> {
@@ -168,7 +184,7 @@ fn render_panel_header(
// Right: collapse/expand chevron
let mut right_side = Flex::row().with_cross_axis_alignment(CrossAxisAlignment::Center);
let chevron_icon = if state.is_expanded {
let chevron_icon = if is_expanded {
Icon::ChevronDown
} else {
Icon::ChevronRight
+34
View File
@@ -398,6 +398,7 @@ pub(super) struct AIBlockStateHandles {
/// A given citation should only appear once per block.
footer_citation_chip_handles: HashMap<AIAgentCitation, MouseStateHandle>,
orchestration_navigation_card_handles: HashMap<AIAgentActionId, MouseStateHandle>,
pub(super) subagent_panel_states: HashMap<AIAgentActionId, super::agent_view::subagent_inline_panel::SubagentPanelState>,
references_section_collapsible_handle: MouseStateHandle,
@@ -1369,6 +1370,10 @@ impl AIBlock {
me.run_secret_redaction_on_user_query(me.client_ids.conversation_id, ctx);
me.spawn_link_detection(ctx);
// Create summarization view immediately if this block has a SummarizeConversation input,
// so the "Summarizing..." UI appears before the API response starts streaming.
me.maybe_create_summarization_view_from_input(ctx);
if me.model.status(ctx).is_streaming() {
me.model
.on_updated_output(Box::new(Self::on_output_status_update), ctx);
@@ -4237,6 +4242,27 @@ impl AIBlock {
});
}
// Create subagent panel state for finished StartAgent actions
if let Some(AIActionStatus::Finished(result)) =
action_model.as_ref(ctx).get_action_status(action_id)
{
if let AIAgentActionResultType::StartAgent(
crate::ai::agent::StartAgentResult::Success { agent_id, .. },
) = &result.result
{
if !me.state_handles.subagent_panel_states.contains_key(action_id) {
if let Some(conversation_id) =
crate::ai::blocklist::agent_view::orchestration_conversation_links::conversation_id_for_agent_id(agent_id, ctx)
{
me.state_handles.subagent_panel_states.insert(
action_id.clone(),
super::agent_view::subagent_inline_panel::SubagentPanelState::new(conversation_id),
);
}
}
}
}
let action_statuses = me
.requested_action_ids
.iter()
@@ -5825,6 +5851,9 @@ pub enum AIBlockAction {
OpenCommentInGitHub {
url: String,
},
ToggleSubagentPanel {
action_id: AIAgentActionId,
},
}
impl TypedActionView for AIBlock {
@@ -6474,6 +6503,11 @@ impl TypedActionView for AIBlock {
initial_index,
});
}
AIBlockAction::ToggleSubagentPanel { action_id } => {
if let Some(state) = self.state_handles.subagent_panel_states.get_mut(action_id) {
state.is_expanded = !state.is_expanded;
}
}
}
ctx.notify();
}
+28 -12
View File
@@ -3403,30 +3403,46 @@ pub struct FindContext<'a> {
pub state: &'a FindState,
}
/// Renders a user avatar with profile image or display name.
/// A palette of colors for the user avatar silhouette, randomly selected per session.
const USER_AVATAR_PALETTE: &[ColorU] = &[
ColorU { r: 99, g: 179, b: 237, a: 255 }, // blue
ColorU { r: 129, g: 230, b: 217, a: 255 }, // teal
ColorU { r: 183, g: 148, b: 244, a: 255 }, // purple
ColorU { r: 252, g: 165, b: 165, a: 255 }, // red/coral
ColorU { r: 251, g: 191, b: 36, a: 255 }, // amber
ColorU { r: 110, g: 231, b: 183, a: 255 }, // green
ColorU { r: 249, g: 168, b: 212, a: 255 }, // pink
ColorU { r: 253, g: 186, b: 116, a: 255 }, // orange
];
fn session_avatar_color() -> ColorU {
use std::sync::OnceLock;
use rand::Rng;
static COLOR: OnceLock<ColorU> = OnceLock::new();
*COLOR.get_or_init(|| {
let idx = rand::thread_rng().gen_range(0..USER_AVATAR_PALETTE.len());
USER_AVATAR_PALETTE[idx]
})
}
/// Renders a user avatar as a silhouette icon with a session-random color.
pub fn render_user_avatar(
user_display_name: &str,
profile_image_path: Option<&String>,
_user_display_name: &str,
_profile_image_path: Option<&String>,
avatar_color: Option<ColorU>,
app: &AppContext,
) -> Box<dyn Element> {
let appearance = Appearance::as_ref(app);
let theme = appearance.theme();
let background = avatar_color.unwrap_or_else(|| blended_colors::accent(theme).into());
let background = avatar_color.unwrap_or_else(session_avatar_color);
let avatar = Avatar::new(
profile_image_path
.map(|url| AvatarContent::Image {
url: url.to_owned(),
display_name: user_display_name.to_owned(),
})
.unwrap_or(AvatarContent::DisplayName(user_display_name.to_owned())),
AvatarContent::Icon(Icon::User),
UiComponentStyles {
width: Some(icon_size(app)),
height: Some(icon_size(app)),
font_family_id: Some(appearance.ui_font_family()),
font_size: Some(appearance.monospace_font_size() - 2.),
background: Some(background.into()),
font_color: Some(blended_colors::text_main(theme, background)),
font_color: Some(ColorU::white()),
border_radius: Some(CornerRadius::with_all(Radius::Percentage(50.))),
..Default::default()
},
@@ -432,28 +432,42 @@ pub(super) fn render_start_agent(
}
}
if let Some(card_data) = child_conversation_card_data {
let navigation_card_handle = props
.state_handles
.orchestration_navigation_card_handles
.get(action_id)
.cloned()
.unwrap_or_else(|| {
log::error!(
"Missing orchestration navigation card handle for StartAgent action {:?}",
action_id
);
MouseStateHandle::default()
});
let status_icon = card_data.status.status_icon_and_color(theme);
column.add_child(render_conversation_navigation_card_row(
&card_data.agent_name,
Some(&card_data.title),
Some(status_icon),
card_data.conversation_id,
navigation_card_handle,
true,
app,
));
// Render inline subagent panel instead of navigation card
if let Some(panel_state) =
props.state_handles.subagent_panel_states.get(action_id)
{
column.add_child(
crate::ai::blocklist::agent_view::subagent_inline_panel::render_subagent_inline_panel(
panel_state,
action_id,
app,
),
);
} else {
// Fallback: render the navigation card if no panel state exists yet
let navigation_card_handle = props
.state_handles
.orchestration_navigation_card_handles
.get(action_id)
.cloned()
.unwrap_or_else(|| {
log::error!(
"Missing orchestration navigation card handle for StartAgent action {:?}",
action_id
);
MouseStateHandle::default()
});
let status_icon = card_data.status.status_icon_and_color(theme);
column.add_child(render_conversation_navigation_card_row(
&card_data.agent_name,
Some(&card_data.title),
Some(status_icon),
card_data.conversation_id,
navigation_card_handle,
true,
app,
));
}
}
return column
+67 -1
View File
@@ -78,7 +78,9 @@ use std::sync::Arc;
use std::time::Duration;
use warp_multi_agent_api::{message, Task, ToolType};
use super::orchestration_events::{OrchestrationEventService, OrchestrationEventServiceEvent};
use super::orchestration_events::{
OrchestrationEventService, OrchestrationEventServiceEvent, PendingEventDetail,
};
use galaxyui::{AppContext, Entity, EntityId, ModelContext, ModelHandle, SingletonEntity};
#[derive(Debug, Clone)]
@@ -1507,6 +1509,9 @@ impl BlocklistAIController {
return;
}
// Handle local subagent events before standard orchestration events.
self.handle_subagent_events(conversation_id, ctx);
if self
.in_flight_response_streams
.has_active_stream_for_conversation(conversation_id, ctx)
@@ -1555,6 +1560,67 @@ impl BlocklistAIController {
}
}
/// Processes local subagent events (questions, answers, summaries) for a conversation.
fn handle_subagent_events(
&mut self,
conversation_id: AIConversationId,
ctx: &mut ModelContext<Self>,
) {
let events = OrchestrationEventService::handle(ctx).update(ctx, |svc, _ctx| {
svc.drain_subagent_events(&conversation_id)
});
for event in events {
match event.detail {
PendingEventDetail::SubagentQuestion {
source_conversation_id,
question_text,
options,
..
} => {
// Auto-answer: pick the first option, or echo the question text
// as a default answer. In a future version, this could invoke the
// parent LLM for a contextual answer.
let answer = options.first().cloned().unwrap_or_else(|| {
format!("Proceed with: {}", question_text)
});
OrchestrationEventService::handle(ctx).update(ctx, |svc, ctx| {
svc.route_answer_to_subagent(source_conversation_id, answer, ctx);
});
}
PendingEventDetail::SubagentAnswer {
answer_text, ..
} => {
// This fires on the child's controller — complete its pending question.
self.complete_ask_user_question_with_answer(answer_text, ctx);
}
PendingEventDetail::SubagentCompletionSummary { .. } => {
// Summary is consumed by the inline panel renderer directly.
// No controller action needed.
}
_ => {}
}
}
}
/// Completes this controller's pending AskUserQuestion with the answer from the parent.
fn complete_ask_user_question_with_answer(
&self,
answer_text: String,
ctx: &mut ModelContext<Self>,
) {
use ai::agent::action_result::AskUserQuestionAnswerItem;
let executor = self.action_model.as_ref(ctx).ask_user_question_executor(ctx);
let answer_item = AskUserQuestionAnswerItem::Answered {
question_id: String::new(),
selected_options: vec![answer_text.clone()],
other_text: answer_text,
};
executor.as_ref(ctx).complete(vec![answer_item]);
}
pub fn resume_conversation(
&mut self,
conversation_id: AIConversationId,
@@ -259,8 +259,8 @@ impl ResponseStream {
let event_type_name = match &response_event.r#type {
Some(warp_multi_agent_api::response_event::Type::Init(_)) => "Init",
Some(warp_multi_agent_api::response_event::Type::ClientActions(a)) => {
log::info!(
"[bedrock-debug] ResponseStream received ClientActions with {} actions",
log::debug!(
"[bedrock] ResponseStream received ClientActions with {} actions",
a.actions.len()
);
"ClientActions"
@@ -552,6 +552,57 @@ impl OrchestrationEventService {
LifecycleEventType::Idle,
result,
);
// Emit completion summary to parent and merge costs
let (parent_id, summary) = {
let history_model = BlocklistAIHistoryModel::as_ref(ctx);
let parent_id = history_model
.conversation(&conversation_id)
.and_then(|c| c.parent_conversation_id());
let summary = parent_id.and_then(|_| {
let conv = history_model.conversation(&conversation_id)?;
let messages = conv.all_linearized_messages();
messages.iter().rev().find_map(|msg| {
let message_content = msg.message.as_ref()?;
match message_content {
warp_multi_agent_api::message::Message::AgentOutput(output)
if !output.text.is_empty() =>
{
let text = if output.text.len() > 500 {
format!("{}...", &output.text[..497])
} else {
output.text.clone()
};
Some(text)
}
_ => None,
}
})
});
(parent_id, summary)
};
if let Some(parent_id) = parent_id {
// Merge child's token usage and costs into parent
BlocklistAIHistoryModel::handle(ctx).update(ctx, |history_model, _ctx| {
let child_usage: Option<(HashMap<String, api::response_event::stream_finished::TokenUsage>, crate::ai::agent::RequestCost)> = history_model
.conversation(&conversation_id)
.map(|c| (c.total_token_usage_by_model().clone(), c.total_request_cost()));
if let Some((token_usage, request_cost)) = child_usage {
if let Some(parent) = history_model.conversation_mut(&parent_id) {
parent.merge_child_usage_raw(&token_usage, request_cost);
}
}
});
if let Some(summary_text) = summary {
self.route_subagent_completion_summary(
conversation_id,
parent_id,
summary_text,
ctx,
);
}
}
}
(Some(ConversationStatus::InProgress), ConversationStatus::Error) => {
let result = self.dispatch_lifecycle_event(
@@ -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(
+1
View File
@@ -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 {