Add session persistence and browser functionality
Some checks failed
Release / Build aarch64-unknown-linux-gnu (push) Has been cancelled
Release / Build aarch64-unknown-linux-musl (push) Has been cancelled
Release / Build armv7-unknown-linux-gnueabihf (push) Has been cancelled
Release / Build armv7-unknown-linux-musleabihf (push) Has been cancelled
Release / Build x86_64-unknown-linux-gnu (push) Has been cancelled
Release / Build x86_64-unknown-linux-musl (push) Has been cancelled
Release / Build aarch64-apple-darwin (push) Has been cancelled
Release / Build x86_64-apple-darwin (push) Has been cancelled
Release / Build aarch64-pc-windows-msvc (push) Has been cancelled
Release / Build x86_64-pc-windows-msvc (push) Has been cancelled
Release / Create Release (push) Has been cancelled

- Implement `StorageManager` for saving, loading, and managing sessions.
- Introduce platform-specific session directories for persistence.
- Add session browser UI for listing, loading, and deleting saved sessions.
- Enable AI-generated descriptions for session summaries.
- Update configurations to support storage settings and description generation.
- Extend README and tests to document and validate new functionality.
This commit is contained in:
2025-10-02 01:33:49 +02:00
parent 1ad6cb8b3f
commit 6b8774f0aa
12 changed files with 818 additions and 26 deletions

View File

@@ -1,6 +1,7 @@
use anyhow::Result;
use owlen_core::{
session::{SessionController, SessionOutcome},
storage::{SessionMeta, StorageManager},
types::{ChatParameters, ChatResponse, Conversation, ModelInfo, Role},
ui::{AppState, AutoScroll, FocusedPanel, InputMode},
};
@@ -55,6 +56,10 @@ pub struct ChatApp {
focused_panel: FocusedPanel, // Currently focused panel for scrolling
chat_cursor: (usize, usize), // Cursor position in Chat panel (row, col)
thinking_cursor: (usize, usize), // Cursor position in Thinking panel (row, col)
storage: StorageManager, // Storage manager for session persistence
saved_sessions: Vec<SessionMeta>, // Cached list of saved sessions
selected_session_index: usize, // Index of selected session in browser
save_name_buffer: String, // Buffer for entering save name
}
impl ChatApp {
@@ -63,6 +68,12 @@ impl ChatApp {
let mut textarea = TextArea::default();
configure_textarea_defaults(&mut textarea);
let storage = StorageManager::new().unwrap_or_else(|e| {
eprintln!("Warning: Failed to initialize storage: {}", e);
StorageManager::with_directory(std::path::PathBuf::from("/tmp/owlen_sessions"))
.expect("Failed to create fallback storage")
});
let app = Self {
controller,
mode: InputMode::Normal,
@@ -93,6 +104,10 @@ impl ChatApp {
focused_panel: FocusedPanel::Input,
chat_cursor: (0, 0),
thinking_cursor: (0, 0),
storage,
saved_sessions: Vec::new(),
selected_session_index: 0,
save_name_buffer: String::new(),
};
(app, session_rx)
@@ -209,6 +224,14 @@ impl ChatApp {
self.thinking_cursor
}
pub fn saved_sessions(&self) -> &[SessionMeta] {
&self.saved_sessions
}
pub fn selected_session_index(&self) -> usize {
self.selected_session_index
}
pub fn cycle_focus_forward(&mut self) {
self.focused_panel = match self.focused_panel {
FocusedPanel::Chat => {
@@ -976,7 +999,11 @@ impl ChatApp {
(KeyCode::Enter, _) => {
// Execute command
let cmd = self.command_buffer.trim();
match cmd {
let parts: Vec<&str> = cmd.split_whitespace().collect();
let command = parts.first().copied().unwrap_or("");
let args = &parts[1..];
match command {
"q" | "quit" => {
return Ok(AppState::Quit);
}
@@ -984,9 +1011,65 @@ impl ChatApp {
self.controller.clear();
self.status = "Conversation cleared".to_string();
}
"w" | "write" => {
// Could implement saving conversation here
self.status = "Conversation saved".to_string();
"w" | "write" | "save" => {
// Save current conversation with AI-generated description
let name = if !args.is_empty() {
Some(args.join(" "))
} else {
None
};
// Generate description if enabled in config
let description = if self.controller.config().storage.generate_descriptions {
self.status = "Generating description...".to_string();
match self.controller.generate_conversation_description().await {
Ok(desc) => Some(desc),
Err(_) => None,
}
} else {
None
};
// Save the conversation with description
match self.controller.conversation_mut().save_active_with_description(&self.storage, name.clone(), description) {
Ok(path) => {
self.status = format!("Session saved: {}", path.display());
self.error = None;
}
Err(e) => {
self.error = Some(format!("Failed to save session: {}", e));
}
}
}
"load" | "open" => {
// Load saved sessions and enter browser mode
match self.storage.list_sessions() {
Ok(sessions) => {
self.saved_sessions = sessions;
self.selected_session_index = 0;
self.mode = InputMode::SessionBrowser;
self.command_buffer.clear();
return Ok(AppState::Running);
}
Err(e) => {
self.error = Some(format!("Failed to list sessions: {}", e));
}
}
}
"sessions" | "ls" => {
// List saved sessions
match self.storage.list_sessions() {
Ok(sessions) => {
self.saved_sessions = sessions;
self.selected_session_index = 0;
self.mode = InputMode::SessionBrowser;
self.command_buffer.clear();
return Ok(AppState::Running);
}
Err(e) => {
self.error = Some(format!("Failed to list sessions: {}", e));
}
}
}
"h" | "help" => {
self.mode = InputMode::Help;
@@ -1097,6 +1180,56 @@ impl ChatApp {
}
_ => {}
},
InputMode::SessionBrowser => match key.code {
KeyCode::Esc => {
self.mode = InputMode::Normal;
}
KeyCode::Enter => {
// Load selected session
if let Some(session) = self.saved_sessions.get(self.selected_session_index) {
match self.controller.conversation_mut().load_from_disk(&self.storage, &session.path) {
Ok(_) => {
self.status = format!("Loaded session: {}", session.name.as_deref().unwrap_or("Unnamed"));
self.error = None;
// Update thinking panel
self.update_thinking_from_last_message();
}
Err(e) => {
self.error = Some(format!("Failed to load session: {}", e));
}
}
}
self.mode = InputMode::Normal;
}
KeyCode::Up | KeyCode::Char('k') => {
if self.selected_session_index > 0 {
self.selected_session_index -= 1;
}
}
KeyCode::Down | KeyCode::Char('j') => {
if self.selected_session_index + 1 < self.saved_sessions.len() {
self.selected_session_index += 1;
}
}
KeyCode::Char('d') => {
// Delete selected session
if let Some(session) = self.saved_sessions.get(self.selected_session_index) {
match self.storage.delete_session(&session.path) {
Ok(_) => {
self.saved_sessions.remove(self.selected_session_index);
if self.selected_session_index >= self.saved_sessions.len() && !self.saved_sessions.is_empty() {
self.selected_session_index = self.saved_sessions.len() - 1;
}
self.status = "Session deleted".to_string();
}
Err(e) => {
self.error = Some(format!("Failed to delete session: {}", e));
}
}
}
}
_ => {}
},
},
_ => {}
}
@@ -1342,7 +1475,7 @@ impl ChatApp {
stream,
}) => {
// Step 3: Model loaded, now generating response
self.status = "Generating response...".to_string();
self.status = format!("Model loaded. Generating response... (streaming)");
self.spawn_stream(response_id, stream);
match self.controller.mark_stream_placeholder(response_id, "") {