thread_view: Adjust empty state and error displays (#36774)

Also changes the message editor placeholder depending on the agent.

Release Notes:

- N/A

---------

Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
This commit is contained in:
Danilo Leal 2025-08-22 17:40:52 -03:00 committed by GitHub
parent 896a35f7be
commit 639417c2bc
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 254 additions and 267 deletions

View file

@ -23,11 +23,11 @@ impl NativeAgentServer {
impl AgentServer for NativeAgentServer { impl AgentServer for NativeAgentServer {
fn name(&self) -> &'static str { fn name(&self) -> &'static str {
"Native Agent" "Zed Agent"
} }
fn empty_state_headline(&self) -> &'static str { fn empty_state_headline(&self) -> &'static str {
"Welcome to the Agent Panel" self.name()
} }
fn empty_state_message(&self) -> &'static str { fn empty_state_message(&self) -> &'static str {

View file

@ -258,6 +258,7 @@ pub struct AcpThreadView {
hovered_recent_history_item: Option<usize>, hovered_recent_history_item: Option<usize>,
entry_view_state: Entity<EntryViewState>, entry_view_state: Entity<EntryViewState>,
message_editor: Entity<MessageEditor>, message_editor: Entity<MessageEditor>,
focus_handle: FocusHandle,
model_selector: Option<Entity<AcpModelSelectorPopover>>, model_selector: Option<Entity<AcpModelSelectorPopover>>,
profile_selector: Option<Entity<ProfileSelector>>, profile_selector: Option<Entity<ProfileSelector>>,
notifications: Vec<WindowHandle<AgentNotification>>, notifications: Vec<WindowHandle<AgentNotification>>,
@ -312,6 +313,13 @@ impl AcpThreadView {
) -> Self { ) -> Self {
let prompt_capabilities = Rc::new(Cell::new(acp::PromptCapabilities::default())); let prompt_capabilities = Rc::new(Cell::new(acp::PromptCapabilities::default()));
let prevent_slash_commands = agent.clone().downcast::<ClaudeCode>().is_some(); let prevent_slash_commands = agent.clone().downcast::<ClaudeCode>().is_some();
let placeholder = if agent.name() == "Zed Agent" {
format!("Message the {} — @ to include context", agent.name())
} else {
format!("Message {} — @ to include context", agent.name())
};
let message_editor = cx.new(|cx| { let message_editor = cx.new(|cx| {
let mut editor = MessageEditor::new( let mut editor = MessageEditor::new(
workspace.clone(), workspace.clone(),
@ -319,7 +327,7 @@ impl AcpThreadView {
history_store.clone(), history_store.clone(),
prompt_store.clone(), prompt_store.clone(),
prompt_capabilities.clone(), prompt_capabilities.clone(),
"Message the agent — @ to include context", placeholder,
prevent_slash_commands, prevent_slash_commands,
editor::EditorMode::AutoHeight { editor::EditorMode::AutoHeight {
min_lines: MIN_EDITOR_LINES, min_lines: MIN_EDITOR_LINES,
@ -381,6 +389,7 @@ impl AcpThreadView {
prompt_capabilities, prompt_capabilities,
_subscriptions: subscriptions, _subscriptions: subscriptions,
_cancel_task: None, _cancel_task: None,
focus_handle: cx.focus_handle(),
} }
} }
@ -404,8 +413,12 @@ impl AcpThreadView {
let connection = match connect_task.await { let connection = match connect_task.await {
Ok(connection) => connection, Ok(connection) => connection,
Err(err) => { Err(err) => {
this.update(cx, |this, cx| { this.update_in(cx, |this, window, cx| {
this.handle_load_error(err, cx); if err.downcast_ref::<LoadError>().is_some() {
this.handle_load_error(err, window, cx);
} else {
this.handle_thread_error(err, cx);
}
cx.notify(); cx.notify();
}) })
.log_err(); .log_err();
@ -522,6 +535,7 @@ impl AcpThreadView {
title_editor, title_editor,
_subscriptions: subscriptions, _subscriptions: subscriptions,
}; };
this.message_editor.focus_handle(cx).focus(window);
this.profile_selector = this.as_native_thread(cx).map(|thread| { this.profile_selector = this.as_native_thread(cx).map(|thread| {
cx.new(|cx| { cx.new(|cx| {
@ -537,7 +551,7 @@ impl AcpThreadView {
cx.notify(); cx.notify();
} }
Err(err) => { Err(err) => {
this.handle_load_error(err, cx); this.handle_load_error(err, window, cx);
} }
}; };
}) })
@ -606,17 +620,28 @@ impl AcpThreadView {
.map(|desc| cx.new(|cx| Markdown::new(desc.into(), None, None, cx))), .map(|desc| cx.new(|cx| Markdown::new(desc.into(), None, None, cx))),
_subscription: subscription, _subscription: subscription,
}; };
if this.message_editor.focus_handle(cx).is_focused(window) {
this.focus_handle.focus(window)
}
cx.notify(); cx.notify();
}) })
.ok(); .ok();
} }
fn handle_load_error(&mut self, err: anyhow::Error, cx: &mut Context<Self>) { fn handle_load_error(
&mut self,
err: anyhow::Error,
window: &mut Window,
cx: &mut Context<Self>,
) {
if let Some(load_err) = err.downcast_ref::<LoadError>() { if let Some(load_err) = err.downcast_ref::<LoadError>() {
self.thread_state = ThreadState::LoadError(load_err.clone()); self.thread_state = ThreadState::LoadError(load_err.clone());
} else { } else {
self.thread_state = ThreadState::LoadError(LoadError::Other(err.to_string().into())) self.thread_state = ThreadState::LoadError(LoadError::Other(err.to_string().into()))
} }
if self.message_editor.focus_handle(cx).is_focused(window) {
self.focus_handle.focus(window)
}
cx.notify(); cx.notify();
} }
@ -633,12 +658,11 @@ impl AcpThreadView {
} }
} }
pub fn title(&self, cx: &App) -> SharedString { pub fn title(&self) -> SharedString {
match &self.thread_state { match &self.thread_state {
ThreadState::Ready { thread, .. } => thread.read(cx).title(), ThreadState::Ready { .. } | ThreadState::Unauthenticated { .. } => "New Thread".into(),
ThreadState::Loading { .. } => "Loading…".into(), ThreadState::Loading { .. } => "Loading…".into(),
ThreadState::LoadError(_) => "Failed to load".into(), ThreadState::LoadError(_) => "Failed to load".into(),
ThreadState::Unauthenticated { .. } => "Authentication Required".into(),
} }
} }
@ -1069,6 +1093,9 @@ impl AcpThreadView {
AcpThreadEvent::LoadError(error) => { AcpThreadEvent::LoadError(error) => {
self.thread_retry_status.take(); self.thread_retry_status.take();
self.thread_state = ThreadState::LoadError(error.clone()); self.thread_state = ThreadState::LoadError(error.clone());
if self.message_editor.focus_handle(cx).is_focused(window) {
self.focus_handle.focus(window)
}
} }
AcpThreadEvent::TitleUpdated => { AcpThreadEvent::TitleUpdated => {
let title = thread.read(cx).title(); let title = thread.read(cx).title();
@ -2338,33 +2365,6 @@ impl AcpThreadView {
.into_any() .into_any()
} }
fn render_agent_logo(&self) -> AnyElement {
Icon::new(self.agent.logo())
.color(Color::Muted)
.size(IconSize::XLarge)
.into_any_element()
}
fn render_error_agent_logo(&self) -> AnyElement {
let logo = Icon::new(self.agent.logo())
.color(Color::Muted)
.size(IconSize::XLarge)
.into_any_element();
h_flex()
.relative()
.justify_center()
.child(div().opacity(0.3).child(logo))
.child(
h_flex()
.absolute()
.right_1()
.bottom_0()
.child(Icon::new(IconName::XCircleFilled).color(Color::Error)),
)
.into_any_element()
}
fn render_rules_item(&self, cx: &Context<Self>) -> Option<AnyElement> { fn render_rules_item(&self, cx: &Context<Self>) -> Option<AnyElement> {
let project_context = self let project_context = self
.as_native_thread(cx)? .as_native_thread(cx)?
@ -2493,8 +2493,7 @@ impl AcpThreadView {
) )
} }
fn render_empty_state(&self, window: &mut Window, cx: &mut Context<Self>) -> AnyElement { fn render_recent_history(&self, window: &mut Window, cx: &mut Context<Self>) -> AnyElement {
let loading = matches!(&self.thread_state, ThreadState::Loading { .. });
let render_history = self let render_history = self
.agent .agent
.clone() .clone()
@ -2506,38 +2505,6 @@ impl AcpThreadView {
v_flex() v_flex()
.size_full() .size_full()
.when(!render_history, |this| {
this.child(
v_flex()
.size_full()
.items_center()
.justify_center()
.child(if loading {
h_flex()
.justify_center()
.child(self.render_agent_logo())
.with_animation(
"pulsating_icon",
Animation::new(Duration::from_secs(2))
.repeat()
.with_easing(pulsating_between(0.4, 1.0)),
|icon, delta| icon.opacity(delta),
)
.into_any()
} else {
self.render_agent_logo().into_any_element()
})
.child(h_flex().mt_4().mb_2().justify_center().child(if loading {
div()
.child(LoadingLabel::new("").size(LabelSize::Large))
.into_any_element()
} else {
Headline::new(self.agent.empty_state_headline())
.size(HeadlineSize::Medium)
.into_any_element()
})),
)
})
.when(render_history, |this| { .when(render_history, |this| {
let recent_history: Vec<_> = self.history_store.update(cx, |history_store, _| { let recent_history: Vec<_> = self.history_store.update(cx, |history_store, _| {
history_store.entries().take(3).collect() history_store.entries().take(3).collect()
@ -2612,196 +2579,118 @@ impl AcpThreadView {
window: &mut Window, window: &mut Window,
cx: &Context<Self>, cx: &Context<Self>,
) -> Div { ) -> Div {
v_flex() v_flex().flex_1().size_full().justify_end().child(
.p_2() v_flex()
.gap_2() .p_2()
.flex_1() .pr_3()
.items_center() .w_full()
.justify_center() .border_t_1()
.child( .border_color(cx.theme().colors().border)
v_flex() .bg(cx.theme().status().warning.opacity(0.04))
.items_center() .child(
.justify_center() h_flex()
.child(self.render_error_agent_logo()) .gap_1p5()
.child( .child(
h_flex().mt_4().mb_1().justify_center().child( Icon::new(IconName::Warning)
Headline::new("Authentication Required").size(HeadlineSize::Medium), .color(Color::Warning)
.size(IconSize::Small),
)
.child(Label::new("Authentication Required")),
)
.children(description.map(|desc| {
div().text_ui(cx).child(self.render_markdown(
desc.clone(),
default_markdown_style(false, false, window, cx),
))
}))
.children(
configuration_view
.cloned()
.map(|view| div().w_full().child(view)),
)
.when(
configuration_view.is_none()
&& description.is_none()
&& pending_auth_method.is_none(),
|el| {
el.child(
Label::new(format!(
"You are not currently authenticated with {}. Please choose one of the following options:",
self.agent.name()
))
.color(Color::Muted)
.mb_1()
.ml_5(),
)
},
)
.when(!connection.auth_methods().is_empty(), |this| {
this.child(
h_flex().justify_end().flex_wrap().gap_1().children(
connection.auth_methods().iter().enumerate().rev().map(
|(ix, method)| {
Button::new(
SharedString::from(method.id.0.clone()),
method.name.clone(),
)
.when(ix == 0, |el| {
el.style(ButtonStyle::Tinted(ui::TintColor::Warning))
})
.on_click({
let method_id = method.id.clone();
cx.listener(move |this, _, window, cx| {
this.authenticate(method_id.clone(), window, cx)
})
})
},
),
), ),
) )
.into_any(), })
) .when_some(pending_auth_method, |el, _| {
.children(description.map(|desc| {
div().text_ui(cx).text_center().child(self.render_markdown(
desc.clone(),
default_markdown_style(false, false, window, cx),
))
}))
.children(
configuration_view
.cloned()
.map(|view| div().px_4().w_full().max_w_128().child(view)),
)
.when(
configuration_view.is_none()
&& description.is_none()
&& pending_auth_method.is_none(),
|el| {
el.child( el.child(
div() h_flex()
.text_ui(cx) .py_4()
.text_center()
.px_4()
.w_full() .w_full()
.max_w_128() .justify_center()
.child(Label::new("Authentication required")), .gap_1()
) .child(
}, Icon::new(IconName::ArrowCircle)
) .size(IconSize::Small)
.when_some(pending_auth_method, |el, _| { .color(Color::Muted)
let spinner_icon = div() .with_animation(
.px_0p5() "arrow-circle",
.id("generating") Animation::new(Duration::from_secs(2)).repeat(),
.tooltip(Tooltip::text("Generating Changes…")) |icon, delta| {
.child( icon.transform(Transformation::rotate(percentage(
Icon::new(IconName::ArrowCircle) delta,
.size(IconSize::Small) )))
.with_animation( },
"arrow-circle", )
Animation::new(Duration::from_secs(2)).repeat(), .into_any_element(),
|icon, delta| {
icon.transform(Transformation::rotate(percentage(delta)))
},
) )
.into_any_element(), .child(Label::new("Authenticating…")),
) )
.into_any(); }),
el.child( )
h_flex()
.text_ui(cx)
.text_center()
.justify_center()
.gap_2()
.px_4()
.w_full()
.max_w_128()
.child(Label::new("Authenticating..."))
.child(spinner_icon),
)
})
.child(
h_flex()
.mt_1p5()
.gap_1()
.flex_wrap()
.justify_center()
.children(connection.auth_methods().iter().enumerate().rev().map(
|(ix, method)| {
Button::new(
SharedString::from(method.id.0.clone()),
method.name.clone(),
)
.style(ButtonStyle::Outlined)
.when(ix == 0, |el| {
el.style(ButtonStyle::Tinted(ui::TintColor::Accent))
})
.size(ButtonSize::Medium)
.label_size(LabelSize::Small)
.on_click({
let method_id = method.id.clone();
cx.listener(move |this, _, window, cx| {
this.authenticate(method_id.clone(), window, cx)
})
})
},
)),
)
} }
fn render_load_error(&self, e: &LoadError, cx: &Context<Self>) -> AnyElement { fn render_load_error(&self, e: &LoadError, cx: &Context<Self>) -> AnyElement {
let mut container = v_flex() let (message, action_slot) = match e {
.items_center() LoadError::NotInstalled {
.justify_center() error_message,
.child(self.render_error_agent_logo()) install_message,
.child( install_command,
v_flex() } => {
.mt_4() let install_command = install_command.clone();
.mb_2() let button = Button::new("install", install_message)
.gap_0p5()
.text_center()
.items_center()
.child(Headline::new("Failed to launch").size(HeadlineSize::Medium))
.child(
Label::new(e.to_string())
.size(LabelSize::Small)
.color(Color::Muted),
),
);
if let LoadError::Unsupported {
upgrade_message,
upgrade_command,
..
} = &e
{
let upgrade_message = upgrade_message.clone();
let upgrade_command = upgrade_command.clone();
container = container.child(
Button::new("upgrade", upgrade_message)
.tooltip(Tooltip::text(upgrade_command.clone()))
.on_click(cx.listener(move |this, _, window, cx| {
let task = this
.workspace
.update(cx, |workspace, cx| {
let project = workspace.project().read(cx);
let cwd = project.first_project_directory(cx);
let shell = project.terminal_settings(&cwd, cx).shell.clone();
let spawn_in_terminal = task::SpawnInTerminal {
id: task::TaskId("upgrade".to_string()),
full_label: upgrade_command.clone(),
label: upgrade_command.clone(),
command: Some(upgrade_command.clone()),
args: Vec::new(),
command_label: upgrade_command.clone(),
cwd,
env: Default::default(),
use_new_terminal: true,
allow_concurrent_runs: true,
reveal: Default::default(),
reveal_target: Default::default(),
hide: Default::default(),
shell,
show_summary: true,
show_command: true,
show_rerun: false,
};
workspace.spawn_in_terminal(spawn_in_terminal, window, cx)
})
.ok();
let Some(task) = task else { return };
cx.spawn_in(window, async move |this, cx| {
if let Some(Ok(_)) = task.await {
this.update_in(cx, |this, window, cx| {
this.reset(window, cx);
})
.ok();
}
})
.detach()
})),
);
} else if let LoadError::NotInstalled {
install_message,
install_command,
..
} = e
{
let install_message = install_message.clone();
let install_command = install_command.clone();
container = container.child(
Button::new("install", install_message)
.style(ButtonStyle::Tinted(ui::TintColor::Accent))
.size(ButtonSize::Medium)
.tooltip(Tooltip::text(install_command.clone())) .tooltip(Tooltip::text(install_command.clone()))
.style(ButtonStyle::Outlined)
.label_size(LabelSize::Small)
.icon(IconName::Download)
.icon_size(IconSize::Small)
.icon_color(Color::Muted)
.icon_position(IconPosition::Start)
.on_click(cx.listener(move |this, _, window, cx| { .on_click(cx.listener(move |this, _, window, cx| {
let task = this let task = this
.workspace .workspace
@ -2841,11 +2730,81 @@ impl AcpThreadView {
} }
}) })
.detach() .detach()
})), }));
);
}
container.into_any() (error_message.clone(), Some(button.into_any_element()))
}
LoadError::Unsupported {
error_message,
upgrade_message,
upgrade_command,
} => {
let upgrade_command = upgrade_command.clone();
let button = Button::new("upgrade", upgrade_message)
.tooltip(Tooltip::text(upgrade_command.clone()))
.style(ButtonStyle::Outlined)
.label_size(LabelSize::Small)
.icon(IconName::Download)
.icon_size(IconSize::Small)
.icon_color(Color::Muted)
.icon_position(IconPosition::Start)
.on_click(cx.listener(move |this, _, window, cx| {
let task = this
.workspace
.update(cx, |workspace, cx| {
let project = workspace.project().read(cx);
let cwd = project.first_project_directory(cx);
let shell = project.terminal_settings(&cwd, cx).shell.clone();
let spawn_in_terminal = task::SpawnInTerminal {
id: task::TaskId("upgrade".to_string()),
full_label: upgrade_command.clone(),
label: upgrade_command.clone(),
command: Some(upgrade_command.clone()),
args: Vec::new(),
command_label: upgrade_command.clone(),
cwd,
env: Default::default(),
use_new_terminal: true,
allow_concurrent_runs: true,
reveal: Default::default(),
reveal_target: Default::default(),
hide: Default::default(),
shell,
show_summary: true,
show_command: true,
show_rerun: false,
};
workspace.spawn_in_terminal(spawn_in_terminal, window, cx)
})
.ok();
let Some(task) = task else { return };
cx.spawn_in(window, async move |this, cx| {
if let Some(Ok(_)) = task.await {
this.update_in(cx, |this, window, cx| {
this.reset(window, cx);
})
.ok();
}
})
.detach()
}));
(error_message.clone(), Some(button.into_any_element()))
}
LoadError::Exited { .. } => ("Server exited with status {status}".into(), None),
LoadError::Other(msg) => (
msg.into(),
Some(self.create_copy_button(msg.to_string()).into_any_element()),
),
};
Callout::new()
.severity(Severity::Error)
.icon(IconName::XCircleFilled)
.title("Failed to Launch")
.description(message)
.actions_slot(div().children(action_slot))
.into_any_element()
} }
fn render_activity_bar( fn render_activity_bar(
@ -3336,6 +3295,19 @@ impl AcpThreadView {
(IconName::Maximize, "Expand Message Editor") (IconName::Maximize, "Expand Message Editor")
}; };
let backdrop = div()
.size_full()
.absolute()
.inset_0()
.bg(cx.theme().colors().panel_background)
.opacity(0.8)
.block_mouse_except_scroll();
let enable_editor = match self.thread_state {
ThreadState::Loading { .. } | ThreadState::Ready { .. } => true,
ThreadState::Unauthenticated { .. } | ThreadState::LoadError(..) => false,
};
v_flex() v_flex()
.on_action(cx.listener(Self::expand_message_editor)) .on_action(cx.listener(Self::expand_message_editor))
.on_action(cx.listener(|this, _: &ToggleProfileSelector, window, cx| { .on_action(cx.listener(|this, _: &ToggleProfileSelector, window, cx| {
@ -3411,6 +3383,7 @@ impl AcpThreadView {
.child(self.render_send_button(cx)), .child(self.render_send_button(cx)),
), ),
) )
.when(!enable_editor, |this| this.child(backdrop))
.into_any() .into_any()
} }
@ -3913,18 +3886,19 @@ impl AcpThreadView {
return; return;
} }
let title = self.title(cx); // TODO: Change this once we have title summarization for external agents.
let title = self.agent.name();
match AgentSettings::get_global(cx).notify_when_agent_waiting { match AgentSettings::get_global(cx).notify_when_agent_waiting {
NotifyWhenAgentWaiting::PrimaryScreen => { NotifyWhenAgentWaiting::PrimaryScreen => {
if let Some(primary) = cx.primary_display() { if let Some(primary) = cx.primary_display() {
self.pop_up(icon, caption.into(), title, window, primary, cx); self.pop_up(icon, caption.into(), title.into(), window, primary, cx);
} }
} }
NotifyWhenAgentWaiting::AllScreens => { NotifyWhenAgentWaiting::AllScreens => {
let caption = caption.into(); let caption = caption.into();
for screen in cx.displays() { for screen in cx.displays() {
self.pop_up(icon, caption.clone(), title.clone(), window, screen, cx); self.pop_up(icon, caption.clone(), title.into(), window, screen, cx);
} }
} }
NotifyWhenAgentWaiting::Never => { NotifyWhenAgentWaiting::Never => {
@ -4423,6 +4397,7 @@ impl AcpThreadView {
Callout::new() Callout::new()
.severity(Severity::Error) .severity(Severity::Error)
.title("Error") .title("Error")
.icon(IconName::XCircle)
.description(error.clone()) .description(error.clone())
.actions_slot(self.create_copy_button(error.to_string())) .actions_slot(self.create_copy_button(error.to_string()))
.dismiss_action(self.dismiss_error_button(cx)) .dismiss_action(self.dismiss_error_button(cx))
@ -4434,6 +4409,7 @@ impl AcpThreadView {
Callout::new() Callout::new()
.severity(Severity::Error) .severity(Severity::Error)
.icon(IconName::XCircle)
.title("Free Usage Exceeded") .title("Free Usage Exceeded")
.description(ERROR_MESSAGE) .description(ERROR_MESSAGE)
.actions_slot( .actions_slot(
@ -4453,6 +4429,7 @@ impl AcpThreadView {
Callout::new() Callout::new()
.severity(Severity::Error) .severity(Severity::Error)
.title("Authentication Required") .title("Authentication Required")
.icon(IconName::XCircle)
.description(error.clone()) .description(error.clone())
.actions_slot( .actions_slot(
h_flex() h_flex()
@ -4478,6 +4455,7 @@ impl AcpThreadView {
Callout::new() Callout::new()
.severity(Severity::Error) .severity(Severity::Error)
.title("Model Prompt Limit Reached") .title("Model Prompt Limit Reached")
.icon(IconName::XCircle)
.description(error_message) .description(error_message)
.actions_slot( .actions_slot(
h_flex() h_flex()
@ -4648,7 +4626,14 @@ impl AcpThreadView {
impl Focusable for AcpThreadView { impl Focusable for AcpThreadView {
fn focus_handle(&self, cx: &App) -> FocusHandle { fn focus_handle(&self, cx: &App) -> FocusHandle {
self.message_editor.focus_handle(cx) match self.thread_state {
ThreadState::Loading { .. } | ThreadState::Ready { .. } => {
self.message_editor.focus_handle(cx)
}
ThreadState::LoadError(_) | ThreadState::Unauthenticated { .. } => {
self.focus_handle.clone()
}
}
} }
} }
@ -4664,6 +4649,7 @@ impl Render for AcpThreadView {
.on_action(cx.listener(Self::toggle_burn_mode)) .on_action(cx.listener(Self::toggle_burn_mode))
.on_action(cx.listener(Self::keep_all)) .on_action(cx.listener(Self::keep_all))
.on_action(cx.listener(Self::reject_all)) .on_action(cx.listener(Self::reject_all))
.track_focus(&self.focus_handle)
.bg(cx.theme().colors().panel_background) .bg(cx.theme().colors().panel_background)
.child(match &self.thread_state { .child(match &self.thread_state {
ThreadState::Unauthenticated { ThreadState::Unauthenticated {
@ -4680,14 +4666,14 @@ impl Render for AcpThreadView {
window, window,
cx, cx,
), ),
ThreadState::Loading { .. } => { ThreadState::Loading { .. } => v_flex()
v_flex().flex_1().child(self.render_empty_state(window, cx))
}
ThreadState::LoadError(e) => v_flex()
.p_2()
.flex_1() .flex_1()
.child(self.render_recent_history(window, cx)),
ThreadState::LoadError(e) => v_flex()
.flex_1()
.size_full()
.items_center() .items_center()
.justify_center() .justify_end()
.child(self.render_load_error(e, cx)), .child(self.render_load_error(e, cx)),
ThreadState::Ready { thread, .. } => { ThreadState::Ready { thread, .. } => {
let thread_clone = thread.clone(); let thread_clone = thread.clone();
@ -4724,7 +4710,7 @@ impl Render for AcpThreadView {
}, },
) )
} else { } else {
this.child(self.render_empty_state(window, cx)) this.child(self.render_recent_history(window, cx))
} }
}) })
} }

View file

@ -2097,7 +2097,7 @@ impl AgentPanel {
.child(title_editor) .child(title_editor)
.into_any_element() .into_any_element()
} else { } else {
Label::new(thread_view.read(cx).title(cx)) Label::new(thread_view.read(cx).title())
.color(Color::Muted) .color(Color::Muted)
.truncate() .truncate()
.into_any_element() .into_any_element()

View file

@ -132,6 +132,7 @@ impl RenderOnce for Callout {
h_flex() h_flex()
.min_w_0() .min_w_0()
.w_full()
.p_2() .p_2()
.gap_2() .gap_2()
.items_start() .items_start()