From 605a9170b05a8e1f473943abbc26d98ecc706b60 Mon Sep 17 00:00:00 2001 From: Alex Clarke Date: Thu, 2 Jul 2026 16:35:10 -0600 Subject: [PATCH] feat: Replay session output when a user re-enters a session so all output can be seen again --- src/client/message.rs | 7 +++ src/config/session.rs | 8 +++ src/repl/mod.rs | 110 ++++++++++++++++++++++++++++++++++++++++-- 3 files changed, 121 insertions(+), 4 deletions(-) diff --git a/src/client/message.rs b/src/client/message.rs index f73b1b9..d8d24bf 100644 --- a/src/client/message.rs +++ b/src/client/message.rs @@ -133,6 +133,13 @@ impl MessageContent { } } + pub fn as_text(&self) -> Option<&str> { + match self { + MessageContent::Text(text) => Some(text), + _ => None, + } + } + pub fn merge_prompt(&mut self, replace_fn: impl Fn(&str) -> String) { match self { MessageContent::Text(text) => *text = replace_fn(text), diff --git a/src/config/session.rs b/src/config/session.rs index b2be303..d82898d 100644 --- a/src/config/session.rs +++ b/src/config/session.rs @@ -163,6 +163,14 @@ impl Session { self.messages.is_empty() && self.compressed_messages.is_empty() } + pub fn messages(&self) -> &[Message] { + &self.messages + } + + pub fn compressed_messages(&self) -> &[Message] { + &self.compressed_messages + } + pub fn name(&self) -> &str { &self.name } diff --git a/src/repl/mod.rs b/src/repl/mod.rs index dec172b..5c41613 100644 --- a/src/repl/mod.rs +++ b/src/repl/mod.rs @@ -6,7 +6,10 @@ use self::completer::ReplCompleter; use self::highlighter::ReplHighlighter; use self::prompt::ReplPrompt; -use crate::client::{call_chat_completions, call_chat_completions_streaming, init_client, oauth}; +use crate::client::{ + Message, MessageRole, call_chat_completions, call_chat_completions_streaming, init_client, + oauth, +}; use crate::config::{ AgentVariables, AppConfig, AssertState, Input, LastMessage, RequestContext, StateFlags, macro_execute, @@ -29,9 +32,9 @@ use log::warn; use parking_lot::RwLock; use reedline::CursorConfig; use reedline::{ - ColumnarMenu, EditCommand, EditMode, Emacs, KeyCode, KeyModifiers, Keybindings, Reedline, - ReedlineEvent, ReedlineMenu, ValidationResult, Validator, Vi, default_emacs_keybindings, - default_vi_insert_keybindings, default_vi_normal_keybindings, + ColumnarMenu, EditCommand, EditMode, Emacs, FileBackedHistory, KeyCode, KeyModifiers, + Keybindings, Reedline, ReedlineEvent, ReedlineMenu, ValidationResult, Validator, Vi, + default_emacs_keybindings, default_vi_insert_keybindings, default_vi_normal_keybindings, }; use reedline::{MenuBuilder, Signal}; use std::sync::LazyLock; @@ -318,6 +321,58 @@ Type ".help" for additional help. } } + { + let (messages_snapshot, compressed_count) = { + let ctx = self.ctx.read(); + if let Some(session) = &ctx.session { + let msgs: Vec = session + .messages() + .iter() + .filter(|m| !m.role.is_system()) + .cloned() + .collect(); + let compressed = session.compressed_messages().len(); + (msgs, compressed) + } else { + (vec![], 0) + } + }; + + if !messages_snapshot.is_empty() || compressed_count > 0 { + let app = Arc::clone(&self.ctx.read().app.config); + if compressed_count > 0 { + println!( + "{}", + dimmed_text(&format!( + "({compressed_count} earlier messages not shown; compressed for context)" + )) + ); + println!(); + } + + for message in &messages_snapshot { + match message.role { + MessageRole::User => { + if let Some(text) = message.content.as_text() { + println!("{}", dimmed_text("You:")); + println!("{text}"); + println!(); + } + } + MessageRole::Assistant => { + if let Some(text) = message.content.as_text() { + app.print_markdown(text)?; + println!(); + } + } + _ => {} + } + } + println!("{}", dimmed_text("─── ↑ previous conversation ───")); + println!(); + } + } + loop { if self.abort_signal.aborted_ctrld() { break; @@ -393,6 +448,13 @@ Type ".help" for additional help. editor = editor.with_buffer_editor(command, temp_file); } + if app.save_shell_history { + let history_path = paths::config_dir().join("repl_history"); + if let Ok(history) = FileBackedHistory::with_file(1000, history_path) { + editor = editor.with_history(Box::new(history)); + } + } + Ok(editor) } @@ -684,6 +746,46 @@ pub async fn run_repl_command( session.set_autonaming(false); } } + if let Some(session) = &ctx.session { + let messages_snapshot: Vec = session + .messages() + .iter() + .filter(|m| !m.role.is_system()) + .cloned() + .collect(); + let compressed_count = session.compressed_messages().len(); + if !messages_snapshot.is_empty() || compressed_count > 0 { + if compressed_count > 0 { + println!( + "{}", + dimmed_text(&format!( + "({compressed_count} earlier messages not shown — compressed for context)" + )) + ); + println!(); + } + for message in &messages_snapshot { + match message.role { + MessageRole::User => { + if let Some(text) = message.content.as_text() { + println!("{}", dimmed_text("You:")); + println!("{text}"); + println!(); + } + } + MessageRole::Assistant => { + if let Some(text) = message.content.as_text() { + app.print_markdown(text)?; + println!(); + } + } + _ => {} + } + } + println!("{}", dimmed_text("─── ↑ previous conversation ───")); + println!(); + } + } } ".install" => { let trimmed = args.map(str::trim).unwrap_or("");