agent: Improve error and warnings display (#36425)

Danilo Leal created

This PR refactors the callout component and improves how we display
errors and warnings in the agent panel, along with improvements for
specific cases (e.g., you have `zed.dev` as your LLM provider and is
signed out).

Still a work in progress, though, wrapping up some details.

Release Notes:

- N/A

Change summary

crates/agent_ui/src/acp/thread_view.rs                            | 145 
crates/agent_ui/src/active_thread.rs                              |   2 
crates/agent_ui/src/agent_configuration/add_llm_provider_modal.rs |   2 
crates/agent_ui/src/agent_panel.rs                                | 345 
crates/agent_ui/src/message_editor.rs                             |  50 
crates/agent_ui/src/ui/preview/usage_callouts.rs                  |  14 
crates/ai_onboarding/src/young_account_banner.rs                  |   2 
crates/language_model/src/registry.rs                             |   2 
crates/settings_ui/src/keybindings.rs                             |  14 
crates/ui/src/components/banner.rs                                |   9 
crates/ui/src/components/callout.rs                               | 217 
crates/ui/src/prelude.rs                                          |   4 
crates/ui/src/styles.rs                                           |   2 
crates/ui/src/styles/severity.rs                                  |  10 
14 files changed, 430 insertions(+), 388 deletions(-)

Detailed changes

crates/agent_ui/src/acp/thread_view.rs šŸ”—

@@ -3259,44 +3259,33 @@ impl AcpThreadView {
             }
         };
 
-        Some(
-            div()
-                .border_t_1()
-                .border_color(cx.theme().colors().border)
-                .child(content),
-        )
+        Some(div().child(content))
     }
 
     fn render_any_thread_error(&self, error: SharedString, cx: &mut Context<'_, Self>) -> Callout {
-        let icon = Icon::new(IconName::XCircle)
-            .size(IconSize::Small)
-            .color(Color::Error);
-
         Callout::new()
-            .icon(icon)
+            .severity(Severity::Error)
             .title("Error")
             .description(error.clone())
-            .secondary_action(self.create_copy_button(error.to_string()))
-            .primary_action(self.dismiss_error_button(cx))
-            .bg_color(self.error_callout_bg(cx))
+            .actions_slot(self.create_copy_button(error.to_string()))
+            .dismiss_action(self.dismiss_error_button(cx))
     }
 
     fn render_payment_required_error(&self, cx: &mut Context<Self>) -> Callout {
         const ERROR_MESSAGE: &str =
             "You reached your free usage limit. Upgrade to Zed Pro for more prompts.";
 
-        let icon = Icon::new(IconName::XCircle)
-            .size(IconSize::Small)
-            .color(Color::Error);
-
         Callout::new()
-            .icon(icon)
+            .severity(Severity::Error)
             .title("Free Usage Exceeded")
             .description(ERROR_MESSAGE)
-            .tertiary_action(self.upgrade_button(cx))
-            .secondary_action(self.create_copy_button(ERROR_MESSAGE))
-            .primary_action(self.dismiss_error_button(cx))
-            .bg_color(self.error_callout_bg(cx))
+            .actions_slot(
+                h_flex()
+                    .gap_0p5()
+                    .child(self.upgrade_button(cx))
+                    .child(self.create_copy_button(ERROR_MESSAGE)),
+            )
+            .dismiss_action(self.dismiss_error_button(cx))
     }
 
     fn render_model_request_limit_reached_error(
@@ -3311,18 +3300,17 @@ impl AcpThreadView {
             }
         };
 
-        let icon = Icon::new(IconName::XCircle)
-            .size(IconSize::Small)
-            .color(Color::Error);
-
         Callout::new()
-            .icon(icon)
+            .severity(Severity::Error)
             .title("Model Prompt Limit Reached")
             .description(error_message)
