211 lines
6.9 KiB
Rust
211 lines
6.9 KiB
Rust
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<i32, String> {
|
|
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,
|
|
)
|
|
}
|
|
}
|