aboutsummaryrefslogtreecommitdiffstats
path: root/crates/atuin-ai/src/tui/spinner.rs
diff options
context:
space:
mode:
authorMichelle Tilley <michelle@michelletilley.net>2026-02-24 11:48:20 -0800
committerGitHub <noreply@github.com>2026-02-24 11:48:20 -0800
commit6ea760bb6b36da241961e8ecd60cb2c5e15c0a78 (patch)
tree18ebbb710cea24e30bc69b5d6bc807518a950746 /crates/atuin-ai/src/tui/spinner.rs
parentfix: forward $PATH to tmux popup in zsh (#3198) (diff)
downloadatuin-6ea760bb6b36da241961e8ecd60cb2c5e15c0a78.zip
feat: Generate commands or ask questions with `atuin ai` (#3199)
This PR refines the system created in #3178 to be suitable for a v1 release. --- ## Overview `atuin-ai` is a separate binary that allows for generating commands and asking questions from the command line. It is fully opt-in. ## Usage `atuin ai init` will output bindings for your shell. Currently, bash, zsh, and fish are supported. ```bash eval "$(atuin ai init)" ``` Once the hooks are installed, just press `?` on an empty prompt line to call up the TUI. `atuin ai` requires an account on [Atuin Hub](https://hub.atuin.sh/); you will be prompted to log in on first use. ## Features ### Command generation Prompt the LLM to create a command, and get one back, no fuss. Press `enter` to run, or `tab` to insert. ``` ┌Ask questions or generate a command:──────────────────────────┐ │ │ │ > Get a list of running docker containers │ │ │ ├──────────────────────────────────────────────────────────────┤ │ │ │ $ docker ps │ │ │ └────[Enter]: Run [Tab]: Insert [f]: Follow-up [Esc]: Cancel┘ ``` ### Follow-up You can follow-up with `f` to specify a refinement prompt to update the command that will be inserted. ``` ┌Ask questions or generate a command:──────────────────────────┐ │ │ │ > Get a list of running docker containers │ │ │ ├──────────────────────────────────────────────────────────────┤ │ │ │ $ docker ps │ │ │ ├──────────────────────────────────────────────────────────────┤ │ │ │ > Actually I want to get all docker containers │ │ │ ├──────────────────────────────────────────────────────────────┤ │ │ │ $ docker ps -a │ │ │ └────[Enter]: Run [Tab]: Insert [f]: Follow-up [Esc]: Cancel┘ ``` You can also follow-up with questions to get responses in natural language. ``` ┌Ask questions or generate a command:──────────────────────────┐ │ │ │ > Get a list of running docker containers │ │ │ ├──────────────────────────────────────────────────────────────┤ │ │ │ $ docker ps │ │ │ ├──────────────────────────────────────────────────────────────┤ │ │ │ > Actually I want to get all docker containers │ │ │ ├──────────────────────────────────────────────────────────────┤ │ │ │ $ docker ps -a │ │ │ ├──────────────────────────────────────────────────────────────┤ │ │ │ > What other useful flags to `docker ps` should I know? │ │ │ ├──────────────────────────────────────────────────────────────┤ │ │ │ Here are some handy `docker ps` flags: │ │ │ │ - `-q` — Only show container IDs (great for piping to │ │ other commands) │ │ - `-s` — Show container sizes │ │ - `-n 5` — Show the last 5 created containers │ │ - `-l` — Show only the latest created container │ │ - `--no-trunc` — Don't truncate output (shows full IDs and │ │ commands) │ │ - `-f` or `--filter` — Filter by condition, e.g.: │ │ - `-f status=exited` — only exited containers │ │ - `-f name=myapp` — filter by name │ │ - `-f ancestor=nginx` — filter by image │ │ - `--format` — Custom output using Go templates, e.g.: │ │ `--format "table {{.Names}}\t{{.Status}}\t{{.Ports}}"` │ │ │ │ A common combo is `docker ps -aq` to get all container │ │ IDs, useful for bulk operations like `docker rm $(docker │ │ ps -aq)`. │ │ │ └────[Enter]: Run [Tab]: Insert [f]: Follow-up [Esc]: Cancel┘ ``` You can use `enter` or `tab` at any time to run or insert the last suggested command, even if it was suggested in a previous turn. ### Conversational and search usage If you prompt the LLM with a question that doesn't imply you want to generate a command, it can respond in natural language, and use web search if necessary to fetch the data it needs. ``` ┌Ask questions or generate a command:──────────────────────────┐ │ │ │ > What is the latest version of atuin? │ │ │ ├──────────────────────────────────────────────────────────────┤ │ │ │ ✓ Used 2 tools │ │ │ │ The latest version of Atuin is **v18.12.0**, available on │ │ the [GitHub releases │ │ page](https://github.com/atuinsh/atuin/releases). │ │ │ └─────────────────────────────────[f]: Follow-up [Esc]: Cancel┘ ``` ### Dangerous or low-confidence command detection The LLM scores its confidence in the command, as well as how dangerous the command is. This information is shown if a threshold is exceeded, and requires an extra confirmation step before running automatically with `enter`. The Atuin Hub server also monitors suggested commands for dangerous patterns the LLM didn't catch, and appends its own assessment at the end of the LLM's own assessment. ``` ┌Ask questions or generate a command:──────────────────────────┐ │ │ │ > Delete all files from $HOME │ │ │ ├──────────────────────────────────────────────────────────────┤ │ │ │ $ rm -rf $HOME/* │ │ │ │ ! ⚠️ This will PERMANENTLY delete ALL files and directories │ │ in your home directory, including documents, downloads, │ │ configurations, SSH keys, and everything else. This is │ │ irreversible and will likely break your system. Also note │ │ this won't delete hidden (dot) files — if you want those │ │ too, that's even more destructive.; [Server] Recursive │ │ delete of critical directory │ │ │ └────[Enter]: Run [Tab]: Insert [f]: Follow-up [Esc]: Cancel┘ ``` --------- Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Diffstat (limited to 'crates/atuin-ai/src/tui/spinner.rs')
-rw-r--r--crates/atuin-ai/src/tui/spinner.rs99
1 files changed, 99 insertions, 0 deletions
diff --git a/crates/atuin-ai/src/tui/spinner.rs b/crates/atuin-ai/src/tui/spinner.rs
new file mode 100644
index 00000000..138e0269
--- /dev/null
+++ b/crates/atuin-ai/src/tui/spinner.rs
@@ -0,0 +1,99 @@
+//! Spinner styles and configuration for TUI animations
+//!
+//! To experiment with different spinners, change `ACTIVE_SPINNER` below.
+
+use std::time::Duration;
+
+/// Active spinner style - change this to experiment with different styles
+pub const ACTIVE_SPINNER: SpinnerStyle = SpinnerStyle::Dots;
+
+/// Spinner style definitions
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub enum SpinnerStyle {
+ /// Classic ASCII line spinner: / - \ |
+ Line,
+ /// Braille dots pattern
+ Dots,
+ /// Growing/shrinking dots
+ Pulse,
+ /// Simple arrow rotation
+ Arrow,
+ /// Block building
+ Block,
+}
+
+impl SpinnerStyle {
+ /// Get the frames for this spinner style
+ pub const fn frames(&self) -> &'static [&'static str] {
+ match self {
+ SpinnerStyle::Line => &["/", "-", "\\", "|"],
+ SpinnerStyle::Dots => &["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"],
+ SpinnerStyle::Pulse => &["·", "•", "●", "•"],
+ SpinnerStyle::Arrow => &["←", "↖", "↑", "↗", "→", "↘", "↓", "↙"],
+ SpinnerStyle::Block => &[
+ "▏", "▎", "▍", "▌", "▋", "▊", "▉", "█", "▉", "▊", "▋", "▌", "▍", "▎", "▏",
+ ],
+ }
+ }
+
+ /// Get the recommended tick interval for this spinner style
+ /// Faster spinners need shorter intervals to look smooth
+ pub const fn tick_interval(&self) -> Duration {
+ match self {
+ SpinnerStyle::Line => Duration::from_millis(150),
+ SpinnerStyle::Dots => Duration::from_millis(80),
+ SpinnerStyle::Pulse => Duration::from_millis(200),
+ SpinnerStyle::Arrow => Duration::from_millis(100),
+ SpinnerStyle::Block => Duration::from_millis(80),
+ }
+ }
+
+ /// Get the frame at the given index (wraps around)
+ pub fn frame_at(&self, index: usize) -> &'static str {
+ let frames = self.frames();
+ frames[index % frames.len()]
+ }
+
+ /// Get the number of frames in this spinner
+ pub fn frame_count(&self) -> usize {
+ self.frames().len()
+ }
+}
+
+/// Get the active spinner's frame at the given index
+pub fn active_frame(index: usize) -> &'static str {
+ ACTIVE_SPINNER.frame_at(index)
+}
+
+/// Get the active spinner's tick interval
+pub fn active_tick_interval() -> Duration {
+ ACTIVE_SPINNER.tick_interval()
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn test_frame_wrapping() {
+ let style = SpinnerStyle::Line;
+ assert_eq!(style.frame_at(0), "/");
+ assert_eq!(style.frame_at(4), "/"); // wraps
+ assert_eq!(style.frame_at(5), "-");
+ }
+
+ #[test]
+ fn test_all_styles_have_frames() {
+ let styles = [
+ SpinnerStyle::Line,
+ SpinnerStyle::Dots,
+ SpinnerStyle::Pulse,
+ SpinnerStyle::Arrow,
+ SpinnerStyle::Block,
+ ];
+ for style in styles {
+ assert!(!style.frames().is_empty());
+ assert!(style.tick_interval().as_millis() > 0);
+ }
+ }
+}