use super::*; impl App { pub(super) fn prepare_project(&mut self, path: PathBuf) { let Ok(path) = fs::canonicalize(path) else { self.error = Some("The selected folder is no longer available.".into()); return; }; let Some(path_text) = path.to_str() else { self.error = Some("The selected folder path is not valid UTF-8.".into()); return; }; if self .projects .iter() .any(|item| item.project.path == path_text) { self.error = Some("That project is already in the sidebar.".into()); return; } self.project_name_input = path .file_name() .and_then(|name| name.to_str()) .unwrap_or("Project") .to_owned(); self.pending_project_path = Some(path); self.error = None; } pub(super) fn create_project(&mut self) { let name = self.project_name_input.trim(); if name.is_empty() { self.error = Some("Project name cannot be empty.".into()); return; } let Some(path) = &self.pending_project_path else { return; }; let Some(path) = path.to_str() else { self.error = Some("The selected folder path is not valid UTF-8.".into()); return; }; let Some(database) = &mut self.database else { return; }; match database.create_project(name, path) { Ok(project) => { self.remember_project(project.id); self.selected_session = None; self.pending_project_path = None; self.project_name_input.clear(); self.error = None; self.reload_projects(); } Err(error) => self.error = Some(error), } } /// Opens an unsaved session on a project and selects it. Nothing reaches the /// database until the first chat turn is stored by [`App::persist_session`]. pub(super) fn create_session(&mut self, project_id: i32) { if self.generating { self.error = Some("Stop the active generation before creating a session.".into()); return; } if self.database.is_none() { return; } let title = draft_title(&self.projects, project_id); self.drafts.entry(project_id).or_insert(title); self.remember_project(project_id); self.selected_session = None; self.conversation.clear(); self.composer.clear(); self.context_used = 0; self.context_limit = self.preferences.context_tokens.max(0) as u32; self.tokens_per_second = None; self.error = None; } pub(super) fn discard_session(&mut self, project_id: i32) { if self.drafts.remove(&project_id).is_some() && self.draft_selected(project_id) { self.conversation.clear(); self.composer.clear(); self.context_used = 0; self.tokens_per_second = None; } self.error = None; } /// Turns the draft on `project_id` into a real session row. Called when the /// first chat turn is about to be written, never before. pub(super) fn persist_session(&mut self, project_id: i32) -> Result { let title = self .drafts .get(&project_id) .cloned() .unwrap_or_else(|| draft_title(&self.projects, project_id)); let database = self .database .as_mut() .ok_or_else(|| "The project database is unavailable.".to_owned())?; let session = database.create_session(project_id, &title)?; self.drafts.remove(&project_id); self.remember_project(project_id); self.selected_session = Some(session.id); self.reload_projects(); Ok(session.id) } /// Selects a project and stores it as the one to reopen on the next launch. pub(super) fn remember_project(&mut self, project_id: i32) { self.selected_project = Some(project_id); if self.preferences.last_project_id == Some(project_id) { return; } self.preferences.last_project_id = Some(project_id); if let Some(database) = &mut self.database && let Err(error) = database.set_last_project(Some(project_id)) { self.error = Some(error); } } /// True when the sidebar row for this project's draft is the active chat. pub(super) fn draft_selected(&self, project_id: i32) -> bool { self.selected_project == Some(project_id) && self.selected_session.is_none() } pub(super) fn reload_projects(&mut self) { if let Some(database) = &mut self.database { match database.load_projects() { Ok(projects) => self.projects = projects, Err(error) => self.error = Some(error), } } } } pub(super) fn draft_title(projects: &[ProjectWithSessions], project_id: i32) -> String { let stored = projects .iter() .find(|item| item.project.id == project_id) .map_or(0, |item| item.sessions.len()); format!("Session {}", stored + 1) } #[cfg(test)] mod tests { use super::*; use crate::database::{Project, Session}; #[test] fn session_states_are_exclusive_and_ordered() { assert_eq!(SessionState::from_id("pinned"), Some(SessionState::Pinned)); assert_eq!(SessionState::from_id("nonsense"), None); assert_eq!(SessionState::default(), SessionState::Normal); let mut ranks = [ SessionState::Archived, SessionState::Normal, SessionState::Pinned, ]; ranks.sort_by_key(|state| state.rank()); assert_eq!( ranks, [ SessionState::Pinned, SessionState::Normal, SessionState::Archived ] ); } #[test] fn draft_titles_follow_the_stored_session_count() { let projects = vec![ ProjectWithSessions { project: project(1, "First"), sessions: vec![session(1, 1), session(2, 1)], }, ProjectWithSessions { project: project(2, "Second"), sessions: Vec::new(), }, ]; assert_eq!(draft_title(&projects, 1), "Session 3"); assert_eq!(draft_title(&projects, 2), "Session 1"); assert_eq!(draft_title(&projects, 99), "Session 1"); } fn project(id: i32, name: &str) -> Project { Project { id, name: name.into(), path: format!("/tmp/{name}"), collapsed: false, } } fn session(id: i32, project_id: i32) -> Session { Session::fixture( id, project_id, &format!("Session {id}"), SessionState::Normal, ) } }