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,84 +855,85 @@ impl AssistantPanel {
), ),
) )
.when(no_error, |parent| { .when(no_error, |parent| {
parent.child( parent
h_flex().child( .child(
Label::new("Ask and build anything.") h_flex().child(
.color(Color::Muted) Label::new("Ask and build anything.")
.mb_2p5(), .color(Color::Muted)
), .mb_2p5(),
) ),
.child( )
Button::new("new-thread", "Start New Thread") .child(
.icon(IconName::Plus) Button::new("new-thread", "Start New Thread")
.icon_position(IconPosition::Start) .icon(IconName::Plus)
.icon_size(IconSize::Small) .icon_position(IconPosition::Start)
.icon_color(Color::Muted) .icon_size(IconSize::Small)
.full_width() .icon_color(Color::Muted)
.key_binding(KeyBinding::for_action_in( .full_width()
&NewThread, .key_binding(KeyBinding::for_action_in(
&focus_handle, &NewThread,
window, &focus_handle,
cx, window,
)) cx,
.on_click(|_event, window, cx| { ))
window.dispatch_action(NewThread.boxed_clone(), cx) .on_click(|_event, window, cx| {
}), window.dispatch_action(NewThread.boxed_clone(), cx)
) }),
.child( )
Button::new("context", "Add Context") .child(
.icon(IconName::FileCode) Button::new("context", "Add Context")
.icon_position(IconPosition::Start) .icon(IconName::FileCode)
.icon_size(IconSize::Small) .icon_position(IconPosition::Start)
.icon_color(Color::Muted) .icon_size(IconSize::Small)
.full_width() .icon_color(Color::Muted)
.key_binding(KeyBinding::for_action_in( .full_width()
&ToggleContextPicker, .key_binding(KeyBinding::for_action_in(
&focus_handle, &ToggleContextPicker,
window, &focus_handle,
cx, window,
)) cx,
.on_click(|_event, window, cx| { ))
window.dispatch_action(ToggleContextPicker.boxed_clone(), cx) .on_click(|_event, window, cx| {
}), window.dispatch_action(ToggleContextPicker.boxed_clone(), cx)
) }),
.child( )
Button::new("mode", "Switch Model") .child(
.icon(IconName::DatabaseZap) Button::new("mode", "Switch Model")
.icon_position(IconPosition::Start) .icon(IconName::DatabaseZap)
.icon_size(IconSize::Small) .icon_position(IconPosition::Start)
.icon_color(Color::Muted) .icon_size(IconSize::Small)
.full_width() .icon_color(Color::Muted)
.key_binding(KeyBinding::for_action_in( .full_width()
&ToggleModelSelector, .key_binding(KeyBinding::for_action_in(
&focus_handle, &ToggleModelSelector,
window, &focus_handle,
cx, window,
)) cx,
.on_click(|_event, window, cx| { ))
window.dispatch_action(ToggleModelSelector.boxed_clone(), cx) .on_click(|_event, window, cx| {
}), window.dispatch_action(ToggleModelSelector.boxed_clone(), cx)
) }),
.child( )
Button::new("settings", "View Settings") .child(
.icon(IconName::Settings) Button::new("settings", "View Settings")
.icon_position(IconPosition::Start) .icon(IconName::Settings)
.icon_size(IconSize::Small) .icon_position(IconPosition::Start)
.icon_color(Color::Muted) .icon_size(IconSize::Small)
.full_width() .icon_color(Color::Muted)
.key_binding(KeyBinding::for_action_in( .full_width()
&OpenConfiguration, .key_binding(KeyBinding::for_action_in(
&focus_handle, &OpenConfiguration,
window, &focus_handle,
cx, window,
)) cx,
.on_click(|_event, window, cx| { ))
window.dispatch_action(OpenConfiguration.boxed_clone(), cx) .on_click(|_event, window, cx| {
}), window.dispatch_action(OpenConfiguration.boxed_clone(), cx)
) }),
)
}) })
.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,23 +962,27 @@ 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()
.gap_1() .gap_1()
.child( .child(
h_flex() h_flex()
.pl_1p5() .pl_1p5()
@ -992,32 +1000,94 @@ 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(
&OpenHistory, KeyBinding::for_action_in(
&self.focus_handle(cx), &OpenHistory,
window, &self.focus_handle(cx),
cx, window,
).map(|kb| kb.size(rems_from_px(12.))),) cx,
).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(
recent_history.into_iter().map(|entry| { v_flex()
// TODO: Add keyboard navigation. .gap_1()
match entry { .children(
HistoryEntry::Thread(thread) => { recent_history.into_iter().map(|entry| {
PastThread::new(thread, cx.entity().downgrade(), false) // TODO: Add keyboard navigation.
.into_any_element() match entry {
} HistoryEntry::Thread(thread) => {
HistoryEntry::Context(context) => { PastThread::new(thread, cx.entity().downgrade(), false)
PastContext::new(context, cx.entity().downgrade(), false) .into_any_element()
.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)] #[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,56 +401,83 @@ 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, Button::new("accept_terms", "I accept the Terms of Service")
LanguageModelProviderTosView::PromptEditorPopup => false, .when(!thread_empty_state, |this| {
LanguageModelProviderTosView::Configuration => false, this.full_width()
};
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")
.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() })
.disabled(accept_terms_disabled) .when(thread_empty_state, |this| {
.on_click({ this.style(ButtonStyle::Tinted(TintColor::Warning))
let state = state.downgrade(); .label_size(LabelSize::Small)
move |_, _window, cx| { })
state .disabled(accept_terms_disabled)
.update(cx, |state, cx| state.accept_terms_of_service(cx)) .on_click({
.ok(); let state = state.downgrade();
} move |_, _window, cx| {
}), state
); .update(cx, |state, cx| state.accept_terms_of_service(cx))
.ok();
}
}),
);
match view_kind { let form = if thread_empty_state {
LanguageModelProviderTosView::PromptEditorPopup => button_container.justify_end(), h_flex()
LanguageModelProviderTosView::Configuration => button_container.justify_start(), .w_full()
LanguageModelProviderTosView::ThreadEmptyState => button_container.justify_center(), .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()) 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