diff --git a/src/app/projects/[projectId]/components/newChat/NewChat.tsx b/src/app/projects/[projectId]/components/newChat/NewChat.tsx index 60bfa85..a12a407 100644 --- a/src/app/projects/[projectId]/components/newChat/NewChat.tsx +++ b/src/app/projects/[projectId]/components/newChat/NewChat.tsx @@ -119,7 +119,7 @@ export const NewChat: FC<{ {startNewChat.isPending ? ( <> - Starting... + Starting... This may take a while. ) : ( <> diff --git a/src/app/projects/[projectId]/sessions/[sessionId]/components/SessionPageContent.tsx b/src/app/projects/[projectId]/sessions/[sessionId]/components/SessionPageContent.tsx index dd6f483..1fd7805 100644 --- a/src/app/projects/[projectId]/sessions/[sessionId]/components/SessionPageContent.tsx +++ b/src/app/projects/[projectId]/sessions/[sessionId]/components/SessionPageContent.tsx @@ -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...

+ )} diff --git a/src/app/projects/[projectId]/sessions/[sessionId]/components/resumeChat/ResumeChat.tsx b/src/app/projects/[projectId]/sessions/[sessionId]/components/resumeChat/ResumeChat.tsx index 9bed06a..f118ff4 100644 --- a/src/app/projects/[projectId]/sessions/[sessionId]/components/resumeChat/ResumeChat.tsx +++ b/src/app/projects/[projectId]/sessions/[sessionId]/components/resumeChat/ResumeChat.tsx @@ -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(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}`, ); diff --git a/src/server/hono/route.ts b/src/server/hono/route.ts index 3ae8424..9d1da73 100644 --- a/src/server/hono/route.ts +++ b/src/server/hono/route.ts @@ -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, diff --git a/src/server/service/claude-code/ClaudeCodeTaskController.ts b/src/server/service/claude-code/ClaudeCodeTaskController.ts index e14805c..6ff33be 100644 --- a/src/server/service/claude-code/ClaudeCodeTaskController.ts +++ b/src/server/service/claude-code/ClaudeCodeTaskController.ts @@ -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, + }); + } }