assistant panel: Stop animation & show explicit state if canceled (#16200)

Thorsten Ball created

This fixes a bug by stopping the animation when a completion is canceled
and it also makes the state more explicit, which I think is very
valuable.



https://github.com/user-attachments/assets/9ede9b25-86ac-4901-8434-7407896bb799


Release Notes:

- N/A

Change summary

crates/assistant/src/assistant.rs       |  7 ++
crates/assistant/src/assistant_panel.rs | 83 +++++++++++++++++---------
crates/assistant/src/context.rs         | 13 +++
crates/proto/proto/zed.proto            |  3 
4 files changed, 75 insertions(+), 31 deletions(-)

Detailed changes

crates/assistant/src/assistant.rs 🔗

@@ -102,6 +102,7 @@ pub enum MessageStatus {
     Pending,
     Done,
     Error(SharedString),
+    Canceled,
 }
 
 impl MessageStatus {
@@ -112,6 +113,7 @@ impl MessageStatus {
             Some(proto::context_message_status::Variant::Error(error)) => {
                 MessageStatus::Error(error.message.into())
             }
+            Some(proto::context_message_status::Variant::Canceled(_)) => MessageStatus::Canceled,
             None => MessageStatus::Pending,
         }
     }
@@ -135,6 +137,11 @@ impl MessageStatus {
                     },
                 )),
             },
+            MessageStatus::Canceled => proto::ContextMessageStatus {
+                variant: Some(proto::context_message_status::Variant::Canceled(
+                    proto::context_message_status::Canceled {},
+                )),
+            },
         }
     }
 }

crates/assistant/src/assistant_panel.rs 🔗

@@ -1975,7 +1975,7 @@ impl ContextEditor {
     fn cancel(&mut self, _: &editor::actions::Cancel, cx: &mut ViewContext<Self>) {
         if self
             .context
-            .update(cx, |context, _| context.cancel_last_assist())
+            .update(cx, |context, cx| context.cancel_last_assist(cx))
         {
             return;
         }
@@ -3042,22 +3042,6 @@ impl ContextEditor {
                                     }
                                 });
 
-                            let trigger = Button::new("show-error", "Error")
-                                .color(Color::Error)
-                                .selected_label_color(Color::Error)
-                                .selected_icon_color(Color::Error)
-                                .icon(IconName::XCircle)
-                                .icon_color(Color::Error)
-                                .icon_size(IconSize::Small)
-                                .icon_position(IconPosition::Start)
-                                .tooltip(move |cx| {
-                                    Tooltip::with_meta(
-                                        "Error interacting with language model",
-                                        None,
-                                        "Click for more details",
-                                        cx,
-                                    )
-                                });
                             h_flex()
                                 .id(("message_header", message_id.as_u64()))
                                 .pl(cx.gutter_dimensions.full_width())
@@ -3066,22 +3050,63 @@ impl ContextEditor {
                                 .relative()
                                 .gap_1()
                                 .child(sender)
-                                .children(
-                                    if let MessageStatus::Error(error) = message.status.clone() {
+                                .children(match &message.status {
+                                    MessageStatus::Error(error) => {
+                                        let error_popover_trigger =
+                                            Button::new("show-error", "Error")
+                                                .color(Color::Error)
+                                                .selected_label_color(Color::Error)
+                                                .selected_icon_color(Color::Error)
+                                                .icon(IconName::XCircle)
+                                                .icon_color(Color::Error)
+                                                .icon_size(IconSize::Small)
+                                                .icon_position(IconPosition::Start)
+                                                .tooltip(move |cx| {
+                                                    Tooltip::with_meta(
+                                                        "Error interacting with language model",
+                                                        None,
+                                                        "Click for more details",
+                                                        cx,
+                                                    )
+                                                });
+
                                         Some(
                                             PopoverMenu::new("show-error-popover")
-                                                .menu(move |cx| {
-                                                    Some(cx.new_view(|cx| ErrorPopover {
-                                                        error: error.clone(),
-                                                        focus_handle: cx.focus_handle(),
-                                                    }))
+                                                .menu({
+                                                    let error = error.clone();
+                                                    move |cx| {
+                                                        Some(cx.new_view(|cx| ErrorPopover {
+                                                            error: error.clone(),
+                                                            focus_handle: cx.focus_handle(),
+                                                        }))
+                                                    }
                                                 })
-                                                .trigger(trigger),
+                                                .trigger(error_popover_trigger)
+                                                .into_any_element(),
                                         )
-                                    } else {
-                                        None
-                                    },
-                                )
+                                    }
+                                    MessageStatus::Canceled => Some(
+                                        ButtonLike::new("canceled")
+                                            .child(
+                                                Icon::new(IconName::XCircle).color(Color::Disabled),
+                                            )
+                                            .child(
+                                                Label::new("Canceled")
+                                                    .size(LabelSize::Small)
+                                                    .color(Color::Disabled),
+                                            )
+                                            .tooltip(move |cx| {
+                                                Tooltip::with_meta(
+                                                    "Canceled",
+                                                    None,
+                                                    "Interaction with the assistant was canceled",
+                                                    cx,
+                                                )
+                                            })
+                                            .into_any_element(),
+                                    ),
+                                    _ => None,
+                                })
                                 .into_any_element()
                         }
                     }),

crates/assistant/src/context.rs 🔗

@@ -401,6 +401,7 @@ impl PartialEq for ImageAnchor {
 
 struct PendingCompletion {
     id: usize,
+    assistant_message_id: MessageId,
     _task: Task<()>,
 }
 
@@ -1806,6 +1807,7 @@ impl Context {
 
         self.pending_completions.push(PendingCompletion {
             id: post_inc(&mut self.completion_count),
+            assistant_message_id: assistant_message.id,
             _task: task,
         });
 
@@ -1827,8 +1829,15 @@ impl Context {
         }
     }
 
-    pub fn cancel_last_assist(&mut self) -> bool {
-        self.pending_completions.pop().is_some()
+    pub fn cancel_last_assist(&mut self, cx: &mut ModelContext<Self>) -> bool {
+        if let Some(pending_completion) = self.pending_completions.pop() {
+            self.update_metadata(pending_completion.assistant_message_id, cx, |metadata| {
+                metadata.status = MessageStatus::Canceled;
+            });
+            true
+        } else {
+            false
+        }
     }
 
     pub fn cycle_message_roles(&mut self, ids: HashSet<MessageId>, cx: &mut ModelContext<Self>) {

crates/proto/proto/zed.proto 🔗

@@ -2310,6 +2310,7 @@ message ContextMessageStatus {
         Done done = 1;
         Pending pending = 2;
         Error error = 3;
+        Canceled canceled = 4;
     }
 
     message Done {}
@@ -2319,6 +2320,8 @@ message ContextMessageStatus {
     message Error {
         string message = 1;
     }
+
+    message Canceled {}
 }
 
 message ContextMessage {