diff --git a/app/src/ai/blocklist/block/status_bar.rs b/app/src/ai/blocklist/block/status_bar.rs index af753be235..68c6ca79f7 100644 --- a/app/src/ai/blocklist/block/status_bar.rs +++ b/app/src/ai/blocklist/block/status_bar.rs @@ -1,3 +1,4 @@ +use markdown_parser::FormattedTextFragment; use std::{collections::HashSet, sync::Arc, time::Duration}; use super::{ @@ -22,11 +23,8 @@ use crate::{ AgentViewController, EphemeralMessageModel, }, terminal::input::{ - buffer_model::InputBufferModel, - message_bar::common::render_standard_message_bar, - message_bar::{Message, MessageItem}, - slash_command_model::SlashCommandModel, - suggestions_mode_model::InputSuggestionsModeModel, + buffer_model::InputBufferModel, message_bar::common::render_wrapping_standard_message_bar, + slash_command_model::SlashCommandModel, suggestions_mode_model::InputSuggestionsModeModel, HandoffComposeState, }, }; @@ -97,7 +95,6 @@ struct StateHandles { stop_button: MouseStateHandle, take_over_button: MouseStateHandle, hide_cli_responses_button: MouseStateHandle, - github_auth_link: MouseStateHandle, /// Tracks hover/press state for the inline `Check now` affordance rendered next to /// `Last seen by agent ...` while the agent is polling a long-running command. force_refresh_button: MouseStateHandle, @@ -916,7 +913,10 @@ impl BlocklistAIStatusBar { )) } - fn render_cloud_mode_setup_terminal_message(&self, app: &AppContext) -> Option { + fn render_cloud_mode_setup_terminal_message( + &self, + app: &AppContext, + ) -> Option> { if !FeatureFlag::CloudModeSetupV2.is_enabled() { return None; } @@ -929,48 +929,42 @@ impl BlocklistAIStatusBar { let error_color = theme.ansi_fg_red(); if let Some(auth_url) = ambient_agent_model.github_auth_url() { - return Some(Message::new(vec![ - MessageItem::Icon { - icon: CoreIcon::Triangle, - color: Some(error_color), - }, - MessageItem::Text { - content: "Missing GitHub authentication. ".into(), - color: Some(error_color), - }, - MessageItem::hyperlink( - "Authenticate GitHub", - auth_url.to_owned(), - self.state_handles.github_auth_link.clone(), - ), - ])); + let error_message = ambient_agent_model + .github_auth_error_message() + .unwrap_or("Missing GitHub authentication."); + return Some(render_wrapping_standard_message_bar( + CoreIcon::Triangle, + error_color, + error_color, + vec![ + FormattedTextFragment::plain_text(format!("{error_message} ")), + FormattedTextFragment::hyperlink("Authenticate GitHub", auth_url.to_owned()), + ], + app, + )); } if ambient_agent_model.is_cancelled() { let color = theme.disabled_text_color(theme.background()).into_solid(); - return Some(Message::new(vec![ - MessageItem::Icon { - icon: CoreIcon::StopFilled, - color: Some(color), - }, - MessageItem::Text { - content: "Cloud agent run cancelled".into(), - color: Some(color), - }, - ])); + return Some(render_wrapping_standard_message_bar( + CoreIcon::StopFilled, + color, + color, + vec![FormattedTextFragment::plain_text( + "Cloud agent run cancelled", + )], + app, + )); } if let Some(error_message) = ambient_agent_model.error_message() { - return Some(Message::new(vec![ - MessageItem::Icon { - icon: CoreIcon::Triangle, - color: Some(error_color), - }, - MessageItem::Text { - content: error_message.to_owned().into(), - color: Some(error_color), - }, - ])); + return Some(render_wrapping_standard_message_bar( + CoreIcon::Triangle, + error_color, + error_color, + vec![FormattedTextFragment::plain_text(error_message.to_owned())], + app, + )); } None @@ -1149,7 +1143,7 @@ impl View for BlocklistAIStatusBar { if let Some(cloud_mode_setup_terminal_message) = self.render_cloud_mode_setup_terminal_message(app) { - return render_standard_message_bar(cloud_mode_setup_terminal_message, None, app); + return cloud_mode_setup_terminal_message; } let status_element = if let Some(cloud_mode_setup_status) = self.render_cloud_mode_setup_status(app) { diff --git a/app/src/terminal/input/message_bar/common.rs b/app/src/terminal/input/message_bar/common.rs index 9d510786a4..b44d1f317e 100644 --- a/app/src/terminal/input/message_bar/common.rs +++ b/app/src/terminal/input/message_bar/common.rs @@ -1,14 +1,15 @@ use crate::ai::blocklist::agent_view::agent_view_bg_color; +use markdown_parser::{FormattedText, FormattedTextFragment, FormattedTextLine}; use pathfinder_color::ColorU; use warp_core::ui::appearance::Appearance; use warp_core::ui::theme::Fill; use warp_core::ui::Icon; use warpui::elements::{ - Border, CacheOption, Clipped, Container, CornerRadius, Element, Hoverable, Image, - ParentElement, Radius, + Border, CacheOption, Clipped, Container, CornerRadius, Element, FormattedTextElement, + Hoverable, Image, ParentElement, Radius, Wrap, WrapFill, DEFAULT_UI_LINE_HEIGHT_RATIO, }; use warpui::platform::Cursor; -use warpui::prelude::{Align, ConstrainedBox, CrossAxisAlignment, Flex, Text}; +use warpui::prelude::{Align, ConstrainedBox, CrossAxisAlignment, Flex, MainAxisSize, Text}; use warpui::ui_components::keyboard_shortcut::keystroke_to_keys; use warpui::{AppContext, SingletonEntity}; @@ -29,7 +30,7 @@ pub fn render_standard_message_bar( right_element: Option>, app: &AppContext, ) -> Box { - use warpui::prelude::{MainAxisAlignment, MainAxisSize}; + use warpui::prelude::MainAxisAlignment; let (left_items, right_chips): (Vec<_>, Vec<_>) = message.items.into_iter().partition(|item| { !matches!( @@ -81,6 +82,51 @@ pub fn render_standard_message_bar( .with_height(standard_message_bar_height(app)) .finish() } +/// Renders a standard message bar variant for inline text and hyperlinks that need to soft-wrap. +/// `render_standard_message_bar` intentionally remains fixed-height and single-line for existing +/// hint/status bars. +pub fn render_wrapping_standard_message_bar( + icon: Icon, + icon_color: ColorU, + text_color: ColorU, + fragments: Vec, + app: &AppContext, +) -> Box { + let appearance = Appearance::as_ref(app); + let theme = appearance.theme(); + let font_size = styles::font_size(app); + let icon = ConstrainedBox::new(icon.to_warpui_icon(Fill::Solid(icon_color)).finish()) + .with_height(font_size) + .with_width(font_size) + .finish(); + let text = FormattedTextElement::new( + FormattedText::new([FormattedTextLine::Line(fragments)]), + font_size, + appearance.ui_font_family(), + appearance.monospace_font_family(), + text_color, + Default::default(), + ) + .with_line_height_ratio(DEFAULT_UI_LINE_HEIGHT_RATIO) + .with_hyperlink_font_color(theme.accent().into()) + .register_default_click_handlers(|url, _ctx, app| { + app.open_url(&url.url); + }) + .finish(); + + Container::new( + Wrap::row() + .with_main_axis_size(MainAxisSize::Max) + .with_cross_axis_alignment(CrossAxisAlignment::Start) + .with_spacing(4.) + .with_child(icon) + .with_child(WrapFill::new(0., text).finish()) + .finish(), + ) + .with_horizontal_padding(*terminal::view::PADDING_LEFT) + .with_vertical_padding(styles::VERTICAL_PADDING) + .finish() +} pub fn render_standard_message(message: Message, app: &AppContext) -> Box { render_message_bar_items(&message.items, app) @@ -92,7 +138,9 @@ fn render_message_bar_items(items: &[MessageItem], app: &AppContext) -> Box = match item { diff --git a/app/src/terminal/view/ambient_agent/model_tests.rs b/app/src/terminal/view/ambient_agent/model_tests.rs index 780abbaee9..4ae7babb6a 100644 --- a/app/src/terminal/view/ambient_agent/model_tests.rs +++ b/app/src/terminal/view/ambient_agent/model_tests.rs @@ -103,6 +103,7 @@ fn github_auth_url_for_initial_run_includes_focus_cloud_mode_next() { model.read(&app, |model, _| { let auth_url = model.github_auth_url().expect("auth url should be present"); + assert_eq!(model.github_auth_error_message(), Some("auth required")); let parsed = Url::parse(auth_url).expect("auth url should parse"); let next = parsed .query_pairs()