mirror of
https://github.com/Sea-Haven-Industries/open-swe.git
synced 2026-09-30 09:13:14 +00:00
feat: add stop button to cancel running agent from web UI (#1433)
* feat: add stop button to cancel running agent from web UI * fix: handle stopped agent runs Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com> --------- Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
This commit is contained in:
parent
10dfc6d2b0
commit
1f619eae0f
4 changed files with 57 additions and 20 deletions
|
|
@ -138,7 +138,9 @@ def _metadata_repo(metadata: dict[str, Any]) -> tuple[str, str, str]:
|
|||
def _run_status_to_agent_status(thread_status: str | None, run_status: str | None) -> str:
|
||||
if thread_status == "busy" or run_status in {"pending", "running"}:
|
||||
return "running"
|
||||
if run_status in {"error", "failed", "timeout", "interrupted"}:
|
||||
if run_status in {"interrupted", "cancelled"}:
|
||||
return "interrupted"
|
||||
if run_status in {"error", "failed", "timeout"}:
|
||||
return "error"
|
||||
if run_status == "success":
|
||||
return "finished"
|
||||
|
|
@ -444,7 +446,7 @@ async def cancel_dashboard_thread(
|
|||
run_id = metadata.get("latest_run_id")
|
||||
if isinstance(run_id, str) and run_id:
|
||||
try:
|
||||
await client.runs.cancel(thread_id, run_id, wait=False)
|
||||
await client.runs.cancel(thread_id, run_id, wait=False, action="interrupt")
|
||||
except Exception:
|
||||
logger.debug("Could not cancel run %s for thread %s", run_id, thread_id, exc_info=True)
|
||||
|
||||
|
|
|
|||
|
|
@ -42,6 +42,13 @@ def test_thread_summary_keeps_repo_when_present() -> None:
|
|||
assert summary["repoFullName"] == "octo/repo"
|
||||
|
||||
|
||||
def test_thread_summary_reports_interrupted_run_status() -> None:
|
||||
summary = thread_api._thread_summary(
|
||||
{"thread_id": "t3", "metadata": {"latest_run_status": "interrupted"}}
|
||||
)
|
||||
assert summary["status"] == "interrupted"
|
||||
|
||||
|
||||
class _FakeThreadsClient:
|
||||
async def create(
|
||||
self, *, thread_id: str, metadata: dict[str, Any], if_exists: str
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import type { ModelSelection } from "@/lib/agents/useModelOptions";
|
|||
import { AgentPromptBar } from "@/components/agents/AgentPromptBar";
|
||||
import { AgentsShell } from "@/components/agents/AgentsSidebar";
|
||||
import { MessageView } from "@/components/agents/ported";
|
||||
import { useSendAgentMessage } from "@/lib/agents/queries";
|
||||
import { useCancelAgentThread, useSendAgentMessage } from "@/lib/agents/queries";
|
||||
import { dropPendingPrompts, getPendingPrompts } from "@/lib/agents/pendingPrompts";
|
||||
import { useAgentThreadStream } from "@/lib/agents/useThreadStream";
|
||||
import { useModelOptions } from "@/lib/agents/useModelOptions";
|
||||
|
|
@ -19,6 +19,7 @@ interface AgentThreadViewProps {
|
|||
|
||||
export function AgentThreadView({ user, thread }: AgentThreadViewProps) {
|
||||
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),
|
||||
|
|
@ -77,7 +78,8 @@ export function AgentThreadView({ user, thread }: AgentThreadViewProps) {
|
|||
}, [thread.messages, pendingPrompts]);
|
||||
|
||||
const hasMessages = displayMessages.length > 0;
|
||||
const isStreaming = thread.status === "running" || pendingPrompts.length > 0;
|
||||
const hasActiveRun = thread.status === "running";
|
||||
const isStreaming = hasActiveRun || pendingPrompts.length > 0;
|
||||
|
||||
return (
|
||||
<AgentsShell user={user} activeThreadId={thread.id}>
|
||||
|
|
@ -95,7 +97,7 @@ export function AgentThreadView({ user, thread }: AgentThreadViewProps) {
|
|||
<AgentPromptBar
|
||||
placeholder="Add a follow up"
|
||||
compact
|
||||
busy={isStreaming}
|
||||
busy={hasActiveRun}
|
||||
disabled={sendMessage.isPending}
|
||||
onSubmit={(content) =>
|
||||
sendMessage.mutate({
|
||||
|
|
@ -104,6 +106,8 @@ export function AgentThreadView({ user, thread }: AgentThreadViewProps) {
|
|||
effort: activeSelection?.effort ?? null,
|
||||
})
|
||||
}
|
||||
onStop={() => cancelThread.mutate()}
|
||||
stopping={cancelThread.isPending}
|
||||
models={models}
|
||||
selection={activeSelection}
|
||||
onSelectionChange={setSelection}
|
||||
|
|
@ -118,7 +122,7 @@ export function AgentThreadView({ user, thread }: AgentThreadViewProps) {
|
|||
<AgentPromptBar
|
||||
placeholder="Send the first message"
|
||||
compact
|
||||
busy={isStreaming}
|
||||
busy={hasActiveRun}
|
||||
disabled={sendMessage.isPending}
|
||||
onSubmit={(content) =>
|
||||
sendMessage.mutate({
|
||||
|
|
@ -127,6 +131,8 @@ export function AgentThreadView({ user, thread }: AgentThreadViewProps) {
|
|||
effort: activeSelection?.effort ?? null,
|
||||
})
|
||||
}
|
||||
onStop={() => cancelThread.mutate()}
|
||||
stopping={cancelThread.isPending}
|
||||
models={models}
|
||||
selection={activeSelection}
|
||||
onSelectionChange={setSelection}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { ArrowUp, ChevronDown, LoaderCircle } from "lucide-react"
|
||||
import { ArrowUp, ChevronDown, LoaderCircle, Square } from "lucide-react"
|
||||
import {
|
||||
memo,
|
||||
useCallback,
|
||||
|
|
@ -23,6 +23,9 @@ export interface CloudPromptBarProps {
|
|||
disabled?: boolean
|
||||
busy?: boolean
|
||||
onSubmit?: (value: string) => void
|
||||
/** Called to stop the running agent. When set, the send button becomes a stop button while busy and the input is empty. */
|
||||
onStop?: () => void
|
||||
stopping?: boolean
|
||||
models?: Array<ModelOption>
|
||||
selection?: ModelSelection | null
|
||||
onSelectionChange?: (next: ModelSelection) => void
|
||||
|
|
@ -39,6 +42,8 @@ export const CloudPromptBar = memo(function CloudPromptBarComponent({
|
|||
disabled = false,
|
||||
busy = false,
|
||||
onSubmit,
|
||||
onStop,
|
||||
stopping = false,
|
||||
models = [],
|
||||
selection = null,
|
||||
onSelectionChange,
|
||||
|
|
@ -103,6 +108,7 @@ export const CloudPromptBar = memo(function CloudPromptBarComponent({
|
|||
}
|
||||
|
||||
const pickerDisabled = combos.length === 0 || !onSelectionChange
|
||||
const showStop = busy && !disabled && !value.trim() && !!onStop
|
||||
|
||||
return (
|
||||
<div
|
||||
|
|
@ -189,19 +195,35 @@ export const CloudPromptBar = memo(function CloudPromptBarComponent({
|
|||
)}
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleSubmit}
|
||||
disabled={!value.trim() || disabled}
|
||||
aria-label="Send message"
|
||||
className="ml-auto flex size-7 shrink-0 items-center justify-center rounded-full bg-[var(--ui-accent)] text-white transition-opacity hover:opacity-90 disabled:cursor-default disabled:opacity-40"
|
||||
>
|
||||
{disabled ? (
|
||||
<LoaderCircle className="size-3.5 animate-spin" />
|
||||
) : (
|
||||
<ArrowUp className="size-3.5" strokeWidth={2.5} />
|
||||
)}
|
||||
</button>
|
||||
{showStop ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onStop}
|
||||
disabled={stopping}
|
||||
aria-label="Stop run"
|
||||
className="ml-auto flex size-7 shrink-0 items-center justify-center rounded-full bg-[var(--ui-accent)] text-white transition-opacity hover:opacity-90 disabled:cursor-default disabled:opacity-40"
|
||||
>
|
||||
{stopping ? (
|
||||
<LoaderCircle className="size-3.5 animate-spin" />
|
||||
) : (
|
||||
<Square className="size-3 fill-current" strokeWidth={0} />
|
||||
)}
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleSubmit}
|
||||
disabled={!value.trim() || disabled}
|
||||
aria-label="Send message"
|
||||
className="ml-auto flex size-7 shrink-0 items-center justify-center rounded-full bg-[var(--ui-accent)] text-white transition-opacity hover:opacity-90 disabled:cursor-default disabled:opacity-40"
|
||||
>
|
||||
{disabled ? (
|
||||
<LoaderCircle className="size-3.5 animate-spin" />
|
||||
) : (
|
||||
<ArrowUp className="size-3.5" strokeWidth={2.5} />
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue