Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions nebula_app/assets/icons/nebula-shield.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions nebula_app/i18n/en-US.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
1 change: 1 addition & 0 deletions nebula_app/i18n/zh-CN.json
Original file line number Diff line number Diff line change
Expand Up @@ -1013,6 +1013,7 @@
"tab_color": "标签颜色"
},
"chrome": {
"local_administrator": "本机 Pebrel 进程正以 Windows 管理员身份运行",
"new_terminal_ctrl_shift_t": "新建终端 (Ctrl+Shift+T)",
"more": "更多",
"scroll_tabs_left": "向左翻标签",
Expand Down
2 changes: 2 additions & 0 deletions nebula_app/src/gpui_shell/assets.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ const NEBULA_ICONS: &[(&str, &[u8])] = icons![
"pin",
"pencil",
"clipboard-paste",
"shield",
"trash-2",
"refresh",
"phone",
Expand Down Expand Up @@ -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";
Expand Down
33 changes: 33 additions & 0 deletions nebula_app/src/gpui_shell/terminal/view/startup_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1248,3 +1248,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());
});
}
12 changes: 12 additions & 0 deletions nebula_app/src/gpui_shell/terminal/view/tab_identity.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,18 @@ fn ssh_label(destination: Option<&str>, directory: &std::path::Path) -> Option<S
}

impl TerminalView {
pub(crate) fn inherits_windows_host_token(&self) -> 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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,7 @@ impl NebulaWorkspace {
color: None,
renaming: None,
pane_count: 0,
local_administrator: false,
}
}

Expand Down
25 changes: 23 additions & 2 deletions nebula_app/src/gpui_shell/workspace/sidebar.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -423,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
// 分屏标记只在没有身份可显示时补位。
Expand Down Expand Up @@ -966,8 +974,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()
Expand All @@ -977,6 +992,12 @@ impl NebulaWorkspace {
.gap_2()
// 两侧工具靠 flex 天然让位,这点内缩只是别让长标题贴到按钮上。
.px_4()
.when(local_administrator, |row| {
row.child(tab_presentation::administrator_badge(
"collapsed-title-admin".into(),
cx,
))
})
.when_some(logo_image, |row, image| {
row.child(
img(image)
Expand Down
93 changes: 93 additions & 0 deletions nebula_app/src/gpui_shell/workspace/tab_presentation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -142,6 +147,7 @@ impl NebulaWorkspace {
color: meta.color,
renaming,
pane_count,
local_administrator,
}
}
}
Expand All @@ -152,3 +158,90 @@ pub(super) fn tooltip(text: SharedString, window: &mut Window, cx: &mut App) ->
})
.build(window, cx)
}

pub(super) fn administrator_badge(selector: SharedString, cx: &App) -> gpui::Stateful<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()
.size(px(TAB_LABEL_ICON_SIZE))
.flex()
.items_center()
.justify_center()
// 权限提示与任务图标同尺寸;矢量图避免 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::{AssetSource as _, TestAppContext};
use gpui_component::Root;

struct BadgeSurface;

impl Render for BadgeSurface {
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
h_flex()
.h(px(36.0))
.items_center()
.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(
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 task = window.debug_bounds("test-task-icon").unwrap();
let title = window.debug_bounds("test-admin-title").unwrap();
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);
}
previous = Some(badge);
}
}
}
7 changes: 7 additions & 0 deletions nebula_app/src/gpui_shell/workspace/top_tabs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,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);
Expand Down Expand Up @@ -364,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| {
Expand Down
Loading