slt/widgets/
commanding.rs1#[derive(Debug, Clone)]
5pub struct CommandPaletteState {
6 pub commands: Vec<PaletteCommand>,
8 pub input: String,
10 pub cursor: usize,
12 pub open: bool,
14 pub last_selected: Option<usize>,
17 selected: usize,
18}
19
20impl CommandPaletteState {
21 pub fn new(commands: Vec<PaletteCommand>) -> Self {
23 Self {
24 commands,
25 input: String::new(),
26 cursor: 0,
27 open: false,
28 last_selected: None,
29 selected: 0,
30 }
31 }
32
33 pub fn toggle(&mut self) {
35 self.open = !self.open;
36 if self.open {
37 self.input.clear();
38 self.cursor = 0;
39 self.selected = 0;
40 }
41 }
42
43 fn fuzzy_score(pattern: &str, text: &str) -> Option<i32> {
44 let pattern = pattern.trim();
45 if pattern.is_empty() {
46 return Some(0);
47 }
48
49 let text_chars: Vec<char> = text.chars().collect();
50 let mut score = 0;
51 let mut search_start = 0usize;
52 let mut prev_match: Option<usize> = None;
53
54 for p in pattern.chars() {
55 let mut found = None;
56 for (idx, ch) in text_chars.iter().enumerate().skip(search_start) {
57 if ch.eq_ignore_ascii_case(&p) {
58 found = Some(idx);
59 break;
60 }
61 }
62
63 let idx = found?;
64 if prev_match.is_some_and(|prev| idx == prev + 1) {
65 score += 3;
66 } else {
67 score += 1;
68 }
69
70 if idx == 0 {
71 score += 2;
72 } else {
73 let prev = text_chars[idx - 1];
74 let curr = text_chars[idx];
75 if matches!(prev, ' ' | '_' | '-') || prev.is_uppercase() || curr.is_uppercase() {
76 score += 2;
77 }
78 }
79
80 prev_match = Some(idx);
81 search_start = idx + 1;
82 }
83
84 Some(score)
85 }
86
87 pub(crate) fn filtered_indices(&self) -> Vec<usize> {
88 let query = self.input.trim();
89 if query.is_empty() {
90 return (0..self.commands.len()).collect();
91 }
92
93 let mut scored: Vec<(usize, i32)> = self
94 .commands
95 .iter()
96 .enumerate()
97 .filter_map(|(i, cmd)| {
98 let mut haystack =
99 String::with_capacity(cmd.label.len() + cmd.description.len() + 1);
100 haystack.push_str(&cmd.label);
101 haystack.push(' ');
102 haystack.push_str(&cmd.description);
103 Self::fuzzy_score(query, &haystack).map(|score| (i, score))
104 })
105 .collect();
106
107 if scored.is_empty() {
108 let tokens: Vec<String> = query.split_whitespace().map(|t| t.to_lowercase()).collect();
109 return self
110 .commands
111 .iter()
112 .enumerate()
113 .filter(|(_, cmd)| {
114 let label = cmd.label.to_lowercase();
115 let desc = cmd.description.to_lowercase();
116 tokens.iter().all(|token| {
117 label.contains(token.as_str()) || desc.contains(token.as_str())
118 })
119 })
120 .map(|(i, _)| i)
121 .collect();
122 }
123
124 scored.sort_by(|a, b| b.1.cmp(&a.1).then_with(|| a.0.cmp(&b.0)));
125 scored.into_iter().map(|(idx, _)| idx).collect()
126 }
127
128 pub(crate) fn selected(&self) -> usize {
129 self.selected
130 }
131
132 pub(crate) fn set_selected(&mut self, s: usize) {
133 self.selected = s;
134 }
135}
136
137#[derive(Debug, Clone)]
142pub struct StreamingTextState {
143 pub content: String,
145 pub streaming: bool,
147 pub(crate) cursor_visible: bool,
149 pub(crate) cursor_tick: u64,
150}
151
152impl StreamingTextState {
153 pub fn new() -> Self {
155 Self {
156 content: String::new(),
157 streaming: false,
158 cursor_visible: true,
159 cursor_tick: 0,
160 }
161 }
162
163 pub fn push(&mut self, chunk: &str) {
165 self.content.push_str(chunk);
166 }
167
168 pub fn finish(&mut self) {
170 self.streaming = false;
171 }
172
173 pub fn start(&mut self) {
175 self.content.clear();
176 self.streaming = true;
177 self.cursor_visible = true;
178 self.cursor_tick = 0;
179 }
180
181 pub fn clear(&mut self) {
183 self.content.clear();
184 self.streaming = false;
185 self.cursor_visible = true;
186 self.cursor_tick = 0;
187 }
188}
189
190impl Default for StreamingTextState {
191 fn default() -> Self {
192 Self::new()
193 }
194}
195
196#[derive(Debug, Clone)]
201pub struct StreamingMarkdownState {
202 pub content: String,
204 pub streaming: bool,
206 pub cursor_visible: bool,
208 pub cursor_tick: u64,
210 pub in_code_block: bool,
212 pub code_block_lang: String,
214}
215
216impl StreamingMarkdownState {
217 pub fn new() -> Self {
219 Self {
220 content: String::new(),
221 streaming: false,
222 cursor_visible: true,
223 cursor_tick: 0,
224 in_code_block: false,
225 code_block_lang: String::new(),
226 }
227 }
228
229 pub fn push(&mut self, chunk: &str) {
231 self.content.push_str(chunk);
232 }
233
234 pub fn start(&mut self) {
236 self.content.clear();
237 self.streaming = true;
238 self.cursor_visible = true;
239 self.cursor_tick = 0;
240 self.in_code_block = false;
241 self.code_block_lang.clear();
242 }
243
244 pub fn finish(&mut self) {
246 self.streaming = false;
247 }
248
249 pub fn clear(&mut self) {
251 self.content.clear();
252 self.streaming = false;
253 self.cursor_visible = true;
254 self.cursor_tick = 0;
255 self.in_code_block = false;
256 self.code_block_lang.clear();
257 }
258}
259
260impl Default for StreamingMarkdownState {
261 fn default() -> Self {
262 Self::new()
263 }
264}
265
266#[derive(Debug, Clone)]
271pub struct ScreenState {
272 stack: Vec<String>,
273}
274
275impl ScreenState {
276 pub fn new(initial: impl Into<String>) -> Self {
278 Self {
279 stack: vec![initial.into()],
280 }
281 }
282
283 pub fn current(&self) -> &str {
285 self.stack
286 .last()
287 .expect("ScreenState always contains at least one screen")
288 .as_str()
289 }
290
291 pub fn push(&mut self, name: impl Into<String>) {
293 self.stack.push(name.into());
294 }
295
296 pub fn pop(&mut self) {
298 if self.can_pop() {
299 self.stack.pop();
300 }
301 }
302
303 pub fn depth(&self) -> usize {
305 self.stack.len()
306 }
307
308 pub fn can_pop(&self) -> bool {
310 self.stack.len() > 1
311 }
312
313 pub fn reset(&mut self) {
315 self.stack.truncate(1);
316 }
317}
318
319#[non_exhaustive]
321#[derive(Debug, Clone, Copy, PartialEq, Eq)]
322pub enum ApprovalAction {
323 Pending,
325 Approved,
327 Rejected,
329}
330
331#[derive(Debug, Clone)]
337pub struct ToolApprovalState {
338 pub tool_name: String,
340 pub description: String,
342 pub action: ApprovalAction,
344}
345
346impl ToolApprovalState {
347 pub fn new(tool_name: impl Into<String>, description: impl Into<String>) -> Self {
349 Self {
350 tool_name: tool_name.into(),
351 description: description.into(),
352 action: ApprovalAction::Pending,
353 }
354 }
355
356 pub fn reset(&mut self) {
358 self.action = ApprovalAction::Pending;
359 }
360}
361
362#[derive(Debug, Clone)]
364pub struct ContextItem {
365 pub label: String,
367 pub tokens: usize,
369}
370
371impl ContextItem {
372 pub fn new(label: impl Into<String>, tokens: usize) -> Self {
374 Self {
375 label: label.into(),
376 tokens,
377 }
378 }
379}