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:
Johannes du Plessis 2026-06-10 13:27:04 -07:00 • committed by GitHub
parent e8770e1004
commit 1135e9342e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 63 additions and 3 deletions

View file

@ -6,7 +6,11 @@ import type { AgentThread, ImageChunk, Message } from "@/lib/agents/types"
import type { ModelSelection } from "@/lib/agents/useModelOptions"
import { AgentPromptBar } from "@/components/agents/AgentPromptBar"
import { MessageView } from "@/components/agents/ported"
import { agentThreadKeys, useSendAgentMessage } from "@/lib/agents/queries"
import {
agentThreadKeys,
useCancelAgentThread,
useSendAgentMessage,
} from "@/lib/agents/queries"
import {
addPendingPrompt,
dropPendingPrompts,
@ -55,6 +59,7 @@ function isPendingPromptConfirmed(
export function AgentThreadView({ thread }: AgentThreadViewProps) {
const queryClient = useQueryClient()
const sendMessage = useSendAgentMessage(thread.id)
const cancelThread = useCancelAgentThread(thread.id)
useAgentThreadStream(thread.id, thread.status === "running")
const [pendingPrompts, setPendingPrompts] = useState<Array<PendingPrompt>>(
() => getPendingPrompts(thread.id)
@ -134,6 +139,10 @@ export function AgentThreadView({ thread }: AgentThreadViewProps) {
]
)
const handleCancel = useCallback(() => {
cancelThread.mutate()
}, [cancelThread])
const displayMessages = useMemo<Array<Message>>(() => {
if (pendingPrompts.length === 0) return thread.messages
const baseTimestamp = new Date().toISOString()
@ -177,6 +186,9 @@ export function AgentThreadView({ thread }: AgentThreadViewProps) {
compact
busy={isStreaming}
disabled={sendMessage.isPending}
canCancel={hasActiveRun}
cancelling={cancelThread.isPending}
onCancel={handleCancel}
onSubmit={handleSubmit}
models={models}
selection={activeSelection}
@ -196,6 +208,9 @@ export function AgentThreadView({ thread }: AgentThreadViewProps) {
compact
busy={isStreaming}
disabled={sendMessage.isPending}
canCancel={hasActiveRun}
cancelling={cancelThread.isPending}
onCancel={handleCancel}
onSubmit={handleSubmit}
models={models}
selection={activeSelection}

View file

@ -1,4 +1,11 @@
import { ArrowUp, ChevronDown, ImagePlus, LoaderCircle, X } from "lucide-react"
import {
ArrowUp,
ChevronDown,
ImagePlus,
LoaderCircle,
Square,
X,
} from "lucide-react"
import {
memo,
useCallback,
@ -31,6 +38,10 @@ export interface CloudPromptBarProps {
compact?: boolean
disabled?: 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
models?: Array<ModelOption>
selection?: ModelSelection | null
@ -73,6 +84,9 @@ export const CloudPromptBar = memo(function CloudPromptBarComponent({
compact = false,
disabled = false,
busy = false,
canCancel = false,
cancelling = false,
onCancel,
onSubmit,
models = [],
selection = null,
@ -346,6 +360,27 @@ export const CloudPromptBar = memo(function CloudPromptBarComponent({
<ImagePlus className="size-4" />
</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
type="button"
onClick={handleSubmit}

View file

@ -167,7 +167,17 @@ export function useCancelAgentThread(threadId: string) {
return useMutation({
mutationFn: () => agentsApi.cancelThread(threadId),
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 })
},
})