-            .tertiary_action(self.upgrade_button(cx))
-            .secondary_action(self.create_copy_button(error_message))
-            .primary_action(self.dismiss_error_button(cx))
-            .bg_color(self.error_callout_bg(cx))
+            .actions_slot(
+                h_flex()
+                    .gap_0p5()
+                    .child(self.upgrade_button(cx))
+                    .child(self.create_copy_button(error_message)),
+            )
+            .dismiss_action(self.dismiss_error_button(cx))
     }
 
     fn render_tool_use_limit_reached_error(
@@ -3338,52 +3326,59 @@ impl AcpThreadView {
 
         let focus_handle = self.focus_handle(cx);
 
-        let icon = Icon::new(IconName::Info)
-            .size(IconSize::Small)
-            .color(Color::Info);
-
         Some(
             Callout::new()
-                .icon(icon)
+                .icon(IconName::Info)
                 .title("Consecutive tool use limit reached.")
-                .when(supports_burn_mode, |this| {
-                    this.secondary_action(
-                        Button::new("continue-burn-mode", "Continue with Burn Mode")
-                            .style(ButtonStyle::Filled)
-                            .style(ButtonStyle::Tinted(ui::TintColor::Accent))
-                            .layer(ElevationIndex::ModalSurface)
-                            .label_size(LabelSize::Small)
-                            .key_binding(
-                                KeyBinding::for_action_in(
-                                    &ContinueWithBurnMode,
-                                    &focus_handle,
-                                    window,
-                                    cx,
-                                )
-                                .map(|kb| kb.size(rems_from_px(10.))),
+                .actions_slot(
+                    h_flex()
+                        .gap_0p5()
+                        .when(supports_burn_mode, |this| {
+                            this.child(
+                                Button::new("continue-burn-mode", "Continue with Burn Mode")
+                                    .style(ButtonStyle::Filled)
+                                    .style(ButtonStyle::Tinted(ui::TintColor::Accent))
+                                    .layer(ElevationIndex::ModalSurface)
+                                    .label_size(LabelSize::Small)
+                                    .key_binding(
+                                        KeyBinding::for_action_in(
+                                            &ContinueWithBurnMode,
+                                            &focus_handle,
+                                            window,
+                                            cx,
+                                        )
+                                        .map(|kb| kb.size(rems_from_px(10.))),
+                                    )
+                                    .tooltip(Tooltip::text(
+                                        "Enable Burn Mode for unlimited tool use.",
+                                    ))
+                                    .on_click({
+                                        cx.listener(move |this, _, _window, cx| {
+                                            thread.update(cx, |thread, _cx| {
+                                                thread.set_completion_mode(CompletionMode::Burn);
+                                            });
+                                            this.resume_chat(cx);
+                                        })
+                                    }),
                             )
-                            .tooltip(Tooltip::text("Enable Burn Mode for unlimited tool use."))
-                            .on_click({
-                                cx.listener(move |this, _, _window, cx| {
-                                    thread.update(cx, |thread, _cx| {
-                                        thread.set_completion_mode(CompletionMode::Burn);
-                                    });
+                        })
+                        .child(
+                            Button::new("continue-conversation", "Continue")
+                                .layer(ElevationIndex::ModalSurface)
+                                .label_size(LabelSize::Small)
+                                .key_binding(
+                                    KeyBinding::for_action_in(
+                                        &ContinueThread,
+                                        &focus_handle,
+                                        window,
+                                        cx,
+                                    )
+                                    .map(|kb| kb.size(rems_from_px(10.))),
+                                )
+                                .on_click(cx.listener(|this, _, _window, cx| {
                                     this.resume_chat(cx);
-                                })
-                            }),
-                    )
-                })
-                .primary_action(
-                    Button::new("continue-conversation", "Continue")
-                        .layer(ElevationIndex::ModalSurface)
-                        .label_size(LabelSize::Small)
-                        .key_binding(
-                            KeyBinding::for_action_in(&ContinueThread, &focus_handle, window, cx)
-                                .map(|kb| kb.size(rems_from_px(10.))),
-                        )
-                        .on_click(cx.listener(|this, _, _window, cx| {
-                            this.resume_chat(cx);
-                        })),
+                                })),
+                        ),
                 ),
         )
     }
@@ -3424,10 +3419,6 @@ impl AcpThreadView {
                 }
             }))
     }
-
-    fn error_callout_bg(&self, cx: &Context<Self>) -> Hsla {
-        cx.theme().status().error.opacity(0.08)
-    }
 }
 
 impl Focusable for AcpThreadView {

crates/agent_ui/src/active_thread.rs šŸ”—

@@ -2597,7 +2597,7 @@ impl ActiveThread {
             .id(("message-container", ix))
             .py_1()
             .px_2p5()
-            .child(Banner::new().severity(ui::Severity::Warning).child(message))
+            .child(Banner::new().severity(Severity::Warning).child(message))
     }
 
     fn render_message_thinking_segment(

crates/agent_ui/src/agent_panel.rs šŸ”—

@@ -48,9 +48,8 @@ use feature_flags::{self, FeatureFlagAppExt};
 use fs::Fs;
 use gpui::{
     Action, Animation, AnimationExt as _, AnyElement, App, AsyncWindowContext, ClipboardItem,
-    Corner, DismissEvent, Entity, EventEmitter, ExternalPaths, FocusHandle, Focusable, Hsla,
-    KeyContext, Pixels, Subscription, Task, UpdateGlobal, WeakEntity, prelude::*,
-    pulsating_between,
+    Corner, DismissEvent, Entity, EventEmitter, ExternalPaths, FocusHandle, Focusable, KeyContext,
+    Pixels, Subscription, Task, UpdateGlobal, WeakEntity, prelude::*, pulsating_between,
 };
 use language::LanguageRegistry;
 use language_model::{
@@ -2712,20 +2711,22 @@ impl AgentPanel {
         action_slot: Option<AnyElement>,
         cx: &mut Context<Self>,
     ) -> impl IntoElement {
-        h_flex()
-            .mt_2()
-            .pl_1p5()
-            .pb_1()
-            .w_full()
-            .justify_between()
-            .border_b_1()
-            .border_color(cx.theme().colors().border_variant)
-            .child(
-                Label::new(label.into())
-                    .size(LabelSize::Small)
-                    .color(Color::Muted),
-            )
-            .children(action_slot)
+        div().pl_1().pr_1p5().child(
+            h_flex()
+                .mt_2()
+                .pl_1p5()
+                .pb_1()
+                .w_full()
+                .justify_between()
+                .border_b_1()
+                .border_color(cx.theme().colors().border_variant)
+                .child(
+                    Label::new(label.into())
+                        .size(LabelSize::Small)
+                        .color(Color::Muted),
+                )
+                .children(action_slot),
+        )
     }
 
     fn render_thread_empty_state(
@@ -2831,22 +2832,12 @@ impl AgentPanel {
                                                 }),
                                         ),
                                 )
-                        })
-                        .when_some(configuration_error.as_ref(), |this, err| {
-                            this.child(self.render_configuration_error(
-                                err,
-                                &focus_handle,
-                                window,
-                                cx,
-                            ))
                         }),
                 )
             })
             .when(!recent_history.is_empty(), |parent| {
-                let focus_handle = focus_handle.clone();
                 parent
                     .overflow_hidden()
-                    .p_1p5()
                     .justify_end()
                     .gap_1()
                     .child(
@@ -2874,10 +2865,11 @@ impl AgentPanel {
                         ),
                     )
                     .child(
-                        v_flex()
-                            .gap_1()
-                            .children(recent_history.into_iter().enumerate().map(
-                                |(index, entry)| {
+                        v_flex().p_1().pr_1p5().gap_1().children(
+                            recent_history
+                                .into_iter()
+                                .enumerate()
+                                .map(|(index, entry)| {
                                     // TODO: Add keyboard navigation.
                                     let is_hovered =
                                         self.hovered_recent_history_item == Some(index);
@@ -2896,30 +2888,68 @@ impl AgentPanel {
                                             },
                                         ))
                                         .into_any_element()
-                                },
-                            )),
+                                }),
+                        ),
                     )
