From 3225e53f2f1cd65c05dc950b10633992076bb7c8 Mon Sep 17 00:00:00 2001 From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com> Date: Mon, 23 Mar 2026 20:12:44 -0300 Subject: [PATCH] agent_ui: Don't reset cursor position when editing queued messages (#52210) ## Context When typing on the read-only queued message editor, we push the content to the main, editable message editor. However, the cursor position was being reset when that happened. This PR fixes that by tracking the cursor offset when doing that, also accounting for the case where there could be pre-existing content in the message editor. ## How to Review - Queue a message in the agent panel - Place your cursor somewhere in the middle of your queued message and type something - See how the cursor position is preserved once it goes down to the main message editor ## Self-Review Checklist - [x] I've reviewed my own diff for quality, security, and reliability - [x] Unsafe blocks (if any) have justifying comments - [x] The content is consistent with the [UI/UX checklist](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist) - [x] Tests cover the new/changed behavior - [x] Performance impact has been considered and is acceptable Release Notes: - Agent: Fix cursor position being reset when editing queued messages. --- crates/agent_ui/src/conversation_view.rs | 25 ++++++++++++---- .../src/conversation_view/thread_view.rs | 24 +++++++++++---- crates/agent_ui/src/message_editor.rs | 30 +++++++++++++++++-- 3 files changed, 66 insertions(+), 13 deletions(-) diff --git a/crates/agent_ui/src/conversation_view.rs b/crates/agent_ui/src/conversation_view.rs index 5ed33487f212d1513f13f00a2f07b3e352cd5fc3..0f4777629136d138048b3f1844433118e6154adc 100644 --- a/crates/agent_ui/src/conversation_view.rs +++ b/crates/agent_ui/src/conversation_view.rs @@ -1173,12 +1173,19 @@ impl ConversationView { &mut self, index: usize, inserted_text: Option<&str>, + cursor_offset: Option, window: &mut Window, cx: &mut Context, ) { if let Some(active) = self.active_thread() { active.update(cx, |active, cx| { - active.move_queued_message_to_main_editor(index, inserted_text, window, cx); + active.move_queued_message_to_main_editor( + index, + inserted_text, + cursor_offset, + window, + cx, + ); }); } } @@ -2192,8 +2199,16 @@ impl ConversationView { &editor, window, move |this, _editor, event, window, cx| match event { - MessageEditorEvent::InputAttempted(text) => this - .move_queued_message_to_main_editor(index, Some(text.as_ref()), window, cx), + MessageEditorEvent::InputAttempted { + text, + cursor_offset, + } => this.move_queued_message_to_main_editor( + index, + Some(text.as_ref()), + Some(*cursor_offset), + window, + cx, + ), MessageEditorEvent::LostFocus => { this.save_queued_message_at_index(index, cx); } @@ -6480,7 +6495,7 @@ pub(crate) mod tests { // Main editor must be empty for this path — it is by default, but // assert to make the precondition explicit. assert!(thread.message_editor.read(cx).is_empty(cx)); - thread.move_queued_message_to_main_editor(0, None, window, cx); + thread.move_queued_message_to_main_editor(0, None, None, window, cx); }); cx.run_until_parked(); @@ -6525,7 +6540,7 @@ pub(crate) mod tests { vec![], cx, ); - thread.move_queued_message_to_main_editor(0, None, window, cx); + thread.move_queued_message_to_main_editor(0, None, None, window, cx); }); cx.run_until_parked(); diff --git a/crates/agent_ui/src/conversation_view/thread_view.rs b/crates/agent_ui/src/conversation_view/thread_view.rs index b1b7ca98923f5dda70e5b256caeed6c4372584f2..f30b324b804d2821eae71b543d485b823367598e 100644 --- a/crates/agent_ui/src/conversation_view/thread_view.rs +++ b/crates/agent_ui/src/conversation_view/thread_view.rs @@ -585,7 +585,7 @@ impl ThreadView { self.cancel_editing(&Default::default(), window, cx); } MessageEditorEvent::LostFocus => {} - MessageEditorEvent::InputAttempted(_) => {} + MessageEditorEvent::InputAttempted { .. } => {} } } @@ -722,7 +722,7 @@ impl ThreadView { ViewEvent::MessageEditorEvent(_editor, MessageEditorEvent::Cancel) => { self.cancel_editing(&Default::default(), window, cx); } - ViewEvent::MessageEditorEvent(_editor, MessageEditorEvent::InputAttempted(_)) => {} + ViewEvent::MessageEditorEvent(_editor, MessageEditorEvent::InputAttempted { .. }) => {} ViewEvent::OpenDiffLocation { path, position, @@ -1440,6 +1440,7 @@ impl ThreadView { &mut self, index: usize, inserted_text: Option<&str>, + cursor_offset: Option, window: &mut Window, cx: &mut Context, ) -> bool { @@ -1455,6 +1456,9 @@ impl ThreadView { if message_editor.read(cx).is_empty(cx) { message_editor.update(cx, |editor, cx| { editor.set_message(queued_content, window, cx); + if let Some(offset) = cursor_offset { + editor.set_cursor_offset(offset, window, cx); + } if let Some(inserted_text) = inserted_text.as_deref() { editor.insert_text(inserted_text, window, cx); } @@ -1463,8 +1467,16 @@ impl ThreadView { return true; } + // Adjust cursor offset accounting for existing content + let existing_len = message_editor.read(cx).text(cx).len(); + let separator = "\n\n"; + message_editor.update(cx, |editor, cx| { - editor.append_message(queued_content, Some("\n\n"), window, cx); + editor.append_message(queued_content, Some(separator), window, cx); + if let Some(offset) = cursor_offset { + let adjusted_offset = existing_len + separator.len() + offset; + editor.set_cursor_offset(adjusted_offset, window, cx); + } if let Some(inserted_text) = inserted_text.as_deref() { editor.insert_text(inserted_text, window, cx); } @@ -3038,7 +3050,7 @@ impl ThreadView { }) .on_click(cx.listener(move |this, _, window, cx| { this.move_queued_message_to_main_editor( - index, None, window, cx, + index, None, None, window, cx, ); })), ) @@ -3112,7 +3124,7 @@ impl ThreadView { }) .on_click(cx.listener(move |this, _, window, cx| { this.move_queued_message_to_main_editor( - index, None, window, cx, + index, None, None, window, cx, ); })), ) @@ -8169,7 +8181,7 @@ impl Render for ThreadView { cx.notify(); })) .on_action(cx.listener(|this, _: &EditFirstQueuedMessage, window, cx| { - this.move_queued_message_to_main_editor(0, None, window, cx); + this.move_queued_message_to_main_editor(0, None, None, window, cx); })) .on_action(cx.listener(|this, _: &ClearMessageQueue, _, cx| { this.local_queued_messages.clear(); diff --git a/crates/agent_ui/src/message_editor.rs b/crates/agent_ui/src/message_editor.rs index a4f444cfe7364dad64098eeb33b40078055a66d6..df8ab3d08aaaa77f9490603efe03ade5d1ecff4d 100644 --- a/crates/agent_ui/src/message_editor.rs +++ b/crates/agent_ui/src/message_editor.rs @@ -151,7 +151,10 @@ pub enum MessageEditorEvent { Cancel, Focus, LostFocus, - InputAttempted(Arc), + InputAttempted { + text: Arc, + cursor_offset: usize, + }, } impl EventEmitter for MessageEditor {} @@ -257,7 +260,15 @@ impl MessageEditor { && editor.read(cx).read_only(cx) && !text.is_empty() { - cx.emit(MessageEditorEvent::InputAttempted(text.clone())); + let editor = editor.read(cx); + let cursor_anchor = editor.selections.newest_anchor().head(); + let cursor_offset = cursor_anchor + .to_offset(&editor.buffer().read(cx).snapshot(cx)) + .0; + cx.emit(MessageEditorEvent::InputAttempted { + text: text.clone(), + cursor_offset, + }); } if let EditorEvent::Edited { .. } = event @@ -1580,6 +1591,21 @@ impl MessageEditor { self.editor.read(cx).text(cx) } + pub fn set_cursor_offset( + &mut self, + offset: usize, + window: &mut Window, + cx: &mut Context, + ) { + self.editor.update(cx, |editor, cx| { + let snapshot = editor.buffer().read(cx).snapshot(cx); + let offset = snapshot.clip_offset(MultiBufferOffset(offset), text::Bias::Left); + editor.change_selections(Default::default(), window, cx, |selections| { + selections.select_ranges([offset..offset]); + }); + }); + } + pub fn insert_text(&mut self, text: &str, window: &mut Window, cx: &mut Context) { if text.is_empty() { return;