first pass of merging in warp (doesn't build)

This commit is contained in:
Ryan Ward
2026-07-01 16:08:58 -05:00
parent 2f64909469
commit 4770ac06b5
3662 changed files with 414574 additions and 89772 deletions
@@ -1,14 +1,12 @@
use galaxyui::elements::{ChildView, Container, Dismiss, Empty};
use galaxyui::ui_components::components::UiComponent;
use galaxyui::{
elements::{ChildView, Container, Dismiss, Empty},
ui_components::components::UiComponent,
AppContext, Element, Entity, SingletonEntity, TypedActionView, View, ViewContext, ViewHandle,
};
use crate::{
appearance::Appearance,
ui_components::dialog::{dialog_styles, Dialog},
view_components::action_button::{ActionButton, DangerPrimaryTheme, NakedTheme},
};
use crate::appearance::Appearance;
use crate::ui_components::dialog::{dialog_styles, Dialog};
use crate::view_components::action_button::{ActionButton, DangerPrimaryTheme, NakedTheme};
const DIALOG_WIDTH: f32 = 450.;
pub enum DestructiveMCPConfirmationDialogEvent {
+96 -63
View File
@@ -1,64 +1,63 @@
#[cfg(not(target_family = "wasm"))]
use std::collections::HashMap;
use std::path::Path;
#[cfg(feature = "local_fs")]
use std::sync::Arc;
use std::{collections::HashMap, path::Path};
#[cfg(feature = "local_fs")]
#[cfg(not(target_family = "wasm"))]
use diesel::SqliteConnection;
use galaxy_core::{
send_telemetry_from_ctx,
ui::{appearance::Appearance, theme::color::internal_colors},
};
use galaxy_editor::{
content::buffer::InitialBufferState, render::element::VerticalExpansionBehavior,
#[cfg(feature = "local_fs")]
use parking_lot::Mutex;
use pathfinder_geometry::vector::vec2f;
use settings::Setting as _;
use uuid::Uuid;
use galaxy_core::send_telemetry_from_ctx;
use galaxy_core::ui::appearance::Appearance;
use galaxy_core::ui::theme::color::internal_colors;
use galaxy_editor::content::buffer::InitialBufferState;
use galaxy_editor::render::element::VerticalExpansionBehavior;
use galaxyui::elements::{
Border, ChildAnchor, ChildView, Container, CornerRadius, CrossAxisAlignment, Flex,
MainAxisAlignment, MainAxisSize, MouseStateHandle, OffsetPositioning, ParentAnchor,
ParentElement, ParentOffsetBounds, Radius, Shrinkable, Stack, Text,
};
use galaxyui::platform::Cursor;
use galaxyui::ui_components::components::UiComponent;
use galaxyui::{
elements::{
Border, ChildAnchor, ChildView, Container, CornerRadius, CrossAxisAlignment, Flex,
MainAxisAlignment, MainAxisSize, MouseStateHandle, OffsetPositioning, ParentAnchor,
ParentElement, ParentOffsetBounds, Radius, Shrinkable, Stack, Text,
},
platform::Cursor,
ui_components::components::UiComponent,
AppContext, Element, Entity, SingletonEntity, TypedActionView, View, ViewContext, ViewHandle,
};
#[cfg(not(target_family = "wasm"))]
use parking_lot::Mutex;
use pathfinder_geometry::vector::vec2f;
use uuid::Uuid;
use crate::{
ai::{
blocklist::secret_redaction::find_secrets_in_text,
mcp::{
parsing::{prettify_json, resolve_json, ParsedTemplatableMCPServerResult},
templatable::CloudTemplatableMCPServer,
MCPServer, TemplatableMCPServer, TemplatableMCPServerInstallation,
TemplatableMCPServerManager, TransportType,
},
},
banner::{Banner, BannerTextContent},
cloud_object::{CloudObject, Space},
code::editor::view::{CodeEditorRenderOptions, CodeEditorView},
persistence::ModelEvent,
server::{
cloud_objects::update_manager::InitiatedBy,
telemetry::{MCPTemplateCreationSource, TelemetryEvent},
},
settings_view::mcp_servers::{
destructive_mcp_confirmation_dialog::{
DestructiveMCPConfirmationDialog, DestructiveMCPConfirmationDialogEvent,
DestructiveMCPConfirmationDialogVariant,
},
style, ServerCardItemId,
},
ui_components::{buttons::icon_button, icons::Icon},
view_components::{
action_button::{ActionButton, DangerNakedTheme, DangerSecondaryTheme, PrimaryTheme},
DismissibleToast,
},
workspace::ToastStack,
GlobalResourceHandlesProvider,
use crate::ai::blocklist::secret_redaction::find_secrets_in_text;
use crate::ai::mcp::parsing::{prettify_json, resolve_json, ParsedTemplatableMCPServerResult};
use crate::ai::mcp::templatable::CloudTemplatableMCPServer;
use crate::ai::mcp::{
MCPServer, TemplatableMCPServer, TemplatableMCPServerInstallation, TemplatableMCPServerManager,
TransportType,
};
use crate::banner::{Banner, BannerTextContent};
use crate::cloud_object::{CloudObject, Space};
use crate::code::editor::view::{CodeEditorRenderOptions, CodeEditorView};
use crate::persistence::ModelEvent;
#[cfg(feature = "local_fs")]
use crate::persistence::{database_file_path_for_scope, establish_ro_connection, PersistenceScope};
use crate::server::cloud_objects::update_manager::InitiatedBy;
use crate::server::telemetry::{MCPTemplateCreationSource, TelemetryEvent};
use crate::settings_view::mcp_servers::destructive_mcp_confirmation_dialog::{
DestructiveMCPConfirmationDialog, DestructiveMCPConfirmationDialogEvent,
DestructiveMCPConfirmationDialogVariant,
};
use crate::settings_view::mcp_servers::{style, ServerCardItemId};
use crate::terminal::safe_mode_settings::SafeModeSettings;
use crate::ui_components::buttons::icon_button;
use crate::ui_components::icons::Icon;
use crate::view_components::action_button::{
ActionButton, DangerNakedTheme, DangerSecondaryTheme, PrimaryTheme,
};
use crate::view_components::DismissibleToast;
use crate::workspace::ToastStack;
use crate::workspaces::user_workspaces::UserWorkspaces;
use crate::GlobalResourceHandlesProvider;
const DEFAULT_JSON_TEXT: &str = r#"{
"": {
@@ -124,7 +123,7 @@ pub struct MCPServersEditPageView {
log_out_icon_button_mouse_handle: MouseStateHandle,
editing_disabled_banner: ViewHandle<Banner<()>>,
#[cfg(not(target_family = "wasm"))]
#[cfg(feature = "local_fs")]
#[allow(dead_code)]
database_connection: Option<Arc<Mutex<SqliteConnection>>>,
}
@@ -175,7 +174,7 @@ impl MCPServersEditPageView {
true,
),
);
editor.set_language_with_path(Path::new("mcp.json"), ctx);
editor.set_language_with_local_path(Path::new("/mcp.json"), ctx);
editor
});
@@ -192,15 +191,14 @@ impl MCPServersEditPageView {
.with_icon(Icon::Warning)
});
#[cfg(not(target_family = "wasm"))]
let database_connection =
crate::persistence::database_file_path()
.to_str()
.and_then(|db_url| {
crate::persistence::establish_ro_connection(db_url)
.ok()
.map(|conn| Arc::new(Mutex::new(conn)))
});
#[cfg(feature = "local_fs")]
let database_connection = database_file_path_for_scope(&PersistenceScope::App)
.to_str()
.and_then(|db_url| {
establish_ro_connection(db_url)
.ok()
.map(|conn| Arc::new(Mutex::new(conn)))
});
Self {
server_card_item_id: None,
@@ -215,7 +213,7 @@ impl MCPServersEditPageView {
log_out_icon_button_mouse_handle: Default::default(),
editing_disabled_banner,
#[cfg(not(target_family = "wasm"))]
#[cfg(feature = "local_fs")]
database_connection,
}
}
@@ -531,10 +529,13 @@ impl MCPServersEditPageView {
ctx: &mut ViewContext<Self>,
templatable_mcp_server: &TemplatableMCPServer,
) -> Result<(), String> {
let safe_mode_enabled = *SafeModeSettings::as_ref(ctx).safe_mode_enabled.value();
let enterprise_enforced =
UserWorkspaces::as_ref(ctx).is_enterprise_secret_redaction_enabled();
let contains_secrets =
!find_secrets_in_text(&templatable_mcp_server.template.json).is_empty();
if contains_secrets {
if should_block_save_for_secrets(safe_mode_enabled, enterprise_enforced, contains_secrets) {
let window_id = ctx.window_id();
ToastStack::handle(ctx).update(ctx, |toast_stack, ctx| {
toast_stack.add_ephemeral_toast(
@@ -903,6 +904,19 @@ impl TypedActionView for MCPServersEditPageView {
return;
}
if parsed_servers
.iter()
.try_for_each(|parsed_server| {
self.detect_secrets_in_templatable_mcp_server(
ctx,
&parsed_server.templatable_mcp_server,
)
})
.is_err()
{
return;
}
for parsed_server in parsed_servers {
TemplatableMCPServerManager::handle(ctx).update(
ctx,
@@ -949,3 +963,22 @@ impl TypedActionView for MCPServersEditPageView {
}
}
}
/// Decide whether to block saving an MCP server config because secret
/// redaction is in force AND the parsed config contains secret-shaped strings.
///
/// We block only when redaction is actually active — either the user-level
/// Settings > Privacy > Secret redaction toggle is on, or the user's workspace
/// has enterprise enforcement enabled. With both off, the user has explicitly
/// opted to embed secrets in the config and we save it as written (#8761).
fn should_block_save_for_secrets(
safe_mode_enabled: bool,
enterprise_enforced: bool,
contains_secrets: bool,
) -> bool {
(safe_mode_enabled || enterprise_enforced) && contains_secrets
}
#[cfg(test)]
#[path = "edit_page_tests.rs"]
mod tests;
@@ -0,0 +1,48 @@
use super::should_block_save_for_secrets;
/// #8761: with redaction disabled and no enterprise enforcement, saving a
/// config that contains secrets must NOT be blocked.
#[test]
fn does_not_block_when_redaction_off_even_if_secrets_present() {
assert!(!should_block_save_for_secrets(false, false, true));
}
/// User-level toggle on AND secrets present → block. This is the case the
/// original check was written to catch; the redaction-aware predicate
/// must preserve it.
#[test]
fn blocks_when_user_redaction_on_and_secrets_present() {
assert!(should_block_save_for_secrets(true, false, true));
}
/// Enterprise enforcement alone is enough to gate the save, even if the
/// user toggled their personal redaction off — orgs that mandate redaction
/// must not be bypassed at the MCP-config layer.
#[test]
fn blocks_when_enterprise_enforced_and_secrets_present() {
assert!(should_block_save_for_secrets(false, true, true));
}
/// Configs without any detected secrets are never blocked, regardless of
/// the redaction-toggle state. The check is purely a guard against
/// accidentally persisting secrets — it has nothing to add when none exist.
#[test]
fn does_not_block_when_no_secrets_regardless_of_toggle() {
for safe_mode in [false, true] {
for enterprise in [false, true] {
assert!(
!should_block_save_for_secrets(safe_mode, enterprise, false),
"expected no block when contains_secrets=false \
(safe_mode={safe_mode}, enterprise={enterprise})",
);
}
}
}
/// Both toggles on AND secrets present → block. Defensive: equivalent to
/// either one being on, but exhaustively pinned for the full 2x2x2 sweep
/// of (safe_mode, enterprise, contains_secrets).
#[test]
fn blocks_when_both_redactions_on_and_secrets_present() {
assert!(should_block_save_for_secrets(true, true, true));
}
@@ -1,42 +1,37 @@
use std::collections::HashMap;
use markdown_parser::parse_markdown;
use galaxy_core::ui::external_product_icon::ExternalProductIcon;
use galaxy_core::ui::icons::Icon;
use warpui::elements::{
Align, Border, ChildView, ConstrainedBox, Container, CornerRadius, CrossAxisAlignment, Empty,
Flex, FormattedTextElement, HighlightedHyperlink, Hoverable, MainAxisAlignment,
MouseStateHandle, Padding, ParentElement, Radius, Shrinkable, Text,
};
use warpui::fonts::{Properties, Weight};
use warpui::keymap::Keystroke;
use warpui::platform::Cursor;
use warpui::ui_components::components::{UiComponent, UiComponentStyles};
use warpui::{
AppContext, Element, Entity, FocusContext, SingletonEntity, TypedActionView, View, ViewContext,
ViewHandle,
};
use crate::ai::mcp::templatable_installation::{VariableType, VariableValue};
use crate::ai::mcp::{TemplatableMCPServer, TemplatableMCPServerManager, TemplateVariable};
use crate::appearance::Appearance;
use crate::editor::Event as EditorEvent;
use crate::editor::{EditorView, SingleLineEditorOptions};
use crate::editor::{EditorView, Event as EditorEvent, SingleLineEditorOptions};
use crate::settings_view::mcp_servers::style::{
INSTALLATION_MODAL_BUTTON_GAP, INSTALLATION_MODAL_BUTTON_PADDING,
INSTALLATION_MODAL_INPUT_VERTICAL_SPACING, INSTALLATION_MODAL_LABEL_VERTICAL_SPACING,
INSTALLATION_MODAL_PADDING, INSTALLATION_MODAL_TITLE_VERTICAL_SPACING,
};
use crate::ui_components::avatar::{Avatar, AvatarContent};
use crate::ui_components::blended_colors;
use crate::view_components::action_button::{
ActionButton, KeystrokeSource, NakedTheme, PrimaryTheme,
};
use crate::view_components::dropdown::{Dropdown, DropdownItem};
use galaxyui::elements::Shrinkable;
use galaxyui::fonts::{Properties, Weight};
use galaxyui::ui_components::button::ButtonVariant;
use galaxyui::ui_components::components::{Coords, UiComponent, UiComponentStyles};
use galaxyui::{
elements::{
Align, Border, ChildView, ConstrainedBox, Container, CrossAxisAlignment, Empty, Flex,
FormattedTextElement, HighlightedHyperlink, Hoverable, MainAxisAlignment, MouseStateHandle,
ParentElement, Text,
},
platform::Cursor,
AppContext, Element, Entity, FocusContext, TypedActionView, View, ViewHandle,
};
use galaxyui::{SingletonEntity, ViewContext};
use markdown_parser::parse_markdown;
use crate::ai::mcp::{TemplatableMCPServer, TemplatableMCPServerManager, TemplateVariable};
use crate::ui_components::{
avatar::{Avatar, AvatarContent},
blended_colors,
};
use galaxyui::elements::{CornerRadius, Padding, Radius};
use galaxy_core::ui::{
color::coloru_with_opacity, external_product_icon::ExternalProductIcon, icons::Icon,
};
pub enum InstallationModalBodyEvent {
Cancel,
@@ -71,26 +66,35 @@ pub struct InstallationModalBody {
templatable_mcp_server: Option<TemplatableMCPServer>,
instructions_in_markdown: Option<String>,
variable_inputs: HashMap<String, VariableInput>,
cancel_mouse_state: MouseStateHandle,
install_mouse_state: MouseStateHandle,
cancel_button: ViewHandle<ActionButton>,
install_button: ViewHandle<ActionButton>,
close_button_mouse_state: MouseStateHandle,
is_shared: bool,
}
impl Default for InstallationModalBody {
fn default() -> Self {
Self::new()
}
}
impl InstallationModalBody {
pub fn new() -> Self {
pub fn new(ctx: &mut ViewContext<Self>) -> Self {
let cancel_button = ctx.add_typed_action_view(|_ctx| {
ActionButton::new("Cancel", NakedTheme).on_click(|ctx| {
ctx.dispatch_typed_action(InstallationModalBodyAction::Cancel);
})
});
let enter_keystroke = Keystroke::parse("enter").expect("valid keystroke");
let install_button = ctx.add_typed_action_view(|ctx| {
ActionButton::new("Install", PrimaryTheme)
.with_keybinding(KeystrokeSource::Fixed(enter_keystroke), ctx)
.on_click(|ctx| {
ctx.dispatch_typed_action(InstallationModalBodyAction::Install);
})
});
Self {
templatable_mcp_server: None,
instructions_in_markdown: None,
variable_inputs: HashMap::new(),
cancel_mouse_state: Default::default(),
install_mouse_state: Default::default(),
cancel_button,
install_button,
close_button_mouse_state: Default::default(),
is_shared: false,
}
@@ -440,89 +444,21 @@ impl InstallationModalBody {
.finish()
}
fn render_action_buttons(&self, appearance: &Appearance) -> Box<dyn Element> {
let cancel_button = appearance
.ui_builder()
.button(ButtonVariant::Text, self.cancel_mouse_state.clone())
.with_text_label("Cancel".into())
.with_style(UiComponentStyles {
font_weight: Some(Weight::Bold),
font_color: Some(appearance.theme().active_ui_text_color().into()),
..Default::default()
})
.with_hovered_styles(UiComponentStyles {
font_color: Some(appearance.theme().disabled_ui_text_color().into()),
..Default::default()
})
.build()
.with_cursor(Cursor::PointingHand)
.on_click(|ctx, _, _| ctx.dispatch_typed_action(InstallationModalBodyAction::Cancel))
.finish();
let corner_down_left_icon = Container::new(
ConstrainedBox::new(
Icon::CornerDownLeft
.to_galaxyui_icon(appearance.theme().active_ui_text_color())
.finish(),
)
.with_width(appearance.monospace_font_size())
.with_height(appearance.monospace_font_size())
.finish(),
)
.with_uniform_padding(2.)
.with_border(Border::all(1.).with_border_fill(coloru_with_opacity(
appearance.theme().active_ui_text_color().into(),
60,
)))
.with_corner_radius(CornerRadius::with_all(Radius::Pixels(4.)))
.finish();
let install_button_label = Flex::row()
.with_cross_axis_alignment(CrossAxisAlignment::Center)
.with_child(
Text::new_inline(
"Install",
appearance.ui_font_family(),
appearance.ui_font_size(),
)
.with_color(appearance.theme().active_ui_text_color().into())
.with_style(Properties::default().weight(Weight::Bold))
.finish(),
)
.with_child(
Container::new(corner_down_left_icon)
.with_margin_left(8.)
.finish(),
)
.finish();
let install_button = appearance
.ui_builder()
.button(ButtonVariant::Accent, self.install_mouse_state.clone())
.with_custom_label(install_button_label)
.with_style(UiComponentStyles {
padding: Some(Coords::uniform(5.).left(10.).right(10.)),
..Default::default()
})
.build()
.with_cursor(Cursor::PointingHand)
.on_click(|ctx, _, _| ctx.dispatch_typed_action(InstallationModalBodyAction::Install))
.finish();
fn render_action_buttons(&self) -> Box<dyn Element> {
Flex::row()
.with_cross_axis_alignment(CrossAxisAlignment::Center)
.with_child(
Container::new(cancel_button)
Container::new(ChildView::new(&self.cancel_button).finish())
.with_margin_right(INSTALLATION_MODAL_BUTTON_GAP)
.finish(),
)
.with_child(Container::new(install_button).finish())
.with_child(Container::new(ChildView::new(&self.install_button).finish()).finish())
.finish()
}
fn render_buttons_row(&self, appearance: &Appearance) -> Box<dyn Element> {
let source_indicator = Self::render_source_indicator(self.is_shared, appearance);
let action_buttons = self.render_action_buttons(appearance);
let action_buttons = self.render_action_buttons();
let spacer = Shrinkable::new(1., Container::new(Empty::new().finish()).finish()).finish();
+65 -71
View File
@@ -1,18 +1,32 @@
use crate::ai::mcp::templatable::GalleryData;
use crate::ai::mcp::MCPServerUpdate;
use crate::modal::Modal;
use crate::modal::ModalEvent;
use crate::modal::ModalViewState;
use crate::server::telemetry::{MCPTemplateInstallationSource, TelemetryEvent};
use crate::settings::{AISettings, AISettingsChangedEvent};
use crate::settings_view::mcp_servers_page::InstallOrigin;
use crate::settings_view::settings_page::{
build_toggle_element, render_body_item_label, LocalOnlyIconState, ToggleState,
};
use crate::util::truncation::truncate_from_end;
use crate::view_components::DismissibleToast;
use crate::ToastStack;
use std::cmp::Ordering;
use std::collections::HashMap;
use std::path::PathBuf;
use markdown_parser::{FormattedText, FormattedTextFragment, FormattedTextLine};
use settings::ToggleableSetting as _;
use strum::IntoEnumIterator;
use uuid::Uuid;
use galaxy_core::features::FeatureFlag;
use galaxy_core::send_telemetry_from_ctx;
use galaxy_core::ui::appearance::AppearanceEvent;
use galaxy_core::ui::theme::color::internal_colors;
use galaxy_core::ui::Icon;
use warpui::elements::{
Align, Border, ChildView, ConstrainedBox, Container, CornerRadius, CrossAxisAlignment,
Expanded, Fill, Flex, FormattedTextElement, HighlightedHyperlink, MainAxisAlignment,
MainAxisSize, ParentElement, Radius, Text,
};
use warpui::ui_components::components::{Coords, UiComponent, UiComponentStyles};
use warpui::ui_components::switch::SwitchStateHandle;
use warpui::{
AppContext, Element, Entity, SingletonEntity, TypedActionView, View, ViewContext, ViewHandle,
};
use crate::ai::mcp::gallery::MCPGalleryManagerEvent;
use crate::ai::mcp::templatable::{GalleryData, TemplatableMCPServer};
use crate::ai::mcp::templatable_manager::{
TemplatableMCPServerManager, TemplatableMCPServerManagerEvent,
};
#[cfg(feature = "local_fs")]
use crate::ai::mcp::{
// Import events for file-based manager and watcher conditionally
@@ -21,59 +35,39 @@ use crate::ai::mcp::{
FileMCPWatcher,
FileMCPWatcherEvent,
};
use crate::{
ai::mcp::{
gallery::MCPGalleryManagerEvent,
logs,
templatable::TemplatableMCPServer,
templatable_manager::{TemplatableMCPServerManager, TemplatableMCPServerManagerEvent},
FileBasedMCPManager, MCPGalleryManager, MCPProvider, TemplatableMCPServerInstallation,
},
appearance::Appearance,
cloud_object::{
model::persistence::{CloudModel, CloudModelEvent},
GenericStringObjectFormat, JsonObjectType,
},
drive::CloudObjectTypeAndId,
editor::{EditorView, PropagateAndNoOpNavigationKeys, SingleLineEditorOptions, TextOptions},
pane_group::Direction,
search_bar::SearchBar,
settings_view::mcp_servers::{
server_card::{
ServerCardEvent, ServerCardOptions, ServerCardStatus, ServerCardView, TitleChip,
},
style,
update_modal::{UpdateModalBody, UpdateModalBodyEvent},
ServerCardItemId,
},
ui_components::blended_colors,
view_components::action_button::{ActionButton, NakedTheme},
workflows::local_workflows::tail_command_for_shell,
workspace::Workspace,
workspaces::user_workspaces::UserWorkspaces,
use crate::ai::mcp::{
logs, FileBasedMCPManager, MCPGalleryManager, MCPProvider, MCPServerUpdate,
TemplatableMCPServerInstallation,
};
use galaxy_core::features::FeatureFlag;
use galaxy_core::send_telemetry_from_ctx;
use galaxy_core::ui::{appearance::AppearanceEvent, theme::color::internal_colors, Icon};
use galaxyui::{
elements::{
Align, Border, ChildView, ConstrainedBox, Container, CornerRadius, CrossAxisAlignment,
Expanded, Fill, Flex, FormattedTextElement, HighlightedHyperlink, MainAxisAlignment,
MainAxisSize, ParentElement, Radius, Text,
},
ui_components::{
components::{Coords, UiComponent, UiComponentStyles},
switch::SwitchStateHandle,
},
AppContext, Element, Entity, SingletonEntity, TypedActionView, View, ViewContext, ViewHandle,
use crate::appearance::Appearance;
use crate::cloud_object::model::persistence::{CloudModel, CloudModelEvent};
use crate::cloud_object::{GenericStringObjectFormat, JsonObjectType};
use crate::drive::CloudObjectTypeAndId;
use crate::editor::{
EditorView, PropagateAndNoOpNavigationKeys, SingleLineEditorOptions, TextOptions,
};
use markdown_parser::{FormattedText, FormattedTextFragment, FormattedTextLine};
use settings::ToggleableSetting as _;
use std::cmp::Ordering;
use std::{collections::HashMap, path::PathBuf};
use strum::IntoEnumIterator;
use uuid::Uuid;
use crate::modal::{Modal, ModalEvent, ModalViewState};
use crate::pane_group::Direction;
use crate::search_bar::SearchBar;
use crate::server::telemetry::{MCPTemplateInstallationSource, TelemetryEvent};
use crate::settings::{AISettings, AISettingsChangedEvent};
use crate::settings_view::mcp_servers::server_card::{
ServerCardEvent, ServerCardOptions, ServerCardStatus, ServerCardView, TitleChip,
};
use crate::settings_view::mcp_servers::update_modal::{UpdateModalBody, UpdateModalBodyEvent};
use crate::settings_view::mcp_servers::{style, ServerCardItemId};
use crate::settings_view::mcp_servers_page::InstallOrigin;
use crate::settings_view::settings_page::{
build_toggle_element, render_body_item_label, LocalOnlyIconState, ToggleState,
};
use crate::ui_components::blended_colors;
use crate::util::truncation::truncate_from_end;
use crate::view_components::action_button::{ActionButton, NakedTheme};
use crate::view_components::DismissibleToast;
use crate::workflows::local_workflows::tail_command_for_shell;
use crate::workspace::Workspace;
use crate::workspaces::user_workspaces::UserWorkspaces;
use crate::ToastStack;
const DESCRIPTION_TEXT: &str = "Add MCP servers to extend the Warp Agent's capabilities. MCP servers expose data sources or tools to agents through a standardized interface, essentially acting like plugins. Add a custom server, or use the presets to get started with popular servers. You can also find team servers that have been shared with you here. ";
@@ -187,7 +181,7 @@ impl MCPServersListPageView {
}
});
let update_modal_body = ctx.add_typed_action_view(|_ctx| UpdateModalBody::new());
let update_modal_body = ctx.add_typed_action_view(UpdateModalBody::new);
ctx.subscribe_to_view(&update_modal_body, |me, _, event, ctx| {
me.handle_update_modal_body_event(event, ctx);
});
@@ -778,7 +772,7 @@ impl MCPServersListPageView {
} else {
self.update_modal_state.view.update(ctx, |modal, ctx| {
modal.body().update(ctx, |body, ctx| {
body.set_installation(installation_uuid, server_name, available_updates);
body.set_installation(installation_uuid, server_name, available_updates, ctx);
ctx.notify();
});
});
@@ -997,8 +991,8 @@ impl MCPServersListPageView {
match event {
UpdateModalBodyEvent::Cancel => {
self.update_modal_state.view.update(ctx, |modal, ctx| {
modal.body().update(ctx, |body, _ctx| {
body.clear();
modal.body().update(ctx, |body, ctx| {
body.clear(ctx);
});
});
self.update_modal_state.close();
@@ -1015,8 +1009,8 @@ impl MCPServersListPageView {
};
self.process_server_update(*installation_uuid, update.clone(), ctx);
self.update_modal_state.view.update(ctx, |modal, ctx| {
modal.body().update(ctx, |body, _ctx| {
body.clear();
modal.body().update(ctx, |body, ctx| {
body.clear(ctx);
});
});
self.update_modal_state.close();
+2 -4
View File
@@ -1,7 +1,5 @@
use std::{
cmp::Ordering,
fmt::{Display, Formatter, Result},
};
use std::cmp::Ordering;
use std::fmt::{Display, Formatter, Result};
use uuid::Uuid;
@@ -1,45 +1,33 @@
use galaxy_core::{
features::FeatureFlag,
ui::{
external_product_icon::ExternalProductIcon,
icons::{Icon, ICON_DIMENSIONS},
theme::{color::internal_colors, AnsiColorIdentifier},
},
};
use galaxyui::{
accessibility::ActionAccessibilityContent,
elements::{
Border, ConstrainedBox, Container, CornerRadius, CrossAxisAlignment, Expanded, Fill, Flex,
FormattedTextElement, HighlightedHyperlink, Hoverable, MainAxisAlignment, MainAxisSize,
MouseState, MouseStateHandle, Padding, ParentElement, Radius, Text, Wrap,
},
fonts::Weight,
platform::Cursor,
ui_components::{
button::ButtonVariant,
chip::Chip,
components::{Coords, UiComponent, UiComponentStyles},
switch::SwitchStateHandle,
},
AppContext, Element, Entity, SingletonEntity, TypedActionView, View, ViewContext,
};
use markdown_parser::{FormattedText, FormattedTextFragment, FormattedTextLine};
use pathfinder_color::ColorU;
use crate::{
ai::mcp::{
templatable::CloudTemplatableMCPServer, MCPServerState, TemplatableMCPServerManager,
},
appearance::Appearance,
cloud_object::CloudObject,
settings_view::mcp_servers::{style, ServerCardItemId},
ui_components::{
avatar::{Avatar, AvatarContent, StatusElementTypes},
blended_colors,
buttons::icon_button,
red_notification_dot::RedNotificationDot,
},
use galaxy_core::features::FeatureFlag;
use galaxy_core::ui::external_product_icon::ExternalProductIcon;
use galaxy_core::ui::icons::{Icon, ICON_DIMENSIONS};
use galaxy_core::ui::theme::color::internal_colors;
use galaxy_core::ui::theme::AnsiColorIdentifier;
use galaxyui::accessibility::ActionAccessibilityContent;
use galaxyui::elements::{
Border, ConstrainedBox, Container, CornerRadius, CrossAxisAlignment, Expanded, Fill, Flex,
FormattedTextElement, HighlightedHyperlink, Hoverable, MainAxisAlignment, MainAxisSize,
MouseState, MouseStateHandle, Padding, ParentElement, Radius, Text, Wrap,
};
use galaxyui::fonts::Weight;
use galaxyui::platform::Cursor;
use galaxyui::ui_components::button::ButtonVariant;
use galaxyui::ui_components::chip::Chip;
use galaxyui::ui_components::components::{Coords, UiComponent, UiComponentStyles};
use galaxyui::ui_components::switch::SwitchStateHandle;
use galaxyui::{AppContext, Element, Entity, SingletonEntity, TypedActionView, View, ViewContext};
use crate::ai::mcp::templatable::CloudTemplatableMCPServer;
use crate::ai::mcp::{MCPServerState, TemplatableMCPServerManager};
use crate::appearance::Appearance;
use crate::cloud_object::{CloudObject, CloudObjectUuidLookup as _};
use crate::settings_view::mcp_servers::{style, ServerCardItemId};
use crate::ui_components::avatar::{Avatar, AvatarContent, StatusElementTypes};
use crate::ui_components::blended_colors;
use crate::ui_components::buttons::icon_button;
use crate::ui_components::red_notification_dot::RedNotificationDot;
/// A chip displayed inline with the server card title, optionally with a leading icon.
#[derive(Debug, Clone)]
+2 -4
View File
@@ -1,8 +1,6 @@
use galaxy_core::ui::appearance::Appearance;
use galaxyui::{
fonts::Weight,
ui_components::components::{Coords, UiComponentStyles},
};
use galaxyui::fonts::Weight;
use galaxyui::ui_components::components::{Coords, UiComponentStyles};
pub const ICON_MARGIN: f32 = 8.;
pub const HEADER_FONT_SIZE: f32 = 18.;
+71 -106
View File
@@ -1,3 +1,21 @@
use chrono::{Local, TimeZone};
use uuid::Uuid;
use galaxy_core::ui::external_product_icon::ExternalProductIcon;
use galaxy_core::ui::icons::Icon;
use galaxy_core::ui::theme::color::internal_colors;
use warpui::elements::{
Align, Border, ChildView, ConstrainedBox, Container, CornerRadius, CrossAxisAlignment, Empty,
Flex, Hoverable, MainAxisAlignment, MouseStateHandle, Padding, ParentElement, Radius,
Shrinkable, Text,
};
use warpui::fonts::{Properties, Weight};
use warpui::keymap::Keystroke;
use warpui::platform::Cursor;
use warpui::ui_components::components::{UiComponent, UiComponentStyles};
use warpui::{
AppContext, Element, Entity, SingletonEntity, TypedActionView, View, ViewContext, ViewHandle,
};
use crate::ai::mcp::{Author, MCPServerUpdate};
use crate::appearance::Appearance;
use crate::settings_view::mcp_servers::style::{
@@ -6,25 +24,9 @@ use crate::settings_view::mcp_servers::style::{
use crate::ui_components::avatar::{Avatar, AvatarContent};
use crate::ui_components::blended_colors;
use crate::util::time_format::format_approx_duration_from_now;
use chrono::{Local, TimeZone};
use galaxy_core::ui::color::coloru_with_opacity;
use galaxy_core::ui::external_product_icon::ExternalProductIcon;
use galaxy_core::ui::icons::Icon;
use galaxy_core::ui::theme::color::internal_colors;
use galaxyui::elements::{Align, Empty, Padding, Shrinkable};
use galaxyui::fonts::{Properties, Weight};
use galaxyui::ui_components::button::ButtonVariant;
use galaxyui::ui_components::components::{Coords, UiComponent, UiComponentStyles};
use galaxyui::SingletonEntity;
use galaxyui::{
elements::{
Border, ConstrainedBox, Container, CornerRadius, CrossAxisAlignment, Flex, Hoverable,
MainAxisAlignment, MouseStateHandle, ParentElement, Radius, Text,
},
platform::Cursor,
AppContext, Element, Entity, TypedActionView, View, ViewContext,
use crate::view_components::action_button::{
ActionButton, KeystrokeSource, NakedTheme, PrimaryTheme,
};
use uuid::Uuid;
pub enum UpdateModalBodyEvent {
Cancel,
@@ -41,21 +43,47 @@ pub enum UpdateModalBodyAction {
SelectOption(usize),
}
#[derive(Default)]
pub struct UpdateModalBody {
installation_uuid: Option<Uuid>,
server_name: Option<String>,
update_options: Vec<MCPServerUpdate>,
selected_updates: Vec<bool>,
cancel_mouse_state: MouseStateHandle,
update_mouse_state: MouseStateHandle,
cancel_button: ViewHandle<ActionButton>,
update_button: ViewHandle<ActionButton>,
close_button_mouse_state: MouseStateHandle,
option_mouse_states: Vec<MouseStateHandle>,
}
impl UpdateModalBody {
pub fn new() -> Self {
Default::default()
pub fn new(ctx: &mut ViewContext<Self>) -> Self {
let cancel_button = ctx.add_typed_action_view(|_ctx| {
ActionButton::new("Cancel", NakedTheme).on_click(|ctx| {
ctx.dispatch_typed_action(UpdateModalBodyAction::Cancel);
})
});
let enter_keystroke = Keystroke::parse("enter").expect("valid keystroke");
let update_button = ctx.add_typed_action_view(|ctx| {
let mut button = ActionButton::new("Update", PrimaryTheme)
.with_keybinding(KeystrokeSource::Fixed(enter_keystroke), ctx)
.on_click(|ctx| {
ctx.dispatch_typed_action(UpdateModalBodyAction::Update);
});
// Initial state has no rows selected, so the button starts disabled.
button.set_disabled(true, ctx);
button
});
Self {
installation_uuid: None,
server_name: None,
update_options: vec![],
selected_updates: vec![],
cancel_button,
update_button,
close_button_mouse_state: Default::default(),
option_mouse_states: vec![],
}
}
pub fn set_installation(
@@ -63,6 +91,7 @@ impl UpdateModalBody {
installation_uuid: Uuid,
server_name: String,
update_options: Vec<MCPServerUpdate>,
ctx: &mut ViewContext<Self>,
) {
self.installation_uuid = Some(installation_uuid);
self.server_name = Some(server_name);
@@ -71,14 +100,25 @@ impl UpdateModalBody {
self.option_mouse_states = (0..self.update_options.len())
.map(|_| MouseStateHandle::default())
.collect();
// No rows are selected yet, so the Update button must start disabled.
self.refresh_update_button_disabled(ctx);
}
pub fn clear(&mut self) {
pub fn clear(&mut self, ctx: &mut ViewContext<Self>) {
self.installation_uuid = None;
self.server_name = None;
self.update_options = vec![];
self.selected_updates = vec![];
self.option_mouse_states = vec![];
self.refresh_update_button_disabled(ctx);
}
/// Sync the Update button's disabled state with the current selection.
fn refresh_update_button_disabled(&mut self, ctx: &mut ViewContext<Self>) {
let has_selection = self.selected_updates.iter().any(|&x| x);
self.update_button.update(ctx, |button, ctx| {
button.set_disabled(!has_selection, ctx);
});
}
fn render_title(&self, appearance: &Appearance) -> Box<dyn Element> {
@@ -298,97 +338,20 @@ impl UpdateModalBody {
.finish()
}
fn render_action_buttons(&self, appearance: &Appearance) -> Box<dyn Element> {
let cancel_button = appearance
.ui_builder()
.button(ButtonVariant::Text, self.cancel_mouse_state.clone())
.with_text_label("Cancel".into())
.with_style(UiComponentStyles {
font_weight: Some(Weight::Bold),
font_color: Some(appearance.theme().active_ui_text_color().into()),
..Default::default()
})
.with_hovered_styles(UiComponentStyles {
font_color: Some(appearance.theme().disabled_ui_text_color().into()),
..Default::default()
})
.build()
.with_cursor(Cursor::PointingHand)
.on_click(|ctx, _, _| ctx.dispatch_typed_action(UpdateModalBodyAction::Cancel))
.finish();
let corner_down_left_icon = Container::new(
ConstrainedBox::new(
Icon::CornerDownLeft
.to_galaxyui_icon(appearance.theme().active_ui_text_color())
.finish(),
)
.with_width(appearance.monospace_font_size())
.with_height(appearance.monospace_font_size())
.finish(),
)
.with_uniform_padding(2.)
.with_border(Border::all(1.).with_border_fill(coloru_with_opacity(
appearance.theme().active_ui_text_color().into(),
60,
)))
.with_corner_radius(CornerRadius::with_all(Radius::Pixels(4.)))
.finish();
let update_button_label = Flex::row()
.with_cross_axis_alignment(CrossAxisAlignment::Center)
.with_child(
Text::new_inline(
"Update",
appearance.ui_font_family(),
appearance.ui_font_size(),
)
.with_color(appearance.theme().active_ui_text_color().into())
.with_style(Properties::default().weight(Weight::Bold))
.finish(),
)
.with_child(
Container::new(corner_down_left_icon)
.with_margin_left(8.)
.finish(),
)
.finish();
let mut update_button_builder = appearance
.ui_builder()
.button(ButtonVariant::Accent, self.update_mouse_state.clone())
.with_custom_label(update_button_label)
.with_style(UiComponentStyles {
padding: Some(Coords::uniform(5.).left(10.).right(10.)),
..Default::default()
});
// Disable the update button if no updates are selected
let has_selection = self.selected_updates.iter().any(|&x| x);
if !has_selection {
update_button_builder = update_button_builder.disabled();
}
let update_button = update_button_builder
.build()
.with_cursor(Cursor::PointingHand)
.on_click(|ctx, _, _| ctx.dispatch_typed_action(UpdateModalBodyAction::Update))
.finish();
fn render_action_buttons(&self) -> Box<dyn Element> {
Flex::row()
.with_cross_axis_alignment(CrossAxisAlignment::Center)
.with_child(
Container::new(cancel_button)
Container::new(ChildView::new(&self.cancel_button).finish())
.with_margin_right(INSTALLATION_MODAL_BUTTON_GAP)
.finish(),
)
.with_child(Container::new(update_button).finish())
.with_child(Container::new(ChildView::new(&self.update_button).finish()).finish())
.finish()
}
fn render_buttons_row(&self, appearance: &Appearance) -> Box<dyn Element> {
let action_buttons = self.render_action_buttons(appearance);
let action_buttons = self.render_action_buttons();
let spacer = Shrinkable::new(1., Container::new(Empty::new().finish()).finish()).finish();
@@ -476,9 +439,11 @@ impl TypedActionView for UpdateModalBody {
}
}
UpdateModalBodyAction::SelectOption(index) => {
// Toggle the selection at the given index
// Toggle the selection at the given index, then sync the
// Update button's disabled state with the new selection.
if let Some(selected) = self.selected_updates.get_mut(*index) {
*selected = !*selected;
self.refresh_update_button_disabled(ctx);
ctx.notify();
}
}