v1.4.0: Auto-compact streaming, Bedrock summarization support, subagent orchestration, and Galaxy rebrand continuation
Major features: - Auto-compact: triggers conversation summarization when context window >= 85%, compacts Bedrock message history to a summary pair, and tracks live context tokens - Bedrock summarization: plumbs `is_summarization` flag through translator/client/response pipeline, handles SummarizeConversation input type, and marks `summarized` in metadata - Session restore: rebuilds bedrock_message_history from persisted task messages via newly-public `convert_proto_message`, preventing empty history on reconnect - Subagent orchestration: adds SubagentQuestion/Answer/CompletionSummary event types, parent-child question routing with depth limits, retry counting, and drain methods - Summarization UI: inline SummarizationView in AI blocks with progress/finished states Refactors: - Rename WarpTheme → GalaxyTheme across ~100 files (rebrand continuation) - Rename warp_home_config_dir → galaxy_home_config_dir and related path functions - Predefined rules: replace "System Defined Rule #N" with descriptive names (e.g. "Correctness Over Speed", "Never Guess") and add lookup helpers - Usage view: replace cumulative input/output token display with live context tokens, cache hit rate calculation, and separate cache read/write stats - Telemetry: remove verbose doc comments, simplify trait definitions - Facts view: simplify delete permission check (always allow local deletion) - Remove warp_managed_paths_watcher.rs (dead code) 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
eaa2ddc75e
commit
6f54e2cb30
@@ -8,6 +8,7 @@ mod inline_agent_view_header;
|
||||
// TODO: Move orchestration_conversation_links module import elsewhere.
|
||||
pub(crate) mod orchestration_conversation_links;
|
||||
pub mod shortcuts;
|
||||
pub(crate) mod subagent_inline_panel;
|
||||
mod zero_state_block;
|
||||
|
||||
pub use agent_input_footer::*;
|
||||
|
||||
@@ -0,0 +1,343 @@
|
||||
//! Inline subagent panel rendered within the parent agent's chat flow.
|
||||
//!
|
||||
//! 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,
|
||||
MainAxisAlignment, MainAxisSize, MouseStateHandle, ParentElement, Radius, Shrinkable, Text,
|
||||
};
|
||||
use galaxyui::{AppContext, SingletonEntity};
|
||||
use pathfinder_color::ColorU;
|
||||
use warp_multi_agent_api as api;
|
||||
|
||||
use crate::ai::agent::conversation::{AIConversationId, ConversationStatus};
|
||||
use crate::ai::blocklist::inline_action::inline_action_header::{
|
||||
ICON_MARGIN, INLINE_ACTION_HEADER_VERTICAL_PADDING, INLINE_ACTION_HORIZONTAL_PADDING,
|
||||
};
|
||||
use crate::ai::blocklist::inline_action::inline_action_icons::icon_size;
|
||||
use crate::ai::blocklist::BlocklistAIHistoryModel;
|
||||
use crate::appearance::Appearance;
|
||||
use crate::ui_components::blended_colors;
|
||||
use crate::ui_components::icons::Icon;
|
||||
|
||||
const MINI_TRANSCRIPT_MAX_LINES: usize = 8;
|
||||
const PANEL_MAX_HEIGHT: f32 = 200.;
|
||||
const PANEL_CORNER_RADIUS: f32 = 8.;
|
||||
|
||||
/// State for a single subagent inline panel instance.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SubagentPanelState {
|
||||
pub conversation_id: AIConversationId,
|
||||
pub is_expanded: bool,
|
||||
pub header_mouse_state: MouseStateHandle,
|
||||
pub expand_button_mouse_state: MouseStateHandle,
|
||||
pub cancel_button_mouse_state: MouseStateHandle,
|
||||
}
|
||||
|
||||
impl SubagentPanelState {
|
||||
pub fn new(conversation_id: AIConversationId) -> Self {
|
||||
Self {
|
||||
conversation_id,
|
||||
is_expanded: false,
|
||||
header_mouse_state: MouseStateHandle::default(),
|
||||
expand_button_mouse_state: MouseStateHandle::default(),
|
||||
cancel_button_mouse_state: MouseStateHandle::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Renders the inline subagent panel for a child conversation.
|
||||
pub fn render_subagent_inline_panel(
|
||||
state: &SubagentPanelState,
|
||||
app: &AppContext,
|
||||
) -> Box<dyn Element> {
|
||||
let appearance = Appearance::as_ref(app);
|
||||
let theme = appearance.theme();
|
||||
|
||||
let history_model = BlocklistAIHistoryModel::as_ref(app);
|
||||
let Some(conversation) = history_model.conversation(&state.conversation_id) else {
|
||||
return Empty::new().finish();
|
||||
};
|
||||
|
||||
let status = conversation.status().clone();
|
||||
let agent_name = conversation
|
||||
.agent_name()
|
||||
.unwrap_or("Subagent")
|
||||
.to_string();
|
||||
|
||||
let panel_bg = blended_colors::neutral_2(theme);
|
||||
|
||||
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,
|
||||
));
|
||||
|
||||
// Body (mini-transcript) — only when expanded
|
||||
if state.is_expanded {
|
||||
let transcript_lines = collect_mini_transcript(&state.conversation_id, app);
|
||||
if !transcript_lines.is_empty() {
|
||||
column.add_child(render_mini_transcript(&transcript_lines, panel_bg, app));
|
||||
}
|
||||
}
|
||||
|
||||
// Footer — show summary when complete
|
||||
if status.is_done() {
|
||||
if let Some(summary) = get_completion_summary(&state.conversation_id, app) {
|
||||
column.add_child(render_summary_footer(&summary, panel_bg, app));
|
||||
}
|
||||
}
|
||||
|
||||
Container::new(column.finish())
|
||||
.with_background_color(panel_bg)
|
||||
.with_corner_radius(CornerRadius::with_all(Radius::Pixels(PANEL_CORNER_RADIUS)))
|
||||
.with_margin_top(4.)
|
||||
.with_margin_bottom(4.)
|
||||
.finish()
|
||||
}
|
||||
|
||||
fn render_panel_header(
|
||||
agent_name: &str,
|
||||
status: &ConversationStatus,
|
||||
state: &SubagentPanelState,
|
||||
_background: ColorU,
|
||||
app: &AppContext,
|
||||
) -> Box<dyn Element> {
|
||||
let appearance = Appearance::as_ref(app);
|
||||
let theme = appearance.theme();
|
||||
let font_family = appearance.ui_font_family();
|
||||
let font_size = appearance.monospace_font_size();
|
||||
let surface = theme.surface_2();
|
||||
|
||||
let mut header_row = Flex::row()
|
||||
.with_main_axis_alignment(MainAxisAlignment::SpaceBetween)
|
||||
.with_main_axis_size(MainAxisSize::Max)
|
||||
.with_cross_axis_alignment(CrossAxisAlignment::Center);
|
||||
|
||||
// Left: status icon + agent name + status text
|
||||
let mut left_side = Flex::row().with_cross_axis_alignment(CrossAxisAlignment::Center);
|
||||
|
||||
let (icon, icon_color) = status.status_icon_and_color(theme);
|
||||
let status_icon_element = ConstrainedBox::new(
|
||||
galaxyui::elements::Icon::new(icon.into(), icon_color).finish(),
|
||||
)
|
||||
.with_width(icon_size(app))
|
||||
.with_height(icon_size(app))
|
||||
.finish();
|
||||
|
||||
left_side.add_child(
|
||||
Container::new(status_icon_element)
|
||||
.with_margin_right(ICON_MARGIN)
|
||||
.finish(),
|
||||
);
|
||||
|
||||
let name_color: ColorU = theme.main_text_color(surface).into();
|
||||
left_side.add_child(
|
||||
Text::new_inline(agent_name.to_string(), font_family, font_size)
|
||||
.with_color(name_color)
|
||||
.finish(),
|
||||
);
|
||||
|
||||
let status_text = match status {
|
||||
ConversationStatus::InProgress => "Working...",
|
||||
ConversationStatus::Success => "Complete",
|
||||
ConversationStatus::Error => "Error",
|
||||
ConversationStatus::Cancelled => "Cancelled",
|
||||
ConversationStatus::Blocked { .. } => "Blocked",
|
||||
};
|
||||
let status_text_color = blended_colors::text_disabled(theme, surface);
|
||||
left_side.add_child(
|
||||
Container::new(
|
||||
Text::new_inline(status_text.to_string(), font_family, font_size)
|
||||
.with_color(status_text_color)
|
||||
.finish(),
|
||||
)
|
||||
.with_margin_left(8.)
|
||||
.finish(),
|
||||
);
|
||||
|
||||
header_row.add_child(Shrinkable::new(1.0, left_side.finish()).finish());
|
||||
|
||||
// Right: collapse/expand chevron
|
||||
let mut right_side = Flex::row().with_cross_axis_alignment(CrossAxisAlignment::Center);
|
||||
|
||||
let chevron_icon = if state.is_expanded {
|
||||
Icon::ChevronDown
|
||||
} else {
|
||||
Icon::ChevronRight
|
||||
};
|
||||
let chevron_color = blended_colors::text_disabled(theme, surface);
|
||||
let chevron = ConstrainedBox::new(
|
||||
galaxyui::elements::Icon::new(chevron_icon.into(), chevron_color).finish(),
|
||||
)
|
||||
.with_width(icon_size(app))
|
||||
.with_height(icon_size(app))
|
||||
.finish();
|
||||
right_side.add_child(
|
||||
Container::new(chevron)
|
||||
.with_margin_right(4.)
|
||||
.finish(),
|
||||
);
|
||||
|
||||
header_row.add_child(right_side.finish());
|
||||
|
||||
Container::new(header_row.finish())
|
||||
.with_padding_left(INLINE_ACTION_HORIZONTAL_PADDING)
|
||||
.with_padding_right(INLINE_ACTION_HORIZONTAL_PADDING)
|
||||
.with_padding_top(INLINE_ACTION_HEADER_VERTICAL_PADDING)
|
||||
.with_padding_bottom(INLINE_ACTION_HEADER_VERTICAL_PADDING)
|
||||
.finish()
|
||||
}
|
||||
|
||||
fn collect_mini_transcript(
|
||||
conversation_id: &AIConversationId,
|
||||
app: &AppContext,
|
||||
) -> Vec<String> {
|
||||
let history_model = BlocklistAIHistoryModel::as_ref(app);
|
||||
let Some(conversation) = history_model.conversation(conversation_id) else {
|
||||
return vec![];
|
||||
};
|
||||
|
||||
let mut lines = Vec::new();
|
||||
let messages = conversation.all_linearized_messages();
|
||||
for msg in messages.iter().rev().take(MINI_TRANSCRIPT_MAX_LINES * 2) {
|
||||
if let Some(text) = extract_message_text(msg) {
|
||||
let truncated = if text.len() > 120 {
|
||||
format!("{}...", &text[..117])
|
||||
} else {
|
||||
text
|
||||
};
|
||||
lines.push(truncated);
|
||||
if lines.len() >= MINI_TRANSCRIPT_MAX_LINES {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
lines.reverse();
|
||||
lines
|
||||
}
|
||||
|
||||
fn extract_message_text(msg: &api::Message) -> Option<String> {
|
||||
let message_content = msg.message.as_ref()?;
|
||||
match message_content {
|
||||
api::message::Message::AgentOutput(output) => {
|
||||
if output.text.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(output.text.clone())
|
||||
}
|
||||
}
|
||||
api::message::Message::UserQuery(query) => {
|
||||
if query.query.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(query.query.clone())
|
||||
}
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn render_mini_transcript(
|
||||
lines: &[String],
|
||||
background: ColorU,
|
||||
app: &AppContext,
|
||||
) -> Box<dyn Element> {
|
||||
let appearance = Appearance::as_ref(app);
|
||||
let theme = appearance.theme();
|
||||
let text_color = blended_colors::text_disabled(theme, background);
|
||||
let font_family = appearance.ui_font_family();
|
||||
let font_size = appearance.monospace_font_size() - 1.;
|
||||
|
||||
let mut column = Flex::column();
|
||||
for line in lines {
|
||||
let prefixed = format!("> {line}");
|
||||
column.add_child(
|
||||
Text::new_inline(prefixed, font_family, font_size)
|
||||
.with_color(text_color)
|
||||
.finish(),
|
||||
);
|
||||
}
|
||||
|
||||
ConstrainedBox::new(
|
||||
Container::new(column.finish())
|
||||
.with_padding_left(INLINE_ACTION_HORIZONTAL_PADDING)
|
||||
.with_padding_right(INLINE_ACTION_HORIZONTAL_PADDING)
|
||||
.with_padding_top(4.)
|
||||
.with_padding_bottom(4.)
|
||||
.finish(),
|
||||
)
|
||||
.with_max_height(PANEL_MAX_HEIGHT)
|
||||
.finish()
|
||||
}
|
||||
|
||||
fn get_completion_summary(
|
||||
conversation_id: &AIConversationId,
|
||||
app: &AppContext,
|
||||
) -> Option<String> {
|
||||
let history_model = BlocklistAIHistoryModel::as_ref(app);
|
||||
let conversation = history_model.conversation(conversation_id)?;
|
||||
|
||||
if !conversation.status().is_done() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let messages = conversation.all_linearized_messages();
|
||||
for msg in messages.iter().rev() {
|
||||
if let Some(text) = extract_message_text(msg) {
|
||||
if !text.is_empty() {
|
||||
let truncated = if text.len() > 300 {
|
||||
format!("{}...", &text[..297])
|
||||
} else {
|
||||
text
|
||||
};
|
||||
return Some(truncated);
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn render_summary_footer(
|
||||
summary: &str,
|
||||
_background: ColorU,
|
||||
app: &AppContext,
|
||||
) -> Box<dyn Element> {
|
||||
let appearance = Appearance::as_ref(app);
|
||||
let theme = appearance.theme();
|
||||
let surface = theme.surface_2();
|
||||
let text_color: ColorU = theme.main_text_color(surface).into();
|
||||
let label_color = blended_colors::text_disabled(theme, surface);
|
||||
let font_family = appearance.ui_font_family();
|
||||
let font_size = appearance.monospace_font_size();
|
||||
|
||||
let mut row = Flex::row().with_cross_axis_alignment(CrossAxisAlignment::Start);
|
||||
row.add_child(
|
||||
Text::new_inline("Summary: ".to_string(), font_family, font_size)
|
||||
.with_color(label_color)
|
||||
.finish(),
|
||||
);
|
||||
row.add_child(
|
||||
Shrinkable::new(
|
||||
1.0,
|
||||
Text::new_inline(summary.to_string(), font_family, font_size)
|
||||
.with_color(text_color)
|
||||
.finish(),
|
||||
)
|
||||
.finish(),
|
||||
);
|
||||
|
||||
Container::new(row.finish())
|
||||
.with_padding_left(INLINE_ACTION_HORIZONTAL_PADDING)
|
||||
.with_padding_right(INLINE_ACTION_HORIZONTAL_PADDING)
|
||||
.with_padding_top(6.)
|
||||
.with_padding_bottom(6.)
|
||||
.finish()
|
||||
}
|
||||
Reference in New Issue
Block a user