diff --git a/apps/studio/components/interfaces/AdvisorCenter/AdvisorSidebar.tsx b/apps/studio/components/interfaces/AdvisorCenter/AdvisorSidebar.tsx index 6ed44b029ee..212b1f4d992 100644 --- a/apps/studio/components/interfaces/AdvisorCenter/AdvisorSidebar.tsx +++ b/apps/studio/components/interfaces/AdvisorCenter/AdvisorSidebar.tsx @@ -94,7 +94,7 @@ export const AdvisorSidebar = () => { const advisorSnap = useAdvisorCenterStateSnapshot() const { data: project } = useSelectedProjectQuery() - const isSidebarOpen = Boolean(advisorSnap.open) + const isSidebarOpen = sidebarManagerState.isSidebarOpen(SIDEBAR_KEYS.ADVISOR_CENTER) const { data: lintData, @@ -228,7 +228,7 @@ export const AdvisorSidebar = () => { const hiddenItemsCount = itemsFilteredByTabOnly.length - filteredItems.length useEffect(() => { - if (!advisorSnap.open) return + if (!isSidebarOpen) return if (advisorSnap.selectedItemId) { const selectedExists = filteredItems.some((item) => item.id === advisorSnap.selectedItemId) @@ -238,7 +238,7 @@ export const AdvisorSidebar = () => { } else if (filteredItems.length === 0) { advisorCenterState.selectItem(undefined) } - }, [advisorSnap.open, filteredItems, advisorSnap.selectedItemId]) + }, [isSidebarOpen, filteredItems, advisorSnap.selectedItemId]) const selectedItem = filteredItems.find((item) => item.id === advisorSnap.selectedItemId) const isDetailView = !!selectedItem diff --git a/apps/studio/components/interfaces/Reports/ReportPadding.tsx b/apps/studio/components/interfaces/Reports/ReportPadding.tsx index 1f0261bd60a..681f9f872d1 100644 --- a/apps/studio/components/interfaces/Reports/ReportPadding.tsx +++ b/apps/studio/components/interfaces/Reports/ReportPadding.tsx @@ -1,19 +1,19 @@ import { PropsWithChildren } from 'react' -import { useAiAssistantStateSnapshot } from 'state/ai-assistant-state' +import { SIDEBAR_KEYS, sidebarManagerState } from 'state/sidebar-manager-state' import { cn } from 'ui' /** * Standardized padding and width layout for non-custom reports */ const ReportPadding = ({ children }: PropsWithChildren<{}>) => { - const snap = useAiAssistantStateSnapshot() + const isAiAssistantOpen = sidebarManagerState.isSidebarOpen(SIDEBAR_KEYS.AI_ASSISTANT) return (
{children} diff --git a/apps/studio/components/interfaces/Support/AIAssistantOption.tsx b/apps/studio/components/interfaces/Support/AIAssistantOption.tsx index 7d1a3372548..6e627930cc6 100644 --- a/apps/studio/components/interfaces/Support/AIAssistantOption.tsx +++ b/apps/studio/components/interfaces/Support/AIAssistantOption.tsx @@ -41,7 +41,7 @@ export const AIAssistantOption = ({ }, [projectRef, organizationSlug, sendEvent]) // If no specific project selected, use the wildcard route - const aiLink = `/project/${projectRef !== NO_PROJECT_MARKER ? projectRef : '_'}?aiAssistantPanelOpen=true&slug=${organizationSlug}` + const aiLink = `/project/${projectRef !== NO_PROJECT_MARKER ? projectRef : '_'}?sidebar=ai-assistant&slug=${organizationSlug}` if (!organizationSlug || organizationSlug === NO_ORG_MARKER) return null diff --git a/apps/studio/components/layouts/AppLayout/AdvisorButton.tsx b/apps/studio/components/layouts/AppLayout/AdvisorButton.tsx index 45beb64a637..9906e3a12e6 100644 --- a/apps/studio/components/layouts/AppLayout/AdvisorButton.tsx +++ b/apps/studio/components/layouts/AppLayout/AdvisorButton.tsx @@ -3,7 +3,6 @@ import { ShieldAlert } from 'lucide-react' import { useParams } from 'common' import { ButtonTooltip } from 'components/ui/ButtonTooltip' import { useProjectLintsQuery } from 'data/lint/lint-query' -import { useAdvisorCenterStateSnapshot } from 'state/advisor-center-state' import { SIDEBAR_KEYS, sidebarManagerState, @@ -13,22 +12,17 @@ import { cn } from 'ui' export const AdvisorButton = () => { const { ref: projectRef } = useParams() - const advisorSnap = useAdvisorCenterStateSnapshot() const sidebarSnap = useSidebarManagerSnapshot() const { data: lints } = useProjectLintsQuery({ projectRef }) const hasCriticalIssues = Array.isArray(lints) && lints.some((lint) => lint.level === 'ERROR') - const handleClick = () => { - if (advisorSnap.open) { - sidebarManagerState.closeSidebar(SIDEBAR_KEYS.ADVISOR_CENTER) - } else { - sidebarManagerState.openSidebar(SIDEBAR_KEYS.ADVISOR_CENTER) - } - } - const isOpen = sidebarSnap.panels[SIDEBAR_KEYS.ADVISOR_CENTER]?.open + const handleClick = () => { + sidebarManagerState.toggleSidebar(SIDEBAR_KEYS.ADVISOR_CENTER) + } + return (
{ +sidebarManagerState.registerSidebar(SIDEBAR_KEYS.ADVISOR_CENTER, { + onClose: () => { + advisorCenterState.selectedItemId = undefined + }, +}) + +sidebarManagerState.registerSidebar(SIDEBAR_KEYS.EDITOR_PANEL, { + onClose: () => { + editorPanelState.reset() + }, +}) + +export const LayoutSidebar = () => { + const router = useRouter() const sidebarSnap = useSidebarManagerSnapshot() - const activeSidebar = isClient ? sidebarSnap.activeSidebar : undefined + const activeSidebar = sidebarSnap.activeSidebar - if (!isClient || !activeSidebar) return null + // Handle URL params for deep linking + useEffect(() => { + if (!router.isReady) return + + const sidebarParamRaw = router.query.sidebar + const validSidebarKeys = Object.values(SIDEBAR_KEYS) as string[] + const sidebarParam = typeof sidebarParamRaw === 'string' ? sidebarParamRaw : null + + if (!sidebarParam || !validSidebarKeys.includes(sidebarParam)) return + + if (sidebarSnap.activeSidebar !== sidebarParam) { + sidebarManagerState.openSidebar(sidebarParam as SidebarKey) + } + }, [router.isReady, router.query.sidebar, sidebarSnap.activeSidebar]) + + if (!activeSidebar) return null let sidebarContent: ReactNode = null diff --git a/apps/studio/components/layouts/ProjectLayout/ProjectLayout.tsx b/apps/studio/components/layouts/ProjectLayout/ProjectLayout.tsx index c651a174e28..a7c62ba60df 100644 --- a/apps/studio/components/layouts/ProjectLayout/ProjectLayout.tsx +++ b/apps/studio/components/layouts/ProjectLayout/ProjectLayout.tsx @@ -1,7 +1,7 @@ import { AnimatePresence, motion } from 'framer-motion' import Head from 'next/head' import { useRouter } from 'next/router' -import { forwardRef, Fragment, PropsWithChildren, ReactNode, useEffect, useState } from 'react' +import { forwardRef, Fragment, PropsWithChildren, ReactNode, useEffect } from 'react' import { LOCAL_STORAGE_KEYS, useParams } from 'common' import { CreateBranchModal } from 'components/interfaces/BranchManagement/CreateBranchModal' @@ -91,7 +91,6 @@ export const ProjectLayout = forwardRef { const router = useRouter() - const [isClient, setIsClient] = useState(false) const { data: selectedOrganization } = useSelectedOrganizationQuery() const { data: selectedProject } = useSelectedProjectQuery() const { mobileMenuOpen, showSidebar, setMobileMenuOpen } = useAppStateSnapshot() @@ -136,10 +135,6 @@ export const ProjectLayout = forwardRef { - setIsClient(true) - }, []) - return ( <> @@ -238,7 +233,7 @@ export const ProjectLayout = forwardRef - + diff --git a/apps/studio/components/ui/AIAssistantPanel/AIAssistant.tsx b/apps/studio/components/ui/AIAssistantPanel/AIAssistant.tsx index dc8ff5f27ed..51f7f3a6522 100644 --- a/apps/studio/components/ui/AIAssistantPanel/AIAssistant.tsx +++ b/apps/studio/components/ui/AIAssistantPanel/AIAssistant.tsx @@ -432,12 +432,14 @@ export const AIAssistant = ({ className }: AIAssistantProps) => { } }, [snap.initialInput]) + const isSidebarOpen = sidebarManagerState.isSidebarOpen(SIDEBAR_KEYS.AI_ASSISTANT) + useEffect(() => { - if (snap.open && isInSQLEditor && !!snippetContent) { + if (isSidebarOpen && isInSQLEditor && !!snippetContent) { snap.setSqlSnippets([{ label: 'Current Query', content: snippetContent }]) } // eslint-disable-next-line react-hooks/exhaustive-deps - }, [snap.open, isInSQLEditor, snippetContent]) + }, [isSidebarOpen, isInSQLEditor, snippetContent]) return ( { - advisorCenterState.open = true - }, - onClose: () => { - advisorCenterState.open = false - advisorCenterState.selectedItemId = undefined - }, -}) - export const getAdvisorCenterStateSnapshot = () => snapshot(advisorCenterState) export const useAdvisorCenterStateSnapshot = ( diff --git a/apps/studio/state/ai-assistant-state.tsx b/apps/studio/state/ai-assistant-state.tsx index 26c9b9e9ff3..d9efc2be855 100644 --- a/apps/studio/state/ai-assistant-state.tsx +++ b/apps/studio/state/ai-assistant-state.tsx @@ -1,13 +1,12 @@ import type { UIMessage as MessageType } from '@ai-sdk/react' import { DBSchema, IDBPDatabase, openDB } from 'idb' import { debounce } from 'lodash' -import { createContext, PropsWithChildren, useContext, useEffect, useState } from 'react' +import { createContext, PropsWithChildren, useContext, useEffect, useRef, useState } from 'react' import { v4 as uuidv4 } from 'uuid' import { proxy, snapshot, subscribe, useSnapshot } from 'valtio' import { LOCAL_STORAGE_KEYS } from 'common' import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' -import { SIDEBAR_KEYS, sidebarManagerState } from './sidebar-manager-state' type SuggestionsType = { title: string @@ -29,7 +28,6 @@ type ChatSession = { } type AiAssistantData = { - open: boolean initialInput: string sqlSnippets?: SqlSnippet[] suggestions?: SuggestionsType @@ -42,14 +40,12 @@ type AiAssistantData = { // Data structure stored in IndexedDB type StoredAiAssistantState = { projectRef: string - open: boolean activeChatId?: string chats: Record model?: AssistantModel } const INITIAL_AI_ASSISTANT: AiAssistantData = { - open: false, initialInput: '', sqlSnippets: undefined, suggestions: undefined, @@ -168,7 +164,6 @@ async function tryMigrateFromLocalStorage( if (parsedFromLocalStorage && typeof parsedFromLocalStorage.chats === 'object') { migratedState = { projectRef: projectRef, - open: parsedFromLocalStorage.open ?? false, activeChatId: parsedFromLocalStorage.activeChatId, chats: parsedFromLocalStorage.chats, model: parsedFromLocalStorage.model ?? INITIAL_AI_ASSISTANT.model, @@ -200,20 +195,6 @@ async function tryMigrateFromLocalStorage( // Helper function to ensure an active chat exists or initialize a new one function ensureActiveChatOrInitialize(state: AiAssistantState) { - // Check URL param again to override loaded 'open' state if present - if (typeof window !== 'undefined') { - const urlParams = new URLSearchParams(window.location.search) - const aiAssistantPanelOpenParam = urlParams.get('aiAssistantPanelOpen') - if (aiAssistantPanelOpenParam !== null) { - const shouldOpen = aiAssistantPanelOpenParam === 'true' - if (shouldOpen) { - sidebarManagerState.openSidebar(SIDEBAR_KEYS.AI_ASSISTANT) - } else { - sidebarManagerState.closeSidebar(SIDEBAR_KEYS.AI_ASSISTANT) - } - } - } - // Ensure an active chat exists after loading/migration if (!state.activeChatId || !state.chats[state.activeChatId]) { const chatIds = Object.keys(state.chats) @@ -234,20 +215,10 @@ export const createAiAssistantState = (): AiAssistantState => { // Initialize with defaults, loading happens asynchronously in the provider const initialState = { ...INITIAL_AI_ASSISTANT } - // Check URL params for initial 'open' state, overriding any loaded state later if present - if (typeof window !== 'undefined') { - const urlParams = new URLSearchParams(window.location.search) - const aiAssistantPanelOpenParam = urlParams.get('aiAssistantPanelOpen') - if (aiAssistantPanelOpenParam !== null) { - initialState.open = aiAssistantPanelOpenParam === 'true' - } - } - const state: AiAssistantState = proxy({ ...initialState, // Spread initial values directly resetAiAssistantPanel: () => { - sidebarManagerState.closeSidebar(SIDEBAR_KEYS.AI_ASSISTANT) Object.assign(state, INITIAL_AI_ASSISTANT) }, @@ -262,14 +233,9 @@ export const createAiAssistantState = (): AiAssistantState => { newChat: ( options?: { name?: string } & Partial< - Pick + Pick > ) => { - if (options?.open === true) { - sidebarManagerState.openSidebar(SIDEBAR_KEYS.AI_ASSISTANT) - } else if (options?.open === false) { - sidebarManagerState.closeSidebar(SIDEBAR_KEYS.AI_ASSISTANT) - } const chatId = uuidv4() const newChat: ChatSession = { id: chatId, @@ -401,33 +367,11 @@ export const createAiAssistantState = (): AiAssistantState => { state.activeChatId = persistedState.activeChatId state.model = persistedState.model ?? INITIAL_AI_ASSISTANT.model - // Check URL param again to override loaded 'open' state if present - if (typeof window !== 'undefined') { - const urlParams = new URLSearchParams(window.location.search) - const aiAssistantPanelOpenParam = urlParams.get('aiAssistantPanelOpen') - if (aiAssistantPanelOpenParam !== null) { - const shouldOpen = aiAssistantPanelOpenParam === 'true' - if (shouldOpen) { - sidebarManagerState.openSidebar(SIDEBAR_KEYS.AI_ASSISTANT) - } else { - sidebarManagerState.closeSidebar(SIDEBAR_KEYS.AI_ASSISTANT) - } - } else if (persistedState.open) { - sidebarManagerState.openSidebar(SIDEBAR_KEYS.AI_ASSISTANT) - } else { - sidebarManagerState.closeSidebar(SIDEBAR_KEYS.AI_ASSISTANT) - } - } else if (persistedState.open) { - sidebarManagerState.openSidebar(SIDEBAR_KEYS.AI_ASSISTANT) - } else { - sidebarManagerState.closeSidebar(SIDEBAR_KEYS.AI_ASSISTANT) - } - // Ensure an active chat exists after loading if (!state.activeChat) { const chatIds = Object.keys(state.chats) if (chatIds.length > 0) { - // Maybe select the most recently updated? For now, first. + // Select the most recently updated chat state.activeChatId = chatIds.sort( (a, b) => (state.chats[b].updatedAt?.getTime() || 0) - @@ -454,7 +398,7 @@ export type AiAssistantState = AiAssistantData & { setModel: (model: AssistantModel) => void newChat: ( options?: { name?: string } & Partial< - Pick + Pick > ) => string selectChat: (id: string) => void @@ -477,42 +421,28 @@ export const AiAssistantStateContextProvider = ({ children }: PropsWithChildren) const { data: project } = useSelectedProjectQuery() // Initialize state. createAiAssistantState now just sets defaults. const [state] = useState(() => createAiAssistantState()) - - useEffect(() => { - const shouldRestoreOpen = state.open - - sidebarManagerState.registerSidebar(SIDEBAR_KEYS.AI_ASSISTANT, { - onOpen: () => { - state.open = true - }, - onClose: () => { - state.open = false - }, - }) - - if (shouldRestoreOpen) { - sidebarManagerState.openSidebar(SIDEBAR_KEYS.AI_ASSISTANT) - } else { - sidebarManagerState.closeSidebar(SIDEBAR_KEYS.AI_ASSISTANT) - } - - return () => { - sidebarManagerState.unregisterSidebar(SIDEBAR_KEYS.AI_ASSISTANT) - } - }, [state]) + const previousProjectRef = useRef(project?.ref) // Effect to load state from IndexedDB on mount or projectRef change useEffect(() => { let isMounted = true - async function loadAndInitializeState() { - if (!project?.ref || typeof window === 'undefined') { - if (project?.ref === undefined) { - state.resetAiAssistantPanel() - } - return // Don't load if no projectRef or not in browser - } + if (typeof window === 'undefined') { + return + } + if (!project?.ref) { + // Only reset if we previously had a project ref (e.g., navigating away) + if (previousProjectRef.current) { + state.resetAiAssistantPanel() + } + previousProjectRef.current = project?.ref + return + } + + previousProjectRef.current = project.ref + + async function loadAndInitializeState() { let loadedState: StoredAiAssistantState | null = null // 1. Try loading from IndexedDB @@ -552,7 +482,6 @@ export const AiAssistantStateContextProvider = ({ children }: PropsWithChildren) // Prepare state for IndexedDB const stateToSave: StoredAiAssistantState = { projectRef: project?.ref, - open: snap.open, activeChatId: snap.activeChatId, model: snap.model, chats: snap.chats diff --git a/apps/studio/state/editor-panel-state.ts b/apps/studio/state/editor-panel-state.ts index 010663a315f..34ffbcd80dc 100644 --- a/apps/studio/state/editor-panel-state.ts +++ b/apps/studio/state/editor-panel-state.ts @@ -79,14 +79,6 @@ export const editorPanelState = proxy({ }, }) -// Register to reset state when sidebar closes (but don't control sidebar open/close) -sidebarManagerState.registerSidebar(SIDEBAR_KEYS.EDITOR_PANEL, { - onClose: () => { - // Auto-reset state when sidebar closes - editorPanelState.reset() - }, -}) - export const getEditorPanelStateSnapshot = () => snapshot(editorPanelState) export const useEditorPanelStateSnapshot = ( diff --git a/apps/studio/state/sidebar-manager-state.ts b/apps/studio/state/sidebar-manager-state.ts index 832f6799670..5175eb559dd 100644 --- a/apps/studio/state/sidebar-manager-state.ts +++ b/apps/studio/state/sidebar-manager-state.ts @@ -56,12 +56,11 @@ const createSidebarManagerState = () => { }, openSidebar(id: SidebarKey) { - if (!state.panels[id]) { - state.registerSidebar(id) - } - const panel = state.panels[id] - if (!panel) return + if (!panel) { + console.warn(`Sidebar "${id}" is not registered. Register it before opening.`) + return + } if (panel.open) { state.activeSidebar = id