aboutsummaryrefslogtreecommitdiffstats
path: root/crates/atuin-client/src/settings.rs (follow)
Commit message (Collapse)AuthorAge
* chore: Refactor CLI auth flows and token storage (#3317)Michelle Tilley2026-03-23
| | | | | | | This PR eplaces the binary `is_hub_sync()` auth routing with an explicit `SyncAuth` enum that classifies the client's authentication state at runtime. This fixes a class of bugs where CLI session tokens were silently mis-stored or used with the wrong auth scheme during Hub migration.
* fix: Disable features in init when that feature is explicitly disabled (#3328)Michelle Tilley2026-03-23
| | | | | | | This PR updates `atuin init` to skip initializers for subfeatures when that subfeature is explicitly disabled with a setting value of `false`. For `ai.enabled = false`, this releases the question mark keybind. Fixes #3325
* feat: Allow headless account ops against Hub server (#3280)Michelle Tilley2026-03-16
|
* feat: Add `atuin setup` (#3257)Michelle Tilley2026-03-12
|
* feat: Allow authenticating with Atuin Hub (#3237)Michelle Tilley2026-03-11
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | ## Summary This PR enables the Atuin CLI to authenticate with Atuin Hub, unifying authentication across CLI sync and Hub features (AI, runbooks, etc.). ### Key Changes - **Dual auth support**: New `AuthToken` enum supports both `Bearer` (Hub) and `Token` (legacy CLI) authentication - **Smart protocol selection**: New `sync_protocol` setting (`auto`/`hub`/`legacy`) determines auth method. By default, `api.atuin.sh` uses Hub auth; custom sync addresses use legacy auth - **Hub login flow**: `atuin login` now initiates an OAuth-like flow for Hub users—generates a code, user authorizes in browser, CLI polls for completion - **Account linking**: After Hub auth, silently attempts to link existing CLI sync account to Hub account for seamless migration - **Graceful fallback**: `sync_auth_token()` prefers Hub token when available, falls back to CLI session token ### Auth Flow 1. User runs `atuin login` (with default sync address) 2. CLI requests auth code from Hub, displays URL 3. User opens URL, logs in/registers on Hub 4. Hub attaches API token to code 5. CLI polls, receives token, saves as hub session 6. If user had existing CLI sync account, it's automatically linked ### Backward Compatibility - Existing self-hosted users: unaffected (legacy auth via `Token` header) - Existing `api.atuin.sh` users: continue working with CLI session until they run `atuin login` - New users: go through Hub flow automatically ## Test Plan - [ ] New user registration via Hub flow - [ ] Existing CLI user can still sync without changes - [ ] `atuin login` links CLI account to Hub account - [ ] Self-hosted users unaffected by changes - [ ] AI commands work after Hub auth --------- Co-authored-by: Ellie Huxtable <ellie@elliehuxtable.com>
* feat: Allow setting multipliers for frequency, recency, and frecency scores ↵Michelle Tilley2026-03-05
| | | | (#3235)
* chore: Move atuin ai subcommand into core binary (#3212)Michelle Tilley2026-03-02
|
* feat: In-memory search index with atuin daemon (#3201)Michelle Tilley2026-02-26
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | ## Summary This PR adds a persistent, in-memory search index to the Atuin daemon, enabling fast fuzzy search without the startup delay of building an index each time the TUI opens. ### Key Changes - **Daemon search service**: A new gRPC service that maintains a Nucleo fuzzy search index in memory - **Real-time index updates**: The daemon listens for history events (new commands, synced records) and updates the index immediately - **Filter mode support**: All existing filter modes work (Global, Host, Session, Directory, Workspace) - **New search engine**: `daemon-fuzzy` search mode that queries the daemon instead of building a local index - **Paged history loading**: Database pagination support for efficient initial index loading - **Configurable logging**: New `[logs]` settings section for daemon and search log configuration - **Component-based daemon architecture**: Refactored daemon internals into a modular, event-driven system - **Fallback to DB search for regex**: Since Nucleo doesn't support regex matching ## Daemon Architecture The daemon has been refactored to use a component-based, event-driven architecture that makes it easier to add new functionality and reason about the system. ### Core Concepts ``` ┌─────────────────────────────────────────────────────────────────────────────┐ │ Atuin Daemon │ │ │ │ ┌─────────────┐ ┌──────────────────────────────────────────────────┐ │ │ │ Daemon │ │ Components │ │ │ │ Handle │────▶│ │ │ │ │ │ │ ┌─────────────┐ ┌─────────────┐ ┌────────────┐ │ │ │ │ • emit() │ │ │ History │ │ Search │ │ Sync │ │ │ │ │ • subscribe │ │ │ Component │ │ Component │ │ Component │ │ │ │ │ • settings │ │ │ │ │ │ │ │ │ │ │ │ • databases │ │ │ gRPC service│ │ gRPC service│ │ background │ │ │ │ └─────────────┘ │ │ WIP history │ │ Nucleo index│ │ sync │ │ │ │ │ │ └─────────────┘ └─────────────┘ └────────────┘ │ │ │ │ └──────────────────────────────────────────────────┘ │ │ │ ▲ │ │ ▼ │ │ │ ┌─────────────────────────────────────┴────────────────────────────────┐ │ │ │ Event Bus (broadcast) │ │ │ │ │ │ │ │ HistoryStarted │ HistoryEnded │ RecordsAdded │ SyncCompleted │ ... │ │ │ └──────────────────────────────────────────────────────────────────────┘ │ │ ▲ │ │ ┌───────────────────────────────────┴──────────────────────────────────┐ │ │ │ Control Service (gRPC) │ │ │ │ External event injection from CLI commands │ │ │ └──────────────────────────────────────────────────────────────────────┘ │ └─────────────────────────────────────────────────────────────────────────────┘ ``` ### DaemonHandle A lightweight, cloneable handle that provides access to shared daemon resources: - **Event emission**: `handle.emit(DaemonEvent::...)` broadcasts to all components - **Event subscription**: `handle.subscribe()` returns a receiver for the event bus - **Settings**: `handle.settings()` for configuration access - **Databases**: `handle.history_db()` and `handle.store()` for data access ### Component Trait Components implement a simple lifecycle: ```rust #[async_trait] trait Component: Send + Sync { fn name(&self) -> &'static str; async fn start(&mut self, handle: DaemonHandle) -> Result<()>; async fn handle_event(&mut self, event: &DaemonEvent) -> Result<()>; async fn stop(&mut self) -> Result<()>; } ``` ### Event-Driven Design Components communicate via events rather than direct coupling: | Event | Emitted By | Consumed By | |-------|-----------|-------------| | `HistoryStarted` | History gRPC | Search (logging) | | `HistoryEnded` | History gRPC | Search (index update) | | `RecordsAdded` | Sync | Search (index update) | | `HistoryPruned` | CLI (via Control) | Search (index rebuild) | | `HistoryDeleted` | CLI (via Control) | Search (index rebuild) | | `ForceSync` | CLI (via Control) | Sync | | `ShutdownRequested` | Signal handler | All (graceful shutdown) | ### External Event Injection CLI commands can inject events into a running daemon: ```rust // After `atuin history prune` emit_event(DaemonEvent::HistoryPruned).await?; // After deleting specific items emit_event(DaemonEvent::HistoryDeleted { ids }).await?; // Request immediate sync emit_event(DaemonEvent::ForceSync).await?; ``` This ensures the daemon's search index stays in sync with database changes made by CLI commands. ## Search Architecture The search service uses a [forked version of Nucleo](https://github.com/atuinsh/nucleo-ext) that adds filter and scorer callbacks, enabling efficient filtering and frecency-based ranking. ``` ┌────────────────────────────────────────────────────────────────┐ │ Atuin Daemon │ │ │ │ ┌─────────────────┐ ┌──────────────────────────────────┐ │ │ │ Event System │───▶│ Search Component │ │ │ │ │ │ │ │ │ │ • RecordsAdded │ │ ┌────────────────────────────┐ │ │ │ │ • HistoryEnded │ │ │ Deduplicated Index │ │ │ │ │ • HistoryPruned │ │ │ │ │ │ │ └─────────────────┘ │ │ CommandData per command: │ │ │ │ │ │ • Global frecency │ │ │ │ ┌─────────────────┐ │ │ • Filter indexes (sets) │ │ │ │ │ Background Task │ │ │ • Invocation history │ │ │ │ │ │ │ └────────────────────────────┘ │ │ │ │ Rebuilds │ │ │ │ │ │ │ frecency map │ │ ▼ │ │ │ │ every 60s │───▶│ ┌────────────────────────────┐ │ │ │ └─────────────────┘ │ │ Nucleo (forked) │ │ │ │ │ │ │ │ │ │ │ │ • Filter callback │ │ │ │ │ │ • Scorer callback │ │ │ │ │ │ • Fuzzy matching │ │ │ │ │ └────────────────────────────┘ │ │ │ └──────────────────────────────────┘ │ │ │ │ │ │ gRPC (Unix socket) │ └──────────────────────────────────────│─────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────────┐ │ Search TUI (Client) │ │ │ │ 1. Send query + filter mode + context to daemon │ │ 2. Receive matching history IDs (ranked by frecency) │ │ 3. Hydrate full records from local SQLite database │ │ 4. Display results in TUI │ └─────────────────────────────────────────────────────────────────┘ ``` ### Nucleo Fork The [nucleo-ext fork](https://github.com/atuinsh/nucleo-ext) adds two key features to Nucleo: 1. **Filter callback**: Pre-filter items before fuzzy matching (used for directory/host/session filtering) 2. **Scorer callback**: Compute custom scores after matching (used for frecency ranking) ```rust // Filter: only include commands run in current directory nucleo.set_filter(Some(Arc::new(|cmd: &String| { passing_commands.contains(cmd) }))); // Scorer: combine fuzzy score with frecency nucleo.set_scorer(Some(Arc::new(|cmd: &String, fuzzy_score: u32| { let frecency = frecency_map.get(cmd).unwrap_or(0); fuzzy_score + (frecency * 10) }))); ``` ### Deduplicated Index Commands are stored once per unique command text, with metadata tracking all invocations: ```rust struct CommandData { command: String, invocations: Vec<Invocation>, // All times this command was run global_frecency: FrecencyData, // Precomputed frecency score // O(1) filter indexes directories: HashSet<String>, // All cwds where command was run hosts: HashSet<String>, // All hostnames sessions: HashSet<String>, // All session IDs } ``` This deduplication means: - **Fewer items to match**: ~13K unique commands vs ~62K history entries - **O(1) filter checks**: HashSet lookups instead of scanning invocations - **Single frecency score**: Global frecency computed once, used for all filter modes ### Frecency Scoring Frecency (frequency + recency) scoring prioritizes recently and frequently used commands: ```rust fn compute_frecency(count: u32, last_used: i64, now: i64) -> u32 { let age_hours = (now - last_used) / 3600; // Recency: decays over time (half-life ~24 hours) let recency = (100.0 * (-age_hours as f64 / 24.0).exp()) as u32; // Frequency: logarithmic scaling let frequency = (count.ln() * 20.0).min(100.0) as u32; recency + frequency } ``` The frecency map is: - **Precomputed by background task** every 60 seconds - **Never computed inline** during search (no latency impact) - **Graceful fallback**: If unavailable, search works without frecency ranking ### Filter Mode Implementation | Filter Mode | Implementation | |-------------|----------------| | Global | No filter (all commands) | | Directory | `command.directories.contains(cwd)` | | Workspace | `command.directories.any(\|d\| d.starts_with(git_root))` | | Host | `command.hosts.contains(hostname)` | | Session | `command.sessions.contains(session_id)` | Filters are pre-computed into a HashSet before the search, making the filter callback O(1). ### Search Flow 1. **Daemon startup**: Loads history from SQLite in pages, builds deduplicated index 2. **Frecency precompute**: Background task builds frecency map after history loads 3. **Search request**: Client sends query with filter mode and context 4. **Filter**: Pre-computed HashSet determines which commands pass the filter 5. **Match**: Nucleo fuzzy matches the query against command text 6. **Score**: Frecency scorer ranks results (fuzzy score + frecency * 10) 7. **Response**: Returns history IDs for the most recent invocation of each matching command 8. **Hydration**: Client fetches full records from local SQLite ### Configuration ```toml # Enable daemon + autostart [daemon] enabled = true autostart = true # Enable daemon-based fuzzy search [search] search_mode = "daemon-fuzzy" ``` ## Performance Performance varies based on several factors, but in most initial testing with the new architecture shows improvement: * **Nucleo performs searches up to 4.5x faster**: direct DB search averages 18.07ms, but the daemon completes the same queries in 3.99ms. * **IPC overhead is significant, but acceptable**: a significant amount of wall-time is taken up by the transfer of data over IPC (via UDS in this case). This averages to about ~7.8ms and accounts for 66% of client-side wall time. * **Tail latency improves at every layer**: p99 times correspond to initial requests, worst-case query patterns, etc. but the average p99 daemon-based response time is 3.6x better than the associated DB-based search p99 time * **Query complexity no longer impacts performance**: the Nucleo-based search shows consistent 2-7ms times regardless of query pattern. The DB-based search had a 17x variance (3.59ms to 62.46ms). Interestingly, @ellie - who has a larger history store than I do - gets even better performance on the IPC layer. This could use a lot more testing in various edge cases and on various hardware, but seems promising. ### Regular DB search ``` Individual calls for: db_search -------------------------------------------------------------------------------------------------------------- # Wall Busy Idle Fields -------------------------------------------------------------------------------------------------------------- 1 32.25ms 32.20ms 47.70µs {"mode":"Fuzzy","query":"^"} 2 19.48ms 19.40ms 84.20µs {"mode":"Fuzzy","query":"^c"} 3 20.40ms 20.10ms 297.00µs {"mode":"Fuzzy","query":"^ca"} 4 13.07ms 13.00ms 69.90µs {"mode":"Fuzzy","query":"^car"} 5 12.17ms 12.10ms 67.10µs {"mode":"Fuzzy","query":"^carg"} 6 20.78ms 20.70ms 76.60µs {"mode":"Fuzzy","query":"^cargo"} 7 9.15ms 9.10ms 53.20µs {"mode":"Fuzzy","query":"^cargo "} 8 10.24ms 10.00ms 237.00µs {"mode":"Fuzzy","query":"^cargo b"} 9 10.01ms 9.68ms 325.00µs {"mode":"Fuzzy","query":"^cargo bu"} 10 5.89ms 5.83ms 57.20µs {"mode":"Fuzzy","query":"^cargo bui"} 11 8.85ms 8.28ms 568.00µs {"mode":"Fuzzy","query":"^cargo buil"} 12 7.70ms 7.49ms 212.00µs {"mode":"Fuzzy","query":"^cargo build"} 13 3.59ms 3.53ms 57.00µs {"mode":"Fuzzy","query":"^cargo build$"} 14 6.50ms 6.44ms 63.60µs {"mode":"Fuzzy","query":"^cargo "} 15 6.48ms 6.38ms 100.00µs {"mode":"Fuzzy","query":"!"} 16 31.68ms 31.60ms 75.90µs {"mode":"Fuzzy","query":"!g"} 17 62.46ms 62.40ms 58.90µs {"mode":"Fuzzy","query":"!gi"} 18 30.35ms 30.30ms 46.90µs {"mode":"Fuzzy","query":"!git"} 19 53.84ms 53.80ms 40.80µs {"mode":"Fuzzy","query":"!git "} 20 19.24ms 19.20ms 39.70µs {"mode":"Fuzzy","query":"!git c"} 21 22.03ms 22.00ms 34.70µs {"mode":"Fuzzy","query":"!git co"} 22 17.13ms 17.00ms 133.00µs {"mode":"Fuzzy","query":"!git com"} 23 16.14ms 15.90ms 242.00µs {"mode":"Fuzzy","query":"!git comm"} 24 5.11ms 5.08ms 28.60µs {"mode":"Fuzzy","query":"!git commi"} 25 7.31ms 7.26ms 52.70µs {"mode":"Fuzzy","query":"!git commit"} Summary: 25 calls Wall: avg=18.07ms, min=3.59ms, max=62.46ms, p50=13.07ms, p99=62.46ms Busy: avg=17.95ms, min=3.53ms, max=62.40ms, p50=13.00ms, p99=62.40ms ``` ### Daemon-based search **Client** ``` Individual calls for: daemon_search -------------------------------------------------------------------------------------------------------------- # Wall Busy Idle Fields -------------------------------------------------------------------------------------------------------------- 1 13.05ms 2.55ms 10.50ms {"query":"^"} 2 10.65ms 1.40ms 9.25ms {"query":"^c"} 3 10.72ms 1.18ms 9.54ms {"query":"^ca"} 4 5.54ms 485.00µs 5.06ms {"query":"^car"} 5 15.02ms 1.02ms 14.00ms {"query":"^carg"} 6 9.49ms 840.00µs 8.65ms {"query":"^cargo"} 7 5.53ms 555.00µs 4.97ms {"query":"^cargo "} 8 8.56ms 717.00µs 7.84ms {"query":"^cargo b"} 9 12.34ms 1.24ms 11.10ms {"query":"^cargo bu"} 10 8.38ms 650.00µs 7.73ms {"query":"^cargo bui"} 11 13.07ms 770.00µs 12.30ms {"query":"^cargo buil"} 12 17.11ms 709.00µs 16.40ms {"query":"^cargo build"} 13 15.41ms 907.00µs 14.50ms {"query":"^cargo build$"} 14 8.19ms 665.00µs 7.52ms {"query":"^cargo "} 15 7.98ms 1.72ms 6.26ms {"query":"!"} 16 13.56ms 856.00µs 12.70ms {"query":"!g"} 17 8.11ms 624.00µs 7.49ms {"query":"!gi"} 18 14.57ms 775.00µs 13.80ms {"query":"!git"} 19 14.18ms 779.00µs 13.40ms {"query":"!git "} 20 9.62ms 802.00µs 8.82ms {"query":"!git c"} 21 15.50ms 1.50ms 14.00ms {"query":"!git co"} 22 11.58ms 1.48ms 10.10ms {"query":"!git com"} 23 13.82ms 2.12ms 11.70ms {"query":"!git comm"} 24 17.48ms 2.18ms 15.30ms {"query":"!git commi"} 25 14.81ms 1.71ms 13.10ms {"query":"!git commit"} Summary: 25 calls Wall: avg=11.77ms, min=5.53ms, max=17.48ms, p50=12.34ms, p99=17.48ms Busy: avg=1.13ms, min=485.00µs, max=2.55ms, p50=856.00µs, p99=2.55ms ``` **Daemon** ``` Individual calls for: daemon_search_query -------------------------------------------------------------------------------------------------------------- # Wall Busy Idle Fields -------------------------------------------------------------------------------------------------------------- 1 1.75ms 250ns 1.75ms {"query":"^","query_id":1} 2 4.58ms 125ns 4.58ms {"query":"^c","query_id":2} 3 4.39ms 250ns 4.39ms {"query":"^ca","query_id":3} 4 2.52ms 125ns 2.52ms {"query":"^car","query_id":4} 5 4.44ms 250ns 4.44ms {"query":"^carg","query_id":5} 6 3.66ms 167ns 3.66ms {"query":"^cargo","query_id":6} 7 2.38ms 84ns 2.38ms {"query":"^cargo ","query_id":7} 8 4.13ms 84ns 4.13ms {"query":"^cargo b","query_id":8} 9 4.40ms 167ns 4.40ms {"query":"^cargo bu","query_id":9} 10 3.87ms 125ns 3.87ms {"query":"^cargo bui","query_id":10} 11 4.36ms 84ns 4.36ms {"query":"^cargo buil","query_id":11} 12 3.96ms 333ns 3.96ms {"query":"^cargo build","query_id":12} 13 4.61ms 167ns 4.61ms {"query":"^cargo build$","query_id":13} 14 4.20ms 209ns 4.20ms {"query":"^cargo ","query_id":14} 15 238.17µs 167ns 238.00µs {"query":"!","query_id":15} 16 4.44ms 125ns 4.44ms {"query":"!g","query_id":16} 17 3.47ms 83ns 3.47ms {"query":"!gi","query_id":17} 18 4.57ms 125ns 4.57ms {"query":"!git","query_id":18} 19 7.15ms 167ns 7.15ms {"query":"!git ","query_id":19} 20 4.27ms 250ns 4.27ms {"query":"!git c","query_id":20} 21 5.19ms 292ns 5.19ms {"query":"!git co","query_id":21} 22 4.29ms 417ns 4.29ms {"query":"!git com","query_id":22} 23 4.08ms 125ns 4.08ms {"query":"!git comm","query_id":23} 24 4.50ms 167ns 4.50ms {"query":"!git commi","query_id":24} 25 4.35ms 208ns 4.35ms {"query":"!git commit","query_id":25} Summary: 25 calls Wall: avg=3.99ms, min=238.17µs, max=7.15ms, p50=4.29ms, p99=7.15ms Busy: avg=182ns, min=83ns, max=417ns, p50=167ns, p99=417ns ``` **Nucleo matching time (in daemon)** ``` Individual calls for: nucleo_match -------------------------------------------------------------------------------------------------------------- # Wall Busy Idle Fields -------------------------------------------------------------------------------------------------------------- 1 1.73ms 125ns 1.73ms {"query":"^","query_id":1} 2 4.57ms 167ns 4.57ms {"query":"^c","query_id":2} 3 4.37ms 125ns 4.37ms {"query":"^ca","query_id":3} 4 2.51ms 84ns 2.51ms {"query":"^car","query_id":4} 5 4.43ms 125ns 4.43ms {"query":"^carg","query_id":5} 6 3.64ms 125ns 3.64ms {"query":"^cargo","query_id":6} 7 2.37ms 84ns 2.37ms {"query":"^cargo ","query_id":7} 8 4.11ms 125ns 4.11ms {"query":"^cargo b","query_id":8} 9 4.36ms 208ns 4.36ms {"query":"^cargo bu","query_id":9} 10 3.85ms 125ns 3.85ms {"query":"^cargo bui","query_id":10} 11 4.35ms 125ns 4.35ms {"query":"^cargo buil","query_id":11} 12 3.94ms 250ns 3.94ms {"query":"^cargo build","query_id":12} 13 4.59ms 125ns 4.59ms {"query":"^cargo build$","query_id":13} 14 4.18ms 84ns 4.18ms {"query":"^cargo ","query_id":14} 15 220.13µs 125ns 220.00µs {"query":"!","query_id":15} 16 4.43ms 125ns 4.43ms {"query":"!g","query_id":16} 17 3.45ms 125ns 3.45ms {"query":"!gi","query_id":17} 18 4.55ms 125ns 4.55ms {"query":"!git","query_id":18} 19 7.12ms 209ns 7.12ms {"query":"!git ","query_id":19} 20 4.25ms 166ns 4.25ms {"query":"!git c","query_id":20} 21 5.18ms 125ns 5.18ms {"query":"!git co","query_id":21} 22 4.27ms 125ns 4.27ms {"query":"!git com","query_id":22} 23 4.06ms 292ns 4.06ms {"query":"!git comm","query_id":23} 24 4.46ms 166ns 4.46ms {"query":"!git commi","query_id":24} 25 4.31ms 208ns 4.31ms {"query":"!git commit","query_id":25} Summary: 25 calls Wall: avg=3.97ms, min=220.13µs, max=7.12ms, p50=4.27ms, p99=7.12ms Busy: avg=147ns, min=84ns, max=292ns, p50=125ns, p99=292ns ```
* feat: Generate commands or ask questions with `atuin ai` (#3199)Michelle Tilley2026-02-24
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | 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>
* feat: add autostart and pid management to daemon (#3180)Ellie Huxtable2026-02-13
|
* feat: add Hub authentication for future sync + extra features (#3010)Ellie Huxtable2026-02-12
|
* fix(shell): fix ATUIN_SESSION errors in tmux popup (#3170)Kkoi2026-02-11
| | | | | | | | | | | | | | | | | | | | | This PR fixes: + #3154 + #3164 + #3166 It changes the following: + Pass ATUIN_SESSION to tmux popup + Use `command rm` to bypass user-defined configs + Disable tmux popup by default ## Checks - [x] I am happy for maintainers to push small adjustments to this PR, to speed up the review cycle - [x] I have checked that there are no existing pull requests for the same thing --------- Co-authored-by: Ellie Huxtable <ellie@elliehuxtable.com>
* feat: Add new custom keybinding system for search TUI (#3127)Michelle Tilley2026-02-05
|
* feat: replace several files with a sqlite db (#3128)Ellie Huxtable2026-02-04
| | | | | | | | | | | | | | | | | | | | | | | These files have been known to have corruption issues. SQLite will perform better across filesystems for reads/writes across threads, and will lock as expected. I've also put the session file in there, though I'm 50/50 on it - I'll be replacing it with keyring storage asap anyway. The key file is _not_ included. It should ~never be changed, and should be easy for the user to secure + manage themselves In the future, instead of creating more files, we can just use this as a kv store Resolves https://github.com/atuinsh/atuin/issues/2336, resolves https://github.com/atuinsh/atuin/issues/1650 ## Checks - [ ] I am happy for maintainers to push small adjustments to this PR, to speed up the review cycle - [ ] I have checked that there are no existing pull requests for the same thing
* fix: larger exit column width on Windows (#3119)Lucas Trzesniewski2026-02-02
| | | | | | | | | | | | | | | | | | | | | | | | | | | | The default width of the `exit` column is 3, which matches the 0-255 values on Linux. However, on Windows it is a 32-bit integer, and can be anything from -2147483648 to 2147483647. Popular tools such as `winget` apparently make use of that. Here's an example with `columns = ["exit", "time", "duration", "directory", "command"]`: <img width="647" height="416" alt="image" src="https://github.com/user-attachments/assets/d73b0487-3c77-4049-8487-01b2aab29a44" /> This PR changes the default column width to 11 on Windows to match these values. The result is: <img width="601" height="412" alt="image" src="https://github.com/user-attachments/assets/b4b95f40-f223-400d-83c4-74d71e070b3e" /> ## Checks - [x] I am happy for maintainers to push small adjustments to this PR, to speed up the review cycle - [x] I have checked that there are no existing pull requests for the same thing
* feat: add option to use tmux display-popup (#3058)Kkoi2026-01-28
| | | | | | | | | | | | | | | | | | | | | | | | This is a "continuation" of #1177 - which was a draft that used FIFOs (named pipes) to get output from the popup, however this causes popup not being closed properly, so in this PR I use tmpfile to store the result and read after popup closes. @ellie could you review this PR please? P.S. Thank you @immae for sharing your idea! ## Feature + Option to use tmux popup window in `config.toml` + Customize window width/height in `config.toml` + Tmux display-popup for `zsh, bash, fish` ## Checks - [x] I am happy for maintainers to push small adjustments to this PR, to speed up the review cycle - [x] I have checked that there are no existing pull requests for the same thing --------- Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
* fix: custom data dir test on windows (#3109)Ellie Huxtable2026-01-27
| | | | | | | | | | | | <!-- Thank you for making a PR! Bug fixes are always welcome, but if you're adding a new feature or changing an existing one, we'd really appreciate if you open an issue, post on the forum, or drop in on Discord --> ## Checks - [ ] I am happy for maintainers to push small adjustments to this PR, to speed up the review cycle - [ ] I have checked that there are no existing pull requests for the same thing
* fix(ui): align cursor when expand column is in the middle (#3103)依云2026-01-27
| | | | | | | | | | | | | | | | Also change the trailing space of each column to be drawn in `render_row` instead so that each column is separated by a space no matter how they are ordered. <!-- Thank you for making a PR! Bug fixes are always welcome, but if you're adding a new feature or changing an existing one, we'd really appreciate if you open an issue, post on the forum, or drop in on Discord --> ## Checks - [x] I am happy for maintainers to push small adjustments to this PR, to speed up the review cycle - [x] I have checked that there are no existing pull requests for the same thing
* feat: support setting a custom data dir in config (#3105)Ellie Huxtable2026-01-27
| | | | | | | | | | | | <!-- Thank you for making a PR! Bug fixes are always welcome, but if you're adding a new feature or changing an existing one, we'd really appreciate if you open an issue, post on the forum, or drop in on Discord --> ## Checks - [ ] I am happy for maintainers to push small adjustments to this PR, to speed up the review cycle - [ ] I have checked that there are no existing pull requests for the same thing
* fix(ui): time column can take up to 9 cellslilydjwg2026-01-24
|
* feat: add custom column support (#3089)Ellie Huxtable2026-01-22
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | Configure the interactive search UI appearance. Resolves #998 ```toml [ui] columns = ["duration", "time", "command"] ``` ### `columns` Default: `["duration", "time", "command"]` Columns to display in the interactive search, from left to right. The selection indicator (`" > "`) is always shown first implicitly. Each column can be specified as: - A simple string (uses default width): `"duration"` - An object with type and optional width/expand: `{ type = "directory", width = 30 }` #### Available column types | Column | Default Width | Description | | --------- | ------------- | ----------------------------------------------- | | duration | 5 | Command execution duration (e.g., "123ms") | | time | 8 | Relative time since execution (e.g., "59m ago") | | datetime | 16 | Absolute timestamp (e.g., "2025-01-22 14:35") | | directory | 20 | Working directory (truncated if too long) | | host | 15 | Hostname | | user | 10 | Username | | exit | 3 | Exit code (colored by success/failure) | | command | * | The command itself (expands by default) | #### Column options - **type**: The column type (required when using object format) - **width**: Custom width in characters (optional, uses default if not specified) - **expand**: If `true`, the column fills remaining space. Default is `true` for `command`, `false` for others. Only one column should have `expand = true`. #### Examples ```toml # Minimal - more space for commands columns = ["duration", "command"] # With custom directory width columns = ["duration", { type = "directory", width = 30 }, "command"] # Show host for multi-machine sync users columns = ["duration", "time", "host", "command"] # Show exit codes prominently columns = ["exit", "duration", "command"] # Make directory expand instead of command columns = ["duration", "time", { type = "directory", expand = true }, { type = "command", expand = false }] ```
* fix: Move thorough search through search.filters w/ workspaces (#2703)Leonidas Loucas2025-12-15
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | Do not just pick first from search.filters, consider all options, filtering based on git-root and workspaces config Fix: atuinsh/atuin#2700 Might Fix: atuinsh/atuin#2536 Wrote a few simple unit tests, and tried it by running the client locally. Not sure about which of "[search] filters" vs "workspaces" should be more respected, so i made it so if workspaces = false, regardless of whats in "[search] filters" workspace gets filtered out. First time looking at atuins code, let me know if this needs any changes. edit: (from my post here: https://github.com/atuinsh/atuin/issues/2536#issuecomment-2808053862) ``` workspaces = true [search] filters = [ "workspace", "directory", "session", "global" ] ``` | `^R` for the ... time| in a git repo | any other dir| | ------------- | ------------- | ------------- | | first | workspace | directory| | second | directory | session| | third| session | global| | fourth | global | directory | | fifth | workspace | session| | sixth | directory | global | ![Image](https://github.com/user-attachments/assets/33150e0a-d219-44d1-8994-b1f98accdfc3) <!-- Thank you for making a PR! Bug fixes are always welcome, but if you're adding a new feature or changing an existing one, we'd really appreciate if you open an issue, post on the forum, or drop in on Discord --> ## Checks - [X] I am happy for maintainers to push small adjustments to this PR, to speed up the review cycle - [X] I have checked that there are no existing pull requests for the same thing - #1655 might be related, but it seems to also implement code that is already merged, and has been open for over a year, hope its ok to suggest this --------- Co-authored-by: Michelle Tilley <michelle@michelletilley.net>
* feat: add various acceptance keys (#2928)Keith Cirkel2025-09-30
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | This change introduces (optional) acceptance keys of Backspace and Left Arrow, when at the start of a line. These two are common muscle memory actions for users. The configuration defaults to false so as not to disrupt existing user patterns. This also adds a test that exercises the various acceptance modes, which as it turns out was quite easy to do. I discussed this on discord where [Ellie suggested I raised an issue](https://discord.com/channels/954121165239115808/1421180955657244703/1422642337481228400), but I felt like a PR would be more tangiable. I've tested this locally and I'm very happy with how these keys work, it fits my needs well. `exit_past_line_start` and `accept_past_line_start` can technically co-exist. When this happens `accept_past_line_start` takes precedence. Is this okay, or should we reconsider the config? Perhaps `acceptance_keys = []` would be better here? I'm very open to changes here. <!-- Thank you for making a PR! Bug fixes are always welcome, but if you're adding a new feature or changing an existing one, we'd really appreciate if you open an issue, post on the forum, or drop in on Discord --> ## Checks - [x] I am happy for maintainers to push small adjustments to this PR, to speed up the review cycle - [x] I have checked that there are no existing pull requests for the same thing
* feat(stats): add dotnet to default common subcommandsLucas Trzesniewski2025-09-25
|
* feat: add session-preload filter mode to include global history from before ↵chitao12342025-09-15
| | | | | | session start This mode mimics the default behavior for many shells.
* feat(tui): add show_numeric_shortcuts config to hide 1-9 shortcuts (#2766)sebbie2025-09-12
| | | | | | - Config option: show_numeric_shortcuts (default: true) - When false, hide 1–9 numeric badges; keep indicator on selected row - Example key added to example config.toml
* fix: expand path for daemon.socket_path (#2870)Paul Barton2025-08-23
| | | | | | | | | | | This parses the daemon.socket_path config item to allow it to be set to something like `"${XDG_RUNTIME_DIR}/atuin.sock"`. Fixes https://github.com/atuinsh/atuin/issues/2490. <!-- Thank you for making a PR! Bug fixes are always welcome, but if you're adding a new feature or changing an existing one, we'd really appreciate if you open an issue, post on the forum, or drop in on Discord --> ## Checks - [x] I am happy for maintainers to push small adjustments to this PR, to speed up the review cycle - [x] I have checked that there are no existing pull requests for the same thing
* fix: honor timezone in inspector stats (#2853)Ray Kohler2025-08-04
| | | | | | | | | * fix: honor timezone in inspector stats fixes: #2567 * docs: fix rustdoc warning about URL markup * fix: pass timezone by value, as suggested by clippy (oops)
* feat: add inline_height_shell_up_key_binding option (#2817)Caio S. Rohwedder2025-07-23
|
* feat: command chaining (#2834)Ellie Huxtable2025-07-22
| | | | | | | * feat: command chaining Allow for smart completion of commands ending in && or || * fmt
* chore: Allow setting script DB path (#2750)Michelle Tilley2025-05-13
| | | | | * chore: Allow setting script DB path * Rename scripts.database_path setting to scripts.db_path to match other crates
* feat: Implement KV as a write-through cache (#2732)Michelle Tilley2025-05-06
|
* feat(stats): add jj to default common subcommands (#2708)Alex Hamilton2025-04-21
|
* feat: support storing, syncing and executing scripts (#2644)Ellie Huxtable2025-04-07
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | * feat: add atuin-scripts crate * initial * define record types * wip * wip * mvp * add show command, make stdin work * rewrite execution to use shebang and script file ALWAYS * rename show -> get, allow fetching script only * fmt * clippy * a bunch of fixes to the edits * update lock * variables * fmt * clippy * pr feedback * fmt
* chore: migrate to rust 2024 (#2635)Ellie Huxtable2025-03-19
| | | | | | | | | * chore: upgrade to 2024 edition * ugh unsafe * format * nixxxxxxxxxxx why
* chore: align daemon and client sync freq (#2628)Ellie Huxtable2025-03-13
|
* fix: improve broken symlink error handling (#2589)James Trew2025-03-09
| | | | | | | | | | | Check atuin setting paths (eg. `db_path`) for broken symlinks on initialization and disable all shell hooks + print error message. sqlite doesn't create db files even with `.create_if_missing` when the db files are a broken symlink. This would cause sqlite to error and atuin to panic on every single keypress. Also improves related error handling when calling atuin client commands directly.
* feat: make new arrow key behavior configurable (#2606)chitao12342025-03-09
| | | | | | | | | | | | | | | | | | | * feat: make new arrow key behavior configurable The arrow key behavior in interactive search was changed in #2453, make it configurable via keys.exit_past_line_start and keys.accept_past_line_end * Update crates/atuin-client/config.toml * Update crates/atuin-client/config.toml * Update crates/atuin-client/config.toml * I've made so many typos with these, sorry --------- Co-authored-by: Ellie Huxtable <ellie@elliehuxtable.com>
* style: Avoid calling `unwrap()` when we don't have to (#2519)Andrew Aylett2025-01-07
| | | | Use `if let` rather than `is_some()` followed by `unwrap()`, and coerce errors instead of calling `unwrap()` when available.
* feat(client): add filter mode enablement and ordering configuration (#2430)Pavel Ivanov2024-11-19
|
* feat: Ultracompact Mode (search-only) (#2357)P T Weir2024-10-07
| | | | | | | | | | | | | | | * feat: add always_show_tabs setting * feat(ultracompact): extra-compact options * feat(ultracompact): enable via auto_hide_height * feat(ultracompact): clarify comment * fix(theming): minor tidyup to theming comment * chore: rustfmt/clippy corrections * chore: testing if CI issue was transient
* chore: enable inline height and compact by default (#2249)Ellie Huxtable2024-08-27
|
* test: add env ATUIN_TEST_LOCAL_TIMEOUT to control test timeout of SQLite (#2337)Jax Young2024-08-05
| | | | | | | | | | | | | | | | | | | | | | | * test: add env ATUIN_TEST_LOCAL_TIMEOUT to control test timeout of SQLite This make it possible to control the timeout of SQLite operations in test. And ATUIN_TEST_LOCAL_TIMEOUT defaults to the default local_timeout, which is actually used in the client. Instead of a small timeout (0.1), this change makes the test less likely to fail and better imitate the default behavior. SQLite operation timeout was first introduced from #1590, including connection and store timeout. The env ATUIN_TEST_SQLITE_STORE_TIMEOUT which added by #1703 only specify the store timeout. This commit doesn't deprecate ATUIN_TEST_SQLITE_STORE_TIMEOUT, but control it by setting its default to the new env ATUIN_TEST_LOCAL_TIMEOUT. * test!: replace ATUIN_TEST_SQLITE_STORE_TIMEOUT with ATUIN_TEST_LOCAL_TIMEOUT This deprecate ATUIN_TEST_SQLITE_STORE_TIMEOUT for simplicity as the new env ATUIN_TEST_LOCAL_TIMEOUT can control both connection and store timeout of SQLite in test. Details see 4d88611. Revert: #1703.
* fix(themes): Restore default theme, refactor (#2294)P T Weir2024-07-23
| | | | | | | | | | | | | | | | | * fix(theme): let the base colour remain unchanged * fix(theme): split out default * fix(theme): make base theme 'default' not an empty string * wip(theme): return styles, not colors * wip(theme): tidy up module structure a little * wip(theme): removed unhandled references to foreground_color * chore: fix cargo fmt * feat(theme): allow crossterm-deserializable colors
* feat(tui): Fixed preview height option (#2286)Tobias Genannt2024-07-17
| | | | | | | | * Added "fixed" to preview strategy Uses max_preview_height as a fixed value to prevent the list from "jumping". * Added test for new setting
* feat(tui): Customizable Themes (#2236)P T Weir2024-07-15
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | * wip: add theme * feat(theme): basic theming approach * feat(theme): adds theming support * fix: split out palette without compact inspector * fix(theme): tidy up implementation * fix(theme): correct yaml to toml * fix(theme): typo in comments * chore: cheer up clippy * fix(themes): ensure tests cannot hit real loading directory * chore: rustfmt * chore: rebase * feat(themes): add rgb hexcode support * fix(theme): add tests * fix(theme): use builtin log levels and correct debug test * feat(theme): adds the ability to derive from a non-base theme * fix(theme): warn if the in-file name of a theme does not match the filename * chore: tidy for rustfmt and clippy * chore: tidy for rustfmt and clippy
* chore: enable record sync by default (#2255)Ellie Huxtable2024-07-11
| | | I'm now pretty confident in the automatic migration. Let's cut people over so they have a better experience
* feat(daemon): follow XDG_RUNTIME_DIR if set (#2171)Ellie Huxtable2024-06-20
| | | | | | | | If XDG_RUNTIME_DIR is set, put the socket file there. If not, default to storing it in our data dir. We cannot default to a path such as /run/user/$UID/ because it does not exist on all systems. Any system running systemd will set this var by default, and ensure that the directory it points to is correctly setup.
* feat(tui): configurable prefix character (#2157)Ellie Huxtable2024-06-18
| | | | | * feat(tui): configurable prefix character * string not char because toml/json lol
* fix: unitless sync_frequence = 0 not parsed by humantime (#2154)Ellie Huxtable2024-06-17
| | | Resolve #2147