slt/widgets/commanding.rs
1/// Default tick budget (~1s at 60Hz) after which a partially-typed chord
2/// is abandoned. Matches the tick clock used by notifications/animation.
3///
4/// Override per call site with
5/// [`Context::key_chord_timeout`](crate::Context::key_chord_timeout).
6pub const DEFAULT_CHORD_TIMEOUT_TICKS: u64 = 60;
7
8/// Cross-frame partial-sequence buffer for
9/// [`Context::key_chord`](crate::Context::key_chord).
10///
11/// Persisted in `FrameState` across frames (same out/in policy as
12/// `keyed_states`). Holds at most one in-flight chord prefix; a mismatching
13/// key or a timeout clears it. You never construct this directly — SLT owns a
14/// single instance per [`Context`](crate::Context) and threads it through the
15/// frame loop for you.
16///
17/// # Example
18///
19/// ```no_run
20/// slt::run(|ui: &mut slt::Context| {
21/// // The buffer is managed internally; just call `key_chord`.
22/// if ui.key_chord("gg") {
23/// // jump to top
24/// }
25/// });
26/// ```
27#[derive(Debug, Default, Clone)]
28pub struct ChordState {
29 /// Characters accumulated so far toward some registered chord.
30 pub(crate) pending: String,
31 /// Tick of the most recent accepted key; used for timeout expiry.
32 pub(crate) last_tick: u64,
33}
34
35/// State for a command palette overlay.
36///
37/// Renders as a modal with a search input and filtered command list.
38#[derive(Debug, Clone)]
39pub struct CommandPaletteState {
40 /// Available commands.
41 pub commands: Vec<PaletteCommand>,
42 /// Current search query.
43 pub input: String,
44 /// Cursor index within `input`.
45 pub cursor: usize,
46 /// Whether the palette modal is open.
47 pub open: bool,
48 /// The last selected command index, set when the user confirms a selection.
49 /// Check this after `response.changed` is true.
50 pub last_selected: Option<usize>,
51 selected: usize,
52 /// Cached filtered indices for the last `input` value. Avoids running
53 /// `fuzzy_score` twice per frame (clamp + render).
54 filter_cache: Option<(String, Vec<usize>)>,
55}
56
57impl CommandPaletteState {
58 /// Create command palette state from a command list.
59 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 /// Toggle open/closed state and reset input when opening.
72 pub fn toggle(&mut self) {
73 self.open = !self.open;
74 if self.open {
75 self.input.clear();
76 self.cursor = 0;
77 self.selected = 0;
78 self.filter_cache = None;
79 }
80 }
81
82 pub(crate) fn fuzzy_score(pattern: &str, text: &str) -> Option<i32> {
83 let pattern = pattern.trim();
84 if pattern.is_empty() {
85 return Some(0);
86 }
87
88 let text_chars: Vec<char> = text.chars().collect();
89 let mut score = 0;
90 let mut search_start = 0usize;
91 let mut prev_match: Option<usize> = None;
92
93 for p in pattern.chars() {
94 let mut found = None;
95 for (idx, ch) in text_chars.iter().enumerate().skip(search_start) {
96 if ch.eq_ignore_ascii_case(&p) {
97 found = Some(idx);
98 break;
99 }
100 }
101
102 let idx = found?;
103 if prev_match.is_some_and(|prev| idx == prev + 1) {
104 score += 3;
105 } else {
106 score += 1;
107 }
108
109 if idx == 0 {
110 score += 2;
111 } else {
112 let prev = text_chars[idx - 1];
113 let curr = text_chars[idx];
114 if matches!(prev, ' ' | '_' | '-') || prev.is_uppercase() || curr.is_uppercase() {
115 score += 2;
116 }
117 }
118
119 prev_match = Some(idx);
120 search_start = idx + 1;
121 }
122
123 Some(score)
124 }
125
126 /// Cached variant of [`Self::filtered_indices`].
127 ///
128 /// Reuses the previous result when `self.input` has not changed since the
129 /// last call. `command_palette()` invokes this twice per frame (before key
130 /// handling, to clamp the selection index, and again for render); on idle
131 /// frames the second call is served from cache instead of re-running
132 /// `fuzzy_score` over the full command list.
133 pub(crate) fn filtered_indices_cached(&mut self) -> &[usize] {
134 let needs_recompute = match &self.filter_cache {
135 Some((cached_input, _)) => *cached_input != self.input,
136 None => true,
137 };
138 if needs_recompute {
139 let indices = self.filtered_indices();
140 self.filter_cache = Some((self.input.clone(), indices));
141 }
142 &self
143 .filter_cache
144 .as_ref()
145 .expect("filter_cache populated above")
146 .1
147 }
148
149 pub(crate) fn filtered_indices(&self) -> Vec<usize> {
150 let query = self.input.trim();
151 if query.is_empty() {
152 return (0..self.commands.len()).collect();
153 }
154
155 let mut scored: Vec<(usize, i32)> = self
156 .commands
157 .iter()
158 .enumerate()
159 .filter_map(|(i, cmd)| {
160 let mut haystack =
161 String::with_capacity(cmd.label.len() + cmd.description.len() + 1);
162 haystack.push_str(&cmd.label);
163 haystack.push(' ');
164 haystack.push_str(&cmd.description);
165 Self::fuzzy_score(query, &haystack).map(|score| (i, score))
166 })
167 .collect();
168
169 if scored.is_empty() {
170 let tokens: Vec<String> = query.split_whitespace().map(|t| t.to_lowercase()).collect();
171 return self
172 .commands
173 .iter()
174 .enumerate()
175 .filter(|(_, cmd)| {
176 let label = cmd.label.to_lowercase();
177 let desc = cmd.description.to_lowercase();
178 tokens.iter().all(|token| {
179 label.contains(token.as_str()) || desc.contains(token.as_str())
180 })
181 })
182 .map(|(i, _)| i)
183 .collect();
184 }
185
186 scored.sort_by(|a, b| b.1.cmp(&a.1).then_with(|| a.0.cmp(&b.0)));
187 scored.into_iter().map(|(idx, _)| idx).collect()
188 }
189
190 pub(crate) fn selected(&self) -> usize {
191 self.selected
192 }
193
194 pub(crate) fn set_selected(&mut self, s: usize) {
195 self.selected = s;
196 }
197}
198
199/// State for a streaming text display.
200///
201/// Accumulates text chunks as they arrive from an LLM stream.
202/// Pass to [`Context::streaming_text`](crate::Context::streaming_text) each frame.
203#[derive(Debug, Clone)]
204pub struct StreamingTextState {
205 /// The accumulated text content.
206 pub content: String,
207 /// Whether the stream is still receiving data.
208 pub streaming: bool,
209 /// Cursor blink state (for the typing indicator).
210 pub(crate) cursor_visible: bool,
211 pub(crate) cursor_tick: u64,
212 /// Monotonic content version, bumped on every content mutation
213 /// (`push` / `start` / `clear`). See [`StreamingTextState::version`].
214 pub(crate) version: u64,
215}
216
217impl StreamingTextState {
218 /// Create a new empty streaming text state.
219 pub fn new() -> Self {
220 Self {
221 content: String::new(),
222 streaming: false,
223 cursor_visible: true,
224 cursor_tick: 0,
225 version: 0,
226 }
227 }
228
229 /// Append a chunk of text (e.g., from an LLM stream delta).
230 pub fn push(&mut self, chunk: &str) {
231 self.content.push_str(chunk);
232 self.version = self.version.wrapping_add(1);
233 }
234
235 /// Mark the stream as complete (hides the typing cursor).
236 pub fn finish(&mut self) {
237 self.streaming = false;
238 }
239
240 /// Start a new streaming session, clearing previous content.
241 pub fn start(&mut self) {
242 self.content.clear();
243 self.streaming = true;
244 self.cursor_visible = true;
245 self.cursor_tick = 0;
246 self.version = self.version.wrapping_add(1);
247 }
248
249 /// Clear all content and reset state.
250 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.version = self.version.wrapping_add(1);
256 }
257
258 /// Monotonic version counter, bumped on every content mutation
259 /// (`push` / `start` / `clear`).
260 ///
261 /// The stream itself changes every token, so this value is **not** a
262 /// useful cache key for the streaming region. Its purpose is the
263 /// inverse: it lets you detect when the stream *did* change so you can
264 /// decide whether the *surrounding static chrome* is stable. Combine a
265 /// hash of your non-streaming inputs into a key for
266 /// [`ContainerBuilder::cached`](crate::ContainerBuilder::cached) and wrap
267 /// the chrome — not the stream — in it.
268 ///
269 /// Since 0.21.0.
270 ///
271 /// # Example
272 /// ```no_run
273 /// # slt::run(|ui: &mut slt::Context| {
274 /// let mut stream = slt::StreamingTextState::new();
275 /// stream.push("hello");
276 /// assert_eq!(stream.version(), 1);
277 /// stream.push(" world");
278 /// assert_eq!(stream.version(), 2);
279 /// # });
280 /// ```
281 pub fn version(&self) -> u64 {
282 self.version
283 }
284}
285
286impl Default for StreamingTextState {
287 fn default() -> Self {
288 Self::new()
289 }
290}
291
292/// State for a streaming markdown display.
293///
294/// Accumulates markdown chunks as they arrive from an LLM stream.
295/// Pass to [`Context::streaming_markdown`](crate::Context::streaming_markdown) each frame.
296#[derive(Debug, Clone)]
297pub struct StreamingMarkdownState {
298 /// The accumulated markdown content.
299 pub content: String,
300 /// Whether the stream is still receiving data.
301 pub streaming: bool,
302 /// Cursor blink state (for the typing indicator).
303 pub cursor_visible: bool,
304 /// Cursor animation tick counter.
305 pub cursor_tick: u64,
306 /// Whether the parser is currently inside a fenced code block.
307 pub in_code_block: bool,
308 /// Language label of the active fenced code block.
309 pub code_block_lang: String,
310 /// Monotonic content version, bumped on every content mutation
311 /// (`push` / `start` / `clear`). See [`StreamingMarkdownState::version`].
312 pub(crate) version: u64,
313}
314
315impl StreamingMarkdownState {
316 /// Create a new empty streaming markdown state.
317 pub fn new() -> Self {
318 Self {
319 content: String::new(),
320 streaming: false,
321 cursor_visible: true,
322 cursor_tick: 0,
323 in_code_block: false,
324 code_block_lang: String::new(),
325 version: 0,
326 }
327 }
328
329 /// Append a markdown chunk (e.g., from an LLM stream delta).
330 pub fn push(&mut self, chunk: &str) {
331 self.content.push_str(chunk);
332 self.version = self.version.wrapping_add(1);
333 }
334
335 /// Start a new streaming session, clearing previous content.
336 pub fn start(&mut self) {
337 self.content.clear();
338 self.streaming = true;
339 self.cursor_visible = true;
340 self.cursor_tick = 0;
341 self.in_code_block = false;
342 self.code_block_lang.clear();
343 self.version = self.version.wrapping_add(1);
344 }
345
346 /// Mark the stream as complete (hides the typing cursor).
347 pub fn finish(&mut self) {
348 self.streaming = false;
349 }
350
351 /// Clear all content and reset state.
352 pub fn clear(&mut self) {
353 self.content.clear();
354 self.streaming = false;
355 self.cursor_visible = true;
356 self.cursor_tick = 0;
357 self.in_code_block = false;
358 self.code_block_lang.clear();
359 self.version = self.version.wrapping_add(1);
360 }
361
362 /// Monotonic version counter, bumped on every content mutation
363 /// (`push` / `start` / `clear`).
364 ///
365 /// As with [`StreamingTextState::version`], use this to detect stream
366 /// deltas and key the *surrounding static chrome* into
367 /// [`ContainerBuilder::cached`](crate::ContainerBuilder::cached) — not to
368 /// cache the stream region itself.
369 ///
370 /// Since 0.21.0.
371 ///
372 /// # Example
373 /// ```no_run
374 /// # slt::run(|ui: &mut slt::Context| {
375 /// let mut md = slt::StreamingMarkdownState::new();
376 /// md.push("# Title");
377 /// assert_eq!(md.version(), 1);
378 /// # });
379 /// ```
380 pub fn version(&self) -> u64 {
381 self.version
382 }
383}
384
385impl Default for StreamingMarkdownState {
386 fn default() -> Self {
387 Self::new()
388 }
389}
390
391/// Navigation stack state for multi-screen apps.
392///
393/// Tracks screen names in a push/pop stack while preserving the root screen.
394/// Each screen gets isolated focus and hook state when used with
395/// [`crate::Context::screen`].
396///
397/// # Example
398///
399/// ```no_run
400/// let mut screens = slt::ScreenState::new("main");
401///
402/// slt::run(|ui| {
403/// let current = screens.current().to_string();
404/// if current == "main" {
405/// if ui.button("Settings").clicked { screens.push("settings"); }
406/// }
407/// if current == "settings" {
408/// if ui.button("Back").clicked { screens.pop(); }
409/// }
410/// });
411/// ```
412#[derive(Debug, Clone)]
413pub struct ScreenState {
414 stack: Vec<String>,
415 focus_state: std::collections::HashMap<String, (usize, usize)>,
416}
417
418impl ScreenState {
419 /// Create a screen stack with an initial root screen.
420 pub fn new(initial: impl Into<String>) -> Self {
421 Self {
422 stack: vec![initial.into()],
423 focus_state: std::collections::HashMap::new(),
424 }
425 }
426
427 /// Return the current screen name (top of the stack).
428 pub fn current(&self) -> &str {
429 self.stack
430 .last()
431 .expect("ScreenState always contains at least one screen")
432 .as_str()
433 }
434
435 /// Push a new screen onto the stack.
436 pub fn push(&mut self, name: impl Into<String>) {
437 self.stack.push(name.into());
438 }
439
440 /// Pop the current screen, preserving the root screen.
441 pub fn pop(&mut self) {
442 if self.can_pop() {
443 self.stack.pop();
444 }
445 }
446
447 /// Return current stack depth.
448 pub fn depth(&self) -> usize {
449 self.stack.len()
450 }
451
452 /// Return `true` if popping is allowed.
453 pub fn can_pop(&self) -> bool {
454 self.stack.len() > 1
455 }
456
457 /// Return `true` if `name` is currently present in the navigation stack.
458 pub fn contains(&self, name: &str) -> bool {
459 self.stack.iter().any(|screen| screen == name)
460 }
461
462 /// Reset to only the root screen.
463 pub fn reset(&mut self) {
464 self.stack.truncate(1);
465 }
466
467 /// Remove retained focus state for a screen that is no longer on the stack.
468 ///
469 /// Returns `false` when the screen is still active or stacked, because
470 /// dropping focus for a live screen would make back navigation jumpy.
471 pub fn remove_inactive(&mut self, name: &str) -> bool {
472 if self.contains(name) {
473 return false;
474 }
475 self.focus_state.remove(name).is_some()
476 }
477
478 /// Retain inactive focus-state entries accepted by `keep`.
479 ///
480 /// Screens still present in the stack are always kept. Returns the number
481 /// of focus-state entries removed.
482 pub fn retain_inactive(&mut self, mut keep: impl FnMut(&str) -> bool) -> usize {
483 let before = self.focus_state.len();
484 let stack = &self.stack;
485 self.focus_state
486 .retain(|name, _| stack.iter().any(|screen| screen == name) || keep(name));
487 before - self.focus_state.len()
488 }
489
490 /// Number of retained per-screen focus entries.
491 ///
492 /// Diagnostic helper for apps with runtime-generated screen names.
493 pub fn focus_state_count(&self) -> usize {
494 self.focus_state.len()
495 }
496
497 /// Apply a deferred navigation request recorded inside a
498 /// [`crate::Context::screen`] closure (issue #279).
499 pub(crate) fn apply_nav(&mut self, nav: ScreenNav) {
500 match nav {
501 ScreenNav::Push(name) => self.push(name),
502 ScreenNav::Pop => self.pop(),
503 ScreenNav::Reset => self.reset(),
504 }
505 }
506
507 pub(crate) fn save_focus(&mut self, name: &str, focus_index: usize, focus_count: usize) {
508 self.focus_state
509 .insert(name.to_string(), (focus_index, focus_count));
510 }
511
512 pub(crate) fn restore_focus(&self, name: &str) -> (usize, usize) {
513 self.focus_state.get(name).copied().unwrap_or((0, 0))
514 }
515}
516
517/// A deferred screen-navigation request recorded inside a
518/// [`crate::Context::screen`] closure and applied to the active
519/// [`ScreenState`] after the closure returns (issue #279).
520#[derive(Debug, Clone)]
521pub(crate) enum ScreenNav {
522 /// Push a new screen onto the stack.
523 Push(String),
524 /// Pop the current screen, preserving the root.
525 Pop,
526 /// Reset to only the root screen.
527 Reset,
528}
529
530/// Named mode system with independent screen stacks.
531///
532/// Each mode contains its own [`ScreenState`]. Switching modes preserves
533/// the previous mode's screen stack, focus, and hook state.
534///
535/// # Example
536///
537/// ```no_run
538/// let mut modes = slt::ModeState::new("app", "home");
539/// modes.add_mode("settings", "general");
540///
541/// slt::run(|ui| {
542/// if ui.key('1') { modes.switch_mode("app"); }
543/// if ui.key('2') { modes.switch_mode("settings"); }
544/// let mode = modes.active_mode().to_string();
545/// ui.text(format!("Mode: {}", mode));
546/// });
547/// ```
548#[derive(Debug, Clone)]
549pub struct ModeState {
550 modes: std::collections::HashMap<String, ScreenState>,
551 active: String,
552}
553
554impl ModeState {
555 /// Create a mode system with an initial mode and screen.
556 pub fn new(mode: impl Into<String>, screen: impl Into<String>) -> Self {
557 let mode = mode.into();
558 let mut modes = std::collections::HashMap::new();
559 modes.insert(mode.clone(), ScreenState::new(screen));
560 Self {
561 modes,
562 active: mode,
563 }
564 }
565
566 /// Add a new mode with an initial screen.
567 pub fn add_mode(&mut self, mode: impl Into<String>, screen: impl Into<String>) {
568 let mode = mode.into();
569 self.modes
570 .entry(mode)
571 .or_insert_with(|| ScreenState::new(screen));
572 }
573
574 /// Switch to a different mode. The mode must have been added with [`Self::add_mode`].
575 ///
576 /// Panics if the mode does not exist. For a non-panicking variant that
577 /// reports success, use [`Self::try_switch_mode`].
578 pub fn switch_mode(&mut self, mode: impl Into<String>) {
579 let mode = mode.into();
580 assert!(self.modes.contains_key(&mode), "mode '{mode}' not found");
581 self.active = mode;
582 }
583
584 /// Switch modes, returning `true` when the mode exists and the switch
585 /// happened, or `false` when the mode has not been registered via
586 /// [`Self::add_mode`].
587 ///
588 /// Prefer this over [`Self::switch_mode`] when the mode name comes from
589 /// user input, key bindings, or anywhere the value could be unexpected
590 /// at runtime — an unknown mode should not crash the host application.
591 pub fn try_switch_mode(&mut self, mode: impl Into<String>) -> bool {
592 let mode = mode.into();
593 if !self.modes.contains_key(&mode) {
594 return false;
595 }
596 self.active = mode;
597 true
598 }
599
600 /// Return the active mode name.
601 pub fn active_mode(&self) -> &str {
602 &self.active
603 }
604
605 /// Get a reference to the active mode's screen state.
606 pub fn screens(&self) -> &ScreenState {
607 self.modes
608 .get(&self.active)
609 .expect("active mode must exist")
610 }
611
612 /// Get a mutable reference to the active mode's screen state.
613 pub fn screens_mut(&mut self) -> &mut ScreenState {
614 self.modes
615 .get_mut(&self.active)
616 .expect("active mode must exist")
617 }
618
619 /// Return `true` when a mode has been registered.
620 pub fn contains_mode(&self, mode: &str) -> bool {
621 self.modes.contains_key(mode)
622 }
623
624 /// Number of registered modes.
625 ///
626 /// Diagnostic helper for dynamic mode sets.
627 pub fn mode_count(&self) -> usize {
628 self.modes.len()
629 }
630
631 /// Remove an inactive mode and its retained screen state.
632 ///
633 /// The active mode is preserved and returns `false`.
634 pub fn remove_mode(&mut self, mode: &str) -> bool {
635 if self.active == mode {
636 return false;
637 }
638 self.modes.remove(mode).is_some()
639 }
640
641 /// Retain inactive modes accepted by `keep`.
642 ///
643 /// The active mode is always retained. Returns the number of modes removed.
644 pub fn retain_modes(&mut self, mut keep: impl FnMut(&str) -> bool) -> usize {
645 let before = self.modes.len();
646 let active = self.active.as_str();
647 self.modes
648 .retain(|mode, _| mode.as_str() == active || keep(mode.as_str()));
649 before - self.modes.len()
650 }
651}
652
653#[cfg(test)]
654mod mode_state_tests {
655 use super::ModeState;
656
657 #[test]
658 fn try_switch_mode_returns_false_for_unknown_mode() {
659 let mut modes = ModeState::new("app", "home");
660 modes.add_mode("settings", "general");
661 assert!(modes.try_switch_mode("settings"));
662 assert_eq!(modes.active_mode(), "settings");
663 assert!(!modes.try_switch_mode("nonexistent"));
664 // Active mode must not change when the switch is rejected.
665 assert_eq!(modes.active_mode(), "settings");
666 }
667
668 #[test]
669 fn remove_mode_preserves_active_mode() {
670 let mut modes = ModeState::new("app", "home");
671 modes.add_mode("settings", "general");
672 modes.add_mode("admin", "dashboard");
673
674 assert_eq!(modes.mode_count(), 3);
675 assert!(!modes.remove_mode("app"));
676 assert!(modes.remove_mode("admin"));
677 assert!(!modes.contains_mode("admin"));
678 assert_eq!(modes.mode_count(), 2);
679 }
680
681 #[test]
682 fn retain_modes_keeps_active_mode() {
683 let mut modes = ModeState::new("app", "home");
684 modes.add_mode("settings", "general");
685 modes.add_mode("admin", "dashboard");
686
687 let removed = modes.retain_modes(|mode| mode == "admin");
688 assert_eq!(removed, 1);
689 assert!(modes.contains_mode("app"));
690 assert!(modes.contains_mode("admin"));
691 assert!(!modes.contains_mode("settings"));
692 }
693}
694
695#[cfg(test)]
696mod streaming_version_tests {
697 //! Issue #273 — the monotonic `version()` counter on streaming states.
698 use super::{StreamingMarkdownState, StreamingTextState};
699
700 #[test]
701 fn text_version_starts_at_zero_and_bumps_on_mutation() {
702 let mut s = StreamingTextState::new();
703 assert_eq!(s.version(), 0, "fresh state has version 0");
704 s.push("a");
705 assert_eq!(s.version(), 1);
706 s.push("b");
707 assert_eq!(s.version(), 2);
708 s.start();
709 assert_eq!(s.version(), 3, "start() is a mutation");
710 s.clear();
711 assert_eq!(s.version(), 4, "clear() is a mutation");
712 }
713
714 #[test]
715 fn text_finish_does_not_bump_version() {
716 let mut s = StreamingTextState::new();
717 s.push("x");
718 let v = s.version();
719 s.finish();
720 assert_eq!(s.version(), v, "finish() only toggles the streaming flag");
721 }
722
723 #[test]
724 fn markdown_version_bumps_on_mutation() {
725 let mut s = StreamingMarkdownState::new();
726 assert_eq!(s.version(), 0);
727 s.push("# h");
728 assert_eq!(s.version(), 1);
729 s.start();
730 assert_eq!(s.version(), 2);
731 s.clear();
732 assert_eq!(s.version(), 3);
733 let v = s.version();
734 s.finish();
735 assert_eq!(s.version(), v, "finish() does not bump");
736 }
737}
738
739/// Approval state for a tool call.
740#[non_exhaustive]
741#[derive(Debug, Clone, Copy, PartialEq, Eq)]
742pub enum ApprovalAction {
743 /// No action taken yet.
744 Pending,
745 /// User approved the tool call.
746 Approved,
747 /// User rejected the tool call.
748 Rejected,
749}
750
751/// State for a tool approval widget.
752///
753/// Displays a tool call with approve/reject buttons for human-in-the-loop
754/// AI workflows. Pass to [`Context::tool_approval`](crate::Context::tool_approval)
755/// each frame.
756#[derive(Debug, Clone)]
757pub struct ToolApprovalState {
758 /// The name of the tool being invoked.
759 pub tool_name: String,
760 /// A human-readable description of what the tool will do.
761 pub description: String,
762 /// The current approval status.
763 pub action: ApprovalAction,
764}
765
766impl ToolApprovalState {
767 /// Create a new tool approval prompt.
768 pub fn new(tool_name: impl Into<String>, description: impl Into<String>) -> Self {
769 Self {
770 tool_name: tool_name.into(),
771 description: description.into(),
772 action: ApprovalAction::Pending,
773 }
774 }
775
776 /// Reset to pending state.
777 pub fn reset(&mut self) {
778 self.action = ApprovalAction::Pending;
779 }
780}
781
782/// Item in a context bar showing active context sources.
783#[derive(Debug, Clone)]
784pub struct ContextItem {
785 /// Display label for this context source.
786 pub label: String,
787 /// Token count or size indicator.
788 pub tokens: usize,
789}
790
791impl ContextItem {
792 /// Create a new context item with a label and token count.
793 pub fn new(label: impl Into<String>, tokens: usize) -> Self {
794 Self {
795 label: label.into(),
796 tokens,
797 }
798 }
799}