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:
@ -62,6 +62,7 @@ pub mod plugin;
|
||||
pub mod ports;
|
||||
pub mod profile;
|
||||
pub mod project;
|
||||
pub mod project_restore;
|
||||
pub mod readiness;
|
||||
pub mod remote;
|
||||
pub mod sandbox;
|
||||
@ -88,6 +89,10 @@ pub use ids::{
|
||||
};
|
||||
|
||||
pub use project::{Project, ProjectPath};
|
||||
pub use project_restore::{
|
||||
OpenProjectRestoreEntry, OpenProjectRestoreFailure, OpenProjectRestoreFailureCode,
|
||||
OpenProjectRestoreState, OPEN_PROJECT_RESTORE_VERSION,
|
||||
};
|
||||
|
||||
pub use agent::{Agent, AgentManifest, AgentOrigin, ManifestEntry};
|
||||
|
||||
|
||||
@ -2307,6 +2307,21 @@ pub trait ProjectStore: Send + Sync {
|
||||
async fn load_workspace(&self) -> Result<crate::layout::Workspace, StoreError>;
|
||||
}
|
||||
|
||||
/// Machine-local persistence of the ordered open-project restore intent.
|
||||
#[async_trait]
|
||||
pub trait OpenProjectRestoreStore: Send + Sync {
|
||||
/// Loads the document, returning an empty v1 state when it does not exist.
|
||||
async fn load_open_project_restore_state(
|
||||
&self,
|
||||
) -> Result<crate::OpenProjectRestoreState, StoreError>;
|
||||
|
||||
/// Persists the complete document.
|
||||
async fn save_open_project_restore_state(
|
||||
&self,
|
||||
state: &crate::OpenProjectRestoreState,
|
||||
) -> Result<(), StoreError>;
|
||||
}
|
||||
|
||||
/// Machine-local persistence of open OS window state.
|
||||
#[async_trait]
|
||||
pub trait WindowStateStore: Send + Sync {
|
||||
|
||||
81
crates/domain/src/project_restore.rs
Normal file
81
crates/domain/src/project_restore.rs
Normal file
@ -0,0 +1,81 @@
|
||||
//! Machine-local intent for restoring projects that were visibly open.
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::ProjectId;
|
||||
|
||||
/// Current on-disk schema version for `open-projects.json`.
|
||||
pub const OPEN_PROJECT_RESTORE_VERSION: u32 = 1;
|
||||
|
||||
/// Stable failure category retained with an unavailable restore entry.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub enum OpenProjectRestoreFailureCode {
|
||||
/// Project no longer exists in the known-project registry.
|
||||
NotFound,
|
||||
/// Project root cannot currently be accessed.
|
||||
Inaccessible,
|
||||
/// A backing store failed.
|
||||
Store,
|
||||
/// Persisted data or project metadata is invalid.
|
||||
Invalid,
|
||||
}
|
||||
|
||||
/// Last observed failure for one retained restore entry.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct OpenProjectRestoreFailure {
|
||||
/// Stable category.
|
||||
pub code: OpenProjectRestoreFailureCode,
|
||||
/// Human-readable diagnostic.
|
||||
pub message: String,
|
||||
}
|
||||
|
||||
/// One project and its stable position in the visible open-project order.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct OpenProjectRestoreEntry {
|
||||
/// Project registry id.
|
||||
pub project_id: ProjectId,
|
||||
/// Last known display name, retained even if the registry entry disappears.
|
||||
pub name: String,
|
||||
/// Last known root, retained for failure details.
|
||||
pub root: String,
|
||||
/// Zero-based position in the restore order.
|
||||
pub position: u32,
|
||||
/// Last restore failure, cleared after a successful open.
|
||||
#[serde(default)]
|
||||
pub last_failure: Option<OpenProjectRestoreFailure>,
|
||||
}
|
||||
|
||||
/// Complete machine-local restore document.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct OpenProjectRestoreState {
|
||||
/// Schema version.
|
||||
pub version: u32,
|
||||
/// Entries in visible order.
|
||||
pub entries: Vec<OpenProjectRestoreEntry>,
|
||||
/// Last mutation timestamp in epoch milliseconds.
|
||||
pub updated_at: i64,
|
||||
}
|
||||
|
||||
impl Default for OpenProjectRestoreState {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
version: OPEN_PROJECT_RESTORE_VERSION,
|
||||
entries: Vec::new(),
|
||||
updated_at: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl OpenProjectRestoreState {
|
||||
/// Sorts by persisted position and rewrites positions to a contiguous sequence.
|
||||
pub fn normalize_positions(&mut self) {
|
||||
self.entries.sort_by_key(|entry| entry.position);
|
||||
for (position, entry) in self.entries.iter_mut().enumerate() {
|
||||
entry.position = position as u32;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user