M2 audit: excise managed connectors and xAI media-gen tools
Managed connectors (grok.com MCP admin) removed root-and-branch: - The managed-MCP fetch/injection pipeline is gone, including the whole kigi-shell-session-support crate (managed-config fetch client, gateway tool catalog + dispatch, header injection, refresh task), reactive managed re-auth, mcp_doctor's grok.com-source discovery, and the [managed_mcps] config surface. - TUI: the 'Managed by grok.com' section, connectors URL/deep-link, Action::OpenManagedConnectors, and session_team_id are gone. Local MCP management (list/toggle/add/remove/auth/tools) is fully intact. - Kept as LOCAL policy: managed-settings.json MCP allow/deny enforcement, the multi-source local MCP merge, folder-trust gating. PluginOrigin Project/User labels kept (they tag locally discovered plugin dirs). imagine/media-gen tools (xAI image/video generation) removed: - image_gen, image_edit, video_gen, image_to_video, reference_to_video implementations, registrations, ToolKind/ToolInput/Output variants (serde-safe), config plumbing end to end, ZDR video machinery, /imagine + /imagine-video commands and guidance text, the bundled imagine skill (added to legacy cleanup so user installs delete it), and the media-gen render path. - Kept: image INPUT (paste/attach, [Image #N] meta, pdf/image fetch, clipboard wrap), generic media-ref rendering, and the generic tool 401-retry machinery (tests renamed, assertions unweakened). - deploy_app stays: it is a permanently-disabled local stub deploying nowhere. 121 files changed, 8 deleted. Gates: workspace check/clippy 0/0, fmt, deny ok; suites green (tools 2554, shell 4862, tui 6608, workspace 1042). Remaining grok.com strings live only in the auth-method ids and changelog archives (§9/M3 sweep).
This commit is contained in:
@@ -1106,7 +1106,6 @@ fn render_location_picker(
|
||||
badge: badge.as_str(),
|
||||
badge_color: (!badge.is_empty()).then_some(theme.accent_user),
|
||||
collapsible: false,
|
||||
underline_last_desc: false,
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
|
||||
@@ -455,8 +455,6 @@ pub enum ButtonAction {
|
||||
ReloadSkills,
|
||||
/// Refresh MCP server list (re-fetch from shell).
|
||||
RefreshMcpList,
|
||||
/// Open grok.com connectors page (MCP tab: press `o`).
|
||||
OpenManagedConnectors,
|
||||
/// Update (fetch latest from source) the selected plugin.
|
||||
UpdateSelectedPlugin,
|
||||
/// Uninstall the selected plugin.
|
||||
@@ -1586,8 +1584,6 @@ pub struct ExtensionsModalState {
|
||||
/// should be mutated by input handlers; the window's copy is a
|
||||
/// rendering hint synced each frame.
|
||||
pub active_tab: ExtensionsTab,
|
||||
/// Session team principal for managed-connectors deep links in section copy.
|
||||
pub session_team_id: Option<String>,
|
||||
/// Hooks list data (fetched from shell).
|
||||
pub hooks_data: TabDataState<kigi_hooks_plugins_types::HooksListResponse>,
|
||||
/// Plugins list data (fetched from shell).
|
||||
@@ -1683,7 +1679,6 @@ impl ExtensionsModalState {
|
||||
Self {
|
||||
window: ModalWindowState::with_tabs(ExtensionsTab::ALL.len()),
|
||||
active_tab: tab,
|
||||
session_team_id: None,
|
||||
hooks_data: TabDataState::Loading,
|
||||
plugins_data: TabDataState::Loading,
|
||||
button_areas: Vec::new(),
|
||||
@@ -2544,8 +2539,7 @@ pub fn render_extensions_modal(
|
||||
ExtensionsTab::McpServers => {
|
||||
if let TabDataState::Loaded(ref servers) = state.mcps_data {
|
||||
use crate::views::mcps_modal::{
|
||||
McpSectionId, section_description_lines, section_for, section_key,
|
||||
section_label,
|
||||
McpSectionId, section_for, section_key, section_label,
|
||||
};
|
||||
|
||||
init_mcps_section_collapse_on_first_load(
|
||||
@@ -2582,10 +2576,7 @@ pub fn render_extensions_modal(
|
||||
);
|
||||
entry_labels.push(section_label(section_id, section_servers.len()));
|
||||
entry_right_labels.push(String::new());
|
||||
entry_desc_lines.push(section_description_lines(
|
||||
section_id,
|
||||
state.session_team_id.as_deref(),
|
||||
));
|
||||
entry_desc_lines.push(vec![]);
|
||||
entry_summary_lines.push(vec![]);
|
||||
entry_fields.push(vec![]);
|
||||
entry_is_header.push(false);
|
||||
@@ -2990,20 +2981,6 @@ pub fn render_extensions_modal(
|
||||
Rect::new(content_area.x, content_area.y, content_area.width, 1)
|
||||
};
|
||||
|
||||
// Underline the Managed section's last description line (the connectors URL) as a link affordance.
|
||||
let managed_section_key =
|
||||
crate::views::mcps_modal::section_key(&crate::views::mcps_modal::McpSectionId::Managed);
|
||||
// `underline_last_desc` and the recorded click band both assume the URL is the
|
||||
// LAST Managed description line; trip a test if that ever stops holding.
|
||||
debug_assert!(
|
||||
crate::views::mcps_modal::section_description_lines(
|
||||
&crate::views::mcps_modal::McpSectionId::Managed,
|
||||
state.session_team_id.as_deref(),
|
||||
)
|
||||
.last()
|
||||
.is_some_and(|l| l.starts_with('[') && l.ends_with(']')),
|
||||
"Managed section's last description line must be the bracketed connectors URL",
|
||||
);
|
||||
let picker_entries: Vec<picker::PickerEntry<'_>> = entry_labels
|
||||
.iter()
|
||||
.enumerate()
|
||||
@@ -3039,7 +3016,6 @@ pub fn render_extensions_modal(
|
||||
badge: entry_badge_text.get(i).map(|s| s.as_str()).unwrap_or(""),
|
||||
badge_color: entry_badge_color.get(i).copied().flatten(),
|
||||
collapsible: is_collapsible,
|
||||
underline_last_desc: group_key.is_some_and(|k| *k == managed_section_key),
|
||||
})
|
||||
}
|
||||
})
|
||||
@@ -3062,7 +3038,6 @@ pub fn render_extensions_modal(
|
||||
// below own the entries area instead.
|
||||
let (item_rects, entry_indices) = if in_input_mode {
|
||||
// No picker render in input mode: clear any stale recorded link band.
|
||||
state.picker_state.link_band = None;
|
||||
(Vec::new(), Vec::new())
|
||||
} else {
|
||||
let content_hit = picker::render_picker_content_with_scrollbar_x(
|
||||
@@ -3502,7 +3477,7 @@ mod tests {
|
||||
let mask = build_entry_non_selectable(
|
||||
&[false, false, true],
|
||||
&[
|
||||
Some("mcp-section:managed".into()),
|
||||
Some("mcp-section:local".into()),
|
||||
Some("mcp-tools:0".into()),
|
||||
None,
|
||||
],
|
||||
@@ -3519,7 +3494,7 @@ mod tests {
|
||||
#[test]
|
||||
fn build_entry_non_selectable_clickable_is_empty_for_mcp_sections() {
|
||||
let mask = build_entry_non_selectable_clickable(&[
|
||||
Some("mcp-section:managed".into()),
|
||||
Some("mcp-section:local".into()),
|
||||
Some("mcp-tools:0".into()),
|
||||
None,
|
||||
]);
|
||||
@@ -3568,8 +3543,8 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
// Fixture layout (managed section, two servers with tools):
|
||||
// 0 section header group_key=Some("mcp-section:managed") data=None
|
||||
// Fixture layout (local section, two servers with tools):
|
||||
// 0 section header group_key=Some("mcp-section:local") data=None
|
||||
// 1 server 0 header group_key=Some("mcp-tools:0") data=Some(0)
|
||||
// 2 tool 0 of svr 0 group_key=None data=Some(0)
|
||||
// 3 tool 1 of svr 0 group_key=None data=Some(0)
|
||||
@@ -3579,7 +3554,7 @@ mod tests {
|
||||
let mut state = ExtensionsModalState::new(ExtensionsTab::McpServers);
|
||||
state.entry_data_indices = vec![None, Some(0), Some(0), Some(0), Some(1), Some(1)];
|
||||
state.entry_group_keys = vec![
|
||||
Some("mcp-section:managed".to_string()),
|
||||
Some("mcp-section:local".to_string()),
|
||||
Some("mcp-tools:0".to_string()),
|
||||
None,
|
||||
None,
|
||||
@@ -3612,12 +3587,12 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mcp_section_managed_collapsed_hides_child_servers() {
|
||||
fn mcp_section_local_collapsed_hides_child_servers() {
|
||||
let mut collapsed = std::collections::HashSet::new();
|
||||
collapsed.insert("mcp-section:managed".to_string());
|
||||
collapsed.insert("mcp-section:local".to_string());
|
||||
assert!(mcp_section_children_hidden(
|
||||
&collapsed,
|
||||
"mcp-section:managed",
|
||||
"mcp-section:local",
|
||||
false
|
||||
));
|
||||
}
|
||||
@@ -3625,10 +3600,10 @@ mod tests {
|
||||
#[test]
|
||||
fn mcp_section_search_forces_children_visible() {
|
||||
let mut collapsed = std::collections::HashSet::new();
|
||||
collapsed.insert("mcp-section:managed".to_string());
|
||||
collapsed.insert("mcp-section:local".to_string());
|
||||
assert!(!mcp_section_children_hidden(
|
||||
&collapsed,
|
||||
"mcp-section:managed",
|
||||
"mcp-section:local",
|
||||
true
|
||||
));
|
||||
}
|
||||
@@ -3638,9 +3613,9 @@ mod tests {
|
||||
let mut state = ExtensionsModalState::new(ExtensionsTab::McpServers);
|
||||
state
|
||||
.mcps_collapsed_sections
|
||||
.insert("mcp-section:managed".to_string());
|
||||
.insert("mcp-section:local".to_string());
|
||||
state.picker_state.query = "linear".into();
|
||||
assert!(state.is_group_expanded(0, "mcp-section:managed"));
|
||||
assert!(state.is_group_expanded(0, "mcp-section:local"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -3655,7 +3630,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn mcp_auth_intercept_on_expand_detects_auth_required_server() {
|
||||
use crate::views::mcps_modal::{McpServerDisplayStatus, McpServerInfo, McpWireSource};
|
||||
use crate::views::mcps_modal::{McpServerDisplayStatus, McpServerInfo};
|
||||
|
||||
let mut state = ExtensionsModalState::new(ExtensionsTab::McpServers);
|
||||
state.mcps_data = TabDataState::Loaded(vec![McpServerInfo {
|
||||
@@ -3666,16 +3641,11 @@ mod tests {
|
||||
auth_required: true,
|
||||
tools: vec![],
|
||||
enabled: true,
|
||||
source: "managed".into(),
|
||||
wire_source: McpWireSource::Managed,
|
||||
source: "local".into(),
|
||||
plugin_name: None,
|
||||
is_managed_gateway: false,
|
||||
}]);
|
||||
state.entry_data_indices = vec![None, Some(0)];
|
||||
state.entry_group_keys = vec![
|
||||
Some("mcp-section:managed".into()),
|
||||
Some("mcp-tools:0".into()),
|
||||
];
|
||||
state.entry_group_keys = vec![Some("mcp-section:local".into()), Some("mcp-tools:0".into())];
|
||||
state.picker_state.selected = 1;
|
||||
assert!(
|
||||
state.mcp_auth_intercept_on_expand(),
|
||||
@@ -3692,7 +3662,6 @@ mod tests {
|
||||
|
||||
fn make_mcp_server_for_rows(
|
||||
name: &str,
|
||||
wire: crate::views::mcps_modal::McpWireSource,
|
||||
tools: Vec<(&str, bool)>,
|
||||
) -> crate::views::mcps_modal::McpServerInfo {
|
||||
use crate::views::mcps_modal::{McpServerDisplayStatus, McpToolDetail};
|
||||
@@ -3715,22 +3684,15 @@ mod tests {
|
||||
tools: tool_details,
|
||||
enabled: true,
|
||||
source: "local".into(),
|
||||
wire_source: wire,
|
||||
plugin_name: None,
|
||||
is_managed_gateway: false,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mcp_collapsed_managed_section_omits_server_rows() {
|
||||
use crate::views::mcps_modal::McpWireSource;
|
||||
|
||||
let servers = vec![
|
||||
make_mcp_server_for_rows("grok_com_linear", McpWireSource::Managed, vec![]),
|
||||
make_mcp_server_for_rows("local-srv", McpWireSource::Local, vec![]),
|
||||
];
|
||||
fn mcp_collapsed_local_section_omits_server_rows() {
|
||||
let servers = vec![make_mcp_server_for_rows("local-srv", vec![])];
|
||||
let mut collapsed = std::collections::HashSet::new();
|
||||
collapsed.insert("mcp-section:managed".to_string());
|
||||
collapsed.insert("mcp-section:local".to_string());
|
||||
let rows = build_mcp_servers_picker_rows(
|
||||
&servers,
|
||||
"",
|
||||
@@ -3738,34 +3700,21 @@ mod tests {
|
||||
&collapsed,
|
||||
&std::collections::HashSet::new(),
|
||||
);
|
||||
assert!(
|
||||
rows.labels
|
||||
.iter()
|
||||
.any(|l| l.starts_with("Managed by grok.com")),
|
||||
"managed section header must appear"
|
||||
);
|
||||
assert!(
|
||||
!rows.labels.iter().any(|l| l == "grok_com_linear"),
|
||||
"servers in collapsed managed section must be omitted"
|
||||
);
|
||||
assert!(
|
||||
rows.labels.iter().any(|l| l.starts_with("Local")),
|
||||
"local section should still render"
|
||||
"local section header must appear"
|
||||
);
|
||||
assert!(
|
||||
!rows.labels.iter().any(|l| l == "local-srv"),
|
||||
"servers in collapsed local section must be omitted"
|
||||
);
|
||||
assert!(rows.labels.iter().any(|l| l == "local-srv"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mcp_tool_rows_emitted_when_tools_expanded_by_server_index() {
|
||||
use crate::views::mcps_modal::McpWireSource;
|
||||
|
||||
let servers = vec![
|
||||
make_mcp_server_for_rows(
|
||||
"alpha",
|
||||
McpWireSource::Managed,
|
||||
vec![("tool-a1", true), ("tool-a2", true)],
|
||||
),
|
||||
make_mcp_server_for_rows("beta", McpWireSource::Managed, vec![("tool-b1", true)]),
|
||||
make_mcp_server_for_rows("alpha", vec![("tool-a1", true), ("tool-a2", true)]),
|
||||
make_mcp_server_for_rows("beta", vec![("tool-b1", true)]),
|
||||
];
|
||||
let mut tools_expanded = std::collections::HashSet::new();
|
||||
tools_expanded.insert(0);
|
||||
@@ -3792,7 +3741,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn mcps_plugin_sections_collapsed_on_first_load() {
|
||||
use crate::views::mcps_modal::{McpServerDisplayStatus, McpServerInfo, McpWireSource};
|
||||
use crate::views::mcps_modal::{McpServerDisplayStatus, McpServerInfo};
|
||||
|
||||
let servers = vec![
|
||||
McpServerInfo {
|
||||
@@ -3804,9 +3753,7 @@ mod tests {
|
||||
tools: vec![],
|
||||
enabled: true,
|
||||
source: "plugin: alpha".into(),
|
||||
wire_source: McpWireSource::Local,
|
||||
plugin_name: Some("alpha".into()),
|
||||
is_managed_gateway: false,
|
||||
},
|
||||
McpServerInfo {
|
||||
name: "p2-srv".into(),
|
||||
@@ -3817,9 +3764,7 @@ mod tests {
|
||||
tools: vec![],
|
||||
enabled: true,
|
||||
source: "plugin: beta".into(),
|
||||
wire_source: McpWireSource::Local,
|
||||
plugin_name: Some("beta".into()),
|
||||
is_managed_gateway: false,
|
||||
},
|
||||
];
|
||||
let mut state = ExtensionsModalState::new(ExtensionsTab::McpServers);
|
||||
@@ -3827,11 +3772,6 @@ mod tests {
|
||||
!state.mcps_collapsed_sections.contains("mcp-section:local"),
|
||||
"Local section starts expanded by default for a less noisy initial view"
|
||||
);
|
||||
assert!(
|
||||
!state
|
||||
.mcps_collapsed_sections
|
||||
.contains("mcp-section:managed")
|
||||
);
|
||||
init_mcps_section_collapse_on_first_load(
|
||||
&mut state.mcps_collapsed_sections,
|
||||
&mut state.mcps_section_collapse_initialized,
|
||||
|
||||
@@ -1,15 +1,8 @@
|
||||
//! MCP server data types, status enum, response conversion, and section
|
||||
//! presentation helpers (labels, description lines, connectors URLs).
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum McpWireSource {
|
||||
Managed,
|
||||
Local,
|
||||
}
|
||||
//! presentation helpers (labels).
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum McpSectionId {
|
||||
Managed,
|
||||
Plugin(String),
|
||||
Local,
|
||||
}
|
||||
@@ -24,9 +17,6 @@ impl Ord for McpSectionId {
|
||||
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
|
||||
use std::cmp::Ordering;
|
||||
match (self, other) {
|
||||
(Self::Managed, Self::Managed) => Ordering::Equal,
|
||||
(Self::Managed, _) => Ordering::Less,
|
||||
(_, Self::Managed) => Ordering::Greater,
|
||||
(Self::Plugin(a), Self::Plugin(b)) => a.cmp(b),
|
||||
(Self::Plugin(_), Self::Local) => Ordering::Less,
|
||||
(Self::Local, Self::Plugin(_)) => Ordering::Greater,
|
||||
@@ -38,86 +28,28 @@ impl Ord for McpSectionId {
|
||||
/// Collapse/expand key for a section header row in the MCP servers tab.
|
||||
pub fn section_key(section: &McpSectionId) -> String {
|
||||
match section {
|
||||
McpSectionId::Managed => "mcp-section:managed".into(),
|
||||
McpSectionId::Plugin(name) => format!("mcp-section:plugin:{name}"),
|
||||
McpSectionId::Local => "mcp-section:local".into(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Display label for a section header, e.g. `"Managed by grok.com (3)"`.
|
||||
/// Display label for a section header, e.g. `"Local (3)"`.
|
||||
pub fn section_label(section: &McpSectionId, count: usize) -> String {
|
||||
match section {
|
||||
McpSectionId::Managed => format!("Managed by grok.com ({count})"),
|
||||
McpSectionId::Plugin(name) => format!("Plugin: {name} ({count})"),
|
||||
McpSectionId::Local => format!("Local ({count})"),
|
||||
}
|
||||
}
|
||||
|
||||
/// Base grok.com connectors URL (no team). Prefer [`managed_connectors_url`] when opening.
|
||||
pub const MANAGED_SECTION_CONNECTORS_URL: &str = "https://grok.com/connectors";
|
||||
|
||||
/// Connectors deep link, appending percent-encoded `teamId` when the session is a team principal.
|
||||
pub fn managed_connectors_url(team_id: Option<&str>) -> String {
|
||||
match team_id.filter(|id| !id.is_empty()) {
|
||||
Some(id) => format!(
|
||||
"{MANAGED_SECTION_CONNECTORS_URL}?teamId={}",
|
||||
urlencoding::encode(id)
|
||||
),
|
||||
None => MANAGED_SECTION_CONNECTORS_URL.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Display form of [`managed_connectors_url`] with the `https://` scheme dropped.
|
||||
///
|
||||
/// Used for the Managed section subtitle so the URL is shorter and more likely
|
||||
/// to fit on one row; the Ctrl+O action still opens the full-scheme URL.
|
||||
pub fn managed_connectors_url_display(team_id: Option<&str>) -> String {
|
||||
let url = managed_connectors_url(team_id);
|
||||
url.strip_prefix("https://").unwrap_or(&url).to_string()
|
||||
}
|
||||
|
||||
/// Description lines shown under the Managed section header (when expanded).
|
||||
/// `team_id` matches the Ctrl+O / open-connectors deep link for the session.
|
||||
pub fn section_description_lines(section: &McpSectionId, team_id: Option<&str>) -> Vec<String> {
|
||||
match section {
|
||||
McpSectionId::Managed => {
|
||||
let url = managed_connectors_url_display(team_id);
|
||||
vec![
|
||||
"Add, remove, or manage connectors. Ctrl+O to open or go to:".into(),
|
||||
format!("[{url}]"),
|
||||
]
|
||||
}
|
||||
McpSectionId::Plugin(_) | McpSectionId::Local => vec![],
|
||||
}
|
||||
}
|
||||
|
||||
/// Classify a server into a UI section.
|
||||
///
|
||||
/// Priority: `grok_com_` prefix or managed wire source → Managed; else plugin
|
||||
/// label → Plugin; else Local. A managed server with a plugin display label
|
||||
/// still lands in Managed.
|
||||
/// Classify a server into a UI section: plugin label → Plugin; else Local.
|
||||
pub fn section_for(server: &McpServerInfo) -> McpSectionId {
|
||||
if server.name.starts_with("grok_com_") || server.wire_source == McpWireSource::Managed {
|
||||
McpSectionId::Managed
|
||||
} else if let Some(ref name) = server.plugin_name {
|
||||
if let Some(ref name) = server.plugin_name {
|
||||
McpSectionId::Plugin(name.clone())
|
||||
} else {
|
||||
McpSectionId::Local
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether the user may delete this server from local config.
|
||||
pub fn is_removable(server: &McpServerInfo) -> bool {
|
||||
server.wire_source == McpWireSource::Local && !server.name.starts_with("grok_com_")
|
||||
}
|
||||
|
||||
fn parse_wire_source(raw: Option<&str>) -> McpWireSource {
|
||||
match raw {
|
||||
Some("managed") => McpWireSource::Managed,
|
||||
_ => McpWireSource::Local,
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_plugin_name(source_label: &str) -> Option<String> {
|
||||
let rest = source_label.strip_prefix("plugin:")?.trim();
|
||||
if rest.is_empty() {
|
||||
@@ -181,11 +113,8 @@ pub struct McpServerInfo {
|
||||
pub enabled: bool,
|
||||
/// Display label from `source_label` or wire `source` (e.g. `"plugin: foo"`).
|
||||
pub source: String,
|
||||
/// Wire `source` enum before display overlay.
|
||||
pub wire_source: McpWireSource,
|
||||
/// Plugin name parsed from `source_label` (`"plugin: …"`).
|
||||
pub plugin_name: Option<String>,
|
||||
pub is_managed_gateway: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
@@ -262,10 +191,7 @@ pub fn convert_list_response(resp: McpsListResponse) -> Vec<McpServerInfo> {
|
||||
} else {
|
||||
(McpServerDisplayStatus::Unavailable, 0, vec![], false, false)
|
||||
};
|
||||
let wire_source = parse_wire_source(entry.source.as_deref());
|
||||
let plugin_name = entry.source_label.as_deref().and_then(parse_plugin_name);
|
||||
let is_managed_gateway = entry.name.starts_with("managed_gateway:")
|
||||
|| entry.config_type.as_deref() == Some("managedGateway");
|
||||
let source = entry
|
||||
.source_label
|
||||
.or(entry.source)
|
||||
@@ -279,19 +205,16 @@ pub fn convert_list_response(resp: McpsListResponse) -> Vec<McpServerInfo> {
|
||||
tools,
|
||||
enabled,
|
||||
source,
|
||||
wire_source,
|
||||
plugin_name,
|
||||
is_managed_gateway,
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
// Stable sort: managed before plugin/local, then alphabetical by name.
|
||||
// Stable sort: plugin before local, then alphabetical by name.
|
||||
servers.sort_by(|a, b| {
|
||||
let source_rank = |s: &McpServerInfo| match section_for(s) {
|
||||
McpSectionId::Managed => 0,
|
||||
McpSectionId::Plugin(_) => 1,
|
||||
McpSectionId::Local => 2,
|
||||
McpSectionId::Plugin(_) => 0,
|
||||
McpSectionId::Local => 1,
|
||||
};
|
||||
source_rank(a)
|
||||
.cmp(&source_rank(b))
|
||||
@@ -354,9 +277,7 @@ mod tests {
|
||||
tools: Vec::new(),
|
||||
enabled: true,
|
||||
source: "local".to_string(),
|
||||
wire_source: McpWireSource::Local,
|
||||
plugin_name: None,
|
||||
is_managed_gateway: false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -394,71 +315,6 @@ mod tests {
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn section_description_lines_managed_includes_connectors_url() {
|
||||
let lines = section_description_lines(&McpSectionId::Managed, None);
|
||||
assert_eq!(lines.len(), 2);
|
||||
// Instruction leads; Ctrl+O hint lives on the first line.
|
||||
assert!(
|
||||
lines[0].contains("Ctrl+O"),
|
||||
"should mention Ctrl+O shortcut: {}",
|
||||
lines[0]
|
||||
);
|
||||
// URL sits alone on the second line, scheme-stripped and bracket-highlighted.
|
||||
assert_eq!(lines[1], "[grok.com/connectors]");
|
||||
assert!(
|
||||
!lines[1].contains("https://"),
|
||||
"displayed URL should drop the scheme: {}",
|
||||
lines[1]
|
||||
);
|
||||
let with_team = section_description_lines(&McpSectionId::Managed, Some("team-1"));
|
||||
assert_eq!(with_team[1], "[grok.com/connectors?teamId=team-1]");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn managed_connectors_url_display_strips_scheme() {
|
||||
assert_eq!(managed_connectors_url_display(None), "grok.com/connectors");
|
||||
assert_eq!(
|
||||
managed_connectors_url_display(Some("team-uuid-1")),
|
||||
"grok.com/connectors?teamId=team-uuid-1"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn managed_connectors_url_appends_team_id_when_present() {
|
||||
assert_eq!(managed_connectors_url(None), MANAGED_SECTION_CONNECTORS_URL);
|
||||
assert_eq!(
|
||||
managed_connectors_url(Some("")),
|
||||
MANAGED_SECTION_CONNECTORS_URL
|
||||
);
|
||||
assert_eq!(
|
||||
managed_connectors_url(Some("team-uuid-1")),
|
||||
format!("{MANAGED_SECTION_CONNECTORS_URL}?teamId=team-uuid-1")
|
||||
);
|
||||
assert_eq!(
|
||||
managed_connectors_url(Some("a b/c")),
|
||||
format!(
|
||||
"{MANAGED_SECTION_CONNECTORS_URL}?teamId={}",
|
||||
urlencoding::encode("a b/c")
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn section_description_lines_local_is_empty() {
|
||||
assert!(section_description_lines(&McpSectionId::Local, None).is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn section_for_grok_com_with_plugin_label_is_managed() {
|
||||
let server = server_from_wire(
|
||||
"grok_com_linear",
|
||||
Some("managed"),
|
||||
Some("plugin: my-plugin"),
|
||||
);
|
||||
assert_eq!(section_for(&server), McpSectionId::Managed);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn section_for_plugin_labeled_local_is_plugin_section() {
|
||||
let server = server_from_wire("my-mcp", Some("local"), Some("plugin: linter"));
|
||||
@@ -468,86 +324,13 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn is_removable_plugin_labeled_local_server() {
|
||||
let server = server_from_wire("my-mcp", Some("local"), Some("plugin: linter"));
|
||||
assert!(is_removable(&server));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn is_removable_rejects_managed_wire_source() {
|
||||
let server = server_from_wire("custom", Some("managed"), None);
|
||||
assert!(!is_removable(&server));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn is_removable_rejects_grok_com_prefix() {
|
||||
let server = server_from_wire("grok_com_slack", Some("local"), None);
|
||||
assert!(!is_removable(&server));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn convert_list_response_parses_plugin_name() {
|
||||
let server = server_from_wire("srv", Some("local"), Some("plugin: example"));
|
||||
assert_eq!(server.wire_source, McpWireSource::Local);
|
||||
assert_eq!(server.plugin_name.as_deref(), Some("example"));
|
||||
assert_eq!(server.source, "plugin: example");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn convert_list_response_classifies_managed_gateway_only_for_gateway_rows() {
|
||||
let gateway = server_from_wire_with_type(
|
||||
"managed_gateway:linear",
|
||||
Some("managed"),
|
||||
None,
|
||||
Some("managedGateway"),
|
||||
);
|
||||
assert!(gateway.is_managed_gateway);
|
||||
|
||||
let legacy_managed = server_from_wire("grok_com_slack", Some("managed"), None);
|
||||
assert!(!legacy_managed.is_managed_gateway);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn gateway_row_uses_managed_section_not_local_uninstall() {
|
||||
let gateway = server_from_wire_with_type(
|
||||
"managed_gateway:linear",
|
||||
Some("managed"),
|
||||
None,
|
||||
Some("managedGateway"),
|
||||
);
|
||||
assert_eq!(section_for(&gateway), McpSectionId::Managed);
|
||||
assert!(!is_removable(&gateway));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn convert_list_response_orders_gateway_rows_by_display_name() {
|
||||
fn gateway_entry(name: &str, display_name: &str) -> McpsServerEntry {
|
||||
McpsServerEntry {
|
||||
name: name.to_string(),
|
||||
display_name: Some(display_name.to_string()),
|
||||
source: Some("managed".to_string()),
|
||||
source_label: None,
|
||||
config_type: Some("managedGateway".to_string()),
|
||||
session: Some(McpsServerSession {
|
||||
enabled: true,
|
||||
status: Some("ready".to_string()),
|
||||
tools: vec![],
|
||||
auth_required: false,
|
||||
}),
|
||||
}
|
||||
}
|
||||
let servers = convert_list_response(McpsListResponse {
|
||||
servers: vec![
|
||||
gateway_entry("managed_gateway:zeta", "Alpha"),
|
||||
gateway_entry("managed_gateway:alpha", "Zeta"),
|
||||
],
|
||||
});
|
||||
assert_eq!(servers[0].display_name.as_deref(), Some("Alpha"));
|
||||
assert_eq!(servers[0].name, "managed_gateway:zeta");
|
||||
assert_eq!(servers[1].display_name.as_deref(), Some("Zeta"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn patch_server_row_updates_existing() {
|
||||
let mut servers = vec![
|
||||
@@ -614,9 +397,7 @@ mod tests {
|
||||
}],
|
||||
enabled: true,
|
||||
source: "local".into(),
|
||||
wire_source: McpWireSource::Local,
|
||||
plugin_name: None,
|
||||
is_managed_gateway: false,
|
||||
}];
|
||||
let mutated = patch_server_row(
|
||||
&mut servers,
|
||||
|
||||
@@ -1089,7 +1089,6 @@ pub fn render_doc_picker_overlay(
|
||||
badge: "",
|
||||
badge_color: None,
|
||||
collapsible: false,
|
||||
underline_last_desc: false,
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
|
||||
@@ -89,9 +89,6 @@ pub struct PickerRow<'a> {
|
||||
/// `description_lines` are empty. The `expanded` field controls
|
||||
/// which glyph is rendered.
|
||||
pub collapsible: bool,
|
||||
/// Underline the final description line (when expanded) so it reads as a
|
||||
/// clickable link. Used for the Managed connectors URL.
|
||||
pub underline_last_desc: bool,
|
||||
}
|
||||
|
||||
/// A key-value pair shown in an expanded picker row.
|
||||
@@ -725,11 +722,9 @@ pub fn compute_row_height(row: &PickerRow<'_>, width: u16) -> usize {
|
||||
/// When `row.expanded && !row.fields.is_empty()`, renders key-value detail lines
|
||||
/// below (indented, label in `gray`, value in `gray_bright`).
|
||||
///
|
||||
/// Rows consumed by a rendered picker row/entry, plus the row band of its
|
||||
/// underlined link line (recorded from what is painted) for click hit-testing.
|
||||
/// Rows consumed by a rendered picker row/entry.
|
||||
pub struct RenderedRow {
|
||||
pub rows: u16,
|
||||
pub link_band: Option<std::ops::Range<u16>>,
|
||||
}
|
||||
|
||||
/// `max_rows` caps rendering to available vertical space; detail fields beyond
|
||||
@@ -749,10 +744,7 @@ pub fn render_picker_row(
|
||||
max_rows: u16,
|
||||
) -> RenderedRow {
|
||||
if max_rows == 0 {
|
||||
return RenderedRow {
|
||||
rows: 0,
|
||||
link_band: None,
|
||||
};
|
||||
return RenderedRow { rows: 0 };
|
||||
}
|
||||
let base_bg = picker_base_bg(bg, theme);
|
||||
let embed = crate::views::modal_window::embedded_row_style(theme, row.selected);
|
||||
@@ -894,7 +886,6 @@ pub fn render_picker_row(
|
||||
|
||||
// Description lines (shown when expanded) or summary lines (collapsed).
|
||||
let mut rows = 1u16;
|
||||
let mut link_band: Option<std::ops::Range<u16>> = None;
|
||||
let secondary_lines: &[&str] = if row.expanded {
|
||||
row.description_lines
|
||||
} else {
|
||||
@@ -907,18 +898,12 @@ pub fn render_picker_row(
|
||||
.fg(theme.text_primary)
|
||||
.bg(base_bg)
|
||||
.add_modifier(Modifier::BOLD);
|
||||
// Underline the final description line when the row opts in, so it reads as a link.
|
||||
let link_style = highlight_style.add_modifier(Modifier::UNDERLINED);
|
||||
let last_line = secondary_lines.len().saturating_sub(1);
|
||||
let max_w = width.saturating_sub(indent) as usize;
|
||||
for (li, desc) in secondary_lines.iter().enumerate() {
|
||||
let is_link = row.underline_last_desc && row.expanded && li == last_line;
|
||||
let hl = if is_link { link_style } else { highlight_style };
|
||||
for desc in secondary_lines.iter() {
|
||||
// Render with [bracket] highlight markers: text inside
|
||||
// [...] is shown in bold/bright, brackets are stripped.
|
||||
let line = parse_highlight_spans(desc, desc_style, hl);
|
||||
let line = parse_highlight_spans(desc, desc_style, highlight_style);
|
||||
let wrapped = word_wrap_line(&line, max_w);
|
||||
let link_start = y + rows;
|
||||
for wrap_line in wrapped {
|
||||
if rows >= max_rows {
|
||||
break;
|
||||
@@ -926,10 +911,6 @@ pub fn render_picker_row(
|
||||
render_styled_spans(buf, &wrap_line, x + indent, y + rows, max_w);
|
||||
rows += 1;
|
||||
}
|
||||
// Record only painted rows, so a vertically-clipped link records nothing.
|
||||
if is_link && y + rows > link_start {
|
||||
link_band = Some(link_start..(y + rows));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1000,7 +981,7 @@ pub fn render_picker_row(
|
||||
}
|
||||
}
|
||||
|
||||
RenderedRow { rows, link_band }
|
||||
RenderedRow { rows }
|
||||
}
|
||||
|
||||
/// Render a single picker entry (header or row).
|
||||
@@ -1019,10 +1000,7 @@ pub fn render_picker_entry(
|
||||
max_rows: u16,
|
||||
) -> RenderedRow {
|
||||
if max_rows == 0 {
|
||||
return RenderedRow {
|
||||
rows: 0,
|
||||
link_band: None,
|
||||
};
|
||||
return RenderedRow { rows: 0 };
|
||||
}
|
||||
match entry {
|
||||
PickerEntry::Header { label } => {
|
||||
@@ -1041,10 +1019,7 @@ pub fn render_picker_entry(
|
||||
Span::styled(sep, sep_style),
|
||||
]);
|
||||
buf.set_line(x, y, &line, width);
|
||||
RenderedRow {
|
||||
rows: 1,
|
||||
link_band: None,
|
||||
}
|
||||
RenderedRow { rows: 1 }
|
||||
}
|
||||
PickerEntry::Row(row) => {
|
||||
render_picker_row(buf, x, y, width, theme, row, hovered, bg, max_rows)
|
||||
@@ -1359,9 +1334,6 @@ pub struct PickerState {
|
||||
pub scroll_offset: Option<usize>,
|
||||
/// Hit areas from the last render (for mouse hit-testing).
|
||||
pub hit_areas: Option<PickerHitAreas>,
|
||||
/// Entry index and absolute row band of the underlined link line from the
|
||||
/// last render (the Managed connectors URL), for click-to-open hit-testing.
|
||||
pub link_band: Option<(usize, std::ops::Range<u16>)>,
|
||||
/// Hit areas for tab labels (one per tab, `None` if tab didn't fit).
|
||||
pub tab_hit_areas: Option<Vec<Option<Rect>>>,
|
||||
/// Hit area for the filter indicator in the search bar.
|
||||
@@ -1387,7 +1359,6 @@ impl Default for PickerState {
|
||||
hovered: None,
|
||||
scroll_offset: None,
|
||||
hit_areas: None,
|
||||
link_band: None,
|
||||
tab_hit_areas: None,
|
||||
filter_area: None,
|
||||
filter_hovered: false,
|
||||
@@ -1427,7 +1398,6 @@ impl PickerState {
|
||||
self.expanded.clear();
|
||||
self.close_hovered = false;
|
||||
self.hit_areas = None;
|
||||
self.link_band = None;
|
||||
self.tab_hit_areas = None;
|
||||
self.filter_area = None;
|
||||
self.filter_hovered = false;
|
||||
@@ -1779,7 +1749,6 @@ fn render_picker_content_inner(
|
||||
scrollbar_x_override: Option<u16>,
|
||||
) -> PickerContentHitAreas {
|
||||
// Cleared each paint; set below if a row underlines its last description line.
|
||||
state.link_band = None;
|
||||
let is_clickable_non_sel = |i: usize| non_selectable_clickable.get(i).copied().unwrap_or(false);
|
||||
let empty_hit = PickerContentHitAreas {
|
||||
item_rects: vec![],
|
||||
@@ -1880,7 +1849,6 @@ fn render_picker_content_inner(
|
||||
|
||||
let mut item_rects = Vec::new();
|
||||
let mut entry_indices = Vec::new();
|
||||
let mut link_band: Option<(usize, std::ops::Range<u16>)> = None;
|
||||
|
||||
// Skip entries until we've consumed scroll_visual visual rows.
|
||||
let mut visual_rows_consumed = 0usize;
|
||||
@@ -1928,9 +1896,6 @@ fn render_picker_content_inner(
|
||||
remaining,
|
||||
);
|
||||
let rows_consumed = rendered.rows;
|
||||
if let Some(band) = rendered.link_band {
|
||||
link_band = Some((entry_idx, band));
|
||||
}
|
||||
|
||||
if !is_header && (!is_non_sel(entry_idx) || is_clickable_non_sel(entry_idx)) {
|
||||
let row_rect = Rect {
|
||||
@@ -1944,8 +1909,6 @@ fn render_picker_content_inner(
|
||||
}
|
||||
y += rows_consumed;
|
||||
}
|
||||
state.link_band = link_band;
|
||||
|
||||
// Scrollbar. (Globally suppressed in minimal mode via
|
||||
// `render::scrollbar::set_scrollbars_hidden`.)
|
||||
if needs_scroll {
|
||||
@@ -3261,76 +3224,6 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn underline_last_desc_underlines_only_the_link_line() {
|
||||
use ratatui::buffer::Buffer;
|
||||
use ratatui::layout::Rect;
|
||||
|
||||
let theme = Theme::current();
|
||||
// Wide enough that each description line fits on a single visual row:
|
||||
// y+0 = label, y+1 = instruction, y+2 = bracket-highlighted URL.
|
||||
let area = Rect::new(0, 0, 60, 6);
|
||||
let desc: &[&str] = &["some instruction text", "[example.com/link]"];
|
||||
|
||||
let render = |underline_last_desc: bool| -> (Buffer, Option<std::ops::Range<u16>>) {
|
||||
let row = PickerRow {
|
||||
label: "Group header",
|
||||
right_label: "",
|
||||
selected: false,
|
||||
expanded: true,
|
||||
fields: &[],
|
||||
description_lines: desc,
|
||||
summary_lines: &[],
|
||||
dimmed: false,
|
||||
indent: 0,
|
||||
badge: "",
|
||||
badge_color: None,
|
||||
collapsible: true,
|
||||
underline_last_desc,
|
||||
};
|
||||
let mut buf = Buffer::empty(area);
|
||||
let rendered = render_picker_row(
|
||||
&mut buf,
|
||||
area.x,
|
||||
area.y,
|
||||
area.width,
|
||||
&theme,
|
||||
&row,
|
||||
false,
|
||||
None,
|
||||
area.height,
|
||||
);
|
||||
(buf, rendered.link_band)
|
||||
};
|
||||
|
||||
// Rows (relative to area top) that have any underlined cell.
|
||||
let underlined_rows = |buf: &Buffer| -> Vec<u16> {
|
||||
(area.y..area.y + area.height)
|
||||
.filter(|&y| {
|
||||
(0..area.width).any(|x| {
|
||||
buf.cell((x, y))
|
||||
.map(|c| c.modifier.contains(Modifier::UNDERLINED))
|
||||
.unwrap_or(false)
|
||||
})
|
||||
})
|
||||
.collect()
|
||||
};
|
||||
|
||||
let (on, on_band) = render(true);
|
||||
// Only the final (link) description line (row y+2) is underlined; the
|
||||
// label (y+0) and instruction (y+1) rows are not.
|
||||
assert_eq!(underlined_rows(&on), vec![2]);
|
||||
// Render <-> hit-test parity: the recorded band equals the painted rows.
|
||||
assert_eq!(on_band, Some(2..3));
|
||||
|
||||
let (off, off_band) = render(false);
|
||||
assert!(
|
||||
underlined_rows(&off).is_empty(),
|
||||
"opt-out rows underline nothing"
|
||||
);
|
||||
assert_eq!(off_band, None, "opt-out records no link band");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn vim_not_searching_char_does_not_type() {
|
||||
for hint in [true, false] {
|
||||
|
||||
@@ -622,7 +622,6 @@ pub(crate) fn build_grouped_picker_entries<'a>(
|
||||
badge: b.badge,
|
||||
badge_color: None,
|
||||
collapsible: b.collapsible,
|
||||
underline_last_desc: false,
|
||||
}));
|
||||
grouped_pos += 1;
|
||||
}
|
||||
|
||||
@@ -1173,7 +1173,6 @@ impl CheatsheetRows {
|
||||
badge: "",
|
||||
badge_color: None,
|
||||
collapsible: true,
|
||||
underline_last_desc: false,
|
||||
}),
|
||||
CheatsheetRowKind::Hint { dimmed, expand } => {
|
||||
let is_expanded =
|
||||
@@ -1196,7 +1195,6 @@ impl CheatsheetRows {
|
||||
badge: "",
|
||||
badge_color: None,
|
||||
collapsible: false,
|
||||
underline_last_desc: false,
|
||||
})
|
||||
}
|
||||
CheatsheetRowKind::Other => PickerEntry::Row(PickerRow {
|
||||
@@ -1212,7 +1210,6 @@ impl CheatsheetRows {
|
||||
badge: "",
|
||||
badge_color: None,
|
||||
collapsible: false,
|
||||
underline_last_desc: false,
|
||||
}),
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1854,7 +1854,6 @@ pub(crate) fn render_session_picker(
|
||||
badge: b.badge,
|
||||
badge_color: None,
|
||||
collapsible: b.collapsible,
|
||||
underline_last_desc: false,
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
@@ -1938,7 +1937,6 @@ pub(crate) fn render_session_picker(
|
||||
badge: if has_snippet { "match" } else { "" },
|
||||
badge_color: Some(theme.accent_user),
|
||||
collapsible: true,
|
||||
underline_last_desc: false,
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user