-                    .when_some(configuration_error.as_ref(), |this, err| {
-                        this.child(self.render_configuration_error(err, &focus_handle, window, cx))
-                    })
+            })
+            .when_some(configuration_error.as_ref(), |this, err| {
+                this.child(self.render_configuration_error(false, err, &focus_handle, window, cx))
             })
     }
 
     fn render_configuration_error(
         &self,
+        border_bottom: bool,
         configuration_error: &ConfigurationError,
         focus_handle: &FocusHandle,
         window: &mut Window,
         cx: &mut App,
     ) -> impl IntoElement {
-        match configuration_error {
-            ConfigurationError::ModelNotFound
-            | ConfigurationError::ProviderNotAuthenticated(_)
-            | ConfigurationError::NoProvider => Banner::new()
-                .severity(ui::Severity::Warning)
-                .child(Label::new(configuration_error.to_string()))
-                .action_slot(
-                    Button::new("settings", "Configure Provider")
+        let zed_provider_configured = AgentSettings::get_global(cx)
+            .default_model
+            .as_ref()
+            .map_or(false, |selection| {
+                selection.provider.0.as_str() == "zed.dev"
+            });
+
+        let callout = if zed_provider_configured {
+            Callout::new()
+                .icon(IconName::Warning)
+                .severity(Severity::Warning)
+                .when(border_bottom, |this| {
+                    this.border_position(ui::BorderPosition::Bottom)
+                })
+                .title("Sign in to continue using Zed as your LLM provider.")
+                .actions_slot(
+                    Button::new("sign_in", "Sign In")
+                        .style(ButtonStyle::Tinted(ui::TintColor::Warning))
+                        .label_size(LabelSize::Small)
+                        .on_click({
+                            let workspace = self.workspace.clone();
+                            move |_, _, cx| {
+                                let Ok(client) =
+                                    workspace.update(cx, |workspace, _| workspace.client().clone())
+                                else {
+                                    return;
+                                };
+
+                                cx.spawn(async move |cx| {
+                                    client.sign_in_with_optional_connect(true, cx).await
+                                })
+                                .detach_and_log_err(cx);
+                            }
+                        }),
+                )
+        } else {
+            Callout::new()
+                .icon(IconName::Warning)
+                .severity(Severity::Warning)
+                .when(border_bottom, |this| {
+                    this.border_position(ui::BorderPosition::Bottom)
+                })
+                .title(configuration_error.to_string())
+                .actions_slot(
+                    Button::new("settings", "Configure")
                         .style(ButtonStyle::Tinted(ui::TintColor::Warning))
                         .label_size(LabelSize::Small)
                         .key_binding(
@@ -2929,16 +2959,23 @@ impl AgentPanel {
                         .on_click(|_event, window, cx| {
                             window.dispatch_action(OpenSettings.boxed_clone(), cx)
                         }),
-                ),
+                )
+        };
+
+        match configuration_error {
+            ConfigurationError::ModelNotFound
+            | ConfigurationError::ProviderNotAuthenticated(_)
+            | ConfigurationError::NoProvider => callout.into_any_element(),
             ConfigurationError::ProviderPendingTermsAcceptance(provider) => {
-                Banner::new().severity(ui::Severity::Warning).child(
-                    h_flex().w_full().children(
+                Banner::new()
+                    .severity(Severity::Warning)
+                    .child(h_flex().w_full().children(
                         provider.render_accept_terms(
                             LanguageModelProviderTosView::ThreadEmptyState,
                             cx,
                         ),
-                    ),
-                )
+                    ))
+                    .into_any_element()
             }
         }
     }
@@ -2970,7 +3007,7 @@ impl AgentPanel {
         let focus_handle = self.focus_handle(cx);
 
         let banner = Banner::new()
-            .severity(ui::Severity::Info)
+            .severity(Severity::Info)
             .child(Label::new("Consecutive tool use limit reached.").size(LabelSize::Small))
             .action_slot(
                 h_flex()
@@ -3081,10 +3118,6 @@ impl AgentPanel {
             }))
     }
 
-    fn error_callout_bg(&self, cx: &Context<Self>) -> Hsla {
-        cx.theme().status().error.opacity(0.08)
-    }
-
     fn render_payment_required_error(
         &self,
         thread: &Entity<ActiveThread>,
@@ -3093,23 +3126,18 @@ impl AgentPanel {
         const ERROR_MESSAGE: &str =
             "You reached your free usage limit. Upgrade to Zed Pro for more prompts.";
 
-        let icon = Icon::new(IconName::XCircle)
-            .size(IconSize::Small)
-            .color(Color::Error);
-
-        div()
-            .border_t_1()
-            .border_color(cx.theme().colors().border)
-            .child(
-                Callout::new()
-                    .icon(icon)
-                    .title("Free Usage Exceeded")
-                    .description(ERROR_MESSAGE)
-                    .tertiary_action(self.upgrade_button(thread, cx))
-                    .secondary_action(self.create_copy_button(ERROR_MESSAGE))
-                    .primary_action(self.dismiss_error_button(thread, cx))
-                    .bg_color(self.error_callout_bg(cx)),
+        Callout::new()
+            .severity(Severity::Error)
+            .icon(IconName::XCircle)
+            .title("Free Usage Exceeded")
+            .description(ERROR_MESSAGE)
+            .actions_slot(
+                h_flex()
+                    .gap_0p5()
+                    .child(self.upgrade_button(thread, cx))
+                    .child(self.create_copy_button(ERROR_MESSAGE)),
             )
+            .dismiss_action(self.dismiss_error_button(thread, cx))
             .into_any_element()
     }
 
@@ -3124,40 +3152,22 @@ impl AgentPanel {
             Plan::ZedProTrial | Plan::ZedFree => "Upgrade to Zed Pro for more prompts.",
         };
 
-        let icon = Icon::new(IconName::XCircle)
-            .size(IconSize::Small)
-            .color(Color::Error);
-
-        div()
-            .border_t_1()
-            .border_color(cx.theme().colors().border)
-            .child(
-                Callout::new()
-                    .icon(icon)
-                    .title("Model Prompt Limit Reached")
-                    .description(error_message)
-                    .tertiary_action(self.upgrade_button(thread, cx))
-                    .secondary_action(self.create_copy_button(error_message))
-                    .primary_action(self.dismiss_error_button(thread, cx))
-                    .bg_color(self.error_callout_bg(cx)),
+        Callout::new()
+            .severity(Severity::Error)
+            .title("Model Prompt Limit Reached")
+            .description(error_message)
+            .actions_slot(
+                h_flex()
+                    .gap_0p5()
+                    .child(self.upgrade_button(thread, cx))
+                    .child(self.create_copy_button(error_message)),
             )
+            .dismiss_action(self.dismiss_error_button(thread, cx))
             .into_any_element()
     }
 
-    fn render_error_message(
-        &self,
-        header: SharedString,
-        message: SharedString,
-        thread: &Entity<ActiveThread>,
-        cx: &mut Context<Self>,
-    ) -> AnyElement {
-        let message_with_header = format!("{}\n{}", header, message);
-
-        let icon = Icon::new(IconName::XCircle)
-            .size(IconSize::Small)
-            .color(Color::Error);
-
-        let retry_button = Button::new("retry", "Retry")
+    fn render_retry_button(&self, thread: &Entity<ActiveThread>) -> AnyElement {
+        Button::new("retry", "Retry")
             .icon(IconName::RotateCw)
             .icon_position(IconPosition::Start)
             .icon_size(IconSize::Small)
@@ -3172,21 +3182,31 @@ impl AgentPanel {
                         });
                     });
                 }
-            });
+            })
+            .into_any_element()
+    }
 
