feat(frontend): groupes repliables par plugin pour skills et tools MCP, bouton Joindre harmonisé avec Envoyer — nouveau composant PluginContributionGroup (en-tête cliquable aria-expanded/aria-controls, badge Chargé/Non chargé, compteur de contributions, replié par défaut pour les plugins non loadés, dépliage forcé pendant la recherche) adopté par SkillsPanel et McpToolPermissionsPanel avec déduplication des plugins — #367 ; bouton Joindre du chat aligné sur le format d'Envoyer (size md, variante secondary, état loading « Ajout… », aria-label dédié, conteneur custom-agent-chat-composer-actions avec data-busy, Stop aligné md, container queries 419.99px en colonne et 279.99px wrap puis pleine largeur en busy) — #368 (QA verte: npm test ciblé ProjectPluginSettings + skills + McpToolPermissionsPanel + CustomAgentChatView + visibleSourceScan 5 fichiers 152 tests OK, Firefox headless layout 600/360/240 et busy 240 PASS, npm run build OK, git diff --check OK)

This commit is contained in:
2026-09-17 09:46:21 +02:00
parent 4c86f9bb6f
commit 5d48a39b18
7 changed files with 453 additions and 51 deletions

View File

@ -1093,6 +1093,7 @@ export function CustomAgentChatView({
? (chatAttachmentDraftCache.get(initialAttachmentCacheKey) ?? [])
: [],
);
const [attachmentPicking, setAttachmentPicking] = useState(false);
const [queuedMessages, setQueuedMessages] = useState<QueuedChatMessage[]>(
() => chatMessageQueueCache.get(initialMessageQueueKey) ?? [],
);
@ -2597,16 +2598,21 @@ export function CustomAgentChatView({
}
async function pickAttachment() {
const path = await system.pickFile();
if (path) {
setAttachments((prev) => [
...prev,
{
id: `path-${Date.now()}-${Math.random().toString(36).slice(2)}`,
label: pathBasename(path),
input: { path, sourceKind: "localFile" },
},
]);
setAttachmentPicking(true);
try {
const path = await system.pickFile();
if (path) {
setAttachments((prev) => [
...prev,
{
id: `path-${Date.now()}-${Math.random().toString(36).slice(2)}`,
label: pathBasename(path),
input: { path, sourceKind: "localFile" },
},
]);
}
} finally {
setAttachmentPicking(false);
}
}
@ -3608,8 +3614,8 @@ export function CustomAgentChatView({
{composerHistoryWarning}
</p>
)}
<div className="flex min-w-0 items-end gap-2">
<div className="relative flex min-w-0 flex-1">
<div className="custom-agent-chat-composer flex min-w-0 items-end gap-2">
<div className="custom-agent-chat-composer-editor relative flex min-w-0 flex-1">
<textarea
ref={composerRef}
aria-label={`${tr("Message CLI personnalisée", "Custom CLI message")} ${domId}`}
@ -3677,39 +3683,47 @@ export function CustomAgentChatView({
↕
</button>
</div>
<Button
size="sm"
variant="ghost"
className="shrink-0 whitespace-nowrap"
disabled={!supported || opening || Boolean(savedConversationLoadFailure)}
onClick={() => void pickAttachment()}
<div
className="custom-agent-chat-composer-actions flex shrink-0 items-end gap-2"
data-busy={effectiveBusy ? "true" : "false"}
>
{t("custom.attach")}
</Button>
{effectiveBusy && (
<Button
size="sm"
variant="danger"
className="min-h-11 shrink-0 whitespace-nowrap"
disabled={!supported || !currentSession || !agent.cancelAgentChat || stopping}
loading={stopping}
title={t("custom.stop")}
aria-label={t("custom.stop")}
onClick={() => void stopCurrentRequest()}
size="md"
variant="secondary"
className="custom-agent-chat-composer-action min-h-11 whitespace-nowrap"
aria-label={tr("Joindre un fichier", "Attach a file")}
disabled={!supported || opening || attachmentPicking || Boolean(savedConversationLoadFailure)}
loading={attachmentPicking}
onClick={() => void pickAttachment()}
>
{stopping ? t("custom.stopping") : t("custom.stop")}
{attachmentPicking ? tr("Ajout…", "Adding…") : t("custom.attach")}
</Button>
)}
<Button
size="sm"
className="min-h-11 shrink-0 whitespace-nowrap"
disabled={!canSubmit}
onClick={() => void send()}
>
{effectiveBusy
? tr("Mettre en attente", "Queue message")
: t("custom.send")}
</Button>
{effectiveBusy && (
<Button
size="md"
variant="danger"
className="custom-agent-chat-composer-action min-h-11 whitespace-nowrap"
disabled={!supported || !currentSession || !agent.cancelAgentChat || stopping}
loading={stopping}
title={t("custom.stop")}
aria-label={t("custom.stop")}
onClick={() => void stopCurrentRequest()}
>
{stopping ? t("custom.stopping") : t("custom.stop")}
</Button>
)}
<Button
size="md"
variant="primary"
className="custom-agent-chat-composer-action min-h-11 whitespace-nowrap"
disabled={!canSubmit}
onClick={() => void send()}
>
{effectiveBusy
? tr("Mettre en attente", "Queue message")
: t("custom.send")}
</Button>
</div>
</div>
</div>
</div>