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:
@ -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 */}
|
||||
|
||||
@ -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([
|
||||
|
||||
Reference in New Issue
Block a user