1use acp_thread::{AgentConnection, Plan};
2use agent_servers::AgentServer;
3use agent_settings::{AgentSettings, NotifyWhenAgentWaiting};
4use audio::{Audio, Sound};
5use std::cell::RefCell;
6use std::collections::BTreeMap;
7use std::path::Path;
8use std::rc::Rc;
9use std::sync::Arc;
10use std::time::Duration;
11
12use agent_client_protocol as acp;
13use assistant_tool::ActionLog;
14use buffer_diff::BufferDiff;
15use collections::{HashMap, HashSet};
16use editor::{
17 AnchorRangeExt, ContextMenuOptions, ContextMenuPlacement, Editor, EditorElement, EditorMode,
18 EditorStyle, MinimapVisibility, MultiBuffer, PathKey,
19};
20use file_icons::FileIcons;
21use gpui::{
22 Action, Animation, AnimationExt, App, BorderStyle, EdgesRefinement, Empty, Entity, EntityId,
23 FocusHandle, Focusable, Hsla, Length, ListOffset, ListState, PlatformDisplay, SharedString,
24 StyleRefinement, Subscription, Task, TextStyle, TextStyleRefinement, Transformation,
25 UnderlineStyle, WeakEntity, Window, WindowHandle, div, linear_color_stop, linear_gradient,
26 list, percentage, point, prelude::*, pulsating_between,
27};
28use language::language_settings::SoftWrap;
29use language::{Buffer, Language};
30use markdown::{HeadingLevelStyles, Markdown, MarkdownElement, MarkdownStyle};
31use parking_lot::Mutex;
32use project::Project;
33use settings::Settings as _;
34use text::{Anchor, BufferSnapshot};
35use theme::ThemeSettings;
36use ui::{Disclosure, Divider, DividerColor, KeyBinding, Tooltip, prelude::*};
37use util::ResultExt;
38use workspace::{CollaboratorId, Workspace};
39use zed_actions::agent::{Chat, NextHistoryMessage, PreviousHistoryMessage};
40
41use ::acp_thread::{
42 AcpThread, AcpThreadEvent, AgentThreadEntry, AssistantMessage, AssistantMessageChunk, Diff,
43 LoadError, MentionPath, ThreadStatus, ToolCall, ToolCallContent, ToolCallStatus,
44};
45
46use crate::acp::completion_provider::{ContextPickerCompletionProvider, MentionSet};
47use crate::acp::message_history::MessageHistory;
48use crate::agent_diff::AgentDiff;
49use crate::message_editor::{MAX_EDITOR_LINES, MIN_EDITOR_LINES};
50use crate::ui::{AgentNotification, AgentNotificationEvent};
51use crate::{
52 AgentDiffPane, AgentPanel, ExpandMessageEditor, Follow, KeepAll, OpenAgentDiff, RejectAll,
53};
54
55const RESPONSE_PADDING_X: Pixels = px(19.);
56
57pub struct AcpThreadView {
58 agent: Rc<dyn AgentServer>,
59 workspace: WeakEntity<Workspace>,
60 project: Entity<Project>,
61 thread_state: ThreadState,
62 diff_editors: HashMap<EntityId, Entity<Editor>>,
63 message_editor: Entity<Editor>,
64 message_set_from_history: Option<BufferSnapshot>,
65 _message_editor_subscription: Subscription,
66 mention_set: Arc<Mutex<MentionSet>>,
67 notifications: Vec<WindowHandle<AgentNotification>>,
68 notification_subscriptions: HashMap<WindowHandle<AgentNotification>, Vec<Subscription>>,
69 last_error: Option<Entity<Markdown>>,
70 list_state: ListState,
71 auth_task: Option<Task<()>>,
72 expanded_tool_calls: HashSet<acp::ToolCallId>,
73 expanded_thinking_blocks: HashSet<(usize, usize)>,
74 edits_expanded: bool,
75 plan_expanded: bool,
76 editor_expanded: bool,
77 message_history: Rc<RefCell<MessageHistory<Vec<acp::ContentBlock>>>>,
78 _cancel_task: Option<Task<()>>,
79}
80
81enum ThreadState {
82 Loading {
83 _task: Task<()>,
84 },
85 Ready {
86 thread: Entity<AcpThread>,
87 _subscription: [Subscription; 2],
88 },
89 LoadError(LoadError),
90 Unauthenticated {
91 connection: Rc<dyn AgentConnection>,
92 },
93}
94
95impl AcpThreadView {
96 pub fn new(
97 agent: Rc<dyn AgentServer>,
98 workspace: WeakEntity<Workspace>,
99 project: Entity<Project>,
100 message_history: Rc<RefCell<MessageHistory<Vec<acp::ContentBlock>>>>,
101 min_lines: usize,
102 max_lines: Option<usize>,
103 window: &mut Window,
104 cx: &mut Context<Self>,
105 ) -> Self {
106 let language = Language::new(
107 language::LanguageConfig {
108 completion_query_characters: HashSet::from_iter(['.', '-', '_', '@']),
109 ..Default::default()
110 },
111 None,
112 );
113
114 let mention_set = Arc::new(Mutex::new(MentionSet::default()));
115
116 let message_editor = cx.new(|cx| {
117 let buffer = cx.new(|cx| Buffer::local("", cx).with_language(Arc::new(language), cx));
118 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
119
120 let mut editor = Editor::new(
121 editor::EditorMode::AutoHeight {
122 min_lines,
123 max_lines: max_lines,
124 },
125 buffer,
126 None,
127 window,
128 cx,
129 );
130 editor.set_placeholder_text("Message the agent - @ to include files", cx);
131 editor.set_show_indent_guides(false, cx);
132 editor.set_soft_wrap();
133 editor.set_use_modal_editing(true);
134 editor.set_completion_provider(Some(Rc::new(ContextPickerCompletionProvider::new(
135 mention_set.clone(),
136 workspace.clone(),
137 cx.weak_entity(),
138 ))));
139 editor.set_context_menu_options(ContextMenuOptions {
140 min_entries_visible: 12,
141 max_entries_visible: 12,
142 placement: Some(ContextMenuPlacement::Above),
143 });
144 editor
145 });
146
147 let message_editor_subscription =
148 cx.subscribe(&message_editor, |this, editor, event, cx| {
149 if let editor::EditorEvent::BufferEdited = &event {
150 let buffer = editor
151 .read(cx)
152 .buffer()
153 .read(cx)
154 .as_singleton()
155 .unwrap()
156 .read(cx)
157 .snapshot();
158 if let Some(message) = this.message_set_from_history.clone()
159 && message.version() != buffer.version()
160 {
161 this.message_set_from_history = None;
162 }
163
164 if this.message_set_from_history.is_none() {
165 this.message_history.borrow_mut().reset_position();
166 }
167 }
168 });
169
170 let mention_set = mention_set.clone();
171
172 let list_state = ListState::new(
173 0,
174 gpui::ListAlignment::Bottom,
175 px(2048.0),
176 cx.processor({
177 move |this: &mut Self, index: usize, window, cx| {
178 let Some((entry, len)) = this.thread().and_then(|thread| {
179 let entries = &thread.read(cx).entries();
180 Some((entries.get(index)?, entries.len()))
181 }) else {
182 return Empty.into_any();
183 };
184 this.render_entry(index, len, entry, window, cx)
185 }
186 }),
187 );
188
189 Self {
190 agent: agent.clone(),
191 workspace: workspace.clone(),
192 project: project.clone(),
193 thread_state: Self::initial_state(agent, workspace, project, window, cx),
194 message_editor,
195 message_set_from_history: None,
196 _message_editor_subscription: message_editor_subscription,
197 mention_set,
198 notifications: Vec::new(),
199 notification_subscriptions: HashMap::default(),
200 diff_editors: Default::default(),
201 list_state: list_state,
202 last_error: None,
203 auth_task: None,
204 expanded_tool_calls: HashSet::default(),
205 expanded_thinking_blocks: HashSet::default(),
206 edits_expanded: false,
207 plan_expanded: false,
208 editor_expanded: false,
209 message_history,
210 _cancel_task: None,
211 }
212 }
213
214 fn initial_state(
215 agent: Rc<dyn AgentServer>,
216 workspace: WeakEntity<Workspace>,
217 project: Entity<Project>,
218 window: &mut Window,
219 cx: &mut Context<Self>,
220 ) -> ThreadState {
221 let root_dir = project
222 .read(cx)
223 .visible_worktrees(cx)
224 .next()
225 .map(|worktree| worktree.read(cx).abs_path())
226 .unwrap_or_else(|| paths::home_dir().as_path().into());
227
228 let connect_task = agent.connect(&root_dir, &project, cx);
229 let load_task = cx.spawn_in(window, async move |this, cx| {
230 let connection = match connect_task.await {
231 Ok(thread) => thread,
232 Err(err) => {
233 this.update(cx, |this, cx| {
234 this.handle_load_error(err, cx);
235 cx.notify();
236 })
237 .log_err();
238 return;
239 }
240 };
241
242 let result = match connection
243 .clone()
244 .new_thread(project.clone(), &root_dir, cx)
245 .await
246 {
247 Err(e) => {
248 let mut cx = cx.clone();
249 if e.is::<acp_thread::AuthRequired>() {
250 this.update(&mut cx, |this, cx| {
251 this.thread_state = ThreadState::Unauthenticated { connection };
252 cx.notify();
253 })
254 .ok();
255 return;
256 } else {
257 Err(e)
258 }
259 }
260 Ok(session_id) => Ok(session_id),
261 };
262
263 this.update_in(cx, |this, window, cx| {
264 match result {
265 Ok(thread) => {
266 let thread_subscription =
267 cx.subscribe_in(&thread, window, Self::handle_thread_event);
268
269 let action_log = thread.read(cx).action_log().clone();
270 let action_log_subscription =
271 cx.observe(&action_log, |_, _, cx| cx.notify());
272
273 this.list_state
274 .splice(0..0, thread.read(cx).entries().len());
275
276 AgentDiff::set_active_thread(&workspace, thread.clone(), window, cx);
277
278 this.thread_state = ThreadState::Ready {
279 thread,
280 _subscription: [thread_subscription, action_log_subscription],
281 };
282
283 cx.notify();
284 }
285 Err(err) => {
286 this.handle_load_error(err, cx);
287 }
288 };
289 })
290 .log_err();
291 });
292
293 ThreadState::Loading { _task: load_task }
294 }
295
296 fn handle_load_error(&mut self, err: anyhow::Error, cx: &mut Context<Self>) {
297 if let Some(load_err) = err.downcast_ref::<LoadError>() {
298 self.thread_state = ThreadState::LoadError(load_err.clone());
299 } else {
300 self.thread_state = ThreadState::LoadError(LoadError::Other(err.to_string().into()))
301 }
302 cx.notify();
303 }
304
305 pub fn thread(&self) -> Option<&Entity<AcpThread>> {
306 match &self.thread_state {
307 ThreadState::Ready { thread, .. } => Some(thread),
308 ThreadState::Unauthenticated { .. }
309 | ThreadState::Loading { .. }
310 | ThreadState::LoadError(..) => None,
311 }
312 }
313
314 pub fn title(&self, cx: &App) -> SharedString {
315 match &self.thread_state {
316 ThreadState::Ready { thread, .. } => thread.read(cx).title(),
317 ThreadState::Loading { .. } => "Loading…".into(),
318 ThreadState::LoadError(_) => "Failed to load".into(),
319 ThreadState::Unauthenticated { .. } => "Not authenticated".into(),
320 }
321 }
322
323 pub fn cancel(&mut self, cx: &mut Context<Self>) {
324 self.last_error.take();
325
326 if let Some(thread) = self.thread() {
327 self._cancel_task = Some(thread.update(cx, |thread, cx| thread.cancel(cx)));
328 }
329 }
330
331 pub fn expand_message_editor(
332 &mut self,
333 _: &ExpandMessageEditor,
334 _window: &mut Window,
335 cx: &mut Context<Self>,
336 ) {
337 self.set_editor_is_expanded(!self.editor_expanded, cx);
338 cx.notify();
339 }
340
341 fn set_editor_is_expanded(&mut self, is_expanded: bool, cx: &mut Context<Self>) {
342 self.editor_expanded = is_expanded;
343 self.message_editor.update(cx, |editor, _| {
344 if self.editor_expanded {
345 editor.set_mode(EditorMode::Full {
346 scale_ui_elements_with_buffer_font_size: false,
347 show_active_line_background: false,
348 sized_by_content: false,
349 })
350 } else {
351 editor.set_mode(EditorMode::AutoHeight {
352 min_lines: MIN_EDITOR_LINES,
353 max_lines: Some(MAX_EDITOR_LINES),
354 })
355 }
356 });
357 cx.notify();
358 }
359
360 fn chat(&mut self, _: &Chat, window: &mut Window, cx: &mut Context<Self>) {
361 self.last_error.take();
362
363 let mut ix = 0;
364 let mut chunks: Vec<acp::ContentBlock> = Vec::new();
365 let project = self.project.clone();
366 self.message_editor.update(cx, |editor, cx| {
367 let text = editor.text(cx);
368 editor.display_map.update(cx, |map, cx| {
369 let snapshot = map.snapshot(cx);
370 for (crease_id, crease) in snapshot.crease_snapshot.creases() {
371 if let Some(project_path) =
372 self.mention_set.lock().path_for_crease_id(crease_id)
373 {
374 let crease_range = crease.range().to_offset(&snapshot.buffer_snapshot);
375 if crease_range.start > ix {
376 chunks.push(text[ix..crease_range.start].into());
377 }
378 if let Some(abs_path) = project.read(cx).absolute_path(&project_path, cx) {
379 let path_str = abs_path.display().to_string();
380 chunks.push(acp::ContentBlock::ResourceLink(acp::ResourceLink {
381 uri: path_str.clone(),
382 name: path_str,
383 annotations: None,
384 description: None,
385 mime_type: None,
386 size: None,
387 title: None,
388 }));
389 }
390 ix = crease_range.end;
391 }
392 }
393
394 if ix < text.len() {
395 let last_chunk = text[ix..].trim();
396 if !last_chunk.is_empty() {
397 chunks.push(last_chunk.into());
398 }
399 }
400 })
401 });
402
403 if chunks.is_empty() {
404 return;
405 }
406
407 let Some(thread) = self.thread() else {
408 return;
409 };
410 let task = thread.update(cx, |thread, cx| thread.send(chunks.clone(), cx));
411
412 cx.spawn(async move |this, cx| {
413 let result = task.await;
414
415 this.update(cx, |this, cx| {
416 if let Err(err) = result {
417 this.last_error =
418 Some(cx.new(|cx| Markdown::new(err.to_string().into(), None, None, cx)))
419 }
420 })
421 })
422 .detach();
423
424 let mention_set = self.mention_set.clone();
425
426 self.set_editor_is_expanded(false, cx);
427
428 self.message_editor.update(cx, |editor, cx| {
429 editor.clear(window, cx);
430 editor.remove_creases(mention_set.lock().drain(), cx)
431 });
432
433 self.scroll_to_bottom(cx);
434
435 self.message_history.borrow_mut().push(chunks);
436 }
437
438 fn previous_history_message(
439 &mut self,
440 _: &PreviousHistoryMessage,
441 window: &mut Window,
442 cx: &mut Context<Self>,
443 ) {
444 if self.message_set_from_history.is_none() && !self.message_editor.read(cx).is_empty(cx) {
445 self.message_editor.update(cx, |editor, cx| {
446 editor.move_up(&Default::default(), window, cx);
447 });
448 return;
449 }
450
451 self.message_set_from_history = Self::set_draft_message(
452 self.message_editor.clone(),
453 self.mention_set.clone(),
454 self.project.clone(),
455 self.message_history
456 .borrow_mut()
457 .prev()
458 .map(|blocks| blocks.as_slice()),
459 window,
460 cx,
461 );
462 }
463
464 fn next_history_message(
465 &mut self,
466 _: &NextHistoryMessage,
467 window: &mut Window,
468 cx: &mut Context<Self>,
469 ) {
470 if self.message_set_from_history.is_none() {
471 self.message_editor.update(cx, |editor, cx| {
472 editor.move_down(&Default::default(), window, cx);
473 });
474 return;
475 }
476
477 let mut message_history = self.message_history.borrow_mut();
478 let next_history = message_history.next();
479
480 let set_draft_message = Self::set_draft_message(
481 self.message_editor.clone(),
482 self.mention_set.clone(),
483 self.project.clone(),
484 Some(
485 next_history
486 .map(|blocks| blocks.as_slice())
487 .unwrap_or_else(|| &[]),
488 ),
489 window,
490 cx,
491 );
492 // If we reset the text to an empty string because we ran out of history,
493 // we don't want to mark it as coming from the history
494 self.message_set_from_history = if next_history.is_some() {
495 set_draft_message
496 } else {
497 None
498 };
499 }
500
501 fn open_agent_diff(&mut self, _: &OpenAgentDiff, window: &mut Window, cx: &mut Context<Self>) {
502 if let Some(thread) = self.thread() {
503 AgentDiffPane::deploy(thread.clone(), self.workspace.clone(), window, cx).log_err();
504 }
505 }
506
507 fn open_edited_buffer(
508 &mut self,
509 buffer: &Entity<Buffer>,
510 window: &mut Window,
511 cx: &mut Context<Self>,
512 ) {
513 let Some(thread) = self.thread() else {
514 return;
515 };
516
517 let Some(diff) =
518 AgentDiffPane::deploy(thread.clone(), self.workspace.clone(), window, cx).log_err()
519 else {
520 return;
521 };
522
523 diff.update(cx, |diff, cx| {
524 diff.move_to_path(PathKey::for_buffer(&buffer, cx), window, cx)
525 })
526 }
527
528 fn set_draft_message(
529 message_editor: Entity<Editor>,
530 mention_set: Arc<Mutex<MentionSet>>,
531 project: Entity<Project>,
532 message: Option<&[acp::ContentBlock]>,
533 window: &mut Window,
534 cx: &mut Context<Self>,
535 ) -> Option<BufferSnapshot> {
536 cx.notify();
537
538 let message = message?;
539
540 let mut text = String::new();
541 let mut mentions = Vec::new();
542
543 for chunk in message {
544 match chunk {
545 acp::ContentBlock::Text(text_content) => {
546 text.push_str(&text_content.text);
547 }
548 acp::ContentBlock::ResourceLink(resource_link) => {
549 let path = Path::new(&resource_link.uri);
550 let start = text.len();
551 let content = MentionPath::new(&path).to_string();
552 text.push_str(&content);
553 let end = text.len();
554 if let Some(project_path) =
555 project.read(cx).project_path_for_absolute_path(&path, cx)
556 {
557 let filename: SharedString = path
558 .file_name()
559 .unwrap_or_default()
560 .to_string_lossy()
561 .to_string()
562 .into();
563 mentions.push((start..end, project_path, filename));
564 }
565 }
566 acp::ContentBlock::Image(_)
567 | acp::ContentBlock::Audio(_)
568 | acp::ContentBlock::Resource(_) => {}
569 }
570 }
571
572 let snapshot = message_editor.update(cx, |editor, cx| {
573 editor.set_text(text, window, cx);
574 editor.buffer().read(cx).snapshot(cx)
575 });
576
577 for (range, project_path, filename) in mentions {
578 let crease_icon_path = if project_path.path.is_dir() {
579 FileIcons::get_folder_icon(false, cx)
580 .unwrap_or_else(|| IconName::Folder.path().into())
581 } else {
582 FileIcons::get_icon(Path::new(project_path.path.as_ref()), cx)
583 .unwrap_or_else(|| IconName::File.path().into())
584 };
585
586 let anchor = snapshot.anchor_before(range.start);
587 let crease_id = crate::context_picker::insert_crease_for_mention(
588 anchor.excerpt_id,
589 anchor.text_anchor,
590 range.end - range.start,
591 filename,
592 crease_icon_path,
593 message_editor.clone(),
594 window,
595 cx,
596 );
597 if let Some(crease_id) = crease_id {
598 mention_set.lock().insert(crease_id, project_path);
599 }
600 }
601
602 let snapshot = snapshot.as_singleton().unwrap().2.clone();
603 Some(snapshot.text)
604 }
605
606 fn handle_thread_event(
607 &mut self,
608 thread: &Entity<AcpThread>,
609 event: &AcpThreadEvent,
610 window: &mut Window,
611 cx: &mut Context<Self>,
612 ) {
613 let count = self.list_state.item_count();
614 match event {
615 AcpThreadEvent::NewEntry => {
616 let index = thread.read(cx).entries().len() - 1;
617 self.sync_thread_entry_view(index, window, cx);
618 self.list_state.splice(count..count, 1);
619 }
620 AcpThreadEvent::EntryUpdated(index) => {
621 let index = *index;
622 self.sync_thread_entry_view(index, window, cx);
623 self.list_state.splice(index..index + 1, 1);
624 }
625 AcpThreadEvent::ToolAuthorizationRequired => {
626 self.notify_with_sound("Waiting for tool confirmation", IconName::Info, window, cx);
627 }
628 AcpThreadEvent::Stopped => {
629 let used_tools = thread.read(cx).used_tools_since_last_user_message();
630 self.notify_with_sound(
631 if used_tools {
632 "Finished running tools"
633 } else {
634 "New message"
635 },
636 IconName::ZedAssistant,
637 window,
638 cx,
639 );
640 }
641 AcpThreadEvent::Error => {
642 self.notify_with_sound(
643 "Agent stopped due to an error",
644 IconName::Warning,
645 window,
646 cx,
647 );
648 }
649 }
650 cx.notify();
651 }
652
653 fn sync_thread_entry_view(
654 &mut self,
655 entry_ix: usize,
656 window: &mut Window,
657 cx: &mut Context<Self>,
658 ) {
659 let Some(multibuffers) = self.entry_diff_multibuffers(entry_ix, cx) else {
660 return;
661 };
662
663 let multibuffers = multibuffers.collect::<Vec<_>>();
664
665 for multibuffer in multibuffers {
666 if self.diff_editors.contains_key(&multibuffer.entity_id()) {
667 return;
668 }
669
670 let editor = cx.new(|cx| {
671 let mut editor = Editor::new(
672 EditorMode::Full {
673 scale_ui_elements_with_buffer_font_size: false,
674 show_active_line_background: false,
675 sized_by_content: true,
676 },
677 multibuffer.clone(),
678 None,
679 window,
680 cx,
681 );
682 editor.set_show_gutter(false, cx);
683 editor.disable_inline_diagnostics();
684 editor.disable_expand_excerpt_buttons(cx);
685 editor.set_show_vertical_scrollbar(false, cx);
686 editor.set_minimap_visibility(MinimapVisibility::Disabled, window, cx);
687 editor.set_soft_wrap_mode(SoftWrap::None, cx);
688 editor.scroll_manager.set_forbid_vertical_scroll(true);
689 editor.set_show_indent_guides(false, cx);
690 editor.set_read_only(true);
691 editor.set_show_breakpoints(false, cx);
692 editor.set_show_code_actions(false, cx);
693 editor.set_show_git_diff_gutter(false, cx);
694 editor.set_expand_all_diff_hunks(cx);
695 editor.set_text_style_refinement(TextStyleRefinement {
696 font_size: Some(
697 TextSize::Small
698 .rems(cx)
699 .to_pixels(ThemeSettings::get_global(cx).agent_font_size(cx))
700 .into(),
701 ),
702 ..Default::default()
703 });
704 editor
705 });
706 let entity_id = multibuffer.entity_id();
707 cx.observe_release(&multibuffer, move |this, _, _| {
708 this.diff_editors.remove(&entity_id);
709 })
710 .detach();
711
712 self.diff_editors.insert(entity_id, editor);
713 }
714 }
715
716 fn entry_diff_multibuffers(
717 &self,
718 entry_ix: usize,
719 cx: &App,
720 ) -> Option<impl Iterator<Item = Entity<MultiBuffer>>> {
721 let entry = self.thread()?.read(cx).entries().get(entry_ix)?;
722 Some(entry.diffs().map(|diff| diff.multibuffer.clone()))
723 }
724
725 fn authenticate(
726 &mut self,
727 method: acp::AuthMethodId,
728 window: &mut Window,
729 cx: &mut Context<Self>,
730 ) {
731 let ThreadState::Unauthenticated { ref connection } = self.thread_state else {
732 return;
733 };
734
735 self.last_error.take();
736 let authenticate = connection.authenticate(method, cx);
737 self.auth_task = Some(cx.spawn_in(window, {
738 let project = self.project.clone();
739 let agent = self.agent.clone();
740 async move |this, cx| {
741 let result = authenticate.await;
742
743 this.update_in(cx, |this, window, cx| {
744 if let Err(err) = result {
745 this.last_error = Some(cx.new(|cx| {
746 Markdown::new(format!("Error: {err}").into(), None, None, cx)
747 }))
748 } else {
749 this.thread_state = Self::initial_state(
750 agent,
751 this.workspace.clone(),
752 project.clone(),
753 window,
754 cx,
755 )
756 }
757 this.auth_task.take()
758 })
759 .ok();
760 }
761 }));
762 }
763
764 fn authorize_tool_call(
765 &mut self,
766 tool_call_id: acp::ToolCallId,
767 option_id: acp::PermissionOptionId,
768 option_kind: acp::PermissionOptionKind,
769 cx: &mut Context<Self>,
770 ) {
771 let Some(thread) = self.thread() else {
772 return;
773 };
774 thread.update(cx, |thread, cx| {
775 thread.authorize_tool_call(tool_call_id, option_id, option_kind, cx);
776 });
777 cx.notify();
778 }
779
780 fn render_entry(
781 &self,
782 index: usize,
783 total_entries: usize,
784 entry: &AgentThreadEntry,
785 window: &mut Window,
786 cx: &Context<Self>,
787 ) -> AnyElement {
788 let primary = match &entry {
789 AgentThreadEntry::UserMessage(message) => div()
790 .py_4()
791 .px_2()
792 .child(
793 v_flex()
794 .p_3()
795 .gap_1p5()
796 .rounded_lg()
797 .shadow_md()
798 .bg(cx.theme().colors().editor_background)
799 .border_1()
800 .border_color(cx.theme().colors().border)
801 .text_xs()
802 .children(message.content.markdown().map(|md| {
803 self.render_markdown(
804 md.clone(),
805 user_message_markdown_style(window, cx),
806 )
807 })),
808 )
809 .into_any(),
810 AgentThreadEntry::AssistantMessage(AssistantMessage { chunks }) => {
811 let style = default_markdown_style(false, window, cx);
812 let message_body = v_flex()
813 .w_full()
814 .gap_2p5()
815 .children(chunks.iter().enumerate().filter_map(
816 |(chunk_ix, chunk)| match chunk {
817 AssistantMessageChunk::Message { block } => {
818 block.markdown().map(|md| {
819 self.render_markdown(md.clone(), style.clone())
820 .into_any_element()
821 })
822 }
823 AssistantMessageChunk::Thought { block } => {
824 block.markdown().map(|md| {
825 self.render_thinking_block(
826 index,
827 chunk_ix,
828 md.clone(),
829 window,
830 cx,
831 )
832 .into_any_element()
833 })
834 }
835 },
836 ))
837 .into_any();
838
839 v_flex()
840 .px_5()
841 .py_1()
842 .when(index + 1 == total_entries, |this| this.pb_4())
843 .w_full()
844 .text_ui(cx)
845 .child(message_body)
846 .into_any()
847 }
848 AgentThreadEntry::ToolCall(tool_call) => div()
849 .py_1p5()
850 .px_5()
851 .child(self.render_tool_call(index, tool_call, window, cx))
852 .into_any(),
853 };
854
855 let Some(thread) = self.thread() else {
856 return primary;
857 };
858 let is_generating = matches!(thread.read(cx).status(), ThreadStatus::Generating);
859 if index == total_entries - 1 && !is_generating {
860 v_flex()
861 .child(primary)
862 .child(self.render_thread_controls(cx))
863 .into_any_element()
864 } else {
865 primary
866 }
867 }
868
869 fn tool_card_header_bg(&self, cx: &Context<Self>) -> Hsla {
870 cx.theme()
871 .colors()
872 .element_background
873 .blend(cx.theme().colors().editor_foreground.opacity(0.025))
874 }
875
876 fn tool_card_border_color(&self, cx: &Context<Self>) -> Hsla {
877 cx.theme().colors().border.opacity(0.6)
878 }
879
880 fn tool_name_font_size(&self) -> Rems {
881 rems_from_px(13.)
882 }
883
884 fn render_thinking_block(
885 &self,
886 entry_ix: usize,
887 chunk_ix: usize,
888 chunk: Entity<Markdown>,
889 window: &Window,
890 cx: &Context<Self>,
891 ) -> AnyElement {
892 let header_id = SharedString::from(format!("thinking-block-header-{}", entry_ix));
893 let key = (entry_ix, chunk_ix);
894 let is_open = self.expanded_thinking_blocks.contains(&key);
895
896 v_flex()
897 .child(
898 h_flex()
899 .id(header_id)
900 .group("disclosure-header")
901 .w_full()
902 .justify_between()
903 .opacity(0.8)
904 .hover(|style| style.opacity(1.))
905 .child(
906 h_flex()
907 .gap_1p5()
908 .child(
909 Icon::new(IconName::ToolBulb)
910 .size(IconSize::Small)
911 .color(Color::Muted),
912 )
913 .child(
914 div()
915 .text_size(self.tool_name_font_size())
916 .child("Thinking"),
917 ),
918 )
919 .child(
920 div().visible_on_hover("disclosure-header").child(
921 Disclosure::new("thinking-disclosure", is_open)
922 .opened_icon(IconName::ChevronUp)
923 .closed_icon(IconName::ChevronDown)
924 .on_click(cx.listener({
925 move |this, _event, _window, cx| {
926 if is_open {
927 this.expanded_thinking_blocks.remove(&key);
928 } else {
929 this.expanded_thinking_blocks.insert(key);
930 }
931 cx.notify();
932 }
933 })),
934 ),
935 )
936 .on_click(cx.listener({
937 move |this, _event, _window, cx| {
938 if is_open {
939 this.expanded_thinking_blocks.remove(&key);
940 } else {
941 this.expanded_thinking_blocks.insert(key);
942 }
943 cx.notify();
944 }
945 })),
946 )
947 .when(is_open, |this| {
948 this.child(
949 div()
950 .relative()
951 .mt_1p5()
952 .ml(px(7.))
953 .pl_4()
954 .border_l_1()
955 .border_color(self.tool_card_border_color(cx))
956 .text_ui_sm(cx)
957 .child(
958 self.render_markdown(chunk, default_markdown_style(false, window, cx)),
959 ),
960 )
961 })
962 .into_any_element()
963 }
964
965 fn render_tool_call(
966 &self,
967 entry_ix: usize,
968 tool_call: &ToolCall,
969 window: &Window,
970 cx: &Context<Self>,
971 ) -> Div {
972 let header_id = SharedString::from(format!("tool-call-header-{}", entry_ix));
973
974 let status_icon = match &tool_call.status {
975 ToolCallStatus::Allowed {
976 status: acp::ToolCallStatus::Pending,
977 }
978 | ToolCallStatus::WaitingForConfirmation { .. } => None,
979 ToolCallStatus::Allowed {
980 status: acp::ToolCallStatus::InProgress,
981 ..
982 } => Some(
983 Icon::new(IconName::ArrowCircle)
984 .color(Color::Accent)
985 .size(IconSize::Small)
986 .with_animation(
987 "running",
988 Animation::new(Duration::from_secs(2)).repeat(),
989 |icon, delta| icon.transform(Transformation::rotate(percentage(delta))),
990 )
991 .into_any(),
992 ),
993 ToolCallStatus::Allowed {
994 status: acp::ToolCallStatus::Completed,
995 ..
996 } => None,
997 ToolCallStatus::Rejected
998 | ToolCallStatus::Canceled
999 | ToolCallStatus::Allowed {
1000 status: acp::ToolCallStatus::Failed,
1001 ..
1002 } => Some(
1003 Icon::new(IconName::X)
1004 .color(Color::Error)
1005 .size(IconSize::Small)
1006 .into_any_element(),
1007 ),
1008 };
1009
1010 let needs_confirmation = match &tool_call.status {
1011 ToolCallStatus::WaitingForConfirmation { .. } => true,
1012 _ => tool_call
1013 .content
1014 .iter()
1015 .any(|content| matches!(content, ToolCallContent::Diff { .. })),
1016 };
1017
1018 let is_collapsible = !tool_call.content.is_empty() && !needs_confirmation;
1019 let is_open = !is_collapsible || self.expanded_tool_calls.contains(&tool_call.id);
1020
1021 v_flex()
1022 .when(needs_confirmation, |this| {
1023 this.rounded_lg()
1024 .border_1()
1025 .border_color(self.tool_card_border_color(cx))
1026 .bg(cx.theme().colors().editor_background)
1027 .overflow_hidden()
1028 })
1029 .child(
1030 h_flex()
1031 .id(header_id)
1032 .w_full()
1033 .gap_1()
1034 .justify_between()
1035 .map(|this| {
1036 if needs_confirmation {
1037 this.px_2()
1038 .py_1()
1039 .rounded_t_md()
1040 .bg(self.tool_card_header_bg(cx))
1041 .border_b_1()
1042 .border_color(self.tool_card_border_color(cx))
1043 } else {
1044 this.opacity(0.8).hover(|style| style.opacity(1.))
1045 }
1046 })
1047 .child(
1048 h_flex()
1049 .id("tool-call-header")
1050 .overflow_x_scroll()
1051 .map(|this| {
1052 if needs_confirmation {
1053 this.text_xs()
1054 } else {
1055 this.text_size(self.tool_name_font_size())
1056 }
1057 })
1058 .gap_1p5()
1059 .child(
1060 Icon::new(match tool_call.kind {
1061 acp::ToolKind::Read => IconName::ToolRead,
1062 acp::ToolKind::Edit => IconName::ToolPencil,
1063 acp::ToolKind::Delete => IconName::ToolDeleteFile,
1064 acp::ToolKind::Move => IconName::ArrowRightLeft,
1065 acp::ToolKind::Search => IconName::ToolSearch,
1066 acp::ToolKind::Execute => IconName::ToolTerminal,
1067 acp::ToolKind::Think => IconName::ToolBulb,
1068 acp::ToolKind::Fetch => IconName::ToolWeb,
1069 acp::ToolKind::Other => IconName::ToolHammer,
1070 })
1071 .size(IconSize::Small)
1072 .color(Color::Muted),
1073 )
1074 .child(if tool_call.locations.len() == 1 {
1075 let name = tool_call.locations[0]
1076 .path
1077 .file_name()
1078 .unwrap_or_default()
1079 .display()
1080 .to_string();
1081
1082 h_flex()
1083 .id(("open-tool-call-location", entry_ix))
1084 .child(name)
1085 .w_full()
1086 .max_w_full()
1087 .pr_1()
1088 .gap_0p5()
1089 .cursor_pointer()
1090 .rounded_sm()
1091 .opacity(0.8)
1092 .hover(|label| {
1093 label.opacity(1.).bg(cx
1094 .theme()
1095 .colors()
1096 .element_hover
1097 .opacity(0.5))
1098 })
1099 .tooltip(Tooltip::text("Jump to File"))
1100 .on_click(cx.listener(move |this, _, window, cx| {
1101 this.open_tool_call_location(entry_ix, 0, window, cx);
1102 }))
1103 .into_any_element()
1104 } else {
1105 self.render_markdown(
1106 tool_call.label.clone(),
1107 default_markdown_style(needs_confirmation, window, cx),
1108 )
1109 .into_any()
1110 }),
1111 )
1112 .child(
1113 h_flex()
1114 .gap_0p5()
1115 .when(is_collapsible, |this| {
1116 this.child(
1117 Disclosure::new(("expand", entry_ix), is_open)
1118 .opened_icon(IconName::ChevronUp)
1119 .closed_icon(IconName::ChevronDown)
1120 .on_click(cx.listener({
1121 let id = tool_call.id.clone();
1122 move |this: &mut Self, _, _, cx: &mut Context<Self>| {
1123 if is_open {
1124 this.expanded_tool_calls.remove(&id);
1125 } else {
1126 this.expanded_tool_calls.insert(id.clone());
1127 }
1128 cx.notify();
1129 }
1130 })),
1131 )
1132 })
1133 .children(status_icon),
1134 )
1135 .on_click(cx.listener({
1136 let id = tool_call.id.clone();
1137 move |this: &mut Self, _, _, cx: &mut Context<Self>| {
1138 if is_open {
1139 this.expanded_tool_calls.remove(&id);
1140 } else {
1141 this.expanded_tool_calls.insert(id.clone());
1142 }
1143 cx.notify();
1144 }
1145 })),
1146 )
1147 .when(is_open, |this| {
1148 this.child(
1149 v_flex()
1150 .text_xs()
1151 .when(is_collapsible, |this| {
1152 this.mt_1()
1153 .border_1()
1154 .border_color(self.tool_card_border_color(cx))
1155 .bg(cx.theme().colors().editor_background)
1156 .rounded_lg()
1157 })
1158 .map(|this| {
1159 if is_open {
1160 match &tool_call.status {
1161 ToolCallStatus::WaitingForConfirmation { options, .. } => this
1162 .children(tool_call.content.iter().map(|content| {
1163 div()
1164 .py_1p5()
1165 .child(
1166 self.render_tool_call_content(
1167 content, window, cx,
1168 ),
1169 )
1170 .into_any_element()
1171 }))
1172 .child(self.render_permission_buttons(
1173 options,
1174 entry_ix,
1175 tool_call.id.clone(),
1176 tool_call.content.is_empty(),
1177 cx,
1178 )),
1179 ToolCallStatus::Allowed { .. } | ToolCallStatus::Canceled => {
1180 this.children(tool_call.content.iter().map(|content| {
1181 div()
1182 .py_1p5()
1183 .child(
1184 self.render_tool_call_content(
1185 content, window, cx,
1186 ),
1187 )
1188 .into_any_element()
1189 }))
1190 }
1191 ToolCallStatus::Rejected => this,
1192 }
1193 } else {
1194 this
1195 }
1196 }),
1197 )
1198 })
1199 }
1200
1201 fn render_tool_call_content(
1202 &self,
1203 content: &ToolCallContent,
1204 window: &Window,
1205 cx: &Context<Self>,
1206 ) -> AnyElement {
1207 match content {
1208 ToolCallContent::ContentBlock { content } => {
1209 if let Some(md) = content.markdown() {
1210 div()
1211 .p_2()
1212 .child(
1213 self.render_markdown(
1214 md.clone(),
1215 default_markdown_style(false, window, cx),
1216 ),
1217 )
1218 .into_any_element()
1219 } else {
1220 Empty.into_any_element()
1221 }
1222 }
1223 ToolCallContent::Diff {
1224 diff: Diff { multibuffer, .. },
1225 ..
1226 } => self.render_diff_editor(multibuffer),
1227 }
1228 }
1229
1230 fn render_permission_buttons(
1231 &self,
1232 options: &[acp::PermissionOption],
1233 entry_ix: usize,
1234 tool_call_id: acp::ToolCallId,
1235 empty_content: bool,
1236 cx: &Context<Self>,
1237 ) -> Div {
1238 h_flex()
1239 .py_1p5()
1240 .px_1p5()
1241 .gap_1()
1242 .justify_end()
1243 .when(!empty_content, |this| {
1244 this.border_t_1()
1245 .border_color(self.tool_card_border_color(cx))
1246 })
1247 .children(options.iter().map(|option| {
1248 let option_id = SharedString::from(option.id.0.clone());
1249 Button::new((option_id, entry_ix), option.name.clone())
1250 .map(|this| match option.kind {
1251 acp::PermissionOptionKind::AllowOnce => {
1252 this.icon(IconName::Check).icon_color(Color::Success)
1253 }
1254 acp::PermissionOptionKind::AllowAlways => {
1255 this.icon(IconName::CheckDouble).icon_color(Color::Success)
1256 }
1257 acp::PermissionOptionKind::RejectOnce => {
1258 this.icon(IconName::X).icon_color(Color::Error)
1259 }
1260 acp::PermissionOptionKind::RejectAlways => {
1261 this.icon(IconName::X).icon_color(Color::Error)
1262 }
1263 })
1264 .icon_position(IconPosition::Start)
1265 .icon_size(IconSize::XSmall)
1266 .on_click(cx.listener({
1267 let tool_call_id = tool_call_id.clone();
1268 let option_id = option.id.clone();
1269 let option_kind = option.kind;
1270 move |this, _, _, cx| {
1271 this.authorize_tool_call(
1272 tool_call_id.clone(),
1273 option_id.clone(),
1274 option_kind,
1275 cx,
1276 );
1277 }
1278 }))
1279 }))
1280 }
1281
1282 fn render_diff_editor(&self, multibuffer: &Entity<MultiBuffer>) -> AnyElement {
1283 v_flex()
1284 .h_full()
1285 .child(
1286 if let Some(editor) = self.diff_editors.get(&multibuffer.entity_id()) {
1287 editor.clone().into_any_element()
1288 } else {
1289 Empty.into_any()
1290 },
1291 )
1292 .into_any()
1293 }
1294
1295 fn render_agent_logo(&self) -> AnyElement {
1296 Icon::new(self.agent.logo())
1297 .color(Color::Muted)
1298 .size(IconSize::XLarge)
1299 .into_any_element()
1300 }
1301
1302 fn render_error_agent_logo(&self) -> AnyElement {
1303 let logo = Icon::new(self.agent.logo())
1304 .color(Color::Muted)
1305 .size(IconSize::XLarge)
1306 .into_any_element();
1307
1308 h_flex()
1309 .relative()
1310 .justify_center()
1311 .child(div().opacity(0.3).child(logo))
1312 .child(
1313 h_flex().absolute().right_1().bottom_0().child(
1314 Icon::new(IconName::XCircle)
1315 .color(Color::Error)
1316 .size(IconSize::Small),
1317 ),
1318 )
1319 .into_any_element()
1320 }
1321
1322 fn render_empty_state(&self, cx: &App) -> AnyElement {
1323 let loading = matches!(&self.thread_state, ThreadState::Loading { .. });
1324
1325 v_flex()
1326 .size_full()
1327 .items_center()
1328 .justify_center()
1329 .child(if loading {
1330 h_flex()
1331 .justify_center()
1332 .child(self.render_agent_logo())
1333 .with_animation(
1334 "pulsating_icon",
1335 Animation::new(Duration::from_secs(2))
1336 .repeat()
1337 .with_easing(pulsating_between(0.4, 1.0)),
1338 |icon, delta| icon.opacity(delta),
1339 )
1340 .into_any()
1341 } else {
1342 self.render_agent_logo().into_any_element()
1343 })
1344 .child(h_flex().mt_4().mb_1().justify_center().child(if loading {
1345 div()
1346 .child(LoadingLabel::new("").size(LabelSize::Large))
1347 .into_any_element()
1348 } else {
1349 Headline::new(self.agent.empty_state_headline())
1350 .size(HeadlineSize::Medium)
1351 .into_any_element()
1352 }))
1353 .child(
1354 div()
1355 .max_w_1_2()
1356 .text_sm()
1357 .text_center()
1358 .map(|this| {
1359 if loading {
1360 this.invisible()
1361 } else {
1362 this.text_color(cx.theme().colors().text_muted)
1363 }
1364 })
1365 .child(self.agent.empty_state_message()),
1366 )
1367 .into_any()
1368 }
1369
1370 fn render_pending_auth_state(&self) -> AnyElement {
1371 v_flex()
1372 .items_center()
1373 .justify_center()
1374 .child(self.render_error_agent_logo())
1375 .child(
1376 h_flex()
1377 .mt_4()
1378 .mb_1()
1379 .justify_center()
1380 .child(Headline::new("Not Authenticated").size(HeadlineSize::Medium)),
1381 )
1382 .into_any()
1383 }
1384
1385 fn render_error_state(&self, e: &LoadError, cx: &Context<Self>) -> AnyElement {
1386 let mut container = v_flex()
1387 .items_center()
1388 .justify_center()
1389 .child(self.render_error_agent_logo())
1390 .child(
1391 v_flex()
1392 .mt_4()
1393 .mb_2()
1394 .gap_0p5()
1395 .text_center()
1396 .items_center()
1397 .child(Headline::new("Failed to launch").size(HeadlineSize::Medium))
1398 .child(
1399 Label::new(e.to_string())
1400 .size(LabelSize::Small)
1401 .color(Color::Muted),
1402 ),
1403 );
1404
1405 if let LoadError::Unsupported {
1406 upgrade_message,
1407 upgrade_command,
1408 ..
1409 } = &e
1410 {
1411 let upgrade_message = upgrade_message.clone();
1412 let upgrade_command = upgrade_command.clone();
1413 container = container.child(Button::new("upgrade", upgrade_message).on_click(
1414 cx.listener(move |this, _, window, cx| {
1415 this.workspace
1416 .update(cx, |workspace, cx| {
1417 let project = workspace.project().read(cx);
1418 let cwd = project.first_project_directory(cx);
1419 let shell = project.terminal_settings(&cwd, cx).shell.clone();
1420 let spawn_in_terminal = task::SpawnInTerminal {
1421 id: task::TaskId("install".to_string()),
1422 full_label: upgrade_command.clone(),
1423 label: upgrade_command.clone(),
1424 command: Some(upgrade_command.clone()),
1425 args: Vec::new(),
1426 command_label: upgrade_command.clone(),
1427 cwd,
1428 env: Default::default(),
1429 use_new_terminal: true,
1430 allow_concurrent_runs: true,
1431 reveal: Default::default(),
1432 reveal_target: Default::default(),
1433 hide: Default::default(),
1434 shell,
1435 show_summary: true,
1436 show_command: true,
1437 show_rerun: false,
1438 };
1439 workspace
1440 .spawn_in_terminal(spawn_in_terminal, window, cx)
1441 .detach();
1442 })
1443 .ok();
1444 }),
1445 ));
1446 }
1447
1448 container.into_any()
1449 }
1450
1451 fn render_activity_bar(
1452 &self,
1453 thread_entity: &Entity<AcpThread>,
1454 window: &mut Window,
1455 cx: &Context<Self>,
1456 ) -> Option<AnyElement> {
1457 let thread = thread_entity.read(cx);
1458 let action_log = thread.action_log();
1459 let changed_buffers = action_log.read(cx).changed_buffers(cx);
1460 let plan = thread.plan();
1461
1462 if changed_buffers.is_empty() && plan.is_empty() {
1463 return None;
1464 }
1465
1466 let editor_bg_color = cx.theme().colors().editor_background;
1467 let active_color = cx.theme().colors().element_selected;
1468 let bg_edit_files_disclosure = editor_bg_color.blend(active_color.opacity(0.3));
1469
1470 let pending_edits = thread.has_pending_edit_tool_calls();
1471
1472 v_flex()
1473 .mt_1()
1474 .mx_2()
1475 .bg(bg_edit_files_disclosure)
1476 .border_1()
1477 .border_b_0()
1478 .border_color(cx.theme().colors().border)
1479 .rounded_t_md()
1480 .shadow(vec![gpui::BoxShadow {
1481 color: gpui::black().opacity(0.15),
1482 offset: point(px(1.), px(-1.)),
1483 blur_radius: px(3.),
1484 spread_radius: px(0.),
1485 }])
1486 .when(!plan.is_empty(), |this| {
1487 this.child(self.render_plan_summary(plan, window, cx))
1488 .when(self.plan_expanded, |parent| {
1489 parent.child(self.render_plan_entries(plan, window, cx))
1490 })
1491 })
1492 .when(!changed_buffers.is_empty(), |this| {
1493 this.child(Divider::horizontal())
1494 .child(self.render_edits_summary(
1495 action_log,
1496 &changed_buffers,
1497 self.edits_expanded,
1498 pending_edits,
1499 window,
1500 cx,
1501 ))
1502 .when(self.edits_expanded, |parent| {
1503 parent.child(self.render_edited_files(
1504 action_log,
1505 &changed_buffers,
1506 pending_edits,
1507 cx,
1508 ))
1509 })
1510 })
1511 .into_any()
1512 .into()
1513 }
1514
1515 fn render_plan_summary(&self, plan: &Plan, window: &mut Window, cx: &Context<Self>) -> Div {
1516 let stats = plan.stats();
1517
1518 let title = if let Some(entry) = stats.in_progress_entry
1519 && !self.plan_expanded
1520 {
1521 h_flex()
1522 .w_full()
1523 .gap_1()
1524 .text_xs()
1525 .text_color(cx.theme().colors().text_muted)
1526 .justify_between()
1527 .child(
1528 h_flex()
1529 .gap_1()
1530 .child(
1531 Label::new("Current:")
1532 .size(LabelSize::Small)
1533 .color(Color::Muted),
1534 )
1535 .child(MarkdownElement::new(
1536 entry.content.clone(),
1537 plan_label_markdown_style(&entry.status, window, cx),
1538 )),
1539 )
1540 .when(stats.pending > 0, |this| {
1541 this.child(
1542 Label::new(format!("{} left", stats.pending))
1543 .size(LabelSize::Small)
1544 .color(Color::Muted)
1545 .mr_1(),
1546 )
1547 })
1548 } else {
1549 let status_label = if stats.pending == 0 {
1550 "All Done".to_string()
1551 } else if stats.completed == 0 {
1552 format!("{}", plan.entries.len())
1553 } else {
1554 format!("{}/{}", stats.completed, plan.entries.len())
1555 };
1556
1557 h_flex()
1558 .w_full()
1559 .gap_1()
1560 .justify_between()
1561 .child(
1562 Label::new("Plan")
1563 .size(LabelSize::Small)
1564 .color(Color::Muted),
1565 )
1566 .child(
1567 Label::new(status_label)
1568 .size(LabelSize::Small)
1569 .color(Color::Muted)
1570 .mr_1(),
1571 )
1572 };
1573
1574 h_flex()
1575 .p_1()
1576 .justify_between()
1577 .when(self.plan_expanded, |this| {
1578 this.border_b_1().border_color(cx.theme().colors().border)
1579 })
1580 .child(
1581 h_flex()
1582 .id("plan_summary")
1583 .w_full()
1584 .gap_1()
1585 .child(Disclosure::new("plan_disclosure", self.plan_expanded))
1586 .child(title)
1587 .on_click(cx.listener(|this, _, _, cx| {
1588 this.plan_expanded = !this.plan_expanded;
1589 cx.notify();
1590 })),
1591 )
1592 }
1593
1594 fn render_plan_entries(&self, plan: &Plan, window: &mut Window, cx: &Context<Self>) -> Div {
1595 v_flex().children(plan.entries.iter().enumerate().flat_map(|(index, entry)| {
1596 let element = h_flex()
1597 .py_1()
1598 .px_2()
1599 .gap_2()
1600 .justify_between()
1601 .bg(cx.theme().colors().editor_background)
1602 .when(index < plan.entries.len() - 1, |parent| {
1603 parent.border_color(cx.theme().colors().border).border_b_1()
1604 })
1605 .child(
1606 h_flex()
1607 .id(("plan_entry", index))
1608 .gap_1p5()
1609 .max_w_full()
1610 .overflow_x_scroll()
1611 .text_xs()
1612 .text_color(cx.theme().colors().text_muted)
1613 .child(match entry.status {
1614 acp::PlanEntryStatus::Pending => Icon::new(IconName::TodoPending)
1615 .size(IconSize::Small)
1616 .color(Color::Muted)
1617 .into_any_element(),
1618 acp::PlanEntryStatus::InProgress => Icon::new(IconName::TodoProgress)
1619 .size(IconSize::Small)
1620 .color(Color::Accent)
1621 .with_animation(
1622 "running",
1623 Animation::new(Duration::from_secs(2)).repeat(),
1624 |icon, delta| {
1625 icon.transform(Transformation::rotate(percentage(delta)))
1626 },
1627 )
1628 .into_any_element(),
1629 acp::PlanEntryStatus::Completed => Icon::new(IconName::TodoComplete)
1630 .size(IconSize::Small)
1631 .color(Color::Success)
1632 .into_any_element(),
1633 })
1634 .child(MarkdownElement::new(
1635 entry.content.clone(),
1636 plan_label_markdown_style(&entry.status, window, cx),
1637 )),
1638 );
1639
1640 Some(element)
1641 }))
1642 }
1643
1644 fn render_edits_summary(
1645 &self,
1646 action_log: &Entity<ActionLog>,
1647 changed_buffers: &BTreeMap<Entity<Buffer>, Entity<BufferDiff>>,
1648 expanded: bool,
1649 pending_edits: bool,
1650 window: &mut Window,
1651 cx: &Context<Self>,
1652 ) -> Div {
1653 const EDIT_NOT_READY_TOOLTIP_LABEL: &str = "Wait until file edits are complete.";
1654
1655 let focus_handle = self.focus_handle(cx);
1656
1657 h_flex()
1658 .p_1()
1659 .justify_between()
1660 .when(expanded, |this| {
1661 this.border_b_1().border_color(cx.theme().colors().border)
1662 })
1663 .child(
1664 h_flex()
1665 .id("edits-container")
1666 .cursor_pointer()
1667 .w_full()
1668 .gap_1()
1669 .child(Disclosure::new("edits-disclosure", expanded))
1670 .map(|this| {
1671 if pending_edits {
1672 this.child(
1673 Label::new(format!(
1674 "Editing {} {}…",
1675 changed_buffers.len(),
1676 if changed_buffers.len() == 1 {
1677 "file"
1678 } else {
1679 "files"
1680 }
1681 ))
1682 .color(Color::Muted)
1683 .size(LabelSize::Small)
1684 .with_animation(
1685 "edit-label",
1686 Animation::new(Duration::from_secs(2))
1687 .repeat()
1688 .with_easing(pulsating_between(0.3, 0.7)),
1689 |label, delta| label.alpha(delta),
1690 ),
1691 )
1692 } else {
1693 this.child(
1694 Label::new("Edits")
1695 .size(LabelSize::Small)
1696 .color(Color::Muted),
1697 )
1698 .child(Label::new("•").size(LabelSize::XSmall).color(Color::Muted))
1699 .child(
1700 Label::new(format!(
1701 "{} {}",
1702 changed_buffers.len(),
1703 if changed_buffers.len() == 1 {
1704 "file"
1705 } else {
1706 "files"
1707 }
1708 ))
1709 .size(LabelSize::Small)
1710 .color(Color::Muted),
1711 )
1712 }
1713 })
1714 .on_click(cx.listener(|this, _, _, cx| {
1715 this.edits_expanded = !this.edits_expanded;
1716 cx.notify();
1717 })),
1718 )
1719 .child(
1720 h_flex()
1721 .gap_1()
1722 .child(
1723 IconButton::new("review-changes", IconName::ListTodo)
1724 .icon_size(IconSize::Small)
1725 .tooltip({
1726 let focus_handle = focus_handle.clone();
1727 move |window, cx| {
1728 Tooltip::for_action_in(
1729 "Review Changes",
1730 &OpenAgentDiff,
1731 &focus_handle,
1732 window,
1733 cx,
1734 )
1735 }
1736 })
1737 .on_click(cx.listener(|_, _, window, cx| {
1738 window.dispatch_action(OpenAgentDiff.boxed_clone(), cx);
1739 })),
1740 )
1741 .child(Divider::vertical().color(DividerColor::Border))
1742 .child(
1743 Button::new("reject-all-changes", "Reject All")
1744 .label_size(LabelSize::Small)
1745 .disabled(pending_edits)
1746 .when(pending_edits, |this| {
1747 this.tooltip(Tooltip::text(EDIT_NOT_READY_TOOLTIP_LABEL))
1748 })
1749 .key_binding(
1750 KeyBinding::for_action_in(
1751 &RejectAll,
1752 &focus_handle.clone(),
1753 window,
1754 cx,
1755 )
1756 .map(|kb| kb.size(rems_from_px(10.))),
1757 )
1758 .on_click({
1759 let action_log = action_log.clone();
1760 cx.listener(move |_, _, _, cx| {
1761 action_log.update(cx, |action_log, cx| {
1762 action_log.reject_all_edits(cx).detach();
1763 })
1764 })
1765 }),
1766 )
1767 .child(
1768 Button::new("keep-all-changes", "Keep All")
1769 .label_size(LabelSize::Small)
1770 .disabled(pending_edits)
1771 .when(pending_edits, |this| {
1772 this.tooltip(Tooltip::text(EDIT_NOT_READY_TOOLTIP_LABEL))
1773 })
1774 .key_binding(
1775 KeyBinding::for_action_in(&KeepAll, &focus_handle, window, cx)
1776 .map(|kb| kb.size(rems_from_px(10.))),
1777 )
1778 .on_click({
1779 let action_log = action_log.clone();
1780 cx.listener(move |_, _, _, cx| {
1781 action_log.update(cx, |action_log, cx| {
1782 action_log.keep_all_edits(cx);
1783 })
1784 })
1785 }),
1786 ),
1787 )
1788 }
1789
1790 fn render_edited_files(
1791 &self,
1792 action_log: &Entity<ActionLog>,
1793 changed_buffers: &BTreeMap<Entity<Buffer>, Entity<BufferDiff>>,
1794 pending_edits: bool,
1795 cx: &Context<Self>,
1796 ) -> Div {
1797 let editor_bg_color = cx.theme().colors().editor_background;
1798
1799 v_flex().children(changed_buffers.into_iter().enumerate().flat_map(
1800 |(index, (buffer, _diff))| {
1801 let file = buffer.read(cx).file()?;
1802 let path = file.path();
1803
1804 let file_path = path.parent().and_then(|parent| {
1805 let parent_str = parent.to_string_lossy();
1806
1807 if parent_str.is_empty() {
1808 None
1809 } else {
1810 Some(
1811 Label::new(format!("/{}{}", parent_str, std::path::MAIN_SEPARATOR_STR))
1812 .color(Color::Muted)
1813 .size(LabelSize::XSmall)
1814 .buffer_font(cx),
1815 )
1816 }
1817 });
1818
1819 let file_name = path.file_name().map(|name| {
1820 Label::new(name.to_string_lossy().to_string())
1821 .size(LabelSize::XSmall)
1822 .buffer_font(cx)
1823 });
1824
1825 let file_icon = FileIcons::get_icon(&path, cx)
1826 .map(Icon::from_path)
1827 .map(|icon| icon.color(Color::Muted).size(IconSize::Small))
1828 .unwrap_or_else(|| {
1829 Icon::new(IconName::File)
1830 .color(Color::Muted)
1831 .size(IconSize::Small)
1832 });
1833
1834 let overlay_gradient = linear_gradient(
1835 90.,
1836 linear_color_stop(editor_bg_color, 1.),
1837 linear_color_stop(editor_bg_color.opacity(0.2), 0.),
1838 );
1839
1840 let element = h_flex()
1841 .group("edited-code")
1842 .id(("file-container", index))
1843 .relative()
1844 .py_1()
1845 .pl_2()
1846 .pr_1()
1847 .gap_2()
1848 .justify_between()
1849 .bg(editor_bg_color)
1850 .when(index < changed_buffers.len() - 1, |parent| {
1851 parent.border_color(cx.theme().colors().border).border_b_1()
1852 })
1853 .child(
1854 h_flex()
1855 .id(("file-name", index))
1856 .pr_8()
1857 .gap_1p5()
1858 .max_w_full()
1859 .overflow_x_scroll()
1860 .child(file_icon)
1861 .child(h_flex().gap_0p5().children(file_name).children(file_path))
1862 .on_click({
1863 let buffer = buffer.clone();
1864 cx.listener(move |this, _, window, cx| {
1865 this.open_edited_buffer(&buffer, window, cx);
1866 })
1867 }),
1868 )
1869 .child(
1870 h_flex()
1871 .gap_1()
1872 .visible_on_hover("edited-code")
1873 .child(
1874 Button::new("review", "Review")
1875 .label_size(LabelSize::Small)
1876 .on_click({
1877 let buffer = buffer.clone();
1878 cx.listener(move |this, _, window, cx| {
1879 this.open_edited_buffer(&buffer, window, cx);
1880 })
1881 }),
1882 )
1883 .child(Divider::vertical().color(DividerColor::BorderVariant))
1884 .child(
1885 Button::new("reject-file", "Reject")
1886 .label_size(LabelSize::Small)
1887 .disabled(pending_edits)
1888 .on_click({
1889 let buffer = buffer.clone();
1890 let action_log = action_log.clone();
1891 move |_, _, cx| {
1892 action_log.update(cx, |action_log, cx| {
1893 action_log
1894 .reject_edits_in_ranges(
1895 buffer.clone(),
1896 vec![Anchor::MIN..Anchor::MAX],
1897 cx,
1898 )
1899 .detach_and_log_err(cx);
1900 })
1901 }
1902 }),
1903 )
1904 .child(
1905 Button::new("keep-file", "Keep")
1906 .label_size(LabelSize::Small)
1907 .disabled(pending_edits)
1908 .on_click({
1909 let buffer = buffer.clone();
1910 let action_log = action_log.clone();
1911 move |_, _, cx| {
1912 action_log.update(cx, |action_log, cx| {
1913 action_log.keep_edits_in_range(
1914 buffer.clone(),
1915 Anchor::MIN..Anchor::MAX,
1916 cx,
1917 );
1918 })
1919 }
1920 }),
1921 ),
1922 )
1923 .child(
1924 div()
1925 .id("gradient-overlay")
1926 .absolute()
1927 .h_full()
1928 .w_12()
1929 .top_0()
1930 .bottom_0()
1931 .right(px(152.))
1932 .bg(overlay_gradient),
1933 );
1934
1935 Some(element)
1936 },
1937 ))
1938 }
1939
1940 fn render_message_editor(&mut self, window: &mut Window, cx: &mut Context<Self>) -> AnyElement {
1941 let focus_handle = self.message_editor.focus_handle(cx);
1942 let editor_bg_color = cx.theme().colors().editor_background;
1943 let (expand_icon, expand_tooltip) = if self.editor_expanded {
1944 (IconName::Minimize, "Minimize Message Editor")
1945 } else {
1946 (IconName::Maximize, "Expand Message Editor")
1947 };
1948
1949 v_flex()
1950 .on_action(cx.listener(Self::expand_message_editor))
1951 .p_2()
1952 .gap_2()
1953 .border_t_1()
1954 .border_color(cx.theme().colors().border)
1955 .bg(editor_bg_color)
1956 .when(self.editor_expanded, |this| {
1957 this.h(vh(0.8, window)).size_full().justify_between()
1958 })
1959 .child(
1960 v_flex()
1961 .relative()
1962 .size_full()
1963 .pt_1()
1964 .pr_2p5()
1965 .child(div().flex_1().child({
1966 let settings = ThemeSettings::get_global(cx);
1967 let font_size = TextSize::Small
1968 .rems(cx)
1969 .to_pixels(settings.agent_font_size(cx));
1970 let line_height = settings.buffer_line_height.value() * font_size;
1971
1972 let text_style = TextStyle {
1973 color: cx.theme().colors().text,
1974 font_family: settings.buffer_font.family.clone(),
1975 font_fallbacks: settings.buffer_font.fallbacks.clone(),
1976 font_features: settings.buffer_font.features.clone(),
1977 font_size: font_size.into(),
1978 line_height: line_height.into(),
1979 ..Default::default()
1980 };
1981
1982 EditorElement::new(
1983 &self.message_editor,
1984 EditorStyle {
1985 background: editor_bg_color,
1986 local_player: cx.theme().players().local(),
1987 text: text_style,
1988 syntax: cx.theme().syntax().clone(),
1989 ..Default::default()
1990 },
1991 )
1992 }))
1993 .child(
1994 h_flex()
1995 .absolute()
1996 .top_0()
1997 .right_0()
1998 .opacity(0.5)
1999 .hover(|this| this.opacity(1.0))
2000 .child(
2001 IconButton::new("toggle-height", expand_icon)
2002 .icon_size(IconSize::XSmall)
2003 .icon_color(Color::Muted)
2004 .tooltip({
2005 let focus_handle = focus_handle.clone();
2006 move |window, cx| {
2007 Tooltip::for_action_in(
2008 expand_tooltip,
2009 &ExpandMessageEditor,
2010 &focus_handle,
2011 window,
2012 cx,
2013 )
2014 }
2015 })
2016 .on_click(cx.listener(|_, _, window, cx| {
2017 window.dispatch_action(Box::new(ExpandMessageEditor), cx);
2018 })),
2019 ),
2020 ),
2021 )
2022 .child(
2023 h_flex()
2024 .flex_none()
2025 .justify_between()
2026 .child(self.render_follow_toggle(cx))
2027 .child(self.render_send_button(cx)),
2028 )
2029 .into_any()
2030 }
2031
2032 fn render_send_button(&self, cx: &mut Context<Self>) -> AnyElement {
2033 if self.thread().map_or(true, |thread| {
2034 thread.read(cx).status() == ThreadStatus::Idle
2035 }) {
2036 let is_editor_empty = self.message_editor.read(cx).is_empty(cx);
2037 IconButton::new("send-message", IconName::Send)
2038 .icon_color(Color::Accent)
2039 .style(ButtonStyle::Filled)
2040 .disabled(self.thread().is_none() || is_editor_empty)
2041 .when(!is_editor_empty, |button| {
2042 button.tooltip(move |window, cx| Tooltip::for_action("Send", &Chat, window, cx))
2043 })
2044 .when(is_editor_empty, |button| {
2045 button.tooltip(Tooltip::text("Type a message to submit"))
2046 })
2047 .on_click(cx.listener(|this, _, window, cx| {
2048 this.chat(&Chat, window, cx);
2049 }))
2050 .into_any_element()
2051 } else {
2052 IconButton::new("stop-generation", IconName::StopFilled)
2053 .icon_color(Color::Error)
2054 .style(ButtonStyle::Tinted(ui::TintColor::Error))
2055 .tooltip(move |window, cx| {
2056 Tooltip::for_action("Stop Generation", &editor::actions::Cancel, window, cx)
2057 })
2058 .on_click(cx.listener(|this, _event, _, cx| this.cancel(cx)))
2059 .into_any_element()
2060 }
2061 }
2062
2063 fn render_follow_toggle(&self, cx: &mut Context<Self>) -> impl IntoElement {
2064 let following = self
2065 .workspace
2066 .read_with(cx, |workspace, _| {
2067 workspace.is_being_followed(CollaboratorId::Agent)
2068 })
2069 .unwrap_or(false);
2070
2071 IconButton::new("follow-agent", IconName::Crosshair)
2072 .icon_size(IconSize::Small)
2073 .icon_color(Color::Muted)
2074 .toggle_state(following)
2075 .selected_icon_color(Some(Color::Custom(cx.theme().players().agent().cursor)))
2076 .tooltip(move |window, cx| {
2077 if following {
2078 Tooltip::for_action("Stop Following Agent", &Follow, window, cx)
2079 } else {
2080 Tooltip::with_meta(
2081 "Follow Agent",
2082 Some(&Follow),
2083 "Track the agent's location as it reads and edits files.",
2084 window,
2085 cx,
2086 )
2087 }
2088 })
2089 .on_click(cx.listener(move |this, _, window, cx| {
2090 this.workspace
2091 .update(cx, |workspace, cx| {
2092 if following {
2093 workspace.unfollow(CollaboratorId::Agent, window, cx);
2094 } else {
2095 workspace.follow(CollaboratorId::Agent, window, cx);
2096 }
2097 })
2098 .ok();
2099 }))
2100 }
2101
2102 fn render_markdown(&self, markdown: Entity<Markdown>, style: MarkdownStyle) -> MarkdownElement {
2103 let workspace = self.workspace.clone();
2104 MarkdownElement::new(markdown, style).on_url_click(move |text, window, cx| {
2105 Self::open_link(text, &workspace, window, cx);
2106 })
2107 }
2108
2109 fn open_link(
2110 url: SharedString,
2111 workspace: &WeakEntity<Workspace>,
2112 window: &mut Window,
2113 cx: &mut App,
2114 ) {
2115 let Some(workspace) = workspace.upgrade() else {
2116 cx.open_url(&url);
2117 return;
2118 };
2119
2120 if let Some(mention_path) = MentionPath::try_parse(&url) {
2121 workspace.update(cx, |workspace, cx| {
2122 let project = workspace.project();
2123 let Some((path, entry)) = project.update(cx, |project, cx| {
2124 let path = project.find_project_path(mention_path.path(), cx)?;
2125 let entry = project.entry_for_path(&path, cx)?;
2126 Some((path, entry))
2127 }) else {
2128 return;
2129 };
2130
2131 if entry.is_dir() {
2132 project.update(cx, |_, cx| {
2133 cx.emit(project::Event::RevealInProjectPanel(entry.id));
2134 });
2135 } else {
2136 workspace
2137 .open_path(path, None, true, window, cx)
2138 .detach_and_log_err(cx);
2139 }
2140 })
2141 } else {
2142 cx.open_url(&url);
2143 }
2144 }
2145
2146 fn open_tool_call_location(
2147 &self,
2148 entry_ix: usize,
2149 location_ix: usize,
2150 window: &mut Window,
2151 cx: &mut Context<Self>,
2152 ) -> Option<()> {
2153 let location = self
2154 .thread()?
2155 .read(cx)
2156 .entries()
2157 .get(entry_ix)?
2158 .locations()?
2159 .get(location_ix)?;
2160
2161 let project_path = self
2162 .project
2163 .read(cx)
2164 .find_project_path(&location.path, cx)?;
2165
2166 let open_task = self
2167 .workspace
2168 .update(cx, |worskpace, cx| {
2169 worskpace.open_path(project_path, None, true, window, cx)
2170 })
2171 .log_err()?;
2172
2173 window
2174 .spawn(cx, async move |cx| {
2175 let item = open_task.await?;
2176
2177 let Some(active_editor) = item.downcast::<Editor>() else {
2178 return anyhow::Ok(());
2179 };
2180
2181 active_editor.update_in(cx, |editor, window, cx| {
2182 let snapshot = editor.buffer().read(cx).snapshot(cx);
2183 let first_hunk = editor
2184 .diff_hunks_in_ranges(
2185 &[editor::Anchor::min()..editor::Anchor::max()],
2186 &snapshot,
2187 )
2188 .next();
2189 if let Some(first_hunk) = first_hunk {
2190 let first_hunk_start = first_hunk.multi_buffer_range().start;
2191 editor.change_selections(Default::default(), window, cx, |selections| {
2192 selections.select_anchor_ranges([first_hunk_start..first_hunk_start]);
2193 })
2194 }
2195 })?;
2196
2197 anyhow::Ok(())
2198 })
2199 .detach_and_log_err(cx);
2200
2201 None
2202 }
2203
2204 pub fn open_thread_as_markdown(
2205 &self,
2206 workspace: Entity<Workspace>,
2207 window: &mut Window,
2208 cx: &mut App,
2209 ) -> Task<anyhow::Result<()>> {
2210 let markdown_language_task = workspace
2211 .read(cx)
2212 .app_state()
2213 .languages
2214 .language_for_name("Markdown");
2215
2216 let (thread_summary, markdown) = if let Some(thread) = self.thread() {
2217 let thread = thread.read(cx);
2218 (thread.title().to_string(), thread.to_markdown(cx))
2219 } else {
2220 return Task::ready(Ok(()));
2221 };
2222
2223 window.spawn(cx, async move |cx| {
2224 let markdown_language = markdown_language_task.await?;
2225
2226 workspace.update_in(cx, |workspace, window, cx| {
2227 let project = workspace.project().clone();
2228
2229 if !project.read(cx).is_local() {
2230 anyhow::bail!("failed to open active thread as markdown in remote project");
2231 }
2232
2233 let buffer = project.update(cx, |project, cx| {
2234 project.create_local_buffer(&markdown, Some(markdown_language), cx)
2235 });
2236 let buffer = cx.new(|cx| {
2237 MultiBuffer::singleton(buffer, cx).with_title(thread_summary.clone())
2238 });
2239
2240 workspace.add_item_to_active_pane(
2241 Box::new(cx.new(|cx| {
2242 let mut editor =
2243 Editor::for_multibuffer(buffer, Some(project.clone()), window, cx);
2244 editor.set_breadcrumb_header(thread_summary);
2245 editor
2246 })),
2247 None,
2248 true,
2249 window,
2250 cx,
2251 );
2252
2253 anyhow::Ok(())
2254 })??;
2255 anyhow::Ok(())
2256 })
2257 }
2258
2259 fn scroll_to_top(&mut self, cx: &mut Context<Self>) {
2260 self.list_state.scroll_to(ListOffset::default());
2261 cx.notify();
2262 }
2263
2264 pub fn scroll_to_bottom(&mut self, cx: &mut Context<Self>) {
2265 if let Some(thread) = self.thread() {
2266 let entry_count = thread.read(cx).entries().len();
2267 self.list_state.reset(entry_count);
2268 cx.notify();
2269 }
2270 }
2271
2272 fn notify_with_sound(
2273 &mut self,
2274 caption: impl Into<SharedString>,
2275 icon: IconName,
2276 window: &mut Window,
2277 cx: &mut Context<Self>,
2278 ) {
2279 self.play_notification_sound(window, cx);
2280 self.show_notification(caption, icon, window, cx);
2281 }
2282
2283 fn play_notification_sound(&self, window: &Window, cx: &mut App) {
2284 let settings = AgentSettings::get_global(cx);
2285 if settings.play_sound_when_agent_done && !window.is_window_active() {
2286 Audio::play_sound(Sound::AgentDone, cx);
2287 }
2288 }
2289
2290 fn show_notification(
2291 &mut self,
2292 caption: impl Into<SharedString>,
2293 icon: IconName,
2294 window: &mut Window,
2295 cx: &mut Context<Self>,
2296 ) {
2297 if window.is_window_active() || !self.notifications.is_empty() {
2298 return;
2299 }
2300
2301 let title = self.title(cx);
2302
2303 match AgentSettings::get_global(cx).notify_when_agent_waiting {
2304 NotifyWhenAgentWaiting::PrimaryScreen => {
2305 if let Some(primary) = cx.primary_display() {
2306 self.pop_up(icon, caption.into(), title, window, primary, cx);
2307 }
2308 }
2309 NotifyWhenAgentWaiting::AllScreens => {
2310 let caption = caption.into();
2311 for screen in cx.displays() {
2312 self.pop_up(icon, caption.clone(), title.clone(), window, screen, cx);
2313 }
2314 }
2315 NotifyWhenAgentWaiting::Never => {
2316 // Don't show anything
2317 }
2318 }
2319 }
2320
2321 fn pop_up(
2322 &mut self,
2323 icon: IconName,
2324 caption: SharedString,
2325 title: SharedString,
2326 window: &mut Window,
2327 screen: Rc<dyn PlatformDisplay>,
2328 cx: &mut Context<Self>,
2329 ) {
2330 let options = AgentNotification::window_options(screen, cx);
2331
2332 let project_name = self.workspace.upgrade().and_then(|workspace| {
2333 workspace
2334 .read(cx)
2335 .project()
2336 .read(cx)
2337 .visible_worktrees(cx)
2338 .next()
2339 .map(|worktree| worktree.read(cx).root_name().to_string())
2340 });
2341
2342 if let Some(screen_window) = cx
2343 .open_window(options, |_, cx| {
2344 cx.new(|_| {
2345 AgentNotification::new(title.clone(), caption.clone(), icon, project_name)
2346 })
2347 })
2348 .log_err()
2349 {
2350 if let Some(pop_up) = screen_window.entity(cx).log_err() {
2351 self.notification_subscriptions
2352 .entry(screen_window)
2353 .or_insert_with(Vec::new)
2354 .push(cx.subscribe_in(&pop_up, window, {
2355 |this, _, event, window, cx| match event {
2356 AgentNotificationEvent::Accepted => {
2357 let handle = window.window_handle();
2358 cx.activate(true);
2359
2360 let workspace_handle = this.workspace.clone();
2361
2362 // If there are multiple Zed windows, activate the correct one.
2363 cx.defer(move |cx| {
2364 handle
2365 .update(cx, |_view, window, _cx| {
2366 window.activate_window();
2367
2368 if let Some(workspace) = workspace_handle.upgrade() {
2369 workspace.update(_cx, |workspace, cx| {
2370 workspace.focus_panel::<AgentPanel>(window, cx);
2371 });
2372 }
2373 })
2374 .log_err();
2375 });
2376
2377 this.dismiss_notifications(cx);
2378 }
2379 AgentNotificationEvent::Dismissed => {
2380 this.dismiss_notifications(cx);
2381 }
2382 }
2383 }));
2384
2385 self.notifications.push(screen_window);
2386
2387 // If the user manually refocuses the original window, dismiss the popup.
2388 self.notification_subscriptions
2389 .entry(screen_window)
2390 .or_insert_with(Vec::new)
2391 .push({
2392 let pop_up_weak = pop_up.downgrade();
2393
2394 cx.observe_window_activation(window, move |_, window, cx| {
2395 if window.is_window_active() {
2396 if let Some(pop_up) = pop_up_weak.upgrade() {
2397 pop_up.update(cx, |_, cx| {
2398 cx.emit(AgentNotificationEvent::Dismissed);
2399 });
2400 }
2401 }
2402 })
2403 });
2404 }
2405 }
2406 }
2407
2408 fn dismiss_notifications(&mut self, cx: &mut Context<Self>) {
2409 for window in self.notifications.drain(..) {
2410 window
2411 .update(cx, |_, window, _| {
2412 window.remove_window();
2413 })
2414 .ok();
2415
2416 self.notification_subscriptions.remove(&window);
2417 }
2418 }
2419
2420 fn render_thread_controls(&self, cx: &Context<Self>) -> impl IntoElement {
2421 let open_as_markdown = IconButton::new("open-as-markdown", IconName::FileText)
2422 .icon_size(IconSize::XSmall)
2423 .icon_color(Color::Ignored)
2424 .tooltip(Tooltip::text("Open Thread as Markdown"))
2425 .on_click(cx.listener(move |this, _, window, cx| {
2426 if let Some(workspace) = this.workspace.upgrade() {
2427 this.open_thread_as_markdown(workspace, window, cx)
2428 .detach_and_log_err(cx);
2429 }
2430 }));
2431
2432 let scroll_to_top = IconButton::new("scroll_to_top", IconName::ArrowUpAlt)
2433 .icon_size(IconSize::XSmall)
2434 .icon_color(Color::Ignored)
2435 .tooltip(Tooltip::text("Scroll To Top"))
2436 .on_click(cx.listener(move |this, _, _, cx| {
2437 this.scroll_to_top(cx);
2438 }));
2439
2440 h_flex()
2441 .mr_1()
2442 .pb_2()
2443 .px(RESPONSE_PADDING_X)
2444 .opacity(0.4)
2445 .hover(|style| style.opacity(1.))
2446 .flex_wrap()
2447 .justify_end()
2448 .child(open_as_markdown)
2449 .child(scroll_to_top)
2450 }
2451}
2452
2453impl Focusable for AcpThreadView {
2454 fn focus_handle(&self, cx: &App) -> FocusHandle {
2455 self.message_editor.focus_handle(cx)
2456 }
2457}
2458
2459impl Render for AcpThreadView {
2460 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
2461 v_flex()
2462 .size_full()
2463 .key_context("AcpThread")
2464 .on_action(cx.listener(Self::chat))
2465 .on_action(cx.listener(Self::previous_history_message))
2466 .on_action(cx.listener(Self::next_history_message))
2467 .on_action(cx.listener(Self::open_agent_diff))
2468 .bg(cx.theme().colors().panel_background)
2469 .child(match &self.thread_state {
2470 ThreadState::Unauthenticated { connection } => v_flex()
2471 .p_2()
2472 .flex_1()
2473 .items_center()
2474 .justify_center()
2475 .child(self.render_pending_auth_state())
2476 .child(h_flex().mt_1p5().justify_center().children(
2477 connection.auth_methods().into_iter().map(|method| {
2478 Button::new(
2479 SharedString::from(method.id.0.clone()),
2480 method.name.clone(),
2481 )
2482 .on_click({
2483 let method_id = method.id.clone();
2484 cx.listener(move |this, _, window, cx| {
2485 this.authenticate(method_id.clone(), window, cx)
2486 })
2487 })
2488 }),
2489 )),
2490 ThreadState::Loading { .. } => v_flex().flex_1().child(self.render_empty_state(cx)),
2491 ThreadState::LoadError(e) => v_flex()
2492 .p_2()
2493 .flex_1()
2494 .items_center()
2495 .justify_center()
2496 .child(self.render_error_state(e, cx)),
2497 ThreadState::Ready { thread, .. } => {
2498 let thread_clone = thread.clone();
2499
2500 v_flex().flex_1().map(|this| {
2501 if self.list_state.item_count() > 0 {
2502 this.child(
2503 list(self.list_state.clone())
2504 .with_sizing_behavior(gpui::ListSizingBehavior::Auto)
2505 .flex_grow()
2506 .into_any(),
2507 )
2508 .children(match thread_clone.read(cx).status() {
2509 ThreadStatus::Idle | ThreadStatus::WaitingForToolConfirmation => {
2510 None
2511 }
2512 ThreadStatus::Generating => div()
2513 .px_5()
2514 .py_2()
2515 .child(LoadingLabel::new("").size(LabelSize::Small))
2516 .into(),
2517 })
2518 .children(self.render_activity_bar(&thread_clone, window, cx))
2519 } else {
2520 this.child(self.render_empty_state(cx))
2521 }
2522 })
2523 }
2524 })
2525 .when_some(self.last_error.clone(), |el, error| {
2526 el.child(
2527 div()
2528 .p_2()
2529 .text_xs()
2530 .border_t_1()
2531 .border_color(cx.theme().colors().border)
2532 .bg(cx.theme().status().error_background)
2533 .child(
2534 self.render_markdown(error, default_markdown_style(false, window, cx)),
2535 ),
2536 )
2537 })
2538 .child(self.render_message_editor(window, cx))
2539 }
2540}
2541
2542fn user_message_markdown_style(window: &Window, cx: &App) -> MarkdownStyle {
2543 let mut style = default_markdown_style(false, window, cx);
2544 let mut text_style = window.text_style();
2545 let theme_settings = ThemeSettings::get_global(cx);
2546
2547 let buffer_font = theme_settings.buffer_font.family.clone();
2548 let buffer_font_size = TextSize::Small.rems(cx);
2549
2550 text_style.refine(&TextStyleRefinement {
2551 font_family: Some(buffer_font),
2552 font_size: Some(buffer_font_size.into()),
2553 ..Default::default()
2554 });
2555
2556 style.base_text_style = text_style;
2557 style.link_callback = Some(Rc::new(move |url, cx| {
2558 if MentionPath::try_parse(url).is_some() {
2559 let colors = cx.theme().colors();
2560 Some(TextStyleRefinement {
2561 background_color: Some(colors.element_background),
2562 ..Default::default()
2563 })
2564 } else {
2565 None
2566 }
2567 }));
2568 style
2569}
2570
2571fn default_markdown_style(buffer_font: bool, window: &Window, cx: &App) -> MarkdownStyle {
2572 let theme_settings = ThemeSettings::get_global(cx);
2573 let colors = cx.theme().colors();
2574
2575 let buffer_font_size = TextSize::Small.rems(cx);
2576
2577 let mut text_style = window.text_style();
2578 let line_height = buffer_font_size * 1.75;
2579
2580 let font_family = if buffer_font {
2581 theme_settings.buffer_font.family.clone()
2582 } else {
2583 theme_settings.ui_font.family.clone()
2584 };
2585
2586 let font_size = if buffer_font {
2587 TextSize::Small.rems(cx)
2588 } else {
2589 TextSize::Default.rems(cx)
2590 };
2591
2592 text_style.refine(&TextStyleRefinement {
2593 font_family: Some(font_family),
2594 font_fallbacks: theme_settings.ui_font.fallbacks.clone(),
2595 font_features: Some(theme_settings.ui_font.features.clone()),
2596 font_size: Some(font_size.into()),
2597 line_height: Some(line_height.into()),
2598 color: Some(cx.theme().colors().text),
2599 ..Default::default()
2600 });
2601
2602 MarkdownStyle {
2603 base_text_style: text_style.clone(),
2604 syntax: cx.theme().syntax().clone(),
2605 selection_background_color: cx.theme().colors().element_selection_background,
2606 code_block_overflow_x_scroll: true,
2607 table_overflow_x_scroll: true,
2608 heading_level_styles: Some(HeadingLevelStyles {
2609 h1: Some(TextStyleRefinement {
2610 font_size: Some(rems(1.15).into()),
2611 ..Default::default()
2612 }),
2613 h2: Some(TextStyleRefinement {
2614 font_size: Some(rems(1.1).into()),
2615 ..Default::default()
2616 }),
2617 h3: Some(TextStyleRefinement {
2618 font_size: Some(rems(1.05).into()),
2619 ..Default::default()
2620 }),
2621 h4: Some(TextStyleRefinement {
2622 font_size: Some(rems(1.).into()),
2623 ..Default::default()
2624 }),
2625 h5: Some(TextStyleRefinement {
2626 font_size: Some(rems(0.95).into()),
2627 ..Default::default()
2628 }),
2629 h6: Some(TextStyleRefinement {
2630 font_size: Some(rems(0.875).into()),
2631 ..Default::default()
2632 }),
2633 }),
2634 code_block: StyleRefinement {
2635 padding: EdgesRefinement {
2636 top: Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(Pixels(8.)))),
2637 left: Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(Pixels(8.)))),
2638 right: Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(Pixels(8.)))),
2639 bottom: Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(Pixels(8.)))),
2640 },
2641 margin: EdgesRefinement {
2642 top: Some(Length::Definite(Pixels(8.).into())),
2643 left: Some(Length::Definite(Pixels(0.).into())),
2644 right: Some(Length::Definite(Pixels(0.).into())),
2645 bottom: Some(Length::Definite(Pixels(12.).into())),
2646 },
2647 border_style: Some(BorderStyle::Solid),
2648 border_widths: EdgesRefinement {
2649 top: Some(AbsoluteLength::Pixels(Pixels(1.))),
2650 left: Some(AbsoluteLength::Pixels(Pixels(1.))),
2651 right: Some(AbsoluteLength::Pixels(Pixels(1.))),
2652 bottom: Some(AbsoluteLength::Pixels(Pixels(1.))),
2653 },
2654 border_color: Some(colors.border_variant),
2655 background: Some(colors.editor_background.into()),
2656 text: Some(TextStyleRefinement {
2657 font_family: Some(theme_settings.buffer_font.family.clone()),
2658 font_fallbacks: theme_settings.buffer_font.fallbacks.clone(),
2659 font_features: Some(theme_settings.buffer_font.features.clone()),
2660 font_size: Some(buffer_font_size.into()),
2661 ..Default::default()
2662 }),
2663 ..Default::default()
2664 },
2665 inline_code: TextStyleRefinement {
2666 font_family: Some(theme_settings.buffer_font.family.clone()),
2667 font_fallbacks: theme_settings.buffer_font.fallbacks.clone(),
2668 font_features: Some(theme_settings.buffer_font.features.clone()),
2669 font_size: Some(buffer_font_size.into()),
2670 background_color: Some(colors.editor_foreground.opacity(0.08)),
2671 ..Default::default()
2672 },
2673 link: TextStyleRefinement {
2674 background_color: Some(colors.editor_foreground.opacity(0.025)),
2675 underline: Some(UnderlineStyle {
2676 color: Some(colors.text_accent.opacity(0.5)),
2677 thickness: px(1.),
2678 ..Default::default()
2679 }),
2680 ..Default::default()
2681 },
2682 ..Default::default()
2683 }
2684}
2685
2686fn plan_label_markdown_style(
2687 status: &acp::PlanEntryStatus,
2688 window: &Window,
2689 cx: &App,
2690) -> MarkdownStyle {
2691 let default_md_style = default_markdown_style(false, window, cx);
2692
2693 MarkdownStyle {
2694 base_text_style: TextStyle {
2695 color: cx.theme().colors().text_muted,
2696 strikethrough: if matches!(status, acp::PlanEntryStatus::Completed) {
2697 Some(gpui::StrikethroughStyle {
2698 thickness: px(1.),
2699 color: Some(cx.theme().colors().text_muted.opacity(0.8)),
2700 })
2701 } else {
2702 None
2703 },
2704 ..default_md_style.base_text_style
2705 },
2706 ..default_md_style
2707 }
2708}
2709
2710#[cfg(test)]
2711mod tests {
2712 use agent_client_protocol::SessionId;
2713 use editor::EditorSettings;
2714 use fs::FakeFs;
2715 use futures::future::try_join_all;
2716 use gpui::{SemanticVersion, TestAppContext, VisualTestContext};
2717 use rand::Rng;
2718 use settings::SettingsStore;
2719
2720 use super::*;
2721
2722 #[gpui::test]
2723 async fn test_notification_for_stop_event(cx: &mut TestAppContext) {
2724 init_test(cx);
2725
2726 let (thread_view, cx) = setup_thread_view(StubAgentServer::default(), cx).await;
2727
2728 let message_editor = cx.read(|cx| thread_view.read(cx).message_editor.clone());
2729 message_editor.update_in(cx, |editor, window, cx| {
2730 editor.set_text("Hello", window, cx);
2731 });
2732
2733 cx.deactivate_window();
2734
2735 thread_view.update_in(cx, |thread_view, window, cx| {
2736 thread_view.chat(&Chat, window, cx);
2737 });
2738
2739 cx.run_until_parked();
2740
2741 assert!(
2742 cx.windows()
2743 .iter()
2744 .any(|window| window.downcast::<AgentNotification>().is_some())
2745 );
2746 }
2747
2748 #[gpui::test]
2749 async fn test_notification_for_error(cx: &mut TestAppContext) {
2750 init_test(cx);
2751
2752 let (thread_view, cx) =
2753 setup_thread_view(StubAgentServer::new(SaboteurAgentConnection), cx).await;
2754
2755 let message_editor = cx.read(|cx| thread_view.read(cx).message_editor.clone());
2756 message_editor.update_in(cx, |editor, window, cx| {
2757 editor.set_text("Hello", window, cx);
2758 });
2759
2760 cx.deactivate_window();
2761
2762 thread_view.update_in(cx, |thread_view, window, cx| {
2763 thread_view.chat(&Chat, window, cx);
2764 });
2765
2766 cx.run_until_parked();
2767
2768 assert!(
2769 cx.windows()
2770 .iter()
2771 .any(|window| window.downcast::<AgentNotification>().is_some())
2772 );
2773 }
2774
2775 #[gpui::test]
2776 async fn test_notification_for_tool_authorization(cx: &mut TestAppContext) {
2777 init_test(cx);
2778
2779 let tool_call_id = acp::ToolCallId("1".into());
2780 let tool_call = acp::ToolCall {
2781 id: tool_call_id.clone(),
2782 title: "Label".into(),
2783 kind: acp::ToolKind::Edit,
2784 status: acp::ToolCallStatus::Pending,
2785 content: vec!["hi".into()],
2786 locations: vec![],
2787 raw_input: None,
2788 };
2789 let connection = StubAgentConnection::new(vec![acp::SessionUpdate::ToolCall(tool_call)])
2790 .with_permission_requests(HashMap::from_iter([(
2791 tool_call_id,
2792 vec![acp::PermissionOption {
2793 id: acp::PermissionOptionId("1".into()),
2794 name: "Allow".into(),
2795 kind: acp::PermissionOptionKind::AllowOnce,
2796 }],
2797 )]));
2798 let (thread_view, cx) = setup_thread_view(StubAgentServer::new(connection), cx).await;
2799
2800 let message_editor = cx.read(|cx| thread_view.read(cx).message_editor.clone());
2801 message_editor.update_in(cx, |editor, window, cx| {
2802 editor.set_text("Hello", window, cx);
2803 });
2804
2805 cx.deactivate_window();
2806
2807 thread_view.update_in(cx, |thread_view, window, cx| {
2808 thread_view.chat(&Chat, window, cx);
2809 });
2810
2811 cx.run_until_parked();
2812
2813 assert!(
2814 cx.windows()
2815 .iter()
2816 .any(|window| window.downcast::<AgentNotification>().is_some())
2817 );
2818 }
2819
2820 async fn setup_thread_view(
2821 agent: impl AgentServer + 'static,
2822 cx: &mut TestAppContext,
2823 ) -> (Entity<AcpThreadView>, &mut VisualTestContext) {
2824 let fs = FakeFs::new(cx.executor());
2825 let project = Project::test(fs, [], cx).await;
2826 let (workspace, cx) =
2827 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
2828
2829 let thread_view = cx.update(|window, cx| {
2830 cx.new(|cx| {
2831 AcpThreadView::new(
2832 Rc::new(agent),
2833 workspace.downgrade(),
2834 project,
2835 Rc::new(RefCell::new(MessageHistory::default())),
2836 1,
2837 None,
2838 window,
2839 cx,
2840 )
2841 })
2842 });
2843 cx.run_until_parked();
2844 (thread_view, cx)
2845 }
2846
2847 struct StubAgentServer<C> {
2848 connection: C,
2849 }
2850
2851 impl<C> StubAgentServer<C> {
2852 fn new(connection: C) -> Self {
2853 Self { connection }
2854 }
2855 }
2856
2857 impl StubAgentServer<StubAgentConnection> {
2858 fn default() -> Self {
2859 Self::new(StubAgentConnection::default())
2860 }
2861 }
2862
2863 impl<C> AgentServer for StubAgentServer<C>
2864 where
2865 C: 'static + AgentConnection + Send + Clone,
2866 {
2867 fn logo(&self) -> ui::IconName {
2868 unimplemented!()
2869 }
2870
2871 fn name(&self) -> &'static str {
2872 unimplemented!()
2873 }
2874
2875 fn empty_state_headline(&self) -> &'static str {
2876 unimplemented!()
2877 }
2878
2879 fn empty_state_message(&self) -> &'static str {
2880 unimplemented!()
2881 }
2882
2883 fn connect(
2884 &self,
2885 _root_dir: &Path,
2886 _project: &Entity<Project>,
2887 _cx: &mut App,
2888 ) -> Task<gpui::Result<Rc<dyn AgentConnection>>> {
2889 Task::ready(Ok(Rc::new(self.connection.clone())))
2890 }
2891 }
2892
2893 #[derive(Clone, Default)]
2894 struct StubAgentConnection {
2895 sessions: Arc<Mutex<HashMap<acp::SessionId, WeakEntity<AcpThread>>>>,
2896 permission_requests: HashMap<acp::ToolCallId, Vec<acp::PermissionOption>>,
2897 updates: Vec<acp::SessionUpdate>,
2898 }
2899
2900 impl StubAgentConnection {
2901 fn new(updates: Vec<acp::SessionUpdate>) -> Self {
2902 Self {
2903 updates,
2904 permission_requests: HashMap::default(),
2905 sessions: Arc::default(),
2906 }
2907 }
2908
2909 fn with_permission_requests(
2910 mut self,
2911 permission_requests: HashMap<acp::ToolCallId, Vec<acp::PermissionOption>>,
2912 ) -> Self {
2913 self.permission_requests = permission_requests;
2914 self
2915 }
2916 }
2917
2918 impl AgentConnection for StubAgentConnection {
2919 fn auth_methods(&self) -> &[acp::AuthMethod] {
2920 &[]
2921 }
2922
2923 fn new_thread(
2924 self: Rc<Self>,
2925 project: Entity<Project>,
2926 _cwd: &Path,
2927 cx: &mut gpui::AsyncApp,
2928 ) -> Task<gpui::Result<Entity<AcpThread>>> {
2929 let session_id = SessionId(
2930 rand::thread_rng()
2931 .sample_iter(&rand::distributions::Alphanumeric)
2932 .take(7)
2933 .map(char::from)
2934 .collect::<String>()
2935 .into(),
2936 );
2937 let thread = cx
2938 .new(|cx| AcpThread::new("Test", self.clone(), project, session_id.clone(), cx))
2939 .unwrap();
2940 self.sessions.lock().insert(session_id, thread.downgrade());
2941 Task::ready(Ok(thread))
2942 }
2943
2944 fn authenticate(
2945 &self,
2946 _method_id: acp::AuthMethodId,
2947 _cx: &mut App,
2948 ) -> Task<gpui::Result<()>> {
2949 unimplemented!()
2950 }
2951
2952 fn prompt(&self, params: acp::PromptRequest, cx: &mut App) -> Task<gpui::Result<()>> {
2953 let sessions = self.sessions.lock();
2954 let thread = sessions.get(¶ms.session_id).unwrap();
2955 let mut tasks = vec![];
2956 for update in &self.updates {
2957 let thread = thread.clone();
2958 let update = update.clone();
2959 let permission_request = if let acp::SessionUpdate::ToolCall(tool_call) = &update
2960 && let Some(options) = self.permission_requests.get(&tool_call.id)
2961 {
2962 Some((tool_call.clone(), options.clone()))
2963 } else {
2964 None
2965 };
2966 let task = cx.spawn(async move |cx| {
2967 if let Some((tool_call, options)) = permission_request {
2968 let permission = thread.update(cx, |thread, cx| {
2969 thread.request_tool_call_permission(
2970 tool_call.clone(),
2971 options.clone(),
2972 cx,
2973 )
2974 })?;
2975 permission.await?;
2976 }
2977 thread.update(cx, |thread, cx| {
2978 thread.handle_session_update(update.clone(), cx).unwrap();
2979 })?;
2980 anyhow::Ok(())
2981 });
2982 tasks.push(task);
2983 }
2984 cx.spawn(async move |_| {
2985 try_join_all(tasks).await?;
2986 Ok(())
2987 })
2988 }
2989
2990 fn cancel(&self, _session_id: &acp::SessionId, _cx: &mut App) {
2991 unimplemented!()
2992 }
2993 }
2994
2995 #[derive(Clone)]
2996 struct SaboteurAgentConnection;
2997
2998 impl AgentConnection for SaboteurAgentConnection {
2999 fn new_thread(
3000 self: Rc<Self>,
3001 project: Entity<Project>,
3002 _cwd: &Path,
3003 cx: &mut gpui::AsyncApp,
3004 ) -> Task<gpui::Result<Entity<AcpThread>>> {
3005 Task::ready(Ok(cx
3006 .new(|cx| {
3007 AcpThread::new(
3008 "SaboteurAgentConnection",
3009 self,
3010 project,
3011 SessionId("test".into()),
3012 cx,
3013 )
3014 })
3015 .unwrap()))
3016 }
3017
3018 fn auth_methods(&self) -> &[acp::AuthMethod] {
3019 &[]
3020 }
3021
3022 fn authenticate(
3023 &self,
3024 _method_id: acp::AuthMethodId,
3025 _cx: &mut App,
3026 ) -> Task<gpui::Result<()>> {
3027 unimplemented!()
3028 }
3029
3030 fn prompt(&self, _params: acp::PromptRequest, _cx: &mut App) -> Task<gpui::Result<()>> {
3031 Task::ready(Err(anyhow::anyhow!("Error prompting")))
3032 }
3033
3034 fn cancel(&self, _session_id: &acp::SessionId, _cx: &mut App) {
3035 unimplemented!()
3036 }
3037 }
3038
3039 fn init_test(cx: &mut TestAppContext) {
3040 cx.update(|cx| {
3041 let settings_store = SettingsStore::test(cx);
3042 cx.set_global(settings_store);
3043 language::init(cx);
3044 Project::init_settings(cx);
3045 AgentSettings::register(cx);
3046 workspace::init_settings(cx);
3047 ThemeSettings::register(cx);
3048 release_channel::init(SemanticVersion::default(), cx);
3049 EditorSettings::register(cx);
3050 });
3051 }
3052}