lean_ctx/server/
tool_visibility.rs1use super::dynamic_tools::{categorize_tool, ToolCategory};
14use crate::core::tool_profiles::ToolProfile;
15
16pub const INVOKER: &str = "ctx_call";
19
20#[must_use]
26pub fn is_tool_visible(
27 name: &str,
28 profile: &ToolProfile,
29 disabled: &[String],
30 is_zed: bool,
31 role_allows: bool,
32) -> bool {
33 if categorize_tool(name) == ToolCategory::Internal {
34 return false;
35 }
36 if !profile.is_tool_enabled(name) {
37 return false;
38 }
39 if disabled.iter().any(|d| d == name) {
40 return false;
41 }
42 if is_zed && name == "ctx_edit" {
43 return false;
44 }
45 role_allows
46}
47
48#[must_use]
58pub fn category_gate_applies(supports_list_changed: bool, explicit_profile: bool) -> bool {
59 supports_list_changed && !explicit_profile
60}
61
62#[must_use]
68pub fn needs_invoker(
69 full_mode: bool,
70 already_present: bool,
71 invoker_role_allowed: bool,
72 disabled: &[String],
73) -> bool {
74 !full_mode && !already_present && invoker_role_allowed && !disabled.iter().any(|d| d == INVOKER)
75}
76
77#[cfg(test)]
78mod tests {
79 use super::*;
80
81 #[test]
82 fn internal_tools_never_visible_even_in_power() {
83 let p = ToolProfile::Power;
85 assert!(!is_tool_visible("ctx_metrics", &p, &[], false, true));
86 assert!(!is_tool_visible("ctx_cost", &p, &[], false, true));
87 assert!(!is_tool_visible("ctx_discover_tools", &p, &[], false, true));
88 }
89
90 #[test]
91 fn core_tool_visible_under_power() {
92 assert!(is_tool_visible(
93 "ctx_read",
94 &ToolProfile::Power,
95 &[],
96 false,
97 true
98 ));
99 }
100
101 #[test]
102 fn standard_exposes_its_advertised_tools() {
103 let p = ToolProfile::Standard;
107 assert!(is_tool_visible("ctx_architecture", &p, &[], false, true));
108 assert!(is_tool_visible("ctx_semantic_search", &p, &[], false, true));
109 assert!(is_tool_visible("ctx_callgraph", &p, &[], false, true));
110 }
111
112 #[test]
113 fn minimal_hides_non_minimal_tools() {
114 let p = ToolProfile::Minimal;
115 assert!(is_tool_visible("ctx_read", &p, &[], false, true));
116 assert!(!is_tool_visible("ctx_architecture", &p, &[], false, true));
117 }
118
119 #[test]
120 fn disabled_list_filters() {
121 let disabled = vec!["ctx_read".to_string()];
122 assert!(!is_tool_visible(
123 "ctx_read",
124 &ToolProfile::Power,
125 &disabled,
126 false,
127 true
128 ));
129 }
130
131 #[test]
132 fn zed_hides_ctx_edit_only() {
133 let p = ToolProfile::Power;
134 assert!(!is_tool_visible("ctx_edit", &p, &[], true, true));
135 assert!(is_tool_visible("ctx_read", &p, &[], true, true));
136 }
137
138 #[test]
139 fn role_block_hides_tool() {
140 assert!(!is_tool_visible(
141 "ctx_read",
142 &ToolProfile::Power,
143 &[],
144 false,
145 false
146 ));
147 }
148
149 #[test]
150 fn category_gate_only_in_default_lean_mode() {
151 assert!(category_gate_applies(true, false));
154 assert!(!category_gate_applies(true, true));
156 assert!(!category_gate_applies(false, false));
158 assert!(!category_gate_applies(false, true));
159 }
160
161 #[test]
162 fn invoker_added_when_missing_in_lazy_mode() {
163 assert!(needs_invoker(false, false, true, &[]));
164 }
165
166 #[test]
167 fn invoker_not_added_in_full_mode() {
168 assert!(!needs_invoker(true, false, true, &[]));
169 }
170
171 #[test]
172 fn invoker_not_duplicated_when_present() {
173 assert!(!needs_invoker(false, true, true, &[]));
174 }
175
176 #[test]
177 fn invoker_respects_role_and_disabled() {
178 assert!(!needs_invoker(false, false, false, &[]));
179 assert!(!needs_invoker(
180 false,
181 false,
182 true,
183 &["ctx_call".to_string()]
184 ));
185 }
186}