From b3907f42a8f953f8f9915a2cfd12d58b50ec2033 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9B=B7=E7=94=B5=E8=8A=BD=E8=A1=A3?= Date: Sat, 9 May 2026 01:43:10 -0400 Subject: [PATCH] Add site compatibility diagnostics page --- crates/ely_app/src/shell/internal_pages.rs | 2 + .../internal_pages/site_compatibility.rs | 429 ++++++++++++++++++ crates/ely_browser_core/src/navigation.rs | 16 +- crates/ely_browser_core/src/state/commands.rs | 17 +- .../tests/site_compatibility.rs | 19 + 5 files changed, 479 insertions(+), 4 deletions(-) create mode 100644 crates/ely_app/src/shell/internal_pages/site_compatibility.rs create mode 100644 crates/ely_browser_core/tests/site_compatibility.rs diff --git a/crates/ely_app/src/shell/internal_pages.rs b/crates/ely_app/src/shell/internal_pages.rs index c212357..8b73f87 100644 --- a/crates/ely_app/src/shell/internal_pages.rs +++ b/crates/ely_app/src/shell/internal_pages.rs @@ -22,6 +22,7 @@ mod search; mod settings; mod shortcuts; mod sidebar_tabs; +mod site_compatibility; mod site_permissions_settings; mod site_settings; mod sleep; @@ -67,6 +68,7 @@ impl ElyShell { "ely://history" => self.render_history_page(snapshot, cx), "ely://archive" => self.render_archive_page(snapshot, cx), "ely://task-manager" => self.render_task_manager_page(snapshot), + "ely://site-compatibility" => self.render_site_compatibility_page(snapshot), "ely://plugins" => self.render_plugin_catalog_page(snapshot, cx), url if url.starts_with("ely://crash/") => self.render_crash_route(snapshot, url, cx), url if url.starts_with("ely://plugin/") => { diff --git a/crates/ely_app/src/shell/internal_pages/site_compatibility.rs b/crates/ely_app/src/shell/internal_pages/site_compatibility.rs new file mode 100644 index 0000000..f88506c --- /dev/null +++ b/crates/ely_app/src/shell/internal_pages/site_compatibility.rs @@ -0,0 +1,429 @@ +use ely_browser_core::BrowserSnapshot; +use ely_design_system::colors; +use ely_domain::{BrowserTab, ProfileKind, SiteOrigin, TabState}; +use gpui::{AnyElement, IntoElement, ParentElement, Styled, div, px, rgb}; +use gpui_component::{IconName, StyledExt, clipboard::Clipboard, scroll::ScrollableElement}; + +use super::{ElyShell, render_canvas_surface}; + +const BUILD_REVISION: &str = env!("ELY_BUILD_REVISION"); +const GPUI_VERSION: &str = env!("ELY_GPUI_VERSION"); +const SERVO_VERSION: &str = env!("ELY_SERVO_VERSION"); + +impl ElyShell { + pub(super) fn render_site_compatibility_page( + &mut self, + snapshot: &BrowserSnapshot, + ) -> AnyElement { + let Some(active_tab) = active_tab(snapshot) else { + return render_canvas_surface( + div().size_full().p_8().flex().flex_col().gap_5().child(render_missing_tab()), + ); + }; + + let origin = origin_for_tab(active_tab); + let report = diagnostic_report(snapshot, active_tab, origin.as_ref()); + + render_canvas_surface( + div() + .size_full() + .p_8() + .flex() + .flex_col() + .gap_5() + .child(render_compatibility_header(snapshot, active_tab, report.clone())) + .child(render_compatibility_summary(snapshot, active_tab, origin.as_ref())) + .child(render_compatibility_rows(snapshot, active_tab, origin.as_ref())), + ) + } +} + +fn render_missing_tab() -> AnyElement { + div() + .rounded_md() + .border_1() + .border_color(rgb(colors::HAIRLINE)) + .bg(rgb(colors::CANVAS_SOFT)) + .px_4() + .py_3() + .flex() + .items_center() + .gap_3() + .child(div().text_color(rgb(colors::ERROR)).child(IconName::TriangleAlert)) + .child( + div() + .text_sm() + .font_semibold() + .text_color(rgb(colors::INK)) + .child("Active tab is unavailable."), + ) + .into_any_element() +} + +fn render_compatibility_header( + snapshot: &BrowserSnapshot, + active_tab: &BrowserTab, + report: String, +) -> AnyElement { + div() + .flex() + .items_end() + .justify_between() + .gap_4() + .child( + div() + .min_w_0() + .flex() + .flex_col() + .gap_2() + .child( + div() + .text_size(px(26.0)) + .text_color(rgb(colors::INK)) + .child("Site Compatibility"), + ) + .child(div().text_sm().truncate().text_color(rgb(colors::MUTED)).child(format!( + "{} / {}", + snapshot.active_profile_name, + diagnostic_url_scope(active_tab) + ))), + ) + .child( + div() + .flex() + .items_center() + .gap_2() + .child( + div() + .flex() + .items_center() + .gap_2() + .text_xs() + .font_semibold() + .text_color(rgb(colors::MUTED)) + .child(IconName::Inspector) + .child("Diagnostics"), + ) + .child(Clipboard::new("copy-site-compatibility-diagnostics").value(report)), + ) + .into_any_element() +} + +fn render_compatibility_summary( + snapshot: &BrowserSnapshot, + active_tab: &BrowserTab, + origin: Option<&SiteOrigin>, +) -> AnyElement { + let (title, detail, icon, color) = match active_tab.state() { + TabState::Ready => ( + "Current page is ready", + "Diagnostics omit URL path and query before copying.", + IconName::CircleCheck, + colors::SUCCESS, + ), + TabState::Loading => ( + "Current page is loading", + "Capture diagnostics after the page reaches a stable state.", + IconName::LoaderCircle, + colors::PRIMARY, + ), + TabState::Crashed => ( + "Current page crashed", + "The copied report includes tab state and profile-scoped permissions.", + IconName::TriangleAlert, + colors::ERROR, + ), + TabState::Discarded => ( + "Current page is sleeping", + "Wake the tab to refresh Servo rendering details.", + IconName::EyeOff, + colors::MUTED, + ), + TabState::Archived => ( + "Current page is archived", + "Restore the tab to refresh Servo rendering details.", + IconName::Folder, + colors::MUTED, + ), + }; + + div() + .rounded_md() + .border_1() + .border_color(rgb(colors::HAIRLINE)) + .bg(rgb(colors::CANVAS_SOFT)) + .px_4() + .py_3() + .flex() + .items_center() + .justify_between() + .gap_4() + .child( + div() + .min_w_0() + .flex() + .items_center() + .gap_3() + .child(div().text_color(rgb(color)).child(icon)) + .child( + div() + .min_w_0() + .flex() + .flex_col() + .gap_1() + .child( + div() + .text_sm() + .font_semibold() + .text_color(rgb(colors::INK)) + .child(title), + ) + .child( + div().text_xs().truncate().text_color(rgb(colors::MUTED)).child(detail), + ), + ), + ) + .child( + div() + .flex() + .items_center() + .gap_3() + .text_xs() + .font_semibold() + .child( + div() + .text_color(rgb(colors::MUTED)) + .child(format!("{} permissions", site_permission_count(snapshot, origin))), + ) + .child( + div() + .text_color(rgb(colors::MUTED)) + .child(format!("{} audits", site_permission_audit_count(snapshot, origin))), + ), + ) + .into_any_element() +} + +fn render_compatibility_rows( + snapshot: &BrowserSnapshot, + active_tab: &BrowserTab, + origin: Option<&SiteOrigin>, +) -> AnyElement { + div() + .flex_1() + .min_h_0() + .flex() + .flex_col() + .overflow_y_scrollbar() + .border_t_1() + .border_color(rgb(colors::HAIRLINE)) + .child(compatibility_row( + IconName::Globe, + "URL Scope", + diagnostic_url_scope(active_tab), + "Scheme, host, and port only", + )) + .child(compatibility_row( + IconName::CircleUser, + "Profile", + &snapshot.active_profile_name, + profile_kind_label(&snapshot.active_profile_kind), + )) + .child(compatibility_row( + IconName::GalleryVerticalEnd, + "Space", + &snapshot.active_space_name, + "Active workspace context", + )) + .child(compatibility_row( + IconName::CircleCheck, + "Tab State", + tab_state_label(active_tab.state()), + active_tab.title(), + )) + .child(compatibility_row( + IconName::Globe, + "Servo", + format!("servo {SERVO_VERSION}"), + "Web rendering engine", + )) + .child(compatibility_row( + IconName::Frame, + "GPUI", + format!("gpui {GPUI_VERSION}"), + "Native shell renderer", + )) + .child(compatibility_row(IconName::GitHub, "Build", BUILD_REVISION, "Git revision")) + .child(compatibility_row( + IconName::CircleCheck, + "Site Permissions", + site_permission_count(snapshot, origin).to_string(), + "Profile-scoped configured decisions", + )) + .child(compatibility_row( + IconName::Inspector, + "Console Summary", + "No console events captured", + "Current Servo host bridge does not expose console events", + )) + .into_any_element() +} + +fn compatibility_row( + icon: IconName, + label: &'static str, + value: impl Into, + detail: impl Into, +) -> AnyElement { + let value = value.into(); + let detail = detail.into(); + + div() + .py_3() + .border_b_1() + .border_color(rgb(colors::HAIRLINE)) + .flex() + .items_center() + .justify_between() + .gap_4() + .child( + div() + .min_w_0() + .flex() + .items_center() + .gap_3() + .child(div().text_color(rgb(colors::MUTED_SOFT)).child(icon)) + .child( + div() + .min_w_0() + .flex() + .flex_col() + .gap_1() + .child( + div() + .text_sm() + .font_semibold() + .truncate() + .text_color(rgb(colors::INK)) + .child(label), + ) + .child( + div().text_xs().truncate().text_color(rgb(colors::MUTED)).child(detail), + ), + ), + ) + .child( + div() + .max_w(px(360.0)) + .truncate() + .text_sm() + .font_semibold() + .text_color(rgb(colors::INK)) + .child(value), + ) + .into_any_element() +} + +fn diagnostic_report( + snapshot: &BrowserSnapshot, + active_tab: &BrowserTab, + origin: Option<&SiteOrigin>, +) -> String { + [ + "ELY Browser Site Compatibility Diagnostics".to_string(), + format!("Build: {BUILD_REVISION}"), + format!("Servo: servo {SERVO_VERSION}"), + format!("GPUI: gpui {GPUI_VERSION}"), + format!("Space: {}", snapshot.active_space_name), + format!("Profile: {}", snapshot.active_profile_name), + format!("Profile kind: {}", profile_kind_label(&snapshot.active_profile_kind)), + format!("URL scope: {}", diagnostic_url_scope(active_tab)), + format!("Tab title: {}", active_tab.title()), + format!("Tab state: {}", tab_state_label(active_tab.state())), + format!("Site permissions: {}", site_permission_count(snapshot, origin)), + format!("Permission audit events: {}", site_permission_audit_count(snapshot, origin)), + "Console summary: No console events captured by the current Servo host bridge.".to_string(), + ] + .join("\n") +} + +fn active_tab(snapshot: &BrowserSnapshot) -> Option<&BrowserTab> { + snapshot.tabs.iter().find(|tab| tab.id() == &snapshot.active_tab_id) +} + +fn origin_for_tab(tab: &BrowserTab) -> Option { + SiteOrigin::from_url(tab.url()).ok().flatten() +} + +fn diagnostic_url_scope(tab: &BrowserTab) -> String { + origin_for_tab(tab) + .map_or_else(|| tab.url().as_str().to_string(), |origin| origin.as_str().to_string()) +} + +fn site_permission_count(snapshot: &BrowserSnapshot, origin: Option<&SiteOrigin>) -> usize { + let Some(origin) = origin else { + return 0; + }; + + snapshot.site_permissions.iter().filter(|entry| entry.origin() == origin).count() +} + +fn site_permission_audit_count(snapshot: &BrowserSnapshot, origin: Option<&SiteOrigin>) -> usize { + let Some(origin) = origin else { + return 0; + }; + + snapshot.site_permission_audit_events.iter().filter(|event| event.origin() == origin).count() +} + +fn profile_kind_label(profile_kind: &ProfileKind) -> &'static str { + match profile_kind { + ProfileKind::Standard => "Standard", + ProfileKind::Private => "Private", + } +} + +fn tab_state_label(state: &TabState) -> &'static str { + match state { + TabState::Loading => "Loading", + TabState::Ready => "Ready", + TabState::Crashed => "Crashed", + TabState::Discarded => "Sleeping", + TabState::Archived => "Archived", + } +} + +#[cfg(test)] +mod tests { + use ely_domain::{ProfileId, SpaceId, TabId, UrlText}; + + use super::{BrowserTab, diagnostic_url_scope}; + + #[test] + fn diagnostic_url_scope_omits_path_and_query() -> Result<(), Box> { + let tab = BrowserTab::new( + TabId::new(), + SpaceId::new(), + ProfileId::new(), + "Example", + UrlText::parse("https://example.com/private/path?token=secret#hash")?, + ); + + assert_eq!(diagnostic_url_scope(&tab), "https://example.com"); + Ok(()) + } + + #[test] + fn diagnostic_url_scope_keeps_internal_route() -> Result<(), Box> { + let tab = BrowserTab::new( + TabId::new(), + SpaceId::new(), + ProfileId::new(), + "Settings", + UrlText::parse("ely://settings/advanced")?, + ); + + assert_eq!(diagnostic_url_scope(&tab), "ely://settings/advanced"); + Ok(()) + } +} diff --git a/crates/ely_browser_core/src/navigation.rs b/crates/ely_browser_core/src/navigation.rs index 3d7cea7..ad894b4 100644 --- a/crates/ely_browser_core/src/navigation.rs +++ b/crates/ely_browser_core/src/navigation.rs @@ -23,6 +23,7 @@ fn internal_page_title(url: &str) -> Option<&'static str> { "ely://history" => Some("History"), "ely://archive" => Some("Archived Tabs"), "ely://task-manager" => Some("Task Manager"), + "ely://site-compatibility" => Some("Site Compatibility"), "ely://plugins" => Some("Plugin Marketplace"), url if crash_route_tab_id(url).is_some() => Some("Tab Recovery"), url if plugin_detail_route_id(url).is_some() => Some("Plugin Details"), @@ -198,6 +199,10 @@ pub(crate) fn task_manager_url() -> Result { internal_page_url("ely://task-manager") } +pub(crate) fn site_compatibility_url() -> Result { + internal_page_url("ely://site-compatibility") +} + pub(crate) fn plugins_url() -> Result { internal_page_url("ely://plugins") } @@ -263,13 +268,22 @@ const SETTINGS_ROUTE_MATCHES: &[SettingsRouteMatch] = &[ }, SettingsRouteMatch { route: "ely://settings/advanced", - exact_terms: &["advanced", "advanced settings", "runtime", "diagnostics", "diagnostic"], + exact_terms: &[ + "advanced", + "advanced settings", + "runtime", + "diagnostics", + "diagnostic", + "compatibility", + "site compatibility", + ], search_terms: &[ "Advanced", "Local runtime policies and audit counters.", "runtime policy", "audit counters", "diagnostics", + "site compatibility", ], }, SettingsRouteMatch { diff --git a/crates/ely_browser_core/src/state/commands.rs b/crates/ely_browser_core/src/state/commands.rs index ce4e5bb..9a021c6 100644 --- a/crates/ely_browser_core/src/state/commands.rs +++ b/crates/ely_browser_core/src/state/commands.rs @@ -11,9 +11,9 @@ use crate::{ move_tab_space_name, new_private_profile_name, new_profile_name, new_space_name, note_body, notes_url, plugin_detail_url, plugin_settings_url, plugins_url, reading_list_url, reading_progress_percent, rename_tab_group_name, search_url, settings_page_url, - settings_url, shortcut_settings_url, space_icon, space_settings_url, split_group_name, - switch_profile_name, sync_status_url, tab_group_color_hex, tab_group_name, tab_note_body, - task_manager_url, + settings_url, shortcut_settings_url, site_compatibility_url, space_icon, + space_settings_url, split_group_name, switch_profile_name, sync_status_url, + tab_group_color_hex, tab_group_name, tab_note_body, task_manager_url, }, }; @@ -267,6 +267,17 @@ impl BrowserCore { self.open_tab(task_manager_url()?); Ok(true) } + "site-compatibility" + | "site compatibility" + | "open-site-compatibility" + | "open site compatibility" + | "compatibility" + | "diagnostics" + | "site-diagnostics" + | "site diagnostics" => { + self.open_tab(site_compatibility_url()?); + Ok(true) + } "plugins" | "open-plugins" | "open plugins" diff --git a/crates/ely_browser_core/tests/site_compatibility.rs b/crates/ely_browser_core/tests/site_compatibility.rs new file mode 100644 index 0000000..7da146f --- /dev/null +++ b/crates/ely_browser_core/tests/site_compatibility.rs @@ -0,0 +1,19 @@ +use std::error::Error; + +use ely_browser_core::{BrowserCore, InitialBrowserConfig}; +use ely_domain::CommandIntent; + +#[test] +fn open_site_compatibility_command_opens_diagnostics_page() -> Result<(), Box> { + let mut core = BrowserCore::new(InitialBrowserConfig::ely_defaults()?)?; + + core.set_command_query(">open-site-compatibility"); + let intent = core.submit_command()?; + let active_tab = core.active_tab()?; + + assert_eq!(intent, Some(CommandIntent::Command("open-site-compatibility".to_string()))); + assert_eq!(active_tab.title(), "Site Compatibility"); + assert_eq!(active_tab.url().as_str(), "ely://site-compatibility"); + assert_eq!(core.snapshot()?.command_query, ""); + Ok(()) +}