acp_tools.rs

  1use std::{
  2    collections::{HashSet, VecDeque},
  3    fmt::Display,
  4    sync::Arc,
  5};
  6
  7use agent_client_protocol::schema as acp;
  8use collections::HashMap;
  9use gpui::{
 10    App, Empty, Entity, EventEmitter, FocusHandle, Focusable, Global, ListAlignment, ListState,
 11    StyleRefinement, Subscription, Task, TextStyleRefinement, Window, actions, list, prelude::*,
 12};
 13use language::LanguageRegistry;
 14use markdown::{CodeBlockRenderer, CopyButtonVisibility, Markdown, MarkdownElement, MarkdownStyle};
 15use project::{AgentId, Project};
 16use settings::Settings;
 17use theme_settings::ThemeSettings;
 18use ui::{CopyButton, Tooltip, WithScrollbar, prelude::*};
 19use util::ResultExt as _;
 20use workspace::{
 21    Item, ItemHandle, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, Workspace,
 22};
 23
 24#[derive(Clone, Copy, PartialEq, Eq)]
 25pub enum StreamMessageDirection {
 26    Incoming,
 27    Outgoing,
 28    /// Lines captured from the agent's stderr. These are not part of the
 29    /// JSON-RPC protocol, but agents often emit useful diagnostics there.
 30    Stderr,
 31}
 32
 33#[derive(Clone)]
 34pub enum StreamMessageContent {
 35    Request {
 36        id: acp::RequestId,
 37        method: Arc<str>,
 38        params: Option<serde_json::Value>,
 39    },
 40    Response {
 41        id: acp::RequestId,
 42        result: Result<Option<serde_json::Value>, acp::Error>,
 43    },
 44    Notification {
 45        method: Arc<str>,
 46        params: Option<serde_json::Value>,
 47    },
 48    /// A raw stderr line from the agent process.
 49    Stderr { line: Arc<str> },
 50}
 51
 52#[derive(Clone)]
 53pub struct StreamMessage {
 54    pub direction: StreamMessageDirection,
 55    pub message: StreamMessageContent,
 56}
 57
 58impl StreamMessage {
 59    /// Build a `StreamMessage` from a raw line captured off the transport.
 60    ///
 61    /// For `Stderr`, the line is wrapped as-is (no JSON parsing). For
 62    /// `Incoming`/`Outgoing`, the line is parsed as JSON-RPC; returns `None`
 63    /// if it doesn't look like a valid JSON-RPC message.
 64    pub fn from_raw_line(direction: StreamMessageDirection, line: &str) -> Option<Self> {
 65        if direction == StreamMessageDirection::Stderr {
 66            return Some(StreamMessage {
 67                direction,
 68                message: StreamMessageContent::Stderr {
 69                    line: Arc::from(line),
 70                },
 71            });
 72        }
 73
 74        let value: serde_json::Value = serde_json::from_str(line).ok()?;
 75        let obj = value.as_object()?;
 76
 77        let parsed_id = obj
 78            .get("id")
 79            .map(|raw| serde_json::from_value::<acp::RequestId>(raw.clone()));
 80
 81        let message = if let Some(method) = obj.get("method").and_then(|m| m.as_str()) {
 82            match parsed_id {
 83                Some(Ok(id)) => StreamMessageContent::Request {
 84                    id,
 85                    method: method.into(),
 86                    params: obj.get("params").cloned(),
 87                },
 88                Some(Err(err)) => {
 89                    log::warn!("Skipping JSON-RPC message with unparsable id: {err}");
 90                    return None;
 91                }
 92                None => StreamMessageContent::Notification {
 93                    method: method.into(),
 94                    params: obj.get("params").cloned(),
 95                },
 96            }
 97        } else if let Some(parsed_id) = parsed_id {
 98            let id = match parsed_id {
 99                Ok(id) => id,
100                Err(err) => {
101                    log::warn!("Skipping JSON-RPC response with unparsable id: {err}");
102                    return None;
103                }
104            };
105            if let Some(error) = obj.get("error") {
106                let acp_err =
107                    serde_json::from_value::<acp::Error>(error.clone()).unwrap_or_else(|err| {
108                        log::warn!("Failed to deserialize ACP error: {err}");
109                        acp::Error::internal_error().data(error.to_string())
110                    });
111                StreamMessageContent::Response {
112                    id,
113                    result: Err(acp_err),
114                }
115            } else {
116                StreamMessageContent::Response {
117                    id,
118                    result: Ok(obj.get("result").cloned()),
119                }
120            }
121        } else {
122            return None;
123        };
124
125        Some(StreamMessage { direction, message })
126    }
127}
128
129actions!(dev, [OpenAcpLogs]);
130
131pub fn init(cx: &mut App) {
132    cx.observe_new(
133        |workspace: &mut Workspace, _window, _cx: &mut Context<Workspace>| {
134            workspace.register_action(|workspace, _: &OpenAcpLogs, window, cx| {
135                let acp_tools =
136                    Box::new(cx.new(|cx| AcpTools::new(workspace.project().clone(), cx)));
137                workspace.add_item_to_active_pane(acp_tools, None, true, window, cx);
138            });
139        },
140    )
141    .detach();
142}
143
144struct GlobalAcpConnectionRegistry(Entity<AcpConnectionRegistry>);
145
146impl Global for GlobalAcpConnectionRegistry {}
147
148/// A raw line captured from the transport (or from stderr), tagged with
149/// direction. Deserialization into [`StreamMessage`] happens on the
150/// registry's foreground task so the ring buffer can be replayed to late
151/// subscribers.
152struct RawStreamLine {
153    direction: StreamMessageDirection,
154    line: Arc<str>,
155}
156
157/// Handle to an ACP connection's log tap. Passed back by
158/// [`AcpConnectionRegistry::set_active_connection`] so that the connection
159/// can publish transport and stderr lines without knowing anything about
160/// the logs panel's channel.
161///
162/// Every line is buffered into the registry's ring, so opening the ACP logs
163/// panel after the fact still shows history. The steady-state cost is
164/// negligible compared to the JSON-RPC serialization that already happened
165/// to produce the line.
166#[derive(Clone)]
167pub struct AcpLogTap {
168    sender: smol::channel::Sender<RawStreamLine>,
169}
170
171impl AcpLogTap {
172    fn emit(&self, direction: StreamMessageDirection, line: &str) {
173        self.sender
174            .try_send(RawStreamLine {
175                direction,
176                line: Arc::from(line),
177            })
178            .log_err();
179    }
180
181    /// Record a line read from the agent's stdout.
182    pub fn emit_incoming(&self, line: &str) {
183        self.emit(StreamMessageDirection::Incoming, line);
184    }
185
186    /// Record a line written to the agent's stdin.
187    pub fn emit_outgoing(&self, line: &str) {
188        self.emit(StreamMessageDirection::Outgoing, line);
189    }
190
191    /// Record a line read from the agent's stderr.
192    pub fn emit_stderr(&self, line: &str) {
193        self.emit(StreamMessageDirection::Stderr, line);
194    }
195}
196
197/// Maximum number of messages retained in the registry's backlog.
198///
199/// Mirrors `MAX_STORED_LOG_ENTRIES` in the LSP log store, so that opening the
200/// ACP logs panel after a session has been running for a while still shows
201/// meaningful history.
202const MAX_BACKLOG_MESSAGES: usize = 2000;
203
204#[derive(Default)]
205pub struct AcpConnectionRegistry {
206    active_agent_id: Option<AgentId>,
207    generation: u64,
208    /// Bounded ring buffer of every message observed on the current connection.
209    /// When a new connection is set, this is cleared.
210    backlog: VecDeque<StreamMessage>,
211    subscribers: Vec<smol::channel::Sender<StreamMessage>>,
212    _broadcast_task: Option<Task<()>>,
213}
214
215impl AcpConnectionRegistry {
216    pub fn default_global(cx: &mut App) -> Entity<Self> {
217        if cx.has_global::<GlobalAcpConnectionRegistry>() {
218            cx.global::<GlobalAcpConnectionRegistry>().0.clone()
219        } else {
220            let registry = cx.new(|_cx| AcpConnectionRegistry::default());
221            cx.set_global(GlobalAcpConnectionRegistry(registry.clone()));
222            registry
223        }
224    }
225
226    /// Register a new active connection and return an [`AcpLogTap`] that
227    /// the connection should hand to its transport + stderr readers.
228    ///
229    /// The tap begins capturing immediately so that opening the ACP logs
230    /// panel after something has already gone wrong still shows the
231    /// leading history (up to [`MAX_BACKLOG_MESSAGES`]).
232    pub fn set_active_connection(
233        &mut self,
234        agent_id: AgentId,
235        cx: &mut Context<Self>,
236    ) -> AcpLogTap {
237        let (sender, raw_rx) = smol::channel::unbounded::<RawStreamLine>();
238        let tap = AcpLogTap { sender };
239
240        self.active_agent_id = Some(agent_id);
241        self.generation += 1;
242        self.backlog.clear();
243        self.subscribers.clear();
244
245        self._broadcast_task = Some(cx.spawn(async move |this, cx| {
246            while let Ok(raw) = raw_rx.recv().await {
247                this.update(cx, |this, _cx| {
248                    let Some(message) = StreamMessage::from_raw_line(raw.direction, &raw.line)
249                    else {
250                        return;
251                    };
252
253                    if this.backlog.len() == MAX_BACKLOG_MESSAGES {
254                        this.backlog.pop_front();
255                    }
256                    this.backlog.push_back(message.clone());
257
258                    this.subscribers.retain(|sender| !sender.is_closed());
259                    for sender in &this.subscribers {
260                        sender.try_send(message.clone()).log_err();
261                    }
262                })
263                .log_err();
264            }
265
266            // The transport closed — clear state so observers (e.g. the ACP
267            // logs tab) can transition back to the disconnected state.
268            this.update(cx, |this, cx| {
269                this.active_agent_id = None;
270                this.subscribers.clear();
271                cx.notify();
272            })
273            .log_err();
274        }));
275
276        cx.notify();
277        tap
278    }
279
280    /// Clear the retained message history for the current connection and force
281    /// watchers to resubscribe so their local correlation state is reset too.
282    pub fn clear_messages(&mut self, cx: &mut Context<Self>) {
283        self.backlog.clear();
284        self.generation += 1;
285        self.subscribers.clear();
286        cx.notify();
287    }
288
289    /// Subscribe to messages on the current connection.
290    ///
291    /// Returns the existing backlog (already-observed messages) together with
292    /// a receiver for new messages. The caller is responsible for flushing the
293    /// backlog into its local state before draining the receiver, so that no
294    /// messages are dropped between the snapshot and live subscription.
295    pub fn subscribe(&mut self) -> (Vec<StreamMessage>, smol::channel::Receiver<StreamMessage>) {
296        let backlog = self.backlog.iter().cloned().collect();
297        let (sender, receiver) = smol::channel::unbounded();
298        self.subscribers.push(sender);
299        (backlog, receiver)
300    }
301}
302
303struct AcpTools {
304    project: Entity<Project>,
305    focus_handle: FocusHandle,
306    expanded: HashSet<usize>,
307    watched_connection: Option<WatchedConnection>,
308    connection_registry: Entity<AcpConnectionRegistry>,
309    _subscription: Subscription,
310}
311
312struct WatchedConnection {
313    agent_id: AgentId,
314    generation: u64,
315    messages: Vec<WatchedConnectionMessage>,
316    list_state: ListState,
317    incoming_request_methods: HashMap<acp::RequestId, Arc<str>>,
318    outgoing_request_methods: HashMap<acp::RequestId, Arc<str>>,
319    _task: Task<()>,
320}
321
322impl AcpTools {
323    fn new(project: Entity<Project>, cx: &mut Context<Self>) -> Self {
324        let connection_registry = AcpConnectionRegistry::default_global(cx);
325
326        let subscription = cx.observe(&connection_registry, |this, _, cx| {
327            this.update_connection(cx);
328            cx.notify();
329        });
330
331        let mut this = Self {
332            project,
333            focus_handle: cx.focus_handle(),
334            expanded: HashSet::default(),
335            watched_connection: None,
336            connection_registry,
337            _subscription: subscription,
338        };
339        this.update_connection(cx);
340        this
341    }
342
343    fn update_connection(&mut self, cx: &mut Context<Self>) {
344        let (generation, agent_id) = {
345            let registry = self.connection_registry.read(cx);
346            (registry.generation, registry.active_agent_id.clone())
347        };
348
349        let Some(agent_id) = agent_id else {
350            self.watched_connection = None;
351            self.expanded.clear();
352            return;
353        };
354
355        if let Some(watched) = self.watched_connection.as_ref() {
356            if watched.generation == generation {
357                return;
358            }
359        }
360
361        self.expanded.clear();
362
363        let (backlog, messages_rx) = self
364            .connection_registry
365            .update(cx, |registry, _cx| registry.subscribe());
366
367        let task = cx.spawn(async move |this, cx| {
368            while let Ok(message) = messages_rx.recv().await {
369                this.update(cx, |this, cx| {
370                    this.push_stream_message(message, cx);
371                })
372                .log_err();
373            }
374        });
375
376        self.watched_connection = Some(WatchedConnection {
377            agent_id,
378            generation,
379            messages: vec![],
380            list_state: ListState::new(0, ListAlignment::Bottom, px(2048.)),
381            incoming_request_methods: HashMap::default(),
382            outgoing_request_methods: HashMap::default(),
383            _task: task,
384        });
385
386        for message in backlog {
387            self.push_stream_message(message, cx);
388        }
389    }
390
391    fn push_stream_message(&mut self, stream_message: StreamMessage, cx: &mut Context<Self>) {
392        let Some(connection) = self.watched_connection.as_mut() else {
393            return;
394        };
395        let language_registry = self.project.read(cx).languages().clone();
396        let index = connection.messages.len();
397
398        let (request_id, method, message_type, params) = match stream_message.message {
399            StreamMessageContent::Request { id, method, params } => {
400                let method_map = match stream_message.direction {
401                    StreamMessageDirection::Incoming => &mut connection.incoming_request_methods,
402                    StreamMessageDirection::Outgoing => &mut connection.outgoing_request_methods,
403                    // Stderr lines never carry request/response correlation.
404                    StreamMessageDirection::Stderr => return,
405                };
406
407                method_map.insert(id.clone(), method.clone());
408                (Some(id), method.into(), MessageType::Request, Ok(params))
409            }
410            StreamMessageContent::Response { id, result } => {
411                let method_map = match stream_message.direction {
412                    StreamMessageDirection::Incoming => &mut connection.outgoing_request_methods,
413                    StreamMessageDirection::Outgoing => &mut connection.incoming_request_methods,
414                    StreamMessageDirection::Stderr => return,
415                };
416
417                if let Some(method) = method_map.remove(&id) {
418                    (Some(id), method.into(), MessageType::Response, result)
419                } else {
420                    (
421                        Some(id),
422                        "[unrecognized response]".into(),
423                        MessageType::Response,
424                        result,
425                    )
426                }
427            }
428            StreamMessageContent::Notification { method, params } => {
429                (None, method.into(), MessageType::Notification, Ok(params))
430            }
431            StreamMessageContent::Stderr { line } => {
432                // Stderr is rendered as plain text inline with JSON-RPC traffic,
433                // using `stderr` as the pseudo-method name so it shows up in the
434                // header the same way real methods do.
435                (
436                    None,
437                    "stderr".into(),
438                    MessageType::Stderr,
439                    Ok(Some(serde_json::Value::String(line.to_string()))),
440                )
441            }
442        };
443
444        let message = WatchedConnectionMessage {
445            name: method,
446            message_type,
447            request_id,
448            direction: stream_message.direction,
449            collapsed_params_md: match params.as_ref() {
450                Ok(params) => params
451                    .as_ref()
452                    .map(|params| collapsed_params_md(params, &language_registry, cx)),
453                Err(err) => {
454                    if let Ok(err) = &serde_json::to_value(err) {
455                        Some(collapsed_params_md(&err, &language_registry, cx))
456                    } else {
457                        None
458                    }
459                }
460            },
461
462            expanded_params_md: None,
463            params,
464        };
465
466        connection.messages.push(message);
467        connection.list_state.splice(index..index, 1);
468        cx.notify();
469    }
470
471    fn serialize_observed_messages(&self) -> Option<String> {
472        let connection = self.watched_connection.as_ref()?;
473
474        let messages: Vec<serde_json::Value> = connection
475            .messages
476            .iter()
477            .filter_map(|message| {
478                let params = match &message.params {
479                    Ok(Some(params)) => params.clone(),
480                    Ok(None) => serde_json::Value::Null,
481                    Err(err) => serde_json::to_value(err).ok()?,
482                };
483                Some(serde_json::json!({
484                    "_direction": match message.direction {
485                        StreamMessageDirection::Incoming => "incoming",
486                        StreamMessageDirection::Outgoing => "outgoing",
487                        StreamMessageDirection::Stderr => "stderr",
488                    },
489                    "_type": message.message_type.to_string().to_lowercase(),
490                    "id": message.request_id,
491                    "method": message.name.to_string(),
492                    "params": params,
493                }))
494            })
495            .collect();
496
497        serde_json::to_string_pretty(&messages).ok()
498    }
499
500    fn clear_messages(&mut self, cx: &mut Context<Self>) {
501        if let Some(connection) = self.watched_connection.as_mut() {
502            connection.messages.clear();
503            connection.list_state.reset(0);
504            connection.incoming_request_methods.clear();
505            connection.outgoing_request_methods.clear();
506            self.expanded.clear();
507            cx.notify();
508        }
509    }
510
511    fn render_message(
512        &mut self,
513        index: usize,
514        window: &mut Window,
515        cx: &mut Context<Self>,
516    ) -> AnyElement {
517        let Some(connection) = self.watched_connection.as_ref() else {
518            return Empty.into_any();
519        };
520
521        let Some(message) = connection.messages.get(index) else {
522            return Empty.into_any();
523        };
524
525        let base_size = TextSize::Editor.rems(cx);
526
527        let theme_settings = ThemeSettings::get_global(cx);
528        let text_style = window.text_style();
529
530        let colors = cx.theme().colors();
531        let expanded = self.expanded.contains(&index);
532
533        v_flex()
534            .id(index)
535            .group("message")
536            .font_buffer(cx)
537            .w_full()
538            .py_3()
539            .pl_4()
540            .pr_5()
541            .gap_2()
542            .items_start()
543            .text_size(base_size)
544            .border_color(colors.border)
545            .border_b_1()
546            .hover(|this| this.bg(colors.element_background.opacity(0.5)))
547            .child(
548                h_flex()
549                    .id(("acp-log-message-header", index))
550                    .w_full()
551                    .gap_2()
552                    .flex_shrink_0()
553                    .cursor_pointer()
554                    .on_click(cx.listener(move |this, _, _, cx| {
555                        if this.expanded.contains(&index) {
556                            this.expanded.remove(&index);
557                        } else {
558                            this.expanded.insert(index);
559                            let Some(connection) = &mut this.watched_connection else {
560                                return;
561                            };
562                            let Some(message) = connection.messages.get_mut(index) else {
563                                return;
564                            };
565                            message.expanded(this.project.read(cx).languages().clone(), cx);
566                            connection.list_state.scroll_to_reveal_item(index);
567                        }
568                        cx.notify()
569                    }))
570                    .child(match message.direction {
571                        StreamMessageDirection::Incoming => Icon::new(IconName::ArrowDown)
572                            .color(Color::Error)
573                            .size(IconSize::Small),
574                        StreamMessageDirection::Outgoing => Icon::new(IconName::ArrowUp)
575                            .color(Color::Success)
576                            .size(IconSize::Small),
577                        StreamMessageDirection::Stderr => Icon::new(IconName::Warning)
578                            .color(Color::Warning)
579                            .size(IconSize::Small),
580                    })
581                    .child(
582                        Label::new(message.name.clone())
583                            .buffer_font(cx)
584                            .color(Color::Muted),
585                    )
586                    .child(div().flex_1())
587                    .child(
588                        div()
589                            .child(ui::Chip::new(message.message_type.to_string()))
590                            .visible_on_hover("message"),
591                    )
592                    .children(
593                        message
594                            .request_id
595                            .as_ref()
596                            .map(|req_id| div().child(ui::Chip::new(req_id.to_string()))),
597                    ),
598            )
599            // I'm aware using markdown is a hack. Trying to get something working for the demo.
600            // Will clean up soon!
601            .when_some(
602                if expanded {
603                    message.expanded_params_md.clone()
604                } else {
605                    message.collapsed_params_md.clone()
606                },
607                |this, params| {
608                    this.child(
609                        div().pl_6().w_full().child(
610                            MarkdownElement::new(
611                                params,
612                                MarkdownStyle {
613                                    base_text_style: text_style,
614                                    selection_background_color: colors.element_selection_background,
615                                    syntax: cx.theme().syntax().clone(),
616                                    code_block_overflow_x_scroll: true,
617                                    code_block: StyleRefinement {
618                                        text: TextStyleRefinement {
619                                            font_family: Some(
620                                                theme_settings.buffer_font.family.clone(),
621                                            ),
622                                            font_size: Some((base_size * 0.8).into()),
623                                            ..Default::default()
624                                        },
625                                        ..Default::default()
626                                    },
627                                    ..Default::default()
628                                },
629                            )
630                            .code_block_renderer(
631                                CodeBlockRenderer::Default {
632                                    copy_button_visibility: if expanded {
633                                        CopyButtonVisibility::VisibleOnHover
634                                    } else {
635                                        CopyButtonVisibility::Hidden
636                                    },
637                                    border: false,
638                                },
639                            ),
640                        ),
641                    )
642                },
643            )
644            .into_any()
645    }
646}
647
648struct WatchedConnectionMessage {
649    name: SharedString,
650    request_id: Option<acp::RequestId>,
651    direction: StreamMessageDirection,
652    message_type: MessageType,
653    params: Result<Option<serde_json::Value>, acp::Error>,
654    collapsed_params_md: Option<Entity<Markdown>>,
655    expanded_params_md: Option<Entity<Markdown>>,
656}
657
658impl WatchedConnectionMessage {
659    fn expanded(&mut self, language_registry: Arc<LanguageRegistry>, cx: &mut App) {
660        let params_md = match &self.params {
661            Ok(Some(params)) => Some(expanded_params_md(params, &language_registry, cx)),
662            Err(err) => {
663                if let Some(err) = &serde_json::to_value(err).log_err() {
664                    Some(expanded_params_md(&err, &language_registry, cx))
665                } else {
666                    None
667                }
668            }
669            _ => None,
670        };
671        self.expanded_params_md = params_md;
672    }
673}
674
675fn collapsed_params_md(
676    params: &serde_json::Value,
677    language_registry: &Arc<LanguageRegistry>,
678    cx: &mut App,
679) -> Entity<Markdown> {
680    let params_json = serde_json::to_string(params).unwrap_or_default();
681    let mut spaced_out_json = String::with_capacity(params_json.len() + params_json.len() / 4);
682
683    for ch in params_json.chars() {
684        match ch {
685            '{' => spaced_out_json.push_str("{ "),
686            '}' => spaced_out_json.push_str(" }"),
687            ':' => spaced_out_json.push_str(": "),
688            ',' => spaced_out_json.push_str(", "),
689            c => spaced_out_json.push(c),
690        }
691    }
692
693    let params_md = format!("```json\n{}\n```", spaced_out_json);
694    cx.new(|cx| Markdown::new(params_md.into(), Some(language_registry.clone()), None, cx))
695}
696
697fn expanded_params_md(
698    params: &serde_json::Value,
699    language_registry: &Arc<LanguageRegistry>,
700    cx: &mut App,
701) -> Entity<Markdown> {
702    let params_json = serde_json::to_string_pretty(params).unwrap_or_default();
703    let params_md = format!("```json\n{}\n```", params_json);
704    cx.new(|cx| Markdown::new(params_md.into(), Some(language_registry.clone()), None, cx))
705}
706
707enum MessageType {
708    Request,
709    Response,
710    Notification,
711    Stderr,
712}
713
714impl Display for MessageType {
715    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
716        match self {
717            MessageType::Request => write!(f, "Request"),
718            MessageType::Response => write!(f, "Response"),
719            MessageType::Notification => write!(f, "Notification"),
720            MessageType::Stderr => write!(f, "Stderr"),
721        }
722    }
723}
724
725enum AcpToolsEvent {}
726
727impl EventEmitter<AcpToolsEvent> for AcpTools {}
728
729impl Item for AcpTools {
730    type Event = AcpToolsEvent;
731
732    fn tab_content_text(&self, _detail: usize, _cx: &App) -> ui::SharedString {
733        format!(
734            "ACP: {}",
735            self.watched_connection
736                .as_ref()
737                .map_or("Disconnected", |connection| connection.agent_id.0.as_ref())
738        )
739        .into()
740    }
741
742    fn tab_icon(&self, _window: &Window, _cx: &App) -> Option<Icon> {
743        Some(ui::Icon::new(IconName::Thread))
744    }
745}
746
747impl Focusable for AcpTools {
748    fn focus_handle(&self, _cx: &App) -> FocusHandle {
749        self.focus_handle.clone()
750    }
751}
752
753impl Render for AcpTools {
754    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
755        v_flex()
756            .track_focus(&self.focus_handle)
757            .size_full()
758            .bg(cx.theme().colors().editor_background)
759            .child(match self.watched_connection.as_ref() {
760                Some(connection) => {
761                    if connection.messages.is_empty() {
762                        h_flex()
763                            .size_full()
764                            .justify_center()
765                            .items_center()
766                            .child("No messages recorded yet")
767                            .into_any()
768                    } else {
769                        div()
770                            .size_full()
771                            .flex_grow()
772                            .child(
773                                list(
774                                    connection.list_state.clone(),
775                                    cx.processor(Self::render_message),
776                                )
777                                .with_sizing_behavior(gpui::ListSizingBehavior::Auto)
778                                .size_full(),
779                            )
780                            .vertical_scrollbar_for(&connection.list_state, window, cx)
781                            .into_any()
782                    }
783                }
784                None => h_flex()
785                    .size_full()
786                    .justify_center()
787                    .items_center()
788                    .child("No active connection")
789                    .into_any(),
790            })
791    }
792}
793
794pub struct AcpToolsToolbarItemView {
795    acp_tools: Option<Entity<AcpTools>>,
796}
797
798impl AcpToolsToolbarItemView {
799    pub fn new() -> Self {
800        Self { acp_tools: None }
801    }
802}
803
804impl Render for AcpToolsToolbarItemView {
805    fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
806        let Some(acp_tools) = self.acp_tools.as_ref() else {
807            return Empty.into_any_element();
808        };
809
810        let acp_tools = acp_tools.clone();
811        let connection_registry = acp_tools.read(cx).connection_registry.clone();
812        let has_messages = acp_tools
813            .read(cx)
814            .watched_connection
815            .as_ref()
816            .is_some_and(|connection| !connection.messages.is_empty());
817
818        h_flex()
819            .gap_2()
820            .child({
821                let message = acp_tools
822                    .read(cx)
823                    .serialize_observed_messages()
824                    .unwrap_or_default();
825
826                CopyButton::new("copy-all-messages", message)
827                    .tooltip_label("Copy All Messages")
828                    .disabled(!has_messages)
829            })
830            .child(
831                IconButton::new("clear_messages", IconName::Trash)
832                    .icon_size(IconSize::Small)
833                    .tooltip(Tooltip::text("Clear Messages"))
834                    .disabled(!has_messages)
835                    .on_click(cx.listener(move |_this, _, _window, cx| {
836                        connection_registry.update(cx, |registry, cx| {
837                            registry.clear_messages(cx);
838                        });
839                        acp_tools.update(cx, |acp_tools, cx| {
840                            acp_tools.clear_messages(cx);
841                        });
842                    })),
843            )
844            .into_any()
845    }
846}
847
848impl EventEmitter<ToolbarItemEvent> for AcpToolsToolbarItemView {}
849
850impl ToolbarItemView for AcpToolsToolbarItemView {
851    fn set_active_pane_item(
852        &mut self,
853        active_pane_item: Option<&dyn ItemHandle>,
854        _window: &mut Window,
855        cx: &mut Context<Self>,
856    ) -> ToolbarItemLocation {
857        if let Some(item) = active_pane_item
858            && let Some(acp_tools) = item.downcast::<AcpTools>()
859        {
860            self.acp_tools = Some(acp_tools);
861            cx.notify();
862            return ToolbarItemLocation::PrimaryRight;
863        }
864        if self.acp_tools.take().is_some() {
865            cx.notify();
866        }
867        ToolbarItemLocation::Hidden
868    }
869}