Files
IdeaSDK/frontend/src/features/agents/ModelServerLaunchBadge.tsx
Blomios fe7ed0aa20 feat(model-server): afficher le téléchargement du modèle llamacpp au démarrage (#54)
Ajoute un handle du téléchargement des modèles lors du démarrage de
llamacpp : le domaine et l'application émettent la progression de
téléchargement du modèle, relayée en événement côté app-tauri, et l'UI
l'affiche via un badge de lancement et un overlay de cellule pendant que
le serveur de modèle démarre.

Backend (B1) : progression de téléchargement dans domain/application,
relais d'événement app-tauri, couverture de tests.
Frontend (F1) : modelServerLaunch, badge et overlay LayoutGrid, tests.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-13 23:14:05 +02:00

118 lines
4.0 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 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,
): { 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" };
case "starting":
return { label: "démarrage du serveur…", 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" };
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" };
case "failed":
// Handled by the failure branch below (richer, actionable).
return null;
}
}
export function ModelServerLaunchBadge({
status,
failure,
}: ModelServerLaunchBadgeProps) {
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="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}
</span>
<button
type="button"
aria-label="toggle launch failure detail"
onClick={() => setShowDetail((v) => !v)}
className="text-xs text-muted underline hover:text-content"
>
Détails
</button>
{showDetail && (
<span aria-label="launch failure detail" className="text-xs text-faint">
code&nbsp;{failed.code}
{failed.cause ? ` · cause ${failed.cause}` : ""}
</span>
)}
</span>
);
}
if (!status) return null;
const described = describeStatus(status);
if (!described) return null;
return (
<span
role={described.role}
aria-label="model server status"
className={cn(
"rounded-full px-2 py-0.5 text-xs font-medium",
described.tone,
)}
>
{described.label}
</span>
);
}