Start on using agent2 from agent_ui

Max Brunsfeld created

Change summary

Cargo.lock                         |  1 
crates/agent2/src/acp.rs           |  1 
crates/agent2/src/agent2.rs        | 12 ++++
crates/agent_ui/Cargo.toml         |  1 
crates/agent_ui/src/agent_panel.rs | 65 +++++++++++++++++++++++++++++++
crates/agent_ui/src/agent_ui.rs    |  1 
6 files changed, 78 insertions(+), 3 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -154,6 +154,7 @@ name = "agent_ui"
 version = "0.1.0"
 dependencies = [
  "agent",
+ "agent2",
  "agent_settings",
  "anyhow",
  "assistant_context",

crates/agent2/src/acp.rs 🔗

@@ -229,6 +229,7 @@ impl Agent for AcpAgent {
         let thread_id: ThreadId = response.thread_id.into();
         let agent = self.clone();
         let thread = cx.new(|_| Thread {
+            title: "The agent2 thread".into(),
             id: thread_id.clone(),
             next_entry_id: ThreadEntryId(0),
             entries: Vec::default(),

crates/agent2/src/agent2.rs 🔗

@@ -1,12 +1,14 @@
 mod acp;
 
-use anyhow::{Result, anyhow};
+use anyhow::Result;
 use async_trait::async_trait;
 use chrono::{DateTime, Utc};
 use gpui::{AppContext, AsyncApp, Context, Entity, SharedString, Task};
 use project::Project;
 use std::{ops::Range, path::PathBuf, sync::Arc};
 
+pub use acp::AcpAgent;
+
 #[async_trait(?Send)]
 pub trait Agent: 'static {
     async fn threads(&self, cx: &mut AsyncApp) -> Result<Vec<AgentThreadSummary>>;
@@ -164,6 +166,7 @@ pub struct Thread {
     next_entry_id: ThreadEntryId,
     entries: Vec<ThreadEntry>,
     agent: Arc<dyn Agent>,
+    title: SharedString,
     project: Entity<Project>,
 }
 
@@ -187,6 +190,7 @@ impl Thread {
     ) -> Self {
         let mut next_entry_id = ThreadEntryId(0);
         Self {
+            title: "A new agent2 thread".into(),
             entries: entries
                 .into_iter()
                 .map(|entry| ThreadEntry {
@@ -201,6 +205,10 @@ impl Thread {
         }
     }
 
+    pub fn title(&self) -> SharedString {
+        self.title.clone()
+    }
+
     pub fn entries(&self) -> &[ThreadEntry] {
         &self.entries
     }
@@ -258,7 +266,7 @@ mod tests {
         .await;
         let project = Project::test(fs, [path!("/private/tmp").as_ref()], cx).await;
         let agent = gemini_agent(project.clone(), cx.to_async()).unwrap();
-        let thread_store = ThreadStore::load(Arc::new(agent), project, &mut cx.to_async())
+        let thread_store = ThreadStore::load(agent, project, &mut cx.to_async())
             .await
             .unwrap();
         let thread = thread_store

crates/agent_ui/Cargo.toml 🔗

@@ -20,6 +20,7 @@ test-support = [
 
 [dependencies]
 agent.workspace = true
+agent2.workspace = true
 agent_settings.workspace = true
 anyhow.workspace = true
 assistant_context.workspace = true

crates/agent_ui/src/agent_panel.rs 🔗

@@ -4,9 +4,11 @@ use std::rc::Rc;
 use std::sync::Arc;
 use std::time::Duration;
 
+use agent2::{AcpAgent, Agent as _};
 use db::kvp::{Dismissable, KEY_VALUE_STORE};
 use serde::{Deserialize, Serialize};
 
+use crate::NewGeminiThread;
 use crate::language_model_selector::ToggleModelSelector;
 use crate::{
     AddContextServer, AgentDiffPane, ContinueThread, ContinueWithBurnMode,
@@ -109,6 +111,12 @@ pub fn init(cx: &mut App) {
                         panel.update(cx, |panel, cx| panel.new_prompt_editor(window, cx));
                     }
                 })
+                .register_action(|workspace, _: &NewGeminiThread, window, cx| {
+                    if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
+                        workspace.focus_panel::<AgentPanel>(window, cx);
+                        panel.update(cx, |panel, cx| panel.new_gemini_thread(window, cx));
+                    }
+                })
                 .register_action(|workspace, action: &OpenRulesLibrary, window, cx| {
                     if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
                         workspace.focus_panel::<AgentPanel>(window, cx);
@@ -183,6 +191,9 @@ enum ActiveView {
         buffer_search_bar: Entity<BufferSearchBar>,
         _subscriptions: Vec<gpui::Subscription>,
     },
+    Agent2Thread {
+        thread: Entity<agent2::Thread>,
+    },
     History,
     Configuration,
 }
@@ -196,7 +207,9 @@ enum WhichFontSize {
 impl ActiveView {
     pub fn which_font_size_used(&self) -> WhichFontSize {
         match self {
-            ActiveView::Thread { .. } | ActiveView::History => WhichFontSize::AgentFont,
+            ActiveView::Thread { .. } | ActiveView::Agent2Thread { .. } | ActiveView::History => {
+                WhichFontSize::AgentFont
+            }
             ActiveView::TextThread { .. } => WhichFontSize::BufferFont,
             ActiveView::Configuration => WhichFontSize::None,
         }
@@ -867,6 +880,42 @@ impl AgentPanel {
         context_editor.focus_handle(cx).focus(window);
     }
 
+    fn new_gemini_thread(&mut self, window: &mut Window, cx: &mut Context<Self>) {
+        let Some(root_dir) = self
+            .project
+            .read(cx)
+            .visible_worktrees(cx)
+            .next()
+            .map(|worktree| worktree.read(cx).abs_path())
+        else {
+            return;
+        };
+
+        let cli_path =
+            Path::new(env!("CARGO_MANIFEST_DIR")).join("../../../gemini-cli/packages/cli");
+        let child = util::command::new_smol_command("node")
+            .arg(cli_path)
+            .arg("--acp")
+            .args(["--model", "gemini-2.5-flash"])
+            .current_dir(root_dir)
+            .stdin(std::process::Stdio::piped())
+            .stdout(std::process::Stdio::piped())
+            .stderr(std::process::Stdio::inherit())
+            .kill_on_drop(true)
+            .spawn()
+            .unwrap();
+
+        let project = self.project.clone();
+        cx.spawn_in(window, async move |this, cx| {
+            let agent = AcpAgent::stdio(child, project, cx);
+            let thread = agent.create_thread(cx).await?;
+            this.update_in(cx, |this, window, cx| {
+                this.set_active_view(ActiveView::Agent2Thread { thread }, window, cx);
+            })
+        })
+        .detach();
+    }
+
     fn deploy_rules_library(
         &mut self,
         action: &OpenRulesLibrary,
@@ -1465,6 +1514,7 @@ impl Focusable for AgentPanel {
     fn focus_handle(&self, cx: &App) -> FocusHandle {
         match &self.active_view {
             ActiveView::Thread { .. } => self.message_editor.focus_handle(cx),
+            ActiveView::Agent2Thread { .. } => self.message_editor.focus_handle(cx),
             ActiveView::History => self.history.focus_handle(cx),
             ActiveView::TextThread { context_editor, .. } => context_editor.focus_handle(cx),
             ActiveView::Configuration => {
@@ -1618,6 +1668,9 @@ impl AgentPanel {
                         .into_any_element(),
                 }
             }
+            ActiveView::Agent2Thread { thread } => Label::new(thread.read(cx).title())
+                .truncate()
+                .into_any_element(),
             ActiveView::TextThread {
                 title_editor,
                 context_editor,
@@ -1785,6 +1838,7 @@ impl AgentPanel {
                     menu = menu
                         .action("New Thread", NewThread::default().boxed_clone())
                         .action("New Text Thread", NewTextThread.boxed_clone())
+                        .action("New Gemini Thread", NewGeminiThread.boxed_clone())
                         .when(!is_empty, |menu| {
                             menu.action(
                                 "New From Summary",
@@ -3023,6 +3077,9 @@ impl AgentPanel {
                     .detach();
                 });
             }
+            ActiveView::Agent2Thread { .. } => {
+                unimplemented!()
+            }
             ActiveView::TextThread { context_editor, .. } => {
                 context_editor.update(cx, |context_editor, cx| {
                     TextThreadEditor::insert_dragged_files(
@@ -3115,6 +3172,12 @@ impl Render for AgentPanel {
                     .child(h_flex().child(self.message_editor.clone()))
                     .children(self.render_last_error(cx))
                     .child(self.render_drag_target(cx)),
+                ActiveView::Agent2Thread { .. } => parent
+                    .relative()
+                    .child(self.render_active_thread_or_empty_state(window, cx))
+                    .child(h_flex().child(self.message_editor.clone()))
+                    .children(self.render_last_error(cx))
+                    .child(self.render_drag_target(cx)),
                 ActiveView::History => parent.child(self.history.clone()),
                 ActiveView::TextThread {
                     context_editor,