fix: reload drawing when edited externally by MCP

Adds an active polling and reload mechanism. It tracks the `updatedAt` timestamp of the current drawing and polls `refreshList` every 5 seconds. If the server's drawing is newer, it clears pending autosaves and reloads the drawing to reflect the external changes.

Also tracks when a title save is in progress to avoid reloading when the current user renamed the drawing.
This commit is contained in:
ruinivist
2026-05-31 01:21:08 +00:00
parent 1b0024d78f
commit 0d0d4b8a85
+42 -8
View File
@@ -108,6 +108,8 @@ export function App() {
const loadVersionRef = useRef(0); const loadVersionRef = useRef(0);
const themeSyncFrameRef = useRef<number | null>(null); const themeSyncFrameRef = useRef<number | null>(null);
const toastTimeoutRef = useRef<number | null>(null); const toastTimeoutRef = useRef<number | null>(null);
const loadedUpdatedAtRef = useRef<string | null>(null);
const inFlightTitleSaveRef = useRef<Promise<void> | null>(null);
const activeDrawing = useMemo( const activeDrawing = useMemo(
() => drawings.find((drawing) => drawing.id === activeId) ?? null, () => drawings.find((drawing) => drawing.id === activeId) ?? null,
@@ -141,6 +143,7 @@ export function App() {
body: JSON.stringify(nextScene), body: JSON.stringify(nextScene),
}) })
.then((body) => { .then((body) => {
loadedUpdatedAtRef.current = body.drawing.updatedAt;
setDrawings((current) => replaceMeta(current, body.drawing)); setDrawings((current) => replaceMeta(current, body.drawing));
if (isManual) { if (isManual) {
setToastMessage("Saved"); setToastMessage("Saved");
@@ -230,6 +233,7 @@ export function App() {
currentIdRef.current = drawing.id; currentIdRef.current = drawing.id;
currentTitleRef.current = drawing.title; currentTitleRef.current = drawing.title;
latestSceneRef.current = nextScene; latestSceneRef.current = nextScene;
loadedUpdatedAtRef.current = drawing.updatedAt;
setDrawings(sortDrawings(list)); setDrawings(sortDrawings(list));
setActiveId(drawing.id); setActiveId(drawing.id);
@@ -323,18 +327,22 @@ export function App() {
return; return;
} }
try { const promise = requestJson<{ ok: true; drawing: DrawingMeta }>(`/api/drawings/${drawingId}`, {
const body = await requestJson<{ ok: true; drawing: DrawingMeta }>(`/api/drawings/${drawingId}`, { method: "PUT",
method: "PUT", body: JSON.stringify({ title: currentTitleRef.current }),
body: JSON.stringify({ title: currentTitleRef.current }), }).then((body) => {
});
currentTitleRef.current = body.drawing.title; currentTitleRef.current = body.drawing.title;
loadedUpdatedAtRef.current = body.drawing.updatedAt;
setActiveTitle(body.drawing.title); setActiveTitle(body.drawing.title);
setDrawings((current) => replaceMeta(current, body.drawing)); setDrawings((current) => replaceMeta(current, body.drawing));
} catch (renameError) { }).catch((renameError) => {
setError(renameError instanceof Error ? renameError.message : "Rename failed"); setError(renameError instanceof Error ? renameError.message : "Rename failed");
} }).finally(() => {
inFlightTitleSaveRef.current = null;
});
inFlightTitleSaveRef.current = promise;
await promise;
}, []); }, []);
const syncThemeTokens = useCallback(() => { const syncThemeTokens = useCallback(() => {
@@ -411,6 +419,32 @@ export function App() {
}; };
}, []); }, []);
useEffect(() => {
const interval = window.setInterval(() => {
void refreshList();
}, 5000);
return () => {
window.clearInterval(interval);
};
}, [refreshList]);
useEffect(() => {
if (
activeDrawing &&
loadedUpdatedAtRef.current &&
activeDrawing.updatedAt > loadedUpdatedAtRef.current &&
!inFlightSaveRef.current &&
!inFlightTitleSaveRef.current
) {
if (timeoutRef.current !== null) {
clearTimeout(timeoutRef.current);
timeoutRef.current = null;
}
void loadDrawing(activeDrawing.id);
}
}, [activeDrawing, loadDrawing]);
useEffect(() => { useEffect(() => {
if (!scene || loading) { if (!scene || loading) {
return; return;