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
Expand Up @@ -413,9 +413,7 @@ export function MessageContent({
return (
<div key={`stopped-${i}`} className='flex items-center gap-[8px]'>
<CircleStop className='h-[16px] w-[16px] flex-shrink-0 text-[var(--text-icon)]' />
<span className='font-base text-[14px] text-[var(--text-body)]'>
Stopped by user
</span>
<span className='font-base text-[14px] text-[var(--text-body)]'>Stopped</span>
</div>
)
}
Expand Down
79 changes: 58 additions & 21 deletions apps/sim/app/workspace/[workspaceId]/home/hooks/use-chat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,8 @@ const STATE_TO_STATUS: Record<string, ToolCallStatus> = {
const DEPLOY_TOOL_NAMES = new Set(['deploy_api', 'deploy_chat', 'deploy_mcp', 'redeploy'])
const RECONNECT_TAIL_ERROR =
'Live reconnect failed before the stream finished. The latest response may be incomplete.'
const CONTINUE_OPTIONS_CONTENT =
'<options>{"continue":{"title":"Continue","description":"Pick up where we left off"}}</options>'

function mapStoredBlock(block: TaskStoredContentBlock): ContentBlock {
const mapped: ContentBlock = {
Expand Down Expand Up @@ -1190,16 +1192,22 @@ export function useChat(

if (storedBlocks.length > 0) {
storedBlocks.push({ type: 'stopped' })
storedBlocks.push({ type: 'text', content: CONTINUE_OPTIONS_CONTENT })
}

const persistedContent =
content && !content.includes('<options>')
? `${content}\n\n${CONTINUE_OPTIONS_CONTENT}`
: content

try {
const res = await fetch(stopPathRef.current, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
chatId,
streamId,
content,
content: persistedContent,
...(storedBlocks.length > 0 && { contentBlocks: storedBlocks }),
}),
})
Expand All @@ -1225,6 +1233,50 @@ export function useChat(
const messagesRef = useRef(messages)
messagesRef.current = messages

const resolveInterruptedToolCalls = useCallback(() => {
setMessages((prev) => {
const hasAnyExecuting = prev.some((m) =>
m.contentBlocks?.some((b) => b.toolCall?.status === 'executing')
)
if (!hasAnyExecuting) return prev

let lastAssistantIdx = -1
for (let i = prev.length - 1; i >= 0; i--) {
if (prev[i].role === 'assistant') {
lastAssistantIdx = i
break
}
}
return prev.map((msg, idx) => {
const hasExecuting = msg.contentBlocks?.some((b) => b.toolCall?.status === 'executing')
const isLastAssistant = idx === lastAssistantIdx
if (!hasExecuting && !isLastAssistant) return msg

const blocks: ContentBlock[] = (msg.contentBlocks ?? []).map((block) => {
if (block.toolCall?.status !== 'executing') return block
return {
...block,
toolCall: {
...block.toolCall,
status: 'cancelled' as const,
displayTitle: 'Stopped',
},
}
})
if (isLastAssistant && !blocks.some((b) => b.type === 'stopped')) {
blocks.push({ type: 'stopped' as const })
}
if (
isLastAssistant &&
!blocks.some((b) => b.type === 'text' && b.content?.includes('<options>'))
) {
blocks.push({ type: 'text', content: CONTINUE_OPTIONS_CONTENT })
}
return { ...msg, contentBlocks: blocks.length > 0 ? blocks : msg.contentBlocks }
})
})
}, [])

const finalize = useCallback(
(options?: { error?: boolean }) => {
sendingRef.current = false
Expand All @@ -1239,6 +1291,8 @@ export function useChat(
}
}

resolveInterruptedToolCalls()

if (options?.error) {
setMessageQueue([])
return
Expand All @@ -1254,7 +1308,7 @@ export function useChat(
})
}
},
[invalidateChatQueries]
[invalidateChatQueries, resolveInterruptedToolCalls]
)
finalizeRef.current = finalize

Expand Down Expand Up @@ -1412,24 +1466,7 @@ export function useChat(
sendingRef.current = false
setIsSending(false)

setMessages((prev) =>
prev.map((msg) => {
if (!msg.contentBlocks?.some((b) => b.toolCall?.status === 'executing')) return msg
const updated = msg.contentBlocks!.map((block) => {
if (block.toolCall?.status !== 'executing') return block
return {
...block,
toolCall: {
...block.toolCall,
status: 'cancelled' as const,
displayTitle: 'Stopped by user',
},
}
})
updated.push({ type: 'stopped' as const })
return { ...msg, contentBlocks: updated }
})
)
resolveInterruptedToolCalls()

if (sid) {
fetch('/api/copilot/chat/abort', {
Expand Down Expand Up @@ -1495,7 +1532,7 @@ export function useChat(

reportManualRunToolStop(workflowId, toolCallId).catch(() => {})
}
}, [invalidateChatQueries, persistPartialResponse, executionStream])
}, [invalidateChatQueries, persistPartialResponse, executionStream, resolveInterruptedToolCalls])

const removeFromQueue = useCallback((id: string) => {
messageQueueRef.current = messageQueueRef.current.filter((m) => m.id !== id)
Expand Down
1 change: 0 additions & 1 deletion bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading