Adjust AIAssistantHeader (#47912)

## Context

As per PR title, just adjusting the Assistant's header a little to
improve the UX

### Before
<img width="442" height="71" alt="image"
src="https://github.com/user-attachments/assets/c714e264-7724-451a-aeaf-7ced456d0639"
/>

### After
<img width="438" height="77" alt="image"
src="https://github.com/user-attachments/assets/11467bf2-7306-4ec9-80e0-2aadd959eff1"
/>

## Changes involved
- Shift permission settings into "More" dropdown
- Chat selection is now "history"
- Added keyboard shortcuts for history and copy chat ID  
<img width="185" height="95" alt="image"
src="https://github.com/user-attachments/assets/fc1c9bdc-9180-48ba-940f-2f39fef53a1d"
/>
<img width="287" height="159" alt="image"
src="https://github.com/user-attachments/assets/8f597306-9ea0-4282-884b-722bab16e4d0"
/>
- Chat name is now clickable to directly edit it
  - Saves on Enter or on blur
  - Resets on Esc
<img width="433" height="70" alt="image"
src="https://github.com/user-attachments/assets/f0c6fb4a-c368-4722-97a2-22ae94cc5511"
/>
<img width="439" height="64" alt="image"
src="https://github.com/user-attachments/assets/1eb21aaf-3979-433a-acc2-378b47b79e94"
/>


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

* **New Features**
* Added keyboard shortcuts to toggle the AI Assistant chat history and
copy the active chat ID.
* Added shortcut hint pills to AI Assistant tooltips and the “More
options” menu.
* Enabled inline editing of the active chat name with save/cancel and
blur support.
* **Improvements**
* Refreshed AI Assistant header actions and icons (including “New chat”
and menu controls) for clearer navigation.
  * Updated onboarding header styling with an assistant icon/animation.
  * Standardized shortcut rendering in tooltip pill formatting.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
Joshen Lim authored and GitHub committed 2026-07-15 13:35:32 +08:00
1 parent fd5ec9fade
commit c97bc6282d
5 files changed
+150 -66

No files matched your search

@@ -1,4 +1,4 @@
import { Check, ChevronDown, Edit, Plus, Trash, X } from 'lucide-react'
import { Check, Edit, History, Plus, Trash, X } from 'lucide-react'
import { useState } from 'react'
import {
Button,
@@ -17,7 +17,10 @@ import {
ScrollArea,
} from 'ui'
import { ShortcutTooltip } from '../ShortcutTooltip'
import { useAiAssistantStateSnapshot } from '@/state/ai-assistant-state'
import { SHORTCUT_IDS } from '@/state/shortcuts/registry'
import { useShortcut } from '@/state/shortcuts/useShortcut'
interface AIAssistantChatSelectorProps {
disabled?: boolean
@@ -25,7 +28,6 @@ interface AIAssistantChatSelectorProps {
export const AIAssistantChatSelector = ({ disabled = false }: AIAssistantChatSelectorProps) => {
const snap = useAiAssistantStateSnapshot()
const currentChat = snap.activeChat?.name
const [chatSelectorOpen, setChatSelectorOpen] = useState(false)
const [editingChatId, setEditingChatId] = useState<string | null>(null)
@@ -33,6 +35,8 @@ export const AIAssistantChatSelector = ({ disabled = false }: AIAssistantChatSel
const chats = Object.entries(snap.chats)
useShortcut(SHORTCUT_IDS.AI_ASSISTANT_TOGGLE_HISTORY, () => setChatSelectorOpen((prev) => !prev))
const handleSelectChat = (id: string) => {
snap.selectChat(id)
setChatSelectorOpen(false)
@@ -78,17 +82,22 @@ export const AIAssistantChatSelector = ({ disabled = false }: AIAssistantChatSel
return (
<Popover open={chatSelectorOpen} onOpenChange={setChatSelectorOpen}>
<PopoverTrigger asChild>
<Button
variant="text"
size="tiny"
iconRight={<ChevronDown size={14} />}
className="max-w-64 truncate"
>
{currentChat}
</Button>
</PopoverTrigger>
<PopoverContent className="w-[250px] p-0" align="center">
<ShortcutTooltip
side="bottom"
label="History"
shortcutId={SHORTCUT_IDS.AI_ASSISTANT_TOGGLE_HISTORY}
>
<PopoverTrigger asChild>
<Button
aria-label="History"
variant="text"
size="tiny"
className="h-7 w-7 p-0"
icon={<History />}
/>
</PopoverTrigger>
</ShortcutTooltip>
<PopoverContent className="w-[250px] p-0" align="end">
<Command>
<CommandInput className="text-xs" placeholder="Search chats..." />
<CommandList>
@@ -1,22 +1,24 @@
import { Clipboard, Ellipsis, Plus, Settings, X } from 'lucide-react'
import { useState } from 'react'
import { Clipboard, Edit, MessageCirclePlus, MoreVertical, Settings, X } from 'lucide-react'
import { KeyboardEvent, useState } from 'react'
import { toast } from 'sonner'
import {
AiIconAnimation,
Button,
copyToClipboard,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
Input,
} from 'ui'
import { Admonition } from 'ui-patterns/admonition'
import { ButtonTooltip } from '../ButtonTooltip'
import { ShortcutTooltip } from '../ShortcutTooltip'
import { ShortcutPills, ShortcutTooltip } from '../ShortcutTooltip'
import { AIAssistantChatSelector } from './AIAssistantChatSelector'
import { AIOptInModal } from './AIOptInModal'
import { useAiAssistantStateSnapshot } from '@/state/ai-assistant-state'
import { SHORTCUT_IDS } from '@/state/shortcuts/registry'
import { SHORTCUT_DEFINITIONS, SHORTCUT_IDS } from '@/state/shortcuts/registry'
import { useShortcut } from '@/state/shortcuts/useShortcut'
interface AIAssistantHeaderProps {
@@ -39,36 +41,80 @@ export const AIAssistantHeader = ({
aiOptInLevel,
}: AIAssistantHeaderProps) => {
const snap = useAiAssistantStateSnapshot()
const [value, setValue] = useState(snap.activeChat?.name)
const [isEditingName, setIsEditingName] = useState(false)
const [isOptInModalOpen, setIsOptInModalOpen] = useState(false)
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: !isChatLoading,
})
useShortcut(SHORTCUT_IDS.AI_ASSISTANT_OPEN_PERMISSIONS, () => setIsOptInModalOpen(true), {
enabled: !isChatLoading,
})
return (
<div className="z-30 sticky top-0">
<div className="border-b border-b-muted flex items-center bg-card gap-x-4 pl-4 pr-3 min-h-(--header-height)">
<div className="text-sm flex-1 flex items-center">
<AiIconAnimation size={18} allowHoverEffect={false} />
<span className="text-border-stronger dark:text-border-strong ml-2">
<svg
viewBox="0 0 24 24"
width="16"
height="16"
stroke="currentColor"
strokeWidth="1"
strokeLinecap="round"
strokeLinejoin="round"
fill="none"
shapeRendering="geometricPrecision"
<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)
}}
>
<path d="M16 3.549L7.12 20.600" />
</svg>
</span>
<AIAssistantChatSelector />
{snap.activeChat?.name}
</Button>
)}
</div>
<div className="flex items-center gap-x-4">
<div className="flex items-center gap-x-4 shrink-0">
<div className="flex items-center">
<AIAssistantChatSelector />
<ShortcutTooltip
side="bottom"
label="New chat"
@@ -78,41 +124,47 @@ export const AIAssistantHeader = ({
variant="text"
aria-label="New chat"
size="tiny"
icon={<Plus strokeWidth={1.5} />}
icon={<MessageCirclePlus />}
onClick={onNewChat}
className="h-7 w-7 p-0"
/>
</ShortcutTooltip>
<ShortcutTooltip side="bottom" shortcutId={SHORTCUT_IDS.AI_ASSISTANT_OPEN_PERMISSIONS}>
<Button
variant="text"
aria-label="Permission settings"
size="tiny"
icon={<Settings strokeWidth={1.5} />}
onClick={() => setIsOptInModalOpen(true)}
className="h-7 w-7 p-0"
disabled={isChatLoading}
/>
</ShortcutTooltip>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<ButtonTooltip
variant="text"
size="tiny"
icon={<Ellipsis strokeWidth={1.5} />}
icon={<MoreVertical />}
className="h-7 w-7 p-0"
disabled={isChatLoading}
tooltip={{ content: { side: 'bottom', text: 'More options' } }}
/>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-40">
<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="gap-x-2"
onClick={() => copyToClipboard(snap.activeChatId ?? '')}
className="justify-between"
onClick={() => setIsOptInModalOpen(true)}
>
<Clipboard size={14} strokeWidth={1.5} />
<span>Copy chat ID</span>
<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>
@@ -133,6 +185,7 @@ export const AIAssistantHeader = ({
</div>
</div>
</div>
{showMetadataWarning && (
<Admonition
type="default"
@@ -1,7 +1,7 @@
import { useParams } from 'common'
import { motion } from 'framer-motion'
import { BarChart, FileText, Shield } from 'lucide-react'
import { Button, Skeleton } from 'ui'
import { AiIconAnimation, Button, Skeleton } from 'ui'
import { codeSnippetPrompts, defaultPrompts } from './AIAssistant.prompts'
import type { SqlSnippet } from './AIAssistant.types'
@@ -48,12 +48,13 @@ export const AIOnboarding = ({
<div className="w-full flex-1 max-h-full min-h-full px-4 flex flex-col gap-0">
<div className="mt-auto w-full space-y-6 py-8 ">
<motion.h2
className="heading-section text-foreground mx-4"
className="heading-section text-foreground mx-4 flex items-center gap-x-3"
initial={{ y: 5, opacity: 0 }}
animate={{ y: 0, opacity: 1 }}
transition={{ duration: 0.3 }}
>
How can I assist you?
<AiIconAnimation size={18} allowHoverEffect={false} />
</motion.h2>
{suggestions?.prompts?.length ? (
<div>
+15 -8
View File
@@ -1,3 +1,4 @@
import { HotkeySequence } from '@tanstack/react-hotkeys'
import { TooltipContentProps } from '@ui/components/shadcn/ui/tooltip'
import { Fragment, useState, type ReactNode } from 'react'
import { KeyboardShortcut, Tooltip, TooltipContent, TooltipTrigger } from 'ui'
@@ -65,15 +66,21 @@ export const ShortcutTooltip = ({
className="flex items-center gap-2"
>
<span>{label}</span>
<span className="flex items-center gap-1">
{def.sequence.map((step, i) => (
<Fragment key={i}>
{i > 0 && <span className="text-foreground-lighter text-[11px]">then</span>}
<KeyboardShortcut keys={hotkeyToKeys(step)} />
</Fragment>
))}
</span>
<ShortcutPills sequence={def.sequence} />
</TooltipContent>
</Tooltip>
)
}
export const ShortcutPills = ({ sequence }: { sequence: HotkeySequence }) => {
return (
<span className="flex items-center gap-1">
{sequence.map((step, i) => (
<Fragment key={i}>
{i > 0 && <span className="text-foreground-lighter text-[11px]">then</span>}
<KeyboardShortcut keys={hotkeyToKeys(step)} />
</Fragment>
))}
</span>
)
}
+14
View File
@@ -71,6 +71,8 @@ export const SHORTCUT_IDS = {
COMMAND_MENU_OPEN: 'command-menu.open',
AI_ASSISTANT_TOGGLE: 'ai-assistant.toggle',
AI_ASSISTANT_NEW_CHAT: 'ai-assistant.new-chat',
AI_ASSISTANT_COPY_CHAT_ID: 'ai-assistant.copy-chat-id',
AI_ASSISTANT_TOGGLE_HISTORY: 'ai-assistant.toggle-history',
AI_ASSISTANT_OPEN_PERMISSIONS: 'ai-assistant.open-permissions',
AI_ASSISTANT_CANCEL_EDIT: 'ai-assistant.cancel-edit',
INLINE_EDITOR_TOGGLE: 'inline-editor.toggle',
@@ -261,6 +263,18 @@ export const SHORTCUT_DEFINITIONS: Record<ShortcutId, ShortcutDefinition> = {
sequence: ['A', 'N'],
showInSettings: false,
},
[SHORTCUT_IDS.AI_ASSISTANT_COPY_CHAT_ID]: {
id: SHORTCUT_IDS.AI_ASSISTANT_COPY_CHAT_ID,
label: 'Copy chat ID',
sequence: ['A', 'C'],
showInSettings: false,
},
[SHORTCUT_IDS.AI_ASSISTANT_TOGGLE_HISTORY]: {
id: SHORTCUT_IDS.AI_ASSISTANT_TOGGLE_HISTORY,
label: 'Toggle chat history',
sequence: ['A', 'Y'],
showInSettings: false,
},
[SHORTCUT_IDS.AI_ASSISTANT_OPEN_PERMISSIONS]: {
id: SHORTCUT_IDS.AI_ASSISTANT_OPEN_PERMISSIONS,
label: 'Permission settings',