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:
154
frontend/src/features/agents/ResumeProjectPanel.tsx
Normal file
154
frontend/src/features/agents/ResumeProjectPanel.tsx
Normal file
@ -0,0 +1,154 @@
|
||||
/**
|
||||
* `ResumeProjectPanel` — the reopen resume panel (L15 / lot B2, ARCHITECTURE
|
||||
* §15.2).
|
||||
*
|
||||
* Mounted **once when a project is (re)opened**, only when the resumable
|
||||
* inventory is non-empty. It lists every agent cell that "was running" and/or
|
||||
* carries a persisted conversation id, and lets the user decide, per agent:
|
||||
* **Reprendre** / **Nouvelle conversation** / **Ignorer**, plus the global
|
||||
* **Tout reprendre** / **Tout ignorer**. Each choice reuses the existing
|
||||
* `launch_agent` flow through {@link useResumeProject}; the panel itself is pure
|
||||
* presentation (it only consumes the view-model, never a gateway directly).
|
||||
*
|
||||
* Status / labels mirror {@link ResumeConversationPopup}: the "en cours" / "clôt"
|
||||
* status is derived **only** from `wasRunning`; an agent whose profile cannot
|
||||
* resume a conversation (`resumeSupported === false`) shows "relance à neuf".
|
||||
*
|
||||
* Empty pending set ⇒ the component renders nothing (no overlay).
|
||||
*/
|
||||
|
||||
import { Button, cn } from "@/shared";
|
||||
import { useResumeProject } from "./useResumeProject";
|
||||
|
||||
export interface ResumeProjectPanelProps {
|
||||
/** Project being (re)opened. */
|
||||
projectId: string;
|
||||
/** Working directory the resumed agents launch in (the project root). */
|
||||
cwd: string;
|
||||
}
|
||||
|
||||
export function ResumeProjectPanel({ projectId, cwd }: ResumeProjectPanelProps) {
|
||||
const vm = useResumeProject(projectId, cwd);
|
||||
|
||||
// No resumable agents ⇒ no panel at all.
|
||||
if (vm.pending.length === 0) return null;
|
||||
|
||||
return (
|
||||
<div
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-label="Reprise des agents"
|
||||
data-testid="resume-project-panel"
|
||||
className="fixed inset-0 z-50 flex items-center justify-center bg-black/55 p-4"
|
||||
>
|
||||
<div className="flex max-h-[80vh] w-full max-w-lg flex-col rounded-lg border border-border bg-surface p-5 shadow-xl">
|
||||
<h4 className="text-sm font-semibold text-content">
|
||||
Reprendre les agents ?
|
||||
</h4>
|
||||
<p className="mt-1 text-sm text-muted">
|
||||
Ces agents tournaient à la dernière fermeture du projet. Choisissez
|
||||
ceux à reprendre.
|
||||
</p>
|
||||
|
||||
{vm.error && (
|
||||
<p
|
||||
role="alert"
|
||||
className="mt-3 rounded-md border border-danger/40 bg-danger/10 px-3 py-2 text-sm text-danger"
|
||||
>
|
||||
{vm.error}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{/* ── Per-agent rows ── */}
|
||||
<ul className="mt-4 flex flex-col gap-2 overflow-auto">
|
||||
{vm.pending.map((a) => {
|
||||
const statusLabel = a.wasRunning ? "en cours" : "clôt";
|
||||
return (
|
||||
<li
|
||||
key={a.agentId}
|
||||
data-testid={`resume-row-${a.agentId}`}
|
||||
className="flex flex-col gap-2 rounded-md border border-border bg-raised p-3"
|
||||
>
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<span className="min-w-0 truncate font-medium text-content">
|
||||
{a.name}
|
||||
</span>
|
||||
<span
|
||||
data-testid={`resume-status-${a.agentId}`}
|
||||
className={cn(
|
||||
"shrink-0 rounded px-1.5 py-0.5 text-xs",
|
||||
a.wasRunning
|
||||
? "bg-primary/15 text-primary"
|
||||
: "bg-muted/15 text-muted",
|
||||
)}
|
||||
>
|
||||
{statusLabel}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{!a.resumeSupported && (
|
||||
<p
|
||||
data-testid={`resume-fresh-note-${a.agentId}`}
|
||||
className="text-xs text-muted"
|
||||
>
|
||||
Historique non disponible pour ce moteur — relance à neuf.
|
||||
</p>
|
||||
)}
|
||||
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="primary"
|
||||
aria-label={`resume ${a.name}`}
|
||||
disabled={vm.busy}
|
||||
onClick={() => void vm.resume(a)}
|
||||
>
|
||||
{a.resumeSupported ? "Reprendre" : "Relancer à neuf"}
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
aria-label={`new conversation ${a.name}`}
|
||||
disabled={vm.busy}
|
||||
onClick={() => void vm.newConversation(a)}
|
||||
>
|
||||
Nouvelle conversation
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
aria-label={`ignore ${a.name}`}
|
||||
disabled={vm.busy}
|
||||
onClick={() => vm.ignore(a.agentId)}
|
||||
>
|
||||
Ignorer
|
||||
</Button>
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
|
||||
{/* ── Global actions ── */}
|
||||
<div className="mt-4 flex items-center justify-end gap-2">
|
||||
<Button
|
||||
variant="ghost"
|
||||
aria-label="ignore all agents"
|
||||
disabled={vm.busy}
|
||||
onClick={() => vm.ignoreAll()}
|
||||
>
|
||||
Tout ignorer
|
||||
</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
aria-label="resume all agents"
|
||||
loading={vm.busy}
|
||||
disabled={vm.busy}
|
||||
onClick={() => void vm.resumeAll()}
|
||||
>
|
||||
Tout reprendre
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user