Order sessions by recent use
This commit is contained in:
1
migrations/20260727190000_add_session_last_used/down.sql
Normal file
1
migrations/20260727190000_add_session_last_used/down.sql
Normal file
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE sessions DROP COLUMN last_used;
|
||||||
4
migrations/20260727190000_add_session_last_used/up.sql
Normal file
4
migrations/20260727190000_add_session_last_used/up.sql
Normal 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();
|
||||||
@@ -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) => {
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user