feat: improve continue chat experience

This commit is contained in:
d-kimsuon
2025-09-03 04:26:47 +09:00
parent 31da82377d
commit e689dd5b34
6 changed files with 81 additions and 34 deletions

View File

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

View File

@@ -45,7 +45,10 @@ export const SessionPageContent: FC<{
// 自動スクロール処理 // 自動スクロール処理
useEffect(() => { useEffect(() => {
if (isRunningTask && conversations.length !== previousConversationLength) { if (
(isRunningTask || isPausedTask) &&
conversations.length !== previousConversationLength
) {
setPreviousConversationLength(conversations.length); setPreviousConversationLength(conversations.length);
const scrollContainer = scrollContainerRef.current; const scrollContainer = scrollContainerRef.current;
if (scrollContainer) { if (scrollContainer) {
@@ -55,7 +58,7 @@ export const SessionPageContent: FC<{
}); });
} }
} }
}, [conversations, isRunningTask, previousConversationLength]); }, [conversations, isRunningTask, isPausedTask, previousConversationLength]);
return ( return (
<div className="flex h-screen"> <div className="flex h-screen">
@@ -134,7 +137,7 @@ export const SessionPageContent: FC<{
</div> </div>
</header> </header>
<main className="w-full px-20 pb-20 relative z-5"> <main className="w-full px-20 pb-10 relative z-5">
<ConversationList <ConversationList
conversations={conversations} conversations={conversations}
getToolResult={getToolResult} getToolResult={getToolResult}

View File

@@ -36,12 +36,13 @@ export const ResumeChat: FC<{
return response.json(); return response.json();
}, },
onSuccess: async (response) => { onSuccess: async (response) => {
setMessage("");
if (sessionId !== response.sessionId) { if (sessionId !== response.sessionId) {
router.push( router.push(
`/projects/${projectId}/sessions/${response.sessionId}#message-${response.userMessageId}`, `/projects/${projectId}/sessions/${response.sessionId}#message-${response.userMessageId}`,
); );
} }
setMessage("");
}, },
}); });
@@ -122,7 +123,7 @@ export const ResumeChat: FC<{
{resumeChat.isPending ? ( {resumeChat.isPending ? (
<> <>
<LoaderIcon className="w-4 h-4 animate-spin" /> <LoaderIcon className="w-4 h-4 animate-spin" />
Starting... This may take a while. Sending... This may take a while.
</> </>
) : isPausedTask || isRunningTask ? ( ) : isPausedTask || isRunningTask ? (
<> <>

View File

@@ -1,7 +1,7 @@
import { execSync } from "node:child_process"; import { execSync } from "node:child_process";
import { query } from "@anthropic-ai/claude-code"; import { query } from "@anthropic-ai/claude-code";
import { ulid } from "ulid";
import prexit from "prexit"; import prexit from "prexit";
import { ulid } from "ulid";
import { type EventBus, getEventBus } from "../events/EventBus"; import { type EventBus, getEventBus } from "../events/EventBus";
import { createMessageGenerator } from "./createMessageGenerator"; import { createMessageGenerator } from "./createMessageGenerator";
import type { import type {
@@ -48,14 +48,15 @@ export class ClaudeCodeTaskController {
); );
if (existingTask) { if (existingTask) {
return this.continueTask(existingTask, message); return await this.continueTask(existingTask, message);
} else { } else {
return await this.startTask(currentSession, message); return await this.startTask(currentSession, message);
} }
} }
private continueTask(task: AliveClaudeCodeTask, message: string) { private async continueTask(task: AliveClaudeCodeTask, message: string) {
task.setNextMessage(message); task.setNextMessage(message);
await task.awaitFirstMessage();
return task; return task;
} }
@@ -67,8 +68,13 @@ export class ClaudeCodeTaskController {
}, },
message: string, message: string,
) { ) {
const { generateMessages, setNextMessage } = const {
createMessageGenerator(message); generateMessages,
setNextMessage,
setFirstMessagePromise,
resolveFirstMessage,
awaitFirstMessage,
} = createMessageGenerator(message);
const task: PendingClaudeCodeTask = { const task: PendingClaudeCodeTask = {
status: "pending", status: "pending",
@@ -78,6 +84,9 @@ export class ClaudeCodeTaskController {
cwd: currentSession.cwd, cwd: currentSession.cwd,
generateMessages, generateMessages,
setNextMessage, setNextMessage,
setFirstMessagePromise,
resolveFirstMessage,
awaitFirstMessage,
onMessageHandlers: [], onMessageHandlers: [],
}; };
@@ -120,25 +129,31 @@ export class ClaudeCodeTaskController {
// 初回の system message だとまだ history ファイルが作成されていないので // 初回の system message だとまだ history ファイルが作成されていないので
if ( if (
!resolved &&
(message.type === "user" || message.type === "assistant") && (message.type === "user" || message.type === "assistant") &&
message.uuid !== undefined message.uuid !== undefined
) { ) {
const runningTask: RunningClaudeCodeTask = { if (!resolved) {
status: "running", const runningTask: RunningClaudeCodeTask = {
id: task.id, status: "running",
projectId: task.projectId, id: task.id,
cwd: task.cwd, projectId: task.projectId,
generateMessages: task.generateMessages, cwd: task.cwd,
setNextMessage: task.setNextMessage, generateMessages: task.generateMessages,
onMessageHandlers: task.onMessageHandlers, setNextMessage: task.setNextMessage,
userMessageId: message.uuid, resolveFirstMessage: task.resolveFirstMessage,
sessionId: message.session_id, setFirstMessagePromise: task.setFirstMessagePromise,
abortController: abortController, awaitFirstMessage: task.awaitFirstMessage,
}; onMessageHandlers: task.onMessageHandlers,
this.tasks.push(runningTask); userMessageId: message.uuid,
aliveTaskResolve(runningTask); sessionId: message.session_id,
resolved = true; abortController: abortController,
};
this.tasks.push(runningTask);
aliveTaskResolve(runningTask);
resolved = true;
}
resolveFirstMessage();
} }
await Promise.all( await Promise.all(
@@ -152,6 +167,8 @@ export class ClaudeCodeTaskController {
...currentTask, ...currentTask,
status: "paused", status: "paused",
}); });
resolved = true;
setFirstMessagePromise();
} }
} }
@@ -204,6 +221,9 @@ export class ClaudeCodeTaskController {
cwd: task.cwd, cwd: task.cwd,
generateMessages: task.generateMessages, generateMessages: task.generateMessages,
setNextMessage: task.setNextMessage, setNextMessage: task.setNextMessage,
resolveFirstMessage: task.resolveFirstMessage,
setFirstMessagePromise: task.setFirstMessagePromise,
awaitFirstMessage: task.awaitFirstMessage,
onMessageHandlers: task.onMessageHandlers, onMessageHandlers: task.onMessageHandlers,
baseSessionId: task.baseSessionId, baseSessionId: task.baseSessionId,
userMessageId: task.userMessageId, userMessageId: task.userMessageId,

View File

@@ -8,11 +8,11 @@ export type MessageGenerator = () => AsyncGenerator<
unknown unknown
>; >;
const createPromise = () => { const createPromise = <T>() => {
let promiseResolve: ((value: string) => void) | undefined; let promiseResolve: ((value: T) => void) | undefined;
let promiseReject: ((reason?: unknown) => void) | undefined; let promiseReject: ((reason?: unknown) => void) | undefined;
const promise = new Promise<string>((resolve, reject) => { const promise = new Promise<T>((resolve, reject) => {
promiseResolve = resolve; promiseResolve = resolve;
promiseReject = reject; promiseReject = reject;
}); });
@@ -33,8 +33,12 @@ export const createMessageGenerator = (
): { ): {
generateMessages: MessageGenerator; generateMessages: MessageGenerator;
setNextMessage: (message: string) => void; setNextMessage: (message: string) => void;
setFirstMessagePromise: () => void;
resolveFirstMessage: () => void;
awaitFirstMessage: () => Promise<void>;
} => { } => {
let currentPromise = createPromise(); let sendMessagePromise = createPromise<string>();
let receivedFirstMessagePromise = createPromise<undefined>();
const createMessage = (message: string): SDKUserMessage => { const createMessage = (message: string): SDKUserMessage => {
return { return {
@@ -50,19 +54,34 @@ export const createMessageGenerator = (
yield createMessage(firstMessage); yield createMessage(firstMessage);
while (true) { while (true) {
const message = await currentPromise.promise; const message = await sendMessagePromise.promise;
currentPromise = createPromise(); sendMessagePromise = createPromise<string>();
yield createMessage(message); yield createMessage(message);
} }
} }
const setNextMessage = (message: string) => { const setNextMessage = (message: string) => {
currentPromise.resolve(message); sendMessagePromise.resolve(message);
};
const setFirstMessagePromise = () => {
receivedFirstMessagePromise = createPromise<undefined>();
};
const resolveFirstMessage = () => {
receivedFirstMessagePromise.resolve(undefined);
};
const awaitFirstMessage = async () => {
await receivedFirstMessagePromise.promise;
}; };
return { return {
generateMessages, generateMessages,
setNextMessage, setNextMessage,
setFirstMessagePromise,
resolveFirstMessage,
awaitFirstMessage,
}; };
}; };

View File

@@ -7,6 +7,9 @@ type BaseClaudeCodeTask = {
cwd: string; cwd: string;
generateMessages: MessageGenerator; generateMessages: MessageGenerator;
setNextMessage: (message: string) => void; setNextMessage: (message: string) => void;
resolveFirstMessage: () => void;
setFirstMessagePromise: () => void;
awaitFirstMessage: () => Promise<void>;
onMessageHandlers: OnMessage[]; onMessageHandlers: OnMessage[];
}; };
@@ -33,6 +36,7 @@ type CompletedClaudeCodeTask = BaseClaudeCodeTask & {
sessionId: string; sessionId: string;
userMessageId: string; userMessageId: string;
abortController: AbortController; abortController: AbortController;
resolveFirstMessage: () => void;
}; };
type FailedClaudeCodeTask = BaseClaudeCodeTask & { type FailedClaudeCodeTask = BaseClaudeCodeTask & {