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