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

@@ -0,0 +1 @@
ALTER TABLE sessions DROP COLUMN last_used;

View File

@@ -0,0 +1,4 @@
ALTER TABLE sessions ADD COLUMN last_used BIGINT NOT NULL DEFAULT 0 CHECK (last_used >= 0);
UPDATE sessions
SET last_used = unixepoch();

View File

@@ -1481,6 +1481,10 @@ impl App {
}); });
match loaded { match loaded {
Ok((messages, a2ui)) => { Ok((messages, a2ui)) => {
if let Err(error) = database.touch_session(session_id) {
self.error = Some(format!("Could not update the session: {error}"));
return Task::none();
}
self.conversation = messages.into_iter().map(ChatMessage::from).collect(); self.conversation = messages.into_iter().map(ChatMessage::from).collect();
generation::promote_legacy_turn_summaries(&mut self.conversation); generation::promote_legacy_turn_summaries(&mut self.conversation);
self.context_notice = None; self.context_notice = None;
@@ -1514,6 +1518,7 @@ impl App {
}; };
self.tokens_per_second = tokens_per_second; self.tokens_per_second = tokens_per_second;
self.error = restore_error; self.error = restore_error;
self.reload_projects();
return Task::batch([scroll_chat_to_end(), self.load_next_a2ui_image()]); return Task::batch([scroll_chat_to_end(), self.load_next_a2ui_image()]);
} }
Err(error) => { Err(error) => {

View File

@@ -3,6 +3,7 @@ use diesel_migrations::{EmbeddedMigrations, MigrationHarness, embed_migrations};
use std::collections::HashMap; use std::collections::HashMap;
use std::fs; use std::fs;
use std::path::Path; use std::path::Path;
use time::OffsetDateTime;
use crate::schema::{a2ui_messages, messages, projects, sessions}; use crate::schema::{a2ui_messages, messages, projects, sessions};
@@ -40,6 +41,7 @@ pub struct Session {
/// Raw column value; read it through [`Session::state`]. /// Raw column value; read it through [`Session::state`].
state: String, state: String,
pub compacted_summary: Option<String>, pub compacted_summary: Option<String>,
last_used: i64,
} }
impl Session { impl Session {
@@ -58,6 +60,7 @@ impl Session {
last_tokens_per_second: None, last_tokens_per_second: None,
state: state.as_id().to_owned(), state: state.as_id().to_owned(),
compacted_summary: None, compacted_summary: None,
last_used: 0,
} }
} }
} }
@@ -105,6 +108,7 @@ impl SessionState {
struct NewSession<'a> { struct NewSession<'a> {
project_id: i32, project_id: i32,
title: &'a str, title: &'a str,
last_used: i64,
} }
#[derive(Clone, Debug, Identifiable, Queryable, Selectable)] #[derive(Clone, Debug, Identifiable, Queryable, Selectable)]
@@ -207,10 +211,15 @@ impl Database {
.or_default() .or_default()
.push(session); .push(session);
} }
// Pinned first, then ordinary, then archived; creation order within each // Preserve sidebar groups while matching DS4's newest-used-first order.
// group. A stable sort keeps the id ordering the query established.
for sessions in sessions_by_project.values_mut() { 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 Ok(project_rows
@@ -275,12 +284,20 @@ impl Database {
pub fn create_session(&mut self, project_id: i32, title: &str) -> Result<Session, String> { pub fn create_session(&mut self, project_id: i32, title: &str) -> Result<Session, String> {
diesel::insert_into(sessions::table) 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()) .returning(Session::as_returning())
.get_result(&mut self.connection) .get_result(&mut self.connection)
.map_err(|error| error.to_string()) .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> { pub fn rename_session(&mut self, session_id: i32, title: &str) -> Result<(), String> {
let title = title.trim(); let title = title.trim();
if title.is_empty() { if title.is_empty() {
@@ -431,6 +448,7 @@ impl Database {
) -> Result<Vec<StoredMessage>, String> { ) -> Result<Vec<StoredMessage>, String> {
self.connection self.connection
.transaction(|connection| { .transaction(|connection| {
touch_session(connection, session_id)?;
let mut stored = Vec::with_capacity(system_messages.len() + 2); let mut stored = Vec::with_capacity(system_messages.len() + 2);
for content in system_messages { for content in system_messages {
stored.push( stored.push(
@@ -497,6 +515,7 @@ impl Database {
) -> Result<Vec<StoredMessage>, String> { ) -> Result<Vec<StoredMessage>, String> {
self.connection self.connection
.transaction(|connection| { .transaction(|connection| {
touch_session(connection, session_id)?;
let mut stored = Vec::with_capacity(system_messages.len() + 3); let mut stored = Vec::with_capacity(system_messages.len() + 3);
stored.push( stored.push(
diesel::insert_into(messages::table) 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)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
@@ -758,6 +787,46 @@ mod tests {
fs::remove_file(path).unwrap(); 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] #[test]
fn a2ui_dismissal_persists_a_fresh_surface_boundary() { fn a2ui_dismissal_persists_a_fresh_surface_boundary() {
let id = SystemTime::now() let id = SystemTime::now()

View File

@@ -47,6 +47,7 @@ diesel::table! {
last_tokens_per_second -> Nullable<Float>, last_tokens_per_second -> Nullable<Float>,
state -> Text, state -> Text,
compacted_summary -> Nullable<Text>, compacted_summary -> Nullable<Text>,
last_used -> BigInt,
} }
} }