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}