feat(windows): persistance et restauration des fenêtres au redémarrage (#40)
Persiste l'état des fenêtres (layout/position) et le restaure au relancement de l'application. Découpage hexagonal : - domaine : modèle et port d'état des fenêtres (layout, ports) - application : use cases de persistance/restauration - infrastructure : store window_state (adapter de persistance) - présentation : câblage app-tauri (state, commands, lib) Couvert par des tests ciblés domaine/application/infrastructure. Depend de #39 (fermeture des fenêtres auxiliaires). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@ -162,7 +162,10 @@ pub use ticket_assistant::{
|
||||
CloseTicketAssistant, CloseTicketAssistantInput, OpenTicketAssistant, OpenTicketAssistantInput,
|
||||
OpenTicketAssistantOutput,
|
||||
};
|
||||
pub use window::{MoveTabToNewWindow, MoveTabToNewWindowInput, MoveTabToNewWindowOutput};
|
||||
pub use window::{
|
||||
MoveTabToNewWindow, MoveTabToNewWindowInput, MoveTabToNewWindowOutput, RestoreOpenWindows,
|
||||
RestoreOpenWindowsOutput, SnapshotOpenWindows, SnapshotOpenWindowsInput,
|
||||
};
|
||||
pub use workstate::{
|
||||
AgentBackgroundTaskState, AgentTicketState, AgentWorkState, AttachLiveAgent,
|
||||
AttachLiveAgentInput, AttachLiveAgentOutput, BackgroundTaskKindLabel, ConversationLogProvider,
|
||||
|
||||
@ -3,4 +3,7 @@
|
||||
|
||||
mod usecases;
|
||||
|
||||
pub use usecases::{MoveTabToNewWindow, MoveTabToNewWindowInput, MoveTabToNewWindowOutput};
|
||||
pub use usecases::{
|
||||
MoveTabToNewWindow, MoveTabToNewWindowInput, MoveTabToNewWindowOutput, RestoreOpenWindows,
|
||||
RestoreOpenWindowsOutput, SnapshotOpenWindows, SnapshotOpenWindowsInput,
|
||||
};
|
||||
|
||||
@ -7,8 +7,10 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use domain::ids::{TabId, WindowId};
|
||||
use domain::layout::Workspace;
|
||||
use domain::ports::{IdGenerator, ProjectStore};
|
||||
use std::collections::HashSet;
|
||||
|
||||
use domain::layout::{PersistedWindowKind, PersistedWindowState, WindowStateSnapshot, Workspace};
|
||||
use domain::ports::{IdGenerator, ProjectStore, StoreError, WindowStateStore};
|
||||
|
||||
use crate::error::AppError;
|
||||
|
||||
@ -71,3 +73,94 @@ impl MoveTabToNewWindow {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Input for [`SnapshotOpenWindows::execute`].
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct SnapshotOpenWindowsInput {
|
||||
/// Open windows captured by the presentation adapter.
|
||||
pub windows: Vec<PersistedWindowState>,
|
||||
}
|
||||
|
||||
/// Persists the latest open OS window snapshot.
|
||||
pub struct SnapshotOpenWindows {
|
||||
store: Arc<dyn WindowStateStore>,
|
||||
}
|
||||
|
||||
impl SnapshotOpenWindows {
|
||||
/// Builds the use case from its store port.
|
||||
#[must_use]
|
||||
pub fn new(store: Arc<dyn WindowStateStore>) -> Self {
|
||||
Self { store }
|
||||
}
|
||||
|
||||
/// Saves a versioned window snapshot.
|
||||
///
|
||||
/// # Errors
|
||||
/// [`AppError::Store`] on persistence failure.
|
||||
pub async fn execute(&self, input: SnapshotOpenWindowsInput) -> Result<(), AppError> {
|
||||
self.store
|
||||
.save_window_state(&WindowStateSnapshot::new(input.windows))
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Output of [`RestoreOpenWindows::execute`].
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct RestoreOpenWindowsOutput {
|
||||
/// Restorable windows, deduplicated by stable label.
|
||||
pub windows: Vec<PersistedWindowState>,
|
||||
}
|
||||
|
||||
/// Loads and filters the latest persisted OS window snapshot.
|
||||
pub struct RestoreOpenWindows {
|
||||
windows: Arc<dyn WindowStateStore>,
|
||||
projects: Arc<dyn ProjectStore>,
|
||||
}
|
||||
|
||||
impl RestoreOpenWindows {
|
||||
/// Builds the use case from its ports.
|
||||
#[must_use]
|
||||
pub fn new(windows: Arc<dyn WindowStateStore>, projects: Arc<dyn ProjectStore>) -> Self {
|
||||
Self { windows, projects }
|
||||
}
|
||||
|
||||
/// Loads restorable windows.
|
||||
///
|
||||
/// Views are ignored if their project no longer exists or if their persisted
|
||||
/// identity is incomplete. Duplicate labels are ignored after the first
|
||||
/// occurrence.
|
||||
///
|
||||
/// # Errors
|
||||
/// [`AppError::Store`] on snapshot read failure.
|
||||
pub async fn execute(&self) -> Result<RestoreOpenWindowsOutput, AppError> {
|
||||
let snapshot = self.windows.load_window_state().await?;
|
||||
let mut seen = HashSet::new();
|
||||
let mut windows = Vec::new();
|
||||
|
||||
for window in snapshot.windows {
|
||||
if window.label.is_empty() || !seen.insert(window.label.clone()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
match window.kind {
|
||||
PersistedWindowKind::Main => windows.push(window),
|
||||
PersistedWindowKind::View => {
|
||||
let Some(project_id) = window.project_id else {
|
||||
continue;
|
||||
};
|
||||
if window.panel.is_none() || window.url.is_none() {
|
||||
continue;
|
||||
}
|
||||
match self.projects.load_project(project_id).await {
|
||||
Ok(_) => windows.push(window),
|
||||
Err(StoreError::NotFound) => {}
|
||||
Err(e) => return Err(e.into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(RestoreOpenWindowsOutput { windows })
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user