1use agent::{HARDCODED_SECURITY_RULES, ToolPermissionDecision};
2use agent_settings::AgentSettings;
3use gpui::{Focusable, ReadGlobal, ScrollHandle, TextStyleRefinement, point, prelude::*};
4use settings::{Settings as _, SettingsStore, ToolPermissionMode};
5use std::sync::Arc;
6use theme::ThemeSettings;
7use ui::{Banner, ContextMenu, Divider, PopoverMenu, Tooltip, prelude::*};
8use util::shell::ShellKind;
9
10use crate::{SettingsWindow, components::SettingsInputField};
11
12/// Tools that support permission rules
13const TOOLS: &[ToolInfo] = &[
14 ToolInfo {
15 id: "terminal",
16 name: "Terminal",
17 description: "Commands executed in the terminal",
18 regex_explanation: "Patterns are matched against each command in the input. Commands chained with &&, ||, ;, or pipes are split and checked individually.",
19 },
20 ToolInfo {
21 id: "edit_file",
22 name: "Edit File",
23 description: "File editing operations",
24 regex_explanation: "Patterns are matched against the file path being edited.",
25 },
26 ToolInfo {
27 id: "delete_path",
28 name: "Delete Path",
29 description: "File and directory deletion",
30 regex_explanation: "Patterns are matched against the path being deleted.",
31 },
32 ToolInfo {
33 id: "move_path",
34 name: "Move Path",
35 description: "File and directory moves/renames",
36 regex_explanation: "Patterns are matched against both the source and destination paths.",
37 },
38 ToolInfo {
39 id: "create_directory",
40 name: "Create Directory",
41 description: "Directory creation",
42 regex_explanation: "Patterns are matched against the directory path being created.",
43 },
44 ToolInfo {
45 id: "save_file",
46 name: "Save File",
47 description: "File saving operations",
48 regex_explanation: "Patterns are matched against the file path being saved.",
49 },
50 ToolInfo {
51 id: "fetch",
52 name: "Fetch",
53 description: "HTTP requests to URLs",
54 regex_explanation: "Patterns are matched against the URL being fetched.",
55 },
56 ToolInfo {
57 id: "web_search",
58 name: "Web Search",
59 description: "Web search queries",
60 regex_explanation: "Patterns are matched against the search query.",
61 },
62];
63
64struct ToolInfo {
65 id: &'static str,
66 name: &'static str,
67 description: &'static str,
68 regex_explanation: &'static str,
69}
70
71/// Renders the main tool permissions setup page showing a list of tools
72pub(crate) fn render_tool_permissions_setup_page(
73 settings_window: &SettingsWindow,
74 scroll_handle: &ScrollHandle,
75 window: &mut Window,
76 cx: &mut Context<SettingsWindow>,
77) -> AnyElement {
78 let tool_items: Vec<AnyElement> = TOOLS
79 .iter()
80 .enumerate()
81 .map(|(i, tool)| render_tool_list_item(settings_window, tool, i, window, cx))
82 .collect();
83
84 let page_description =
85 "Configure regex patterns to control which tool actions require confirmation.";
86
87 let scroll_step = px(40.);
88
89 v_flex()
90 .id("tool-permissions-page")
91 .on_action({
92 let scroll_handle = scroll_handle.clone();
93 move |_: &menu::SelectNext, window, cx| {
94 window.focus_next(cx);
95 let current_offset = scroll_handle.offset();
96 scroll_handle.set_offset(point(current_offset.x, current_offset.y - scroll_step));
97 }
98 })
99 .on_action({
100 let scroll_handle = scroll_handle.clone();
101 move |_: &menu::SelectPrevious, window, cx| {
102 window.focus_prev(cx);
103 let current_offset = scroll_handle.offset();
104 scroll_handle.set_offset(point(current_offset.x, current_offset.y + scroll_step));
105 }
106 })
107 .min_w_0()
108 .size_full()
109 .pt_2p5()
110 .px_8()
111 .pb_16()
112 .overflow_y_scroll()
113 .track_scroll(scroll_handle)
114 .child(Label::new("Tool Permission Rules").size(LabelSize::Large))
115 .child(
116 Label::new(page_description)
117 .size(LabelSize::Small)
118 .color(Color::Muted),
119 )
120 .child(
121 v_flex()
122 .mt_4()
123 .children(tool_items.into_iter().enumerate().flat_map(|(i, item)| {
124 let mut elements: Vec<AnyElement> = vec![item];
125 if i + 1 < TOOLS.len() {
126 elements.push(Divider::horizontal().into_any_element());
127 }
128 elements
129 })),
130 )
131 .into_any_element()
132}
133
134fn render_tool_list_item(
135 _settings_window: &SettingsWindow,
136 tool: &'static ToolInfo,
137 tool_index: usize,
138 _window: &mut Window,
139 cx: &mut Context<SettingsWindow>,
140) -> AnyElement {
141 let rules = get_tool_rules(tool.id, cx);
142 let rule_count =
143 rules.always_allow.len() + rules.always_deny.len() + rules.always_confirm.len();
144
145 let rule_summary = if rule_count > 0 {
146 Some(format!("{} rules", rule_count))
147 } else {
148 None
149 };
150
151 let render_fn = get_tool_render_fn(tool.id);
152
153 h_flex()
154 .w_full()
155 .py_3()
156 .justify_between()
157 .child(
158 v_flex()
159 .child(h_flex().gap_1().child(Label::new(tool.name)).when_some(
160 rule_summary,
161 |this, summary| {
162 this.child(
163 Label::new(summary)
164 .size(LabelSize::Small)
165 .color(Color::Muted),
166 )
167 },
168 ))
169 .child(
170 Label::new(tool.description)
171 .size(LabelSize::Small)
172 .color(Color::Muted),
173 ),
174 )
175 .child({
176 let tool_name = tool.name;
177 Button::new(format!("configure-{}", tool.id), "Configure")
178 .tab_index(tool_index as isize)
179 .style(ButtonStyle::OutlinedGhost)
180 .size(ButtonSize::Medium)
181 .icon(IconName::ChevronRight)
182 .icon_position(IconPosition::End)
183 .icon_color(Color::Muted)
184 .icon_size(IconSize::Small)
185 .on_click(cx.listener(move |this, _, window, cx| {
186 this.push_dynamic_sub_page(
187 tool_name,
188 "Configure Tool Rules",
189 None,
190 render_fn,
191 window,
192 cx,
193 );
194 }))
195 })
196 .into_any_element()
197}
198
199fn get_tool_render_fn(
200 tool_id: &str,
201) -> fn(&SettingsWindow, &ScrollHandle, &mut Window, &mut Context<SettingsWindow>) -> AnyElement {
202 match tool_id {
203 "terminal" => render_terminal_tool_config,
204 "edit_file" => render_edit_file_tool_config,
205 "delete_path" => render_delete_path_tool_config,
206 "move_path" => render_move_path_tool_config,
207 "create_directory" => render_create_directory_tool_config,
208 "save_file" => render_save_file_tool_config,
209 "fetch" => render_fetch_tool_config,
210 "web_search" => render_web_search_tool_config,
211 _ => render_terminal_tool_config, // fallback
212 }
213}
214
215/// Renders an individual tool's permission configuration page
216pub(crate) fn render_tool_config_page(
217 tool_id: &'static str,
218 _settings_window: &SettingsWindow,
219 scroll_handle: &ScrollHandle,
220 window: &mut Window,
221 cx: &mut Context<SettingsWindow>,
222) -> AnyElement {
223 let tool = TOOLS.iter().find(|t| t.id == tool_id).unwrap();
224 let rules = get_tool_rules(tool_id, cx);
225 let page_title = format!("{} Tool", tool.name);
226 let scroll_step = px(80.);
227
228 v_flex()
229 .id(format!("tool-config-page-{}", tool_id))
230 .on_action({
231 let scroll_handle = scroll_handle.clone();
232 move |_: &menu::SelectNext, window, cx| {
233 window.focus_next(cx);
234 let current_offset = scroll_handle.offset();
235 scroll_handle.set_offset(point(current_offset.x, current_offset.y - scroll_step));
236 }
237 })
238 .on_action({
239 let scroll_handle = scroll_handle.clone();
240 move |_: &menu::SelectPrevious, window, cx| {
241 window.focus_prev(cx);
242 let current_offset = scroll_handle.offset();
243 scroll_handle.set_offset(point(current_offset.x, current_offset.y + scroll_step));
244 }
245 })
246 .min_w_0()
247 .size_full()
248 .pt_2p5()
249 .px_8()
250 .pb_16()
251 .overflow_y_scroll()
252 .track_scroll(scroll_handle)
253 .child(
254 v_flex()
255 .min_w_0()
256 .child(Label::new(page_title).size(LabelSize::Large))
257 .child(
258 Label::new(tool.regex_explanation)
259 .size(LabelSize::Small)
260 .color(Color::Muted),
261 ),
262 )
263 .when(tool_id == "terminal", |this| {
264 this.child(render_hardcoded_security_banner(cx))
265 })
266 .child(render_verification_section(tool_id, window, cx))
267 .child(
268 v_flex()
269 .mt_6()
270 .min_w_0()
271 .w_full()
272 .gap_5()
273 .child(render_default_mode_section(tool_id, rules.default_mode, cx))
274 .child(Divider::horizontal().color(ui::DividerColor::BorderFaded))
275 .child(render_rule_section(
276 tool_id,
277 "Always Deny",
278 "If any of these regexes match, the tool action will be denied.",
279 RuleType::Deny,
280 &rules.always_deny,
281 cx,
282 ))
283 .child(Divider::horizontal().color(ui::DividerColor::BorderFaded))
284 .child(render_rule_section(
285 tool_id,
286 "Always Allow",
287 "If any of these regexes match, the tool action will be approved—unless an Always Confirm or Always Deny regex matches.",
288 RuleType::Allow,
289 &rules.always_allow,
290 cx,
291 ))
292 .child(Divider::horizontal().color(ui::DividerColor::BorderFaded))
293 .child(render_rule_section(
294 tool_id,
295 "Always Confirm",
296 "If any of these regexes match, a confirmation will be shown unless an Always Deny regex matches.",
297 RuleType::Confirm,
298 &rules.always_confirm,
299 cx,
300 )),
301 )
302 .into_any_element()
303}
304
305fn render_hardcoded_security_banner(cx: &mut Context<SettingsWindow>) -> AnyElement {
306 let pattern_labels = HARDCODED_SECURITY_RULES.terminal_deny.iter().map(|rule| {
307 h_flex()
308 .gap_1()
309 .child(
310 Icon::new(IconName::Dash)
311 .color(Color::Hidden)
312 .size(IconSize::Small),
313 )
314 .child(
315 Label::new(rule.pattern.clone())
316 .size(LabelSize::Small)
317 .color(Color::Muted)
318 .buffer_font(cx),
319 )
320 });
321
322 v_flex()
323 .mt_3()
324 .child(
325 Banner::new().child(
326 v_flex()
327 .py_1()
328 .gap_1()
329 .child(
330 Label::new(
331 "The following patterns are always blocked and cannot be overridden:",
332 )
333 .size(LabelSize::Small),
334 )
335 .children(pattern_labels),
336 ),
337 )
338 .into_any_element()
339}
340
341fn render_verification_section(
342 tool_id: &'static str,
343 window: &mut Window,
344 cx: &mut Context<SettingsWindow>,
345) -> AnyElement {
346 let input_id = format!("{}-verification-input", tool_id);
347
348 let settings = AgentSettings::get_global(cx);
349 let always_allow_enabled = settings.always_allow_tool_actions;
350
351 let editor = window.use_keyed_state(input_id, cx, |window, cx| {
352 let mut editor = editor::Editor::single_line(window, cx);
353 editor.set_placeholder_text("Enter a rule to see how it applies…", window, cx);
354
355 let global_settings = ThemeSettings::get_global(cx);
356 editor.set_text_style_refinement(TextStyleRefinement {
357 font_family: Some(global_settings.buffer_font.family.clone()),
358 font_size: Some(rems(0.75).into()),
359 ..Default::default()
360 });
361
362 editor
363 });
364
365 cx.observe(&editor, |_, _, cx| cx.notify()).detach();
366
367 let focus_handle = editor.focus_handle(cx).tab_index(0).tab_stop(true);
368
369 let current_text = editor.read(cx).text(cx);
370 let (decision, matched_patterns) = if current_text.is_empty() {
371 (None, Vec::new())
372 } else {
373 let matches = find_matched_patterns(tool_id, ¤t_text, cx);
374 let decision = evaluate_test_input(tool_id, ¤t_text, cx);
375 (Some(decision), matches)
376 };
377
378 let always_allow_description = "The Always Allow Tool Actions setting is enabled: all tools will be allowed regardless of these rules.";
379 let theme_colors = cx.theme().colors();
380
381 v_flex()
382 .mt_3()
383 .min_w_0()
384 .gap_2()
385 .when(always_allow_enabled, |this| {
386 this.child(
387 Banner::new()
388 .severity(Severity::Warning)
389 .wrap_content(false)
390 .child(
391 Label::new(always_allow_description)
392 .size(LabelSize::Small)
393 .mt(px(3.))
394 .mr_8(),
395 )
396 .action_slot(
397 Button::new("configure_setting", "Configure Setting")
398 .label_size(LabelSize::Small)
399 .on_click(cx.listener(|this, _, window, cx| {
400 this.navigate_to_setting(
401 "agent.always_allow_tool_actions",
402 window,
403 cx,
404 );
405 })),
406 ),
407 )
408 })
409 .child(
410 v_flex()
411 .p_2p5()
412 .gap_1p5()
413 .bg(theme_colors.surface_background.opacity(0.15))
414 .border_1()
415 .border_dashed()
416 .border_color(theme_colors.border_variant)
417 .rounded_sm()
418 .child(
419 Label::new("Test Your Rules")
420 .color(Color::Muted)
421 .size(LabelSize::Small),
422 )
423 .child(
424 h_flex()
425 .w_full()
426 .h_8()
427 .px_2()
428 .rounded_md()
429 .border_1()
430 .border_color(theme_colors.border)
431 .bg(theme_colors.editor_background)
432 .track_focus(&focus_handle)
433 .child(editor),
434 )
435 .when(decision.is_some(), |this| {
436 if matched_patterns.is_empty() {
437 this.child(
438 Label::new("No regex matches, using the default action (confirm).")
439 .size(LabelSize::Small)
440 .color(Color::Muted),
441 )
442 } else {
443 this.child(render_matched_patterns(&matched_patterns, cx))
444 }
445 }),
446 )
447 .into_any_element()
448}
449
450#[derive(Clone, Debug)]
451struct MatchedPattern {
452 pattern: String,
453 rule_type: RuleType,
454 is_overridden: bool,
455}
456
457fn find_matched_patterns(tool_id: &str, input: &str, cx: &App) -> Vec<MatchedPattern> {
458 let settings = AgentSettings::get_global(cx);
459 let rules = match settings.tool_permissions.tools.get(tool_id) {
460 Some(rules) => rules,
461 None => return Vec::new(),
462 };
463
464 let mut matched = Vec::new();
465 let mut has_deny_match = false;
466 let mut has_confirm_match = false;
467
468 for rule in &rules.always_deny {
469 if rule.is_match(input) {
470 has_deny_match = true;
471 matched.push(MatchedPattern {
472 pattern: rule.pattern.clone(),
473 rule_type: RuleType::Deny,
474 is_overridden: false,
475 });
476 }
477 }
478
479 for rule in &rules.always_confirm {
480 if rule.is_match(input) {
481 has_confirm_match = true;
482 matched.push(MatchedPattern {
483 pattern: rule.pattern.clone(),
484 rule_type: RuleType::Confirm,
485 is_overridden: has_deny_match,
486 });
487 }
488 }
489
490 for rule in &rules.always_allow {
491 if rule.is_match(input) {
492 matched.push(MatchedPattern {
493 pattern: rule.pattern.clone(),
494 rule_type: RuleType::Allow,
495 is_overridden: has_deny_match || has_confirm_match,
496 });
497 }
498 }
499
500 matched
501}
502
503fn render_matched_patterns(patterns: &[MatchedPattern], cx: &App) -> AnyElement {
504 v_flex()
505 .gap_1()
506 .children(patterns.iter().map(|pattern| {
507 let (type_label, color) = match pattern.rule_type {
508 RuleType::Deny => ("Always Deny", Color::Error),
509 RuleType::Confirm => ("Always Confirm", Color::Warning),
510 RuleType::Allow => ("Always Allow", Color::Success),
511 };
512
513 let type_color = if pattern.is_overridden {
514 Color::Muted
515 } else {
516 color
517 };
518
519 h_flex()
520 .gap_1()
521 .child(
522 Label::new(pattern.pattern.clone())
523 .size(LabelSize::Small)
524 .color(Color::Muted)
525 .buffer_font(cx)
526 .when(pattern.is_overridden, |this| this.strikethrough()),
527 )
528 .child(
529 Icon::new(IconName::Dash)
530 .size(IconSize::Small)
531 .color(Color::Custom(cx.theme().colors().icon_muted.opacity(0.4))),
532 )
533 .child(
534 Label::new(type_label)
535 .size(LabelSize::XSmall)
536 .color(type_color)
537 .when(pattern.is_overridden, |this| {
538 this.strikethrough().alpha(0.5)
539 }),
540 )
541 }))
542 .into_any_element()
543}
544
545fn evaluate_test_input(tool_id: &str, input: &str, cx: &App) -> ToolPermissionDecision {
546 let settings = AgentSettings::get_global(cx);
547
548 // Always pass false for always_allow_tool_actions so we test the actual rules,
549 // not the global override that bypasses all checks.
550 // ShellKind is only used for terminal tool's hardcoded security rules;
551 // for other tools, the check returns None immediately.
552 ToolPermissionDecision::from_input(
553 tool_id,
554 input,
555 &settings.tool_permissions,
556 false,
557 ShellKind::system(),
558 )
559}
560
561fn render_rule_section(
562 tool_id: &'static str,
563 title: &'static str,
564 description: &'static str,
565 rule_type: RuleType,
566 patterns: &[String],
567 cx: &mut Context<SettingsWindow>,
568) -> AnyElement {
569 let section_id = format!("{}-{:?}-section", tool_id, rule_type);
570
571 let user_patterns: Vec<_> = patterns.iter().enumerate().collect();
572
573 v_flex()
574 .id(section_id)
575 .child(Label::new(title))
576 .child(
577 Label::new(description)
578 .size(LabelSize::Small)
579 .color(Color::Muted),
580 )
581 .child(
582 v_flex()
583 .mt_2()
584 .w_full()
585 .gap_1p5()
586 .when(patterns.is_empty(), |this| {
587 this.child(render_pattern_empty_state(cx))
588 })
589 .when(!user_patterns.is_empty(), |this| {
590 this.child(v_flex().gap_1p5().children(user_patterns.iter().map(
591 |(index, pattern)| {
592 render_user_pattern_row(
593 tool_id,
594 rule_type,
595 *index,
596 (*pattern).clone(),
597 cx,
598 )
599 },
600 )))
601 })
602 .child(render_add_pattern_input(tool_id, rule_type, cx)),
603 )
604 .into_any_element()
605}
606
607fn render_pattern_empty_state(cx: &mut Context<SettingsWindow>) -> AnyElement {
608 h_flex()
609 .p_2()
610 .rounded_md()
611 .border_1()
612 .border_dashed()
613 .border_color(cx.theme().colors().border_variant)
614 .child(
615 Label::new("No patterns configured")
616 .size(LabelSize::Small)
617 .color(Color::Disabled),
618 )
619 .into_any_element()
620}
621
622fn render_user_pattern_row(
623 tool_id: &'static str,
624 rule_type: RuleType,
625 index: usize,
626 pattern: String,
627 cx: &mut Context<SettingsWindow>,
628) -> AnyElement {
629 let pattern_for_delete = pattern.clone();
630 let pattern_for_update = pattern.clone();
631 let tool_id_for_delete = tool_id.to_string();
632 let tool_id_for_update = tool_id.to_string();
633 let input_id = format!("{}-{:?}-pattern-{}", tool_id, rule_type, index);
634 let delete_id = format!("{}-{:?}-delete-{}", tool_id, rule_type, index);
635
636 SettingsInputField::new()
637 .with_id(input_id)
638 .with_initial_text(pattern)
639 .tab_index(0)
640 .with_buffer_font()
641 .color(Color::Default)
642 .action_slot(
643 IconButton::new(delete_id, IconName::Trash)
644 .icon_size(IconSize::Small)
645 .icon_color(Color::Muted)
646 .tooltip(Tooltip::text("Delete Pattern"))
647 .on_click(cx.listener(move |_, _, _, cx| {
648 delete_pattern(&tool_id_for_delete, rule_type, &pattern_for_delete, cx);
649 })),
650 )
651 .on_confirm(move |new_pattern, _window, cx| {
652 if let Some(new_pattern) = new_pattern {
653 let new_pattern = new_pattern.trim().to_string();
654 if !new_pattern.is_empty() && new_pattern != pattern_for_update {
655 update_pattern(
656 &tool_id_for_update,
657 rule_type,
658 &pattern_for_update,
659 new_pattern,
660 cx,
661 );
662 }
663 }
664 })
665 .into_any_element()
666}
667
668fn render_add_pattern_input(
669 tool_id: &'static str,
670 rule_type: RuleType,
671 _cx: &mut Context<SettingsWindow>,
672) -> AnyElement {
673 let tool_id_owned = tool_id.to_string();
674 let input_id = format!("{}-{:?}-new-pattern", tool_id, rule_type);
675
676 SettingsInputField::new()
677 .with_id(input_id)
678 .with_placeholder("Add regex pattern…")
679 .tab_index(0)
680 .with_buffer_font()
681 .display_clear_button()
682 .display_confirm_button()
683 .on_confirm(move |pattern, _window, cx| {
684 if let Some(pattern) = pattern {
685 if !pattern.trim().is_empty() {
686 save_pattern(&tool_id_owned, rule_type, pattern.trim().to_string(), cx);
687 }
688 }
689 })
690 .into_any_element()
691}
692
693fn render_default_mode_section(
694 tool_id: &'static str,
695 current_mode: ToolPermissionMode,
696 _cx: &mut Context<SettingsWindow>,
697) -> AnyElement {
698 let mode_label = match current_mode {
699 ToolPermissionMode::Allow => "Allow",
700 ToolPermissionMode::Deny => "Deny",
701 ToolPermissionMode::Confirm => "Confirm",
702 };
703
704 let tool_id_owned = tool_id.to_string();
705
706 h_flex()
707 .justify_between()
708 .child(
709 v_flex().child(Label::new("Default Action")).child(
710 Label::new("Action to take when no patterns match.")
711 .size(LabelSize::Small)
712 .color(Color::Muted),
713 ),
714 )
715 .child(
716 PopoverMenu::new(format!("default-mode-{}", tool_id))
717 .trigger(
718 Button::new(format!("mode-trigger-{}", tool_id), mode_label)
719 .tab_index(0_isize)
720 .style(ButtonStyle::Outlined)
721 .size(ButtonSize::Medium)
722 .icon(IconName::ChevronDown)
723 .icon_position(IconPosition::End)
724 .icon_size(IconSize::Small),
725 )
726 .menu(move |window, cx| {
727 let tool_id = tool_id_owned.clone();
728 Some(ContextMenu::build(window, cx, move |menu, _, _| {
729 let tool_id_confirm = tool_id.clone();
730 let tool_id_allow = tool_id.clone();
731 let tool_id_deny = tool_id;
732
733 menu.entry("Confirm", None, move |_, cx| {
734 set_default_mode(&tool_id_confirm, ToolPermissionMode::Confirm, cx);
735 })
736 .entry("Allow", None, move |_, cx| {
737 set_default_mode(&tool_id_allow, ToolPermissionMode::Allow, cx);
738 })
739 .entry("Deny", None, move |_, cx| {
740 set_default_mode(&tool_id_deny, ToolPermissionMode::Deny, cx);
741 })
742 }))
743 })
744 .anchor(gpui::Corner::TopRight),
745 )
746 .into_any_element()
747}
748
749#[derive(Clone, Copy, PartialEq, Debug)]
750pub(crate) enum RuleType {
751 Allow,
752 Deny,
753 Confirm,
754}
755
756struct ToolRulesView {
757 default_mode: ToolPermissionMode,
758 always_allow: Vec<String>,
759 always_deny: Vec<String>,
760 always_confirm: Vec<String>,
761}
762
763fn get_tool_rules(tool_name: &str, cx: &App) -> ToolRulesView {
764 let settings = AgentSettings::get_global(cx);
765
766 let tool_rules = settings.tool_permissions.tools.get(tool_name);
767
768 match tool_rules {
769 Some(rules) => ToolRulesView {
770 default_mode: rules.default_mode,
771 always_allow: rules
772 .always_allow
773 .iter()
774 .map(|r| r.pattern.clone())
775 .collect(),
776 always_deny: rules
777 .always_deny
778 .iter()
779 .map(|r| r.pattern.clone())
780 .collect(),
781 always_confirm: rules
782 .always_confirm
783 .iter()
784 .map(|r| r.pattern.clone())
785 .collect(),
786 },
787 None => ToolRulesView {
788 default_mode: ToolPermissionMode::Confirm,
789 always_allow: Vec::new(),
790 always_deny: Vec::new(),
791 always_confirm: Vec::new(),
792 },
793 }
794}
795
796fn save_pattern(tool_name: &str, rule_type: RuleType, pattern: String, cx: &mut App) {
797 let tool_name = tool_name.to_string();
798
799 SettingsStore::global(cx).update_settings_file(<dyn fs::Fs>::global(cx), move |settings, _| {
800 let tool_permissions = settings
801 .agent
802 .get_or_insert_default()
803 .tool_permissions
804 .get_or_insert_default();
805 let tool_rules = tool_permissions
806 .tools
807 .entry(Arc::from(tool_name.as_str()))
808 .or_default();
809
810 let rule = settings::ToolRegexRule {
811 pattern,
812 case_sensitive: None,
813 };
814
815 let rules_list = match rule_type {
816 RuleType::Allow => tool_rules.always_allow.get_or_insert_default(),
817 RuleType::Deny => tool_rules.always_deny.get_or_insert_default(),
818 RuleType::Confirm => tool_rules.always_confirm.get_or_insert_default(),
819 };
820
821 if !rules_list.0.iter().any(|r| r.pattern == rule.pattern) {
822 rules_list.0.push(rule);
823 }
824 });
825}
826
827fn update_pattern(
828 tool_name: &str,
829 rule_type: RuleType,
830 old_pattern: &str,
831 new_pattern: String,
832 cx: &mut App,
833) {
834 let tool_name = tool_name.to_string();
835 let old_pattern = old_pattern.to_string();
836
837 SettingsStore::global(cx).update_settings_file(<dyn fs::Fs>::global(cx), move |settings, _| {
838 let tool_permissions = settings
839 .agent
840 .get_or_insert_default()
841 .tool_permissions
842 .get_or_insert_default();
843
844 if let Some(tool_rules) = tool_permissions.tools.get_mut(tool_name.as_str()) {
845 let rules_list = match rule_type {
846 RuleType::Allow => &mut tool_rules.always_allow,
847 RuleType::Deny => &mut tool_rules.always_deny,
848 RuleType::Confirm => &mut tool_rules.always_confirm,
849 };
850
851 if let Some(list) = rules_list {
852 if let Some(rule) = list.0.iter_mut().find(|r| r.pattern == old_pattern) {
853 rule.pattern = new_pattern;
854 }
855 }
856 }
857 });
858}
859
860fn delete_pattern(tool_name: &str, rule_type: RuleType, pattern: &str, cx: &mut App) {
861 let tool_name = tool_name.to_string();
862 let pattern = pattern.to_string();
863
864 SettingsStore::global(cx).update_settings_file(<dyn fs::Fs>::global(cx), move |settings, _| {
865 let tool_permissions = settings
866 .agent
867 .get_or_insert_default()
868 .tool_permissions
869 .get_or_insert_default();
870
871 if let Some(tool_rules) = tool_permissions.tools.get_mut(tool_name.as_str()) {
872 let rules_list = match rule_type {
873 RuleType::Allow => &mut tool_rules.always_allow,
874 RuleType::Deny => &mut tool_rules.always_deny,
875 RuleType::Confirm => &mut tool_rules.always_confirm,
876 };
877
878 if let Some(list) = rules_list {
879 list.0.retain(|r| r.pattern != pattern);
880 }
881 }
882 });
883}
884
885fn set_default_mode(tool_name: &str, mode: ToolPermissionMode, cx: &mut App) {
886 let tool_name = tool_name.to_string();
887
888 SettingsStore::global(cx).update_settings_file(<dyn fs::Fs>::global(cx), move |settings, _| {
889 let tool_permissions = settings
890 .agent
891 .get_or_insert_default()
892 .tool_permissions
893 .get_or_insert_default();
894 let tool_rules = tool_permissions
895 .tools
896 .entry(Arc::from(tool_name.as_str()))
897 .or_default();
898 tool_rules.default_mode = Some(mode);
899 });
900}
901
902// Macro to generate render functions for each tool
903macro_rules! tool_config_page_fn {
904 ($fn_name:ident, $tool_id:literal) => {
905 pub fn $fn_name(
906 settings_window: &SettingsWindow,
907 scroll_handle: &ScrollHandle,
908 window: &mut Window,
909 cx: &mut Context<SettingsWindow>,
910 ) -> AnyElement {
911 render_tool_config_page($tool_id, settings_window, scroll_handle, window, cx)
912 }
913 };
914}
915
916tool_config_page_fn!(render_terminal_tool_config, "terminal");
917tool_config_page_fn!(render_edit_file_tool_config, "edit_file");
918tool_config_page_fn!(render_delete_path_tool_config, "delete_path");
919tool_config_page_fn!(render_move_path_tool_config, "move_path");
920tool_config_page_fn!(render_create_directory_tool_config, "create_directory");
921tool_config_page_fn!(render_save_file_tool_config, "save_file");
922tool_config_page_fn!(render_fetch_tool_config, "fetch");
923tool_config_page_fn!(render_web_search_tool_config, "web_search");