diff --git a/Cargo.lock b/Cargo.lock index d7840659235de3df350102696365b2ee99958107..352415b1730c5c240d46e02051e7da10eb2724a1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -154,6 +154,7 @@ name = "agent_ui" version = "0.1.0" dependencies = [ "agent", + "agent2", "agent_settings", "anyhow", "assistant_context", diff --git a/crates/agent2/src/acp.rs b/crates/agent2/src/acp.rs index 0269144800c0a7aa696eb35f6d22d74514dea794..04762f09b0fd93a940e1d4c87e542d9decdef127 100644 --- a/crates/agent2/src/acp.rs +++ b/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(), diff --git a/crates/agent2/src/agent2.rs b/crates/agent2/src/agent2.rs index 6d2dfc843b3b2ee8a61f313caa153b6ab51c7a9a..f61f86fab7d1520cc887d37c6273a0860b947dc6 100644 --- a/crates/agent2/src/agent2.rs +++ b/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>; @@ -164,6 +166,7 @@ pub struct Thread { next_entry_id: ThreadEntryId, entries: Vec, agent: Arc, + title: SharedString, project: Entity, } @@ -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 diff --git a/crates/agent_ui/Cargo.toml b/crates/agent_ui/Cargo.toml index 070e8eb585016bdeebb5a2358e9f4d35e5624445..33e5aa8de6be1f0a557bdacf0815c710718b2047 100644 --- a/crates/agent_ui/Cargo.toml +++ b/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 diff --git a/crates/agent_ui/src/agent_panel.rs b/crates/agent_ui/src/agent_panel.rs index eed50f1842ff262050ad50acb34f6ee43b8479cc..37962905d0f9a0d9951461ddc9f5e9e6de8ab3c9 100644 --- a/crates/agent_ui/src/agent_panel.rs +++ b/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::(cx) { + workspace.focus_panel::(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::(cx) { workspace.focus_panel::(window, cx); @@ -183,6 +191,9 @@ enum ActiveView { buffer_search_bar: Entity, _subscriptions: Vec, }, + Agent2Thread { + thread: Entity, + }, 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) { + 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, diff --git a/crates/agent_ui/src/agent_ui.rs b/crates/agent_ui/src/agent_ui.rs index 4babe4f676054740ea88645235ccbcb834d3fc18..3f77b2fa70151626e0782e1cdbe8b33f1c2cb40b 100644 --- a/crates/agent_ui/src/agent_ui.rs +++ b/crates/agent_ui/src/agent_ui.rs @@ -55,6 +55,7 @@ actions!( agent, [ NewTextThread, + NewGeminiThread, ToggleContextPicker, ToggleNavigationMenu, ToggleOptionsMenu,