feat(ui): édition templates — nom d'agent par défaut, permissions système et MCP outils — #242

TemplateEditor/TemplatesPanel étendus aux nouveaux champs, projection
useTemplates/adapters/ports, panneau Agents aligné, templatePresentation.ts
extrait. Tests frontend étendus (templates, agents, routage chat WS).
This commit is contained in:
2026-08-23 11:00:04 +02:00
parent d6c80be56d
commit f952807882
13 changed files with 551 additions and 174 deletions

View File

@ -20,6 +20,7 @@ import { Button, Input, Panel, SmallDropdown, Spinner, cn } from "@/shared";
import { TerminalView } from "@/features/terminals/TerminalView";
import { AnnouncementsPreview } from "@/features/announcements";
import { useDrift } from "@/features/templates/useDrift";
import { templateProfileLabel } from "@/features/templates/templatePresentation";
import { useGateways } from "@/app/di";
import { useAgents } from "./useAgents";
import { correlateModelServerStatus } from "./modelServerLaunch";
@ -203,7 +204,11 @@ export function AgentsPanel({ projectId, projectRoot = "" }: AgentsPanelProps) {
}
}
const canCreate = newName.trim().length > 0 && !vm.busy;
const selectedTemplate = templates.find((t) => t.id === newTemplateId) ?? null;
const selectedTemplateMissingProfile =
selectedTemplate !== null && selectedTemplate.defaultProfileId.trim().length === 0;
const canCreate =
newName.trim().length > 0 && !vm.busy && !selectedTemplateMissingProfile;
async function handleCreate(e: React.FormEvent) {
e.preventDefault();
@ -225,6 +230,14 @@ export function AgentsPanel({ projectId, projectRoot = "" }: AgentsPanelProps) {
setNewTemplateId("");
}
function handleTemplateChange(templateId: string) {
setNewTemplateId(templateId);
const next = templates.find((template) => template.id === templateId);
if (next?.defaultAgentName?.trim()) {
setNewName(next.defaultAgentName.trim());
}
}
function handleLaunch(agentId: string) {
setPanelNodeIds((prev) => ({
...prev,
@ -238,10 +251,9 @@ export function AgentsPanel({ projectId, projectRoot = "" }: AgentsPanelProps) {
setActiveAgentId(null);
}
const selectedAgent = vm.agents.find((a) => a.id === vm.selectedAgentId) ?? null;
// Determine if a template is chosen → profile selector is hidden (template imposes it).
const hasTemplate = newTemplateId !== "";
const selectedAgent = vm.agents.find((a) => a.id === vm.selectedAgentId) ?? null;
const profileLabel = (profile: AgentProfile): string => {
const model =
@ -297,7 +309,7 @@ export function AgentsPanel({ projectId, projectRoot = "" }: AgentsPanelProps) {
id="agent-template-select"
aria-label="agent template"
value={newTemplateId}
onChange={setNewTemplateId}
onChange={handleTemplateChange}
className="w-full"
disabled={vm.busy}
size="md"
@ -306,6 +318,28 @@ export function AgentsPanel({ projectId, projectRoot = "" }: AgentsPanelProps) {
...templates.map((t) => ({ value: t.id, label: t.name })),
]}
/>
{selectedTemplate && (
<div className="rounded-md border border-border bg-raised px-3 py-2 text-xs text-muted">
<div>
Nom initial:{" "}
<span className="text-content">
{selectedTemplate.defaultAgentName || selectedTemplate.name}
</span>
</div>
<div>
Modèle appliqué:{" "}
<span className="text-content">
{templateProfileLabel(selectedTemplate, vm.profiles)}
</span>
</div>
{selectedTemplateMissingProfile && (
<p role="alert" className="mt-1 text-danger">
Ce template n'a pas de modèle AI par défaut. Choisis un autre
template ou édite-le avant de créer l'agent.
</p>
)}
</div>
)}
</div>
{/* Profile selector — hidden when a template is chosen */}

View File

@ -391,6 +391,38 @@ describe("AgentsPanel (with MockAgentGateway)", () => {
await screen.findByText("From Template");
});
it("selecting a template pre-fills its default agent name and applies its default profile", async () => {
const agent = new MockAgentGateway();
const tmpl = new MockTemplateGateway(agent);
const t = await tmpl.createTemplate({
name: "Main template",
content: "# ctx",
defaultProfileId: "profile-main",
defaultAgentName: "Main",
});
const createFromTemplateSpy = vi.spyOn(tmpl, "createAgentFromTemplate");
renderPanel(agent, new MockProfileGateway(), "/home/me/proj", tmpl);
await waitForIdle();
chooseDropdownOption("agent template", "Main template");
expect((screen.getByLabelText("agent name") as HTMLInputElement).value).toBe("Main");
expect(screen.getByText(/Modèle appliqué:/)).toBeTruthy();
expect(screen.getByText("Profil profile-main")).toBeTruthy();
fireEvent.click(screen.getByRole("button", { name: "create agent" }));
await waitFor(() => {
expect(createFromTemplateSpy).toHaveBeenCalledWith(PROJECT_ID, t.id, {
name: "Main",
synchronized: true,
});
});
const agents = await agent.listAgents(PROJECT_ID);
expect(agents[0].name).toBe("Main");
expect(agents[0].profileId).toBe("profile-main");
});
it("shows profiles in the dropdown when profiles are configured", async () => {
const profile = new MockProfileGateway();
await profile.configureProfiles([