feat: implement watch claude code project folders and sync state

This commit is contained in:
d-kimsuon
2025-08-31 16:37:53 +09:00
parent 14b074c03c
commit be9914670c
13 changed files with 503 additions and 8 deletions

View File

@@ -0,0 +1,105 @@
import { useQueryClient } from "@tanstack/react-query";
import { useCallback, useEffect } from "react";
import { honoClient } from "../lib/api/client";
import type { SSEEvent } from "../server/service/events/types";
type ParsedEvent = {
event: string;
data: SSEEvent;
id: string;
};
const parseSSEEvent = (text: string): ParsedEvent => {
const lines = text.split("\n");
const eventIndex = lines.findIndex((line) => line.startsWith("event:"));
const dataIndex = lines.findIndex((line) => line.startsWith("data:"));
const idIndex = lines.findIndex((line) => line.startsWith("id:"));
const endIndex = (index: number) => {
const targets = [eventIndex, dataIndex, idIndex, lines.length].filter(
(current) => current > index,
);
return Math.min(...targets);
};
if (eventIndex === -1 || dataIndex === -1 || idIndex === -1) {
console.error("failed", text);
throw new Error("Failed to parse SSE event");
}
const event = lines.slice(eventIndex, endIndex(eventIndex)).join("\n");
const data = lines.slice(dataIndex, endIndex(dataIndex)).join("\n");
const id = lines.slice(idIndex, endIndex(idIndex)).join("\n");
return {
id: id.slice("id:".length).trim(),
event: event.slice("event:".length).trim(),
data: JSON.parse(
data.slice(data.indexOf("{"), data.indexOf("}") + 1),
) as SSEEvent,
};
};
const parseSSEEvents = (text: string): ParsedEvent[] => {
const eventTexts = text
.split("\n\n")
.filter((eventText) => eventText.length > 0);
return eventTexts.map((eventText) => parseSSEEvent(eventText));
};
let isInitialized = false;
export const useServerEvents = () => {
const queryClient = useQueryClient();
const listener = useCallback(async () => {
console.log("listening to events");
const response = await honoClient.api.events.state_changes.$get();
if (!response.ok) {
throw new Error("Failed to fetch events");
}
const reader = response.body?.getReader();
if (!reader) {
throw new Error("Failed to get reader");
}
const decoder = new TextDecoder();
while (true) {
const { done, value } = await reader.read();
if (done) break;
const events = parseSSEEvents(decoder.decode(value));
for (const event of events) {
console.log("data", event);
if (event.data.type === "project_changed") {
console.log("invalidating projects");
await queryClient.invalidateQueries({ queryKey: ["projects"] });
}
if (event.data.type === "session_changed") {
console.log("invalidating sessions");
await queryClient.invalidateQueries({ queryKey: ["sessions"] });
}
}
}
}, [queryClient]);
useEffect(() => {
if (isInitialized === false) {
void listener()
.then(() => {
console.log("registered events listener");
isInitialized = true;
})
.catch((error) => {
console.error("failed to register events listener", error);
isInitialized = true;
});
}
}, [listener]);
};