agent2 basic message editor

Agus Zubiaga and Smit Barmase created

Co-authored-by: Smit Barmase <heysmitbarmase@gmail.com>

Change summary

Cargo.lock                            |   3 
crates/agent2/Cargo.toml              |   3 
crates/agent2/src/thread_element.rs   | 113 ++++++++++++++++++++++++++--
crates/agent_ui/src/agent_panel.rs    |  10 +-
crates/agent_ui/src/agent_ui.rs       |   1 
crates/agent_ui/src/message_editor.rs |   3 
crates/zed_actions/src/lib.rs         |   7 +
7 files changed, 123 insertions(+), 17 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -117,6 +117,7 @@ dependencies = [
  "base64 0.22.1",
  "chrono",
  "collections",
+ "editor",
  "env_logger 0.11.8",
  "futures 0.3.31",
  "gpui",
@@ -126,9 +127,11 @@ dependencies = [
  "serde_json",
  "settings",
  "smol",
+ "ui",
  "util",
  "uuid",
  "workspace-hack",
+ "zed_actions",
 ]
 
 [[package]]

crates/agent2/Cargo.toml 🔗

@@ -25,15 +25,18 @@ async-trait.workspace = true
 base64.workspace = true
 chrono.workspace = true
 collections.workspace = true
+editor.workspace = true
 futures.workspace = true
 gpui.workspace = true
 language.workspace = true
 parking_lot.workspace = true
 project.workspace = true
 smol.workspace = true
+ui.workspace = true
 util.workspace = true
 uuid.workspace = true
 workspace-hack.workspace = true
+zed_actions.workspace = true
 
 [dev-dependencies]
 env_logger.workspace = true

crates/agent2/src/thread_element.rs 🔗

@@ -1,27 +1,124 @@
-use gpui::{App, Entity, SharedString, Window, div, prelude::*};
+use std::sync::Arc;
 
-use crate::Thread;
+use anyhow::Result;
+use editor::{Editor, MultiBuffer};
+use gpui::{App, Entity, Focusable, SharedString, Window, div, prelude::*};
+use gpui::{FocusHandle, Task};
+use language::Buffer;
+use ui::Tooltip;
+use ui::prelude::*;
+use zed_actions::agent::Chat;
+
+use crate::{Message, MessageChunk, Role, Thread};
 
 pub struct ThreadElement {
     thread: Entity<Thread>,
+    // todo! use full message editor from agent2
+    message_editor: Entity<Editor>,
+    send_task: Option<Task<Result<()>>>,
 }
 
 impl ThreadElement {
-    pub fn new(thread: Entity<Thread>) -> Self {
-        Self { thread }
+    pub fn new(thread: Entity<Thread>, window: &mut Window, cx: &mut Context<Self>) -> Self {
+        let message_editor = cx.new(|cx| {
+            let buffer = cx.new(|cx| Buffer::local("", cx));
+            let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
+
+            let mut editor = Editor::new(
+                editor::EditorMode::AutoHeight {
+                    min_lines: 5,
+                    max_lines: None,
+                },
+                buffer,
+                None,
+                window,
+                cx,
+            );
+            editor.set_placeholder_text("Send a message", cx);
+            editor.set_soft_wrap();
+            editor
+        });
+
+        Self {
+            thread,
+            message_editor,
+            send_task: None,
+        }
     }
 
     pub fn title(&self, cx: &App) -> SharedString {
         self.thread.read(cx).title()
     }
 
-    pub fn cancel(&self, window: &mut Window, cx: &mut Context<Self>) {
-        // todo!
+    pub fn cancel(&mut self) {
+        self.send_task.take();
+    }
+
+    fn chat(&mut self, _: &Chat, window: &mut Window, cx: &mut Context<Self>) {
+        let text = self.message_editor.read(cx).text(cx);
+        if text.is_empty() {
+            return;
+        }
+
+        self.send_task = Some(self.thread.update(cx, |thread, cx| {
+            let message = Message {
+                role: Role::User,
+                chunks: vec![MessageChunk::Text { chunk: text.into() }],
+            };
+            thread.send(message, cx)
+        }));
+
+        self.message_editor.update(cx, |editor, cx| {
+            editor.clear(window, cx);
+        });
+    }
+}
+
+impl Focusable for ThreadElement {
+    fn focus_handle(&self, cx: &App) -> FocusHandle {
+        self.message_editor.focus_handle(cx)
     }
 }
 
 impl Render for ThreadElement {
-    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
-        div().child("agent 2")
+    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);
+
+        v_flex()
+            .key_context("MessageEditor")
+            .on_action(cx.listener(Self::chat))
+            .child(div().h_full())
+            .child(
+                div()
+                    .bg(cx.theme().colors().editor_background)
+                    .border_t_1()
+                    .border_color(cx.theme().colors().border)
+                    .p_2()
+                    .child(self.message_editor.clone()),
+            )
+            .child(
+                h_flex().p_2().justify_end().child(
+                    IconButton::new("send-message", IconName::Send)
+                        .icon_color(Color::Accent)
+                        .style(ButtonStyle::Filled)
+                        .disabled(is_editor_empty)
+                        .on_click({
+                            let focus_handle = focus_handle.clone();
+                            move |_event, window, cx| {
+                                focus_handle.dispatch_action(&Chat, window, cx);
+                            }
+                        })
+                        .when(!is_editor_empty, |button| {
+                            button.tooltip(move |window, cx| {
+                                Tooltip::for_action("Send", &Chat, window, cx)
+                            })
+                        })
+                        .when(is_editor_empty, |button| {
+                            button.tooltip(Tooltip::text("Type a message to submit"))
+                        }),
+                ),
+            )
     }
 }

crates/agent_ui/src/agent_panel.rs 🔗

@@ -754,7 +754,7 @@ impl AgentPanel {
                 thread.update(cx, |thread, cx| thread.cancel_last_completion(window, cx));
             }
             ActiveView::Agent2Thread { thread_element, .. } => {
-                thread_element.update(cx, |thread_element, cx| thread_element.cancel(window, cx));
+                thread_element.update(cx, |thread_element, _cx| thread_element.cancel());
             }
             ActiveView::TextThread { .. } | ActiveView::History | ActiveView::Configuration => {}
         }
