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>
This commit is contained in:
141
frontend/src/features/agents/modelServerLaunch.ts
Normal file
141
frontend/src/features/agents/modelServerLaunch.ts
Normal file
@ -0,0 +1,141 @@
|
||||
/**
|
||||
* Shared correlation of an agent to its local model server's launch state
|
||||
* (F35 / ticket #54). One place owns the chain
|
||||
* `agentId → profileId → opencode.localModelServerId → statusByServer[serverId]`
|
||||
* so both the {@link AgentsPanel} badge and the full-cell overlay in
|
||||
* `LayoutGrid`/`LeafView` read the same truth instead of duplicating it.
|
||||
*
|
||||
* Pure data + a thin `useGateways`-backed hook: no Tauri names here (the event
|
||||
* stream and profile list come through the gateways/adapters).
|
||||
*/
|
||||
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
|
||||
import type { Agent, AgentProfile, ModelServerStatus } from "@/domain";
|
||||
import { useGateways } from "@/app/di";
|
||||
|
||||
/**
|
||||
* Correlate a pinned agent to its model server's current lifecycle status, if
|
||||
* any. Returns `undefined` when the agent's profile binds no local model server
|
||||
* or when that server has emitted no status yet.
|
||||
*/
|
||||
export function correlateModelServerStatus(
|
||||
agent: Pick<Agent, "profileId"> | undefined,
|
||||
profiles: AgentProfile[],
|
||||
statusByServer: Record<string, ModelServerStatus>,
|
||||
): ModelServerStatus | undefined {
|
||||
if (!agent) return undefined;
|
||||
const serverId = profiles.find((p) => p.id === agent.profileId)?.opencode
|
||||
?.localModelServerId;
|
||||
return serverId ? statusByServer[serverId] : undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* The full-cell overlay text for a model server that is still preparing, or
|
||||
* `null` when no overlay should show. Only the "not yet usable" states raise a
|
||||
* veil — `ready`/`failed`/`notConfigured` (and an absent status) return `null`:
|
||||
* - `downloading` → "Téléchargement du modèle…",
|
||||
* - `starting`/`probing` → "Chargement du serveur…".
|
||||
*
|
||||
* F1 MVP ignores the progress fields (bytes/percent) — the bar/% is the F2
|
||||
* stretch. This is the single predicate that decides whether the launch overlay
|
||||
* covers a cell.
|
||||
*/
|
||||
export function modelServerOverlayText(
|
||||
status: ModelServerStatus | undefined,
|
||||
): string | null {
|
||||
if (!status) return null;
|
||||
switch (status.state) {
|
||||
case "downloading":
|
||||
return "Téléchargement du modèle…";
|
||||
case "starting":
|
||||
case "probing":
|
||||
return "Chargement du serveur…";
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/** What {@link useModelServerLaunchState} exposes to a consumer. */
|
||||
export interface ModelServerLaunchState {
|
||||
/**
|
||||
* The correlated model-server status for a pinned agent (or `undefined` when
|
||||
* it has no bound server / no status yet). Stable across renders unless the
|
||||
* profiles or the per-server status map change.
|
||||
*/
|
||||
statusForAgent: (
|
||||
agent: Pick<Agent, "profileId"> | undefined,
|
||||
) => ModelServerStatus | undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Self-contained correlation source for consumers that do NOT already hold the
|
||||
* `useAgents` view-model (e.g. `LeafView`). Loads the profiles once and folds
|
||||
* every `modelServerStatusChanged` event into a per-server status map, exactly
|
||||
* like `useAgents` does — reusing {@link correlateModelServerStatus} for the
|
||||
* lookup. A profile change (which may rebind the server) re-pulls the profiles.
|
||||
*
|
||||
* `profile`/`system` may be absent in unit tests that inject a partial gateway
|
||||
* set; both are guarded, in which case the status is simply never found.
|
||||
*/
|
||||
export function useModelServerLaunchState(
|
||||
_projectId: string,
|
||||
): ModelServerLaunchState {
|
||||
const { profile, system } = useGateways();
|
||||
const [profiles, setProfiles] = useState<AgentProfile[]>([]);
|
||||
const [statusByServer, setStatusByServer] = useState<
|
||||
Record<string, ModelServerStatus>
|
||||
>({});
|
||||
|
||||
// Load (and reload) the profiles: the `profileId → localModelServerId`
|
||||
// binding lives on the profile, so we need the current list to correlate.
|
||||
const reloadProfiles = useCallback(() => {
|
||||
if (!profile) return;
|
||||
profile
|
||||
.listProfiles()
|
||||
.then(setProfiles)
|
||||
.catch(() => {
|
||||
/* ignore — no binding resolvable, overlay stays off */
|
||||
});
|
||||
}, [profile]);
|
||||
|
||||
useEffect(() => {
|
||||
reloadProfiles();
|
||||
}, [reloadProfiles]);
|
||||
|
||||
// Fold the model-server lifecycle stream, keyed by server id — the same
|
||||
// reduction `useAgents` performs. A profile change may rebind the server, so
|
||||
// re-pull the profiles too.
|
||||
useEffect(() => {
|
||||
if (!system) return;
|
||||
let unsubscribe: (() => void) | undefined;
|
||||
let cancelled = false;
|
||||
void system
|
||||
.onDomainEvent((event) => {
|
||||
if (event.type === "modelServerStatusChanged") {
|
||||
setStatusByServer((prev) => ({
|
||||
...prev,
|
||||
[event.serverId]: event.status,
|
||||
}));
|
||||
} else if (event.type === "agentProfileChanged") {
|
||||
reloadProfiles();
|
||||
}
|
||||
})
|
||||
.then((un) => {
|
||||
if (cancelled) un();
|
||||
else unsubscribe = un;
|
||||
});
|
||||
return () => {
|
||||
cancelled = true;
|
||||
unsubscribe?.();
|
||||
};
|
||||
}, [system, reloadProfiles]);
|
||||
|
||||
const statusForAgent = useCallback(
|
||||
(agent: Pick<Agent, "profileId"> | undefined) =>
|
||||
correlateModelServerStatus(agent, profiles, statusByServer),
|
||||
[profiles, statusByServer],
|
||||
);
|
||||
|
||||
return { statusForAgent };
|
||||
}
|
||||
Reference in New Issue
Block a user