acp.rs

  1mod server;
  2mod thread_view;
  3
  4use agentic_coding_protocol::{self as acp, Role};
  5use anyhow::{Context as _, Result};
  6use buffer_diff::BufferDiff;
  7use chrono::{DateTime, Utc};
  8use editor::MultiBuffer;
  9use futures::channel::oneshot;
 10use gpui::{AppContext, Context, Entity, EventEmitter, SharedString, Task};
 11use language::{Buffer, LanguageRegistry};
 12use markdown::Markdown;
 13use project::Project;
 14use std::{mem, ops::Range, path::PathBuf, sync::Arc};
 15use ui::{App, IconName};
 16use util::{ResultExt, debug_panic};
 17
 18pub use server::AcpServer;
 19pub use thread_view::AcpThreadView;
 20
 21#[derive(Debug, Clone, PartialEq, Eq, Hash)]
 22pub struct ThreadId(SharedString);
 23
 24#[derive(Copy, Clone, Debug, PartialEq, Eq)]
 25pub struct FileVersion(u64);
 26
 27#[derive(Debug)]
 28pub struct AgentThreadSummary {
 29    pub id: ThreadId,
 30    pub title: String,
 31    pub created_at: DateTime<Utc>,
 32}
 33
 34#[derive(Clone, Debug, PartialEq, Eq)]
 35pub struct FileContent {
 36    pub path: PathBuf,
 37    pub version: FileVersion,
 38    pub content: SharedString,
 39}
 40
 41#[derive(Clone, Debug, Eq, PartialEq)]
 42pub struct Message {
 43    pub role: acp::Role,
 44    pub chunks: Vec<MessageChunk>,
 45}
 46
 47impl Message {
 48    fn into_acp(self, cx: &App) -> acp::Message {
 49        acp::Message {
 50            role: self.role,
 51            chunks: self
 52                .chunks
 53                .into_iter()
 54                .map(|chunk| chunk.into_acp(cx))
 55                .collect(),
 56        }
 57    }
 58}
 59
 60#[derive(Clone, Debug, Eq, PartialEq)]
 61pub enum MessageChunk {
 62    Text {
 63        chunk: Entity<Markdown>,
 64    },
 65    File {
 66        content: FileContent,
 67    },
 68    Directory {
 69        path: PathBuf,
 70        contents: Vec<FileContent>,
 71    },
 72    Symbol {
 73        path: PathBuf,
 74        range: Range<u64>,
 75        version: FileVersion,
 76        name: SharedString,
 77        content: SharedString,
 78    },
 79    Fetch {
 80        url: SharedString,
 81        content: SharedString,
 82    },
 83}
 84
 85impl MessageChunk {
 86    pub fn from_acp(
 87        chunk: acp::MessageChunk,
 88        language_registry: Arc<LanguageRegistry>,
 89        cx: &mut App,
 90    ) -> Self {
 91        match chunk {
 92            acp::MessageChunk::Text { chunk } => MessageChunk::Text {
 93                chunk: cx.new(|cx| Markdown::new(chunk.into(), Some(language_registry), None, cx)),
 94            },
 95        }
 96    }
 97
 98    pub fn into_acp(self, cx: &App) -> acp::MessageChunk {
 99        match self {
100            MessageChunk::Text { chunk } => acp::MessageChunk::Text {
101                chunk: chunk.read(cx).source().to_string(),
102            },
103            MessageChunk::File { .. } => todo!(),
104            MessageChunk::Directory { .. } => todo!(),
105            MessageChunk::Symbol { .. } => todo!(),
106            MessageChunk::Fetch { .. } => todo!(),
107        }
108    }
109
110    pub fn from_str(chunk: &str, language_registry: Arc<LanguageRegistry>, cx: &mut App) -> Self {
111        MessageChunk::Text {
112            chunk: cx.new(|cx| {
113                Markdown::new(chunk.to_owned().into(), Some(language_registry), None, cx)
114            }),
115        }
116    }
117}
118
119#[derive(Debug)]
120pub enum AgentThreadEntryContent {
121    Message(Message),
122    ToolCall(ToolCall),
123}
124
125#[derive(Debug)]
126pub struct ToolCall {
127    id: ToolCallId,
128    label: Entity<Markdown>,
129    icon: IconName,
130    status: ToolCallStatus,
131}
132
133#[derive(Debug)]
134pub enum ToolCallStatus {
135    WaitingForConfirmation {
136        confirmation: ToolCallConfirmation,
137        respond_tx: oneshot::Sender<acp::ToolCallConfirmationOutcome>,
138    },
139    Allowed {
140        status: acp::ToolCallStatus,
141        content: Option<ToolCallContent>,
142    },
143    Rejected,
144}
145
146#[derive(Debug)]
147pub enum ToolCallConfirmation {
148    Edit {
149        diff: Diff,
150        description: Option<Entity<Markdown>>,
151    },
152    Execute {
153        command: String,
154        root_command: String,
155        description: Option<Entity<Markdown>>,
156    },
157    Mcp {
158        server_name: String,
159        tool_name: String,
160        tool_display_name: String,
161        description: Option<Entity<Markdown>>,
162    },
163    Fetch {
164        urls: Vec<String>,
165        description: Option<Entity<Markdown>>,
166    },
167    Other {
168        description: Entity<Markdown>,
169    },
170}
171
172impl ToolCallConfirmation {
173    pub fn from_acp(
174        confirmation: acp::ToolCallConfirmation,
175        language_registry: Arc<LanguageRegistry>,
176        cx: &mut App,
177    ) -> Self {
178        let to_md = |description: String, cx: &mut App| -> Entity<Markdown> {
179            cx.new(|cx| {
180                Markdown::new(
181                    description.into(),
182                    Some(language_registry.clone()),
183                    None,
184                    cx,
185                )
186            })
187        };
188
189        match confirmation {
190            acp::ToolCallConfirmation::Edit { diff, description } => Self::Edit {
191                diff: Diff::from_acp(diff, language_registry.clone(), cx),
192                description: description.map(|description| to_md(description, cx)),
193            },
194            acp::ToolCallConfirmation::Execute {
195                command,
196                root_command,
197                description,
198            } => Self::Execute {
199                command,
200                root_command,
201                description: description.map(|description| to_md(description, cx)),
202            },
203            acp::ToolCallConfirmation::Mcp {
204                server_name,
205                tool_name,
206                tool_display_name,
207                description,
208            } => Self::Mcp {
209                server_name,
210                tool_name,
211                tool_display_name,
212                description: description.map(|description| to_md(description, cx)),
213            },
214            acp::ToolCallConfirmation::Fetch { urls, description } => Self::Fetch {
215                urls,
216                description: description.map(|description| to_md(description, cx)),
217            },
218            acp::ToolCallConfirmation::Other { description } => Self::Other {
219                description: to_md(description, cx),
220            },
221        }
222    }
223}
224
225#[derive(Debug)]
226pub enum ToolCallContent {
227    Markdown { markdown: Entity<Markdown> },
228    Diff { diff: Diff },
229}
230
231#[derive(Debug)]
232pub struct Diff {
233    // todo! show path somewhere
234    buffer: Entity<MultiBuffer>,
235    _path: PathBuf,
236    _task: Task<Result<()>>,
237}
238
239impl Diff {
240    pub fn from_acp(
241        diff: acp::Diff,
242        language_registry: Arc<LanguageRegistry>,
243        cx: &mut App,
244    ) -> Self {
245        let acp::Diff {
246            path,
247            old_text,
248            new_text,
249        } = diff;
250
251        let buffer = cx.new(|cx| Buffer::local(new_text, cx));
252        let text_snapshot = buffer.read(cx).text_snapshot();
253        let buffer_diff = cx.new(|cx| BufferDiff::new(&text_snapshot, cx));
254
255        let multibuffer = cx.new(|cx| {
256            let mut multibuffer = MultiBuffer::singleton(buffer.clone(), cx);
257            multibuffer.add_diff(buffer_diff.clone(), cx);
258            multibuffer
259        });
260
261        Self {
262            buffer: multibuffer,
263            _path: path.clone(),
264            _task: cx.spawn(async move |cx| {
265                let diff_snapshot = BufferDiff::update_diff(
266                    buffer_diff.clone(),
267                    text_snapshot.clone(),
268                    old_text.map(|o| o.into()),
269                    true,
270                    true,
271                    None,
272                    Some(language_registry.clone()),
273                    cx,
274                )
275                .await?;
276
277                buffer_diff.update(cx, |diff, cx| {
278                    diff.set_snapshot(diff_snapshot, &text_snapshot, cx)
279                })?;
280
281                if let Some(language) = language_registry
282                    .language_for_file_path(&path)
283                    .await
284                    .log_err()
285                {
286                    buffer.update(cx, |buffer, cx| buffer.set_language(Some(language), cx))?;
287                }
288
289                anyhow::Ok(())
290            }),
291        }
292    }
293}
294
295/// A `ThreadEntryId` that is known to be a ToolCall
296#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
297pub struct ToolCallId(ThreadEntryId);
298
299impl ToolCallId {
300    pub fn as_u64(&self) -> u64 {
301        self.0.0
302    }
303}
304
305#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
306pub struct ThreadEntryId(pub u64);
307
308impl ThreadEntryId {
309    pub fn post_inc(&mut self) -> Self {
310        let id = *self;
311        self.0 += 1;
312        id
313    }
314}
315
316#[derive(Debug)]
317pub struct ThreadEntry {
318    pub id: ThreadEntryId,
319    pub content: AgentThreadEntryContent,
320}
321
322pub struct AcpThread {
323    id: ThreadId,
324    next_entry_id: ThreadEntryId,
325    entries: Vec<ThreadEntry>,
326    server: Arc<AcpServer>,
327    title: SharedString,
328    project: Entity<Project>,
329}
330
331enum AcpThreadEvent {
332    NewEntry,
333    EntryUpdated(usize),
334}
335
336impl EventEmitter<AcpThreadEvent> for AcpThread {}
337
338impl AcpThread {
339    pub fn new(
340        server: Arc<AcpServer>,
341        thread_id: ThreadId,
342        entries: Vec<AgentThreadEntryContent>,
343        project: Entity<Project>,
344        _: &mut Context<Self>,
345    ) -> Self {
346        let mut next_entry_id = ThreadEntryId(0);
347        Self {
348            title: "A new agent2 thread".into(),
349            entries: entries
350                .into_iter()
351                .map(|entry| ThreadEntry {
352                    id: next_entry_id.post_inc(),
353                    content: entry,
354                })
355                .collect(),
356            server,
357            id: thread_id,
358            next_entry_id,
359            project,
360        }
361    }
362
363    pub fn title(&self) -> SharedString {
364        self.title.clone()
365    }
366
367    pub fn entries(&self) -> &[ThreadEntry] {
368        &self.entries
369    }
370
371    pub fn push_entry(
372        &mut self,
373        entry: AgentThreadEntryContent,
374        cx: &mut Context<Self>,
375    ) -> ThreadEntryId {
376        let id = self.next_entry_id.post_inc();
377        self.entries.push(ThreadEntry { id, content: entry });
378        cx.emit(AcpThreadEvent::NewEntry);
379        id
380    }
381
382    pub fn push_assistant_chunk(&mut self, chunk: acp::MessageChunk, cx: &mut Context<Self>) {
383        let entries_len = self.entries.len();
384        if let Some(last_entry) = self.entries.last_mut()
385            && let AgentThreadEntryContent::Message(Message {
386                ref mut chunks,
387                role: Role::Assistant,
388            }) = last_entry.content
389        {
390            cx.emit(AcpThreadEvent::EntryUpdated(entries_len - 1));
391
392            if let (
393                Some(MessageChunk::Text { chunk: old_chunk }),
394                acp::MessageChunk::Text { chunk: new_chunk },
395            ) = (chunks.last_mut(), &chunk)
396            {
397                old_chunk.update(cx, |old_chunk, cx| {
398                    old_chunk.append(&new_chunk, cx);
399                });
400            } else {
401                chunks.push(MessageChunk::from_acp(
402                    chunk,
403                    self.project.read(cx).languages().clone(),
404                    cx,
405                ));
406            }
407
408            return;
409        }
410
411        let chunk = MessageChunk::from_acp(chunk, self.project.read(cx).languages().clone(), cx);
412
413        self.push_entry(
414            AgentThreadEntryContent::Message(Message {
415                role: Role::Assistant,
416                chunks: vec![chunk],
417            }),
418            cx,
419        );
420    }
421
422    pub fn request_tool_call(
423        &mut self,
424        label: String,
425        icon: acp::Icon,
426        confirmation: acp::ToolCallConfirmation,
427        cx: &mut Context<Self>,
428    ) -> ToolCallRequest {
429        let (tx, rx) = oneshot::channel();
430
431        let status = ToolCallStatus::WaitingForConfirmation {
432            confirmation: ToolCallConfirmation::from_acp(
433                confirmation,
434                self.project.read(cx).languages().clone(),
435                cx,
436            ),
437            respond_tx: tx,
438        };
439
440        let id = self.insert_tool_call(label, status, icon, cx);
441        ToolCallRequest { id, outcome: rx }
442    }
443
444    pub fn push_tool_call(
445        &mut self,
446        label: String,
447        icon: acp::Icon,
448        cx: &mut Context<Self>,
449    ) -> ToolCallId {
450        let status = ToolCallStatus::Allowed {
451            status: acp::ToolCallStatus::Running,
452            content: None,
453        };
454
455        self.insert_tool_call(label, status, icon, cx)
456    }
457
458    fn insert_tool_call(
459        &mut self,
460        label: String,
461        status: ToolCallStatus,
462        icon: acp::Icon,
463        cx: &mut Context<Self>,
464    ) -> ToolCallId {
465        let language_registry = self.project.read(cx).languages().clone();
466
467        let entry_id = self.push_entry(
468            AgentThreadEntryContent::ToolCall(ToolCall {
469                // todo! clean up id creation
470                id: ToolCallId(ThreadEntryId(self.entries.len() as u64)),
471                label: cx.new(|cx| {
472                    Markdown::new(label.into(), Some(language_registry.clone()), None, cx)
473                }),
474                icon: acp_icon_to_ui_icon(icon),
475                status,
476            }),
477            cx,
478        );
479
480        ToolCallId(entry_id)
481    }
482
483    pub fn authorize_tool_call(
484        &mut self,
485        id: ToolCallId,
486        outcome: acp::ToolCallConfirmationOutcome,
487        cx: &mut Context<Self>,
488    ) {
489        let Some(entry) = self.entry_mut(id.0) else {
490            return;
491        };
492
493        let AgentThreadEntryContent::ToolCall(call) = &mut entry.content else {
494            debug_panic!("expected ToolCall");
495            return;
496        };
497
498        let new_status = if outcome == acp::ToolCallConfirmationOutcome::Reject {
499            ToolCallStatus::Rejected
500        } else {
501            ToolCallStatus::Allowed {
502                status: acp::ToolCallStatus::Running,
503                content: None,
504            }
505        };
506
507        let curr_status = mem::replace(&mut call.status, new_status);
508
509        if let ToolCallStatus::WaitingForConfirmation { respond_tx, .. } = curr_status {
510            respond_tx.send(outcome).log_err();
511        } else {
512            debug_panic!("tried to authorize an already authorized tool call");
513        }
514
515        cx.emit(AcpThreadEvent::EntryUpdated(id.as_u64() as usize));
516    }
517
518    pub fn update_tool_call(
519        &mut self,
520        id: ToolCallId,
521        new_status: acp::ToolCallStatus,
522        new_content: Option<acp::ToolCallContent>,
523        cx: &mut Context<Self>,
524    ) -> Result<()> {
525        let language_registry = self.project.read(cx).languages().clone();
526        let entry = self.entry_mut(id.0).context("Entry not found")?;
527
528        match &mut entry.content {
529            AgentThreadEntryContent::ToolCall(call) => match &mut call.status {
530                ToolCallStatus::Allowed { content, status } => {
531                    *content = new_content.map(|new_content| match new_content {
532                        acp::ToolCallContent::Markdown { markdown } => ToolCallContent::Markdown {
533                            markdown: cx.new(|cx| {
534                                Markdown::new(
535                                    markdown.into(),
536                                    Some(language_registry.clone()),
537                                    None,
538                                    cx,
539                                )
540                            }),
541                        },
542                        acp::ToolCallContent::Diff { diff } => ToolCallContent::Diff {
543                            diff: Diff::from_acp(diff, language_registry, cx),
544                        },
545                    });
546                    *status = new_status;
547                }
548                ToolCallStatus::WaitingForConfirmation { .. } => {
549                    anyhow::bail!("Tool call hasn't been authorized yet")
550                }
551                ToolCallStatus::Rejected => {
552                    anyhow::bail!("Tool call was rejected and therefore can't be updated")
553                }
554            },
555            _ => anyhow::bail!("Entry is not a tool call"),
556        }
557
558        cx.emit(AcpThreadEvent::EntryUpdated(id.as_u64() as usize));
559        Ok(())
560    }
561
562    fn entry_mut(&mut self, id: ThreadEntryId) -> Option<&mut ThreadEntry> {
563        let entry = self.entries.get_mut(id.0 as usize);
564        debug_assert!(
565            entry.is_some(),
566            "We shouldn't give out ids to entries that don't exist"
567        );
568        entry
569    }
570
571    /// Returns true if the last turn is awaiting tool authorization
572    pub fn waiting_for_tool_confirmation(&self) -> bool {
573        for entry in self.entries.iter().rev() {
574            match &entry.content {
575                AgentThreadEntryContent::ToolCall(call) => match call.status {
576                    ToolCallStatus::WaitingForConfirmation { .. } => return true,
577                    ToolCallStatus::Allowed { .. } | ToolCallStatus::Rejected => continue,
578                },
579                AgentThreadEntryContent::Message(_) => {
580                    // Reached the beginning of the turn
581                    return false;
582                }
583            }
584        }
585        false
586    }
587
588    pub fn send(&mut self, message: &str, cx: &mut Context<Self>) -> Task<Result<()>> {
589        let agent = self.server.clone();
590        let id = self.id.clone();
591        let chunk = MessageChunk::from_str(message, self.project.read(cx).languages().clone(), cx);
592        let message = Message {
593            role: Role::User,
594            chunks: vec![chunk],
595        };
596        self.push_entry(AgentThreadEntryContent::Message(message.clone()), cx);
597        let acp_message = message.into_acp(cx);
598        cx.spawn(async move |_, cx| {
599            agent.send_message(id, acp_message, cx).await?;
600            Ok(())
601        })
602    }
603}
604
605fn acp_icon_to_ui_icon(icon: acp::Icon) -> IconName {
606    match icon {
607        acp::Icon::FileSearch => IconName::FileSearch,
608        acp::Icon::Folder => IconName::Folder,
609        acp::Icon::Globe => IconName::Globe,
610        acp::Icon::Hammer => IconName::Hammer,
611        acp::Icon::LightBulb => IconName::LightBulb,
612        acp::Icon::Pencil => IconName::Pencil,
613        acp::Icon::Regex => IconName::Regex,
614        acp::Icon::Terminal => IconName::Terminal,
615    }
616}
617
618pub struct ToolCallRequest {
619    pub id: ToolCallId,
620    pub outcome: oneshot::Receiver<acp::ToolCallConfirmationOutcome>,
621}
622
623#[cfg(test)]
624mod tests {
625    use super::*;
626    use futures::{FutureExt as _, channel::mpsc, select};
627    use gpui::{AsyncApp, TestAppContext};
628    use project::FakeFs;
629    use serde_json::json;
630    use settings::SettingsStore;
631    use smol::stream::StreamExt as _;
632    use std::{env, path::Path, process::Stdio, time::Duration};
633    use util::path;
634
635    fn init_test(cx: &mut TestAppContext) {
636        env_logger::try_init().ok();
637        cx.update(|cx| {
638            let settings_store = SettingsStore::test(cx);
639            cx.set_global(settings_store);
640            Project::init_settings(cx);
641            language::init(cx);
642        });
643    }
644
645    #[gpui::test]
646    async fn test_gemini_basic(cx: &mut TestAppContext) {
647        init_test(cx);
648
649        cx.executor().allow_parking();
650
651        let fs = FakeFs::new(cx.executor());
652        let project = Project::test(fs, [], cx).await;
653        let server = gemini_acp_server(project.clone(), cx.to_async()).unwrap();
654        let thread = server.create_thread(&mut cx.to_async()).await.unwrap();
655        thread
656            .update(cx, |thread, cx| thread.send("Hello from Zed!", cx))
657            .await
658            .unwrap();
659
660        thread.read_with(cx, |thread, _| {
661            assert_eq!(thread.entries.len(), 2);
662            assert!(matches!(
663                thread.entries[0].content,
664                AgentThreadEntryContent::Message(Message {
665                    role: Role::User,
666                    ..
667                })
668            ));
669            assert!(matches!(
670                thread.entries[1].content,
671                AgentThreadEntryContent::Message(Message {
672                    role: Role::Assistant,
673                    ..
674                })
675            ));
676        });
677    }
678
679    #[gpui::test]
680    async fn test_gemini_tool_call(cx: &mut TestAppContext) {
681        init_test(cx);
682
683        cx.executor().allow_parking();
684
685        let fs = FakeFs::new(cx.executor());
686        fs.insert_tree(
687            path!("/private/tmp"),
688            json!({"foo": "Lorem ipsum dolor", "bar": "bar", "baz": "baz"}),
689        )
690        .await;
691        let project = Project::test(fs, [path!("/private/tmp").as_ref()], cx).await;
692        let server = gemini_acp_server(project.clone(), cx.to_async()).unwrap();
693        let thread = server.create_thread(&mut cx.to_async()).await.unwrap();
694        thread
695            .update(cx, |thread, cx| {
696                thread.send(
697                    "Read the '/private/tmp/foo' file and tell me what you see.",
698                    cx,
699                )
700            })
701            .await
702            .unwrap();
703        thread.read_with(cx, |thread, _cx| {
704            assert!(matches!(
705                &thread.entries()[1].content,
706                AgentThreadEntryContent::ToolCall(ToolCall {
707                    status: ToolCallStatus::Allowed { .. },
708                    ..
709                })
710            ));
711
712            assert!(matches!(
713                thread.entries[2].content,
714                AgentThreadEntryContent::Message(Message {
715                    role: Role::Assistant,
716                    ..
717                })
718            ));
719        });
720    }
721
722    #[gpui::test]
723    async fn test_gemini_tool_call_with_confirmation(cx: &mut TestAppContext) {
724        init_test(cx);
725
726        cx.executor().allow_parking();
727
728        let fs = FakeFs::new(cx.executor());
729        let project = Project::test(fs, [path!("/private/tmp").as_ref()], cx).await;
730        let server = gemini_acp_server(project.clone(), cx.to_async()).unwrap();
731        let thread = server.create_thread(&mut cx.to_async()).await.unwrap();
732        let full_turn = thread.update(cx, |thread, cx| {
733            thread.send(r#"Run `echo "Hello, world!"`"#, cx)
734        });
735
736        run_until_tool_call(&thread, cx).await;
737
738        let tool_call_id = thread.read_with(cx, |thread, _cx| {
739            let AgentThreadEntryContent::ToolCall(ToolCall {
740                id,
741                status:
742                    ToolCallStatus::WaitingForConfirmation {
743                        confirmation: ToolCallConfirmation::Execute { root_command, .. },
744                        ..
745                    },
746                ..
747            }) = &thread.entries()[1].content
748            else {
749                panic!();
750            };
751
752            assert_eq!(root_command, "echo");
753
754            *id
755        });
756
757        thread.update(cx, |thread, cx| {
758            thread.authorize_tool_call(tool_call_id, acp::ToolCallConfirmationOutcome::Allow, cx);
759
760            assert!(matches!(
761                &thread.entries()[1].content,
762                AgentThreadEntryContent::ToolCall(ToolCall {
763                    status: ToolCallStatus::Allowed { .. },
764                    ..
765                })
766            ));
767        });
768
769        full_turn.await.unwrap();
770
771        thread.read_with(cx, |thread, cx| {
772            let AgentThreadEntryContent::ToolCall(ToolCall {
773                status:
774                    ToolCallStatus::Allowed {
775                        content: Some(ToolCallContent::Markdown { markdown }),
776                        ..
777                    },
778                ..
779            }) = &thread.entries()[1].content
780            else {
781                panic!();
782            };
783
784            markdown.read_with(cx, |md, _cx| {
785                assert!(
786                    md.source().contains("Hello, world!"),
787                    r#"Expected '{}' to contain "Hello, world!""#,
788                    md.source()
789                );
790            });
791        });
792    }
793
794    async fn run_until_tool_call(thread: &Entity<AcpThread>, cx: &mut TestAppContext) {
795        let (mut tx, mut rx) = mpsc::channel::<()>(1);
796
797        let subscription = cx.update(|cx| {
798            cx.subscribe(thread, move |thread, _, cx| {
799                if thread
800                    .read(cx)
801                    .entries
802                    .iter()
803                    .any(|e| matches!(e.content, AgentThreadEntryContent::ToolCall(_)))
804                {
805                    tx.try_send(()).unwrap();
806                }
807            })
808        });
809
810        select! {
811            _ = futures::FutureExt::fuse(smol::Timer::after(Duration::from_secs(10))) => {
812                panic!("Timeout waiting for tool call")
813            }
814            _ = rx.next().fuse() => {
815                drop(subscription);
816            }
817        }
818    }
819
820    pub fn gemini_acp_server(project: Entity<Project>, mut cx: AsyncApp) -> Result<Arc<AcpServer>> {
821        let cli_path =
822            Path::new(env!("CARGO_MANIFEST_DIR")).join("../../../gemini-cli/packages/cli");
823        let mut command = util::command::new_smol_command("node");
824        command
825            .arg(cli_path)
826            .arg("--acp")
827            .current_dir("/private/tmp")
828            .stdin(Stdio::piped())
829            .stdout(Stdio::piped())
830            .stderr(Stdio::inherit())
831            .kill_on_drop(true);
832
833        if let Ok(gemini_key) = std::env::var("GEMINI_API_KEY") {
834            command.env("GEMINI_API_KEY", gemini_key);
835        }
836
837        let child = command.spawn().unwrap();
838
839        Ok(AcpServer::stdio(child, project, &mut cx))
840    }
841}