From fd188da879d977ca847f10708c39dd4801a204c4 Mon Sep 17 00:00:00 2001 From: Michelle Tilley Date: Tue, 14 Apr 2026 16:03:08 -0700 Subject: feat: Allow resuming previous AI sessions (#3407) This PR introduces session continuation to Atuin AI. * Conversations with Atuin AI are stored in a local SQLite database * Upon startup, Atuin AI tries to find a session to resume based on its directory/workspace and the time since the last event * If found, Atuin AI will show a note that the session has been resumed, and an event is added to help the LLM know where the invocation boundaries are * If not, Atuin AI will create a new conversation * The user can create a new conversation with `/new` * The new setting `ai.session_continue_minutes`, which defaults to `60`, controls how old the last event in a session can be before it's no longer considered for automatic resuming. image ## Architecture A new `SessionService` trait defines an API contract for a service that can manage session data. `LocalSessionService` implements this, with `DaemonSessionService` a possible future extension point. `SessionManager` owns a `dyn SessionService` and delegates as appropriate. --- .../src/tui/components/session_continue.rs | 49 ++++++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 crates/atuin-ai/src/tui/components/session_continue.rs (limited to 'crates/atuin-ai/src/tui/components/session_continue.rs') diff --git a/crates/atuin-ai/src/tui/components/session_continue.rs b/crates/atuin-ai/src/tui/components/session_continue.rs new file mode 100644 index 00000000..bfbfb191 --- /dev/null +++ b/crates/atuin-ai/src/tui/components/session_continue.rs @@ -0,0 +1,49 @@ +use chrono_humanize::HumanTime; +use eye_declare::{Elements, Hooks, Span, Text, component, element, props}; +use ratatui::style::{Color, Modifier, Style}; + +#[props] +pub(crate) struct SessionContinue { + pub continued_at: Option>, +} + +#[derive(Default)] +pub(crate) struct SessionContinueState { + /// Frozen on mount so the label doesn't change on every render. + label: Option, +} + +#[component(props = SessionContinue, state = SessionContinueState)] +fn session_continue( + _props: &SessionContinue, + state: &SessionContinueState, + hooks: &mut Hooks, +) -> Elements { + hooks.use_mount(|props, state| { + state.label = Some(match props.continued_at { + Some(t) => { + let human = HumanTime::from(t - chrono::Utc::now()); + format!( + " Continuing previous session (last active {human}) - type /new to start a new session" + ) + } + None => { + " Continuing previous session - type /new to start a new session".to_string() + } + }); + }); + + let resume_label = state + .label + .as_deref() + .unwrap_or(" Continuing previous session - type /new to start a new session"); + + element! { + Text { + Span( + text: resume_label, + style: Style::default().fg(Color::DarkGray).add_modifier(Modifier::ITALIC), + ) + } + } +} -- cgit v1.3.1