Detailed changes
@@ -0,0 +1,7 @@
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M10.3404 3.53018H2.73401" stroke="#C6CAD0" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M10.3404 7.62592H2.73401" stroke="#C6CAD0" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M6.24467 11.7217H2.73401" stroke="#C6CAD0" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M10.3404 9.96637L8.58511 11.7217L10.3404 13.477" stroke="#C6CAD0" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M13.266 3.53018V10.5515C13.266 10.8619 13.1427 11.1595 12.9232 11.379C12.7038 11.5984 12.4061 11.7217 12.0958 11.7217H8.58511" stroke="#C6CAD0" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+</svg>
@@ -1165,6 +1165,20 @@ impl ConnectionView {
}
}
+ fn move_queued_message_to_main_editor(
+ &mut self,
+ index: usize,
+ inserted_text: Option<&str>,
+ window: &mut Window,
+ cx: &mut Context<Self>,
+ ) {
+ if let Some(active) = self.active_thread() {
+ active.update(cx, |active, cx| {
+ active.move_queued_message_to_main_editor(index, inserted_text, window, cx);
+ });
+ }
+ }
+
fn handle_thread_event(
&mut self,
thread: &Entity<AcpThread>,
@@ -2162,6 +2176,7 @@ impl ConnectionView {
for (index, editor) in editors.into_iter().enumerate() {
if let Some(content) = queued_messages.get(index) {
editor.update(cx, |editor, cx| {
+ editor.set_read_only(true, cx);
editor.set_message(content.clone(), window, cx);
});
}
@@ -2190,6 +2205,7 @@ impl ConnectionView {
window,
cx,
);
+ editor.set_read_only(true, cx);
editor.set_message(content, window, cx);
editor
});
@@ -2198,6 +2214,8 @@ impl ConnectionView {
&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::LostFocus => {
this.save_queued_message_at_index(index, cx);
}
@@ -6084,4 +6102,86 @@ pub(crate) mod tests {
assert_eq!(tool_call_id, acp::ToolCallId::new("tc-b"));
});
}
+
+ #[gpui::test]
+ async fn test_move_queued_message_to_empty_main_editor(cx: &mut TestAppContext) {
+ init_test(cx);
+
+ let (connection_view, cx) =
+ setup_thread_view(StubAgentServer::default_response(), cx).await;
+
+ // Add a plain-text message to the queue directly.
+ active_thread(&connection_view, cx).update_in(cx, |thread, window, cx| {
+ thread.add_to_queue(
+ vec![acp::ContentBlock::Text(acp::TextContent::new(
+ "queued message".to_string(),
+ ))],
+ vec![],
+ cx,
+ );
+ // 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);
+ });
+
+ cx.run_until_parked();
+
+ // Queue should now be empty.
+ let queue_len = active_thread(&connection_view, cx)
+ .read_with(cx, |thread, _cx| thread.local_queued_messages.len());
+ assert_eq!(queue_len, 0, "Queue should be empty after move");
+
+ // Main editor should contain the queued message text.
+ let text = message_editor(&connection_view, cx).update(cx, |editor, cx| editor.text(cx));
+ assert_eq!(
+ text, "queued message",
+ "Main editor should contain the moved queued message"
+ );
+ }
+
+ #[gpui::test]
+ async fn test_move_queued_message_to_non_empty_main_editor(cx: &mut TestAppContext) {
+ init_test(cx);
+
+ let (connection_view, cx) =
+ setup_thread_view(StubAgentServer::default_response(), cx).await;
+
+ // Seed the main editor with existing content.
+ message_editor(&connection_view, cx).update_in(cx, |editor, window, cx| {
+ editor.set_message(
+ vec![acp::ContentBlock::Text(acp::TextContent::new(
+ "existing content".to_string(),
+ ))],
+ window,
+ cx,
+ );
+ });
+
+ // Add a plain-text message to the queue.
+ active_thread(&connection_view, cx).update_in(cx, |thread, window, cx| {
+ thread.add_to_queue(
+ vec![acp::ContentBlock::Text(acp::TextContent::new(
+ "queued message".to_string(),
+ ))],
+ vec![],
+ cx,
+ );
+ thread.move_queued_message_to_main_editor(0, None, window, cx);
+ });
+
+ cx.run_until_parked();
+
+ // Queue should now be empty.
+ let queue_len = active_thread(&connection_view, cx)
+ .read_with(cx, |thread, _cx| thread.local_queued_messages.len());
+ assert_eq!(queue_len, 0, "Queue should be empty after move");
+
+ // Main editor should contain existing content + separator + queued content.
+ let text = message_editor(&connection_view, cx).update(cx, |editor, cx| editor.text(cx));
+ assert_eq!(
+ text, "existing content\n\nqueued message",
+ "Main editor should have existing content and queued message separated by two newlines"
+ );
+ }
}
@@ -463,6 +463,7 @@ impl ThreadView {
self.cancel_editing(&Default::default(), window, cx);
}
MessageEditorEvent::LostFocus => {}
+ MessageEditorEvent::InputAttempted(_) => {}
}
}
@@ -577,6 +578,7 @@ impl ThreadView {
ViewEvent::MessageEditorEvent(_editor, MessageEditorEvent::Cancel) => {
self.cancel_editing(&Default::default(), window, cx);
}
+ ViewEvent::MessageEditorEvent(_editor, MessageEditorEvent::InputAttempted(_)) => {}
ViewEvent::OpenDiffLocation {
path,
position,
@@ -1217,6 +1219,44 @@ impl ThreadView {
self.send_content(contents_task, window, cx);
}
+ pub fn move_queued_message_to_main_editor(
+ &mut self,
+ index: usize,
+ inserted_text: Option<&str>,
+ window: &mut Window,
+ cx: &mut Context<Self>,
+ ) -> bool {
+ let Some(queued_message) = self.remove_from_queue(index, cx) else {
+ return false;
+ };
+ let queued_content = queued_message.content;
+ let message_editor = self.message_editor.clone();
+ let inserted_text = inserted_text.map(ToOwned::to_owned);
+
+ window.focus(&message_editor.focus_handle(cx), cx);
+
+ if message_editor.read(cx).is_empty(cx) {
+ message_editor.update(cx, |editor, cx| {
+ editor.set_message(queued_content, window, cx);
+ if let Some(inserted_text) = inserted_text.as_deref() {
+ editor.insert_text(inserted_text, window, cx);
+ }
+ });
+ cx.notify();
+ return true;
+ }
+
+ message_editor.update(cx, |editor, cx| {
+ editor.append_message(queued_content, Some("\n\n"), window, cx);
+ if let Some(inserted_text) = inserted_text.as_deref() {
+ editor.insert_text(inserted_text, window, cx);
+ }
+ });
+
+ cx.notify();
+ true
+ }
+
// editor methods
pub fn expand_message_editor(
@@ -2663,50 +2703,24 @@ impl ThreadView {
.child(if editor_focused {
h_flex()
.gap_1()
- .min_w_40()
+ .min_w(rems_from_px(150.))
+ .justify_end()
.child(
- IconButton::new(("cancel_edit", index), IconName::Close)
- .icon_size(IconSize::Small)
- .icon_color(Color::Error)
- .tooltip({
- let focus_handle = editor.focus_handle(cx);
- move |_window, cx| {
- Tooltip::for_action_in(
- "Cancel Edit",
- &editor::actions::Cancel,
- &focus_handle,
- cx,
- )
- }
- })
- .on_click({
- let main_editor = self.message_editor.clone();
- cx.listener(move |_, _, window, cx| {
- window.focus(&main_editor.focus_handle(cx), cx);
- })
- }),
- )
- .child(
- IconButton::new(("save_edit", index), IconName::Check)
+ IconButton::new(("edit", index), IconName::Pencil)
.icon_size(IconSize::Small)
- .icon_color(Color::Success)
- .tooltip({
- let focus_handle = editor.focus_handle(cx);
- move |_window, cx| {
- Tooltip::for_action_in(
- "Save Edit",
- &Chat,
- &focus_handle,
- cx,
- )
- }
+ .tooltip(|_window, cx| {
+ Tooltip::with_meta(
+ "Edit Queued Message",
+ None,
+ "Type anything to edit",
+ cx,
+ )
})
- .on_click({
- let main_editor = self.message_editor.clone();
- cx.listener(move |_, _, window, cx| {
- window.focus(&main_editor.focus_handle(cx), cx);
- })
- }),
+ .on_click(cx.listener(move |this, _, window, cx| {
+ this.move_queued_message_to_main_editor(
+ index, None, window, cx,
+ );
+ })),
)
.child(
Button::new(("send_now_focused", index), "Send Now")
@@ -2728,62 +2742,64 @@ impl ThreadView {
)
} else {
h_flex()
- .gap_1()
.when(!is_next, |this| this.visible_on_hover("queue_entry"))
+ .gap_1()
+ .min_w(rems_from_px(150.))
+ .justify_end()
.child(
- IconButton::new(("edit", index), IconName::Pencil)
+ IconButton::new(("delete", index), IconName::Trash)
.icon_size(IconSize::Small)
.tooltip({
let focus_handle = focus_handle.clone();
move |_window, cx| {
if is_next {
Tooltip::for_action_in(
- "Edit",
- &EditFirstQueuedMessage,
+ "Remove Message from Queue",
+ &RemoveFirstQueuedMessage,
&focus_handle,
cx,
)
} else {
- Tooltip::simple("Edit", cx)
+ Tooltip::simple(
+ "Remove Message from Queue",
+ cx,
+ )
}
}
})
- .on_click({
- let editor = editor.clone();
- cx.listener(move |_, _, window, cx| {
- window.focus(&editor.focus_handle(cx), cx);
- })
- }),
+ .on_click(cx.listener(move |this, _, _, cx| {
+ this.remove_from_queue(index, cx);
+ cx.notify();
+ })),
)
.child(
- IconButton::new(("delete", index), IconName::Trash)
+ IconButton::new(("edit", index), IconName::Pencil)
.icon_size(IconSize::Small)
.tooltip({
let focus_handle = focus_handle.clone();
move |_window, cx| {
if is_next {
Tooltip::for_action_in(
- "Remove Message from Queue",
- &RemoveFirstQueuedMessage,
+ "Edit",
+ &EditFirstQueuedMessage,
&focus_handle,
cx,
)
} else {
- Tooltip::simple(
- "Remove Message from Queue",
- cx,
- )
+ Tooltip::simple("Edit", cx)
}
}
})
- .on_click(cx.listener(move |this, _, _, cx| {
- this.remove_from_queue(index, cx);
- cx.notify();
+ .on_click(cx.listener(move |this, _, window, cx| {
+ this.move_queued_message_to_main_editor(
+ index, None, window, cx,
+ );
})),
)
.child(
Button::new(("send_now", index), "Send Now")
.label_size(LabelSize::Small)
+ .when(is_next, |this| this.style(ButtonStyle::Outlined))
.when(is_next && message_editor.is_empty(cx), |this| {
let action: Box<dyn gpui::Action> =
if can_fast_track {
@@ -2792,7 +2808,7 @@ impl ThreadView {
Box::new(SendNextQueuedMessage)
};
- this.style(ButtonStyle::Outlined).key_binding(
+ this.key_binding(
KeyBinding::for_action_in(
action.as_ref(),
&focus_handle.clone(),
@@ -2801,9 +2817,6 @@ impl ThreadView {
.map(|kb| kb.size(keybinding_size)),
)
})
- .when(is_next && !message_editor.is_empty(cx), |this| {
- this.style(ButtonStyle::Outlined)
- })
.on_click(cx.listener(move |this, _, window, cx| {
this.send_queued_message_at_index(
index, true, window, cx,
@@ -3281,7 +3294,12 @@ impl ThreadView {
.on_click(cx.listener(|this, _event, _, cx| this.cancel_generation(cx)))
.into_any_element()
} else {
- IconButton::new("send-message", IconName::Send)
+ let send_icon = if is_generating {
+ IconName::QueueMessage
+ } else {
+ IconName::Send
+ };
+ IconButton::new("send-message", send_icon)
.style(ButtonStyle::Filled)
.map(|this| {
if is_editor_empty && !is_generating {
@@ -7577,9 +7595,7 @@ impl Render for ThreadView {
cx.notify();
}))
.on_action(cx.listener(|this, _: &EditFirstQueuedMessage, window, cx| {
- if let Some(editor) = this.queued_message_editors.first() {
- window.focus(&editor.focus_handle(cx), cx);
- }
+ this.move_queued_message_to_main_editor(0, None, window, cx);
}))
.on_action(cx.listener(|this, _: &ClearMessageQueue, _, cx| {
this.local_queued_messages.clear();
@@ -114,7 +114,7 @@ impl EntryViewState {
cx.subscribe(&message_editor, move |_, editor, event, cx| {
cx.emit(EntryViewEvent {
entry_index: index,
- view_event: ViewEvent::MessageEditorEvent(editor, *event),
+ view_event: ViewEvent::MessageEditorEvent(editor, event.clone()),
})
})
.detach();
@@ -51,13 +51,14 @@ pub struct MessageEditor {
_parse_slash_command_task: Task<()>,
}
-#[derive(Clone, Copy, Debug)]
+#[derive(Clone, Debug)]
pub enum MessageEditorEvent {
Send,
SendImmediately,
Cancel,
Focus,
LostFocus,
+ InputAttempted(Arc<str>),
}
impl EventEmitter<MessageEditorEvent> for MessageEditor {}
@@ -186,6 +187,18 @@ impl MessageEditor {
subscriptions.push(cx.subscribe_in(&editor, window, {
move |this, editor, event, window, cx| {
+ let input_attempted_text = match event {
+ EditorEvent::InputHandled { text, .. } => Some(text),
+ EditorEvent::InputIgnored { text } => Some(text),
+ _ => None,
+ };
+ if let Some(text) = input_attempted_text
+ && editor.read(cx).read_only(cx)
+ && !text.is_empty()
+ {
+ cx.emit(MessageEditorEvent::InputAttempted(text.clone()));
+ }
+
if let EditorEvent::Edited { .. } = event
&& !editor.read(cx).read_only(cx)
{
@@ -1195,13 +1208,45 @@ impl MessageEditor {
message: Vec<acp::ContentBlock>,
window: &mut Window,
cx: &mut Context<Self>,
+ ) {
+ self.clear(window, cx);
+ self.insert_message_blocks(message, false, window, cx);
+ }
+
+ pub fn append_message(
+ &mut self,
+ message: Vec<acp::ContentBlock>,
+ separator: Option<&str>,
+ window: &mut Window,
+ cx: &mut Context<Self>,
+ ) {
+ if message.is_empty() {
+ return;
+ }
+
+ if let Some(separator) = separator
+ && !separator.is_empty()
+ && !self.is_empty(cx)
+ {
+ self.editor.update(cx, |editor, cx| {
+ editor.insert(separator, window, cx);
+ });
+ }
+
+ self.insert_message_blocks(message, true, window, cx);
+ }
+
+ fn insert_message_blocks(
+ &mut self,
+ message: Vec<acp::ContentBlock>,
+ append_to_existing: bool,
+ window: &mut Window,
+ cx: &mut Context<Self>,
) {
let Some(workspace) = self.workspace.upgrade() else {
return;
};
- self.clear(window, cx);
-
let path_style = workspace.read(cx).project().read(cx).path_style(cx);
let mut text = String::new();
let mut mentions = Vec::new();
@@ -1275,13 +1320,31 @@ impl MessageEditor {
}
}
- let snapshot = self.editor.update(cx, |editor, cx| {
- editor.set_text(text, window, cx);
- editor.buffer().read(cx).snapshot(cx)
- });
+ if text.is_empty() && mentions.is_empty() {
+ return;
+ }
+
+ let insertion_start = if append_to_existing {
+ self.editor.read(cx).text(cx).len()
+ } else {
+ 0
+ };
+
+ let snapshot = if append_to_existing {
+ self.editor.update(cx, |editor, cx| {
+ editor.insert(&text, window, cx);
+ editor.buffer().read(cx).snapshot(cx)
+ })
+ } else {
+ self.editor.update(cx, |editor, cx| {
+ editor.set_text(text, window, cx);
+ editor.buffer().read(cx).snapshot(cx)
+ })
+ };
for (range, mention_uri, mention) in mentions {
- let anchor = snapshot.anchor_before(MultiBufferOffset(range.start));
+ let adjusted_start = insertion_start + range.start;
+ let anchor = snapshot.anchor_before(MultiBufferOffset(adjusted_start));
let Some((crease_id, tx)) = insert_crease_for_mention(
anchor.excerpt_id,
anchor.text_anchor,
@@ -1306,6 +1369,7 @@ impl MessageEditor {
)
});
}
+
cx.notify();
}
@@ -1313,6 +1377,16 @@ impl MessageEditor {
self.editor.read(cx).text(cx)
}
+ pub fn insert_text(&mut self, text: &str, window: &mut Window, cx: &mut Context<Self>) {
+ if text.is_empty() {
+ return;
+ }
+
+ self.editor.update(cx, |editor, cx| {
+ editor.insert(text, window, cx);
+ });
+ }
+
pub fn set_placeholder_text(
&mut self,
placeholder: &str,
@@ -3461,4 +3535,241 @@ mod tests {
text
);
}
+
+ // Helper that creates a minimal MessageEditor inside a window, returning both
+ // the entity and the underlying VisualTestContext so callers can drive updates.
+ async fn setup_message_editor(
+ cx: &mut TestAppContext,
+ ) -> (Entity<MessageEditor>, &mut VisualTestContext) {
+ let fs = FakeFs::new(cx.executor());
+ fs.insert_tree("/project", json!({"file.txt": ""})).await;
+ let project = Project::test(fs, [Path::new(path!("/project"))], cx).await;
+
+ let (multi_workspace, cx) =
+ cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
+ let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
+ let history =
+ cx.update(|window, cx| cx.new(|cx| crate::ThreadHistory::new(None, window, cx)));
+
+ let message_editor = cx.update(|window, cx| {
+ cx.new(|cx| {
+ MessageEditor::new(
+ workspace.downgrade(),
+ project.downgrade(),
+ None,
+ history.downgrade(),
+ None,
+ Default::default(),
+ Default::default(),
+ "Test Agent".into(),
+ "Test",
+ EditorMode::AutoHeight {
+ min_lines: 1,
+ max_lines: None,
+ },
+ window,
+ cx,
+ )
+ })
+ });
+
+ cx.run_until_parked();
+ (message_editor, cx)
+ }
+
+ #[gpui::test]
+ async fn test_set_message_plain_text(cx: &mut TestAppContext) {
+ init_test(cx);
+ let (message_editor, cx) = setup_message_editor(cx).await;
+
+ message_editor.update_in(cx, |editor, window, cx| {
+ editor.set_message(
+ vec![acp::ContentBlock::Text(acp::TextContent::new(
+ "hello world".to_string(),
+ ))],
+ window,
+ cx,
+ );
+ });
+
+ let text = message_editor.update(cx, |editor, cx| editor.text(cx));
+ assert_eq!(text, "hello world");
+ assert!(!message_editor.update(cx, |editor, cx| editor.is_empty(cx)));
+ }
+
+ #[gpui::test]
+ async fn test_set_message_replaces_existing_content(cx: &mut TestAppContext) {
+ init_test(cx);
+ let (message_editor, cx) = setup_message_editor(cx).await;
+
+ // Set initial content.
+ message_editor.update_in(cx, |editor, window, cx| {
+ editor.set_message(
+ vec![acp::ContentBlock::Text(acp::TextContent::new(
+ "old content".to_string(),
+ ))],
+ window,
+ cx,
+ );
+ });
+
+ // Replace with new content.
+ message_editor.update_in(cx, |editor, window, cx| {
+ editor.set_message(
+ vec![acp::ContentBlock::Text(acp::TextContent::new(
+ "new content".to_string(),
+ ))],
+ window,
+ cx,
+ );
+ });
+
+ let text = message_editor.update(cx, |editor, cx| editor.text(cx));
+ assert_eq!(
+ text, "new content",
+ "set_message should replace old content"
+ );
+ }
+
+ #[gpui::test]
+ async fn test_append_message_to_empty_editor(cx: &mut TestAppContext) {
+ init_test(cx);
+ let (message_editor, cx) = setup_message_editor(cx).await;
+
+ message_editor.update_in(cx, |editor, window, cx| {
+ editor.append_message(
+ vec![acp::ContentBlock::Text(acp::TextContent::new(
+ "appended".to_string(),
+ ))],
+ Some("\n\n"),
+ window,
+ cx,
+ );
+ });
+
+ let text = message_editor.update(cx, |editor, cx| editor.text(cx));
+ assert_eq!(
+ text, "appended",
+ "No separator should be inserted when the editor is empty"
+ );
+ }
+
+ #[gpui::test]
+ async fn test_append_message_to_non_empty_editor(cx: &mut TestAppContext) {
+ init_test(cx);
+ let (message_editor, cx) = setup_message_editor(cx).await;
+
+ // Seed initial content.
+ message_editor.update_in(cx, |editor, window, cx| {
+ editor.set_message(
+ vec![acp::ContentBlock::Text(acp::TextContent::new(
+ "initial".to_string(),
+ ))],
+ window,
+ cx,
+ );
+ });
+
+ // Append with separator.
+ message_editor.update_in(cx, |editor, window, cx| {
+ editor.append_message(
+ vec![acp::ContentBlock::Text(acp::TextContent::new(
+ "appended".to_string(),
+ ))],
+ Some("\n\n"),
+ window,
+ cx,
+ );
+ });
+
+ let text = message_editor.update(cx, |editor, cx| editor.text(cx));
+ assert_eq!(
+ text, "initial\n\nappended",
+ "Separator should appear between existing and appended content"
+ );
+ }
+
+ #[gpui::test]
+ async fn test_append_message_preserves_mention_offset(cx: &mut TestAppContext) {
+ init_test(cx);
+
+ let fs = FakeFs::new(cx.executor());
+ fs.insert_tree("/project", json!({"file.txt": "content"}))
+ .await;
+ let project = Project::test(fs, [Path::new(path!("/project"))], cx).await;
+
+ let (multi_workspace, cx) =
+ cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
+ let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
+ let history =
+ cx.update(|window, cx| cx.new(|cx| crate::ThreadHistory::new(None, window, cx)));
+
+ let message_editor = cx.update(|window, cx| {
+ cx.new(|cx| {
+ MessageEditor::new(
+ workspace.downgrade(),
+ project.downgrade(),
+ None,
+ history.downgrade(),
+ None,
+ Default::default(),
+ Default::default(),
+ "Test Agent".into(),
+ "Test",
+ EditorMode::AutoHeight {
+ min_lines: 1,
+ max_lines: None,
+ },
+ window,
+ cx,
+ )
+ })
+ });
+
+ cx.run_until_parked();
+
+ // Seed plain-text prefix so the editor is non-empty before appending.
+ message_editor.update_in(cx, |editor, window, cx| {
+ editor.set_message(
+ vec![acp::ContentBlock::Text(acp::TextContent::new(
+ "prefix text".to_string(),
+ ))],
+ window,
+ cx,
+ );
+ });
+
+ // Append a message that contains a ResourceLink mention.
+ message_editor.update_in(cx, |editor, window, cx| {
+ editor.append_message(
+ vec![acp::ContentBlock::ResourceLink(acp::ResourceLink::new(
+ "file.txt",
+ "file:///project/file.txt",
+ ))],
+ Some("\n\n"),
+ window,
+ cx,
+ );
+ });
+
+ cx.run_until_parked();
+
+ // The mention should be registered in the mention_set so that contents()
+ // will emit it as a structured block rather than plain text.
+ let mention_uris =
+ message_editor.update(cx, |editor, cx| editor.mention_set.read(cx).mentions());
+ assert_eq!(
+ mention_uris.len(),
+ 1,
+ "Expected exactly one mention in the mention_set after append, got: {mention_uris:?}"
+ );
+
+ // The editor text should start with the prefix, then the separator, then
+ // the mention placeholder — confirming the offset was computed correctly.
+ let text = message_editor.update(cx, |editor, cx| editor.text(cx));
+ assert!(
+ text.starts_with("prefix text\n\n"),
+ "Expected text to start with 'prefix text\\n\\n', got: {text:?}"
+ );
+ }
}
@@ -191,6 +191,7 @@ pub enum IconName {
Power,
Public,
PullRequest,
+ QueueMessage,
Quote,
Reader,
RefreshTitle,