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,
+ });
+ }
}