acp: Cancel editing when focus is lost and message was not changed (#36822)

Bennet Bo Fenner created

Release Notes:

- N/A

Change summary

crates/acp_thread/src/acp_thread.rs       |  2 +-
crates/agent_ui/src/acp/message_editor.rs | 16 ++++++++++------
crates/agent_ui/src/acp/thread_view.rs    | 13 +++++++++++++
3 files changed, 24 insertions(+), 7 deletions(-)

Detailed changes

crates/acp_thread/src/acp_thread.rs 🔗

@@ -509,7 +509,7 @@ impl ContentBlock {
         "`Image`".into()
     }
 
-    fn to_markdown<'a>(&'a self, cx: &'a App) -> &'a str {
+    pub fn to_markdown<'a>(&'a self, cx: &'a App) -> &'a str {
         match self {
             ContentBlock::Empty => "",
             ContentBlock::Markdown { markdown } => markdown.read(cx).source(),

crates/agent_ui/src/acp/message_editor.rs 🔗

@@ -74,6 +74,7 @@ pub enum MessageEditorEvent {
     Send,
     Cancel,
     Focus,
+    LostFocus,
 }
 
 impl EventEmitter<MessageEditorEvent> for MessageEditor {}
@@ -131,10 +132,14 @@ impl MessageEditor {
             editor
         });
 
-        cx.on_focus(&editor.focus_handle(cx), window, |_, _, cx| {
+        cx.on_focus_in(&editor.focus_handle(cx), window, |_, _, cx| {
             cx.emit(MessageEditorEvent::Focus)
         })
         .detach();
+        cx.on_focus_out(&editor.focus_handle(cx), window, |_, _, _, cx| {
+            cx.emit(MessageEditorEvent::LostFocus)
+        })
+        .detach();
 
         let mut subscriptions = Vec::new();
         subscriptions.push(cx.subscribe_in(&editor, window, {
@@ -1169,17 +1174,16 @@ impl MessageEditor {
         })
     }
 
+    pub fn text(&self, cx: &App) -> String {
+        self.editor.read(cx).text(cx)
+    }
+
     #[cfg(test)]
     pub fn set_text(&mut self, text: &str, window: &mut Window, cx: &mut Context<Self>) {
         self.editor.update(cx, |editor, cx| {
             editor.set_text(text, window, cx);
         });
     }
-
-    #[cfg(test)]
-    pub fn text(&self, cx: &App) -> String {
-        self.editor.read(cx).text(cx)
-    }
 }
 
 fn render_directory_contents(entries: Vec<(Arc<Path>, PathBuf, String)>) -> String {

crates/agent_ui/src/acp/thread_view.rs 🔗

@@ -762,6 +762,7 @@ impl AcpThreadView {
             MessageEditorEvent::Focus => {
                 self.cancel_editing(&Default::default(), window, cx);
             }
+            MessageEditorEvent::LostFocus => {}
         }
     }
 
@@ -793,6 +794,18 @@ impl AcpThreadView {
                     cx.notify();
                 }
             }
+            ViewEvent::MessageEditorEvent(editor, MessageEditorEvent::LostFocus) => {
+                if let Some(thread) = self.thread()
+                    && let Some(AgentThreadEntry::UserMessage(user_message)) =
+                        thread.read(cx).entries().get(event.entry_index)
+                    && user_message.id.is_some()
+                {
+                    if editor.read(cx).text(cx).as_str() == user_message.content.to_markdown(cx) {
+                        self.editing_message = None;
+                        cx.notify();
+                    }
+                }
+            }
             ViewEvent::MessageEditorEvent(editor, MessageEditorEvent::Send) => {
                 self.regenerate(event.entry_index, editor, window, cx);
             }