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

@ -14,7 +14,7 @@
import { Channel, invoke } from "@tauri-apps/api/core";
import type { Agent, TerminalSession } from "@/domain";
import type { Agent, ResumableAgent, TerminalSession } from "@/domain";
import type {
AgentGateway,
ConversationDetails,
@ -45,6 +45,15 @@ export class TauriAgentGateway implements AgentGateway {
return invoke<LiveAgent[]>("list_live_agents", { projectId });
}
listResumableAgents(projectId: string): Promise<ResumableAgent[]> {
// `list_resumable_agents` takes a top-level `projectId` (camelCase) and
// returns `{ resumable: ResumableAgentDto[] }`. The DTO is already camelCase
// and shape-compatible with `ResumableAgent`, so we just unwrap the list.
return invoke<{ resumable: ResumableAgent[] }>("list_resumable_agents", {
projectId,
}).then((res) => res.resumable);
}
attachLiveAgent(
projectId: string,
agentId: string,

View File

@ -29,6 +29,7 @@ import type {
MemoryType,
Project,
ProfileAvailability,
ResumableAgent,
Skill,
SkillScope,
Template,
@ -211,6 +212,21 @@ export class MockAgentGateway implements AgentGateway {
}));
}
/**
* Resumable agents per project, seeded by tests via
* {@link _setResumableAgents}. Empty by default (no panel on open).
*/
private resumable = new Map<string, ResumableAgent[]>();
/** Seeds the resumable inventory returned for a project (deterministic tests). */
_setResumableAgents(projectId: string, list: ResumableAgent[]): void {
this.resumable.set(projectId, list);
}
async listResumableAgents(projectId: string): Promise<ResumableAgent[]> {
return structuredClone(this.resumable.get(projectId) ?? []);
}
async attachLiveAgent(
projectId: string,
agentId: string,