acp_tools.rs

  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}