feat(model-server): frontend modèles locaux — badge de statut & CRUD serveurs (#35) et wizard multi-profils OpenCode (#36)
Sprint « Modeles locaux », couche frontend. #35 : - F35.1 badge de statut de lancement du serveur local (ModelServerLaunchBadge + useAgentsModelServer). - F35.2 feature model-servers : CRUD (ModelServersPanel / useModelServers / gateway modelServer) et ModelServerSelect. #36 : - Liste multi-profils OpenCode dans le wizard de premier lancement, gateway de clonage (clone_opencode_profile_from_seed). Tests verts (exécution réelle) : tsc propre, vitest 608/608. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
115
frontend/src/features/agents/ModelServerLaunchBadge.tsx
Normal file
115
frontend/src/features/agents/ModelServerLaunchBadge.tsx
Normal file
@ -0,0 +1,115 @@
|
||||
/**
|
||||
* `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 "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 {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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user