From 563bc6f8fd74393e0cb1937457139d88f93c640e Mon Sep 17 00:00:00 2001 From: Yahya Alhinai Date: Sun, 28 Jun 2026 04:03:25 +0000 Subject: [PATCH] feat: add collapsible pod stream sidebars --- frontend/src/components/PodView.tsx | 657 +++++++++++++++++----------- scripts/verify-frontend.mjs | 4 +- 2 files changed, 408 insertions(+), 253 deletions(-) diff --git a/frontend/src/components/PodView.tsx b/frontend/src/components/PodView.tsx index 95834fe..ebcb90f 100644 --- a/frontend/src/components/PodView.tsx +++ b/frontend/src/components/PodView.tsx @@ -1,4 +1,4 @@ -import { useEffect, useRef, useState } from 'react'; +import { type CSSProperties, useEffect, useRef, useState } from 'react'; import { RoomEvent, Track } from 'livekit-client'; import { ArrowLeftIcon, @@ -9,6 +9,8 @@ import { FileTextIcon, MessageSquareIcon, MonitorUpIcon, + PanelLeftIcon, + PanelRightIcon, RadioTowerIcon, SparklesIcon, TriangleAlertIcon, @@ -42,6 +44,14 @@ import { EmptyTitle, } from '@/components/ui/empty'; import { Separator } from '@/components/ui/separator'; +import { + Sidebar, + SidebarContent, + SidebarHeader, + SidebarInset, + SidebarProvider, + SidebarRail, +} from '@/components/ui/sidebar'; import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'; import { cn } from '@/lib/utils'; @@ -69,6 +79,15 @@ function snapshot(room: Room, fallbackName: string): PInfo[] { return [local, ...remotes]; } +function readStoredBool(key: string, fallback: boolean): boolean { + try { + const value = localStorage.getItem(key); + return value === null ? fallback : value === 'true'; + } catch { + return fallback; + } +} + export function PodView({ team, me, @@ -86,6 +105,12 @@ export function PodView({ const [sharing, setSharing] = useState(false); const [playingBeat, setPlayingBeat] = useState(false); const [note, setNote] = useState(null); + const [leftStreamOpen, setLeftStreamOpen] = useState(() => + readStoredBool('podman.myStreamOpen', true), + ); + const [rightStreamOpen, setRightStreamOpen] = useState(() => + readStoredBool('podman.teamStreamOpen', true), + ); const { active, hermes, voiceCue, actionUrl, respond } = useInterventions(room); const activity = usePodActivity(team.id, me); @@ -136,6 +161,14 @@ export function PodView({ }; }, []); + useEffect(() => { + localStorage.setItem('podman.myStreamOpen', String(leftStreamOpen)); + }, [leftStreamOpen]); + + useEffect(() => { + localStorage.setItem('podman.teamStreamOpen', String(rightStreamOpen)); + }, [rightStreamOpen]); + async function toggleBeat() { if (!room) return; setNote(null); @@ -200,230 +233,283 @@ export function PodView({ const podmanPresent = participants.some((p) => p.name.toLowerCase() === 'podman'); return ( -
-
-
-
- - - - - Leave pod - -
-
-

{team.name}

- - {room ? 'live' : 'local'} - -
-

{team.repo}

-
-
- -
- - -
-
- - {devMode && ( - - - Local mode - LiveKit is not configured for this session. - - )} - - {note && ( - - - Room notice - {note} - - )} - -
-
-
- - - -
- - - - Room state - People and media currently visible to PodMan. - - - {participants.length === 0 ? ( - - - - - - Connecting - Waiting for LiveKit room state. - - - ) : ( -
- {participants.map((p) => ( - - ))} + + setLeftStreamOpen((open) => !open)} + emptyTitle="No personal signal yet" + emptyDescription="Start the local git watcher or share your IDE screen to populate this lane." + /> + + +
+
+
+ + + + + + {leftStreamOpen ? 'Collapse my stream' : 'Expand my stream'} + + + + + + + Leave pod + +
+
+

{team.name}

+ + {room ? 'live' : 'local'} +
- )} - - -

- Roster: {team.members.join(', ') || 'No saved members'} -

-
- - -
- - -
- {activity.error &&

{activity.error}

} -
- - -
-
-
-
+
+ +
+ + + +
+ + + {devMode && ( + + + Local mode + LiveKit is not configured for this session. + + )} + + {note && ( + + + Room notice + {note} + + )} + +
+
+
+ + + +
+ + + + Room state + People and media currently visible to PodMan. + + + {participants.length === 0 ? ( + + + + + + Connecting + Waiting for LiveKit room state. + + + ) : ( +
+ {participants.map((p) => ( + + ))} +
+ )} +
+ +

+ Roster: {team.members.join(', ') || 'No saved members'} +

+
+
+ + {activity.error && ( +

{activity.error}

+ )} +
+ + +
+
+
+ + setRightStreamOpen((open) => !open)} + emptyTitle="No teammate signal yet" + emptyDescription="Waiting for other members' screen, git, or collision events." + /> + + ); } @@ -475,54 +561,123 @@ function StatusLine({ label, value }: { label: string; value: string }) { ); } -function ActivityStream({ +function ActivitySidebar({ + side, title, + collapsedLabel, description, events, connected, + open, + onToggle, emptyTitle, emptyDescription, }: { + side: 'left' | 'right'; title: string; + collapsedLabel: string; description: string; events: PodActivityEvent[]; connected: boolean; + open: boolean; + onToggle: () => void; emptyTitle: string; emptyDescription: string; }) { + const ToggleIcon = side === 'left' ? PanelLeftIcon : PanelRightIcon; + const critical = events.filter((event) => event.severity === 'critical').length; + return ( - - - {title} - {description} - - - {connected ? 'streaming' : 'syncing'} + + +
+
+
+

{title}

+ + {connected ? 'streaming' : 'syncing'} + +
+

{description}

+
+ + + + + {open ? `Collapse ${title}` : `Expand ${title}`} + +
+ + +
+ +
+ {events.length ? ( +
{events.map((event) => ( ))}
-
- ) : ( - - - - - - {emptyTitle} - {emptyDescription} - - - )} - -
+ ) : ( + + + + + + {emptyTitle} + {emptyDescription} + + + )} + + +
+ {events.slice(0, 8).map((event) => { + const Icon = activityIcon(event.kind); + return ( + + + + + + {event.title} + + + ); + })} +
+ + + ); } diff --git a/scripts/verify-frontend.mjs b/scripts/verify-frontend.mjs index 0a78514..5ea37f1 100644 --- a/scripts/verify-frontend.mjs +++ b/scripts/verify-frontend.mjs @@ -262,8 +262,8 @@ try { await podCard.getByPlaceholder('Your name').fill(verifyMember); await podCard.getByRole('button', { name: 'Add and join' }).click(); await page.getByRole('button', { name: 'Share screen' }).waitFor({ timeout: 15_000 }); - await page.getByText('My stream').waitFor({ timeout: 15_000 }); - await page.getByText('Team stream').waitFor({ timeout: 15_000 }); + await page.getByRole('heading', { name: 'My stream' }).waitFor({ timeout: 15_000 }); + await page.getByRole('heading', { name: 'Team stream' }).waitFor({ timeout: 15_000 }); const joinedText = await page.locator('body').innerText(); const hasPodView =