diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index 8a0b282d9b9d5c6bab492391bdabfb1c09131bed..f4c76b10573dd2e36e797c20230739d6d6a77e46 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -278,6 +278,7 @@ pub struct AcpThreadView { notification_subscriptions: HashMap, Vec>, thread_retry_status: Option, thread_error: Option, + thread_error_markdown: Option>, thread_feedback: ThreadFeedbackState, list_state: ListState, auth_task: Option>, @@ -415,6 +416,7 @@ impl AcpThreadView { list_state: list_state, thread_retry_status: None, thread_error: None, + thread_error_markdown: None, thread_feedback: Default::default(), auth_task: None, expanded_tool_calls: HashSet::default(), @@ -798,6 +800,7 @@ impl AcpThreadView { if should_retry { self.thread_error = None; + self.thread_error_markdown = None; self.reset(window, cx); } } @@ -1327,6 +1330,7 @@ impl AcpThreadView { fn clear_thread_error(&mut self, cx: &mut Context) { self.thread_error = None; + self.thread_error_markdown = None; cx.notify(); } @@ -5344,9 +5348,9 @@ impl AcpThreadView { } } - fn render_thread_error(&self, cx: &mut Context) -> Option
{ + fn render_thread_error(&mut self, window: &mut Window, cx: &mut Context) -> Option
{ let content = match self.thread_error.as_ref()? { - ThreadError::Other(error) => self.render_any_thread_error(error.clone(), cx), + ThreadError::Other(error) => self.render_any_thread_error(error.clone(), window, cx), ThreadError::Refusal => self.render_refusal_error(cx), ThreadError::AuthenticationRequired(error) => { self.render_authentication_required_error(error.clone(), cx) @@ -5431,7 +5435,12 @@ impl AcpThreadView { .dismiss_action(self.dismiss_error_button(cx)) } - fn render_any_thread_error(&self, error: SharedString, cx: &mut Context<'_, Self>) -> Callout { + fn render_any_thread_error( + &mut self, + error: SharedString, + window: &mut Window, + cx: &mut Context<'_, Self>, + ) -> Callout { let can_resume = self .thread() .map_or(false, |thread| thread.read(cx).can_resume(cx)); @@ -5444,11 +5453,24 @@ impl AcpThreadView { supports_burn_mode && thread.completion_mode() == CompletionMode::Normal }); + let markdown = if let Some(markdown) = &self.thread_error_markdown { + markdown.clone() + } else { + let markdown = cx.new(|cx| Markdown::new(error.clone(), None, None, cx)); + self.thread_error_markdown = Some(markdown.clone()); + markdown + }; + + let markdown_style = default_markdown_style(false, true, window, cx); + let description = self + .render_markdown(markdown, markdown_style) + .into_any_element(); + Callout::new() .severity(Severity::Error) - .title("Error") .icon(IconName::XCircle) - .description(error.clone()) + .title("An Error Happened") + .description_slot(description) .actions_slot( h_flex() .gap_0p5() @@ -5467,11 +5489,9 @@ impl AcpThreadView { }) .when(can_resume, |this| { this.child( - Button::new("retry", "Retry") - .icon(IconName::RotateCw) - .icon_position(IconPosition::Start) + IconButton::new("retry", IconName::RotateCw) .icon_size(IconSize::Small) - .label_size(LabelSize::Small) + .tooltip(Tooltip::text("Retry Generation")) .on_click(cx.listener(|this, _, _window, cx| { this.resume_chat(cx); })), @@ -5613,7 +5633,6 @@ impl AcpThreadView { IconButton::new("copy", IconName::Copy) .icon_size(IconSize::Small) - .icon_color(Color::Muted) .tooltip(Tooltip::text("Copy Error Message")) .on_click(move |_, _, cx| { cx.write_to_clipboard(ClipboardItem::new_string(message.clone())) @@ -5623,7 +5642,6 @@ impl AcpThreadView { fn dismiss_error_button(&self, cx: &mut Context) -> impl IntoElement { IconButton::new("dismiss", IconName::Close) .icon_size(IconSize::Small) - .icon_color(Color::Muted) .tooltip(Tooltip::text("Dismiss Error")) .on_click(cx.listener({ move |this, _, _, cx| { @@ -5841,7 +5859,7 @@ impl Render for AcpThreadView { None } }) - .children(self.render_thread_error(cx)) + .children(self.render_thread_error(window, cx)) .when_some( self.new_server_version_available.as_ref().filter(|_| { !has_messages || !matches!(self.thread_state, ThreadState::Ready { .. }) @@ -5974,6 +5992,7 @@ fn default_markdown_style( }, link: TextStyleRefinement { background_color: Some(colors.editor_foreground.opacity(0.025)), + color: Some(colors.text_accent), underline: Some(UnderlineStyle { color: Some(colors.text_accent.opacity(0.5)), thickness: px(1.), diff --git a/crates/language_model/src/language_model.rs b/crates/language_model/src/language_model.rs index 4f0eed34331980ec0fd499c6a77e49e94b524fe0..606b0921b29f056ddea22947f08b2686af37d639 100644 --- a/crates/language_model/src/language_model.rs +++ b/crates/language_model/src/language_model.rs @@ -138,7 +138,7 @@ pub enum LanguageModelCompletionError { provider: LanguageModelProviderName, message: String, }, - #[error("permission error with {provider}'s API: {message}")] + #[error("Permission error with {provider}'s API: {message}")] PermissionError { provider: LanguageModelProviderName, message: String, diff --git a/crates/ui/src/components/callout.rs b/crates/ui/src/components/callout.rs index b5d1d7f25531cc956388da9d4a977bdfd14204b9..4eb849d7f640aca78b70645f5f93301281ca6627 100644 --- a/crates/ui/src/components/callout.rs +++ b/crates/ui/src/components/callout.rs @@ -30,6 +30,7 @@ pub struct Callout { icon: Option, title: Option, description: Option, + description_slot: Option, actions_slot: Option, dismiss_action: Option, line_height: Option, @@ -44,6 +45,7 @@ impl Callout { icon: None, title: None, description: None, + description_slot: None, actions_slot: None, dismiss_action: None, line_height: None, @@ -76,6 +78,13 @@ impl Callout { self } + /// Allows for any element—like markdown elements—to fill the description slot of the callout. + /// This method wins over `description` if both happen to be set. + pub fn description_slot(mut self, description: impl IntoElement) -> Self { + self.description_slot = Some(description.into_any_element()); + self + } + /// Sets the primary call-to-action button. pub fn actions_slot(mut self, action: impl IntoElement) -> Self { self.actions_slot = Some(action.into_any_element()); @@ -179,15 +188,27 @@ impl RenderOnce for Callout { ) }), ) - .when_some(self.description, |this, description| { - this.child( - div() - .w_full() - .flex_1() - .text_ui_sm(cx) - .text_color(cx.theme().colors().text_muted) - .child(description), - ) + .map(|this| { + if let Some(description_slot) = self.description_slot { + this.child( + div() + .w_full() + .flex_1() + .text_ui_sm(cx) + .child(description_slot), + ) + } else if let Some(description) = self.description { + this.child( + div() + .w_full() + .flex_1() + .text_ui_sm(cx) + .text_color(cx.theme().colors().text_muted) + .child(description), + ) + } else { + this + } }), ) }