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:
2026-07-13 08:00:20 +02:00
parent 55087b5d9b
commit 2eb51d3335
15 changed files with 887 additions and 40 deletions

View File

@ -96,8 +96,8 @@ pub use store::{
embedder_from_profile, index_token_size, onnx_model_is_cached, should_use_vector,
AdaptiveMemoryRecall, BackgroundTaskReconcileReport, EmbedderEnvProbe, FsBackgroundTaskStore,
FsEmbedderProfileStore, FsEmbedderPromptStore, FsLiveStateStore, FsMemoryStore,
FsPermissionStore, FsProfileStore, FsProjectStore, FsSkillStore, FsTemplateStore, HashEmbedder,
IdeaiContextStore, NaiveMemoryRecall, OnnxModelInfo, StubEmbedder, VectorMemoryRecall,
DEFAULT_OLLAMA_BASE_URL, ONNX_CACHE_SUBDIR, RECOMMENDED_ONNX_MODELS, VECTOR_HTTP_ENABLED,
VECTOR_ONNX_ENABLED,
FsPermissionStore, FsProfileStore, FsProjectStore, FsSkillStore, FsTemplateStore,
FsWindowStateStore, HashEmbedder, IdeaiContextStore, NaiveMemoryRecall, OnnxModelInfo,
StubEmbedder, VectorMemoryRecall, DEFAULT_OLLAMA_BASE_URL, ONNX_CACHE_SUBDIR,
RECOMMENDED_ONNX_MODELS, VECTOR_HTTP_ENABLED, VECTOR_ONNX_ENABLED,
};

View File

@ -15,6 +15,7 @@ mod project;
mod skill;
mod template;
mod vector;
mod window_state;
pub use background_task::{BackgroundTaskReconcileReport, FsBackgroundTaskStore};
pub use context::IdeaiContextStore;
@ -35,3 +36,4 @@ pub use project::FsProjectStore;
pub use skill::FsSkillStore;
pub use template::FsTemplateStore;
pub use vector::{should_use_vector, AdaptiveMemoryRecall, VectorMemoryRecall};
pub use window_state::FsWindowStateStore;

View File

@ -0,0 +1,68 @@
//! JSON-file implementation of the [`WindowStateStore`] port.
//!
//! The snapshot is machine-local and lives under the app-data directory:
//! `<app_data_dir>/windows.json`.
use std::sync::Arc;
use async_trait::async_trait;
use domain::layout::WindowStateSnapshot;
use domain::ports::{FileSystem, RemotePath, StoreError, WindowStateStore};
/// File name of the persisted OS window-state snapshot.
const WINDOWS_FILE: &str = "windows.json";
/// JSON-file implementation of [`WindowStateStore`].
#[derive(Clone)]
pub struct FsWindowStateStore {
fs: Arc<dyn FileSystem>,
app_data_dir: String,
}
impl FsWindowStateStore {
/// Builds the store from an injected filesystem and app-data directory.
#[must_use]
pub fn new(fs: Arc<dyn FileSystem>, app_data_dir: impl Into<String>) -> Self {
Self {
fs,
app_data_dir: app_data_dir.into(),
}
}
fn path(&self) -> RemotePath {
let base = self.app_data_dir.trim_end_matches(['/', '\\']);
RemotePath::new(format!("{base}/{WINDOWS_FILE}"))
}
async fn ensure_dir(&self) -> Result<(), StoreError> {
let dir = RemotePath::new(self.app_data_dir.trim_end_matches(['/', '\\']).to_owned());
self.fs
.create_dir_all(&dir)
.await
.map_err(|e| StoreError::Io(e.to_string()))
}
}
#[async_trait]
impl WindowStateStore for FsWindowStateStore {
async fn save_window_state(&self, snapshot: &WindowStateSnapshot) -> Result<(), StoreError> {
self.ensure_dir().await?;
let bytes = serde_json::to_vec_pretty(snapshot)
.map_err(|e| StoreError::Serialization(e.to_string()))?;
self.fs
.write(&self.path(), &bytes)
.await
.map_err(|e| StoreError::Io(e.to_string()))
}
async fn load_window_state(&self) -> Result<WindowStateSnapshot, StoreError> {
match self.fs.read(&self.path()).await {
Ok(bytes) => {
serde_json::from_slice(&bytes).map_err(|e| StoreError::Serialization(e.to_string()))
}
Err(domain::ports::FsError::NotFound(_)) => Ok(WindowStateSnapshot::default()),
Err(e) => Err(StoreError::Io(e.to_string())),
}
}
}

View File

@ -0,0 +1,60 @@
//! Integration tests for [`FsWindowStateStore`] against a real temp directory.
use std::path::PathBuf;
use std::sync::Arc;
use domain::layout::{PersistedWindowKind, PersistedWindowState, WindowStateSnapshot};
use domain::ports::{FileSystem, WindowStateStore};
use infrastructure::{FsWindowStateStore, LocalFileSystem};
use uuid::Uuid;
struct TempDir(PathBuf);
impl TempDir {
fn new() -> Self {
let path = std::env::temp_dir().join(format!("idea-window-store-{}", Uuid::new_v4()));
std::fs::create_dir_all(&path).unwrap();
Self(path)
}
fn app_data_dir(&self) -> String {
self.0.to_string_lossy().into_owned()
}
}
impl Drop for TempDir {
fn drop(&mut self) {
let _ = std::fs::remove_dir_all(&self.0);
}
}
#[tokio::test]
async fn window_state_save_then_load_roundtrips() {
let tmp = TempDir::new();
let fs: Arc<dyn FileSystem> = Arc::new(LocalFileSystem::new());
let store = FsWindowStateStore::new(fs, tmp.app_data_dir());
assert_eq!(
store.load_window_state().await.unwrap(),
WindowStateSnapshot::default()
);
let snapshot = WindowStateSnapshot::new(vec![PersistedWindowState {
label: "main".to_owned(),
kind: PersistedWindowKind::Main,
panel: None,
project_id: None,
url: None,
visible: true,
maximized: true,
fullscreen: false,
outer_position: None,
outer_size: None,
monitor: None,
last_focused_at: None,
}]);
store.save_window_state(&snapshot).await.unwrap();
assert_eq!(store.load_window_state().await.unwrap(), snapshot);
assert!(tmp.0.join("windows.json").exists());
}