agent_ui: Refine UI design displaying sandbox information (#59980)
Adding a series of UI tweaks to refine how we display whether a thread is covered by sandbox. Most notably, separating all the tooltip UI stuff into a dedicated component so we can get a preview and iterate more easily on all of its states. Release Notes: - N/A
This commit is contained in:
1 parent
8ee64e6145
commit
f0e8b4c555
10 files changed
+372
-235
No files matched your search
@@ -1,4 +1,6 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M5 5C5 3.89543 5.89543 3 7 3H9C10.1046 3 11 3.89543 11 5V6H5V5Z" stroke="black" stroke-width="1.2"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M3.25 6.5C3.25 5.80964 3.80964 5.25 4.5 5.25H11.5C12.1904 5.25 12.75 5.80964 12.75 6.5V12.5C12.75 13.1904 12.1904 13.75 11.5 13.75H4.5C3.80964 13.75 3.25 13.1904 3.25 12.5V6.5ZM8.75 9.66146C8.90559 9.48517 9 9.25361 9 9C9 8.44772 8.55228 8 8 8C7.44772 8 7 8.44772 7 9C7 9.25361 7.09441 9.48517 7.25 9.66146V11C7.25 11.4142 7.58579 11.75 8 11.75C8.41421 11.75 8.75 11.4142 8.75 11V9.66146Z" fill="black"/>
|
||||
<path d="M8 9V11" stroke="#DCE0E5" stroke-width="1.2" stroke-linecap="round"/>
|
||||
<path d="M8 10C8.55228 10 9 9.55228 9 9C9 8.44772 8.55228 8 8 8C7.44772 8 7 8.44772 7 9C7 9.55228 7.44772 10 8 10Z" fill="#DCE0E5"/>
|
||||
<path d="M11 5.75H5C4.30964 5.75 3.75 6.30964 3.75 7V12C3.75 12.6904 4.30964 13.25 5 13.25H11C11.6904 13.25 12.25 12.6904 12.25 12V7C12.25 6.30964 11.6904 5.75 11 5.75Z" stroke="#DCE0E5" stroke-width="1.2" stroke-linejoin="round"/>
|
||||
<path d="M7.99998 2.79321C6.367 2.79321 5.04321 4.117 5.04321 5.74998H10.9568C10.9568 4.117 9.63296 2.79321 7.99998 2.79321Z" stroke="#DCE0E5" stroke-width="1.2" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 667 B After Width: | Height: | Size: 735 B |
File renamed without changes.
@@ -0,0 +1,6 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M8 9V11" stroke="#DCE0E5" stroke-width="1.2" stroke-linecap="round"/>
|
||||
<path d="M8 10C8.55228 10 9 9.55228 9 9C9 8.44772 8.55228 8 8 8C7.44772 8 7 8.44772 7 9C7 9.55228 7.44772 10 8 10Z" fill="#DCE0E5"/>
|
||||
<path d="M11 5.75H5C4.30964 5.75 3.75 6.30964 3.75 7V12C3.75 12.6904 4.30964 13.25 5 13.25H11C11.6904 13.25 12.25 12.6904 12.25 12V7C12.25 6.30964 11.6904 5.75 11 5.75Z" stroke="#DCE0E5" stroke-width="1.2" stroke-linejoin="round"/>
|
||||
<path d="M5.779 5.74999C5.17527 4.23271 5.91585 2.5133 7.43313 1.90957C8.34244 1.54775 9.32436 1.66876 10.0952 2.15042" stroke="#DCE0E5" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 752 B |
@@ -1,7 +0,0 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M5 5C5 3.89543 5.89543 3 7 3H9C10.1046 3 11 3.89543 11 5V6H5V5Z" stroke="black" stroke-width="1.2"/>
|
||||
<path d="M8 9V11" stroke="black" stroke-width="1.2" stroke-linecap="round"/>
|
||||
<circle cx="8" cy="9" r="1" fill="black"/>
|
||||
<rect x="3.75" y="5.75" width="8.5" height="7.5" rx="1.25" stroke="black" stroke-width="1.2" stroke-linejoin="round"/>
|
||||
<path d="M2.5 2.5L13.5 13.5" stroke="black" stroke-width="1.2" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 540 B |
@@ -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)),
|
||||
|
||||
@@ -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<Self>) -> Option<AnyElement> {
|
||||
pub fn render_sandbox_status(&mut self, cx: &mut Context<Self>) -> Option<AnyElement> {
|
||||
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<AnyElement> {
|
||||
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<AnyElement> {
|
||||
fn sandbox_git_rows(git: &GitSandboxPolicy) -> Vec<SandboxRow> {
|
||||
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<AnyElement> {
|
||||
fn sandbox_fs_rows(fs: &SandboxFsPolicy) -> Vec<SandboxRow> {
|
||||
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<AnyElement> {
|
||||
fn sandbox_network_rows(network: &SandboxNetPolicy) -> Vec<SandboxRow> {
|
||||
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<AnyElement>) -> 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<Self>) -> List {
|
||||
let max_content_width = AgentSettings::get_global(cx).max_content_width;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<SharedString>) -> Self {
|
||||
Self::Message(message.into())
|
||||
}
|
||||
|
||||
pub fn path(path: impl Into<PathBuf>) -> Self {
|
||||
Self::Path(path.into())
|
||||
}
|
||||
|
||||
pub fn git(path: impl Into<PathBuf>) -> Self {
|
||||
Self::Git(path.into())
|
||||
}
|
||||
|
||||
pub fn domain(domain: impl Into<SharedString>) -> 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<SandboxRow>,
|
||||
}
|
||||
|
||||
impl SandboxGroup {
|
||||
pub fn new(heading: impl Into<SharedString>) -> 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<Item = SandboxRow>) -> 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<SandboxGroup>,
|
||||
}
|
||||
|
||||
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<SandboxSection>,
|
||||
},
|
||||
DisabledForThread {
|
||||
settings: SandboxSection,
|
||||
},
|
||||
DisabledInSettings,
|
||||
}
|
||||
|
||||
impl SandboxStatusTooltip {
|
||||
pub fn enabled(settings: SandboxSection, thread: Option<SandboxSection>) -> 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()
|
||||
}
|
||||
}
|
||||
@@ -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!{"
|
||||
|
||||
@@ -183,8 +183,8 @@ pub enum IconName {
|
||||
ListX,
|
||||
LoadCircle,
|
||||
LocationEdit,
|
||||
LockOutlined,
|
||||
LockOutlinedOff,
|
||||
Lock,
|
||||
LockOff,
|
||||
MagnifyingGlass,
|
||||
Maximize,
|
||||
MaximizeAlt,
|
||||
|
||||
Reference in new issue
Block a user