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
ad6bc4586a
commit
a313e9d869
4 changed files with 217 additions and 101 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -403,6 +403,7 @@ dependencies = [
|
||||||
"parking_lot",
|
"parking_lot",
|
||||||
"paths",
|
"paths",
|
||||||
"picker",
|
"picker",
|
||||||
|
"postage",
|
||||||
"pretty_assertions",
|
"pretty_assertions",
|
||||||
"project",
|
"project",
|
||||||
"prompt_store",
|
"prompt_store",
|
||||||
|
|
|
@ -67,6 +67,7 @@ ordered-float.workspace = true
|
||||||
parking_lot.workspace = true
|
parking_lot.workspace = true
|
||||||
paths.workspace = true
|
paths.workspace = true
|
||||||
picker.workspace = true
|
picker.workspace = true
|
||||||
|
postage.workspace = true
|
||||||
project.workspace = true
|
project.workspace = true
|
||||||
prompt_store.workspace = true
|
prompt_store.workspace = true
|
||||||
proto.workspace = true
|
proto.workspace = true
|
||||||
|
|
|
@ -21,12 +21,13 @@ use futures::{
|
||||||
future::{Shared, join_all},
|
future::{Shared, join_all},
|
||||||
};
|
};
|
||||||
use gpui::{
|
use gpui::{
|
||||||
AppContext, ClipboardEntry, Context, Entity, EventEmitter, FocusHandle, Focusable,
|
Animation, AnimationExt as _, AppContext, ClipboardEntry, Context, Entity, EntityId,
|
||||||
HighlightStyle, Image, ImageFormat, Img, KeyContext, Subscription, Task, TextStyle,
|
EventEmitter, FocusHandle, Focusable, HighlightStyle, Image, ImageFormat, Img, KeyContext,
|
||||||
UnderlineStyle, WeakEntity,
|
Subscription, Task, TextStyle, UnderlineStyle, WeakEntity, pulsating_between,
|
||||||
};
|
};
|
||||||
use language::{Buffer, Language};
|
use language::{Buffer, Language};
|
||||||
use language_model::LanguageModelImage;
|
use language_model::LanguageModelImage;
|
||||||
|
use postage::stream::Stream as _;
|
||||||
use project::{CompletionIntent, Project, ProjectItem, ProjectPath, Worktree};
|
use project::{CompletionIntent, Project, ProjectItem, ProjectPath, Worktree};
|
||||||
use prompt_store::{PromptId, PromptStore};
|
use prompt_store::{PromptId, PromptStore};
|
||||||
use rope::Point;
|
use rope::Point;
|
||||||
|
@ -44,10 +45,10 @@ use std::{
|
||||||
use text::{OffsetRangeExt, ToOffset as _};
|
use text::{OffsetRangeExt, ToOffset as _};
|
||||||
use theme::ThemeSettings;
|
use theme::ThemeSettings;
|
||||||
use ui::{
|
use ui::{
|
||||||
ActiveTheme, AnyElement, App, ButtonCommon, ButtonLike, ButtonStyle, Color, Icon, IconName,
|
ActiveTheme, AnyElement, App, ButtonCommon, ButtonLike, ButtonStyle, Color, Element as _,
|
||||||
IconSize, InteractiveElement, IntoElement, Label, LabelCommon, LabelSize, ParentElement,
|
FluentBuilder as _, Icon, IconName, IconSize, InteractiveElement, IntoElement, Label,
|
||||||
Render, SelectableButton, SharedString, Styled, TextSize, TintColor, Toggleable, Window, div,
|
LabelCommon, LabelSize, ParentElement, Render, SelectableButton, SharedString, Styled,
|
||||||
h_flex, px,
|
TextSize, TintColor, Toggleable, Window, div, h_flex, px,
|
||||||
};
|
};
|
||||||
use util::{ResultExt, debug_panic};
|
use util::{ResultExt, debug_panic};
|
||||||
use workspace::{Workspace, notifications::NotifyResultExt as _};
|
use workspace::{Workspace, notifications::NotifyResultExt as _};
|
||||||
|
@ -246,7 +247,7 @@ impl MessageEditor {
|
||||||
.buffer_snapshot
|
.buffer_snapshot
|
||||||
.anchor_before(start_anchor.to_offset(&snapshot.buffer_snapshot) + content_len + 1);
|
.anchor_before(start_anchor.to_offset(&snapshot.buffer_snapshot) + content_len + 1);
|
||||||
|
|
||||||
let crease_id = if let MentionUri::File { abs_path } = &mention_uri
|
let crease = if let MentionUri::File { abs_path } = &mention_uri
|
||||||
&& let Some(extension) = abs_path.extension()
|
&& let Some(extension) = abs_path.extension()
|
||||||
&& let Some(extension) = extension.to_str()
|
&& let Some(extension) = extension.to_str()
|
||||||
&& Img::extensions().contains(&extension)
|
&& Img::extensions().contains(&extension)
|
||||||
|
@ -272,29 +273,31 @@ impl MessageEditor {
|
||||||
Ok(image)
|
Ok(image)
|
||||||
})
|
})
|
||||||
.shared();
|
.shared();
|
||||||
insert_crease_for_image(
|
insert_crease_for_mention(
|
||||||
*excerpt_id,
|
*excerpt_id,
|
||||||
start,
|
start,
|
||||||
content_len,
|
content_len,
|
||||||
Some(abs_path.as_path().into()),
|
mention_uri.name().into(),
|
||||||
image,
|
IconName::Image.path().into(),
|
||||||
|
Some(image),
|
||||||
self.editor.clone(),
|
self.editor.clone(),
|
||||||
window,
|
window,
|
||||||
cx,
|
cx,
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
crate::context_picker::insert_crease_for_mention(
|
insert_crease_for_mention(
|
||||||
*excerpt_id,
|
*excerpt_id,
|
||||||
start,
|
start,
|
||||||
content_len,
|
content_len,
|
||||||
crease_text,
|
crease_text,
|
||||||
mention_uri.icon_path(cx),
|
mention_uri.icon_path(cx),
|
||||||
|
None,
|
||||||
self.editor.clone(),
|
self.editor.clone(),
|
||||||
window,
|
window,
|
||||||
cx,
|
cx,
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
let Some(crease_id) = crease_id else {
|
let Some((crease_id, tx)) = crease else {
|
||||||
return Task::ready(());
|
return Task::ready(());
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -331,7 +334,9 @@ impl MessageEditor {
|
||||||
|
|
||||||
// Notify the user if we failed to load the mentioned context
|
// Notify the user if we failed to load the mentioned context
|
||||||
cx.spawn_in(window, async move |this, cx| {
|
cx.spawn_in(window, async move |this, cx| {
|
||||||
if task.await.notify_async_err(cx).is_none() {
|
let result = task.await.notify_async_err(cx);
|
||||||
|
drop(tx);
|
||||||
|
if result.is_none() {
|
||||||
this.update(cx, |this, cx| {
|
this.update(cx, |this, cx| {
|
||||||
this.editor.update(cx, |editor, cx| {
|
this.editor.update(cx, |editor, cx| {
|
||||||
// Remove mention
|
// Remove mention
|
||||||
|
@ -857,12 +862,13 @@ impl MessageEditor {
|
||||||
snapshot.anchor_before(start_anchor.to_offset(&snapshot) + content_len)
|
snapshot.anchor_before(start_anchor.to_offset(&snapshot) + content_len)
|
||||||
});
|
});
|
||||||
let image = Arc::new(image);
|
let image = Arc::new(image);
|
||||||
let Some(crease_id) = insert_crease_for_image(
|
let Some((crease_id, tx)) = insert_crease_for_mention(
|
||||||
excerpt_id,
|
excerpt_id,
|
||||||
text_anchor,
|
text_anchor,
|
||||||
content_len,
|
content_len,
|
||||||
None.clone(),
|
MentionUri::PastedImage.name().into(),
|
||||||
Task::ready(Ok(image.clone())).shared(),
|
IconName::Image.path().into(),
|
||||||
|
Some(Task::ready(Ok(image.clone())).shared()),
|
||||||
self.editor.clone(),
|
self.editor.clone(),
|
||||||
window,
|
window,
|
||||||
cx,
|
cx,
|
||||||
|
@ -877,6 +883,7 @@ impl MessageEditor {
|
||||||
.update(|_, cx| LanguageModelImage::from_image(image, cx))
|
.update(|_, cx| LanguageModelImage::from_image(image, cx))
|
||||||
.map_err(|e| e.to_string())?
|
.map_err(|e| e.to_string())?
|
||||||
.await;
|
.await;
|
||||||
|
drop(tx);
|
||||||
if let Some(image) = image {
|
if let Some(image) = image {
|
||||||
Ok(Mention::Image(MentionImage {
|
Ok(Mention::Image(MentionImage {
|
||||||
data: image.source,
|
data: image.source,
|
||||||
|
@ -1097,18 +1104,20 @@ impl MessageEditor {
|
||||||
|
|
||||||
for (range, mention_uri, mention) in mentions {
|
for (range, mention_uri, mention) in mentions {
|
||||||
let anchor = snapshot.anchor_before(range.start);
|
let anchor = snapshot.anchor_before(range.start);
|
||||||
let Some(crease_id) = crate::context_picker::insert_crease_for_mention(
|
let Some((crease_id, tx)) = insert_crease_for_mention(
|
||||||
anchor.excerpt_id,
|
anchor.excerpt_id,
|
||||||
anchor.text_anchor,
|
anchor.text_anchor,
|
||||||
range.end - range.start,
|
range.end - range.start,
|
||||||
mention_uri.name().into(),
|
mention_uri.name().into(),
|
||||||
mention_uri.icon_path(cx),
|
mention_uri.icon_path(cx),
|
||||||
|
None,
|
||||||
self.editor.clone(),
|
self.editor.clone(),
|
||||||
window,
|
window,
|
||||||
cx,
|
cx,
|
||||||
) else {
|
) else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
|
drop(tx);
|
||||||
|
|
||||||
self.mention_set.mentions.insert(
|
self.mention_set.mentions.insert(
|
||||||
crease_id,
|
crease_id,
|
||||||
|
@ -1227,23 +1236,21 @@ impl Render for MessageEditor {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn insert_crease_for_image(
|
pub(crate) fn insert_crease_for_mention(
|
||||||
excerpt_id: ExcerptId,
|
excerpt_id: ExcerptId,
|
||||||
anchor: text::Anchor,
|
anchor: text::Anchor,
|
||||||
content_len: usize,
|
content_len: usize,
|
||||||
abs_path: Option<Arc<Path>>,
|
crease_label: SharedString,
|
||||||
image: Shared<Task<Result<Arc<Image>, String>>>,
|
crease_icon: SharedString,
|
||||||
|
// abs_path: Option<Arc<Path>>,
|
||||||
|
image: Option<Shared<Task<Result<Arc<Image>, String>>>>,
|
||||||
editor: Entity<Editor>,
|
editor: Entity<Editor>,
|
||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
cx: &mut App,
|
cx: &mut App,
|
||||||
) -> Option<CreaseId> {
|
) -> Option<(CreaseId, postage::barrier::Sender)> {
|
||||||
let crease_label = abs_path
|
let (tx, rx) = postage::barrier::channel();
|
||||||
.as_ref()
|
|
||||||
.and_then(|path| path.file_name())
|
|
||||||
.map(|name| name.to_string_lossy().to_string().into())
|
|
||||||
.unwrap_or(SharedString::from("Image"));
|
|
||||||
|
|
||||||
editor.update(cx, |editor, cx| {
|
let crease_id = editor.update(cx, |editor, cx| {
|
||||||
let snapshot = editor.buffer().read(cx).snapshot(cx);
|
let snapshot = editor.buffer().read(cx).snapshot(cx);
|
||||||
|
|
||||||
let start = snapshot.anchor_in_excerpt(excerpt_id, anchor)?;
|
let start = snapshot.anchor_in_excerpt(excerpt_id, anchor)?;
|
||||||
|
@ -1252,7 +1259,15 @@ pub(crate) fn insert_crease_for_image(
|
||||||
let end = snapshot.anchor_before(start.to_offset(&snapshot) + content_len);
|
let end = snapshot.anchor_before(start.to_offset(&snapshot) + content_len);
|
||||||
|
|
||||||
let placeholder = FoldPlaceholder {
|
let placeholder = FoldPlaceholder {
|
||||||
render: render_image_fold_icon_button(crease_label, image, cx.weak_entity()),
|
render: render_fold_icon_button(
|
||||||
|
crease_label,
|
||||||
|
crease_icon,
|
||||||
|
start..end,
|
||||||
|
rx,
|
||||||
|
image,
|
||||||
|
cx.weak_entity(),
|
||||||
|
cx,
|
||||||
|
),
|
||||||
merge_adjacent: false,
|
merge_adjacent: false,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
|
@ -1269,63 +1284,112 @@ pub(crate) fn insert_crease_for_image(
|
||||||
editor.fold_creases(vec![crease], false, window, cx);
|
editor.fold_creases(vec![crease], false, window, cx);
|
||||||
|
|
||||||
Some(ids[0])
|
Some(ids[0])
|
||||||
})
|
})?;
|
||||||
|
|
||||||
|
Some((crease_id, tx))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_image_fold_icon_button(
|
fn render_fold_icon_button(
|
||||||
label: SharedString,
|
label: SharedString,
|
||||||
image_task: Shared<Task<Result<Arc<Image>, String>>>,
|
icon: SharedString,
|
||||||
|
range: Range<Anchor>,
|
||||||
|
mut loading_finished: postage::barrier::Receiver,
|
||||||
|
image_task: Option<Shared<Task<Result<Arc<Image>, String>>>>,
|
||||||
editor: WeakEntity<Editor>,
|
editor: WeakEntity<Editor>,
|
||||||
|
cx: &mut App,
|
||||||
) -> Arc<dyn Send + Sync + Fn(FoldId, Range<Anchor>, &mut App) -> AnyElement> {
|
) -> Arc<dyn Send + Sync + Fn(FoldId, Range<Anchor>, &mut App) -> AnyElement> {
|
||||||
Arc::new({
|
let loading = cx.new(|cx| {
|
||||||
move |fold_id, fold_range, cx| {
|
let loading = cx.spawn(async move |this, cx| {
|
||||||
let is_in_text_selection = editor
|
loading_finished.recv().await;
|
||||||
.update(cx, |editor, cx| editor.is_range_selected(&fold_range, cx))
|
this.update(cx, |this: &mut LoadingContext, cx| {
|
||||||
.unwrap_or_default();
|
this.loading = None;
|
||||||
|
cx.notify();
|
||||||
ButtonLike::new(fold_id)
|
})
|
||||||
.style(ButtonStyle::Filled)
|
.ok();
|
||||||
.selected_style(ButtonStyle::Tinted(TintColor::Accent))
|
});
|
||||||
.toggle_state(is_in_text_selection)
|
LoadingContext {
|
||||||
.child(
|
id: cx.entity_id(),
|
||||||
h_flex()
|
label,
|
||||||
.gap_1()
|
icon,
|
||||||
.child(
|
range,
|
||||||
Icon::new(IconName::Image)
|
editor,
|
||||||
.size(IconSize::XSmall)
|
loading: Some(loading),
|
||||||
.color(Color::Muted),
|
image: image_task.clone(),
|
||||||
)
|
|
||||||
.child(
|
|
||||||
Label::new(label.clone())
|
|
||||||
.size(LabelSize::Small)
|
|
||||||
.buffer_font(cx)
|
|
||||||
.single_line(),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.hoverable_tooltip({
|
|
||||||
let image_task = image_task.clone();
|
|
||||||
move |_, cx| {
|
|
||||||
let image = image_task.peek().cloned().transpose().ok().flatten();
|
|
||||||
let image_task = image_task.clone();
|
|
||||||
cx.new::<ImageHover>(|cx| ImageHover {
|
|
||||||
image,
|
|
||||||
_task: cx.spawn(async move |this, cx| {
|
|
||||||
if let Ok(image) = image_task.clone().await {
|
|
||||||
this.update(cx, |this, cx| {
|
|
||||||
if this.image.replace(image).is_none() {
|
|
||||||
cx.notify();
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.ok();
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
})
|
|
||||||
.into()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.into_any_element()
|
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
|
Arc::new(move |_fold_id, _fold_range, _cx| loading.clone().into_any_element())
|
||||||
|
}
|
||||||
|
|
||||||
|
struct LoadingContext {
|
||||||
|
id: EntityId,
|
||||||
|
label: SharedString,
|
||||||
|
icon: SharedString,
|
||||||
|
range: Range<Anchor>,
|
||||||
|
editor: WeakEntity<Editor>,
|
||||||
|
loading: Option<Task<()>>,
|
||||||
|
image: Option<Shared<Task<Result<Arc<Image>, String>>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Render for LoadingContext {
|
||||||
|
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||||
|
let is_in_text_selection = self
|
||||||
|
.editor
|
||||||
|
.update(cx, |editor, cx| editor.is_range_selected(&self.range, cx))
|
||||||
|
.unwrap_or_default();
|
||||||
|
ButtonLike::new(("loading-context", self.id))
|
||||||
|
.style(ButtonStyle::Filled)
|
||||||
|
.selected_style(ButtonStyle::Tinted(TintColor::Accent))
|
||||||
|
.toggle_state(is_in_text_selection)
|
||||||
|
.when_some(self.image.clone(), |el, image_task| {
|
||||||
|
el.hoverable_tooltip(move |_, cx| {
|
||||||
|
let image = image_task.peek().cloned().transpose().ok().flatten();
|
||||||
|
let image_task = image_task.clone();
|
||||||
|
cx.new::<ImageHover>(|cx| ImageHover {
|
||||||
|
image,
|
||||||
|
_task: cx.spawn(async move |this, cx| {
|
||||||
|
if let Ok(image) = image_task.clone().await {
|
||||||
|
this.update(cx, |this, cx| {
|
||||||
|
if this.image.replace(image).is_none() {
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.ok();
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
.into()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.child(
|
||||||
|
h_flex()
|
||||||
|
.gap_1()
|
||||||
|
.child(
|
||||||
|
Icon::from_path(self.icon.clone())
|
||||||
|
.size(IconSize::XSmall)
|
||||||
|
.color(Color::Muted),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
Label::new(self.label.clone())
|
||||||
|
.size(LabelSize::Small)
|
||||||
|
.buffer_font(cx)
|
||||||
|
.single_line(),
|
||||||
|
)
|
||||||
|
.map(|el| {
|
||||||
|
if self.loading.is_some() {
|
||||||
|
el.with_animation(
|
||||||
|
"loading-context-crease",
|
||||||
|
Animation::new(Duration::from_secs(2))
|
||||||
|
.repeat()
|
||||||
|
.with_easing(pulsating_between(0.4, 0.8)),
|
||||||
|
|label, delta| label.opacity(delta),
|
||||||
|
)
|
||||||
|
.into_any()
|
||||||
|
} else {
|
||||||
|
el.into_any()
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct ImageHover {
|
struct ImageHover {
|
||||||
|
|
|
@ -277,6 +277,7 @@ pub struct AcpThreadView {
|
||||||
terminal_expanded: bool,
|
terminal_expanded: bool,
|
||||||
editing_message: Option<usize>,
|
editing_message: Option<usize>,
|
||||||
prompt_capabilities: Rc<Cell<PromptCapabilities>>,
|
prompt_capabilities: Rc<Cell<PromptCapabilities>>,
|
||||||
|
is_loading_contents: bool,
|
||||||
_cancel_task: Option<Task<()>>,
|
_cancel_task: Option<Task<()>>,
|
||||||
_subscriptions: [Subscription; 3],
|
_subscriptions: [Subscription; 3],
|
||||||
}
|
}
|
||||||
|
@ -389,6 +390,7 @@ impl AcpThreadView {
|
||||||
history_store,
|
history_store,
|
||||||
hovered_recent_history_item: None,
|
hovered_recent_history_item: None,
|
||||||
prompt_capabilities,
|
prompt_capabilities,
|
||||||
|
is_loading_contents: false,
|
||||||
_subscriptions: subscriptions,
|
_subscriptions: subscriptions,
|
||||||
_cancel_task: None,
|
_cancel_task: None,
|
||||||
focus_handle: cx.focus_handle(),
|
focus_handle: cx.focus_handle(),
|
||||||
|
@ -823,6 +825,11 @@ impl AcpThreadView {
|
||||||
|
|
||||||
fn send(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
fn send(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
let Some(thread) = self.thread() else { return };
|
let Some(thread) = self.thread() else { return };
|
||||||
|
|
||||||
|
if self.is_loading_contents {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
self.history_store.update(cx, |history, cx| {
|
self.history_store.update(cx, |history, cx| {
|
||||||
history.push_recently_opened_entry(
|
history.push_recently_opened_entry(
|
||||||
HistoryEntryId::AcpThread(thread.read(cx).session_id().clone()),
|
HistoryEntryId::AcpThread(thread.read(cx).session_id().clone()),
|
||||||
|
@ -876,6 +883,15 @@ impl AcpThreadView {
|
||||||
let Some(thread) = self.thread().cloned() else {
|
let Some(thread) = self.thread().cloned() else {
|
||||||
return;
|
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 task = cx.spawn_in(window, async move |this, cx| {
|
||||||
let (contents, tracked_buffers) = contents.await?;
|
let (contents, tracked_buffers) = contents.await?;
|
||||||
|
|
||||||
|
@ -896,6 +912,7 @@ impl AcpThreadView {
|
||||||
action_log.buffer_read(buffer, cx)
|
action_log.buffer_read(buffer, cx)
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
drop(guard);
|
||||||
thread.send(contents, cx)
|
thread.send(contents, cx)
|
||||||
})?;
|
})?;
|
||||||
send.await
|
send.await
|
||||||
|
@ -950,19 +967,24 @@ impl AcpThreadView {
|
||||||
let Some(thread) = self.thread().cloned() else {
|
let Some(thread) = self.thread().cloned() else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
if self.is_loading_contents {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
let Some(rewind) = thread.update(cx, |thread, cx| {
|
let Some(user_message_id) = thread.update(cx, |thread, _| {
|
||||||
let user_message_id = thread.entries().get(entry_ix)?.user_message()?.id.clone()?;
|
thread.entries().get(entry_ix)?.user_message()?.id.clone()
|
||||||
Some(thread.rewind(user_message_id, cx))
|
|
||||||
}) else {
|
}) else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
let contents = message_editor.update(cx, |message_editor, cx| message_editor.contents(cx));
|
let contents = message_editor.update(cx, |message_editor, cx| message_editor.contents(cx));
|
||||||
|
|
||||||
let task = cx.foreground_executor().spawn(async move {
|
let task = cx.spawn(async move |_, cx| {
|
||||||
rewind.await?;
|
let contents = contents.await?;
|
||||||
contents.await
|
thread
|
||||||
|
.update(cx, |thread, cx| thread.rewind(user_message_id, cx))?
|
||||||
|
.await?;
|
||||||
|
Ok(contents)
|
||||||
});
|
});
|
||||||
self.send_impl(task, window, cx);
|
self.send_impl(task, window, cx);
|
||||||
}
|
}
|
||||||
|
@ -1341,25 +1363,34 @@ impl AcpThreadView {
|
||||||
base_container
|
base_container
|
||||||
.child(
|
.child(
|
||||||
IconButton::new("cancel", IconName::Close)
|
IconButton::new("cancel", IconName::Close)
|
||||||
|
.disabled(self.is_loading_contents)
|
||||||
.icon_color(Color::Error)
|
.icon_color(Color::Error)
|
||||||
.icon_size(IconSize::XSmall)
|
.icon_size(IconSize::XSmall)
|
||||||
.on_click(cx.listener(Self::cancel_editing))
|
.on_click(cx.listener(Self::cancel_editing))
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
IconButton::new("regenerate", IconName::Return)
|
if self.is_loading_contents {
|
||||||
.icon_color(Color::Muted)
|
div()
|
||||||
.icon_size(IconSize::XSmall)
|
.id("loading-edited-message-content")
|
||||||
.tooltip(Tooltip::text(
|
.tooltip(Tooltip::text("Loading Added Context…"))
|
||||||
"Editing will restart the thread from this point."
|
.child(loading_contents_spinner(IconSize::XSmall))
|
||||||
))
|
.into_any_element()
|
||||||
.on_click(cx.listener({
|
} else {
|
||||||
let editor = editor.clone();
|
IconButton::new("regenerate", IconName::Return)
|
||||||
move |this, _, window, cx| {
|
.icon_color(Color::Muted)
|
||||||
this.regenerate(
|
.icon_size(IconSize::XSmall)
|
||||||
entry_ix, &editor, window, cx,
|
.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 {
|
} else {
|
||||||
|
@ -3542,7 +3573,14 @@ impl AcpThreadView {
|
||||||
.thread()
|
.thread()
|
||||||
.is_some_and(|thread| thread.read(cx).status() != ThreadStatus::Idle);
|
.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)
|
IconButton::new("stop-generation", IconName::Stop)
|
||||||
.icon_color(Color::Error)
|
.icon_color(Color::Error)
|
||||||
.style(ButtonStyle::Tinted(ui::TintColor::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 {
|
impl Focusable for AcpThreadView {
|
||||||
fn focus_handle(&self, cx: &App) -> FocusHandle {
|
fn focus_handle(&self, cx: &App) -> FocusHandle {
|
||||||
match self.thread_state {
|
match self.thread_state {
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue