feat(agents): role orchestrateur du contexte global expose et configurable par agent — isOrchestrator distinct des permissions MCP, DTO update_project_orchestrator (terminologie wire legacy conservee), refus idea_context_update explicite avec writer effectif + orientation idea_context_propose, surface AgentsPanel + adaptateurs frontend — #276 (QA verte: cargo fmt/check, tests cibles application/app-tauri/infrastructure/backend, npm typecheck, 69/69 tests frontend agents/adapters)

This commit is contained in:
2026-09-07 22:49:12 +02:00
parent ad2c6b4354
commit cb5196fd69
24 changed files with 663 additions and 37 deletions

View File

@ -188,6 +188,11 @@ export function AgentsPanel({ projectId, projectRoot = "" }: AgentsPanelProps) {
agentId: string;
profileId: string;
} | null>(null);
const [pendingWriterChange, setPendingWriterChange] = useState<{
agentId: string;
agentName: string;
currentWriterName: string;
} | null>(null);
async function confirmProfileChange() {
if (!pendingProfileChange) return;
@ -204,6 +209,27 @@ export function AgentsPanel({ projectId, projectRoot = "" }: AgentsPanelProps) {
}
}
function requestWriterChange(agent: Agent) {
if (agent.isOrchestrator || vm.busy) return;
const currentWriter = vm.agents.find((candidate) => candidate.isOrchestrator);
if (currentWriter && currentWriter.id !== agent.id) {
setPendingWriterChange({
agentId: agent.id,
agentName: agent.name,
currentWriterName: currentWriter.name,
});
return;
}
void vm.updateProjectOrchestrator(agent.id);
}
async function confirmWriterChange() {
if (!pendingWriterChange) return;
const agentId = pendingWriterChange.agentId;
setPendingWriterChange(null);
await vm.updateProjectOrchestrator(agentId);
}
const selectedTemplate = templates.find((t) => t.id === newTemplateId) ?? null;
const selectedTemplateMissingProfile =
selectedTemplate !== null && selectedTemplate.defaultProfileId.trim().length === 0;
@ -440,6 +466,14 @@ export function AgentsPanel({ projectId, projectRoot = "" }: AgentsPanelProps) {
>
<span className="flex min-w-0 max-w-full flex-wrap items-center gap-2">
<span className="font-medium text-content">{a.name}</span>
{a.isOrchestrator && (
<span
aria-label="Writer contexte"
className="rounded-full border border-border-strong bg-raised px-2 py-0.5 text-xs font-medium text-content"
>
Writer contexte
</span>
)}
{agentDrift && (
<span
aria-label="update available"
@ -471,6 +505,35 @@ export function AgentsPanel({ projectId, projectRoot = "" }: AgentsPanelProps) {
)}
</button>
{isSelected && (
<section
aria-label={`Rôle dans le projet — ${a.name}`}
className="rounded-md border border-border bg-surface p-3"
>
<h4 className="text-xs font-semibold uppercase tracking-wide text-muted">
Rôle dans le projet
</h4>
<label className="mt-2 flex cursor-pointer items-start gap-2">
<input
type="radio"
name="project-context-writer"
aria-label={`Writer du contexte projet global pour ${a.name}`}
checked={a.isOrchestrator === true}
disabled={vm.busy}
onChange={() => requestWriterChange(a)}
className="mt-0.5"
/>
<span className="text-sm font-medium text-content">
Writer du contexte projet global
</span>
</label>
<p className="mt-1 pl-5 text-xs text-muted">
Peut modifier directement le contexte partagé du projet.
Un seul agent peut avoir ce rôle.
</p>
</section>
)}
{limitState && (
<AgentLimitBadge
state={limitState}
@ -747,6 +810,44 @@ export function AgentsPanel({ projectId, projectRoot = "" }: AgentsPanelProps) {
</div>
</div>
)}
{pendingWriterChange && (
<div
role="dialog"
aria-modal="true"
aria-label="Confirmer le changement de writer contexte"
className="fixed inset-0 z-50 flex items-center justify-center bg-black/55 p-4"
>
<div className="w-full max-w-sm rounded-lg border border-border bg-surface p-5 shadow-xl">
<h4 className="text-sm font-semibold text-content">
Changer le writer du contexte
</h4>
<p className="mt-2 text-sm text-muted">
Le rôle sera retiré de {pendingWriterChange.currentWriterName} et
attribué à {pendingWriterChange.agentName}. Continuer&nbsp;?
</p>
<div className="mt-4 flex items-center justify-end gap-2">
<Button
variant="ghost"
aria-label="Annuler le changement de writer contexte"
disabled={vm.busy}
onClick={() => setPendingWriterChange(null)}
>
Annuler
</Button>
<Button
variant="primary"
aria-label="Confirmer le changement de writer contexte"
loading={vm.busy}
disabled={vm.busy}
onClick={() => void confirmWriterChange()}
>
Continuer
</Button>
</div>
</div>
</div>
)}
</Panel>
);
}