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