mirror of
https://github.com/Sea-Haven-Industries/open-swe.git
synced 2026-10-05 15:22:11 +00:00
feat: add cancel run button to agent UI (#1485)
* feat: add cancel run button to agent UI Wire the existing cancel-thread mutation into the agent thread view by adding a stop button to the prompt bar that appears while a run is active. Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com> * fix: preserve thread messages when cancel response omits them --------- Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
This commit is contained in:
parent
e8770e1004
commit
1135e9342e
3 changed files with 63 additions and 3 deletions
|
|
@ -6,7 +6,11 @@ import type { AgentThread, ImageChunk, Message } from "@/lib/agents/types"
|
||||||
import type { ModelSelection } from "@/lib/agents/useModelOptions"
|
import type { ModelSelection } from "@/lib/agents/useModelOptions"
|
||||||
import { AgentPromptBar } from "@/components/agents/AgentPromptBar"
|
import { AgentPromptBar } from "@/components/agents/AgentPromptBar"
|
||||||
import { MessageView } from "@/components/agents/ported"
|
import { MessageView } from "@/components/agents/ported"
|
||||||
import { agentThreadKeys, useSendAgentMessage } from "@/lib/agents/queries"
|
import {
|
||||||
|
agentThreadKeys,
|
||||||
|
useCancelAgentThread,
|
||||||
|
useSendAgentMessage,
|
||||||
|
} from "@/lib/agents/queries"
|
||||||
import {
|
import {
|
||||||
addPendingPrompt,
|
addPendingPrompt,
|
||||||
dropPendingPrompts,
|
dropPendingPrompts,
|
||||||
|
|
@ -55,6 +59,7 @@ function isPendingPromptConfirmed(
|
||||||
export function AgentThreadView({ thread }: AgentThreadViewProps) {
|
export function AgentThreadView({ thread }: AgentThreadViewProps) {
|
||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
const sendMessage = useSendAgentMessage(thread.id)
|
const sendMessage = useSendAgentMessage(thread.id)
|
||||||
|
const cancelThread = useCancelAgentThread(thread.id)
|
||||||
useAgentThreadStream(thread.id, thread.status === "running")
|
useAgentThreadStream(thread.id, thread.status === "running")
|
||||||
const [pendingPrompts, setPendingPrompts] = useState<Array<PendingPrompt>>(
|
const [pendingPrompts, setPendingPrompts] = useState<Array<PendingPrompt>>(
|
||||||
() => getPendingPrompts(thread.id)
|
() => getPendingPrompts(thread.id)
|
||||||
|
|
@ -134,6 +139,10 @@ export function AgentThreadView({ thread }: AgentThreadViewProps) {
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const handleCancel = useCallback(() => {
|
||||||
|
cancelThread.mutate()
|
||||||
|
}, [cancelThread])
|
||||||
|
|
||||||
const displayMessages = useMemo<Array<Message>>(() => {
|
const displayMessages = useMemo<Array<Message>>(() => {
|
||||||
if (pendingPrompts.length === 0) return thread.messages
|
if (pendingPrompts.length === 0) return thread.messages
|
||||||
const baseTimestamp = new Date().toISOString()
|
const baseTimestamp = new Date().toISOString()
|
||||||
|
|
@ -177,6 +186,9 @@ export function AgentThreadView({ thread }: AgentThreadViewProps) {
|
||||||
compact
|
compact
|
||||||
busy={isStreaming}
|
busy={isStreaming}
|
||||||
disabled={sendMessage.isPending}
|
disabled={sendMessage.isPending}
|
||||||
|
canCancel={hasActiveRun}
|
||||||
|
cancelling={cancelThread.isPending}
|
||||||
|
onCancel={handleCancel}
|
||||||
onSubmit={handleSubmit}
|
onSubmit={handleSubmit}
|
||||||
models={models}
|
models={models}
|
||||||
selection={activeSelection}
|
selection={activeSelection}
|
||||||
|
|
@ -196,6 +208,9 @@ export function AgentThreadView({ thread }: AgentThreadViewProps) {
|
||||||
compact
|
compact
|
||||||
busy={isStreaming}
|
busy={isStreaming}
|
||||||
disabled={sendMessage.isPending}
|
disabled={sendMessage.isPending}
|
||||||
|
canCancel={hasActiveRun}
|
||||||
|
cancelling={cancelThread.isPending}
|
||||||
|
onCancel={handleCancel}
|
||||||
onSubmit={handleSubmit}
|
onSubmit={handleSubmit}
|
||||||
models={models}
|
models={models}
|
||||||
selection={activeSelection}
|
selection={activeSelection}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,11 @@
|
||||||
import { ArrowUp, ChevronDown, ImagePlus, LoaderCircle, X } from "lucide-react"
|
import {
|
||||||
|
ArrowUp,
|
||||||
|
ChevronDown,
|
||||||
|
ImagePlus,
|
||||||
|
LoaderCircle,
|
||||||
|
Square,
|
||||||
|
X,
|
||||||
|
} from "lucide-react"
|
||||||
import {
|
import {
|
||||||
memo,
|
memo,
|
||||||
useCallback,
|
useCallback,
|
||||||
|
|
@ -31,6 +38,10 @@ export interface CloudPromptBarProps {
|
||||||
compact?: boolean
|
compact?: boolean
|
||||||
disabled?: boolean
|
disabled?: boolean
|
||||||
busy?: boolean
|
busy?: boolean
|
||||||
|
/** When true, a stop button is shown next to send to cancel the active run. */
|
||||||
|
canCancel?: boolean
|
||||||
|
cancelling?: boolean
|
||||||
|
onCancel?: () => void
|
||||||
onSubmit?: (value: string, images: Array<ImageChunk>) => void
|
onSubmit?: (value: string, images: Array<ImageChunk>) => void
|
||||||
models?: Array<ModelOption>
|
models?: Array<ModelOption>
|
||||||
selection?: ModelSelection | null
|
selection?: ModelSelection | null
|
||||||
|
|
@ -73,6 +84,9 @@ export const CloudPromptBar = memo(function CloudPromptBarComponent({
|
||||||
compact = false,
|
compact = false,
|
||||||
disabled = false,
|
disabled = false,
|
||||||
busy = false,
|
busy = false,
|
||||||
|
canCancel = false,
|
||||||
|
cancelling = false,
|
||||||
|
onCancel,
|
||||||
onSubmit,
|
onSubmit,
|
||||||
models = [],
|
models = [],
|
||||||
selection = null,
|
selection = null,
|
||||||
|
|
@ -346,6 +360,27 @@ export const CloudPromptBar = memo(function CloudPromptBarComponent({
|
||||||
<ImagePlus className="size-4" />
|
<ImagePlus className="size-4" />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
{canCancel && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={onCancel}
|
||||||
|
disabled={cancelling}
|
||||||
|
aria-label="Cancel run"
|
||||||
|
title="Cancel run"
|
||||||
|
className="flex size-7 shrink-0 items-center justify-center rounded-full border border-[var(--ui-border)] bg-[var(--ui-panel-2)] text-[color:var(--ui-text-muted)] transition-colors hover:text-[color:var(--ui-text)] disabled:cursor-default disabled:opacity-60"
|
||||||
|
>
|
||||||
|
{cancelling ? (
|
||||||
|
<LoaderCircle className="size-3.5 animate-spin" />
|
||||||
|
) : (
|
||||||
|
<Square
|
||||||
|
className="size-3"
|
||||||
|
fill="currentColor"
|
||||||
|
strokeWidth={0}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={handleSubmit}
|
onClick={handleSubmit}
|
||||||
|
|
|
||||||
|
|
@ -167,7 +167,17 @@ export function useCancelAgentThread(threadId: string) {
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: () => agentsApi.cancelThread(threadId),
|
mutationFn: () => agentsApi.cancelThread(threadId),
|
||||||
onSuccess: (thread) => {
|
onSuccess: (thread) => {
|
||||||
queryClient.setQueryData(agentThreadKeys.detail(threadId), thread)
|
queryClient.setQueryData(
|
||||||
|
agentThreadKeys.detail(threadId),
|
||||||
|
(prev: typeof thread | undefined) => {
|
||||||
|
if (!prev) return thread
|
||||||
|
return {
|
||||||
|
...thread,
|
||||||
|
messages:
|
||||||
|
thread.messages.length > 0 ? thread.messages : prev.messages,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
queryClient.invalidateQueries({ queryKey: agentThreadKeys.all })
|
queryClient.invalidateQueries({ queryKey: agentThreadKeys.all })
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue