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:
@ -124,7 +124,7 @@ describe("AgentLimitBadge (render)", () => {
|
||||
const expected = `limité jusqu'à ${formatResetTime(at)}`;
|
||||
expect(screen.getByText(expected)).toBeTruthy();
|
||||
// No countdown / cancel button without an armed resume.
|
||||
expect(screen.queryByLabelText("cancel resume")).toBeNull();
|
||||
expect(screen.queryByLabelText("annuler la reprise")).toBeNull();
|
||||
});
|
||||
|
||||
it("shows plain 'limité' when no reset time is known", () => {
|
||||
@ -145,8 +145,8 @@ describe("AgentLimitBadge (render)", () => {
|
||||
expect(screen.getByText("limité")).toBeTruthy();
|
||||
expect(screen.getByText(/heure inconnue/)).toBeTruthy();
|
||||
// The human-net form: a resume-time input + a "schedule resume" button.
|
||||
expect(screen.getByLabelText("resume time")).toBeTruthy();
|
||||
expect(screen.getByLabelText("schedule resume")).toBeTruthy();
|
||||
expect(screen.getByLabelText("heure de reprise")).toBeTruthy();
|
||||
expect(screen.getByLabelText("programmer la reprise")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("does NOT show the resume-time form when a suspected limit has a time", () => {
|
||||
@ -160,7 +160,7 @@ describe("AgentLimitBadge (render)", () => {
|
||||
);
|
||||
expect(screen.getByText(`limité jusqu'à ${formatResetTime(at)}`)).toBeTruthy();
|
||||
expect(screen.queryByText(/heure inconnue/)).toBeNull();
|
||||
expect(screen.queryByLabelText("resume time")).toBeNull();
|
||||
expect(screen.queryByLabelText("heure de reprise")).toBeNull();
|
||||
});
|
||||
|
||||
it("submitting the resume-time form arms a resume at the chosen epoch-ms", () => {
|
||||
@ -172,10 +172,10 @@ describe("AgentLimitBadge (render)", () => {
|
||||
onSetResumeAt={onSetResumeAt}
|
||||
/>,
|
||||
);
|
||||
fireEvent.change(screen.getByLabelText("resume time"), {
|
||||
fireEvent.change(screen.getByLabelText("heure de reprise"), {
|
||||
target: { value: "23:45" },
|
||||
});
|
||||
fireEvent.click(screen.getByLabelText("schedule resume"));
|
||||
fireEvent.click(screen.getByLabelText("programmer la reprise"));
|
||||
expect(onSetResumeAt).toHaveBeenCalledTimes(1);
|
||||
const ms = onSetResumeAt.mock.calls[0][0] as number;
|
||||
const d = new Date(ms);
|
||||
@ -193,7 +193,7 @@ describe("AgentLimitBadge (render)", () => {
|
||||
/>,
|
||||
);
|
||||
expect(
|
||||
(screen.getByLabelText("schedule resume") as HTMLButtonElement).disabled,
|
||||
(screen.getByLabelText("programmer la reprise") as HTMLButtonElement).disabled,
|
||||
).toBe(true);
|
||||
expect(onSetResumeAt).not.toHaveBeenCalled();
|
||||
});
|
||||
@ -209,11 +209,11 @@ describe("AgentLimitBadge (render)", () => {
|
||||
/>,
|
||||
);
|
||||
// Countdown label present (≈ "reprise dans 1m 30s").
|
||||
expect(screen.getByLabelText("resume countdown").textContent).toMatch(
|
||||
expect(screen.getByLabelText("compte à rebours de reprise").textContent).toMatch(
|
||||
/reprise dans/,
|
||||
);
|
||||
|
||||
const btn = screen.getByLabelText("cancel resume");
|
||||
const btn = screen.getByLabelText("annuler la reprise");
|
||||
fireEvent.click(btn);
|
||||
expect(onCancelResume).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
@ -228,7 +228,7 @@ describe("AgentLimitBadge (render)", () => {
|
||||
/>,
|
||||
);
|
||||
expect(
|
||||
(screen.getByLabelText("cancel resume") as HTMLButtonElement).disabled,
|
||||
(screen.getByLabelText("annuler la reprise") as HTMLButtonElement).disabled,
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
@ -14,6 +14,7 @@
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
import { Button, Input, cn } from "@/shared";
|
||||
import { useI18n } from "@/i18n";
|
||||
import type { AgentLimitState } from "./useAgents";
|
||||
|
||||
/**
|
||||
@ -75,6 +76,7 @@ export function AgentLimitBadge({
|
||||
onSetResumeAt,
|
||||
busy = false,
|
||||
}: AgentLimitBadgeProps) {
|
||||
const { tr } = useI18n();
|
||||
const { limitedUntil, resumeFireAt, suspected } = state;
|
||||
|
||||
// Tick a local clock once per second only while a resume countdown is armed,
|
||||
@ -106,17 +108,17 @@ export function AgentLimitBadge({
|
||||
|
||||
const limitLabel =
|
||||
limitedUntil !== undefined
|
||||
? `limité jusqu'à ${formatResetTime(limitedUntil)}`
|
||||
: "limité";
|
||||
? tr(`limité jusqu'à ${formatResetTime(limitedUntil)}`, `limited until ${formatResetTime(limitedUntil)}`)
|
||||
: tr("limité", "limited");
|
||||
|
||||
return (
|
||||
<span className="flex min-w-0 flex-wrap items-center gap-2">
|
||||
<span
|
||||
aria-label="session limit"
|
||||
aria-label={tr("limite de session", "session limit")}
|
||||
title={
|
||||
suspected
|
||||
? "Limite de session détectée sans heure fiable"
|
||||
: "Limite de session atteinte"
|
||||
? tr("Limite de session détectée sans heure fiable", "Session limit detected without a reliable time")
|
||||
: tr("Limite de session atteinte", "Session limit reached")
|
||||
}
|
||||
className={cn(
|
||||
"rounded-full px-2 py-0.5 text-xs font-medium",
|
||||
@ -134,14 +136,14 @@ export function AgentLimitBadge({
|
||||
<form
|
||||
onSubmit={handleSubmitResumeTime}
|
||||
className="flex items-center gap-2"
|
||||
aria-label="resume time form"
|
||||
aria-label={tr("formulaire d'heure de reprise", "resume time form")}
|
||||
>
|
||||
<span className="text-xs text-muted" role="note">
|
||||
heure inconnue
|
||||
{tr("heure inconnue", "unknown time")}
|
||||
</span>
|
||||
<Input
|
||||
type="time"
|
||||
aria-label="resume time"
|
||||
aria-label={tr("heure de reprise", "resume time")}
|
||||
value={resumeTime}
|
||||
disabled={busy}
|
||||
onChange={(e) => setResumeTime(e.target.value)}
|
||||
@ -151,27 +153,27 @@ export function AgentLimitBadge({
|
||||
type="submit"
|
||||
size="sm"
|
||||
variant="primary"
|
||||
aria-label="schedule resume"
|
||||
aria-label={tr("programmer la reprise", "schedule resume")}
|
||||
disabled={busy || resumeTime.trim() === ""}
|
||||
>
|
||||
Programmer la reprise
|
||||
{tr("Programmer la reprise", "Schedule resume")}
|
||||
</Button>
|
||||
</form>
|
||||
)}
|
||||
|
||||
{resumeFireAt !== undefined && (
|
||||
<span className="flex items-center gap-2">
|
||||
<span aria-label="resume countdown" className="text-xs text-muted">
|
||||
reprise dans {formatCountdown(resumeFireAt - now)}
|
||||
<span aria-label={tr("compte à rebours de reprise", "resume countdown")} className="text-xs text-muted">
|
||||
{tr("reprise dans", "resuming in")} {formatCountdown(resumeFireAt - now)}
|
||||
</span>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
aria-label="cancel resume"
|
||||
aria-label={tr("annuler la reprise", "cancel resume")}
|
||||
disabled={busy}
|
||||
onClick={onCancelResume}
|
||||
>
|
||||
Annuler la reprise
|
||||
{tr("Annuler la reprise", "Cancel resume")}
|
||||
</Button>
|
||||
</span>
|
||||
)}
|
||||
|
||||
@ -17,6 +17,7 @@
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
import { Button, Input, Panel, SmallDropdown, Spinner, cn } from "@/shared";
|
||||
import { useI18n } from "@/i18n";
|
||||
import { TerminalView } from "@/features/terminals/TerminalView";
|
||||
import { AnnouncementsPreview } from "@/features/announcements";
|
||||
import { useDrift } from "@/features/templates/useDrift";
|
||||
@ -45,6 +46,7 @@ export interface AgentsPanelProps {
|
||||
}
|
||||
|
||||
export function AgentsPanel({ projectId, projectRoot = "" }: AgentsPanelProps) {
|
||||
const { tr } = useI18n();
|
||||
const vm = useAgents(projectId);
|
||||
const drift = useDrift(projectId);
|
||||
const gateways = useGateways();
|
||||
@ -260,7 +262,7 @@ export function AgentsPanel({ projectId, projectRoot = "" }: AgentsPanelProps) {
|
||||
};
|
||||
|
||||
return (
|
||||
<Panel title="Agents" className="flex flex-col gap-0">
|
||||
<Panel title={tr("Agents", "Agents")} className="flex flex-col gap-0">
|
||||
{vm.error && (
|
||||
<p
|
||||
role="alert"
|
||||
@ -280,12 +282,12 @@ export function AgentsPanel({ projectId, projectRoot = "" }: AgentsPanelProps) {
|
||||
htmlFor="agent-name-input"
|
||||
className="text-xs font-medium text-muted"
|
||||
>
|
||||
Name
|
||||
{tr("Nom", "Name")}
|
||||
</label>
|
||||
<Input
|
||||
id="agent-name-input"
|
||||
aria-label="agent name"
|
||||
placeholder="My agent"
|
||||
aria-label={tr("Nom de l’agent", "Agent name")}
|
||||
placeholder={tr("Mon agent", "My agent")}
|
||||
value={newName}
|
||||
onChange={(e) => setNewName(e.target.value)}
|
||||
className="w-full"
|
||||
@ -298,39 +300,38 @@ export function AgentsPanel({ projectId, projectRoot = "" }: AgentsPanelProps) {
|
||||
htmlFor="agent-template-select"
|
||||
className="text-xs font-medium text-muted"
|
||||
>
|
||||
Template
|
||||
{tr("Modèle", "Template")}
|
||||
</label>
|
||||
<SmallDropdown
|
||||
id="agent-template-select"
|
||||
aria-label="agent template"
|
||||
aria-label={tr("Modèle d’agent", "Agent template")}
|
||||
value={newTemplateId}
|
||||
onChange={handleTemplateChange}
|
||||
className="w-full"
|
||||
disabled={vm.busy}
|
||||
size="md"
|
||||
options={[
|
||||
{ value: "", label: "(none / from scratch)" },
|
||||
{ value: "", label: tr("(aucun / partir de zéro)", "(none / from scratch)") },
|
||||
...templates.map((t) => ({ value: t.id, label: t.name })),
|
||||
]}
|
||||
/>
|
||||
{selectedTemplate && (
|
||||
<div className="rounded-md border border-border bg-raised px-3 py-2 text-xs text-muted">
|
||||
<div>
|
||||
Nom initial:{" "}
|
||||
{tr("Nom initial", "Initial name")}: {" "}
|
||||
<span className="text-content">
|
||||
{selectedTemplate.defaultAgentName || selectedTemplate.name}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
Modèle appliqué:{" "}
|
||||
{tr("Modèle appliqué", "Applied model")}: {" "}
|
||||
<span className="text-content">
|
||||
{templateProfileLabel(selectedTemplate, vm.profiles)}
|
||||
{templateProfileLabel(selectedTemplate, vm.profiles, tr)}
|
||||
</span>
|
||||
</div>
|
||||
{selectedTemplateMissingProfile && (
|
||||
<p role="alert" className="mt-1 text-danger">
|
||||
Ce template n'a pas de modèle AI par défaut. Choisis un autre
|
||||
template ou édite-le avant de créer l'agent.
|
||||
{tr("Ce modèle n’a pas de profil IA par défaut. Choisissez un autre modèle ou modifiez-le avant de créer l’agent.", "This template has no default AI profile. Choose another template or edit it before creating the agent.")}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
@ -344,21 +345,21 @@ export function AgentsPanel({ projectId, projectRoot = "" }: AgentsPanelProps) {
|
||||
htmlFor="agent-profile-select"
|
||||
className="text-xs font-medium text-muted"
|
||||
>
|
||||
Profile
|
||||
{tr("Profil", "Profile")}
|
||||
{vm.profiles.length === 0 && (
|
||||
<span className="ml-1 text-faint">(no profiles configured — you can still enter an id)</span>
|
||||
<span className="ml-1 text-faint">{tr("(aucun profil configuré — vous pouvez saisir un identifiant)", "(no profiles configured — you can still enter an id)")}</span>
|
||||
)}
|
||||
</label>
|
||||
{vm.profiles.length > 0 ? (
|
||||
<SmallDropdown
|
||||
id="agent-profile-select"
|
||||
aria-label="agent profile"
|
||||
aria-label={tr("Profil de l’agent", "Agent profile")}
|
||||
value={newProfileId}
|
||||
onChange={setNewProfileId}
|
||||
className="w-full"
|
||||
size="md"
|
||||
options={[
|
||||
{ value: "", label: "— select profile —" },
|
||||
{ value: "", label: tr("— sélectionner un profil —", "— select profile —") },
|
||||
...vm.profiles.map((p) => ({
|
||||
value: p.id,
|
||||
label: profileLabel(p),
|
||||
@ -368,8 +369,8 @@ export function AgentsPanel({ projectId, projectRoot = "" }: AgentsPanelProps) {
|
||||
) : (
|
||||
<Input
|
||||
id="agent-profile-select"
|
||||
aria-label="agent profile"
|
||||
placeholder="profile-id (optional)"
|
||||
aria-label={tr("Profil de l’agent", "Agent profile")}
|
||||
placeholder={tr("identifiant-profil (facultatif)", "profile-id (optional)")}
|
||||
value={newProfileId}
|
||||
onChange={(e) => setNewProfileId(e.target.value)}
|
||||
className="min-w-32"
|
||||
@ -381,19 +382,19 @@ export function AgentsPanel({ projectId, projectRoot = "" }: AgentsPanelProps) {
|
||||
<Button
|
||||
type="submit"
|
||||
variant="primary"
|
||||
aria-label="create agent"
|
||||
aria-label={tr("Créer l’agent", "Create agent")}
|
||||
disabled={!canCreate}
|
||||
loading={vm.busy}
|
||||
className="self-end"
|
||||
>
|
||||
Create
|
||||
{tr("Créer", "Create")}
|
||||
</Button>
|
||||
</form>
|
||||
|
||||
{/* ── Agent list ── */}
|
||||
<div className="p-4">
|
||||
{vm.agents.length === 0 ? (
|
||||
<p className="text-sm text-muted">No agents yet.</p>
|
||||
<p className="text-sm text-muted">{tr("Aucun agent pour le moment.", "No agents yet.")}</p>
|
||||
) : (
|
||||
<ul className="flex flex-col divide-y divide-border">
|
||||
{vm.agents.map((a) => {
|
||||
@ -438,27 +439,27 @@ export function AgentsPanel({ projectId, projectRoot = "" }: AgentsPanelProps) {
|
||||
<span className="font-medium text-content">{a.name}</span>
|
||||
{a.isOrchestrator && (
|
||||
<span
|
||||
aria-label="Writer contexte"
|
||||
aria-label={tr("Responsable du contexte", "Context writer")}
|
||||
className="rounded-full border border-border-strong bg-raised px-2 py-0.5 text-xs font-medium text-content"
|
||||
>
|
||||
Writer contexte
|
||||
{tr("Responsable du contexte", "Context writer")}
|
||||
</span>
|
||||
)}
|
||||
{agentDrift && (
|
||||
<span
|
||||
aria-label="update available"
|
||||
aria-label={tr("Mise à jour disponible", "Update available")}
|
||||
className="rounded-full bg-primary/20 px-2 py-0.5 text-xs font-medium text-primary"
|
||||
>
|
||||
update available
|
||||
{tr("mise à jour disponible", "update available")}
|
||||
</span>
|
||||
)}
|
||||
{delegationSource && (
|
||||
<span
|
||||
aria-label={`delegation source ${delegationSource}`}
|
||||
aria-label={tr(`source de délégation ${delegationSource}`, `delegation source ${delegationSource}`)}
|
||||
title={
|
||||
delegationSource === "mcp"
|
||||
? "Last delegation arrived via the MCP server"
|
||||
: "Last delegation arrived via .ideai/requests"
|
||||
? tr("La dernière délégation est arrivée via le serveur MCP", "Last delegation arrived via the MCP server")
|
||||
: tr("La dernière délégation est arrivée via .ideai/requests", "Last delegation arrived via .ideai/requests")
|
||||
}
|
||||
className="rounded-full bg-raised px-2 py-0.5 text-xs font-medium uppercase tracking-wide text-muted"
|
||||
>
|
||||
@ -470,36 +471,35 @@ export function AgentsPanel({ projectId, projectRoot = "" }: AgentsPanelProps) {
|
||||
<span className="text-xs text-muted">{profileName}</span>
|
||||
{live && (
|
||||
<span className="block w-full min-w-0 truncate text-xs text-primary">
|
||||
running in IdeA · {live.sessionId ?? live.nodeId}
|
||||
{tr("actif dans IdeA", "running in IdeA")} · {live.sessionId ?? live.nodeId}
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
|
||||
{isSelected && (
|
||||
<section
|
||||
aria-label={`Rôle dans le projet — ${a.name}`}
|
||||
aria-label={`${tr("Rôle dans le projet", "Role in project")} — ${a.name}`}
|
||||
className="rounded-md border border-border bg-surface p-3"
|
||||
>
|
||||
<h4 className="text-xs font-semibold uppercase tracking-wide text-muted">
|
||||
Rôle dans le projet
|
||||
{tr("Rôle dans le projet", "Role in project")}
|
||||
</h4>
|
||||
<label className="mt-2 flex cursor-pointer items-start gap-2">
|
||||
<input
|
||||
type="radio"
|
||||
name="project-context-writer"
|
||||
aria-label={`Writer du contexte projet global pour ${a.name}`}
|
||||
aria-label={tr(`Responsable du contexte global du projet pour ${a.name}`, `Global project context writer for ${a.name}`)}
|
||||
checked={a.isOrchestrator === true}
|
||||
disabled={vm.busy}
|
||||
onChange={() => requestWriterChange(a)}
|
||||
className="mt-0.5"
|
||||
/>
|
||||
<span className="text-sm font-medium text-content">
|
||||
Writer du contexte projet global
|
||||
{tr("Responsable du contexte global du projet", "Global project context writer")}
|
||||
</span>
|
||||
</label>
|
||||
<p className="mt-1 pl-5 text-xs text-muted">
|
||||
Peut modifier directement le contexte partagé du projet.
|
||||
Un seul agent peut avoir ce rôle.
|
||||
{tr("Peut modifier directement le contexte partagé du projet. Un seul agent peut avoir ce rôle.", "Can directly edit the project’s shared context. Only one agent can have this role.")}
|
||||
</p>
|
||||
</section>
|
||||
)}
|
||||
@ -531,7 +531,7 @@ export function AgentsPanel({ projectId, projectRoot = "" }: AgentsPanelProps) {
|
||||
engine abandons the conversation history → confirmation. */}
|
||||
{vm.profiles.length > 0 && (
|
||||
<SmallDropdown
|
||||
aria-label={`profile for ${a.name}`}
|
||||
aria-label={tr(`Profil de ${a.name}`, `Profile for ${a.name}`)}
|
||||
value={a.profileId}
|
||||
disabled={vm.busy}
|
||||
onChange={(profileId) => {
|
||||
@ -562,13 +562,13 @@ export function AgentsPanel({ projectId, projectRoot = "" }: AgentsPanelProps) {
|
||||
<Button
|
||||
size="sm"
|
||||
variant="primary"
|
||||
aria-label={`sync ${a.name}`}
|
||||
aria-label={tr(`Synchroniser ${a.name}`, `Sync ${a.name}`)}
|
||||
disabled={vm.busy || drift.driftBusy}
|
||||
onClick={() =>
|
||||
void drift.syncAgent(a.id, vm.refresh)
|
||||
}
|
||||
>
|
||||
Sync
|
||||
{tr("Synchroniser", "Sync")}
|
||||
</Button>
|
||||
)}
|
||||
{isRunning || live ? (
|
||||
@ -577,41 +577,41 @@ export function AgentsPanel({ projectId, projectRoot = "" }: AgentsPanelProps) {
|
||||
variant="ghost"
|
||||
aria-label={
|
||||
stopState === "stopping"
|
||||
? `Arrêt en cours… — ${a.name}`
|
||||
? tr(`Arrêt en cours… — ${a.name}`, `Stopping… — ${a.name}`)
|
||||
: stopState === "incomplete"
|
||||
? `Réessayer l’arrêt — ${a.name}`
|
||||
: `stop ${a.name}`
|
||||
? tr(`Réessayer l’arrêt — ${a.name}`, `Retry stopping — ${a.name}`)
|
||||
: tr(`Arrêter ${a.name}`, `Stop ${a.name}`)
|
||||
}
|
||||
disabled={stopState === "stopping"}
|
||||
onClick={() => void handleStop(a.id)}
|
||||
className="text-danger hover:text-danger"
|
||||
>
|
||||
{stopState === "stopping"
|
||||
? "Arrêt en cours…"
|
||||
? tr("Arrêt en cours…", "Stopping…")
|
||||
: stopState === "incomplete"
|
||||
? "Réessayer l’arrêt"
|
||||
: "Stop"}
|
||||
? tr("Réessayer l’arrêt", "Retry stopping")
|
||||
: tr("Arrêter", "Stop")}
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="primary"
|
||||
aria-label={`launch ${a.name}`}
|
||||
aria-label={tr(`Lancer ${a.name}`, `Launch ${a.name}`)}
|
||||
disabled={vm.busy || activeAgentId !== null}
|
||||
onClick={() => handleLaunch(a.id)}
|
||||
>
|
||||
Launch
|
||||
{tr("Lancer", "Launch")}
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
aria-label={`delete ${a.name}`}
|
||||
aria-label={tr(`Supprimer ${a.name}`, `Delete ${a.name}`)}
|
||||
disabled={vm.busy}
|
||||
onClick={() => void vm.deleteAgent(a.id)}
|
||||
className="text-danger hover:text-danger"
|
||||
>
|
||||
Delete
|
||||
{tr("Supprimer", "Delete")}
|
||||
</Button>
|
||||
</div>
|
||||
</li>
|
||||
@ -653,12 +653,12 @@ export function AgentsPanel({ projectId, projectRoot = "" }: AgentsPanelProps) {
|
||||
<div className="flex flex-col gap-2 border-t border-border p-4">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<h4 className="text-sm font-semibold text-content">
|
||||
Context — {selectedAgent.name}
|
||||
{tr("Contexte", "Context")} — {selectedAgent.name}
|
||||
</h4>
|
||||
<code className="text-xs text-muted">{selectedAgent.contextPath}</code>
|
||||
</div>
|
||||
<textarea
|
||||
aria-label="agent context"
|
||||
aria-label={tr("Contexte de l’agent", "Agent context")}
|
||||
value={editedContext}
|
||||
onChange={(e) => setEditedContext(e.target.value)}
|
||||
rows={10}
|
||||
@ -677,7 +677,7 @@ export function AgentsPanel({ projectId, projectRoot = "" }: AgentsPanelProps) {
|
||||
disabled={vm.busy}
|
||||
onClick={() => void vm.saveContext(editedContext)}
|
||||
>
|
||||
Save
|
||||
{tr("Enregistrer", "Save")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@ -688,34 +688,33 @@ export function AgentsPanel({ projectId, projectRoot = "" }: AgentsPanelProps) {
|
||||
<div
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-label="Confirm profile change"
|
||||
aria-label={tr("Confirmer le changement de profil", "Confirm profile change")}
|
||||
className="fixed inset-0 z-50 flex items-center justify-center bg-black/55 p-4"
|
||||
>
|
||||
<div className="w-full max-w-sm rounded-lg border border-border bg-surface p-5 shadow-xl">
|
||||
<h4 className="text-sm font-semibold text-content">
|
||||
Changer le moteur IA
|
||||
{tr("Changer le moteur IA", "Change AI engine")}
|
||||
</h4>
|
||||
<p className="mt-2 text-sm text-muted">
|
||||
Changer le moteur abandonne l'historique de conversation (le
|
||||
contexte et la mémoire sont conservés). Continuer ?
|
||||
{tr("Changer le moteur abandonne l’historique de conversation (le contexte et la mémoire sont conservés). Continuer ?", "Changing the engine discards conversation history (context and memory are preserved). Continue?")}
|
||||
</p>
|
||||
<div className="mt-4 flex items-center justify-end gap-2">
|
||||
<Button
|
||||
variant="ghost"
|
||||
aria-label="cancel profile change"
|
||||
aria-label={tr("Annuler le changement de profil", "Cancel profile change")}
|
||||
disabled={vm.busy}
|
||||
onClick={() => setPendingProfileChange(null)}
|
||||
>
|
||||
Annuler
|
||||
{tr("Annuler", "Cancel")}
|
||||
</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
aria-label="confirm profile change"
|
||||
aria-label={tr("Confirmer le changement de profil", "Confirm profile change")}
|
||||
loading={vm.busy}
|
||||
disabled={vm.busy}
|
||||
onClick={() => void confirmProfileChange()}
|
||||
>
|
||||
Continuer
|
||||
{tr("Continuer", "Continue")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@ -726,34 +725,33 @@ export function AgentsPanel({ projectId, projectRoot = "" }: AgentsPanelProps) {
|
||||
<div
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-label="Confirmer le changement de writer contexte"
|
||||
aria-label={tr("Confirmer le changement de responsable du contexte", "Confirm context writer change")}
|
||||
className="fixed inset-0 z-50 flex items-center justify-center bg-black/55 p-4"
|
||||
>
|
||||
<div className="w-full max-w-sm rounded-lg border border-border bg-surface p-5 shadow-xl">
|
||||
<h4 className="text-sm font-semibold text-content">
|
||||
Changer le writer du contexte
|
||||
{tr("Changer le responsable du contexte", "Change context writer")}
|
||||
</h4>
|
||||
<p className="mt-2 text-sm text-muted">
|
||||
Le rôle sera retiré de {pendingWriterChange.currentWriterName} et
|
||||
attribué à {pendingWriterChange.agentName}. Continuer ?
|
||||
{tr(`Le rôle sera retiré de ${pendingWriterChange.currentWriterName} et attribué à ${pendingWriterChange.agentName}. Continuer ?`, `The role will be removed from ${pendingWriterChange.currentWriterName} and assigned to ${pendingWriterChange.agentName}. Continue?`)}
|
||||
</p>
|
||||
<div className="mt-4 flex items-center justify-end gap-2">
|
||||
<Button
|
||||
variant="ghost"
|
||||
aria-label="Annuler le changement de writer contexte"
|
||||
aria-label={tr("Annuler le changement de responsable du contexte", "Cancel context writer change")}
|
||||
disabled={vm.busy}
|
||||
onClick={() => setPendingWriterChange(null)}
|
||||
>
|
||||
Annuler
|
||||
{tr("Annuler", "Cancel")}
|
||||
</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
aria-label="Confirmer le changement de writer contexte"
|
||||
aria-label={tr("Confirmer le changement de responsable du contexte", "Confirm context writer change")}
|
||||
loading={vm.busy}
|
||||
disabled={vm.busy}
|
||||
onClick={() => void confirmWriterChange()}
|
||||
>
|
||||
Continuer
|
||||
{tr("Continuer", "Continue")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@ -766,23 +764,13 @@ export function AgentsPanel({ projectId, projectRoot = "" }: AgentsPanelProps) {
|
||||
const EFFORT_DEFAULT_VALUE = "__profile_default__";
|
||||
const EFFORT_CUSTOM_VALUE = "__custom_effort__";
|
||||
|
||||
const GENERIC_EFFORT_OPTIONS: EffortOption[] = [
|
||||
{
|
||||
value: "low",
|
||||
label: "Rapide (par défaut)",
|
||||
hint: "Fallback générique léger.",
|
||||
},
|
||||
{
|
||||
value: "medium",
|
||||
label: "Standard (par défaut)",
|
||||
hint: "Fallback générique équilibré.",
|
||||
},
|
||||
{
|
||||
value: "high",
|
||||
label: "Approfondi (par défaut)",
|
||||
hint: "Fallback générique profond.",
|
||||
},
|
||||
];
|
||||
function genericEffortOptions(tr: (fr: string, en: string) => string): EffortOption[] {
|
||||
return [
|
||||
{ value: "low", label: tr("Rapide (par défaut)", "Fast (default)"), hint: tr("Réglage générique léger.", "Light generic fallback.") },
|
||||
{ value: "medium", label: tr("Standard (par défaut)", "Standard (default)"), hint: tr("Réglage générique équilibré.", "Balanced generic fallback.") },
|
||||
{ value: "high", label: tr("Approfondi (par défaut)", "Thorough (default)"), hint: tr("Réglage générique approfondi.", "Deep generic fallback.") },
|
||||
];
|
||||
}
|
||||
|
||||
function rawEffortValue(selection: EffortSelection | undefined): string {
|
||||
return selection?.value ?? "";
|
||||
@ -799,11 +787,12 @@ function EffortSelector({
|
||||
busy: boolean;
|
||||
onChange: (effort: EffortSelection | null) => void;
|
||||
}) {
|
||||
const { tr } = useI18n();
|
||||
const nativeOptions = profile?.effortOptions ?? [];
|
||||
const hasNativeOptions = nativeOptions.length > 0;
|
||||
const displayedOptions = hasNativeOptions
|
||||
? nativeOptions
|
||||
: GENERIC_EFFORT_OPTIONS;
|
||||
: genericEffortOptions(tr);
|
||||
const [customText, setCustomText] = useState(rawEffortValue(agent.effort));
|
||||
const [forceCustom, setForceCustom] = useState(agent.effort?.kind === "custom");
|
||||
|
||||
@ -843,7 +832,7 @@ function EffortSelector({
|
||||
return (
|
||||
<span className="flex min-w-[11rem] max-w-full flex-wrap items-center gap-1.5">
|
||||
<SmallDropdown
|
||||
aria-label={`effort for ${agent.name}`}
|
||||
aria-label={tr(`Effort pour ${agent.name}`, `Effort for ${agent.name}`)}
|
||||
value={selectedOption}
|
||||
disabled={busy}
|
||||
onChange={(value) => {
|
||||
@ -868,20 +857,20 @@ function EffortSelector({
|
||||
}
|
||||
}}
|
||||
options={[
|
||||
{ value: EFFORT_DEFAULT_VALUE, label: "Effort: profil" },
|
||||
{ value: EFFORT_DEFAULT_VALUE, label: tr("Effort : profil", "Effort: profile") },
|
||||
...displayedOptions.map((option) => ({
|
||||
value: `${hasNativeOptions ? "preset" : "fallback"}:${option.value}`,
|
||||
label: option.label,
|
||||
})),
|
||||
{ value: EFFORT_CUSTOM_VALUE, label: "Personnalisé" },
|
||||
{ value: EFFORT_CUSTOM_VALUE, label: tr("Personnalisé", "Custom") },
|
||||
]}
|
||||
/>
|
||||
{customVisible && (
|
||||
<Input
|
||||
aria-label={`custom effort for ${agent.name}`}
|
||||
aria-label={tr(`Effort personnalisé pour ${agent.name}`, `Custom effort for ${agent.name}`)}
|
||||
value={customText}
|
||||
disabled={busy}
|
||||
placeholder="valeur brute"
|
||||
placeholder={tr("valeur brute", "raw value")}
|
||||
onChange={(event) => setCustomText(event.target.value)}
|
||||
onBlur={commitCustom}
|
||||
onKeyDown={(event) => {
|
||||
@ -903,21 +892,22 @@ function NetworkPermissionBadge({
|
||||
}: {
|
||||
state: ResolvedAgentSystemPermissions;
|
||||
}) {
|
||||
const { tr } = useI18n();
|
||||
const locked = state.runtimeLock.state === "locked";
|
||||
const label = locked
|
||||
? "Verrouillé"
|
||||
? tr("Verrouillé", "Locked")
|
||||
: state.effective === "allow"
|
||||
? "Autorisé"
|
||||
? tr("Autorisé", "Allowed")
|
||||
: state.effective === "deny"
|
||||
? "Interdit"
|
||||
: "Demande";
|
||||
? tr("Interdit", "Denied")
|
||||
: tr("Demande", "Ask");
|
||||
return (
|
||||
<span
|
||||
aria-label={`network ${label}`}
|
||||
aria-label={`${tr("Réseau", "Network")} ${label}`}
|
||||
title={
|
||||
locked
|
||||
? (state.runtimeLock.reason ?? "Réseau verrouillé par le runtime")
|
||||
: `État effectif : ${label}`
|
||||
? (state.runtimeLock.reason ?? tr("Réseau verrouillé par l’exécution", "Network locked by runtime"))
|
||||
: `${tr("État effectif", "Effective state")} : ${label}`
|
||||
}
|
||||
className={cn(
|
||||
"rounded-full px-2 py-0.5 text-xs font-medium",
|
||||
@ -930,7 +920,7 @@ function NetworkPermissionBadge({
|
||||
: "bg-primary/15 text-primary",
|
||||
)}
|
||||
>
|
||||
Réseau: {label}
|
||||
{tr("Réseau", "Network")}: {label}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
@ -11,6 +11,7 @@ import {
|
||||
__resetProjectCliInteractionActivityForTests,
|
||||
hasProjectCliInteraction,
|
||||
} from "@/features/projects/projectCliInteractionActivity";
|
||||
import { publishProjectIconChanged } from "@/features/projects/projectIcons";
|
||||
import {
|
||||
PluginCommandRegistry,
|
||||
PluginLayoutRegistry,
|
||||
@ -197,6 +198,23 @@ describe("CustomAgentChatView", () => {
|
||||
__resetProjectCliInteractionActivityForTests();
|
||||
});
|
||||
|
||||
it("renders the project icon as a non-interactive custom-CLI watermark and updates it immediately", async () => {
|
||||
const agent = {
|
||||
reattachAgentChat: vi.fn(async (sessionId: string) => ({ sessionId, scrollback: [] })),
|
||||
sendAgentChat: vi.fn(), cancelAgentChat: vi.fn(), closeAgentChat: vi.fn(),
|
||||
};
|
||||
const project = { readProjectSettings: vi.fn(async () => ({ themeColor: null, icon: { set: "devicon" as const, name: "python" } })) };
|
||||
render(<DIProvider gateways={{ agent, project, system: { pickFile: vi.fn(async () => null) } } as unknown as Gateways}>
|
||||
<CustomAgentChatView projectId="project-1" agentId="agent-1" agentName="Worker" profile={profile} cwd="/repo" nodeId="node-1" sessionId="chat-1" conversationId="conversation-1" onSessionId={vi.fn()} onConversationId={vi.fn()} />
|
||||
</DIProvider>);
|
||||
|
||||
const watermark = await screen.findByTestId("project-icon-watermark");
|
||||
expect(watermark.style.pointerEvents).toBe("none");
|
||||
expect(watermark.textContent).toBe("Py");
|
||||
publishProjectIconChanged({ projectId: "project-1", icon: { set: "lucide", name: "star" } });
|
||||
await waitFor(() => expect(watermark.textContent).toBe("★"));
|
||||
});
|
||||
|
||||
it("settles an isolated conversation id without recovered turns as an actionable unavailable state (#307/#314)", async () => {
|
||||
const agent = {
|
||||
launchAgentChat: vi.fn(async () => ({
|
||||
@ -246,7 +264,7 @@ describe("CustomAgentChatView", () => {
|
||||
expect(
|
||||
await screen.findByText("La conversation liée n’a pas pu être restaurée."),
|
||||
).toBeTruthy();
|
||||
expect(screen.getByText("Son transcript n’est plus disponible après la reprise.")).toBeTruthy();
|
||||
expect(screen.getByText("Sa transcription n’est plus disponible après la reprise.")).toBeTruthy();
|
||||
expect(
|
||||
screen.queryByText("Conversation liée — récupération du transcript en cours."),
|
||||
).toBeNull();
|
||||
@ -370,10 +388,10 @@ describe("CustomAgentChatView", () => {
|
||||
});
|
||||
|
||||
expect(
|
||||
await screen.findByText("------- redemarrage de l'agent -------"),
|
||||
await screen.findByText("------- redémarrage de l’agent -------"),
|
||||
).not.toBeNull();
|
||||
expect(
|
||||
screen.getByText("------- redemarrage de l'agent termine -------"),
|
||||
screen.getByText("------- redémarrage de l’agent terminé -------"),
|
||||
).not.toBeNull();
|
||||
expect(screen.getAllByTestId("agent-restart-separator")).toHaveLength(2);
|
||||
|
||||
@ -403,7 +421,7 @@ describe("CustomAgentChatView", () => {
|
||||
});
|
||||
});
|
||||
expect(
|
||||
screen.getByText("------- redemarrage de l'agent echoue -------"),
|
||||
screen.getByText("------- redémarrage de l’agent échoué -------"),
|
||||
).not.toBeNull();
|
||||
expect(screen.getByText("runtime unavailable")).not.toBeNull();
|
||||
});
|
||||
@ -676,7 +694,7 @@ describe("CustomAgentChatView", () => {
|
||||
</DIProvider>,
|
||||
);
|
||||
|
||||
const composer = await screen.findByLabelText(/message CLI custom/) as HTMLTextAreaElement;
|
||||
const composer = await screen.findByLabelText(/Message CLI personnalisée/) as HTMLTextAreaElement;
|
||||
await waitFor(() => expect(agent.reattachAgentChat).toHaveBeenCalled());
|
||||
|
||||
fireEvent.change(composer, { target: { value: "first prompt" } });
|
||||
@ -712,7 +730,7 @@ describe("CustomAgentChatView", () => {
|
||||
it("recalls history from the exact end of a multiline draft and restores it verbatim", async () => {
|
||||
const agent = renderHistoryChat();
|
||||
const composer = (await screen.findByLabelText(
|
||||
/message CLI custom/,
|
||||
/Message CLI personnalisée/,
|
||||
)) as HTMLTextAreaElement;
|
||||
await waitFor(() => expect(agent.reattachAgentChat).toHaveBeenCalled());
|
||||
await sendHistoryPrompt(composer, agent, "historic prompt");
|
||||
@ -742,7 +760,7 @@ describe("CustomAgentChatView", () => {
|
||||
it("keeps ArrowUp native away from a simple end caret or during modified/IME input", async () => {
|
||||
const agent = renderHistoryChat();
|
||||
const composer = (await screen.findByLabelText(
|
||||
/message CLI custom/,
|
||||
/Message CLI personnalisée/,
|
||||
)) as HTMLTextAreaElement;
|
||||
await waitFor(() => expect(agent.reattachAgentChat).toHaveBeenCalled());
|
||||
await sendHistoryPrompt(composer, agent, "historic prompt");
|
||||
@ -778,7 +796,7 @@ describe("CustomAgentChatView", () => {
|
||||
it("does not consume ArrowUp with empty history and never mutates recalled entries", async () => {
|
||||
const agent = renderHistoryChat();
|
||||
const composer = (await screen.findByLabelText(
|
||||
/message CLI custom/,
|
||||
/Message CLI personnalisée/,
|
||||
)) as HTMLTextAreaElement;
|
||||
await waitFor(() => expect(agent.reattachAgentChat).toHaveBeenCalled());
|
||||
|
||||
@ -838,7 +856,7 @@ describe("CustomAgentChatView", () => {
|
||||
</DIProvider>,
|
||||
);
|
||||
|
||||
let composer = await screen.findByLabelText(/message CLI custom/) as HTMLTextAreaElement;
|
||||
let composer = await screen.findByLabelText(/Message CLI personnalisée/) as HTMLTextAreaElement;
|
||||
fireEvent.change(composer, { target: { value: "remember me" } });
|
||||
fireEvent.click(screen.getByRole("button", { name: "Envoyer" }));
|
||||
await waitFor(() => expect(agent.sendAgentChat).toHaveBeenCalledWith(
|
||||
@ -859,7 +877,7 @@ describe("CustomAgentChatView", () => {
|
||||
</DIProvider>,
|
||||
);
|
||||
|
||||
composer = await screen.findByLabelText(/message CLI custom/) as HTMLTextAreaElement;
|
||||
composer = await screen.findByLabelText(/Message CLI personnalisée/) as HTMLTextAreaElement;
|
||||
fireEvent.keyDown(composer, { key: "ArrowUp" });
|
||||
await waitFor(() => expect(composer.value).toBe("remember me"));
|
||||
});
|
||||
@ -897,7 +915,7 @@ describe("CustomAgentChatView", () => {
|
||||
</DIProvider>,
|
||||
);
|
||||
|
||||
const composer = await screen.findByLabelText(/message CLI custom/);
|
||||
const composer = await screen.findByLabelText(/Message CLI personnalisée/);
|
||||
fireEvent.change(composer, { target: { value: "persist before send" } });
|
||||
fireEvent.click(screen.getByRole("button", { name: "Envoyer" }));
|
||||
|
||||
@ -911,7 +929,7 @@ describe("CustomAgentChatView", () => {
|
||||
});
|
||||
try {
|
||||
const agent = renderHistoryChat();
|
||||
const composer = await screen.findByLabelText(/message CLI custom/);
|
||||
const composer = await screen.findByLabelText(/Message CLI personnalisée/);
|
||||
fireEvent.change(composer, { target: { value: "still send" } });
|
||||
fireEvent.click(screen.getByRole("button", { name: "Envoyer" }));
|
||||
|
||||
@ -1020,7 +1038,7 @@ describe("CustomAgentChatView", () => {
|
||||
),
|
||||
);
|
||||
|
||||
fireEvent.change(screen.getByLabelText(/message CLI custom/), {
|
||||
fireEvent.change(screen.getByLabelText(/Message CLI personnalisée/), {
|
||||
target: { value: "Initial prompt" },
|
||||
});
|
||||
nowMs = userReceivedAtMs;
|
||||
@ -1100,7 +1118,7 @@ describe("CustomAgentChatView", () => {
|
||||
),
|
||||
);
|
||||
|
||||
fireEvent.change(screen.getByLabelText(/message CLI custom/), {
|
||||
fireEvent.change(screen.getByLabelText(/Message CLI personnalisée/), {
|
||||
target: { value: "Initial prompt" },
|
||||
});
|
||||
nowMs = userReceivedAtMs;
|
||||
@ -1235,9 +1253,9 @@ describe("CustomAgentChatView", () => {
|
||||
|
||||
try {
|
||||
render(<Harness />);
|
||||
const composer = await screen.findByLabelText(/message CLI custom/) as HTMLTextAreaElement;
|
||||
const composer = await screen.findByLabelText(/Message CLI personnalisée/) as HTMLTextAreaElement;
|
||||
const handle = screen.getByRole("button", {
|
||||
name: "redimensionner le composer node-1",
|
||||
name: "Redimensionner l’éditeur node-1",
|
||||
});
|
||||
expect(composer.style.height).toBe("64px");
|
||||
expect(handle.style.cursor).toBe("ns-resize");
|
||||
@ -1401,7 +1419,7 @@ describe("CustomAgentChatView", () => {
|
||||
|
||||
expect(screen.getByText("Initial reply")).toBeTruthy();
|
||||
expect(screen.getByRole("button", { name: "Envoyer" }).hasAttribute("disabled")).toBe(true);
|
||||
expect(screen.getByLabelText(/message CLI custom/).hasAttribute("disabled")).toBe(true);
|
||||
expect(screen.getByLabelText(/Message CLI personnalisée/).hasAttribute("disabled")).toBe(true);
|
||||
|
||||
act(() => {
|
||||
secondReattach.resolve({ sessionId: "chat-session-1", scrollback });
|
||||
@ -1411,7 +1429,7 @@ describe("CustomAgentChatView", () => {
|
||||
expect(screen.getByText("Partial answer while away")).toBeTruthy();
|
||||
expect(screen.queryByText(/Reprise effectuée:/)).toBeNull();
|
||||
expect(screen.getByRole("button", { name: "Stop" })).toBeTruthy();
|
||||
expect(screen.getByLabelText(/message CLI custom/).hasAttribute("disabled")).toBe(true);
|
||||
expect(screen.getByLabelText(/Message CLI personnalisée/).hasAttribute("disabled")).toBe(true);
|
||||
});
|
||||
|
||||
it("restores visible conversation from conversationId cache when layout returns before sessionId", async () => {
|
||||
@ -1594,7 +1612,7 @@ describe("CustomAgentChatView", () => {
|
||||
),
|
||||
);
|
||||
|
||||
fireEvent.change(screen.getByLabelText(/message CLI custom/), {
|
||||
fireEvent.change(screen.getByLabelText(/Message CLI personnalisée/), {
|
||||
target: { value: "first turn" },
|
||||
});
|
||||
fireEvent.click(screen.getByRole("button", { name: "Envoyer" }));
|
||||
@ -1619,12 +1637,12 @@ describe("CustomAgentChatView", () => {
|
||||
);
|
||||
expect(agent.cancelAgentChat).toHaveBeenCalledTimes(1);
|
||||
resolveCancel();
|
||||
await waitFor(() => expect(screen.getByText("Request stopped.")).toBeTruthy());
|
||||
await waitFor(() => expect(screen.getByText("Demande arrêtée.")).toBeTruthy());
|
||||
expect(hasProjectCliInteraction("project-1")).toBe(false);
|
||||
expect(onSessionId).not.toHaveBeenCalledWith(null);
|
||||
expect(agent.stopLiveAgent).not.toHaveBeenCalled();
|
||||
expect(agent.closeAgentChat).not.toHaveBeenCalled();
|
||||
const composer = screen.getByLabelText(/message CLI custom/);
|
||||
const composer = screen.getByLabelText(/Message CLI personnalisée/);
|
||||
expect(composer).toHaveProperty("disabled", false);
|
||||
|
||||
fireEvent.change(composer, {
|
||||
@ -1696,7 +1714,7 @@ describe("CustomAgentChatView", () => {
|
||||
</DIProvider>,
|
||||
);
|
||||
|
||||
const composer = await screen.findByLabelText(/message CLI custom/);
|
||||
const composer = await screen.findByLabelText(/Message CLI personnalisée/);
|
||||
await waitFor(() => expect(composer).toHaveProperty("disabled", true));
|
||||
expect(screen.getByRole("button", { name: "Stop" })).toBeTruthy();
|
||||
|
||||
@ -1791,7 +1809,7 @@ describe("CustomAgentChatView", () => {
|
||||
);
|
||||
|
||||
expect(await screen.findByText("Main attend Architect")).toBeTruthy();
|
||||
expect(screen.getByLabelText(/message CLI custom/)).toHaveProperty("disabled", true);
|
||||
expect(screen.getByLabelText(/Message CLI personnalisée/)).toHaveProperty("disabled", true);
|
||||
});
|
||||
|
||||
it("falls back to a fresh launch when reattach reports NOT_FOUND", async () => {
|
||||
@ -1895,7 +1913,7 @@ describe("CustomAgentChatView", () => {
|
||||
),
|
||||
);
|
||||
|
||||
fireEvent.change(screen.getByLabelText(/message CLI custom/), {
|
||||
fireEvent.change(screen.getByLabelText(/Message CLI personnalisée/), {
|
||||
target: { value: "hello agent" },
|
||||
});
|
||||
fireEvent.click(screen.getByRole("button", { name: "Envoyer" }));
|
||||
@ -1974,13 +1992,13 @@ describe("CustomAgentChatView", () => {
|
||||
),
|
||||
);
|
||||
|
||||
fireEvent.change(screen.getByLabelText(/message CLI custom/), {
|
||||
fireEvent.change(screen.getByLabelText(/Message CLI personnalisée/), {
|
||||
target: { value: "first request" },
|
||||
});
|
||||
fireEvent.click(screen.getByRole("button", { name: "Envoyer" }));
|
||||
await screen.findByText("final first request");
|
||||
|
||||
fireEvent.change(screen.getByLabelText(/message CLI custom/), {
|
||||
fireEvent.change(screen.getByLabelText(/Message CLI personnalisée/), {
|
||||
target: { value: "second request" },
|
||||
});
|
||||
fireEvent.click(screen.getByRole("button", { name: "Envoyer" }));
|
||||
@ -2117,16 +2135,16 @@ describe("CustomAgentChatView", () => {
|
||||
),
|
||||
);
|
||||
|
||||
fireEvent.change(screen.getByLabelText(/message CLI custom/), {
|
||||
fireEvent.change(screen.getByLabelText(/Message CLI personnalisée/), {
|
||||
target: { value: "ship progress UI" },
|
||||
});
|
||||
fireEvent.click(screen.getByRole("button", { name: "Envoyer" }));
|
||||
|
||||
await screen.findByText("Progress");
|
||||
await screen.findByText("Progression");
|
||||
expect(screen.getAllByTestId("chat-turn-marker")).toHaveLength(2);
|
||||
expect(screen.getByText("turn.started")).toBeTruthy();
|
||||
expect(screen.getByText("turn.completed")).toBeTruthy();
|
||||
expect(screen.getByText("Progress").closest("[data-testid='chat-progress-status']")).toBeTruthy();
|
||||
expect(screen.getByText("Progression").closest("[data-testid='chat-progress-status']")).toBeTruthy();
|
||||
expect(screen.getByText("Analyse")).toBeTruthy();
|
||||
expect(screen.getByText("Lecture du contexte disponible.")).toBeTruthy();
|
||||
expect(screen.getByText("MCP")).toBeTruthy();
|
||||
@ -2203,13 +2221,13 @@ describe("CustomAgentChatView", () => {
|
||||
),
|
||||
);
|
||||
|
||||
fireEvent.change(screen.getByLabelText(/message CLI custom/), {
|
||||
fireEvent.change(screen.getByLabelText(/Message CLI personnalisée/), {
|
||||
target: { value: "first" },
|
||||
});
|
||||
fireEvent.click(screen.getByRole("button", { name: "Envoyer" }));
|
||||
await screen.findByText("done first");
|
||||
|
||||
fireEvent.change(screen.getByLabelText(/message CLI custom/), {
|
||||
fireEvent.change(screen.getByLabelText(/Message CLI personnalisée/), {
|
||||
target: { value: "second" },
|
||||
});
|
||||
fireEvent.click(screen.getByRole("button", { name: "Envoyer" }));
|
||||
@ -2251,7 +2269,7 @@ describe("CustomAgentChatView", () => {
|
||||
),
|
||||
);
|
||||
|
||||
fireEvent.change(screen.getByLabelText(/message CLI custom/), {
|
||||
fireEvent.change(screen.getByLabelText(/Message CLI personnalisée/), {
|
||||
target: { value: "first request" },
|
||||
});
|
||||
fireEvent.click(screen.getByRole("button", { name: "Envoyer" }));
|
||||
@ -2271,7 +2289,7 @@ describe("CustomAgentChatView", () => {
|
||||
});
|
||||
});
|
||||
expect(screen.getByText("Running first request")).toBeTruthy();
|
||||
expect(screen.getByText("Tool").parentElement?.textContent).toContain("En cours");
|
||||
expect(screen.getByText("Outil").parentElement?.textContent).toContain("En cours");
|
||||
|
||||
act(() => {
|
||||
harness.emit(0, {
|
||||
@ -2289,7 +2307,7 @@ describe("CustomAgentChatView", () => {
|
||||
});
|
||||
await screen.findByText("final first request");
|
||||
|
||||
fireEvent.change(screen.getByLabelText(/message CLI custom/), {
|
||||
fireEvent.change(screen.getByLabelText(/Message CLI personnalisée/), {
|
||||
target: { value: "second request" },
|
||||
});
|
||||
fireEvent.click(screen.getByRole("button", { name: "Envoyer" }));
|
||||
@ -2308,7 +2326,7 @@ describe("CustomAgentChatView", () => {
|
||||
});
|
||||
});
|
||||
expect(screen.getByText("Second request context loaded.")).toBeTruthy();
|
||||
expect(screen.getByText("Progress").parentElement?.textContent).toContain("En cours");
|
||||
expect(screen.getByText("Progression").parentElement?.textContent).toContain("En cours");
|
||||
|
||||
act(() => {
|
||||
harness.emit(1, {
|
||||
@ -2379,7 +2397,7 @@ describe("CustomAgentChatView", () => {
|
||||
),
|
||||
);
|
||||
|
||||
fireEvent.change(screen.getByLabelText(/message CLI custom/), {
|
||||
fireEvent.change(screen.getByLabelText(/Message CLI personnalisée/), {
|
||||
target: { value: "first request" },
|
||||
});
|
||||
fireEvent.click(screen.getByRole("button", { name: "Envoyer" }));
|
||||
@ -2412,7 +2430,7 @@ describe("CustomAgentChatView", () => {
|
||||
});
|
||||
await screen.findByText("done first request");
|
||||
|
||||
fireEvent.change(screen.getByLabelText(/message CLI custom/), {
|
||||
fireEvent.change(screen.getByLabelText(/Message CLI personnalisée/), {
|
||||
target: { value: "second request" },
|
||||
});
|
||||
fireEvent.click(screen.getByRole("button", { name: "Envoyer" }));
|
||||
@ -2487,7 +2505,7 @@ describe("CustomAgentChatView", () => {
|
||||
),
|
||||
);
|
||||
|
||||
fireEvent.change(screen.getByLabelText(/message CLI custom/), {
|
||||
fireEvent.change(screen.getByLabelText(/Message CLI personnalisée/), {
|
||||
target: { value: "first request" },
|
||||
});
|
||||
fireEvent.click(screen.getByRole("button", { name: "Envoyer" }));
|
||||
@ -2522,7 +2540,7 @@ describe("CustomAgentChatView", () => {
|
||||
});
|
||||
await screen.findByText("done first request");
|
||||
|
||||
fireEvent.change(screen.getByLabelText(/message CLI custom/), {
|
||||
fireEvent.change(screen.getByLabelText(/Message CLI personnalisée/), {
|
||||
target: { value: "second request" },
|
||||
});
|
||||
fireEvent.click(screen.getByRole("button", { name: "Envoyer" }));
|
||||
@ -2599,7 +2617,7 @@ describe("CustomAgentChatView", () => {
|
||||
),
|
||||
);
|
||||
|
||||
fireEvent.change(screen.getByLabelText(/message CLI custom/), {
|
||||
fireEvent.change(screen.getByLabelText(/Message CLI personnalisée/), {
|
||||
target: { value: "first request" },
|
||||
});
|
||||
fireEvent.click(screen.getByRole("button", { name: "Envoyer" }));
|
||||
@ -2636,7 +2654,7 @@ describe("CustomAgentChatView", () => {
|
||||
});
|
||||
expect(screen.queryByText("Late first request completion")).toBeNull();
|
||||
|
||||
fireEvent.change(screen.getByLabelText(/message CLI custom/), {
|
||||
fireEvent.change(screen.getByLabelText(/Message CLI personnalisée/), {
|
||||
target: { value: "second request" },
|
||||
});
|
||||
fireEvent.click(screen.getByRole("button", { name: "Envoyer" }));
|
||||
@ -2727,7 +2745,7 @@ describe("CustomAgentChatView", () => {
|
||||
),
|
||||
);
|
||||
|
||||
fireEvent.change(screen.getByLabelText(/message CLI custom/), {
|
||||
fireEvent.change(screen.getByLabelText(/Message CLI personnalisée/), {
|
||||
target: { value: "delegate after intent" },
|
||||
});
|
||||
fireEvent.click(screen.getByRole("button", { name: "Envoyer" }));
|
||||
@ -2831,7 +2849,7 @@ describe("CustomAgentChatView", () => {
|
||||
),
|
||||
);
|
||||
|
||||
fireEvent.change(screen.getByLabelText(/message CLI custom/), {
|
||||
fireEvent.change(screen.getByLabelText(/Message CLI personnalisée/), {
|
||||
target: { value: "delegate once" },
|
||||
});
|
||||
fireEvent.click(screen.getByRole("button", { name: "Envoyer" }));
|
||||
@ -2882,7 +2900,7 @@ describe("CustomAgentChatView", () => {
|
||||
expect(screen.getAllByText("validate")).toHaveLength(2);
|
||||
expect(screen.queryByText("Appel inter-agent")).toBeNull();
|
||||
expect(screen.queryByText("reply delivered")).toBeNull();
|
||||
expect(screen.queryByText("Tool")).toBeNull();
|
||||
expect(screen.queryByText("Outil")).toBeNull();
|
||||
expect(screen.queryByText("started")).toBeNull();
|
||||
await waitFor(() =>
|
||||
expect(screen.queryByRole("status", { name: "Loading" })).toBeNull(),
|
||||
@ -2939,7 +2957,7 @@ describe("CustomAgentChatView", () => {
|
||||
),
|
||||
);
|
||||
|
||||
fireEvent.change(screen.getByLabelText(/message CLI custom/), {
|
||||
fireEvent.change(screen.getByLabelText(/Message CLI personnalisée/), {
|
||||
target: { value: "read many tickets" },
|
||||
});
|
||||
fireEvent.click(screen.getByRole("button", { name: "Envoyer" }));
|
||||
@ -3032,7 +3050,7 @@ describe("CustomAgentChatView", () => {
|
||||
),
|
||||
);
|
||||
|
||||
fireEvent.change(screen.getByLabelText(/message CLI custom/), {
|
||||
fireEvent.change(screen.getByLabelText(/Message CLI personnalisée/), {
|
||||
target: { value: "read ticket" },
|
||||
});
|
||||
fireEvent.click(screen.getByRole("button", { name: "Envoyer" }));
|
||||
@ -3108,7 +3126,7 @@ describe("CustomAgentChatView", () => {
|
||||
),
|
||||
);
|
||||
|
||||
fireEvent.change(screen.getByLabelText(/message CLI custom/), {
|
||||
fireEvent.change(screen.getByLabelText(/Message CLI personnalisée/), {
|
||||
target: { value: "download model" },
|
||||
});
|
||||
fireEvent.click(screen.getByRole("button", { name: "Envoyer" }));
|
||||
@ -3134,7 +3152,7 @@ describe("CustomAgentChatView", () => {
|
||||
expect(screen.getAllByTestId("chat-tool-progress")).toHaveLength(1);
|
||||
expect(screen.queryByText("Downloading 10%")).toBeNull();
|
||||
expect(screen.getByText("Downloading 75%")).toBeTruthy();
|
||||
expect(screen.getByText("Tool").parentElement?.textContent).toContain("En cours");
|
||||
expect(screen.getByText("Outil").parentElement?.textContent).toContain("En cours");
|
||||
});
|
||||
|
||||
it("expands only truncated MCP progress details and preserves expansion across updates", async () => {
|
||||
@ -3187,7 +3205,7 @@ describe("CustomAgentChatView", () => {
|
||||
),
|
||||
);
|
||||
|
||||
fireEvent.change(screen.getByLabelText(/message CLI custom/), {
|
||||
fireEvent.change(screen.getByLabelText(/Message CLI personnalisée/), {
|
||||
target: { value: "inspect mcp payloads" },
|
||||
});
|
||||
fireEvent.click(screen.getByRole("button", { name: "Envoyer" }));
|
||||
@ -3308,7 +3326,7 @@ describe("CustomAgentChatView", () => {
|
||||
),
|
||||
);
|
||||
|
||||
fireEvent.change(screen.getByLabelText(/message CLI custom/), {
|
||||
fireEvent.change(screen.getByLabelText(/Message CLI personnalisée/), {
|
||||
target: { value: "stream progress" },
|
||||
});
|
||||
fireEvent.click(screen.getByRole("button", { name: "Envoyer" }));
|
||||
@ -3382,7 +3400,7 @@ describe("CustomAgentChatView", () => {
|
||||
),
|
||||
);
|
||||
|
||||
fireEvent.change(screen.getByLabelText(/message CLI custom/), {
|
||||
fireEvent.change(screen.getByLabelText(/Message CLI personnalisée/), {
|
||||
target: { value: "run tools" },
|
||||
});
|
||||
fireEvent.click(screen.getByRole("button", { name: "Envoyer" }));
|
||||
@ -3473,7 +3491,7 @@ describe("CustomAgentChatView", () => {
|
||||
),
|
||||
);
|
||||
|
||||
fireEvent.change(screen.getByLabelText(/message CLI custom/), {
|
||||
fireEvent.change(screen.getByLabelText(/Message CLI personnalisée/), {
|
||||
target: { value: "call tool" },
|
||||
});
|
||||
fireEvent.click(screen.getByRole("button", { name: "Envoyer" }));
|
||||
@ -3499,7 +3517,7 @@ describe("CustomAgentChatView", () => {
|
||||
expect(screen.getAllByTestId("chat-tool-progress")).toHaveLength(1);
|
||||
expect(screen.queryByText("Searching...")).toBeNull();
|
||||
expect(screen.getByText("Found results")).toBeTruthy();
|
||||
expect(screen.getByText("Tool").parentElement?.textContent).toContain("Terminé");
|
||||
expect(screen.getByText("Outil").parentElement?.textContent).toContain("Terminé");
|
||||
|
||||
act(() => {
|
||||
emitChunk?.({ kind: "final", content: "done" });
|
||||
@ -3560,7 +3578,7 @@ describe("CustomAgentChatView", () => {
|
||||
),
|
||||
);
|
||||
|
||||
fireEvent.change(screen.getByLabelText(/message CLI custom/), {
|
||||
fireEvent.change(screen.getByLabelText(/Message CLI personnalisée/), {
|
||||
target: { value: "ship progress UI" },
|
||||
});
|
||||
fireEvent.click(screen.getByRole("button", { name: "Envoyer" }));
|
||||
@ -3580,7 +3598,7 @@ describe("CustomAgentChatView", () => {
|
||||
});
|
||||
|
||||
expect(screen.queryByRole("status", { name: "Loading" })).toBeNull();
|
||||
expect(screen.getByText("Progress").parentElement?.textContent).toContain("En cours");
|
||||
expect(screen.getByText("Progression").parentElement?.textContent).toContain("En cours");
|
||||
|
||||
act(() => {
|
||||
emitChunk?.({ kind: "final", content: "done" });
|
||||
@ -3591,7 +3609,7 @@ describe("CustomAgentChatView", () => {
|
||||
expect(screen.queryByRole("status", { name: "Loading" })).toBeNull(),
|
||||
);
|
||||
expect(screen.getByText("Lecture du contexte disponible.")).toBeTruthy();
|
||||
expect(screen.getByText("Progress").parentElement?.textContent).toContain("Terminé");
|
||||
expect(screen.getByText("Progression").parentElement?.textContent).toContain("Terminé");
|
||||
});
|
||||
|
||||
it("ignores late progress chunks after the final answer without reopening the turn", async () => {
|
||||
@ -3644,7 +3662,7 @@ describe("CustomAgentChatView", () => {
|
||||
),
|
||||
);
|
||||
|
||||
fireEvent.change(screen.getByLabelText(/message CLI custom/), {
|
||||
fireEvent.change(screen.getByLabelText(/Message CLI personnalisée/), {
|
||||
target: { value: "ship ordered stream" },
|
||||
});
|
||||
fireEvent.click(screen.getByRole("button", { name: "Envoyer" }));
|
||||
@ -3734,7 +3752,7 @@ describe("CustomAgentChatView", () => {
|
||||
),
|
||||
);
|
||||
|
||||
const composer = screen.getByLabelText(/message CLI custom/) as HTMLTextAreaElement;
|
||||
const composer = screen.getByLabelText(/Message CLI personnalisée/) as HTMLTextAreaElement;
|
||||
fireEvent.change(composer, { target: { value: "/c" } });
|
||||
|
||||
const menu = await screen.findByRole("listbox", {
|
||||
@ -3804,7 +3822,7 @@ describe("CustomAgentChatView", () => {
|
||||
),
|
||||
);
|
||||
|
||||
const composer = screen.getByLabelText(/message CLI custom/) as HTMLTextAreaElement;
|
||||
const composer = screen.getByLabelText(/Message CLI personnalisée/) as HTMLTextAreaElement;
|
||||
fireEvent.change(composer, { target: { value: "hello /c" } });
|
||||
expect(screen.queryByRole("listbox", { name: "suggestions commandes slash" })).toBeNull();
|
||||
expect(agent.listSlashCommands).not.toHaveBeenCalled();
|
||||
@ -3889,7 +3907,7 @@ describe("CustomAgentChatView", () => {
|
||||
),
|
||||
);
|
||||
|
||||
const composer = screen.getByLabelText(/message CLI custom/) as HTMLTextAreaElement;
|
||||
const composer = screen.getByLabelText(/Message CLI personnalisée/) as HTMLTextAreaElement;
|
||||
fireEvent.change(composer, { target: { value: "/explain focus this" } });
|
||||
fireEvent.keyDown(composer, { key: "Enter" });
|
||||
|
||||
@ -3958,7 +3976,7 @@ describe("CustomAgentChatView", () => {
|
||||
|
||||
expect(await screen.findByText("old conversation")).toBeTruthy();
|
||||
|
||||
const composer = screen.getByLabelText(/message CLI custom/) as HTMLTextAreaElement;
|
||||
const composer = screen.getByLabelText(/Message CLI personnalisée/) as HTMLTextAreaElement;
|
||||
fireEvent.change(composer, { target: { value: "/clean" } });
|
||||
fireEvent.keyDown(composer, { key: "Enter" });
|
||||
|
||||
@ -4053,7 +4071,7 @@ describe("CustomAgentChatView", () => {
|
||||
),
|
||||
);
|
||||
|
||||
const composer = screen.getByLabelText(/message CLI custom/) as HTMLTextAreaElement;
|
||||
const composer = screen.getByLabelText(/Message CLI personnalisée/) as HTMLTextAreaElement;
|
||||
fireEvent.change(composer, { target: { value: "/help" } });
|
||||
fireEvent.keyDown(composer, { key: "Enter" });
|
||||
|
||||
@ -4135,7 +4153,7 @@ describe("CustomAgentChatView", () => {
|
||||
),
|
||||
);
|
||||
|
||||
const composer = screen.getByLabelText(/message CLI custom/);
|
||||
const composer = screen.getByLabelText(/Message CLI personnalisée/);
|
||||
fireEvent.change(composer, { target: { value: "/explain" } });
|
||||
fireEvent.keyDown(composer, { key: "Enter" });
|
||||
|
||||
@ -4219,7 +4237,7 @@ describe("CustomAgentChatView", () => {
|
||||
),
|
||||
);
|
||||
|
||||
const composer = screen.getByLabelText(/message CLI custom/) as HTMLTextAreaElement;
|
||||
const composer = screen.getByLabelText(/Message CLI personnalisée/) as HTMLTextAreaElement;
|
||||
fireEvent.change(composer, { target: { value: "/profile" } });
|
||||
fireEvent.keyDown(composer, { key: "Enter" });
|
||||
|
||||
@ -4232,14 +4250,14 @@ describe("CustomAgentChatView", () => {
|
||||
expect(agent.sendAgentChat).not.toHaveBeenCalled();
|
||||
|
||||
const confirmButton = screen.getByRole("button", {
|
||||
name: "Reset et changer le profil",
|
||||
name: "Réinitialiser et changer le profil",
|
||||
}) as HTMLButtonElement;
|
||||
expect(confirmButton.disabled).toBe(true);
|
||||
|
||||
fireEvent.change(screen.getByLabelText("profil cible"), {
|
||||
fireEvent.change(screen.getByLabelText("Profil cible"), {
|
||||
target: { value: "codex-high" },
|
||||
});
|
||||
fireEvent.click(screen.getByLabelText("Je confirme que la session courante sera reset."));
|
||||
fireEvent.click(screen.getByLabelText("Je confirme que la session courante sera réinitialisée."));
|
||||
expect(confirmButton.disabled).toBe(false);
|
||||
fireEvent.click(confirmButton);
|
||||
|
||||
@ -4314,7 +4332,7 @@ describe("CustomAgentChatView", () => {
|
||||
Object.defineProperty(file, "arrayBuffer", {
|
||||
value: vi.fn(async () => new Uint8Array([1, 2, 3]).buffer),
|
||||
});
|
||||
const composer = screen.getByLabelText(/message CLI custom/) as HTMLTextAreaElement;
|
||||
const composer = screen.getByLabelText(/Message CLI personnalisée/) as HTMLTextAreaElement;
|
||||
fireEvent.paste(composer, {
|
||||
clipboardData: {
|
||||
items: [
|
||||
@ -4332,7 +4350,7 @@ describe("CustomAgentChatView", () => {
|
||||
expect(screen.getByTestId("attachment-preview-clipboard.png")).toBeTruthy();
|
||||
await waitFor(() => expect(composer.value).toBe("[Image 1]"));
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: "retirer clipboard.png" }));
|
||||
fireEvent.click(screen.getByRole("button", { name: "Retirer clipboard.png" }));
|
||||
|
||||
await waitFor(() =>
|
||||
expect(screen.queryByText("Fichier joint: clipboard.png")).toBeNull(),
|
||||
@ -4402,7 +4420,7 @@ describe("CustomAgentChatView", () => {
|
||||
),
|
||||
);
|
||||
|
||||
fireEvent.paste(screen.getByLabelText(/message CLI custom/), {
|
||||
fireEvent.paste(screen.getByLabelText(/Message CLI personnalisée/), {
|
||||
clipboardData: {
|
||||
items: [],
|
||||
files: [],
|
||||
@ -4492,7 +4510,7 @@ describe("CustomAgentChatView", () => {
|
||||
),
|
||||
);
|
||||
|
||||
fireEvent.paste(screen.getByLabelText(/message CLI custom/), {
|
||||
fireEvent.paste(screen.getByLabelText(/Message CLI personnalisée/), {
|
||||
clipboardData: {
|
||||
items: [],
|
||||
files: [],
|
||||
@ -4564,7 +4582,7 @@ describe("CustomAgentChatView", () => {
|
||||
Object.defineProperty(file, "arrayBuffer", {
|
||||
value: vi.fn(async () => new Uint8Array([1, 2, 3]).buffer),
|
||||
});
|
||||
fireEvent.paste(screen.getByLabelText(/message CLI custom/), {
|
||||
fireEvent.paste(screen.getByLabelText(/Message CLI personnalisée/), {
|
||||
clipboardData: {
|
||||
items: [
|
||||
{
|
||||
@ -4645,7 +4663,7 @@ describe("CustomAgentChatView", () => {
|
||||
Object.defineProperty(file, "arrayBuffer", {
|
||||
value: vi.fn(async () => new Uint8Array([4, 5, 6]).buffer),
|
||||
});
|
||||
fireEvent.paste(screen.getByLabelText(/message CLI custom/), {
|
||||
fireEvent.paste(screen.getByLabelText(/Message CLI personnalisée/), {
|
||||
clipboardData: {
|
||||
items: [],
|
||||
files: [file],
|
||||
@ -4716,7 +4734,7 @@ describe("CustomAgentChatView", () => {
|
||||
),
|
||||
);
|
||||
|
||||
const composer = screen.getByLabelText(/message CLI custom/) as HTMLTextAreaElement;
|
||||
const composer = screen.getByLabelText(/Message CLI personnalisée/) as HTMLTextAreaElement;
|
||||
fireEvent.change(composer, { target: { value: "prefix suffix" } });
|
||||
composer.setSelectionRange(7, 13);
|
||||
|
||||
@ -4802,7 +4820,7 @@ describe("CustomAgentChatView", () => {
|
||||
),
|
||||
);
|
||||
|
||||
const composer = screen.getByLabelText(/message CLI custom/) as HTMLTextAreaElement;
|
||||
const composer = screen.getByLabelText(/Message CLI personnalisée/) as HTMLTextAreaElement;
|
||||
fireEvent.change(composer, { target: { value: "voir maintenant" } });
|
||||
composer.setSelectionRange(5, 5);
|
||||
const file = new File(["ignored"], "cursor.png", { type: "image/png" });
|
||||
@ -4904,7 +4922,7 @@ describe("CustomAgentChatView", () => {
|
||||
files: [file],
|
||||
getData: () => "",
|
||||
};
|
||||
const composer = screen.getByLabelText(/message CLI custom/);
|
||||
const composer = screen.getByLabelText(/Message CLI personnalisée/);
|
||||
|
||||
fireEvent.paste(composer, { clipboardData });
|
||||
await screen.findByText("Fichier joint: duplicate.pdf");
|
||||
@ -4994,7 +5012,7 @@ describe("CustomAgentChatView", () => {
|
||||
</DIProvider>,
|
||||
);
|
||||
await waitFor(() =>
|
||||
expect((screen.getByLabelText(/message CLI custom/) as HTMLTextAreaElement).value).toBe(""),
|
||||
expect((screen.getByLabelText(/Message CLI personnalisée/) as HTMLTextAreaElement).value).toBe(""),
|
||||
);
|
||||
|
||||
rerender(
|
||||
@ -5028,7 +5046,7 @@ describe("CustomAgentChatView", () => {
|
||||
</DIProvider>,
|
||||
);
|
||||
await waitFor(() =>
|
||||
expect((screen.getByLabelText(/message CLI custom/) as HTMLTextAreaElement).value).toBe(""),
|
||||
expect((screen.getByLabelText(/Message CLI personnalisée/) as HTMLTextAreaElement).value).toBe(""),
|
||||
);
|
||||
});
|
||||
|
||||
@ -5083,7 +5101,7 @@ describe("CustomAgentChatView", () => {
|
||||
await waitFor(() =>
|
||||
expect(agent.readChatDraft).toHaveBeenCalledWith(scope),
|
||||
);
|
||||
const composer = screen.getByLabelText(/message CLI custom/) as HTMLTextAreaElement;
|
||||
const composer = screen.getByLabelText(/Message CLI personnalisée/) as HTMLTextAreaElement;
|
||||
fireEvent.change(composer, { target: { value: "draft with file" } });
|
||||
|
||||
const file = new File(["ignored"], "not-persisted.png", { type: "image/png" });
|
||||
@ -5159,7 +5177,7 @@ describe("CustomAgentChatView", () => {
|
||||
|
||||
await waitFor(() => expect(agent.sendAgentChat).toHaveBeenCalledTimes(1));
|
||||
expect(agent.clearChatDraft).toHaveBeenCalledWith(scope);
|
||||
expect((screen.getByLabelText(/message CLI custom/) as HTMLTextAreaElement).value).toBe("");
|
||||
expect((screen.getByLabelText(/Message CLI personnalisée/) as HTMLTextAreaElement).value).toBe("");
|
||||
});
|
||||
|
||||
it("clears the persisted draft when a slash command is consumed", async () => {
|
||||
@ -5224,7 +5242,7 @@ describe("CustomAgentChatView", () => {
|
||||
}),
|
||||
);
|
||||
expect(agent.clearChatDraft).toHaveBeenCalledWith(scope);
|
||||
expect((screen.getByLabelText(/message CLI custom/) as HTMLTextAreaElement).value).toBe("");
|
||||
expect((screen.getByLabelText(/Message CLI personnalisée/) as HTMLTextAreaElement).value).toBe("");
|
||||
});
|
||||
|
||||
it("keeps the chat shell bounded with a scrollable message area and fixed composer", async () => {
|
||||
@ -5457,7 +5475,7 @@ describe("CustomAgentChatView", () => {
|
||||
),
|
||||
);
|
||||
|
||||
fireEvent.change(screen.getByLabelText(/message CLI custom/), {
|
||||
fireEvent.change(screen.getByLabelText(/Message CLI personnalisée/), {
|
||||
target: { value: "please recover" },
|
||||
});
|
||||
fireEvent.click(screen.getByRole("button", { name: "Envoyer" }));
|
||||
@ -5531,7 +5549,7 @@ describe("CustomAgentChatView", () => {
|
||||
),
|
||||
);
|
||||
|
||||
fireEvent.change(screen.getByLabelText(/message CLI custom/), {
|
||||
fireEvent.change(screen.getByLabelText(/Message CLI personnalisée/), {
|
||||
target: { value: "please recover" },
|
||||
});
|
||||
fireEvent.click(screen.getByRole("button", { name: "Envoyer" }));
|
||||
@ -5595,7 +5613,7 @@ describe("CustomAgentChatView", () => {
|
||||
),
|
||||
);
|
||||
|
||||
fireEvent.change(screen.getByLabelText(/message CLI custom/), {
|
||||
fireEvent.change(screen.getByLabelText(/Message CLI personnalisée/), {
|
||||
target: { value: "long turn" },
|
||||
});
|
||||
fireEvent.click(screen.getByRole("button", { name: "Envoyer" }));
|
||||
@ -5603,7 +5621,7 @@ describe("CustomAgentChatView", () => {
|
||||
|
||||
fireEvent.click(await screen.findByRole("button", { name: "Stop" }));
|
||||
|
||||
expect(await screen.findByText("Request already stopped.")).toBeTruthy();
|
||||
expect(await screen.findByText("Demande déjà arrêtée.")).toBeTruthy();
|
||||
expect(onSessionId).not.toHaveBeenCalledWith(null);
|
||||
expect(agent.cancelAgentChat).toHaveBeenCalledWith("gone-session");
|
||||
expect(agent.stopLiveAgent).not.toHaveBeenCalled();
|
||||
|
||||
@ -31,6 +31,7 @@ import type {
|
||||
SlashCommand,
|
||||
} from "@/domain";
|
||||
import { useGateways } from "@/app/di";
|
||||
import { useI18n } from "@/i18n";
|
||||
import {
|
||||
AnnouncementsPreview,
|
||||
shortRestartFailure,
|
||||
@ -43,10 +44,12 @@ import {
|
||||
markProjectCliInteractionStopped,
|
||||
} from "@/features/projects/projectCliInteractionActivity";
|
||||
import { Button, EntryTimestamp, Spinner, cn, normalizeTimestamp } from "@/shared";
|
||||
import { onProjectIconChanged, ProjectIcon } from "@/features/projects/projectIcons";
|
||||
import type {
|
||||
ChatAttachmentInput,
|
||||
ChatDraftScope,
|
||||
ConversationTranscriptUnavailableReason,
|
||||
ProjectIconRef,
|
||||
} from "@/ports";
|
||||
|
||||
export interface CustomAgentChatViewProps {
|
||||
@ -947,7 +950,8 @@ export function CustomAgentChatView({
|
||||
onSessionId,
|
||||
onConversationId,
|
||||
}: CustomAgentChatViewProps) {
|
||||
const { agent, profile: profileGateway, system, workState } = useGateways();
|
||||
const { agent, profile: profileGateway, project, system, workState } = useGateways();
|
||||
const { t, tr } = useI18n();
|
||||
const pluginRuntime = usePluginRuntime();
|
||||
const initialConversationCacheKey = conversationCacheKey(
|
||||
projectId,
|
||||
@ -1006,6 +1010,7 @@ export function CustomAgentChatView({
|
||||
const [busy, setBusy] = useState(false);
|
||||
const [stopping, setStopping] = useState(false);
|
||||
const [error, setError] = useState<ChatErrorState | null>(null);
|
||||
const [projectIcon, setProjectIcon] = useState<ProjectIconRef | null>(null);
|
||||
const scrollRef = useRef<HTMLDivElement | null>(null);
|
||||
const composerRef = useRef<HTMLTextAreaElement | null>(null);
|
||||
const chatRootRef = useRef<HTMLDivElement | null>(null);
|
||||
@ -1048,6 +1053,17 @@ export function CustomAgentChatView({
|
||||
onConversationIdRef.current = onConversationId;
|
||||
const domId = nodeId ?? `agent-${agentId}`;
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
void project?.readProjectSettings(projectId).then((settings) => {
|
||||
if (!cancelled) setProjectIcon(settings.icon ?? null);
|
||||
}).catch(() => undefined);
|
||||
const unsubscribe = onProjectIconChanged((change) => {
|
||||
if (change.projectId === projectId) setProjectIcon(change.icon);
|
||||
});
|
||||
return () => { cancelled = true; unsubscribe(); };
|
||||
}, [project, projectId]);
|
||||
|
||||
const clampComposerHeight = useCallback(
|
||||
(heightPx: number): number => {
|
||||
const visibleHeight =
|
||||
@ -1173,7 +1189,7 @@ export function CustomAgentChatView({
|
||||
delegationActive;
|
||||
const delegationFallbackText = requesterDelegation.delegationTargetName
|
||||
? `${agentName} attend ${requesterDelegation.delegationTargetName}`
|
||||
: "Délégation en cours.";
|
||||
: tr("Délégation en cours.", "Delegation in progress.");
|
||||
|
||||
const refreshRequesterDelegation = useCallback(async () => {
|
||||
if (!workState) {
|
||||
@ -1391,21 +1407,21 @@ export function CustomAgentChatView({
|
||||
nextTurns.push({
|
||||
role: "restart",
|
||||
key,
|
||||
text: "------- redemarrage de l'agent -------",
|
||||
text: tr("------- redémarrage de l’agent -------", "------- restarting agent -------"),
|
||||
});
|
||||
} else if (event.type === "agentRestartCompleted") {
|
||||
changed = true;
|
||||
nextTurns.push({
|
||||
role: "restart",
|
||||
key,
|
||||
text: "------- redemarrage de l'agent termine -------",
|
||||
text: tr("------- redémarrage de l’agent terminé -------", "------- agent restart completed -------"),
|
||||
});
|
||||
} else {
|
||||
changed = true;
|
||||
nextTurns.push({
|
||||
role: "restart",
|
||||
key,
|
||||
text: "------- redemarrage de l'agent echoue -------",
|
||||
text: tr("------- redémarrage de l’agent échoué -------", "------- agent restart failed -------"),
|
||||
message: shortRestartFailure(event.message),
|
||||
failed: true,
|
||||
});
|
||||
@ -2028,7 +2044,7 @@ export function CustomAgentChatView({
|
||||
if (!profileDialog) return;
|
||||
if (!agent.changeAgentProfile) {
|
||||
setProfileDialog((prev) =>
|
||||
prev ? { ...prev, error: "Changement de profil indisponible." } : prev,
|
||||
prev ? { ...prev, error: tr("Changement de profil indisponible.", "Profile change unavailable.") } : prev,
|
||||
);
|
||||
return;
|
||||
}
|
||||
@ -2077,7 +2093,7 @@ export function CustomAgentChatView({
|
||||
turns: [
|
||||
{
|
||||
role: "tool",
|
||||
label: `Profil changé vers ${selected.name}.`,
|
||||
label: tr(`Profil changé vers ${selected.name}.`, `Profile changed to ${selected.name}.`),
|
||||
receivedAtMs: null,
|
||||
},
|
||||
],
|
||||
@ -2095,7 +2111,7 @@ export function CustomAgentChatView({
|
||||
async function executeSlashCommand(invocation: SlashCommandInvocation) {
|
||||
if (!agent.executeSlashCommand) {
|
||||
setError({
|
||||
message: `Commande ${invocation.name} indisponible dans ce runtime.`,
|
||||
message: tr(`Commande ${invocation.name} indisponible dans ce runtime.`, `Command ${invocation.name} is unavailable in this runtime.`),
|
||||
openFailure: false,
|
||||
});
|
||||
return;
|
||||
@ -2144,7 +2160,7 @@ export function CustomAgentChatView({
|
||||
...prev.turns,
|
||||
{
|
||||
role: "tool",
|
||||
label: `Commande plugin ${result.command.name} exécutée.`,
|
||||
label: tr(`Commande plugin ${result.command.name} exécutée.`, `Plugin command ${result.command.name} executed.`),
|
||||
receivedAtMs: null,
|
||||
},
|
||||
],
|
||||
@ -2152,7 +2168,7 @@ export function CustomAgentChatView({
|
||||
return;
|
||||
}
|
||||
} catch (e) {
|
||||
const message = `Commande ${invocation.name} échouée: ${describe(e)}`;
|
||||
const message = `${tr(`Commande ${invocation.name} échouée :`, `Command ${invocation.name} failed:`)} ${describe(e)}`;
|
||||
setError({ message, openFailure: false });
|
||||
updateChatViewState((prev) => ({
|
||||
...prev,
|
||||
@ -2253,7 +2269,7 @@ export function CustomAgentChatView({
|
||||
const persisted = writeComposerHistory(composerHistoryKey, nextHistory);
|
||||
setComposerHistory(nextHistory);
|
||||
setComposerHistoryWarning(
|
||||
persisted ? null : "Historique des prompts non sauvegardé.",
|
||||
persisted ? null : tr("Historique des prompts non sauvegardé.", "Prompt history was not saved."),
|
||||
);
|
||||
setComposerHistoryIndex(null);
|
||||
draftBeforeHistoryNavigationRef.current = "";
|
||||
@ -2364,7 +2380,7 @@ export function CustomAgentChatView({
|
||||
...completed.turns,
|
||||
{
|
||||
role: "tool",
|
||||
label: "Request stopped.",
|
||||
label: tr("Demande arrêtée.", "Request stopped."),
|
||||
receivedAtMs: null,
|
||||
},
|
||||
],
|
||||
@ -2384,7 +2400,7 @@ export function CustomAgentChatView({
|
||||
...completed,
|
||||
turns: [
|
||||
...completed.turns,
|
||||
{ role: "tool", label: "Request already stopped.", receivedAtMs: null },
|
||||
{ role: "tool", label: tr("Demande déjà arrêtée.", "Request already stopped."), receivedAtMs: null },
|
||||
],
|
||||
};
|
||||
});
|
||||
@ -2397,7 +2413,7 @@ export function CustomAgentChatView({
|
||||
...prev.turns,
|
||||
{
|
||||
role: "error",
|
||||
text: `Arrêt de la demande échoué: ${describe(e)}`,
|
||||
text: `${tr("Échec de l’arrêt de la demande", "Failed to stop request")}: ${describe(e)}`,
|
||||
receivedAtMs: null,
|
||||
},
|
||||
],
|
||||
@ -2413,22 +2429,27 @@ export function CustomAgentChatView({
|
||||
data-testid="custom-agent-chat-view"
|
||||
className="relative z-0 flex h-full min-h-0 flex-col overflow-hidden bg-surface text-content"
|
||||
>
|
||||
{projectIcon && (
|
||||
<div data-testid="project-icon-watermark" aria-hidden className="pointer-events-none absolute inset-0 z-0 flex items-center justify-center overflow-hidden text-content opacity-[0.035]" style={{ pointerEvents: "none" }}>
|
||||
<ProjectIcon icon={projectIcon} size={260} decorative />
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
role="toolbar"
|
||||
aria-label="custom agent chat actions"
|
||||
aria-label={t("custom.actions")}
|
||||
className="relative z-20 flex shrink-0 items-center gap-2 overflow-hidden border-b border-border px-3 py-2"
|
||||
>
|
||||
<div className="min-w-0 flex-1 overflow-hidden">
|
||||
<div className="truncate text-sm font-medium">{agentName}</div>
|
||||
<div className="truncate text-xs text-muted">
|
||||
CLI custom · {profile.name}
|
||||
{tr("CLI personnalisée", "Custom CLI")} · {profile.name}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{!supported && (
|
||||
<p role="alert" className="m-3 rounded-md border border-warning/40 bg-warning/10 p-2 text-xs text-warning">
|
||||
CLI custom indisponible pour ce profil ou ce transport. Utilisez la TUI native.
|
||||
{t("custom.unsupported")}
|
||||
</p>
|
||||
)}
|
||||
{error && (
|
||||
@ -2438,10 +2459,12 @@ export function CustomAgentChatView({
|
||||
>
|
||||
{error.openFailure ? (
|
||||
<div className="flex min-w-0 flex-col gap-2">
|
||||
<div className="font-medium">Le CLI custom n'a pas pu démarrer.</div>
|
||||
<div className="break-words">
|
||||
Cause: {error.message}
|
||||
</div>
|
||||
<div className="font-medium">{t("custom.startFailure")}</div>
|
||||
{(!mobile || !/invalid node id/i.test(error.message)) && (
|
||||
<div className="break-words">
|
||||
{tr("Cause", "Cause")}: {error.message}
|
||||
</div>
|
||||
)}
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<Button
|
||||
size="sm"
|
||||
@ -2449,7 +2472,7 @@ export function CustomAgentChatView({
|
||||
disabled={opening}
|
||||
onClick={() => setOpenRetryNonce((value) => value + 1)}
|
||||
>
|
||||
Réessayer
|
||||
{t("common.retry")}
|
||||
</Button>
|
||||
{onOpenNativeTui && (
|
||||
<Button
|
||||
@ -2457,7 +2480,7 @@ export function CustomAgentChatView({
|
||||
variant="secondary"
|
||||
onClick={onOpenNativeTui}
|
||||
>
|
||||
Ouvrir en TUI native
|
||||
{t("custom.openTui")}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
@ -2485,10 +2508,10 @@ export function CustomAgentChatView({
|
||||
id={`profile-command-title-${domId}`}
|
||||
className="text-sm font-semibold text-content"
|
||||
>
|
||||
Changer de profil
|
||||
{t("custom.profile.change")}
|
||||
</h2>
|
||||
<p className="mt-1 text-xs text-muted">
|
||||
Valider ce changement va réinitialiser la session courante. L'historique de cette CLI custom sera perdu.
|
||||
{t("custom.profile.warning")}
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
@ -2497,14 +2520,14 @@ export function CustomAgentChatView({
|
||||
disabled={profileDialog.busy}
|
||||
onClick={() => setProfileDialog(null)}
|
||||
>
|
||||
Fermer
|
||||
{t("common.close")}
|
||||
</Button>
|
||||
</div>
|
||||
<div className="mt-3 flex min-w-0 flex-col gap-2">
|
||||
<label className="flex min-w-0 flex-col gap-1 text-xs text-muted">
|
||||
Profil
|
||||
{t("custom.profile")}
|
||||
<select
|
||||
aria-label="profil cible"
|
||||
aria-label={tr("Profil cible", "Target profile")}
|
||||
value={profileDialog.selectedProfileId}
|
||||
disabled={profileDialog.busy}
|
||||
className="rounded-md border border-border bg-raised px-2 py-1 text-sm text-content"
|
||||
@ -2538,7 +2561,7 @@ export function CustomAgentChatView({
|
||||
)
|
||||
}
|
||||
/>
|
||||
<span>Je confirme que la session courante sera reset.</span>
|
||||
<span>{tr("Je confirme que la session courante sera réinitialisée.", "I confirm that the current session will be reset.")}</span>
|
||||
</label>
|
||||
{profileDialog.error && (
|
||||
<p role="alert" className="text-xs text-danger">
|
||||
@ -2552,7 +2575,7 @@ export function CustomAgentChatView({
|
||||
disabled={profileDialog.busy}
|
||||
onClick={() => setProfileDialog(null)}
|
||||
>
|
||||
Annuler
|
||||
{tr("Annuler", "Cancel")}
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
@ -2561,7 +2584,7 @@ export function CustomAgentChatView({
|
||||
loading={profileDialog.busy}
|
||||
onClick={() => void confirmProfileCommandChange()}
|
||||
>
|
||||
Reset et changer le profil
|
||||
{tr("Réinitialiser et changer le profil", "Reset and change profile")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@ -2584,18 +2607,18 @@ export function CustomAgentChatView({
|
||||
<div className="flex items-center gap-2 text-sm text-muted">
|
||||
<Spinner size={14} />
|
||||
<span>
|
||||
{externalSessionId ? "Reprise en cours..." : "Ouverture de la session structurée…"}
|
||||
{externalSessionId ? t("custom.resume") : t("custom.opening")}
|
||||
</span>
|
||||
</div>
|
||||
) : unavailableLinkedTranscript ? (
|
||||
<div className="m-auto flex max-w-md flex-col items-center gap-3 rounded-md border border-warning/40 bg-warning/10 p-4 text-center">
|
||||
<p className="text-sm font-medium text-content">
|
||||
La conversation liée n’a pas pu être restaurée.
|
||||
{tr("La conversation liée n’a pas pu être restaurée.", "The linked conversation could not be restored.")}
|
||||
</p>
|
||||
<p className="text-xs text-muted">
|
||||
{unavailableLinkedTranscript === "conversationNotFound"
|
||||
? "Cette conversation est introuvable dans le stockage disponible."
|
||||
: "Son transcript n’est plus disponible après la reprise."}
|
||||
? tr("Cette conversation est introuvable dans le stockage disponible.", "This conversation cannot be found in available storage.")
|
||||
: tr("Sa transcription n’est plus disponible après la reprise.", "Its transcript is no longer available after restoration.")}
|
||||
</p>
|
||||
<div className="flex flex-wrap justify-center gap-2">
|
||||
<Button
|
||||
@ -2604,16 +2627,16 @@ export function CustomAgentChatView({
|
||||
disabled={opening}
|
||||
onClick={retryUnavailableTranscript}
|
||||
>
|
||||
Réessayer
|
||||
{t("common.retry")}
|
||||
</Button>
|
||||
<Button size="sm" variant="secondary" onClick={unlinkUnavailableConversation}>
|
||||
Délier la conversation
|
||||
{tr("Délier la conversation", "Unlink conversation")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
) : displayTurns.length === 0 ? (
|
||||
<p className="m-auto text-xs text-muted">
|
||||
Envoyez un message pour démarrer la conversation structurée.
|
||||
{t("custom.empty")}
|
||||
</p>
|
||||
) : (
|
||||
displayTurns.map((turn, index) => (
|
||||
@ -2661,14 +2684,14 @@ export function CustomAgentChatView({
|
||||
className="h-8 w-8 shrink-0 rounded border border-border object-cover"
|
||||
/>
|
||||
)}
|
||||
<span className="min-w-0 truncate">Fichier joint: {item.label}</span>
|
||||
<span className="min-w-0 truncate">{t("custom.attachment")}: {item.label}</span>
|
||||
<button
|
||||
type="button"
|
||||
aria-label={`retirer ${item.label}`}
|
||||
aria-label={tr(`Retirer ${item.label}`, `Remove ${item.label}`)}
|
||||
className="shrink-0 text-content"
|
||||
onClick={() => removeAttachment(item)}
|
||||
>
|
||||
Retirer
|
||||
{t("custom.removeAttachment")}
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
@ -2678,7 +2701,7 @@ export function CustomAgentChatView({
|
||||
<div
|
||||
id={`slash-command-menu-${domId}`}
|
||||
role="listbox"
|
||||
aria-label="suggestions commandes slash"
|
||||
aria-label={t("custom.slashSuggestions")}
|
||||
className="max-h-48 overflow-y-auto rounded-md border border-border bg-surface py-1 shadow-lg"
|
||||
>
|
||||
{slashCommands.map((command, index) => {
|
||||
@ -2705,7 +2728,7 @@ export function CustomAgentChatView({
|
||||
{command.shortDescription}
|
||||
</span>
|
||||
{command.requiresConfirmation && (
|
||||
<span className="shrink-0 text-muted">confirm</span>
|
||||
<span className="shrink-0 text-muted">{tr("confirmation", "confirm")}</span>
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
@ -2721,7 +2744,7 @@ export function CustomAgentChatView({
|
||||
<div className="relative flex min-w-0 flex-1">
|
||||
<textarea
|
||||
ref={composerRef}
|
||||
aria-label={`message CLI custom ${domId}`}
|
||||
aria-label={`${tr("Message CLI personnalisée", "Custom CLI message")} ${domId}`}
|
||||
aria-controls={slashMenuOpen ? `slash-command-menu-${domId}` : undefined}
|
||||
aria-activedescendant={
|
||||
slashMenuOpen ? `slash-command-${domId}-${slashActiveIndex}` : undefined
|
||||
@ -2733,7 +2756,7 @@ export function CustomAgentChatView({
|
||||
rows={2}
|
||||
value={draft}
|
||||
disabled={!supported || opening || effectiveBusy}
|
||||
placeholder="Message à l'agent…"
|
||||
placeholder={t("custom.message")}
|
||||
style={{ height: clampComposerHeight(composerHeightPx) }}
|
||||
onChange={(e) => editDraft(e.target.value)}
|
||||
onPaste={(e) => void pasteClipboardFiles(e)}
|
||||
@ -2772,8 +2795,8 @@ export function CustomAgentChatView({
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
aria-label={`redimensionner le composer ${domId}`}
|
||||
title="Redimensionner le composer"
|
||||
aria-label={`${tr("Redimensionner l’éditeur", "Resize composer")} ${domId}`}
|
||||
title={t("custom.resize")}
|
||||
className="absolute right-1.5 top-1.5 h-4 w-4 rounded-sm border border-border bg-raised text-[9px] leading-3 text-muted hover:text-content focus:border-primary focus:outline-none"
|
||||
style={{ cursor: "ns-resize" }}
|
||||
onMouseDown={startComposerResize}
|
||||
@ -2793,7 +2816,7 @@ export function CustomAgentChatView({
|
||||
disabled={!supported || opening || effectiveBusy}
|
||||
onClick={() => void pickAttachment()}
|
||||
>
|
||||
Joindre
|
||||
{t("custom.attach")}
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
@ -2805,11 +2828,11 @@ export function CustomAgentChatView({
|
||||
: !canSend
|
||||
}
|
||||
loading={stopping}
|
||||
title={effectiveBusy ? "Arrêter la demande en cours" : undefined}
|
||||
aria-label={effectiveBusy ? "Stop" : undefined}
|
||||
title={effectiveBusy ? t("custom.stop") : undefined}
|
||||
aria-label={effectiveBusy ? t("custom.stop") : undefined}
|
||||
onClick={() => (effectiveBusy ? void stopCurrentRequest() : void send())}
|
||||
>
|
||||
{stopping ? "Stopping..." : effectiveBusy ? "Stop" : "Envoyer"}
|
||||
{stopping ? t("custom.stopping") : effectiveBusy ? t("custom.stop") : t("custom.send")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@ -2830,6 +2853,7 @@ function ChatBubble({
|
||||
expandedProgressKeys: Set<string>;
|
||||
onToggleProgressExpanded: (key: string) => void;
|
||||
}) {
|
||||
const { tr } = useI18n();
|
||||
if (turn.role === "restart") {
|
||||
return (
|
||||
<div
|
||||
@ -2851,7 +2875,7 @@ function ChatBubble({
|
||||
return (
|
||||
<div className="flex min-w-0 justify-center">
|
||||
<span className="flex max-w-[80%] items-baseline gap-2 truncate rounded-md border border-border bg-raised px-2 py-1 text-xs text-muted">
|
||||
<span className="truncate">{turn.label}</span>
|
||||
<span className="truncate">{turn.label === "Activité" ? tr("Activité", "Activity") : turn.label}</span>
|
||||
<EntryTimestamp atMs={turn.receivedAtMs} />
|
||||
</span>
|
||||
</div>
|
||||
@ -2874,7 +2898,7 @@ function ChatBubble({
|
||||
<div className="flex min-w-0 justify-end">
|
||||
<div className="min-w-0 max-w-[82%] overflow-hidden rounded-md border border-success/50 bg-success/10 px-3 py-2 text-sm text-content">
|
||||
<ChatMessageMeta
|
||||
label="Terminé"
|
||||
label={tr("Terminé", "Completed")}
|
||||
receivedAtMs={turn.receivedAtMs}
|
||||
labelClassName="font-semibold text-success"
|
||||
/>
|
||||
@ -2889,7 +2913,7 @@ function ChatBubble({
|
||||
<div className="min-w-0 max-w-[86%] overflow-hidden rounded-md border border-danger/40 bg-danger/10 px-3 py-2 text-xs text-danger">
|
||||
{turn.role === "error" && (
|
||||
<ChatMessageMeta
|
||||
label="Échec"
|
||||
label={tr("Échec", "Failed")}
|
||||
receivedAtMs={turn.receivedAtMs}
|
||||
labelClassName="font-semibold text-danger"
|
||||
/>
|
||||
@ -2897,8 +2921,8 @@ function ChatBubble({
|
||||
{turn.role === "error" && sessionLabel && (
|
||||
<p className="mb-1 font-medium text-danger/85">{sessionLabel}</p>
|
||||
)}
|
||||
{turn.role === "unknown" ? "Chunk inconnu reçu: " : ""}
|
||||
<span className="whitespace-pre-wrap break-words">{turn.text}</span>
|
||||
{turn.role === "unknown" ? tr("Fragment inconnu reçu : ", "Unknown chunk received: ") : ""}
|
||||
<span className="whitespace-pre-wrap break-words">{turn.text === "Erreur agent" ? tr("Erreur agent", "Agent error") : turn.text}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@ -2914,7 +2938,7 @@ function ChatBubble({
|
||||
)}
|
||||
>
|
||||
<ChatMessageMeta
|
||||
label={user ? "Vous" : requesterName}
|
||||
label={user ? tr("Vous", "You") : requesterName}
|
||||
receivedAtMs={turn.receivedAtMs}
|
||||
/>
|
||||
<p className="whitespace-pre-wrap break-words">{turn.text}</p>
|
||||
@ -2922,7 +2946,7 @@ function ChatBubble({
|
||||
<div className="mt-1 flex flex-col gap-0.5 text-xs text-muted">
|
||||
{turn.attachments.map((attachment) => (
|
||||
<p key={attachment} className="truncate">
|
||||
Fichier: {attachment}
|
||||
{tr("Fichier", "File")}: {attachment}
|
||||
</p>
|
||||
))}
|
||||
</div>
|
||||
@ -2930,7 +2954,7 @@ function ChatBubble({
|
||||
{!user && turn.pending && (
|
||||
<span className="mt-1 inline-flex items-center gap-1 text-xs text-muted">
|
||||
<Spinner size={12} />
|
||||
Réponse en cours
|
||||
{tr("Réponse en cours", "Reply in progress")}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
@ -2957,28 +2981,30 @@ function ChatMessageMeta({
|
||||
);
|
||||
}
|
||||
|
||||
function progressTitle(progress: ReplyProgress): string {
|
||||
if (isDelegationProgress(progress)) return "Délégation";
|
||||
type Tr = (french: string, english: string) => string;
|
||||
|
||||
function progressTitle(progress: ReplyProgress, tr: Tr): string {
|
||||
if (isDelegationProgress(progress)) return tr("Délégation", "Delegation");
|
||||
if (progress.kind === "mcp") return "MCP";
|
||||
if (progress.kind === "tool") return "Tool";
|
||||
if (progress.kind === "message") return "Progress";
|
||||
if (progress.kind === "turn") return "Tour";
|
||||
return "Activité";
|
||||
if (progress.kind === "tool") return tr("Outil", "Tool");
|
||||
if (progress.kind === "message") return tr("Progression", "Progress");
|
||||
if (progress.kind === "turn") return tr("Tour", "Turn");
|
||||
return tr("Activité", "Activity");
|
||||
}
|
||||
|
||||
function progressSourceLabel(progress: ReplyProgress): string {
|
||||
function progressSourceLabel(progress: ReplyProgress, tr: Tr): string {
|
||||
if (progress.source === "ideaLocal") return "IdeA";
|
||||
return progress.provider ?? "Provider";
|
||||
return progress.provider ?? tr("Fournisseur", "Provider");
|
||||
}
|
||||
|
||||
function progressStageLabel(stage: ReplyProgressStage): string {
|
||||
function progressStageLabel(stage: ReplyProgressStage, tr: Tr): string {
|
||||
switch (stage) {
|
||||
case "started":
|
||||
return "En cours";
|
||||
return tr("En cours", "In progress");
|
||||
case "delta":
|
||||
return "En cours";
|
||||
return tr("En cours", "In progress");
|
||||
case "completed":
|
||||
return "Terminé";
|
||||
return tr("Terminé", "Completed");
|
||||
case "info":
|
||||
return "info";
|
||||
}
|
||||
@ -3125,6 +3151,7 @@ function ExpandableProgressDetail({
|
||||
expanded: boolean;
|
||||
onToggle: () => void;
|
||||
}) {
|
||||
const { tr } = useI18n();
|
||||
const contentId = useId();
|
||||
const detailRef = useRef<HTMLParagraphElement>(null);
|
||||
const truncated = useIsTextTruncated(detailRef, content, expanded);
|
||||
@ -3155,7 +3182,7 @@ function ExpandableProgressDetail({
|
||||
aria-controls={contentId}
|
||||
onClick={onToggle}
|
||||
>
|
||||
{expanded ? "Afficher moins" : "Afficher plus"}
|
||||
{expanded ? tr("Afficher moins", "Show less") : tr("Afficher plus", "Show more")}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
@ -3163,8 +3190,9 @@ function ExpandableProgressDetail({
|
||||
}
|
||||
|
||||
function TurnMarker({ progress }: { progress: ReplyProgress }) {
|
||||
const stage = progressStageLabel(progress.stage);
|
||||
const label = progress.label.trim() || "Tour";
|
||||
const { tr } = useI18n();
|
||||
const stage = progressStageLabel(progress.stage, tr);
|
||||
const label = progress.label.trim() || tr("Tour", "Turn");
|
||||
return (
|
||||
<div data-testid="chat-turn-marker" className="flex min-w-0 justify-center">
|
||||
<div className="flex max-w-[72%] items-center gap-2 overflow-hidden text-[11px] text-muted">
|
||||
@ -3193,7 +3221,8 @@ function ToolProgressCard({
|
||||
expanded: boolean;
|
||||
onToggleExpanded: () => void;
|
||||
}) {
|
||||
const stage = progressStageLabel(progress.stage);
|
||||
const { tr } = useI18n();
|
||||
const stage = progressStageLabel(progress.stage, tr);
|
||||
const delegation = isDelegationProgress(progress) ? delegationSummary(progress) : null;
|
||||
const detail = delegation
|
||||
? delegation.detail
|
||||
@ -3203,8 +3232,8 @@ function ToolProgressCard({
|
||||
? `${delegation.requester ?? requesterName} ${
|
||||
progress.stage === "completed" ? "←" : "→"
|
||||
} ${delegation.target}`
|
||||
: "Appel inter-agent"
|
||||
: progressTitle(progress);
|
||||
: tr("Appel inter-agent", "Inter-agent call")
|
||||
: progressTitle(progress, tr);
|
||||
return (
|
||||
<div data-testid="chat-tool-progress" className="flex min-w-0 justify-center">
|
||||
<div
|
||||
@ -3233,9 +3262,9 @@ function ToolProgressCard({
|
||||
)}
|
||||
{(progress.source || progress.nativeEvent || progress.provider) && (
|
||||
<div className="mt-0.5 flex min-w-0 flex-wrap justify-center gap-x-2 gap-y-0.5 text-[11px] text-muted">
|
||||
<span>{progressSourceLabel(progress)}</span>
|
||||
<span>{progressSourceLabel(progress, tr)}</span>
|
||||
{progress.nativeEvent && (
|
||||
<span className="truncate">event: {progress.nativeEvent}</span>
|
||||
<span className="truncate">{tr("événement", "event")}: {progress.nativeEvent}</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
@ -3266,6 +3295,7 @@ function ProgressBubble({
|
||||
expanded: boolean;
|
||||
onToggleExpanded: () => void;
|
||||
}) {
|
||||
const { tr } = useI18n();
|
||||
if (isTimelineProgress(progress)) return <TurnMarker progress={progress} />;
|
||||
if (isCenteredToolProgress(progress)) {
|
||||
return (
|
||||
@ -3278,9 +3308,9 @@ function ProgressBubble({
|
||||
/>
|
||||
);
|
||||
}
|
||||
const title = progressTitle(progress);
|
||||
const stage = progressStageLabel(progress.stage);
|
||||
const source = progressSourceLabel(progress);
|
||||
const title = progressTitle(progress, tr);
|
||||
const stage = progressStageLabel(progress.stage, tr);
|
||||
const source = progressSourceLabel(progress, tr);
|
||||
const detail = progress.text?.trim() || null;
|
||||
return (
|
||||
<div data-testid="chat-progress-status" className="flex min-w-0 justify-end">
|
||||
@ -3299,7 +3329,7 @@ function ProgressBubble({
|
||||
<div className="mt-0.5 flex min-w-0 flex-wrap justify-end gap-x-2 gap-y-0.5 text-[11px] text-muted">
|
||||
<span>{source}</span>
|
||||
{progress.nativeEvent && (
|
||||
<span className="truncate">event: {progress.nativeEvent}</span>
|
||||
<span className="truncate">{tr("événement", "event")}: {progress.nativeEvent}</span>
|
||||
)}
|
||||
</div>
|
||||
{detail && (
|
||||
|
||||
@ -27,11 +27,11 @@ describe("ModelServerLaunchBadge (F35.1)", () => {
|
||||
const { rerender } = render(
|
||||
<ModelServerLaunchBadge status={{ state: "probing" }} />,
|
||||
);
|
||||
expect(screen.getByLabelText("model server status").textContent).toMatch(
|
||||
expect(screen.getByLabelText("état du serveur de modèle").textContent).toMatch(
|
||||
/vérification/i,
|
||||
);
|
||||
rerender(<ModelServerLaunchBadge status={{ state: "starting" }} />);
|
||||
expect(screen.getByLabelText("model server status").textContent).toMatch(
|
||||
expect(screen.getByLabelText("état du serveur de modèle").textContent).toMatch(
|
||||
/démarrage/i,
|
||||
);
|
||||
});
|
||||
@ -40,13 +40,13 @@ describe("ModelServerLaunchBadge (F35.1)", () => {
|
||||
const { rerender } = render(
|
||||
<ModelServerLaunchBadge status={{ state: "ready", reused: true }} />,
|
||||
);
|
||||
expect(screen.getByLabelText("model server status").textContent).toMatch(
|
||||
expect(screen.getByLabelText("état du serveur de modèle").textContent).toMatch(
|
||||
/réutilisé/i,
|
||||
);
|
||||
rerender(
|
||||
<ModelServerLaunchBadge status={{ state: "ready", reused: false }} />,
|
||||
);
|
||||
const label = screen.getByLabelText("model server status").textContent ?? "";
|
||||
const label = screen.getByLabelText("état du serveur de modèle").textContent ?? "";
|
||||
expect(label).toMatch(/prêt/i);
|
||||
expect(label).not.toMatch(/réutilisé/i);
|
||||
});
|
||||
@ -64,7 +64,7 @@ describe("ModelServerLaunchBadge (F35.1)", () => {
|
||||
const alert = screen.getByRole("alert");
|
||||
expect(alert.textContent).toMatch(/did not become ready on :8080/);
|
||||
// The code is not exposed by default (logs/detail stay hidden).
|
||||
expect(screen.queryByLabelText("launch failure detail")).toBeNull();
|
||||
expect(screen.queryByLabelText("détail de l'échec du lancement")).toBeNull();
|
||||
});
|
||||
|
||||
it("reveals the stable code/cause only when Détails is toggled", () => {
|
||||
@ -73,8 +73,8 @@ describe("ModelServerLaunchBadge (F35.1)", () => {
|
||||
failure={{ cause: "model_server", code: "SPAWN", message: "boom" }}
|
||||
/>,
|
||||
);
|
||||
fireEvent.click(screen.getByLabelText("toggle launch failure detail"));
|
||||
const detail = screen.getByLabelText("launch failure detail");
|
||||
fireEvent.click(screen.getByLabelText("afficher ou masquer le détail de l'échec"));
|
||||
const detail = screen.getByLabelText("détail de l'échec du lancement");
|
||||
expect(detail.textContent).toMatch(/SPAWN/);
|
||||
expect(detail.textContent).toMatch(/model_server/);
|
||||
});
|
||||
|
||||
@ -18,6 +18,7 @@
|
||||
import { useState } from "react";
|
||||
|
||||
import { cn } from "@/shared";
|
||||
import { useI18n } from "@/i18n";
|
||||
import type { ModelServerStatus } from "@/domain";
|
||||
import type { AgentLaunchFailure } from "./useAgents";
|
||||
|
||||
@ -31,20 +32,21 @@ export interface ModelServerLaunchBadgeProps {
|
||||
/** Human label + tone for a non-failed lifecycle state. */
|
||||
function describeStatus(
|
||||
status: ModelServerStatus,
|
||||
tr: (french: string, english: string) => string,
|
||||
): { label: string; tone: string; role?: "status" } | null {
|
||||
switch (status.state) {
|
||||
case "notConfigured":
|
||||
return null; // Nothing to surface for an unmanaged endpoint.
|
||||
case "probing":
|
||||
return { label: "vérification du serveur…", tone: "bg-muted/20 text-muted", role: "status" };
|
||||
return { label: tr("vérification du serveur…", "checking server…"), tone: "bg-muted/20 text-muted", role: "status" };
|
||||
case "starting":
|
||||
return { label: "démarrage du serveur…", tone: "bg-warning/20 text-warning", role: "status" };
|
||||
return { label: tr("démarrage du serveur…", "starting server…"), tone: "bg-warning/20 text-warning", role: "status" };
|
||||
case "downloading":
|
||||
return { label: "téléchargement du modèle…", tone: "bg-warning/20 text-warning", role: "status" };
|
||||
return { label: tr("téléchargement du modèle…", "downloading model…"), tone: "bg-warning/20 text-warning", role: "status" };
|
||||
case "ready":
|
||||
return status.reused
|
||||
? { label: "serveur prêt (réutilisé)", tone: "bg-success/20 text-success" }
|
||||
: { label: "serveur prêt", tone: "bg-success/20 text-success" };
|
||||
? { label: tr("serveur prêt (réutilisé)", "server ready (reused)"), tone: "bg-success/20 text-success" }
|
||||
: { label: tr("serveur prêt", "server ready"), tone: "bg-success/20 text-success" };
|
||||
case "failed":
|
||||
// Handled by the failure branch below (richer, actionable).
|
||||
return null;
|
||||
@ -55,6 +57,7 @@ export function ModelServerLaunchBadge({
|
||||
status,
|
||||
failure,
|
||||
}: ModelServerLaunchBadgeProps) {
|
||||
const { tr } = useI18n();
|
||||
const [showDetail, setShowDetail] = useState(false);
|
||||
|
||||
// A launch failure (from `agentLaunchFailed`) or a `failed` status both mean
|
||||
@ -72,26 +75,26 @@ export function ModelServerLaunchBadge({
|
||||
<span className="flex min-w-0 flex-wrap items-center gap-2">
|
||||
<span
|
||||
role="alert"
|
||||
aria-label="model server launch failed"
|
||||
aria-label={tr("échec du lancement du serveur de modèle", "model server launch failed")}
|
||||
className={cn(
|
||||
"rounded-full px-2 py-0.5 text-xs font-medium",
|
||||
"bg-danger/20 text-danger",
|
||||
)}
|
||||
>
|
||||
Échec du lancement : {failed.message}
|
||||
{tr("Échec du lancement :", "Launch failed:")} {failed.message}
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
aria-label="toggle launch failure detail"
|
||||
aria-label={tr("afficher ou masquer le détail de l'échec", "toggle launch failure detail")}
|
||||
onClick={() => setShowDetail((v) => !v)}
|
||||
className="text-xs text-muted underline hover:text-content"
|
||||
>
|
||||
Détails
|
||||
{tr("Détails", "Details")}
|
||||
</button>
|
||||
{showDetail && (
|
||||
<span aria-label="launch failure detail" className="text-xs text-faint">
|
||||
code {failed.code}
|
||||
{failed.cause ? ` · cause ${failed.cause}` : ""}
|
||||
<span aria-label={tr("détail de l'échec du lancement", "launch failure detail")} className="text-xs text-faint">
|
||||
{tr("code", "code")} {failed.code}
|
||||
{failed.cause ? ` · ${tr("cause", "cause")} ${failed.cause}` : ""}
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
@ -99,13 +102,13 @@ export function ModelServerLaunchBadge({
|
||||
}
|
||||
|
||||
if (!status) return null;
|
||||
const described = describeStatus(status);
|
||||
const described = describeStatus(status, tr);
|
||||
if (!described) return null;
|
||||
|
||||
return (
|
||||
<span
|
||||
role={described.role}
|
||||
aria-label="model server status"
|
||||
aria-label={tr("état du serveur de modèle", "model server status")}
|
||||
className={cn(
|
||||
"rounded-full px-2 py-0.5 text-xs font-medium",
|
||||
described.tone,
|
||||
|
||||
@ -17,6 +17,7 @@
|
||||
*/
|
||||
|
||||
import { Button, cn } from "@/shared";
|
||||
import { useI18n } from "@/i18n";
|
||||
import { useResumeProject } from "./useResumeProject";
|
||||
|
||||
export interface ResumeProjectPanelProps {
|
||||
@ -27,6 +28,7 @@ export interface ResumeProjectPanelProps {
|
||||
}
|
||||
|
||||
export function ResumeProjectPanel({ projectId, cwd }: ResumeProjectPanelProps) {
|
||||
const { tr } = useI18n();
|
||||
const vm = useResumeProject(projectId, cwd);
|
||||
|
||||
// No resumable agents ⇒ no panel at all.
|
||||
@ -36,17 +38,16 @@ export function ResumeProjectPanel({ projectId, cwd }: ResumeProjectPanelProps)
|
||||
<div
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-label="Reprise des agents"
|
||||
aria-label={tr("Reprise des agents", "Resume 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 ?
|
||||
{tr("Reprendre les agents ?", "Resume agents?")}
|
||||
</h4>
|
||||
<p className="mt-1 text-sm text-muted">
|
||||
Ces agents tournaient à la dernière fermeture du projet. Choisissez
|
||||
ceux à reprendre.
|
||||
{tr("Ces agents tournaient à la dernière fermeture du projet. Choisissez ceux à reprendre.", "These agents were running when the project was last closed. Choose which ones to resume.")}
|
||||
</p>
|
||||
|
||||
{vm.error && (
|
||||
@ -61,7 +62,7 @@ export function ResumeProjectPanel({ projectId, cwd }: ResumeProjectPanelProps)
|
||||
{/* ── 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";
|
||||
const statusLabel = a.wasRunning ? tr("en cours", "running") : tr("clôt", "closed");
|
||||
return (
|
||||
<li
|
||||
key={a.agentId}
|
||||
@ -90,7 +91,7 @@ export function ResumeProjectPanel({ projectId, cwd }: ResumeProjectPanelProps)
|
||||
data-testid={`resume-fresh-note-${a.agentId}`}
|
||||
className="text-xs text-muted"
|
||||
>
|
||||
Historique non disponible pour ce moteur — relance à neuf.
|
||||
{tr("Historique non disponible pour ce moteur — relance à neuf.", "History is unavailable for this engine — starting fresh.")}
|
||||
</p>
|
||||
)}
|
||||
|
||||
@ -98,28 +99,28 @@ export function ResumeProjectPanel({ projectId, cwd }: ResumeProjectPanelProps)
|
||||
<Button
|
||||
size="sm"
|
||||
variant="primary"
|
||||
aria-label={`resume ${a.name}`}
|
||||
aria-label={tr(`reprendre ${a.name}`, `resume ${a.name}`)}
|
||||
disabled={vm.busy}
|
||||
onClick={() => void vm.resume(a)}
|
||||
>
|
||||
{a.resumeSupported ? "Reprendre" : "Relancer à neuf"}
|
||||
{a.resumeSupported ? tr("Reprendre", "Resume") : tr("Relancer à neuf", "Start fresh")}
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
aria-label={`new conversation ${a.name}`}
|
||||
aria-label={tr(`nouvelle conversation ${a.name}`, `new conversation ${a.name}`)}
|
||||
disabled={vm.busy}
|
||||
onClick={() => void vm.newConversation(a)}
|
||||
>
|
||||
Nouvelle conversation
|
||||
{tr("Nouvelle conversation", "New conversation")}
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
aria-label={`ignore ${a.name}`}
|
||||
aria-label={tr(`ignorer ${a.name}`, `ignore ${a.name}`)}
|
||||
disabled={vm.busy}
|
||||
onClick={() => vm.ignore(a.agentId)}
|
||||
>
|
||||
Ignorer
|
||||
{tr("Ignorer", "Ignore")}
|
||||
</Button>
|
||||
</div>
|
||||
</li>
|
||||
@ -131,20 +132,20 @@ export function ResumeProjectPanel({ projectId, cwd }: ResumeProjectPanelProps)
|
||||
<div className="mt-4 flex items-center justify-end gap-2">
|
||||
<Button
|
||||
variant="ghost"
|
||||
aria-label="ignore all agents"
|
||||
aria-label={tr("ignorer tous les agents", "ignore all agents")}
|
||||
disabled={vm.busy}
|
||||
onClick={() => vm.ignoreAll()}
|
||||
>
|
||||
Tout ignorer
|
||||
{tr("Tout ignorer", "Ignore all")}
|
||||
</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
aria-label="resume all agents"
|
||||
aria-label={tr("reprendre tous les agents", "resume all agents")}
|
||||
loading={vm.busy}
|
||||
disabled={vm.busy}
|
||||
onClick={() => void vm.resumeAll()}
|
||||
>
|
||||
Tout reprendre
|
||||
{tr("Tout reprendre", "Resume all")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -77,7 +77,7 @@ async function waitForIdle() {
|
||||
// for the agent list to be rendered.
|
||||
await waitFor(() => {
|
||||
// Panel has finished its initial load when the form is accessible.
|
||||
expect(screen.getByLabelText("agent name")).toBeTruthy();
|
||||
expect(screen.getByLabelText("Nom de l’agent")).toBeTruthy();
|
||||
});
|
||||
}
|
||||
|
||||
@ -96,18 +96,18 @@ function chooseDropdownOption(label: string | RegExp, optionName: string | RegEx
|
||||
async function createAgent(name: string, profileId?: string) {
|
||||
await waitForIdle();
|
||||
|
||||
fireEvent.change(screen.getByLabelText("agent name"), {
|
||||
fireEvent.change(screen.getByLabelText("Nom de l’agent"), {
|
||||
target: { value: name },
|
||||
});
|
||||
|
||||
if (profileId) {
|
||||
const profileInput = screen.queryByLabelText("agent profile");
|
||||
const profileInput = screen.queryByLabelText("Profil de l’agent");
|
||||
if (profileInput) {
|
||||
chooseDropdownOption("agent profile", profileId);
|
||||
chooseDropdownOption("Profil de l’agent", profileId);
|
||||
}
|
||||
}
|
||||
|
||||
const createBtn = screen.getByRole("button", { name: "create agent" });
|
||||
const createBtn = screen.getByRole("button", { name: "Créer l’agent" });
|
||||
expect((createBtn as HTMLButtonElement).disabled).toBe(false);
|
||||
fireEvent.click(createBtn);
|
||||
}
|
||||
@ -120,7 +120,7 @@ describe("AgentsPanel (with MockAgentGateway)", () => {
|
||||
it("shows 'No agents yet.' when the project has no agents", async () => {
|
||||
renderPanel();
|
||||
await waitForIdle();
|
||||
expect(screen.getByText("No agents yet.")).toBeTruthy();
|
||||
expect(screen.getByText("Aucun agent pour le moment.")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("creating an agent adds it to the list", async () => {
|
||||
@ -162,7 +162,7 @@ describe("AgentsPanel (with MockAgentGateway)", () => {
|
||||
|
||||
renderPanel(agent);
|
||||
|
||||
expect(await screen.findByText("Writer contexte")).toBeTruthy();
|
||||
expect(await screen.findByText("Responsable du contexte")).toBeTruthy();
|
||||
const rowButton = screen
|
||||
.getAllByRole("button", { name: /main/i })
|
||||
.find((button) => button.hasAttribute("aria-pressed"))!;
|
||||
@ -176,7 +176,7 @@ describe("AgentsPanel (with MockAgentGateway)", () => {
|
||||
).toBeTruthy();
|
||||
expect(
|
||||
(screen.getByRole("radio", {
|
||||
name: "Writer du contexte projet global pour Main",
|
||||
name: "Responsable du contexte global du projet pour Main",
|
||||
}) as HTMLInputElement).checked,
|
||||
).toBe(true);
|
||||
});
|
||||
@ -201,7 +201,7 @@ describe("AgentsPanel (with MockAgentGateway)", () => {
|
||||
fireEvent.click(rowButton);
|
||||
fireEvent.click(
|
||||
await screen.findByRole("radio", {
|
||||
name: "Writer du contexte projet global pour Architect",
|
||||
name: "Responsable du contexte global du projet pour Architect",
|
||||
}),
|
||||
);
|
||||
|
||||
@ -212,7 +212,7 @@ describe("AgentsPanel (with MockAgentGateway)", () => {
|
||||
|
||||
fireEvent.click(
|
||||
screen.getByRole("button", {
|
||||
name: "Confirmer le changement de writer contexte",
|
||||
name: "Confirmer le changement de responsable du contexte",
|
||||
}),
|
||||
);
|
||||
|
||||
@ -221,7 +221,7 @@ describe("AgentsPanel (with MockAgentGateway)", () => {
|
||||
expect(screen.queryByRole("dialog")).toBeNull();
|
||||
expect(
|
||||
(screen.getByRole("radio", {
|
||||
name: "Writer du contexte projet global pour Architect",
|
||||
name: "Responsable du contexte global du projet pour Architect",
|
||||
}) as HTMLInputElement).checked,
|
||||
).toBe(true);
|
||||
});
|
||||
@ -254,12 +254,12 @@ describe("AgentsPanel (with MockAgentGateway)", () => {
|
||||
fireEvent.click(rowButton);
|
||||
fireEvent.click(
|
||||
await screen.findByRole("radio", {
|
||||
name: "Writer du contexte projet global pour QA",
|
||||
name: "Responsable du contexte global du projet pour QA",
|
||||
}),
|
||||
);
|
||||
fireEvent.click(
|
||||
screen.getByRole("button", {
|
||||
name: "Confirmer le changement de writer contexte",
|
||||
name: "Confirmer le changement de responsable du contexte",
|
||||
}),
|
||||
);
|
||||
|
||||
@ -272,7 +272,7 @@ describe("AgentsPanel (with MockAgentGateway)", () => {
|
||||
renderPanel();
|
||||
await waitForIdle();
|
||||
// Name field is empty by default → disabled
|
||||
const btn = screen.getByRole("button", { name: "create agent" });
|
||||
const btn = screen.getByRole("button", { name: "Créer l’agent" });
|
||||
expect((btn as HTMLButtonElement).disabled).toBe(true);
|
||||
});
|
||||
|
||||
@ -304,7 +304,7 @@ describe("AgentsPanel (with MockAgentGateway)", () => {
|
||||
renderPanel(new MockAgentGateway(), profile);
|
||||
await waitForIdle();
|
||||
|
||||
openDropdown("agent profile");
|
||||
openDropdown("Profil de l’agent");
|
||||
const labels = screen
|
||||
.getAllByRole("option")
|
||||
.map((option) => option.textContent);
|
||||
@ -326,14 +326,14 @@ describe("AgentsPanel (with MockAgentGateway)", () => {
|
||||
await waitForIdle();
|
||||
|
||||
// Click on the agent row button (aria-pressed) to select it — not the
|
||||
// "launch Alpha" action button.
|
||||
// "Lancer Alpha" action button.
|
||||
const buttons = screen.getAllByRole("button", { name: /alpha/i });
|
||||
const rowBtn = buttons.find(
|
||||
(b) => b.hasAttribute("aria-pressed"),
|
||||
)!;
|
||||
fireEvent.click(rowBtn);
|
||||
|
||||
const textarea = await screen.findByLabelText("agent context");
|
||||
const textarea = await screen.findByLabelText("Contexte de l’agent");
|
||||
expect((textarea as HTMLTextAreaElement).value).toBe("## Alpha context");
|
||||
});
|
||||
|
||||
@ -352,10 +352,10 @@ describe("AgentsPanel (with MockAgentGateway)", () => {
|
||||
const rowBtn = buttons.find((b) => b.hasAttribute("aria-pressed"))!;
|
||||
fireEvent.click(rowBtn);
|
||||
|
||||
const textarea = await screen.findByLabelText("agent context");
|
||||
const textarea = await screen.findByLabelText("Contexte de l’agent");
|
||||
fireEvent.change(textarea, { target: { value: "updated content" } });
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: "Save" }));
|
||||
fireEvent.click(screen.getByRole("button", { name: "Enregistrer" }));
|
||||
|
||||
// After save the gateway should hold the new content.
|
||||
await waitFor(async () => {
|
||||
@ -379,9 +379,9 @@ describe("AgentsPanel (with MockAgentGateway)", () => {
|
||||
let rowBtn = buttons.find((b) => b.hasAttribute("aria-pressed"))!;
|
||||
fireEvent.click(rowBtn);
|
||||
|
||||
let textarea = await screen.findByLabelText("agent context");
|
||||
let textarea = await screen.findByLabelText("Contexte de l’agent");
|
||||
fireEvent.change(textarea, { target: { value: "persisted after reopen" } });
|
||||
fireEvent.click(screen.getByRole("button", { name: "Save" }));
|
||||
fireEvent.click(screen.getByRole("button", { name: "Enregistrer" }));
|
||||
|
||||
await waitFor(async () => {
|
||||
const agents = await agent.listAgents(PROJECT_ID);
|
||||
@ -398,7 +398,7 @@ describe("AgentsPanel (with MockAgentGateway)", () => {
|
||||
rowBtn = buttons.find((b) => b.hasAttribute("aria-pressed"))!;
|
||||
fireEvent.click(rowBtn);
|
||||
|
||||
textarea = await screen.findByLabelText("agent context");
|
||||
textarea = await screen.findByLabelText("Contexte de l’agent");
|
||||
expect((textarea as HTMLTextAreaElement).value).toBe("persisted after reopen");
|
||||
});
|
||||
|
||||
@ -413,7 +413,7 @@ describe("AgentsPanel (with MockAgentGateway)", () => {
|
||||
|
||||
await screen.findByText("Gamma");
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: "delete Gamma" }));
|
||||
fireEvent.click(screen.getByRole("button", { name: "Supprimer Gamma" }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByText("Gamma")).toBeNull();
|
||||
@ -437,7 +437,7 @@ describe("AgentsPanel (with MockAgentGateway)", () => {
|
||||
renderPanel(agent);
|
||||
await waitForIdle();
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: "launch Delta" }));
|
||||
fireEvent.click(screen.getByRole("button", { name: "Lancer Delta" }));
|
||||
|
||||
// The terminal container div (data-testid="terminal-view") should be mounted.
|
||||
await waitFor(() => {
|
||||
@ -466,13 +466,13 @@ describe("AgentsPanel (with MockAgentGateway)", () => {
|
||||
await waitForIdle();
|
||||
|
||||
// Launch
|
||||
fireEvent.click(screen.getByRole("button", { name: "launch Echo" }));
|
||||
fireEvent.click(screen.getByRole("button", { name: "Lancer Echo" }));
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("terminal-view")).toBeTruthy();
|
||||
});
|
||||
|
||||
// Stop
|
||||
fireEvent.click(screen.getByRole("button", { name: "stop Echo" }));
|
||||
fireEvent.click(screen.getByRole("button", { name: "Arrêter Echo" }));
|
||||
const stopping = screen.getByRole("button", { name: "Arrêt en cours… — Echo" });
|
||||
expect((stopping as HTMLButtonElement).disabled).toBe(true);
|
||||
expect(screen.getByTestId("terminal-view")).toBeTruthy();
|
||||
@ -498,9 +498,9 @@ describe("AgentsPanel (with MockAgentGateway)", () => {
|
||||
|
||||
renderPanel(agent);
|
||||
await waitForIdle();
|
||||
fireEvent.click(screen.getByRole("button", { name: "launch Main" }));
|
||||
fireEvent.click(screen.getByRole("button", { name: "Lancer Main" }));
|
||||
await screen.findByTestId("terminal-view");
|
||||
fireEvent.click(screen.getByRole("button", { name: "stop Main" }));
|
||||
fireEvent.click(screen.getByRole("button", { name: "Arrêter Main" }));
|
||||
|
||||
expect(
|
||||
await screen.findByText("Arrêt incomplet — Certains travaux sont encore actifs."),
|
||||
@ -524,9 +524,9 @@ describe("AgentsPanel (with MockAgentGateway)", () => {
|
||||
const stop = vi.spyOn(agent, "stopLiveAgent");
|
||||
|
||||
renderPanel(agent);
|
||||
await screen.findByRole("button", { name: "stop Headless" });
|
||||
await screen.findByRole("button", { name: "Arrêter Headless" });
|
||||
expect(screen.queryByTestId("terminal-view")).toBeNull();
|
||||
fireEvent.click(screen.getByRole("button", { name: "stop Headless" }));
|
||||
fireEvent.click(screen.getByRole("button", { name: "Arrêter Headless" }));
|
||||
|
||||
await waitFor(() => expect(stop).toHaveBeenCalled());
|
||||
await expect(agent.listLiveAgents(PROJECT_ID)).resolves.toEqual([]);
|
||||
@ -548,15 +548,15 @@ describe("AgentsPanel (with MockAgentGateway)", () => {
|
||||
await waitForIdle();
|
||||
|
||||
// Select the template in the dropdown
|
||||
chooseDropdownOption("agent template", "My Template");
|
||||
chooseDropdownOption("Modèle d’agent", "My Template");
|
||||
|
||||
// Fill agent name
|
||||
fireEvent.change(screen.getByLabelText("agent name"), {
|
||||
fireEvent.change(screen.getByLabelText("Nom de l’agent"), {
|
||||
target: { value: "From Template" },
|
||||
});
|
||||
|
||||
// Click Create
|
||||
fireEvent.click(screen.getByRole("button", { name: "create agent" }));
|
||||
fireEvent.click(screen.getByRole("button", { name: "Créer l’agent" }));
|
||||
|
||||
// createAgentFromTemplate should have been called with the correct args
|
||||
await waitFor(() => {
|
||||
@ -585,12 +585,12 @@ describe("AgentsPanel (with MockAgentGateway)", () => {
|
||||
renderPanel(agent, new MockProfileGateway(), "/home/me/proj", tmpl);
|
||||
await waitForIdle();
|
||||
|
||||
chooseDropdownOption("agent template", "Main template");
|
||||
expect((screen.getByLabelText("agent name") as HTMLInputElement).value).toBe("Main");
|
||||
chooseDropdownOption("Modèle d’agent", "Main template");
|
||||
expect((screen.getByLabelText("Nom de l’agent") as HTMLInputElement).value).toBe("Main");
|
||||
expect(screen.getByText(/Modèle appliqué:/)).toBeTruthy();
|
||||
expect(screen.getByText("Profil profile-main")).toBeTruthy();
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: "create agent" }));
|
||||
fireEvent.click(screen.getByRole("button", { name: "Créer l’agent" }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(createFromTemplateSpy).toHaveBeenCalledWith(PROJECT_ID, t.id, {
|
||||
@ -619,7 +619,7 @@ describe("AgentsPanel (with MockAgentGateway)", () => {
|
||||
renderPanel(new MockAgentGateway(), profile);
|
||||
await waitForIdle();
|
||||
|
||||
openDropdown("agent profile");
|
||||
openDropdown("Profil de l’agent");
|
||||
const options = screen.getAllByRole("option").map((o) => o.textContent);
|
||||
expect(options).toContain("Claude Code");
|
||||
});
|
||||
@ -634,7 +634,7 @@ describe("AgentsPanel (with MockAgentGateway)", () => {
|
||||
renderPanel(new MockAgentGateway(), profile);
|
||||
await waitForIdle();
|
||||
|
||||
openDropdown("agent profile");
|
||||
openDropdown("Profil de l’agent");
|
||||
const options = screen.getAllByRole("option").map((o) => o.textContent ?? "");
|
||||
expect(options.some((text) => text.includes("OpenCode Local Durable"))).toBe(true);
|
||||
});
|
||||
@ -770,7 +770,7 @@ describe("AgentsPanel profile hot-swap (A2)", () => {
|
||||
await waitForIdle();
|
||||
await screen.findByText("Swap");
|
||||
|
||||
chooseDropdownOption("profile for Swap", /Codex CLI/);
|
||||
chooseDropdownOption("Profil de Swap", /Codex CLI/);
|
||||
|
||||
// No gateway call yet — only the dialog appears.
|
||||
expect(swapSpy).not.toHaveBeenCalled();
|
||||
@ -786,9 +786,9 @@ describe("AgentsPanel profile hot-swap (A2)", () => {
|
||||
await waitForIdle();
|
||||
await screen.findByText("Swap");
|
||||
|
||||
chooseDropdownOption("profile for Swap", /Codex CLI/);
|
||||
chooseDropdownOption("Profil de Swap", /Codex CLI/);
|
||||
fireEvent.click(
|
||||
screen.getByRole("button", { name: "cancel profile change" }),
|
||||
screen.getByRole("button", { name: "Annuler le changement de profil" }),
|
||||
);
|
||||
|
||||
await waitFor(() => expect(screen.queryByRole("dialog")).toBeNull());
|
||||
@ -807,9 +807,9 @@ describe("AgentsPanel profile hot-swap (A2)", () => {
|
||||
await waitForIdle();
|
||||
await screen.findByText("Swap");
|
||||
|
||||
chooseDropdownOption("profile for Swap", /Codex CLI/);
|
||||
chooseDropdownOption("Profil de Swap", /Codex CLI/);
|
||||
fireEvent.click(
|
||||
screen.getByRole("button", { name: "confirm profile change" }),
|
||||
screen.getByRole("button", { name: "Confirmer le changement de profil" }),
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
@ -830,13 +830,13 @@ describe("AgentsPanel profile hot-swap (A2)", () => {
|
||||
await waitForIdle();
|
||||
await screen.findByText("Swap");
|
||||
|
||||
chooseDropdownOption("profile for Swap", /Codex CLI/);
|
||||
chooseDropdownOption("Profil de Swap", /Codex CLI/);
|
||||
|
||||
const dialog = screen.getByRole("dialog");
|
||||
const text = dialog.textContent ?? "";
|
||||
// Spec §15.1: « Changer le moteur abandonne l'historique de conversation
|
||||
// (le contexte et la mémoire sont conservés). Continuer ? » — must be FRENCH.
|
||||
expect(text).toMatch(/abandonne l'historique de conversation/i);
|
||||
expect(text).toMatch(/abandonne l.historique de conversation/i);
|
||||
expect(text).toMatch(/contexte et la mémoire sont conservés/i);
|
||||
expect(text).toMatch(/Continuer\s*\?/);
|
||||
});
|
||||
@ -852,13 +852,13 @@ describe("AgentsPanel effort selection (#131)", () => {
|
||||
await waitForIdle();
|
||||
await screen.findByText("Thinker");
|
||||
|
||||
openDropdown("effort for Thinker");
|
||||
openDropdown("Effort pour Thinker");
|
||||
const labels = screen
|
||||
.getAllByRole("option")
|
||||
.map((option) => option.textContent);
|
||||
|
||||
expect(labels).toEqual([
|
||||
"Effort: profil",
|
||||
"Effort : profil",
|
||||
"Léger",
|
||||
"Standard",
|
||||
"Profond",
|
||||
@ -878,7 +878,7 @@ describe("AgentsPanel effort selection (#131)", () => {
|
||||
await waitForIdle();
|
||||
await screen.findByText("Thinker");
|
||||
|
||||
chooseDropdownOption("effort for Thinker", "Profond");
|
||||
chooseDropdownOption("Effort pour Thinker", "Profond");
|
||||
|
||||
await waitFor(() => {
|
||||
expect(updateSpy).toHaveBeenCalledWith(PROJECT_ID, created.id, {
|
||||
@ -911,14 +911,14 @@ describe("AgentsPanel effort selection (#131)", () => {
|
||||
|
||||
renderPanel(agent, profile);
|
||||
await waitForIdle();
|
||||
await screen.findByText("Fallback");
|
||||
await screen.findByText("Effort : profil");
|
||||
|
||||
openDropdown("effort for Fallback");
|
||||
openDropdown("Effort pour Fallback");
|
||||
const labels = screen
|
||||
.getAllByRole("option")
|
||||
.map((option) => option.textContent);
|
||||
expect(labels).toEqual([
|
||||
"Effort: profil",
|
||||
"Effort : profil",
|
||||
"Rapide (par défaut)",
|
||||
"Standard (par défaut)",
|
||||
"Approfondi (par défaut)",
|
||||
@ -943,10 +943,10 @@ describe("AgentsPanel effort selection (#131)", () => {
|
||||
|
||||
renderPanel(agent, await seededProfiles());
|
||||
await waitForIdle();
|
||||
await screen.findByText("Custom");
|
||||
await screen.findByText("Effort : profil");
|
||||
|
||||
chooseDropdownOption("effort for Custom", "Personnalisé");
|
||||
const input = screen.getByLabelText("custom effort for Custom");
|
||||
chooseDropdownOption("Effort pour Custom", "Personnalisé");
|
||||
const input = screen.getByLabelText("Effort personnalisé pour Custom");
|
||||
fireEvent.change(input, { target: { value: "x-provider-deep" } });
|
||||
fireEvent.blur(input);
|
||||
|
||||
@ -974,7 +974,7 @@ describe("AgentsPanel live refresh on domain events", () => {
|
||||
);
|
||||
|
||||
// Initially empty.
|
||||
await waitFor(() => expect(screen.getByText("No agents yet.")).toBeTruthy());
|
||||
await waitFor(() => expect(screen.getByText("Aucun agent pour le moment.")).toBeTruthy());
|
||||
|
||||
// Simulate the orchestrator creating an agent directly in the store
|
||||
// (bypassing the UI's createAgent path), then emitting `agentLaunched`.
|
||||
@ -1029,7 +1029,7 @@ describe("AgentsPanel live refresh on domain events", () => {
|
||||
// subscription effect must no-op rather than throw.
|
||||
renderPanel();
|
||||
await waitForIdle();
|
||||
expect(screen.getByText("No agents yet.")).toBeTruthy();
|
||||
expect(screen.getByText("Aucun agent pour le moment.")).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
@ -1071,7 +1071,7 @@ describe("AgentsPanel orchestration source badge (M4)", () => {
|
||||
source: "mcp",
|
||||
});
|
||||
|
||||
const badge = await screen.findByLabelText("delegation source mcp");
|
||||
const badge = await screen.findByLabelText("source de délégation mcp");
|
||||
expect(badge.textContent?.toLowerCase()).toContain("mcp");
|
||||
});
|
||||
|
||||
@ -1086,7 +1086,7 @@ describe("AgentsPanel orchestration source badge (M4)", () => {
|
||||
source: "file",
|
||||
});
|
||||
|
||||
const badge = await screen.findByLabelText("delegation source file");
|
||||
const badge = await screen.findByLabelText("source de délégation file");
|
||||
expect(badge.textContent?.toLowerCase()).toContain("file");
|
||||
});
|
||||
|
||||
@ -1103,8 +1103,8 @@ describe("AgentsPanel orchestration source badge (M4)", () => {
|
||||
|
||||
// Give the effect a tick; the map must stay empty → no badge for any source.
|
||||
await waitFor(() => expect(screen.getByText("Requester")).toBeTruthy());
|
||||
expect(screen.queryByLabelText("delegation source mcp")).toBeNull();
|
||||
expect(screen.queryByLabelText("delegation source file")).toBeNull();
|
||||
expect(screen.queryByLabelText("source de délégation mcp")).toBeNull();
|
||||
expect(screen.queryByLabelText("source de délégation file")).toBeNull();
|
||||
});
|
||||
|
||||
it("does not badge an agent whose id never matched a processed requester", async () => {
|
||||
@ -1120,6 +1120,6 @@ describe("AgentsPanel orchestration source badge (M4)", () => {
|
||||
});
|
||||
|
||||
await waitFor(() => expect(screen.getByText("Requester")).toBeTruthy());
|
||||
expect(screen.queryByLabelText("delegation source mcp")).toBeNull();
|
||||
expect(screen.queryByLabelText("source de délégation mcp")).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
@ -168,7 +168,7 @@ describe("ResumeProjectPanel — per-agent actions", () => {
|
||||
]);
|
||||
await screen.findByTestId("resume-project-panel");
|
||||
|
||||
fireEvent.click(screen.getByLabelText("resume Architect"));
|
||||
fireEvent.click(screen.getByLabelText("reprendre Architect"));
|
||||
|
||||
await waitFor(() => expect(launchSpy).toHaveBeenCalledTimes(1));
|
||||
const [projectId, agentId, options] = launchSpy.mock.calls[0];
|
||||
@ -197,7 +197,7 @@ describe("ResumeProjectPanel — per-agent actions", () => {
|
||||
"relance à neuf",
|
||||
);
|
||||
// And the primary button reads "Relancer à neuf" (FR), not "Reprendre".
|
||||
const btn = screen.getByLabelText("resume Legacy");
|
||||
const btn = screen.getByLabelText("reprendre Legacy");
|
||||
expect(btn.textContent).toBe("Relancer à neuf");
|
||||
|
||||
fireEvent.click(btn);
|
||||
@ -215,7 +215,7 @@ describe("ResumeProjectPanel — per-agent actions", () => {
|
||||
]);
|
||||
await screen.findByTestId("resume-project-panel");
|
||||
|
||||
fireEvent.click(screen.getByLabelText("new conversation Architect"));
|
||||
fireEvent.click(screen.getByLabelText("nouvelle conversation Architect"));
|
||||
|
||||
await waitFor(() => expect(launchSpy).toHaveBeenCalledTimes(1));
|
||||
// setCellConversation cleared BEFORE the launch.
|
||||
@ -238,7 +238,7 @@ describe("ResumeProjectPanel — per-agent actions", () => {
|
||||
]);
|
||||
await screen.findByTestId("resume-project-panel");
|
||||
|
||||
fireEvent.click(screen.getByLabelText("ignore Doomed"));
|
||||
fireEvent.click(screen.getByLabelText("ignorer Doomed"));
|
||||
|
||||
await waitFor(() =>
|
||||
expect(screen.queryByTestId("resume-row-a4")).toBeNull(),
|
||||
@ -255,7 +255,7 @@ describe("ResumeProjectPanel — global actions", () => {
|
||||
]);
|
||||
await screen.findByTestId("resume-project-panel");
|
||||
|
||||
fireEvent.click(screen.getByLabelText("resume all agents"));
|
||||
fireEvent.click(screen.getByLabelText("reprendre tous les agents"));
|
||||
|
||||
await waitFor(() => expect(launchSpy).toHaveBeenCalledTimes(2));
|
||||
const launchedAgentIds = launchSpy.mock.calls.map((c) => c[1]).sort();
|
||||
@ -273,7 +273,7 @@ describe("ResumeProjectPanel — global actions", () => {
|
||||
]);
|
||||
await screen.findByTestId("resume-project-panel");
|
||||
|
||||
fireEvent.click(screen.getByLabelText("ignore all agents"));
|
||||
fireEvent.click(screen.getByLabelText("ignorer tous les agents"));
|
||||
|
||||
await waitFor(() =>
|
||||
expect(screen.queryByTestId("resume-project-panel")).toBeNull(),
|
||||
|
||||
Reference in New Issue
Block a user