From 28a5d65f1a0f7895a5d13ab4634274d314da13a8 Mon Sep 17 00:00:00 2001 From: Roberto Musso Date: Fri, 27 Feb 2026 22:49:16 +0100 Subject: [PATCH] =?UTF-8?q?feat(floating-ai):=20step=203=20=E2=80=94=20cre?= =?UTF-8?q?ate=20double-click=20hook?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.6 --- docs/floating-ai-integration-guide.md | 2 +- src/renderer/components/layout/AppShell.tsx | 13 +++++- src/renderer/hooks/useDoubleClickAI.ts | 44 +++++++++++++++++++++ 3 files changed, 56 insertions(+), 3 deletions(-) create mode 100644 src/renderer/hooks/useDoubleClickAI.ts diff --git a/docs/floating-ai-integration-guide.md b/docs/floating-ai-integration-guide.md index 0a2ee73..64f3b68 100644 --- a/docs/floating-ai-integration-guide.md +++ b/docs/floating-ai-integration-guide.md @@ -355,7 +355,7 @@ return ( ## Step 3: Create Double-Click Hook -**Status**: [ ] +**Status**: [x] 2026-02-27 **Prerequisites**: Step 2 completed **Creates**: `src/renderer/hooks/useDoubleClickAI.ts` **Modifies**: `src/renderer/components/layout/AppShell.tsx` (add hook call) diff --git a/src/renderer/components/layout/AppShell.tsx b/src/renderer/components/layout/AppShell.tsx index 9822937..f8e7512 100644 --- a/src/renderer/components/layout/AppShell.tsx +++ b/src/renderer/components/layout/AppShell.tsx @@ -18,6 +18,7 @@ import { Palette } from 'lucide-react'; import { trpc } from '@/lib/trpc'; +import { useDoubleClickAI } from '@/hooks/useDoubleClickAI'; import { Sidebar, SidebarContent, @@ -84,6 +85,16 @@ function findScrollableAncestor(el: Element | null): Element | null { } export function AppShell({ children }: AppShellProps) { + return ( + + {children} + + ); +} + +function AppShellInner({ children }: AppShellProps) { + useDoubleClickAI(); + const collapsedQuery = trpc.settings.getSidebarCollapsed.useQuery(undefined, { staleTime: Infinity, }); @@ -209,7 +220,6 @@ export function AppShell({ children }: AppShellProps) { }, [openCurtain, closeCurtain]); return ( - <> - ); } diff --git a/src/renderer/hooks/useDoubleClickAI.ts b/src/renderer/hooks/useDoubleClickAI.ts new file mode 100644 index 0000000..d8593e0 --- /dev/null +++ b/src/renderer/hooks/useDoubleClickAI.ts @@ -0,0 +1,44 @@ +import { useEffect } from 'react'; +import { useFloatingChat } from '@/context/FloatingChatContext'; + +// Elements where double-click should NOT trigger the AI popup +const INTERACTIVE_TAGS = new Set(['INPUT', 'TEXTAREA', 'SELECT']); + +export function useDoubleClickAI(): void { + const { openAtSection, state } = useFloatingChat(); + + useEffect(() => { + const handler = (e: MouseEvent) => { + const target = e.target as HTMLElement; + + // Skip interactive elements (preserve text selection behavior) + if (INTERACTIVE_TAGS.has(target.tagName)) return; + + // Skip contenteditable elements UNLESS they're inside Milkdown + if (target.isContentEditable) { + const inMilkdown = + target.closest('.milkdown-container') || + target.closest('.crepe-editor'); + if (!inMilkdown) return; + // For Milkdown: only trigger if no text was selected by the double-click + const selection = window.getSelection(); + if (selection && selection.toString().trim().length > 0) return; + } + + // Walk up DOM to find nearest [data-ai-section] + const sectionEl = (target as Element).closest('[data-ai-section]'); + if (!sectionEl) return; + + const sectionId = sectionEl.getAttribute('data-ai-section'); + if (!sectionId) return; + + // If popup is already open at THIS section, do nothing + if (state.isOpen && state.activeSectionId === sectionId) return; + + openAtSection(sectionId); + }; + + document.addEventListener('dblclick', handler); + return () => document.removeEventListener('dblclick', handler); + }, [openAtSection, state.isOpen, state.activeSectionId]); +}