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