diff --git a/assets/icons/file_icons/lock.svg b/assets/icons/file_icons/lock.svg
index 10ae33869a..4d21d5db07 100644
--- a/assets/icons/file_icons/lock.svg
+++ b/assets/icons/file_icons/lock.svg
@@ -1,4 +1,6 @@
diff --git a/assets/icons/lock_outlined.svg b/assets/icons/lock.svg
similarity index 100%
rename from assets/icons/lock_outlined.svg
rename to assets/icons/lock.svg
diff --git a/assets/icons/lock_off.svg b/assets/icons/lock_off.svg
new file mode 100644
index 0000000000..9efaa51aa8
--- /dev/null
+++ b/assets/icons/lock_off.svg
@@ -0,0 +1,6 @@
+
diff --git a/assets/icons/lock_outlined_off.svg b/assets/icons/lock_outlined_off.svg
deleted file mode 100644
index 74e5851dd7..0000000000
--- a/assets/icons/lock_outlined_off.svg
+++ /dev/null
@@ -1,7 +0,0 @@
-
diff --git a/crates/agent_ui/src/agent_panel.rs b/crates/agent_ui/src/agent_panel.rs
index a26d2c652e..a26935e859 100644
--- a/crates/agent_ui/src/agent_panel.rs
+++ b/crates/agent_ui/src/agent_panel.rs
@@ -6234,6 +6234,13 @@ impl AgentPanel {
.with_handle(self.new_thread_menu_handle.clone())
.menu(move |window, cx| new_thread_menu_builder(window, cx));
+ let sandbox_status = self
+ .active_conversation_view()
+ .and_then(|conversation_view| conversation_view.read(cx).root_thread_view())
+ .and_then(|thread_view| {
+ thread_view.update(cx, |thread_view, cx| thread_view.render_sandbox_status(cx))
+ });
+
base_container
.child(
h_flex()
@@ -6256,11 +6263,11 @@ impl AgentPanel {
)
.child(
h_flex()
+ .px_1()
.h_full()
.flex_none()
.gap_1()
- .pl_1()
- .pr_1()
+ .children(sandbox_status)
.when(can_create_entries, |this| this.child(new_thread_menu))
.child(full_screen_button)
.child(self.render_panel_options_menu(window, cx)),
diff --git a/crates/agent_ui/src/conversation_view/thread_view.rs b/crates/agent_ui/src/conversation_view/thread_view.rs
index 2519d28628..ed4bbeda81 100644
--- a/crates/agent_ui/src/conversation_view/thread_view.rs
+++ b/crates/agent_ui/src/conversation_view/thread_view.rs
@@ -24,6 +24,7 @@ use sandbox::{GitSandboxPolicy, SandboxFsPolicy, SandboxNetPolicy, SandboxPolicy
use crate::completion_provider::AvailableSkill;
use crate::message_editor::SharedSessionCapabilities;
+use crate::ui::{SandboxGroup, SandboxRow, SandboxSection, SandboxStatusTooltip};
use db::kvp::KeyValueStore;
use gpui::List;
@@ -4141,7 +4142,6 @@ impl ThreadView {
.gap_0p5()
.child(self.render_add_context_button(cx))
.child(self.render_follow_toggle(cx))
- .children(self.render_sandbox_status(cx))
.children(self.render_fast_mode_control(cx))
.children(self.render_thinking_control(cx)),
)
@@ -4619,13 +4619,7 @@ impl ThreadView {
}
}
- /// A small lock icon summarizing the sandbox state. Hovering shows the
- /// write-access paths and network-access domains from the user's settings
- /// and the per-thread overrides. The lock is struck through when sandboxing
- /// is turned off (in settings, or for this thread). Shown whenever
- /// sandboxing is *applicable* to the project, even when disabled, so the
- /// user can always see and change the state. Clicking opens the settings.
- fn render_sandbox_status(&mut self, cx: &mut Context) -> Option {
+ pub fn render_sandbox_status(&mut self, cx: &mut Context) -> Option {
let status = self.refresh_sandbox_status(cx)?;
let settings_sandbox = status.settings_sandbox.clone();
let thread_sandbox = status.thread_sandbox.clone();
@@ -4634,52 +4628,59 @@ impl ThreadView {
// The lock is struck only when the *merged* result is unsandboxed (the
// agent runs with ambient permissions). A layer that is merely wide open
// but still sandboxed keeps the closed lock.
- let icon = if settings_sandbox
+ let (icon, icon_color) = if settings_sandbox
.clone()
.merge(thread_sandbox.clone())
.is_unsandboxed()
{
- IconName::LockOutlinedOff
+ (IconName::LockOff, Color::Muted)
} else {
- IconName::LockOutlined
+ (IconName::Lock, Color::Default)
};
- let state = match (settings_sandbox, thread_sandbox) {
+ let tooltip = match (settings_sandbox, thread_sandbox) {
// No sandbox at all because the user turned it off in settings: the
// per-thread layer is moot, so don't show it.
- (ThreadSandbox::Unsandboxed, _) => SandboxTooltipState::DisabledInSettings,
+ (ThreadSandbox::Unsandboxed, _) => SandboxStatusTooltip::disabled_in_settings(),
// Sandboxed by settings, but disabled for this thread: show the
// settings scope (greyed) for context above the disabled status.
(ThreadSandbox::Sandboxed(settings_policy), ThreadSandbox::Unsandboxed) => {
- SandboxTooltipState::DisabledForThread {
- settings: augment_settings_sandbox_policy(settings_policy, baseline),
- }
+ let settings = augment_settings_sandbox_policy(settings_policy, baseline);
+ SandboxStatusTooltip::disabled_for_thread(sandbox_section(&settings, true))
}
(
ThreadSandbox::Sandboxed(settings_policy),
ThreadSandbox::Sandboxed(thread_policy),
- ) => SandboxTooltipState::Enabled {
- settings: augment_settings_sandbox_policy(settings_policy, baseline),
- thread: thread_policy,
- },
+ ) => {
+ let settings = augment_settings_sandbox_policy(settings_policy, baseline);
+ // Omit the per-thread section when it grants nothing extra.
+ let thread = (!sandbox_policy_grants_nothing(&thread_policy))
+ .then(|| sandbox_section(&thread_policy, false));
+ SandboxStatusTooltip::enabled(sandbox_section(&settings, true), thread)
+ }
};
Some(
- IconButton::new("sandbox-status", icon)
- .icon_size(IconSize::Small)
- .icon_color(Color::Muted)
- .tooltip(Tooltip::element(move |_window, cx| {
- render_sandbox_status_tooltip(&state, cx)
- }))
- .on_click(|_, window, cx| {
- window.dispatch_action(
- Box::new(zed_actions::OpenSettingsAt {
- path: zed_actions::AGENT_SANDBOX_SETTINGS_PATH.to_string(),
- target: None,
+ h_flex()
+ .gap_1()
+ .child(
+ IconButton::new("sandbox-status", icon)
+ .icon_size(IconSize::Small)
+ .icon_color(icon_color)
+ .tooltip(Tooltip::element(move |_window, _cx| {
+ tooltip.clone().into_any_element()
+ }))
+ .on_click(|_, window, cx| {
+ window.dispatch_action(
+ Box::new(zed_actions::OpenSettingsAt {
+ path: zed_actions::AGENT_SANDBOX_SETTINGS_PATH.to_string(),
+ target: None,
+ }),
+ cx,
+ );
}),
- cx,
- );
- })
+ )
+ .child(Divider::vertical().h_4())
.into_any_element(),
)
}
@@ -5596,74 +5597,6 @@ impl Render for TokenUsageTooltip {
}
}
-/// Which sandbox state the status tooltip describes.
-#[derive(Clone)]
-enum SandboxTooltipState {
- /// Sandboxing is on: show the settings policy and the per-thread overrides.
- Enabled {
- settings: SandboxPolicy,
- thread: SandboxPolicy,
- },
- /// Turned off via the persistent `allow_unsandboxed` setting.
- DisabledInSettings,
- /// Turned off for this thread (the "run without sandbox for this thread"
- /// fallback). The settings policy is still shown, greyed out, for context.
- DisabledForThread { settings: SandboxPolicy },
-}
-
-/// Build the hover tooltip for the sandbox status icon. Always opens with a
-/// "Sandboxing settings" title, then describes the active state.
-///
-/// Returns the raw content only: `Tooltip::element` wraps it in the tooltip
-/// container, so wrapping it again here would double the background.
-fn render_sandbox_status_tooltip(state: &SandboxTooltipState, cx: &App) -> AnyElement {
- let mut body = v_flex()
- .min_w(rems(15.))
- .gap_2()
- .child(Label::new("Sandboxing settings"))
- .child(Divider::horizontal());
-
- match state {
- SandboxTooltipState::DisabledInSettings => {
- body = body.child(
- Label::new("Sandboxing is disabled in settings")
- .size(LabelSize::Small)
- .color(Color::Muted),
- );
- }
- SandboxTooltipState::DisabledForThread { settings } => {
- // The settings policy is moot while disabled, so show it greyed out
- // for context above the active "disabled for this thread" status.
- let settings_section =
- render_sandbox_policy_section("From your settings", settings, true, cx);
- body = body
- .children(
- settings_section
- .map(|section| div().opacity(0.5).child(section).into_any_element()),
- )
- .child(Divider::horizontal())
- .child(Label::new("Sandboxing is disabled for this thread").size(LabelSize::Small));
- }
- SandboxTooltipState::Enabled { settings, thread } => {
- let settings_section =
- render_sandbox_policy_section("From your settings", settings, true, cx);
- let thread_section =
- render_sandbox_policy_section("Allowed in this thread", thread, false, cx);
- body = body.children(settings_section);
- if let Some(section) = thread_section {
- body = body.child(Divider::horizontal()).child(section);
- }
- }
- }
-
- body.into_any_element()
-}
-
-/// Render one section of the sandbox status tooltip.
-///
-/// When `show_empty` is true the section is always rendered, and groups that
-/// grant nothing show "None". When false, empty groups are omitted and the
-/// whole section is dropped (`None`) if it grants nothing at all.
/// Fold the always-granted baseline writable paths (the project's worktree
/// roots, derived from the same source the terminal tool uses) and, on Linux,
/// the host-isolated `/tmp` overlay into a settings policy for display. These
@@ -5692,41 +5625,34 @@ fn augment_settings_sandbox_policy(
policy
}
-fn render_sandbox_policy_section(
- title: &str,
- policy: &SandboxPolicy,
- show_empty: bool,
- cx: &App,
-) -> Option {
- let write_rows = sandbox_fs_rows(&policy.fs, cx);
- let network_rows = sandbox_network_rows(&policy.network, cx);
+fn sandbox_section(policy: &SandboxPolicy, show_empty: bool) -> SandboxSection {
let write_empty = fs_grants_nothing(&policy.fs);
let network_empty = network_grants_nothing(&policy.network);
- // Git access is only ever surfaced when *granted* (the `.git` dirs become
- // writable), so it never shows a "None" row — unlike write/network, it's
- // omitted entirely when denied, regardless of `show_empty`.
+ // Git access is only surfaced when granted, so it never shows a "None" row.
let git_empty = git_grants_nothing(&policy.git);
- if !show_empty && write_empty && network_empty && git_empty {
- return None;
+ let mut section = SandboxSection::new();
+ if show_empty || !write_empty {
+ section =
+ section.group(SandboxGroup::new("Write Access").rows(sandbox_fs_rows(&policy.fs)));
}
+ if show_empty || !network_empty {
+ section = section
+ .group(SandboxGroup::new("Network Access").rows(sandbox_network_rows(&policy.network)));
+ }
+ if !git_empty {
+ section = section
+ .group(SandboxGroup::new("Git Metadata Access").rows(sandbox_git_rows(&policy.git)));
+ }
+ section
+}
- let write_group =
- (show_empty || !write_empty).then(|| sandbox_status_group("Write access", write_rows));
- let network_group = (show_empty || !network_empty)
- .then(|| sandbox_status_group("Network access", network_rows));
- let git_group = (!git_empty)
- .then(|| sandbox_status_group("Git metadata access", sandbox_git_rows(&policy.git, cx)));
-
- Some(
- v_flex()
- .gap_1()
- .child(Label::new(title.to_string()))
- .children(write_group)
- .children(network_group)
- .children(git_group)
- .into_any_element(),
- )
+/// Whether a policy grants nothing worth surfacing, used to decide whether to
+/// show the per-thread overrides section at all.
+fn sandbox_policy_grants_nothing(policy: &SandboxPolicy) -> bool {
+ fs_grants_nothing(&policy.fs)
+ && network_grants_nothing(&policy.network)
+ && git_grants_nothing(&policy.git)
}
/// Git access grants nothing to surface unless `.git` writes are allowed *and*
@@ -5737,35 +5663,16 @@ fn git_grants_nothing(git: &GitSandboxPolicy) -> bool {
/// Rows for the Git-access group: one row per writable `.git` directory (these
/// may live outside the project for a linked worktree).
-fn sandbox_git_rows(git: &GitSandboxPolicy, cx: &App) -> Vec {
+fn sandbox_git_rows(git: &GitSandboxPolicy) -> Vec {
match git {
GitSandboxPolicy::Allowed { git_dirs } if !git_dirs.is_empty() => git_dirs
.iter()
- .map(|path| sandbox_git_path_row(path, cx))
+ .map(|path| SandboxRow::git(path.clone()))
.collect(),
_ => Vec::new(),
}
}
-/// A `.git` directory row, tagged with a Git icon to distinguish it from plain
-/// writable paths.
-fn sandbox_git_path_row(path: &Path, cx: &App) -> AnyElement {
- h_flex()
- .min_w_0()
- .gap_1()
- .child(
- Icon::new(IconName::GitBranch)
- .color(Color::Muted)
- .size(IconSize::Small),
- )
- .child(
- Label::new(path.display().to_string())
- .size(LabelSize::XSmall)
- .buffer_font(cx),
- )
- .into_any_element()
-}
-
fn fs_grants_nothing(fs: &SandboxFsPolicy) -> bool {
matches!(fs, SandboxFsPolicy::Restricted { writable_paths } if writable_paths.is_empty())
}
@@ -5780,95 +5687,35 @@ fn network_grants_nothing(network: &SandboxNetPolicy) -> bool {
/// Rows for the write-access group: a message for the "all"/"none" cases, or one
/// row per granted path.
-fn sandbox_fs_rows(fs: &SandboxFsPolicy, cx: &App) -> Vec {
+fn sandbox_fs_rows(fs: &SandboxFsPolicy) -> Vec {
match fs {
- SandboxFsPolicy::Unrestricted => vec![sandbox_message_row("All paths (unrestricted)")],
+ SandboxFsPolicy::Unrestricted => vec![SandboxRow::message("All paths (unrestricted)")],
SandboxFsPolicy::Restricted { writable_paths } if writable_paths.is_empty() => {
- vec![sandbox_message_row("None")]
+ vec![SandboxRow::message("None")]
}
SandboxFsPolicy::Restricted { writable_paths } => writable_paths
.iter()
- .map(|path| sandbox_path_row(path, cx))
+ .map(|path| SandboxRow::path(path.clone()))
.collect(),
}
}
/// Rows for the network-access group: a message for the "all"/"none" cases, or
/// one row per allowed domain.
-fn sandbox_network_rows(network: &SandboxNetPolicy, cx: &App) -> Vec {
+fn sandbox_network_rows(network: &SandboxNetPolicy) -> Vec {
match network {
- SandboxNetPolicy::Unrestricted => vec![sandbox_message_row("All domains (unrestricted)")],
- SandboxNetPolicy::Blocked => vec![sandbox_message_row("None")],
+ SandboxNetPolicy::Unrestricted => vec![SandboxRow::message("All domains (unrestricted)")],
+ SandboxNetPolicy::Blocked => vec![SandboxRow::message("None")],
SandboxNetPolicy::Restricted { allowed_domains } if allowed_domains.is_empty() => {
- vec![sandbox_message_row("None")]
+ vec![SandboxRow::message("None")]
}
SandboxNetPolicy::Restricted { allowed_domains } => allowed_domains
.iter()
- .map(|domain| sandbox_domain_row(domain, cx))
+ .map(|domain| SandboxRow::domain(domain.clone()))
.collect(),
}
}
-/// A "Write access" / "Network access" group: a muted header, then the indented
-/// rows.
-fn sandbox_status_group(heading: &str, rows: Vec) -> impl IntoElement {
- v_flex()
- .gap_0p5()
- .child(
- Label::new(heading.to_string())
- .size(LabelSize::Small)
- .color(Color::Muted),
- )
- .child(v_flex().pl_3().gap_0p5().children(rows))
-}
-
-/// A non-path row (e.g. "All paths (unrestricted)", "None") in a status group.
-fn sandbox_message_row(message: &str) -> AnyElement {
- Label::new(message.to_string())
- .size(LabelSize::XSmall)
- .color(Color::Muted)
- .into_any_element()
-}
-
-fn sandbox_path_row(path: &Path, cx: &App) -> AnyElement {
- let display_path = path.display().to_string();
- let icon = FileIcons::get_icon(path, cx)
- .map(Icon::from_path)
- .map(|icon| icon.color(Color::Muted).size(IconSize::Small))
- .unwrap_or_else(|| {
- Icon::new(IconName::Folder)
- .color(Color::Muted)
- .size(IconSize::Small)
- });
- h_flex()
- .min_w_0()
- .gap_1()
- .child(icon)
- .child(
- Label::new(display_path)
- .size(LabelSize::XSmall)
- .buffer_font(cx),
- )
- .into_any_element()
-}
-
-fn sandbox_domain_row(domain: &str, cx: &App) -> AnyElement {
- h_flex()
- .min_w_0()
- .gap_1()
- .child(
- Icon::new(IconName::Public)
- .color(Color::Muted)
- .size(IconSize::Small),
- )
- .child(
- Label::new(domain.to_string())
- .size(LabelSize::XSmall)
- .buffer_font(cx),
- )
- .into_any_element()
-}
-
impl ThreadView {
fn render_entries(&mut self, cx: &mut Context) -> List {
let max_content_width = AgentSettings::get_global(cx).max_content_width;
diff --git a/crates/agent_ui/src/ui.rs b/crates/agent_ui/src/ui.rs
index 4d355de28e..5fdb2afb5f 100644
--- a/crates/agent_ui/src/ui.rs
+++ b/crates/agent_ui/src/ui.rs
@@ -2,12 +2,14 @@ mod agent_notification;
mod end_trial_upsell;
mod mention_crease;
mod model_selector_components;
+mod sandbox_status_tooltip;
mod undo_reject_toast;
pub use agent_notification::*;
pub use end_trial_upsell::*;
pub use mention_crease::*;
pub use model_selector_components::*;
+pub use sandbox_status_tooltip::*;
pub use undo_reject_toast::*;
/// Returns the appropriate [`DocumentationSide`] for documentation asides
diff --git a/crates/agent_ui/src/ui/sandbox_status_tooltip.rs b/crates/agent_ui/src/ui/sandbox_status_tooltip.rs
new file mode 100644
index 0000000000..506d437b53
--- /dev/null
+++ b/crates/agent_ui/src/ui/sandbox_status_tooltip.rs
@@ -0,0 +1,280 @@
+use std::path::PathBuf;
+
+use file_icons::FileIcons;
+use gpui::{AnyElement, App};
+use ui::{Divider, prelude::*};
+
+#[derive(Clone)]
+pub enum SandboxRow {
+ Message(SharedString),
+ Path(PathBuf),
+ Git(PathBuf),
+ Domain(SharedString),
+}
+
+impl SandboxRow {
+ pub fn message(message: impl Into) -> Self {
+ Self::Message(message.into())
+ }
+
+ pub fn path(path: impl Into) -> Self {
+ Self::Path(path.into())
+ }
+
+ pub fn git(path: impl Into) -> Self {
+ Self::Git(path.into())
+ }
+
+ pub fn domain(domain: impl Into) -> Self {
+ Self::Domain(domain.into())
+ }
+
+ fn render(self, cx: &App) -> AnyElement {
+ let icon_basic = |icon_name: IconName| {
+ Icon::new(icon_name)
+ .color(Color::Muted)
+ .size(IconSize::Small)
+ };
+
+ let (icon, label) = match self {
+ SandboxRow::Message(message) => {
+ return Label::new(message)
+ .size(LabelSize::XSmall)
+ .color(Color::Muted)
+ .into_any_element();
+ }
+ SandboxRow::Path(path) => {
+ let icon = FileIcons::get_icon(&path, cx)
+ .map(|icon| {
+ Icon::from_path(icon)
+ .color(Color::Muted)
+ .size(IconSize::Small)
+ })
+ .unwrap_or_else(|| icon_basic(IconName::Folder));
+ (icon, path.display().to_string())
+ }
+ SandboxRow::Git(path) => (icon_basic(IconName::GitBranch), path.display().to_string()),
+ SandboxRow::Domain(domain) => (icon_basic(IconName::Public), domain.to_string()),
+ };
+
+ h_flex()
+ .min_w_0()
+ .w_full()
+ .gap_1()
+ .child(icon)
+ .child(
+ Label::new(label)
+ .size(LabelSize::XSmall)
+ .buffer_font(cx)
+ .truncate_start(),
+ )
+ .into_any_element()
+ }
+}
+
+/// A labelled group of rows (e.g. "Write Access", "Network Access").
+#[derive(Clone)]
+pub struct SandboxGroup {
+ heading: SharedString,
+ rows: Vec,
+}
+
+impl SandboxGroup {
+ pub fn new(heading: impl Into) -> Self {
+ Self {
+ heading: heading.into(),
+ rows: Vec::new(),
+ }
+ }
+
+ pub fn row(mut self, row: SandboxRow) -> Self {
+ self.rows.push(row);
+ self
+ }
+
+ pub fn rows(mut self, rows: impl IntoIterator- ) -> Self {
+ self.rows.extend(rows);
+ self
+ }
+
+ fn render(self, cx: &App) -> impl IntoElement {
+ v_flex()
+ .gap_1()
+ .child(
+ Label::new(self.heading)
+ .size(LabelSize::Small)
+ .color(Color::Muted),
+ )
+ .children(self.rows.into_iter().map(|row| row.render(cx)))
+ }
+}
+
+#[derive(Clone, Default)]
+pub struct SandboxSection {
+ groups: Vec,
+}
+
+impl SandboxSection {
+ pub fn new() -> Self {
+ Self::default()
+ }
+
+ pub fn group(mut self, group: SandboxGroup) -> Self {
+ self.groups.push(group);
+ self
+ }
+
+ fn render(self, cx: &App) -> AnyElement {
+ let mut section = v_flex().gap_1();
+ for (index, group) in self.groups.into_iter().enumerate() {
+ if index > 0 {
+ section = section.child(Divider::horizontal());
+ }
+ section = section.child(group.render(cx));
+ }
+ section.into_any_element()
+ }
+}
+
+#[derive(Clone, IntoElement, RegisterComponent)]
+pub enum SandboxStatusTooltip {
+ Enabled {
+ settings: SandboxSection,
+ thread: Option,
+ },
+ DisabledForThread {
+ settings: SandboxSection,
+ },
+ DisabledInSettings,
+}
+
+impl SandboxStatusTooltip {
+ pub fn enabled(settings: SandboxSection, thread: Option) -> Self {
+ Self::Enabled { settings, thread }
+ }
+
+ pub fn disabled_for_thread(settings: SandboxSection) -> Self {
+ Self::DisabledForThread { settings }
+ }
+
+ pub fn disabled_in_settings() -> Self {
+ Self::DisabledInSettings
+ }
+}
+
+impl RenderOnce for SandboxStatusTooltip {
+ fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
+ let content = match self {
+ SandboxStatusTooltip::DisabledInSettings => v_flex()
+ .child(
+ Label::new("You have sandboxing disabled in settings.")
+ .size(LabelSize::Small)
+ .color(Color::Muted),
+ )
+ .into_any_element(),
+ SandboxStatusTooltip::DisabledForThread { settings } => v_flex()
+ .gap_1()
+ .child(div().opacity(0.5).child(settings.render(cx)))
+ .child(Divider::horizontal())
+ .child(Label::new("Sandboxing is disabled for this thread").size(LabelSize::Small))
+ .into_any_element(),
+ SandboxStatusTooltip::Enabled { settings, thread } => v_flex()
+ .child(settings.render(cx))
+ .children(thread.map(|thread| {
+ v_flex()
+ .child(Divider::horizontal())
+ .child(thread.render(cx))
+ }))
+ .into_any_element(),
+ };
+
+ v_flex()
+ .min_w(rems(15.))
+ .gap_1()
+ .child(Label::new("Sandboxing"))
+ .child(content)
+ }
+}
+
+impl Component for SandboxStatusTooltip {
+ fn scope() -> ComponentScope {
+ ComponentScope::Agent
+ }
+
+ fn name() -> &'static str {
+ "Sandbox Status Tooltip"
+ }
+
+ fn description() -> &'static str {
+ "The tooltip shown on the sandboxing lock icon in the agent panel, \
+ describing the filesystem, network, and Git access granted to the \
+ agent for each of the possible sandbox states."
+ }
+
+ fn preview(_window: &mut Window, cx: &mut App) -> AnyElement {
+ let settings_section = SandboxSection::new()
+ .group(SandboxGroup::new("Write Access").rows([
+ SandboxRow::path("/Users/you/project"),
+ SandboxRow::path("/tmp (isolated)"),
+ ]))
+ .group(SandboxGroup::new("Network Access").rows([
+ SandboxRow::domain("github.com"),
+ SandboxRow::domain("*.npmjs.org"),
+ ]));
+
+ let thread_section = SandboxSection::new()
+ .group(
+ SandboxGroup::new("Write Access").row(SandboxRow::path("/Users/you/project/build")),
+ )
+ .group(SandboxGroup::new("Network Access").row(SandboxRow::message("None")))
+ .group(
+ SandboxGroup::new("Git Metadata Access")
+ .row(SandboxRow::git("/Users/you/project/.git")),
+ );
+
+ let unrestricted_section = SandboxSection::new()
+ .group(
+ SandboxGroup::new("Write Access")
+ .row(SandboxRow::message("All paths (unrestricted)")),
+ )
+ .group(
+ SandboxGroup::new("Network Access")
+ .row(SandboxRow::message("All domains (unrestricted)")),
+ );
+
+ let container = || div().p_2().elevation_2(cx).max_w_112();
+
+ v_flex()
+ .gap_4()
+ .child(example_group(vec![
+ single_example(
+ "Enabled",
+ container()
+ .child(SandboxStatusTooltip::enabled(
+ settings_section.clone(),
+ Some(thread_section),
+ ))
+ .into_any_element(),
+ ),
+ single_example(
+ "Enabled (unrestricted, no overrides)",
+ container()
+ .child(SandboxStatusTooltip::enabled(unrestricted_section, None))
+ .into_any_element(),
+ ),
+ single_example(
+ "Disabled for thread",
+ container()
+ .child(SandboxStatusTooltip::disabled_for_thread(settings_section))
+ .into_any_element(),
+ ),
+ single_example(
+ "Disabled in settings",
+ container()
+ .child(SandboxStatusTooltip::disabled_in_settings())
+ .into_any_element(),
+ ),
+ ]))
+ .into_any_element()
+ }
+}
diff --git a/crates/edit_prediction_ui/src/edit_prediction_button.rs b/crates/edit_prediction_ui/src/edit_prediction_button.rs
index 62d6209f1f..46a2dc6c74 100644
--- a/crates/edit_prediction_ui/src/edit_prediction_button.rs
+++ b/crates/edit_prediction_ui/src/edit_prediction_button.rs
@@ -910,7 +910,7 @@ impl EditPredictionButton {
menu = menu.item(
ContextMenuEntry::new("Configure Excluded Files")
- .icon(IconName::LockOutlined)
+ .icon(IconName::Lock)
.icon_color(Color::Muted)
.documentation_aside(DocumentationSide::Left, |_| {
Label::new(indoc!{"
diff --git a/crates/icons/src/icons.rs b/crates/icons/src/icons.rs
index 6588d3f07f..a94b43e801 100644
--- a/crates/icons/src/icons.rs
+++ b/crates/icons/src/icons.rs
@@ -183,8 +183,8 @@ pub enum IconName {
ListX,
LoadCircle,
LocationEdit,
- LockOutlined,
- LockOutlinedOff,
+ Lock,
+ LockOff,
MagnifyingGlass,
Maximize,
MaximizeAlt,