feat: Wait on cancel, better UX when canceling (#578)

This commit is contained in:
Brace Sproul 2025-07-28 16:28:10 -07:00 • committed by GitHub
parent 88d1b8e734
commit d61965ce6b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 33 additions and 12 deletions

View file

@ -49,7 +49,7 @@
"katex": "^0.16.21", "katex": "^0.16.21",
"langgraph-nextjs-api-passthrough": "^0.1.4", "langgraph-nextjs-api-passthrough": "^0.1.4",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"lucide-react": "^0.476.0", "lucide-react": "^0.532.0",
"next-themes": "^0.4.4", "next-themes": "^0.4.4",
"nuqs": "^2.4.1", "nuqs": "^2.4.1",
"react": "^19.0.0", "react": "^19.0.0",

View file

@ -1,5 +1,5 @@
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Loader2 } from "lucide-react"; import { Loader2, SquareStop } from "lucide-react";
import { UseStream } from "@langchain/langgraph-sdk/react"; import { UseStream } from "@langchain/langgraph-sdk/react";
import { useCancelStream } from "@/hooks/useCancelStream"; import { useCancelStream } from "@/hooks/useCancelStream";
@ -16,7 +16,7 @@ export function CancelStreamButton({
runId, runId,
streamName, streamName,
}: CancelStreamButtonProps) { }: CancelStreamButtonProps) {
const { cancelRun } = useCancelStream({ const { cancelRun, cancelLoading } = useCancelStream({
stream, stream,
threadId, threadId,
runId, runId,
@ -35,9 +35,19 @@ export function CancelStreamButton({
size="sm" size="sm"
variant="destructive" variant="destructive"
className="h-8 px-3 text-xs" className="h-8 px-3 text-xs"
disabled={cancelLoading}
> >
<Loader2 className="mr-1 h-3 w-3 animate-spin" /> {cancelLoading ? (
Stop {streamName} <>
<SquareStop className="mr-1 h-3 w-3 animate-pulse" />
<p className="animate-pulse">Stopping {streamName}</p>
</>
) : (
<>
<Loader2 className="mr-1 h-3 w-3 animate-spin" />
Stop {streamName}
</>
)}
</Button> </Button>
); );
} }

View file

@ -1,6 +1,7 @@
import { UseStream } from "@langchain/langgraph-sdk/react"; import { UseStream } from "@langchain/langgraph-sdk/react";
import { PlannerGraphState } from "@open-swe/shared/open-swe/planner/types"; import { PlannerGraphState } from "@open-swe/shared/open-swe/planner/types";
import { GraphState } from "@open-swe/shared/open-swe/types"; import { GraphState } from "@open-swe/shared/open-swe/types";
import { useState } from "react";
import { toast } from "sonner"; import { toast } from "sonner";
interface UseCancelStreamProps<State extends PlannerGraphState | GraphState> { interface UseCancelStreamProps<State extends PlannerGraphState | GraphState> {
@ -16,6 +17,7 @@ export function useCancelStream<State extends PlannerGraphState | GraphState>({
runId, runId,
streamName, streamName,
}: UseCancelStreamProps<State>) { }: UseCancelStreamProps<State>) {
const [cancelLoading, setCancelLoading] = useState(false);
const cancelRun = async () => { const cancelRun = async () => {
if (!threadId || !runId) { if (!threadId || !runId) {
toast.error(`Cannot cancel ${streamName}: Missing thread or run ID`); toast.error(`Cannot cancel ${streamName}: Missing thread or run ID`);
@ -23,9 +25,12 @@ export function useCancelStream<State extends PlannerGraphState | GraphState>({
} }
try { try {
await stream.client.runs.cancel(threadId, runId); setCancelLoading(true);
await stream.client.runs.cancel(threadId, runId, true);
toast.success(`${streamName} cancelled successfully`, { toast.success(`${streamName} cancelled successfully`, {
description: "The running operation has been stopped", description: "The running operation has been stopped",
duration: 5000,
richColors: true,
}); });
} catch (error) { } catch (error) {
const errorStr = String(error); const errorStr = String(error);
@ -34,15 +39,21 @@ export function useCancelStream<State extends PlannerGraphState | GraphState>({
if (isAbortError) { if (isAbortError) {
toast.info(`${streamName} operation cancelled`, { toast.info(`${streamName} operation cancelled`, {
description: "The stream was successfully stopped", description: "The stream was successfully stopped",
duration: 5000,
richColors: true,
}); });
} else { } else {
console.error(`Error cancelling ${streamName} run:`, error); console.error(`Error cancelling ${streamName} run:`, error);
toast.error(`Failed to cancel ${streamName}`, { toast.error(`Failed to cancel ${streamName}`, {
description: errorStr || "Unknown error occurred", description: errorStr || "Unknown error occurred",
duration: 5000,
richColors: true,
}); });
} }
} finally {
setCancelLoading(false);
} }
}; };
return { cancelRun }; return { cancelRun, cancelLoading };
} }

View file

@ -4257,7 +4257,7 @@ __metadata:
katex: ^0.16.21 katex: ^0.16.21
langgraph-nextjs-api-passthrough: ^0.1.4 langgraph-nextjs-api-passthrough: ^0.1.4
lodash: ^4.17.21 lodash: ^4.17.21
lucide-react: ^0.476.0 lucide-react: ^0.532.0
next: ^15.2.3 next: ^15.2.3
next-themes: ^0.4.4 next-themes: ^0.4.4
nuqs: ^2.4.1 nuqs: ^2.4.1
@ -14594,12 +14594,12 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"lucide-react@npm:^0.476.0": "lucide-react@npm:^0.532.0":
version: 0.476.0 version: 0.532.0
resolution: "lucide-react@npm:0.476.0" resolution: "lucide-react@npm:0.532.0"
peerDependencies: peerDependencies:
react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0 react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0
checksum: fcfb5a4ce7be4d6184cca9cd1641bc9c967b70e8a29d2704d3a71a4c08d58b89aaabfbc436f323e7a93b2d2ef5f46782e99a88118267ed6f0670df70d35805d8 checksum: 5d2312b8836f8502f45bf6dbc355c7ab980b9f1f87ad8d12b81bc2f2de51d6176326aecf5bef09f56f65a50bef4d183b6df875a9d0b50777994f1effc27036e8
languageName: node languageName: node
linkType: hard linkType: hard