fix(ai-toolbar): gate paste capture on AI availability

This commit is contained in:
2026-04-22 17:54:49 -04:00
parent e817fb52cf
commit bfb29d3986
2 changed files with 17 additions and 4 deletions

View File

@@ -121,6 +121,7 @@ export const AIToolbar = ({
"Project sync tomorrow from 9am to 10am on Google Meet with a weekly repeat.", "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.", "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 // Ref to imperatively open the file picker from the keyboard shortcut
const imageTriggerRef = useRef<{ open: () => void }>(null); const imageTriggerRef = useRef<{ open: () => void }>(null);
@@ -153,7 +154,7 @@ export const AIToolbar = ({
// focused element or OS clipboard model (X11/Wayland). // focused element or OS clipboard model (X11/Wayland).
// This is the approach used by Excalidraw's actionPaste. // This is the approach used by Excalidraw's actionPaste.
useEffect(() => { useEffect(() => {
if (!isAuthenticated || isPending) return; if (!isAuthenticated || isPending || !canUseAi || aiLoading) return;
// ── Handler 1: paste event (works when textarea is NOT focused) ─────── // ── Handler 1: paste event (works when textarea is NOT focused) ───────
const handleDocumentPaste = (e: ClipboardEvent) => { const handleDocumentPaste = (e: ClipboardEvent) => {
@@ -247,9 +248,9 @@ export const AIToolbar = ({
document.removeEventListener("paste", handlePasteHandled, { document.removeEventListener("paste", handlePasteHandled, {
capture: true, 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) { if (isPending) {
return ( return (
@@ -261,7 +262,6 @@ export const AIToolbar = ({
} }
const hasImages = imagePreviews.length > 0; const hasImages = imagePreviews.length > 0;
const canUseAi = adminAiEnabled && aiEnabled;
const showDisabledState = isAuthenticated && !canUseAi; const showDisabledState = isAuthenticated && !canUseAi;
return ( return (

View File

@@ -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 ──────────────────────────────────── // ─── Cycle 8: Multi-image thumbnail strip ────────────────────────────────────
// //
// When multiple images are attached, they render as a horizontal scrollable // When multiple images are attached, they render as a horizontal scrollable