| ... | @@ -12,6 +12,10 @@ import { | ... | @@ -12,6 +12,10 @@ import { |
| 12 | } from "react"; | 12 | } from "react"; |
| 13 | | 13 | |
| 14 | import { BrowserSurfaceSlot } from "~/browser/BrowserSurfaceSlot"; | 14 | import { BrowserSurfaceSlot } from "~/browser/BrowserSurfaceSlot"; |
| | 15 | import { |
| | 16 | findActiveBrowserRecordingRuntimeTabId, |
| | 17 | useActiveBrowserRecordingTabIds, |
| | 18 | } from "~/browser/browserRecording"; |
| 15 | import { useBrowserSurfaceStore } from "~/browser/browserSurfaceStore"; | 19 | import { useBrowserSurfaceStore } from "~/browser/browserSurfaceStore"; |
| 16 | import type { BrowserViewportResizeDirection } from "~/browser/browserViewportLayout"; | 20 | import type { BrowserViewportResizeDirection } from "~/browser/browserViewportLayout"; |
| 17 | import { previewRuntimeTabId } from "~/browser/previewRuntimeTabId"; | 21 | import { previewRuntimeTabId } from "~/browser/previewRuntimeTabId"; |
| ... | @@ -151,6 +155,10 @@ function BrowserMiniPlayer({ | ... | @@ -151,6 +155,10 @@ function BrowserMiniPlayer({ |
| 151 | const previewState = useThreadPreviewState(threadRef); | 155 | const previewState = useThreadPreviewState(threadRef); |
| 152 | const snapshot = previewState.sessions[tabId] ?? null; | 156 | const snapshot = previewState.sessions[tabId] ?? null; |
| 153 | const runtimeTabId = previewRuntimeTabId(threadRef, previewState.serverEpoch, tabId); | 157 | const runtimeTabId = previewRuntimeTabId(threadRef, previewState.serverEpoch, tabId); |
| | 158 | const recordingTabIds = useActiveBrowserRecordingTabIds(); |
| | 159 | const recording = |
| | 160 | recordingTabIds.has(runtimeTabId) || |
| | 161 | findActiveBrowserRecordingRuntimeTabId(threadRef, tabId) !== null; |
| 154 | const desktopOverlay = previewState.desktopByTabId[tabId] ?? null; | 162 | const desktopOverlay = previewState.desktopByTabId[tabId] ?? null; |
| 155 | const fittedSourceContent = useBrowserSurfaceStore( | 163 | const fittedSourceContent = useBrowserSurfaceStore( |
| 156 | (state) => state.byTabId[runtimeTabId]?.fittedSourceContent ?? null, | 164 | (state) => state.byTabId[runtimeTabId]?.fittedSourceContent ?? null, |
| ... | @@ -189,6 +197,7 @@ function BrowserMiniPlayer({ | ... | @@ -189,6 +197,7 @@ function BrowserMiniPlayer({ |
| 189 | sourceSize={sourceSize} | 197 | sourceSize={sourceSize} |
| 190 | composerOverlayElement={composerOverlayElement} | 198 | composerOverlayElement={composerOverlayElement} |
| 191 | label="Floating browser preview" | 199 | label="Floating browser preview" |
| | 200 | recording={recording} |
| 192 | onOpenInPanel={openInPanel} | 201 | onOpenInPanel={openInPanel} |
| 193 | pillActions={ | 202 | pillActions={ |
| 194 | <Tooltip> | 203 | <Tooltip> |
| ... | @@ -314,6 +323,7 @@ function MiniPlayerShell({ | ... | @@ -314,6 +323,7 @@ function MiniPlayerShell({ |
| 314 | label, | 323 | label, |
| 315 | onOpenInPanel, | 324 | onOpenInPanel, |
| 316 | pillActions, | 325 | pillActions, |
| | 326 | recording = false, |
| 317 | cornerRadius = frameCornerRadius, | 327 | cornerRadius = frameCornerRadius, |
| 318 | children, | 328 | children, |
| 319 | }: { | 329 | }: { |
| ... | @@ -324,6 +334,7 @@ function MiniPlayerShell({ | ... | @@ -324,6 +334,7 @@ function MiniPlayerShell({ |
| 324 | readonly label: string; | 334 | readonly label: string; |
| 325 | readonly onOpenInPanel: () => void; | 335 | readonly onOpenInPanel: () => void; |
| 326 | readonly pillActions?: ReactNode; | 336 | readonly pillActions?: ReactNode; |
| | 337 | readonly recording?: boolean; |
| 327 | /** The clip radius for a given frame; the pill stays inside the curve. */ | 338 | /** The clip radius for a given frame; the pill stays inside the curve. */ |
| 328 | readonly cornerRadius?: (frame: PreviewMiniPlayerSize) => number; | 339 | readonly cornerRadius?: (frame: PreviewMiniPlayerSize) => number; |
| 329 | readonly children: (frame: PreviewMiniPlayerFrame) => ReactNode; | 340 | readonly children: (frame: PreviewMiniPlayerFrame) => ReactNode; |
| ... | @@ -443,9 +454,18 @@ function MiniPlayerShell({ | ... | @@ -443,9 +454,18 @@ function MiniPlayerShell({ |
| 443 | style={{ right: pillInset, top: pillInset }} | 454 | style={{ right: pillInset, top: pillInset }} |
| 444 | > | 455 | > |
| 445 | <div | 456 | <div |
| 446 | aria-hidden="true" | 457 | role={recording ? "status" : undefined} |
| 447 | className="absolute right-0 top-0 size-2 rounded-full bg-foreground/25 shadow-sm ring-1 ring-background/70 transition-opacity group-hover:opacity-0 group-focus-within:opacity-0" | 458 | aria-label={recording ? "Recording preview" : undefined} |
| 448 | /> | 459 | aria-hidden={!recording} |
| | 460 | className="absolute right-0 top-0 size-2 transition-opacity group-hover:opacity-0 group-focus-within:opacity-0" |
| | 461 | > |
| | 462 | <span |
| | 463 | className={cn( |
| | 464 | "block size-2 rounded-full shadow-sm ring-1 ring-background/70", |
| | 465 | recording ? "bg-red-500 motion-safe:animate-status-pulse" : "bg-foreground/25", |
| | 466 | )} |
| | 467 | /> |
| | 468 | </div> |
| 449 | <div | 469 | <div |
| 450 | className="pointer-events-none absolute right-0 top-0 flex h-8 cursor-grab items-center gap-0.5 rounded-lg border border-border/80 bg-popover/92 p-0.5 opacity-0 shadow-lg/20 backdrop-blur-xl transition-opacity group-hover:pointer-events-auto group-hover:opacity-100 group-focus-within:pointer-events-auto group-focus-within:opacity-100 active:cursor-grabbing" | 470 | className="pointer-events-none absolute right-0 top-0 flex h-8 cursor-grab items-center gap-0.5 rounded-lg border border-border/80 bg-popover/92 p-0.5 opacity-0 shadow-lg/20 backdrop-blur-xl transition-opacity group-hover:pointer-events-auto group-hover:opacity-100 group-focus-within:pointer-events-auto group-focus-within:opacity-100 active:cursor-grabbing" |
| 451 | onPointerDown={(event) => beginGesture(event, null)} | 471 | onPointerDown={(event) => beginGesture(event, null)} |
| ... | @@ -453,6 +473,11 @@ function MiniPlayerShell({ | ... | @@ -453,6 +473,11 @@ function MiniPlayerShell({ |
| 453 | onPointerUp={endGesture} | 473 | onPointerUp={endGesture} |
| 454 | onPointerCancel={endGesture} | 474 | onPointerCancel={endGesture} |
| 455 | > | 475 | > |
| | 476 | {recording ? ( |
| | 477 | <span aria-hidden className="flex size-6 shrink-0 items-center justify-center"> |
| | 478 | <span className="size-2 rounded-full bg-red-500 motion-safe:animate-status-pulse" /> |
| | 479 | </span> |
| | 480 | ) : null} |
| 456 | <Tooltip> | 481 | <Tooltip> |
| 457 | <TooltipTrigger | 482 | <TooltipTrigger |
| 458 | render={ | 483 | render={ |