feat(domain,application,infrastructure,backend,web-server,app-tauri,frontend): lot utilisateur #325 #326 — icônes projet avec watermark CLI custom et localisation FR/EN (QA verte)
Lot en un seul commit: les deux features partagent les mêmes contrats (domain/settings.rs, DTO, commandes Tauri/HTTP, web-server) et les mêmes surfaces UI — la séparation par fichiers était impossible sans chirurgie de hunks risquant des états intermédiaires non compilables (convention lot des batches #310-#313 et #315). #325 — icône configurable par projet, watermark CLI custom: - domaine: ProjectIconSet (devicon, simple-icons, lucide, idea), ProjectIconRef validé (nom ASCII 1..=64, -_. internes), champ icon optionnel sur ProjectSettings; sérialisation camelCase et try_from strict - application: UpdateProjectSettings.execute accepte un patch theme_color OU icon (absent = inchangé, null = défaut produit), tests in-dependants avec store mémoire - backend/app-tauri/web-server: ProjectIconRefDto, UpdateProjectSettingsRequestDto avec icon optionnel (désérialisation additive), commandes update_project_settings alignées Tauri/HTTP, tests DTO et HTTP (défaut null, set, clear, nom invalide rejeté) - frontend: catalogue projectIcons (recherche EN), ProjectIconPicker en grille multi-lignes dans Paramètres projet, affichage de l'icône dans ProjectTabs et watermark grand format à faible opacité en fond de CLI custom; ports ProjectSettings.icon, Partial<ProjectSettings>, adapters tauri/mock/http et tests alignés #326 — localisation FR/EN et traduction des textes IdeA: - domaine: IdeaLanguage (fr | en), champ language optionnel sur IdeaUserSettings (absence = défaut produit, série additive validée) - backend/web-server: UserSettingsDto.language, update_user_settings accepte language, tests HTTP alignés; fixtures structured_launch_d3 mises à jour - frontend: i18n/ (catalogue FR/EN, I18nProvider, scan anti-régression des sources visibles), GeneralSettings avec sélecteur de langue dans les paramètres, setupI18n dans vitest; migration de l'ensemble des textes hardcodés vers le catalogue sur toutes les surfaces (agents, layout, projets, tickets, sprints, web, plugins, permissions, git, templates, skills, mémoire, model servers, embedder, first-run, devices, scheduled tasks, workstate, dialogs), FR conservé par défaut; recherche d'icônes #325 en anglais conformément au ticket QA verte sur feature/batch-325-326-project-icons-localisation. Aucun push.
This commit is contained in:
@ -21,6 +21,9 @@ import { useLayout } from "@/features/layout/useLayout";
|
||||
import type { LayoutViewModel } from "@/features/layout/useLayout";
|
||||
import { Button, EntryTimestamp, Panel, Spinner, cn } from "@/shared";
|
||||
import { useProjectWorkState } from "./useProjectWorkState";
|
||||
import { useI18n } from "@/i18n";
|
||||
|
||||
type Translator = (french: string, english: string) => string;
|
||||
|
||||
export interface ProjectWorkStatePanelProps {
|
||||
projectId: string;
|
||||
@ -53,59 +56,52 @@ export class WorkStatePanelErrorBoundary extends Component<
|
||||
|
||||
render() {
|
||||
if (this.state.error) {
|
||||
return (
|
||||
<Panel title="Work">
|
||||
<div
|
||||
role="alert"
|
||||
className="rounded-md border border-danger/40 bg-danger/10 px-3 py-2 text-sm text-danger"
|
||||
>
|
||||
<p className="font-medium">Work panel failed to render.</p>
|
||||
<p className="mt-1 text-xs text-danger/80">
|
||||
{this.state.error.message}
|
||||
</p>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
className="mt-2"
|
||||
onClick={this.retry}
|
||||
>
|
||||
Retry
|
||||
</Button>
|
||||
</div>
|
||||
</Panel>
|
||||
);
|
||||
return <WorkStateError error={this.state.error} onRetry={this.retry} />;
|
||||
}
|
||||
|
||||
return <div key={this.state.retryKey}>{this.props.children}</div>;
|
||||
}
|
||||
}
|
||||
|
||||
function WorkStateError({ error, onRetry }: { error: Error; onRetry: () => void }) {
|
||||
const { tr } = useI18n();
|
||||
return (
|
||||
<Panel title={tr("Travail", "Work")}>
|
||||
<div role="alert" className="rounded-md border border-danger/40 bg-danger/10 px-3 py-2 text-sm text-danger">
|
||||
<p className="font-medium">{tr("Le panneau Travail n'a pas pu s'afficher.", "Work panel failed to render.")}</p>
|
||||
<p className="mt-1 text-xs text-danger/80">{error.message}</p>
|
||||
<Button size="sm" variant="ghost" className="mt-2" onClick={onRetry}>{tr("Réessayer", "Retry")}</Button>
|
||||
</div>
|
||||
</Panel>
|
||||
);
|
||||
}
|
||||
|
||||
function shortTicket(ticket: string): string {
|
||||
return ticket.length <= 8 ? ticket : ticket.slice(0, 8);
|
||||
}
|
||||
|
||||
function formatStatus(status: AgentTicketState["status"]): string {
|
||||
return status === "inProgress" ? "In progress" : "Queued";
|
||||
function formatStatus(status: AgentTicketState["status"], tr: Translator): string {
|
||||
return status === "inProgress" ? tr("En cours", "In progress") : tr("En attente", "Queued");
|
||||
}
|
||||
|
||||
function formatTaskStatus(status: BackgroundCompletion["status"]): string {
|
||||
function formatTaskStatus(status: BackgroundCompletion["status"], tr: Translator): string {
|
||||
switch (status) {
|
||||
case "running":
|
||||
return "Running";
|
||||
return tr("En cours", "Running");
|
||||
case "cancelling":
|
||||
return "Cancelling";
|
||||
return tr("Annulation…", "Cancelling");
|
||||
case "completed":
|
||||
return "Completed";
|
||||
return tr("Terminée", "Completed");
|
||||
case "failed":
|
||||
return "Failed";
|
||||
return tr("Échec", "Failed");
|
||||
case "cancelFailed":
|
||||
return "Cancel failed";
|
||||
return tr("Échec de l'annulation", "Cancel failed");
|
||||
case "cancelled":
|
||||
return "Cancelled";
|
||||
return tr("Annulée", "Cancelled");
|
||||
case "delivered":
|
||||
return "Delivered";
|
||||
return tr("Livrée", "Delivered");
|
||||
case "pending":
|
||||
return "Pending";
|
||||
return tr("En attente", "Pending");
|
||||
}
|
||||
}
|
||||
|
||||
@ -145,14 +141,15 @@ function agentStatusLabel(
|
||||
busy: boolean,
|
||||
runtimeState: AgentWorkState["runtimeState"],
|
||||
stopState: AgentStopUiState,
|
||||
tr: Translator,
|
||||
): string {
|
||||
if (stopState === "stopping" || runtimeState === "stopping") {
|
||||
return "Arrêt en cours…";
|
||||
return tr("Arrêt en cours…", "Stopping…");
|
||||
}
|
||||
if (stopState === "incomplete") return "Arrêt incomplet";
|
||||
if (stopState === "stopped" || runtimeState === "stopped") return "Arrêté";
|
||||
if (busy) return "Busy";
|
||||
return live ? "Idle" : "Offline";
|
||||
if (stopState === "incomplete") return tr("Arrêt incomplet", "Incomplete stop");
|
||||
if (stopState === "stopped" || runtimeState === "stopped") return tr("Arrêté", "Stopped");
|
||||
if (busy) return tr("Occupé", "Busy");
|
||||
return live ? tr("Inactif", "Idle") : tr("Hors ligne", "Offline");
|
||||
}
|
||||
|
||||
function agentStatusClass(
|
||||
@ -175,14 +172,14 @@ function agentStatusClass(
|
||||
return "bg-raised text-muted";
|
||||
}
|
||||
|
||||
function backgroundTaskSummary(tasks: BackgroundCompletion[]): string | null {
|
||||
function backgroundTaskSummary(tasks: BackgroundCompletion[], tr: Translator): string | null {
|
||||
if (tasks.length === 0) return null;
|
||||
const running = tasks.filter((task) => canCancelTask(task.status)).length;
|
||||
const failed = tasks.filter((task) => canRetryTask(task.status)).length;
|
||||
if (running > 0 && failed > 0) return `${tasks.length} background tasks (${running} active, ${failed} failed)`;
|
||||
if (running > 0) return `${running} background task${running > 1 ? "s" : ""} active`;
|
||||
if (failed > 0) return `${failed} background task${failed > 1 ? "s" : ""} need attention`;
|
||||
return `${tasks.length} background task${tasks.length > 1 ? "s" : ""}`;
|
||||
if (running > 0 && failed > 0) return tr(`${tasks.length} tâches de fond (${running} actives, ${failed} en échec)`, `${tasks.length} background tasks (${running} active, ${failed} failed)`);
|
||||
if (running > 0) return tr(`${running} tâche${running > 1 ? "s" : ""} de fond active${running > 1 ? "s" : ""}`, `${running} background task${running > 1 ? "s" : ""} active`);
|
||||
if (failed > 0) return tr(`${failed} tâche${failed > 1 ? "s" : ""} de fond à vérifier`, `${failed} background task${failed > 1 ? "s" : ""} need attention`);
|
||||
return tr(`${tasks.length} tâche${tasks.length > 1 ? "s" : ""} de fond`, `${tasks.length} background task${tasks.length > 1 ? "s" : ""}`);
|
||||
}
|
||||
|
||||
function compactIntent(text: string | null): string | null {
|
||||
@ -207,8 +204,8 @@ function compactIndicator({
|
||||
return null;
|
||||
}
|
||||
|
||||
function requesterLabel(ticket: AgentTicketState): string {
|
||||
if (ticket.source.kind === "human") return "Human";
|
||||
function requesterLabel(ticket: AgentTicketState, tr: Translator): string {
|
||||
if (ticket.source.kind === "human") return tr("Humain", "Human");
|
||||
const id = shortTicket(ticket.source.agentId);
|
||||
return ticket.requesterLabel.trim()
|
||||
? `${ticket.requesterLabel} (${id})`
|
||||
@ -228,11 +225,11 @@ function summaryLabel(status: ConversationPreviewStatus): string {
|
||||
}
|
||||
}
|
||||
|
||||
function summaryText(summary: ConversationWorkSummary): string | null {
|
||||
if (summary.objectivePreview) return `Goal: ${summary.objectivePreview}`;
|
||||
function summaryText(summary: ConversationWorkSummary, tr: Translator): string | null {
|
||||
if (summary.objectivePreview) return `${tr("Objectif :", "Goal:")} ${summary.objectivePreview}`;
|
||||
if (summary.summaryPreview) return summary.summaryPreview;
|
||||
const lastTurn = (summary.recentTurns ?? []).at(-1);
|
||||
return lastTurn ? `Last: ${lastTurn.textPreview}` : null;
|
||||
return lastTurn ? `${tr("Dernier :", "Last:")} ${lastTurn.textPreview}` : null;
|
||||
}
|
||||
|
||||
function goToCell(nodeId: string): void {
|
||||
@ -262,7 +259,8 @@ function ConversationSummaryLine({
|
||||
summary: ConversationWorkSummary;
|
||||
onOpenConversation?: (conversationId: string) => void;
|
||||
}) {
|
||||
const text = summaryText(summary);
|
||||
const { tr } = useI18n();
|
||||
const text = summaryText(summary, tr);
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
const [copied, setCopied] = useState(false);
|
||||
if (!text) return null;
|
||||
@ -277,12 +275,12 @@ function ConversationSummaryLine({
|
||||
: "bg-raised text-muted",
|
||||
)}
|
||||
>
|
||||
{summaryLabel(summary.status)}
|
||||
{tr(summary.status === "ready" ? "Résumé" : summary.status === "missing" ? "Aucun résumé" : summary.status === "partial" ? "Partiel" : "Indisponible", summaryLabel(summary.status))}
|
||||
</span>
|
||||
{onOpenConversation ? (
|
||||
<button
|
||||
type="button"
|
||||
aria-label={`open conversation ${summary.conversationId}`}
|
||||
aria-label={tr(`ouvrir la conversation ${summary.conversationId}`, `open conversation ${summary.conversationId}`)}
|
||||
className="min-w-0 flex-1 break-words text-left hover:text-content hover:underline"
|
||||
onClick={() => onOpenConversation(summary.conversationId)}
|
||||
>
|
||||
@ -296,7 +294,7 @@ function ConversationSummaryLine({
|
||||
className="shrink-0 rounded px-1 py-0.5 text-[11px] text-muted hover:bg-raised hover:text-content"
|
||||
onClick={() => setExpanded((v) => !v)}
|
||||
>
|
||||
{expanded ? "Hide" : "View"}
|
||||
{expanded ? tr("Masquer", "Hide") : tr("Afficher", "View")}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
@ -309,23 +307,23 @@ function ConversationSummaryLine({
|
||||
});
|
||||
}}
|
||||
>
|
||||
{copied ? "Copied" : "Copy"}
|
||||
{copied ? tr("Copié", "Copied") : tr("Copier", "Copy")}
|
||||
</button>
|
||||
</div>
|
||||
{expanded && (
|
||||
<div
|
||||
aria-label={`conversation ${summary.conversationId} details`}
|
||||
aria-label={tr(`détails de la conversation ${summary.conversationId}`, `conversation ${summary.conversationId} details`)}
|
||||
className="mt-1 space-y-1 border-l border-border pl-2"
|
||||
>
|
||||
{summary.objectivePreview && (
|
||||
<p>
|
||||
<span className="font-medium text-content">Goal:</span>{" "}
|
||||
<span className="font-medium text-content">{tr("Objectif :", "Goal:")}</span>{" "}
|
||||
{summary.objectivePreview}
|
||||
</p>
|
||||
)}
|
||||
{summary.summaryPreview && (
|
||||
<p>
|
||||
<span className="font-medium text-content">Summary:</span>{" "}
|
||||
<span className="font-medium text-content">{tr("Résumé :", "Summary:")}</span>{" "}
|
||||
{summary.summaryPreview}
|
||||
</p>
|
||||
)}
|
||||
@ -357,6 +355,7 @@ function TicketRow({
|
||||
summary?: ConversationWorkSummary;
|
||||
onOpenConversation?: (conversationId: string) => void;
|
||||
}) {
|
||||
const { tr } = useI18n();
|
||||
const truncated = ticket.taskLen > ticket.taskPreview.length;
|
||||
return (
|
||||
<li className="min-w-0 text-xs text-muted">
|
||||
@ -369,15 +368,15 @@ function TicketRow({
|
||||
: "bg-raised text-muted",
|
||||
)}
|
||||
>
|
||||
#{ticket.position + 1} {formatStatus(ticket.status)}
|
||||
#{ticket.position + 1} {formatStatus(ticket.status, tr)}
|
||||
</span>
|
||||
<span className="min-w-0 flex-1">
|
||||
<span className="font-medium text-content">{requesterLabel(ticket)}</span>
|
||||
<span className="font-medium text-content">{requesterLabel(ticket, tr)}</span>
|
||||
<span className="text-muted"> · </span>
|
||||
<span className="break-words">{ticket.taskPreview}</span>
|
||||
{truncated && (
|
||||
<span
|
||||
aria-label={`${ticket.taskLen - ticket.taskPreview.length} more characters`}
|
||||
aria-label={tr(`${ticket.taskLen - ticket.taskPreview.length} caractères supplémentaires`, `${ticket.taskLen - ticket.taskPreview.length} more characters`)}
|
||||
className="text-muted"
|
||||
>
|
||||
{" "}
|
||||
@ -429,6 +428,7 @@ function BackgroundTaskRow({
|
||||
task: BackgroundCompletion;
|
||||
onRefresh: () => Promise<void>;
|
||||
}) {
|
||||
const { tr } = useI18n();
|
||||
const { workState } = useGateways();
|
||||
const [open, setOpen] = useState(false);
|
||||
const [liveOpen, setLiveOpen] = useState(false);
|
||||
@ -516,12 +516,12 @@ function BackgroundTaskRow({
|
||||
taskStatusClass(task.status),
|
||||
)}
|
||||
>
|
||||
{formatTaskStatus(task.status)}
|
||||
{formatTaskStatus(task.status, tr)}
|
||||
</span>
|
||||
<span className="min-w-0 flex-1">
|
||||
<span className="font-medium text-content">{task.kind}</span>
|
||||
{task.exitCode !== null && (
|
||||
<span className="text-muted"> · exit {task.exitCode}</span>
|
||||
<span className="text-muted"> · {tr("sortie", "exit")} {task.exitCode}</span>
|
||||
)}
|
||||
</span>
|
||||
<Button
|
||||
@ -533,7 +533,7 @@ function BackgroundTaskRow({
|
||||
void runAction((id) => workState.cancelBackgroundTask(id))
|
||||
}
|
||||
>
|
||||
Cancel
|
||||
{tr("Annuler", "Cancel")}
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
@ -542,7 +542,7 @@ function BackgroundTaskRow({
|
||||
loading={liveBusy}
|
||||
onClick={() => void toggleLive()}
|
||||
>
|
||||
{liveOpen ? "Detach" : "Live"}
|
||||
{liveOpen ? tr("Détacher", "Detach") : tr("Direct", "Live")}
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
@ -550,7 +550,7 @@ function BackgroundTaskRow({
|
||||
disabled={!hasOutput}
|
||||
onClick={() => setOpen((v) => !v)}
|
||||
>
|
||||
Output
|
||||
{tr("Sortie", "Output")}
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
@ -561,7 +561,7 @@ function BackgroundTaskRow({
|
||||
void runAction((id) => workState.retryBackgroundTask(id))
|
||||
}
|
||||
>
|
||||
Retry
|
||||
{tr("Réessayer", "Retry")}
|
||||
</Button>
|
||||
<code
|
||||
title={task.taskId}
|
||||
@ -572,7 +572,7 @@ function BackgroundTaskRow({
|
||||
</div>
|
||||
{task.summary && isTerminalTaskStatus(task.status) && (
|
||||
<p
|
||||
aria-label={`task ${shortTicket(task.taskId)} summary`}
|
||||
aria-label={tr(`résumé de la tâche ${shortTicket(task.taskId)}`, `task ${shortTicket(task.taskId)} summary`)}
|
||||
title={task.summary}
|
||||
className="mt-1 truncate pl-1 text-[11px] text-muted"
|
||||
>
|
||||
@ -586,7 +586,7 @@ function BackgroundTaskRow({
|
||||
)}
|
||||
{open && hasOutput && (
|
||||
<pre
|
||||
aria-label={`task ${shortTicket(task.taskId)} output`}
|
||||
aria-label={tr(`sortie de la tâche ${shortTicket(task.taskId)}`, `task ${shortTicket(task.taskId)} output`)}
|
||||
className="mt-1 max-h-40 overflow-auto whitespace-pre-wrap rounded border border-border bg-canvas p-2 text-[11px] text-muted"
|
||||
>
|
||||
{task.stdoutTail ? `stdout\n${task.stdoutTail}` : ""}
|
||||
@ -596,10 +596,10 @@ function BackgroundTaskRow({
|
||||
)}
|
||||
{liveOpen && (
|
||||
<pre
|
||||
aria-label={`task ${shortTicket(task.taskId)} live output`}
|
||||
aria-label={tr(`sortie directe de la tâche ${shortTicket(task.taskId)}`, `task ${shortTicket(task.taskId)} live output`)}
|
||||
className="mt-1 max-h-40 overflow-auto whitespace-pre-wrap rounded border border-border bg-canvas p-2 text-[11px] text-muted"
|
||||
>
|
||||
{liveOutput || "No live output yet."}
|
||||
{liveOutput || tr("Aucune sortie directe pour le moment.", "No live output yet.")}
|
||||
</pre>
|
||||
)}
|
||||
</li>
|
||||
@ -624,6 +624,7 @@ function AgentRow({
|
||||
onOpenConversation?: (conversationId: string) => void;
|
||||
}) {
|
||||
const { agent: agentGateway } = useGateways();
|
||||
const { tr } = useI18n();
|
||||
const [message, setMessage] = useState<string | null>(null);
|
||||
const [actionBusy, setActionBusy] = useState(false);
|
||||
const [stopState, setStopState] = useState<AgentStopUiState>("idle");
|
||||
@ -644,16 +645,16 @@ function AgentRow({
|
||||
const currentTurn =
|
||||
busy || tickets.some((ticket) => ticket.status === "inProgress");
|
||||
const activeTicket = tickets.find((ticket) => ticket.status === "inProgress");
|
||||
const statusLabel = agentStatusLabel(live, busy, agent.runtimeState, stopState);
|
||||
const statusLabel = agentStatusLabel(live, busy, agent.runtimeState, stopState, tr);
|
||||
const stopLabel =
|
||||
stopState === "stopping" || agent.runtimeState === "stopping"
|
||||
? "Arrêt en cours…"
|
||||
? tr("Arrêt en cours…", "Stopping…")
|
||||
: stopState === "incomplete"
|
||||
? "Réessayer l’arrêt"
|
||||
? tr("Réessayer l’arrêt", "Retry stopping")
|
||||
: stopState === "stopped" || agent.runtimeState === "stopped"
|
||||
? "Arrêté"
|
||||
: "Stop";
|
||||
const taskSummary = backgroundTaskSummary(backgroundTasks);
|
||||
? tr("Arrêté", "Stopped")
|
||||
: tr("Arrêter", "Stop");
|
||||
const taskSummary = backgroundTaskSummary(backgroundTasks, tr);
|
||||
const activityLine = compactIntent(
|
||||
activeTicket?.taskPreview ??
|
||||
(busyState.state === "busy" ? `Ticket ${shortTicket(busyState.ticket)}` : null),
|
||||
@ -699,9 +700,9 @@ function AgentRow({
|
||||
async function stop(confirm = true): Promise<void> {
|
||||
if (!agent.live || !agentGateway) return;
|
||||
const warning = currentTurn
|
||||
? " The current turn will be interrupted."
|
||||
? tr(" Le tour en cours sera interrompu.", " The current turn will be interrupted.")
|
||||
: "";
|
||||
if (confirm && !window.confirm(`Stop ${agent.name}?${warning}`)) return;
|
||||
if (confirm && !window.confirm(`${tr("Arrêter", "Stop")} ${agent.name} ?${warning}`)) return;
|
||||
setActionBusy(true);
|
||||
setStopState("stopping");
|
||||
setMessage(null);
|
||||
@ -714,7 +715,7 @@ function AgentRow({
|
||||
);
|
||||
} catch {
|
||||
setStopState("incomplete");
|
||||
setMessage("Certains travaux sont encore actifs.");
|
||||
setMessage(tr("Certains travaux sont encore actifs.", "Some work is still active."));
|
||||
setActionBusy(false);
|
||||
return;
|
||||
}
|
||||
@ -733,7 +734,7 @@ function AgentRow({
|
||||
await refreshAfterAction();
|
||||
} catch (e) {
|
||||
setMessage(
|
||||
`Arrêt confirmé. Impossible d’actualiser l’affichage : ${
|
||||
`${tr("Arrêt confirmé. Impossible d’actualiser l’affichage :", "Stop confirmed. Unable to refresh the display:")} ${
|
||||
e instanceof Error ? e.message : String(e)
|
||||
}`,
|
||||
);
|
||||
@ -780,7 +781,7 @@ function AgentRow({
|
||||
aria-controls={`work-agent-details-${agent.agentId}`}
|
||||
onClick={() => setDetailsOpen((value) => !value)}
|
||||
>
|
||||
{detailsOpen ? "Hide" : "Details"}
|
||||
{detailsOpen ? tr("Masquer", "Hide") : tr("Détails", "Details")}
|
||||
</button>
|
||||
)}
|
||||
</span>
|
||||
@ -799,7 +800,7 @@ function AgentRow({
|
||||
variant="ghost"
|
||||
onClick={() => goToCell(agent.live!.nodeId)}
|
||||
>
|
||||
Open
|
||||
{tr("Ouvrir", "Open")}
|
||||
</Button>
|
||||
)}
|
||||
{agent.live && !liveVisible && (
|
||||
@ -811,11 +812,11 @@ function AgentRow({
|
||||
title={
|
||||
attachTarget
|
||||
? undefined
|
||||
: "No empty visible cell available"
|
||||
: tr("Aucune cellule visible vide disponible", "No empty visible cell available")
|
||||
}
|
||||
onClick={() => void attach()}
|
||||
>
|
||||
Attach
|
||||
{tr("Attacher", "Attach")}
|
||||
</Button>
|
||||
)}
|
||||
{agent.live && (
|
||||
@ -839,7 +840,7 @@ function AgentRow({
|
||||
</div>
|
||||
)}
|
||||
{agent.live && !liveVisible && !attachTarget && (
|
||||
<p className="text-xs text-muted">No empty visible cell.</p>
|
||||
<p className="text-xs text-muted">{tr("Aucune cellule visible vide.", "No empty visible cell.")}</p>
|
||||
)}
|
||||
{message && (
|
||||
<p role="alert" className="text-xs text-danger">
|
||||
@ -848,7 +849,7 @@ function AgentRow({
|
||||
)}
|
||||
{tickets.length > 0 && (
|
||||
<ul
|
||||
aria-label={`${agent.name} tickets`}
|
||||
aria-label={`${tr("Tickets de", "Tickets for")} ${agent.name}`}
|
||||
className="flex flex-col gap-1"
|
||||
>
|
||||
{tickets.map((ticket) => (
|
||||
@ -864,9 +865,9 @@ function AgentRow({
|
||||
{inbox.length > 0 && (
|
||||
<div>
|
||||
<p className="text-[11px] font-semibold uppercase tracking-wide text-faint">
|
||||
Inbox
|
||||
{tr("Boîte de réception", "Inbox")}
|
||||
</p>
|
||||
<ul aria-label={`${agent.name} inbox`} className="mt-1 flex flex-col gap-1">
|
||||
<ul aria-label={`${tr("Boîte de réception de", "Inbox for")} ${agent.name}`} className="mt-1 flex flex-col gap-1">
|
||||
{inbox.map((item) => (
|
||||
<InboxRow key={item.id} item={item} />
|
||||
))}
|
||||
@ -882,12 +883,12 @@ function AgentRow({
|
||||
aria-controls={`work-agent-background-${agent.agentId}`}
|
||||
onClick={() => setBackgroundOpen((value) => !value)}
|
||||
>
|
||||
{backgroundOpen ? "Hide background tasks" : taskSummary}
|
||||
{backgroundOpen ? tr("Masquer les tâches en arrière-plan", "Hide background tasks") : taskSummary}
|
||||
</button>
|
||||
{backgroundOpen && (
|
||||
<ul
|
||||
id={`work-agent-background-${agent.agentId}`}
|
||||
aria-label={`${agent.name} background tasks`}
|
||||
aria-label={`${tr("Tâches de fond de", "Background tasks for")} ${agent.name}`}
|
||||
className="mt-1 flex flex-col gap-1"
|
||||
>
|
||||
{backgroundTasks.map((task) => (
|
||||
@ -911,6 +912,7 @@ function ProjectWorkStatePanelContent({
|
||||
projectId,
|
||||
onOpenConversation,
|
||||
}: ProjectWorkStatePanelProps) {
|
||||
const { tr } = useI18n();
|
||||
const vm = useProjectWorkState(projectId);
|
||||
const layoutVm = useLayout(projectId);
|
||||
const agents = vm.state?.agents ?? [];
|
||||
@ -924,7 +926,7 @@ function ProjectWorkStatePanelContent({
|
||||
|
||||
return (
|
||||
<Panel
|
||||
title="Work"
|
||||
title={tr("Travail", "Work")}
|
||||
actions={
|
||||
<Button
|
||||
size="sm"
|
||||
@ -932,7 +934,7 @@ function ProjectWorkStatePanelContent({
|
||||
onClick={() => void vm.refresh()}
|
||||
loading={vm.busy}
|
||||
>
|
||||
Refresh
|
||||
{tr("Rafraîchir", "Refresh")}
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
@ -947,10 +949,10 @@ function ProjectWorkStatePanelContent({
|
||||
{vm.busy && vm.state === null ? (
|
||||
<div className="flex items-center gap-2 text-sm text-muted">
|
||||
<Spinner size={14} />
|
||||
<span>Loading work state…</span>
|
||||
<span>{tr("Chargement de l'état de travail…", "Loading work state…")}</span>
|
||||
</div>
|
||||
) : agents.length === 0 ? (
|
||||
<p className="text-sm text-muted">No agent work state.</p>
|
||||
<p className="text-sm text-muted">{tr("Aucun état de travail d'agent.", "No agent work state.")}</p>
|
||||
) : (
|
||||
<ul className="flex flex-col divide-y divide-border">
|
||||
{agents.map((agent) => (
|
||||
|
||||
@ -48,23 +48,23 @@ async function openAgentDetails(agentName: string): Promise<HTMLElement> {
|
||||
const name = await screen.findByText(agentName);
|
||||
const row = name.closest("li") as HTMLElement | null;
|
||||
expect(row).toBeTruthy();
|
||||
const button = within(row!).queryByRole("button", { name: "Details" });
|
||||
const button = within(row!).queryByRole("button", { name: "Détails" });
|
||||
if (button) fireEvent.click(button);
|
||||
return row!;
|
||||
}
|
||||
|
||||
async function openBackgroundTasks(agentName: string): Promise<HTMLElement> {
|
||||
const row = await openAgentDetails(agentName);
|
||||
const toggle = within(row).getByRole("button", { name: /background task/i });
|
||||
const toggle = within(row).getByRole("button", { name: /tâche.*de fond/i });
|
||||
fireEvent.click(toggle);
|
||||
return await within(row).findByLabelText(`${agentName} background tasks`);
|
||||
return await within(row).findByLabelText(`Tâches de fond de ${agentName}`);
|
||||
}
|
||||
|
||||
describe("ProjectWorkStatePanel", () => {
|
||||
it("renders the empty state", async () => {
|
||||
renderPanel();
|
||||
|
||||
expect(await screen.findByText("No agent work state.")).toBeTruthy();
|
||||
expect(await screen.findByText("Aucun état de travail d'agent.")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("renders an idle offline agent", async () => {
|
||||
@ -83,9 +83,9 @@ describe("ProjectWorkStatePanel", () => {
|
||||
renderPanel(workState);
|
||||
|
||||
expect(await screen.findByText("Planner")).toBeTruthy();
|
||||
expect(screen.getByText("Offline")).toBeTruthy();
|
||||
expect(screen.queryByText("Idle")).toBeNull();
|
||||
expect(screen.queryByLabelText("Planner tickets")).toBeNull();
|
||||
expect(screen.getByText("Hors ligne")).toBeTruthy();
|
||||
expect(screen.queryByText("Inactif")).toBeNull();
|
||||
expect(screen.queryByLabelText("Tickets de Planner")).toBeNull();
|
||||
});
|
||||
|
||||
it("renders a live busy agent with a short ticket", async () => {
|
||||
@ -113,7 +113,7 @@ describe("ProjectWorkStatePanel", () => {
|
||||
renderPanel(workState);
|
||||
|
||||
expect(await screen.findByText("Builder")).toBeTruthy();
|
||||
expect(screen.getByText("Busy")).toBeTruthy();
|
||||
expect(screen.getByText("Occupé")).toBeTruthy();
|
||||
expect(screen.getByText("Ticket 4c65d981")).toBeTruthy();
|
||||
});
|
||||
|
||||
@ -150,8 +150,8 @@ describe("ProjectWorkStatePanel", () => {
|
||||
const stopped = within(childRow).getByText("Arrêté");
|
||||
expect(stopping.className).not.toContain("text-warning");
|
||||
expect(stopped.className).not.toContain("text-warning");
|
||||
expect(within(mainRow).queryByText("Busy")).toBeNull();
|
||||
expect(within(childRow).queryByText("Busy")).toBeNull();
|
||||
expect(within(mainRow).queryByText("Occupé")).toBeNull();
|
||||
expect(within(childRow).queryByText("Occupé")).toBeNull();
|
||||
});
|
||||
|
||||
it("renders an in-progress human ticket", async () => {
|
||||
@ -186,14 +186,14 @@ describe("ProjectWorkStatePanel", () => {
|
||||
renderPanel(workState);
|
||||
|
||||
expect(await screen.findByText("Wire the workstate queue UI")).toBeTruthy();
|
||||
expect(screen.queryByLabelText("Implementer tickets")).toBeNull();
|
||||
expect(screen.queryByLabelText("Tickets de Implementer")).toBeNull();
|
||||
await openAgentDetails("Implementer");
|
||||
const list = await screen.findByLabelText("Implementer tickets");
|
||||
expect(within(list).getByText("#1 In progress")).toBeTruthy();
|
||||
expect(within(list).getByText("Human")).toBeTruthy();
|
||||
const list = await screen.findByLabelText("Tickets de Implementer");
|
||||
expect(within(list).getByText("#1 En cours")).toBeTruthy();
|
||||
expect(within(list).getByText("Humain")).toBeTruthy();
|
||||
expect(within(list).getByText("Wire the workstate queue UI")).toBeTruthy();
|
||||
expect(within(list).getByLabelText("ticket ticket-i")).toBeTruthy();
|
||||
expect(within(list).getByLabelText("9 more characters")).toBeTruthy();
|
||||
expect(within(list).getByLabelText("9 caractères supplémentaires")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("keeps long ticket intent out of the default agent row", async () => {
|
||||
@ -233,7 +233,7 @@ describe("ProjectWorkStatePanel", () => {
|
||||
expect(screen.queryByText(longPreview)).toBeNull();
|
||||
expect(screen.getByText("Goal: inspect the entire repository and p...")).toBeTruthy();
|
||||
await openAgentDetails("Git");
|
||||
const list = await screen.findByLabelText("Git tickets");
|
||||
const list = await screen.findByLabelText("Tickets de Git");
|
||||
expect(within(list).getByText(longPreview)).toBeTruthy();
|
||||
});
|
||||
|
||||
@ -275,13 +275,13 @@ describe("ProjectWorkStatePanel", () => {
|
||||
renderPanel(workState);
|
||||
|
||||
expect(await screen.findByText("Reviewer")).toBeTruthy();
|
||||
expect(screen.queryByLabelText("Reviewer tickets")).toBeNull();
|
||||
expect(screen.queryByLabelText("Tickets de Reviewer")).toBeNull();
|
||||
await openAgentDetails("Reviewer");
|
||||
const list = await screen.findByLabelText("Reviewer tickets");
|
||||
const list = await screen.findByLabelText("Tickets de Reviewer");
|
||||
expect(within(list).getByText("QA (agent-qa)")).toBeTruthy();
|
||||
expect(within(list).getByText("Main (agent-ma)")).toBeTruthy();
|
||||
expect(list.textContent?.indexOf("#1 In progress")).toBeLessThan(
|
||||
list.textContent?.indexOf("#2 Queued") ?? -1,
|
||||
expect(list.textContent?.indexOf("#1 En cours")).toBeLessThan(
|
||||
list.textContent?.indexOf("#2 En attente") ?? -1,
|
||||
);
|
||||
});
|
||||
|
||||
@ -381,42 +381,42 @@ describe("ProjectWorkStatePanel", () => {
|
||||
|
||||
expect(await screen.findByText("Runner")).toBeTruthy();
|
||||
expect(screen.getByText("6 bg")).toBeTruthy();
|
||||
expect(screen.queryByText("6 background tasks (3 active, 3 failed)")).toBeNull();
|
||||
expect(screen.queryByLabelText("Runner background tasks")).toBeNull();
|
||||
expect(screen.queryByText("6 tâches de fond (3 actives, 3 en échec)")).toBeNull();
|
||||
expect(screen.queryByLabelText("Tâches de fond de Runner")).toBeNull();
|
||||
const list = await openBackgroundTasks("Runner");
|
||||
const cancelButtons = within(list).getAllByRole("button", { name: "Cancel" });
|
||||
const retryButtons = within(list).getAllByRole("button", { name: "Retry" });
|
||||
const cancelButtons = within(list).getAllByRole("button", { name: "Annuler" });
|
||||
const retryButtons = within(list).getAllByRole("button", { name: "Réessayer" });
|
||||
// waiting + queued + cancelling are cancellable; failed + expired + cancelFailed are retryable.
|
||||
expect(cancelButtons.filter((b) => !(b as HTMLButtonElement).disabled)).toHaveLength(3);
|
||||
expect(retryButtons.filter((b) => !(b as HTMLButtonElement).disabled)).toHaveLength(3);
|
||||
expect(list.textContent).toContain("Cancelling");
|
||||
expect(list.textContent).toContain("Cancel failed");
|
||||
expect(list.textContent).toContain("Annulation…");
|
||||
expect(list.textContent).toContain("Échec de l'annulation");
|
||||
const rows = within(list).getAllByRole("listitem");
|
||||
const rowTexts = rows.map((row) => row.textContent ?? "");
|
||||
expect(rowTexts).toHaveLength(6);
|
||||
const cancelFailedRow = rows[0] as HTMLElement;
|
||||
const cancellingRow = rows[1] as HTMLElement;
|
||||
expect(within(cancellingRow).getByText("Cancelling")).toBeTruthy();
|
||||
expect(within(cancellingRow).getByText("Annulation…")).toBeTruthy();
|
||||
expect(
|
||||
(within(cancellingRow).getByRole("button", {
|
||||
name: "Cancel",
|
||||
name: "Annuler",
|
||||
}) as HTMLButtonElement).disabled,
|
||||
).toBe(false);
|
||||
expect(within(cancelFailedRow).getByText("Cancel failed")).toBeTruthy();
|
||||
expect(within(cancelFailedRow).getByText("Échec de l'annulation")).toBeTruthy();
|
||||
expect(
|
||||
(within(cancelFailedRow).getByRole("button", {
|
||||
name: "Retry",
|
||||
name: "Réessayer",
|
||||
}) as HTMLButtonElement).disabled,
|
||||
).toBe(false);
|
||||
|
||||
// Rows are ordered most-recently-updated first (updatedAtMs desc), taskId as
|
||||
// the deterministic tiebreak — locks the fix for the dead finishedAtMs sort.
|
||||
expect(rowTexts[0]).toContain("Cancel failed");
|
||||
expect(rowTexts[1]).toContain("Cancelling");
|
||||
expect(rowTexts[2]).toContain("Cancelled");
|
||||
expect(rowTexts[3]).toContain("Failed");
|
||||
expect(rowTexts[4]).toContain("Pending");
|
||||
expect(rowTexts[5]).toContain("Pending");
|
||||
expect(rowTexts[0]).toContain("Échec de l'annulation");
|
||||
expect(rowTexts[1]).toContain("Annulation…");
|
||||
expect(rowTexts[2]).toContain("Annulée");
|
||||
expect(rowTexts[3]).toContain("Échec");
|
||||
expect(rowTexts[4]).toContain("En attente");
|
||||
expect(rowTexts[5]).toContain("En attente");
|
||||
});
|
||||
|
||||
it("shows a terminal task summary and hides it when absent or non-terminal (#5)", async () => {
|
||||
@ -470,20 +470,20 @@ describe("ProjectWorkStatePanel", () => {
|
||||
|
||||
expect(await screen.findByText("Reporter")).toBeTruthy();
|
||||
expect(screen.getByText("4 bg")).toBeTruthy();
|
||||
expect(screen.queryByText("4 background tasks (1 active, 1 failed)")).toBeNull();
|
||||
expect(screen.queryByLabelText("Reporter background tasks")).toBeNull();
|
||||
expect(screen.queryByText("4 tâches de fond (1 active, 1 en échec)")).toBeNull();
|
||||
expect(screen.queryByLabelText("Tâches de fond de Reporter")).toBeNull();
|
||||
const list = await openBackgroundTasks("Reporter");
|
||||
// Terminal tasks with a summary render it discreetly.
|
||||
expect(
|
||||
within(list).getByLabelText("task done-wit summary").textContent,
|
||||
within(list).getByLabelText("résumé de la tâche done-wit").textContent,
|
||||
).toBe("Build succeeded in 12s");
|
||||
expect(
|
||||
within(list).getByText("Compile error: missing semicolon"),
|
||||
).toBeTruthy();
|
||||
// A completed task without a summary shows no summary line.
|
||||
expect(within(list).queryByLabelText("task done-no- summary")).toBeNull();
|
||||
expect(within(list).queryByLabelText("résumé de la tâche done-no-")).toBeNull();
|
||||
// A still-running task never shows its (non-terminal) summary.
|
||||
expect(within(list).queryByLabelText("task run-with summary")).toBeNull();
|
||||
expect(within(list).queryByLabelText("résumé de la tâche run-with")).toBeNull();
|
||||
expect(
|
||||
within(list).queryByText("should-not-appear-while-running"),
|
||||
).toBeNull();
|
||||
@ -519,29 +519,29 @@ describe("ProjectWorkStatePanel", () => {
|
||||
const view = renderPanel(workState);
|
||||
const list = await openBackgroundTasks("Runner");
|
||||
|
||||
fireEvent.click(within(list).getByRole("button", { name: "Live" }));
|
||||
fireEvent.click(within(list).getByRole("button", { name: "Direct" }));
|
||||
|
||||
expect(attachSpy).toHaveBeenCalledWith("task-live-output", expect.any(Function));
|
||||
expect(
|
||||
(await within(list).findByLabelText("task task-liv live output")).textContent,
|
||||
(await within(list).findByLabelText("sortie directe de la tâche task-liv")).textContent,
|
||||
).toContain("already printed");
|
||||
expect(workState._backgroundTaskSubscriberCount("task-live-output")).toBe(1);
|
||||
|
||||
workState._emitBackgroundTaskOutput("task-live-output", "new line\n");
|
||||
await waitFor(() =>
|
||||
expect(
|
||||
within(list).getByLabelText("task task-liv live output").textContent,
|
||||
within(list).getByLabelText("sortie directe de la tâche task-liv").textContent,
|
||||
).toContain("new line"),
|
||||
);
|
||||
|
||||
fireEvent.click(within(list).getByRole("button", { name: "Detach" }));
|
||||
fireEvent.click(within(list).getByRole("button", { name: "Détacher" }));
|
||||
await waitFor(() =>
|
||||
expect(workState._backgroundTaskSubscriberCount("task-live-output")).toBe(0),
|
||||
);
|
||||
expect(within(list).queryByLabelText("task task-liv live output")).toBeNull();
|
||||
expect(within(list).queryByLabelText("sortie directe de la tâche task-liv")).toBeNull();
|
||||
|
||||
fireEvent.click(within(list).getByRole("button", { name: "Live" }));
|
||||
await within(list).findByLabelText("task task-liv live output");
|
||||
fireEvent.click(within(list).getByRole("button", { name: "Direct" }));
|
||||
await within(list).findByLabelText("sortie directe de la tâche task-liv");
|
||||
expect(workState._backgroundTaskSubscriberCount("task-live-output")).toBe(1);
|
||||
view.unmount();
|
||||
expect(workState._backgroundTaskSubscriberCount("task-live-output")).toBe(0);
|
||||
@ -563,9 +563,9 @@ describe("ProjectWorkStatePanel", () => {
|
||||
renderPanel(workState);
|
||||
|
||||
expect(await screen.findByText("Legacy")).toBeTruthy();
|
||||
expect(screen.getByText("Offline")).toBeTruthy();
|
||||
expect(screen.queryByText("Idle")).toBeNull();
|
||||
expect(screen.queryByLabelText("Legacy tickets")).toBeNull();
|
||||
expect(screen.getByText("Hors ligne")).toBeTruthy();
|
||||
expect(screen.queryByText("Inactif")).toBeNull();
|
||||
expect(screen.queryByLabelText("Tickets de Legacy")).toBeNull();
|
||||
});
|
||||
|
||||
it("ignores a stray top-level backgroundTasks array (nested tasks intact) (#5)", async () => {
|
||||
@ -631,10 +631,10 @@ describe("ProjectWorkStatePanel", () => {
|
||||
);
|
||||
|
||||
expect(screen.getByRole("alert").textContent).toContain(
|
||||
"Work panel failed to render.",
|
||||
"Le panneau Travail n'a pas pu s'afficher.",
|
||||
);
|
||||
expect(screen.getByText("render exploded")).toBeTruthy();
|
||||
expect(screen.getByRole("button", { name: "Retry" })).toBeTruthy();
|
||||
expect(screen.getByRole("button", { name: "Réessayer" })).toBeTruthy();
|
||||
});
|
||||
|
||||
it("renders conversation objective and summary joined by conversation id", async () => {
|
||||
@ -695,18 +695,18 @@ describe("ProjectWorkStatePanel", () => {
|
||||
renderPanel(workState);
|
||||
|
||||
expect(await screen.findByText("Continue implementation")).toBeTruthy();
|
||||
expect(screen.queryByLabelText("Summarizer tickets")).toBeNull();
|
||||
expect(screen.queryByText("Goal: Ship the Work panel summaries")).toBeNull();
|
||||
expect(screen.queryByLabelText("Tickets de Summarizer")).toBeNull();
|
||||
expect(screen.queryByText("Objectif : Ship the Work panel summaries")).toBeNull();
|
||||
expect(
|
||||
screen.queryByText("Previous turn established the adapter boundary."),
|
||||
).toBeNull();
|
||||
expect(screen.queryByRole("button", { name: "View" })).toBeNull();
|
||||
expect(screen.queryByRole("button", { name: "Copy" })).toBeNull();
|
||||
expect(screen.queryByRole("button", { name: "Afficher" })).toBeNull();
|
||||
expect(screen.queryByRole("button", { name: "Copier" })).toBeNull();
|
||||
await openAgentDetails("Summarizer");
|
||||
const list = await screen.findByLabelText("Summarizer tickets");
|
||||
expect(within(list).getAllByText("Summary")).toHaveLength(2);
|
||||
const list = await screen.findByLabelText("Tickets de Summarizer");
|
||||
expect(within(list).getAllByText("Résumé")).toHaveLength(2);
|
||||
expect(
|
||||
within(list).getByText("Goal: Ship the Work panel summaries"),
|
||||
within(list).getByText("Objectif : Ship the Work panel summaries"),
|
||||
).toBeTruthy();
|
||||
expect(
|
||||
within(list).getByText(
|
||||
@ -781,18 +781,18 @@ describe("ProjectWorkStatePanel", () => {
|
||||
renderPanel(workState);
|
||||
|
||||
expect(await screen.findByText("Validate partial context")).toBeTruthy();
|
||||
expect(screen.queryByLabelText("Contextualizer tickets")).toBeNull();
|
||||
expect(screen.queryByLabelText("Tickets de Contextualizer")).toBeNull();
|
||||
await openAgentDetails("Contextualizer");
|
||||
const list = await screen.findByLabelText("Contextualizer tickets");
|
||||
expect(within(list).getByText("#1 In progress")).toBeTruthy();
|
||||
const list = await screen.findByLabelText("Tickets de Contextualizer");
|
||||
expect(within(list).getByText("#1 En cours")).toBeTruthy();
|
||||
expect(within(list).getByText("Validate partial context")).toBeTruthy();
|
||||
expect(within(list).getByText("Partial")).toBeTruthy();
|
||||
expect(within(list).getByText("Partiel")).toBeTruthy();
|
||||
expect(
|
||||
within(list).getByText("Last: Only the latest prompt was available."),
|
||||
within(list).getByText("Dernier : Only the latest prompt was available."),
|
||||
).toBeTruthy();
|
||||
expect(within(list).getByText("#2 Queued")).toBeTruthy();
|
||||
expect(within(list).getByText("#2 En attente")).toBeTruthy();
|
||||
expect(within(list).getByText("Validate unavailable context")).toBeTruthy();
|
||||
expect(within(list).getByText("Unavailable")).toBeTruthy();
|
||||
expect(within(list).getByText("Indisponible")).toBeTruthy();
|
||||
expect(
|
||||
within(list).getByText("Summary service unavailable right now."),
|
||||
).toBeTruthy();
|
||||
@ -803,7 +803,7 @@ describe("ProjectWorkStatePanel", () => {
|
||||
const system = new MockSystemGateway();
|
||||
renderPanel(workState, system);
|
||||
|
||||
await screen.findByText("No agent work state.");
|
||||
await screen.findByText("Aucun état de travail d'agent.");
|
||||
const spy = vi.spyOn(workState, "getProjectWorkState");
|
||||
workState._setProjectWorkState(PROJECT_ID, {
|
||||
agents: [
|
||||
@ -831,7 +831,7 @@ describe("ProjectWorkStatePanel", () => {
|
||||
const system = new MockSystemGateway();
|
||||
renderPanel(workState, system);
|
||||
|
||||
await screen.findByText("No agent work state.");
|
||||
await screen.findByText("Aucun état de travail d'agent.");
|
||||
const spy = vi.spyOn(workState, "getProjectWorkState");
|
||||
workState._setProjectWorkState(PROJECT_ID, {
|
||||
agents: [
|
||||
@ -865,7 +865,7 @@ describe("ProjectWorkStatePanel", () => {
|
||||
|
||||
expect(await screen.findByText("Delegatee")).toBeTruthy();
|
||||
await openAgentDetails("Delegatee");
|
||||
expect(screen.getByText("#1 In progress")).toBeTruthy();
|
||||
expect(screen.getByText("#1 En cours")).toBeTruthy();
|
||||
await waitFor(() => expect(spy).toHaveBeenCalled());
|
||||
});
|
||||
|
||||
@ -895,7 +895,7 @@ describe("ProjectWorkStatePanel", () => {
|
||||
renderPanel(workState, new MockSystemGateway(), { layout, agent });
|
||||
|
||||
await openAgentDetails("Visible");
|
||||
fireEvent.click(await screen.findByRole("button", { name: "Open" }));
|
||||
fireEvent.click(await screen.findByRole("button", { name: "Ouvrir" }));
|
||||
|
||||
expect(host.style.outline).toContain("2px solid");
|
||||
expect(launchSpy).not.toHaveBeenCalled();
|
||||
@ -936,7 +936,7 @@ describe("ProjectWorkStatePanel", () => {
|
||||
renderPanel(workState, new MockSystemGateway(), { layout, agent });
|
||||
|
||||
await openAgentDetails("Hidden");
|
||||
const attach = await screen.findByRole("button", { name: "Attach" });
|
||||
const attach = await screen.findByRole("button", { name: "Attacher" });
|
||||
await waitFor(() => expect((attach as HTMLButtonElement).disabled).toBe(false));
|
||||
fireEvent.click(attach);
|
||||
|
||||
@ -985,9 +985,9 @@ describe("ProjectWorkStatePanel", () => {
|
||||
});
|
||||
|
||||
await openAgentDetails("Hidden");
|
||||
const attach = await screen.findByRole("button", { name: "Attach" });
|
||||
const attach = await screen.findByRole("button", { name: "Attacher" });
|
||||
await waitFor(() => expect((attach as HTMLButtonElement).disabled).toBe(true));
|
||||
expect(await screen.findByText("No empty visible cell.")).toBeTruthy();
|
||||
expect(await screen.findByText("Aucune cellule visible vide.")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("Stop confirms with a busy warning, stops the live agent, and refreshes Work", async () => {
|
||||
@ -1050,11 +1050,11 @@ describe("ProjectWorkStatePanel", () => {
|
||||
|
||||
await openAgentDetails("Runner");
|
||||
const row = await openAgentDetails("Runner");
|
||||
fireEvent.click(within(row).getByRole("button", { name: "Stop" }));
|
||||
fireEvent.click(within(row).getByRole("button", { name: "Arrêter" }));
|
||||
|
||||
await waitFor(() =>
|
||||
expect(confirmSpy).toHaveBeenCalledWith(
|
||||
"Stop Runner? The current turn will be interrupted.",
|
||||
"Arrêter Runner ? Le tour en cours sera interrompu.",
|
||||
),
|
||||
);
|
||||
await waitFor(() =>
|
||||
@ -1067,7 +1067,7 @@ describe("ProjectWorkStatePanel", () => {
|
||||
const stopping = within(row).getByRole("button", { name: "Arrêt en cours…" });
|
||||
expect((stopping as HTMLButtonElement).disabled).toBe(true);
|
||||
expect(within(row).getAllByText("Arrêt en cours…")).toHaveLength(2);
|
||||
expect(within(row).queryByText("Busy")).toBeNull();
|
||||
expect(within(row).queryByText("Occupé")).toBeNull();
|
||||
|
||||
workState._setProjectWorkState(PROJECT_ID, {
|
||||
agents: [
|
||||
@ -1096,7 +1096,7 @@ describe("ProjectWorkStatePanel", () => {
|
||||
});
|
||||
await waitFor(() => expect(refreshSpy).toHaveBeenCalled());
|
||||
await waitFor(() => expect(within(row).getAllByText("Arrêté")).toHaveLength(1));
|
||||
expect(within(row).queryByText("Busy")).toBeNull();
|
||||
expect(within(row).queryByText("Occupé")).toBeNull();
|
||||
await waitFor(async () => {
|
||||
const leaf = leaves(await layout.loadLayout(PROJECT_ID))[0];
|
||||
expect(leaf.session).toBeNull();
|
||||
@ -1130,7 +1130,7 @@ describe("ProjectWorkStatePanel", () => {
|
||||
renderPanel(workState, new MockSystemGateway(), { agent });
|
||||
|
||||
const row = await openAgentDetails("Main");
|
||||
fireEvent.click(within(row).getByRole("button", { name: "Stop" }));
|
||||
fireEvent.click(within(row).getByRole("button", { name: "Arrêter" }));
|
||||
|
||||
expect(await within(row).findByText("Arrêt incomplet")).toBeTruthy();
|
||||
expect(within(row).getByRole("alert").textContent).toBe(
|
||||
@ -1197,12 +1197,15 @@ describe("ProjectWorkStatePanel", () => {
|
||||
renderPanel(workState);
|
||||
|
||||
await openAgentDetails("Viewer");
|
||||
const list = await screen.findByLabelText("Viewer tickets");
|
||||
fireEvent.click(within(list).getByRole("button", { name: "View" }));
|
||||
const list = await screen.findByLabelText("Tickets de Viewer");
|
||||
fireEvent.click(within(list).getByRole("button", { name: "Afficher" }));
|
||||
const details = within(list).getByLabelText(
|
||||
"détails de la conversation conversation-view",
|
||||
);
|
||||
|
||||
expect(within(list).getByText("Goal:")).toBeTruthy();
|
||||
expect(within(list).getByText("Keep it compact")).toBeTruthy();
|
||||
expect(within(list).getByText("Summary:")).toBeTruthy();
|
||||
expect(within(details).getByText(/^Objectif/)).toBeTruthy();
|
||||
expect(within(details).getByText("Keep it compact")).toBeTruthy();
|
||||
expect(within(details).getByText(/^Résumé/)).toBeTruthy();
|
||||
expect(
|
||||
within(list).getByText("Only the exposed summary preview."),
|
||||
).toBeTruthy();
|
||||
@ -1260,13 +1263,13 @@ describe("ProjectWorkStatePanel", () => {
|
||||
renderPanel(workState);
|
||||
|
||||
await openAgentDetails("Copier");
|
||||
const list = await screen.findByLabelText("Copier tickets");
|
||||
fireEvent.click(within(list).getByRole("button", { name: "Copy" }));
|
||||
const list = await screen.findByLabelText("Tickets de Copier");
|
||||
fireEvent.click(within(list).getByRole("button", { name: "Copier" }));
|
||||
|
||||
await waitFor(() =>
|
||||
expect(writeText).toHaveBeenCalledWith("Copy this summary preview only."),
|
||||
);
|
||||
expect(await within(list).findByRole("button", { name: "Copied" })).toBeTruthy();
|
||||
expect(await within(list).findByRole("button", { name: "Copié" })).toBeTruthy();
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user