aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--crates/atuin-ai/src/commands/inline.rs11
-rw-r--r--crates/atuin-daemon/src/components/sync.rs61
-rw-r--r--crates/atuin/src/command/client/init.rs28
-rw-r--r--crates/atuin/src/command/client/init/bash.rs11
-rw-r--r--crates/atuin/src/command/client/init/fish.rs11
-rw-r--r--crates/atuin/src/command/client/init/zsh.rs12
-rw-r--r--docs/docs/ai/introduction.md10
-rw-r--r--docs/docs/faq.md12
8 files changed, 129 insertions, 27 deletions
diff --git a/crates/atuin-ai/src/commands/inline.rs b/crates/atuin-ai/src/commands/inline.rs
index 37e5ab81..803c7d72 100644
--- a/crates/atuin-ai/src/commands/inline.rs
+++ b/crates/atuin-ai/src/commands/inline.rs
@@ -77,10 +77,19 @@ async fn ensure_hub_session(settings: &atuin_client::settings::Settings) -> Resu
.active_hub_endpoint()
.unwrap_or("https://hub.atuin.sh".to_string());
+ let will_sync = settings.is_hub_sync();
+
info!("No Hub session found, prompting for authentication");
println!("Atuin AI requires authenticating with Atuin Hub.");
- println!("This is separate from your sync setup.");
+ if will_sync {
+ println!(
+ "Once logged in, your shell history will be synchronized via Atuin Hub if auto_sync is enabled or when manually syncing."
+ )
+ }
+ println!(
+ "If you have an existing Atuin sync account, you can log in with your existing credentials."
+ );
println!("Press enter to begin (or esc to cancel).");
if !wait_for_login_confirmation()? {
bail!("authentication canceled");
diff --git a/crates/atuin-daemon/src/components/sync.rs b/crates/atuin-daemon/src/components/sync.rs
index 6217706a..314b375e 100644
--- a/crates/atuin-daemon/src/components/sync.rs
+++ b/crates/atuin-daemon/src/components/sync.rs
@@ -2,6 +2,8 @@
//!
//! Handles periodic synchronization with the Atuin cloud server.
+use std::time::Duration;
+
use eyre::Result;
use rand::Rng;
use tokio::sync::mpsc;
@@ -23,6 +25,16 @@ enum SyncCommand {
Stop,
}
+/// Sync state - tracks whether we're in normal operation or retrying after failure.
+#[derive(Clone, Copy, PartialEq, Eq)]
+enum SyncState {
+ /// Normal operation. Periodic syncs only run if auto_sync is enabled.
+ Idle,
+ /// Retrying after a sync failure. Retries continue regardless of auto_sync
+ /// until the sync succeeds.
+ Retrying,
+}
+
/// Sync component - handles periodic cloud synchronization.
///
/// This component:
@@ -123,29 +135,43 @@ async fn sync_loop(handle: DaemonHandle, mut cmd_rx: mpsc::Receiver<SyncCommand>
// we may end up running a lot of syncs in a hot loop.
ticker.set_missed_tick_behavior(MissedTickBehavior::Skip);
+ let mut sync_state = SyncState::Idle;
+
loop {
tokio::select! {
_ = ticker.tick() => {
- do_sync_tick(
+ let settings = handle.settings().await;
+
+ // Skip periodic ticks if auto_sync is disabled AND we're not retrying
+ // a previous failure. Retries must continue regardless of auto_sync.
+ if !settings.auto_sync && sync_state == SyncState::Idle {
+ tracing::debug!("auto_sync disabled, skipping periodic sync tick");
+ continue;
+ }
+
+ sync_state = do_sync_tick(
&handle,
&history_store,
&alias_store,
&var_store,
&mut ticker,
max_interval,
+ &settings,
).await;
}
cmd = cmd_rx.recv() => {
match cmd {
Some(SyncCommand::ForceSync) => {
tracing::info!("executing force sync");
- do_sync_tick(
+ let settings = handle.settings().await;
+ sync_state = do_sync_tick(
&handle,
&history_store,
&alias_store,
&var_store,
&mut ticker,
max_interval,
+ &settings,
).await;
}
Some(SyncCommand::Stop) | None => {
@@ -159,6 +185,8 @@ async fn sync_loop(handle: DaemonHandle, mut cmd_rx: mpsc::Receiver<SyncCommand>
}
/// Execute a single sync tick.
+///
+/// Returns the new sync state: `Idle` on success, `Retrying` on failure.
async fn do_sync_tick(
handle: &DaemonHandle,
history_store: &HistoryStore,
@@ -166,10 +194,8 @@ async fn do_sync_tick(
var_store: &VarStore,
ticker: &mut time::Interval,
max_interval: f64,
-) {
- // Clone settings since we need them across await points
- let settings = handle.settings().await.clone();
-
+ settings: &Settings,
+) -> SyncState {
tracing::info!("sync tick");
// Check if logged in
@@ -177,17 +203,17 @@ async fn do_sync_tick(
Ok(v) => v,
Err(e) => {
tracing::warn!("failed to check login status, skipping sync tick: {e}");
- return;
+ return SyncState::Idle;
}
};
if !logged_in {
tracing::debug!("not logged in, skipping sync tick");
- return;
+ return SyncState::Idle;
}
// Perform the sync
- let res = sync::sync(&settings, handle.store()).await;
+ let res = sync::sync(settings, handle.store()).await;
match res {
Err(e) => {
@@ -206,10 +232,16 @@ async fn do_sync_tick(
new_interval = max_interval;
}
- *ticker = time::interval(time::Duration::from_secs(new_interval as u64));
+ *ticker = time::interval_at(
+ tokio::time::Instant::now() + Duration::from_secs(new_interval as u64),
+ time::Duration::from_secs(new_interval as u64),
+ );
ticker.reset_after(time::Duration::from_secs(new_interval as u64));
+ ticker.set_missed_tick_behavior(MissedTickBehavior::Skip);
tracing::error!("backing off, next sync tick in {new_interval}");
+
+ SyncState::Retrying
}
Ok((uploaded_count, downloaded_records)) => {
tracing::info!(
@@ -245,13 +277,20 @@ async fn do_sync_tick(
// Reset backoff on success
if ticker.period().as_secs() != settings.daemon.sync_frequency {
- *ticker = time::interval(time::Duration::from_secs(settings.daemon.sync_frequency));
+ *ticker = time::interval_at(
+ tokio::time::Instant::now()
+ + Duration::from_secs(settings.daemon.sync_frequency),
+ time::Duration::from_secs(settings.daemon.sync_frequency),
+ );
+ ticker.set_missed_tick_behavior(MissedTickBehavior::Skip);
}
// Store sync time
if let Err(e) = Settings::save_sync_time().await {
tracing::error!("failed to save sync time: {e}");
}
+
+ SyncState::Idle
}
}
}
diff --git a/crates/atuin/src/command/client/init.rs b/crates/atuin/src/command/client/init.rs
index 99fce33d..00c6c2fc 100644
--- a/crates/atuin/src/command/client/init.rs
+++ b/crates/atuin/src/command/client/init.rs
@@ -26,6 +26,10 @@ pub struct Cmd {
/// Disable the binding of the Up Arrow key to atuin
#[clap(long)]
disable_up_arrow: bool,
+
+ /// Disable the binding of ? to Atuin AI
+ #[clap(long)]
+ disable_ai: bool,
}
#[derive(Clone, Copy, ValueEnum, Debug)]
@@ -97,13 +101,28 @@ $env.config = (
fn static_init(&self, tmux: &Tmux) {
match self.shell {
Shell::Zsh => {
- zsh::init_static(self.disable_up_arrow, self.disable_ctrl_r, tmux);
+ zsh::init_static(
+ self.disable_up_arrow,
+ self.disable_ctrl_r,
+ self.disable_ai,
+ tmux,
+ );
}
Shell::Bash => {
- bash::init_static(self.disable_up_arrow, self.disable_ctrl_r, tmux);
+ bash::init_static(
+ self.disable_up_arrow,
+ self.disable_ctrl_r,
+ self.disable_ai,
+ tmux,
+ );
}
Shell::Fish => {
- fish::init_static(self.disable_up_arrow, self.disable_ctrl_r, tmux);
+ fish::init_static(
+ self.disable_up_arrow,
+ self.disable_ctrl_r,
+ self.disable_ai,
+ tmux,
+ );
}
Shell::Nu => {
self.init_nu(tmux);
@@ -136,6 +155,7 @@ $env.config = (
var_store,
self.disable_up_arrow,
self.disable_ctrl_r,
+ self.disable_ai,
&settings.tmux,
)
.await?;
@@ -146,6 +166,7 @@ $env.config = (
var_store,
self.disable_up_arrow,
self.disable_ctrl_r,
+ self.disable_ai,
&settings.tmux,
)
.await?;
@@ -156,6 +177,7 @@ $env.config = (
var_store,
self.disable_up_arrow,
self.disable_ctrl_r,
+ self.disable_ai,
&settings.tmux,
)
.await?;
diff --git a/crates/atuin/src/command/client/init/bash.rs b/crates/atuin/src/command/client/init/bash.rs
index 8e1349ed..745c239a 100644
--- a/crates/atuin/src/command/client/init/bash.rs
+++ b/crates/atuin/src/command/client/init/bash.rs
@@ -11,7 +11,7 @@ fn print_tmux_config(tmux: &Tmux) {
}
}
-pub fn init_static(disable_up_arrow: bool, disable_ctrl_r: bool, tmux: &Tmux) {
+pub fn init_static(disable_up_arrow: bool, disable_ctrl_r: bool, disable_ai: bool, tmux: &Tmux) {
let base = include_str!("../../../shell/atuin.bash");
let (bind_ctrl_r, bind_up_arrow) = if std::env::var("ATUIN_NOBIND").is_ok() {
@@ -24,6 +24,12 @@ pub fn init_static(disable_up_arrow: bool, disable_ctrl_r: bool, tmux: &Tmux) {
println!("__atuin_bind_ctrl_r={bind_ctrl_r}");
println!("__atuin_bind_up_arrow={bind_up_arrow}");
println!("{base}");
+
+ #[cfg(feature = "ai")]
+ if !disable_ai {
+ let bind_ai = atuin_ai::commands::init::generate_bash_integration();
+ println!("{bind_ai}");
+ }
}
pub async fn init(
@@ -31,9 +37,10 @@ pub async fn init(
vars: VarStore,
disable_up_arrow: bool,
disable_ctrl_r: bool,
+ disable_ai: bool,
tmux: &Tmux,
) -> Result<()> {
- init_static(disable_up_arrow, disable_ctrl_r, tmux);
+ init_static(disable_up_arrow, disable_ctrl_r, disable_ai, tmux);
let aliases = atuin_dotfiles::shell::bash::alias_config(&aliases).await;
let vars = atuin_dotfiles::shell::bash::var_config(&vars).await;
diff --git a/crates/atuin/src/command/client/init/fish.rs b/crates/atuin/src/command/client/init/fish.rs
index 4a1cda6f..6d6c8c23 100644
--- a/crates/atuin/src/command/client/init/fish.rs
+++ b/crates/atuin/src/command/client/init/fish.rs
@@ -37,7 +37,7 @@ fn print_bindings(
println!("{indent}end");
}
-pub fn init_static(disable_up_arrow: bool, disable_ctrl_r: bool, tmux: &Tmux) {
+pub fn init_static(disable_up_arrow: bool, disable_ctrl_r: bool, disable_ai: bool, tmux: &Tmux) {
let indent = " ".repeat(4);
let base = include_str!("../../../shell/atuin.fish");
@@ -84,6 +84,12 @@ pub fn init_static(disable_up_arrow: bool, disable_ctrl_r: bool, tmux: &Tmux) {
);
println!("end");
+
+ #[cfg(feature = "ai")]
+ if !disable_ai {
+ let bind_ai = atuin_ai::commands::init::generate_fish_integration();
+ println!("{bind_ai}");
+ }
}
}
@@ -92,9 +98,10 @@ pub async fn init(
vars: VarStore,
disable_up_arrow: bool,
disable_ctrl_r: bool,
+ disable_ai: bool,
tmux: &Tmux,
) -> Result<()> {
- init_static(disable_up_arrow, disable_ctrl_r, tmux);
+ init_static(disable_up_arrow, disable_ctrl_r, disable_ai, tmux);
let aliases = atuin_dotfiles::shell::fish::alias_config(&aliases).await;
let vars = atuin_dotfiles::shell::fish::var_config(&vars).await;
diff --git a/crates/atuin/src/command/client/init/zsh.rs b/crates/atuin/src/command/client/init/zsh.rs
index 0ab832fe..5d588aa0 100644
--- a/crates/atuin/src/command/client/init/zsh.rs
+++ b/crates/atuin/src/command/client/init/zsh.rs
@@ -11,7 +11,7 @@ fn print_tmux_config(tmux: &Tmux) {
}
}
-pub fn init_static(disable_up_arrow: bool, disable_ctrl_r: bool, tmux: &Tmux) {
+pub fn init_static(disable_up_arrow: bool, disable_ctrl_r: bool, disable_ai: bool, tmux: &Tmux) {
let base = include_str!("../../../shell/atuin.zsh");
print_tmux_config(tmux);
@@ -36,6 +36,13 @@ bindkey -M vicmd 'k' atuin-up-search-vicmd";
if !disable_up_arrow {
println!("{BIND_UP_ARROW}");
}
+
+ #[cfg(feature = "ai")]
+ if !disable_ai {
+ let bind_ai = atuin_ai::commands::init::generate_zsh_integration();
+
+ println!("{bind_ai}");
+ }
}
}
@@ -44,9 +51,10 @@ pub async fn init(
vars: VarStore,
disable_up_arrow: bool,
disable_ctrl_r: bool,
+ disable_ai: bool,
tmux: &Tmux,
) -> Result<()> {
- init_static(disable_up_arrow, disable_ctrl_r, tmux);
+ init_static(disable_up_arrow, disable_ctrl_r, disable_ai, tmux);
let aliases = atuin_dotfiles::shell::zsh::alias_config(&aliases).await;
let vars = atuin_dotfiles::shell::zsh::var_config(&vars).await;
diff --git a/docs/docs/ai/introduction.md b/docs/docs/ai/introduction.md
index 0c235428..1c154992 100644
--- a/docs/docs/ai/introduction.md
+++ b/docs/docs/ai/introduction.md
@@ -1,18 +1,16 @@
# Atuin AI
-Atuin AI is a subcommand that enables shell command generation and other information lookup via an LLM directly from your terminal. It is completely opt-in, and will not change the behavior of Atuin at all if you choose not to use it.
+Atuin AI is a subcommand that enables shell command generation and other information lookup via an LLM directly from your terminal.
Atuin AI requires an account on [Atuin Hub](https://hub.atuin.sh/), and you'll be prompted to login upon first use of the binary.
## Getting Started
-Atuin AI currently supports zsh, bash, and fish shells. To get started, add the following to your shell's initialization file:
+Atuin AI currently supports zsh, bash, and fish shells. Your shell's usual `atuin init` call will automatically bind the question mark key to the Atuin AI UI (only when the prompt is empty).
-```bash
-eval "$(atuin ai init)"
-```
+!!! note "Disabling Atuin AI"
-Once you've set it up and restarted your shell, you can invoke Atuin AI by pressing question mark (`?`) on an empty terminal line.
+ You can disable the default question mark key binding by passing `--disable-ai` to your shell's `atuin init` call.
## Settings
diff --git a/docs/docs/faq.md b/docs/docs/faq.md
index f5e867e7..32338ec9 100644
--- a/docs/docs/faq.md
+++ b/docs/docs/faq.md
@@ -37,6 +37,18 @@ eval "$(atuin init zsh --disable-up-arrow)"
See [key binding](../configuration/key-binding.md) for more
+## How do I remove the default question mark binding for Atuin AI?
+
+Open your shell config file, find the line containing `atuin init`.
+
+Add `--disable-ai`
+
+EG:
+
+```
+eval "$(atuin init zsh --disable-ai)"
+```
+
## How do I edit a command instead of running it immediately?
Press tab! By default, enter will execute a command, and tab will insert it ready for editing.