acp: Animate loading context creases (#36814)
- Add pulsating animation for context creases while they're loading - Add spinner in message editors (replacing send button) during the window where sending has been requested, but we haven't finished loading the message contents to send to the model - During the same window, ignore further send requests, so we don't end up sending the same message twice if you mash enter while loading is in progress - Wait for context to load before rewinding the thread when sending an edited past message, avoiding an empty-looking state during the same window Release Notes: - N/A
This commit is contained in:
parent
d49409caba
commit
19764794b7
4 changed files with 217 additions and 101 deletions
|
@ -277,6 +277,7 @@ pub struct AcpThreadView {
|
|||
terminal_expanded: bool,
|
||||
editing_message: Option<usize>,
|
||||
prompt_capabilities: Rc<Cell<PromptCapabilities>>,
|
||||
is_loading_contents: bool,
|
||||
_cancel_task: Option<Task<()>>,
|
||||
_subscriptions: [Subscription; 3],
|
||||
}
|
||||
|
@ -389,6 +390,7 @@ impl AcpThreadView {
|
|||
history_store,
|
||||
hovered_recent_history_item: None,
|
||||
prompt_capabilities,
|
||||
is_loading_contents: false,
|
||||
_subscriptions: subscriptions,
|
||||
_cancel_task: None,
|
||||
focus_handle: cx.focus_handle(),
|
||||
|
@ -823,6 +825,11 @@ impl AcpThreadView {
|
|||
|
||||
fn send(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let Some(thread) = self.thread() else { return };
|
||||
|
||||
if self.is_loading_contents {
|
||||
return;
|
||||
}
|
||||
|
||||
self.history_store.update(cx, |history, cx| {
|
||||
history.push_recently_opened_entry(
|
||||
HistoryEntryId::AcpThread(thread.read(cx).session_id().clone()),
|
||||
|
@ -876,6 +883,15 @@ impl AcpThreadView {
|
|||
let Some(thread) = self.thread().cloned() else {
|
||||
return;
|
||||
};
|
||||
|
||||
self.is_loading_contents = true;
|
||||
let guard = cx.new(|_| ());
|
||||
cx.observe_release(&guard, |this, _guard, cx| {
|
||||
this.is_loading_contents = false;
|
||||
cx.notify();
|
||||
})
|
||||
.detach();
|
||||
|
||||
let task = cx.spawn_in(window, async move |this, cx| {
|
||||
let (contents, tracked_buffers) = contents.await?;
|
||||
|
||||
|
@ -896,6 +912,7 @@ impl AcpThreadView {
|
|||
action_log.buffer_read(buffer, cx)
|
||||
}
|
||||
});
|
||||
drop(guard);
|
||||
thread.send(contents, cx)
|
||||
})?;
|
||||
send.await
|
||||
|
@ -950,19 +967,24 @@ impl AcpThreadView {
|
|||
let Some(thread) = self.thread().cloned() else {
|
||||
return;
|
||||
};
|
||||
if self.is_loading_contents {
|
||||
return;
|
||||
}
|
||||
|
||||
let Some(rewind) = thread.update(cx, |thread, cx| {
|
||||
let user_message_id = thread.entries().get(entry_ix)?.user_message()?.id.clone()?;
|
||||
Some(thread.rewind(user_message_id, cx))
|
||||
let Some(user_message_id) = thread.update(cx, |thread, _| {
|
||||
thread.entries().get(entry_ix)?.user_message()?.id.clone()
|
||||
}) else {
|
||||
return;
|
||||
};
|
||||
|
||||
let contents = message_editor.update(cx, |message_editor, cx| message_editor.contents(cx));
|
||||
|
||||
let task = cx.foreground_executor().spawn(async move {
|
||||
rewind.await?;
|
||||
contents.await
|
||||
let task = cx.spawn(async move |_, cx| {
|
||||
let contents = contents.await?;
|
||||
thread
|
||||
.update(cx, |thread, cx| thread.rewind(user_message_id, cx))?
|
||||
.await?;
|
||||
Ok(contents)
|
||||
});
|
||||
self.send_impl(task, window, cx);
|
||||
}
|
||||
|
@ -1341,25 +1363,34 @@ impl AcpThreadView {
|
|||
base_container
|
||||
.child(
|
||||
IconButton::new("cancel", IconName::Close)
|
||||
.disabled(self.is_loading_contents)
|
||||
.icon_color(Color::Error)
|
||||
.icon_size(IconSize::XSmall)
|
||||
.on_click(cx.listener(Self::cancel_editing))
|
||||
)
|
||||
.child(
|
||||
IconButton::new("regenerate", IconName::Return)
|
||||
.icon_color(Color::Muted)
|
||||
.icon_size(IconSize::XSmall)
|
||||
.tooltip(Tooltip::text(
|
||||
"Editing will restart the thread from this point."
|
||||
))
|
||||
.on_click(cx.listener({
|
||||
let editor = editor.clone();
|
||||
move |this, _, window, cx| {
|
||||
this.regenerate(
|
||||
entry_ix, &editor, window, cx,
|
||||
);
|
||||
}
|
||||
})),
|
||||
if self.is_loading_contents {
|
||||
div()
|
||||
.id("loading-edited-message-content")
|
||||
.tooltip(Tooltip::text("Loading Added Context…"))
|
||||
.child(loading_contents_spinner(IconSize::XSmall))
|
||||
.into_any_element()
|
||||
} else {
|
||||
IconButton::new("regenerate", IconName::Return)
|
||||
.icon_color(Color::Muted)
|
||||
.icon_size(IconSize::XSmall)
|
||||
.tooltip(Tooltip::text(
|
||||
"Editing will restart the thread from this point."
|
||||
))
|
||||
.on_click(cx.listener({
|
||||
let editor = editor.clone();
|
||||
move |this, _, window, cx| {
|
||||
this.regenerate(
|
||||
entry_ix, &editor, window, cx,
|
||||
);
|
||||
}
|
||||
})).into_any_element()
|
||||
}
|
||||
)
|
||||
)
|
||||
} else {
|
||||
|
@ -3542,7 +3573,14 @@ impl AcpThreadView {
|
|||
.thread()
|
||||
.is_some_and(|thread| thread.read(cx).status() != ThreadStatus::Idle);
|
||||
|
||||
if is_generating && is_editor_empty {
|
||||
if self.is_loading_contents {
|
||||
div()
|
||||
.id("loading-message-content")
|
||||
.px_1()
|
||||
.tooltip(Tooltip::text("Loading Added Context…"))
|
||||
.child(loading_contents_spinner(IconSize::default()))
|
||||
.into_any_element()
|
||||
} else if is_generating && is_editor_empty {
|
||||
IconButton::new("stop-generation", IconName::Stop)
|
||||
.icon_color(Color::Error)
|
||||
.style(ButtonStyle::Tinted(ui::TintColor::Error))
|
||||
|
@ -4643,6 +4681,18 @@ impl AcpThreadView {
|
|||
}
|
||||
}
|
||||
|
||||
fn loading_contents_spinner(size: IconSize) -> AnyElement {
|
||||
Icon::new(IconName::LoadCircle)
|
||||
.size(size)
|
||||
.color(Color::Accent)
|
||||
.with_animation(
|
||||
"load_context_circle",
|
||||
Animation::new(Duration::from_secs(3)).repeat(),
|
||||
|icon, delta| icon.transform(Transformation::rotate(percentage(delta))),
|
||||
)
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
impl Focusable for AcpThreadView {
|
||||
fn focus_handle(&self, cx: &App) -> FocusHandle {
|
||||
match self.thread_state {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue