diff --git a/src/app/projects/[projectId]/components/chatForm/useChatMutations.ts b/src/app/projects/[projectId]/components/chatForm/useChatMutations.ts index f77a818..72d49ab 100644 --- a/src/app/projects/[projectId]/components/chatForm/useChatMutations.ts +++ b/src/app/projects/[projectId]/components/chatForm/useChatMutations.ts @@ -32,13 +32,7 @@ export const useNewChatMutation = ( }, onSuccess: async (response) => { onSuccess?.(); - router.push( - `/projects/${projectId}/sessions/${response.sessionId}` + - response.userMessageId !== - undefined - ? `#message-${response.userMessageId}` - : "", - ); + router.push(`/projects/${projectId}/sessions/${response.sessionId}`); }, }); }; diff --git a/src/server/service/claude-code/ClaudeCodeExecutor.ts b/src/server/service/claude-code/ClaudeCodeExecutor.ts index d3c1678..b9de1b2 100644 --- a/src/server/service/claude-code/ClaudeCodeExecutor.ts +++ b/src/server/service/claude-code/ClaudeCodeExecutor.ts @@ -23,13 +23,17 @@ export class ClaudeCodeExecutor { ); } - public get features() { + public get version() { + return this.claudeCodeVersion?.version; + } + + public get availableFeatures() { return { - enableToolApproval: + canUseTool: this.claudeCodeVersion?.greaterThanOrEqual( new ClaudeCodeVersion({ major: 1, minor: 0, patch: 82 }), ) ?? false, - extractUuidFromSDKMessage: + uuidOnSDKMessage: this.claudeCodeVersion?.greaterThanOrEqual( new ClaudeCodeVersion({ major: 1, minor: 0, patch: 86 }), ) ?? false, @@ -44,7 +48,7 @@ export class ClaudeCodeExecutor { options: { pathToClaudeCodeExecutable: this.pathToClaudeCodeExecutable, ...baseOptions, - ...(this.features.enableToolApproval ? { canUseTool } : {}), + ...(this.availableFeatures.canUseTool ? { canUseTool } : {}), }, }); } diff --git a/src/server/service/claude-code/ClaudeCodeTaskController.ts b/src/server/service/claude-code/ClaudeCodeTaskController.ts index 4883b31..50cbaa0 100644 --- a/src/server/service/claude-code/ClaudeCodeTaskController.ts +++ b/src/server/service/claude-code/ClaudeCodeTaskController.ts @@ -1,7 +1,8 @@ import prexit from "prexit"; import { ulid } from "ulid"; import type { Config } from "../../config/config"; -import { getEventBus, type IEventBus } from "../events/EventBus"; +import { eventBus } from "../events/EventBus"; +import { predictSessionsDatabase } from "../session/PredictSessionsDatabase"; import { ClaudeCodeExecutor } from "./ClaudeCodeExecutor"; import { createMessageGenerator } from "./createMessageGenerator"; import type { @@ -169,9 +170,20 @@ export class ClaudeCodeTaskController { ); if (existingTask) { + console.log( + `Alive task for session(id=${currentSession.sessionId}) continued.`, + ); const result = await this.continueTask(existingTask, message); return result; } else { + if (currentSession.sessionId === undefined) { + console.log(`New task started.`); + } else { + console.log( + `New task started for existing session(id=${currentSession.sessionId}).`, + ); + } + const result = await this.startTask(currentSession, message); return result; } @@ -253,32 +265,37 @@ export class ClaudeCodeTaskController { }); } - // 初回の system message だとまだ history ファイルが作成されていないので - if (message.type === "user" || message.type === "assistant") { - // 本来は message.uuid の存在チェックをしたいが、古いバージョンでは存在しないことがある - if (!resolved) { - const runningTask: RunningClaudeCodeTask = { - status: "running", - id: task.id, - projectId: task.projectId, - cwd: task.cwd, - generateMessages: task.generateMessages, - setNextMessage: task.setNextMessage, - resolveFirstMessage: task.resolveFirstMessage, - setFirstMessagePromise: task.setFirstMessagePromise, - awaitFirstMessage: task.awaitFirstMessage, - onMessageHandlers: task.onMessageHandlers, - userMessageId: message.uuid, - sessionId: message.session_id, - abortController: abortController, - }; - this.tasks.push(runningTask); - aliveTaskResolve(runningTask); - resolved = true; - } - - eventBus.emit("sessionListChanged", { - projectId: task.projectId, + if ( + message.type === "system" && + message.subtype === "init" && + currentSession.sessionId === undefined + ) { + // because it takes time for the Claude Code file to be updated, simulate the message + predictSessionsDatabase.createPredictSession({ + id: message.session_id, + jsonlFilePath: message.session_id, + conversations: [ + { + type: "user", + message: { + role: "user", + content: userMessage, + }, + isSidechain: false, + userType: "external", + cwd: message.cwd, + sessionId: message.session_id, + version: this.claudeCode.version?.toString() ?? "unknown", + uuid: message.uuid, + timestamp: new Date().toISOString(), + parentUuid: null, + }, + ], + meta: { + firstCommand: null, + lastModifiedAt: new Date().toISOString(), + messageCount: 0, + }, }); } diff --git a/src/server/service/claude-code/ClaudeCodeVersion.ts b/src/server/service/claude-code/ClaudeCodeVersion.ts index db3b4f3..9193a7e 100644 --- a/src/server/service/claude-code/ClaudeCodeVersion.ts +++ b/src/server/service/claude-code/ClaudeCodeVersion.ts @@ -43,6 +43,10 @@ export class ClaudeCodeVersion { return this.version.patch; } + public toString() { + return `${this.major}.${this.minor}.${this.patch}`; + } + public equals(other: ClaudeCodeVersion) { return ( this.version.major === other.version.major && diff --git a/src/server/service/claude-code/types.ts b/src/server/service/claude-code/types.ts index 83889df..c920d14 100644 --- a/src/server/service/claude-code/types.ts +++ b/src/server/service/claude-code/types.ts @@ -20,21 +20,18 @@ export type PendingClaudeCodeTask = BaseClaudeCodeTask & { export type RunningClaudeCodeTask = BaseClaudeCodeTask & { status: "running"; sessionId: string; - userMessageId: string | undefined; abortController: AbortController; }; export type PausedClaudeCodeTask = BaseClaudeCodeTask & { status: "paused"; sessionId: string; - userMessageId: string | undefined; abortController: AbortController; }; type CompletedClaudeCodeTask = BaseClaudeCodeTask & { status: "completed"; sessionId: string; - userMessageId: string | undefined; abortController: AbortController; resolveFirstMessage: () => void; }; diff --git a/src/server/service/session/PredictSessionsDatabase.ts b/src/server/service/session/PredictSessionsDatabase.ts index d26ec5f..161b273 100644 --- a/src/server/service/session/PredictSessionsDatabase.ts +++ b/src/server/service/session/PredictSessionsDatabase.ts @@ -7,19 +7,19 @@ import type { Session, SessionDetail } from "../types"; class PredictSessionsDatabase { private storage = new Map(); - private get allPredictSessions() { - return Array.from(this.storage.values()); - } - public getPredictSessions(projectId: string): Session[] { - return this.allPredictSessions.filter( + return Array.from(this.storage.values()).filter( ({ jsonlFilePath }) => encodeProjectIdFromSessionFilePath(jsonlFilePath) === projectId, ); } - public getPredictSession(sessionId: string): SessionDetail | null { - return this.storage.get(sessionId) ?? null; + public getPredictSession(sessionId: string): SessionDetail { + const session = this.storage.get(sessionId); + if (!session) { + throw new Error("Session not found"); + } + return session; } public createPredictSession(session: SessionDetail) { diff --git a/src/server/service/session/SessionRepository.ts b/src/server/service/session/SessionRepository.ts index 6f43cf2..df9be24 100644 --- a/src/server/service/session/SessionRepository.ts +++ b/src/server/service/session/SessionRepository.ts @@ -1,4 +1,4 @@ -import { existsSync, statSync } from "node:fs"; +import { existsSync } from "node:fs"; import { readdir, readFile } from "node:fs/promises"; import { resolve } from "node:path"; import { parseJsonl } from "../parseJsonl"; @@ -19,6 +19,15 @@ export class SessionRepository { if (!existsSync(sessionPath)) { const predictSession = predictSessionsDatabase.getPredictSession(sessionId); + if (predictSession) { + return { + session: predictSession, + }; + } + + throw new Error("Session not found"); + } + const content = await readFile(sessionPath, "utf-8"); if (predictSession !== null) { return { @@ -78,6 +87,13 @@ export class SessionRepository { (a, b) => b.lastModifiedAt.getTime() - a.lastModifiedAt.getTime(), ), ); + const sessionMap = new Map( + sessions.map((session) => [session.id, session]), + ); + + const predictSessions = predictSessionsDatabase + .getPredictSessions(projectId) + .filter((session) => !sessionMap.has(session.id)); const sessionMap = new Map( sessions.map((session) => [session.id, session] as const), @@ -114,22 +130,11 @@ export class SessionRepository { }); return { - sessions: [ - ...predictSessions, - ...(await Promise.all( - sessions - .slice(0, Math.min(maxCount, sessions.length)) - .map(async (item) => { - return { - ...item, - meta: await sessionMetaStorage.getSessionMeta( - projectId, - item.id, - ), - }; - }), - )), - ], + sessions: [...predictSessions, ...sessions].sort((a, b) => { + return ( + getTime(b.meta.lastModifiedAt) - getTime(a.meta.lastModifiedAt) + ); + }), }; } catch (error) { console.warn(`Failed to read sessions for project ${projectId}:`, error);