assistant2: Ensure errors are also displayed in populated new thread view (#27869)

Follow-up to https://github.com/zed-industries/zed/pull/27812

This PR makes sure these errors cases also show up in the panel's empty
state even when there is past data.

| No ToS | Missing Provider |
|--------|--------|
| ![CleanShot 2025-04-01 at 4  49
36@2x](https://github.com/user-attachments/assets/6da6bdc9-daa6-4a7b-a224-989eb845e205)
| ![CleanShot 2025-04-01 at 4  50
04@2x](https://github.com/user-attachments/assets/bddf62cb-3727-44b5-b115-9a88313c6d85)
|

Release Notes:

- N/A
This commit is contained in:
Danilo Leal 2025-04-01 17:06:34 -03:00 committed by GitHub
parent 92059803fb
commit 192097f58f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 243 additions and 143 deletions

View file

@ -26,7 +26,9 @@ use prompt_library::{PromptLibrary, open_prompt_library};
use prompt_store::PromptBuilder; use prompt_store::PromptBuilder;
use settings::{Settings, update_settings_file}; use settings::{Settings, update_settings_file};
use time::UtcOffset; use time::UtcOffset;
use ui::{ContextMenu, KeyBinding, PopoverMenu, PopoverMenuHandle, Tab, Tooltip, prelude::*}; use ui::{
Banner, ContextMenu, KeyBinding, PopoverMenu, PopoverMenuHandle, Tab, Tooltip, prelude::*,
};
use util::ResultExt as _; use util::ResultExt as _;
use workspace::Workspace; use workspace::Workspace;
use workspace::dock::{DockPosition, Panel, PanelEvent}; use workspace::dock::{DockPosition, Panel, PanelEvent};
@ -838,6 +840,7 @@ impl AssistantPanel {
v_flex() v_flex()
.size_full() .size_full()
.when(recent_history.is_empty(), |this| { .when(recent_history.is_empty(), |this| {
let configuration_error_ref = &configuration_error;
this.child( this.child(
v_flex() v_flex()
.size_full() .size_full()
@ -852,7 +855,8 @@ impl AssistantPanel {
), ),
) )
.when(no_error, |parent| { .when(no_error, |parent| {
parent.child( parent
.child(
h_flex().child( h_flex().child(
Label::new("Ask and build anything.") Label::new("Ask and build anything.")
.color(Color::Muted) .color(Color::Muted)
@ -929,7 +933,7 @@ impl AssistantPanel {
) )
}) })
.map(|parent| { .map(|parent| {
match configuration_error { match configuration_error_ref {
Some(ConfigurationError::ProviderNotAuthenticated) Some(ConfigurationError::ProviderNotAuthenticated)
| Some(ConfigurationError::NoProvider) => { | Some(ConfigurationError::NoProvider) => {
parent parent
@ -958,19 +962,23 @@ impl AssistantPanel {
}), }),
) )
} }
Some(ConfigurationError::ProviderPendingTermsAcceptance(provider)) => parent Some(ConfigurationError::ProviderPendingTermsAcceptance(provider)) => {
.children( parent.children(
provider.render_accept_terms( provider.render_accept_terms(
LanguageModelProviderTosView::ThreadEmptyState, LanguageModelProviderTosView::ThreadFreshStart,
cx, cx,
), ),
), )
}
None => parent, None => parent,
} }
}) })
) )
}) })
.when(!recent_history.is_empty(), |parent| { .when(!recent_history.is_empty(), |parent| {
let focus_handle = focus_handle.clone();
let configuration_error_ref = &configuration_error;
parent parent
.p_1p5() .p_1p5()
.justify_end() .justify_end()
@ -992,18 +1000,23 @@ impl AssistantPanel {
Button::new("view-history", "View All") Button::new("view-history", "View All")
.style(ButtonStyle::Subtle) .style(ButtonStyle::Subtle)
.label_size(LabelSize::Small) .label_size(LabelSize::Small)
.key_binding(KeyBinding::for_action_in( .key_binding(
KeyBinding::for_action_in(
&OpenHistory, &OpenHistory,
&self.focus_handle(cx), &self.focus_handle(cx),
window, window,
cx, cx,
).map(|kb| kb.size(rems_from_px(12.))),) ).map(|kb| kb.size(rems_from_px(12.))),
)
.on_click(move |_event, window, cx| { .on_click(move |_event, window, cx| {
window.dispatch_action(OpenHistory.boxed_clone(), cx); window.dispatch_action(OpenHistory.boxed_clone(), cx);
}), }),
), ),
) )
.child(v_flex().gap_1().children( .child(
v_flex()
.gap_1()
.children(
recent_history.into_iter().map(|entry| { recent_history.into_iter().map(|entry| {
// TODO: Add keyboard navigation. // TODO: Add keyboard navigation.
match entry { match entry {
@ -1017,7 +1030,64 @@ impl AssistantPanel {
} }
} }
}), }),
)) )
)
.map(|parent| {
match configuration_error_ref {
Some(ConfigurationError::ProviderNotAuthenticated)
| Some(ConfigurationError::NoProvider) => {
parent
.child(
Banner::new()
.severity(ui::Severity::Warning)
.children(
Label::new(
"Configure at least one LLM provider to start using the panel.",
)
.size(LabelSize::Small),
)
.action_slot(
Button::new("settings", "Configure Provider")
.style(ButtonStyle::Tinted(ui::TintColor::Warning))
.label_size(LabelSize::Small)
.key_binding(
KeyBinding::for_action_in(
&OpenConfiguration,
&focus_handle,
window,
cx,
)
.map(|kb| kb.size(rems_from_px(12.))),
)
.on_click(|_event, window, cx| {
window.dispatch_action(
OpenConfiguration.boxed_clone(),
cx,
)
}),
),
)
}
Some(ConfigurationError::ProviderPendingTermsAcceptance(provider)) => {
parent
.child(
Banner::new()
.severity(ui::Severity::Warning)
.children(
h_flex()
.w_full()
.children(
provider.render_accept_terms(
LanguageModelProviderTosView::ThreadtEmptyState,
cx,
),
),
),
)
}
None => parent,
}
})
}) })
} }

