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

@ -22,16 +22,15 @@ use application::{
GitInitInput, GitLogInput, GitStagePathInput, GitStatusInput, InspectConversationInput,
LaunchAgentInput, ListAgentsInput, ListDevicesInput, ListLayoutsInput, ListMemoriesInput,
ListResumableAgentsInput, ListSkillsInput, ListSlashCommandsInput, LiveSessions,
LoadLayoutInput, McpRuntime, MutateLayoutInput, OpenPluginLayoutWindowInput, OpenProjectInput,
LoadLayoutInput, McpRuntime, MutateLayoutInput, OpenPluginLayoutWindowInput,
ReadAgentContextInput, ReadConversationPageInput, ReadMcpToolPermissionsInput,
ReadMemoryIndexInput, ReadProjectContextInput, RecallMemoryInput, ReconcileLayoutsInput,
ReconcileLiveStateInput, RenameDeviceInput, RenameLayoutInput, ResolveAgentPermissionsInput,
ResolveAgentSystemPermissionsInput, ResolveMemoryLinksInput, RevokeDeviceInput,
RotateConversationLogInput, SaveChatDraftInput, SetActiveLayoutInput,
SetAgentMcpToolAllowedInput, SlashCommandEffect, SnapshotRunningAgentsInput,
StopLiveAgentInput, SyncAgentWithTemplateInput, UnassignSkillFromAgentInput,
UpdateAgentContactableInput, UpdateAgentContextInput, UpdateAgentEffortInput,
UpdateAgentMcpToolPermissionsInput, UpdateAgentPermissionsInput,
ReadMemoryIndexInput, ReadProjectContextInput, RecallMemoryInput, RenameDeviceInput,
RenameLayoutInput, ResolveAgentPermissionsInput, ResolveAgentSystemPermissionsInput,
ResolveMemoryLinksInput, RevokeDeviceInput, RotateConversationLogInput, SaveChatDraftInput,
SetActiveLayoutInput, SetAgentMcpToolAllowedInput, SlashCommandEffect,
SnapshotRunningAgentsInput, StopLiveAgentInput, SyncAgentWithTemplateInput,
UnassignSkillFromAgentInput, UpdateAgentContactableInput, UpdateAgentContextInput,
UpdateAgentEffortInput, UpdateAgentMcpToolPermissionsInput, UpdateAgentPermissionsInput,
UpdateAgentSystemPermissionsInput, UpdateMemoryInput, UpdateProjectContextInput,
UpdateProjectMcpToolPermissionsInput, UpdateProjectOrchestratorInput,
UpdateProjectPermissionsInput, UpdateProjectSystemPermissionsInput, UpdateSkillInput,
@ -65,20 +64,22 @@ use crate::dto::{
LayoutOperationDto, ListLayoutsDto, LiveAgentListDto, ManagedTempGcResponseDto,
ManagedTempHandleRequestDto, ManagedTempLeaseDto, ManagedTempLeaseResponseDto, MemoryDto,
MemoryIndexDto, MemoryLinksDto, MemoryListDto, ModelServerConfigDto, ModelServerConfigListDto,
OpenCodeProviderListDto, OpenTerminalRequestDto, PreviewModelServerCommandDto, ProfileDto,
ProfileListDto, ProfileModelCatalogDto, ProjectDto, ProjectListDto,
ProjectMcpToolPermissionsDto, ProjectPermissionsDto, ProjectSystemPermissionsDto,
ProjectWorkStateDto, ReadAgentContextResponseDto, ReadChatDraftResponseDto,
ReadConversationPageRequestDto, ReattachChatDto, ReattachResultDto, RecallMemoryRequestDto,
RenameLayoutRequestDto, ReplyChunk, ResizeTerminalRequestDto,
ResolveAgentPermissionsRequestDto, ResolveAgentPermissionsResponseDto,
ResolveAgentSystemPermissionsRequestDto, ResolvedAgentSystemPermissionsDto,
ResumableAgentListDto, SaveChatDraftRequestDto, SaveEmbedderProfileRequestDto,
SaveModelServerRequestDto, SaveOpenCodeProviderProfileRequestDto, SaveProfileRequestDto,
SetActiveLayoutRequestDto, SetActiveLayoutResultDto, SetAgentMcpToolAllowedRequestDto,
SkillDto, SkillListDto, SlashCommandEffectDto, SlashCommandListDto, StopLiveAgentRequestDto,
StopLiveAgentResponseDto, SyncAgentWithTemplateRequestDto, SyncResultDto, TemplateDto,
TemplateListDto, TerminalClosedDto, TerminalSessionDto, TurnPageDto, UnassignSkillRequestDto,
OpenCodeProviderListDto, OpenProjectRestoreStateDto, OpenTerminalRequestDto,
PreviewModelServerCommandDto, ProfileDto, ProfileListDto, ProfileModelCatalogDto, ProjectDto,
ProjectListDto, ProjectMcpToolPermissionsDto, ProjectPermissionsDto,
ProjectSystemPermissionsDto, ProjectWorkStateDto, ReadAgentContextResponseDto,
ReadChatDraftResponseDto, ReadConversationPageRequestDto, ReattachChatDto, ReattachResultDto,
RecallMemoryRequestDto, RemoveOpenProjectRestoreEntryRequestDto, RenameLayoutRequestDto,
ReplyChunk, ResizeTerminalRequestDto, ResolveAgentPermissionsRequestDto,
ResolveAgentPermissionsResponseDto, ResolveAgentSystemPermissionsRequestDto,
ResolvedAgentSystemPermissionsDto, RestoreOpenProjectsOutputDto, ResumableAgentListDto,
RetryOpenProjectRestoreRequestDto, SaveChatDraftRequestDto, SaveEmbedderProfileRequestDto,
SaveModelServerRequestDto, SaveOpenCodeProviderProfileRequestDto,
SaveOpenProjectRestoreOrderRequestDto, SaveProfileRequestDto, SetActiveLayoutRequestDto,
SetActiveLayoutResultDto, SetAgentMcpToolAllowedRequestDto, SkillDto, SkillListDto,
SlashCommandEffectDto, SlashCommandListDto, StopLiveAgentRequestDto, StopLiveAgentResponseDto,
SyncAgentWithTemplateRequestDto, SyncResultDto, TemplateDto, TemplateListDto,
TerminalClosedDto, TerminalSessionDto, TurnPageDto, UnassignSkillRequestDto,
UpdateAgentContactableRequestDto, UpdateAgentContextRequestDto, UpdateAgentEffortRequestDto,
UpdateAgentMcpToolPermissionsRequestDto, UpdateAgentPermissionsRequestDto,
UpdateAgentSystemPermissionsRequestDto, UpdateMemoryRequestDto, UpdateProjectContextRequestDto,
@ -438,9 +439,12 @@ pub async fn create_project(
.execute(request.into())
.await
.map_err(ErrorDto::from)?;
// Start tailing this project's `.ideai/requests/` tree so an orchestrator
// agent can delegate agent/skill creation to IdeA (§14.3).
state.ensure_orchestrator_watch(&output.project);
state
.record_project_opened_for_restore
.execute(output.project.clone())
.await
.map_err(ErrorDto::from)?;
state.activate_project(&output.project).await;
Ok(ProjectDto::from(output))
}
@ -467,36 +471,9 @@ pub(crate) async fn open_project_for_adapter(
) -> Result<ProjectDto, ErrorDto> {
let id = parse_project_id(&project_id)?;
let output = state
.open_project
.execute(OpenProjectInput { project_id: id })
.open_project_and_activate(id)
.await
.map_err(ErrorDto::from)?;
// R0c (§3.4 « Trou C ») : dé-doublonne les `layouts.json` portant plusieurs
// feuilles sur le même agent AVANT toute reprise (`list_resumable_agents`
// relit la version persistée), de sorte qu'on ne propose / ne relance qu'une
// session par agent. Idempotent (no-op sans doublon) et best-effort : un échec
// ne doit pas bloquer l'ouverture.
let _ = state
.reconcile_layouts
.execute(ReconcileLayoutsInput { project_id: id })
.await;
// Réconcilie les lignes de live-state fantômes : un crash ne déclenche jamais
// `close_project`, donc l'ouverture est le seul filet fiable pour repasser en
// `idle` les agents `working`/`waiting`/`blocked` dont la session est morte.
// Acte système (appelle le port live-state directement, jamais via la surface
// MCP self-only). Best-effort : un échec ne bloque pas l'ouverture. Tourne
// AVANT toute relance d'agent (toute reprise réécrit ensuite en LWW).
let _ = state
.reconcile_live_state
.execute(ReconcileLiveStateInput { project_id: id })
.await;
// Réconcilie les tâches de fond persistées au même point de boot que le
// live-state : les Running sans handle vivant deviennent terminales, les
// complétions WakeOwner non livrées repassent dans l'inbox unifiée.
let _ = state.reconcile_background_tasks.execute(id).await;
// (Re)start the orchestrator watcher for this project (idempotent, §14.3).
state.ensure_orchestrator_watch(&output.project);
state.reconcile_claude_run_dirs(&output.project).await;
Ok(ProjectDto::from(output))
}
@ -633,10 +610,92 @@ pub async fn close_project(project_id: String, state: State<'_, AppState>) -> Re
workspace: None,
})
.await
.map_err(ErrorDto::from)?;
state
.record_project_closed_for_restore
.execute(id)
.await
.map(|_| ())
.map_err(ErrorDto::from)
}
/// Reads the ordered open-project restore state without opening projects.
#[tauri::command]
pub async fn read_open_project_restore_state(
state: State<'_, AppState>,
) -> Result<OpenProjectRestoreStateDto, ErrorDto> {
state
.read_open_project_restore_state
.execute()
.await
.map(Into::into)
.map_err(ErrorDto::from)
}
/// Restores all retained projects and applies normal-open runtime side effects.
#[tauri::command]
pub async fn restore_open_projects(
state: State<'_, AppState>,
) -> Result<RestoreOpenProjectsOutputDto, ErrorDto> {
state
.restore_open_projects_and_activate()
.await
.map(Into::into)
.map_err(ErrorDto::from)
}
/// Retries one retained entry, or all entries carrying a restore failure.
#[tauri::command]
pub async fn retry_open_project_restore(
request: RetryOpenProjectRestoreRequestDto,
state: State<'_, AppState>,
) -> Result<RestoreOpenProjectsOutputDto, ErrorDto> {
let project_id = request
.project_id
.as_deref()
.map(parse_project_id)
.transpose()?;
state
.retry_open_project_restore_and_activate(project_id)
.await
.map(Into::into)
.map_err(ErrorDto::from)
}
/// Removes one retained entry from future restoration.
#[tauri::command]
pub async fn remove_project_from_restore(
request: RemoveOpenProjectRestoreEntryRequestDto,
state: State<'_, AppState>,
) -> Result<OpenProjectRestoreStateDto, ErrorDto> {
let project_id = parse_project_id(&request.project_id)?;
state
.remove_project_from_restore
.execute(project_id)
.await
.map(Into::into)
.map_err(ErrorDto::from)
}
/// Persists a new visible order; omitted retained entries keep their prior order.
#[tauri::command]
pub async fn save_open_project_restore_order(
request: SaveOpenProjectRestoreOrderRequestDto,
state: State<'_, AppState>,
) -> Result<OpenProjectRestoreStateDto, ErrorDto> {
let ordered_project_ids = request
.ordered_project_ids
.iter()
.map(|id| parse_project_id(id))
.collect::<Result<Vec<_>, _>>()?;
state
.save_open_project_restore_order
.execute(ordered_project_ids)
.await
.map(Into::into)
.map_err(ErrorDto::from)
}
/// `list_projects` — list the projects known to the registry.
///
/// # Errors

View File

@ -254,6 +254,11 @@ pub fn run() {
commands::create_project,
commands::open_project,
commands::close_project,
commands::read_open_project_restore_state,
commands::restore_open_projects,
commands::retry_open_project_restore,
commands::remove_project_from_restore,
commands::save_open_project_restore_order,
commands::list_projects,
commands::read_project_context,
commands::update_project_context,