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:
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user