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:
@ -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
|
||||
|
||||
Reference in New Issue
Block a user