agent_ui: Make thread loading state clearer (#43765)

Closes https://github.com/zed-industries/zed/issues/43721

This PR makes the loading state clearer by disabling the message editor
while the agent is loading as well as pulsating the icon.

Release Notes:

- agent: Made the thread loading state clearer in the agent panel.
This commit is contained in:
Danilo Leal
2025-11-29 00:52:44 +00:00
committed by GitHub
parent 63eb3ea7e0
commit be8605bb10
2 changed files with 43 additions and 28 deletions
+14 -8
View File
@@ -675,6 +675,8 @@ impl AcpThreadView {
})
});
this.message_editor.focus_handle(cx).focus(window);
cx.notify();
}
Err(err) => {
@@ -1009,6 +1011,10 @@ impl AcpThreadView {
}
}
pub fn is_loading(&self) -> bool {
matches!(self.thread_state, ThreadState::Loading { .. })
}
fn resume_chat(&mut self, cx: &mut Context<Self>) {
self.thread_error.take();
let Some(thread) = self.thread() else {
@@ -4196,8 +4202,10 @@ impl AcpThreadView {
.block_mouse_except_scroll();
let enable_editor = match self.thread_state {
ThreadState::Loading { .. } | ThreadState::Ready { .. } => true,
ThreadState::Unauthenticated { .. } | ThreadState::LoadError(..) => false,
ThreadState::Ready { .. } => true,
ThreadState::Loading { .. }
| ThreadState::Unauthenticated { .. }
| ThreadState::LoadError(..) => false,
};
v_flex()
@@ -5858,12 +5866,10 @@ fn placeholder_text(agent_name: &str, has_commands: bool) -> String {
impl Focusable for AcpThreadView {
fn focus_handle(&self, cx: &App) -> FocusHandle {
match self.thread_state {
ThreadState::Loading { .. } | ThreadState::Ready { .. } => {
self.active_editor(cx).focus_handle(cx)
}
ThreadState::LoadError(_) | ThreadState::Unauthenticated { .. } => {
self.focus_handle.clone()
}
ThreadState::Ready { .. } => self.active_editor(cx).focus_handle(cx),
ThreadState::Loading { .. }
| ThreadState::LoadError(_)
| ThreadState::Unauthenticated { .. } => self.focus_handle.clone(),
}
}
}
+29 -20
View File
@@ -1,7 +1,4 @@
use std::ops::Range;
use std::path::Path;
use std::rc::Rc;
use std::sync::Arc;
use std::{ops::Range, path::Path, rc::Rc, sync::Arc, time::Duration};
use acp_thread::AcpThread;
use agent::{ContextServerRegistry, DbThreadMetadata, HistoryEntry, HistoryStore};
@@ -46,9 +43,9 @@ use extension::ExtensionEvents;
use extension_host::ExtensionStore;
use fs::Fs;
use gpui::{
Action, AnyElement, App, AsyncWindowContext, Corner, DismissEvent, Entity, EventEmitter,
ExternalPaths, FocusHandle, Focusable, KeyContext, Pixels, Subscription, Task, UpdateGlobal,
WeakEntity, prelude::*,
Action, Animation, AnimationExt, AnyElement, App, AsyncWindowContext, Corner, DismissEvent,
Entity, EventEmitter, ExternalPaths, FocusHandle, Focusable, KeyContext, Pixels, Subscription,
Task, UpdateGlobal, WeakEntity, prelude::*, pulsating_between,
};
use language::LanguageRegistry;
use language_model::{ConfigurationError, LanguageModelRegistry};
@@ -58,10 +55,9 @@ use rules_library::{RulesLibrary, open_rules_library};
use search::{BufferSearchBar, buffer_search};
use settings::{Settings, update_settings_file};
use theme::ThemeSettings;
use ui::utils::WithRemSize;
use ui::{
Callout, ContextMenu, ContextMenuEntry, KeyBinding, PopoverMenu, PopoverMenuHandle,
ProgressBar, Tab, Tooltip, prelude::*,
ProgressBar, Tab, Tooltip, prelude::*, utils::WithRemSize,
};
use util::ResultExt as _;
use workspace::{
@@ -2159,28 +2155,41 @@ impl AgentPanel {
let selected_agent_label = self.selected_agent.label();
let is_thread_loading = self
.active_thread_view()
.map(|thread| thread.read(cx).is_loading())
.unwrap_or(false);
let has_custom_icon = selected_agent_custom_icon.is_some();
let selected_agent = div()
.id("selected_agent_icon")
.when_some(selected_agent_custom_icon, |this, icon_path| {
let label = selected_agent_label.clone();
this.px_1()
.child(Icon::from_external_svg(icon_path).color(Color::Muted))
.tooltip(move |_window, cx| {
Tooltip::with_meta(label.clone(), None, "Selected Agent", cx)
})
})
.when(!has_custom_icon, |this| {
this.when_some(self.selected_agent.icon(), |this, icon| {
let label = selected_agent_label.clone();
this.px_1()
.child(Icon::new(icon).color(Color::Muted))
.tooltip(move |_window, cx| {
Tooltip::with_meta(label.clone(), None, "Selected Agent", cx)
})
this.px_1().child(Icon::new(icon).color(Color::Muted))
})
})
.into_any_element();
.tooltip(move |_, cx| {
Tooltip::with_meta(selected_agent_label.clone(), None, "Selected Agent", cx)
});
let selected_agent = if is_thread_loading {
selected_agent
.with_animation(
"pulsating-icon",
Animation::new(Duration::from_secs(1))
.repeat()
.with_easing(pulsating_between(0.2, 0.6)),
|icon, delta| icon.opacity(delta),
)
.into_any_element()
} else {
selected_agent.into_any_element()
};
h_flex()
.id("agent-panel-toolbar")