@@ -919,7 +919,8 @@ impl AgentPanel {
         cx.spawn_in(window, async move |this, cx| {
             let agent = AcpAgent::stdio(child, project, cx);
             let thread = agent.create_thread(cx).await?;
-            let thread_element = cx.new(|_cx| agent2::ThreadElement::new(thread))?;
+            let thread_element =
+                cx.new_window_entity(|window, cx| agent2::ThreadElement::new(thread, window, cx))?;
             this.update_in(cx, |this, window, cx| {
                 this.set_active_view(ActiveView::Agent2Thread { thread_element }, window, cx);
             })
@@ -1521,10 +1522,7 @@ impl Focusable for AgentPanel {
     fn focus_handle(&self, cx: &App) -> FocusHandle {
         match &self.active_view {
             ActiveView::Thread { message_editor, .. } => message_editor.focus_handle(cx),
-            ActiveView::Agent2Thread { .. } => {
-                // todo! add own message editor to agent2
-                cx.focus_handle()
-            }
+            ActiveView::Agent2Thread { thread_element, .. } => thread_element.focus_handle(cx),
             ActiveView::History => self.history.focus_handle(cx),
             ActiveView::TextThread { context_editor, .. } => context_editor.focus_handle(cx),
             ActiveView::Configuration => {

crates/agent_ui/src/agent_ui.rs 🔗

@@ -66,7 +66,6 @@ actions!(
         OpenHistory,
         AddContextServer,
         RemoveSelectedThread,
-        Chat,
         ChatWithFollow,
         CycleNextInlineAssist,
         CyclePreviousInlineAssist,

crates/agent_ui/src/message_editor.rs 🔗

@@ -47,13 +47,14 @@ use ui::{
 };
 use util::ResultExt as _;
 use workspace::{CollaboratorId, Workspace};
+use zed_actions::agent::Chat;
 use zed_llm_client::CompletionIntent;
 
 use crate::context_picker::{ContextPicker, ContextPickerCompletionProvider, crease_for_mention};
 use crate::context_strip::{ContextStrip, ContextStripEvent, SuggestContextKind};
 use crate::profile_selector::ProfileSelector;
 use crate::{
-    ActiveThread, AgentDiffPane, Chat, ChatWithFollow, ExpandMessageEditor, Follow, KeepAll,
+    ActiveThread, AgentDiffPane, ChatWithFollow, ExpandMessageEditor, Follow, KeepAll,
     ModelUsageContext, NewThread, OpenAgentDiff, RejectAll, RemoveAllContext, ToggleBurnMode,
     ToggleContextPicker, ToggleProfileSelector, register_agent_preview,
 };

crates/zed_actions/src/lib.rs 🔗

@@ -198,7 +198,12 @@ pub mod agent {
 
     actions!(
         agent,
-        [OpenConfiguration, OpenOnboardingModal, ResetOnboarding]
+        [
+            OpenConfiguration,
+            OpenOnboardingModal,
+            ResetOnboarding,
+            Chat
+        ]
     );
 }