Order sessions by recent use

This commit is contained in:
Georg Bauer
2026-07-27 18:53:39 +02:00
parent 1017087fc9
commit 7f14b6008a
5 changed files with 84 additions and 4 deletions

View File

@@ -3,6 +3,7 @@ use diesel_migrations::{EmbeddedMigrations, MigrationHarness, embed_migrations};
use std::collections::HashMap;
use std::fs;
use std::path::Path;
use time::OffsetDateTime;
use crate::schema::{a2ui_messages, messages, projects, sessions};
@@ -40,6 +41,7 @@ pub struct Session {
/// Raw column value; read it through [`Session::state`].
state: String,
pub compacted_summary: Option<String>,
last_used: i64,
}
impl Session {
@@ -58,6 +60,7 @@ impl Session {
last_tokens_per_second: None,
state: state.as_id().to_owned(),
compacted_summary: None,
last_used: 0,
}
}
}
@@ -105,6 +108,7 @@ impl SessionState {
struct NewSession<'a> {
project_id: i32,
title: &'a str,
last_used: i64,
}
#[derive(Clone, Debug, Identifiable, Queryable, Selectable)]
@@ -207,10 +211,15 @@ impl Database {
.or_default()
.push(session);
}
// Pinned first, then ordinary, then archived; creation order within each
// group. A stable sort keeps the id ordering the query established.
// Preserve sidebar groups while matching DS4's newest-used-first order.
for sessions in sessions_by_project.values_mut() {
sessions.sort_by_key(|session| session.state().rank());
sessions.sort_by(|left, right| {
left.state()
.rank()
.cmp(&right.state().rank())
.then_with(|| right.last_used.cmp(&left.last_used))
.then_with(|| right.id.cmp(&left.id))
});
}
Ok(project_rows
@@ -275,12 +284,20 @@ impl Database {
pub fn create_session(&mut self, project_id: i32, title: &str) -> Result<Session, String> {
diesel::insert_into(sessions::table)
.values(NewSession { project_id, title })
.values(NewSession {
project_id,
title,
last_used: OffsetDateTime::now_utc().unix_timestamp(),
})
.returning(Session::as_returning())
.get_result(&mut self.connection)
.map_err(|error| error.to_string())
}
pub fn touch_session(&mut self, session_id: i32) -> Result<(), String> {
touch_session(&mut self.connection, session_id).map_err(|error| error.to_string())
}
pub fn rename_session(&mut self, session_id: i32, title: &str) -> Result<(), String> {
let title = title.trim();
if title.is_empty() {
@@ -431,6 +448,7 @@ impl Database {
) -> Result<Vec<StoredMessage>, String> {
self.connection
.transaction(|connection| {
touch_session(connection, session_id)?;
let mut stored = Vec::with_capacity(system_messages.len() + 2);
for content in system_messages {
stored.push(
@@ -497,6 +515,7 @@ impl Database {
) -> Result<Vec<StoredMessage>, String> {
self.connection
.transaction(|connection| {
touch_session(connection, session_id)?;
let mut stored = Vec::with_capacity(system_messages.len() + 3);
stored.push(
diesel::insert_into(messages::table)
@@ -686,6 +705,16 @@ impl Database {
}
}
fn touch_session(
connection: &mut SqliteConnection,
session_id: i32,
) -> Result<(), diesel::result::Error> {
diesel::update(sessions::table.find(session_id))
.set(sessions::last_used.eq(OffsetDateTime::now_utc().unix_timestamp()))
.execute(connection)
.map(|_| ())
}
#[cfg(test)]
mod tests {
use super::*;
@@ -758,6 +787,46 @@ mod tests {
fs::remove_file(path).unwrap();
}
#[test]
fn sessions_are_ordered_by_recent_use_within_sidebar_groups() {
let id = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_nanos();
let path = std::env::temp_dir().join(format!("ds4-session-order-{id}.sqlite3"));
let mut database = Database::open(&path).unwrap();
let project = database
.create_project("DS4", "/tmp/ds4-session-order")
.unwrap();
let older = database.create_session(project.id, "Older").unwrap();
let newer = database.create_session(project.id, "Newer").unwrap();
let pinned = database.create_session(project.id, "Pinned").unwrap();
database
.set_session_state(pinned.id, SessionState::Pinned)
.unwrap();
diesel::update(sessions::table.find(older.id))
.set(sessions::last_used.eq(100_i64))
.execute(&mut database.connection)
.unwrap();
diesel::update(sessions::table.find(newer.id))
.set(sessions::last_used.eq(200_i64))
.execute(&mut database.connection)
.unwrap();
let loaded = database.load_projects().unwrap();
assert_eq!(
loaded[0]
.sessions
.iter()
.map(|session| session.id)
.collect::<Vec<_>>(),
[pinned.id, newer.id, older.id]
);
drop(database);
fs::remove_file(path).unwrap();
}
#[test]
fn a2ui_dismissal_persists_a_fresh_surface_boundary() {
let id = SystemTime::now()