
This PR improves the consecutive tool call UX by allowing users to quickly continue an interrupted with one-click. What we do here is insert a hidden "Continue" message that will just nudge the LLM to keep going. We're also using the opportunity to upsell the previously called "Max Mode", now rebranded as "Burn Mode", which allows users to don't be interrupted anymore if they ever have 25 consecutive tool calls again. Release Notes: - agent: Improve consecutive tool call UX by allowing users to quickly continue an interrupted thread with one click. --------- Co-authored-by: Ben Brandt <benjamin.j.brandt@gmail.com> Co-authored-by: Agus Zubiaga <hi@aguz.me> Co-authored-by: Agus Zubiaga <agus@zed.dev>
195 lines
6.2 KiB
Rust
195 lines
6.2 KiB
Rust
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.
|
|
///
|
|
/// # Usage Example
|
|
///
|
|
/// ```
|
|
/// use ui::{Banner};
|
|
///
|
|
/// Banner::new()
|
|
/// .severity(Severity::Info)
|
|
/// .children(Label::new("This is an informational message"))
|
|
/// .action_slot(
|
|
/// Button::new("learn-more", "Learn More")
|
|
/// .icon(IconName::ArrowUpRight)
|
|
/// .icon_size(IconSize::XSmall)
|
|
/// .icon_position(IconPosition::End),
|
|
/// )
|
|
/// ```
|
|
#[derive(IntoElement, RegisterComponent)]
|
|
pub struct Banner {
|
|
severity: Severity,
|
|
children: Vec<AnyElement>,
|
|
icon: Option<(IconName, Option<Color>)>,
|
|
action_slot: Option<AnyElement>,
|
|
}
|
|
|
|
impl Banner {
|
|
/// Creates a new `Banner` component with default styling.
|
|
pub fn new() -> Self {
|
|
Self {
|
|
severity: Severity::Info,
|
|
children: Vec::new(),
|
|
icon: None,
|
|
action_slot: None,
|
|
}
|
|
}
|
|
|
|
/// Sets the severity of the banner.
|
|
pub fn severity(mut self, severity: Severity) -> Self {
|
|
self.severity = severity;
|
|
self
|
|
}
|
|
|
|
/// Sets an icon to display in the banner with an optional color.
|
|
pub fn icon(mut self, icon: IconName, color: Option<impl Into<Color>>) -> Self {
|
|
self.icon = Some((icon, color.map(|c| c.into())));
|
|
self
|
|
}
|
|
|
|
/// A slot for actions, such as CTA or dismissal buttons.
|
|
pub fn action_slot(mut self, element: impl IntoElement) -> Self {
|
|
self.action_slot = Some(element.into_any_element());
|
|
self
|
|
}
|
|
}
|
|
|
|
impl ParentElement for Banner {
|
|
fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
|
|
self.children.extend(elements)
|
|
}
|
|
}
|
|
|
|
impl RenderOnce for Banner {
|
|
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
|
|
let base = h_flex()
|
|
.py_0p5()
|
|
.rounded_sm()
|
|
.flex_wrap()
|
|
.justify_between()
|
|
.border_1();
|
|
|
|
let (icon, icon_color, bg_color, border_color) = match self.severity {
|
|
Severity::Info => (
|
|
IconName::Info,
|
|
Color::Muted,
|
|
cx.theme().status().info_background.opacity(0.5),
|
|
cx.theme().colors().border.opacity(0.5),
|
|
),
|
|
Severity::Success => (
|
|
IconName::Check,
|
|
Color::Success,
|
|
cx.theme().status().success.opacity(0.1),
|
|
cx.theme().status().success.opacity(0.2),
|
|
),
|
|
Severity::Warning => (
|
|
IconName::Warning,
|
|
Color::Warning,
|
|
cx.theme().status().warning_background.opacity(0.5),
|
|
cx.theme().status().warning_border.opacity(0.4),
|
|
),
|
|
Severity::Error => (
|
|
IconName::XCircle,
|
|
Color::Error,
|
|
cx.theme().status().error.opacity(0.1),
|
|
cx.theme().status().error.opacity(0.2),
|
|
),
|
|
};
|
|
|
|
let mut container = base.bg(bg_color).border_color(border_color);
|
|
|
|
let mut content_area = h_flex().id("content_area").gap_1p5().overflow_x_scroll();
|
|
|
|
if self.icon.is_none() {
|
|
content_area =
|
|
content_area.child(Icon::new(icon).size(IconSize::XSmall).color(icon_color));
|
|
}
|
|
|
|
content_area = content_area.children(self.children);
|
|
|
|
if let Some(action_slot) = self.action_slot {
|
|
container = container
|
|
.pl_2()
|
|
.pr_0p5()
|
|
.gap_2()
|
|
.child(content_area)
|
|
.child(action_slot);
|
|
} else {
|
|
container = container.px_2().child(div().w_full().child(content_area));
|
|
}
|
|
|
|
container
|
|
}
|
|
}
|
|
|
|
impl Component for Banner {
|
|
fn scope() -> ComponentScope {
|
|
ComponentScope::Notification
|
|
}
|
|
|
|
fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
|
|
let severity_examples = vec![
|
|
single_example(
|
|
"Default",
|
|
Banner::new()
|
|
.child(Label::new("This is a default banner with no customization"))
|
|
.into_any_element(),
|
|
),
|
|
single_example(
|
|
"Info",
|
|
Banner::new()
|
|
.severity(Severity::Info)
|
|
.child(Label::new("This is an informational message"))
|
|
.action_slot(
|
|
Button::new("learn-more", "Learn More")
|
|
.icon(IconName::ArrowUpRight)
|
|
.icon_size(IconSize::XSmall)
|
|
.icon_position(IconPosition::End),
|
|
)
|
|
.into_any_element(),
|
|
),
|
|
single_example(
|
|
"Success",
|
|
Banner::new()
|
|
.severity(Severity::Success)
|
|
.child(Label::new("Operation completed successfully"))
|
|
.action_slot(Button::new("dismiss", "Dismiss"))
|
|
.into_any_element(),
|
|
),
|
|
single_example(
|
|
"Warning",
|
|
Banner::new()
|
|
.severity(Severity::Warning)
|
|
.child(Label::new("Your settings file uses deprecated settings"))
|
|
.action_slot(Button::new("update", "Update Settings"))
|
|
.into_any_element(),
|
|
),
|
|
single_example(
|
|
"Error",
|
|
Banner::new()
|
|
.severity(Severity::Error)
|
|
.child(Label::new("Connection error: unable to connect to server"))
|
|
.action_slot(Button::new("reconnect", "Retry"))
|
|
.into_any_element(),
|
|
),
|
|
];
|
|
|
|
Some(
|
|
example_group(severity_examples)
|
|
.vertical()
|
|
.into_any_element(),
|
|
)
|
|
}
|
|
}
|