Files
Saxon FletcherandClaude Opus 5 fd8d213135 feat: add Explorer chat discovery and controls (#49032)
<img width="1693" height="1038" alt="image"
src="https://github.com/user-attachments/assets/9ef52446-c517-4ea9-ada6-ac6dc57c8af4"
/>


## Summary

- add an Explorer-specific chat toolbar with rename, chat ID,
permission, and branching controls
- list searchable non-support chats in Explorer with reactive updates
and safe rehydrated-date sorting
- add chat creation entry points to Explorer home and navigation menus
- route chat recent items correctly and show chat icons across shared
tab surfaces
- make the assistant sidebar expand action open the active chat in
Explorer

This is PR 3 of 3 and is stacked on #49031. Review #48973 first, then
#49031, then this PR. Compared with its base, this PR contains only
discovery, toolbar, and cross-surface integration work.

## To Test
- visit /explorer
- Create a new chat either via home tab chat input OR the chats sidebar
- Validate chats show up in sidebar
- Validate chat conversation works
- Close the chat tab and open a chat via sidebar
- Change permission settings via the chat tab toolbar and verify it
persists


## Test plan

- `mise exec node@22 -- pnpm --dir apps/studio exec tsc --noEmit`
- focused Vitest suite: 4 files / 5 tests covering reactive chat lists,
navigation filtering and sorting, recent-item routing, and sidebar
handoff
- 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 chat creation from the Explorer home, navigation, and new-tab
menu.
- Added an Explorer chat toolbar with editable names, ID copying,
permissions, shortcuts, and metadata warnings.
- Added searchable chat history with sorting, active-chat highlighting,
and clear empty states.
  - Chats can now open directly in Explorer from the AI Assistant panel.
  - Recent items now link correctly to chats and notebooks.

- **Bug Fixes**
- Improved chat list updates after creation, deletion, and restored
sessions.
  - Added safer handling for chats without update timestamps.

- **Tests**
- Expanded coverage for chat navigation, creation, metadata warnings,
shortcuts, and recent-item links.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

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

236 lines
7.4 KiB
TypeScript

import {
Clipboard,
Edit,
Maximize,
MessageCirclePlus,
MoreVertical,
Settings,
X,
} from 'lucide-react'
import { KeyboardEvent, useState } from 'react'
import { toast } from 'sonner'
import {
Button,
copyToClipboard,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
Input,
} from 'ui'
import { ButtonTooltip } from '../ButtonTooltip'
import { ShortcutPills, ShortcutTooltip } from '../ShortcutTooltip'
import { AIAssistantChatSelector } from './AIAssistantChatSelector'
import { AIAssistantMetadataWarning } from './AIAssistantMetadataWarning'
import { useCreateChat } from '@/components/interfaces/Explorer/hooks'
import { useAiAssistantStateSnapshot } from '@/state/ai-assistant-state'
import { SHORTCUT_DEFINITIONS, SHORTCUT_IDS } from '@/state/shortcuts/registry'
import { useShortcut } from '@/state/shortcuts/useShortcut'
interface AIAssistantHeaderProps {
isChatLoading: boolean
shortcutsEnabled?: boolean
onNewChat: () => void
onCloseAssistant: () => void
showMetadataWarning: boolean
updatedOptInSinceMCP: boolean
isHipaaProjectDisallowed: boolean
aiOptInLevel: 'disabled' | 'schema' | 'full' | string | undefined
}
export const AIAssistantHeader = ({
isChatLoading,
shortcutsEnabled = true,
onNewChat,
onCloseAssistant,
showMetadataWarning,
updatedOptInSinceMCP,
isHipaaProjectDisallowed,
aiOptInLevel,
}: AIAssistantHeaderProps) => {
const snap = useAiAssistantStateSnapshot()
const { openChat } = useCreateChat()
const [value, setValue] = useState(snap.activeChat?.name)
const [isEditingName, setIsEditingName] = useState(false)
const [isOptInModalOpen, setIsOptInModalOpen] = useState(false)
const handleOpenInExplorer = () => {
if (!snap.activeChatId) return
openChat(snap.activeChatId)
onCloseAssistant()
}
const handleCopyChatId = () => {
copyToClipboard(snap.activeChatId ?? '', () => {
toast.success(`Copied chat ID for ${snap.activeChat?.name}`)
})
}
const handleSaveName = () => {
if (snap.activeChatId && value?.trim()) {
snap.renameChat(snap.activeChatId, value.trim())
}
setIsEditingName(false)
}
const handleKeyDownInput = (e: KeyboardEvent<HTMLInputElement>) => {
if (e.key === 'Escape') {
e.preventDefault()
e.stopPropagation()
setIsEditingName(false)
setValue(snap.activeChat?.name)
} else if (e.key === 'Enter') {
e.preventDefault()
e.stopPropagation()
handleSaveName()
}
}
const handleBlurInput = () => {
if (isEditingName) handleSaveName()
}
useShortcut(SHORTCUT_IDS.AI_ASSISTANT_COPY_CHAT_ID, handleCopyChatId, {
enabled: shortcutsEnabled && !isChatLoading,
})
useShortcut(SHORTCUT_IDS.AI_ASSISTANT_OPEN_PERMISSIONS, () => setIsOptInModalOpen(true), {
enabled: shortcutsEnabled && !isChatLoading,
})
useShortcut(SHORTCUT_IDS.AI_ASSISTANT_MAXIMIZE, handleOpenInExplorer, {
enabled: shortcutsEnabled && !isChatLoading,
})
return (
<div className="z-30 sticky top-0">
<div className="border-b border-b-muted flex items-center bg-card gap-x-4 px-3 min-h-(--header-height)">
<div className="text-sm flex-1 min-w-0 flex items-center gap-x-1">
{isEditingName ? (
<Input
autoFocus
value={value}
size="tiny"
onChange={(e) => setValue(e.target.value)}
onKeyDown={handleKeyDownInput}
onBlur={handleBlurInput}
/>
) : (
<Button
variant="text"
className="group min-w-0"
iconRight={<Edit className="transition opacity-0 group-hover:opacity-100" />}
onClick={() => {
setValue(snap.activeChat?.name)
setIsEditingName(true)
}}
>
{snap.activeChat?.name}
</Button>
)}
</div>
<div className="flex items-center gap-x-4 shrink-0">
<div className="flex items-center">
<AIAssistantChatSelector shortcutsEnabled={shortcutsEnabled} />
<ShortcutTooltip
side="bottom"
label="New chat"
shortcutId={SHORTCUT_IDS.AI_ASSISTANT_NEW_CHAT}
>
<Button
variant="text"
aria-label="New chat"
size="tiny"
icon={<MessageCirclePlus />}
onClick={onNewChat}
className="h-7 w-7 p-0"
/>
</ShortcutTooltip>
<ShortcutTooltip
side="bottom"
label="Open in Explorer"
shortcutId={SHORTCUT_IDS.AI_ASSISTANT_MAXIMIZE}
>
<Button
variant="text"
aria-label="Open in Explorer"
size="tiny"
icon={<Maximize />}
onClick={handleOpenInExplorer}
className="h-7 w-7 p-0"
/>
</ShortcutTooltip>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<ButtonTooltip
variant="text"
size="tiny"
icon={<MoreVertical />}
className="h-7 w-7 p-0"
disabled={isChatLoading}
tooltip={{ content: { side: 'bottom', text: 'More options' } }}
/>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-60">
<DropdownMenuItem className="justify-between" onClick={handleCopyChatId}>
<div className="flex items-center gap-x-2">
<Clipboard size={14} />
<span>Copy chat ID</span>
</div>
<ShortcutPills
sequence={SHORTCUT_DEFINITIONS[SHORTCUT_IDS.AI_ASSISTANT_COPY_CHAT_ID].sequence}
/>
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem
className="justify-between"
onClick={() => setIsOptInModalOpen(true)}
>
<div className="flex items-center gap-x-2">
<Settings size={14} />
<span>Permission settings</span>
</div>
<ShortcutPills
sequence={
SHORTCUT_DEFINITIONS[SHORTCUT_IDS.AI_ASSISTANT_OPEN_PERMISSIONS].sequence
}
/>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
<ShortcutTooltip
side="bottom"
label="Close assistant"
shortcutId={SHORTCUT_IDS.AI_ASSISTANT_TOGGLE}
>
<Button
aria-label="Close Assistant"
variant="text"
className="w-7 h-7"
onClick={onCloseAssistant}
icon={<X strokeWidth={1.5} />}
/>
</ShortcutTooltip>
</div>
</div>
</div>
<AIAssistantMetadataWarning
visible={isOptInModalOpen}
onVisibleChange={setIsOptInModalOpen}
showMetadataWarning={showMetadataWarning}
updatedOptInSinceMCP={updatedOptInSinceMCP}
isHipaaProjectDisallowed={isHipaaProjectDisallowed}
aiOptInLevel={aiOptInLevel}
/>
</div>
)
}