mirror of
https://github.com/aljazceru/claude-code-viewer.git
synced 2025-12-21 15:24:20 +01:00
feat: improve interactivity by predict sessions
This commit is contained in:
22
.vscode/settings.json
vendored
22
.vscode/settings.json
vendored
@@ -7,7 +7,27 @@
|
|||||||
"biome.enabled": true,
|
"biome.enabled": true,
|
||||||
// autofix
|
// autofix
|
||||||
"editor.formatOnSave": false,
|
"editor.formatOnSave": false,
|
||||||
"[typescript][typescriptreact][javascript][javascriptreact][json][jsonc][json][yaml]": {
|
"[typescript]": {
|
||||||
|
"editor.formatOnSave": true,
|
||||||
|
"editor.defaultFormatter": "biomejs.biome"
|
||||||
|
},
|
||||||
|
"[typescriptreact]": {
|
||||||
|
"editor.formatOnSave": true,
|
||||||
|
"editor.defaultFormatter": "biomejs.biome"
|
||||||
|
},
|
||||||
|
"[javascript]": {
|
||||||
|
"editor.formatOnSave": true,
|
||||||
|
"editor.defaultFormatter": "biomejs.biome"
|
||||||
|
},
|
||||||
|
"[javascriptreact]": {
|
||||||
|
"editor.formatOnSave": true,
|
||||||
|
"editor.defaultFormatter": "biomejs.biome"
|
||||||
|
},
|
||||||
|
"[json]": {
|
||||||
|
"editor.formatOnSave": true,
|
||||||
|
"editor.defaultFormatter": "biomejs.biome"
|
||||||
|
},
|
||||||
|
"[jsonc]": {
|
||||||
"editor.formatOnSave": true,
|
"editor.formatOnSave": true,
|
||||||
"editor.defaultFormatter": "biomejs.biome"
|
"editor.defaultFormatter": "biomejs.biome"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -32,13 +32,7 @@ export const useNewChatMutation = (
|
|||||||
},
|
},
|
||||||
onSuccess: async (response) => {
|
onSuccess: async (response) => {
|
||||||
onSuccess?.();
|
onSuccess?.();
|
||||||
router.push(
|
router.push(`/projects/${projectId}/sessions/${response.sessionId}`);
|
||||||
`/projects/${projectId}/sessions/${response.sessionId}` +
|
|
||||||
response.userMessageId !==
|
|
||||||
undefined
|
|
||||||
? `#message-${response.userMessageId}`
|
|
||||||
: "",
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -70,9 +64,7 @@ export const useResumeChatMutation = (projectId: string, sessionId: string) => {
|
|||||||
},
|
},
|
||||||
onSuccess: async (response) => {
|
onSuccess: async (response) => {
|
||||||
if (sessionId !== response.sessionId) {
|
if (sessionId !== response.sessionId) {
|
||||||
router.push(
|
router.push(`/projects/${projectId}/sessions/${response.sessionId}`);
|
||||||
`/projects/${projectId}/sessions/${response.sessionId}#message-${response.userMessageId}`,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -27,13 +27,13 @@ export const initialize = async (deps: {
|
|||||||
|
|
||||||
console.log("Initializing sessions cache");
|
console.log("Initializing sessions cache");
|
||||||
const results = await Promise.all(
|
const results = await Promise.all(
|
||||||
projects.map((project) => deps.sessionRepository.getSessions(project.id))
|
projects.map((project) => deps.sessionRepository.getSessions(project.id)),
|
||||||
);
|
);
|
||||||
console.log(
|
console.log(
|
||||||
`${results.reduce(
|
`${results.reduce(
|
||||||
(s, { sessions }) => s + sessions.length,
|
(s, { sessions }) => s + sessions.length,
|
||||||
0
|
0,
|
||||||
)} sessions cache initialized`
|
)} sessions cache initialized`,
|
||||||
);
|
);
|
||||||
} catch {
|
} catch {
|
||||||
// do nothing
|
// do nothing
|
||||||
|
|||||||
@@ -337,7 +337,6 @@ export const routes = async (app: HonoAppType) => {
|
|||||||
return c.json({
|
return c.json({
|
||||||
taskId: task.id,
|
taskId: task.id,
|
||||||
sessionId: task.sessionId,
|
sessionId: task.sessionId,
|
||||||
userMessageId: task.userMessageId,
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -373,7 +372,6 @@ export const routes = async (app: HonoAppType) => {
|
|||||||
return c.json({
|
return c.json({
|
||||||
taskId: task.id,
|
taskId: task.id,
|
||||||
sessionId: task.sessionId,
|
sessionId: task.sessionId,
|
||||||
userMessageId: task.userMessageId,
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -385,7 +383,6 @@ export const routes = async (app: HonoAppType) => {
|
|||||||
id: task.id,
|
id: task.id,
|
||||||
status: task.status,
|
status: task.status,
|
||||||
sessionId: task.sessionId,
|
sessionId: task.sessionId,
|
||||||
userMessageId: task.userMessageId,
|
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ export class FileCacheStorage<const T> {
|
|||||||
|
|
||||||
public static load<const LoadSchema>(
|
public static load<const LoadSchema>(
|
||||||
key: string,
|
key: string,
|
||||||
schema: z.ZodType<LoadSchema>
|
schema: z.ZodType<LoadSchema>,
|
||||||
) {
|
) {
|
||||||
const instance = new FileCacheStorage<LoadSchema>(key);
|
const instance = new FileCacheStorage<LoadSchema>(key);
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
export class InMemoryCacheStorage<const T> {
|
export class InMemoryCacheStorage<const T> {
|
||||||
private storage = new Map<string, T>();
|
private storage = new Map<string, T>();
|
||||||
|
|
||||||
public constructor() {}
|
|
||||||
|
|
||||||
public get(key: string) {
|
public get(key: string) {
|
||||||
return this.storage.get(key);
|
return this.storage.get(key);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,13 +23,17 @@ export class ClaudeCodeExecutor {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public get features() {
|
public get version() {
|
||||||
|
return this.claudeCodeVersion?.version;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get availableFeatures() {
|
||||||
return {
|
return {
|
||||||
enableToolApproval:
|
canUseTool:
|
||||||
this.claudeCodeVersion?.greaterThanOrEqual(
|
this.claudeCodeVersion?.greaterThanOrEqual(
|
||||||
new ClaudeCodeVersion({ major: 1, minor: 0, patch: 82 }),
|
new ClaudeCodeVersion({ major: 1, minor: 0, patch: 82 }),
|
||||||
) ?? false,
|
) ?? false,
|
||||||
extractUuidFromSDKMessage:
|
uuidOnSDKMessage:
|
||||||
this.claudeCodeVersion?.greaterThanOrEqual(
|
this.claudeCodeVersion?.greaterThanOrEqual(
|
||||||
new ClaudeCodeVersion({ major: 1, minor: 0, patch: 86 }),
|
new ClaudeCodeVersion({ major: 1, minor: 0, patch: 86 }),
|
||||||
) ?? false,
|
) ?? false,
|
||||||
@@ -44,7 +48,7 @@ export class ClaudeCodeExecutor {
|
|||||||
options: {
|
options: {
|
||||||
pathToClaudeCodeExecutable: this.pathToClaudeCodeExecutable,
|
pathToClaudeCodeExecutable: this.pathToClaudeCodeExecutable,
|
||||||
...baseOptions,
|
...baseOptions,
|
||||||
...(this.features.enableToolApproval ? { canUseTool } : {}),
|
...(this.availableFeatures.canUseTool ? { canUseTool } : {}),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import prexit from "prexit";
|
|||||||
import { ulid } from "ulid";
|
import { ulid } from "ulid";
|
||||||
import type { Config } from "../../config/config";
|
import type { Config } from "../../config/config";
|
||||||
import { eventBus } from "../events/EventBus";
|
import { eventBus } from "../events/EventBus";
|
||||||
|
import { predictSessionsDatabase } from "../session/PredictSessionsDatabase";
|
||||||
import { ClaudeCodeExecutor } from "./ClaudeCodeExecutor";
|
import { ClaudeCodeExecutor } from "./ClaudeCodeExecutor";
|
||||||
import { createMessageGenerator } from "./createMessageGenerator";
|
import { createMessageGenerator } from "./createMessageGenerator";
|
||||||
import type {
|
import type {
|
||||||
@@ -168,9 +169,20 @@ export class ClaudeCodeTaskController {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (existingTask) {
|
if (existingTask) {
|
||||||
|
console.log(
|
||||||
|
`Alive task for session(id=${currentSession.sessionId}) continued.`,
|
||||||
|
);
|
||||||
const result = await this.continueTask(existingTask, message);
|
const result = await this.continueTask(existingTask, message);
|
||||||
return result;
|
return result;
|
||||||
} else {
|
} 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);
|
const result = await this.startTask(currentSession, message);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
@@ -188,7 +200,7 @@ export class ClaudeCodeTaskController {
|
|||||||
projectId: string;
|
projectId: string;
|
||||||
sessionId?: string;
|
sessionId?: string;
|
||||||
},
|
},
|
||||||
message: string,
|
userMessage: string,
|
||||||
) {
|
) {
|
||||||
const {
|
const {
|
||||||
generateMessages,
|
generateMessages,
|
||||||
@@ -196,7 +208,7 @@ export class ClaudeCodeTaskController {
|
|||||||
setFirstMessagePromise,
|
setFirstMessagePromise,
|
||||||
resolveFirstMessage,
|
resolveFirstMessage,
|
||||||
awaitFirstMessage,
|
awaitFirstMessage,
|
||||||
} = createMessageGenerator(message);
|
} = createMessageGenerator(userMessage);
|
||||||
|
|
||||||
const task: PendingClaudeCodeTask = {
|
const task: PendingClaudeCodeTask = {
|
||||||
status: "pending",
|
status: "pending",
|
||||||
@@ -252,14 +264,41 @@ export class ClaudeCodeTaskController {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 初回の system message だとまだ history ファイルが作成されていないので
|
if (
|
||||||
if (message.type === "user" || message.type === "assistant") {
|
message.type === "system" &&
|
||||||
// 本来は message.uuid の存在チェックをしたいが、古いバージョンでは存在しないことがある
|
message.subtype === "init" &&
|
||||||
if (!resolved) {
|
currentSession.sessionId === undefined
|
||||||
console.log(
|
) {
|
||||||
"[DEBUG startTask] 10. Resolving task for first time",
|
// 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,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!resolved) {
|
||||||
const runningTask: RunningClaudeCodeTask = {
|
const runningTask: RunningClaudeCodeTask = {
|
||||||
status: "running",
|
status: "running",
|
||||||
id: task.id,
|
id: task.id,
|
||||||
@@ -271,23 +310,15 @@ export class ClaudeCodeTaskController {
|
|||||||
setFirstMessagePromise: task.setFirstMessagePromise,
|
setFirstMessagePromise: task.setFirstMessagePromise,
|
||||||
awaitFirstMessage: task.awaitFirstMessage,
|
awaitFirstMessage: task.awaitFirstMessage,
|
||||||
onMessageHandlers: task.onMessageHandlers,
|
onMessageHandlers: task.onMessageHandlers,
|
||||||
userMessageId: message.uuid,
|
|
||||||
sessionId: message.session_id,
|
sessionId: message.session_id,
|
||||||
abortController: abortController,
|
abortController: abortController,
|
||||||
};
|
};
|
||||||
this.tasks.push(runningTask);
|
this.tasks.push(runningTask);
|
||||||
console.log(
|
|
||||||
"[DEBUG startTask] 11. About to call aliveTaskResolve",
|
|
||||||
);
|
|
||||||
aliveTaskResolve(runningTask);
|
aliveTaskResolve(runningTask);
|
||||||
resolved = true;
|
resolved = true;
|
||||||
console.log(
|
|
||||||
"[DEBUG startTask] 12. aliveTaskResolve called, resolved=true",
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
resolveFirstMessage();
|
resolveFirstMessage();
|
||||||
}
|
|
||||||
|
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
task.onMessageHandlers.map(async (onMessageHandler) => {
|
task.onMessageHandlers.map(async (onMessageHandler) => {
|
||||||
@@ -302,6 +333,7 @@ export class ClaudeCodeTaskController {
|
|||||||
});
|
});
|
||||||
resolved = true;
|
resolved = true;
|
||||||
setFirstMessagePromise();
|
setFirstMessagePromise();
|
||||||
|
predictSessionsDatabase.deletePredictSession(currentTask.sessionId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -372,7 +404,6 @@ export class ClaudeCodeTaskController {
|
|||||||
awaitFirstMessage: task.awaitFirstMessage,
|
awaitFirstMessage: task.awaitFirstMessage,
|
||||||
onMessageHandlers: task.onMessageHandlers,
|
onMessageHandlers: task.onMessageHandlers,
|
||||||
baseSessionId: task.baseSessionId,
|
baseSessionId: task.baseSessionId,
|
||||||
userMessageId: task.userMessageId,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -43,6 +43,10 @@ export class ClaudeCodeVersion {
|
|||||||
return this.version.patch;
|
return this.version.patch;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public toString() {
|
||||||
|
return `${this.major}.${this.minor}.${this.patch}`;
|
||||||
|
}
|
||||||
|
|
||||||
public equals(other: ClaudeCodeVersion) {
|
public equals(other: ClaudeCodeVersion) {
|
||||||
return (
|
return (
|
||||||
this.version.major === other.version.major &&
|
this.version.major === other.version.major &&
|
||||||
|
|||||||
@@ -20,21 +20,18 @@ export type PendingClaudeCodeTask = BaseClaudeCodeTask & {
|
|||||||
export type RunningClaudeCodeTask = BaseClaudeCodeTask & {
|
export type RunningClaudeCodeTask = BaseClaudeCodeTask & {
|
||||||
status: "running";
|
status: "running";
|
||||||
sessionId: string;
|
sessionId: string;
|
||||||
userMessageId: string | undefined;
|
|
||||||
abortController: AbortController;
|
abortController: AbortController;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type PausedClaudeCodeTask = BaseClaudeCodeTask & {
|
export type PausedClaudeCodeTask = BaseClaudeCodeTask & {
|
||||||
status: "paused";
|
status: "paused";
|
||||||
sessionId: string;
|
sessionId: string;
|
||||||
userMessageId: string | undefined;
|
|
||||||
abortController: AbortController;
|
abortController: AbortController;
|
||||||
};
|
};
|
||||||
|
|
||||||
type CompletedClaudeCodeTask = BaseClaudeCodeTask & {
|
type CompletedClaudeCodeTask = BaseClaudeCodeTask & {
|
||||||
status: "completed";
|
status: "completed";
|
||||||
sessionId: string;
|
sessionId: string;
|
||||||
userMessageId: string | undefined;
|
|
||||||
abortController: AbortController;
|
abortController: AbortController;
|
||||||
resolveFirstMessage: () => void;
|
resolveFirstMessage: () => void;
|
||||||
};
|
};
|
||||||
@@ -56,7 +53,7 @@ export type AliveClaudeCodeTask = RunningClaudeCodeTask | PausedClaudeCodeTask;
|
|||||||
|
|
||||||
export type SerializableAliveTask = Pick<
|
export type SerializableAliveTask = Pick<
|
||||||
AliveClaudeCodeTask,
|
AliveClaudeCodeTask,
|
||||||
"id" | "status" | "sessionId" | "userMessageId"
|
"id" | "status" | "sessionId"
|
||||||
>;
|
>;
|
||||||
|
|
||||||
export type PermissionRequest = {
|
export type PermissionRequest = {
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { dirname } from "node:path";
|
||||||
|
|
||||||
export const encodeProjectId = (fullPath: string) => {
|
export const encodeProjectId = (fullPath: string) => {
|
||||||
return Buffer.from(fullPath).toString("base64url");
|
return Buffer.from(fullPath).toString("base64url");
|
||||||
};
|
};
|
||||||
@@ -5,3 +7,7 @@ export const encodeProjectId = (fullPath: string) => {
|
|||||||
export const decodeProjectId = (id: string) => {
|
export const decodeProjectId = (id: string) => {
|
||||||
return Buffer.from(id, "base64url").toString("utf-8");
|
return Buffer.from(id, "base64url").toString("utf-8");
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const encodeProjectIdFromSessionFilePath = (sessionFilePath: string) => {
|
||||||
|
return encodeProjectId(dirname(sessionFilePath));
|
||||||
|
};
|
||||||
|
|||||||
@@ -3,15 +3,15 @@ import { readdir, readFile } from "node:fs/promises";
|
|||||||
import { basename, resolve } from "node:path";
|
import { basename, resolve } from "node:path";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { FileCacheStorage } from "../../lib/storage/FileCacheStorage";
|
import { FileCacheStorage } from "../../lib/storage/FileCacheStorage";
|
||||||
|
import { InMemoryCacheStorage } from "../../lib/storage/InMemoryCacheStorage";
|
||||||
import { parseJsonl } from "../parseJsonl";
|
import { parseJsonl } from "../parseJsonl";
|
||||||
import type { ProjectMeta } from "../types";
|
import type { ProjectMeta } from "../types";
|
||||||
import { decodeProjectId } from "./id";
|
import { decodeProjectId } from "./id";
|
||||||
import { InMemoryCacheStorage } from "../../lib/storage/InMemoryCacheStorage";
|
|
||||||
|
|
||||||
class ProjectMetaStorage {
|
class ProjectMetaStorage {
|
||||||
private projectPathCache = FileCacheStorage.load(
|
private projectPathCache = FileCacheStorage.load(
|
||||||
"project-path-cache",
|
"project-path-cache",
|
||||||
z.string().nullable()
|
z.string().nullable(),
|
||||||
);
|
);
|
||||||
private projectMetaCache = new InMemoryCacheStorage<ProjectMeta>();
|
private projectMetaCache = new InMemoryCacheStorage<ProjectMeta>();
|
||||||
|
|
||||||
@@ -31,7 +31,7 @@ class ProjectMetaStorage {
|
|||||||
({
|
({
|
||||||
fullPath: resolve(claudeProjectPath, d.name),
|
fullPath: resolve(claudeProjectPath, d.name),
|
||||||
stats: statSync(resolve(claudeProjectPath, d.name)),
|
stats: statSync(resolve(claudeProjectPath, d.name)),
|
||||||
} as const)
|
}) as const,
|
||||||
)
|
)
|
||||||
.sort((a, b) => {
|
.sort((a, b) => {
|
||||||
return a.stats.mtime.getTime() - b.stats.mtime.getTime();
|
return a.stats.mtime.getTime() - b.stats.mtime.getTime();
|
||||||
@@ -70,7 +70,7 @@ class ProjectMetaStorage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async extractProjectPathFromJsonl(
|
private async extractProjectPathFromJsonl(
|
||||||
filePath: string
|
filePath: string,
|
||||||
): Promise<string | null> {
|
): Promise<string | null> {
|
||||||
const cached = this.projectPathCache.get(filePath);
|
const cached = this.projectPathCache.get(filePath);
|
||||||
if (cached !== undefined) {
|
if (cached !== undefined) {
|
||||||
|
|||||||
34
src/server/service/session/PredictSessionsDatabase.ts
Normal file
34
src/server/service/session/PredictSessionsDatabase.ts
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import { encodeProjectIdFromSessionFilePath } from "../project/id";
|
||||||
|
import type { Session, SessionDetail } from "../types";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* For interactively experience, handle sessions not already persisted to the filesystem.
|
||||||
|
*/
|
||||||
|
class PredictSessionsDatabase {
|
||||||
|
private storage = new Map<string, SessionDetail>();
|
||||||
|
|
||||||
|
public getPredictSessions(projectId: string): Session[] {
|
||||||
|
return Array.from(this.storage.values()).filter(
|
||||||
|
({ jsonlFilePath }) =>
|
||||||
|
encodeProjectIdFromSessionFilePath(jsonlFilePath) === projectId,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
||||||
|
this.storage.set(session.id, session);
|
||||||
|
}
|
||||||
|
|
||||||
|
public deletePredictSession(sessionId: string) {
|
||||||
|
this.storage.delete(sessionId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const predictSessionsDatabase = new PredictSessionsDatabase();
|
||||||
@@ -1,9 +1,11 @@
|
|||||||
|
import { existsSync } from "node:fs";
|
||||||
import { readdir, readFile } from "node:fs/promises";
|
import { readdir, readFile } from "node:fs/promises";
|
||||||
import { resolve } from "node:path";
|
import { resolve } from "node:path";
|
||||||
import { parseJsonl } from "../parseJsonl";
|
import { parseJsonl } from "../parseJsonl";
|
||||||
import { decodeProjectId } from "../project/id";
|
import { decodeProjectId } from "../project/id";
|
||||||
import type { Session, SessionDetail } from "../types";
|
import type { Session, SessionDetail } from "../types";
|
||||||
import { decodeSessionId, encodeSessionId } from "./id";
|
import { decodeSessionId, encodeSessionId } from "./id";
|
||||||
|
import { predictSessionsDatabase } from "./PredictSessionsDatabase";
|
||||||
import { sessionMetaStorage } from "./sessionMetaStorage";
|
import { sessionMetaStorage } from "./sessionMetaStorage";
|
||||||
|
|
||||||
const getTime = (date: string | null) => {
|
const getTime = (date: string | null) => {
|
||||||
@@ -19,6 +21,17 @@ export class SessionRepository {
|
|||||||
session: SessionDetail;
|
session: SessionDetail;
|
||||||
}> {
|
}> {
|
||||||
const sessionPath = decodeSessionId(projectId, sessionId);
|
const sessionPath = decodeSessionId(projectId, sessionId);
|
||||||
|
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");
|
const content = await readFile(sessionPath, "utf-8");
|
||||||
|
|
||||||
const conversations = parseJsonl(content);
|
const conversations = parseJsonl(content);
|
||||||
@@ -53,9 +66,16 @@ export class SessionRepository {
|
|||||||
),
|
),
|
||||||
})),
|
})),
|
||||||
);
|
);
|
||||||
|
const sessionMap = new Map<string, Session>(
|
||||||
|
sessions.map((session) => [session.id, session]),
|
||||||
|
);
|
||||||
|
|
||||||
|
const predictSessions = predictSessionsDatabase
|
||||||
|
.getPredictSessions(projectId)
|
||||||
|
.filter((session) => !sessionMap.has(session.id));
|
||||||
|
|
||||||
return {
|
return {
|
||||||
sessions: sessions.sort((a, b) => {
|
sessions: [...predictSessions, ...sessions].sort((a, b) => {
|
||||||
return (
|
return (
|
||||||
getTime(b.meta.lastModifiedAt) - getTime(a.meta.lastModifiedAt)
|
getTime(b.meta.lastModifiedAt) - getTime(a.meta.lastModifiedAt)
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,16 +1,15 @@
|
|||||||
import { statSync } from "node:fs";
|
import { statSync } from "node:fs";
|
||||||
import { readFile } from "node:fs/promises";
|
import { readFile } from "node:fs/promises";
|
||||||
import { FileCacheStorage } from "../../lib/storage/FileCacheStorage";
|
import { FileCacheStorage } from "../../lib/storage/FileCacheStorage";
|
||||||
|
import { InMemoryCacheStorage } from "../../lib/storage/InMemoryCacheStorage";
|
||||||
import {
|
import {
|
||||||
type ParsedCommand,
|
type ParsedCommand,
|
||||||
parseCommandXml,
|
parseCommandXml,
|
||||||
parsedCommandSchema,
|
parsedCommandSchema,
|
||||||
} from "../parseCommandXml";
|
} from "../parseCommandXml";
|
||||||
import { parseJsonl } from "../parseJsonl";
|
import { parseJsonl } from "../parseJsonl";
|
||||||
import { sessionMetaSchema } from "../schema";
|
|
||||||
import type { SessionMeta } from "../types";
|
import type { SessionMeta } from "../types";
|
||||||
import { decodeSessionId } from "./id";
|
import { decodeSessionId } from "./id";
|
||||||
import { InMemoryCacheStorage } from "../../lib/storage/InMemoryCacheStorage";
|
|
||||||
|
|
||||||
const ignoreCommands = [
|
const ignoreCommands = [
|
||||||
"/clear",
|
"/clear",
|
||||||
@@ -24,13 +23,13 @@ const ignoreCommands = [
|
|||||||
class SessionMetaStorage {
|
class SessionMetaStorage {
|
||||||
private firstCommandCache = FileCacheStorage.load(
|
private firstCommandCache = FileCacheStorage.load(
|
||||||
"first-command-cache",
|
"first-command-cache",
|
||||||
parsedCommandSchema
|
parsedCommandSchema,
|
||||||
);
|
);
|
||||||
private sessionMetaCache = new InMemoryCacheStorage<SessionMeta>();
|
private sessionMetaCache = new InMemoryCacheStorage<SessionMeta>();
|
||||||
|
|
||||||
public async getSessionMeta(
|
public async getSessionMeta(
|
||||||
projectId: string,
|
projectId: string,
|
||||||
sessionId: string
|
sessionId: string,
|
||||||
): Promise<SessionMeta> {
|
): Promise<SessionMeta> {
|
||||||
const cached = this.sessionMetaCache.get(sessionId);
|
const cached = this.sessionMetaCache.get(sessionId);
|
||||||
if (cached !== undefined) {
|
if (cached !== undefined) {
|
||||||
@@ -60,7 +59,7 @@ class SessionMetaStorage {
|
|||||||
|
|
||||||
private getFirstCommand = (
|
private getFirstCommand = (
|
||||||
jsonlFilePath: string,
|
jsonlFilePath: string,
|
||||||
lines: string[]
|
lines: string[],
|
||||||
): ParsedCommand | null => {
|
): ParsedCommand | null => {
|
||||||
const cached = this.firstCommandCache.get(jsonlFilePath);
|
const cached = this.firstCommandCache.get(jsonlFilePath);
|
||||||
if (cached !== undefined) {
|
if (cached !== undefined) {
|
||||||
|
|||||||
Reference in New Issue
Block a user