agent: Use callout for displaying errors instead of toasts (#33680)
This PR makes all errors in the agent panel to use the `Callout` component instead of toasts. Reason for that is because the toasts obscured part of the panel's UI, which wasn't ideal. We can also be more expressive here with a background color, which I think helps with parsing the message. Release Notes: - agent: Improved how we display errors in the panel.
This commit is contained in:
parent
a5b2428897
commit
42f788185a
2 changed files with 155 additions and 174 deletions
|
@ -41,7 +41,7 @@ use editor::{Anchor, AnchorRangeExt as _, Editor, EditorEvent, MultiBuffer};
|
||||||
use fs::Fs;
|
use fs::Fs;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
Action, Animation, AnimationExt as _, AnyElement, App, AsyncWindowContext, ClipboardItem,
|
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,
|
KeyContext, Pixels, Subscription, Task, UpdateGlobal, WeakEntity, linear_color_stop,
|
||||||
linear_gradient, prelude::*, pulsating_between,
|
linear_gradient, prelude::*, pulsating_between,
|
||||||
};
|
};
|
||||||
|
@ -59,7 +59,7 @@ use theme::ThemeSettings;
|
||||||
use time::UtcOffset;
|
use time::UtcOffset;
|
||||||
use ui::utils::WithRemSize;
|
use ui::utils::WithRemSize;
|
||||||
use ui::{
|
use ui::{
|
||||||
Banner, CheckboxWithLabel, ContextMenu, ElevationIndex, KeyBinding, PopoverMenu,
|
Banner, Callout, CheckboxWithLabel, ContextMenu, ElevationIndex, KeyBinding, PopoverMenu,
|
||||||
PopoverMenuHandle, ProgressBar, Tab, Tooltip, Vector, VectorName, prelude::*,
|
PopoverMenuHandle, ProgressBar, Tab, Tooltip, Vector, VectorName, prelude::*,
|
||||||
};
|
};
|
||||||
use util::ResultExt as _;
|
use util::ResultExt as _;
|
||||||
|
@ -2689,58 +2689,90 @@ impl AgentPanel {
|
||||||
Some(div().px_2().pb_2().child(banner).into_any_element())
|
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(
|
fn render_payment_required_error(
|
||||||
&self,
|
&self,
|
||||||
thread: &Entity<ActiveThread>,
|
thread: &Entity<ActiveThread>,
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) -> AnyElement {
|
) -> 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.";
|
const ERROR_MESSAGE: &str =
|
||||||
|
"You reached your free usage limit. Upgrade to Zed Pro for more prompts.";
|
||||||
|
|
||||||
v_flex()
|
let icon = Icon::new(IconName::XCircle)
|
||||||
.gap_0p5()
|
.size(IconSize::Small)
|
||||||
.child(
|
.color(Color::Error);
|
||||||
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();
|
|
||||||
});
|
|
||||||
|
|
||||||
cx.open_url(&zed_urls::account_url(cx));
|
div()
|
||||||
cx.notify();
|
.border_t_1()
|
||||||
}
|
.border_color(cx.theme().colors().border)
|
||||||
})))
|
.child(
|
||||||
.child(Button::new("dismiss", "Dismiss").on_click(cx.listener({
|
Callout::new()
|
||||||
let thread = thread.clone();
|
.icon(icon)
|
||||||
move |_, _, _, cx| {
|
.title("Free Usage Exceeded")
|
||||||
thread.update(cx, |this, _cx| {
|
.description(ERROR_MESSAGE)
|
||||||
this.clear_last_error();
|
.tertiary_action(self.upgrade_button(thread, cx))
|
||||||
});
|
.secondary_action(self.create_copy_button(ERROR_MESSAGE))
|
||||||
|
.primary_action(self.dismiss_error_button(thread, cx))
|
||||||
cx.notify();
|
.bg_color(self.error_callout_bg(cx)),
|
||||||
}
|
|
||||||
}))),
|
|
||||||
)
|
)
|
||||||
.into_any()
|
.into_any_element()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_model_request_limit_reached_error(
|
fn render_model_request_limit_reached_error(
|
||||||
|
@ -2750,67 +2782,28 @@ impl AgentPanel {
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) -> AnyElement {
|
) -> AnyElement {
|
||||||
let error_message = match plan {
|
let error_message = match plan {
|
||||||
Plan::ZedPro => {
|
Plan::ZedPro => "Upgrade to usage-based billing for more prompts.",
|
||||||
"Model request limit reached. Upgrade to usage-based billing for more requests."
|
Plan::ZedProTrial | Plan::Free => "Upgrade to Zed Pro for more prompts.",
|
||||||
}
|
|
||||||
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",
|
|
||||||
};
|
};
|
||||||
|
|
||||||
v_flex()
|
let icon = Icon::new(IconName::XCircle)
|
||||||
.gap_0p5()
|
.size(IconSize::Small)
|
||||||
.child(
|
.color(Color::Error);
|
||||||
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));
|
div()
|
||||||
cx.notify();
|
.border_t_1()
|
||||||
}
|
.border_color(cx.theme().colors().border)
|
||||||
})),
|
.child(
|
||||||
)
|
Callout::new()
|
||||||
.child(Button::new("dismiss", "Dismiss").on_click(cx.listener({
|
.icon(icon)
|
||||||
let thread = thread.clone();
|
.title("Model Prompt Limit Reached")
|
||||||
move |_, _, _, cx| {
|
.description(error_message)
|
||||||
thread.update(cx, |this, _cx| {
|
.tertiary_action(self.upgrade_button(thread, cx))
|
||||||
this.clear_last_error();
|
.secondary_action(self.create_copy_button(error_message))
|
||||||
});
|
.primary_action(self.dismiss_error_button(thread, cx))
|
||||||
|
.bg_color(self.error_callout_bg(cx)),
|
||||||
cx.notify();
|
|
||||||
}
|
|
||||||
}))),
|
|
||||||
)
|
)
|
||||||
.into_any()
|
.into_any_element()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_error_message(
|
fn render_error_message(
|
||||||
|
@ -2821,40 +2814,24 @@ impl AgentPanel {
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) -> AnyElement {
|
) -> AnyElement {
|
||||||
let message_with_header = format!("{}\n{}", header, message);
|
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(
|
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 {
|
fn key_context(&self) -> KeyContext {
|
||||||
let mut key_context = KeyContext::new_with_defaults();
|
let mut key_context = KeyContext::new_with_defaults();
|
||||||
key_context.add("AgentPanel");
|
key_context.add("AgentPanel");
|
||||||
|
@ -3089,18 +3057,9 @@ impl Render for AgentPanel {
|
||||||
thread.clone().into_any_element()
|
thread.clone().into_any_element()
|
||||||
})
|
})
|
||||||
.children(self.render_tool_use_limit_reached(window, cx))
|
.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| {
|
.when_some(thread.read(cx).last_error(), |this, last_error| {
|
||||||
this.child(
|
this.child(
|
||||||
div()
|
div()
|
||||||
.absolute()
|
|
||||||
.right_3()
|
|
||||||
.bottom_12()
|
|
||||||
.max_w_96()
|
|
||||||
.py_2()
|
|
||||||
.px_3()
|
|
||||||
.elevation_2(cx)
|
|
||||||
.occlude()
|
|
||||||
.child(match last_error {
|
.child(match last_error {
|
||||||
ThreadError::PaymentRequired => {
|
ThreadError::PaymentRequired => {
|
||||||
self.render_payment_required_error(thread, cx)
|
self.render_payment_required_error(thread, cx)
|
||||||
|
@ -3114,6 +3073,7 @@ impl Render for AgentPanel {
|
||||||
.into_any(),
|
.into_any(),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
.child(h_flex().child(message_editor.clone()))
|
||||||
.child(self.render_drag_target(cx)),
|
.child(self.render_drag_target(cx)),
|
||||||
ActiveView::History => parent.child(self.history.clone()),
|
ActiveView::History => parent.child(self.history.clone()),
|
||||||
ActiveView::TextThread {
|
ActiveView::TextThread {
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
use gpui::AnyElement;
|
use gpui::{AnyElement, Hsla};
|
||||||
|
|
||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
|
|
||||||
|
@ -24,7 +24,9 @@ pub struct Callout {
|
||||||
description: Option<SharedString>,
|
description: Option<SharedString>,
|
||||||
primary_action: Option<AnyElement>,
|
primary_action: Option<AnyElement>,
|
||||||
secondary_action: Option<AnyElement>,
|
secondary_action: Option<AnyElement>,
|
||||||
|
tertiary_action: Option<AnyElement>,
|
||||||
line_height: Option<Pixels>,
|
line_height: Option<Pixels>,
|
||||||
|
bg_color: Option<Hsla>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Callout {
|
impl Callout {
|
||||||
|
@ -36,7 +38,9 @@ impl Callout {
|
||||||
description: None,
|
description: None,
|
||||||
primary_action: None,
|
primary_action: None,
|
||||||
secondary_action: None,
|
secondary_action: None,
|
||||||
|
tertiary_action: None,
|
||||||
line_height: None,
|
line_height: None,
|
||||||
|
bg_color: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -71,64 +75,81 @@ impl Callout {
|
||||||
self
|
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.
|
/// Sets a custom line height for the callout content.
|
||||||
pub fn line_height(mut self, line_height: Pixels) -> Self {
|
pub fn line_height(mut self, line_height: Pixels) -> Self {
|
||||||
self.line_height = Some(line_height);
|
self.line_height = Some(line_height);
|
||||||
self
|
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 {
|
impl RenderOnce for Callout {
|
||||||
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
|
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||||
let line_height = self.line_height.unwrap_or(window.line_height());
|
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()
|
h_flex()
|
||||||
.w_full()
|
|
||||||
.p_2()
|
.p_2()
|
||||||
.gap_2()
|
.gap_2()
|
||||||
.items_start()
|
.items_start()
|
||||||
.bg(cx.theme().colors().panel_background)
|
.bg(bg_color)
|
||||||
.overflow_x_hidden()
|
.overflow_x_hidden()
|
||||||
.when_some(self.icon, |this, icon| {
|
.when_some(self.icon, |this, icon| {
|
||||||
this.child(h_flex().h(line_height).justify_center().child(icon))
|
this.child(h_flex().h(line_height).justify_center().child(icon))
|
||||||
})
|
})
|
||||||
.child(
|
.child(
|
||||||
v_flex()
|
v_flex()
|
||||||
|
.min_w_0()
|
||||||
.w_full()
|
.w_full()
|
||||||
.child(
|
.child(
|
||||||
h_flex()
|
h_flex()
|
||||||
.h(line_height)
|
.h(line_height)
|
||||||
.w_full()
|
.w_full()
|
||||||
.gap_1()
|
.gap_1()
|
||||||
.flex_wrap()
|
|
||||||
.justify_between()
|
.justify_between()
|
||||||
.when_some(self.title, |this, title| {
|
.when_some(self.title, |this, title| {
|
||||||
this.child(h_flex().child(Label::new(title).size(LabelSize::Small)))
|
this.child(h_flex().child(Label::new(title).size(LabelSize::Small)))
|
||||||
})
|
})
|
||||||
.when(
|
.when(has_actions, |this| {
|
||||||
self.primary_action.is_some() || self.secondary_action.is_some(),
|
this.child(
|
||||||
|this| {
|
h_flex()
|
||||||
this.child(
|
.gap_0p5()
|
||||||
h_flex()
|
.when_some(self.tertiary_action, |this, action| {
|
||||||
.gap_0p5()
|
this.child(action)
|
||||||
.when_some(self.secondary_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.primary_action, |this, action| {
|
||||||
}),
|
this.child(action)
|
||||||
)
|
}),
|
||||||
},
|
)
|
||||||
),
|
}),
|
||||||
)
|
)
|
||||||
.when_some(self.description, |this, description| {
|
.when_some(self.description, |this, description| {
|
||||||
this.child(
|
this.child(
|
||||||
div()
|
div()
|
||||||
.w_full()
|
.w_full()
|
||||||
.flex_1()
|
.flex_1()
|
||||||
.child(description)
|
|
||||||
.text_ui_sm(cx)
|
.text_ui_sm(cx)
|
||||||
.text_color(cx.theme().colors().text_muted),
|
.text_color(cx.theme().colors().text_muted)
|
||||||
|
.child(description),
|
||||||
)
|
)
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue