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:
2026-07-11 15:50:18 +02:00
parent b82ac76f8b
commit 72476a650a
23 changed files with 1893 additions and 15 deletions

View File

@ -13,6 +13,7 @@ import type {
Agent,
AgentProfile,
GatewayError,
ModelServerStatus,
TerminalSession,
} from "@/domain";
import type { LiveAgent, OpenTerminalOptions, TerminalHandle } from "@/ports";
@ -41,6 +42,17 @@ export interface AgentLimitState {
suspected?: boolean;
}
/**
* An agent-launch failure surfaced to the UI (F35), from `agentLaunchFailed`.
* `cause` namespaces the origin (e.g. `"model_server"`); `code`/`message` are
* the stable, actionable failure details.
*/
export interface AgentLaunchFailure {
cause: string;
code: string;
message: string;
}
/** What the agents UI needs from this hook. */
export interface AgentsViewModel {
/** All agents known to the project. */
@ -67,6 +79,19 @@ export interface AgentsViewModel {
* absent from the map is unlimited.
*/
limitByAgent: Record<string, AgentLimitState>;
/**
* Local model-server lifecycle status keyed by `serverId` (F35), folded from
* `modelServerStatusChanged`. Correlate an agent to its server through the
* profile's `opencode.localModelServerId`. A server absent from the map has no
* observed status yet.
*/
modelServerStatusByServer: Record<string, ModelServerStatus>;
/**
* Last agent-launch failure keyed by agent id (F35), folded from
* `agentLaunchFailed`. Carries an actionable `message`/`code`/`cause`. Cleared
* for an agent as soon as it launches successfully (`agentLaunched`).
*/
launchFailureByAgent: Record<string, AgentLaunchFailure>;
/** Last error message, or `null`. */
error: string | null;
/** Whether a request is in flight. */
@ -154,6 +179,12 @@ export function useAgents(projectId: string): AgentsViewModel {
const [limitByAgent, setLimitByAgent] = useState<
Record<string, AgentLimitState>
>({});
const [modelServerStatusByServer, setModelServerStatusByServer] = useState<
Record<string, ModelServerStatus>
>({});
const [launchFailureByAgent, setLaunchFailureByAgent] = useState<
Record<string, AgentLaunchFailure>
>({});
const refresh = useCallback(async () => {
setBusy(true);
@ -208,6 +239,15 @@ export function useAgents(projectId: string): AgentsViewModel {
void refresh();
void refreshLiveAgents();
}
// A successful launch supersedes any prior launch failure for that agent
// (F35): drop its stale error banner.
if (event.type === "agentLaunched") {
setLaunchFailureByAgent((prev) => {
if (!(event.agentId in prev)) return prev;
const { [event.agentId]: _cleared, ...rest } = prev;
return rest;
});
}
// Record which door a delegation came through (mcp vs file). Absent
// `source` (older backend) leaves the map untouched → no badge.
if (
@ -269,6 +309,24 @@ export function useAgents(projectId: string): AgentsViewModel {
},
}));
break;
// F35 — local model server lifecycle during launch, keyed by server id.
case "modelServerStatusChanged":
setModelServerStatusByServer((prev) => ({
...prev,
[event.serverId]: event.status,
}));
break;
// F35 — a launch failed with an actionable cause/message, keyed by agent.
case "agentLaunchFailed":
setLaunchFailureByAgent((prev) => ({
...prev,
[event.agentId]: {
cause: event.cause,
code: event.code,
message: event.message,
},
}));
break;
default:
break;
}
@ -475,6 +533,8 @@ export function useAgents(projectId: string): AgentsViewModel {
liveAgents,
delegationSourceByRequester,
limitByAgent,
modelServerStatusByServer,
launchFailureByAgent,
error,
busy,
runningAgentId,