diff --git a/src/components/ai-toolbar.tsx b/src/components/ai-toolbar.tsx index ac1417b..fde82eb 100644 --- a/src/components/ai-toolbar.tsx +++ b/src/components/ai-toolbar.tsx @@ -121,6 +121,7 @@ export const AIToolbar = ({ "Project sync tomorrow from 9am to 10am on Google Meet with a weekly repeat.", "Dentist appointment on May 14 at 3pm at Smile Studio, add confirmation #A4821.", ]; + const canUseAi = adminAiEnabled && aiEnabled; // Ref to imperatively open the file picker from the keyboard shortcut const imageTriggerRef = useRef<{ open: () => void }>(null); @@ -153,7 +154,7 @@ export const AIToolbar = ({ // focused element or OS clipboard model (X11/Wayland). // This is the approach used by Excalidraw's actionPaste. useEffect(() => { - if (!isAuthenticated || isPending) return; + if (!isAuthenticated || isPending || !canUseAi || aiLoading) return; // ── Handler 1: paste event (works when textarea is NOT focused) ─────── const handleDocumentPaste = (e: ClipboardEvent) => { @@ -247,9 +248,9 @@ export const AIToolbar = ({ document.removeEventListener("paste", handlePasteHandled, { capture: true, }); - document.removeEventListener("keydown", handleDocumentKeydown); + document.removeEventListener("keydown", handleDocumentKeydown); }; - }, [isAuthenticated, isPending]); // onImagesSelect intentionally omitted — ref stays current + }, [isAuthenticated, isPending, canUseAi, aiLoading]); // onImagesSelect intentionally omitted — ref stays current if (isPending) { return ( @@ -261,7 +262,6 @@ export const AIToolbar = ({ } const hasImages = imagePreviews.length > 0; - const canUseAi = adminAiEnabled && aiEnabled; const showDisabledState = isAuthenticated && !canUseAi; return ( diff --git a/tests/ai-toolbar.test.ts b/tests/ai-toolbar.test.ts index fb1b2bc..819c3e0 100644 --- a/tests/ai-toolbar.test.ts +++ b/tests/ai-toolbar.test.ts @@ -323,6 +323,19 @@ describe("Keyboard shortcuts – toolbar integration contract", () => { }); }); +describe("Global paste capture – AI availability gate", () => { + test("document-level paste handlers only arm when AI is actually usable", () => { + const source = readToolbarSource(); + + expect(source).toContain( + "if (!isAuthenticated || isPending || !canUseAi || aiLoading) return;", + ); + expect(source).toContain( + "}, [isAuthenticated, isPending, canUseAi, aiLoading]);", + ); + }); +}); + // ─── Cycle 8: Multi-image thumbnail strip ──────────────────────────────────── // // When multiple images are attached, they render as a horizontal scrollable