From 28ac84ed019d9f22f8387abfe76d0ca2f0673ac7 Mon Sep 17 00:00:00 2001 From: Agus Zubiaga Date: Tue, 1 Jul 2025 17:15:20 -0300 Subject: [PATCH] Jump to gemini thread view immediately Co-authored-by: Conrad Irwin --- crates/acp/src/thread_view.rs | 128 +++++++++++++++++++++++------ crates/agent_ui/src/agent_panel.rs | 30 +------ 2 files changed, 106 insertions(+), 52 deletions(-) diff --git a/crates/acp/src/thread_view.rs b/crates/acp/src/thread_view.rs index 314bcecdbb45bd3a5a1f6c87060b0f8a7d6dadc2..79cdfdd6ddbdb90e187612249131345a2e1e88e8 100644 --- a/crates/acp/src/thread_view.rs +++ b/crates/acp/src/thread_view.rs @@ -1,3 +1,4 @@ +use std::path::Path; use std::rc::Rc; use anyhow::Result; @@ -9,25 +10,63 @@ use gpui::{ use gpui::{FocusHandle, Task}; use language::Buffer; use markdown::{HeadingLevelStyles, MarkdownElement, MarkdownStyle}; +use project::Project; use settings::Settings as _; use theme::ThemeSettings; use ui::Tooltip; use ui::prelude::*; +use util::ResultExt; use zed_actions::agent::Chat; -use crate::{AcpThread, AcpThreadEvent, AgentThreadEntryContent, MessageChunk, Role, ThreadEntry}; +use crate::{ + AcpServer, AcpThread, AcpThreadEvent, AgentThreadEntryContent, MessageChunk, Role, ThreadEntry, +}; pub struct AcpThreadView { - thread: Entity, + thread_state: ThreadState, // todo! use full message editor from agent2 message_editor: Entity, list_state: ListState, send_task: Option>>, - _subscription: Subscription, +} + +enum ThreadState { + Loading { + _task: Task<()>, + }, + Ready { + thread: Entity, + _subscription: Subscription, + }, + LoadError(SharedString), } impl AcpThreadView { - pub fn new(thread: Entity, window: &mut Window, cx: &mut Context) -> Self { + pub fn new(project: Entity, window: &mut Window, cx: &mut Context) -> Self { + let Some(root_dir) = project + .read(cx) + .visible_worktrees(cx) + .next() + .map(|worktree| worktree.read(cx).abs_path()) + else { + todo!(); + }; + + 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 message_editor = cx.new(|cx| { let buffer = cx.new(|cx| Buffer::local("", cx)); let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx)); @@ -47,43 +86,79 @@ impl AcpThreadView { editor }); - let subscription = cx.subscribe(&thread, |this, _, event, cx| { - let count = this.list_state.item_count(); - match event { - AcpThreadEvent::NewEntry => { - this.list_state.splice(count..count, 1); - } - AcpThreadEvent::LastEntryUpdated => { - this.list_state.splice(count - 1..count, 1); - } - } - cx.notify(); + let project = project.clone(); + let load_task = cx.spawn_in(window, async move |this, cx| { + let agent = AcpServer::stdio(child, project, cx); + let result = agent.create_thread(cx).await; + + this.update(cx, |this, cx| { + match result { + Ok(thread) => { + let subscription = cx.subscribe(&thread, |this, _, event, cx| { + let count = this.list_state.item_count(); + match event { + AcpThreadEvent::NewEntry => { + this.list_state.splice(count..count, 1); + } + AcpThreadEvent::LastEntryUpdated => { + this.list_state.splice(count - 1..count, 1); + } + } + cx.notify(); + }); + this.list_state + .splice(0..0, thread.read(cx).entries().len()); + + this.thread_state = ThreadState::Ready { + thread, + _subscription: subscription, + }; + } + Err(e) => this.thread_state = ThreadState::LoadError(e.to_string().into()), + }; + cx.notify(); + }) + .log_err(); }); let list_state = ListState::new( - thread.read(cx).entries.len(), + 0, gpui::ListAlignment::Top, px(1000.0), cx.processor({ move |this: &mut Self, item: usize, window, cx| { - let Some(entry) = this.thread.read(cx).entries.get(item) else { + let Some(entry) = this + .thread() + .and_then(|thread| thread.read(cx).entries.get(item)) + else { return Empty.into_any(); }; this.render_entry(entry, window, cx) } }), ); + Self { - thread, + thread_state: ThreadState::Loading { _task: load_task }, message_editor, send_task: None, list_state: list_state, - _subscription: subscription, + } + } + + fn thread(&self) -> Option<&Entity> { + match &self.thread_state { + ThreadState::Ready { thread, .. } => Some(thread), + _ => None, } } pub fn title(&self, cx: &App) -> SharedString { - self.thread.read(cx).title() + match &self.thread_state { + ThreadState::Ready { thread, .. } => thread.read(cx).title(), + ThreadState::Loading { .. } => "Loading...".into(), + ThreadState::LoadError(_) => "Failed to load".into(), + } } pub fn cancel(&mut self) { @@ -95,8 +170,9 @@ impl AcpThreadView { if text.is_empty() { return; } + let Some(thread) = self.thread() else { return }; - let task = self.thread.update(cx, |thread, cx| thread.send(&text, cx)); + let task = thread.update(cx, |thread, cx| thread.send(&text, cx)); self.send_task = Some(cx.spawn(async move |this, cx| { task.await?; @@ -179,14 +255,18 @@ impl Render for AcpThreadView { v_flex() .key_context("MessageEditor") .on_action(cx.listener(Self::chat)) - .child( - div() + .child(match &self.thread_state { + ThreadState::Loading { .. } => div().p_2().child(Label::new("Loading...")), + ThreadState::LoadError(e) => div() + .p_2() + .child(Label::new(format!("Failed to load {e}")).into_any_element()), + ThreadState::Ready { .. } => div() .child( list(self.list_state.clone()) .with_sizing_behavior(gpui::ListSizingBehavior::Infer), ) .p_2(), - ) + }) .when(self.send_task.is_some(), |this| { this.child( div().p_2().child( diff --git a/crates/agent_ui/src/agent_panel.rs b/crates/agent_ui/src/agent_panel.rs index 149fcb22595f8a270419a3e4bedf3bc76bfe14a1..ce38ff023b80cc7e28e25bde2f4c3c589b7739d2 100644 --- a/crates/agent_ui/src/agent_panel.rs +++ b/crates/agent_ui/src/agent_panel.rs @@ -4,7 +4,6 @@ use std::rc::Rc; use std::sync::Arc; use std::time::Duration; -use acp::AcpServer; use db::kvp::{Dismissable, KEY_VALUE_STORE}; use serde::{Deserialize, Serialize}; @@ -890,36 +889,11 @@ impl AgentPanel { } 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 { - todo!(); - }; - - 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 = AcpServer::stdio(child, project, cx); - let thread = agent.create_thread(cx).await?; let thread_view = - cx.new_window_entity(|window, cx| acp::AcpThreadView::new(thread, window, cx))?; + cx.new_window_entity(|window, cx| acp::AcpThreadView::new(project, window, cx))?; this.update_in(cx, |this, window, cx| { this.set_active_view(ActiveView::AcpThread { thread_view }, window, cx); })