assistant: Show error messages in popover notification (#15808)

<img width="644" alt="image"
src="https://github.com/user-attachments/assets/ee6f2e60-e50a-481e-98b4-6c4b72a9b882">


Release Notes:

- N/A

---------

Co-authored-by: Thorsten <thorsten@zed.dev>
This commit is contained in:
Bennet Bo Fenner 2024-08-05 17:11:42 +02:00 committed by GitHub
parent 294892c470
commit be0ccf47ee
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 98 additions and 40 deletions

View file

@ -34,9 +34,9 @@ use fs::Fs;
use gpui::{ use gpui::{
div, percentage, point, Action, Animation, AnimationExt, AnyElement, AnyView, AppContext, div, percentage, point, Action, Animation, AnimationExt, AnyElement, AnyView, AppContext,
AsyncWindowContext, ClipboardItem, Context as _, DismissEvent, Empty, Entity, EventEmitter, AsyncWindowContext, ClipboardItem, Context as _, DismissEvent, Empty, Entity, EventEmitter,
FocusHandle, FocusableView, InteractiveElement, IntoElement, Model, ParentElement, Pixels, FocusHandle, FocusableView, FontWeight, InteractiveElement, IntoElement, Model, ParentElement,
Render, SharedString, StatefulInteractiveElement, Styled, Subscription, Task, Transformation, Pixels, Render, SharedString, StatefulInteractiveElement, Styled, Subscription, Task,
UpdateGlobal, View, ViewContext, VisualContext, WeakView, WindowContext, Transformation, UpdateGlobal, View, ViewContext, VisualContext, WeakView, WindowContext,
}; };
use indexed_docs::IndexedDocsStore; use indexed_docs::IndexedDocsStore;
use language::{ use language::{
@ -1316,6 +1316,7 @@ pub struct ContextEditor {
_subscriptions: Vec<Subscription>, _subscriptions: Vec<Subscription>,
active_edit_step: Option<ActiveEditStep>, active_edit_step: Option<ActiveEditStep>,
assistant_panel: WeakView<AssistantPanel>, assistant_panel: WeakView<AssistantPanel>,
error_message: Option<SharedString>,
} }
const DEFAULT_TAB_TITLE: &str = "New Context"; const DEFAULT_TAB_TITLE: &str = "New Context";
@ -1373,6 +1374,7 @@ impl ContextEditor {
_subscriptions, _subscriptions,
active_edit_step: None, active_edit_step: None,
assistant_panel, assistant_panel,
error_message: None,
}; };
this.update_message_headers(cx); this.update_message_headers(cx);
this.insert_slash_command_output_sections(sections, cx); this.insert_slash_command_output_sections(sections, cx);
@ -1406,7 +1408,9 @@ impl ContextEditor {
fn assist(&mut self, _: &Assist, cx: &mut ViewContext<Self>) { fn assist(&mut self, _: &Assist, cx: &mut ViewContext<Self>) {
if !self.apply_edit_step(cx) { if !self.apply_edit_step(cx) {
self.error_message = None;
self.send_to_model(cx); self.send_to_model(cx);
cx.notify();
} }
} }
@ -1804,6 +1808,9 @@ impl ContextEditor {
} }
} }
ContextEvent::Operation(_) => {} ContextEvent::Operation(_) => {}
ContextEvent::AssistError(error_message) => {
self.error_message = Some(SharedString::from(error_message.clone()));
}
} }
} }
@ -2142,7 +2149,10 @@ impl ContextEditor {
div() div()
.id("error") .id("error")
.tooltip(move |cx| Tooltip::text(error.clone(), cx)) .tooltip(move |cx| Tooltip::text(error.clone(), cx))
.child(Icon::new(IconName::XCircle)), .child(
Icon::new(IconName::ExclamationTriangle)
.color(Color::Error),
),
) )
} else { } else {
None None
@ -2327,25 +2337,33 @@ impl ContextEditor {
.unwrap_or_else(|| Cow::Borrowed(DEFAULT_TAB_TITLE)) .unwrap_or_else(|| Cow::Borrowed(DEFAULT_TAB_TITLE))
} }
fn render_notice(&self, cx: &mut ViewContext<Self>) -> Option<impl IntoElement> { fn dismiss_error_message(&mut self, cx: &mut ViewContext<Self>) {
self.error_message = None;
cx.notify();
}
fn render_notice(&self, cx: &mut ViewContext<Self>) -> Option<AnyElement> {
let nudge = self let nudge = self
.assistant_panel .assistant_panel
.upgrade() .upgrade()
.map(|assistant_panel| assistant_panel.read(cx).show_zed_ai_notice); .map(|assistant_panel| assistant_panel.read(cx).show_zed_ai_notice);
if nudge.unwrap_or(false) { if let Some(error) = self.error_message.clone() {
Some(Self::render_error_popover(error, cx).into_any_element())
} else if nudge.unwrap_or(false) {
Some( Some(
v_flex() v_flex()
.elevation_3(cx) .elevation_3(cx)
.p_4() .p_2()
.gap_2() .gap_2()
.child(Label::new("Use Zed AI"))
.child( .child(
div() Label::new("Use Zed AI")
.id("sign-in") .size(LabelSize::Small)
.child(Label::new("Sign in to use Zed AI")) .color(Color::Muted),
.cursor_pointer() )
.on_click(cx.listener(|this, _event, cx| { .child(h_flex().justify_end().child(
Button::new("sign-in", "Sign in to use Zed AI").on_click(cx.listener(
|this, _event, cx| {
let client = this let client = this
.workspace .workspace
.update(cx, |workspace, _| workspace.client().clone()) .update(cx, |workspace, _| workspace.client().clone())
@ -2358,8 +2376,10 @@ impl ContextEditor {
}) })
.detach_and_log_err(cx) .detach_and_log_err(cx)
} }
})), },
), )),
))
.into_any_element(),
) )
} else if let Some(configuration_error) = configuration_error(cx) { } else if let Some(configuration_error) = configuration_error(cx) {
let label = match configuration_error { let label = match configuration_error {
@ -2369,27 +2389,51 @@ impl ContextEditor {
Some( Some(
v_flex() v_flex()
.elevation_3(cx) .elevation_3(cx)
.p_4() .p_2()
.gap_2() .gap_2()
.child(Label::new(label)) .child(Label::new(label).size(LabelSize::Small).color(Color::Muted))
.child( .child(
div() h_flex().justify_end().child(
.id("open-configuration") Button::new("open-configuration", "Open configuration")
.child(Label::new("Open configuration")) .icon(IconName::Settings)
.cursor_pointer() .icon_size(IconSize::Small)
.on_click({ .on_click({
let focus_handle = self.focus_handle(cx).clone(); let focus_handle = self.focus_handle(cx).clone();
move |_event, cx| { move |_event, cx| {
focus_handle.dispatch_action(&ShowConfiguration, cx); focus_handle.dispatch_action(&ShowConfiguration, cx);
} }
}), }),
), ),
)
.into_any_element(),
) )
} else { } else {
None None
} }
} }
fn render_error_popover(error: SharedString, cx: &mut ViewContext<Self>) -> Div {
v_flex()
.p_2()
.elevation_2(cx)
.bg(cx.theme().colors().surface_background)
.min_w_24()
.occlude()
.child(
Label::new("Error interacting with language model")
.size(LabelSize::Small)
.weight(FontWeight::BOLD)
.color(Color::Muted),
)
.child(Label::new(error).size(LabelSize::Small))
.child(
h_flex().justify_end().child(
Button::new("dismiss", "Dismiss")
.on_click(cx.listener(|this, _, cx| this.dismiss_error_message(cx))),
),
)
}
fn render_send_button(&self, cx: &mut ViewContext<Self>) -> impl IntoElement { fn render_send_button(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let focus_handle = self.focus_handle(cx).clone(); let focus_handle = self.focus_handle(cx).clone();
let button_text = match self.workflow_step_for_cursor(cx) { let button_text = match self.workflow_step_for_cursor(cx) {
@ -2487,23 +2531,32 @@ impl Render for ContextEditor {
div() div()
.flex_grow() .flex_grow()
.bg(cx.theme().colors().editor_background) .bg(cx.theme().colors().editor_background)
.child(self.editor.clone()) .child(self.editor.clone()),
)
.child(
h_flex()
.flex_none()
.relative()
.when_some(self.render_notice(cx), |this, notice| {
this.child(
div()
.absolute()
.w_3_4()
.min_w_24()
.max_w_128()
.right_4()
.bottom_9()
.child(notice),
)
})
.child( .child(
h_flex() h_flex()
.w_full() .w_full()
.absolute() .absolute()
.bottom_0() .right_4()
.p_4() .bottom_2()
.justify_end() .justify_end()
.child( .child(self.render_send_button(cx)),
v_flex()
.gap_2()
.items_end()
.when_some(self.render_notice(cx), |this, notice| {
this.child(notice)
})
.child(self.render_send_button(cx)),
),
), ),
) )
} }

View file

@ -281,6 +281,7 @@ impl ContextOperation {
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub enum ContextEvent { pub enum ContextEvent {
AssistError(String),
MessagesEdited, MessagesEdited,
SummaryChanged, SummaryChanged,
EditStepsChanged, EditStepsChanged,
@ -1580,6 +1581,10 @@ impl Context {
.err() .err()
.map(|error| error.to_string().trim().to_string()); .map(|error| error.to_string().trim().to_string());
if let Some(error_message) = error_message.as_ref() {
cx.emit(ContextEvent::AssistError(error_message.to_string()));
}
this.update_metadata(assistant_message_id, cx, |metadata| { this.update_metadata(assistant_message_id, cx, |metadata| {
if let Some(error_message) = error_message.as_ref() { if let Some(error_message) = error_message.as_ref() {
metadata.status = metadata.status =