feat: better session creation handling

This commit is contained in:
Georg Bauer
2026-07-25 12:02:04 +02:00
parent 4292e0d3f9
commit 51c1b2a7dc
5 changed files with 182 additions and 58 deletions

View File

@@ -58,37 +58,61 @@ impl App {
}
}
pub(super) fn create_session(&mut self) {
/// 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;
}
let Some(project_id) = self.selected_project else {
self.error = Some("Select a project first.".into());
if self.database.is_none() {
return;
};
let default_number = self
.selected_project()
.map(|project| project.sessions.len() + 1)
.unwrap_or(1);
let title = format!("Session {default_number}");
let Some(database) = &mut self.database else {
return;
};
match database.create_session(project_id, &title) {
Ok(session) => {
self.selected_session = Some(session.id);
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;
self.reload_projects();
}
Err(error) => self.error = Some(error),
}
let title = draft_title(&self.projects, project_id);
self.drafts.entry(project_id).or_insert(title);
self.selected_project = Some(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.selected_project = Some(project_id);
self.selected_session = Some(session.id);
self.reload_projects();
Ok(session.id)
}
/// 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) {
@@ -100,3 +124,53 @@ impl App {
}
}
}
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 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}"),
}
}
fn session(id: i32, project_id: i32) -> Session {
Session {
id,
project_id,
title: format!("Session {id}"),
context_used: 0,
context_limit: 0,
last_tokens_per_second: None,
}
}
}