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
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:
@@ -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, "▌") {
|
||||
|
||||
Reference in New Issue
Block a user