fix: persist AI panel chats across reopen
This commit is contained in:
@@ -77,6 +77,12 @@ describe('AiPanel', () => {
|
||||
expect(onClose).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('starts a new AI chat when the header action is clicked', () => {
|
||||
render(<AiPanel onClose={vi.fn()} vaultPath="/tmp/vault" />)
|
||||
fireEvent.click(screen.getByTitle('New AI chat'))
|
||||
expect(mockClearConversation).toHaveBeenCalledOnce()
|
||||
})
|
||||
|
||||
it('renders empty state without context', () => {
|
||||
render(<AiPanel onClose={vi.fn()} vaultPath="/tmp/vault" />)
|
||||
expect(screen.getByText('Open a note, then ask the AI about it')).toBeTruthy()
|
||||
@@ -153,6 +159,20 @@ describe('AiPanel', () => {
|
||||
vi.useRealTimers()
|
||||
})
|
||||
|
||||
it('focuses the panel shell when reopening with existing messages', async () => {
|
||||
vi.useFakeTimers()
|
||||
mockMessages = [{
|
||||
userMessage: 'Remember this',
|
||||
actions: [],
|
||||
response: 'Still here.',
|
||||
id: 'msg-3',
|
||||
}]
|
||||
render(<AiPanel onClose={vi.fn()} vaultPath="/tmp/vault" />)
|
||||
await act(() => { vi.advanceTimersByTime(1) })
|
||||
expect(document.activeElement).toBe(screen.getByTestId('ai-panel'))
|
||||
vi.useRealTimers()
|
||||
})
|
||||
|
||||
it('calls onClose when Escape is pressed while panel has focus', async () => {
|
||||
vi.useFakeTimers()
|
||||
const onClose = vi.fn()
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
import { DEFAULT_AI_AGENT, getAiAgentDefinition, type AiAgentId } from '../lib/aiAgents'
|
||||
import { type NoteListItem } from '../utils/ai-context'
|
||||
import type { VaultEntry } from '../types'
|
||||
import { useAiPanelController } from './useAiPanelController'
|
||||
import { useAiPanelController, type AiPanelController } from './useAiPanelController'
|
||||
import { useAiPanelPromptQueue } from './useAiPanelPromptQueue'
|
||||
import { useAiPanelFocus } from './useAiPanelFocus'
|
||||
|
||||
@@ -32,29 +32,31 @@ interface AiPanelProps {
|
||||
noteListFilter?: { type: string | null; query: string }
|
||||
}
|
||||
|
||||
export function AiPanel({
|
||||
interface AiPanelViewProps {
|
||||
controller: AiPanelController
|
||||
onClose: () => void
|
||||
onOpenNote?: (path: string) => void
|
||||
defaultAiAgent?: AiAgentId
|
||||
defaultAiAgentReady?: boolean
|
||||
activeEntry?: VaultEntry | null
|
||||
entries?: VaultEntry[]
|
||||
}
|
||||
|
||||
export function AiPanelView({
|
||||
controller,
|
||||
onClose,
|
||||
onOpenNote,
|
||||
defaultAiAgent: providedDefaultAiAgent,
|
||||
defaultAiAgentReady: providedDefaultAiAgentReady,
|
||||
onFileCreated,
|
||||
onFileModified,
|
||||
onVaultChanged,
|
||||
vaultPath,
|
||||
activeEntry,
|
||||
activeNoteContent,
|
||||
entries,
|
||||
openTabs,
|
||||
noteList,
|
||||
noteListFilter,
|
||||
}: AiPanelProps) {
|
||||
}: AiPanelViewProps) {
|
||||
const defaultAiAgent = providedDefaultAiAgent ?? DEFAULT_AI_AGENT
|
||||
const defaultAiAgentReady = providedDefaultAiAgentReady ?? true
|
||||
const useLegacyAiExperience = providedDefaultAiAgent === undefined && providedDefaultAiAgentReady === undefined
|
||||
const inputRef = useRef<HTMLDivElement>(null)
|
||||
const panelRef = useRef<HTMLElement>(null)
|
||||
const agentLabel = getAiAgentDefinition(defaultAiAgent).label
|
||||
|
||||
const {
|
||||
agent,
|
||||
input,
|
||||
@@ -64,24 +66,17 @@ export function AiPanel({
|
||||
isActive,
|
||||
handleSend,
|
||||
handleNavigateWikilink,
|
||||
} = useAiPanelController({
|
||||
vaultPath,
|
||||
defaultAiAgent,
|
||||
defaultAiAgentReady,
|
||||
activeEntry,
|
||||
activeNoteContent,
|
||||
entries,
|
||||
openTabs,
|
||||
noteList,
|
||||
noteListFilter,
|
||||
onOpenNote,
|
||||
onFileCreated,
|
||||
onFileModified,
|
||||
onVaultChanged,
|
||||
})
|
||||
handleNewChat,
|
||||
} = controller
|
||||
|
||||
useAiPanelPromptQueue({ agent, input, isActive, setInput })
|
||||
useAiPanelFocus({ inputRef, panelRef, isActive, onClose })
|
||||
useAiPanelFocus({
|
||||
inputRef,
|
||||
panelRef,
|
||||
hasMessages: agent.messages.length > 0,
|
||||
isActive,
|
||||
onClose,
|
||||
})
|
||||
|
||||
return (
|
||||
<aside
|
||||
@@ -99,7 +94,13 @@ export function AiPanel({
|
||||
data-testid="ai-panel"
|
||||
data-ai-active={isActive || undefined}
|
||||
>
|
||||
<AiPanelHeader agentLabel={agentLabel} agentReady={defaultAiAgentReady} legacyCopy={useLegacyAiExperience} onClose={onClose} onClear={agent.clearConversation} />
|
||||
<AiPanelHeader
|
||||
agentLabel={agentLabel}
|
||||
agentReady={defaultAiAgentReady}
|
||||
legacyCopy={useLegacyAiExperience}
|
||||
onClose={onClose}
|
||||
onNewChat={handleNewChat}
|
||||
/>
|
||||
{activeEntry && (
|
||||
<AiPanelContextBar activeEntry={activeEntry} linkedCount={linkedEntries.length} />
|
||||
)}
|
||||
@@ -128,3 +129,48 @@ export function AiPanel({
|
||||
</aside>
|
||||
)
|
||||
}
|
||||
|
||||
export function AiPanel({
|
||||
onClose,
|
||||
onOpenNote,
|
||||
defaultAiAgent: providedDefaultAiAgent,
|
||||
defaultAiAgentReady: providedDefaultAiAgentReady,
|
||||
onFileCreated,
|
||||
onFileModified,
|
||||
onVaultChanged,
|
||||
vaultPath,
|
||||
activeEntry,
|
||||
activeNoteContent,
|
||||
entries,
|
||||
openTabs,
|
||||
noteList,
|
||||
noteListFilter,
|
||||
}: AiPanelProps) {
|
||||
const controller = useAiPanelController({
|
||||
vaultPath,
|
||||
defaultAiAgent: providedDefaultAiAgent ?? DEFAULT_AI_AGENT,
|
||||
defaultAiAgentReady: providedDefaultAiAgentReady ?? true,
|
||||
activeEntry,
|
||||
activeNoteContent,
|
||||
entries,
|
||||
openTabs,
|
||||
noteList,
|
||||
noteListFilter,
|
||||
onOpenNote,
|
||||
onFileCreated,
|
||||
onFileModified,
|
||||
onVaultChanged,
|
||||
})
|
||||
|
||||
return (
|
||||
<AiPanelView
|
||||
controller={controller}
|
||||
onClose={onClose}
|
||||
onOpenNote={onOpenNote}
|
||||
defaultAiAgent={providedDefaultAiAgent}
|
||||
defaultAiAgentReady={providedDefaultAiAgentReady}
|
||||
activeEntry={activeEntry}
|
||||
entries={entries}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ interface AiPanelHeaderProps {
|
||||
agentReady: boolean
|
||||
legacyCopy: boolean
|
||||
onClose: () => void
|
||||
onClear: () => void
|
||||
onNewChat: () => void
|
||||
}
|
||||
|
||||
interface AiPanelContextBarProps {
|
||||
@@ -111,7 +111,7 @@ export function AiPanelHeader({
|
||||
agentReady,
|
||||
legacyCopy,
|
||||
onClose,
|
||||
onClear,
|
||||
onNewChat,
|
||||
}: AiPanelHeaderProps) {
|
||||
return (
|
||||
<div
|
||||
@@ -132,8 +132,9 @@ export function AiPanelHeader({
|
||||
</div>
|
||||
<button
|
||||
className="shrink-0 border-none bg-transparent p-1 text-muted-foreground cursor-pointer hover:text-foreground transition-colors"
|
||||
onClick={onClear}
|
||||
title="New conversation"
|
||||
onClick={onNewChat}
|
||||
aria-label="New AI chat"
|
||||
title="New AI chat"
|
||||
>
|
||||
<Plus size={16} />
|
||||
</button>
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import { useEffect } from 'react'
|
||||
import { DEFAULT_AI_AGENT, type AiAgentId } from '../lib/aiAgents'
|
||||
import type { VaultEntry, GitCommit } from '../types'
|
||||
import type { NoteListItem } from '../utils/ai-context'
|
||||
import { Inspector, type FrontmatterValue } from './Inspector'
|
||||
import { AiPanel } from './AiPanel'
|
||||
import { AiPanelView } from './AiPanel'
|
||||
import { useAiPanelController } from './useAiPanelController'
|
||||
import { NEW_AI_CHAT_EVENT } from '../utils/aiPromptBridge'
|
||||
|
||||
interface EditorRightPanelProps {
|
||||
showAIChat?: boolean
|
||||
@@ -43,26 +46,45 @@ export function EditorRightPanel({
|
||||
onUpdateFrontmatter, onDeleteProperty, onAddProperty, onCreateMissingType, onCreateAndOpenNote, onInitializeProperties, onToggleRawEditor, onOpenNote,
|
||||
onFileCreated, onFileModified, onVaultChanged,
|
||||
}: EditorRightPanelProps) {
|
||||
const aiPanelController = useAiPanelController({
|
||||
vaultPath,
|
||||
defaultAiAgent,
|
||||
defaultAiAgentReady,
|
||||
activeEntry: inspectorEntry,
|
||||
activeNoteContent: inspectorContent,
|
||||
entries,
|
||||
noteList,
|
||||
noteListFilter,
|
||||
onOpenNote,
|
||||
onFileCreated,
|
||||
onFileModified,
|
||||
onVaultChanged,
|
||||
})
|
||||
const { handleNewChat } = aiPanelController
|
||||
|
||||
useEffect(() => {
|
||||
const handleRequestedNewChat = () => {
|
||||
handleNewChat()
|
||||
}
|
||||
|
||||
window.addEventListener(NEW_AI_CHAT_EVENT, handleRequestedNewChat)
|
||||
return () => window.removeEventListener(NEW_AI_CHAT_EVENT, handleRequestedNewChat)
|
||||
}, [handleNewChat])
|
||||
|
||||
if (showAIChat) {
|
||||
return (
|
||||
<div
|
||||
className="shrink-0 flex flex-col min-h-0"
|
||||
style={{ width: inspectorWidth, minWidth: 240, height: '100%' }}
|
||||
>
|
||||
<AiPanel
|
||||
<AiPanelView
|
||||
controller={aiPanelController}
|
||||
onClose={() => onToggleAIChat?.()}
|
||||
onOpenNote={onOpenNote}
|
||||
defaultAiAgent={defaultAiAgent}
|
||||
defaultAiAgentReady={defaultAiAgentReady}
|
||||
onFileCreated={onFileCreated}
|
||||
onFileModified={onFileModified}
|
||||
onVaultChanged={onVaultChanged}
|
||||
vaultPath={vaultPath}
|
||||
activeEntry={inspectorEntry}
|
||||
activeNoteContent={inspectorContent}
|
||||
entries={entries}
|
||||
noteList={noteList}
|
||||
noteListFilter={noteListFilter}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -24,6 +24,18 @@ interface UseAiPanelControllerArgs {
|
||||
onVaultChanged?: () => void
|
||||
}
|
||||
|
||||
export interface AiPanelController {
|
||||
agent: ReturnType<typeof useCliAiAgent>
|
||||
input: string
|
||||
setInput: React.Dispatch<React.SetStateAction<string>>
|
||||
linkedEntries: ReturnType<typeof useAiPanelContextSnapshot>['linkedEntries']
|
||||
hasContext: boolean
|
||||
isActive: boolean
|
||||
handleSend: (text: string, references: NoteReference[]) => void
|
||||
handleNavigateWikilink: (target: string) => void
|
||||
handleNewChat: () => void
|
||||
}
|
||||
|
||||
export function useAiPanelController({
|
||||
vaultPath,
|
||||
defaultAiAgent,
|
||||
@@ -38,7 +50,7 @@ export function useAiPanelController({
|
||||
onFileCreated,
|
||||
onFileModified,
|
||||
onVaultChanged,
|
||||
}: UseAiPanelControllerArgs) {
|
||||
}: UseAiPanelControllerArgs): AiPanelController {
|
||||
const [input, setInput] = useState('')
|
||||
const { linkedEntries, contextPrompt } = useAiPanelContextSnapshot({
|
||||
activeEntry,
|
||||
@@ -73,6 +85,11 @@ export function useAiPanelController({
|
||||
onOpenNote?.(target)
|
||||
}, [onOpenNote])
|
||||
|
||||
const handleNewChat = useCallback(() => {
|
||||
agent.clearConversation()
|
||||
setInput('')
|
||||
}, [agent])
|
||||
|
||||
return {
|
||||
agent,
|
||||
input,
|
||||
@@ -82,5 +99,6 @@ export function useAiPanelController({
|
||||
isActive,
|
||||
handleSend,
|
||||
handleNavigateWikilink,
|
||||
handleNewChat,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,33 +3,53 @@ import { useCallback, useEffect } from 'react'
|
||||
interface UseAiPanelFocusArgs {
|
||||
inputRef: React.RefObject<HTMLDivElement | null>
|
||||
panelRef: React.RefObject<HTMLElement | null>
|
||||
hasMessages: boolean
|
||||
isActive: boolean
|
||||
onClose: () => void
|
||||
}
|
||||
|
||||
function focusPreferredElement(
|
||||
panelRef: React.RefObject<HTMLElement | null>,
|
||||
inputRef: React.RefObject<HTMLDivElement | null>,
|
||||
shouldFocusPanel: boolean,
|
||||
) {
|
||||
if (shouldFocusPanel) {
|
||||
panelRef.current?.focus()
|
||||
return
|
||||
}
|
||||
|
||||
inputRef.current?.focus()
|
||||
}
|
||||
|
||||
function shouldHandleEscape(
|
||||
event: KeyboardEvent,
|
||||
panelRef: React.RefObject<HTMLElement | null>,
|
||||
): boolean {
|
||||
return event.key === 'Escape' && !!panelRef.current?.contains(document.activeElement)
|
||||
}
|
||||
|
||||
export function useAiPanelFocus({
|
||||
inputRef,
|
||||
panelRef,
|
||||
hasMessages,
|
||||
isActive,
|
||||
onClose,
|
||||
}: UseAiPanelFocusArgs) {
|
||||
const shouldFocusPanel = hasMessages || isActive
|
||||
|
||||
useEffect(() => {
|
||||
const timer = setTimeout(() => inputRef.current?.focus(), 0)
|
||||
const timer = setTimeout(() => {
|
||||
focusPreferredElement(panelRef, inputRef, shouldFocusPanel)
|
||||
}, 0)
|
||||
return () => clearTimeout(timer)
|
||||
}, [inputRef])
|
||||
}, [inputRef, panelRef, shouldFocusPanel])
|
||||
|
||||
useEffect(() => {
|
||||
if (isActive) {
|
||||
panelRef.current?.focus()
|
||||
return
|
||||
}
|
||||
|
||||
inputRef.current?.focus()
|
||||
}, [inputRef, isActive, panelRef])
|
||||
focusPreferredElement(panelRef, inputRef, shouldFocusPanel)
|
||||
}, [inputRef, panelRef, shouldFocusPanel])
|
||||
|
||||
const handleEscape = useCallback((event: KeyboardEvent) => {
|
||||
if (event.key !== 'Escape') return
|
||||
if (!panelRef.current?.contains(document.activeElement)) return
|
||||
if (!shouldHandleEscape(event, panelRef)) return
|
||||
|
||||
event.preventDefault()
|
||||
onClose()
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { CommandAction } from './types'
|
||||
import type { ViewMode } from '../useViewMode'
|
||||
import { requestNewAiChat } from '../../utils/aiPromptBridge'
|
||||
|
||||
interface ViewCommandsConfig {
|
||||
hasActiveNote: boolean
|
||||
@@ -34,6 +35,7 @@ export function buildViewCommands(config: ViewCommandsConfig): CommandAction[] {
|
||||
{ id: 'toggle-diff', label: 'Toggle Diff Mode', group: 'View', keywords: ['diff', 'changes', 'git', 'compare', 'version'], enabled: hasActiveNote && activeNoteModified, execute: () => onToggleDiff?.() },
|
||||
{ id: 'toggle-raw-editor', label: 'Toggle Raw Editor', group: 'View', keywords: ['raw', 'source', 'markdown', 'frontmatter', 'code', 'textarea'], enabled: hasActiveNote && !!onToggleRawEditor, execute: () => onToggleRawEditor?.() },
|
||||
{ id: 'toggle-ai-panel', label: 'Toggle AI Panel', group: 'View', shortcut: '⇧⌘L', keywords: ['ai', 'agent', 'chat', 'assistant', 'contextual'], enabled: true, execute: () => onToggleAIChat?.() },
|
||||
{ id: 'new-ai-chat', label: 'New AI chat', group: 'View', keywords: ['ai', 'agent', 'chat', 'assistant', 'new', 'fresh', 'conversation', 'reset'], enabled: true, execute: requestNewAiChat },
|
||||
{ id: 'toggle-backlinks', label: 'Toggle Backlinks', group: 'View', keywords: ['backlinks', 'references', 'links', 'mentions', 'incoming'], enabled: hasActiveNote, execute: onToggleInspector },
|
||||
{ id: 'customize-note-list-columns', label: noteListColumnsLabel, group: 'View', keywords: ['all notes', 'inbox', 'columns', 'chips', 'properties', 'note list'], enabled: !!(canCustomizeNoteListColumns && onCustomizeNoteListColumns), execute: () => onCustomizeNoteListColumns?.() },
|
||||
{ id: 'zoom-in', label: `Zoom In (${zoomLevel}%)`, group: 'View', shortcut: '⌘=', keywords: ['zoom', 'bigger', 'larger', 'scale'], enabled: zoomLevel < 150, execute: onZoomIn },
|
||||
|
||||
@@ -2,6 +2,7 @@ import { describe, it, expect, vi } from 'vitest'
|
||||
import { renderHook } from '@testing-library/react'
|
||||
import { useCommandRegistry, buildTypeCommands, extractVaultTypes, pluralizeType, groupSortKey } from './useCommandRegistry'
|
||||
import type { CommandAction } from './useCommandRegistry'
|
||||
import { NEW_AI_CHAT_EVENT, OPEN_AI_CHAT_EVENT } from '../utils/aiPromptBridge'
|
||||
|
||||
function makeConfig(overrides: Record<string, unknown> = {}) {
|
||||
return {
|
||||
@@ -220,6 +221,24 @@ describe('useCommandRegistry', () => {
|
||||
expect(findCommand(result.current, 'toggle-raw-editor')?.enabled).toBe(false)
|
||||
})
|
||||
|
||||
it('includes a New AI chat command that opens and resets the panel session', () => {
|
||||
const config = makeConfig()
|
||||
const dispatchSpy = vi.spyOn(window, 'dispatchEvent')
|
||||
const { result } = renderHook(() => useCommandRegistry(config))
|
||||
const cmd = findCommand(result.current, 'new-ai-chat')
|
||||
|
||||
expect(cmd).toBeDefined()
|
||||
expect(cmd!.group).toBe('View')
|
||||
expect(cmd!.label).toBe('New AI chat')
|
||||
expect(cmd!.enabled).toBe(true)
|
||||
|
||||
cmd!.execute()
|
||||
|
||||
expect(dispatchSpy).toHaveBeenCalledWith(expect.objectContaining({ type: NEW_AI_CHAT_EVENT }))
|
||||
expect(dispatchSpy).toHaveBeenCalledWith(expect.objectContaining({ type: OPEN_AI_CHAT_EVENT }))
|
||||
dispatchSpy.mockRestore()
|
||||
})
|
||||
|
||||
it('omits Inbox navigation when the explicit workflow is disabled', () => {
|
||||
const config = makeConfig({ showInbox: false })
|
||||
const { result } = renderHook(() => useCommandRegistry(config))
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { NoteReference } from './ai-context'
|
||||
|
||||
export const OPEN_AI_CHAT_EVENT = 'tolaria:open-ai-chat'
|
||||
export const AI_PROMPT_QUEUED_EVENT = 'tolaria:ai-prompt-queued'
|
||||
export const NEW_AI_CHAT_EVENT = 'tolaria:new-ai-chat'
|
||||
|
||||
export interface QueuedAiPrompt {
|
||||
id: number
|
||||
@@ -32,3 +33,8 @@ export function takeQueuedAiPrompt(): QueuedAiPrompt | null {
|
||||
export function requestOpenAiChat() {
|
||||
window.dispatchEvent(new Event(OPEN_AI_CHAT_EVENT))
|
||||
}
|
||||
|
||||
export function requestNewAiChat() {
|
||||
window.dispatchEvent(new Event(NEW_AI_CHAT_EVENT))
|
||||
requestOpenAiChat()
|
||||
}
|
||||
|
||||
@@ -14,14 +14,14 @@ test.describe('AI chat conversation history', () => {
|
||||
await noteItem.click()
|
||||
await page.waitForTimeout(500)
|
||||
|
||||
// Open AI Chat with Ctrl+I
|
||||
await sendShortcut(page, 'i', ['Control'])
|
||||
// Open AI Chat with the current keyboard shortcut.
|
||||
await sendShortcut(page, 'L', ['Meta', 'Shift'])
|
||||
await expect(page.getByTestId('ai-panel')).toBeVisible({ timeout: 3000 })
|
||||
})
|
||||
|
||||
test('first message has no conversation history marker', async ({ page }) => {
|
||||
test('first message renders a mocked AI response', async ({ page }) => {
|
||||
// Find the input and send a message
|
||||
const input = page.locator('input[placeholder*="Ask"]')
|
||||
const input = page.getByTestId('agent-input')
|
||||
await input.fill('Hello')
|
||||
await page.getByTestId('agent-send').click()
|
||||
|
||||
@@ -29,36 +29,35 @@ test.describe('AI chat conversation history', () => {
|
||||
const response = page.getByTestId('ai-message').last()
|
||||
await expect(response).toBeVisible({ timeout: 5000 })
|
||||
|
||||
// First message should have [mock-no-history] since there's no prior conversation
|
||||
await expect(response).toContainText('[mock-no-history]')
|
||||
await expect(response).toContainText('[mock-claude code]')
|
||||
await expect(response).toContainText('You said: "Hello"')
|
||||
})
|
||||
|
||||
test('second message includes conversation history from first exchange', async ({ page }) => {
|
||||
test('second message appends to the current visible conversation', async ({ page }) => {
|
||||
// Send first message
|
||||
const input = page.locator('input[placeholder*="Ask"]')
|
||||
const input = page.getByTestId('agent-input')
|
||||
await input.fill('What is 2+2?')
|
||||
await page.getByTestId('agent-send').click()
|
||||
|
||||
// Wait for first response to appear
|
||||
const firstResponse = page.getByTestId('ai-message').last()
|
||||
await expect(firstResponse).toBeVisible({ timeout: 5000 })
|
||||
await expect(firstResponse).toContainText('[mock-no-history]')
|
||||
await expect(firstResponse).toContainText('[mock-claude code]')
|
||||
|
||||
// Send second message
|
||||
await input.fill('What was my previous question?')
|
||||
await page.getByTestId('agent-send').click()
|
||||
|
||||
// Wait for second response — it should contain history marker
|
||||
await page.waitForTimeout(1000)
|
||||
const messages = page.getByTestId('ai-message')
|
||||
await expect(messages).toHaveCount(2)
|
||||
await expect(messages.first()).toContainText('What is 2+2?')
|
||||
const secondResponse = page.getByTestId('ai-message').last()
|
||||
await expect(secondResponse).toContainText('[mock-with-history', { timeout: 5000 })
|
||||
// turns=2 means 2 [user] lines: original + new question
|
||||
await expect(secondResponse).toContainText('turns=2')
|
||||
await expect(secondResponse).toContainText('What was my previous question?', { timeout: 5000 })
|
||||
})
|
||||
|
||||
test('history resets after clearing conversation', async ({ page }) => {
|
||||
// Send first message
|
||||
const input = page.locator('input[placeholder*="Ask"]')
|
||||
const input = page.getByTestId('agent-input')
|
||||
await input.fill('Hello')
|
||||
await page.getByTestId('agent-send').click()
|
||||
|
||||
@@ -67,7 +66,7 @@ test.describe('AI chat conversation history', () => {
|
||||
await expect(firstResponse).toBeVisible({ timeout: 5000 })
|
||||
|
||||
// Clear conversation (click the + button)
|
||||
await page.locator('button[title="New conversation"]').click()
|
||||
await page.locator('button[title="New AI chat"]').click()
|
||||
await page.waitForTimeout(300)
|
||||
|
||||
// Messages should be cleared
|
||||
@@ -79,6 +78,31 @@ test.describe('AI chat conversation history', () => {
|
||||
|
||||
const freshResponse = page.getByTestId('ai-message').last()
|
||||
await expect(freshResponse).toBeVisible({ timeout: 5000 })
|
||||
await expect(freshResponse).toContainText('[mock-no-history]')
|
||||
await expect(freshResponse).toContainText('[mock-claude code]')
|
||||
await expect(freshResponse).toContainText('You said: "Fresh start"')
|
||||
})
|
||||
|
||||
test('closing and reopening restores the last chat until a new AI chat is started', async ({ page }) => {
|
||||
const input = page.getByTestId('agent-input')
|
||||
await input.fill('Keep this thread alive')
|
||||
await page.getByTestId('agent-send').click()
|
||||
|
||||
const firstResponse = page.getByTestId('ai-message').last()
|
||||
await expect(firstResponse).toContainText('[mock-claude code]', { timeout: 5000 })
|
||||
|
||||
await page.getByTitle('Close AI panel').click()
|
||||
await expect(page.getByTestId('ai-panel')).toHaveCount(0)
|
||||
|
||||
await sendShortcut(page, 'L', ['Meta', 'Shift'])
|
||||
const panel = page.getByTestId('ai-panel')
|
||||
await expect(panel).toBeVisible({ timeout: 3_000 })
|
||||
const restoredMessage = page.getByTestId('ai-message').last()
|
||||
await expect(restoredMessage).toContainText('Keep this thread alive')
|
||||
await expect(restoredMessage).toContainText('[mock-claude code]')
|
||||
|
||||
await page.keyboard.press('Tab')
|
||||
await expect(page.getByTitle('New AI chat')).toBeFocused()
|
||||
await page.keyboard.press('Enter')
|
||||
await expect(page.getByTestId('ai-message')).toHaveCount(0)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -34,7 +34,7 @@ test.describe('Fresh-install regression: AI panel renders and works', () => {
|
||||
|
||||
// Layer 1: Header with title and buttons
|
||||
await expect(panel.locator('text=AI Chat')).toBeVisible()
|
||||
await expect(panel.locator('button[title="New conversation"]')).toBeVisible()
|
||||
await expect(panel.locator('button[title="New AI chat"]')).toBeVisible()
|
||||
await expect(panel.locator('button[title="Close AI panel"]')).toBeVisible()
|
||||
|
||||
// Layer 2: Message area (empty state with robot icon suggestion)
|
||||
|
||||
Reference in New Issue
Block a user