diff --git a/assets/icons/check_double.svg b/assets/icons/check_double.svg
new file mode 100644
index 0000000000..5c17d95a6b
--- /dev/null
+++ b/assets/icons/check_double.svg
@@ -0,0 +1 @@
+
diff --git a/assets/settings/default.json b/assets/settings/default.json
index c5f3475be4..c09c45f025 100644
--- a/assets/settings/default.json
+++ b/assets/settings/default.json
@@ -633,6 +633,8 @@
// The model to use.
"model": "claude-3-5-sonnet-latest"
},
+ // When enabled, the agent can run potentially destructive actions without asking for your confirmation.
+ "always_allow_tool_actions": false,
"default_profile": "write",
"profiles": {
"ask": {
diff --git a/crates/agent/src/active_thread.rs b/crates/agent/src/active_thread.rs
index 64b9f4cd79..ed224af9c6 100644
--- a/crates/agent/src/active_thread.rs
+++ b/crates/agent/src/active_thread.rs
@@ -24,7 +24,7 @@ use language::{Buffer, LanguageRegistry};
use language_model::{LanguageModelRegistry, LanguageModelToolUseId, Role};
use markdown::{Markdown, MarkdownStyle};
use project::ProjectItem as _;
-use settings::Settings as _;
+use settings::{Settings as _, update_settings_file};
use std::rc::Rc;
use std::sync::Arc;
use std::time::Duration;
@@ -1686,6 +1686,12 @@ impl ActiveThread {
let is_status_finished = matches!(&tool_use.status, ToolUseStatus::Finished(_));
+ let fs = self
+ .workspace
+ .upgrade()
+ .map(|workspace| workspace.read(cx).app_state().fs.clone());
+ let needs_confirmation = matches!(&tool_use.status, ToolUseStatus::NeedsConfirmation);
+
let status_icons = div().child(match &tool_use.status {
ToolUseStatus::Pending | ToolUseStatus::NeedsConfirmation => {
let icon = Icon::new(IconName::Warning)
@@ -1810,7 +1816,7 @@ impl ActiveThread {
if is_status_finished {
element.right_7()
} else {
- element.right_12()
+ element.right(px(46.))
}
})
.bg(linear_gradient(
@@ -1904,7 +1910,6 @@ impl ActiveThread {
h_flex()
.group("disclosure-header")
.relative()
- .gap_1p5()
.justify_between()
.py_1()
.map(|element| {
@@ -1918,6 +1923,8 @@ impl ActiveThread {
.map(|element| {
if is_open {
element.border_b_1().rounded_t_md()
+ } else if needs_confirmation {
+ element.rounded_t_md()
} else {
element.rounded_md()
}
@@ -1975,9 +1982,115 @@ impl ActiveThread {
parent.child(
v_flex()
.bg(cx.theme().colors().editor_background)
- .rounded_b_lg()
+ .map(|element| {
+ if needs_confirmation {
+ element.rounded_none()
+ } else {
+ element.rounded_b_lg()
+ }
+ })
.child(results_content),
)
+ })
+ .when(needs_confirmation, |this| {
+ this.child(
+ h_flex()
+ .py_1()
+ .pl_2()
+ .pr_1()
+ .gap_1()
+ .justify_between()
+ .bg(cx.theme().colors().editor_background)
+ .border_t_1()
+ .border_color(self.tool_card_border_color(cx))
+ .rounded_b_lg()
+ .child(Label::new("Action Confirmation").color(Color::Muted).size(LabelSize::Small))
+ .child(
+ h_flex()
+ .gap_0p5()
+ .child({
+ let tool_id = tool_use.id.clone();
+ Button::new(
+ "always-allow-tool-action",
+ "Always Allow",
+ )
+ .label_size(LabelSize::Small)
+ .icon(IconName::CheckDouble)
+ .icon_position(IconPosition::Start)
+ .icon_size(IconSize::Small)
+ .icon_color(Color::Success)
+ .tooltip(move |window, cx| {
+ Tooltip::with_meta(
+ "Never ask for permission",
+ None,
+ "Restore the original behavior in your Agent Panel settings",
+ window,
+ cx,
+ )
+ })
+ .on_click(cx.listener(
+ move |this, event, window, cx| {
+ if let Some(fs) = fs.clone() {
+ update_settings_file::(
+ fs.clone(),
+ cx,
+ |settings, _| {
+ settings.set_always_allow_tool_actions(true);
+ },
+ );
+ }
+ this.handle_allow_tool(
+ tool_id.clone(),
+ event,
+ window,
+ cx,
+ )
+ },
+ ))
+ })
+ .child(ui::Divider::vertical())
+ .child({
+ let tool_id = tool_use.id.clone();
+ Button::new("allow-tool-action", "Allow")
+ .label_size(LabelSize::Small)
+ .icon(IconName::Check)
+ .icon_position(IconPosition::Start)
+ .icon_size(IconSize::Small)
+ .icon_color(Color::Success)
+ .on_click(cx.listener(
+ move |this, event, window, cx| {
+ this.handle_allow_tool(
+ tool_id.clone(),
+ event,
+ window,
+ cx,
+ )
+ },
+ ))
+ })
+ .child({
+ let tool_id = tool_use.id.clone();
+ let tool_name: Arc = tool_use.name.into();
+ Button::new("deny-tool", "Deny")
+ .label_size(LabelSize::Small)
+ .icon(IconName::Close)
+ .icon_position(IconPosition::Start)
+ .icon_size(IconSize::Small)
+ .icon_color(Color::Error)
+ .on_click(cx.listener(
+ move |this, event, window, cx| {
+ this.handle_deny_tool(
+ tool_id.clone(),
+ tool_name.clone(),
+ event,
+ window,
+ cx,
+ )
+ },
+ ))
+ }),
+ ),
+ )
}),
)
}
@@ -2102,114 +2215,6 @@ impl ActiveThread {
}
}
- fn render_confirmations<'a>(
- &'a mut self,
- cx: &'a mut Context,
- ) -> impl Iterator- + 'a {
- let thread = self.thread.read(cx);
-
- thread.tools_needing_confirmation().map(|tool| {
- // Note: This element should be removed once a more full-fledged permission UX is implemented.
- let beta_tag = h_flex()
- .id("beta-tag")
- .h(px(18.))
- .px_1()
- .gap_1()
- .border_1()
- .border_color(cx.theme().colors().text_accent.opacity(0.2))
- .border_dashed()
- .rounded_sm()
- .bg(cx.theme().colors().text_accent.opacity(0.1))
- .hover(|style| style.bg(cx.theme().colors().text_accent.opacity(0.2)))
- .child(Label::new("Beta").size(LabelSize::XSmall))
- .child(Icon::new(IconName::Info).color(Color::Accent).size(IconSize::Indicator))
- .tooltip(
- Tooltip::text(
- "A future release will introduce a way to remember your answers to these. In the meantime, you can avoid these prompts by adding \"assistant\": { \"always_allow_tool_actions\": true } to your settings.json."
- )
- );
-
- v_flex()
- .mt_2()
- .mx_4()
- .border_1()
- .border_color(self.tool_card_border_color(cx))
- .rounded_lg()
- .child(
- h_flex()
- .py_1()
- .pl_2()
- .pr_1()
- .justify_between()
- .rounded_t_lg()
- .border_b_1()
- .border_color(self.tool_card_border_color(cx))
- .bg(self.tool_card_header_bg(cx))
- .child(
- h_flex()
- .gap_1()
- .child(Label::new("Action Confirmation").size(LabelSize::Small))
- .child(beta_tag),
- )
- .child(
- h_flex()
- .gap_1()
- .child({
- let tool_id = tool.id.clone();
- Button::new("allow-tool-action", "Allow")
- .label_size(LabelSize::Small)
- .icon(IconName::Check)
- .icon_position(IconPosition::Start)
- .icon_size(IconSize::Small)
- .icon_color(Color::Success)
- .on_click(cx.listener(move |this, event, window, cx| {
- this.handle_allow_tool(
- tool_id.clone(),
- event,
- window,
- cx,
- )
- }))
- })
- .child({
- let tool_id = tool.id.clone();
- let tool_name = tool.name.clone();
- Button::new("deny-tool", "Deny")
- .label_size(LabelSize::Small)
- .icon(IconName::Close)
- .icon_position(IconPosition::Start)
- .icon_size(IconSize::Small)
- .icon_color(Color::Error)
- .on_click(cx.listener(move |this, event, window, cx| {
- this.handle_deny_tool(
- tool_id.clone(),
- tool_name.clone(),
- event,
- window,
- cx,
- )
- }))
- }),
- ),
- )
- .child(
- div()
- .id("action_container")
- .rounded_b_lg()
- .bg(cx.theme().colors().editor_background)
- .overflow_y_scroll()
- .max_h_40()
- .p_2p5()
- .child(
- Label::new(&tool.ui_text)
- .size(LabelSize::Small)
- .buffer_font(cx),
- ),
- )
- .into_any()
- })
- }
-
fn dismiss_notifications(&mut self, cx: &mut Context) {
for window in self.notifications.drain(..) {
window
@@ -2262,7 +2267,6 @@ impl Render for ActiveThread {
.size_full()
.relative()
.child(list(self.list_state.clone()).flex_grow())
- .children(self.render_confirmations(cx))
.child(self.render_vertical_scrollbar(cx))
}
}
diff --git a/crates/agent/src/assistant_configuration.rs b/crates/agent/src/assistant_configuration.rs
index 1b6b096ee4..1506620511 100644
--- a/crates/agent/src/assistant_configuration.rs
+++ b/crates/agent/src/assistant_configuration.rs
@@ -4,11 +4,14 @@ mod tool_picker;
use std::sync::Arc;
+use assistant_settings::AssistantSettings;
use assistant_tool::{ToolSource, ToolWorkingSet};
use collections::HashMap;
use context_server::manager::ContextServerManager;
+use fs::Fs;
use gpui::{Action, AnyView, App, Entity, EventEmitter, FocusHandle, Focusable, Subscription};
use language_model::{LanguageModelProvider, LanguageModelProviderId, LanguageModelRegistry};
+use settings::{Settings, update_settings_file};
use ui::{Disclosure, Divider, DividerColor, ElevationIndex, Indicator, Switch, prelude::*};
use util::ResultExt as _;
use zed_actions::ExtensionCategoryFilter;
@@ -19,6 +22,7 @@ pub(crate) use manage_profiles_modal::ManageProfilesModal;
use crate::AddContextServer;
pub struct AssistantConfiguration {
+ fs: Arc,
focus_handle: FocusHandle,
configuration_views_by_provider: HashMap,
context_server_manager: Entity,
@@ -29,6 +33,7 @@ pub struct AssistantConfiguration {
impl AssistantConfiguration {
pub fn new(
+ fs: Arc,
context_server_manager: Entity,
tools: Arc,
window: &mut Window,
@@ -54,6 +59,7 @@ impl AssistantConfiguration {
);
let mut this = Self {
+ fs,
focus_handle,
configuration_views_by_provider: HashMap::default(),
context_server_manager,
@@ -167,6 +173,55 @@ impl AssistantConfiguration {
)
}
+ fn render_command_permission(&mut self, cx: &mut Context) -> impl IntoElement {
+ let always_allow_tool_actions = AssistantSettings::get_global(cx).always_allow_tool_actions;
+
+ const HEADING: &str = "Allow running tools without asking for confirmation";
+
+ v_flex()
+ .p(DynamicSpacing::Base16.rems(cx))
+ .gap_2()
+ .flex_1()
+ .child(Headline::new("General Settings").size(HeadlineSize::Small))
+ .child(
+ h_flex()
+ .p_2p5()
+ .rounded_sm()
+ .bg(cx.theme().colors().editor_background)
+ .border_1()
+ .border_color(cx.theme().colors().border)
+ .gap_4()
+ .justify_between()
+ .flex_wrap()
+ .child(
+ v_flex()
+ .gap_0p5()
+ .max_w_5_6()
+ .child(Label::new(HEADING))
+ .child(Label::new("When enabled, the agent can perform potentially destructive actions without asking for your confirmation.").color(Color::Muted)),
+ )
+ .child(
+ Switch::new(
+ "always-allow-tool-actions-switch",
+ always_allow_tool_actions.into(),
+ )
+ .on_click({
+ let fs = self.fs.clone();
+ move |state, _window, cx| {
+ let allow = state == &ToggleState::Selected;
+ update_settings_file::(
+ fs.clone(),
+ cx,
+ move |settings, _| {
+ settings.set_always_allow_tool_actions(allow);
+ },
+ );
+ }
+ }),
+ ),
+ )
+ }
+
fn render_context_servers_section(&mut self, cx: &mut Context) -> impl IntoElement {
let context_servers = self.context_server_manager.read(cx).all_servers().clone();
let tools_by_source = self.tools.tools_by_source(cx);
@@ -358,6 +413,8 @@ impl Render for AssistantConfiguration {
.bg(cx.theme().colors().panel_background)
.size_full()
.overflow_y_scroll()
+ .child(self.render_command_permission(cx))
+ .child(Divider::horizontal().color(DividerColor::Border))
.child(self.render_context_servers_section(cx))
.child(Divider::horizontal().color(DividerColor::Border))
.child(
diff --git a/crates/agent/src/assistant_panel.rs b/crates/agent/src/assistant_panel.rs
index cff3f61a21..e64e5c7360 100644
--- a/crates/agent/src/assistant_panel.rs
+++ b/crates/agent/src/assistant_panel.rs
@@ -482,11 +482,13 @@ impl AssistantPanel {
pub(crate) fn open_configuration(&mut self, window: &mut Window, cx: &mut Context) {
let context_server_manager = self.thread_store.read(cx).context_server_manager();
let tools = self.thread_store.read(cx).tools();
+ let fs = self.fs.clone();
self.active_view = ActiveView::Configuration;
- self.configuration = Some(
- cx.new(|cx| AssistantConfiguration::new(context_server_manager, tools, window, cx)),
- );
+ self.configuration =
+ Some(cx.new(|cx| {
+ AssistantConfiguration::new(fs, context_server_manager, tools, window, cx)
+ }));
if let Some(configuration) = self.configuration.as_ref() {
self.configuration_subscription = Some(cx.subscribe_in(
diff --git a/crates/assistant_settings/src/assistant_settings.rs b/crates/assistant_settings/src/assistant_settings.rs
index c5b496e23d..e3026fc118 100644
--- a/crates/assistant_settings/src/assistant_settings.rs
+++ b/crates/assistant_settings/src/assistant_settings.rs
@@ -325,6 +325,15 @@ impl AssistantSettingsContent {
}
}
+ pub fn set_always_allow_tool_actions(&mut self, allow: bool) {
+ let AssistantSettingsContent::Versioned(VersionedAssistantSettingsContent::V2(settings)) =
+ self
+ else {
+ return;
+ };
+ settings.always_allow_tool_actions = Some(allow);
+ }
+
pub fn set_profile(&mut self, profile_id: AgentProfileId) {
let AssistantSettingsContent::Versioned(VersionedAssistantSettingsContent::V2(settings)) =
self
diff --git a/crates/assistant_tools/src/bash_tool.rs b/crates/assistant_tools/src/bash_tool.rs
index 9423d3b81b..f504fb61c3 100644
--- a/crates/assistant_tools/src/bash_tool.rs
+++ b/crates/assistant_tools/src/bash_tool.rs
@@ -46,10 +46,21 @@ impl Tool for BashTool {
fn ui_text(&self, input: &serde_json::Value) -> String {
match serde_json::from_value::(input.clone()) {
Ok(input) => {
- if input.command.contains('\n') {
- MarkdownString::code_block("bash", &input.command).0
- } else {
- MarkdownString::inline_code(&input.command).0
+ let mut lines = input.command.lines();
+ let first_line = lines.next().unwrap_or_default();
+ let remaining_line_count = lines.count();
+ match remaining_line_count {
+ 0 => MarkdownString::inline_code(&first_line).0,
+ 1 => {
+ MarkdownString::inline_code(&format!(
+ "{} - {} more line",
+ first_line, remaining_line_count
+ ))
+ .0
+ }
+ n => {
+ MarkdownString::inline_code(&format!("{} - {} more lines", first_line, n)).0
+ }
}
}
Err(_) => "Run bash command".to_string(),
diff --git a/crates/icons/src/icons.rs b/crates/icons/src/icons.rs
index 31224cb37b..345a0d5ebf 100644
--- a/crates/icons/src/icons.rs
+++ b/crates/icons/src/icons.rs
@@ -46,6 +46,7 @@ pub enum IconName {
Brain,
CaseSensitive,
Check,
+ CheckDouble,
ChevronDown,
/// This chevron indicates a popover menu.
ChevronDownSmall,