configure_context_server_tools_modal.rs

  1use assistant_tool::{ToolSource, ToolWorkingSet};
  2use context_server::ContextServerId;
  3use gpui::{
  4    DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, ScrollHandle, Window, prelude::*,
  5};
  6use ui::{Divider, DividerColor, Modal, ModalHeader, WithScrollbar, prelude::*};
  7use workspace::{ModalView, Workspace};
  8
  9pub struct ConfigureContextServerToolsModal {
 10    context_server_id: ContextServerId,
 11    tools: Entity<ToolWorkingSet>,
 12    focus_handle: FocusHandle,
 13    expanded_tools: std::collections::HashMap<String, bool>,
 14    scroll_handle: ScrollHandle,
 15}
 16
 17impl ConfigureContextServerToolsModal {
 18    fn new(
 19        context_server_id: ContextServerId,
 20        tools: Entity<ToolWorkingSet>,
 21        _window: &mut Window,
 22        cx: &mut Context<Self>,
 23    ) -> Self {
 24        Self {
 25            context_server_id,
 26            tools,
 27            focus_handle: cx.focus_handle(),
 28            expanded_tools: std::collections::HashMap::new(),
 29            scroll_handle: ScrollHandle::new(),
 30        }
 31    }
 32
 33    pub fn toggle(
 34        context_server_id: ContextServerId,
 35        tools: Entity<ToolWorkingSet>,
 36        workspace: &mut Workspace,
 37        window: &mut Window,
 38        cx: &mut Context<Workspace>,
 39    ) {
 40        workspace.toggle_modal(window, cx, |window, cx| {
 41            Self::new(context_server_id, tools, window, cx)
 42        });
 43    }
 44
 45    fn cancel(&mut self, _: &menu::Cancel, _: &mut Window, cx: &mut Context<Self>) {
 46        cx.emit(DismissEvent)
 47    }
 48
 49    fn render_modal_content(
 50        &self,
 51        window: &mut Window,
 52        cx: &mut Context<Self>,
 53    ) -> impl IntoElement {
 54        let tools_by_source = self.tools.read(cx).tools_by_source(cx);
 55        let server_tools = tools_by_source
 56            .get(&ToolSource::ContextServer {
 57                id: self.context_server_id.0.clone().into(),
 58            })
 59            .map(|tools| tools.as_slice())
 60            .unwrap_or(&[]);
 61
 62        div()
 63            .size_full()
 64            .pb_2()
 65            .child(
 66                v_flex()
 67                    .id("modal_content")
 68                    .px_2()
 69                    .gap_1()
 70                    .max_h_128()
 71                    .overflow_y_scroll()
 72                    .track_scroll(&self.scroll_handle)
 73                    .children(server_tools.iter().enumerate().flat_map(|(index, tool)| {
 74                        let tool_name = tool.name();
 75                        let is_expanded = self
 76                            .expanded_tools
 77                            .get(&tool_name)
 78                            .copied()
 79                            .unwrap_or(false);
 80
 81                        let icon = if is_expanded {
 82                            IconName::ChevronUp
 83                        } else {
 84                            IconName::ChevronDown
 85                        };
 86
 87                        let mut items = vec![
 88                            v_flex()
 89                                .child(
 90                                    h_flex()
 91                                        .id(SharedString::from(format!("tool-header-{}", index)))
 92                                        .py_1()
 93                                        .pl_1()
 94                                        .pr_2()
 95                                        .w_full()
 96                                        .justify_between()
 97                                        .rounded_sm()
 98                                        .hover(|s| s.bg(cx.theme().colors().element_hover))
 99                                        .child(
100                                            Label::new(tool_name.clone())
101                                                .buffer_font(cx)
102                                                .size(LabelSize::Small),
103                                        )
104                                        .child(
105                                            Icon::new(icon)
106                                                .size(IconSize::Small)
107                                                .color(Color::Muted),
108                                        )
109                                        .on_click(cx.listener({
110                                            move |this, _event, _window, _cx| {
111                                                let current = this
112                                                    .expanded_tools
113                                                    .get(&tool_name)
114                                                    .copied()
115                                                    .unwrap_or(false);
116                                                this.expanded_tools
117                                                    .insert(tool_name.clone(), !current);
118                                                _cx.notify();
119                                            }
120                                        })),
121                                )
122                                .when(is_expanded, |this| {
123                                    this.child(
124                                        Label::new(tool.description()).color(Color::Muted).mx_1(),
125                                    )
126                                })
127                                .into_any_element(),
128                        ];
129
130                        if index < server_tools.len() - 1 {
131                            items.push(
132                                h_flex()
133                                    .w_full()
134                                    .child(Divider::horizontal().color(DividerColor::BorderVariant))
135                                    .into_any_element(),
136                            );
137                        }
138
139                        items
140                    })),
141            )
142            .vertical_scrollbar_for(self.scroll_handle.clone(), window, cx)
143            .into_any_element()
144    }
145}
146
147impl ModalView for ConfigureContextServerToolsModal {}
148
149impl Focusable for ConfigureContextServerToolsModal {
150    fn focus_handle(&self, _cx: &App) -> FocusHandle {
151        self.focus_handle.clone()
152    }
153}
154
155impl EventEmitter<DismissEvent> for ConfigureContextServerToolsModal {}
156
157impl Render for ConfigureContextServerToolsModal {
158    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
159        div()
160            .key_context("ContextServerToolsModal")
161            .occlude()
162            .elevation_3(cx)
163            .w(rems(34.))
164            .on_action(cx.listener(Self::cancel))
165            .track_focus(&self.focus_handle)
166            .child(
167                Modal::new("configure-context-server-tools", None::<ScrollHandle>)
168                    .header(
169                        ModalHeader::new()
170                            .headline(format!("Tools from {}", self.context_server_id.0))
171                            .show_dismiss_button(true),
172                    )
173                    .child(self.render_modal_content(window, cx)),
174            )
175    }
176}