From 4d81ec537f91ebed0d5498a36596a516dbf7d26b Mon Sep 17 00:00:00 2001 From: Ellie Huxtable Date: Sat, 11 Apr 2026 05:26:52 +0100 Subject: feat: track coding agent shell usage (#3388) https://github.com/user-attachments/assets/7868c7a4-6a91-4c93-ac6a-e8665cf1f799 ## 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 --- crates/atuin-client/src/database.rs | 39 +++++++++++++++++++++++++++++++++++++ crates/atuin-client/src/history.rs | 18 +++++++++++++++++ crates/atuin-client/src/settings.rs | 14 +++++++++++++ 3 files changed, 71 insertions(+) (limited to 'crates/atuin-client') diff --git a/crates/atuin-client/src/database.rs b/crates/atuin-client/src/database.rs index 7c63368d..75ef51c3 100644 --- a/crates/atuin-client/src/database.rs +++ b/crates/atuin-client/src/database.rs @@ -5,6 +5,7 @@ use std::{ time::Duration, }; +use crate::history::{AUTHOR_FILTER_ALL_AGENT, AUTHOR_FILTER_ALL_USER, KNOWN_AGENTS}; use async_trait::async_trait; use atuin_common::utils; use fs_err as fs; @@ -53,6 +54,8 @@ pub struct OptFilters { pub offset: Option, pub reverse: bool, pub include_duplicates: bool, + /// Author filter. Supports special values `$all-user` and `$all-agent`. + pub authors: Vec, } pub async fn current_context() -> eyre::Result { @@ -85,6 +88,38 @@ impl Context { } } +/// Each entry is OR'd: `$all-user` → NOT IN agents, `$all-agent` → IN agents, literal → exact match. +fn apply_author_filter(sql: &mut SqlBuilder, authors: &[String]) { + let mut conditions: Vec = Vec::new(); + let agent_list: String = KNOWN_AGENTS.iter().map(quote).join(", "); + let author_expr = "CASE \ + WHEN author IS NULL OR trim(author) = '' THEN \ + CASE \ + WHEN instr(hostname, ':') > 0 THEN substr(hostname, instr(hostname, ':') + 1) \ + ELSE hostname \ + END \ + ELSE author \ + END"; + + for author in authors { + match author.as_str() { + AUTHOR_FILTER_ALL_USER => { + conditions.push(format!("{author_expr} NOT IN ({agent_list})")); + } + AUTHOR_FILTER_ALL_AGENT => { + conditions.push(format!("{author_expr} IN ({agent_list})")); + } + literal => { + conditions.push(format!("{author_expr} = {}", quote(literal))); + } + } + } + + if !conditions.is_empty() { + sql.and_where(format!("({})", conditions.join(" OR "))); + } +} + fn get_session_start_time(session_id: &str) -> Option { if let Ok(uuid) = Uuid::parse_str(session_id) && let Some(timestamp) = uuid.get_timestamp() @@ -595,6 +630,10 @@ impl Database for Sqlite { .map(|after| sql.and_where_gt("timestamp", quote(after.unix_timestamp_nanos() as i64))) }); + if !filter_options.authors.is_empty() { + apply_author_filter(&mut sql, &filter_options.authors); + } + sql.and_where_is_null("deleted_at"); let query = sql.sql().expect("bug in search query. please report"); diff --git a/crates/atuin-client/src/history.rs b/crates/atuin-client/src/history.rs index a5adc233..996208d9 100644 --- a/crates/atuin-client/src/history.rs +++ b/crates/atuin-client/src/history.rs @@ -18,6 +18,24 @@ use time::OffsetDateTime; mod builder; pub mod store; +/// Known AI agent author values. Used to expand `$all-agent` and `$all-user` filters. +pub const KNOWN_AGENTS: &[&str] = &["claude-code", "codex", "copilot"]; +pub const AUTHOR_FILTER_ALL_USER: &str = "$all-user"; +pub const AUTHOR_FILTER_ALL_AGENT: &str = "$all-agent"; + +pub fn is_known_agent(author: &str) -> bool { + KNOWN_AGENTS.contains(&author) +} + +pub fn author_matches_filters(author: &str, filters: &[String]) -> bool { + filters.is_empty() + || filters.iter().any(|filter| match filter.as_str() { + AUTHOR_FILTER_ALL_USER => !is_known_agent(author), + AUTHOR_FILTER_ALL_AGENT => is_known_agent(author), + literal => author == literal, + }) +} + pub(crate) const HISTORY_VERSION_V0: &str = "v0"; pub(crate) const HISTORY_VERSION_V1: &str = "v1"; const HISTORY_RECORD_VERSION_V0: u16 = 0; diff --git a/crates/atuin-client/src/settings.rs b/crates/atuin-client/src/settings.rs index 25c3bd65..5944de59 100644 --- a/crates/atuin-client/src/settings.rs +++ b/crates/atuin-client/src/settings.rs @@ -565,6 +565,13 @@ pub struct Search { /// The overall frecency score multiplier for the search index (default: 1.0). /// Applied after combining recency and frequency scores. pub frecency_score_multiplier: f64, + + /// Filter history by author. Special values: + /// - `$all-user`: any author that is NOT a known AI agent (default) + /// - `$all-agent`: any known AI agent author + /// - literal strings like "ellie", "claude-code" + #[serde(default = "Search::default_authors")] + pub authors: Vec, } #[derive(Clone, Debug, Deserialize, Serialize)] @@ -844,10 +851,17 @@ impl Default for Search { recency_score_multiplier: 1.0, frequency_score_multiplier: 1.0, frecency_score_multiplier: 1.0, + authors: Self::default_authors(), } } } +impl Search { + fn default_authors() -> Vec { + vec![crate::history::AUTHOR_FILTER_ALL_USER.to_string()] + } +} + impl Default for Tmux { fn default() -> Self { Self { -- cgit v1.3.1