Files
IdeA/frontend/src/features/agents/ModelServerLaunchBadge.tsx
Blomios 1a1429f10e 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.
2026-09-11 21:53:56 +02:00

121 lines
4.5 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* `ModelServerLaunchBadge` — presentational launch-state indicator for an agent
* whose OpenCode profile binds a local model server (F35.1). All state is folded
* in {@link useAgents} (`modelServerStatusByServer` + `launchFailureByAgent`);
* this component only renders it.
*
* It shows, in order of severity:
* - a launch **failure** with its actionable message (not a bare "agent failed")
* — the full logs are never shown by default; a "Détails" toggle reveals the
* stable code/cause only;
* - otherwise the model-server lifecycle: `probing`, `starting`, `ready`
* (distinguishing a reused server from a freshly-started one).
*
* Renders nothing when there is neither a status to show nor a failure, so a
* plain (non-local-model) agent row stays untouched.
*/
import { useState } from "react";
import { cn } from "@/shared";
import { useI18n } from "@/i18n";
import type { ModelServerStatus } from "@/domain";
import type { AgentLaunchFailure } from "./useAgents";
export interface ModelServerLaunchBadgeProps {
/** The bound server's lifecycle status, when one has been observed. */
status?: ModelServerStatus;
/** The agent's last launch failure, when one occurred. */
failure?: AgentLaunchFailure;
}
/** 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: tr("vérification du serveur…", "checking server…"), tone: "bg-muted/20 text-muted", role: "status" };
case "starting":
return { label: tr("démarrage du serveur…", "starting server…"), tone: "bg-warning/20 text-warning", role: "status" };
case "downloading":
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: 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;
}
}
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
// the launch could not complete — prefer the agent-scoped failure (it carries
// the actionable message); fall back to the server `failed` status.
const failed: { code: string; message: string; cause?: string } | null =
failure
? failure
: status?.state === "failed"
? { code: status.code, message: status.message }
: null;
if (failed) {
return (
<span className="flex min-w-0 flex-wrap items-center gap-2">
<span
role="alert"
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",
)}
>
{tr("Échec du lancement :", "Launch failed:")} {failed.message}
</span>
<button
type="button"
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"
>
{tr("Détails", "Details")}
</button>
{showDetail && (
<span aria-label={tr("détail de l'échec du lancement", "launch failure detail")} className="text-xs text-faint">
{tr("code", "code")}&nbsp;{failed.code}
{failed.cause ? ` · ${tr("cause", "cause")} ${failed.cause}` : ""}
</span>
)}
</span>
);
}
if (!status) return null;
const described = describeStatus(status, tr);
if (!described) return null;
return (
<span
role={described.role}
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,
)}
>
{described.label}
</span>
);
}