Reopen the last project with a focused draft chat

This commit is contained in:
Georg Bauer
2026-07-25 23:52:39 +02:00
parent 4408ed8b26
commit e44680541d
8 changed files with 71 additions and 13 deletions

View File

@@ -24,7 +24,7 @@ use crate::settings::{
RuntimePreferences, SpeculativePreferences, SsdPreferences, SteeringPreferences,
StreamingCacheBudget,
};
use iced::widget::{markdown, scrollable};
use iced::widget::{markdown, scrollable, text_input};
use iced::{Size, Subscription, Task, keyboard, window};
use rfd::AsyncFileDialog;
use std::collections::{HashMap, HashSet, VecDeque};
@@ -208,6 +208,14 @@ impl App {
Err(error) => return Self::failed(error, main_window),
};
let context_limit = preferences.context_tokens.max(0) as u32;
// Reopen on the project we left, with a fresh draft chat.
let last_project = preferences
.last_project_id
.filter(|id| projects.iter().any(|item| item.project.id == *id));
let drafts = last_project
.map(|id| (id, projects::draft_title(&projects, id)))
.into_iter()
.collect();
let metrics =
Arc::new(Metrics::new(&application_support_path().join("kv-cache")));
let metrics_snapshot = metrics.snapshot();
@@ -228,9 +236,9 @@ impl App {
preference_draft,
preferences_open: false,
preference_error: None,
selected_project: None,
selected_project: last_project,
selected_session: None,
drafts: HashMap::new(),
drafts,
session_menu: None,
session_rename: None,
expanded_archives: HashSet::new(),
@@ -367,8 +375,7 @@ impl App {
return window::gain_focus(id);
}
}
Message::WindowOpened(id) =>
{
Message::WindowOpened(id) => {
#[cfg(target_os = "macos")]
if id == self.main_window && self._native_menu.is_none() {
match crate::native_menu::install() {
@@ -379,6 +386,9 @@ impl App {
}
}
}
if id == self.main_window && self.selected_project.is_some() {
return focus_composer();
}
}
Message::WindowClosed(id) => {
if id == self.main_window {
@@ -714,7 +724,7 @@ impl App {
Some("Stop the active generation before changing sessions.".into());
return Task::none();
}
self.selected_project = Some(project_id);
self.remember_project(project_id);
self.selected_session = None;
self.conversation.clear();
self.composer.clear();
@@ -747,6 +757,10 @@ impl App {
let _ = fs::remove_file(session_checkpoint_path(session_id));
}
self.drafts.remove(&project_id);
if self.preferences.last_project_id == Some(project_id) {
self.preferences.last_project_id = None;
let _ = database.set_last_project(None);
}
if self.selected_project == Some(project_id) {
self.selected_project = None;
self.selected_session = None;
@@ -761,7 +775,12 @@ impl App {
}
}
}
Message::CreateSession(project_id) => self.create_session(project_id),
Message::CreateSession(project_id) => {
self.create_session(project_id);
if self.draft_selected(project_id) {
return focus_composer();
}
}
Message::DiscardSession(project_id) => self.discard_session(project_id),
Message::OpenSessionMenu(session_id) => {
self.session_rename = None;
@@ -858,7 +877,7 @@ impl App {
Ok(messages) => {
self.conversation = messages.into_iter().map(ChatMessage::from).collect();
self.composer.clear();
self.selected_project = Some(project_id);
self.remember_project(project_id);
self.selected_session = Some(session_id);
let (used, limit, tokens_per_second) = saved_context.unwrap_or_default();
self.context_used = used.max(0) as u32;
@@ -1067,6 +1086,14 @@ pub(super) fn chat_scroll_id() -> scrollable::Id {
scrollable::Id::new("chat-transcript")
}
pub(super) fn composer_id() -> text_input::Id {
text_input::Id::new("chat-composer")
}
fn focus_composer() -> Task<Message> {
text_input::focus(composer_id())
}
fn scroll_chat_to_end() -> Task<Message> {
scrollable::snap_to(chat_scroll_id(), scrollable::RelativeOffset::END)
}

View File

@@ -47,7 +47,7 @@ impl App {
match database.create_project(name, path) {
Ok(project) => {
self.selected_project = Some(project.id);
self.remember_project(project.id);
self.selected_session = None;
self.pending_project_path = None;
self.project_name_input.clear();
@@ -70,7 +70,7 @@ impl App {
}
let title = draft_title(&self.projects, project_id);
self.drafts.entry(project_id).or_insert(title);
self.selected_project = Some(project_id);
self.remember_project(project_id);
self.selected_session = None;
self.conversation.clear();
self.composer.clear();
@@ -104,12 +104,26 @@ impl App {
.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.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()
@@ -125,7 +139,7 @@ impl App {
}
}
fn draft_title(projects: &[ProjectWithSessions], project_id: i32) -> String {
pub(super) fn draft_title(projects: &[ProjectWithSessions], project_id: i32) -> String {
let stored = projects
.iter()
.find(|item| item.project.id == project_id)

View File

@@ -7,7 +7,7 @@ use model_manager::{download_status_bar, format_bytes, format_duration};
use super::{
ActiveDownload, App, DetailTab, Message, MetricsPoint, ModelDownload, ModelOperation,
chat_scroll_id, models_path,
chat_scroll_id, composer_id, models_path,
};
use crate::database::{ProjectWithSessions, Session, SessionState};
use crate::model::{

View File

@@ -133,6 +133,7 @@ impl App {
}
}
let composer = text_input("Ask DS4Server anything…", &self.composer)
.id(composer_id())
.on_input(Message::ComposerChanged)
.on_submit(Message::SubmitPrompt)
.padding(12)

View File

@@ -55,6 +55,8 @@ pub struct AppPreferences {
pub endpoint_enabled: bool,
pub endpoint_cors: bool,
pub sidebar_collapsed: bool,
/// Project the app reopens on. Cleared when that project goes away.
pub last_project_id: Option<i32>,
}
impl Default for AppPreferences {
@@ -98,6 +100,7 @@ impl Default for AppPreferences {
endpoint_enabled: true,
endpoint_cors: false,
sidebar_collapsed: false,
last_project_id: None,
}
}
}
@@ -441,6 +444,14 @@ impl Database {
.map_err(|error| error.to_string())
}
pub fn set_last_project(&mut self, project_id: Option<i32>) -> Result<(), String> {
diesel::update(preferences::table.find(1))
.set(preferences::last_project_id.eq(project_id))
.execute(&mut self.connection)
.map(|_| ())
.map_err(|error| error.to_string())
}
#[allow(clippy::too_many_arguments)]
pub fn update_preferences(
&mut self,
@@ -851,6 +862,7 @@ mod tests {
assert_eq!(loaded[0].sessions[0].id, ordinary);
database.set_sidebar_collapsed(true).unwrap();
database.set_last_project(Some(project.id)).unwrap();
database.delete_project(project.id).unwrap();
assert!(database.load_projects().unwrap().is_empty());
@@ -859,6 +871,7 @@ mod tests {
let mut reopened = Database::open(&path).unwrap();
let preferences = reopened.load_preferences().unwrap();
assert!(preferences.sidebar_collapsed);
assert_eq!(preferences.last_project_id, Some(project.id));
assert_eq!(preferences.selected_model, "glm-5.2");
assert_eq!(preferences.idle_timeout_minutes, 30);
assert_eq!(preferences.endpoint_port, 4567);

View File

@@ -58,6 +58,7 @@ diesel::table! {
endpoint_enabled -> Bool,
endpoint_cors -> Bool,
sidebar_collapsed -> Bool,
last_project_id -> Nullable<Integer>,
}
}