-        div()
-            .border_t_1()
-            .border_color(cx.theme().colors().border)
-            .child(
-                Callout::new()
-                    .icon(icon)
-                    .title(header)
-                    .description(message.clone())
-                    .primary_action(retry_button)
-                    .secondary_action(self.dismiss_error_button(thread, cx))
-                    .tertiary_action(self.create_copy_button(message_with_header))
-                    .bg_color(self.error_callout_bg(cx)),
+    fn render_error_message(
+        &self,
+        header: SharedString,
+        message: SharedString,
+        thread: &Entity<ActiveThread>,
+        cx: &mut Context<Self>,
+    ) -> AnyElement {
+        let message_with_header = format!("{}\n{}", header, message);
+
+        Callout::new()
+            .severity(Severity::Error)
+            .icon(IconName::XCircle)
+            .title(header)
+            .description(message.clone())
+            .actions_slot(
+                h_flex()
+                    .gap_0p5()
+                    .child(self.render_retry_button(thread))
+                    .child(self.create_copy_button(message_with_header)),
             )
+            .dismiss_action(self.dismiss_error_button(thread, cx))
             .into_any_element()
     }
 
@@ -3195,60 +3215,39 @@ impl AgentPanel {
         message: SharedString,
         can_enable_burn_mode: bool,
         thread: &Entity<ActiveThread>,
-        cx: &mut Context<Self>,
     ) -> AnyElement {
-        let icon = Icon::new(IconName::XCircle)
-            .size(IconSize::Small)
-            .color(Color::Error);
-
-        let retry_button = Button::new("retry", "Retry")
-            .icon(IconName::RotateCw)
-            .icon_position(IconPosition::Start)
-            .icon_size(IconSize::Small)
-            .label_size(LabelSize::Small)
-            .on_click({
-                let thread = thread.clone();
-                move |_, window, cx| {
-                    thread.update(cx, |thread, cx| {
-                        thread.clear_last_error();
-                        thread.thread().update(cx, |thread, cx| {
-                            thread.retry_last_completion(Some(window.window_handle()), cx);
-                        });
-                    });
-                }
-            });
-
-        let mut callout = Callout::new()
-            .icon(icon)
+        Callout::new()
+            .severity(Severity::Error)
             .title("Error")
             .description(message.clone())
-            .bg_color(self.error_callout_bg(cx))
-            .primary_action(retry_button);
-
-        if can_enable_burn_mode {
-            let burn_mode_button = Button::new("enable_burn_retry", "Enable Burn Mode and Retry")
-                .icon(IconName::ZedBurnMode)
-                .icon_position(IconPosition::Start)
-                .icon_size(IconSize::Small)
-                .label_size(LabelSize::Small)
-                .on_click({
-                    let thread = thread.clone();
-                    move |_, window, cx| {
-                        thread.update(cx, |thread, cx| {
-                            thread.clear_last_error();
-                            thread.thread().update(cx, |thread, cx| {
-                                thread.enable_burn_mode_and_retry(Some(window.window_handle()), cx);
-                            });
-                        });
-                    }
-                });
-            callout = callout.secondary_action(burn_mode_button);
-        }
-
-        div()
-            .border_t_1()
-            .border_color(cx.theme().colors().border)
-            .child(callout)
+            .actions_slot(
+                h_flex()
+                    .gap_0p5()
+                    .when(can_enable_burn_mode, |this| {
+                        this.child(
+                            Button::new("enable_burn_retry", "Enable Burn Mode and Retry")
+                                .icon(IconName::ZedBurnMode)
+                                .icon_position(IconPosition::Start)
+                                .icon_size(IconSize::Small)
+                                .label_size(LabelSize::Small)
+                                .on_click({
+                                    let thread = thread.clone();
+                                    move |_, window, cx| {
+                                        thread.update(cx, |thread, cx| {
+                                            thread.clear_last_error();
+                                            thread.thread().update(cx, |thread, cx| {
+                                                thread.enable_burn_mode_and_retry(
+                                                    Some(window.window_handle()),
+                                                    cx,
+                                                );
+                                            });
+                                        });
+                                    }
+                                }),
+                        )
+                    })
+                    .child(self.render_retry_button(thread)),
+            )
             .into_any_element()
     }
 
@@ -3503,7 +3502,6 @@ impl Render for AgentPanel {
                                         message,
                                         can_enable_burn_mode,
                                         thread,
-                                        cx,
                                     ),
                                 })
                                 .into_any(),
