improvement(mothership): show continue options on abort (#3746)

* Show continue options on abort

* Fix lint

* Fix
This commit is contained in:
Siddharth Ganesan
2026-03-24 16:08:28 -07:00
committed by GitHub
parent 77eafabb63
commit b9926df8e0
3 changed files with 59 additions and 25 deletions

View File

@@ -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>
)
}

View File

@@ -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 = {
@@ -1190,8 +1192,14 @@ 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',
@@ -1199,7 +1207,7 @@ export function useChat(
body: JSON.stringify({
chatId,
streamId,
content,
content: persistedContent,
...(storedBlocks.length > 0 && { contentBlocks: storedBlocks }),
}),
})
@@ -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
@@ -1239,6 +1291,8 @@ export function useChat(
}
}
resolveInterruptedToolCalls()
if (options?.error) {
setMessageQueue([])
return
@@ -1254,7 +1308,7 @@ export function useChat(
})
}
},
[invalidateChatQueries]
[invalidateChatQueries, resolveInterruptedToolCalls]
)
finalizeRef.current = finalize
@@ -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', {
@@ -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)

View File

@@ -1,6 +1,5 @@
{
"lockfileVersion": 1,
"configVersion": 0,
"workspaces": {
"": {
"name": "simstudio",