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]);
+}