feat(flow chat): add cancel button (#6869)

* feat(flow chat): add cancel button

Add cancel button to flow chat interface that appears when a flow is executing.
- Replace send button with red stop button when processing
- Wire up cancel functionality to stop flow execution
- Support both polling and streaming modes
- Fixes #6868

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com>

* fix

---------

Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com>
This commit is contained in:
centdix
2025-10-21 17:41:56 +02:00
committed by GitHub
parent 3b61d890cb
commit a29d3ea507
2 changed files with 54 additions and 12 deletions
@@ -1,6 +1,6 @@
<script lang="ts">
import { Button, Alert } from '$lib/components/common'
import { MessageCircle, Loader2, ArrowUp } from 'lucide-svelte'
import { MessageCircle, Loader2, ArrowUp, Square } from 'lucide-svelte'
import { workspaceStore } from '$lib/stores'
import autosize from '$lib/autosize'
import FlowChatMessage from './FlowChatMessage.svelte'
@@ -123,16 +123,28 @@
rows={3}
></textarea>
<div class="flex-shrink-0 pr-2">
<Button
color="blue"
size="xs2"
btnClasses="!rounded-full !p-1.5"
startIcon={{ icon: ArrowUp }}
disabled={!manager.inputMessage?.trim() || manager.isLoading || deploymentInProgress}
on:click={() => manager.sendMessage()}
iconOnly
title={deploymentInProgress ? 'Deployment in progress' : 'Send message (Enter)'}
/>
{#if manager.isWaitingForResponse || manager.isLoading}
<Button
color="red"
size="xs2"
btnClasses="!rounded-full !p-1.5"
startIcon={{ icon: Square }}
on:click={() => manager.cancelCurrentJob()}
iconOnly
title="Cancel execution"
/>
{:else}
<Button
color="blue"
size="xs2"
btnClasses="!rounded-full !p-1.5"
startIcon={{ icon: ArrowUp }}
disabled={!manager.inputMessage?.trim() || manager.isLoading || deploymentInProgress}
on:click={() => manager.sendMessage()}
iconOnly
title={deploymentInProgress ? 'Deployment in progress' : 'Send message (Enter)'}
/>
{/if}
</div>
</div>
</div>
@@ -1,5 +1,5 @@
import type { FlowConversationMessage } from '$lib/gen/types.gen'
import { FlowConversationService } from '$lib/gen'
import { FlowConversationService, JobService } from '$lib/gen'
import { sendUserToast } from '$lib/toast'
import { waitJob } from '$lib/components/waitJob'
import { tick } from 'svelte'
@@ -32,6 +32,7 @@ class FlowChatManager {
loadingMoreMessages = $state(false)
currentEventSource = $state<EventSource | undefined>(undefined)
pollingInterval = $state<ReturnType<typeof setInterval> | undefined>(undefined)
currentJobId = $state<string | undefined>(undefined)
// Private state
#conversationsCache = $state<Record<string, ChatMessage[]>>({})
@@ -69,6 +70,7 @@ class FlowChatManager {
this.stopPolling()
this.isLoading = false
this.isWaitingForResponse = false
this.currentJobId = undefined
}
// Public methods for component to call
@@ -86,6 +88,28 @@ class FlowChatManager {
this.page = 1
}
async cancelCurrentJob() {
if (!this.#workspace) {
return
}
try {
if (this.currentJobId) {
await JobService.cancelQueuedJob({
workspace: this.#workspace,
id: this.currentJobId,
requestBody: {}
})
sendUserToast(`Job ${this.currentJobId} cancelled`)
}
} catch (error) {
console.error('Error cancelling job:', error)
sendUserToast('Could not cancel job', true)
} finally {
this.cleanup()
}
}
async loadConversationMessages(conversationId?: string) {
this.page = 1
await this.loadMessages(true, conversationId)
@@ -370,6 +394,9 @@ class FlowChatManager {
const data = JSON.parse(event.data)
if (data.type === 'update') {
if (data.flow_stream_job_id) {
this.currentJobId = data.flow_stream_job_id
}
// Process new stream content
if (data.new_result_stream) {
// Stop polling since we are receiving last step streaming
@@ -476,6 +503,9 @@ class FlowChatManager {
return
}
// Store the current job ID so it can be cancelled
this.currentJobId = jobId
if (isNewConversation) {
await this.#refreshConversations?.()
}