mirror of
https://github.com/aljazceru/claude-code-viewer.git
synced 2025-12-21 15:24:20 +01:00
perf: added pagination for session in order to improve large project's performance issue
This commit is contained in:
@@ -25,24 +25,30 @@ import {
|
|||||||
CollapsibleContent,
|
CollapsibleContent,
|
||||||
CollapsibleTrigger,
|
CollapsibleTrigger,
|
||||||
} from "@/components/ui/collapsible";
|
} from "@/components/ui/collapsible";
|
||||||
import { projectDetailQuery } from "../../../../lib/api/queries";
|
|
||||||
import { useConfig } from "../../../hooks/useConfig";
|
import { useConfig } from "../../../hooks/useConfig";
|
||||||
import { useProject } from "../hooks/useProject";
|
import { useProject } from "../hooks/useProject";
|
||||||
import { firstCommandToTitle } from "../services/firstCommandToTitle";
|
import { firstCommandToTitle } from "../services/firstCommandToTitle";
|
||||||
import { NewChatModal } from "./newChat/NewChatModal";
|
import { NewChatModal } from "./newChat/NewChatModal";
|
||||||
|
|
||||||
export const ProjectPageContent = ({ projectId }: { projectId: string }) => {
|
export const ProjectPageContent = ({ projectId }: { projectId: string }) => {
|
||||||
const {
|
const { data, fetchNextPage, hasNextPage, isFetchingNextPage } =
|
||||||
data: { project, sessions },
|
useProject(projectId);
|
||||||
} = useProject(projectId);
|
|
||||||
const { config } = useConfig();
|
const { config } = useConfig();
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const [isSettingsOpen, setIsSettingsOpen] = useState(false);
|
const [isSettingsOpen, setIsSettingsOpen] = useState(false);
|
||||||
|
|
||||||
|
// Flatten all pages to get project and sessions
|
||||||
|
const project = data.pages.at(0)?.project;
|
||||||
|
const sessions = data.pages.flatMap((page) => page.sessions);
|
||||||
|
|
||||||
|
if (!project) {
|
||||||
|
throw new Error("Unreachable: Project must be defined.");
|
||||||
|
}
|
||||||
|
|
||||||
// biome-ignore lint/correctness/useExhaustiveDependencies: invalidate when config changed
|
// biome-ignore lint/correctness/useExhaustiveDependencies: invalidate when config changed
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
void queryClient.invalidateQueries({
|
void queryClient.invalidateQueries({
|
||||||
queryKey: projectDetailQuery(projectId).queryKey,
|
queryKey: ["projects", projectId],
|
||||||
});
|
});
|
||||||
}, [config.hideNoUserMessageSession, config.unifySameTitleSession]);
|
}, [config.hideNoUserMessageSession, config.unifySameTitleSession]);
|
||||||
|
|
||||||
@@ -170,10 +176,8 @@ export const ProjectPageContent = ({ projectId }: { projectId: string }) => {
|
|||||||
</p>
|
</p>
|
||||||
<p className="text-sm text-muted-foreground">
|
<p className="text-sm text-muted-foreground">
|
||||||
Last modified:{" "}
|
Last modified:{" "}
|
||||||
{session.meta.lastModifiedAt
|
{session.lastModifiedAt
|
||||||
? new Date(
|
? new Date(session.lastModifiedAt).toLocaleDateString()
|
||||||
session.meta.lastModifiedAt,
|
|
||||||
).toLocaleDateString()
|
|
||||||
: ""}
|
: ""}
|
||||||
</p>
|
</p>
|
||||||
<p className="text-xs text-muted-foreground font-mono">
|
<p className="text-xs text-muted-foreground font-mono">
|
||||||
@@ -195,6 +199,21 @@ export const ProjectPageContent = ({ projectId }: { projectId: string }) => {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Load More Button */}
|
||||||
|
{sessions.length > 0 && hasNextPage && (
|
||||||
|
<div className="mt-6 flex justify-center">
|
||||||
|
<Button
|
||||||
|
onClick={() => fetchNextPage()}
|
||||||
|
disabled={isFetchingNextPage}
|
||||||
|
variant="outline"
|
||||||
|
size="lg"
|
||||||
|
className="min-w-[200px]"
|
||||||
|
>
|
||||||
|
{isFetchingNextPage ? "Loading..." : "Load More"}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</section>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,10 +1,14 @@
|
|||||||
import { useSuspenseQuery } from "@tanstack/react-query";
|
import { useSuspenseInfiniteQuery } from "@tanstack/react-query";
|
||||||
import { projectDetailQuery } from "../../../../lib/api/queries";
|
import { projectDetailQuery } from "../../../../lib/api/queries";
|
||||||
|
|
||||||
export const useProject = (projectId: string) => {
|
export const useProject = (projectId: string) => {
|
||||||
return useSuspenseQuery({
|
return useSuspenseInfiniteQuery({
|
||||||
queryKey: projectDetailQuery(projectId).queryKey,
|
queryKey: ["projects", projectId],
|
||||||
queryFn: projectDetailQuery(projectId).queryFn,
|
queryFn: async ({ pageParam }) => {
|
||||||
|
return await projectDetailQuery(projectId, pageParam).queryFn();
|
||||||
|
},
|
||||||
|
initialPageParam: undefined as string | undefined,
|
||||||
|
getNextPageParam: (lastPage) => lastPage.nextCursor,
|
||||||
refetchOnReconnect: true,
|
refetchOnReconnect: true,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -15,9 +15,12 @@ export default async function ProjectPage({ params }: ProjectPageProps) {
|
|||||||
|
|
||||||
const queryClient = new QueryClient();
|
const queryClient = new QueryClient();
|
||||||
|
|
||||||
await queryClient.prefetchQuery({
|
await queryClient.prefetchInfiniteQuery({
|
||||||
queryKey: projectDetailQuery(projectId).queryKey,
|
queryKey: ["projects", projectId],
|
||||||
queryFn: projectDetailQuery(projectId).queryFn,
|
queryFn: async ({ pageParam }) => {
|
||||||
|
return await projectDetailQuery(projectId, pageParam).queryFn();
|
||||||
|
},
|
||||||
|
initialPageParam: undefined as string | undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -35,7 +35,9 @@ export const SessionPageContent: FC<{
|
|||||||
projectId,
|
projectId,
|
||||||
sessionId,
|
sessionId,
|
||||||
);
|
);
|
||||||
const { data: project } = useProject(projectId);
|
const { data: projectData } = useProject(projectId);
|
||||||
|
// biome-ignore lint/style/noNonNullAssertion: useSuspenseInfiniteQuery guarantees at least one page
|
||||||
|
const project = projectData.pages[0]!.project;
|
||||||
|
|
||||||
const abortTask = useMutation({
|
const abortTask = useMutation({
|
||||||
mutationFn: async (sessionId: string) => {
|
mutationFn: async (sessionId: string) => {
|
||||||
@@ -111,7 +113,7 @@ export const SessionPageContent: FC<{
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="px-1 sm:px-5 flex flex-wrap items-center gap-1 sm:gap-2">
|
<div className="px-1 sm:px-5 flex flex-wrap items-center gap-1 sm:gap-2">
|
||||||
{project?.project.claudeProjectPath && (
|
{project?.claudeProjectPath && (
|
||||||
<Link
|
<Link
|
||||||
href={`/projects/${projectId}`}
|
href={`/projects/${projectId}`}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
@@ -122,8 +124,7 @@ export const SessionPageContent: FC<{
|
|||||||
className="h-6 sm:h-8 text-xs sm:text-sm flex items-center hover:bg-blue-50/60 hover:border-blue-300/60 hover:shadow-sm transition-all duration-200 cursor-pointer"
|
className="h-6 sm:h-8 text-xs sm:text-sm flex items-center hover:bg-blue-50/60 hover:border-blue-300/60 hover:shadow-sm transition-all duration-200 cursor-pointer"
|
||||||
>
|
>
|
||||||
<ExternalLinkIcon className="w-3 h-3 sm:w-4 sm:h-4 mr-1" />
|
<ExternalLinkIcon className="w-3 h-3 sm:w-4 sm:h-4 mr-1" />
|
||||||
{project.project.meta.projectPath ??
|
{project.meta.projectPath ?? project.claudeProjectPath}
|
||||||
project.project.claudeProjectPath}
|
|
||||||
</Badge>
|
</Badge>
|
||||||
</Link>
|
</Link>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -24,8 +24,12 @@ export const MobileSidebar: FC<MobileSidebarProps> = ({
|
|||||||
onClose,
|
onClose,
|
||||||
}) => {
|
}) => {
|
||||||
const {
|
const {
|
||||||
data: { sessions },
|
data: projectData,
|
||||||
|
fetchNextPage,
|
||||||
|
hasNextPage,
|
||||||
|
isFetchingNextPage,
|
||||||
} = useProject(projectId);
|
} = useProject(projectId);
|
||||||
|
const sessions = projectData.pages.flatMap((page) => page.sessions);
|
||||||
const [activeTab, setActiveTab] = useState<"sessions" | "mcp" | "settings">(
|
const [activeTab, setActiveTab] = useState<"sessions" | "mcp" | "settings">(
|
||||||
"sessions",
|
"sessions",
|
||||||
);
|
);
|
||||||
@@ -71,9 +75,15 @@ export const MobileSidebar: FC<MobileSidebarProps> = ({
|
|||||||
case "sessions":
|
case "sessions":
|
||||||
return (
|
return (
|
||||||
<SessionsTab
|
<SessionsTab
|
||||||
sessions={sessions}
|
sessions={sessions.map((session) => ({
|
||||||
|
...session,
|
||||||
|
lastModifiedAt: new Date(session.lastModifiedAt),
|
||||||
|
}))}
|
||||||
currentSessionId={currentSessionId}
|
currentSessionId={currentSessionId}
|
||||||
projectId={projectId}
|
projectId={projectId}
|
||||||
|
hasNextPage={hasNextPage}
|
||||||
|
isFetchingNextPage={isFetchingNextPage}
|
||||||
|
onLoadMore={() => fetchNextPage()}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
case "mcp":
|
case "mcp":
|
||||||
|
|||||||
@@ -30,8 +30,12 @@ export const SessionSidebar: FC<{
|
|||||||
}) => {
|
}) => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const {
|
const {
|
||||||
data: { sessions },
|
data: projectData,
|
||||||
|
fetchNextPage,
|
||||||
|
hasNextPage,
|
||||||
|
isFetchingNextPage,
|
||||||
} = useProject(projectId);
|
} = useProject(projectId);
|
||||||
|
const sessions = projectData.pages.flatMap((page) => page.sessions);
|
||||||
const [activeTab, setActiveTab] = useState<"sessions" | "mcp" | "settings">(
|
const [activeTab, setActiveTab] = useState<"sessions" | "mcp" | "settings">(
|
||||||
"sessions",
|
"sessions",
|
||||||
);
|
);
|
||||||
@@ -53,9 +57,15 @@ export const SessionSidebar: FC<{
|
|||||||
case "sessions":
|
case "sessions":
|
||||||
return (
|
return (
|
||||||
<SessionsTab
|
<SessionsTab
|
||||||
sessions={sessions}
|
sessions={sessions.map((session) => ({
|
||||||
|
...session,
|
||||||
|
lastModifiedAt: new Date(session.lastModifiedAt),
|
||||||
|
}))}
|
||||||
currentSessionId={currentSessionId}
|
currentSessionId={currentSessionId}
|
||||||
projectId={projectId}
|
projectId={projectId}
|
||||||
|
hasNextPage={hasNextPage}
|
||||||
|
isFetchingNextPage={isFetchingNextPage}
|
||||||
|
onLoadMore={() => fetchNextPage()}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
case "mcp":
|
case "mcp":
|
||||||
|
|||||||
@@ -16,7 +16,17 @@ export const SessionsTab: FC<{
|
|||||||
sessions: Session[];
|
sessions: Session[];
|
||||||
currentSessionId: string;
|
currentSessionId: string;
|
||||||
projectId: string;
|
projectId: string;
|
||||||
}> = ({ sessions, currentSessionId, projectId }) => {
|
hasNextPage?: boolean;
|
||||||
|
isFetchingNextPage?: boolean;
|
||||||
|
onLoadMore?: () => void;
|
||||||
|
}> = ({
|
||||||
|
sessions,
|
||||||
|
currentSessionId,
|
||||||
|
projectId,
|
||||||
|
hasNextPage,
|
||||||
|
isFetchingNextPage,
|
||||||
|
onLoadMore,
|
||||||
|
}) => {
|
||||||
const aliveTasks = useAtomValue(aliveTasksAtom);
|
const aliveTasks = useAtomValue(aliveTasksAtom);
|
||||||
|
|
||||||
// Sort sessions: Running > Paused > Others, then by lastModifiedAt (newest first)
|
// Sort sessions: Running > Paused > Others, then by lastModifiedAt (newest first)
|
||||||
@@ -43,12 +53,8 @@ export const SessionsTab: FC<{
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Then sort by lastModifiedAt (newest first)
|
// Then sort by lastModifiedAt (newest first)
|
||||||
const aTime = a.meta.lastModifiedAt
|
const aTime = a.lastModifiedAt ? a.lastModifiedAt.getTime() : 0;
|
||||||
? new Date(a.meta.lastModifiedAt).getTime()
|
const bTime = b.lastModifiedAt ? b.lastModifiedAt.getTime() : 0;
|
||||||
: 0;
|
|
||||||
const bTime = b.meta.lastModifiedAt
|
|
||||||
? new Date(b.meta.lastModifiedAt).getTime()
|
|
||||||
: 0;
|
|
||||||
return bTime - aTime;
|
return bTime - aTime;
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -121,9 +127,9 @@ export const SessionsTab: FC<{
|
|||||||
<MessageSquareIcon className="w-3 h-3" />
|
<MessageSquareIcon className="w-3 h-3" />
|
||||||
<span>{session.meta.messageCount}</span>
|
<span>{session.meta.messageCount}</span>
|
||||||
</div>
|
</div>
|
||||||
{session.meta.lastModifiedAt && (
|
{session.lastModifiedAt && (
|
||||||
<span className="text-xs text-sidebar-foreground/60">
|
<span className="text-xs text-sidebar-foreground/60">
|
||||||
{new Date(session.meta.lastModifiedAt).toLocaleDateString(
|
{new Date(session.lastModifiedAt).toLocaleDateString(
|
||||||
"en-US",
|
"en-US",
|
||||||
{
|
{
|
||||||
month: "short",
|
month: "short",
|
||||||
@@ -137,6 +143,21 @@ export const SessionsTab: FC<{
|
|||||||
</Link>
|
</Link>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|
||||||
|
{/* Load More Button */}
|
||||||
|
{hasNextPage && onLoadMore && (
|
||||||
|
<div className="p-2">
|
||||||
|
<Button
|
||||||
|
onClick={onLoadMore}
|
||||||
|
disabled={isFetchingNextPage}
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
className="w-full"
|
||||||
|
>
|
||||||
|
{isFetchingNextPage ? "Loading..." : "Load More"}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -49,8 +49,8 @@ export const ProjectList: FC = () => {
|
|||||||
<CardContent className="space-y-2">
|
<CardContent className="space-y-2">
|
||||||
<p className="text-sm text-muted-foreground">
|
<p className="text-sm text-muted-foreground">
|
||||||
Last modified:{" "}
|
Last modified:{" "}
|
||||||
{project.meta.lastModifiedAt
|
{project.lastModifiedAt
|
||||||
? new Date(project.meta.lastModifiedAt).toLocaleDateString()
|
? new Date(project.lastModifiedAt).toLocaleDateString()
|
||||||
: ""}
|
: ""}
|
||||||
</p>
|
</p>
|
||||||
<p className="text-xs text-muted-foreground">
|
<p className="text-xs text-muted-foreground">
|
||||||
|
|||||||
@@ -16,12 +16,15 @@ export const projectListQuery = {
|
|||||||
},
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const projectDetailQuery = (projectId: string) =>
|
export const projectDetailQuery = (projectId: string, cursor?: string) =>
|
||||||
({
|
({
|
||||||
queryKey: ["projects", projectId],
|
queryKey: cursor
|
||||||
|
? ["projects", projectId, cursor]
|
||||||
|
: ["projects", projectId],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
const response = await honoClient.api.projects[":projectId"].$get({
|
const response = await honoClient.api.projects[":projectId"].$get({
|
||||||
param: { projectId },
|
param: { projectId },
|
||||||
|
query: { cursor },
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
|
import prexit from "prexit";
|
||||||
|
import { claudeCodeTaskController } from "../service/claude-code/ClaudeCodeTaskController";
|
||||||
import { eventBus } from "../service/events/EventBus";
|
import { eventBus } from "../service/events/EventBus";
|
||||||
import { fileWatcher } from "../service/events/fileWatcher";
|
import { fileWatcher } from "../service/events/fileWatcher";
|
||||||
|
import type { InternalEventDeclaration } from "../service/events/InternalEventDeclaration";
|
||||||
import type { ProjectRepository } from "../service/project/ProjectRepository";
|
import type { ProjectRepository } from "../service/project/ProjectRepository";
|
||||||
import { projectMetaStorage } from "../service/project/projectMetaStorage";
|
import { projectMetaStorage } from "../service/project/projectMetaStorage";
|
||||||
import type { SessionRepository } from "../service/session/SessionRepository";
|
import type { SessionRepository } from "../service/session/SessionRepository";
|
||||||
@@ -11,14 +14,18 @@ export const initialize = async (deps: {
|
|||||||
}): Promise<void> => {
|
}): Promise<void> => {
|
||||||
fileWatcher.startWatching();
|
fileWatcher.startWatching();
|
||||||
|
|
||||||
setInterval(() => {
|
const intervalId = setInterval(() => {
|
||||||
eventBus.emit("heartbeat", {});
|
eventBus.emit("heartbeat", {});
|
||||||
}, 10 * 1000);
|
}, 10 * 1000);
|
||||||
|
|
||||||
eventBus.on("sessionChanged", (event) => {
|
const onSessionChanged = (
|
||||||
|
event: InternalEventDeclaration["sessionChanged"],
|
||||||
|
) => {
|
||||||
projectMetaStorage.invalidateProject(event.projectId);
|
projectMetaStorage.invalidateProject(event.projectId);
|
||||||
sessionMetaStorage.invalidateSession(event.projectId, event.sessionId);
|
sessionMetaStorage.invalidateSession(event.projectId, event.sessionId);
|
||||||
});
|
};
|
||||||
|
|
||||||
|
eventBus.on("sessionChanged", onSessionChanged);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
console.log("Initializing projects cache");
|
console.log("Initializing projects cache");
|
||||||
@@ -38,4 +45,11 @@ export const initialize = async (deps: {
|
|||||||
} catch {
|
} catch {
|
||||||
// do nothing
|
// do nothing
|
||||||
}
|
}
|
||||||
|
|
||||||
|
prexit(() => {
|
||||||
|
clearInterval(intervalId);
|
||||||
|
eventBus.off("sessionChanged", onSessionChanged);
|
||||||
|
fileWatcher.stop();
|
||||||
|
claudeCodeTaskController.abortAllTasks();
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -4,9 +4,9 @@ import { zValidator } from "@hono/zod-validator";
|
|||||||
import { setCookie } from "hono/cookie";
|
import { setCookie } from "hono/cookie";
|
||||||
import { streamSSE } from "hono/streaming";
|
import { streamSSE } from "hono/streaming";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { type Config, configSchema } from "../config/config";
|
import { configSchema } from "../config/config";
|
||||||
import { env } from "../lib/env";
|
import { env } from "../lib/env";
|
||||||
import { ClaudeCodeTaskController } from "../service/claude-code/ClaudeCodeTaskController";
|
import { claudeCodeTaskController } from "../service/claude-code/ClaudeCodeTaskController";
|
||||||
import type { SerializableAliveTask } from "../service/claude-code/types";
|
import type { SerializableAliveTask } from "../service/claude-code/types";
|
||||||
import { adaptInternalEventToSSE } from "../service/events/adaptInternalEventToSSE";
|
import { adaptInternalEventToSSE } from "../service/events/adaptInternalEventToSSE";
|
||||||
import { eventBus } from "../service/events/EventBus";
|
import { eventBus } from "../service/events/EventBus";
|
||||||
@@ -25,16 +25,6 @@ import { initialize } from "./initialize";
|
|||||||
import { configMiddleware } from "./middleware/config.middleware";
|
import { configMiddleware } from "./middleware/config.middleware";
|
||||||
|
|
||||||
export const routes = async (app: HonoAppType) => {
|
export const routes = async (app: HonoAppType) => {
|
||||||
let taskController: ClaudeCodeTaskController | null = null;
|
|
||||||
const getTaskController = (config: Config) => {
|
|
||||||
if (!taskController) {
|
|
||||||
taskController = new ClaudeCodeTaskController(config);
|
|
||||||
} else {
|
|
||||||
taskController.updateConfig(config);
|
|
||||||
}
|
|
||||||
return taskController;
|
|
||||||
};
|
|
||||||
|
|
||||||
const sessionRepository = new SessionRepository();
|
const sessionRepository = new SessionRepository();
|
||||||
const projectRepository = new ProjectRepository();
|
const projectRepository = new ProjectRepository();
|
||||||
|
|
||||||
@@ -49,6 +39,10 @@ export const routes = async (app: HonoAppType) => {
|
|||||||
app
|
app
|
||||||
// middleware
|
// middleware
|
||||||
.use(configMiddleware)
|
.use(configMiddleware)
|
||||||
|
.use(async (c, next) => {
|
||||||
|
claudeCodeTaskController.updateConfig(c.get("config"));
|
||||||
|
await next();
|
||||||
|
})
|
||||||
|
|
||||||
// routes
|
// routes
|
||||||
.get("/config", async (c) => {
|
.get("/config", async (c) => {
|
||||||
@@ -72,12 +66,18 @@ export const routes = async (app: HonoAppType) => {
|
|||||||
return c.json({ projects });
|
return c.json({ projects });
|
||||||
})
|
})
|
||||||
|
|
||||||
.get("/projects/:projectId", async (c) => {
|
.get(
|
||||||
|
"/projects/:projectId",
|
||||||
|
zValidator("query", z.object({ cursor: z.string().optional() })),
|
||||||
|
async (c) => {
|
||||||
const { projectId } = c.req.param();
|
const { projectId } = c.req.param();
|
||||||
|
const { cursor } = c.req.valid("query");
|
||||||
|
|
||||||
const [{ project }, { sessions }] = await Promise.all([
|
const [{ project }, { sessions, nextCursor }] = await Promise.all([
|
||||||
projectRepository.getProject(projectId),
|
projectRepository.getProject(projectId),
|
||||||
sessionRepository.getSessions(projectId).then(({ sessions }) => {
|
sessionRepository
|
||||||
|
.getSessions(projectId, { cursor })
|
||||||
|
.then(({ sessions }) => {
|
||||||
let filteredSessions = sessions;
|
let filteredSessions = sessions;
|
||||||
|
|
||||||
// Filter sessions based on hideNoUserMessageSession setting
|
// Filter sessions based on hideNoUserMessageSession setting
|
||||||
@@ -119,18 +119,18 @@ export const routes = async (app: HonoAppType) => {
|
|||||||
if (existingSession) {
|
if (existingSession) {
|
||||||
// Keep the session with the latest modification date
|
// Keep the session with the latest modification date
|
||||||
if (
|
if (
|
||||||
session.meta.lastModifiedAt &&
|
session.lastModifiedAt &&
|
||||||
existingSession.meta.lastModifiedAt
|
existingSession.lastModifiedAt
|
||||||
) {
|
) {
|
||||||
if (
|
if (
|
||||||
new Date(session.meta.lastModifiedAt) >
|
session.lastModifiedAt >
|
||||||
new Date(existingSession.meta.lastModifiedAt)
|
existingSession.lastModifiedAt
|
||||||
) {
|
) {
|
||||||
sessionMap.set(title, session);
|
sessionMap.set(title, session);
|
||||||
}
|
}
|
||||||
} else if (
|
} else if (
|
||||||
session.meta.lastModifiedAt &&
|
session.lastModifiedAt &&
|
||||||
!existingSession.meta.lastModifiedAt
|
!existingSession.lastModifiedAt
|
||||||
) {
|
) {
|
||||||
sessionMap.set(title, session);
|
sessionMap.set(title, session);
|
||||||
}
|
}
|
||||||
@@ -145,12 +145,14 @@ export const routes = async (app: HonoAppType) => {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
sessions: filteredSessions,
|
sessions: filteredSessions,
|
||||||
|
nextCursor: sessions.at(-1)?.id,
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
] as const);
|
] as const);
|
||||||
|
|
||||||
return c.json({ project, sessions });
|
return c.json({ project, sessions, nextCursor });
|
||||||
})
|
},
|
||||||
|
)
|
||||||
|
|
||||||
.get("/projects/:projectId/sessions/:sessionId", async (c) => {
|
.get("/projects/:projectId/sessions/:sessionId", async (c) => {
|
||||||
const { projectId, sessionId } = c.req.param();
|
const { projectId, sessionId } = c.req.param();
|
||||||
@@ -324,9 +326,7 @@ export const routes = async (app: HonoAppType) => {
|
|||||||
return c.json({ error: "Project path not found" }, 400);
|
return c.json({ error: "Project path not found" }, 400);
|
||||||
}
|
}
|
||||||
|
|
||||||
const task = await getTaskController(
|
const task = await claudeCodeTaskController.startOrContinueTask(
|
||||||
c.get("config"),
|
|
||||||
).startOrContinueTask(
|
|
||||||
{
|
{
|
||||||
projectId,
|
projectId,
|
||||||
cwd: project.meta.projectPath,
|
cwd: project.meta.projectPath,
|
||||||
@@ -358,9 +358,7 @@ export const routes = async (app: HonoAppType) => {
|
|||||||
return c.json({ error: "Project path not found" }, 400);
|
return c.json({ error: "Project path not found" }, 400);
|
||||||
}
|
}
|
||||||
|
|
||||||
const task = await getTaskController(
|
const task = await claudeCodeTaskController.startOrContinueTask(
|
||||||
c.get("config"),
|
|
||||||
).startOrContinueTask(
|
|
||||||
{
|
{
|
||||||
projectId,
|
projectId,
|
||||||
sessionId,
|
sessionId,
|
||||||
@@ -378,7 +376,7 @@ export const routes = async (app: HonoAppType) => {
|
|||||||
|
|
||||||
.get("/tasks/alive", async (c) => {
|
.get("/tasks/alive", async (c) => {
|
||||||
return c.json({
|
return c.json({
|
||||||
aliveTasks: getTaskController(c.get("config")).aliveTasks.map(
|
aliveTasks: claudeCodeTaskController.aliveTasks.map(
|
||||||
(task): SerializableAliveTask => ({
|
(task): SerializableAliveTask => ({
|
||||||
id: task.id,
|
id: task.id,
|
||||||
status: task.status,
|
status: task.status,
|
||||||
@@ -393,7 +391,7 @@ export const routes = async (app: HonoAppType) => {
|
|||||||
zValidator("json", z.object({ sessionId: z.string() })),
|
zValidator("json", z.object({ sessionId: z.string() })),
|
||||||
async (c) => {
|
async (c) => {
|
||||||
const { sessionId } = c.req.valid("json");
|
const { sessionId } = c.req.valid("json");
|
||||||
getTaskController(c.get("config")).abortTask(sessionId);
|
claudeCodeTaskController.abortTask(sessionId);
|
||||||
return c.json({ message: "Task aborted" });
|
return c.json({ message: "Task aborted" });
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -409,7 +407,7 @@ export const routes = async (app: HonoAppType) => {
|
|||||||
),
|
),
|
||||||
async (c) => {
|
async (c) => {
|
||||||
const permissionResponse = c.req.valid("json");
|
const permissionResponse = c.req.valid("json");
|
||||||
getTaskController(c.get("config")).respondToPermissionRequest(
|
claudeCodeTaskController.respondToPermissionRequest(
|
||||||
permissionResponse,
|
permissionResponse,
|
||||||
);
|
);
|
||||||
return c.json({ message: "Permission response received" });
|
return c.json({ message: "Permission response received" });
|
||||||
|
|||||||
@@ -41,14 +41,18 @@ export class ClaudeCodeExecutor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public query(prompt: CCQueryPrompt, options: CCQueryOptions) {
|
public query(prompt: CCQueryPrompt, options: CCQueryOptions) {
|
||||||
const { canUseTool, ...baseOptions } = options;
|
const { canUseTool, permissionMode, ...baseOptions } = options;
|
||||||
|
|
||||||
return query({
|
return query({
|
||||||
prompt,
|
prompt,
|
||||||
options: {
|
options: {
|
||||||
pathToClaudeCodeExecutable: this.pathToClaudeCodeExecutable,
|
pathToClaudeCodeExecutable: this.pathToClaudeCodeExecutable,
|
||||||
...baseOptions,
|
...baseOptions,
|
||||||
...(this.availableFeatures.canUseTool ? { canUseTool } : {}),
|
...(this.availableFeatures.canUseTool
|
||||||
|
? { canUseTool, permissionMode }
|
||||||
|
: {
|
||||||
|
permissionMode: "bypassPermissions",
|
||||||
|
}),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
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";
|
||||||
@@ -14,22 +13,21 @@ import type {
|
|||||||
RunningClaudeCodeTask,
|
RunningClaudeCodeTask,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
|
|
||||||
export class ClaudeCodeTaskController {
|
class ClaudeCodeTaskController {
|
||||||
private claudeCode: ClaudeCodeExecutor;
|
private claudeCode: ClaudeCodeExecutor;
|
||||||
private tasks: ClaudeCodeTask[] = [];
|
private tasks: ClaudeCodeTask[] = [];
|
||||||
private config: Config;
|
private config: Config;
|
||||||
private pendingPermissionRequests: Map<string, PermissionRequest> = new Map();
|
private pendingPermissionRequests: Map<string, PermissionRequest> = new Map();
|
||||||
private permissionResponses: Map<string, PermissionResponse> = new Map();
|
private permissionResponses: Map<string, PermissionResponse> = new Map();
|
||||||
|
|
||||||
constructor(config: Config) {
|
constructor() {
|
||||||
this.claudeCode = new ClaudeCodeExecutor();
|
this.claudeCode = new ClaudeCodeExecutor();
|
||||||
this.config = config;
|
this.config = {
|
||||||
|
hideNoUserMessageSession: false,
|
||||||
prexit(() => {
|
unifySameTitleSession: false,
|
||||||
this.aliveTasks.forEach((task) => {
|
enterKeyBehavior: "shift-enter-send",
|
||||||
task.abortController.abort();
|
permissionMode: "default",
|
||||||
});
|
};
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public updateConfig(config: Config) {
|
public updateConfig(config: Config) {
|
||||||
@@ -292,9 +290,9 @@ export class ClaudeCodeTaskController {
|
|||||||
],
|
],
|
||||||
meta: {
|
meta: {
|
||||||
firstCommand: null,
|
firstCommand: null,
|
||||||
lastModifiedAt: new Date().toISOString(),
|
|
||||||
messageCount: 0,
|
messageCount: 0,
|
||||||
},
|
},
|
||||||
|
lastModifiedAt: new Date(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -407,6 +405,12 @@ export class ClaudeCodeTaskController {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public abortAllTasks() {
|
||||||
|
for (const task of this.aliveTasks) {
|
||||||
|
task.abortController.abort();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private upsertExistingTask(task: ClaudeCodeTask) {
|
private upsertExistingTask(task: ClaudeCodeTask) {
|
||||||
const target = this.tasks.find((t) => t.id === task.id);
|
const target = this.tasks.find((t) => t.id === task.id);
|
||||||
|
|
||||||
@@ -425,3 +429,5 @@ export class ClaudeCodeTaskController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const claudeCodeTaskController = new ClaudeCodeTaskController();
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { existsSync } from "node:fs";
|
import { existsSync, statSync } from "node:fs";
|
||||||
import { access, constants, readdir } from "node:fs/promises";
|
import { access, constants, readdir } from "node:fs/promises";
|
||||||
import { resolve } from "node:path";
|
import { resolve } from "node:path";
|
||||||
import { claudeProjectsDirPath } from "../paths";
|
import { claudeProjectsDirPath } from "../paths";
|
||||||
@@ -19,6 +19,7 @@ export class ProjectRepository {
|
|||||||
project: {
|
project: {
|
||||||
id: projectId,
|
id: projectId,
|
||||||
claudeProjectPath: fullPath,
|
claudeProjectPath: fullPath,
|
||||||
|
lastModifiedAt: statSync(fullPath).mtime,
|
||||||
meta,
|
meta,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -50,6 +51,7 @@ export class ProjectRepository {
|
|||||||
return {
|
return {
|
||||||
id,
|
id,
|
||||||
claudeProjectPath: fullPath,
|
claudeProjectPath: fullPath,
|
||||||
|
lastModifiedAt: statSync(fullPath).mtime,
|
||||||
meta: await projectMetaStorage.getProjectMeta(id),
|
meta: await projectMetaStorage.getProjectMeta(id),
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
@@ -58,12 +60,8 @@ export class ProjectRepository {
|
|||||||
return {
|
return {
|
||||||
projects: projects.sort((a, b) => {
|
projects: projects.sort((a, b) => {
|
||||||
return (
|
return (
|
||||||
(b.meta.lastModifiedAt
|
(b.lastModifiedAt ? b.lastModifiedAt.getTime() : 0) -
|
||||||
? new Date(b.meta.lastModifiedAt).getTime()
|
(a.lastModifiedAt ? a.lastModifiedAt.getTime() : 0)
|
||||||
: 0) -
|
|
||||||
(a.meta.lastModifiedAt
|
|
||||||
? new Date(a.meta.lastModifiedAt).getTime()
|
|
||||||
: 0)
|
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -37,8 +37,6 @@ class ProjectMetaStorage {
|
|||||||
return a.stats.mtime.getTime() - b.stats.mtime.getTime();
|
return a.stats.mtime.getTime() - b.stats.mtime.getTime();
|
||||||
});
|
});
|
||||||
|
|
||||||
const lastModifiedUnixTime = files.at(-1)?.stats.mtime.getTime();
|
|
||||||
|
|
||||||
let projectPath: string | null = null;
|
let projectPath: string | null = null;
|
||||||
|
|
||||||
for (const file of files) {
|
for (const file of files) {
|
||||||
@@ -54,9 +52,6 @@ class ProjectMetaStorage {
|
|||||||
const projectMeta: ProjectMeta = {
|
const projectMeta: ProjectMeta = {
|
||||||
projectName: projectPath ? basename(projectPath) : null,
|
projectName: projectPath ? basename(projectPath) : null,
|
||||||
projectPath,
|
projectPath,
|
||||||
lastModifiedAt: lastModifiedUnixTime
|
|
||||||
? new Date(lastModifiedUnixTime).toISOString()
|
|
||||||
: null,
|
|
||||||
sessionCount: files.length,
|
sessionCount: files.length,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -4,12 +4,10 @@ import { parsedCommandSchema } from "./parseCommandXml";
|
|||||||
export const projectMetaSchema = z.object({
|
export const projectMetaSchema = z.object({
|
||||||
projectName: z.string().nullable(),
|
projectName: z.string().nullable(),
|
||||||
projectPath: z.string().nullable(),
|
projectPath: z.string().nullable(),
|
||||||
lastModifiedAt: z.string().nullable(),
|
|
||||||
sessionCount: z.number(),
|
sessionCount: z.number(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const sessionMetaSchema = z.object({
|
export const sessionMetaSchema = z.object({
|
||||||
messageCount: z.number(),
|
messageCount: z.number(),
|
||||||
firstCommand: parsedCommandSchema.nullable(),
|
firstCommand: parsedCommandSchema.nullable(),
|
||||||
lastModifiedAt: z.string().nullable(),
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -14,12 +14,8 @@ class PredictSessionsDatabase {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public getPredictSession(sessionId: string): SessionDetail {
|
public getPredictSession(sessionId: string): SessionDetail | null {
|
||||||
const session = this.storage.get(sessionId);
|
return this.storage.get(sessionId) ?? null;
|
||||||
if (!session) {
|
|
||||||
throw new Error("Session not found");
|
|
||||||
}
|
|
||||||
return session;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public createPredictSession(session: SessionDetail) {
|
public createPredictSession(session: SessionDetail) {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { existsSync } from "node:fs";
|
import { existsSync, statSync } 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";
|
||||||
@@ -8,11 +8,6 @@ import { decodeSessionId, encodeSessionId } from "./id";
|
|||||||
import { predictSessionsDatabase } from "./PredictSessionsDatabase";
|
import { predictSessionsDatabase } from "./PredictSessionsDatabase";
|
||||||
import { sessionMetaStorage } from "./sessionMetaStorage";
|
import { sessionMetaStorage } from "./sessionMetaStorage";
|
||||||
|
|
||||||
const getTime = (date: string | null) => {
|
|
||||||
if (date === null) return 0;
|
|
||||||
return new Date(date).getTime();
|
|
||||||
};
|
|
||||||
|
|
||||||
export class SessionRepository {
|
export class SessionRepository {
|
||||||
public async getSession(
|
public async getSession(
|
||||||
projectId: string,
|
projectId: string,
|
||||||
@@ -33,14 +28,16 @@ export class SessionRepository {
|
|||||||
throw new Error("Session not found");
|
throw new Error("Session not found");
|
||||||
}
|
}
|
||||||
const content = await readFile(sessionPath, "utf-8");
|
const content = await readFile(sessionPath, "utf-8");
|
||||||
|
const allLines = content.split("\n").filter((line) => line.trim());
|
||||||
|
|
||||||
const conversations = parseJsonl(content);
|
const conversations = parseJsonl(allLines.join("\n"));
|
||||||
|
|
||||||
const sessionDetail: SessionDetail = {
|
const sessionDetail: SessionDetail = {
|
||||||
id: sessionId,
|
id: sessionId,
|
||||||
jsonlFilePath: sessionPath,
|
jsonlFilePath: sessionPath,
|
||||||
meta: await sessionMetaStorage.getSessionMeta(projectId, sessionId),
|
meta: await sessionMetaStorage.getSessionMeta(projectId, sessionId),
|
||||||
conversations,
|
conversations,
|
||||||
|
lastModifiedAt: statSync(sessionPath).mtime,
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -50,36 +47,88 @@ export class SessionRepository {
|
|||||||
|
|
||||||
public async getSessions(
|
public async getSessions(
|
||||||
projectId: string,
|
projectId: string,
|
||||||
|
options?: {
|
||||||
|
maxCount?: number;
|
||||||
|
cursor?: string;
|
||||||
|
},
|
||||||
): Promise<{ sessions: Session[] }> {
|
): Promise<{ sessions: Session[] }> {
|
||||||
|
const { maxCount = 20, cursor } = options ?? {};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const claudeProjectPath = decodeProjectId(projectId);
|
const claudeProjectPath = decodeProjectId(projectId);
|
||||||
const dirents = await readdir(claudeProjectPath, { withFileTypes: true });
|
const dirents = await readdir(claudeProjectPath, { withFileTypes: true });
|
||||||
const sessions = await Promise.all(
|
const sessions = await Promise.all(
|
||||||
dirents
|
dirents
|
||||||
.filter((d) => d.isFile() && d.name.endsWith(".jsonl"))
|
.filter((d) => d.isFile() && d.name.endsWith(".jsonl"))
|
||||||
.map(async (d) => ({
|
.map(async (d) => {
|
||||||
id: encodeSessionId(resolve(claudeProjectPath, d.name)),
|
const sessionId = encodeSessionId(
|
||||||
|
resolve(claudeProjectPath, d.name),
|
||||||
|
);
|
||||||
|
const stats = statSync(resolve(claudeProjectPath, d.name));
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: sessionId,
|
||||||
jsonlFilePath: resolve(claudeProjectPath, d.name),
|
jsonlFilePath: resolve(claudeProjectPath, d.name),
|
||||||
|
lastModifiedAt: stats.mtime,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
).then((fetched) =>
|
||||||
|
fetched.sort(
|
||||||
|
(a, b) => b.lastModifiedAt.getTime() - a.lastModifiedAt.getTime(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
const sessionMap = new Map(
|
||||||
|
sessions.map((session) => [session.id, session] as const),
|
||||||
|
);
|
||||||
|
|
||||||
|
const index =
|
||||||
|
cursor !== undefined
|
||||||
|
? sessions.findIndex((session) => session.id === cursor)
|
||||||
|
: -1;
|
||||||
|
|
||||||
|
if (index !== -1) {
|
||||||
|
return {
|
||||||
|
sessions: await Promise.all(
|
||||||
|
sessions
|
||||||
|
.slice(index + 1, Math.min(index + 1 + maxCount, sessions.length))
|
||||||
|
.map(async (item) => {
|
||||||
|
return {
|
||||||
|
...item,
|
||||||
meta: await sessionMetaStorage.getSessionMeta(
|
meta: await sessionMetaStorage.getSessionMeta(
|
||||||
projectId,
|
projectId,
|
||||||
encodeSessionId(resolve(claudeProjectPath, d.name)),
|
item.id,
|
||||||
),
|
),
|
||||||
})),
|
};
|
||||||
);
|
}),
|
||||||
const sessionMap = new Map<string, Session>(
|
),
|
||||||
sessions.map((session) => [session.id, session]),
|
};
|
||||||
);
|
}
|
||||||
|
|
||||||
const predictSessions = predictSessionsDatabase
|
const predictSessions = predictSessionsDatabase
|
||||||
.getPredictSessions(projectId)
|
.getPredictSessions(projectId)
|
||||||
.filter((session) => !sessionMap.has(session.id));
|
.filter((session) => !sessionMap.has(session.id))
|
||||||
|
.sort((a, b) => {
|
||||||
|
return b.lastModifiedAt.getTime() - a.lastModifiedAt.getTime();
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
sessions: [...predictSessions, ...sessions].sort((a, b) => {
|
sessions: [
|
||||||
return (
|
...predictSessions,
|
||||||
getTime(b.meta.lastModifiedAt) - getTime(a.meta.lastModifiedAt)
|
...(await Promise.all(
|
||||||
);
|
sessions
|
||||||
|
.slice(0, Math.min(maxCount, sessions.length))
|
||||||
|
.map(async (item) => {
|
||||||
|
return {
|
||||||
|
...item,
|
||||||
|
meta: await sessionMetaStorage.getSessionMeta(
|
||||||
|
projectId,
|
||||||
|
item.id,
|
||||||
|
),
|
||||||
|
};
|
||||||
}),
|
}),
|
||||||
|
)),
|
||||||
|
],
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn(`Failed to read sessions for project ${projectId}:`, error);
|
console.warn(`Failed to read sessions for project ${projectId}:`, error);
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
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 { InMemoryCacheStorage } from "../../lib/storage/InMemoryCacheStorage";
|
||||||
@@ -38,18 +37,12 @@ class SessionMetaStorage {
|
|||||||
|
|
||||||
const sessionPath = decodeSessionId(projectId, sessionId);
|
const sessionPath = decodeSessionId(projectId, sessionId);
|
||||||
|
|
||||||
const stats = statSync(sessionPath);
|
|
||||||
const lastModifiedUnixTime = stats.mtime.getTime();
|
|
||||||
|
|
||||||
const content = await readFile(sessionPath, "utf-8");
|
const content = await readFile(sessionPath, "utf-8");
|
||||||
const lines = content.split("\n");
|
const lines = content.split("\n");
|
||||||
|
|
||||||
const sessionMeta: SessionMeta = {
|
const sessionMeta: SessionMeta = {
|
||||||
messageCount: lines.length,
|
messageCount: lines.length,
|
||||||
firstCommand: this.getFirstCommand(sessionPath, lines),
|
firstCommand: this.getFirstCommand(sessionPath, lines),
|
||||||
lastModifiedAt: lastModifiedUnixTime
|
|
||||||
? new Date(lastModifiedUnixTime).toISOString()
|
|
||||||
: null,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
this.sessionMetaCache.save(sessionId, sessionMeta);
|
this.sessionMetaCache.save(sessionId, sessionMeta);
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import type { projectMetaSchema, sessionMetaSchema } from "./schema";
|
|||||||
export type Project = {
|
export type Project = {
|
||||||
id: string;
|
id: string;
|
||||||
claudeProjectPath: string;
|
claudeProjectPath: string;
|
||||||
|
lastModifiedAt: Date;
|
||||||
meta: ProjectMeta;
|
meta: ProjectMeta;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -13,6 +14,7 @@ export type ProjectMeta = z.infer<typeof projectMetaSchema>;
|
|||||||
export type Session = {
|
export type Session = {
|
||||||
id: string;
|
id: string;
|
||||||
jsonlFilePath: string;
|
jsonlFilePath: string;
|
||||||
|
lastModifiedAt: Date;
|
||||||
meta: SessionMeta;
|
meta: SessionMeta;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user