diff --git a/apps/ai-game-creator-shell/src/features/project-workspace/DesignWorkspacePanel.tsx b/apps/ai-game-creator-shell/src/features/project-workspace/DesignWorkspacePanel.tsx index 9f4c6b770..789a96fbd 100644 --- a/apps/ai-game-creator-shell/src/features/project-workspace/DesignWorkspacePanel.tsx +++ b/apps/ai-game-creator-shell/src/features/project-workspace/DesignWorkspacePanel.tsx @@ -7,7 +7,7 @@ import { Folder, RefreshCw, } from 'lucide-react'; -import { useCallback, useEffect, useMemo, useState } from 'react'; +import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { resolveTauriInvoke } from '../../app/tauri'; import type { DesignView, DesignWorkspaceEntry } from '../../app/types'; @@ -181,61 +181,78 @@ export function DesignWorkspacePanel({ const [expandedPaths, setExpandedPaths] = useState>( () => new Set(), ); + const refreshTimerRef = useRef(null); - const loadWorkspace = useCallback(async () => { - const invoke = resolveTauriInvoke(); - if (!invoke || !projectPath.trim()) { - setLoading(false); - return; - } - setLoading(true); - setError(''); - try { - const [nextView, nextFiles] = await Promise.all([ - invoke('hydrate_design_agent_session', { - projectPath, - }), - invoke('list_design_workspace', { - projectPath, - }), - ]); - setView(nextView); - setFiles(nextFiles); - setExpandedPaths((current) => { - const next = new Set(current); - if (current.size === 0) { - nextFiles - .filter((entry) => entry.kind === 'directory') - .forEach((entry) => next.add(entry.path)); - } else { - nextFiles - .filter( - (entry) => entry.kind === 'directory' && next.has(entry.path), - ) - .forEach((entry) => next.add(entry.path)); - } - return next; - }); - setSelectedPath((current) => - current && - nextFiles.some( - (entry) => entry.kind !== 'directory' && entry.path === current, - ) - ? current - : null, - ); - } catch (nextError) { - setError( - nextError instanceof Error ? nextError.message : String(nextError), - ); - setFiles([]); - } finally { - setLoading(false); - } - }, [projectPath]); + const loadWorkspace = useCallback( + async ({ + showLoading, + hydrateSession, + }: { + showLoading: boolean; + hydrateSession: boolean; + }) => { + const invoke = resolveTauriInvoke(); + if (!invoke || !projectPath.trim()) { + if (showLoading) setLoading(false); + return; + } + if (showLoading) { + setLoading(true); + setError(''); + } + try { + const nextFilesPromise = invoke( + 'list_design_workspace', + { projectPath }, + ); + const nextViewPromise = hydrateSession + ? invoke('hydrate_design_agent_session', { + projectPath, + }) + : Promise.resolve(null); + const [nextFiles, nextView] = await Promise.all([ + nextFilesPromise, + nextViewPromise, + ]); + if (hydrateSession) setView(nextView); + setFiles(nextFiles); + setExpandedPaths((current) => { + const next = new Set(current); + if (current.size === 0) { + nextFiles + .filter((entry) => entry.kind === 'directory') + .forEach((entry) => next.add(entry.path)); + } else { + nextFiles + .filter( + (entry) => entry.kind === 'directory' && next.has(entry.path), + ) + .forEach((entry) => next.add(entry.path)); + } + return next; + }); + setSelectedPath((current) => + current && + nextFiles.some( + (entry) => entry.kind !== 'directory' && entry.path === current, + ) + ? current + : null, + ); + } catch (nextError) { + setError( + nextError instanceof Error ? nextError.message : String(nextError), + ); + if (showLoading) setFiles([]); + } finally { + if (showLoading) setLoading(false); + } + }, + [projectPath], + ); useEffect(() => { - void loadWorkspace(); + void loadWorkspace({ showLoading: true, hydrateSession: true }); }, [loadWorkspace]); useEffect(() => { @@ -245,11 +262,20 @@ export function DesignWorkspacePanel({ } let disposed = false; let cleanup: (() => void) | null = null; + const scheduleRefresh = () => { + if (refreshTimerRef.current !== null) { + window.clearTimeout(refreshTimerRef.current); + } + refreshTimerRef.current = window.setTimeout(() => { + refreshTimerRef.current = null; + void loadWorkspace({ showLoading: false, hydrateSession: false }); + }, 250); + }; void listen<{ projectPath: string; kind: string }>( 'design-agent-update', (event) => { if (!disposed && event.payload.projectPath === projectPath) { - void loadWorkspace(); + scheduleRefresh(); } }, ) @@ -263,6 +289,10 @@ export function DesignWorkspacePanel({ .catch(() => undefined); return () => { disposed = true; + if (refreshTimerRef.current !== null) { + window.clearTimeout(refreshTimerRef.current); + refreshTimerRef.current = null; + } cleanup?.(); }; }, [loadWorkspace, projectPath]); @@ -316,7 +346,9 @@ export function DesignWorkspacePanel({