Files
Saxon FletcherandClaude Opus 5 85db94762c feat: add routed assistant chat tabs to Explorer (#49031)
<img width="1695" height="1042" alt="image"
src="https://github.com/user-attachments/assets/b02e64ff-32ac-4aee-bda5-00b56f3a29e5"
/>


## Summary

- add routed Explorer chat pages backed by explicit assistant chat IDs
- wait for persisted assistant state before creating chats so cold-load
creation is not overwritten
- register chat tabs and keep tab labels, navigation, close behavior,
and missing-chat cleanup in sync
- create and branch Explorer chats without changing the assistant
sidebar selection
- support both Next.js and TanStack Router paths

This is PR 2 of 3 and is stacked on #48973. Review and merge #48973
first. The Explorer discovery, toolbar, and cross-surface entry points
follow in #49032. This PR focuses purely on setting up chat tab types,
routes and assistant conversation.

## To Test
- Create a new assistant chat via the assistant sidebar, send a message
etc
- Copy the chat id
- Visit /explorer/chat/[id]
- Verify chat shows up, you can send more messages, chat is synced
across tab and sidebar
- Close the tab

## Test plan

- `mise exec node@22 -- pnpm --dir apps/studio exec tsc --noEmit`
- focused Vitest suite: 4 files / 30 tests covering assistant hydration,
chat creation, routed chat rendering, and tab lifecycle
- ESLint on changed TypeScript files
- Prettier check on changed source files


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Added Explorer chat pages with support for opening, selecting, and
branching chats.
* Added chat tabs, stable navigation, chat icons, and fallback behavior
when tabs are closed.
* Added chat creation that waits for assistant state to finish loading.
* **Bug Fixes**
  * Removed tabs for deleted or unavailable chats.
  * Improved editor tab navigation and history clearing behavior.
* **Tests**
* Added coverage for chat routing, tab management, chat creation, and
assistant-state loading.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-14 11:19:42 +10:00

145 lines
4.3 KiB
TypeScript

import { useRouter } from 'next/router'
import { createMarkdownCellSkeleton, createQueryCellSkeleton } from './utils'
import { useSelectedOrganizationQuery } from '@/hooks/misc/useSelectedOrganization'
import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
import { generateUuid } from '@/lib/api/snippets.browser'
import { useProfile } from '@/lib/profile'
import type { AssistantModel } from '@/state/ai-assistant-state'
import { useAiAssistantState, whenAiAssistantInitialized } from '@/state/ai-assistant-state'
import { useExplorerQueryStateSnapshot } from '@/state/explorer-query'
import { useNotebooksStateSnapshot } from '@/state/notebooks/notebooks-state'
import { type Notebook } from '@/state/notebooks/types'
import { Notebooks } from '@/types'
export const useCreateNotebook = () => {
const router = useRouter()
const { profile } = useProfile()
const { data: project } = useSelectedProjectQuery()
const notebooksSnap = useNotebooksStateSnapshot()
const createNotebook = ({
id: idOverride,
name,
cells,
}: { id?: string; name?: string; cells?: Notebooks.Content['cells'] } = {}) => {
if (!profile) return console.error('Profile is required')
if (!project) return console.error('Project is required')
const sampleMdCell1 = createMarkdownCellSkeleton({
content: `
# Title
A brief description on what this notebook is about
`.trim(),
})
const sampleMdCell2 = createMarkdownCellSkeleton({
content: `
## Section
This is a sample paragraph to demonstrate the Markdown cells
1. List item 1
2. List item 2
3. List item 3
`.trim(),
})
const sampleQueryCell = createQueryCellSkeleton({ sql: 'select * from colors;' })
// [Joshen] Just adding sample data to play around with, keep for now - clean up at the end
const DEFAULT_CELLS = [
sampleMdCell1,
sampleMdCell2,
sampleQueryCell,
] as Notebooks.Content['cells']
const id = idOverride ?? generateUuid()
const notebook: Notebook = {
id,
type: 'notebook',
name: name ?? 'New Notebook',
description: '',
visibility: 'project',
favorite: false,
content: {
schema_version: 1,
cells: cells ?? DEFAULT_CELLS,
},
owner_id: profile.id,
project_id: project.id,
}
notebooksSnap.addNotebook({ projectRef: project.ref, notebook })
notebooksSnap.addNeedsSaving(notebook.id)
router.push(`/project/${project.ref}/explorer/notebook/${notebook.id}`)
}
return { createNotebook }
}
export const useCreateChat = () => {
const router = useRouter()
const { data: project } = useSelectedProjectQuery()
const { data: organization } = useSelectedOrganizationQuery()
const aiAssistantState = useAiAssistantState()
const openChat = (id: string) => {
if (!project) {
console.error('Project is required')
return undefined
}
router.push(`/project/${project.ref}/explorer/chat/${id}`)
}
const createChat = async ({
name,
initialMessage,
model,
}: {
name?: string
initialMessage?: string
model?: AssistantModel
} = {}) => {
if (!project) {
console.error('Project is required')
return undefined
}
// Hydration replaces the chat map and the selected model wholesale, so wait it out before
// creating anything — otherwise the new chat is dropped as soon as the persisted state lands.
await whenAiAssistantInitialized(aiAssistantState)
aiAssistantState.setContext({
projectRef: project.ref,
orgSlug: organization?.slug,
connectionString: project.connectionString ?? '',
})
if (model) aiAssistantState.setModel(model)
const id = aiAssistantState.createChat({ name, initialMessage })
router.push(`/project/${project.ref}/explorer/chat/${id}`)
return id
}
return { createChat, openChat }
}
export const useCreateQuery = () => {
const router = useRouter()
const { data: project } = useSelectedProjectQuery()
const querySnap = useExplorerQueryStateSnapshot()
const createQuery = () => {
if (!project) return console.error('Project is required')
const id = generateUuid()
querySnap.createDraft({ id, projectRef: project.ref })
router.push(`/project/${project.ref}/explorer/query/${id}`)
return id
}
return { createQuery }
}