View file

@ -353,7 +353,10 @@ pub trait LanguageModelProvider: 'static {
#[derive(PartialEq, Eq)] #[derive(PartialEq, Eq)]
pub enum LanguageModelProviderTosView { pub enum LanguageModelProviderTosView {
ThreadEmptyState, /// When there are some past interactions in the Agent Panel.
ThreadtEmptyState,
/// When there are no past interactions in the Agent Panel.
ThreadFreshStart,
PromptEditorPopup, PromptEditorPopup,
Configuration, Configuration,
} }

View file

@ -401,39 +401,30 @@ fn render_accept_terms(
let accept_terms_disabled = state.read(cx).accept_terms.is_some(); let accept_terms_disabled = state.read(cx).accept_terms.is_some();
let thread_fresh_start = matches!(view_kind, LanguageModelProviderTosView::ThreadFreshStart);
let thread_empty_state = matches!(view_kind, LanguageModelProviderTosView::ThreadtEmptyState);
let terms_button = Button::new("terms_of_service", "Terms of Service") let terms_button = Button::new("terms_of_service", "Terms of Service")
.style(ButtonStyle::Subtle) .style(ButtonStyle::Subtle)
.icon(IconName::ArrowUpRight) .icon(IconName::ArrowUpRight)
.icon_color(Color::Muted) .icon_color(Color::Muted)
.icon_size(IconSize::XSmall) .icon_size(IconSize::XSmall)
.when(thread_empty_state, |this| this.label_size(LabelSize::Small))
.on_click(move |_, _window, cx| cx.open_url("https://zed.dev/terms-of-service")); .on_click(move |_, _window, cx| cx.open_url("https://zed.dev/terms-of-service"));
let thread_view = match view_kind { let button_container = h_flex().child(
LanguageModelProviderTosView::ThreadEmptyState => true,
LanguageModelProviderTosView::PromptEditorPopup => false,
LanguageModelProviderTosView::Configuration => false,
};
let form = v_flex()
.w_full()
.gap_2()
.child(
h_flex()
.flex_wrap()
.when(thread_view, |this| this.justify_center())
.child(Label::new(
"To start using Zed AI, please read and accept the",
))
.child(terms_button),
)
.child({
let button_container = h_flex().w_full().child(
Button::new("accept_terms", "I accept the Terms of Service") Button::new("accept_terms", "I accept the Terms of Service")
.when(!thread_empty_state, |this| {
this.full_width()
.style(ButtonStyle::Tinted(TintColor::Accent)) .style(ButtonStyle::Tinted(TintColor::Accent))
.icon(IconName::Check) .icon(IconName::Check)
.icon_position(IconPosition::Start) .icon_position(IconPosition::Start)
.icon_size(IconSize::Small) .icon_size(IconSize::Small)
.full_width() })
.when(thread_empty_state, |this| {
this.style(ButtonStyle::Tinted(TintColor::Warning))
.label_size(LabelSize::Small)
})
.disabled(accept_terms_disabled) .disabled(accept_terms_disabled)
.on_click({ .on_click({
let state = state.downgrade(); let state = state.downgrade();
@ -445,12 +436,48 @@ fn render_accept_terms(
}), }),
); );
let form = if thread_empty_state {
h_flex()
.w_full()
.flex_wrap()
.justify_between()
.child(
h_flex()
.child(
Label::new("To start using Zed AI, please read and accept the")
.size(LabelSize::Small),
)
.child(terms_button),
)
.child(button_container)
} else {
v_flex()
.w_full()
.gap_2()
.child(
h_flex()
.flex_wrap()
.when(thread_fresh_start, |this| this.justify_center())
.child(Label::new(
"To start using Zed AI, please read and accept the",
))
.child(terms_button),
)
.child({
match view_kind { match view_kind {
LanguageModelProviderTosView::PromptEditorPopup => button_container.justify_end(), LanguageModelProviderTosView::PromptEditorPopup => {
LanguageModelProviderTosView::Configuration => button_container.justify_start(), button_container.w_full().justify_end()
LanguageModelProviderTosView::ThreadEmptyState => button_container.justify_center(),
} }
}); LanguageModelProviderTosView::Configuration => {
button_container.w_full().justify_start()
}
LanguageModelProviderTosView::ThreadFreshStart => {
button_container.w_full().justify_center()
}
LanguageModelProviderTosView::ThreadtEmptyState => div().w_0(),
}
})
};
Some(form.into_any()) Some(form.into_any())
} }

View file

@ -130,7 +130,7 @@ impl RenderOnce for Banner {
.child(content_area) .child(content_area)
.child(action_slot); .child(action_slot);
} else { } else {
container = container.px_2().child(content_area); container = container.px_2().child(div().w_full().child(content_area));
} }
container container