Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
/** @vitest-environment jsdom */

import { act } from 'react'
import { authClientMock } from '@sim/testing/mocks/auth-client.mock'
import { providersModelsMock } from '@sim/testing/mocks/providers-models.mock'
import { providersUtilsMock } from '@sim/testing/mocks/providers-utils.mock'
import { createRoot, type Root } from 'react-dom/client'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { Chat } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/chat/chat'
import { useChatStore } from '@/stores/chat/store'
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'

vi.mock('@/lib/auth/auth-client', () => authClientMock)
vi.mock('@/providers/models', () => providersModelsMock)
vi.mock('@/providers/utils', () => providersUtilsMock)
vi.mock('@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-workflow-execution', () => ({
useWorkflowExecution: () => ({ handleRunWorkflow: vi.fn(), handleCancelExecution: vi.fn() }),
isChatWorkflowRunResult: () => false,
WorkflowAttachmentUploadError: class extends Error {},
}))
vi.mock('@/app/workspace/[workspaceId]/w/[workflowId]/components/chat/components', () => ({
ChatMessage: () => null,
OutputSelect: () => null,
}))
vi.mock('@/app/workspace/[workspaceId]/w/[workflowId]/hooks/float', () => ({
useFloatDrag: () => ({}),
useFloatBoundarySync: () => {},
useFloatResize: () => ({}),
}))

let root: Root
let container: HTMLDivElement

beforeEach(() => {
vi.useFakeTimers()
vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true)
Object.defineProperty(HTMLElement.prototype, 'scrollTo', { configurable: true, value: vi.fn() })
vi.stubGlobal(
'ResizeObserver',
class {
observe() {}
unobserve() {}
disconnect() {}
}
)
useChatStore.setState({ ...useChatStore.getInitialState(), isChatOpen: true })
useWorkflowRegistry.setState({ activeWorkflowId: 'workflow-a' })
useChatStore.getState().addMessage({ workflowId: 'workflow-a', type: 'user', content: 'first' })
useChatStore.getState().addMessage({ workflowId: 'workflow-a', type: 'user', content: 'second' })
useChatStore.getState().addMessage({
workflowId: 'workflow-a',
type: 'workflow',
content: 'partial',
isStreaming: true,
})
container = document.createElement('div')
document.body.appendChild(container)
root = createRoot(container)
act(() => root.render(<Chat />))
})

afterEach(() => {
act(() => root.unmount())
container.remove()
useChatStore.setState(useChatStore.getInitialState())
useWorkflowRegistry.setState({ activeWorkflowId: null })
Reflect.deleteProperty(HTMLElement.prototype, 'scrollTo')
vi.useRealTimers()
})

function input() {
const element = container.querySelector<HTMLInputElement>(
'input[placeholder="Type a message..."]'
)
if (!element) throw new Error('Chat composer missing')
return element
}

function press(key: string) {
act(() => {
input().dispatchEvent(new KeyboardEvent('keydown', { key, bubbles: true }))
})
}

describe('floating chat prompt history', () => {
it('keeps the history cursor while assistant output streams and finalizes', () => {
press('ArrowUp')
expect(input().value).toBe('second')
const responseId = useChatStore.getState().messages[2].id
act(() => useChatStore.getState().setMessageContent(responseId, 'next chunk'))
press('ArrowUp')
expect(input().value).toBe('first')
act(() => useChatStore.getState().finalizeMessageStream(responseId))
press('ArrowDown')
expect(input().value).toBe('second')
press('ArrowDown')
expect(input().value).toBe('')
})

it('resets navigation when the workflow changes even with identical prompt history', () => {
act(() => {
useChatStore
.getState()
.addMessage({ workflowId: 'workflow-b', type: 'user', content: 'first' })
useChatStore
.getState()
.addMessage({ workflowId: 'workflow-b', type: 'user', content: 'second' })
})
press('ArrowUp')
expect(input().value).toBe('second')
act(() => useWorkflowRegistry.setState({ activeWorkflowId: 'workflow-b' }))
press('ArrowUp')
expect(input().value).toBe('second')
act(() =>
useChatStore
.getState()
.addMessage({ workflowId: 'workflow-b', type: 'user', content: 'newest' })
)
press('ArrowUp')
expect(input().value).toBe('newest')
act(() => useChatStore.getState().clearChat('workflow-b'))
press('ArrowDown')
expect(input().value).toBe('newest')
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,17 @@ export function Chat() {
}))
)

const promptHistory = useChatStore(
useShallow((state) =>
!activeWorkflowId
? []
: state.messages
.filter((message) => message.workflowId === activeWorkflowId && message.type === 'user')
.map((message) => message.content)
.filter((content): content is string => typeof content === 'string')
)
)

const hasConsoleHydrated = useTerminalConsoleStore((state) => state._hasHydrated)
const entries = useWorkflowConsoleEntries(
hasConsoleHydrated && typeof activeWorkflowId === 'string' ? activeWorkflowId : undefined
Expand All @@ -277,7 +288,6 @@ export function Chat() {
const { addToQueue } = useOperationQueue()

const [chatMessage, setChatMessage] = useState('')
const [promptHistory, setPromptHistory] = useState<string[]>([])
const [historyIndex, setHistoryIndex] = useState(-1)
const [moreMenuOpen, setMoreMenuOpen] = useState(false)

Expand Down Expand Up @@ -428,23 +438,9 @@ export function Chat() {
}
)

const userMessages = useMemo(() => {
return workflowMessages
.filter((msg) => msg.type === 'user')
.map((msg) => msg.content)
.filter((content): content is string => typeof content === 'string')
}, [workflowMessages])

useEffect(() => {
if (!activeWorkflowId) {
setPromptHistory([])
setHistoryIndex(-1)
return
}

setPromptHistory(userMessages)
setHistoryIndex(-1)
}, [activeWorkflowId, userMessages])
}, [activeWorkflowId, promptHistory])

/**
* Auto-scroll to bottom when messages load and chat is open
Expand Down Expand Up @@ -724,9 +720,6 @@ export function Chat() {
}
const messageAttachments = toChatMessageAttachments(result.uploadedAttachments)

if (sentMessage && promptHistory[promptHistory.length - 1] !== sentMessage) {
setPromptHistory((prev) => [...prev, sentMessage])
}
setHistoryIndex(-1)

const messageContent =
Expand Down Expand Up @@ -759,7 +752,6 @@ export function Chat() {
chatFiles,
activeWorkflowId,
isExecuting,
promptHistory,
getConversationId,
addMessage,
handleRunWorkflow,
Expand Down
Loading
Loading