/** * `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 ( {tr("Échec du lancement :", "Launch failed:")} {failed.message} {showDetail && ( {tr("code", "code")} {failed.code} {failed.cause ? ` · ${tr("cause", "cause")} ${failed.cause}` : ""} )} ); } if (!status) return null; const described = describeStatus(status, tr); if (!described) return null; return ( {described.label} ); }