Skip to content

Commit 52cc796

Browse files
authored
fix(ui): clean up stale workflow state and provider policy handling (#8400)
* fix(ui): clean up stale workflow state and provider policy handling * chore(tests): address cleanup review findings
1 parent 46e9b5b commit 52cc796

13 files changed

Lines changed: 388 additions & 715 deletions

File tree

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
/** @vitest-environment jsdom */
2+
3+
import { act } from 'react'
4+
import { authClientMock } from '@sim/testing/mocks/auth-client.mock'
5+
import { providersModelsMock } from '@sim/testing/mocks/providers-models.mock'
6+
import { providersUtilsMock } from '@sim/testing/mocks/providers-utils.mock'
7+
import { createRoot, type Root } from 'react-dom/client'
8+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
9+
import { Chat } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/chat/chat'
10+
import { useChatStore } from '@/stores/chat/store'
11+
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
12+
13+
vi.mock('@/lib/auth/auth-client', () => authClientMock)
14+
vi.mock('@/providers/models', () => providersModelsMock)
15+
vi.mock('@/providers/utils', () => providersUtilsMock)
16+
vi.mock('@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-workflow-execution', () => ({
17+
useWorkflowExecution: () => ({ handleRunWorkflow: vi.fn(), handleCancelExecution: vi.fn() }),
18+
isChatWorkflowRunResult: () => false,
19+
WorkflowAttachmentUploadError: class extends Error {},
20+
}))
21+
vi.mock('@/app/workspace/[workspaceId]/w/[workflowId]/components/chat/components', () => ({
22+
ChatMessage: () => null,
23+
OutputSelect: () => null,
24+
}))
25+
vi.mock('@/app/workspace/[workspaceId]/w/[workflowId]/hooks/float', () => ({
26+
useFloatDrag: () => ({}),
27+
useFloatBoundarySync: () => {},
28+
useFloatResize: () => ({}),
29+
}))
30+
31+
let root: Root
32+
let container: HTMLDivElement
33+
34+
beforeEach(() => {
35+
vi.useFakeTimers()
36+
vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true)
37+
Object.defineProperty(HTMLElement.prototype, 'scrollTo', { configurable: true, value: vi.fn() })
38+
vi.stubGlobal(
39+
'ResizeObserver',
40+
class {
41+
observe() {}
42+
unobserve() {}
43+
disconnect() {}
44+
}
45+
)
46+
useChatStore.setState({ ...useChatStore.getInitialState(), isChatOpen: true })
47+
useWorkflowRegistry.setState({ activeWorkflowId: 'workflow-a' })
48+
useChatStore.getState().addMessage({ workflowId: 'workflow-a', type: 'user', content: 'first' })
49+
useChatStore.getState().addMessage({ workflowId: 'workflow-a', type: 'user', content: 'second' })
50+
useChatStore.getState().addMessage({
51+
workflowId: 'workflow-a',
52+
type: 'workflow',
53+
content: 'partial',
54+
isStreaming: true,
55+
})
56+
container = document.createElement('div')
57+
document.body.appendChild(container)
58+
root = createRoot(container)
59+
act(() => root.render(<Chat />))
60+
})
61+
62+
afterEach(() => {
63+
act(() => root.unmount())
64+
container.remove()
65+
useChatStore.setState(useChatStore.getInitialState())
66+
useWorkflowRegistry.setState({ activeWorkflowId: null })
67+
Reflect.deleteProperty(HTMLElement.prototype, 'scrollTo')
68+
vi.useRealTimers()
69+
})
70+
71+
function input() {
72+
const element = container.querySelector<HTMLInputElement>(
73+
'input[placeholder="Type a message..."]'
74+
)
75+
if (!element) throw new Error('Chat composer missing')
76+
return element
77+
}
78+
79+
function press(key: string) {
80+
act(() => {
81+
input().dispatchEvent(new KeyboardEvent('keydown', { key, bubbles: true }))
82+
})
83+
}
84+
85+
describe('floating chat prompt history', () => {
86+
it('keeps the history cursor while assistant output streams and finalizes', () => {
87+
press('ArrowUp')
88+
expect(input().value).toBe('second')
89+
const responseId = useChatStore.getState().messages[2].id
90+
act(() => useChatStore.getState().setMessageContent(responseId, 'next chunk'))
91+
press('ArrowUp')
92+
expect(input().value).toBe('first')
93+
act(() => useChatStore.getState().finalizeMessageStream(responseId))
94+
press('ArrowDown')
95+
expect(input().value).toBe('second')
96+
press('ArrowDown')
97+
expect(input().value).toBe('')
98+
})
99+
100+
it('resets navigation when the workflow changes even with identical prompt history', () => {
101+
act(() => {
102+
useChatStore
103+
.getState()
104+
.addMessage({ workflowId: 'workflow-b', type: 'user', content: 'first' })
105+
useChatStore
106+
.getState()
107+
.addMessage({ workflowId: 'workflow-b', type: 'user', content: 'second' })
108+
})
109+
press('ArrowUp')
110+
expect(input().value).toBe('second')
111+
act(() => useWorkflowRegistry.setState({ activeWorkflowId: 'workflow-b' }))
112+
press('ArrowUp')
113+
expect(input().value).toBe('second')
114+
act(() =>
115+
useChatStore
116+
.getState()
117+
.addMessage({ workflowId: 'workflow-b', type: 'user', content: 'newest' })
118+
)
119+
press('ArrowUp')
120+
expect(input().value).toBe('newest')
121+
act(() => useChatStore.getState().clearChat('workflow-b'))
122+
press('ArrowDown')
123+
expect(input().value).toBe('newest')
124+
})
125+
})

‎apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/chat/chat.tsx‎

Lines changed: 12 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -267,6 +267,17 @@ export function Chat() {
267267
}))
268268
)
269269

270+
const promptHistory = useChatStore(
271+
useShallow((state) =>
272+
!activeWorkflowId
273+
? []
274+
: state.messages
275+
.filter((message) => message.workflowId === activeWorkflowId && message.type === 'user')
276+
.map((message) => message.content)
277+
.filter((content): content is string => typeof content === 'string')
278+
)
279+
)
280+
270281
const hasConsoleHydrated = useTerminalConsoleStore((state) => state._hasHydrated)
271282
const entries = useWorkflowConsoleEntries(
272283
hasConsoleHydrated && typeof activeWorkflowId === 'string' ? activeWorkflowId : undefined
@@ -277,7 +288,6 @@ export function Chat() {
277288
const { addToQueue } = useOperationQueue()
278289

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

@@ -428,23 +438,9 @@ export function Chat() {
428438
}
429439
)
430440

431-
const userMessages = useMemo(() => {
432-
return workflowMessages
433-
.filter((msg) => msg.type === 'user')
434-
.map((msg) => msg.content)
435-
.filter((content): content is string => typeof content === 'string')
436-
}, [workflowMessages])
437-
438441
useEffect(() => {
439-
if (!activeWorkflowId) {
440-
setPromptHistory([])
441-
setHistoryIndex(-1)
442-
return
443-
}
444-
445-
setPromptHistory(userMessages)
446442
setHistoryIndex(-1)
447-
}, [activeWorkflowId, userMessages])
443+
}, [activeWorkflowId, promptHistory])
448444

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

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

732725
const messageContent =
@@ -759,7 +752,6 @@ export function Chat() {
759752
chatFiles,
760753
activeWorkflowId,
761754
isExecuting,
762-
promptHistory,
763755
getConversationId,
764756
addMessage,
765757
handleRunWorkflow,

0 commit comments

Comments
 (0)