From 80a950c0227b1347f231a5eb2a13f1da4cc51e71 Mon Sep 17 00:00:00 2001 From: harryalbert Date: Thu, 4 Jun 2026 20:30:30 +0000 Subject: [PATCH 1/4] Add orchestration message display setting Co-Authored-By: Oz --- app/src/ai/agent/mod.rs | 1 + app/src/ai/agent/util.rs | 2 +- app/src/ai/blocklist/block.rs | 44 ++++++- .../block/view_impl/orchestration.rs | 83 +++++++++---- app/src/ai/blocklist/block_tests.rs | 63 ++++++++-- app/src/settings/ai.rs | 77 ++++++++++++ app/src/settings_view/ai_page.rs | 113 +++++++++++++++++- app/src/settings_view/mod.rs | 6 + app/src/workspace/view.rs | 18 +++ 9 files changed, 360 insertions(+), 47 deletions(-) diff --git a/app/src/ai/agent/mod.rs b/app/src/ai/agent/mod.rs index e0f5438849..c348e13956 100644 --- a/app/src/ai/agent/mod.rs +++ b/app/src/ai/agent/mod.rs @@ -39,6 +39,7 @@ use warp_editor::render::model::LineCount; use warp_multi_agent_api::{diff_hunk as diff_hunk_api, AgentEvent, AgentType}; pub use self::api::{MaybeAIAgentOutputMessage, MessageToAIAgentOutputMessageError}; +pub(crate) use self::util::parse_markdown_into_text_and_code_sections; use super::llms::LLMId; use crate::ai::block_context::BlockContext; use crate::ai::blocklist::block::view_impl::output::are_all_text_sections_empty; diff --git a/app/src/ai/agent/util.rs b/app/src/ai/agent/util.rs index a2493a8447..4d80559232 100644 --- a/app/src/ai/agent/util.rs +++ b/app/src/ai/agent/util.rs @@ -32,7 +32,7 @@ lazy_static! { } /// Converts the given `markdown_text` into corresponding `Text` and `Code` `AIAgentOutputStep`s. -pub(super) fn parse_markdown_into_text_and_code_sections( +pub(crate) fn parse_markdown_into_text_and_code_sections( markdown_text: &str, ) -> Vec { let mut sections = vec![]; diff --git a/app/src/ai/blocklist/block.rs b/app/src/ai/blocklist/block.rs index 4792a64709..8fa035dff3 100644 --- a/app/src/ai/blocklist/block.rs +++ b/app/src/ai/blocklist/block.rs @@ -171,6 +171,7 @@ use crate::server::telemetry::{ use crate::settings::{ AISettings, AISettingsChangedEvent, AgentModeCodingPermissionsType, FontSettings, InputModeSettings, InputModeSettingsChangedEvent, InputSettings, + OrchestrationMessageDisplayMode, }; use crate::settings_view::SettingsSection; use crate::terminal::find::TerminalFindModel; @@ -852,14 +853,35 @@ pub(crate) fn received_message_collapsible_id(message_id: &str) -> MessageId { fn default_collapsible_state_for_orchestration_action( action: &AIAgentActionType, + display_mode: OrchestrationMessageDisplayMode, ) -> Option { match action { - AIAgentActionType::StartAgent { .. } => Some(CollapsibleElementState::default()), - AIAgentActionType::SendMessageToAgent { .. } => Some(CollapsibleElementState::collapsed()), + AIAgentActionType::StartAgent { .. } => Some(default_orchestration_collapsible_state( + display_mode.should_expand_start_agent_prompt(), + )), + AIAgentActionType::SendMessageToAgent { .. } => { + Some(default_orchestration_collapsible_state( + display_mode.should_expand_agent_message_body(), + )) + } _ => None, } } +fn default_collapsible_state_for_orchestration_message( + display_mode: OrchestrationMessageDisplayMode, +) -> CollapsibleElementState { + default_orchestration_collapsible_state(display_mode.should_expand_agent_message_body()) +} + +fn default_orchestration_collapsible_state(expanded: bool) -> CollapsibleElementState { + if expanded { + CollapsibleElementState::default() + } else { + CollapsibleElementState::collapsed() + } +} + pub struct AIBlock { model: Rc>, terminal_model: Arc>, @@ -1133,6 +1155,10 @@ impl AIBlock { } ctx.notify(); } + AISettingsChangedEvent::ThinkingDisplayMode { .. } + | AISettingsChangedEvent::OrchestrationMessageDisplayMode { .. } => { + ctx.notify(); + } _ => {} }, ); @@ -2148,10 +2174,14 @@ impl AIBlock { } // Register collapsible state for orchestration action messages. + let orchestration_message_display_mode = + AISettings::as_ref(ctx).orchestration_message_display_mode; match &message.message { AIAgentOutputMessageType::Action(AIAgentAction { action, .. }) => { - if let Some(state) = default_collapsible_state_for_orchestration_action(action) - { + if let Some(state) = default_collapsible_state_for_orchestration_action( + action, + orchestration_message_display_mode, + ) { self.collapsible_block_states .entry(message.id.clone()) .or_insert(state); @@ -2163,7 +2193,11 @@ impl AIBlock { received_message_collapsible_id(&received_message.message_id); self.collapsible_block_states .entry(collapsible_id.clone()) - .or_insert_with(CollapsibleElementState::collapsed); + .or_insert_with(|| { + default_collapsible_state_for_orchestration_message( + orchestration_message_display_mode, + ) + }); self.state_handles .transcript_avatar_handles .entry(collapsible_id) diff --git a/app/src/ai/blocklist/block/view_impl/orchestration.rs b/app/src/ai/blocklist/block/view_impl/orchestration.rs index e1deeaa294..547b32c1d5 100644 --- a/app/src/ai/blocklist/block/view_impl/orchestration.rs +++ b/app/src/ai/blocklist/block/view_impl/orchestration.rs @@ -9,12 +9,15 @@ use warpui::elements::{ use warpui::platform::Cursor; use warpui::{AppContext, Element, SingletonEntity}; -use super::common::render_scrollable_collapsible_content; +use super::common::{ + render_scrollable_collapsible_content, render_text_sections, TextSectionsProps, +}; use super::output::{action_icon, Props}; use super::WithContentItemSpacing; use crate::ai::agent::conversation::{ AIConversation, AIConversationId, ConversationStatus, StatusColorStyle, }; +use crate::ai::agent::parse_markdown_into_text_and_code_sections; use crate::ai::agent::{ AIAgentActionId, AIAgentActionResultType, MessageId, ReceivedMessageDisplay, SendMessageToAgentResult, StartAgentExecutionMode, StartAgentResult, @@ -26,6 +29,7 @@ use crate::ai::blocklist::agent_view::orchestration_conversation_links::{ dispatch_focus_or_open_child_agent_pane, }; use crate::ai::blocklist::block::model::AIBlockModelHelper; +use crate::ai::blocklist::block::secret_redaction::SecretRedactionState; use crate::ai::blocklist::block::{ received_message_collapsible_id, AIBlockAction, CollapsibleExpansionState, }; @@ -38,6 +42,7 @@ use crate::ai::blocklist::inline_action::requested_action::{ }; use crate::ai::blocklist::BlocklistAIHistoryModel; use crate::appearance::Appearance; +use crate::code::editor_management::CodeSource; use crate::terminal::view::TerminalAction; use crate::ui_components::blended_colors; use crate::ui_components::icons::Icon; @@ -281,14 +286,8 @@ fn render_transcript_row( ); } if !data.body.is_empty() { - let body_element = Container::new( - Text::new(data.body.to_string(), font_family, font_size) - .with_color(body_color) - .with_selectable(true) - .finish(), - ) - .with_margin_top(8.) - .finish(); + let body_element = + render_collapsible_markdown_body(data.body, body_color, false, props, app); if let Some(body) = render_collapsible_body(data.message_id, body_element, data.is_streaming, props) { @@ -534,7 +533,8 @@ pub(super) fn render_send_message( } else { blended_colors::text_disabled(theme, theme.surface_2()) }; - let message_element = render_collapsible_text_body(message, message_color, true, app); + let message_element = + render_collapsible_markdown_body(message, message_color, true, props, app); if let Some(body) = render_collapsible_body( message_id, message_element, @@ -621,10 +621,11 @@ pub(super) fn render_start_agent( )); if has_prompt { - let prompt_element = render_collapsible_text_body( + let prompt_element = render_collapsible_markdown_body( prompt, blended_colors::text_disabled(theme, theme.surface_2()), true, + props, app, ); if let Some(body) = render_collapsible_body(message_id, prompt_element, false, props) { @@ -706,7 +707,8 @@ pub(super) fn render_start_agent( } else { blended_colors::text_disabled(theme, theme.surface_2()) }; - let prompt_element = render_collapsible_text_body(prompt, prompt_color, true, app); + let prompt_element = + render_collapsible_markdown_body(prompt, prompt_color, true, props, app); if let Some(body) = render_collapsible_body( message_id, prompt_element, @@ -753,26 +755,55 @@ fn start_agent_in_progress_prefix(execution_mode: &StartAgentExecutionMode) -> & } } -/// Renders a selectable text block below an orchestration action header, using a muted color. +/// Renders a selectable markdown block below an orchestration action header, using a muted color. /// Used for both StartAgent prompts and SendMessageToAgent message bodies. -fn render_collapsible_text_body( +fn render_collapsible_markdown_body( text: &str, text_color: ColorU, align_with_status_row_text: bool, + props: Props, app: &AppContext, ) -> Box { - let appearance = Appearance::as_ref(app); - let mut container = Container::new( - Text::new( - text.to_string(), - appearance.ui_font_family(), - appearance.monospace_font_size(), - ) - .with_color(text_color) - .with_selectable(true) - .finish(), - ) - .with_margin_top(4.); + let sections = parse_markdown_into_text_and_code_sections(text); + let empty_secret_redaction_state = SecretRedactionState::default(); + let mut text_section_index = 0; + let mut code_section_index = 0; + let mut table_section_index = 0; + let mut image_section_index = 0; + let body = render_text_sections( + TextSectionsProps { + model: props.model, + starting_text_section_index: &mut text_section_index, + starting_code_section_index: &mut code_section_index, + starting_table_section_index: &mut table_section_index, + starting_image_section_index: &mut image_section_index, + sections: §ions, + text_color, + selectable: true, + find_context: None, + current_working_directory: props.current_working_directory, + shell_launch_data: props.shell_launch_data, + embedded_code_editor_views: &[], + code_snippet_button_handles: &[], + table_section_handles: &[], + image_section_tooltip_handles: &[], + is_ai_input_enabled: props.is_ai_input_enabled, + open_code_block_action_factory: (None as Option< + &'static dyn Fn(CodeSource) -> AIBlockAction, + >), + copy_code_action_factory: (None as Option<&'static dyn Fn(String) -> AIBlockAction>), + detected_links: None, + secret_redaction_state: &empty_secret_redaction_state, + is_selecting_text: props.is_selecting_text, + item_spacing: 6., + #[cfg(feature = "local_fs")] + resolved_code_block_paths: Some(props.resolved_code_block_paths), + #[cfg(feature = "local_fs")] + resolved_blocklist_image_sources: Some(props.resolved_blocklist_image_sources), + }, + app, + ); + let mut container = Container::new(body).with_margin_top(4.); if align_with_status_row_text { container = container diff --git a/app/src/ai/blocklist/block_tests.rs b/app/src/ai/blocklist/block_tests.rs index 7227c00b74..4b9074547d 100644 --- a/app/src/ai/blocklist/block_tests.rs +++ b/app/src/ai/blocklist/block_tests.rs @@ -17,7 +17,7 @@ use crate::ai::blocklist::action_model::{ compose_run_agents_child_prompt, run_agents_to_start_agent_mode, }; use crate::auth::UserUid; -use crate::settings::AISettings; +use crate::settings::{AISettings, OrchestrationMessageDisplayMode}; use crate::test_util::settings::initialize_settings_for_tests; use crate::workspaces::user_profiles::{UserProfileWithUID, UserProfiles}; @@ -55,6 +55,7 @@ fn orchestration_send_message_starts_collapsed() { subject: "Status".to_string(), message: "Body".to_string(), }, + OrchestrationMessageDisplayMode::CurrentBehavior, ) .expect("send-message actions should get a collapsible state"); @@ -66,15 +67,17 @@ fn orchestration_send_message_starts_collapsed() { #[test] fn orchestration_start_agent_keeps_expanded_default() { - let state = - default_collapsible_state_for_orchestration_action(&AIAgentActionType::StartAgent { + let state = default_collapsible_state_for_orchestration_action( + &AIAgentActionType::StartAgent { version: StartAgentVersion::V1, name: "child-agent".to_string(), prompt: "Investigate".to_string(), execution_mode: StartAgentExecutionMode::local_harness("claude-code".to_string()), lifecycle_subscription: None, - }) - .expect("start-agent actions should get a collapsible state"); + }, + OrchestrationMessageDisplayMode::CurrentBehavior, + ) + .expect("start-agent actions should get a collapsible state"); assert!(matches!( state.expansion_state, @@ -87,10 +90,52 @@ fn orchestration_start_agent_keeps_expanded_default() { #[test] fn non_orchestration_actions_do_not_get_collapsible_state_defaults() { - assert!( - default_collapsible_state_for_orchestration_action(&AIAgentActionType::OpenCodeReview) - .is_none() - ); + assert!(default_collapsible_state_for_orchestration_action( + &AIAgentActionType::OpenCodeReview, + OrchestrationMessageDisplayMode::CurrentBehavior, + ) + .is_none()); +} + +#[test] +fn orchestration_always_show_starts_messages_expanded() { + let state = default_collapsible_state_for_orchestration_action( + &AIAgentActionType::SendMessageToAgent { + addresses: vec!["child-agent".to_string()], + subject: "Status".to_string(), + message: "Body".to_string(), + }, + OrchestrationMessageDisplayMode::AlwaysShow, + ) + .expect("send-message actions should get a collapsible state"); + + assert!(matches!( + state.expansion_state, + CollapsibleExpansionState::Expanded { + is_finished: false, + scroll_pinned_to_bottom: true + } + )); +} + +#[test] +fn orchestration_always_collapse_starts_agent_prompt_collapsed() { + let state = default_collapsible_state_for_orchestration_action( + &AIAgentActionType::StartAgent { + version: StartAgentVersion::V1, + name: "child-agent".to_string(), + prompt: "Investigate".to_string(), + execution_mode: StartAgentExecutionMode::local_harness("claude-code".to_string()), + lifecycle_subscription: None, + }, + OrchestrationMessageDisplayMode::AlwaysCollapse, + ) + .expect("start-agent actions should get a collapsible state"); + + assert!(matches!( + state.expansion_state, + CollapsibleExpansionState::Collapsed + )); } #[test] diff --git a/app/src/settings/ai.rs b/app/src/settings/ai.rs index b6c0383d73..e3f820d0c0 100644 --- a/app/src/settings/ai.rs +++ b/app/src/settings/ai.rs @@ -395,6 +395,80 @@ impl ThinkingDisplayMode { } } +/// Controls how orchestration message bodies are expanded by default. +#[derive( + Default, + Debug, + serde::Serialize, + serde::Deserialize, + PartialEq, + Copy, + Clone, + EnumIter, + schemars::JsonSchema, + settings_value::SettingsValue, +)] +#[schemars( + description = "Controls how orchestration messages are expanded by default.", + rename_all = "snake_case" +)] +pub enum OrchestrationMessageDisplayMode { + /// Preserve the existing behavior: start-agent prompts expand, messages collapse. + #[default] + CurrentBehavior, + /// Start all orchestration message bodies expanded. + AlwaysShow, + /// Start all orchestration message bodies collapsed. + AlwaysCollapse, +} + +settings::macros::implement_setting_for_enum!( + OrchestrationMessageDisplayMode, + AISettings, + SupportedPlatforms::ALL, + SyncToCloud::Globally(RespectUserSyncSetting::Yes), + private: false, + toml_path: "agents.warp_agent.other.orchestration_message_display_mode", + description: "Controls how orchestration message bodies are expanded by default.", +); + +impl OrchestrationMessageDisplayMode { + /// Display name for the settings dropdown. + pub fn display_name(&self) -> &'static str { + match self { + OrchestrationMessageDisplayMode::CurrentBehavior => "Current behavior", + OrchestrationMessageDisplayMode::AlwaysShow => "Always expand", + OrchestrationMessageDisplayMode::AlwaysCollapse => "Always collapse", + } + } + + pub fn command_palette_description(&self) -> &'static str { + match self { + OrchestrationMessageDisplayMode::CurrentBehavior => { + "Set orchestration message display: current behavior" + } + OrchestrationMessageDisplayMode::AlwaysShow => { + "Set orchestration message display: always expand" + } + OrchestrationMessageDisplayMode::AlwaysCollapse => { + "Set orchestration message display: always collapse" + } + } + } + + pub fn should_expand_start_agent_prompt(&self) -> bool { + matches!( + self, + OrchestrationMessageDisplayMode::CurrentBehavior + | OrchestrationMessageDisplayMode::AlwaysShow + ) + } + + pub fn should_expand_agent_message_body(&self) -> bool { + matches!(self, OrchestrationMessageDisplayMode::AlwaysShow) + } +} + /// Controls what happens when a user submits a new prompt while the agent is /// still responding to an earlier prompt. /// @@ -1322,6 +1396,9 @@ define_settings_group!(AISettings, settings: [ // Controls how agent thinking/reasoning traces are displayed. thinking_display_mode: ThinkingDisplayMode, + // Controls how orchestration message bodies are expanded by default. + orchestration_message_display_mode: OrchestrationMessageDisplayMode, + // Default behavior when the user submits a new prompt while the agent is still // responding. Per-conversation overrides live on `QueuedQueryModel`; this // setting is the fallback used when a conversation has no explicit override. diff --git a/app/src/settings_view/ai_page.rs b/app/src/settings_view/ai_page.rs index 4822171d96..1e2c1aab65 100644 --- a/app/src/settings_view/ai_page.rs +++ b/app/src/settings_view/ai_page.rs @@ -82,11 +82,11 @@ use crate::settings::{ AwsBedrockCredentialsEnabled, CanUseWarpCreditsForFallback, CodeSettings, CodebaseContextEnabled, FileBasedMcpEnabled, GitOperationsAutogenEnabled, IncludeAgentCommandsInHistory, InputSettings, IntelligentAutosuggestionsEnabled, MemoryEnabled, - NLDInTerminalEnabled, NaturalLanguageAutosuggestionsEnabled, PromptSubmissionMode, - RuleSuggestionsEnabled, SharedBlockTitleGenerationEnabled, ShouldRenderCLIAgentToolbar, - ShouldRenderUseAgentToolbarForUserCommands, ShouldShowOzUpdatesInZeroState, ShowAgentTips, - ShowConversationHistory, ShowHintText, ThinkingDisplayMode, VoiceInputEnabled, - WarpDriveContextEnabled, + NLDInTerminalEnabled, NaturalLanguageAutosuggestionsEnabled, OrchestrationMessageDisplayMode, + PromptSubmissionMode, RuleSuggestionsEnabled, SharedBlockTitleGenerationEnabled, + ShouldRenderCLIAgentToolbar, ShouldRenderUseAgentToolbarForUserCommands, + ShouldShowOzUpdatesInZeroState, ShowAgentTips, ShowConversationHistory, ShowHintText, + ThinkingDisplayMode, VoiceInputEnabled, WarpDriveContextEnabled, }; use crate::terminal::session_settings::{SessionSettings, SessionSettingsChangedEvent}; use crate::terminal::CLIAgent; @@ -335,6 +335,35 @@ pub fn init_actions_from_parent_view( .collect(); app.register_fixed_bindings(mode_bindings); } + { + use warpui::keymap::FixedBinding; + + let ai_context = context.clone() & id!(flags::IS_ANY_AI_ENABLED); + let mode_bindings: Vec = OrchestrationMessageDisplayMode::iter() + .map(|mode| { + let context_flag = match mode { + OrchestrationMessageDisplayMode::CurrentBehavior => { + flags::ORCHESTRATION_MESSAGE_DISPLAY_CURRENT_BEHAVIOR + } + OrchestrationMessageDisplayMode::AlwaysShow => { + flags::ORCHESTRATION_MESSAGE_DISPLAY_ALWAYS_SHOW + } + OrchestrationMessageDisplayMode::AlwaysCollapse => { + flags::ORCHESTRATION_MESSAGE_DISPLAY_ALWAYS_COLLAPSE + } + }; + FixedBinding::empty( + mode.command_palette_description(), + builder(SettingsAction::AI( + AISettingsPageAction::SetOrchestrationMessageDisplayMode(mode), + )), + ai_context.clone() & !id!(context_flag), + ) + .with_group(bindings::BindingGroup::WarpAi.as_str()) + }) + .collect(); + app.register_fixed_bindings(mode_bindings); + } if FeatureFlag::QueueSlashCommand.is_enabled() { use warpui::keymap::FixedBinding; @@ -641,6 +670,7 @@ pub struct AISettingsPageView { dragged_context_window_value: Option, thinking_display_mode_dropdown: ViewHandle>, + orchestration_message_display_mode_dropdown: ViewHandle>, default_prompt_submission_mode_dropdown: ViewHandle>, #[cfg(feature = "local_fs")] conversation_layout_dropdown: ViewHandle>, @@ -786,6 +816,17 @@ impl AISettingsPageView { ); }); } + let orchestration_message_display_mode_dropdown = + OtherAIWidget::create_orchestration_message_display_mode_dropdown(ctx); + { + let current_mode = AISettings::as_ref(ctx).orchestration_message_display_mode; + orchestration_message_display_mode_dropdown.update(ctx, |dropdown, ctx| { + dropdown.set_selected_by_action( + AISettingsPageAction::SetOrchestrationMessageDisplayMode(current_mode), + ctx, + ); + }); + } let default_prompt_submission_mode_dropdown = OtherAIWidget::create_default_prompt_submission_mode_dropdown(ctx); @@ -1202,6 +1243,18 @@ impl AISettingsPageView { ); }); } + AISettingsChangedEvent::OrchestrationMessageDisplayMode { .. } => { + let current_mode = AISettings::as_ref(ctx).orchestration_message_display_mode; + me.orchestration_message_display_mode_dropdown + .update(ctx, |dropdown, ctx| { + dropdown.set_selected_by_action( + AISettingsPageAction::SetOrchestrationMessageDisplayMode( + current_mode, + ), + ctx, + ); + }); + } AISettingsChangedEvent::PromptSubmissionMode { .. } => { let current_mode = AISettings::as_ref(ctx).default_prompt_submission_mode; me.default_prompt_submission_mode_dropdown @@ -1734,6 +1787,7 @@ impl AISettingsPageView { mcp_denylist_dropdown, mcp_denylist_mouse_state_handles, thinking_display_mode_dropdown, + orchestration_message_display_mode_dropdown, default_prompt_submission_mode_dropdown, #[cfg(feature = "local_fs")] conversation_layout_dropdown, @@ -2836,6 +2890,7 @@ pub enum AISettingsPageAction { ToggleShowAgentTips, ToggleShowOzUpdatesInZeroState, SetThinkingDisplayMode(ThinkingDisplayMode), + SetOrchestrationMessageDisplayMode(OrchestrationMessageDisplayMode), SetPromptSubmissionMode(PromptSubmissionMode), AttemptLoginGatedUpgrade, RemoveCLIAgentToolbarEnabledCommand(String), @@ -3295,6 +3350,14 @@ impl TypedActionView for AISettingsPageView { }); ctx.notify(); } + AISettingsPageAction::SetOrchestrationMessageDisplayMode(mode) => { + AISettings::handle(ctx).update(ctx, |settings, ctx| { + report_if_error!(settings + .orchestration_message_display_mode + .set_value(*mode, ctx)); + }); + ctx.notify(); + } AISettingsPageAction::SetPromptSubmissionMode(mode) => { AISettings::handle(ctx).update(ctx, |settings, ctx| { report_if_error!(settings @@ -6367,13 +6430,36 @@ impl OtherAIWidget { dropdown }) } + + fn create_orchestration_message_display_mode_dropdown( + ctx: &mut ViewContext, + ) -> ViewHandle> { + let items: Vec> = + OrchestrationMessageDisplayMode::iter() + .map(|mode| { + DropdownItem::new( + mode.display_name(), + AISettingsPageAction::SetOrchestrationMessageDisplayMode(mode), + ) + }) + .collect(); + + ctx.add_typed_action_view(|ctx| { + let mut dropdown = Dropdown::new(ctx); + dropdown.set_top_bar_max_width(AI_SETTINGS_DROPDOWN_WIDTH); + dropdown.set_menu_width(AI_SETTINGS_DROPDOWN_WIDTH, ctx); + dropdown.set_menu_max_height(AI_SETTINGS_DROPDOWN_MAX_HEIGHT, ctx); + dropdown.add_items(items, ctx); + dropdown + }) + } } impl SettingsWidget for OtherAIWidget { type View = AISettingsPageView; fn search_terms(&self) -> &str { - "other oz updates zero state empty changelog new conversation agent what's new use agent footer toolbar layout chip chips rearrange re-arrange thinking expanded reasoning collapse never show hide conversation history" + "other oz updates zero state empty changelog new conversation agent what's new use agent footer toolbar layout chip chips rearrange re-arrange thinking expanded reasoning collapse never show orchestration messages child agents collapse expand hide conversation history" } fn render( @@ -6459,6 +6545,21 @@ impl SettingsWidget for OtherAIWidget { &view.thinking_display_mode_dropdown, )); + column.add_child(render_dropdown_item( + appearance, + "Orchestration message display", + Some("Controls whether child-agent prompts and messages start expanded."), + None, + LocalOnlyIconState::for_setting( + OrchestrationMessageDisplayMode::storage_key(), + OrchestrationMessageDisplayMode::sync_to_cloud(), + &mut view.local_only_icon_tooltip_states.borrow_mut(), + app, + ), + (!is_any_ai_enabled).then(|| appearance.theme().disabled_ui_text_color()), + &view.orchestration_message_display_mode_dropdown, + )); + // TODO: OpenConversationLayoutPreference should not depend on local_fs, but it lives under the external editor settings // which does require local_fs. It was a mistake to put it there, but now we keep it there for backward compatibility. #[cfg(feature = "local_fs")] diff --git a/app/src/settings_view/mod.rs b/app/src/settings_view/mod.rs index f3d7fec065..f9108ade12 100644 --- a/app/src/settings_view/mod.rs +++ b/app/src/settings_view/mod.rs @@ -507,6 +507,12 @@ pub mod flags { pub const THINKING_DISPLAY_SHOW_AND_COLLAPSE: &str = "Thinking_Display_ShowAndCollapse"; pub const THINKING_DISPLAY_ALWAYS_SHOW: &str = "Thinking_Display_AlwaysShow"; pub const THINKING_DISPLAY_NEVER_SHOW: &str = "Thinking_Display_NeverShow"; + pub const ORCHESTRATION_MESSAGE_DISPLAY_CURRENT_BEHAVIOR: &str = + "Orchestration_Message_Display_CurrentBehavior"; + pub const ORCHESTRATION_MESSAGE_DISPLAY_ALWAYS_SHOW: &str = + "Orchestration_Message_Display_AlwaysShow"; + pub const ORCHESTRATION_MESSAGE_DISPLAY_ALWAYS_COLLAPSE: &str = + "Orchestration_Message_Display_AlwaysCollapse"; pub const PROMPT_SUBMISSION_INTERRUPT: &str = "Prompt_Submission_Interrupt"; pub const PROMPT_SUBMISSION_QUEUE: &str = "Prompt_Submission_Queue"; pub const SHOW_TERMINAL_INPUT_MESSAGE_LINE_FLAG: &str = "Show_Terminal_Input_Message_Line"; diff --git a/app/src/workspace/view.rs b/app/src/workspace/view.rs index 59c9def458..32a3f4664b 100644 --- a/app/src/workspace/view.rs +++ b/app/src/workspace/view.rs @@ -21844,6 +21844,24 @@ impl Workspace { } } + match ai_settings.orchestration_message_display_mode { + crate::settings::OrchestrationMessageDisplayMode::CurrentBehavior => { + context + .set + .insert(flags::ORCHESTRATION_MESSAGE_DISPLAY_CURRENT_BEHAVIOR); + } + crate::settings::OrchestrationMessageDisplayMode::AlwaysShow => { + context + .set + .insert(flags::ORCHESTRATION_MESSAGE_DISPLAY_ALWAYS_SHOW); + } + crate::settings::OrchestrationMessageDisplayMode::AlwaysCollapse => { + context + .set + .insert(flags::ORCHESTRATION_MESSAGE_DISPLAY_ALWAYS_COLLAPSE); + } + } + match ai_settings.default_prompt_submission_mode { crate::settings::PromptSubmissionMode::Interrupt => { context.set.insert(flags::PROMPT_SUBMISSION_INTERRUPT); From 67c4e2387fabc53ecd924ab47d8e88ba4b6c3057 Mon Sep 17 00:00:00 2001 From: harryalbert Date: Fri, 5 Jun 2026 15:06:27 -0400 Subject: [PATCH 2/4] clean up options --- app/src/ai/blocklist/block.rs | 77 +++++++++++++++- app/src/ai/blocklist/block_tests.rs | 137 +++++++++++++++++++++------- app/src/settings/ai.rs | 38 ++++---- app/src/settings_view/ai_page.rs | 8 +- app/src/settings_view/mod.rs | 4 +- app/src/workspace/view.rs | 4 +- 6 files changed, 204 insertions(+), 64 deletions(-) diff --git a/app/src/ai/blocklist/block.rs b/app/src/ai/blocklist/block.rs index 8fa035dff3..792884b5b0 100644 --- a/app/src/ai/blocklist/block.rs +++ b/app/src/ai/blocklist/block.rs @@ -784,7 +784,7 @@ impl CollapsibleElementState { } fn finish_reasoning(&mut self, app: &AppContext) { - let should_auto_collapse = self.should_auto_collapse_reasoning_on_finish(); + let should_auto_collapse = self.should_auto_collapse_on_finish(); let thinking_mode = AISettings::as_ref(app).thinking_display_mode; self.sync_finished_state(true); @@ -816,6 +816,23 @@ impl CollapsibleElementState { } } + /// Applies child-agent message display behavior after streaming finishes. + fn finish_orchestration_message(&mut self, display_mode: OrchestrationMessageDisplayMode) { + let should_auto_collapse = self.should_auto_collapse_on_finish(); + + self.sync_finished_state(true); + + if display_mode.should_collapse_agent_message_body_on_finish() && should_auto_collapse { + self.expansion_state = CollapsibleExpansionState::Collapsed; + } else if let CollapsibleExpansionState::Expanded { + scroll_pinned_to_bottom, + .. + } = &mut self.expansion_state + { + *scroll_pinned_to_bottom = false; + } + } + fn toggle_expansion(&mut self) { if !self.last_known_is_finished { self.user_toggled_while_streaming = true; @@ -831,7 +848,7 @@ impl CollapsibleElementState { } } - fn should_auto_collapse_reasoning_on_finish(&self) -> bool { + fn should_auto_collapse_on_finish(&self) -> bool { !self.user_toggled_while_streaming && matches!( self.expansion_state, @@ -856,9 +873,7 @@ fn default_collapsible_state_for_orchestration_action( display_mode: OrchestrationMessageDisplayMode, ) -> Option { match action { - AIAgentActionType::StartAgent { .. } => Some(default_orchestration_collapsible_state( - display_mode.should_expand_start_agent_prompt(), - )), + AIAgentActionType::StartAgent { .. } => Some(CollapsibleElementState::default()), AIAgentActionType::SendMessageToAgent { .. } => { Some(default_orchestration_collapsible_state( display_mode.should_expand_agent_message_body(), @@ -2374,7 +2389,59 @@ impl AIBlock { self.keyboard_navigable_buttons = Some(menu); } + /// Applies final display behavior to child-agent message bodies. + fn finish_orchestration_message_collapsible_states( + &mut self, + output: &AIAgentOutput, + ctx: &mut ViewContext, + ) { + let display_mode = AISettings::as_ref(ctx).orchestration_message_display_mode; + for message in &output.messages { + match &message.message { + AIAgentOutputMessageType::Action(AIAgentAction { + action: AIAgentActionType::SendMessageToAgent { .. }, + .. + }) => { + self.collapsible_block_states + .entry(message.id.clone()) + .or_insert_with(|| { + default_orchestration_collapsible_state( + display_mode.should_expand_agent_message_body(), + ) + }) + .finish_orchestration_message(display_mode); + } + AIAgentOutputMessageType::MessagesReceivedFromAgents { messages } => { + for received_message in messages { + let collapsible_id = + received_message_collapsible_id(&received_message.message_id); + self.collapsible_block_states + .entry(collapsible_id) + .or_insert_with(|| { + default_collapsible_state_for_orchestration_message(display_mode) + }) + .finish_orchestration_message(display_mode); + } + } + AIAgentOutputMessageType::Text(_) + | AIAgentOutputMessageType::Reasoning { .. } + | AIAgentOutputMessageType::Summarization { .. } + | AIAgentOutputMessageType::Subagent(_) + | AIAgentOutputMessageType::Action(_) + | AIAgentOutputMessageType::TodoOperation(_) + | AIAgentOutputMessageType::WebSearch(_) + | AIAgentOutputMessageType::WebFetch(_) + | AIAgentOutputMessageType::CommentsAddressed { .. } + | AIAgentOutputMessageType::DebugOutput { .. } + | AIAgentOutputMessageType::ArtifactCreated(_) + | AIAgentOutputMessageType::SkillInvoked(_) + | AIAgentOutputMessageType::EventsFromAgents { .. } => {} + } + } + } + fn handle_complete_output(&mut self, output: &AIAgentOutput, ctx: &mut ViewContext) { + self.finish_orchestration_message_collapsible_states(output, ctx); let mut suggestions = BlocklistAIHistoryModel::as_ref(ctx) .existing_suggestions_for_conversation(self.client_ids.conversation_id) .cloned() diff --git a/app/src/ai/blocklist/block_tests.rs b/app/src/ai/blocklist/block_tests.rs index 4b9074547d..ac82212109 100644 --- a/app/src/ai/blocklist/block_tests.rs +++ b/app/src/ai/blocklist/block_tests.rs @@ -8,7 +8,8 @@ use warp_util::local_or_remote_path::LocalOrRemotePath; use warpui::{App, SingletonEntity}; use super::{ - default_collapsible_state_for_orchestration_action, received_message_collapsible_id, + default_collapsible_state_for_orchestration_action, + default_collapsible_state_for_orchestration_message, received_message_collapsible_id, user_avatar_info_for_conversation_creator, CollapsibleElementState, CollapsibleExpansionState, UserAvatarInfo, }; @@ -47,6 +48,37 @@ fn collapsed_initializer_starts_collapsed() { )); } +#[test] +fn orchestration_show_and_collapse_collapses_after_finish() { + let mut state = default_collapsible_state_for_orchestration_message( + OrchestrationMessageDisplayMode::ShowAndCollapse, + ); + + state.finish_orchestration_message(OrchestrationMessageDisplayMode::ShowAndCollapse); + + assert!(matches!( + state.expansion_state, + CollapsibleExpansionState::Collapsed + )); +} + +#[test] +fn orchestration_always_show_stays_expanded_after_finish() { + let mut state = default_collapsible_state_for_orchestration_message( + OrchestrationMessageDisplayMode::AlwaysShow, + ); + + state.finish_orchestration_message(OrchestrationMessageDisplayMode::AlwaysShow); + + assert!(matches!( + state.expansion_state, + CollapsibleExpansionState::Expanded { + is_finished: true, + scroll_pinned_to_bottom: false + } + )); +} + #[test] fn orchestration_send_message_starts_collapsed() { let state = default_collapsible_state_for_orchestration_action( @@ -55,7 +87,7 @@ fn orchestration_send_message_starts_collapsed() { subject: "Status".to_string(), message: "Body".to_string(), }, - OrchestrationMessageDisplayMode::CurrentBehavior, + OrchestrationMessageDisplayMode::AlwaysCollapse, ) .expect("send-message actions should get a collapsible state"); @@ -66,46 +98,52 @@ fn orchestration_send_message_starts_collapsed() { } #[test] -fn orchestration_start_agent_keeps_expanded_default() { - let state = default_collapsible_state_for_orchestration_action( - &AIAgentActionType::StartAgent { - version: StartAgentVersion::V1, - name: "child-agent".to_string(), - prompt: "Investigate".to_string(), - execution_mode: StartAgentExecutionMode::local_harness("claude-code".to_string()), - lifecycle_subscription: None, - }, - OrchestrationMessageDisplayMode::CurrentBehavior, - ) - .expect("start-agent actions should get a collapsible state"); +fn orchestration_start_agent_prompt_stays_expanded_for_all_message_modes() { + for display_mode in [ + OrchestrationMessageDisplayMode::ShowAndCollapse, + OrchestrationMessageDisplayMode::AlwaysCollapse, + OrchestrationMessageDisplayMode::AlwaysShow, + ] { + let state = default_collapsible_state_for_orchestration_action( + &AIAgentActionType::StartAgent { + version: StartAgentVersion::V1, + name: "child-agent".to_string(), + prompt: "Investigate".to_string(), + execution_mode: StartAgentExecutionMode::local_harness("claude-code".to_string()), + lifecycle_subscription: None, + }, + display_mode, + ) + .expect("start-agent actions should get a collapsible state"); - assert!(matches!( - state.expansion_state, - CollapsibleExpansionState::Expanded { - is_finished: false, - scroll_pinned_to_bottom: true - } - )); + assert!(matches!( + state.expansion_state, + CollapsibleExpansionState::Expanded { + is_finished: false, + scroll_pinned_to_bottom: true + } + )); + } } #[test] fn non_orchestration_actions_do_not_get_collapsible_state_defaults() { assert!(default_collapsible_state_for_orchestration_action( &AIAgentActionType::OpenCodeReview, - OrchestrationMessageDisplayMode::CurrentBehavior, + OrchestrationMessageDisplayMode::AlwaysCollapse, ) .is_none()); } #[test] -fn orchestration_always_show_starts_messages_expanded() { +fn orchestration_show_and_collapse_starts_sent_messages_expanded() { let state = default_collapsible_state_for_orchestration_action( &AIAgentActionType::SendMessageToAgent { addresses: vec!["child-agent".to_string()], subject: "Status".to_string(), message: "Body".to_string(), }, - OrchestrationMessageDisplayMode::AlwaysShow, + OrchestrationMessageDisplayMode::ShowAndCollapse, ) .expect("send-message actions should get a collapsible state"); @@ -119,23 +157,56 @@ fn orchestration_always_show_starts_messages_expanded() { } #[test] -fn orchestration_always_collapse_starts_agent_prompt_collapsed() { +fn orchestration_always_show_starts_sent_messages_expanded() { let state = default_collapsible_state_for_orchestration_action( - &AIAgentActionType::StartAgent { - version: StartAgentVersion::V1, - name: "child-agent".to_string(), - prompt: "Investigate".to_string(), - execution_mode: StartAgentExecutionMode::local_harness("claude-code".to_string()), - lifecycle_subscription: None, + &AIAgentActionType::SendMessageToAgent { + addresses: vec!["child-agent".to_string()], + subject: "Status".to_string(), + message: "Body".to_string(), }, - OrchestrationMessageDisplayMode::AlwaysCollapse, + OrchestrationMessageDisplayMode::AlwaysShow, ) - .expect("start-agent actions should get a collapsible state"); + .expect("send-message actions should get a collapsible state"); assert!(matches!( state.expansion_state, + CollapsibleExpansionState::Expanded { + is_finished: false, + scroll_pinned_to_bottom: true + } + )); +} + +#[test] +fn orchestration_received_messages_follow_initial_message_display_mode() { + let show_and_collapse = default_collapsible_state_for_orchestration_message( + OrchestrationMessageDisplayMode::ShowAndCollapse, + ); + assert!(matches!( + show_and_collapse.expansion_state, + CollapsibleExpansionState::Expanded { + is_finished: false, + scroll_pinned_to_bottom: true + } + )); + let collapsed = default_collapsible_state_for_orchestration_message( + OrchestrationMessageDisplayMode::AlwaysCollapse, + ); + assert!(matches!( + collapsed.expansion_state, CollapsibleExpansionState::Collapsed )); + let expanded = default_collapsible_state_for_orchestration_message( + OrchestrationMessageDisplayMode::AlwaysShow, + ); + + assert!(matches!( + expanded.expansion_state, + CollapsibleExpansionState::Expanded { + is_finished: false, + scroll_pinned_to_bottom: true + } + )); } #[test] diff --git a/app/src/settings/ai.rs b/app/src/settings/ai.rs index e3f820d0c0..67c9f4a752 100644 --- a/app/src/settings/ai.rs +++ b/app/src/settings/ai.rs @@ -395,7 +395,7 @@ impl ThinkingDisplayMode { } } -/// Controls how orchestration message bodies are expanded by default. +/// Controls how child-agent message bodies are displayed. #[derive( Default, Debug, @@ -409,16 +409,16 @@ impl ThinkingDisplayMode { settings_value::SettingsValue, )] #[schemars( - description = "Controls how orchestration messages are expanded by default.", + description = "Controls how child-agent messages are displayed.", rename_all = "snake_case" )] pub enum OrchestrationMessageDisplayMode { - /// Preserve the existing behavior: start-agent prompts expand, messages collapse. - #[default] - CurrentBehavior, - /// Start all orchestration message bodies expanded. + /// Show child-agent messages while streaming, then collapse them. + ShowAndCollapse, + /// Keep child-agent message bodies expanded. AlwaysShow, - /// Start all orchestration message bodies collapsed. + /// Keep child-agent message bodies collapsed. + #[default] AlwaysCollapse, } @@ -429,43 +429,45 @@ settings::macros::implement_setting_for_enum!( SyncToCloud::Globally(RespectUserSyncSetting::Yes), private: false, toml_path: "agents.warp_agent.other.orchestration_message_display_mode", - description: "Controls how orchestration message bodies are expanded by default.", + description: "Controls how child-agent messages are displayed.", ); impl OrchestrationMessageDisplayMode { /// Display name for the settings dropdown. pub fn display_name(&self) -> &'static str { match self { - OrchestrationMessageDisplayMode::CurrentBehavior => "Current behavior", - OrchestrationMessageDisplayMode::AlwaysShow => "Always expand", + OrchestrationMessageDisplayMode::ShowAndCollapse => "Show & collapse", + OrchestrationMessageDisplayMode::AlwaysShow => "Always show", OrchestrationMessageDisplayMode::AlwaysCollapse => "Always collapse", } } pub fn command_palette_description(&self) -> &'static str { match self { - OrchestrationMessageDisplayMode::CurrentBehavior => { - "Set orchestration message display: current behavior" + OrchestrationMessageDisplayMode::ShowAndCollapse => { + "Set child-agent message display: show & collapse" } OrchestrationMessageDisplayMode::AlwaysShow => { - "Set orchestration message display: always expand" + "Set child-agent message display: always show" } OrchestrationMessageDisplayMode::AlwaysCollapse => { - "Set orchestration message display: always collapse" + "Set child-agent message display: always collapse" } } } - pub fn should_expand_start_agent_prompt(&self) -> bool { + /// Whether child-agent message bodies should expand while streaming. + pub fn should_expand_agent_message_body(&self) -> bool { matches!( self, - OrchestrationMessageDisplayMode::CurrentBehavior + OrchestrationMessageDisplayMode::ShowAndCollapse | OrchestrationMessageDisplayMode::AlwaysShow ) } - pub fn should_expand_agent_message_body(&self) -> bool { - matches!(self, OrchestrationMessageDisplayMode::AlwaysShow) + /// Whether child-agent message bodies should collapse after streaming. + pub fn should_collapse_agent_message_body_on_finish(&self) -> bool { + matches!(self, OrchestrationMessageDisplayMode::ShowAndCollapse) } } diff --git a/app/src/settings_view/ai_page.rs b/app/src/settings_view/ai_page.rs index 1e2c1aab65..05d61597d1 100644 --- a/app/src/settings_view/ai_page.rs +++ b/app/src/settings_view/ai_page.rs @@ -342,8 +342,8 @@ pub fn init_actions_from_parent_view( let mode_bindings: Vec = OrchestrationMessageDisplayMode::iter() .map(|mode| { let context_flag = match mode { - OrchestrationMessageDisplayMode::CurrentBehavior => { - flags::ORCHESTRATION_MESSAGE_DISPLAY_CURRENT_BEHAVIOR + OrchestrationMessageDisplayMode::ShowAndCollapse => { + flags::ORCHESTRATION_MESSAGE_DISPLAY_SHOW_AND_COLLAPSE } OrchestrationMessageDisplayMode::AlwaysShow => { flags::ORCHESTRATION_MESSAGE_DISPLAY_ALWAYS_SHOW @@ -6547,8 +6547,8 @@ impl SettingsWidget for OtherAIWidget { column.add_child(render_dropdown_item( appearance, - "Orchestration message display", - Some("Controls whether child-agent prompts and messages start expanded."), + "Child-agent message display", + Some("Controls whether messages to and from child agents stay expanded."), None, LocalOnlyIconState::for_setting( OrchestrationMessageDisplayMode::storage_key(), diff --git a/app/src/settings_view/mod.rs b/app/src/settings_view/mod.rs index f9108ade12..6c0978a8f7 100644 --- a/app/src/settings_view/mod.rs +++ b/app/src/settings_view/mod.rs @@ -507,8 +507,8 @@ pub mod flags { pub const THINKING_DISPLAY_SHOW_AND_COLLAPSE: &str = "Thinking_Display_ShowAndCollapse"; pub const THINKING_DISPLAY_ALWAYS_SHOW: &str = "Thinking_Display_AlwaysShow"; pub const THINKING_DISPLAY_NEVER_SHOW: &str = "Thinking_Display_NeverShow"; - pub const ORCHESTRATION_MESSAGE_DISPLAY_CURRENT_BEHAVIOR: &str = - "Orchestration_Message_Display_CurrentBehavior"; + pub const ORCHESTRATION_MESSAGE_DISPLAY_SHOW_AND_COLLAPSE: &str = + "Orchestration_Message_Display_ShowAndCollapse"; pub const ORCHESTRATION_MESSAGE_DISPLAY_ALWAYS_SHOW: &str = "Orchestration_Message_Display_AlwaysShow"; pub const ORCHESTRATION_MESSAGE_DISPLAY_ALWAYS_COLLAPSE: &str = diff --git a/app/src/workspace/view.rs b/app/src/workspace/view.rs index 32a3f4664b..48ee7642e0 100644 --- a/app/src/workspace/view.rs +++ b/app/src/workspace/view.rs @@ -21845,10 +21845,10 @@ impl Workspace { } match ai_settings.orchestration_message_display_mode { - crate::settings::OrchestrationMessageDisplayMode::CurrentBehavior => { + crate::settings::OrchestrationMessageDisplayMode::ShowAndCollapse => { context .set - .insert(flags::ORCHESTRATION_MESSAGE_DISPLAY_CURRENT_BEHAVIOR); + .insert(flags::ORCHESTRATION_MESSAGE_DISPLAY_SHOW_AND_COLLAPSE); } crate::settings::OrchestrationMessageDisplayMode::AlwaysShow => { context From 51a95692e64535bd0d25c862a54709900e608a66 Mon Sep 17 00:00:00 2001 From: harryalbert Date: Fri, 5 Jun 2026 15:29:24 -0400 Subject: [PATCH 3/4] clean up formatting --- app/src/ai/blocklist/block.rs | 4 +-- .../block/view_impl/orchestration.rs | 26 ++++++++++++------- app/src/settings_view/ai_page.rs | 4 +-- 3 files changed, 20 insertions(+), 14 deletions(-) diff --git a/app/src/ai/blocklist/block.rs b/app/src/ai/blocklist/block.rs index 792884b5b0..7ab7b66508 100644 --- a/app/src/ai/blocklist/block.rs +++ b/app/src/ai/blocklist/block.rs @@ -816,7 +816,7 @@ impl CollapsibleElementState { } } - /// Applies child-agent message display behavior after streaming finishes. + /// Applies orchestration message display behavior after streaming finishes. fn finish_orchestration_message(&mut self, display_mode: OrchestrationMessageDisplayMode) { let should_auto_collapse = self.should_auto_collapse_on_finish(); @@ -2389,7 +2389,7 @@ impl AIBlock { self.keyboard_navigable_buttons = Some(menu); } - /// Applies final display behavior to child-agent message bodies. + /// Applies final display behavior to orchestration message bodies. fn finish_orchestration_message_collapsible_states( &mut self, output: &AIAgentOutput, diff --git a/app/src/ai/blocklist/block/view_impl/orchestration.rs b/app/src/ai/blocklist/block/view_impl/orchestration.rs index 547b32c1d5..b904304013 100644 --- a/app/src/ai/blocklist/block/view_impl/orchestration.rs +++ b/app/src/ai/blocklist/block/view_impl/orchestration.rs @@ -49,6 +49,16 @@ use crate::ui_components::icons::Icon; const GENERATING_TITLE_PLACEHOLDER: &str = "Generating title..."; const ORCHESTRATION_COLLAPSED_MAX_HEIGHT: f32 = 200.; + +/// Local section counters for markdown blocks outside the main output stream. +#[derive(Default)] +struct StandaloneMarkdownSectionIndices { + text: usize, + code: usize, + table: usize, + image: usize, +} + #[derive(Clone, Debug, PartialEq, Eq)] struct OrchestrationParticipant { display_name: String, @@ -755,8 +765,7 @@ fn start_agent_in_progress_prefix(execution_mode: &StartAgentExecutionMode) -> & } } -/// Renders a selectable markdown block below an orchestration action header, using a muted color. -/// Used for both StartAgent prompts and SendMessageToAgent message bodies. +/// Renders orchestration prompts and messages as markdown-backed sections. fn render_collapsible_markdown_body( text: &str, text_color: ColorU, @@ -766,17 +775,14 @@ fn render_collapsible_markdown_body( ) -> Box { let sections = parse_markdown_into_text_and_code_sections(text); let empty_secret_redaction_state = SecretRedactionState::default(); - let mut text_section_index = 0; - let mut code_section_index = 0; - let mut table_section_index = 0; - let mut image_section_index = 0; + let mut section_indices = StandaloneMarkdownSectionIndices::default(); let body = render_text_sections( TextSectionsProps { model: props.model, - starting_text_section_index: &mut text_section_index, - starting_code_section_index: &mut code_section_index, - starting_table_section_index: &mut table_section_index, - starting_image_section_index: &mut image_section_index, + starting_text_section_index: &mut section_indices.text, + starting_code_section_index: &mut section_indices.code, + starting_table_section_index: &mut section_indices.table, + starting_image_section_index: &mut section_indices.image, sections: §ions, text_color, selectable: true, diff --git a/app/src/settings_view/ai_page.rs b/app/src/settings_view/ai_page.rs index 05d61597d1..4515088a8f 100644 --- a/app/src/settings_view/ai_page.rs +++ b/app/src/settings_view/ai_page.rs @@ -6547,8 +6547,8 @@ impl SettingsWidget for OtherAIWidget { column.add_child(render_dropdown_item( appearance, - "Child-agent message display", - Some("Controls whether messages to and from child agents stay expanded."), + "Orchestration message display", + Some("Controls whether orchestration messages stay expanded."), None, LocalOnlyIconState::for_setting( OrchestrationMessageDisplayMode::storage_key(), From 44d3696191b444b01fe37c2cbbffef755de86eb6 Mon Sep 17 00:00:00 2001 From: harryalbert Date: Fri, 5 Jun 2026 16:10:29 -0400 Subject: [PATCH 4/4] revert markdown formatting change (should be its own PR I think) --- app/src/ai/agent/mod.rs | 1 - app/src/ai/agent/util.rs | 2 +- .../block/view_impl/orchestration.rs | 91 ++++++------------- 3 files changed, 28 insertions(+), 66 deletions(-) diff --git a/app/src/ai/agent/mod.rs b/app/src/ai/agent/mod.rs index c348e13956..e0f5438849 100644 --- a/app/src/ai/agent/mod.rs +++ b/app/src/ai/agent/mod.rs @@ -39,7 +39,6 @@ use warp_editor::render::model::LineCount; use warp_multi_agent_api::{diff_hunk as diff_hunk_api, AgentEvent, AgentType}; pub use self::api::{MaybeAIAgentOutputMessage, MessageToAIAgentOutputMessageError}; -pub(crate) use self::util::parse_markdown_into_text_and_code_sections; use super::llms::LLMId; use crate::ai::block_context::BlockContext; use crate::ai::blocklist::block::view_impl::output::are_all_text_sections_empty; diff --git a/app/src/ai/agent/util.rs b/app/src/ai/agent/util.rs index 4d80559232..a2493a8447 100644 --- a/app/src/ai/agent/util.rs +++ b/app/src/ai/agent/util.rs @@ -32,7 +32,7 @@ lazy_static! { } /// Converts the given `markdown_text` into corresponding `Text` and `Code` `AIAgentOutputStep`s. -pub(crate) fn parse_markdown_into_text_and_code_sections( +pub(super) fn parse_markdown_into_text_and_code_sections( markdown_text: &str, ) -> Vec { let mut sections = vec![]; diff --git a/app/src/ai/blocklist/block/view_impl/orchestration.rs b/app/src/ai/blocklist/block/view_impl/orchestration.rs index b904304013..e1deeaa294 100644 --- a/app/src/ai/blocklist/block/view_impl/orchestration.rs +++ b/app/src/ai/blocklist/block/view_impl/orchestration.rs @@ -9,15 +9,12 @@ use warpui::elements::{ use warpui::platform::Cursor; use warpui::{AppContext, Element, SingletonEntity}; -use super::common::{ - render_scrollable_collapsible_content, render_text_sections, TextSectionsProps, -}; +use super::common::render_scrollable_collapsible_content; use super::output::{action_icon, Props}; use super::WithContentItemSpacing; use crate::ai::agent::conversation::{ AIConversation, AIConversationId, ConversationStatus, StatusColorStyle, }; -use crate::ai::agent::parse_markdown_into_text_and_code_sections; use crate::ai::agent::{ AIAgentActionId, AIAgentActionResultType, MessageId, ReceivedMessageDisplay, SendMessageToAgentResult, StartAgentExecutionMode, StartAgentResult, @@ -29,7 +26,6 @@ use crate::ai::blocklist::agent_view::orchestration_conversation_links::{ dispatch_focus_or_open_child_agent_pane, }; use crate::ai::blocklist::block::model::AIBlockModelHelper; -use crate::ai::blocklist::block::secret_redaction::SecretRedactionState; use crate::ai::blocklist::block::{ received_message_collapsible_id, AIBlockAction, CollapsibleExpansionState, }; @@ -42,23 +38,12 @@ use crate::ai::blocklist::inline_action::requested_action::{ }; use crate::ai::blocklist::BlocklistAIHistoryModel; use crate::appearance::Appearance; -use crate::code::editor_management::CodeSource; use crate::terminal::view::TerminalAction; use crate::ui_components::blended_colors; use crate::ui_components::icons::Icon; const GENERATING_TITLE_PLACEHOLDER: &str = "Generating title..."; const ORCHESTRATION_COLLAPSED_MAX_HEIGHT: f32 = 200.; - -/// Local section counters for markdown blocks outside the main output stream. -#[derive(Default)] -struct StandaloneMarkdownSectionIndices { - text: usize, - code: usize, - table: usize, - image: usize, -} - #[derive(Clone, Debug, PartialEq, Eq)] struct OrchestrationParticipant { display_name: String, @@ -296,8 +281,14 @@ fn render_transcript_row( ); } if !data.body.is_empty() { - let body_element = - render_collapsible_markdown_body(data.body, body_color, false, props, app); + let body_element = Container::new( + Text::new(data.body.to_string(), font_family, font_size) + .with_color(body_color) + .with_selectable(true) + .finish(), + ) + .with_margin_top(8.) + .finish(); if let Some(body) = render_collapsible_body(data.message_id, body_element, data.is_streaming, props) { @@ -543,8 +534,7 @@ pub(super) fn render_send_message( } else { blended_colors::text_disabled(theme, theme.surface_2()) }; - let message_element = - render_collapsible_markdown_body(message, message_color, true, props, app); + let message_element = render_collapsible_text_body(message, message_color, true, app); if let Some(body) = render_collapsible_body( message_id, message_element, @@ -631,11 +621,10 @@ pub(super) fn render_start_agent( )); if has_prompt { - let prompt_element = render_collapsible_markdown_body( + let prompt_element = render_collapsible_text_body( prompt, blended_colors::text_disabled(theme, theme.surface_2()), true, - props, app, ); if let Some(body) = render_collapsible_body(message_id, prompt_element, false, props) { @@ -717,8 +706,7 @@ pub(super) fn render_start_agent( } else { blended_colors::text_disabled(theme, theme.surface_2()) }; - let prompt_element = - render_collapsible_markdown_body(prompt, prompt_color, true, props, app); + let prompt_element = render_collapsible_text_body(prompt, prompt_color, true, app); if let Some(body) = render_collapsible_body( message_id, prompt_element, @@ -765,51 +753,26 @@ fn start_agent_in_progress_prefix(execution_mode: &StartAgentExecutionMode) -> & } } -/// Renders orchestration prompts and messages as markdown-backed sections. -fn render_collapsible_markdown_body( +/// Renders a selectable text block below an orchestration action header, using a muted color. +/// Used for both StartAgent prompts and SendMessageToAgent message bodies. +fn render_collapsible_text_body( text: &str, text_color: ColorU, align_with_status_row_text: bool, - props: Props, app: &AppContext, ) -> Box { - let sections = parse_markdown_into_text_and_code_sections(text); - let empty_secret_redaction_state = SecretRedactionState::default(); - let mut section_indices = StandaloneMarkdownSectionIndices::default(); - let body = render_text_sections( - TextSectionsProps { - model: props.model, - starting_text_section_index: &mut section_indices.text, - starting_code_section_index: &mut section_indices.code, - starting_table_section_index: &mut section_indices.table, - starting_image_section_index: &mut section_indices.image, - sections: §ions, - text_color, - selectable: true, - find_context: None, - current_working_directory: props.current_working_directory, - shell_launch_data: props.shell_launch_data, - embedded_code_editor_views: &[], - code_snippet_button_handles: &[], - table_section_handles: &[], - image_section_tooltip_handles: &[], - is_ai_input_enabled: props.is_ai_input_enabled, - open_code_block_action_factory: (None as Option< - &'static dyn Fn(CodeSource) -> AIBlockAction, - >), - copy_code_action_factory: (None as Option<&'static dyn Fn(String) -> AIBlockAction>), - detected_links: None, - secret_redaction_state: &empty_secret_redaction_state, - is_selecting_text: props.is_selecting_text, - item_spacing: 6., - #[cfg(feature = "local_fs")] - resolved_code_block_paths: Some(props.resolved_code_block_paths), - #[cfg(feature = "local_fs")] - resolved_blocklist_image_sources: Some(props.resolved_blocklist_image_sources), - }, - app, - ); - let mut container = Container::new(body).with_margin_top(4.); + let appearance = Appearance::as_ref(app); + let mut container = Container::new( + Text::new( + text.to_string(), + appearance.ui_font_family(), + appearance.monospace_font_size(), + ) + .with_color(text_color) + .with_selectable(true) + .finish(), + ) + .with_margin_top(4.); if align_with_status_row_text { container = container