From 6fdc3ee09880cd3010a1dfc59e9dc947f43eecbd Mon Sep 17 00:00:00 2001 From: GeekMr <140129782+WilliamWang1721@users.noreply.github.com> Date: Sat, 3 Oct 2026 13:35:50 +0800 Subject: [PATCH 1/3] Show confirmed local administrator shields in terminal tab chrome --- nebula_app/i18n/en-US.json | 1 + nebula_app/i18n/zh-CN.json | 1 + .../gpui_shell/terminal/view/startup_tests.rs | 33 ++++++++ .../gpui_shell/terminal/view/tab_identity.rs | 12 +++ .../workspace/settings_navigation.rs | 1 + .../src/gpui_shell/workspace/sidebar.rs | 26 ++++++- .../gpui_shell/workspace/tab_presentation.rs | 75 +++++++++++++++++++ .../src/gpui_shell/workspace/top_tabs.rs | 8 ++ 8 files changed, 155 insertions(+), 2 deletions(-) diff --git a/nebula_app/i18n/en-US.json b/nebula_app/i18n/en-US.json index 52874d55b..cf76228a2 100644 --- a/nebula_app/i18n/en-US.json +++ b/nebula_app/i18n/en-US.json @@ -1013,6 +1013,7 @@ "tab_color": "Tab color" }, "chrome": { + "local_administrator": "Local Pebrel process is running as Windows administrator", "new_terminal_ctrl_shift_t": "New terminal (Ctrl+Shift+T)", "more": "More", "scroll_tabs_left": "Scroll tabs left", diff --git a/nebula_app/i18n/zh-CN.json b/nebula_app/i18n/zh-CN.json index a61a8c5b9..bf0994a7c 100644 --- a/nebula_app/i18n/zh-CN.json +++ b/nebula_app/i18n/zh-CN.json @@ -1013,6 +1013,7 @@ "tab_color": "标签颜色" }, "chrome": { + "local_administrator": "本机 Pebrel 进程正以 Windows 管理员身份运行", "new_terminal_ctrl_shift_t": "新建终端 (Ctrl+Shift+T)", "more": "更多", "scroll_tabs_left": "向左翻标签", diff --git a/nebula_app/src/gpui_shell/terminal/view/startup_tests.rs b/nebula_app/src/gpui_shell/terminal/view/startup_tests.rs index 05a157893..e0489c125 100644 --- a/nebula_app/src/gpui_shell/terminal/view/startup_tests.rs +++ b/nebula_app/src/gpui_shell/terminal/view/startup_tests.rs @@ -1231,3 +1231,36 @@ fn a_failed_codex_chooser_does_not_start_an_automatic_retry_loop(cx: &mut TestAp assert!(receiver.try_iter().all(|message| !matches!(message, Msg::Input(_)))); }); } + +#[gpui::test] +fn host_administrator_scope_excludes_remote_and_wsl_shells(cx: &mut TestAppContext) { + let (view, window, _) = open(cx); + view.update(window, |view, _| { + let context = |program: &str| { + crate::runtime_exec::PaneExecContext::from_pty_options(&nebula_terminal::tty::Options { + shell: Some(nebula_terminal::tty::Shell::new(program.into(), vec![])), + ..Default::default() + }) + }; + view.suggest.suggest_env = crate::display::SuggestEnv::Local; + view.exec_context = Some(context("pwsh.exe")); + assert!(view.inherits_windows_host_token()); + view.suggest.suggest_env = crate::display::SuggestEnv::Shell { + scope: crate::nebula_history::HistoryScope::Ssh("user@remote".into()), + }; + assert!(!view.inherits_windows_host_token()); + view.suggest.suggest_env = crate::display::SuggestEnv::Shell { + scope: crate::nebula_history::HistoryScope::Wsl("Ubuntu".into()), + }; + assert!(!view.inherits_windows_host_token()); + view.suggest.suggest_env = crate::display::SuggestEnv::Local; + view.exec_context = Some(context("wsl.exe")); + assert!(!view.inherits_windows_host_token()); + view.exec_context = Some(context("pwsh.exe")); + view.ssh_destination = Some("user@remote".into()); + assert!(!view.inherits_windows_host_token()); + view.ssh_destination = None; + view.exec_context = None; + assert!(!view.inherits_windows_host_token()); + }); +} diff --git a/nebula_app/src/gpui_shell/terminal/view/tab_identity.rs b/nebula_app/src/gpui_shell/terminal/view/tab_identity.rs index b498f67e7..b47058dbb 100644 --- a/nebula_app/src/gpui_shell/terminal/view/tab_identity.rs +++ b/nebula_app/src/gpui_shell/terminal/view/tab_identity.rs @@ -11,6 +11,18 @@ fn ssh_label(destination: Option<&str>, directory: &std::path::Path) -> Option bool { + self.ssh_destination.is_none() + && self + .exec_context + .as_ref() + .is_some_and(|context| context.wsl_distribution().is_none()) + && matches!( + self.suggest.suggest_env.history_scope(), + crate::nebula_history::HistoryScope::Local + ) + } + /// 目录来自 shell 上报;不要从带 Powerline 图标的提示符或选区反推路径。 pub fn working_directory(&self) -> Option<&str> { let path = self.cwd.as_str(); diff --git a/nebula_app/src/gpui_shell/workspace/settings_navigation.rs b/nebula_app/src/gpui_shell/workspace/settings_navigation.rs index 4d121eaf0..cd4ed1829 100644 --- a/nebula_app/src/gpui_shell/workspace/settings_navigation.rs +++ b/nebula_app/src/gpui_shell/workspace/settings_navigation.rs @@ -143,6 +143,7 @@ impl NebulaWorkspace { color: None, renaming: None, pane_count: 0, + local_administrator: false, } } diff --git a/nebula_app/src/gpui_shell/workspace/sidebar.rs b/nebula_app/src/gpui_shell/workspace/sidebar.rs index 38267f506..ff6b2e3af 100644 --- a/nebula_app/src/gpui_shell/workspace/sidebar.rs +++ b/nebula_app/src/gpui_shell/workspace/sidebar.rs @@ -241,6 +241,7 @@ impl NebulaWorkspace { color: tab_color, renaming, pane_count, + local_administrator, } = self.tab_presentation(ix, cx, dark); let hover_group: SharedString = format!("sidebar-tab-hover-{ix}").into(); let has_program_glyph = program_glyph.is_some(); @@ -475,6 +476,13 @@ impl NebulaWorkspace { ) }, ) + .when(local_administrator, |row| { + row.child(tab_presentation::administrator_badge( + format!("sidebar-admin-{ix}").into(), + label_px, + cx, + )) + }) // GPUI truncates the actual shaped title within its flex column. .child(match renaming { Some(input) => div() @@ -964,8 +972,15 @@ impl NebulaWorkspace { let chrome_family = theme.mono_font_family.clone(); let symbol_family: SharedString = crate::font_install::REQUIRED_FONT_FAMILY.into(); let label_px = settings.map(|settings| settings.ui_font_size_px).unwrap_or(15.0); - let TabPresentation { title, logo_image, logo_pending, program_glyph, pane_count, .. } = - self.tab_presentation(self.active, cx, dark); + let TabPresentation { + title, + logo_image, + logo_pending, + program_glyph, + pane_count, + local_administrator, + .. + } = self.tab_presentation(self.active, cx, dark); slot.child( h_flex() .absolute() @@ -975,6 +990,13 @@ impl NebulaWorkspace { .gap_2() // 两侧工具靠 flex 天然让位,这点内缩只是别让长标题贴到按钮上。 .px_4() + .when(local_administrator, |row| { + row.child(tab_presentation::administrator_badge( + "collapsed-title-admin".into(), + label_px, + cx, + )) + }) .when_some(logo_image, |row, image| { row.child( img(image) diff --git a/nebula_app/src/gpui_shell/workspace/tab_presentation.rs b/nebula_app/src/gpui_shell/workspace/tab_presentation.rs index 0f6d6dc22..2697a37ee 100644 --- a/nebula_app/src/gpui_shell/workspace/tab_presentation.rs +++ b/nebula_app/src/gpui_shell/workspace/tab_presentation.rs @@ -42,6 +42,7 @@ pub(super) struct TabPresentation { /// 本 tab 的分屏数(Terminal tab 才 > 0)。`> 1` 时行首图标换成 2×2 分屏 /// 标记、行尾挂一枚数量胶囊;见 [`pane_header::split_badge`]。 pub(super) pane_count: usize, + pub(super) local_administrator: bool, } impl NebulaWorkspace { @@ -77,6 +78,10 @@ impl NebulaWorkspace { let title = self.tab_title(ix, cx); let is_settings = self.tabs[ix].is_settings(); let is_terminal = self.tabs[ix].is_terminal(); + let local_administrator = crate::platform::elevation::is_elevated().unwrap_or(false) + && self.tabs[ix] + .focused_view() + .is_some_and(|view| view.read(cx).inherits_windows_host_token()); let pane_count = match &self.tabs[ix] { WorkspaceTab::Terminal { panes, .. } => panes.len(), _ => 0, @@ -142,6 +147,7 @@ impl NebulaWorkspace { color: meta.color, renaming, pane_count, + local_administrator, } } } @@ -152,3 +158,72 @@ pub(super) fn tooltip(text: SharedString, window: &mut Window, cx: &mut App) -> }) .build(window, cx) } + +pub(super) fn administrator_badge(selector: SharedString, label_px: f32, cx: &App) -> gpui::Div { + let label = crate::gpui_shell::config::ui_language(cx) + .text(crate::i18n::Message::ChromeLocalAdministrator); + div() + .id(selector.clone()) + .debug_selector(move || selector.to_string()) + .role(gpui::accesskit::Role::Image) + .aria_label(label) + .flex_shrink_0() + .w(px(label_px.max(TAB_LABEL_ICON_W))) + .flex() + .items_center() + .justify_center() + .text_size(px(label_px)) + .text_color(cx.theme().warning) + .child("🛡") + .tooltip(move |window, cx| tooltip(label.into(), window, cx)) +} + +#[cfg(all(test, feature = "gpui-test-support"))] +mod tests { + use super::*; + use gpui::TestAppContext; + use gpui_component::Root; + + struct BadgeSurface; + + impl Render for BadgeSurface { + fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { + h_flex() + .h(px(36.0)) + .items_center() + .child(administrator_badge("test-administrator".into(), 15.0, cx)) + .child(div().debug_selector(|| "test-admin-title".into()).child("pwsh")) + } + } + + #[gpui::test] + fn administrator_badge_keeps_a_readable_slot_with_reduced_motion(cx: &mut TestAppContext) { + cx.update(|cx| { + gpui_component::init(cx); + cx.set_global(crate::gpui_shell::config::Settings::load( + nebula_settings::ThemeName::Nord, + )); + }); + let (_, window) = cx.add_window_view(|window, cx| { + let surface = cx.new(|_| BadgeSurface); + Root::new(surface, window, cx) + }); + let mut previous = None; + for reduced_motion in [false, true] { + window.update(|window, cx| { + cx.set_reduce_motion(reduced_motion); + window.refresh(); + window.draw(cx).clear(cx); + }); + let badge = window.debug_bounds("test-administrator").unwrap(); + let title = window.debug_bounds("test-admin-title").unwrap(); + assert!(badge.size.width >= px(15.0)); + assert!(badge.size.height > px(0.0)); + assert!(badge.right() <= title.left()); + if let Some(previous) = previous { + assert_eq!(badge, previous); + } + previous = Some(badge); + } + } +} diff --git a/nebula_app/src/gpui_shell/workspace/top_tabs.rs b/nebula_app/src/gpui_shell/workspace/top_tabs.rs index a169c1a5e..c9c7a33fc 100644 --- a/nebula_app/src/gpui_shell/workspace/top_tabs.rs +++ b/nebula_app/src/gpui_shell/workspace/top_tabs.rs @@ -190,6 +190,7 @@ impl NebulaWorkspace { color, renaming, pane_count, + local_administrator, } = self.top_tab_presentation(ix, cx, dark); let hover_group: SharedString = format!("top-tab-hover-{ix}").into(); let cross_window_drag = self.cross_window_drag_payload(ix, cx); @@ -417,6 +418,13 @@ impl NebulaWorkspace { ) }, ) + .when(local_administrator, |row| { + row.child(super::tab_presentation::administrator_badge( + format!("top-tab-admin-{ix}").into(), + label_px, + cx, + )) + }) .child(match renaming { Some(input) => div() .flex_1() From e601dd0de58a5eac8f28838ce7c5e037d0d025cc Mon Sep 17 00:00:00 2001 From: GeekMr <140129782+WilliamWang1721@users.noreply.github.com> Date: Sat, 3 Oct 2026 13:50:02 +0800 Subject: [PATCH 2/3] fix(chrome): return the stateful administrator badge element --- nebula_app/src/gpui_shell/workspace/tab_presentation.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/nebula_app/src/gpui_shell/workspace/tab_presentation.rs b/nebula_app/src/gpui_shell/workspace/tab_presentation.rs index 2697a37ee..d5db6210c 100644 --- a/nebula_app/src/gpui_shell/workspace/tab_presentation.rs +++ b/nebula_app/src/gpui_shell/workspace/tab_presentation.rs @@ -159,7 +159,11 @@ pub(super) fn tooltip(text: SharedString, window: &mut Window, cx: &mut App) -> .build(window, cx) } -pub(super) fn administrator_badge(selector: SharedString, label_px: f32, cx: &App) -> gpui::Div { +pub(super) fn administrator_badge( + selector: SharedString, + label_px: f32, + cx: &App, +) -> gpui::Stateful { let label = crate::gpui_shell::config::ui_language(cx) .text(crate::i18n::Message::ChromeLocalAdministrator); div() From ac2413903b3f0b53a0f10afe79e91df66bdc1476 Mon Sep 17 00:00:00 2001 From: kuddev Date: Tue, 6 Oct 2026 14:50:20 +0800 Subject: [PATCH 3/3] fix(ui): place an equal-size administrator shield before task icons --- nebula_app/assets/icons/nebula-shield.svg | 3 ++ nebula_app/src/gpui_shell/assets.rs | 2 + .../src/gpui_shell/workspace/sidebar.rs | 15 ++++--- .../gpui_shell/workspace/tab_presentation.rs | 42 ++++++++++++------- .../src/gpui_shell/workspace/top_tabs.rs | 13 +++--- 5 files changed, 46 insertions(+), 29 deletions(-) create mode 100644 nebula_app/assets/icons/nebula-shield.svg diff --git a/nebula_app/assets/icons/nebula-shield.svg b/nebula_app/assets/icons/nebula-shield.svg new file mode 100644 index 000000000..4b3e9aac8 --- /dev/null +++ b/nebula_app/assets/icons/nebula-shield.svg @@ -0,0 +1,3 @@ + + + diff --git a/nebula_app/src/gpui_shell/assets.rs b/nebula_app/src/gpui_shell/assets.rs index 3c4727dc4..e96e9756b 100644 --- a/nebula_app/src/gpui_shell/assets.rs +++ b/nebula_app/src/gpui_shell/assets.rs @@ -41,6 +41,7 @@ const NEBULA_ICONS: &[(&str, &[u8])] = icons![ "pin", "pencil", "clipboard-paste", + "shield", "trash-2", "refresh", "phone", @@ -109,6 +110,7 @@ pub mod nav { /// Lucide pencil;固定组件资产集未收录,命令行内编辑动作需要明确图形语义。 pub const PENCIL: &str = "icons/nebula-pencil.svg"; pub const CLIPBOARD_PASTE: &str = "icons/nebula-clipboard-paste.svg"; + pub const SHIELD: &str = "icons/nebula-shield.svg"; /// Lucide trash-2;删除保存命令不能借用表示 Backspace 的 `IconName::Delete`。 pub const TRASH: &str = "icons/nebula-trash-2.svg"; pub const REFRESH: &str = "icons/nebula-refresh.svg"; diff --git a/nebula_app/src/gpui_shell/workspace/sidebar.rs b/nebula_app/src/gpui_shell/workspace/sidebar.rs index d5aead706..3694786a9 100644 --- a/nebula_app/src/gpui_shell/workspace/sidebar.rs +++ b/nebula_app/src/gpui_shell/workspace/sidebar.rs @@ -424,6 +424,13 @@ impl NebulaWorkspace { .bg(color), ) }) + // 权限属于整个会话,先于程序身份呈现,且不被 AI 图标替换。 + .when(local_administrator, |row| { + row.child(tab_presentation::administrator_badge( + format!("sidebar-admin-{ix}").into(), + cx, + )) + }) // 行首图标的优先级:**先身份、后形态**。AI 品牌图 / 程序字位 // 表达「这个 tab 里在跑什么」,它必须跟随聚焦 pane;2×2 // 分屏标记只在没有身份可显示时补位。 @@ -476,13 +483,6 @@ impl NebulaWorkspace { ) }, ) - .when(local_administrator, |row| { - row.child(tab_presentation::administrator_badge( - format!("sidebar-admin-{ix}").into(), - label_px, - cx, - )) - }) // GPUI truncates the actual shaped title within its flex column. .child(match renaming { Some(input) => div() @@ -995,7 +995,6 @@ impl NebulaWorkspace { .when(local_administrator, |row| { row.child(tab_presentation::administrator_badge( "collapsed-title-admin".into(), - label_px, cx, )) }) diff --git a/nebula_app/src/gpui_shell/workspace/tab_presentation.rs b/nebula_app/src/gpui_shell/workspace/tab_presentation.rs index d5db6210c..ef405eb17 100644 --- a/nebula_app/src/gpui_shell/workspace/tab_presentation.rs +++ b/nebula_app/src/gpui_shell/workspace/tab_presentation.rs @@ -159,11 +159,7 @@ pub(super) fn tooltip(text: SharedString, window: &mut Window, cx: &mut App) -> .build(window, cx) } -pub(super) fn administrator_badge( - selector: SharedString, - label_px: f32, - cx: &App, -) -> gpui::Stateful { +pub(super) fn administrator_badge(selector: SharedString, cx: &App) -> gpui::Stateful { let label = crate::gpui_shell::config::ui_language(cx) .text(crate::i18n::Message::ChromeLocalAdministrator); div() @@ -172,20 +168,24 @@ pub(super) fn administrator_badge( .role(gpui::accesskit::Role::Image) .aria_label(label) .flex_shrink_0() - .w(px(label_px.max(TAB_LABEL_ICON_W))) + .size(px(TAB_LABEL_ICON_SIZE)) .flex() .items_center() .justify_center() - .text_size(px(label_px)) - .text_color(cx.theme().warning) - .child("🛡") + // 权限提示与任务图标同尺寸;矢量图避免 emoji 字体改变颜色和几何。 + .child( + Icon::new(Icon::empty()) + .path(crate::gpui_shell::assets::nav::SHIELD) + .size(px(TAB_LABEL_ICON_SIZE)) + .text_color(cx.theme().warning), + ) .tooltip(move |window, cx| tooltip(label.into(), window, cx)) } #[cfg(all(test, feature = "gpui-test-support"))] mod tests { use super::*; - use gpui::TestAppContext; + use gpui::{AssetSource as _, TestAppContext}; use gpui_component::Root; struct BadgeSurface; @@ -195,13 +195,25 @@ mod tests { h_flex() .h(px(36.0)) .items_center() - .child(administrator_badge("test-administrator".into(), 15.0, cx)) + .child(administrator_badge("test-administrator".into(), cx)) + .child( + div() + .debug_selector(|| "test-task-icon".into()) + .size(px(TAB_LABEL_ICON_SIZE)) + .flex_shrink_0() + .child(Icon::new(IconName::SquareTerminal).size(px(TAB_LABEL_ICON_SIZE))), + ) .child(div().debug_selector(|| "test-admin-title".into()).child("pwsh")) } } #[gpui::test] fn administrator_badge_keeps_a_readable_slot_with_reduced_motion(cx: &mut TestAppContext) { + let asset = crate::gpui_shell::assets::NebulaAssets + .load(crate::gpui_shell::assets::nav::SHIELD) + .unwrap() + .expect("administrator shield must be embedded in the product"); + assert!(std::str::from_utf8(&asset).unwrap().contains("viewBox=\"0 0 24 24\"")); cx.update(|cx| { gpui_component::init(cx); cx.set_global(crate::gpui_shell::config::Settings::load( @@ -220,10 +232,12 @@ mod tests { window.draw(cx).clear(cx); }); let badge = window.debug_bounds("test-administrator").unwrap(); + let task = window.debug_bounds("test-task-icon").unwrap(); let title = window.debug_bounds("test-admin-title").unwrap(); - assert!(badge.size.width >= px(15.0)); - assert!(badge.size.height > px(0.0)); - assert!(badge.right() <= title.left()); + assert_eq!(badge.size, gpui::size(px(TAB_LABEL_ICON_SIZE), px(TAB_LABEL_ICON_SIZE))); + assert_eq!(badge.size, task.size); + assert!(badge.right() <= task.left()); + assert!(task.right() <= title.left()); if let Some(previous) = previous { assert_eq!(badge, previous); } diff --git a/nebula_app/src/gpui_shell/workspace/top_tabs.rs b/nebula_app/src/gpui_shell/workspace/top_tabs.rs index ec464cc97..778aa22df 100644 --- a/nebula_app/src/gpui_shell/workspace/top_tabs.rs +++ b/nebula_app/src/gpui_shell/workspace/top_tabs.rs @@ -365,6 +365,12 @@ impl NebulaWorkspace { .bg(color), ) }) + .when(local_administrator, |row| { + row.child(super::tab_presentation::administrator_badge( + format!("top-tab-admin-{ix}").into(), + cx, + )) + }) // 图标优先级与侧栏同源:先身份(跟随聚焦 pane),分屏标记 // 只在没有身份图标时补位。理由见 sidebar.rs 同处注释。 .when(is_settings, |row| { @@ -421,13 +427,6 @@ impl NebulaWorkspace { ) }, ) - .when(local_administrator, |row| { - row.child(super::tab_presentation::administrator_badge( - format!("top-tab-admin-{ix}").into(), - label_px, - cx, - )) - }) .child(match renaming { Some(input) => div() .flex_1()