agent_ui: Make "waiting confirmation" state more apparent (#41998)
This PR changes the loading/generating indicator when in the "waiting for tool call confirmation" state so that's a bit more visible and discernible as needing your attention, as opposed to a regular generating state. <img width="400" alt="Screenshot 2025-11-05 at 10 46@2x" src="https://github.com/user-attachments/assets/88adbf97-20fb-49c4-9c77-b0a3a22aa14e" /> Release Notes: - agent: Improved the "waiting for confirmation" state visibility so that you more rapidly know the agent is waiting for you to act.
This commit is contained in:
@@ -2051,6 +2051,15 @@ impl AcpThreadView {
|
||||
.into_any(),
|
||||
};
|
||||
|
||||
let needs_confirmation = if let AgentThreadEntry::ToolCall(tool_call) = entry {
|
||||
matches!(
|
||||
tool_call.status,
|
||||
ToolCallStatus::WaitingForConfirmation { .. }
|
||||
)
|
||||
} else {
|
||||
false
|
||||
};
|
||||
|
||||
let Some(thread) = self.thread() else {
|
||||
return primary;
|
||||
};
|
||||
@@ -2059,7 +2068,13 @@ impl AcpThreadView {
|
||||
v_flex()
|
||||
.w_full()
|
||||
.child(primary)
|
||||
.child(self.render_thread_controls(&thread, cx))
|
||||
.map(|this| {
|
||||
if needs_confirmation {
|
||||
this.child(self.render_generating(true))
|
||||
} else {
|
||||
this.child(self.render_thread_controls(&thread, cx))
|
||||
}
|
||||
})
|
||||
.when_some(
|
||||
self.thread_feedback.comments_editor.clone(),
|
||||
|this, editor| this.child(Self::render_feedback_feedback_editor(editor, cx)),
|
||||
@@ -4829,6 +4844,31 @@ impl AcpThreadView {
|
||||
}
|
||||
}
|
||||
|
||||
fn render_generating(&self, confirmation: bool) -> impl IntoElement {
|
||||
h_flex()
|
||||
.id("generating-spinner")
|
||||
.py_2()
|
||||
.px(rems_from_px(22.))
|
||||
.map(|this| {
|
||||
if confirmation {
|
||||
this.gap_2()
|
||||
.child(
|
||||
h_flex()
|
||||
.w_2()
|
||||
.child(SpinnerLabel::sand().size(LabelSize::Small)),
|
||||
)
|
||||
.child(
|
||||
LoadingLabel::new("Waiting Confirmation")
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
} else {
|
||||
this.child(SpinnerLabel::new().size(LabelSize::Small))
|
||||
}
|
||||
})
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
fn render_thread_controls(
|
||||
&self,
|
||||
thread: &Entity<AcpThread>,
|
||||
@@ -4836,12 +4876,7 @@ impl AcpThreadView {
|
||||
) -> impl IntoElement {
|
||||
let is_generating = matches!(thread.read(cx).status(), ThreadStatus::Generating);
|
||||
if is_generating {
|
||||
return h_flex().id("thread-controls-container").child(
|
||||
div()
|
||||
.py_2()
|
||||
.px(rems_from_px(22.))
|
||||
.child(SpinnerLabel::new().size(LabelSize::Small)),
|
||||
);
|
||||
return self.render_generating(false).into_any_element();
|
||||
}
|
||||
|
||||
let open_as_markdown = IconButton::new("open-as-markdown", IconName::FileMarkdown)
|
||||
@@ -4929,7 +4964,10 @@ impl AcpThreadView {
|
||||
);
|
||||
}
|
||||
|
||||
container.child(open_as_markdown).child(scroll_to_top)
|
||||
container
|
||||
.child(open_as_markdown)
|
||||
.child(scroll_to_top)
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
fn render_feedback_feedback_editor(editor: Entity<Editor>, cx: &Context<Self>) -> Div {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use crate::prelude::*;
|
||||
use gpui::{Animation, AnimationExt, FontWeight, pulsating_between};
|
||||
use gpui::{Animation, AnimationExt, FontWeight};
|
||||
use std::time::Duration;
|
||||
|
||||
#[derive(IntoElement)]
|
||||
@@ -84,38 +84,29 @@ impl RenderOnce for LoadingLabel {
|
||||
fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement {
|
||||
let text = self.text.clone();
|
||||
|
||||
self.base
|
||||
.color(Color::Muted)
|
||||
.with_animations(
|
||||
"loading_label",
|
||||
vec![
|
||||
Animation::new(Duration::from_secs(1)),
|
||||
Animation::new(Duration::from_secs(1)).repeat(),
|
||||
],
|
||||
move |mut label, animation_ix, delta| {
|
||||
match animation_ix {
|
||||
0 => {
|
||||
let chars_to_show = (delta * text.len() as f32).ceil() as usize;
|
||||
let text = SharedString::from(text[0..chars_to_show].to_string());
|
||||
label.set_text(text);
|
||||
}
|
||||
1 => match delta {
|
||||
d if d < 0.25 => label.set_text(text.clone()),
|
||||
d if d < 0.5 => label.set_text(format!("{}.", text)),
|
||||
d if d < 0.75 => label.set_text(format!("{}..", text)),
|
||||
_ => label.set_text(format!("{}...", text)),
|
||||
},
|
||||
_ => {}
|
||||
self.base.color(Color::Muted).with_animations(
|
||||
"loading_label",
|
||||
vec![
|
||||
Animation::new(Duration::from_secs(1)),
|
||||
Animation::new(Duration::from_secs(1)).repeat(),
|
||||
],
|
||||
move |mut label, animation_ix, delta| {
|
||||
match animation_ix {
|
||||
0 => {
|
||||
let chars_to_show = (delta * text.len() as f32).ceil() as usize;
|
||||
let text = SharedString::from(text[0..chars_to_show].to_string());
|
||||
label.set_text(text);
|
||||
}
|
||||
label
|
||||
},
|
||||
)
|
||||
.with_animation(
|
||||
"pulsating-label",
|
||||
Animation::new(Duration::from_secs(2))
|
||||
.repeat()
|
||||
.with_easing(pulsating_between(0.6, 1.)),
|
||||
|label, delta| label.map_element(|label| label.alpha(delta)),
|
||||
)
|
||||
1 => match delta {
|
||||
d if d < 0.25 => label.set_text(text.clone()),
|
||||
d if d < 0.5 => label.set_text(format!("{}.", text)),
|
||||
d if d < 0.75 => label.set_text(format!("{}..", text)),
|
||||
_ => label.set_text(format!("{}...", text)),
|
||||
},
|
||||
_ => {}
|
||||
}
|
||||
label
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ pub enum SpinnerVariant {
|
||||
#[default]
|
||||
Dots,
|
||||
DotsVariant,
|
||||
Sand,
|
||||
}
|
||||
|
||||
/// A spinner indication, based on the label component, that loops through
|
||||
@@ -41,6 +42,11 @@ impl SpinnerVariant {
|
||||
match self {
|
||||
SpinnerVariant::Dots => vec!["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"],
|
||||
SpinnerVariant::DotsVariant => vec!["⣼", "⣹", "⢻", "⠿", "⡟", "⣏", "⣧", "⣶"],
|
||||
SpinnerVariant::Sand => vec![
|
||||
"⠁", "⠂", "⠄", "⡀", "⡈", "⡐", "⡠", "⣀", "⣁", "⣂", "⣄", "⣌", "⣔", "⣤", "⣥", "⣦",
|
||||
"⣮", "⣶", "⣷", "⣿", "⡿", "⠿", "⢟", "⠟", "⡛", "⠛", "⠫", "⢋", "⠋", "⠍", "⡉", "⠉",
|
||||
"⠑", "⠡", "⢁",
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,6 +54,7 @@ impl SpinnerVariant {
|
||||
match self {
|
||||
SpinnerVariant::Dots => Duration::from_millis(1000),
|
||||
SpinnerVariant::DotsVariant => Duration::from_millis(1000),
|
||||
SpinnerVariant::Sand => Duration::from_millis(2000),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,6 +62,7 @@ impl SpinnerVariant {
|
||||
match self {
|
||||
SpinnerVariant::Dots => "spinner_label_dots",
|
||||
SpinnerVariant::DotsVariant => "spinner_label_dots_variant",
|
||||
SpinnerVariant::Sand => "spinner_label_dots_variant_2",
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -83,6 +91,10 @@ impl SpinnerLabel {
|
||||
pub fn dots_variant() -> Self {
|
||||
Self::with_variant(SpinnerVariant::DotsVariant)
|
||||
}
|
||||
|
||||
pub fn sand() -> Self {
|
||||
Self::with_variant(SpinnerVariant::Sand)
|
||||
}
|
||||
}
|
||||
|
||||
impl LabelCommon for SpinnerLabel {
|
||||
@@ -185,6 +197,7 @@ impl Component for SpinnerLabel {
|
||||
"Dots Variant",
|
||||
SpinnerLabel::dots_variant().into_any_element(),
|
||||
),
|
||||
single_example("Sand Variant", SpinnerLabel::sand().into_any_element()),
|
||||
];
|
||||
|
||||
Some(example_group(examples).vertical().into_any_element())
|
||||
|
||||
Reference in New Issue
Block a user