diff --git a/src/lib/sse/callSSE.ts b/src/lib/sse/callSSE.ts index 49a2efe..4a0b9fa 100644 --- a/src/lib/sse/callSSE.ts +++ b/src/lib/sse/callSSE.ts @@ -1,12 +1,15 @@ import type { SSEEventMap } from "../../types/sse"; -export const callSSE = () => { +export const callSSE = (options?: { onOpen?: (event: Event) => void }) => { + const { onOpen } = options ?? {}; + const eventSource = new EventSource( new URL("/api/sse", window.location.origin).href, ); const handleOnOpen = (event: Event) => { console.log("SSE connection opened", event); + onOpen?.(event); }; eventSource.onopen = handleOnOpen; diff --git a/src/lib/sse/components/ServerEventsProvider.tsx b/src/lib/sse/components/ServerEventsProvider.tsx index 5018c32..bad7d7c 100644 --- a/src/lib/sse/components/ServerEventsProvider.tsx +++ b/src/lib/sse/components/ServerEventsProvider.tsx @@ -1,3 +1,4 @@ +import { useQueryClient } from "@tanstack/react-query"; import { useAtom } from "jotai"; import { type FC, @@ -7,6 +8,7 @@ import { useRef, } from "react"; import type { SSEEvent } from "../../../types/sse"; +import { projectListQuery } from "../../api/queries"; import { callSSE } from "../callSSE"; import { type EventListener, @@ -21,9 +23,18 @@ export const ServerEventsProvider: FC = ({ children }) => { Map void>> >(new Map()); const [, setSSEState] = useAtom(sseAtom); + const queryClient = useQueryClient(); useEffect(() => { - const sse = callSSE(); + const sse = callSSE({ + onOpen: async () => { + // reconnect 中のイベントは購読できないので + // open 時にまとめて invalidate する + await queryClient.invalidateQueries({ + queryKey: projectListQuery.queryKey, + }); + }, + }); sseRef.current = sse; const { removeEventListener } = sse.addEventListener("connect", (event) => { @@ -39,7 +50,7 @@ export const ServerEventsProvider: FC = ({ children }) => { sse.cleanUp(); removeEventListener(); }; - }, [setSSEState]); + }, [setSSEState, queryClient]); const addEventListener = useCallback( (eventType: T, listener: EventListener) => { diff --git a/src/server/hono/route.ts b/src/server/hono/route.ts index 133cd48..d7cfc6d 100644 --- a/src/server/hono/route.ts +++ b/src/server/hono/route.ts @@ -1,5 +1,4 @@ import { readdir } from "node:fs/promises"; -import { homedir } from "node:os"; import { resolve } from "node:path"; import { zValidator } from "@hono/zod-validator"; import { setCookie } from "hono/cookie"; @@ -18,6 +17,7 @@ import { getBranches } from "../service/git/getBranches"; import { getCommits } from "../service/git/getCommits"; import { getDiff } from "../service/git/getDiff"; import { getMcpList } from "../service/mcp/getMcpList"; +import { claudeCommandsDirPath } from "../service/paths"; import { getProject } from "../service/project/getProject"; import { getProjects } from "../service/project/getProjects"; import { getSession } from "../service/session/getSession"; @@ -194,7 +194,7 @@ export const routes = (app: HonoAppType) => { const { project } = await getProject(projectId); const [globalCommands, projectCommands] = await Promise.allSettled([ - readdir(resolve(homedir(), ".claude", "commands"), { + readdir(claudeCommandsDirPath, { withFileTypes: true, }).then((dirents) => dirents diff --git a/src/server/service/events/fileWatcher.ts b/src/server/service/events/fileWatcher.ts index aa1351d..290de2c 100644 --- a/src/server/service/events/fileWatcher.ts +++ b/src/server/service/events/fileWatcher.ts @@ -1,6 +1,6 @@ import { type FSWatcher, watch } from "node:fs"; import z from "zod"; -import { claudeProjectPath } from "../paths"; +import { claudeProjectsDirPath } from "../paths"; import { getEventBus, type IEventBus } from "./EventBus"; const fileRegExp = /(?.*?)\/(?.*?)\.jsonl/; @@ -24,10 +24,10 @@ export class FileWatcherService { this.isWatching = true; try { - console.log("Starting file watcher on:", claudeProjectPath); + console.log("Starting file watcher on:", claudeProjectsDirPath); // メインプロジェクトディレクトリを監視 this.watcher = watch( - claudeProjectPath, + claudeProjectsDirPath, { persistent: false, recursive: true }, (eventType, filename) => { if (!filename) return; diff --git a/src/server/service/paths.ts b/src/server/service/paths.ts index e87e728..f2b4e50 100644 --- a/src/server/service/paths.ts +++ b/src/server/service/paths.ts @@ -1,4 +1,20 @@ import { homedir } from "node:os"; import { resolve } from "node:path"; -export const claudeProjectPath = resolve(homedir(), ".claude", "projects"); +// biome-ignore lint/complexity/useLiteralKeys: typescript restriction +const GLOBAL_CLAUDE_DIR = process.env["GLOBAL_CLAUDE_DIR"]; + +export const globalClaudeDirectoryPath = + GLOBAL_CLAUDE_DIR === undefined + ? resolve(homedir(), ".claude") + : resolve(GLOBAL_CLAUDE_DIR); + +export const claudeProjectsDirPath = resolve( + globalClaudeDirectoryPath, + "projects", +); + +export const claudeCommandsDirPath = resolve( + globalClaudeDirectoryPath, + "commands", +); diff --git a/src/server/service/project/getProjects.ts b/src/server/service/project/getProjects.ts index 49e4a3a..9396033 100644 --- a/src/server/service/project/getProjects.ts +++ b/src/server/service/project/getProjects.ts @@ -1,8 +1,7 @@ import { constants } from "node:fs"; import { access, readdir } from "node:fs/promises"; import { resolve } from "node:path"; - -import { claudeProjectPath } from "../paths"; +import { claudeProjectsDirPath } from "../paths"; import type { Project } from "../types"; import { getProjectMeta } from "./getProjectMeta"; import { encodeProjectId } from "./id"; @@ -10,20 +9,24 @@ import { encodeProjectId } from "./id"; export const getProjects = async (): Promise<{ projects: Project[] }> => { try { // Check if the claude projects directory exists - await access(claudeProjectPath, constants.F_OK); + await access(claudeProjectsDirPath, constants.F_OK); } catch (_error) { // Directory doesn't exist, return empty array - console.warn(`Claude projects directory not found at ${claudeProjectPath}`); + console.warn( + `Claude projects directory not found at ${claudeProjectsDirPath}`, + ); return { projects: [] }; } try { - const dirents = await readdir(claudeProjectPath, { withFileTypes: true }); + const dirents = await readdir(claudeProjectsDirPath, { + withFileTypes: true, + }); const projects = await Promise.all( dirents .filter((d) => d.isDirectory()) .map(async (d) => { - const fullPath = resolve(claudeProjectPath, d.name); + const fullPath = resolve(claudeProjectsDirPath, d.name); const id = encodeProjectId(fullPath); return {