feat(domain,application,infrastructure,backend,app-tauri,frontend): lot utilisateur #350 — animation d'écartement des onglets au drag/reorder, icônes projet N&B colorisées derrière les messages, tools/skills plugin utilisables par les agents autorisés en headless, refs stables des skills assignés (QA verte)

- Tabs (shared/ui): cible de drop matérialisée par écartement before/after pendant le drag/reorder, reset via rAF sans reliquat, clavier et annonces préservés.
- projectIcons: icônes projet en noir et blanc par défaut, colorisées avec la couleur du projet, occupant la cellule et visibles derrière les messages rendus légèrement transparents.
- Permissions plugin headless: PluginMcpServerSpec transporte l'allowlist exacte authorized_tools regroupée par (pluginId, serverId) depuis les PluginToolRef; projection Codex enabled_tools + default_tools_approval_mode="approve" — une permission déjà accordée au niveau projet/template ne déclenche pas de seconde approbation, un tool non assigné reste hors surface appelable; réserves orchestrator/mcp server+tools alignées, dto backend + dto_agents app-tauri.
- Skills: ref stable plugin:<pluginId>:<skillId> exposée dans le contexte agent, idea_skill_read(ref=...) canonique via read_assigned_by_ref, erreur structurée ambiguousSkillName avec candidates; SkillsPanel dédoublonné (android-build-install); agents-dev/L12-skills.md à jour.
- Docs: ARCHITECTURE.md section 22.3 (projection des autorisations agent vers les runtimes plugin).
- QA verte #350 reconfirmée avant commit.
This commit is contained in:
2026-09-13 14:11:31 +02:00
parent 1e15bf9f96
commit 92d0dce2b7
23 changed files with 660 additions and 95 deletions

View File

@ -45,6 +45,7 @@ import {
} from "@/features/projects/projectCliInteractionActivity";
import { Button, EntryTimestamp, Spinner, cn, normalizeTimestamp } from "@/shared";
import { onProjectIconChanged, ProjectIcon } from "@/features/projects/projectIcons";
import { onProjectThemeChanged, validThemeColor } from "@/features/projects/projectTheme";
import type {
ChatAttachmentInput,
ChatDraftScope,
@ -1012,6 +1013,7 @@ export function CustomAgentChatView({
const [stopTerminalOverride, setStopTerminalOverride] = useState(false);
const [error, setError] = useState<ChatErrorState | null>(null);
const [projectIcon, setProjectIcon] = useState<ProjectIconRef | null>(null);
const [projectThemeColor, setProjectThemeColor] = useState<string | null>(null);
const scrollRef = useRef<HTMLDivElement | null>(null);
const composerRef = useRef<HTMLTextAreaElement | null>(null);
const chatRootRef = useRef<HTMLDivElement | null>(null);
@ -1062,12 +1064,24 @@ export function CustomAgentChatView({
useEffect(() => {
let cancelled = false;
void project?.readProjectSettings(projectId).then((settings) => {
if (!cancelled) setProjectIcon(settings.icon ?? null);
if (!cancelled) {
setProjectIcon(settings.icon ?? null);
setProjectThemeColor(validThemeColor(settings.themeColor));
}
}).catch(() => undefined);
const unsubscribe = onProjectIconChanged((change) => {
const unsubscribeIcon = onProjectIconChanged((change) => {
if (change.projectId === projectId) setProjectIcon(change.icon);
});
return () => { cancelled = true; unsubscribe(); };
const unsubscribeTheme = onProjectThemeChanged((change) => {
if (change.projectId === projectId) {
setProjectThemeColor(validThemeColor(change.themeColor));
}
});
return () => {
cancelled = true;
unsubscribeIcon();
unsubscribeTheme();
};
}, [project, projectId]);
const clampComposerHeight = useCallback(
@ -2494,9 +2508,14 @@ export function CustomAgentChatView({
>
<div
data-testid="project-icon-watermark"
data-color-mode={projectThemeColor ? "project" : "monochrome"}
aria-hidden="true"
className="pointer-events-none absolute inset-0 z-0 flex items-center justify-center overflow-hidden text-content opacity-[0.035]"
style={{ pointerEvents: "none", containerType: "size" }}
className="pointer-events-none absolute inset-0 z-0 flex items-center justify-center overflow-hidden text-content opacity-[0.055]"
style={{
pointerEvents: "none",
containerType: "size",
color: projectThemeColor ?? undefined,
}}
>
<ProjectIcon
icon={projectIcon}
@ -2521,14 +2540,14 @@ export function CustomAgentChatView({
</div>
{!supported && (
<p role="alert" className="m-3 rounded-md border border-warning/40 bg-warning/10 p-2 text-xs text-warning">
<p role="alert" className="relative z-10 m-3 rounded-md border border-warning/40 bg-warning/10 p-2 text-xs text-warning">
{t("custom.unsupported")}
</p>
)}
{error && (
<div
role="alert"
className="m-3 rounded-md border border-danger/40 bg-danger/10 p-2 text-xs text-danger"
className="relative z-10 m-3 rounded-md border border-danger/40 bg-danger/10 p-2 text-xs text-danger"
>
{error.openFailure ? (
<div className="flex min-w-0 flex-col gap-2">
@ -2569,7 +2588,7 @@ export function CustomAgentChatView({
aria-modal="true"
aria-labelledby={`profile-command-title-${domId}`}
className={cn(
"border border-border bg-surface p-3 shadow-lg",
"relative z-30 border border-border bg-surface p-3 shadow-lg",
mobile
? "fixed inset-x-0 bottom-0 z-40 m-0 rounded-t-md pb-[max(0.75rem,env(safe-area-inset-bottom))]"
: "m-3 rounded-md",
@ -2667,7 +2686,7 @@ export function CustomAgentChatView({
<div
ref={scrollRef}
data-testid="custom-agent-chat-scroll"
className="flex min-h-0 flex-1 basis-0 flex-col gap-2 overflow-x-hidden overflow-y-auto px-3 py-3"
className="relative z-10 flex min-h-0 flex-1 basis-0 flex-col gap-2 overflow-x-hidden overflow-y-auto px-3 py-3"
>
<div className="shrink-0">
<AnnouncementsPreview
@ -2738,7 +2757,7 @@ export function CustomAgentChatView({
<div
data-testid="custom-agent-chat-composer"
className={cn(
"flex shrink-0 flex-col gap-2 border-t border-border bg-raised/40 p-2",
"relative z-10 flex shrink-0 flex-col gap-2 border-t border-border bg-raised/40 p-2",
mobile && "pb-[max(0.5rem,env(safe-area-inset-bottom))]",
)}
>
@ -2947,7 +2966,7 @@ function ChatBubble({
if (turn.role === "tool") {
return (
<div className="flex min-w-0 justify-center">
<span className="flex max-w-[80%] items-baseline gap-2 truncate rounded-md border border-border bg-raised px-2 py-1 text-xs text-muted">
<span className="flex max-w-[80%] items-baseline gap-2 truncate rounded-md border border-border bg-raised/80 px-2 py-1 text-xs text-muted">
<span className="truncate">{turn.label === "Activité" ? tr("Activité", "Activity") : turn.label}</span>
<EntryTimestamp atMs={turn.receivedAtMs} />
</span>
@ -3007,7 +3026,7 @@ function ChatBubble({
<div
className={cn(
"min-w-0 max-w-[82%] overflow-hidden rounded-lg border px-3 py-2 text-sm text-content",
user ? "border-border bg-raised" : "border-primary/25 bg-primary/10",
user ? "border-border bg-raised/85" : "border-primary/25 bg-primary/10",
)}
>
<ChatMessageMeta