From e758750c16aa246b4cfdde2c154bd3f23ee5b3cd Mon Sep 17 00:00:00 2001 From: Aastha Khatri Date: Sun, 17 May 2026 22:52:44 +0530 Subject: [PATCH 1/5] fix: show active collaborator count in status bar --- .../components/MainPlaygroundPage.tsx | 52 ++++++++++++++++--- 1 file changed, 44 insertions(+), 8 deletions(-) diff --git a/modules/playground/components/MainPlaygroundPage.tsx b/modules/playground/components/MainPlaygroundPage.tsx index a4d5089..2329261 100644 --- a/modules/playground/components/MainPlaygroundPage.tsx +++ b/modules/playground/components/MainPlaygroundPage.tsx @@ -2,6 +2,7 @@ import { usePlaygroundActions } from "@/modules/playground/hooks/usePlaygroundActions"; import { Button } from "@/components/ui/button"; import { ErrorBoundary } from "@/components/error-boundary"; +import { fetchCollabToken, getOrCreateYDoc } from "@/lib/yjs"; import { ResizableHandle, @@ -23,17 +24,10 @@ const PlaygroundEditor = dynamic( { ssr: false } ); -import { - AlertCircle, - FolderOpen, -} from "lucide-react"; -import { CollaborationAvatars } from "@/modules/playground/components/collaboration-avatars"; -import { TemplateFileTree } from "@/modules/playground/components/playground-explorer"; import { usePlayground } from "@/modules/playground/hooks/usePlayground"; import { useAI } from "@/modules/playground/hooks/useAI"; import AIChatPanel from "@/modules/playground/components/ai-chat-panel"; import AISettingsDialog from "@/modules/playground/components/ai-settings-dialog"; -import { useParams } from "next/navigation"; import WebContainerPreview from "@/modules/webcontainers/components/webcontainer-preview"; import { useWebContainer } from "@/modules/webcontainers/hooks/useWebContainer"; import { useFileExplorer } from "@/modules/playground/hooks/useFileExplorer"; @@ -82,6 +76,7 @@ const [showAISettings, setShowAISettings] = useState(false); const [isCommandPaletteOpen, setIsCommandPaletteOpen] = useState(false); const [isDeployDialogOpen, setIsDeployDialogOpen] = useState(false); const [cursorPosition, setCursorPosition] = useState({ line: 1, col: 1 }); +const [collaboratorCount, setCollaboratorCount] = useState(0); const sidebar = useSidebar(); const { @@ -119,6 +114,47 @@ const [cursorPosition, setCursorPosition] = useState({ line: 1, col: 1 }); } }, [id, setPlaygroundId, templateData, setTemplateData, openFiles.length]); + useEffect(() => { + if (!id) return; + + let cleanup = () => {}; + + void (async () => { + try { + const token = await fetchCollabToken(id); + const { provider } = getOrCreateYDoc(id, token); + + const updateCollaborators = () => { + const states = Array.from(provider.awareness.getStates().values()); + + const activeUsers = states + .filter((s: any) => s.user) + .map((s: any) => s.user); + + const uniqueUsers = Array.from( + new Map(activeUsers.map((u: any) => [u.name, u])).values() + ); + + setCollaboratorCount(uniqueUsers.length); + }; + + provider.awareness.on("change", updateCollaborators); + + updateCollaborators(); + + cleanup = () => { + provider.awareness.off("change", updateCollaborators); + }; + } catch (error) { + console.error("Failed to track collaborators:", error); + } + })(); + + return () => { + cleanup(); + }; +}, [id]); + // Auto-open default file when preview is shown if no file is open useEffect(() => { if (isPreviewVisible && !activeFileId && templateData) { @@ -378,7 +414,7 @@ if (!playgroundData && !templateData && !error) { activeFile={activeFile} cursorPosition={cursorPosition} containerStatus={containerStatus} - collaboratorCount={0} + collaboratorCount={collaboratorCount} openFileCount={openFiles.length} /> From 77341b2b2c665238516c830c6ff7a92f9b4c5ae0 Mon Sep 17 00:00:00 2001 From: Aastha Khatri Date: Sun, 17 May 2026 23:12:25 +0530 Subject: [PATCH 2/5] fix: prevent collaborator awareness cleanup race condition --- .../playground/components/MainPlaygroundPage.tsx | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/modules/playground/components/MainPlaygroundPage.tsx b/modules/playground/components/MainPlaygroundPage.tsx index 2329261..b11a495 100644 --- a/modules/playground/components/MainPlaygroundPage.tsx +++ b/modules/playground/components/MainPlaygroundPage.tsx @@ -117,14 +117,19 @@ const [collaboratorCount, setCollaboratorCount] = useState(0); useEffect(() => { if (!id) return; - let cleanup = () => {}; + let cancelled = false; void (async () => { try { const token = await fetchCollabToken(id); + + if (cancelled) return; + const { provider } = getOrCreateYDoc(id, token); const updateCollaborators = () => { + if (cancelled) return; + const states = Array.from(provider.awareness.getStates().values()); const activeUsers = states @@ -142,16 +147,16 @@ const [collaboratorCount, setCollaboratorCount] = useState(0); updateCollaborators(); - cleanup = () => { + if (cancelled) { provider.awareness.off("change", updateCollaborators); - }; + } } catch (error) { console.error("Failed to track collaborators:", error); } })(); return () => { - cleanup(); + cancelled = true; }; }, [id]); From 1892967345d0a77235dc9ba91bf9ad8828d37ecf Mon Sep 17 00:00:00 2001 From: Aastha Khatri Date: Sun, 17 May 2026 23:22:22 +0530 Subject: [PATCH 3/5] fix: use hook destructuring for AI chat handlers --- .../components/MainPlaygroundPage.tsx | 53 ++----------------- 1 file changed, 5 insertions(+), 48 deletions(-) diff --git a/modules/playground/components/MainPlaygroundPage.tsx b/modules/playground/components/MainPlaygroundPage.tsx index b11a495..e89797f 100644 --- a/modules/playground/components/MainPlaygroundPage.tsx +++ b/modules/playground/components/MainPlaygroundPage.tsx @@ -71,6 +71,7 @@ const MainPlaygroundPage = ({ initialData, id }: MainPlaygroundPageProps) => { const [templateData, setTemplateDataState] = useState(parsedTemplate); const [error] = useState(null); const { saveTemplateData } = usePlayground(id); + const { toggleChat } = useAI(); const [isPreviewVisible, setIsPreviewVisible] = useState(false); const [showAISettings, setShowAISettings] = useState(false); const [isCommandPaletteOpen, setIsCommandPaletteOpen] = useState(false); @@ -114,51 +115,7 @@ const [collaboratorCount, setCollaboratorCount] = useState(0); } }, [id, setPlaygroundId, templateData, setTemplateData, openFiles.length]); - useEffect(() => { - if (!id) return; - - let cancelled = false; - - void (async () => { - try { - const token = await fetchCollabToken(id); - - if (cancelled) return; - - const { provider } = getOrCreateYDoc(id, token); - - const updateCollaborators = () => { - if (cancelled) return; - - const states = Array.from(provider.awareness.getStates().values()); - - const activeUsers = states - .filter((s: any) => s.user) - .map((s: any) => s.user); - - const uniqueUsers = Array.from( - new Map(activeUsers.map((u: any) => [u.name, u])).values() - ); - - setCollaboratorCount(uniqueUsers.length); - }; - - provider.awareness.on("change", updateCollaborators); - - updateCollaborators(); - - if (cancelled) { - provider.awareness.off("change", updateCollaborators); - } - } catch (error) { - console.error("Failed to track collaborators:", error); - } - })(); - - return () => { - cancelled = true; - }; -}, [id]); + // Auto-open default file when preview is shown if no file is open useEffect(() => { @@ -342,7 +299,7 @@ if (!playgroundData && !templateData && !error) { handleDownloadZip={handleDownloadZip} setShowAISettings={setShowAISettings} closeAllFiles={closeAllFiles} - toggleAIChat={() => useAI.getState().toggleChat()} + toggleAIChat={toggleChat} /> {/* ==== CONTENT ==== */} @@ -407,7 +364,7 @@ if (!playgroundData && !templateData && !error) { setIsPreviewVisible(true)} - onOpenAI={() => useAI.getState().toggleChat()} + onOpenAI={toggleChat} onDownload={handleDownloadZip} onOpenCommandPalette={() => setIsCommandPaletteOpen(true)} /> @@ -443,7 +400,7 @@ if (!playgroundData && !templateData && !error) { onSaveAll={handleSaveAll} onDownload={handleDownloadZip} onTogglePreview={() => setIsPreviewVisible((prev) => !prev)} - onToggleAI={() => useAI.getState().toggleChat()} + onToggleAI={toggleChat} onToggleSidebar={() => sidebar.toggleSidebar()} onOpenSettings={() => setShowAISettings(true)} onCloseAllFiles={closeAllFiles} From 75e800aa241d050e7501dad7704c9741077de75b Mon Sep 17 00:00:00 2001 From: Aastha Khatri Date: Thu, 21 May 2026 14:47:43 +0530 Subject: [PATCH 4/5] fix: restore missing AlertCircle and FolderOpen imports --- .../components/MainPlaygroundPage.tsx | 100 +++++++++++++----- 1 file changed, 73 insertions(+), 27 deletions(-) diff --git a/modules/playground/components/MainPlaygroundPage.tsx b/modules/playground/components/MainPlaygroundPage.tsx index e89797f..5aa6a8d 100644 --- a/modules/playground/components/MainPlaygroundPage.tsx +++ b/modules/playground/components/MainPlaygroundPage.tsx @@ -2,7 +2,6 @@ import { usePlaygroundActions } from "@/modules/playground/hooks/usePlaygroundActions"; import { Button } from "@/components/ui/button"; import { ErrorBoundary } from "@/components/error-boundary"; -import { fetchCollabToken, getOrCreateYDoc } from "@/lib/yjs"; import { ResizableHandle, @@ -24,14 +23,22 @@ const PlaygroundEditor = dynamic( { ssr: false } ); +import { + AlertCircle, + FolderOpen, +} from "lucide-react"; + +import { CollaborationAvatars } from "@/modules/playground/components/collaboration-avatars"; +import { TemplateFileTree } from "@/modules/playground/components/playground-explorer"; import { usePlayground } from "@/modules/playground/hooks/usePlayground"; import { useAI } from "@/modules/playground/hooks/useAI"; import AIChatPanel from "@/modules/playground/components/ai-chat-panel"; import AISettingsDialog from "@/modules/playground/components/ai-settings-dialog"; +import { useParams } from "next/navigation"; import WebContainerPreview from "@/modules/webcontainers/components/webcontainer-preview"; import { useWebContainer } from "@/modules/webcontainers/hooks/useWebContainer"; import { useFileExplorer } from "@/modules/playground/hooks/useFileExplorer"; - +import { fetchCollabToken, getOrCreateYDoc } from "@/lib/yjs"; import { TemplateFile, TemplateFolder, @@ -109,37 +116,76 @@ const [collaboratorCount, setCollaboratorCount] = useState(0); useEffect(() => { - setPlaygroundId(id); - if (templateData && !openFiles.length) { - setTemplateData(templateData); - } - }, [id, setPlaygroundId, templateData, setTemplateData, openFiles.length]); + setPlaygroundId(id); + if (templateData && !openFiles.length) { + setTemplateData(templateData); + } +}, [id, setPlaygroundId, templateData, setTemplateData, openFiles.length]); + +// Collaborator tracking +useEffect(() => { + if (!id) return; + + let cancelled = false; + let cleanup = () => {}; + + void (async () => { + try { + const token = await fetchCollabToken(id); + const { provider } = getOrCreateYDoc(id, token); + + const updateCollaborators = () => { + if (cancelled) return; + const states = Array.from(provider.awareness.getStates().values()); + const activeUsers = states + .filter((s: any) => s.user) + .map((s: any) => s.user); + const uniqueUsers = Array.from( + new Map(activeUsers.map((u: any) => [u.name, u])).values() + ); + setCollaboratorCount(uniqueUsers.length); + }; - + provider.awareness.on("change", updateCollaborators); + updateCollaborators(); - // Auto-open default file when preview is shown if no file is open - useEffect(() => { - if (isPreviewVisible && !activeFileId && templateData) { - const findDefaultFile = (items: any[]): TemplateFile | null => { - for (const item of items) { - if (!("folderName" in item)) { - if (["App.tsx", "App.jsx", "index.tsx", "index.jsx", "index.js", "main.tsx", "main.js", "index.html"].includes(`${item.filename}.${item.fileExtension}`)) { - return item; - } - } else { - const found = findDefaultFile(item.items); - if (found) return found; - } - } - return null; + cleanup = () => { + provider.awareness.off("change", updateCollaborators); }; + } catch (error) { + console.error("Failed to track collaborators:", error); + } + })(); - const defaultFile = findDefaultFile(templateData.items); - if (defaultFile) { - openFile(defaultFile); + return () => { + cancelled = true; + cleanup(); + }; +}, [id]); + +// Auto-open default file when preview is shown if no file is open +useEffect(() => { + if (isPreviewVisible && !activeFileId && templateData) { + const findDefaultFile = (items: any[]): TemplateFile | null => { + for (const item of items) { + if (!("folderName" in item)) { + if (["App.tsx", "App.jsx", "index.tsx", "index.jsx", "index.js", "main.tsx", "main.js", "index.html"].includes(`${item.filename}.${item.fileExtension}`)) { + return item; + } + } else { + const found = findDefaultFile(item.items); + if (found) return found; + } } + return null; + }; + + const defaultFile = findDefaultFile(templateData.items); + if (defaultFile) { + openFile(defaultFile); } - }, [isPreviewVisible, activeFileId, templateData, openFile]); + } +}, [isPreviewVisible, activeFileId, templateData, openFile]); // Create wrapper functions that pass saveTemplateData const wrappedHandleAddFile = useCallback( From f4f28de32797b72aaf2b4009f3c649e70f1d08c3 Mon Sep 17 00:00:00 2001 From: Aastha Khatri Date: Thu, 21 May 2026 15:00:20 +0530 Subject: [PATCH 5/5] fix: add cancelled check before subscribing to awareness to prevent listener leak --- modules/playground/components/MainPlaygroundPage.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/modules/playground/components/MainPlaygroundPage.tsx b/modules/playground/components/MainPlaygroundPage.tsx index 5aa6a8d..8c8f6c8 100644 --- a/modules/playground/components/MainPlaygroundPage.tsx +++ b/modules/playground/components/MainPlaygroundPage.tsx @@ -134,6 +134,8 @@ useEffect(() => { const token = await fetchCollabToken(id); const { provider } = getOrCreateYDoc(id, token); + if (cancelled) return; + const updateCollaborators = () => { if (cancelled) return; const states = Array.from(provider.awareness.getStates().values());