Skip to repository content

tenant.openagents/omega

No repository description is available.

OpenAgents Git authority 2026-07-28T05:20:20.772Z Public web read
NIP-34 coordinate30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omega
MaintainersHidden in public view
References2 branches · 1 tag
Read-only clonegit clone https://openagents.com/git/tenant.openagents/omega.git
Browse files

acp_tools.rs

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