Skip to repository content843 lines · 31.6 KB · rust
tenant.openagents/omega
No repository description is available.
OpenAgents Git authority 2026-07-28T03:02:48.718Z Public web read
NIP-34 coordinate
30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omegaMaintainersHidden in public view
References2 branches · 1 tag
Read-only clone
git clone https://openagents.com/git/tenant.openagents/omega.gitBrowse files
acp_tools.rs
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 ¶ms {
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