1#[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 filter_cache: Option<(String, Vec<usize>)>,
21}
22
23impl CommandPaletteState {
24 pub fn new(commands: Vec<PaletteCommand>) -> Self {
26 Self {
27 commands,
28 input: String::new(),
29 cursor: 0,
30 open: false,
31 last_selected: None,
32 selected: 0,
33 filter_cache: None,
34 }
35 }
36
37 pub fn toggle(&mut self) {
39 self.open = !self.open;
40 if self.open {
41 self.input.clear();
42 self.cursor = 0;
43 self.selected = 0;
44 self.filter_cache = None;
45 }
46 }
47
48 fn fuzzy_score(pattern: &str, text: &str) -> Option<i32> {
49 let pattern = pattern.trim();
50 if pattern.is_empty() {
51 return Some(0);
52 }
53
54 let text_chars: Vec<char> = text.chars().collect();
55 let mut score = 0;
56 let mut search_start = 0usize;
57 let mut prev_match: Option<usize> = None;
58
59 for p in pattern.chars() {
60 let mut found = None;
61 for (idx, ch) in text_chars.iter().enumerate().skip(search_start) {
62 if ch.eq_ignore_ascii_case(&p) {
63 found = Some(idx);
64 break;
65 }
66 }
67
68 let idx = found?;
69 if prev_match.is_some_and(|prev| idx == prev + 1) {
70 score += 3;
71 } else {
72 score += 1;
73 }
74
75 if idx == 0 {
76 score += 2;
77 } else {
78 let prev = text_chars[idx - 1];
79 let curr = text_chars[idx];
80 if matches!(prev, ' ' | '_' | '-') || prev.is_uppercase() || curr.is_uppercase() {
81 score += 2;
82 }
83 }
84
85 prev_match = Some(idx);
86 search_start = idx + 1;
87 }
88
89 Some(score)
90 }
91
92 pub(crate) fn filtered_indices_cached(&mut self) -> &[usize] {
100 let needs_recompute = match &self.filter_cache {
101 Some((cached_input, _)) => *cached_input != self.input,
102 None => true,
103 };
104 if needs_recompute {
105 let indices = self.filtered_indices();
106 self.filter_cache = Some((self.input.clone(), indices));
107 }
108 &self
109 .filter_cache
110 .as_ref()
111 .expect("filter_cache populated above")
112 .1
113 }
114
115 pub(crate) fn filtered_indices(&self) -> Vec<usize> {
116 let query = self.input.trim();
117 if query.is_empty() {
118 return (0..self.commands.len()).collect();
119 }
120
121 let mut scored: Vec<(usize, i32)> = self
122 .commands
123 .iter()
124 .enumerate()
125 .filter_map(|(i, cmd)| {
126 let mut haystack =
127 String::with_capacity(cmd.label.len() + cmd.description.len() + 1);
128 haystack.push_str(&cmd.label);
129 haystack.push(' ');
130 haystack.push_str(&cmd.description);
131 Self::fuzzy_score(query, &haystack).map(|score| (i, score))
132 })
133 .collect();
134
135 if scored.is_empty() {
136 let tokens: Vec<String> = query.split_whitespace().map(|t| t.to_lowercase()).collect();
137 return self
138 .commands
139 .iter()
140 .enumerate()
141 .filter(|(_, cmd)| {
142 let label = cmd.label.to_lowercase();
143 let desc = cmd.description.to_lowercase();
144 tokens.iter().all(|token| {
145 label.contains(token.as_str()) || desc.contains(token.as_str())
146 })
147 })
148 .map(|(i, _)| i)
149 .collect();
150 }
151
152 scored.sort_by(|a, b| b.1.cmp(&a.1).then_with(|| a.0.cmp(&b.0)));
153 scored.into_iter().map(|(idx, _)| idx).collect()
154 }
155
156 pub(crate) fn selected(&self) -> usize {
157 self.selected
158 }
159
160 pub(crate) fn set_selected(&mut self, s: usize) {
161 self.selected = s;
162 }
163}
164
165#[derive(Debug, Clone)]
170pub struct StreamingTextState {
171 pub content: String,
173 pub streaming: bool,
175 pub(crate) cursor_visible: bool,
177 pub(crate) cursor_tick: u64,
178}
179
180impl StreamingTextState {
181 pub fn new() -> Self {
183 Self {
184 content: String::new(),
185 streaming: false,
186 cursor_visible: true,
187 cursor_tick: 0,
188 }
189 }
190
191 pub fn push(&mut self, chunk: &str) {
193 self.content.push_str(chunk);
194 }
195
196 pub fn finish(&mut self) {
198 self.streaming = false;
199 }
200
201 pub fn start(&mut self) {
203 self.content.clear();
204 self.streaming = true;
205 self.cursor_visible = true;
206 self.cursor_tick = 0;
207 }
208
209 pub fn clear(&mut self) {
211 self.content.clear();
212 self.streaming = false;
213 self.cursor_visible = true;
214 self.cursor_tick = 0;
215 }
216}
217
218impl Default for StreamingTextState {
219 fn default() -> Self {
220 Self::new()
221 }
222}
223
224#[derive(Debug, Clone)]
229pub struct StreamingMarkdownState {
230 pub content: String,
232 pub streaming: bool,
234 pub cursor_visible: bool,
236 pub cursor_tick: u64,
238 pub in_code_block: bool,
240 pub code_block_lang: String,
242}
243
244impl StreamingMarkdownState {
245 pub fn new() -> Self {
247 Self {
248 content: String::new(),
249 streaming: false,
250 cursor_visible: true,
251 cursor_tick: 0,
252 in_code_block: false,
253 code_block_lang: String::new(),
254 }
255 }
256
257 pub fn push(&mut self, chunk: &str) {
259 self.content.push_str(chunk);
260 }
261
262 pub fn start(&mut self) {
264 self.content.clear();
265 self.streaming = true;
266 self.cursor_visible = true;
267 self.cursor_tick = 0;
268 self.in_code_block = false;
269 self.code_block_lang.clear();
270 }
271
272 pub fn finish(&mut self) {
274 self.streaming = false;
275 }
276
277 pub fn clear(&mut self) {
279 self.content.clear();
280 self.streaming = false;
281 self.cursor_visible = true;
282 self.cursor_tick = 0;
283 self.in_code_block = false;
284 self.code_block_lang.clear();
285 }
286}
287
288impl Default for StreamingMarkdownState {
289 fn default() -> Self {
290 Self::new()
291 }
292}
293
294#[derive(Debug, Clone)]
316pub struct ScreenState {
317 stack: Vec<String>,
318 focus_state: std::collections::HashMap<String, (usize, usize)>,
319}
320
321impl ScreenState {
322 pub fn new(initial: impl Into<String>) -> Self {
324 Self {
325 stack: vec![initial.into()],
326 focus_state: std::collections::HashMap::new(),
327 }
328 }
329
330 pub fn current(&self) -> &str {
332 self.stack
333 .last()
334 .expect("ScreenState always contains at least one screen")
335 .as_str()
336 }
337
338 pub fn push(&mut self, name: impl Into<String>) {
340 self.stack.push(name.into());
341 }
342
343 pub fn pop(&mut self) {
345 if self.can_pop() {
346 self.stack.pop();
347 }
348 }
349
350 pub fn depth(&self) -> usize {
352 self.stack.len()
353 }
354
355 pub fn can_pop(&self) -> bool {
357 self.stack.len() > 1
358 }
359
360 pub fn reset(&mut self) {
362 self.stack.truncate(1);
363 }
364
365 pub(crate) fn save_focus(&mut self, name: &str, focus_index: usize, focus_count: usize) {
366 self.focus_state
367 .insert(name.to_string(), (focus_index, focus_count));
368 }
369
370 pub(crate) fn restore_focus(&self, name: &str) -> (usize, usize) {
371 self.focus_state.get(name).copied().unwrap_or((0, 0))
372 }
373}
374
375#[derive(Debug, Clone)]
394pub struct ModeState {
395 modes: std::collections::HashMap<String, ScreenState>,
396 active: String,
397}
398
399impl ModeState {
400 pub fn new(mode: impl Into<String>, screen: impl Into<String>) -> Self {
402 let mode = mode.into();
403 let mut modes = std::collections::HashMap::new();
404 modes.insert(mode.clone(), ScreenState::new(screen));
405 Self {
406 modes,
407 active: mode,
408 }
409 }
410
411 pub fn add_mode(&mut self, mode: impl Into<String>, screen: impl Into<String>) {
413 let mode = mode.into();
414 self.modes
415 .entry(mode)
416 .or_insert_with(|| ScreenState::new(screen));
417 }
418
419 pub fn switch_mode(&mut self, mode: impl Into<String>) {
424 let mode = mode.into();
425 assert!(
426 self.modes.contains_key(&mode),
427 "mode '{}' not found",
428 mode
429 );
430 self.active = mode;
431 }
432
433 pub fn try_switch_mode(&mut self, mode: impl Into<String>) -> bool {
441 let mode = mode.into();
442 if !self.modes.contains_key(&mode) {
443 return false;
444 }
445 self.active = mode;
446 true
447 }
448
449 pub fn active_mode(&self) -> &str {
451 &self.active
452 }
453
454 pub fn screens(&self) -> &ScreenState {
456 self.modes
457 .get(&self.active)
458 .expect("active mode must exist")
459 }
460
461 pub fn screens_mut(&mut self) -> &mut ScreenState {
463 self.modes
464 .get_mut(&self.active)
465 .expect("active mode must exist")
466 }
467}
468
469#[cfg(test)]
470mod mode_state_tests {
471 use super::ModeState;
472
473 #[test]
474 fn try_switch_mode_returns_false_for_unknown_mode() {
475 let mut modes = ModeState::new("app", "home");
476 modes.add_mode("settings", "general");
477 assert!(modes.try_switch_mode("settings"));
478 assert_eq!(modes.active_mode(), "settings");
479 assert!(!modes.try_switch_mode("nonexistent"));
480 assert_eq!(modes.active_mode(), "settings");
482 }
483}
484
485#[non_exhaustive]
487#[derive(Debug, Clone, Copy, PartialEq, Eq)]
488pub enum ApprovalAction {
489 Pending,
491 Approved,
493 Rejected,
495}
496
497#[derive(Debug, Clone)]
503pub struct ToolApprovalState {
504 pub tool_name: String,
506 pub description: String,
508 pub action: ApprovalAction,
510}
511
512impl ToolApprovalState {
513 pub fn new(tool_name: impl Into<String>, description: impl Into<String>) -> Self {
515 Self {
516 tool_name: tool_name.into(),
517 description: description.into(),
518 action: ApprovalAction::Pending,
519 }
520 }
521
522 pub fn reset(&mut self) {
524 self.action = ApprovalAction::Pending;
525 }
526}
527
528#[derive(Debug, Clone)]
530pub struct ContextItem {
531 pub label: String,
533 pub tokens: usize,
535}
536
537impl ContextItem {
538 pub fn new(label: impl Into<String>, tokens: usize) -> Self {
540 Self {
541 label: label.into(),
542 tokens,
543 }
544 }
545}