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 settings::{Settings, update_settings_file};
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 workspace::Workspace;
use workspace::dock::{DockPosition, Panel, PanelEvent};
@ -838,6 +840,7 @@ impl AssistantPanel {
v_flex()
.size_full()
.when(recent_history.is_empty(), |this| {
let configuration_error_ref = &configuration_error;
this.child(
v_flex()
.size_full()
@ -852,84 +855,85 @@ impl AssistantPanel {
),
)
.when(no_error, |parent| {
parent.child(
h_flex().child(
Label::new("Ask and build anything.")
.color(Color::Muted)
.mb_2p5(),
),
)
.child(
Button::new("new-thread", "Start New Thread")
.icon(IconName::Plus)
.icon_position(IconPosition::Start)
.icon_size(IconSize::Small)
.icon_color(Color::Muted)
.full_width()
.key_binding(KeyBinding::for_action_in(
&NewThread,
&focus_handle,
window,
cx,
))
.on_click(|_event, window, cx| {
window.dispatch_action(NewThread.boxed_clone(), cx)
}),
)
.child(
Button::new("context", "Add Context")
.icon(IconName::FileCode)
.icon_position(IconPosition::Start)
.icon_size(IconSize::Small)
.icon_color(Color::Muted)
.full_width()
.key_binding(KeyBinding::for_action_in(
&ToggleContextPicker,
&focus_handle,
window,
cx,
))
.on_click(|_event, window, cx| {
window.dispatch_action(ToggleContextPicker.boxed_clone(), cx)
}),
)
.child(
Button::new("mode", "Switch Model")
.icon(IconName::DatabaseZap)
.icon_position(IconPosition::Start)
.icon_size(IconSize::Small)
.icon_color(Color::Muted)
.full_width()
.key_binding(KeyBinding::for_action_in(
&ToggleModelSelector,
&focus_handle,
window,
cx,
))
.on_click(|_event, window, cx| {
window.dispatch_action(ToggleModelSelector.boxed_clone(), cx)
}),
)
.child(
Button::new("settings", "View Settings")
.icon(IconName::Settings)
.icon_position(IconPosition::Start)
.icon_size(IconSize::Small)
.icon_color(Color::Muted)
.full_width()
.key_binding(KeyBinding::for_action_in(
&OpenConfiguration,
&focus_handle,
window,
cx,
))
.on_click(|_event, window, cx| {
window.dispatch_action(OpenConfiguration.boxed_clone(), cx)
}),
)
parent
.child(
h_flex().child(
Label::new("Ask and build anything.")
.color(Color::Muted)
.mb_2p5(),
),
)
.child(
Button::new("new-thread", "Start New Thread")
.icon(IconName::Plus)
.icon_position(IconPosition::Start)
.icon_size(IconSize::Small)
.icon_color(Color::Muted)
.full_width()
.key_binding(KeyBinding::for_action_in(
&NewThread,
&focus_handle,
window,
cx,
))
.on_click(|_event, window, cx| {
window.dispatch_action(NewThread.boxed_clone(), cx)
}),
)
.child(
Button::new("context", "Add Context")
.icon(IconName::FileCode)
.icon_position(IconPosition::Start)
.icon_size(IconSize::Small)
.icon_color(Color::Muted)
.full_width()
.key_binding(KeyBinding::for_action_in(
&ToggleContextPicker,
&focus_handle,
window,
cx,
))
.on_click(|_event, window, cx| {
window.dispatch_action(ToggleContextPicker.boxed_clone(), cx)
}),
)
.child(
Button::new("mode", "Switch Model")
.icon(IconName::DatabaseZap)
.icon_position(IconPosition::Start)
.icon_size(IconSize::Small)
.icon_color(Color::Muted)
.full_width()
.key_binding(KeyBinding::for_action_in(
&ToggleModelSelector,
&focus_handle,
window,
cx,
))
.on_click(|_event, window, cx| {
window.dispatch_action(ToggleModelSelector.boxed_clone(), cx)
}),
)
.child(
Button::new("settings", "View Settings")
.icon(IconName::Settings)
.icon_position(IconPosition::Start)
.icon_size(IconSize::Small)
.icon_color(Color::Muted)
.full_width()
.key_binding(KeyBinding::for_action_in(
&OpenConfiguration,
&focus_handle,
window,
cx,
))
.on_click(|_event, window, cx| {
window.dispatch_action(OpenConfiguration.boxed_clone(), cx)
}),
)
})
.map(|parent| {
match configuration_error {
match configuration_error_ref {
Some(ConfigurationError::ProviderNotAuthenticated)
| Some(ConfigurationError::NoProvider) => {
parent
@ -958,23 +962,27 @@ impl AssistantPanel {
}),
)
}
Some(ConfigurationError::ProviderPendingTermsAcceptance(provider)) => parent
.children(
Some(ConfigurationError::ProviderPendingTermsAcceptance(provider)) => {
parent.children(
provider.render_accept_terms(
LanguageModelProviderTosView::ThreadEmptyState,
LanguageModelProviderTosView::ThreadFreshStart,
cx,
),
),
)
}
None => parent,
}
})
)
})
.when(!recent_history.is_empty(), |parent| {
let focus_handle = focus_handle.clone();
let configuration_error_ref = &configuration_error;
parent
.p_1p5()
.justify_end()
.gap_1()
.justify_end()
.gap_1()
.child(
h_flex()
.pl_1p5()
@ -992,32 +1000,94 @@ impl AssistantPanel {
Button::new("view-history", "View All")
.style(ButtonStyle::Subtle)
.label_size(LabelSize::Small)
.key_binding(KeyBinding::for_action_in(
&OpenHistory,
&self.focus_handle(cx),
window,
cx,
).map(|kb| kb.size(rems_from_px(12.))),)
.key_binding(
KeyBinding::for_action_in(
&OpenHistory,
&self.focus_handle(cx),
window,
cx,
).map(|kb| kb.size(rems_from_px(12.))),
)
.on_click(move |_event, window, cx| {
window.dispatch_action(OpenHistory.boxed_clone(), cx);
}),
),
)
.child(v_flex().gap_1().children(
recent_history.into_iter().map(|entry| {
// TODO: Add keyboard navigation.
match entry {
HistoryEntry::Thread(thread) => {
PastThread::new(thread, cx.entity().downgrade(), false)
.into_any_element()
}
HistoryEntry::Context(context) => {
PastContext::new(context, cx.entity().downgrade(), false)
.into_any_element()
}
.child(
v_flex()
.gap_1()
.children(
recent_history.into_iter().map(|entry| {
// TODO: Add keyboard navigation.
match entry {
HistoryEntry::Thread(thread) => {
PastThread::new(thread, cx.entity().downgrade(), false)
.into_any_element()
}
HistoryEntry::Context(context) => {
PastContext::new(context, cx.entity().downgrade(), false)
.into_any_element()
}
}
}),
)
)
.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)]
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,
Configuration,
}

View file

@ -401,56 +401,83 @@ fn render_accept_terms(
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")
.style(ButtonStyle::Subtle)
.icon(IconName::ArrowUpRight)
.icon_color(Color::Muted)
.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"));
let thread_view = match view_kind {
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")
let button_container = h_flex().child(
Button::new("accept_terms", "I accept the Terms of Service")
.when(!thread_empty_state, |this| {
this.full_width()
.style(ButtonStyle::Tinted(TintColor::Accent))
.icon(IconName::Check)
.icon_position(IconPosition::Start)
.icon_size(IconSize::Small)
.full_width()
.disabled(accept_terms_disabled)
.on_click({
let state = state.downgrade();
move |_, _window, cx| {
state
.update(cx, |state, cx| state.accept_terms_of_service(cx))
.ok();
}
}),
);
})
.when(thread_empty_state, |this| {
this.style(ButtonStyle::Tinted(TintColor::Warning))
.label_size(LabelSize::Small)
})
.disabled(accept_terms_disabled)
.on_click({
let state = state.downgrade();
move |_, _window, cx| {
state
.update(cx, |state, cx| state.accept_terms_of_service(cx))
.ok();
}
}),
);
match view_kind {
LanguageModelProviderTosView::PromptEditorPopup => button_container.justify_end(),
LanguageModelProviderTosView::Configuration => button_container.justify_start(),
LanguageModelProviderTosView::ThreadEmptyState => button_container.justify_center(),
}
});
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 {
LanguageModelProviderTosView::PromptEditorPopup => {
button_container.w_full().justify_end()
}
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())
}

View file

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