@@ -41,7 +41,7 @@ use editor::{Anchor, AnchorRangeExt as _, Editor, EditorEvent, MultiBuffer};
use fs::Fs;
use gpui::{
Action, Animation, AnimationExt as _, AnyElement, App, AsyncWindowContext, ClipboardItem,
- Corner, DismissEvent, Entity, EventEmitter, ExternalPaths, FocusHandle, Focusable, FontWeight,
+ Corner, DismissEvent, Entity, EventEmitter, ExternalPaths, FocusHandle, Focusable, Hsla,
KeyContext, Pixels, Subscription, Task, UpdateGlobal, WeakEntity, linear_color_stop,
linear_gradient, prelude::*, pulsating_between,
};
@@ -59,7 +59,7 @@ use theme::ThemeSettings;
use time::UtcOffset;
use ui::utils::WithRemSize;
use ui::{
- Banner, CheckboxWithLabel, ContextMenu, ElevationIndex, KeyBinding, PopoverMenu,
+ Banner, Callout, CheckboxWithLabel, ContextMenu, ElevationIndex, KeyBinding, PopoverMenu,
PopoverMenuHandle, ProgressBar, Tab, Tooltip, Vector, VectorName, prelude::*,
};
use util::ResultExt as _;
@@ -2689,58 +2689,90 @@ impl AgentPanel {
Some(div().px_2().pb_2().child(banner).into_any_element())
}
+ fn create_copy_button(&self, message: impl Into<String>) -> impl IntoElement {
+ let message = message.into();
+
+ 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()))
+ })
+ }
+
+ fn dismiss_error_button(
+ &self,
+ thread: &Entity<ActiveThread>,
+ cx: &mut Context<Self>,
+ ) -> impl IntoElement {
+ IconButton::new("dismiss", IconName::Close)
+ .icon_size(IconSize::Small)
+ .icon_color(Color::Muted)
+ .tooltip(Tooltip::text("Dismiss Error"))
+ .on_click(cx.listener({
+ let thread = thread.clone();
+ move |_, _, _, cx| {
+ thread.update(cx, |this, _cx| {
+ this.clear_last_error();
+ });
+
+ cx.notify();
+ }
+ }))
+ }
+
+ fn upgrade_button(
+ &self,
+ thread: &Entity<ActiveThread>,
+ cx: &mut Context<Self>,
+ ) -> impl IntoElement {
+ Button::new("upgrade", "Upgrade")
+ .label_size(LabelSize::Small)
+ .style(ButtonStyle::Tinted(ui::TintColor::Accent))
+ .on_click(cx.listener({
+ let thread = thread.clone();
+ move |_, _, _, cx| {
+ thread.update(cx, |this, _cx| {
+ this.clear_last_error();
+ });
+
+ cx.open_url(&zed_urls::account_url(cx));
+ cx.notify();
+ }
+ }))
+ }
+
+ 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>,
cx: &mut Context<Self>,
) -> AnyElement {
- const ERROR_MESSAGE: &str = "Free tier exceeded. Subscribe and add payment to continue using Zed LLMs. You'll be billed at cost for tokens used.";
-
- v_flex()
- .gap_0p5()
- .child(
- h_flex()
- .gap_1p5()
- .items_center()
- .child(Icon::new(IconName::XCircle).color(Color::Error))
- .child(Label::new("Free Usage Exceeded").weight(FontWeight::MEDIUM)),
- )
- .child(
- div()
- .id("error-message")
- .max_h_24()
- .overflow_y_scroll()
- .child(Label::new(ERROR_MESSAGE)),
- )
- .child(
- h_flex()
- .justify_end()
- .mt_1()
- .gap_1()
- .child(self.create_copy_button(ERROR_MESSAGE))
- .child(Button::new("subscribe", "Subscribe").on_click(cx.listener({
- let thread = thread.clone();
- move |_, _, _, cx| {
- thread.update(cx, |this, _cx| {
- this.clear_last_error();
- });
+ const ERROR_MESSAGE: &str =
+ "You reached your free usage limit. Upgrade to Zed Pro for more prompts.";
- cx.open_url(&zed_urls::account_url(cx));
- cx.notify();
- }
- })))
- .child(Button::new("dismiss", "Dismiss").on_click(cx.listener({
- let thread = thread.clone();
- move |_, _, _, cx| {
- thread.update(cx, |this, _cx| {
- this.clear_last_error();
- });
+ let icon = Icon::new(IconName::XCircle)
+ .size(IconSize::Small)
+ .color(Color::Error);
- cx.notify();
- }
- }))),
+ 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)),
)
- .into_any()
+ .into_any_element()
}
fn render_model_request_limit_reached_error(
@@ -2750,67 +2782,28 @@ impl AgentPanel {
cx: &mut Context<Self>,
) -> AnyElement {
let error_message = match plan {
- Plan::ZedPro => {
- "Model request limit reached. Upgrade to usage-based billing for more requests."
- }
- Plan::ZedProTrial => {
- "Model request limit reached. Upgrade to Zed Pro for more requests."
- }
- Plan::Free => "Model request limit reached. Upgrade to Zed Pro for more requests.",
- };
- let call_to_action = match plan {
- Plan::ZedPro => "Upgrade to usage-based billing",
- Plan::ZedProTrial => "Upgrade to Zed Pro",
- Plan::Free => "Upgrade to Zed Pro",
+ Plan::ZedPro => "Upgrade to usage-based billing for more prompts.",
+ Plan::ZedProTrial | Plan::Free => "Upgrade to Zed Pro for more prompts.",
};
- v_flex()
- .gap_0p5()
- .child(
- h_flex()
- .gap_1p5()
- .items_center()
- .child(Icon::new(IconName::XCircle).color(Color::Error))
- .child(Label::new("Model Request Limit Reached").weight(FontWeight::MEDIUM)),
- )
- .child(
- div()
- .id("error-message")
- .max_h_24()
- .overflow_y_scroll()
- .child(Label::new(error_message)),
- )
- .child(
- h_flex()
- .justify_end()
- .mt_1()
- .gap_1()
- .child(self.create_copy_button(error_message))
- .child(
- Button::new("subscribe", call_to_action).on_click(cx.listener({
- let thread = thread.clone();
- move |_, _, _, cx| {
- thread.update(cx, |this, _cx| {
- this.clear_last_error();
- });
-
- cx.open_url(&zed_urls::account_url(cx));
- cx.notify();
- }
- })),
- )
- .child(Button::new("dismiss", "Dismiss").on_click(cx.listener({
- let thread = thread.clone();
- move |_, _, _, cx| {
- thread.update(cx, |this, _cx| {
- this.clear_last_error();
- });
+ let icon = Icon::new(IconName::XCircle)
+ .size(IconSize::Small)
+ .color(Color::Error);
- cx.notify();
- }
- }))),
+ 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)),
)
- .into_any()
+ .into_any_element()
}
fn render_error_message(
@@ -2821,40 +2814,24 @@ impl AgentPanel {
cx: &mut Context<Self>,
) -> AnyElement {
let message_with_header = format!("{}\n{}", header, message);
- v_flex()
- .gap_0p5()
- .child(
- h_flex()
- .gap_1p5()
- .items_center()
- .child(Icon::new(IconName::XCircle).color(Color::Error))
- .child(Label::new(header).weight(FontWeight::MEDIUM)),
- )
- .child(
- div()
- .id("error-message")
- .max_h_32()
- .overflow_y_scroll()
- .child(Label::new(message.clone())),
- )
- .child(
- h_flex()
- .justify_end()
- .mt_1()
- .gap_1()
- .child(self.create_copy_button(message_with_header))
- .child(Button::new("dismiss", "Dismiss").on_click(cx.listener({
- let thread = thread.clone();
- move |_, _, _, cx| {
- thread.update(cx, |this, _cx| {
- this.clear_last_error();
- });
- cx.notify();
- }
- }))),
+ 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(header)
+ .description(message.clone())
+ .primary_action(self.dismiss_error_button(thread, cx))
+ .secondary_action(self.create_copy_button(message_with_header))
+ .bg_color(self.error_callout_bg(cx)),
)
- .into_any()
+ .into_any_element()
}
fn render_prompt_editor(
@@ -2999,15 +2976,6 @@ impl AgentPanel {
}
}
- fn create_copy_button(&self, message: impl Into<String>) -> impl IntoElement {
- let message = message.into();
- IconButton::new("copy", IconName::Copy)
- .on_click(move |_, _, cx| {
- cx.write_to_clipboard(ClipboardItem::new_string(message.clone()))
- })
- .tooltip(Tooltip::text("Copy Error Message"))
- }
-
fn key_context(&self) -> KeyContext {
let mut key_context = KeyContext::new_with_defaults();
key_context.add("AgentPanel");
@@ -3089,18 +3057,9 @@ impl Render for AgentPanel {
thread.clone().into_any_element()
})
.children(self.render_tool_use_limit_reached(window, cx))
- .child(h_flex().child(message_editor.clone()))
.when_some(thread.read(cx).last_error(), |this, last_error| {
this.child(
div()
- .absolute()
- .right_3()
- .bottom_12()
- .max_w_96()
- .py_2()
- .px_3()
- .elevation_2(cx)
- .occlude()
.child(match last_error {
ThreadError::PaymentRequired => {
self.render_payment_required_error(thread, cx)
@@ -3114,6 +3073,7 @@ impl Render for AgentPanel {
.into_any(),
)
})
+ .child(h_flex().child(message_editor.clone()))
.child(self.render_drag_target(cx)),
ActiveView::History => parent.child(self.history.clone()),
ActiveView::TextThread {
@@ -1,4 +1,4 @@
-use gpui::AnyElement;
+use gpui::{AnyElement, Hsla};
use crate::prelude::*;
@@ -24,7 +24,9 @@ pub struct Callout {
description: Option<SharedString>,
primary_action: Option<AnyElement>,
secondary_action: Option<AnyElement>,
+ tertiary_action: Option<AnyElement>,
line_height: Option<Pixels>,
+ bg_color: Option<Hsla>,
}
impl Callout {
@@ -36,7 +38,9 @@ impl Callout {
description: None,
primary_action: None,
secondary_action: None,
+ tertiary_action: None,
line_height: None,
+ bg_color: None,
}
}
@@ -71,64 +75,81 @@ impl Callout {
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());
+ self
+ }
+
/// Sets a custom line height for the callout content.
pub fn line_height(mut self, line_height: Pixels) -> Self {
self.line_height = Some(line_height);
self
}
+
+ /// Sets a custom background color for the callout content.
+ pub fn bg_color(mut self, color: Hsla) -> Self {
+ self.bg_color = Some(color);
+ self
+ }
}
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();
h_flex()
- .w_full()
.p_2()
.gap_2()
.items_start()
- .bg(cx.theme().colors().panel_background)
+ .bg(bg_color)
.overflow_x_hidden()
.when_some(self.icon, |this, icon| {
this.child(h_flex().h(line_height).justify_center().child(icon))
})
.child(
v_flex()
+ .min_w_0()
.w_full()
.child(
h_flex()
.h(line_height)
.w_full()
.gap_1()
- .flex_wrap()
.justify_between()
.when_some(self.title, |this, title| {
this.child(h_flex().child(Label::new(title).size(LabelSize::Small)))
})
- .when(
- self.primary_action.is_some() || self.secondary_action.is_some(),
- |this| {
- this.child(
- h_flex()
- .gap_0p5()
- .when_some(self.secondary_action, |this, action| {
- this.child(action)
- })
- .when_some(self.primary_action, |this, action| {
- this.child(action)
- }),
- )
- },
- ),
+ .when(has_actions, |this| {
+ this.child(
+ h_flex()
+ .gap_0p5()
+ .when_some(self.tertiary_action, |this, action| {
+ this.child(action)
+ })
+ .when_some(self.secondary_action, |this, action| {
+ this.child(action)
+ })
+ .when_some(self.primary_action, |this, action| {
+ this.child(action)
+ }),
+ )
+ }),
)
.when_some(self.description, |this, description| {
this.child(
div()
.w_full()
.flex_1()
- .child(description)
.text_ui_sm(cx)
- .text_color(cx.theme().colors().text_muted),
+ .text_color(cx.theme().colors().text_muted)
+ .child(description),
)
}),
)