1pub const DEFAULT_CHORD_TIMEOUT_TICKS: u64 = 60;
7
8#[derive(Debug, Default, Clone)]
28pub struct ChordState {
29 pub(crate) pending: String,
31 pub(crate) last_tick: u64,
33}
34
35#[derive(Debug, Clone)]
39pub struct CommandPaletteState {
40 commands: Vec<PaletteCommand>,
42 pub input: String,
44 pub cursor: usize,
46 pub open: bool,
48 pub last_selected: Option<usize>,
51 selected: usize,
52 filter_cache: Option<(String, Vec<usize>)>,
55}
56
57impl CommandPaletteState {
58 pub fn new(commands: Vec<PaletteCommand>) -> Self {
60 Self {
61 commands,
62 input: String::new(),
63 cursor: 0,
64 open: false,
65 last_selected: None,
66 selected: 0,
67 filter_cache: None,
68 }
69 }
70
71 pub fn commands(&self) -> &[PaletteCommand] {
73 &self.commands
74 }
75
76 pub fn set_commands(&mut self, commands: Vec<PaletteCommand>) {
78 self.commands = commands;
79 self.synchronize_commands();
80 }
81
82 pub fn push_command(&mut self, command: PaletteCommand) {
84 self.commands.push(command);
85 self.synchronize_commands();
86 }
87
88 pub fn remove_command(&mut self, index: usize) -> Option<PaletteCommand> {
90 if index >= self.commands.len() {
91 return None;
92 }
93 let command = self.commands.remove(index);
94 self.synchronize_commands();
95 Some(command)
96 }
97
98 pub fn clear_commands(&mut self) {
100 self.commands.clear();
101 self.synchronize_commands();
102 }
103
104 fn synchronize_commands(&mut self) {
105 self.filter_cache = None;
106 self.selected = 0;
107 self.last_selected = self
108 .last_selected
109 .filter(|&index| index < self.commands.len());
110 }
111
112 pub fn toggle(&mut self) {
114 self.open = !self.open;
115 if self.open {
116 self.input.clear();
117 self.cursor = 0;
118 self.selected = 0;
119 self.filter_cache = None;
120 }
121 }
122
123 pub(crate) fn fuzzy_score(pattern: &str, text: &str) -> Option<i32> {
124 let pattern = pattern.trim();
125 if pattern.is_empty() {
126 return Some(0);
127 }
128
129 let text_chars: Vec<char> = text.chars().collect();
130 let mut score = 0;
131 let mut search_start = 0usize;
132 let mut prev_match: Option<usize> = None;
133
134 for p in pattern.chars() {
135 let mut found = None;
136 for (idx, ch) in text_chars.iter().enumerate().skip(search_start) {
137 if ch.eq_ignore_ascii_case(&p) {
138 found = Some(idx);
139 break;
140 }
141 }
142
143 let idx = found?;
144 if prev_match.is_some_and(|prev| idx == prev + 1) {
145 score += 3;
146 } else {
147 score += 1;
148 }
149
150 if idx == 0 {
151 score += 2;
152 } else {
153 let prev = text_chars[idx - 1];
154 let curr = text_chars[idx];
155 if matches!(prev, ' ' | '_' | '-') || prev.is_uppercase() || curr.is_uppercase() {
156 score += 2;
157 }
158 }
159
160 prev_match = Some(idx);
161 search_start = idx + 1;
162 }
163
164 Some(score)
165 }
166
167 pub(crate) fn filtered_indices_cached(&mut self) -> &[usize] {
175 let needs_recompute = match &self.filter_cache {
176 Some((cached_input, _)) => *cached_input != self.input,
177 None => true,
178 };
179 if needs_recompute {
180 let indices = self.filtered_indices();
181 self.filter_cache = Some((self.input.clone(), indices));
182 }
183 &self
184 .filter_cache
185 .as_ref()
186 .expect("filter_cache populated above")
187 .1
188 }
189
190 pub(crate) fn filtered_indices(&self) -> Vec<usize> {
191 let query = self.input.trim();
192 if query.is_empty() {
193 return (0..self.commands.len()).collect();
194 }
195
196 let mut scored: Vec<(usize, i32)> = self
197 .commands
198 .iter()
199 .enumerate()
200 .filter_map(|(i, cmd)| {
201 let mut haystack =
202 String::with_capacity(cmd.label.len() + cmd.description.len() + 1);
203 haystack.push_str(&cmd.label);
204 haystack.push(' ');
205 haystack.push_str(&cmd.description);
206 Self::fuzzy_score(query, &haystack).map(|score| (i, score))
207 })
208 .collect();
209
210 if scored.is_empty() {
211 let tokens: Vec<String> = query.split_whitespace().map(|t| t.to_lowercase()).collect();
212 return self
213 .commands
214 .iter()
215 .enumerate()
216 .filter(|(_, cmd)| {
217 let label = cmd.label.to_lowercase();
218 let desc = cmd.description.to_lowercase();
219 tokens.iter().all(|token| {
220 label.contains(token.as_str()) || desc.contains(token.as_str())
221 })
222 })
223 .map(|(i, _)| i)
224 .collect();
225 }
226
227 scored.sort_by(|a, b| b.1.cmp(&a.1).then_with(|| a.0.cmp(&b.0)));
228 scored.into_iter().map(|(idx, _)| idx).collect()
229 }
230
231 pub(crate) fn selected(&self) -> usize {
232 self.selected
233 }
234
235 pub(crate) fn set_selected(&mut self, s: usize) {
236 self.selected = s;
237 }
238}
239
240#[derive(Debug, Clone)]
245pub struct StreamingTextState {
246 pub content: String,
248 pub streaming: bool,
250 pub(crate) cursor_visible: bool,
252 pub(crate) cursor_tick: u64,
253 pub(crate) version: u64,
256}
257
258impl StreamingTextState {
259 pub fn new() -> Self {
261 Self {
262 content: String::new(),
263 streaming: false,
264 cursor_visible: true,
265 cursor_tick: 0,
266 version: 0,
267 }
268 }
269
270 pub fn push(&mut self, chunk: &str) {
272 self.content.push_str(chunk);
273 self.version = self.version.wrapping_add(1);
274 }
275
276 pub fn finish(&mut self) {
278 self.streaming = false;
279 }
280
281 pub fn start(&mut self) {
283 self.content.clear();
284 self.streaming = true;
285 self.cursor_visible = true;
286 self.cursor_tick = 0;
287 self.version = self.version.wrapping_add(1);
288 }
289
290 pub fn clear(&mut self) {
292 self.content.clear();
293 self.streaming = false;
294 self.cursor_visible = true;
295 self.cursor_tick = 0;
296 self.version = self.version.wrapping_add(1);
297 }
298
299 pub fn version(&self) -> u64 {
323 self.version
324 }
325}
326
327impl Default for StreamingTextState {
328 fn default() -> Self {
329 Self::new()
330 }
331}
332
333#[derive(Debug, Clone)]
338pub struct StreamingMarkdownState {
339 pub content: String,
341 pub streaming: bool,
343 pub cursor_visible: bool,
345 pub cursor_tick: u64,
347 pub in_code_block: bool,
349 pub code_block_lang: String,
351 pub(crate) version: u64,
354}
355
356impl StreamingMarkdownState {
357 pub fn new() -> Self {
359 Self {
360 content: String::new(),
361 streaming: false,
362 cursor_visible: true,
363 cursor_tick: 0,
364 in_code_block: false,
365 code_block_lang: String::new(),
366 version: 0,
367 }
368 }
369
370 pub fn push(&mut self, chunk: &str) {
372 self.content.push_str(chunk);
373 self.version = self.version.wrapping_add(1);
374 }
375
376 pub fn start(&mut self) {
378 self.content.clear();
379 self.streaming = true;
380 self.cursor_visible = true;
381 self.cursor_tick = 0;
382 self.in_code_block = false;
383 self.code_block_lang.clear();
384 self.version = self.version.wrapping_add(1);
385 }
386
387 pub fn finish(&mut self) {
389 self.streaming = false;
390 }
391
392 pub fn clear(&mut self) {
394 self.content.clear();
395 self.streaming = false;
396 self.cursor_visible = true;
397 self.cursor_tick = 0;
398 self.in_code_block = false;
399 self.code_block_lang.clear();
400 self.version = self.version.wrapping_add(1);
401 }
402
403 pub fn version(&self) -> u64 {
422 self.version
423 }
424}
425
426impl Default for StreamingMarkdownState {
427 fn default() -> Self {
428 Self::new()
429 }
430}
431
432#[derive(Debug)]
454pub struct ScreenState {
455 id: u64,
456 stack: Vec<String>,
457 focus_state: std::collections::HashMap<String, (usize, usize)>,
458}
459
460impl Clone for ScreenState {
461 fn clone(&self) -> Self {
462 Self {
463 id: next_screen_state_id(),
464 stack: self.stack.clone(),
465 focus_state: self.focus_state.clone(),
466 }
467 }
468}
469
470fn next_screen_state_id() -> u64 {
471 static NEXT_ID: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(1);
472 NEXT_ID.fetch_add(1, std::sync::atomic::Ordering::Relaxed)
473}
474
475impl ScreenState {
476 pub fn new(initial: impl Into<String>) -> Self {
478 Self {
479 id: next_screen_state_id(),
480 stack: vec![initial.into()],
481 focus_state: std::collections::HashMap::new(),
482 }
483 }
484
485 pub(crate) fn id(&self) -> u64 {
486 self.id
487 }
488
489 pub fn current(&self) -> &str {
491 self.stack
492 .last()
493 .expect("ScreenState always contains at least one screen")
494 .as_str()
495 }
496
497 pub fn push(&mut self, name: impl Into<String>) {
499 self.stack.push(name.into());
500 }
501
502 pub fn pop(&mut self) {
504 if self.can_pop() {
505 self.stack.pop();
506 }
507 }
508
509 pub fn depth(&self) -> usize {
511 self.stack.len()
512 }
513
514 pub fn can_pop(&self) -> bool {
516 self.stack.len() > 1
517 }
518
519 pub fn contains(&self, name: &str) -> bool {
521 self.stack.iter().any(|screen| screen == name)
522 }
523
524 pub fn reset(&mut self) {
526 self.stack.truncate(1);
527 }
528
529 pub fn remove_inactive(&mut self, name: &str) -> bool {
534 if self.contains(name) {
535 return false;
536 }
537 self.focus_state.remove(name).is_some()
538 }
539
540 pub fn retain_inactive(&mut self, mut keep: impl FnMut(&str) -> bool) -> usize {
545 let before = self.focus_state.len();
546 let stack = &self.stack;
547 self.focus_state
548 .retain(|name, _| stack.iter().any(|screen| screen == name) || keep(name));
549 before - self.focus_state.len()
550 }
551
552 pub fn focus_state_count(&self) -> usize {
556 self.focus_state.len()
557 }
558
559 pub(crate) fn apply_nav(&mut self, nav: ScreenNav) {
562 match nav {
563 ScreenNav::Push(name) => self.push(name),
564 ScreenNav::Pop => self.pop(),
565 ScreenNav::Reset => self.reset(),
566 }
567 }
568
569 pub(crate) fn save_focus(&mut self, name: &str, focus_index: usize, focus_count: usize) {
570 self.focus_state
571 .insert(name.to_string(), (focus_index, focus_count));
572 }
573
574 pub(crate) fn restore_focus(&self, name: &str) -> (usize, usize) {
575 self.focus_state.get(name).copied().unwrap_or((0, 0))
576 }
577}
578
579#[derive(Debug, Clone)]
583pub(crate) enum ScreenNav {
584 Push(String),
586 Pop,
588 Reset,
590}
591
592#[derive(Debug, Clone)]
611pub struct ModeState {
612 modes: std::collections::HashMap<String, ScreenState>,
613 active: String,
614}
615
616impl ModeState {
617 pub fn new(mode: impl Into<String>, screen: impl Into<String>) -> Self {
619 let mode = mode.into();
620 let mut modes = std::collections::HashMap::new();
621 modes.insert(mode.clone(), ScreenState::new(screen));
622 Self {
623 modes,
624 active: mode,
625 }
626 }
627
628 pub fn add_mode(&mut self, mode: impl Into<String>, screen: impl Into<String>) {
630 let mode = mode.into();
631 self.modes
632 .entry(mode)
633 .or_insert_with(|| ScreenState::new(screen));
634 }
635
636 pub fn switch_mode(&mut self, mode: impl Into<String>) {
641 let mode = mode.into();
642 assert!(self.modes.contains_key(&mode), "mode '{mode}' not found");
643 self.active = mode;
644 }
645
646 pub fn try_switch_mode(&mut self, mode: impl Into<String>) -> bool {
654 let mode = mode.into();
655 if !self.modes.contains_key(&mode) {
656 return false;
657 }
658 self.active = mode;
659 true
660 }
661
662 pub fn active_mode(&self) -> &str {
664 &self.active
665 }
666
667 pub fn screens(&self) -> &ScreenState {
669 self.modes
670 .get(&self.active)
671 .expect("active mode must exist")
672 }
673
674 pub fn screens_mut(&mut self) -> &mut ScreenState {
676 self.modes
677 .get_mut(&self.active)
678 .expect("active mode must exist")
679 }
680
681 pub fn contains_mode(&self, mode: &str) -> bool {
683 self.modes.contains_key(mode)
684 }
685
686 pub fn mode_count(&self) -> usize {
690 self.modes.len()
691 }
692
693 pub fn remove_mode(&mut self, mode: &str) -> bool {
697 if self.active == mode {
698 return false;
699 }
700 self.modes.remove(mode).is_some()
701 }
702
703 pub fn retain_modes(&mut self, mut keep: impl FnMut(&str) -> bool) -> usize {
707 let before = self.modes.len();
708 let active = self.active.as_str();
709 self.modes
710 .retain(|mode, _| mode.as_str() == active || keep(mode.as_str()));
711 before - self.modes.len()
712 }
713}
714
715#[cfg(test)]
716mod mode_state_tests {
717 use super::ModeState;
718
719 #[test]
720 fn try_switch_mode_returns_false_for_unknown_mode() {
721 let mut modes = ModeState::new("app", "home");
722 modes.add_mode("settings", "general");
723 assert!(modes.try_switch_mode("settings"));
724 assert_eq!(modes.active_mode(), "settings");
725 assert!(!modes.try_switch_mode("nonexistent"));
726 assert_eq!(modes.active_mode(), "settings");
728 }
729
730 #[test]
731 fn remove_mode_preserves_active_mode() {
732 let mut modes = ModeState::new("app", "home");
733 modes.add_mode("settings", "general");
734 modes.add_mode("admin", "dashboard");
735
736 assert_eq!(modes.mode_count(), 3);
737 assert!(!modes.remove_mode("app"));
738 assert!(modes.remove_mode("admin"));
739 assert!(!modes.contains_mode("admin"));
740 assert_eq!(modes.mode_count(), 2);
741 }
742
743 #[test]
744 fn retain_modes_keeps_active_mode() {
745 let mut modes = ModeState::new("app", "home");
746 modes.add_mode("settings", "general");
747 modes.add_mode("admin", "dashboard");
748
749 let removed = modes.retain_modes(|mode| mode == "admin");
750 assert_eq!(removed, 1);
751 assert!(modes.contains_mode("app"));
752 assert!(modes.contains_mode("admin"));
753 assert!(!modes.contains_mode("settings"));
754 }
755}
756
757#[cfg(test)]
758mod streaming_version_tests {
759 use super::{StreamingMarkdownState, StreamingTextState};
761
762 #[test]
763 fn text_version_starts_at_zero_and_bumps_on_mutation() {
764 let mut s = StreamingTextState::new();
765 assert_eq!(s.version(), 0, "fresh state has version 0");
766 s.push("a");
767 assert_eq!(s.version(), 1);
768 s.push("b");
769 assert_eq!(s.version(), 2);
770 s.start();
771 assert_eq!(s.version(), 3, "start() is a mutation");
772 s.clear();
773 assert_eq!(s.version(), 4, "clear() is a mutation");
774 }
775
776 #[test]
777 fn text_finish_does_not_bump_version() {
778 let mut s = StreamingTextState::new();
779 s.push("x");
780 let v = s.version();
781 s.finish();
782 assert_eq!(s.version(), v, "finish() only toggles the streaming flag");
783 }
784
785 #[test]
786 fn markdown_version_bumps_on_mutation() {
787 let mut s = StreamingMarkdownState::new();
788 assert_eq!(s.version(), 0);
789 s.push("# h");
790 assert_eq!(s.version(), 1);
791 s.start();
792 assert_eq!(s.version(), 2);
793 s.clear();
794 assert_eq!(s.version(), 3);
795 let v = s.version();
796 s.finish();
797 assert_eq!(s.version(), v, "finish() does not bump");
798 }
799}
800
801#[non_exhaustive]
803#[derive(Debug, Clone, Copy, PartialEq, Eq)]
804pub enum ApprovalAction {
805 Pending,
807 Approved,
809 Rejected,
811}
812
813#[derive(Debug, Clone)]
819pub struct ToolApprovalState {
820 pub tool_name: String,
822 pub description: String,
824 pub action: ApprovalAction,
826}
827
828impl ToolApprovalState {
829 pub fn new(tool_name: impl Into<String>, description: impl Into<String>) -> Self {
831 Self {
832 tool_name: tool_name.into(),
833 description: description.into(),
834 action: ApprovalAction::Pending,
835 }
836 }
837
838 pub fn reset(&mut self) {
840 self.action = ApprovalAction::Pending;
841 }
842}
843
844#[derive(Debug, Clone)]
846pub struct ContextItem {
847 pub label: String,
849 pub tokens: usize,
851}
852
853impl ContextItem {
854 pub fn new(label: impl Into<String>, tokens: usize) -> Self {
856 Self {
857 label: label.into(),
858 tokens,
859 }
860 }
861}