import { useEffect, useState } from 'react'; import { Show, SignUp, SignInButton, SignUpButton, UserButton, useAuth, useUser, } from '@clerk/react'; import type { Room } from 'livekit-client'; import { AlertCircleIcon, RefreshCwIcon, SparklesIcon, } from 'lucide-react'; import type { Pod, PodInput } from '@podman/shared'; import { joinPod } from './lib/pod.js'; import * as api from './lib/api.js'; import { PodCard } from './components/PodCard.js'; import { CreatePodForm } from './components/CreatePodForm.js'; import { PodView } from './components/PodView.js'; import { GraphView } from './components/GraphView.js'; import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'; import { Button } from '@/components/ui/button'; import { Card, CardAction, CardContent, CardDescription, CardHeader, CardTitle, } from '@/components/ui/card'; import { Empty, EmptyContent, EmptyDescription, EmptyHeader, EmptyMedia, EmptyTitle, } from '@/components/ui/empty'; import { Skeleton } from '@/components/ui/skeleton'; const SESSION_KEY = 'podman.session'; function pathPodId(): string | null { const [segment] = window.location.pathname.split('/').filter(Boolean); return segment ? decodeURIComponent(segment) : null; } function setPodPath(podId: string, replace = false): void { const next = `/${encodeURIComponent(podId)}`; if (window.location.pathname === next) return; window.history[replace ? 'replaceState' : 'pushState']({}, '', next); } function setHomePath(): void { if (window.location.pathname === '/') return; window.history.pushState({}, '', '/'); } function replacePath(path: string): void { window.history.replaceState({}, '', path || '/'); } function firstNameFrom(value: string | null | undefined): string { return value?.trim().split(/\s+/).filter(Boolean)[0] ?? ''; } export default function App() { const { getToken, isLoaded, isSignedIn } = useAuth(); const { user } = useUser(); const [pods, setPods] = useState([]); const [loading, setLoading] = useState(true); const [pending, setPending] = useState>(new Set()); const [error, setError] = useState(null); const [presence, setPresence] = useState>({}); const [joinedPodId, setJoinedPodId] = useState(null); const [member, setMember] = useState(''); const [devMode, setDevMode] = useState(false); const [room, setRoom] = useState(null); const [restoring, setRestoring] = useState(false); const [graphPodId, setGraphPodId] = useState(null); const joinedPod = joinedPodId ? (pods.find((p) => p.id === joinedPodId) ?? null) : null; const userEmail = user?.primaryEmailAddress?.emailAddress; const defaultMemberName = user?.firstName?.trim() || firstNameFrom(user?.fullName) || firstNameFrom(userEmail?.split('@')[0]); const currentUserProfile = { displayName: defaultMemberName, email: userEmail, imageUrl: user?.imageUrl, }; useEffect(() => { api.setAuthTokenGetter(isSignedIn ? getToken : null); return () => api.setAuthTokenGetter(null); }, [getToken, isSignedIn]); async function refresh() { setLoading(true); try { const [nextPods, nextPresence] = await Promise.all([ api.listPods(), api.getPresence().catch(() => presence), ]); setPods(nextPods); setPresence(nextPresence); setError(null); } catch (e) { setError((e as Error).message); } finally { setLoading(false); } } const startPending = (key: string) => setPending((s) => new Set(s).add(key)); const endPending = (key: string) => setPending((s) => { const n = new Set(s); n.delete(key); return n; }); async function connectToPod(podId: string, who: string, replaceRoute = false) { const previousPath = window.location.pathname; setPodPath(podId, replaceRoute); try { const result = await joinPod( podId, who, who, isSignedIn ? getToken : undefined, currentUserProfile, ); setRoom(result.room); setDevMode(result.mode === 'dev'); setMember(who); setJoinedPodId(podId); sessionStorage.setItem(SESSION_KEY, JSON.stringify({ podId, member: who })); } catch (e) { replacePath(previousPath); throw e; } } useEffect(() => { if (!isSignedIn) { setLoading(false); return; } void refresh(); }, [isSignedIn]); useEffect(() => { if (!isSignedIn || joinedPodId) return; let alive = true; const tick = async () => { try { const p = await api.getPresence(); if (alive) { setPresence(p); } } catch { /* presence is best-effort */ } }; void tick(); const id = window.setInterval(() => void tick(), 5000); return () => { alive = false; window.clearInterval(id); }; }, [isSignedIn, joinedPodId]); useEffect(() => { if (!isSignedIn) return; const routedPodId = pathPodId(); const raw = sessionStorage.getItem(SESSION_KEY); if (!raw) { return; } let saved: { podId: string; member: string }; try { saved = JSON.parse(raw); } catch { sessionStorage.removeItem(SESSION_KEY); return; } const podId = routedPodId ?? saved.podId; setRestoring(true); void (async () => { try { await connectToPod(podId, saved.member, !!routedPodId); } catch { sessionStorage.removeItem(SESSION_KEY); } finally { setRestoring(false); } })(); }, [isSignedIn]); useEffect(() => { const onPopState = () => { if (!isSignedIn) return; const routedPodId = pathPodId(); if (!routedPodId) { room?.disconnect(); setRoom(null); setJoinedPodId(null); return; } const raw = sessionStorage.getItem(SESSION_KEY); if (!raw) return; try { const saved = JSON.parse(raw) as { member: string }; if (saved.member && routedPodId !== joinedPodId) { void connectToPod(routedPodId, saved.member, true); } } catch { sessionStorage.removeItem(SESSION_KEY); } }; window.addEventListener('popstate', onPopState); return () => window.removeEventListener('popstate', onPopState); }, [isSignedIn, joinedPodId, room]); async function run(key: string, fn: () => Promise) { startPending(key); setError(null); try { await fn(); } catch (e) { setError((e as Error).message); } finally { endPending(key); } } const upsert = (p: Pod) => setPods((cur) => cur.map((x) => (x.id === p.id ? p : x))); async function handleCreate(input: PodInput): Promise { startPending('new'); setError(null); try { const created = await api.createPod(input, currentUserProfile); setPods((cur) => [...cur, created]); } catch (e) { setError((e as Error).message); throw e; } finally { endPending('new'); } } const handleUpdate = (id: string, patch: PodInput) => run(id, async () => upsert(await api.updatePod(id, patch))); const handleDelete = (id: string) => run(id, async () => { await api.deletePod(id); setPods((cur) => cur.filter((x) => x.id !== id)); if (pathPodId() === id) setHomePath(); }); const handleAddMember = (id: string, name: string) => run(id, async () => upsert(await api.addMember(id, name, currentUserProfile))); const handleRemoveMember = (id: string, name: string) => run(id, async () => upsert(await api.removeMember(id, name))); async function handleJoin(pod: Pod, who: string) { startPending(pod.id); setError(null); try { await connectToPod(pod.id, who); } catch (e) { setError((e as Error).message); } finally { endPending(pod.id); } } async function handleAddAndJoin(pod: Pod) { startPending(pod.id); setError(null); try { if (!defaultMemberName) { throw new Error('Sign in with Clerk before joining a pod.'); } upsert(await api.addMember(pod.id, defaultMemberName, currentUserProfile)); await connectToPod(pod.id, defaultMemberName); } catch (e) { setError((e as Error).message); } finally { endPending(pod.id); } } function handleLeave() { room?.disconnect(); setRoom(null); setJoinedPodId(null); sessionStorage.removeItem(SESSION_KEY); setHomePath(); void refresh(); } const showReconnecting = restoring || (joinedPodId !== null && joinedPod === null); if (!isLoaded) { return (
); } if (!isSignedIn) { return ; } if (joinedPod) { return ( ); } if (graphPodId) { return setGraphPodId(null)} />; } return (
PM

PodMan

{error && ( PodMan could not complete that action {error} )} {showReconnecting ? ( Reconnecting to your pod Restoring the saved LiveKit session. ) : (

Workspaces

Active pods

{loading ? (
) : pods.length ? (
{pods.map((pod) => ( ))}
) : ( No pods yet Create the first room for this team. )}
)}
); } function AuthGate() { return (
PM

PodMan

Team memory

Create your account to enter PodMan

PodMan saves your context across pods so agents can learn from your work in every room you join.

); } function PodSkeleton() { return (
); }