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