mirror of
https://github.com/aljazceru/claude-code-viewer.git
synced 2026-01-04 14:14:22 +01:00
refactor: add effect-ts and refactor codes
This commit is contained in:
@@ -1,8 +1,53 @@
|
||||
import { NodeContext } from "@effect/platform-node";
|
||||
import { Effect } from "effect";
|
||||
import { handle } from "hono/vercel";
|
||||
import { honoApp } from "../../../server/hono/app";
|
||||
import { InitializeService } from "../../../server/hono/initialize";
|
||||
import { routes } from "../../../server/hono/route";
|
||||
import { ClaudeCodeLifeCycleService } from "../../../server/service/claude-code/ClaudeCodeLifeCycleService";
|
||||
import { ClaudeCodePermissionService } from "../../../server/service/claude-code/ClaudeCodePermissionService";
|
||||
import { ClaudeCodeSessionProcessService } from "../../../server/service/claude-code/ClaudeCodeSessionProcessService";
|
||||
import { EventBus } from "../../../server/service/events/EventBus";
|
||||
import { FileWatcherService } from "../../../server/service/events/fileWatcher";
|
||||
import { ProjectMetaService } from "../../../server/service/project/ProjectMetaService";
|
||||
import { ProjectRepository } from "../../../server/service/project/ProjectRepository";
|
||||
import { VirtualConversationDatabase } from "../../../server/service/session/PredictSessionsDatabase";
|
||||
import { SessionMetaService } from "../../../server/service/session/SessionMetaService";
|
||||
import { SessionRepository } from "../../../server/service/session/SessionRepository";
|
||||
|
||||
await routes(honoApp);
|
||||
const program = routes(honoApp);
|
||||
|
||||
await Effect.runPromise(
|
||||
program.pipe(
|
||||
// 依存の浅い順にコンテナに pipe する必要がある
|
||||
|
||||
/** Application */
|
||||
Effect.provide(InitializeService.Live),
|
||||
|
||||
/** Domain */
|
||||
Effect.provide(ClaudeCodeLifeCycleService.Live),
|
||||
Effect.provide(ClaudeCodePermissionService.Live),
|
||||
Effect.provide(ClaudeCodeSessionProcessService.Live),
|
||||
|
||||
// Shared Services
|
||||
Effect.provide(FileWatcherService.Live),
|
||||
Effect.provide(EventBus.Live),
|
||||
|
||||
/** Infrastructure */
|
||||
|
||||
// Repository
|
||||
Effect.provide(ProjectRepository.Live),
|
||||
Effect.provide(SessionRepository.Live),
|
||||
|
||||
// StorageService
|
||||
Effect.provide(ProjectMetaService.Live),
|
||||
Effect.provide(SessionMetaService.Live),
|
||||
Effect.provide(VirtualConversationDatabase.Live),
|
||||
|
||||
/** Platform */
|
||||
Effect.provide(NodeContext.layer),
|
||||
),
|
||||
);
|
||||
|
||||
export const GET = handle(honoApp);
|
||||
export const POST = handle(honoApp);
|
||||
|
||||
@@ -5,11 +5,11 @@ import { useSetAtom } from "jotai";
|
||||
import type { FC, PropsWithChildren } from "react";
|
||||
import { projectDetailQuery, sessionDetailQuery } from "../../lib/api/queries";
|
||||
import { useServerEventListener } from "../../lib/sse/hook/useServerEventListener";
|
||||
import { aliveTasksAtom } from "../projects/[projectId]/sessions/[sessionId]/store/aliveTasksAtom";
|
||||
import { sessionProcessesAtom } from "../projects/[projectId]/sessions/[sessionId]/store/sessionProcessesAtom";
|
||||
|
||||
export const SSEEventListeners: FC<PropsWithChildren> = ({ children }) => {
|
||||
const queryClient = useQueryClient();
|
||||
const setAliveTasks = useSetAtom(aliveTasksAtom);
|
||||
const setSessionProcesses = useSetAtom(sessionProcessesAtom);
|
||||
|
||||
useServerEventListener("sessionListChanged", async (event) => {
|
||||
// invalidate session list
|
||||
@@ -25,8 +25,8 @@ export const SSEEventListeners: FC<PropsWithChildren> = ({ children }) => {
|
||||
});
|
||||
});
|
||||
|
||||
useServerEventListener("taskChanged", async ({ aliveTasks }) => {
|
||||
setAliveTasks(aliveTasks);
|
||||
useServerEventListener("sessionProcessChanged", async ({ processes }) => {
|
||||
setSessionProcesses(processes);
|
||||
});
|
||||
|
||||
return <>{children}</>;
|
||||
|
||||
18
src/app/components/SyncSessionProcess.tsx
Normal file
18
src/app/components/SyncSessionProcess.tsx
Normal file
@@ -0,0 +1,18 @@
|
||||
"use client";
|
||||
|
||||
import { useSetAtom } from "jotai";
|
||||
import { type FC, type PropsWithChildren, useEffect } from "react";
|
||||
import type { PublicSessionProcess } from "../../types/session-process";
|
||||
import { sessionProcessesAtom } from "../projects/[projectId]/sessions/[sessionId]/store/sessionProcessesAtom";
|
||||
|
||||
export const SyncSessionProcess: FC<
|
||||
PropsWithChildren<{ initProcesses: PublicSessionProcess[] }>
|
||||
> = ({ children, initProcesses }) => {
|
||||
const setSessionProcesses = useSetAtom(sessionProcessesAtom);
|
||||
|
||||
useEffect(() => {
|
||||
setSessionProcesses(initProcesses);
|
||||
}, [initProcesses, setSessionProcesses]);
|
||||
|
||||
return <>{children}</>;
|
||||
};
|
||||
@@ -7,8 +7,10 @@ import { SSEProvider } from "../lib/sse/components/SSEProvider";
|
||||
import { RootErrorBoundary } from "./components/RootErrorBoundary";
|
||||
|
||||
import "./globals.css";
|
||||
import { honoClient } from "../lib/api/client";
|
||||
import { configQuery } from "../lib/api/queries";
|
||||
import { SSEEventListeners } from "./components/SSEEventListeners";
|
||||
import { SyncSessionProcess } from "./components/SyncSessionProcess";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
export const fetchCache = "force-no-store";
|
||||
@@ -40,6 +42,10 @@ export default async function RootLayout({
|
||||
queryFn: configQuery.queryFn,
|
||||
});
|
||||
|
||||
const initSessionProcesses = await honoClient.api.cc["session-processes"]
|
||||
.$get({})
|
||||
.then((response) => response.json());
|
||||
|
||||
return (
|
||||
<html lang="en">
|
||||
<body
|
||||
@@ -48,7 +54,13 @@ export default async function RootLayout({
|
||||
<RootErrorBoundary>
|
||||
<QueryClientProviderWrapper>
|
||||
<SSEProvider>
|
||||
<SSEEventListeners>{children}</SSEEventListeners>
|
||||
<SSEEventListeners>
|
||||
<SyncSessionProcess
|
||||
initProcesses={initSessionProcesses.processes}
|
||||
>
|
||||
{children}
|
||||
</SyncSessionProcess>
|
||||
</SSEEventListeners>
|
||||
</SSEProvider>
|
||||
</QueryClientProviderWrapper>
|
||||
</RootErrorBoundary>
|
||||
|
||||
@@ -4,4 +4,7 @@ export type { CommandCompletionRef } from "./CommandCompletion";
|
||||
export { CommandCompletion } from "./CommandCompletion";
|
||||
export type { FileCompletionRef } from "./FileCompletion";
|
||||
export { FileCompletion } from "./FileCompletion";
|
||||
export { useNewChatMutation, useResumeChatMutation } from "./useChatMutations";
|
||||
export {
|
||||
useContinueSessionProcessMutation,
|
||||
useCreateSessionProcessMutation,
|
||||
} from "./useChatMutations";
|
||||
|
||||
@@ -2,20 +2,24 @@ import { useMutation } from "@tanstack/react-query";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { honoClient } from "../../../../../lib/api/client";
|
||||
|
||||
export const useNewChatMutation = (
|
||||
export const useCreateSessionProcessMutation = (
|
||||
projectId: string,
|
||||
onSuccess?: () => void,
|
||||
) => {
|
||||
const router = useRouter();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async (options: { message: string }) => {
|
||||
const response = await honoClient.api.projects[":projectId"][
|
||||
"new-session"
|
||||
].$post(
|
||||
mutationFn: async (options: {
|
||||
message: string;
|
||||
baseSessionId?: string;
|
||||
}) => {
|
||||
const response = await honoClient.api.cc["session-processes"].$post(
|
||||
{
|
||||
param: { projectId },
|
||||
json: { message: options.message },
|
||||
json: {
|
||||
projectId,
|
||||
baseSessionId: options.baseSessionId,
|
||||
message: options.message,
|
||||
},
|
||||
},
|
||||
{
|
||||
init: {
|
||||
@@ -32,22 +36,32 @@ export const useNewChatMutation = (
|
||||
},
|
||||
onSuccess: async (response) => {
|
||||
onSuccess?.();
|
||||
router.push(`/projects/${projectId}/sessions/${response.sessionId}`);
|
||||
router.push(
|
||||
`/projects/${projectId}/sessions/${response.sessionProcess.sessionId}`,
|
||||
);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const useResumeChatMutation = (projectId: string, sessionId: string) => {
|
||||
const router = useRouter();
|
||||
|
||||
export const useContinueSessionProcessMutation = (
|
||||
projectId: string,
|
||||
baseSessionId: string,
|
||||
) => {
|
||||
return useMutation({
|
||||
mutationFn: async (options: { message: string }) => {
|
||||
const response = await honoClient.api.projects[":projectId"].sessions[
|
||||
":sessionId"
|
||||
].resume.$post(
|
||||
mutationFn: async (options: {
|
||||
message: string;
|
||||
sessionProcessId: string;
|
||||
}) => {
|
||||
const response = await honoClient.api.cc["session-processes"][
|
||||
":sessionProcessId"
|
||||
].continue.$post(
|
||||
{
|
||||
param: { projectId, sessionId },
|
||||
json: { resumeMessage: options.message },
|
||||
param: { sessionProcessId: options.sessionProcessId },
|
||||
json: {
|
||||
projectId: projectId,
|
||||
baseSessionId: baseSessionId,
|
||||
continueMessage: options.message,
|
||||
},
|
||||
},
|
||||
{
|
||||
init: {
|
||||
@@ -62,10 +76,5 @@ export const useResumeChatMutation = (projectId: string, sessionId: string) => {
|
||||
|
||||
return response.json();
|
||||
},
|
||||
onSuccess: async (response) => {
|
||||
if (sessionId !== response.sessionId) {
|
||||
router.push(`/projects/${projectId}/sessions/${response.sessionId}`);
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,16 +1,19 @@
|
||||
import type { FC } from "react";
|
||||
import { useConfig } from "../../../../hooks/useConfig";
|
||||
import { ChatInput, useNewChatMutation } from "../chatForm";
|
||||
import { ChatInput, useCreateSessionProcessMutation } from "../chatForm";
|
||||
|
||||
export const NewChat: FC<{
|
||||
projectId: string;
|
||||
onSuccess?: () => void;
|
||||
}> = ({ projectId, onSuccess }) => {
|
||||
const startNewChat = useNewChatMutation(projectId, onSuccess);
|
||||
const createSessionProcess = useCreateSessionProcessMutation(
|
||||
projectId,
|
||||
onSuccess,
|
||||
);
|
||||
const { config } = useConfig();
|
||||
|
||||
const handleSubmit = async (message: string) => {
|
||||
await startNewChat.mutateAsync({ message });
|
||||
await createSessionProcess.mutateAsync({ message });
|
||||
};
|
||||
|
||||
const getPlaceholder = () => {
|
||||
@@ -25,8 +28,8 @@ export const NewChat: FC<{
|
||||
<ChatInput
|
||||
projectId={projectId}
|
||||
onSubmit={handleSubmit}
|
||||
isPending={startNewChat.isPending}
|
||||
error={startNewChat.error}
|
||||
isPending={createSessionProcess.isPending}
|
||||
error={createSessionProcess.error}
|
||||
placeholder={getPlaceholder()}
|
||||
buttonText="Start Chat"
|
||||
minHeight="min-h-[200px]"
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
} from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import type { FC } from "react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { PermissionDialog } from "@/components/PermissionDialog";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { usePermissionRequests } from "@/hooks/usePermissionRequests";
|
||||
@@ -20,10 +20,11 @@ import { Badge } from "../../../../../../components/ui/badge";
|
||||
import { honoClient } from "../../../../../../lib/api/client";
|
||||
import { useProject } from "../../../hooks/useProject";
|
||||
import { firstCommandToTitle } from "../../../services/firstCommandToTitle";
|
||||
import { useAliveTask } from "../hooks/useAliveTask";
|
||||
import { useSession } from "../hooks/useSession";
|
||||
import { useSessionProcess } from "../hooks/useSessionProcess";
|
||||
import { ConversationList } from "./conversationList/ConversationList";
|
||||
import { DiffModal } from "./diffModal";
|
||||
import { ContinueChat } from "./resumeChat/ContinueChat";
|
||||
import { ResumeChat } from "./resumeChat/ResumeChat";
|
||||
import { SessionSidebar } from "./sessionSidebar/SessionSidebar";
|
||||
|
||||
@@ -40,9 +41,12 @@ export const SessionPageContent: FC<{
|
||||
const project = projectData.pages[0]!.project;
|
||||
|
||||
const abortTask = useMutation({
|
||||
mutationFn: async (sessionId: string) => {
|
||||
const response = await honoClient.api.tasks.abort.$post({
|
||||
json: { sessionId },
|
||||
mutationFn: async (sessionProcessId: string) => {
|
||||
const response = await honoClient.api.cc["session-processes"][
|
||||
":sessionProcessId"
|
||||
].abort.$post({
|
||||
param: { sessionProcessId },
|
||||
json: { projectId },
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
@@ -52,13 +56,18 @@ export const SessionPageContent: FC<{
|
||||
return response.json();
|
||||
},
|
||||
});
|
||||
const sessionProcess = useSessionProcess();
|
||||
|
||||
const { isRunningTask, isPausedTask } = useAliveTask(sessionId);
|
||||
const { currentPermissionRequest, isDialogOpen, onPermissionResponse } =
|
||||
usePermissionRequests();
|
||||
|
||||
const relatedSessionProcess = useMemo(
|
||||
() => sessionProcess.getSessionProcess(sessionId),
|
||||
[sessionProcess, sessionId],
|
||||
);
|
||||
|
||||
// Set up task completion notifications
|
||||
useTaskNotifications(isRunningTask);
|
||||
useTaskNotifications(relatedSessionProcess?.status === "running");
|
||||
|
||||
const [previousConversationLength, setPreviousConversationLength] =
|
||||
useState(0);
|
||||
@@ -69,7 +78,7 @@ export const SessionPageContent: FC<{
|
||||
// 自動スクロール処理
|
||||
useEffect(() => {
|
||||
if (
|
||||
(isRunningTask || isPausedTask) &&
|
||||
relatedSessionProcess?.status === "running" &&
|
||||
conversations.length !== previousConversationLength
|
||||
) {
|
||||
setPreviousConversationLength(conversations.length);
|
||||
@@ -81,7 +90,11 @@ export const SessionPageContent: FC<{
|
||||
});
|
||||
}
|
||||
}
|
||||
}, [conversations, isRunningTask, isPausedTask, previousConversationLength]);
|
||||
}, [
|
||||
conversations,
|
||||
relatedSessionProcess?.status,
|
||||
previousConversationLength,
|
||||
]);
|
||||
|
||||
return (
|
||||
<div className="flex h-screen max-h-screen overflow-hidden">
|
||||
@@ -136,7 +149,7 @@ export const SessionPageContent: FC<{
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
{isRunningTask && (
|
||||
{relatedSessionProcess?.status === "running" && (
|
||||
<div className="flex items-center gap-1 sm:gap-2 p-1 bg-primary/10 border border-primary/20 rounded-lg mx-1 sm:mx-5">
|
||||
<LoaderIcon className="w-3 h-3 sm:w-4 sm:h-4 animate-spin" />
|
||||
<div className="flex-1">
|
||||
@@ -148,7 +161,7 @@ export const SessionPageContent: FC<{
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
abortTask.mutate(sessionId);
|
||||
abortTask.mutate(relatedSessionProcess.id);
|
||||
}}
|
||||
>
|
||||
<XIcon className="w-3 h-3 sm:w-4 sm:h-4" />
|
||||
@@ -157,7 +170,7 @@ export const SessionPageContent: FC<{
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isPausedTask && (
|
||||
{relatedSessionProcess?.status === "paused" && (
|
||||
<div className="flex items-center gap-1 sm:gap-2 p-1 bg-primary/10 border border-primary/20 rounded-lg mx-1 sm:mx-5">
|
||||
<PauseIcon className="w-3 h-3 sm:w-4 sm:h-4" />
|
||||
<div className="flex-1">
|
||||
@@ -169,7 +182,7 @@ export const SessionPageContent: FC<{
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
abortTask.mutate(sessionId);
|
||||
abortTask.mutate(relatedSessionProcess.id);
|
||||
}}
|
||||
>
|
||||
<XIcon className="w-3 h-3 sm:w-4 sm:h-4" />
|
||||
@@ -190,7 +203,7 @@ export const SessionPageContent: FC<{
|
||||
getToolResult={getToolResult}
|
||||
/>
|
||||
|
||||
{isRunningTask && (
|
||||
{relatedSessionProcess?.status === "running" && (
|
||||
<div className="flex justify-start items-center py-8">
|
||||
<div className="flex flex-col items-center gap-4">
|
||||
<div className="flex items-center gap-2">
|
||||
@@ -207,12 +220,15 @@ export const SessionPageContent: FC<{
|
||||
</div>
|
||||
)}
|
||||
|
||||
<ResumeChat
|
||||
projectId={projectId}
|
||||
sessionId={sessionId}
|
||||
isPausedTask={isPausedTask}
|
||||
isRunningTask={isRunningTask}
|
||||
/>
|
||||
{relatedSessionProcess !== undefined ? (
|
||||
<ContinueChat
|
||||
projectId={projectId}
|
||||
sessionId={sessionId}
|
||||
sessionProcessId={relatedSessionProcess.id}
|
||||
/>
|
||||
) : (
|
||||
<ResumeChat projectId={projectId} sessionId={sessionId} />
|
||||
)}
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
import type { FC } from "react";
|
||||
import { useConfig } from "../../../../../../hooks/useConfig";
|
||||
import {
|
||||
ChatInput,
|
||||
useContinueSessionProcessMutation,
|
||||
} from "../../../../components/chatForm";
|
||||
|
||||
export const ContinueChat: FC<{
|
||||
projectId: string;
|
||||
sessionId: string;
|
||||
sessionProcessId: string;
|
||||
}> = ({ projectId, sessionId, sessionProcessId }) => {
|
||||
const continueSessionProcess = useContinueSessionProcessMutation(
|
||||
projectId,
|
||||
sessionId,
|
||||
);
|
||||
const { config } = useConfig();
|
||||
|
||||
const handleSubmit = async (message: string) => {
|
||||
await continueSessionProcess.mutateAsync({ message, sessionProcessId });
|
||||
};
|
||||
|
||||
const getPlaceholder = () => {
|
||||
const isEnterSend = config?.enterKeyBehavior === "enter-send";
|
||||
if (isEnterSend) {
|
||||
return "Type your message... (Start with / for commands, Enter to send)";
|
||||
}
|
||||
return "Type your message... (Start with / for commands, Shift+Enter to send)";
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="border-t border-border/50 bg-muted/20 p-4 mt-6">
|
||||
<ChatInput
|
||||
projectId={projectId}
|
||||
onSubmit={handleSubmit}
|
||||
isPending={continueSessionProcess.isPending}
|
||||
error={continueSessionProcess.error}
|
||||
placeholder={getPlaceholder()}
|
||||
buttonText={"Send"}
|
||||
minHeight="min-h-[100px]"
|
||||
containerClassName="space-y-2"
|
||||
buttonSize="default"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -2,27 +2,21 @@ import type { FC } from "react";
|
||||
import { useConfig } from "../../../../../../hooks/useConfig";
|
||||
import {
|
||||
ChatInput,
|
||||
useResumeChatMutation,
|
||||
useCreateSessionProcessMutation,
|
||||
} from "../../../../components/chatForm";
|
||||
|
||||
export const ResumeChat: FC<{
|
||||
projectId: string;
|
||||
sessionId: string;
|
||||
isPausedTask: boolean;
|
||||
isRunningTask: boolean;
|
||||
}> = ({ projectId, sessionId, isPausedTask, isRunningTask }) => {
|
||||
const resumeChat = useResumeChatMutation(projectId, sessionId);
|
||||
}> = ({ projectId, sessionId }) => {
|
||||
const createSessionProcess = useCreateSessionProcessMutation(projectId);
|
||||
const { config } = useConfig();
|
||||
|
||||
const handleSubmit = async (message: string) => {
|
||||
await resumeChat.mutateAsync({ message });
|
||||
};
|
||||
|
||||
const getButtonText = () => {
|
||||
if (isPausedTask || isRunningTask) {
|
||||
return "Send";
|
||||
}
|
||||
return "Resume";
|
||||
await createSessionProcess.mutateAsync({
|
||||
message,
|
||||
baseSessionId: sessionId,
|
||||
});
|
||||
};
|
||||
|
||||
const getPlaceholder = () => {
|
||||
@@ -38,10 +32,10 @@ export const ResumeChat: FC<{
|
||||
<ChatInput
|
||||
projectId={projectId}
|
||||
onSubmit={handleSubmit}
|
||||
isPending={resumeChat.isPending}
|
||||
error={resumeChat.error}
|
||||
isPending={createSessionProcess.isPending}
|
||||
error={createSessionProcess.error}
|
||||
placeholder={getPlaceholder()}
|
||||
buttonText={getButtonText()}
|
||||
buttonText={"Resume"}
|
||||
minHeight="min-h-[100px]"
|
||||
containerClassName="space-y-2"
|
||||
buttonSize="default"
|
||||
|
||||
@@ -6,7 +6,7 @@ import type { FC } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { mcpListQuery } from "../../../../../../../lib/api/queries";
|
||||
|
||||
export const McpTab: FC = () => {
|
||||
export const McpTab: FC<{ projectId: string }> = ({ projectId }) => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const {
|
||||
@@ -14,12 +14,14 @@ export const McpTab: FC = () => {
|
||||
isLoading,
|
||||
error,
|
||||
} = useQuery({
|
||||
queryKey: mcpListQuery.queryKey,
|
||||
queryFn: mcpListQuery.queryFn,
|
||||
queryKey: mcpListQuery(projectId).queryKey,
|
||||
queryFn: mcpListQuery(projectId).queryFn,
|
||||
});
|
||||
|
||||
const handleReload = () => {
|
||||
queryClient.invalidateQueries({ queryKey: mcpListQuery.queryKey });
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: mcpListQuery(projectId).queryKey,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -87,7 +87,7 @@ export const MobileSidebar: FC<MobileSidebarProps> = ({
|
||||
/>
|
||||
);
|
||||
case "mcp":
|
||||
return <McpTab />;
|
||||
return <McpTab projectId={projectId} />;
|
||||
case "settings":
|
||||
return <SettingsTab openingProjectId={projectId} />;
|
||||
default:
|
||||
|
||||
@@ -69,7 +69,7 @@ export const SessionSidebar: FC<{
|
||||
/>
|
||||
);
|
||||
case "mcp":
|
||||
return <McpTab />;
|
||||
return <McpTab projectId={projectId} />;
|
||||
case "settings":
|
||||
return <SettingsTab openingProjectId={projectId} />;
|
||||
default:
|
||||
|
||||
@@ -10,7 +10,7 @@ import { cn } from "@/lib/utils";
|
||||
import type { Session } from "../../../../../../../server/service/types";
|
||||
import { NewChatModal } from "../../../../components/newChat/NewChatModal";
|
||||
import { firstCommandToTitle } from "../../../../services/firstCommandToTitle";
|
||||
import { aliveTasksAtom } from "../../store/aliveTasksAtom";
|
||||
import { sessionProcessesAtom } from "../../store/sessionProcessesAtom";
|
||||
|
||||
export const SessionsTab: FC<{
|
||||
sessions: Session[];
|
||||
@@ -27,18 +27,22 @@ export const SessionsTab: FC<{
|
||||
isFetchingNextPage,
|
||||
onLoadMore,
|
||||
}) => {
|
||||
const aliveTasks = useAtomValue(aliveTasksAtom);
|
||||
const sessionProcesses = useAtomValue(sessionProcessesAtom);
|
||||
|
||||
// Sort sessions: Running > Paused > Others, then by lastModifiedAt (newest first)
|
||||
const sortedSessions = [...sessions].sort((a, b) => {
|
||||
const aTask = aliveTasks.find((task) => task.sessionId === a.id);
|
||||
const bTask = aliveTasks.find((task) => task.sessionId === b.id);
|
||||
const aProcess = sessionProcesses.find(
|
||||
(process) => process.sessionId === a.id,
|
||||
);
|
||||
const bProcess = sessionProcesses.find(
|
||||
(process) => process.sessionId === b.id,
|
||||
);
|
||||
|
||||
const aStatus = aTask?.status;
|
||||
const bStatus = bTask?.status;
|
||||
const aStatus = aProcess?.status;
|
||||
const bStatus = bProcess?.status;
|
||||
|
||||
// Define priority: running = 0, paused = 1, others = 2
|
||||
const getPriority = (status: string | undefined) => {
|
||||
const getPriority = (status: "paused" | "running" | undefined) => {
|
||||
if (status === "running") return 0;
|
||||
if (status === "paused") return 1;
|
||||
return 2;
|
||||
@@ -86,11 +90,11 @@ export const SessionsTab: FC<{
|
||||
? firstCommandToTitle(session.meta.firstCommand)
|
||||
: session.id;
|
||||
|
||||
const aliveTask = aliveTasks.find(
|
||||
const sessionProcess = sessionProcesses.find(
|
||||
(task) => task.sessionId === session.id,
|
||||
);
|
||||
const isRunning = aliveTask?.status === "running";
|
||||
const isPaused = aliveTask?.status === "paused";
|
||||
const isRunning = sessionProcess?.status === "running";
|
||||
const isPaused = sessionProcess?.status === "paused";
|
||||
|
||||
return (
|
||||
<Link
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { useAtom } from "jotai";
|
||||
import { useMemo } from "react";
|
||||
import { aliveTasksQuery } from "../../../../../../lib/api/queries";
|
||||
import { aliveTasksAtom } from "../store/aliveTasksAtom";
|
||||
|
||||
export const useAliveTask = (sessionId: string) => {
|
||||
const [aliveTasks, setAliveTasks] = useAtom(aliveTasksAtom);
|
||||
|
||||
useQuery({
|
||||
queryKey: aliveTasksQuery.queryKey,
|
||||
queryFn: async () => {
|
||||
const { aliveTasks } = await aliveTasksQuery.queryFn();
|
||||
setAliveTasks(aliveTasks);
|
||||
return aliveTasks;
|
||||
},
|
||||
refetchOnReconnect: true,
|
||||
});
|
||||
|
||||
const taskInfo = useMemo(() => {
|
||||
const aliveTask = aliveTasks.find((task) => task.sessionId === sessionId);
|
||||
|
||||
return {
|
||||
aliveTask: aliveTasks.find((task) => task.sessionId === sessionId),
|
||||
isRunningTask: aliveTask?.status === "running",
|
||||
isPausedTask: aliveTask?.status === "paused",
|
||||
} as const;
|
||||
}, [aliveTasks, sessionId]);
|
||||
|
||||
return taskInfo;
|
||||
};
|
||||
@@ -3,9 +3,13 @@ import { useSessionQuery } from "./useSessionQuery";
|
||||
|
||||
export const useSession = (projectId: string, sessionId: string) => {
|
||||
const query = useSessionQuery(projectId, sessionId);
|
||||
const session = query.data?.session;
|
||||
if (session === undefined || session === null) {
|
||||
throw new Error("Session not found");
|
||||
}
|
||||
|
||||
const toolResultMap = useMemo(() => {
|
||||
const entries = query.data.session.conversations.flatMap((conversation) => {
|
||||
const entries = session.conversations.flatMap((conversation) => {
|
||||
if (conversation.type !== "user") {
|
||||
return [];
|
||||
}
|
||||
@@ -28,7 +32,7 @@ export const useSession = (projectId: string, sessionId: string) => {
|
||||
});
|
||||
|
||||
return new Map(entries);
|
||||
}, [query.data.session.conversations]);
|
||||
}, [session.conversations]);
|
||||
|
||||
const getToolResult = useCallback(
|
||||
(toolUseId: string) => {
|
||||
@@ -38,8 +42,8 @@ export const useSession = (projectId: string, sessionId: string) => {
|
||||
);
|
||||
|
||||
return {
|
||||
session: query.data.session,
|
||||
conversations: query.data.session.conversations,
|
||||
session,
|
||||
conversations: session.conversations,
|
||||
getToolResult,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
import { useAtomValue } from "jotai";
|
||||
import { useCallback } from "react";
|
||||
import { sessionProcessesAtom } from "../store/sessionProcessesAtom";
|
||||
|
||||
export const useSessionProcess = () => {
|
||||
const sessionProcesses = useAtomValue(sessionProcessesAtom);
|
||||
|
||||
const getSessionProcess = useCallback(
|
||||
(sessionId: string) => {
|
||||
const targetProcess = sessionProcesses.find(
|
||||
(process) => process.sessionId === sessionId,
|
||||
);
|
||||
|
||||
return targetProcess;
|
||||
},
|
||||
[sessionProcesses],
|
||||
);
|
||||
|
||||
return {
|
||||
sessionProcesses,
|
||||
getSessionProcess,
|
||||
};
|
||||
};
|
||||
@@ -1,4 +0,0 @@
|
||||
import { atom } from "jotai";
|
||||
import type { SerializableAliveTask } from "../../../../../../server/service/claude-code/types";
|
||||
|
||||
export const aliveTasksAtom = atom<SerializableAliveTask[]>([]);
|
||||
@@ -0,0 +1,4 @@
|
||||
import { atom } from "jotai";
|
||||
import type { PublicSessionProcess } from "../../../../../../types/session-process";
|
||||
|
||||
export const sessionProcessesAtom = atom<PublicSessionProcess[]>([]);
|
||||
Reference in New Issue
Block a user