agent: Display "generating" label in the active thread (#28297)

Release Notes:

- N/A

---------

Co-authored-by: Bennet Bo Fenner <bennetbo@gmx.de>
This commit is contained in:
Danilo Leal 2025-04-08 14:31:32 -03:00 committed by GitHub
parent 85c5d8af3a
commit 698cdc4d1a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 162 additions and 81 deletions

View file

@ -1199,16 +1199,62 @@ impl ActiveThread {
let context_store = self.context_store.clone();
let workspace = self.workspace.clone();
let thread = self.thread.read(cx);
// Get all the data we need from thread before we start using it in closures
let checkpoint = thread.checkpoint_for_message(message_id);
let context = thread.context_for_message(message_id).collect::<Vec<_>>();
let tool_uses = thread.tool_uses_for_message(message_id, cx);
let has_tool_uses = !tool_uses.is_empty();
let is_generating = thread.is_generating();
let is_first_message = ix == 0;
let is_last_message = ix == self.messages.len() - 1;
let show_feedback = is_last_message && message.role != Role::User;
let needs_confirmation = tool_uses.iter().any(|tool_use| tool_use.needs_confirmation);
let generating_label = (is_generating && is_last_message).then(|| {
Label::new("Generating")
.color(Color::Muted)
.size(LabelSize::Small)
.with_animation(
"generating-label",
Animation::new(Duration::from_secs(1)).repeat(),
|mut label, delta| {
let text = match delta {
d if d < 0.25 => "Generating",
d if d < 0.5 => "Generating.",
d if d < 0.75 => "Generating..",
_ => "Generating...",
};
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)),
)
});
// Don't render user messages that are just there for returning tool results.
if message.role == Role::User && thread.message_has_tool_results(message_id) {
if let Some(generating_label) = generating_label {
return h_flex()
.w_full()
.h_10()
.py_1p5()
.pl_4()
.pb_3()
.child(generating_label)
.into_any_element();
}
return Empty.into_any();
}
@ -1220,9 +1266,6 @@ impl ActiveThread {
.filter(|(id, _)| *id == message_id)
.map(|(_, state)| state.editor.clone());
let first_message = ix == 0;
let show_feedback = ix == self.messages.len() - 1 && message.role != Role::User;
let colors = cx.theme().colors();
let active_color = colors.element_active;
let editor_bg_color = colors.editor_background;
@ -1391,7 +1434,7 @@ impl ActiveThread {
Role::User => v_flex()
.id(("message-container", ix))
.map(|this| {
if first_message {
if is_first_message {
this.pt_2()
} else {
this.pt_4()
@ -1509,15 +1552,11 @@ impl ActiveThread {
.border_l_1()
.border_color(cx.theme().colors().border_variant)
.children(message_content)
.gap_2p5()
.pb_2p5()
.when(!tool_uses.is_empty(), |parent| {
parent.child(
div().children(
tool_uses
.into_iter()
.map(|tool_use| self.render_tool_use(tool_use, window, cx)),
),
.when(has_tool_uses, |parent| {
parent.children(
tool_uses
.into_iter()
.map(|tool_use| self.render_tool_use(tool_use, window, cx)),
)
}),
Role::System => div().id(("message-container", ix)).py_1().px_2().child(
@ -1530,9 +1569,6 @@ impl ActiveThread {
v_flex()
.w_full()
.when(first_message, |parent| {
parent.child(self.render_rules_item(cx))
})
.when_some(checkpoint, |parent, checkpoint| {
let mut is_pending = false;
let mut error = None;
@ -1602,65 +1638,56 @@ impl ActiveThread {
.child(ui::Divider::horizontal()),
)
})
.when(is_first_message, |parent| {
parent.child(self.render_rules_item(cx))
})
.child(styled_message)
.when(
show_feedback && !self.thread.read(cx).is_generating(),
|parent| {
parent.child(feedback_items).when_some(
self.feedback_message_editor.clone(),
|parent, feedback_editor| {
let focus_handle = feedback_editor.focus_handle(cx);
parent.child(
v_flex()
.key_context("AgentFeedbackMessageEditor")
.on_action(cx.listener(|this, _: &menu::Cancel, _, cx| {
this.feedback_message_editor = None;
cx.notify();
}))
.on_action(cx.listener(|this, _: &menu::Confirm, _, cx| {
this.submit_feedback_message(cx);
cx.notify();
}))
.on_action(cx.listener(Self::confirm_editing_message))
.mx_4()
.mb_3()
.p_2()
.rounded_md()
.border_1()
.border_color(cx.theme().colors().border)
.bg(cx.theme().colors().editor_background)
.child(feedback_editor)
.child(
h_flex()
.gap_1()
.justify_end()
.child(
Button::new("dismiss-feedback-message", "Cancel")
.label_size(LabelSize::Small)
.key_binding(
KeyBinding::for_action_in(
&menu::Cancel,
&focus_handle,
window,
cx,
)
.map(|kb| kb.size(rems_from_px(10.))),
)
.on_click(cx.listener(|this, _, _, cx| {
this.feedback_message_editor = None;
cx.notify();
})),
)
.child(
Button::new(
"submit-feedback-message",
"Share Feedback",
)
.style(ButtonStyle::Tinted(ui::TintColor::Accent))
.when(!needs_confirmation && generating_label.is_some(), |this| {
this.child(
h_flex()
.h_8()
.mt_2()
.mb_4()
.ml_4()
.py_1p5()
.child(generating_label.unwrap()),
)
})
.when(show_feedback && !is_generating, |parent| {
parent.child(feedback_items).when_some(
self.feedback_message_editor.clone(),
|parent, feedback_editor| {
let focus_handle = feedback_editor.focus_handle(cx);
parent.child(
v_flex()
.key_context("AgentFeedbackMessageEditor")
.on_action(cx.listener(|this, _: &menu::Cancel, _, cx| {
this.feedback_message_editor = None;
cx.notify();
}))
.on_action(cx.listener(|this, _: &menu::Confirm, _, cx| {
this.submit_feedback_message(cx);
cx.notify();
}))
.on_action(cx.listener(Self::confirm_editing_message))
.my_3()
.mx_4()
.p_2()
.rounded_md()
.border_1()
.border_color(cx.theme().colors().border)
.bg(cx.theme().colors().editor_background)
.child(feedback_editor)
.child(
h_flex()
.gap_1()
.justify_end()
.child(
Button::new("dismiss-feedback-message", "Cancel")
.label_size(LabelSize::Small)
.key_binding(
KeyBinding::for_action_in(
&menu::Confirm,
&menu::Cancel,
&focus_handle,
window,
cx,
@ -1668,16 +1695,38 @@ impl ActiveThread {
.map(|kb| kb.size(rems_from_px(10.))),
)
.on_click(cx.listener(|this, _, _, cx| {
this.submit_feedback_message(cx);
this.feedback_message_editor = None;
cx.notify();
})),
)
.child(
Button::new(
"submit-feedback-message",
"Share Feedback",
)
.style(ButtonStyle::Tinted(ui::TintColor::Accent))
.label_size(LabelSize::Small)
.key_binding(
KeyBinding::for_action_in(
&menu::Confirm,
&focus_handle,
window,
cx,
)
.map(|kb| kb.size(rems_from_px(10.))),
)
.on_click(
cx.listener(|this, _, _, cx| {
this.submit_feedback_message(cx);
cx.notify();
}),
),
),
)
},
)
},
)
),
),
)
},
)
})
.into_any()
}
@ -2160,6 +2209,7 @@ impl ActiveThread {
if !tool_use.needs_confirmation {
element.child(
v_flex()
.my_1p5()
.child(
h_flex()
.group("disclosure-header")
@ -2231,6 +2281,7 @@ impl ActiveThread {
)
} else {
v_flex()
.my_3()
.rounded_lg()
.border_1()
.border_color(self.tool_card_border_color(cx))
@ -2333,7 +2384,32 @@ impl ActiveThread {
.border_t_1()
.border_color(self.tool_card_border_color(cx))
.rounded_b_lg()
.child(Label::new("Action Confirmation").color(Color::Muted).size(LabelSize::Small))
.child(
Label::new("Waiting for Confirmation…")
.color(Color::Muted)
.size(LabelSize::Small)
.with_animation(
"generating-label",
Animation::new(Duration::from_secs(1)).repeat(),
|mut label, delta| {
let text = match delta {
d if d < 0.25 => "Waiting for Confirmation",
d if d < 0.5 => "Waiting for Confirmation.",
d if d < 0.75 => "Waiting for Confirmation..",
_ => "Waiting for Confirmation...",
};
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)),
),
)
.child(
h_flex()
.gap_0p5()
@ -2448,7 +2524,7 @@ impl ActiveThread {
};
div()
.pt_1()
.pt_2()
.px_2p5()
.child(
h_flex()

View file

@ -51,6 +51,11 @@ impl Label {
label: label.into(),
}
}
/// Sets the text of the [`Label`].
pub fn set_text(&mut self, text: impl Into<SharedString>) {
self.label = text.into();
}
}
// nate: If we are going to do this, we might as well just