import { AgentPanel } from "@agent-native/core/client/agent-chat";
import { appPath, agentNativePath } from "@agent-native/core/client/api-path";
import { writeClipboardText } from "@agent-native/core/client/clipboard";
import {
useActionMutation,
useActionQuery,
useSession,
getBrowserTabId,
readClientAppState,
writeClientAppState,
useChangeVersions,
} from "@agent-native/core/client/hooks";
import { useT } from "@agent-native/core/client/i18n";
import { buildSignInReturnHref } from "@agent-native/core/client/ui";
import {
BUILDER_CREDITS_UPGRADE_URL,
type BuilderCreditsStatus,
} from "@shared/builder-credits";
import { isStoredButUnservableFinalizeError } from "@shared/finalize-recovery";
import {
isLoomEmbedBackedRecording,
isLoomRecordingSource,
} from "@shared/loom";
import {
CLIP_SHARE_REF,
DASHBOARD_REDIRECT_PARAM,
DASHBOARD_REDIRECT_VALUE,
REF_PARAM,
} from "@shared/share-attribution";
import {
IconShare3,
IconArrowLeft,
IconChevronDown,
IconCalendar,
IconScissors,
IconAlertTriangle,
IconHelpCircle,
IconClipboardCopy,
IconFileText,
IconSparkles,
IconExternalLink,
} from "@tabler/icons-react";
import { useQuery } from "@tanstack/react-query";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import {
Link,
useParams,
useNavigate,
NavLink,
useSearchParams,
} from "react-router";
import { toast } from "sonner";
import { EditableRecordingTitle } from "@/components/editable-recording-title";
import { EditorLayout } from "@/components/editor/editor-layout";
import { CommentsPanel } from "@/components/player/comments-panel";
import { RecordingOptionsMenu } from "@/components/player/delete-recording-menu";
import { InsightsPanel } from "@/components/player/insights-panel";
import { ReactionsTray } from "@/components/player/reactions-tray";
import { RecordingViewsBadge } from "@/components/player/recording-views-badge";
import { SettingsPanel } from "@/components/player/settings-panel";
import { ShareRecordingPopover } from "@/components/player/share-dialog";
import {
TimestampedCommentButton,
TimestampedCommentBar,
} from "@/components/player/timestamped-comment-button";
import { TranscriptPanel } from "@/components/player/transcript-panel";
import {
VideoPlayer,
type VideoPlayerHandle,
} from "@/components/player/video-player";
import { StorageSetupCard } from "@/components/recorder/storage-setup-card";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { Spinner } from "@/components/ui/spinner";
import { Switch } from "@/components/ui/switch";
import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs";
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { isDefaultTitle, useAutoTitleBridge } from "@/hooks/use-auto-title";
import { usePlayerShortcuts } from "@/hooks/use-player-shortcuts";
import { useViewTracking } from "@/hooks/use-view-tracking";
import enMessages from "@/i18n/en-US";
import { parsePlaybackSpeed } from "@/lib/playback-speed";
import { recordingShareUrl } from "@/lib/recording-link";
import { isStorageSetupFailureReason } from "@/lib/storage-failures";
import { cn } from "@/lib/utils";
import { STALE_PENDING_TRANSCRIPT_REASON } from "../../shared/transcript-status";
const UPLOAD_STUCK_TIMEOUT_MS = 5 * 60 * 1000;
const PROCESSING_STUCK_TIMEOUT_MS = 12 * 60 * 1000;
const READY_MEDIA_SETTLE_POLL_MS = 20 * 1000;
const READY_MEDIA_SETTLE_POLL_INTERVAL_MS = 1000;
export function meta() {
return [{ title: enMessages.recordingRoute.pageTitle }];
}
type SidePanel = "transcript" | "comments" | "insights" | "agent" | "settings";
type WorkflowKind = "pr" | "sop" | "ticket" | "email";
const WORKFLOW_MENU_ITEMS: Array<{
kind: WorkflowKind;
labelKey: string;
tooltipKey?: string;
}> = [
{ kind: "pr", labelKey: "recordingPage.generatePrSummary" },
{
kind: "sop",
labelKey: "recordingPage.generateSop",
tooltipKey: "recordingPage.generateSopTooltip",
},
{ kind: "ticket", labelKey: "recordingPage.generateTicket" },
{ kind: "email", labelKey: "recordingPage.generateEmail" },
];
interface GeneratedWorkflowState {
kind?: WorkflowKind;
status?: "generating" | "ready" | "failed" | string;
content?: string;
recordingId?: string;
requestedAt?: string;
error?: string;
}
interface SilenceRemovalStatus {
kind?: "remove-silences";
status?: "queued" | "working" | "completed" | "failed";
message?: string | null;
updatedAt?: string;
}
function useIsCompactRecordingLayout() {
const [isCompact, setIsCompact] = useState(false);
useEffect(() => {
const query = window.matchMedia("(max-width: 1279px)");
const update = () => setIsCompact(query.matches);
update();
query.addEventListener("change", update);
return () => query.removeEventListener("change", update);
}, []);
return isCompact;
}
function isNativeSaveFailureReason(reason: string | null | undefined): boolean {
return /native recording upload|native fullscreen|screencapture|avconvert/i.test(
reason ?? "",
);
}
function failureDetail(reason: string | null | undefined): string | null {
const trimmed = reason?.trim();
if (!trimmed) return null;
return trimmed.length > 1200 ? `${trimmed.slice(0, 1200)}...` : trimmed;
}
function nativeSaveFailureMessage(reason: string | null | undefined): string {
const text = reason ?? "";
if (
/finalization callback failed|missing required metadata|missing playback metadata|corrupted or incomplete|missing moov/i.test(
text,
)
) {
return "macOS could not finish writing this desktop recording. The local file is incomplete, so discard it from the Clips menu and record again.";
}
if (/too large|compression/i.test(text)) {
return "Clips tried to compress this desktop recording, but it is still too large to upload. The original is saved locally and can be retried from the Clips menu.";
}
return "The desktop recorder finished and saved a local copy, but Clips could not upload it. You can retry from the Clips menu without recording again.";
}
function InsightsUnavailableState() {
const t = useT();
return (
{t("sharePage.ownerInsights")}
{t("sharePage.ownerInsightsDescription")}
);
}
function parseTimeParam(raw: string | null): number {
if (!raw) return 0;
const value = raw.trim();
if (!value) return 0;
if (/^\d+(\.\d+)?$/.test(value)) {
return Math.floor(parseFloat(value) * 1000);
}
if (/^\d+:\d+(:\d+)?$/.test(value)) {
const parts = value.split(":").map((part) => parseInt(part, 10));
if (parts.length === 2) return (parts[0] * 60 + parts[1]) * 1000;
if (parts.length === 3) {
return (parts[0] * 3600 + parts[1] * 60 + parts[2]) * 1000;
}
}
const match = value.match(/^(?:(\d+)h)?(?:(\d+)m)?(?:(\d+)s)?$/i);
if (!match) return 0;
const hours = parseInt(match[1] ?? "0", 10);
const minutes = parseInt(match[2] ?? "0", 10);
const seconds = parseInt(match[3] ?? "0", 10);
return (hours * 3600 + minutes * 60 + seconds) * 1000;
}
export default function RecordingPage() {
const t = useT();
useAutoTitleBridge();
const { recordingId } = useParams<{ recordingId: string }>();
const navigate = useNavigate();
const [searchParams] = useSearchParams();
const startMs = parseTimeParam(searchParams.get("t"));
const panelParam = searchParams.get("panel");
const { session, isLoading: sessionLoading } = useSession();
const playerRef = useRef(null);
const [panel, setPanel] = useState("agent");
const [theaterMode, setTheaterMode] = useState(false);
const [editing, setEditing] = useState(false);
const [currentMs, setCurrentMs] = useState(0);
const [commentOpen, setCommentOpen] = useState(false);
const [commentAtMs, setCommentAtMs] = useState(0);
const isCompactLayout = useIsCompactRecordingLayout();
// Resolve the playback position for reactions/comments. Native