1use std::{
2 collections::HashSet,
3 fmt::Display,
4 rc::{Rc, Weak},
5 sync::Arc,
6};
7
8use agent_client_protocol as acp;
9use agent_ui::{Agent, AgentConnectionStore, AgentPanel};
10use collections::HashMap;
11use gpui::{
12 App, Empty, Entity, EventEmitter, FocusHandle, Focusable, ListAlignment, ListState,
13 SharedString, StyleRefinement, Subscription, Task, TextStyleRefinement, WeakEntity, Window,
14 actions, list, prelude::*,
15};
16use language::LanguageRegistry;
17use markdown::{CodeBlockRenderer, CopyButtonVisibility, Markdown, MarkdownElement, MarkdownStyle};
18use project::{AgentId, Project};
19use settings::Settings;
20use theme_settings::ThemeSettings;
21use ui::{
22 ContextMenu, CopyButton, DropdownMenu, DropdownStyle, IconPosition, Tooltip, WithScrollbar,
23 prelude::*,
24};
25use util::ResultExt as _;
26use workspace::{
27 Item, ItemHandle, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, Workspace,
28};
29
30actions!(dev, [OpenAcpLogs]);
31
32pub fn init(cx: &mut App) {
33 cx.observe_new(
34 |workspace: &mut Workspace, _window, _cx: &mut Context<Workspace>| {
35 workspace.register_action(|workspace, _: &OpenAcpLogs, window, cx| {
36 let connection_store = workspace
37 .panel::<AgentPanel>(cx)
38 .map(|panel| panel.read(cx).connection_store().clone());
39 let acp_tools = Box::new(cx.new(|cx| {
40 AcpTools::new(
41 workspace.weak_handle(),
42 workspace.project().clone(),
43 connection_store.clone(),
44 cx,
45 )
46 }));
47 workspace.add_item_to_active_pane(acp_tools, None, true, window, cx);
48 });
49 },
50 )
51 .detach();
52}
53
54struct AcpTools {
55 workspace: WeakEntity<Workspace>,
56 project: Entity<Project>,
57 focus_handle: FocusHandle,
58 expanded: HashSet<usize>,
59 watched_connections: HashMap<AgentId, WatchedConnection>,
60 selected_connection: Option<AgentId>,
61 connection_store: Option<Entity<AgentConnectionStore>>,
62 _workspace_subscription: Option<Subscription>,
63 _connection_store_subscription: Option<Subscription>,
64}
65
66struct WatchedConnection {
67 agent_id: AgentId,
68 messages: Vec<WatchedConnectionMessage>,
69 list_state: ListState,
70 connection: Weak<acp::ClientSideConnection>,
71 incoming_request_methods: HashMap<acp::RequestId, Arc<str>>,
72 outgoing_request_methods: HashMap<acp::RequestId, Arc<str>>,
73 _task: Task<()>,
74}
75
76impl AcpTools {
77 fn new(
78 workspace: WeakEntity<Workspace>,
79 project: Entity<Project>,
80 connection_store: Option<Entity<AgentConnectionStore>>,
81 cx: &mut Context<Self>,
82 ) -> Self {
83 let workspace_subscription = workspace.upgrade().map(|workspace| {
84 cx.observe(&workspace, |this, _, cx| {
85 this.update_connection_store(cx);
86 })
87 });
88
89 let mut this = Self {
90 workspace,
91 project,
92 focus_handle: cx.focus_handle(),
93 expanded: HashSet::default(),
94 watched_connections: HashMap::default(),
95 selected_connection: None,
96 connection_store: None,
97 _workspace_subscription: workspace_subscription,
98 _connection_store_subscription: None,
99 };
100 this.set_connection_store(connection_store, cx);
101 this
102 }
103
104 fn set_connection_store(
105 &mut self,
106 connection_store: Option<Entity<AgentConnectionStore>>,
107 cx: &mut Context<Self>,
108 ) {
109 if self.connection_store == connection_store {
110 return;
111 }
112
113 self.connection_store = connection_store.clone();
114 self._connection_store_subscription = connection_store.as_ref().map(|connection_store| {
115 cx.observe(connection_store, |this, _, cx| {
116 this.refresh_connections(cx);
117 })
118 });
119 self.refresh_connections(cx);
120 }
121
122 fn update_connection_store(&mut self, cx: &mut Context<Self>) {
123 let connection_store = self.workspace.upgrade().and_then(|workspace| {
124 workspace
125 .read(cx)
126 .panel::<AgentPanel>(cx)
127 .map(|panel| panel.read(cx).connection_store().clone())
128 });
129
130 self.set_connection_store(connection_store, cx);
131 }
132
133 fn refresh_connections(&mut self, cx: &mut Context<Self>) {
134 let mut did_change = false;
135 let active_connections = self
136 .connection_store
137 .as_ref()
138 .map(|connection_store| connection_store.read(cx).active_acp_connections(cx))
139 .unwrap_or_default();
140
141 self.watched_connections
142 .retain(|agent_id, watched_connection| {
143 let should_retain = active_connections.iter().any(|active_connection| {
144 &active_connection.agent_id == agent_id
145 && Rc::ptr_eq(
146 &active_connection.connection,
147 &watched_connection
148 .connection
149 .upgrade()
150 .unwrap_or_else(|| active_connection.connection.clone()),
151 )
152 });
153
154 if !should_retain {
155 did_change = true;
156 }
157
158 should_retain
159 });
160
161 for active_connection in active_connections {
162 let should_create_watcher = self
163 .watched_connections
164 .get(&active_connection.agent_id)
165 .is_none_or(|watched_connection| {
166 watched_connection
167 .connection
168 .upgrade()
169 .is_none_or(|connection| {
170 !Rc::ptr_eq(&connection, &active_connection.connection)
171 })
172 });
173
174 if !should_create_watcher {
175 continue;
176 }
177
178 let agent_id = active_connection.agent_id.clone();
179 let connection = active_connection.connection;
180 let mut receiver = connection.subscribe();
181 let task = cx.spawn({
182 let agent_id = agent_id.clone();
183 async move |this, cx| {
184 while let Ok(message) = receiver.recv().await {
185 this.update(cx, |this, cx| {
186 this.push_stream_message(&agent_id, message, cx);
187 })
188 .ok();
189 }
190 }
191 });
192
193 self.watched_connections.insert(
194 agent_id.clone(),
195 WatchedConnection {
196 agent_id,
197 messages: vec![],
198 list_state: ListState::new(0, ListAlignment::Bottom, px(2048.)),
199 connection: Rc::downgrade(&connection),
200 incoming_request_methods: HashMap::default(),
201 outgoing_request_methods: HashMap::default(),
202 _task: task,
203 },
204 );
205 did_change = true;
206 }
207
208 let previous_selected_connection = self.selected_connection.clone();
209 self.selected_connection = self
210 .selected_connection
211 .clone()
212 .filter(|agent_id| self.watched_connections.contains_key(agent_id))
213 .or_else(|| self.watched_connections.keys().next().cloned());
214
215 if self.selected_connection != previous_selected_connection {
216 self.expanded.clear();
217 did_change = true;
218 }
219
220 if did_change {
221 cx.notify();
222 }
223 }
224
225 fn select_connection(&mut self, agent_id: Option<AgentId>, cx: &mut Context<Self>) {
226 if self.selected_connection == agent_id {
227 return;
228 }
229
230 self.selected_connection = agent_id;
231 self.expanded.clear();
232 cx.notify();
233 }
234
235 fn restart_selected_connection(&mut self, cx: &mut Context<Self>) {
236 let Some(agent_id) = self.selected_connection.clone() else {
237 return;
238 };
239 let Some(workspace) = self.workspace.upgrade() else {
240 return;
241 };
242
243 workspace.update(cx, |workspace, cx| {
244 let Some(panel) = workspace.panel::<AgentPanel>(cx) else {
245 return;
246 };
247
248 let fs = workspace.app_state().fs.clone();
249 let (thread_store, connection_store) = {
250 let panel = panel.read(cx);
251 (
252 panel.thread_store().clone(),
253 panel.connection_store().clone(),
254 )
255 };
256 let agent = Agent::from(agent_id.clone());
257 let server = agent.server(fs, thread_store);
258
259 connection_store.update(cx, |store, cx| {
260 store.restart_connection(agent, server, cx);
261 });
262 });
263 }
264
265 fn selected_connection_status(
266 &self,
267 cx: &App,
268 ) -> Option<agent_ui::agent_connection_store::AgentConnectionStatus> {
269 let agent_id = self.selected_connection.clone()?;
270 let connection_store = self.connection_store.as_ref()?;
271 let agent = Agent::from(agent_id);
272 Some(connection_store.read(cx).connection_status(&agent, cx))
273 }
274
275 fn selected_watched_connection(&self) -> Option<&WatchedConnection> {
276 let selected_connection = self.selected_connection.as_ref()?;
277 self.watched_connections.get(selected_connection)
278 }
279
280 fn selected_watched_connection_mut(&mut self) -> Option<&mut WatchedConnection> {
281 let selected_connection = self.selected_connection.clone()?;
282 self.watched_connections.get_mut(&selected_connection)
283 }
284
285 fn connection_menu_entries(&self) -> Vec<SharedString> {
286 let mut entries: Vec<_> = self
287 .watched_connections
288 .values()
289 .map(|connection| connection.agent_id.0.clone())
290 .collect();
291 entries.sort();
292 entries
293 }
294
295 fn push_stream_message(
296 &mut self,
297 agent_id: &AgentId,
298 stream_message: acp::StreamMessage,
299 cx: &mut Context<Self>,
300 ) {
301 let Some(connection) = self.watched_connections.get_mut(agent_id) else {
302 return;
303 };
304 let language_registry = self.project.read(cx).languages().clone();
305 let index = connection.messages.len();
306
307 let (request_id, method, message_type, params) = match stream_message.message {
308 acp::StreamMessageContent::Request { id, method, params } => {
309 let method_map = match stream_message.direction {
310 acp::StreamMessageDirection::Incoming => {
311 &mut connection.incoming_request_methods
312 }
313 acp::StreamMessageDirection::Outgoing => {
314 &mut connection.outgoing_request_methods
315 }
316 };
317
318 method_map.insert(id.clone(), method.clone());
319 (Some(id), method.into(), MessageType::Request, Ok(params))
320 }
321 acp::StreamMessageContent::Response { id, result } => {
322 let method_map = match stream_message.direction {
323 acp::StreamMessageDirection::Incoming => {
324 &mut connection.outgoing_request_methods
325 }
326 acp::StreamMessageDirection::Outgoing => {
327 &mut connection.incoming_request_methods
328 }
329 };
330
331 if let Some(method) = method_map.remove(&id) {
332 (Some(id), method.into(), MessageType::Response, result)
333 } else {
334 (
335 Some(id),
336 "[unrecognized response]".into(),
337 MessageType::Response,
338 result,
339 )
340 }
341 }
342 acp::StreamMessageContent::Notification { method, params } => {
343 (None, method.into(), MessageType::Notification, Ok(params))
344 }
345 };
346
347 let message = WatchedConnectionMessage {
348 name: method,
349 message_type,
350 request_id,
351 direction: stream_message.direction,
352 collapsed_params_md: match params.as_ref() {
353 Ok(params) => params
354 .as_ref()
355 .map(|params| collapsed_params_md(params, &language_registry, cx)),
356 Err(err) => {
357 if let Ok(err) = &serde_json::to_value(err) {
358 Some(collapsed_params_md(&err, &language_registry, cx))
359 } else {
360 None
361 }
362 }
363 },
364
365 expanded_params_md: None,
366 params,
367 };
368
369 connection.messages.push(message);
370 connection.list_state.splice(index..index, 1);
371 cx.notify();
372 }
373
374 fn serialize_observed_messages(&self) -> Option<String> {
375 let connection = self.selected_watched_connection()?;
376
377 let messages: Vec<serde_json::Value> = connection
378 .messages
379 .iter()
380 .filter_map(|message| {
381 let params = match &message.params {
382 Ok(Some(params)) => params.clone(),
383 Ok(None) => serde_json::Value::Null,
384 Err(err) => serde_json::to_value(err).ok()?,
385 };
386 Some(serde_json::json!({
387 "_direction": match message.direction {
388 acp::StreamMessageDirection::Incoming => "incoming",
389 acp::StreamMessageDirection::Outgoing => "outgoing",
390 },
391 "_type": message.message_type.to_string().to_lowercase(),
392 "id": message.request_id,
393 "method": message.name.to_string(),
394 "params": params,
395 }))
396 })
397 .collect();
398
399 serde_json::to_string_pretty(&messages).ok()
400 }
401
402 fn clear_messages(&mut self, cx: &mut Context<Self>) {
403 if let Some(connection) = self.selected_watched_connection_mut() {
404 connection.messages.clear();
405 connection.list_state.reset(0);
406 self.expanded.clear();
407 cx.notify();
408 }
409 }
410
411 fn selected_connection_label(&self) -> SharedString {
412 self.selected_connection
413 .as_ref()
414 .map(|agent_id| agent_id.0.clone())
415 .unwrap_or_else(|| SharedString::from("No connection selected"))
416 }
417
418 fn connection_menu(&self, window: &mut Window, cx: &mut Context<Self>) -> Entity<ContextMenu> {
419 let entries = self.connection_menu_entries();
420 let selected_connection = self.selected_connection.clone();
421 let acp_tools = cx.entity().downgrade();
422
423 ContextMenu::build(window, cx, move |mut menu, _window, _cx| {
424 if entries.is_empty() {
425 return menu.entry("No active connections", None, |_, _| {});
426 }
427
428 for entry in &entries {
429 let label = entry.clone();
430 let selected = selected_connection
431 .as_ref()
432 .is_some_and(|agent_id| agent_id.0.as_ref() == label.as_ref());
433 let weak_acp_tools = acp_tools.clone();
434 menu = menu.toggleable_entry(
435 label.clone(),
436 selected,
437 IconPosition::Start,
438 None,
439 move |_window, cx| {
440 weak_acp_tools
441 .update(cx, |this, cx| {
442 this.select_connection(Some(AgentId(label.clone())), cx);
443 })
444 .ok();
445 },
446 );
447 }
448
449 menu
450 })
451 }
452
453 fn render_message(
454 &mut self,
455 index: usize,
456 window: &mut Window,
457 cx: &mut Context<Self>,
458 ) -> AnyElement {
459 let Some(connection) = self.selected_watched_connection() else {
460 return Empty.into_any();
461 };
462
463 let Some(message) = connection.messages.get(index) else {
464 return Empty.into_any();
465 };
466
467 let base_size = TextSize::Editor.rems(cx);
468
469 let theme_settings = ThemeSettings::get_global(cx);
470 let text_style = window.text_style();
471
472 let colors = cx.theme().colors();
473 let expanded = self.expanded.contains(&index);
474
475 v_flex()
476 .id(index)
477 .group("message")
478 .font_buffer(cx)
479 .w_full()
480 .py_3()
481 .pl_4()
482 .pr_5()
483 .gap_2()
484 .items_start()
485 .text_size(base_size)
486 .border_color(colors.border)
487 .border_b_1()
488 .hover(|this| this.bg(colors.element_background.opacity(0.5)))
489 .child(
490 h_flex()
491 .id(("acp-log-message-header", index))
492 .w_full()
493 .gap_2()
494 .flex_shrink_0()
495 .cursor_pointer()
496 .on_click(cx.listener(move |this, _, _, cx| {
497 if this.expanded.contains(&index) {
498 this.expanded.remove(&index);
499 } else {
500 this.expanded.insert(index);
501 let project = this.project.clone();
502 let Some(connection) = this.selected_watched_connection_mut() else {
503 return;
504 };
505 let Some(message) = connection.messages.get_mut(index) else {
506 return;
507 };
508 message.expanded(project.read(cx).languages().clone(), cx);
509 connection.list_state.scroll_to_reveal_item(index);
510 }
511 cx.notify()
512 }))
513 .child(match message.direction {
514 acp::StreamMessageDirection::Incoming => Icon::new(IconName::ArrowDown)
515 .color(Color::Error)
516 .size(IconSize::Small),
517 acp::StreamMessageDirection::Outgoing => Icon::new(IconName::ArrowUp)
518 .color(Color::Success)
519 .size(IconSize::Small),
520 })
521 .child(
522 Label::new(message.name.clone())
523 .buffer_font(cx)
524 .color(Color::Muted),
525 )
526 .child(div().flex_1())
527 .child(
528 div()
529 .child(ui::Chip::new(message.message_type.to_string()))
530 .visible_on_hover("message"),
531 )
532 .children(
533 message
534 .request_id
535 .as_ref()
536 .map(|req_id| div().child(ui::Chip::new(req_id.to_string()))),
537 ),
538 )
539 // I'm aware using markdown is a hack. Trying to get something working for the demo.
540 // Will clean up soon!
541 .when_some(
542 if expanded {
543 message.expanded_params_md.clone()
544 } else {
545 message.collapsed_params_md.clone()
546 },
547 |this, params| {
548 this.child(
549 div().pl_6().w_full().child(
550 MarkdownElement::new(
551 params,
552 MarkdownStyle {
553 base_text_style: text_style,
554 selection_background_color: colors.element_selection_background,
555 syntax: cx.theme().syntax().clone(),
556 code_block_overflow_x_scroll: true,
557 code_block: StyleRefinement {
558 text: TextStyleRefinement {
559 font_family: Some(
560 theme_settings.buffer_font.family.clone(),
561 ),
562 font_size: Some((base_size * 0.8).into()),
563 ..Default::default()
564 },
565 ..Default::default()
566 },
567 ..Default::default()
568 },
569 )
570 .code_block_renderer(
571 CodeBlockRenderer::Default {
572 copy_button_visibility: if expanded {
573 CopyButtonVisibility::VisibleOnHover
574 } else {
575 CopyButtonVisibility::Hidden
576 },
577 border: false,
578 },
579 ),
580 ),
581 )
582 },
583 )
584 .into_any()
585 }
586}
587
588struct WatchedConnectionMessage {
589 name: SharedString,
590 request_id: Option<acp::RequestId>,
591 direction: acp::StreamMessageDirection,
592 message_type: MessageType,
593 params: Result<Option<serde_json::Value>, acp::Error>,
594 collapsed_params_md: Option<Entity<Markdown>>,
595 expanded_params_md: Option<Entity<Markdown>>,
596}
597
598impl WatchedConnectionMessage {
599 fn expanded(&mut self, language_registry: Arc<LanguageRegistry>, cx: &mut App) {
600 let params_md = match &self.params {
601 Ok(Some(params)) => Some(expanded_params_md(params, &language_registry, cx)),
602 Err(err) => {
603 if let Some(err) = &serde_json::to_value(err).log_err() {
604 Some(expanded_params_md(&err, &language_registry, cx))
605 } else {
606 None
607 }
608 }
609 _ => None,
610 };
611 self.expanded_params_md = params_md;
612 }
613}
614
615fn collapsed_params_md(
616 params: &serde_json::Value,
617 language_registry: &Arc<LanguageRegistry>,
618 cx: &mut App,
619) -> Entity<Markdown> {
620 let params_json = serde_json::to_string(params).unwrap_or_default();
621 let mut spaced_out_json = String::with_capacity(params_json.len() + params_json.len() / 4);
622
623 for ch in params_json.chars() {
624 match ch {
625 '{' => spaced_out_json.push_str("{ "),
626 '}' => spaced_out_json.push_str(" }"),
627 ':' => spaced_out_json.push_str(": "),
628 ',' => spaced_out_json.push_str(", "),
629 c => spaced_out_json.push(c),
630 }
631 }
632
633 let params_md = format!("```json\n{}\n```", spaced_out_json);
634 cx.new(|cx| Markdown::new(params_md.into(), Some(language_registry.clone()), None, cx))
635}
636
637fn expanded_params_md(
638 params: &serde_json::Value,
639 language_registry: &Arc<LanguageRegistry>,
640 cx: &mut App,
641) -> Entity<Markdown> {
642 let params_json = serde_json::to_string_pretty(params).unwrap_or_default();
643 let params_md = format!("```json\n{}\n```", params_json);
644 cx.new(|cx| Markdown::new(params_md.into(), Some(language_registry.clone()), None, cx))
645}
646
647enum MessageType {
648 Request,
649 Response,
650 Notification,
651}
652
653impl Display for MessageType {
654 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
655 match self {
656 MessageType::Request => write!(f, "Request"),
657 MessageType::Response => write!(f, "Response"),
658 MessageType::Notification => write!(f, "Notification"),
659 }
660 }
661}
662
663enum AcpToolsEvent {}
664
665impl EventEmitter<AcpToolsEvent> for AcpTools {}
666
667impl Item for AcpTools {
668 type Event = AcpToolsEvent;
669
670 fn tab_content_text(&self, _detail: usize, _cx: &App) -> ui::SharedString {
671 format!(
672 "ACP: {}",
673 self.selected_watched_connection()
674 .map_or("Disconnected", |connection| connection.agent_id.0.as_ref())
675 )
676 .into()
677 }
678
679 fn tab_icon(&self, _window: &Window, _cx: &App) -> Option<Icon> {
680 Some(ui::Icon::new(IconName::Thread))
681 }
682}
683
684impl Focusable for AcpTools {
685 fn focus_handle(&self, _cx: &App) -> FocusHandle {
686 self.focus_handle.clone()
687 }
688}
689
690impl Render for AcpTools {
691 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
692 v_flex()
693 .track_focus(&self.focus_handle)
694 .size_full()
695 .bg(cx.theme().colors().editor_background)
696 .child(
697 h_flex()
698 .px_3()
699 .py_2()
700 .border_b_1()
701 .border_color(cx.theme().colors().border)
702 .child(
703 DropdownMenu::new(
704 "acp-connection-selector",
705 self.selected_connection_label(),
706 self.connection_menu(window, cx),
707 )
708 .style(DropdownStyle::Subtle)
709 .disabled(self.watched_connections.is_empty()),
710 ),
711 )
712 .child(match self.selected_watched_connection() {
713 Some(connection) => {
714 if connection.messages.is_empty() {
715 h_flex()
716 .size_full()
717 .justify_center()
718 .items_center()
719 .child("No messages recorded yet")
720 .into_any()
721 } else {
722 div()
723 .size_full()
724 .flex_grow()
725 .child(
726 list(
727 connection.list_state.clone(),
728 cx.processor(Self::render_message),
729 )
730 .with_sizing_behavior(gpui::ListSizingBehavior::Auto)
731 .size_full(),
732 )
733 .vertical_scrollbar_for(&connection.list_state, window, cx)
734 .into_any()
735 }
736 }
737 None => h_flex()
738 .size_full()
739 .justify_center()
740 .items_center()
741 .child("No active connection")
742 .into_any(),
743 })
744 }
745}
746
747pub struct AcpToolsToolbarItemView {
748 acp_tools: Option<Entity<AcpTools>>,
749}
750
751impl AcpToolsToolbarItemView {
752 pub fn new() -> Self {
753 Self { acp_tools: None }
754 }
755}
756
757impl Render for AcpToolsToolbarItemView {
758 fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
759 let Some(acp_tools) = self.acp_tools.as_ref() else {
760 return Empty.into_any_element();
761 };
762
763 let acp_tools = acp_tools.clone();
764 let (has_messages, can_restart) = {
765 let acp_tools = acp_tools.read(cx);
766 (
767 acp_tools
768 .selected_watched_connection()
769 .is_some_and(|connection| !connection.messages.is_empty()),
770 acp_tools.selected_connection_status(cx)
771 != Some(agent_ui::agent_connection_store::AgentConnectionStatus::Connecting),
772 )
773 };
774
775 h_flex()
776 .gap_2()
777 .child(
778 IconButton::new("restart_connection", IconName::RotateCw)
779 .icon_size(IconSize::Small)
780 .tooltip(Tooltip::text("Restart Connection"))
781 .disabled(!can_restart)
782 .on_click(cx.listener({
783 let acp_tools = acp_tools.clone();
784 move |_this, _, _window, cx| {
785 acp_tools.update(cx, |acp_tools, cx| {
786 acp_tools.restart_selected_connection(cx);
787 });
788 }
789 })),
790 )
791 .child({
792 let message = acp_tools
793 .read(cx)
794 .serialize_observed_messages()
795 .unwrap_or_default();
796
797 CopyButton::new("copy-all-messages", message)
798 .tooltip_label("Copy All Messages")
799 .disabled(!has_messages)
800 })
801 .child(
802 IconButton::new("clear_messages", IconName::Trash)
803 .icon_size(IconSize::Small)
804 .tooltip(Tooltip::text("Clear Messages"))
805 .disabled(!has_messages)
806 .on_click(cx.listener(move |_this, _, _window, cx| {
807 acp_tools.update(cx, |acp_tools, cx| {
808 acp_tools.clear_messages(cx);
809 });
810 })),
811 )
812 .into_any()
813 }
814}
815
816impl EventEmitter<ToolbarItemEvent> for AcpToolsToolbarItemView {}
817
818impl ToolbarItemView for AcpToolsToolbarItemView {
819 fn set_active_pane_item(
820 &mut self,
821 active_pane_item: Option<&dyn ItemHandle>,
822 _window: &mut Window,
823 cx: &mut Context<Self>,
824 ) -> ToolbarItemLocation {
825 if let Some(item) = active_pane_item
826 && let Some(acp_tools) = item.downcast::<AcpTools>()
827 {
828 self.acp_tools = Some(acp_tools);
829 cx.notify();
830 return ToolbarItemLocation::PrimaryRight;
831 }
832 if self.acp_tools.take().is_some() {
833 cx.notify();
834 }
835 ToolbarItemLocation::Hidden
836 }
837}