@@ -3531,16 +3529,13 @@ impl Render for AgentPanel {
                             if !self.should_render_onboarding(cx)
                                 && let Some(err) = configuration_error.as_ref()
                             {
-                                this.child(
-                                    div().bg(cx.theme().colors().editor_background).p_2().child(
-                                        self.render_configuration_error(
-                                            err,
-                                            &self.focus_handle(cx),
-                                            window,
-                                            cx,
-                                        ),
-                                    ),
-                                )
+                                this.child(self.render_configuration_error(
+                                    true,
+                                    err,
+                                    &self.focus_handle(cx),
+                                    window,
+                                    cx,
+                                ))
                             } else {
                                 this
                             }

crates/agent_ui/src/message_editor.rs šŸ”—

@@ -1323,14 +1323,10 @@ impl MessageEditor {
         token_usage_ratio: TokenUsageRatio,
         cx: &mut Context<Self>,
     ) -> Option<Div> {
-        let icon = if token_usage_ratio == TokenUsageRatio::Exceeded {
-            Icon::new(IconName::Close)
-                .color(Color::Error)
-                .size(IconSize::XSmall)
+        let (icon, severity) = if token_usage_ratio == TokenUsageRatio::Exceeded {
+            (IconName::Close, Severity::Error)
         } else {
-            Icon::new(IconName::Warning)
-                .color(Color::Warning)
-                .size(IconSize::XSmall)
+            (IconName::Warning, Severity::Warning)
         };
 
         let title = if token_usage_ratio == TokenUsageRatio::Exceeded {
@@ -1345,29 +1341,33 @@ impl MessageEditor {
             "To continue, start a new thread from a summary."
         };
 
-        let mut callout = Callout::new()
+        let callout = Callout::new()
             .line_height(line_height)
+            .severity(severity)
             .icon(icon)
             .title(title)
             .description(description)
-            .primary_action(
-                Button::new("start-new-thread", "Start New Thread")
-                    .label_size(LabelSize::Small)
-                    .on_click(cx.listener(|this, _, window, cx| {
-                        let from_thread_id = Some(this.thread.read(cx).id().clone());
-                        window.dispatch_action(Box::new(NewThread { from_thread_id }), cx);
-                    })),
-            );
-
-        if self.is_using_zed_provider(cx) {
-            callout = callout.secondary_action(
-                IconButton::new("burn-mode-callout", IconName::ZedBurnMode)
-                    .icon_size(IconSize::XSmall)
-                    .on_click(cx.listener(|this, _event, window, cx| {
-                        this.toggle_burn_mode(&ToggleBurnMode, window, cx);
-                    })),
+            .actions_slot(
+                h_flex()
+                    .gap_0p5()
+                    .when(self.is_using_zed_provider(cx), |this| {
+                        this.child(
+                            IconButton::new("burn-mode-callout", IconName::ZedBurnMode)
+                                .icon_size(IconSize::XSmall)
+                                .on_click(cx.listener(|this, _event, window, cx| {
+                                    this.toggle_burn_mode(&ToggleBurnMode, window, cx);
+                                })),
+                        )
+                    })
+                    .child(
+                        Button::new("start-new-thread", "Start New Thread")
+                            .label_size(LabelSize::Small)
+                            .on_click(cx.listener(|this, _, window, cx| {
+                                let from_thread_id = Some(this.thread.read(cx).id().clone());
+                                window.dispatch_action(Box::new(NewThread { from_thread_id }), cx);
+                            })),
+                    ),
             );
-        }
 
         Some(
             div()

crates/agent_ui/src/ui/preview/usage_callouts.rs šŸ”—

@@ -80,14 +80,10 @@ impl RenderOnce for UsageCallout {
             }
         };
 
-        let icon = if is_limit_reached {
-            Icon::new(IconName::Close)
-                .color(Color::Error)
-                .size(IconSize::XSmall)
+        let (icon, severity) = if is_limit_reached {
+            (IconName::Close, Severity::Error)
         } else {
-            Icon::new(IconName::Warning)
-                .color(Color::Warning)
-                .size(IconSize::XSmall)
+            (IconName::Warning, Severity::Warning)
         };
 
         div()
@@ -95,10 +91,12 @@ impl RenderOnce for UsageCallout {
             .border_color(cx.theme().colors().border)
             .child(
                 Callout::new()
+                    .icon(icon)
+                    .severity(severity)
                     .icon(icon)
                     .title(title)
                     .description(message)
-                    .primary_action(
+                    .actions_slot(
                         Button::new("upgrade", button_text)
                             .label_size(LabelSize::Small)
                             .on_click(move |_, _, cx| {

crates/ai_onboarding/src/young_account_banner.rs šŸ”—

@@ -17,6 +17,6 @@ impl RenderOnce for YoungAccountBanner {
         div()
             .max_w_full()
             .my_1()
-            .child(Banner::new().severity(ui::Severity::Warning).child(label))
+            .child(Banner::new().severity(Severity::Warning).child(label))
     }
 }

crates/language_model/src/registry.rs šŸ”—

@@ -21,7 +21,7 @@ impl Global for GlobalLanguageModelRegistry {}
 pub enum ConfigurationError {
     #[error("Configure at least one LLM provider to start using the panel.")]
     NoProvider,
-    #[error("LLM Provider is not configured or does not support the configured model.")]
+    #[error("LLM provider is not configured or does not support the configured model.")]
     ModelNotFound,
     #[error("{} LLM provider is not configured.", .0.name().0)]
     ProviderNotAuthenticated(Arc<dyn LanguageModelProvider>),

crates/settings_ui/src/keybindings.rs šŸ”—

@@ -2021,21 +2021,21 @@ impl RenderOnce for SyntaxHighlightedText {
 
 #[derive(PartialEq)]
 struct InputError {
-    severity: ui::Severity,
+    severity: Severity,
     content: SharedString,
 }
 
 impl InputError {
     fn warning(message: impl Into<SharedString>) -> Self {
         Self {
-            severity: ui::Severity::Warning,
+            severity: Severity::Warning,
             content: message.into(),
         }
     }
 
     fn error(message: anyhow::Error) -> Self {
         Self {
-            severity: ui::Severity::Error,
+            severity: Severity::Error,
             content: message.to_string().into(),
         }
     }
@@ -2162,9 +2162,11 @@ impl KeybindingEditorModal {
     }
 
     fn set_error(&mut self, error: InputError, cx: &mut Context<Self>) -> bool {
-        if self.error.as_ref().is_some_and(|old_error| {
-            old_error.severity == ui::Severity::Warning && *old_error == error
-        }) {
+        if self
+            .error
+            .as_ref()
+            .is_some_and(|old_error| old_error.severity == Severity::Warning && *old_error == error)
+        {
             false
         } else {
             self.error = Some(error);

crates/ui/src/components/banner.rs šŸ”—

@@ -1,15 +1,6 @@
 use crate::prelude::*;
 use gpui::{AnyElement, IntoElement, ParentElement, Styled};
 
-/// Severity levels that determine the style of the banner.
-#[derive(Debug, Clone, Copy, PartialEq, Eq)]
-pub enum Severity {
-    Info,
-    Success,
-    Warning,
-    Error,
-}
-
 /// Banners provide informative and brief messages without interrupting the user.
 /// This component offers four severity levels that can be used depending on the message.
 ///

crates/ui/src/components/callout.rs šŸ”—

@@ -1,7 +1,13 @@
-use gpui::{AnyElement, Hsla};
+use gpui::AnyElement;
 
 use crate::prelude::*;
 
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub enum BorderPosition {
+    Top,
+    Bottom,
+}
+
 /// A callout component for displaying important information that requires user attention.
 ///
 /// # Usage Example
@@ -10,42 +16,48 @@ use crate::prelude::*;
 /// use ui::{Callout};
 ///
 /// Callout::new()
-///     .icon(Icon::new(IconName::Warning).color(Color::Warning))
+///     .severity(Severity::Warning)
+///     .icon(IconName::Warning)
 ///     .title(Label::new("Be aware of your subscription!"))
 ///     .description(Label::new("Your subscription is about to expire. Renew now!"))
-///     .primary_action(Button::new("renew", "Renew Now"))
-///     .secondary_action(Button::new("remind", "Remind Me Later"))
+///     .actions_slot(Button::new("renew", "Renew Now"))
 /// ```
 ///
 #[derive(IntoElement, RegisterComponent)]
 pub struct Callout {
-    icon: Option<Icon>,
+    severity: Severity,
+    icon: Option<IconName>,
     title: Option<SharedString>,
     description: Option<SharedString>,
-    primary_action: Option<AnyElement>,
-    secondary_action: Option<AnyElement>,
-    tertiary_action: Option<AnyElement>,
+    actions_slot: Option<AnyElement>,
+    dismiss_action: Option<AnyElement>,
     line_height: Option<Pixels>,
-    bg_color: Option<Hsla>,
+    border_position: BorderPosition,
 }
 
 impl Callout {
     /// Creates a new `Callout` component with default styling.
     pub fn new() -> Self {
         Self {
+            severity: Severity::Info,
             icon: None,
             title: None,
             description: None,
-            primary_action: None,
-            secondary_action: None,
-            tertiary_action: None,
+            actions_slot: None,
+            dismiss_action: None,
             line_height: None,
-            bg_color: None,
+            border_position: BorderPosition::Top,
         }
     }
 
+    /// Sets the severity of the callout.
+    pub fn severity(mut self, severity: Severity) -> Self {
+        self.severity = severity;
+        self
+    }
+
     /// Sets the icon to display in the callout.
-    pub fn icon(mut self, icon: Icon) -> Self {
+    pub fn icon(mut self, icon: IconName) -> Self {
         self.icon = Some(icon);
         self
     }
@@ -64,20 +76,14 @@ impl Callout {
     }
 
     /// Sets the primary call-to-action button.
-    pub fn primary_action(mut self, action: impl IntoElement) -> Self {
-        self.primary_action = Some(action.into_any_element());
-        self
-    }
-
-    /// Sets an optional secondary call-to-action button.
-    pub fn secondary_action(mut self, action: impl IntoElement) -> Self {
-        self.secondary_action = Some(action.into_any_element());
+    pub fn actions_slot(mut self, action: impl IntoElement) -> Self {
+        self.actions_slot = Some(action.into_any_element());
         self
     }
 
     /// Sets an optional tertiary call-to-action button.
-    pub fn tertiary_action(mut self, action: impl IntoElement) -> Self {
-        self.tertiary_action = Some(action.into_any_element());
+    pub fn dismiss_action(mut self, action: impl IntoElement) -> Self {
+        self.dismiss_action = Some(action.into_any_element());
         self
     }
 
@@ -87,9 +93,9 @@ impl Callout {
         self
     }
 
-    /// Sets a custom background color for the callout content.
-    pub fn bg_color(mut self, color: Hsla) -> Self {
-        self.bg_color = Some(color);
+    /// Sets the border position in the callout.
+    pub fn border_position(mut self, border_position: BorderPosition) -> Self {
+        self.border_position = border_position;
         self
     }
 }
@@ -97,21 +103,51 @@ impl Callout {
 impl RenderOnce for Callout {
     fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
         let line_height = self.line_height.unwrap_or(window.line_height());
-        let bg_color = self
-            .bg_color
-            .unwrap_or(cx.theme().colors().panel_background);
-        let has_actions = self.primary_action.is_some()
-            || self.secondary_action.is_some()
-            || self.tertiary_action.is_some();
+
+        let has_actions = self.actions_slot.is_some() || self.dismiss_action.is_some();
+
+        let (icon, icon_color, bg_color) = match self.severity {
+            Severity::Info => (
+                IconName::Info,
+                Color::Muted,
+                cx.theme().colors().panel_background.opacity(0.),
+            ),
+            Severity::Success => (
+                IconName::Check,
+                Color::Success,
+                cx.theme().status().success.opacity(0.1),
+            ),
+            Severity::Warning => (
+                IconName::Warning,
+                Color::Warning,
+                cx.theme().status().warning_background.opacity(0.2),
+            ),
+            Severity::Error => (
+                IconName::XCircle,
+                Color::Error,
+                cx.theme().status().error.opacity(0.08),
+            ),
+        };
 
         h_flex()
+            .min_w_0()
             .p_2()
             .gap_2()
             .items_start()
+            .map(|this| match self.border_position {
+                BorderPosition::Top => this.border_t_1(),
+                BorderPosition::Bottom => this.border_b_1(),
+            })
+            .border_color(cx.theme().colors().border)
             .bg(bg_color)
             .overflow_x_hidden()
-            .when_some(self.icon, |this, icon| {
-                this.child(h_flex().h(line_height).justify_center().child(icon))
+            .when(self.icon.is_some(), |this| {
+                this.child(
+                    h_flex()
+                        .h(line_height)
+                        .justify_center()
+                        .child(Icon::new(icon).size(IconSize::Small).color(icon_color)),
+                )
             })
             .child(
                 v_flex()
@@ -119,10 +155,11 @@ impl RenderOnce for Callout {
                     .w_full()
                     .child(
                         h_flex()
-                            .h(line_height)
+                            .min_h(line_height)
                             .w_full()
                             .gap_1()
                             .justify_between()
+                            .flex_wrap()
                             .when_some(self.title, |this, title| {
                                 this.child(h_flex().child(Label::new(title).size(LabelSize::Small)))
                             })
@@ -130,13 +167,10 @@ impl RenderOnce for Callout {
                                 this.child(
                                     h_flex()
                                         .gap_0p5()
-                                        .when_some(self.tertiary_action, |this, action| {
+                                        .when_some(self.actions_slot, |this, action| {
                                             this.child(action)
                                         })
-                                        .when_some(self.secondary_action, |this, action| {
-                                            this.child(action)
-                                        })
-                                        .when_some(self.primary_action, |this, action| {
+                                        .when_some(self.dismiss_action, |this, action| {
                                             this.child(action)
                                         }),
                                 )
@@ -168,84 +202,101 @@ impl Component for Callout {
     }
 
     fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
-        let callout_examples = vec![
+        let single_action = || Button::new("got-it", "Got it").label_size(LabelSize::Small);
+        let multiple_actions = || {
+            h_flex()
+                .gap_0p5()
+                .child(Button::new("update", "Backup & Update").label_size(LabelSize::Small))
+                .child(Button::new("dismiss", "Dismiss").label_size(LabelSize::Small))
+        };
+
+        let basic_examples = vec![
             single_example(
                 "Simple with Title Only",
                 Callout::new()
-                    .icon(
-                        Icon::new(IconName::Info)
-                            .color(Color::Accent)
-                            .size(IconSize::Small),
-                    )
+                    .icon(IconName::Info)
                     .title("System maintenance scheduled for tonight")
-                    .primary_action(Button::new("got-it", "Got it").label_size(LabelSize::Small))
+                    .actions_slot(single_action())
                     .into_any_element(),
             )
             .width(px(580.)),
             single_example(
                 "With Title and Description",
                 Callout::new()
-                    .icon(
-                        Icon::new(IconName::Warning)
-                            .color(Color::Warning)
-                            .size(IconSize::Small),
-                    )
+                    .icon(IconName::Warning)
                     .title("Your settings contain deprecated values")
                     .description(
                         "We'll backup your current settings and update them to the new format.",
                     )
-                    .primary_action(
-                        Button::new("update", "Backup & Update").label_size(LabelSize::Small),
-                    )
-                    .secondary_action(
-                        Button::new("dismiss", "Dismiss").label_size(LabelSize::Small),
-                    )
+                    .actions_slot(single_action())
                     .into_any_element(),
             )
             .width(px(580.)),
             single_example(
                 "Error with Multiple Actions",
                 Callout::new()
-                    .icon(
-                        Icon::new(IconName::Close)
-                            .color(Color::Error)
-                            .size(IconSize::Small),
-                    )
+                    .icon(IconName::Close)
                     .title("Thread reached the token limit")
                     .description("Start a new thread from a summary to continue the conversation.")
-                    .primary_action(
-                        Button::new("new-thread", "Start New Thread").label_size(LabelSize::Small),
-                    )
-                    .secondary_action(
-                        Button::new("view-summary", "View Summary").label_size(LabelSize::Small),
-                    )
+                    .actions_slot(multiple_actions())
                     .into_any_element(),
             )
             .width(px(580.)),
             single_example(
                 "Multi-line Description",
                 Callout::new()
-                    .icon(
-                        Icon::new(IconName::Sparkle)
-                            .color(Color::Accent)
-                            .size(IconSize::Small),
-                    )
+                    .icon(IconName::Sparkle)
                     .title("Upgrade to Pro")
                     .description("• Unlimited threads\n• Priority support\n• Advanced analytics")
-                    .primary_action(
-                        Button::new("upgrade", "Upgrade Now").label_size(LabelSize::Small),
-                    )
-                    .secondary_action(
-                        Button::new("learn-more", "Learn More").label_size(LabelSize::Small),
-                    )
+                    .actions_slot(multiple_actions())
                     .into_any_element(),
             )
             .width(px(580.)),
         ];
 
+        let severity_examples = vec![
+            single_example(
+                "Info",
+                Callout::new()
+                    .icon(IconName::Info)
+                    .title("System maintenance scheduled for tonight")
+                    .actions_slot(single_action())
+                    .into_any_element(),
+            ),
+            single_example(
+                "Warning",
+                Callout::new()
+                    .severity(Severity::Warning)
+                    .icon(IconName::Triangle)
+                    .title("System maintenance scheduled for tonight")
+                    .actions_slot(single_action())
+                    .into_any_element(),
+            ),
+            single_example(
+                "Error",
+                Callout::new()
+                    .severity(Severity::Error)
+                    .icon(IconName::XCircle)
+                    .title("System maintenance scheduled for tonight")
+                    .actions_slot(single_action())
+                    .into_any_element(),
+            ),
+            single_example(
+                "Success",
+                Callout::new()
+                    .severity(Severity::Success)
+                    .icon(IconName::Check)
+                    .title("System maintenance scheduled for tonight")
+                    .actions_slot(single_action())
+                    .into_any_element(),
+            ),
+        ];
+
         Some(
-            example_group(callout_examples)
-                .vertical()
+            v_flex()
+                .gap_4()
+                .child(example_group(basic_examples).vertical())
+                .child(example_group_with_title("Severity", severity_examples).vertical())
                 .into_any_element(),
         )
     }

crates/ui/src/prelude.rs šŸ”—

@@ -14,7 +14,9 @@ pub use ui_macros::RegisterComponent;
 
 pub use crate::DynamicSpacing;
 pub use crate::animation::{AnimationDirection, AnimationDuration, DefaultAnimations};
-pub use crate::styles::{PlatformStyle, StyledTypography, TextSize, rems_from_px, vh, vw};
+pub use crate::styles::{
+    PlatformStyle, Severity, StyledTypography, TextSize, rems_from_px, vh, vw,
+};
 pub use crate::traits::clickable::*;
 pub use crate::traits::disableable::*;
 pub use crate::traits::fixed::*;

crates/ui/src/styles.rs šŸ”—

@@ -3,6 +3,7 @@ mod appearance;
 mod color;
 mod elevation;
 mod platform;
+mod severity;
 mod spacing;
 mod typography;
 mod units;
@@ -11,6 +12,7 @@ pub use appearance::*;
 pub use color::*;
 pub use elevation::*;
 pub use platform::*;
+pub use severity::*;
 pub use spacing::*;
 pub use typography::*;
 pub use units::*;

crates/ui/src/styles/severity.rs šŸ”—

@@ -0,0 +1,10 @@
+/// Severity levels that determine the style of the component.
+/// Usually, it affects the background. Most of the time,
+/// it also follows with an icon corresponding the severity level.
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub enum Severity {
+    Info,
+    Success,
+    Warning,
+    Error,
+}