Files
Joshen Lim 14bda8a5cf Joshenlim/fe 4438 explorer allow deleting notebookchat without first having to (#50701)
### Context

Adds context menu to the explorer nav to allow users to delete notebooks
or chats from there (instead of having to open the notebook / chat
first, then select delete from the header)
<img width="304" height="339" alt="image"
src="https://github.com/user-attachments/assets/2ad0f21c-0d49-407c-97a2-ef11ae59e280"
/>

### Changes involved:
- Consolidates the rendering of explorer nav items into a single
component that both `ExplorerNavNotebook`, `ExplorerNavHome` and
`ExplorerNavChats` use.
- We were previously rendering a link for notebook, and a button for
chats. But chats' button was just calling `router.push` under the hood.
- Opting to use a Context to handle the delete functionality such that
we can render the confirmation modal just once at the layout level since
there's multiple places that have this delete functionality - notebook
tab, chat tab, context menu in each of the explorer navs

### To test
- [ ] Verify that navigating around the explorer is status quo
- [ ] Verify that you can delete a notebook/chat from within the
notebook/chat tab
- [ ] Verify that you can delete a notebook/chat from the explorer nav
- [ ] Verify that after deleting a notebook/chat from either locations,
the tabs should clear

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

- **New Features**
- Unified chat and notebook navigation with direct links and active-item
highlighting.
- Added context-menu options to delete chats and notebooks with
confirmation and status notifications.
- Added double-click support for keeping recently viewed chats and
notebooks open as tabs.
  - Deleted items and their related tabs are removed automatically.

- **Tests**
- Updated Explorer navigation tests for the unified link-based item
behavior.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-09-22 16:21:55 +08:00

124 lines
4.4 KiB
TypeScript

import { Clipboard, MessageSquare, MoreVertical, Settings, Trash } from 'lucide-react'
import { useState } from 'react'
import { toast } from 'sonner'
import {
copyToClipboard,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from 'ui'
import {
ExplorerToolbar,
ExplorerToolbarAction,
ExplorerToolbarActions,
ExplorerToolbarIcon,
ExplorerToolbarTitle,
} from './ExplorerToolbar'
import { useExplorerDeleteItem } from '@/components/layouts/ExplorerLayout/ExplorerProvider'
import { AIAssistantMetadataWarning } from '@/components/ui/AIAssistantPanel/AIAssistantMetadataWarning'
import type { AssistantChatHeaderProps } from '@/components/ui/AIAssistantPanel/AssistantChat'
import { ShortcutPills } from '@/components/ui/ShortcutTooltip'
import { useAiAssistantStateSnapshot } from '@/state/ai-assistant-state'
import { SHORTCUT_DEFINITIONS, SHORTCUT_IDS } from '@/state/shortcuts/registry'
import { useShortcut } from '@/state/shortcuts/useShortcut'
interface ExplorerChatToolbarProps extends AssistantChatHeaderProps {
chatId: string
shortcutsEnabled: boolean
}
export const ExplorerChatToolbar = ({
chatId,
shortcutsEnabled,
isChatLoading,
showMetadataWarning,
updatedOptInSinceMCP,
aiOptInLevel,
}: ExplorerChatToolbarProps) => {
const snap = useAiAssistantStateSnapshot()
const chat = snap.chats[chatId]
const [isOptInModalOpen, setIsOptInModalOpen] = useState(false)
const { onSelectDelete } = useExplorerDeleteItem()
const handleCopyChatId = () => {
copyToClipboard(chatId, () => toast.success(`Copied chat ID for ${chat?.name}`))
}
const handleSaveName = (name: string) => {
if (name.trim()) snap.renameChat(chatId, name.trim())
}
useShortcut(SHORTCUT_IDS.AI_ASSISTANT_COPY_CHAT_ID, handleCopyChatId, {
enabled: shortcutsEnabled && !isChatLoading,
})
useShortcut(SHORTCUT_IDS.AI_ASSISTANT_OPEN_PERMISSIONS, () => setIsOptInModalOpen(true), {
enabled: shortcutsEnabled && !isChatLoading,
})
return (
<div className="z-30 sticky top-0">
<ExplorerToolbar aria-label="Chat toolbar">
<ExplorerToolbarIcon>
<MessageSquare size={16} strokeWidth={2} />
</ExplorerToolbarIcon>
<ExplorerToolbarTitle onSaveTitle={handleSaveName}>{chat?.name ?? ''}</ExplorerToolbarTitle>
<ExplorerToolbarActions>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<ExplorerToolbarAction
aria-label="More options"
icon={<MoreVertical size={16} strokeWidth={2} />}
disabled={isChatLoading}
/>
</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>
<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>
<DropdownMenuSeparator />
<DropdownMenuItem
className="gap-x-2"
onClick={() => onSelectDelete({ id: chatId, type: 'chat', name: chat?.name ?? '' })}
>
<Trash size={14} />
<span>Delete chat</span>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</ExplorerToolbarActions>
</ExplorerToolbar>
<AIAssistantMetadataWarning
visible={isOptInModalOpen}
onVisibleChange={setIsOptInModalOpen}
showMetadataWarning={showMetadataWarning}
updatedOptInSinceMCP={updatedOptInSinceMCP}
aiOptInLevel={aiOptInLevel}
/>
</div>
)
}