Show errors from ACP when requests error

Conrad Irwin and Mikayla Maki created

Co-authored-by: Mikayla Maki <mikayla.c.maki@gmail.com>

Change summary

Cargo.lock                    |  1 
crates/acp/Cargo.toml         |  1 
crates/acp/src/server.rs      | 22 ++++++++++-
crates/acp/src/thread_view.rs | 66 +++++++++++++++++++++++++++++++++++-
4 files changed, 84 insertions(+), 6 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -22,6 +22,7 @@ dependencies = [
  "markdown",
  "parking_lot",
  "project",
+ "proto",
  "serde_json",
  "settings",
  "smol",

crates/acp/Cargo.toml 🔗

@@ -31,6 +31,7 @@ log.workspace = true
 markdown.workspace = true
 parking_lot.workspace = true
 project.workspace = true
+proto.workspace = true
 settings.workspace = true
 smol.workspace = true
 theme.workspace = true

crates/acp/src/server.rs 🔗

@@ -270,13 +270,18 @@ impl AcpServer {
     }
 
     pub async fn create_thread(self: Arc<Self>, cx: &mut AsyncApp) -> Result<Entity<AcpThread>> {
-        let response = self.connection.request(acp::CreateThreadParams).await?;
+        let response = self
+            .connection
+            .request(acp::CreateThreadParams)
+            .await
+            .map_err(to_anyhow)?;
+
         let thread_id: ThreadId = response.thread_id.into();
         let server = self.clone();
         let thread = cx.new(|_| AcpThread {
             // todo!
             title: "ACP Thread".into(),
-            id: thread_id.clone(),
+            id: thread_id.clone(), // Either<ErrorState, Id>
             next_entry_id: ThreadEntryId(0),
             entries: Vec::default(),
             project: self.project.clone(),
@@ -297,7 +302,8 @@ impl AcpServer {
                 thread_id: thread_id.clone().into(),
                 message,
             })
-            .await?;
+            .await
+            .map_err(to_anyhow)?;
         Ok(())
     }
 
@@ -306,6 +312,16 @@ impl AcpServer {
     }
 }
 
+#[track_caller]
+fn to_anyhow(e: acp::Error) -> anyhow::Error {
+    log::error!(
+        "failed to send message: {code}: {message}",
+        code = e.code,
+        message = e.message
+    );
+    anyhow::anyhow!(e.message)
+}
+
 impl From<acp::ThreadId> for ThreadId {
     fn from(thread_id: acp::ThreadId) -> Self {
         Self(thread_id.0.into())

crates/acp/src/thread_view.rs 🔗

@@ -12,8 +12,14 @@ use gpui::{
 };
 use gpui::{FocusHandle, Task};
 use language::Buffer;
+<<<<<<< HEAD
 use language::language_settings::SoftWrap;
 use markdown::{HeadingLevelStyles, MarkdownElement, MarkdownStyle};
+||||||| parent of 47b80cc740 (Show errors from ACP when requests error)
+use markdown::{HeadingLevelStyles, MarkdownElement, MarkdownStyle};
+=======
+use markdown::{HeadingLevelStyles, Markdown, MarkdownElement, MarkdownStyle};
+>>>>>>> 47b80cc740 (Show errors from ACP when requests error)
 use project::Project;
 use settings::Settings as _;
 use theme::ThemeSettings;
@@ -32,6 +38,7 @@ pub struct AcpThreadView {
     // todo! reconsider structure. currently pretty sparse, but easy to clean up if we need to delete entries.
     thread_entry_views: Vec<Option<ThreadEntryView>>,
     message_editor: Entity<Editor>,
+    last_error: Option<Entity<Markdown>>,
     list_state: ListState,
     send_task: Option<Task<Result<()>>>,
 }
@@ -96,6 +103,7 @@ impl AcpThreadView {
             message_editor,
             send_task: None,
             list_state: list_state,
+            last_error: None,
         }
     }
 
@@ -137,8 +145,37 @@ impl AcpThreadView {
             this.update_in(cx, |this, window, cx| {
                 match result {
                     Ok(thread) => {
+<<<<<<< HEAD
                         let subscription =
                             cx.subscribe_in(&thread, window, Self::handle_thread_event);
+||||||| parent of 47b80cc740 (Show errors from ACP when requests error)
+                        let subscription = cx.subscribe(&thread, |this, _, event, cx| {
+                            let count = this.list_state.item_count();
+                            match event {
+                                AcpThreadEvent::NewEntry => {
+                                    this.list_state.splice(count..count, 1);
+                                }
+                                AcpThreadEvent::EntryUpdated(index) => {
+                                    this.list_state.splice(*index..*index + 1, 1);
+                                }
+                            }
+                            cx.notify();
+                        });
+=======
+                        dbg!(&thread);
+                        let subscription = cx.subscribe(&thread, |this, _, event, cx| {
+                            let count = this.list_state.item_count();
+                            match event {
+                                AcpThreadEvent::NewEntry => {
+                                    this.list_state.splice(count..count, 1);
+                                }
+                                AcpThreadEvent::EntryUpdated(index) => {
+                                    this.list_state.splice(*index..*index + 1, 1);
+                                }
+                            }
+                            cx.notify();
+                        });
+>>>>>>> 47b80cc740 (Show errors from ACP when requests error)
                         this.list_state
                             .splice(0..0, thread.read(cx).entries().len());
 
@@ -148,6 +185,7 @@ impl AcpThreadView {
                         };
                     }
                     Err(e) => {
+                        dbg!(&e);
                         if let Some(exit_status) = agent.exit_status() {
                             this.thread_state = ThreadState::LoadError(
                                 format!(
@@ -189,6 +227,7 @@ impl AcpThreadView {
     }
 
     fn chat(&mut self, _: &Chat, window: &mut Window, cx: &mut Context<Self>) {
+        self.last_error.take();
         let text = self.message_editor.read(cx).text(cx);
         if text.is_empty() {
             return;
@@ -198,9 +237,15 @@ impl AcpThreadView {
         let task = thread.update(cx, |thread, cx| thread.send(&text, cx));
 
         self.send_task = Some(cx.spawn(async move |this, cx| {
-            task.await?;
+            let result = task.await;
 
-            this.update(cx, |this, _cx| {
+            this.update(cx, |this, cx| {
+                if let Err(err) = result {
+                    this.last_error =
+                        Some(cx.new(|cx| {
+                            Markdown::new(format!("Error: {err}").into(), None, None, cx)
+                        }))
+                }
                 this.send_task.take();
             })
         }));
@@ -865,7 +910,7 @@ impl Focusable for AcpThreadView {
 }
 
 impl Render for AcpThreadView {
-    fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
+    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
         let text = self.message_editor.read(cx).text(cx);
         let is_editor_empty = text.is_empty();
         let focus_handle = self.message_editor.focus_handle(cx);
@@ -907,6 +952,21 @@ impl Render for AcpThreadView {
                         .into()
                     })),
             })
+            .when_some(self.last_error.clone(), |el, error| {
+                el.child(
+                    div()
+                        .text_xs()
+                        .p_2()
+                        .gap_2()
+                        .border_t_1()
+                        .border_color(cx.theme().status().error_border)
+                        .bg(cx.theme().status().error_background)
+                        .child(MarkdownElement::new(
+                            error,
+                            default_markdown_style(window, cx),
+                        )),
+                )
+            })
             .child(
                 v_flex()
                     .bg(cx.theme().colors().editor_background)