1use context_server::ContextServerCommand;
2use gpui::{DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, WeakEntity, prelude::*};
3use project::project_settings::{ContextServerConfiguration, ProjectSettings};
4use serde_json::json;
5use settings::update_settings_file;
6use ui::{KeyBinding, Modal, ModalFooter, ModalHeader, Section, Tooltip, prelude::*};
7use ui_input::SingleLineInput;
8use workspace::{ModalView, Workspace};
9
10use crate::AddContextServer;
11
12pub struct AddContextServerModal {
13 workspace: WeakEntity<Workspace>,
14 name_editor: Entity<SingleLineInput>,
15 command_editor: Entity<SingleLineInput>,
16}
17
18impl AddContextServerModal {
19 pub fn register(
20 workspace: &mut Workspace,
21 _window: Option<&mut Window>,
22 _cx: &mut Context<Workspace>,
23 ) {
24 workspace.register_action(|workspace, _: &AddContextServer, window, cx| {
25 let workspace_handle = cx.entity().downgrade();
26 workspace.toggle_modal(window, cx, |window, cx| {
27 Self::new(workspace_handle, window, cx)
28 })
29 });
30 }
31
32 pub fn new(
33 workspace: WeakEntity<Workspace>,
34 window: &mut Window,
35 cx: &mut Context<Self>,
36 ) -> Self {
37 let name_editor =
38 cx.new(|cx| SingleLineInput::new(window, cx, "my-custom-server").label("Name"));
39 let command_editor = cx.new(|cx| {
40 SingleLineInput::new(window, cx, "Command").label("Command to run the MCP server")
41 });
42
43 Self {
44 name_editor,
45 command_editor,
46 workspace,
47 }
48 }
49
50 fn confirm(&mut self, _: &menu::Confirm, cx: &mut Context<Self>) {
51 let name = self
52 .name_editor
53 .read(cx)
54 .editor()
55 .read(cx)
56 .text(cx)
57 .trim()
58 .to_string();
59 let command = self
60 .command_editor
61 .read(cx)
62 .editor()
63 .read(cx)
64 .text(cx)
65 .trim()
66 .to_string();
67
68 if name.is_empty() || command.is_empty() {
69 return;
70 }
71
72 let mut command_parts = command.split(' ').map(|part| part.trim().to_string());
73 let Some(path) = command_parts.next() else {
74 return;
75 };
76 let args = command_parts.collect::<Vec<_>>();
77
78 if let Some(workspace) = self.workspace.upgrade() {
79 workspace.update(cx, |workspace, cx| {
80 let fs = workspace.app_state().fs.clone();
81 update_settings_file::<ProjectSettings>(fs.clone(), cx, |settings, _| {
82 settings.context_servers.insert(
83 name.into(),
84 ContextServerConfiguration {
85 command: Some(ContextServerCommand {
86 path,
87 args,
88 env: None,
89 }),
90 settings: Some(json!({})),
91 },
92 );
93 });
94 });
95 }
96
97 cx.emit(DismissEvent);
98 }
99
100 fn cancel(&mut self, _: &menu::Cancel, cx: &mut Context<Self>) {
101 cx.emit(DismissEvent);
102 }
103}
104
105impl ModalView for AddContextServerModal {}
106
107impl Focusable for AddContextServerModal {
108 fn focus_handle(&self, cx: &App) -> FocusHandle {
109 self.name_editor.focus_handle(cx).clone()
110 }
111}
112
113impl EventEmitter<DismissEvent> for AddContextServerModal {}
114
115impl Render for AddContextServerModal {
116 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
117 let is_name_empty = self.name_editor.read(cx).is_empty(cx);
118 let is_command_empty = self.command_editor.read(cx).is_empty(cx);
119
120 let focus_handle = self.focus_handle(cx);
121
122 div()
123 .elevation_3(cx)
124 .w(rems(34.))
125 .key_context("AddContextServerModal")
126 .on_action(
127 cx.listener(|this, _: &menu::Cancel, _window, cx| this.cancel(&menu::Cancel, cx)),
128 )
129 .on_action(
130 cx.listener(|this, _: &menu::Confirm, _window, cx| {
131 this.confirm(&menu::Confirm, cx)
132 }),
133 )
134 .capture_any_mouse_down(cx.listener(|this, _, window, cx| {
135 this.focus_handle(cx).focus(window);
136 }))
137 .on_mouse_down_out(cx.listener(|_this, _, _, cx| cx.emit(DismissEvent)))
138 .child(
139 Modal::new("add-context-server", None)
140 .header(ModalHeader::new().headline("Add MCP Server"))
141 .section(
142 Section::new().child(
143 v_flex()
144 .gap_2()
145 .child(self.name_editor.clone())
146 .child(self.command_editor.clone()),
147 ),
148 )
149 .footer(
150 ModalFooter::new().end_slot(
151 h_flex()
152 .gap_2()
153 .child(
154 Button::new("cancel", "Cancel")
155 .key_binding(
156 KeyBinding::for_action_in(
157 &menu::Cancel,
158 &focus_handle,
159 window,
160 cx,
161 )
162 .map(|kb| kb.size(rems_from_px(12.))),
163 )
164 .on_click(cx.listener(|this, _event, _window, cx| {
165 this.cancel(&menu::Cancel, cx)
166 })),
167 )
168 .child(
169 Button::new("add-server", "Add Server")
170 .disabled(is_name_empty || is_command_empty)
171 .key_binding(
172 KeyBinding::for_action_in(
173 &menu::Confirm,
174 &focus_handle,
175 window,
176 cx,
177 )
178 .map(|kb| kb.size(rems_from_px(12.))),
179 )
180 .map(|button| {
181 if is_name_empty {
182 button.tooltip(Tooltip::text("Name is required"))
183 } else if is_command_empty {
184 button.tooltip(Tooltip::text("Command is required"))
185 } else {
186 button
187 }
188 })
189 .on_click(cx.listener(|this, _event, _window, cx| {
190 this.confirm(&menu::Confirm, cx)
191 })),
192 ),
193 ),
194 ),
195 )
196 }
197}