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.
121 lines
4.5 KiB
TypeScript
121 lines
4.5 KiB
TypeScript
/**
|
||
* `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")} {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>
|
||
);
|
||
}
|