From ddae50663174fbcd1ab78cabbf012002a9af3054 Mon Sep 17 00:00:00 2001 From: suryo12 Date: Sun, 24 May 2026 16:41:47 +0700 Subject: [PATCH] refactor(web): replace slideout panel window event with jotai atom (fixes #1358) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace the `SLIDEOUT_PANEL_OPENED_EVENT` window event with a `slideoutOpenedTickAtom` jotai atom. The dispatcher in `SidebarSlideOutPanel` now bumps the tick via `useSetAtom`, and the listener in `Thread` reads it via `useAtomValue` and reacts on change behind a ref guard that skips the initial render — preserving the one-shot-per-open semantics of the previous event. This removes the implicit cross-module string contract, makes the signal traceable through React DevTools / jotai inspector, and lets TypeScript catch typos that the string-based event API silently swallowed. --- .../components/assistant-ui/thread.tsx | 21 +++++++++++-------- .../ui/sidebar/SidebarSlideOutPanel.tsx | 8 ++++--- surfsense_web/lib/layout-events.ts | 10 ++++++++- 3 files changed, 26 insertions(+), 13 deletions(-) diff --git a/surfsense_web/components/assistant-ui/thread.tsx b/surfsense_web/components/assistant-ui/thread.tsx index 6876ce23e..24f070b4e 100644 --- a/surfsense_web/components/assistant-ui/thread.tsx +++ b/surfsense_web/components/assistant-ui/thread.tsx @@ -105,7 +105,7 @@ import { useMediaQuery } from "@/hooks/use-media-query"; import { useElectronAPI } from "@/hooks/use-platform"; import { captureDisplayToPngDataUrl } from "@/lib/chat/display-media-capture"; import { getMentionDocKey } from "@/lib/chat/mention-doc-key"; -import { SLIDEOUT_PANEL_OPENED_EVENT } from "@/lib/layout-events"; +import { slideoutOpenedTickAtom } from "@/lib/layout-events"; import { cn } from "@/lib/utils"; const COMPOSER_PLACEHOLDER = "Ask anything, type / for prompts, type @ to mention docs"; @@ -478,15 +478,18 @@ const Composer: FC = () => { editorRef.current?.focus(); }, [isDesktop, showDocumentPopover, showPromptPicker, threadId]); - // Close document picker when a slide-out panel (inbox, etc.) opens. + // Close document picker when a sidebar slide-out panel (inbox, etc.) opens. + const slideoutOpenedTick = useAtomValue(slideoutOpenedTickAtom); + const isFirstSlideoutTickRef = useRef(true); useEffect(() => { - const handler = () => { - setShowDocumentPopover(false); - setMentionQuery(""); - }; - window.addEventListener(SLIDEOUT_PANEL_OPENED_EVENT, handler); - return () => window.removeEventListener(SLIDEOUT_PANEL_OPENED_EVENT, handler); - }, []); + void slideoutOpenedTick; + if (isFirstSlideoutTickRef.current) { + isFirstSlideoutTickRef.current = false; + return; + } + setShowDocumentPopover(false); + setMentionQuery(""); + }, [slideoutOpenedTick]); // Sync editor text into assistant-ui's composer and mirror the chip // atom from the editor's reported ``docs``. The editor is the diff --git a/surfsense_web/components/layout/ui/sidebar/SidebarSlideOutPanel.tsx b/surfsense_web/components/layout/ui/sidebar/SidebarSlideOutPanel.tsx index 3fa4dd5d3..52b2cf998 100644 --- a/surfsense_web/components/layout/ui/sidebar/SidebarSlideOutPanel.tsx +++ b/surfsense_web/components/layout/ui/sidebar/SidebarSlideOutPanel.tsx @@ -1,9 +1,10 @@ "use client"; +import { useSetAtom } from "jotai"; import { AnimatePresence, motion } from "motion/react"; import { useCallback, useEffect } from "react"; import { useIsMobile } from "@/hooks/use-mobile"; -import { SLIDEOUT_PANEL_OPENED_EVENT } from "@/lib/layout-events"; +import { slideoutOpenedTickAtom } from "@/lib/layout-events"; interface SidebarSlideOutPanelProps { open: boolean; @@ -29,12 +30,13 @@ export function SidebarSlideOutPanel({ children, }: SidebarSlideOutPanelProps) { const isMobile = useIsMobile(); + const bumpSlideoutOpenedTick = useSetAtom(slideoutOpenedTickAtom); useEffect(() => { if (open) { - window.dispatchEvent(new Event(SLIDEOUT_PANEL_OPENED_EVENT)); + bumpSlideoutOpenedTick((tick) => tick + 1); } - }, [open]); + }, [open, bumpSlideoutOpenedTick]); const handleEscape = useCallback( (e: KeyboardEvent) => { diff --git a/surfsense_web/lib/layout-events.ts b/surfsense_web/lib/layout-events.ts index 45c52f7a4..27ea8de39 100644 --- a/surfsense_web/lib/layout-events.ts +++ b/surfsense_web/lib/layout-events.ts @@ -1 +1,9 @@ -export const SLIDEOUT_PANEL_OPENED_EVENT = "slideout-panel-opened"; +import { atom } from "jotai"; + +/** + * Tick counter that increments each time a sidebar slide-out panel opens. + * Consumers read this with `useAtomValue` and react to it changing — guard + * the initial render with a ref so the effect only fires on subsequent + * opens, matching the one-shot semantics of the previous window event. + */ +export const slideoutOpenedTickAtom = atom(0);