feat(domain,application,infrastructure,backend,app-tauri,web-server,frontend): restaurer les projets ouverts au redémarrage dans l'ordre mémorisé — #303 (état versionné OpenProjectRestoreState persisté sous <app_data_dir>/open-projects.json via port dédié OpenProjectRestoreStore, distinct du registre projects.json et sans recycler workspace.json; sept use cases cadrés: read/record opened/record closed/save order/restore/retry/remove, mutations sérialisées par mutex applicatif contre les lost updates, tombstones conservés à leur position avec lastFailure et codes stables notFound/inaccessible/store/invalid; prévalidation root par metadata+listing sans interrompre les autres restaurations; activation centralisée BackendCore::activate_project réutilisée par ouverture Tauri/HTTP, création, restore et retry: reconciliation layouts/live-state/background tasks, watcher orchestrateur + serveur MCP, réparation run dirs Claude; cinq commandes exposées Tauri et HTTP avec contrats DTO identiques: restore_open_projects, retry_open_project_restore, remove_project_from_restore, save_open_project_restore_order, read_open_project_restore_state; frontend: DTO de reprise + ProjectGateway étendu, useProjects initialise par listProjects+restoreOpenProjects et reconstruit openTabs dans l'ordre opened sans notification quand tout réussit, notification unique singulier/pluriel avec Voir les détails/Réessayer, détail par projet avec cause française et Retirer de la reprise, retry réussi réinséré à la position mémorisée, onglet conservé si closeProject échoué; saveOpenProjectRestoreOrder exposé non appelé car l'UI ne permet pas encore de réordonnancer; QA verte reconfirmée par Git: rustup run stable cargo fmt --all -- --check, git diff --check, cargo test -p backend open_project_restore_tests 1/1 couvrant absence initiale/ordre/fermeture/inaccessible/retry/notFound/remove/watcher+MCP, cargo test -p web-server authorized_open_project_restore_commands_share_the_tauri_contract 1/1, cargo test -p domain 311+ suites vertes, cargo check -p application -p infrastructure -p backend -p app-tauri -p web-server OK, npm run typecheck OK, npm test ciblé 3 fichiers/37 tests OK, npm run build OK; réserves hors périmètre documentées au carnet: application tests/layout_usecases.rs 2 échecs fixtures plugin package NotFound, infrastructure 1 timing test cancellation process, npm test large 32 échecs web shell/LS7/tickets web — préexistants sans lien avec #303; restant: rebuild AppImage demandé après validation puis validation live fermeture/relance; fichiers tickets/296/*, tickets/299-303/*, counter/index, attachments/ et scripts/ hors périmètre; aucun push)

This commit is contained in:
2026-09-10 08:14:28 +02:00
parent 3911ce1f4d
commit 8df80ae67e
25 changed files with 1812 additions and 134 deletions

View File

@ -11,6 +11,7 @@ mod embedder;
mod live_state;
mod mcp_tool_permission;
mod memory;
mod open_project_restore;
mod permission;
mod profile;
mod project;
@ -37,6 +38,7 @@ pub use embedder::{
pub use live_state::FsLiveStateStore;
pub use mcp_tool_permission::FsMcpToolPermissionStore;
pub use memory::{index_token_size, FsMemoryStore, NaiveMemoryRecall};
pub use open_project_restore::FsOpenProjectRestoreStore;
pub use permission::FsPermissionStore;
pub use profile::{FsEmbedderProfileStore, FsProfileStore};
pub use project::FsProjectStore;

View File

@ -0,0 +1,76 @@
//! JSON persistence for the ordered open-project restore intent.
use std::sync::Arc;
use async_trait::async_trait;
use domain::ports::{FileSystem, OpenProjectRestoreStore, RemotePath, StoreError};
use domain::{OpenProjectRestoreState, OPEN_PROJECT_RESTORE_VERSION};
const FILE_NAME: &str = "open-projects.json";
/// Filesystem-backed restore store under the machine-local app-data directory.
#[derive(Clone)]
pub struct FsOpenProjectRestoreStore {
fs: Arc<dyn FileSystem>,
app_data_dir: String,
}
impl FsOpenProjectRestoreStore {
/// Builds the store.
#[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 {
RemotePath::new(format!(
"{}/{}",
self.app_data_dir.trim_end_matches(['/', '\\']),
FILE_NAME
))
}
}
#[async_trait]
impl OpenProjectRestoreStore for FsOpenProjectRestoreStore {
async fn load_open_project_restore_state(&self) -> Result<OpenProjectRestoreState, StoreError> {
match self.fs.read(&self.path()).await {
Ok(bytes) => {
let mut state: OpenProjectRestoreState = serde_json::from_slice(&bytes)
.map_err(|error| StoreError::Serialization(error.to_string()))?;
if state.version != OPEN_PROJECT_RESTORE_VERSION {
return Err(StoreError::Invalid(format!(
"unsupported open-project restore version {}",
state.version
)));
}
state.normalize_positions();
Ok(state)
}
Err(domain::ports::FsError::NotFound(_)) => Ok(OpenProjectRestoreState::default()),
Err(error) => Err(StoreError::Io(error.to_string())),
}
}
async fn save_open_project_restore_state(
&self,
state: &OpenProjectRestoreState,
) -> Result<(), StoreError> {
self.fs
.create_dir_all(&RemotePath::new(
self.app_data_dir.trim_end_matches(['/', '\\']).to_owned(),
))
.await
.map_err(|error| StoreError::Io(error.to_string()))?;
let mut bytes = serde_json::to_vec_pretty(state)
.map_err(|error| StoreError::Serialization(error.to_string()))?;
bytes.push(b'\n');
self.fs
.write(&self.path(), &bytes)
.await
.map_err(|error| StoreError::Io(error.to_string()))
}
}