feat: abort running task

This commit is contained in:
d-kimsuon
2025-09-02 21:48:42 +09:00
parent 55f70633fd
commit 60b9c658f5
5 changed files with 68 additions and 3 deletions

View File

@@ -119,7 +119,7 @@ export const NewChat: FC<{
{startNewChat.isPending ? (
<>
<LoaderIcon className="w-4 h-4 animate-spin" />
Starting...
Starting... This may take a while.
</>
) : (
<>

View File

@@ -1,10 +1,12 @@
"use client";
import { ArrowLeftIcon, LoaderIcon } from "lucide-react";
import { useMutation } from "@tanstack/react-query";
import { ArrowLeftIcon, LoaderIcon, XIcon } from "lucide-react";
import Link from "next/link";
import type { FC } from "react";
import { useEffect, useRef, useState } from "react";
import { Button } from "@/components/ui/button";
import { honoClient } from "../../../../../../lib/api/client";
import { firstCommandToTitle } from "../../../services/firstCommandToTitle";
import { useIsResummingTask } from "../hooks/useIsResummingTask";
import { useSession } from "../hooks/useSession";
@@ -21,6 +23,20 @@ export const SessionPageContent: FC<{
sessionId,
);
const abortTask = useMutation({
mutationFn: async (sessionId: string) => {
const response = await honoClient.api.tasks.abort.$post({
json: { sessionId },
});
if (!response.ok) {
throw new Error(response.statusText);
}
return response.json();
},
});
const { isResummingTask } = useIsResummingTask(sessionId);
const [previouConversationLength, setPreviouConversationLength] = useState(0);
@@ -77,6 +93,16 @@ export const SessionPageContent: FC<{
Conversation is being resumed...
</p>
</div>
<Button
variant="ghost"
size="sm"
onClick={() => {
abortTask.mutate(sessionId);
}}
>
<XIcon className="w-4 h-4" />
Abort
</Button>
</div>
)}

View File

@@ -1,4 +1,4 @@
import { useMutation } from "@tanstack/react-query";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import {
AlertCircleIcon,
LoaderIcon,
@@ -29,6 +29,7 @@ export const ResumeChat: FC<{
}> = ({ projectId, sessionId }) => {
const router = useRouter();
const textareaRef = useRef<HTMLTextAreaElement>(null);
const queryClient = useQueryClient();
const resumeChat = useMutation({
mutationFn: async (options: { message: string }) => {
@@ -47,6 +48,7 @@ export const ResumeChat: FC<{
},
onSuccess: (response) => {
setMessage("");
queryClient.invalidateQueries({ queryKey: ["runningTasks"] });
router.push(
`/projects/${projectId}/sessions/${response.nextSessionId}#message-${response.userMessageId}`,
);

View File

@@ -228,6 +228,16 @@ export const routes = (app: HonoAppType) => {
return c.json({ runningTasks: taskController.runningTasks });
})
.post(
"/tasks/abort",
zValidator("json", z.object({ sessionId: z.string() })),
async (c) => {
const { sessionId } = c.req.valid("json");
taskController.abortTask(sessionId);
return c.json({ message: "Task aborted" });
},
)
.get("/events/state_changes", async (c) => {
return streamSSE(
c,

View File

@@ -21,6 +21,7 @@ type RunningClaudeCodeTask = BaseClaudeCodeTask & {
status: "running";
nextSessionId: string;
userMessageId: string;
abortController: AbortController;
};
type CompletedClaudeCodeTask = BaseClaudeCodeTask & {
@@ -104,6 +105,8 @@ export class ClaudeCodeTaskController {
const handleTask = async () => {
try {
const abortController = new AbortController();
for await (const message of query({
prompt: task.message,
options: {
@@ -111,6 +114,7 @@ export class ClaudeCodeTaskController {
cwd: task.cwd,
pathToClaudeCodeExecutable: this.pathToClaudeCodeExecutable,
permissionMode: "bypassPermissions",
abortController,
},
})) {
// 初回の sysmte message だとまだ history ファイルが作成されていないので
@@ -124,6 +128,7 @@ export class ClaudeCodeTaskController {
status: "running",
nextSessionId: message.session_id,
userMessageId: message.uuid,
abortController,
};
this.updateExistingTask(runningTask);
runningTaskResolve(runningTask);
@@ -167,4 +172,26 @@ export class ClaudeCodeTaskController {
return runningTaskPromise;
}
public abortTask(sessionId: string) {
const task = this.tasks
.filter((task) => task.status === "running")
.find((task) => task.nextSessionId === sessionId);
if (!task) {
throw new Error("Running Task not found");
}
task.abortController.abort();
this.updateExistingTask({
id: task.id,
status: "failed",
cwd: task.cwd,
message: task.message,
onMessageHandlers: task.onMessageHandlers,
projectId: task.projectId,
nextSessionId: task.nextSessionId,
sessionId: task.sessionId,
userMessageId: task.userMessageId,
});
}
}