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:
2026-07-13 23:14:05 +02:00
parent c67ec4f7bd
commit fe7ed0aa20
12 changed files with 891 additions and 29 deletions

View File

@ -38,6 +38,10 @@ import {
TargetAnnouncementsOverlay,
useTargetAnnouncements,
} from "@/features/announcements";
import {
modelServerOverlayText,
useModelServerLaunchState,
} from "@/features/agents";
import { leaves, normalizeWeights, resizeAdjacent } from "./layout";
import { useLayout, type LayoutViewModel } from "./useLayout";
@ -367,6 +371,18 @@ function LeafView({
// write-portal veil can be mutually excluded with the F3 overlay (exactly one
// full-cell veil at a time, F3 first). Same source the overlay itself reads.
const { active: busyActive } = useTargetAnnouncements(agentId ?? "");
// Ticket #54 — local model-server launch state for this cell's pinned agent.
// Correlated `agentId → profileId → opencode.localModelServerId → status` via
// the shared hook (same reduction as `useAgents`). While the bound server is
// still preparing (downloading a model / starting / probing), a full-cell
// overlay replaces the timeout/error the empty terminal used to show. It has
// the HIGHEST veil priority: nothing else in the cell is usable until the
// server is ready, so it suppresses the write-portal and F3 veils.
const { statusForAgent } = useModelServerLaunchState(projectId);
const pinnedAgent = agentId
? agents.find((a) => a.id === agentId)
: undefined;
const modelServerOverlay = modelServerOverlayText(statusForAgent(pinnedAgent));
const agentWork = agentId
? workState?.agents.find((row) => row.agentId === agentId)
: undefined;
@ -894,7 +910,8 @@ function LeafView({
is being injected into the agent's PTY, a grey veil with a centred
message sits above the terminal. Only ever shown for an agent cell —
and never together with the F3 overlay (exactly one veil, F3 first). */}
{shouldShowWritePortalVeil(agentId != null, Boolean(overlay), busyActive) && (
{!modelServerOverlay &&
shouldShowWritePortalVeil(agentId != null, Boolean(overlay), busyActive) && (
<div
data-testid="write-portal-overlay"
role="status"
@ -931,9 +948,46 @@ function LeafView({
busy state (agentBusyChanged + read-model hydration), never by the raw
PTY — it retracts at idle even when a turn ends without a completion
event. Self-guards on `active` (busy) and renders null otherwise. */}
{agentId != null && (
{!modelServerOverlay && agentId != null && (
<TargetAnnouncementsOverlay projectId={projectId} agentId={agentId} />
)}
{/* Ticket #54 — model-server launch veil. Top-priority full-cell overlay
(suppresses the write-portal + F3 veils above): while the pinned
agent's bound local server is downloading its model / starting /
probing, the empty terminal is covered rather than left to time out.
Retracts at `ready`/`failed` (the predicate returns null then). */}
{modelServerOverlay && (
<div
data-testid="model-server-overlay"
role="status"
aria-live="polite"
aria-label="model server launch"
style={{
position: "absolute",
inset: 0,
zIndex: CELL_Z.veil,
display: "flex",
alignItems: "center",
justifyContent: "center",
background: "rgba(0, 0, 0, 0.55)",
color: "var(--color-content, #e0e0e0)",
fontSize: 13,
pointerEvents: "none",
userSelect: "none",
}}
>
<span
style={{
background: "var(--color-surface, #1e1e1e)",
border: "1px solid var(--color-border, #3a3a3a)",
borderRadius: 4,
padding: "6px 12px",
}}
>
{modelServerOverlay}
</span>
</div>
)}
</div>
{busyNotice && (
<div