- 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>
155 lines
5.4 KiB
TypeScript
155 lines
5.4 KiB
TypeScript
/**
|
|
* `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>
|
|
);
|
|
}
|