- #70: implémentation suppression modèles locaux téléchargés - #100: correction scroll OpenCode - #102: correction fit TUI après switch/layout - memory note scoping UX
This commit is contained in:
@ -15,6 +15,7 @@ import { useEffect, useRef, useState } from "react";
|
||||
|
||||
import type {
|
||||
LocalModelServerConfig,
|
||||
ModelArtifact,
|
||||
ModelServerCommandPreview,
|
||||
ModelSource,
|
||||
StopPolicy,
|
||||
@ -47,6 +48,35 @@ const STOP_POLICIES: { value: StopPolicy; label: string }[] = [
|
||||
{ value: "stopWhenUnused", label: "Stop when unused" },
|
||||
];
|
||||
|
||||
function formatBytes(bytes: number): string {
|
||||
if (!Number.isFinite(bytes) || bytes < 0) return "";
|
||||
const units = ["B", "KB", "MB", "GB", "TB"] as const;
|
||||
let value = bytes;
|
||||
let unit = 0;
|
||||
while (value >= 1000 && unit < units.length - 1) {
|
||||
value /= 1000;
|
||||
unit += 1;
|
||||
}
|
||||
const digits = unit === 0 || value >= 10 ? 0 : 1;
|
||||
return `${value.toFixed(digits)} ${units[unit]}`;
|
||||
}
|
||||
|
||||
function deleteArtifactConfirmation(
|
||||
server: LocalModelServerConfig,
|
||||
artifact: Extract<ModelArtifact, { state: "downloaded" }>,
|
||||
): string {
|
||||
const size =
|
||||
artifact.sizeBytes == null ? "" : `Espace libéré : ${formatBytes(artifact.sizeBytes)}.`;
|
||||
return [
|
||||
"Supprimer le modèle téléchargé ?",
|
||||
`Serveur : ${server.name}`,
|
||||
"Le serveur local restera configuré, mais IdeA devra retélécharger ce modèle au prochain lancement.",
|
||||
size,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join("\n");
|
||||
}
|
||||
|
||||
export interface ModelServersPanelProps {
|
||||
/** The model-server registry view-model (from `useModelServers`). */
|
||||
vm: ModelServersViewModel;
|
||||
@ -73,6 +103,14 @@ export function ModelServersPanel({ vm }: ModelServersPanelProps) {
|
||||
if (saved) setDraft(null);
|
||||
}
|
||||
|
||||
async function confirmDeleteArtifact(
|
||||
server: LocalModelServerConfig,
|
||||
artifact: Extract<ModelArtifact, { state: "downloaded" }>,
|
||||
) {
|
||||
if (!window.confirm(deleteArtifactConfirmation(server, artifact))) return;
|
||||
await vm.deleteArtifact(server.id);
|
||||
}
|
||||
|
||||
return (
|
||||
<Panel
|
||||
aria-label="local model servers"
|
||||
@ -98,6 +136,11 @@ export function ModelServersPanel({ vm }: ModelServersPanelProps) {
|
||||
{vm.error}
|
||||
</p>
|
||||
)}
|
||||
{vm.notice && (
|
||||
<p role="status" className="text-sm text-muted">
|
||||
{vm.notice}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{vm.servers.length === 0 && !draft && (
|
||||
<p className="text-xs text-muted">
|
||||
@ -107,40 +150,69 @@ export function ModelServersPanel({ vm }: ModelServersPanelProps) {
|
||||
)}
|
||||
|
||||
<ul className="flex list-none flex-col gap-2 p-0">
|
||||
{vm.servers.map((server) => (
|
||||
<li
|
||||
key={server.id}
|
||||
className="flex items-center gap-2 rounded-md border border-border bg-raised p-2"
|
||||
>
|
||||
<span className="flex min-w-0 flex-col">
|
||||
<strong className="truncate text-sm text-content">
|
||||
{server.name}
|
||||
</strong>
|
||||
<span className="truncate text-xs text-muted">
|
||||
{server.baseURL} · {server.servedModelName}
|
||||
{server.autoStart ? " · auto-start" : ""}
|
||||
{vm.servers.map((server) => {
|
||||
const artifact = server.artifact;
|
||||
const downloaded =
|
||||
artifact?.state === "downloaded" ? artifact : undefined;
|
||||
const deletingArtifact = vm.deletingArtifactId === server.id;
|
||||
const downloadingArtifact = artifact?.state === "downloading";
|
||||
return (
|
||||
<li
|
||||
key={server.id}
|
||||
className="flex items-center gap-2 rounded-md border border-border bg-raised p-2"
|
||||
>
|
||||
<span className="flex min-w-0 flex-col">
|
||||
<strong className="truncate text-sm text-content">
|
||||
{server.name}
|
||||
</strong>
|
||||
<span className="truncate text-xs text-muted">
|
||||
{server.baseURL} · {server.servedModelName}
|
||||
{server.autoStart ? " · auto-start" : ""}
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
<span className="ml-auto flex items-center gap-1">
|
||||
<Button
|
||||
size="sm"
|
||||
aria-label={`edit ${server.name}`}
|
||||
onClick={() => startEdit(server)}
|
||||
disabled={vm.busy}
|
||||
>
|
||||
Edit
|
||||
</Button>
|
||||
<IconButton
|
||||
size="sm"
|
||||
aria-label={`delete ${server.name}`}
|
||||
onClick={() => void vm.remove(server.id)}
|
||||
disabled={vm.busy}
|
||||
>
|
||||
×
|
||||
</IconButton>
|
||||
</span>
|
||||
</li>
|
||||
))}
|
||||
<span className="ml-auto flex items-center gap-1">
|
||||
{downloadingArtifact && (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
aria-label={`download in progress ${server.name}`}
|
||||
disabled
|
||||
>
|
||||
Téléchargement en cours
|
||||
</Button>
|
||||
)}
|
||||
{downloaded && (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="danger"
|
||||
aria-label={`delete downloaded model ${server.name}`}
|
||||
onClick={() => void confirmDeleteArtifact(server, downloaded)}
|
||||
loading={deletingArtifact}
|
||||
disabled={vm.busy && !deletingArtifact}
|
||||
>
|
||||
{deletingArtifact ? "Suppression..." : "Supprimer le modèle téléchargé"}
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
size="sm"
|
||||
aria-label={`edit ${server.name}`}
|
||||
onClick={() => startEdit(server)}
|
||||
disabled={vm.busy}
|
||||
>
|
||||
Edit
|
||||
</Button>
|
||||
<IconButton
|
||||
size="sm"
|
||||
aria-label={`delete ${server.name}`}
|
||||
onClick={() => void vm.remove(server.id)}
|
||||
disabled={vm.busy}
|
||||
>
|
||||
×
|
||||
</IconButton>
|
||||
</span>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
|
||||
{draft && (
|
||||
|
||||
Reference in New Issue
Block a user