feat(agent): reprise des sessions au redémarrage (B2) — commande + ResumeProjectPanel

- Tauri : commande list_resumable_agents (projectId) + ResumableAgentListDto
  { resumable } / ResumableAgentDto (camelCase, conversationId omis si None),
  câblage state.rs (réutilise stores + ProfileStore, aucun nouveau port).
- Front : gateway listResumableAgents, ResumeProjectPanel monté à l'ouverture
  de projet (opt-in, FR) : Reprendre (launch_agent nodeId+conversationId),
  Nouvelle conversation (setCellConversation(null) puis launch), Ignorer,
  Tout reprendre/ignorer. resumeSupported=false ⇒ « relance à neuf ».
- doc : §15.2 coquille agents→resumable (alignée use case/back/front).

Tests : app-tauri dto 9/9 ; vitest 324 (+10) ; workspace Rust 0 échec. 0 régression.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-09 13:04:41 +02:00
parent b82e3e1a40
commit 7375f706da
15 changed files with 992 additions and 6 deletions

View File

@ -1305,6 +1305,72 @@ pub fn parse_agent_id(raw: &str) -> Result<AgentId, ErrorDto> {
})
}
// ---------------------------------------------------------------------------
// Resumable agents (§15.2 — Chantier B2)
// ---------------------------------------------------------------------------
use application::{ListResumableAgentsOutput, ResumableAgent};
/// One resumable agent cell, as seen by the frontend (§15.2).
///
/// Mirrors [`ResumableAgent`]: the agent's identity + its host cell, the CLI
/// conversation id to resume (absent ⇒ fresh relaunch), the `was_running` flag
/// frozen at close, and whether the agent's profile can resume a CLI
/// conversation. `conversationId` is **omitted from the wire when `None`**
/// (`skip_serializing_if`), so the TypeScript side sees an absent key — not
/// `null`.
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ResumableAgentDto {
/// The resumable agent's id (UUID string).
pub agent_id: String,
/// The agent's display name (resolved from the manifest).
pub name: String,
/// The host layout leaf where the agent is relaunched/resumed (UUID string).
pub node_id: String,
/// Persistent CLI conversation id carried by the cell. Absent ⇒ fresh
/// relaunch (no history to resume).
#[serde(skip_serializing_if = "Option::is_none")]
pub conversation_id: Option<String>,
/// The `agent_was_running` flag frozen at the cell's close.
pub was_running: bool,
/// `true` when the agent's profile carries a usable resume strategy.
pub resume_supported: bool,
}
impl From<ResumableAgent> for ResumableAgentDto {
fn from(r: ResumableAgent) -> Self {
Self {
agent_id: r.agent_id.to_string(),
name: r.name,
node_id: r.node_id.to_string(),
conversation_id: r.conversation_id,
was_running: r.was_running,
resume_supported: r.resume_supported,
}
}
}
/// Response DTO for `list_resumable_agents` (§15.2).
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ResumableAgentListDto {
/// The resumable agent cells, in layout-traversal order.
pub resumable: Vec<ResumableAgentDto>,
}
impl From<ListResumableAgentsOutput> for ResumableAgentListDto {
fn from(out: ListResumableAgentsOutput) -> Self {
Self {
resumable: out
.resumable
.into_iter()
.map(ResumableAgentDto::from)
.collect(),
}
}
}
// ---------------------------------------------------------------------------
// Templates & sync (L7)
// ---------------------------------------------------------------------------