import { IconAlertTriangle, IconArrowLeft, IconCalendarEvent, IconChevronDown, IconChevronRight, IconCircleCheck, IconCopy, IconDownload, IconExternalLink, IconFolderOpen, IconPencil, IconInfoCircle, IconHistory, IconMicrophone2, IconPlayerPlay, IconRefresh, IconSearch, IconShieldLock, IconTrash, IconUpload, IconX, } from "@tabler/icons-react"; import { invoke } from "@tauri-apps/api/core"; import { emit, listen } from "@tauri-apps/api/event"; import { getCurrentWindow } from "@tauri-apps/api/window"; import { open as openExternal } from "@tauri-apps/plugin-shell"; import { type ReactNode, type RefObject, useCallback, useEffect, useRef, useState, } from "react"; import { FeedbackButton } from "./components/FeedbackButton"; import { CamIcon, CloseIcon, GoogleIcon, LibraryIcon, ScreenCamIcon, ScreenIcon, SettingsIcon, } from "./components/Icons"; import { MediaDeviceRow } from "./components/MediaDeviceRow"; import { ReadinessPanel } from "./components/ReadinessPanel"; import { SourceRow, type CaptureSource } from "./components/SourceRow"; import { Switch } from "./components/Switch"; import { Tooltip, TooltipContent, TooltipTrigger } from "./components/Tooltip"; import { UpdateBanner } from "./components/UpdateBanner"; import { useMediaDevices } from "./hooks/useMediaDevices"; import { useMeetingTranscription } from "./hooks/useMeetingTranscription"; import { stopAllMicMeters } from "./hooks/useMicMeter"; import { useWhisperSettings } from "./hooks/useWhisperSettings"; import { startBubbleFramePump } from "./lib/bubble-pump"; import { startBubbleWebrtc, type BubbleWebrtcHandle, } from "./lib/bubble-webrtc"; import { getCameraStreamWithFallback, isMediaConstraintFailure, } from "./lib/media-capture-constraints"; import { sendNativeNotification } from "./lib/native-notification"; import { openMeetingJoinUrl } from "./lib/open-meeting-join-url"; import { DESKTOP_CAPTURE_PERMISSION_MESSAGE, isHardCapturePermissionError, MACOS_CAPTURE_PERMISSION_MESSAGE, MACOS_SCREEN_PERMISSION_MESSAGE, MACOS_SPEECH_PERMISSION_MESSAGE, MACOS_UPDATE_RESTART_MESSAGE, } from "./lib/permissions"; import { isMacPlatform, isWindowsPlatform } from "./lib/platform"; import { dismissBrowserRecordingBackup, createPrivateAgentRewindRecording, exportBrowserRecordingBackup, getRewindClipOrigin, listBrowserRecordingBackups, retryBrowserRecordingBackup, scheduleNativeBackupCleanupAfterProcessing, shouldUseNativeFullscreenRecording, startRecording, type LocalExportedFile, type PendingBrowserRecordingUpload, type RecorderHandle, type RecorderStopResult, } from "./lib/recorder"; import { copyRecordingShareLink, recordingShareUrl, } from "./lib/recording-link"; import { REWIND_AGENT_PROMPT } from "./lib/rewind-agent-prompt"; import { getRewindStatusPresentation } from "./lib/rewind-status"; import { loadBool, loadString, loadStringAllowEmpty, saveBool, saveString, } from "./lib/storage"; import { canCheckForUpdates, installAndRestart, isUpdatePendingRestart, retryUpdateCheck, useUpdateStatus, type UpdateStatus, } from "./lib/updater"; import { normalizeServerUrl } from "./lib/url"; import { installDesktopVoiceDictation, type VoiceMode, type VoiceProvider, type VoiceShortcutPreference, } from "./lib/voice-dictation"; import { useFeatureConfig, type FeatureConfig, type LocalRecordingMode, type ScreenMemoryStatus, } from "./shared/config"; interface PendingNativeUpload { kind: "native"; recordingId: string; serverUrl: string; folderPath?: string; durationMs: number; width?: number | null; height?: number | null; bytes: number; hasAudio: boolean; hasCamera: boolean; savedAt: string; lastAttemptAt?: string | null; lastError?: string | null; retryCount: number; corrupt?: boolean; } type PendingDesktopUpload = PendingNativeUpload | PendingBrowserRecordingUpload; type PopoverView = | "recorder" | "memory" | "rewind-settings" | "settings" | "meetings" | "dictation"; interface PopoverMeeting { id: string; title: string; scheduledStart: string | null; scheduledEnd: string | null; joinUrl: string | null; platform: string | null; transcriptStatus: string | null; } interface RewindMeetingHistoryAvailability { available: boolean; reason?: string | null; coveredFrom?: string | null; } interface RewindAgentHandoffRequest { requestId: string; status: "pending" | "processing" | "ready" | "declined" | "failed"; requestedAt: string; startAt: string; endAt: string; durationMs: number; reason: string; includeMicrophone: boolean; includeSystemAudio: boolean; reviewRequired: boolean; agentClipRetention: "forever" | "24-hours" | "7-days" | "30-days"; recordingId?: string; agentUrl?: string; contextUrl?: string; expiresAt?: string; error?: string; } interface RewindExtensionRequest { requestId: string; recordingId: string; seconds: 30 | 300; status: "pending" | "processing" | "ready" | "failed"; updatedAt: string; preRollRecordingId?: string; actualDurationMs?: number; error?: string; } interface NativeRewindUploadResult { recordingId: string; durationMs: number; } interface DueRewindAgentHandoff { requestId: string; recordingId: string; } interface LocalRecordingNotice { folderPath?: string; files: LocalExportedFile[]; } interface ShareLinkNotice { recordingId: string; origin: string; url: string; copied: boolean; } interface ScreenMemoryExportResult { folderPath: string; files: Array<{ path: string; fileName: string; bytes: number; mimeType: string; }>; } interface RewindEgressEvent { requestId: string; occurredAt: string; state: | "prepared" | "completed" | "failed" | "local-evidence-read" | "handoff-requested"; evidenceCount: number; packetBytes: number; error?: string | null; operation?: string | null; receipt?: { evidence?: Array<{ id: string; momentId: string; sourceType: "app-context" | "transcript" | "ocr" | "chapter"; capturedAt?: string | null; }>; frames?: Array<{ timestamp: string; segmentId: string }>; mediaInterval?: { startAt: string; endAt: string }; reviewRequired?: boolean; } | null; } interface RewindLocalAskResult { query: string; answerSummary: string; evidence: Array<{ id: string; sourceType: "app-context" | "transcript" | "ocr"; capturedAt: string; excerpt: string; confidence?: number | null; segmentId: string; offsetMs: number; }>; coverage: { segmentsConsidered: number; transcriptIndexesReady: number; ocrIndexesReady: number; gaps: Array<{ kind: string; source: string; startedAt?: string | null; endedAt?: string | null; detail: string; }>; }; confidence: string; truncated: boolean; } interface RewindExcludedApplication { bundleId: string; name: string; path?: string | null; installed: boolean; } interface RewindAgentConnectionStatus { client: "codex" | "claude-code"; configured: boolean; configPath: string; storeDir: string; } type MeetingTranscriptionMode = "manual" | "ask" | "auto"; type CaptureMode = "screen" | "screen-camera" | "camera"; type VideoStorageStatus = "checking" | "configured" | "missing"; const STORAGE_SETUP_HELP_TEXT = "Clips is 100% free and open source, so you need to hook up a way to store your clips. Connect storage with Builder.io for free-tier storage and AI, or use S3-compatible object storage and your own LLM keys."; const STORAGE_SETUP_FAILURE_RE = /video storage is not connected|no video storage configured|file upload provider|storage provider|connect builder|s3-compatible/i; const DEFAULT_SCREEN_MEMORY_CONFIG = { enabled: false, paused: false, retentionHours: 8, maxBytes: 20 * 1024 * 1024 * 1024, segmentSeconds: 5 * 60, sampleIntervalSeconds: 10, captureMode: "visuals" as const, reviewBeforeSending: true, autoPreviewBeforeSending: true, agentClipRetention: "forever" as const, excludedBundleIds: [ "com.1password.1password", "com.agilebits.onepassword7", "com.bitwarden.desktop", "com.dashlane.dashlane", "com.lastpass.lastpass", ], excludePrivateWindows: false, }; function parseExcludedBundleIds(value: string): string[] { return [ ...new Set( value .split(/[\n,]/) .map((id) => id.trim()) .filter(Boolean), ), ]; } function isStorageSetupFailureMessage(message: string | null | undefined) { return STORAGE_SETUP_FAILURE_RE.test(message ?? ""); } const STORAGE_KEY = "clips:server-url"; const MODE_KEY = "clips:last-mode"; const VOICE_SHORTCUT_KEY = "clips:voice-shortcut"; const VOICE_SHORTCUT_CONFIGURED_KEY = "clips:voice-shortcut-configured"; const VOICE_CUSTOM_SHORTCUT_KEY = "clips:voice-custom-shortcut"; const POPOVER_CUSTOM_SHORTCUT_KEY = "clips:popover-custom-shortcut"; const RECORD_CUSTOM_SHORTCUT_KEY = "clips:record-custom-shortcut"; const VOICE_MODE_KEY = "clips:voice-mode"; const VOICE_PROVIDER_KEY = "clips:voice-provider"; const VOICE_INSTRUCTIONS_KEY = "clips:voice-instructions"; const AUTH_TOKEN_KEY = "clips:auth-token"; const SOURCE_KEY = "clips:last-source"; const CAM_ON_KEY = "clips:camera-on"; const MIC_ON_KEY = "clips:mic-on"; const SYSTEM_AUDIO_KEY = "clips:system-audio"; const READINESS_REVIEWED_KEY = "clips:readiness-reviewed"; const REWIND_DOCS_URL = "https://www.agent-native.com/docs/template-clips#agent-readable-clips"; // Sensible defaults so the user never has to type a URL on first launch. // Dev builds point at the local dev server; production builds point at the // hosted Clips instance. The user can still override from Settings. // Dev points at the Clips dev server (shared-app-config says 8094). // Prod points at the hosted Clips instance. User can override from Settings. const DEFAULT_URL = import.meta.env.DEV ? "http://localhost:8094" : "https://clips.agent-native.com"; function normalizeCaptureSource(value: string): CaptureSource { if (value === "region" && isMacPlatform()) return "region"; return value === "window" ? "window" : "full-screen"; } type FetchInput = Parameters[0]; type FetchInit = Parameters[1]; let authFetchInstalled = false; let currentServerOrigin = ""; let currentAuthToken = ""; function originForUrl(value: string, base?: string): string | null { try { return new URL(value, base).origin; } catch { return null; } } function originForServer(serverUrl: string): string { return originForUrl(serverUrl) ?? serverUrl.trim().replace(/\/+$/, ""); } function serverUrlForPendingUpload( upload: PendingDesktopUpload, currentServerUrl: string, ): string { const normalizedCurrent = normalizeServerUrl(currentServerUrl); return normalizedCurrent || normalizeServerUrl(upload.serverUrl || ""); } // "configured"/"missing" are definitive answers from the server; "unknown" // means the check could not be completed (network error, unreachable server, or // an unparseable/non-OK response). An "unknown" result must never downgrade an // already-connected user to the setup flow. type VideoStorageProbe = "configured" | "missing" | "unknown"; async function hasConfiguredVideoStorage( serverUrl: string, ): Promise { const base = serverUrl.replace(/\/+$/, ""); // Track whether any endpoint gave a definitive answer. If both checks throw // or return non-OK/unparseable responses, we can't tell and return "unknown". let sawDefinitiveAnswer = false; try { const uploadStatus = await fetch( `${base}/_agent-native/file-upload/status`, { credentials: "include", cache: "no-store", }, ); if (uploadStatus.ok) { const body = (await uploadStatus.json().catch(() => null)) as { configured?: boolean; } | null; if (body) { sawDefinitiveAnswer = true; if (body.configured) return "configured"; } } } catch { // Fall through to the Builder status endpoint. } try { const builderStatus = await fetch(`${base}/_agent-native/builder/status`, { credentials: "include", cache: "no-store", }); if (builderStatus.ok) { const body = (await builderStatus.json().catch(() => null)) as { configured?: boolean; } | null; if (body) { sawDefinitiveAnswer = true; if (body.configured) return "configured"; } } } catch { // Network error or unreachable server — treat as indeterminate below. } return sawDefinitiveAnswer ? "missing" : "unknown"; } function authTokenStorageKey(serverUrl: string): string { return `${AUTH_TOKEN_KEY}:${originForServer(serverUrl)}`; } function loadDesktopAuthToken(serverUrl: string): string { return loadString(authTokenStorageKey(serverUrl), ""); } function setDesktopAuthContext(serverUrl: string, token: string): void { currentServerOrigin = originForServer(serverUrl); currentAuthToken = token.trim(); } function saveDesktopAuthToken(serverUrl: string, token: string): void { const trimmed = token.trim(); if (!trimmed) return; saveString(authTokenStorageKey(serverUrl), trimmed); setDesktopAuthContext(serverUrl, trimmed); } function clearDesktopAuthToken(serverUrl: string): void { saveString(authTokenStorageKey(serverUrl), ""); if (currentServerOrigin === originForServer(serverUrl)) { currentAuthToken = ""; } } function urlForFetchInput(input: FetchInput): string | null { if (typeof input === "string") return input; if (input instanceof URL) return input.toString(); if (typeof Request !== "undefined" && input instanceof Request) { return input.url; } return null; } function installAuthFetchInterceptor(): void { if (authFetchInstalled || typeof window === "undefined") return; authFetchInstalled = true; const nativeFetch = window.fetch.bind(window); window.fetch = (input: FetchInput, init?: FetchInit) => { const rawUrl = urlForFetchInput(input); const targetOrigin = rawUrl ? originForUrl(rawUrl, window.location.href) : null; if (!targetOrigin || targetOrigin !== currentServerOrigin) { return nativeFetch(input, init); } const requestHeaders = typeof Request !== "undefined" && input instanceof Request ? input.headers : undefined; const headers = new Headers(init?.headers ?? requestHeaders); if (!headers.has("X-Request-Source")) { headers.set("X-Request-Source", "clips-desktop"); } if (currentAuthToken && !headers.has("Authorization")) { headers.set("Authorization", `Bearer ${currentAuthToken}`); } return nativeFetch(input, { ...init, headers }); }; } type ByokVoiceProvider = Extract; type VoiceProviderMode = "native" | "whisper" | "builder" | "byok"; type MacosPrivacyPane = | "camera" | "microphone" | "screen" | "speech" | "accessibility" | "input-monitoring"; const MACOS_PRIVACY_URLS: Record = { camera: "x-apple.systempreferences:com.apple.settings.PrivacySecurity.extension?Privacy_Camera", microphone: "x-apple.systempreferences:com.apple.settings.PrivacySecurity.extension?Privacy_Microphone", screen: "x-apple.systempreferences:com.apple.settings.PrivacySecurity.extension?Privacy_ScreenCapture", speech: "x-apple.systempreferences:com.apple.settings.PrivacySecurity.extension?Privacy_SpeechRecognition", accessibility: "x-apple.systempreferences:com.apple.settings.PrivacySecurity.extension?Privacy_Accessibility", "input-monitoring": "x-apple.systempreferences:com.apple.settings.PrivacySecurity.extension?Privacy_ListenEvent", }; const WINDOWS_PRIVACY_URLS: Partial> = { camera: "ms-settings:privacy-webcam", microphone: "ms-settings:privacy-microphone", // No dedicated screen-capture privacy page that works on all Windows // versions, so open the top-level Privacy settings page. screen: "ms-settings:privacy", speech: "ms-settings:privacy-speechtyping", accessibility: "ms-settings:easeofaccess", "input-monitoring": "ms-settings:privacy", }; function openPrivacySettings(pane: MacosPrivacyPane): void { if (isMacPlatform()) { invoke("open_macos_privacy_settings", { pane }).catch((nativeErr) => { console.warn( "[clips-tray] native macOS privacy settings open failed; falling back:", nativeErr, ); openExternal(MACOS_PRIVACY_URLS[pane]).catch((err) => { console.error("[clips-tray] open macOS privacy settings failed:", err); }); }); return; } if (isWindowsPlatform()) { const url = WINDOWS_PRIVACY_URLS[pane]; if (url) { openExternal(url).catch((err) => { console.error( "[clips-tray] open Windows privacy settings failed:", err, ); }); } return; } } // Same explicit-drag pattern the toolbar/bubble overlays use — // `data-tauri-drag-region` has been unreliable, so we call `startDragging()` // directly on mousedown. Clicks on buttons/inputs still reach their handlers // since we only start a drag when the mousedown target isn't inside one. function handlePopoverHeaderMouseDown(event: React.MouseEvent) { if (event.button !== 0) return; const target = event.target as HTMLElement; if (target.closest("button, a, input, select, textarea")) return; getCurrentWindow() .startDragging() .catch((err) => { console.warn("[clips-popover] startDragging failed:", err); }); } function nativeVoiceProvider(): VoiceProvider { return isMacPlatform() ? "macos-native" : "browser"; } function isByokVoiceProvider(value: VoiceProvider): value is ByokVoiceProvider { return value === "gemini" || value === "groq"; } function voiceProviderMode(value: VoiceProvider): VoiceProviderMode { if (isByokVoiceProvider(value)) return "byok"; if (value === "builder" || value === "builder-gemini") return "builder"; if (value === "whisper") return "whisper"; return "native"; } function normalizeVoiceProvider(value: string): VoiceProvider { const native = nativeVoiceProvider(); if (value === "auto") return native; if (value === "builder") return "builder-gemini"; if (value === "macos-native" && !isMacPlatform()) return "browser"; // Symmetric migration: a persisted "browser" preference from a non-Mac // install (or an older build) silently ran native transcription on Mac // via resolveProvider()'s mic-override branch with zero UI indication. // Normalize the stale value at the source instead (D1). if (value === "browser" && isMacPlatform()) return "macos-native"; return value === "browser" || value === "macos-native" || value === "whisper" || value === "builder-gemini" || value === "gemini" || value === "groq" ? value : native; } function formatAgo(iso: string): string { try { const delta = (Date.now() - new Date(iso).getTime()) / 1000; if (delta < 60) return "just now"; if (delta < 3600) return `${Math.floor(delta / 60)}m ago`; if (delta < 86400) return `${Math.floor(delta / 3600)}h ago`; return `${Math.floor(delta / 86400)}d ago`; } catch { return ""; } } function formatMeetingWhen(meeting: PopoverMeeting): string { const startMs = Date.parse(meeting.scheduledStart ?? ""); if (Number.isNaN(startMs)) return "Upcoming"; const endMs = Date.parse(meeting.scheduledEnd ?? ""); const now = Date.now(); if (startMs <= now && (Number.isNaN(endMs) || endMs >= now)) { return "Now"; } if (startMs > now && startMs - now < 60 * 60 * 1000) { return `in ${Math.max(1, Math.round((startMs - now) / 60000))}m`; } const start = new Date(startMs); const time = start.toLocaleTimeString([], { hour: "numeric", minute: "2-digit", }); const today = new Date(now); const tomorrow = new Date(now + 24 * 60 * 60 * 1000); if (start.toDateString() === today.toDateString()) return `Today ${time}`; if (start.toDateString() === tomorrow.toDateString()) { return `Tomorrow ${time}`; } return `${start.toLocaleDateString([], { weekday: "short", month: "short", day: "numeric", })} ${time}`; } function meetingCanStartNotes(meeting: PopoverMeeting): boolean { const startMs = Date.parse(meeting.scheduledStart ?? ""); if (Number.isNaN(startMs)) return false; const endMs = Date.parse(meeting.scheduledEnd ?? ""); const now = Date.now(); return ( startMs <= now + 10 * 60 * 1000 && (Number.isNaN(endMs) || endMs >= now) ); } function voiceShortcutLabel( shortcut: VoiceShortcutPreference, customShortcut: string, ): string { switch (shortcut) { case "fn": return "Fn"; case "cmd-shift-space": return "Cmd+Shift+Space"; case "ctrl-shift-space": return "Ctrl+Shift+Space"; case "custom": return customShortcut || "Custom shortcut"; case "both": return "Fn, Cmd+Shift+Space, or Ctrl+Shift+Space"; } } function voiceProviderLabel(provider: VoiceProvider): string { if (provider === "whisper") return "Local Whisper"; if (provider === "builder" || provider === "builder-gemini") { return "Builder.io cleanup"; } if (provider === "gemini") return "Google Gemini"; if (provider === "groq") return "Groq"; if (provider === "macos-native") return "macOS on-device"; return "Browser speech"; } function formatFileSize(bytes: number): string { if (!Number.isFinite(bytes) || bytes <= 0) return ""; if (bytes < 1024 * 1024) return `${Math.round(bytes / 1024)} KB`; return `${(bytes / (1024 * 1024)).toFixed(bytes < 10 * 1024 * 1024 ? 1 : 0)} MB`; } function measurePopoverHeight(el: HTMLElement): number { const rect = el.getBoundingClientRect(); const style = window.getComputedStyle(el); const borderY = Number.parseFloat(style.borderTopWidth || "0") + Number.parseFloat(style.borderBottomWidth || "0"); const candidates = [rect.height, el.scrollHeight + borderY]; // When a direct child is the scrollable content pane, the shell's own // scrollHeight already includes the fixed footer but excludes content that // is hidden inside that child. Add only that hidden delta to the shell // baseline. Measuring the child's full height alone would omit the footer // and leave the resumed recorder window partially clipped. const directChildOverflow = Array.from(el.children).reduce((total, child) => { if (!(child instanceof HTMLElement)) return total; return total + Math.max(0, child.scrollHeight - child.clientHeight); }, 0); candidates.push(el.scrollHeight + borderY + directChildOverflow); // ResizeObserver on `.app` alone misses scroll-only and absolutely // positioned growth. Measure descendant bounds so menus, banners, and // settings sections can grow the native window even when `.app` is capped // by the current viewport height. let lowestBottom = rect.bottom; for (const child of Array.from(el.querySelectorAll("*"))) { const childStyle = window.getComputedStyle(child); if (childStyle.display === "none") continue; const childRect = child.getBoundingClientRect(); if (childRect.width === 0 && childRect.height === 0) continue; lowestBottom = Math.max(lowestBottom, childRect.bottom); // Recorder Home deliberately keeps its footer fixed and lets the content // region scroll when the native window is short. A newly inserted row can // therefore grow `child.scrollHeight` without changing any descendant's // visible bounding box. Include that hidden overflow in the desired // window height so a state transition (for example paused -> remembering) // can grow the popover back to its natural size. const childBorderY = Number.parseFloat(childStyle.borderTopWidth || "0") + Number.parseFloat(childStyle.borderBottomWidth || "0"); candidates.push( childRect.top - rect.top + child.scrollHeight + childBorderY, ); } candidates.push(lowestBottom - rect.top); return Math.ceil(Math.max(...candidates)); } function usePopoverAutoSize( ref: RefObject, options: { disabled: boolean; width: number }, ): void { const { disabled, width } = options; useEffect(() => { const el = ref.current; if (!el || disabled) return; let animationFrame = 0; let settleTimer = 0; let lastHeight = 0; let lastWidth = 0; const push = () => { animationFrame = 0; const height = measurePopoverHeight(el); if ( height > 0 && (Math.abs(height - lastHeight) >= 2 || Math.abs(width - lastWidth) >= 1) ) { lastHeight = height; lastWidth = width; invoke("resize_popover", { height, width }).catch(() => {}); } }; const schedule = () => { if (!animationFrame) { animationFrame = window.requestAnimationFrame(push); } window.clearTimeout(settleTimer); settleTimer = window.setTimeout(push, 80); }; const resizeObserver = new ResizeObserver(schedule); const observeTree = () => { resizeObserver.disconnect(); resizeObserver.observe(el); for (const child of Array.from(el.querySelectorAll("*"))) { resizeObserver.observe(child); } }; const mutationObserver = new MutationObserver(() => { observeTree(); schedule(); }); observeTree(); mutationObserver.observe(el, { attributes: true, characterData: true, childList: true, subtree: true, }); schedule(); if (document.fonts) { document.fonts.ready.then(schedule).catch(() => {}); } return () => { if (animationFrame) window.cancelAnimationFrame(animationFrame); window.clearTimeout(settleTimer); mutationObserver.disconnect(); resizeObserver.disconnect(); }; }, [disabled, ref, width]); } export function App() { const featureConfig = useFeatureConfig(); const [serverUrl, setServerUrl] = useState(() => loadString(STORAGE_KEY, DEFAULT_URL).replace(/\/+$/, ""), ); const [mode, setMode] = useState( () => loadString(MODE_KEY, "screen-camera") as CaptureMode, ); const [source, setSource] = useState(() => normalizeCaptureSource(loadString(SOURCE_KEY, "full-screen")), ); const [cameraOn, setCameraOn] = useState(() => loadBool(CAM_ON_KEY, false), ); const [micOn, setMicOn] = useState(() => loadBool(MIC_ON_KEY, true)); const [systemAudioOn, setSystemAudioOn] = useState(() => loadBool(SYSTEM_AUDIO_KEY, true), ); const [voiceShortcut, setVoiceShortcut] = useState( () => { if (!loadBool(VOICE_SHORTCUT_CONFIGURED_KEY, false)) { return "cmd-shift-space"; } const saved = loadString(VOICE_SHORTCUT_KEY, "cmd-shift-space"); return saved === "fn" || saved === "cmd-shift-space" || saved === "ctrl-shift-space" || saved === "custom" || saved === "both" ? saved : "cmd-shift-space"; }, ); const [voiceCustomShortcut, setVoiceCustomShortcut] = useState(() => loadStringAllowEmpty(VOICE_CUSTOM_SHORTCUT_KEY, "Cmd+Shift+D"), ); const [popoverCustomShortcut, setPopoverCustomShortcut] = useState( () => loadStringAllowEmpty(POPOVER_CUSTOM_SHORTCUT_KEY, ""), ); const [recordCustomShortcut, setRecordCustomShortcut] = useState(() => loadStringAllowEmpty(RECORD_CUSTOM_SHORTCUT_KEY, ""), ); const [voiceMode, setVoiceMode] = useState(() => { const saved = loadString(VOICE_MODE_KEY, "push-to-talk"); return saved === "toggle" ? "toggle" : "push-to-talk"; }); const [voiceProvider, setVoiceProvider] = useState(() => { return normalizeVoiceProvider( loadString(VOICE_PROVIDER_KEY, nativeVoiceProvider()), ); }); const [voiceInstructions, setVoiceInstructions] = useState(() => loadString(VOICE_INSTRUCTIONS_KEY, ""), ); const localRecordingMode: LocalRecordingMode = featureConfig?.localRecordingMode ?? "off"; const [pendingUploads, setPendingUploads] = useState( [], ); const [retryingUploadId, setRetryingUploadId] = useState(null); const [exportingUploadId, setExportingUploadId] = useState( null, ); const [dismissingUploadId, setDismissingUploadId] = useState( null, ); const [localRecordingNotice, setLocalRecordingNotice] = useState(null); const [shareLinkNotice, setShareLinkNotice] = useState(null); const [popoverView, setPopoverView] = useState("recorder"); const [rewindSettingsReturnView, setRewindSettingsReturnView] = useState< "recorder" | "settings" >("recorder"); const [homeScreenMemoryStatus, setHomeScreenMemoryStatus] = useState(null); const [homeScreenMemoryBusy, setHomeScreenMemoryBusy] = useState(false); const homeScreenMemoryRefreshVersionRef = useRef(0); const [rewindAgentPromptCopied, setRewindAgentPromptCopied] = useState(false); const [agentHandoff, setAgentHandoff] = useState(null); const agentHandoffProcessingRef = useRef(null); const agentHandoffPreviewedRef = useRef>(new Set()); const rewindExtensionProcessingRef = useRef>(new Set()); const [agentHandoffPreviewBusy, setAgentHandoffPreviewBusy] = useState(false); const [agentHandoffPreviewError, setAgentHandoffPreviewError] = useState< string | null >(null); const [promptRewindEnable, setPromptRewindEnable] = useState(false); const [meetings, setMeetings] = useState([]); const [meetingsLoading, setMeetingsLoading] = useState(false); const [meetingsError, setMeetingsError] = useState(null); const [meetingStartMessage, setMeetingStartMessage] = useState( null, ); const [ rewindMeetingHistoryAvailability, setRewindMeetingHistoryAvailability, ] = useState>({}); const [activeMeetingId, setActiveMeetingId] = useState(null); const [readinessOpen, setReadinessOpen] = useState( () => !loadBool(READINESS_REVIEWED_KEY, false), ); const [rewindHomeOpen, setRewindHomeOpen] = useState(false); const [recorder, setRecorder] = useState(null); const [recError, setRecError] = useState(null); const [cameraError, setCameraError] = useState(null); const [shortcutRegistrationError, setShortcutRegistrationError] = useState< string | null >(null); // Latched true the moment the user clicks Start Recording and cleared // when the recorder fully stops/cancels. We use this to suppress the // popover auto-hide during the macOS screen-picker focus dance. const [recordingFlowActive, setRecordingFlowActive] = useState(false); const [recordingStopFinalizing, setRecordingStopFinalizing] = useState(false); const [lastRecordingId, setLastRecordingId] = useState(null); const [authStatus, setAuthStatus] = useState<"unknown" | "authed" | "anon">( "unknown", ); const [videoStorageStatus, setVideoStorageStatus] = useState("checking"); const [signedInAs, setSignedInAs] = useState(null); const [signInPending, setSignInPending] = useState(false); const [signInError, setSignInError] = useState(null); // Ref-based lock so two fast clicks cannot both enter signInExternal() // (state updates are async; refs are synchronous). const signInInflightRef = useRef(false); // Stored so Cancel can stop the polling loop. const pollIntervalRef = useRef | null>(null); const isRecording = recorder !== null; // Whether the popover window is shown; driven by the visibility effect below. const [popoverVisible, setPopoverVisible] = useState(false); const homeRewindPresentation = getRewindStatusPresentation({ status: homeScreenMemoryStatus, config: featureConfig?.screenMemory ?? DEFAULT_SCREEN_MEMORY_CONFIG, clipRecordingActive: isRecording || recordingFlowActive, }); const homeRewindOn = featureConfig?.screenMemory?.enabled === true && featureConfig.screenMemory.paused !== true; const refreshHomeScreenMemoryStatus = useCallback(() => { const version = ++homeScreenMemoryRefreshVersionRef.current; invoke("screen_memory_status") .then((status) => { if (version === homeScreenMemoryRefreshVersionRef.current) { setHomeScreenMemoryStatus(status); } }) .catch(() => {}); }, []); useEffect(() => { if (featureConfig?.screenMemory?.enabled !== true) { homeScreenMemoryRefreshVersionRef.current += 1; setHomeScreenMemoryStatus(null); return; } let cancelled = false; const refresh = () => { if (!cancelled) refreshHomeScreenMemoryStatus(); }; refresh(); const timer = window.setInterval(refresh, popoverVisible ? 5_000 : 30_000); let unlisten: (() => void) | undefined; listen("clips:screen-memory-changed", refresh) .then((stopListening) => { if (cancelled) { stopListening(); return; } unlisten = stopListening; }) .catch(() => {}); return () => { cancelled = true; homeScreenMemoryRefreshVersionRef.current += 1; window.clearInterval(timer); unlisten?.(); }; }, [ featureConfig?.screenMemory?.enabled, popoverVisible, refreshHomeScreenMemoryStatus, ]); const recordShortcutHandlerRef = useRef<() => void>(() => {}); // Mirrors `bubbleActive` (assigned below once it is computed) so device // probes can synchronously tell whether the camera bubble owns the grant. const bubbleActiveRef = useRef(false); const { cameraId, setCameraId, micId, setMicId, cameraLabel, setCameraLabel, micLabel, setMicLabel, selectedMicId, selectedMicLabel, cameraDevices, micDevices, loadDevices, requestDeviceAccess, } = useMediaDevices({ bubbleActiveRef, popoverVisible, setCameraError, setRecError, }); const voiceDictationEnabled = featureConfig?.voiceEnabled !== false; const fnShortcutEnabled = voiceDictationEnabled && (voiceShortcut === "fn" || voiceShortcut === "both"); const updateVoiceShortcut = useCallback((value: VoiceShortcutPreference) => { saveBool(VOICE_SHORTCUT_CONFIGURED_KEY, true); setVoiceShortcut(value); }, []); useEffect(() => { installAuthFetchInterceptor(); setDesktopAuthContext(serverUrl, loadDesktopAuthToken(serverUrl)); }, [serverUrl]); const refreshVideoStorageStatus = useCallback(async () => { if (authStatus !== "authed" || localRecordingMode !== "off") { setVideoStorageStatus("configured"); return true; } setVideoStorageStatus((prev) => (prev === "missing" ? prev : "checking")); const probe = await hasConfiguredVideoStorage(serverUrl); if (probe === "unknown") { // The check couldn't be completed (offline/unreachable). Never downgrade // an already-connected user to "missing" on an indeterminate result; // preserve the last known status and let the poll retry. If we never // determined a status, fall back to "checking" so the poll keeps trying // rather than hard-blocking the record button. setVideoStorageStatus((prev) => prev === "configured" || prev === "missing" ? prev : "checking", ); return false; } setVideoStorageStatus(probe); return probe === "configured"; }, [authStatus, localRecordingMode, serverUrl]); useEffect(() => { void refreshVideoStorageStatus(); }, [refreshVideoStorageStatus]); useEffect(() => { if ( authStatus !== "authed" || localRecordingMode !== "off" || // Re-poll while storage is "missing" (server may become configured) and // while still "checking" (an indeterminate/unreachable first probe should // keep retrying instead of hard-blocking the record button). (videoStorageStatus !== "missing" && videoStorageStatus !== "checking") ) { return; } const interval = window.setInterval(() => { void refreshVideoStorageStatus(); }, 5000); return () => window.clearInterval(interval); }, [ authStatus, localRecordingMode, refreshVideoStorageStatus, videoStorageStatus, ]); useEffect(() => { return installDesktopVoiceDictation({ enabled: voiceDictationEnabled, serverUrl, shortcut: voiceShortcut, mode: voiceMode, provider: voiceProvider, micDeviceId: selectedMicId || null, micDeviceLabel: selectedMicLabel || null, instructions: voiceInstructions, }); }, [ serverUrl, voiceShortcut, voiceDictationEnabled, voiceMode, voiceProvider, selectedMicId, selectedMicLabel, voiceInstructions, ]); useEffect(() => { invoke("set_fn_shortcut_enabled", { enabled: fnShortcutEnabled }).catch( (err) => { console.warn("[clips-tray] set_fn_shortcut_enabled failed:", err); }, ); }, [fnShortcutEnabled]); useEffect(() => { let cancelled = false; invoke("set_custom_shortcuts", { voice: voiceShortcut === "custom" ? voiceCustomShortcut : null, popover: popoverCustomShortcut.trim() ? popoverCustomShortcut : null, record: recordCustomShortcut.trim() ? recordCustomShortcut : null, }) .then(() => { if (!cancelled) setShortcutRegistrationError(null); }) .catch((err) => { console.warn("[clips-tray] set_custom_shortcuts failed:", err); if (!cancelled) { setShortcutRegistrationError( err instanceof Error ? err.message : String(err), ); } }); return () => { cancelled = true; }; }, [ popoverCustomShortcut, recordCustomShortcut, voiceCustomShortcut, voiceShortcut, ]); // ---- auth status -------------------------------------------------------- // The Tauri WebView has its own cookie jar (separate from the user's // browser). Before anything else, check whether we have a session cookie // for the Clips server; if not, surface a Sign in button. const checkAuth = useCallback(async () => { try { const res = await fetch( `${serverUrl.replace(/\/+$/, "")}/_agent-native/auth/session`, { credentials: "include" }, ); if (!res.ok) { if (res.status === 401 || res.status === 403) { clearDesktopAuthToken(serverUrl); } setAuthStatus("anon"); setSignedInAs(null); return false; } const json = (await res.json().catch(() => null)) as { email?: string; token?: string; error?: string; } | null; if (json?.email) { if (json.token) saveDesktopAuthToken(serverUrl, json.token); setAuthStatus("authed"); setSignedInAs(json.email); return true; } setAuthStatus("anon"); setSignedInAs(null); clearDesktopAuthToken(serverUrl); return false; } catch { setAuthStatus("anon"); setSignedInAs(null); return false; } }, [serverUrl]); useEffect(() => { checkAuth(); }, [checkAuth]); // Push the current server URL to the Rust meetings watcher so it can // poll the backend for upcoming events. The watcher no-ops until this // fires — we re-push on every server-url change so a settings tweak // flows through immediately. useEffect(() => { invoke("meetings_watcher_set_server_url", { serverUrl }).catch(() => { // Command may be missing on older builds — best-effort. }); }, [serverUrl]); // The Rust-side meetings watcher fetches the backend with `reqwest`, which // does NOT inherit the popover WebView's cookie jar or fetch interceptor. // We forward both the legacy cookie string and the desktop bearer token. // Re-push on: // - boot // - sign-in / sign-out (signedInAs change) // - the watcher emitting `meetings:auth-needed` (401) — usually means // the cookie expired and we need to send a fresh one. useEffect(() => { function pushSession() { const cookie = typeof document !== "undefined" ? document.cookie || "" : ""; const authToken = loadDesktopAuthToken(serverUrl); invoke("meetings_watcher_set_session", { cookie, authToken }).catch( () => { // Older builds may not expose this command yet — best-effort. }, ); } pushSession(); let unlisten: (() => void) | null = null; listen("meetings:auth-needed", () => { console.warn("[clips-popover] meetings:auth-needed — re-pushing session"); pushSession(); }) .then((u) => { unlisten = u; }) .catch(() => {}); return () => { if (unlisten) { try { unlisten(); } catch { // ignore } } }; }, [signedInAs, serverUrl]); // Tray "Upcoming Meetings" submenu click → open that meeting's notes page in // the browser. The rich meeting UI (transcript + AI notes) lives in the web // app, not this popover, so we deep-link to it. Without this listener the // tray click emitted `meetings:open` into the void and nothing happened. useEffect(() => { let unlisten: (() => void) | null = null; listen<{ meetingId?: string }>("meetings:open", (ev) => { const id = ev.payload?.meetingId; if (!id) return; const base = serverUrl.replace(/\/+$/, ""); const url = `${base}/meetings/${encodeURIComponent(id)}`; import("@tauri-apps/plugin-shell") .then(({ open }) => open(url)) .catch((err) => { console.error("[clips-popover] open meeting failed:", err); }); }) .then((u) => { unlisten = u; }) .catch(() => {}); return () => { if (unlisten) { try { unlisten(); } catch { // ignore } } }; }, [serverUrl]); // Open meeting join URLs (Zoom / Meet / Teams) when the meeting // notification banner asks. Centralized here so any future surface that // emits `meetings:open-join-url` works the same way. useEffect(() => { let unlisten: (() => void) | null = null; listen<{ joinUrl?: string | null }>("meetings:open-join-url", (ev) => { const url = ev.payload?.joinUrl; if (!url) return; openMeetingJoinUrl(url).catch((err) => { console.error("[clips-popover] open join url failed:", err); }); }) .then((u) => { unlisten = u; }) .catch(() => {}); return () => { if (unlisten) { try { unlisten(); } catch { // ignore } } }; }, []); const callClipsAction = useCallback( async ( name: string, body: Record, opts?: { method?: "GET" | "POST"; signal?: AbortSignal }, ): Promise => { const base = serverUrl.replace(/\/+$/, ""); const method = opts?.method ?? "POST"; const headers = new Headers(); const authToken = loadDesktopAuthToken(serverUrl); if (authToken) headers.set("Authorization", `Bearer ${authToken}`); // GET actions read their args from the query string; POST actions send a // JSON body. let url = `${base}/_agent-native/actions/${name}`; let requestBody: string | undefined; if (method === "GET") { const params = new URLSearchParams(); for (const [key, value] of Object.entries(body)) { if (value != null) params.set(key, String(value)); } const qs = params.toString(); if (qs) url += `?${qs}`; } else { headers.set("Content-Type", "application/json"); requestBody = JSON.stringify(body); } const response = await fetch(url, { method, credentials: "include", headers, body: requestBody, signal: opts?.signal, }); const text = await response.text().catch(() => ""); let json: any = null; try { json = text ? JSON.parse(text) : null; } catch { // Keep text fallback below. } if (!response.ok) { const message = json?.error || json?.message || (response.status === 401 ? "Sign in to transcribe meetings." : text.slice(0, 180) || `Request failed (${response.status})`); throw new Error(message); } return (json?.result ?? json) as T; }, [serverUrl], ); const updateAgentHandoff = useCallback( async ( requestId: string, status: RewindAgentHandoffRequest["status"], result?: Record, error?: string, ) => { await invoke("screen_memory_update_agent_handoff", { requestId, status, result: result ?? null, error: error ?? null, }); }, [], ); const previewAgentHandoff = useCallback( async (request: RewindAgentHandoffRequest) => { setAgentHandoffPreviewBusy(true); setAgentHandoffPreviewError(null); try { await invoke("rewind_agent_handoff_preview", { requestId: request.requestId, startedAt: request.startAt, endedAt: request.endAt, includeMic: request.includeMicrophone, includeSystemAudio: request.includeSystemAudio, }); } catch (error) { setAgentHandoffPreviewError( error instanceof Error ? error.message : String(error), ); } finally { setAgentHandoffPreviewBusy(false); } }, [], ); const processAgentHandoff = useCallback( async (request: RewindAgentHandoffRequest) => { if (agentHandoffProcessingRef.current === request.requestId) return; agentHandoffProcessingRef.current = request.requestId; const processing = { ...request, status: "processing" as const }; setAgentHandoff(processing); let recordingId: string | null = null; try { await updateAgentHandoff(request.requestId, "processing"); const hasAudio = request.includeMicrophone || request.includeSystemAudio; const recording = await createPrivateAgentRewindRecording( serverUrl, hasAudio, request.startAt, ); recordingId = recording.id; await invoke("rewind_agent_handoff_upload", { requestId: request.requestId, startedAt: request.startAt, endedAt: request.endAt, serverUrl, recordingId, authToken: loadDesktopAuthToken(serverUrl), cookie: typeof document !== "undefined" ? document.cookie || "" : "", uploadMode: recording.uploadMode, includeMic: request.includeMicrophone, includeSystemAudio: request.includeSystemAudio, }); const retentionHours = { forever: null, "24-hours": 24, "7-days": 7 * 24, "30-days": 30 * 24, }[request.agentClipRetention]; const autoDeleteAt = retentionHours ? new Date( Date.now() + retentionHours * 60 * 60 * 1_000, ).toISOString() : null; await callClipsAction("update-recording", { id: recordingId, ...(autoDeleteAt ? { expiresAt: autoDeleteAt } : {}), }); const link = await callClipsAction<{ recordingId: string; url: string; contextUrl: string; expiresAt: string; }>("create-recording-agent-link", { recordingId }); const ready: RewindAgentHandoffRequest = { ...processing, status: "ready", recordingId, agentUrl: link.url, contextUrl: link.contextUrl, expiresAt: link.expiresAt, }; await updateAgentHandoff(request.requestId, "ready", { recordingId, agentUrl: link.url, contextUrl: link.contextUrl, expiresAt: link.expiresAt, ...(autoDeleteAt ? { autoDeleteAt } : {}), }); setAgentHandoff(ready); } catch (error) { const message = error instanceof Error ? error.message : String(error); if (recordingId) { await callClipsAction("trash-recording", { id: recordingId, skipIfReady: true, }).catch(() => {}); } await updateAgentHandoff( request.requestId, "failed", undefined, message, ).catch(() => {}); setAgentHandoff({ ...processing, status: "failed", error: message }); } finally { agentHandoffProcessingRef.current = null; } }, [callClipsAction, serverUrl, updateAgentHandoff], ); const processRewindExtension = useCallback( async (request: RewindExtensionRequest) => { if (rewindExtensionProcessingRef.current.has(request.requestId)) return; rewindExtensionProcessingRef.current.add(request.requestId); let preRollRecordingId: string | null = null; try { const origin = getRewindClipOrigin(request.recordingId); if (!origin) { throw new Error( "Clips Alpha no longer has the local start time for this Clip.", ); } const endedAtMs = Date.parse(origin.startedAt); if (!Number.isFinite(endedAtMs)) { throw new Error("The original Clip start time is invalid."); } await callClipsAction("update-rewind-extension-request", { recordingId: request.recordingId, requestId: request.requestId, status: "processing", }); const startedAt = new Date( endedAtMs - request.seconds * 1_000, ).toISOString(); const recording = await createPrivateAgentRewindRecording( serverUrl, origin.includeMicrophone || origin.includeSystemAudio, startedAt, ); preRollRecordingId = recording.id; const upload = await invoke( "rewind_agent_handoff_upload", { requestId: `handoff-${request.requestId}`, startedAt, endedAt: origin.startedAt, serverUrl, recordingId: recording.id, authToken: loadDesktopAuthToken(serverUrl), cookie: typeof document !== "undefined" ? document.cookie || "" : "", uploadMode: recording.uploadMode, includeMic: origin.includeMicrophone, includeSystemAudio: origin.includeSystemAudio, }, ); await callClipsAction("update-rewind-extension-request", { recordingId: request.recordingId, requestId: request.requestId, status: "ready", preRollRecordingId: recording.id, actualDurationMs: Math.round(upload.durationMs), }); } catch (error) { const message = error instanceof Error ? error.message : String(error); if (preRollRecordingId) { await callClipsAction("trash-recording", { id: preRollRecordingId, skipIfReady: true, }).catch(() => {}); } await callClipsAction("update-rewind-extension-request", { recordingId: request.recordingId, requestId: request.requestId, status: "failed", error: message, }).catch(() => {}); } finally { rewindExtensionProcessingRef.current.delete(request.requestId); } }, [callClipsAction, serverUrl], ); useEffect(() => { if ( authStatus !== "authed" || featureConfig?.screenMemory?.enabled !== true ) { return; } let cancelled = false; const poll = async () => { const result = await callClipsAction<{ requests?: RewindExtensionRequest[]; }>("list-rewind-extension-requests", {}, { method: "GET" }).catch( () => null, ); if (cancelled) return; for (const request of result?.requests ?? []) { void processRewindExtension(request); } }; void poll(); const timer = window.setInterval(() => void poll(), 3_000); return () => { cancelled = true; window.clearInterval(timer); }; }, [ authStatus, callClipsAction, featureConfig?.screenMemory?.enabled, processRewindExtension, ]); useEffect(() => { if (featureConfig?.screenMemory?.enabled !== true || agentHandoff) return; let cancelled = false; const poll = () => { invoke( "screen_memory_next_agent_handoff", ) .then((request) => { if (cancelled || !request) return; setAgentHandoff(request); getCurrentWindow() .show() .catch(() => {}); getCurrentWindow() .setFocus() .catch(() => {}); if (!request.reviewRequired) { void processAgentHandoff(request); } else if ( featureConfig?.screenMemory?.autoPreviewBeforeSending !== false && !agentHandoffPreviewedRef.current.has(request.requestId) ) { // Polling and config refreshes can rerender this surface. Mark the // request before preparing QuickTime so one approval request opens // one local preview, while leaving the manual control available. agentHandoffPreviewedRef.current.add(request.requestId); void previewAgentHandoff(request); } }) .catch(() => {}); }; poll(); const timer = window.setInterval(poll, 2_000); return () => { cancelled = true; window.clearInterval(timer); }; }, [ agentHandoff, featureConfig?.screenMemory?.autoPreviewBeforeSending, featureConfig?.screenMemory?.enabled, previewAgentHandoff, processAgentHandoff, ]); useEffect(() => { if (featureConfig?.screenMemory?.enabled !== true) return; let cancelled = false; const sweep = async () => { const due = await invoke( "screen_memory_due_agent_handoffs", ).catch(() => []); if (cancelled) return; for (const item of due) { try { const cleanup = await callClipsAction<{ deleted: boolean; reason: string; }>("delete-agent-recording-if-unpromoted", { id: item.recordingId, }); if (cleanup.deleted) { await invoke("screen_memory_mark_agent_handoff_deleted", { requestId: item.requestId, }); } else if (cleanup.reason === "promoted") { await invoke("screen_memory_cancel_agent_handoff_cleanup", { requestId: item.requestId, }); } } catch (error) { console.warn( "[clips-tray] agent-created Clip cleanup failed:", error, ); } } }; void sweep(); const timer = window.setInterval(() => void sweep(), 60_000); return () => { cancelled = true; window.clearInterval(timer); }; }, [callClipsAction, featureConfig?.screenMemory?.enabled]); const fetchUpcomingMeetings = useCallback(async () => { if (authStatus !== "authed") { setMeetings([]); setMeetingsError(null); return; } setMeetingsLoading(true); setMeetingsError(null); try { const result = await callClipsAction<{ meetings?: unknown[] }>( "list-meetings", { view: "upcoming", limit: 3, upcomingWithinMin: 24 * 60 }, { method: "GET" }, ); const list = Array.isArray(result.meetings) ? result.meetings : []; setMeetings( list.slice(0, 3).map((raw) => { const meeting = raw as Partial; return { id: String(meeting.id ?? ""), title: meeting.title || "Untitled meeting", scheduledStart: meeting.scheduledStart ?? null, scheduledEnd: meeting.scheduledEnd ?? null, joinUrl: meeting.joinUrl ?? null, platform: meeting.platform ?? null, transcriptStatus: meeting.transcriptStatus ?? null, }; }), ); } catch (err) { setMeetings([]); setMeetingsError( err instanceof Error ? err.message : "Could not load meetings.", ); } finally { setMeetingsLoading(false); } }, [authStatus, callClipsAction]); useEffect(() => { let cancelled = false; if (popoverView !== "meetings" || meetings.length === 0) { setRewindMeetingHistoryAvailability({}); return () => { cancelled = true; }; } Promise.all( meetings.map(async (meeting) => { if (!meeting.scheduledStart) return [meeting.id, { available: false }] as const; try { const availability = await invoke( "rewind_meeting_history_status", { scheduledStart: meeting.scheduledStart }, ); return [meeting.id, availability] as const; } catch (error) { return [ meeting.id, { available: false, reason: error instanceof Error ? error.message : "Earlier local meeting audio is unavailable.", }, ] as const; } }), ).then((entries) => { if (!cancelled) setRewindMeetingHistoryAvailability(Object.fromEntries(entries)); }); return () => { cancelled = true; }; }, [meetings, popoverView]); const startMeetingNotes = useCallback( (meeting: PopoverMeeting, includeFromMeetingStart = false) => { setActiveMeetingId(meeting.id); setMeetingStartMessage( includeFromMeetingStart ? `Including earlier local audio for ${meeting.title}…` : `Starting notes for ${meeting.title}…`, ); emit("meetings:start-transcription", { meetingId: meeting.id, joinUrl: meeting.joinUrl, reason: "user", scheduledStart: meeting.scheduledStart, includeFromMeetingStart, }).catch((err) => { console.error("[clips-popover] start meeting notes failed:", err); setActiveMeetingId(null); setMeetingStartMessage( "Could not start notes. Try again from Meetings.", ); }); }, [], ); const startMeetingNotesAndJoin = useCallback( (meeting: PopoverMeeting, includeFromMeetingStart = false) => { if (meeting.joinUrl) { openMeetingJoinUrl(meeting.joinUrl).catch((err) => { console.error("[clips-popover] open meeting join url failed:", err); }); } startMeetingNotes(meeting, includeFromMeetingStart); hidePopover(); }, [startMeetingNotes], ); const showActiveMeetingPill = useCallback((meetingId: string) => { invoke("recording_pill_show", { meetingId, mode: "meeting" }).catch( (err) => { console.error("[clips-popover] show meeting pill failed:", err); }, ); emit("clips:pill-context", { meetingId, mode: "meeting" }).catch(() => {}); }, []); useEffect(() => { invoke("get_active_meeting_id") .then((meetingId) => { if (meetingId) setActiveMeetingId((current) => current ?? meetingId); }) .catch(() => {}); let stopped = false; const unlistens: Array<() => void> = []; const track = (promise: Promise<() => void>) => { promise .then((unlisten) => { if (stopped) { unlisten(); return; } unlistens.push(unlisten); }) .catch(() => {}); }; track( listen<{ meetingId?: string | null }>( "meetings:transcription-started", (event) => { if (event.payload?.meetingId) { setActiveMeetingId(event.payload.meetingId); setMeetingStartMessage("Meeting notes are live and staying local."); } }, ), ); track( listen<{ meetingId?: string | null }>( "meetings:transcription-stopped", (event) => { setActiveMeetingId((current) => !event.payload?.meetingId || event.payload.meetingId === current ? null : current, ); setMeetingStartMessage(null); }, ), ); track( listen<{ meetingId?: string | null; error?: string }>( "meetings:transcription-error", (event) => { setActiveMeetingId((current) => !event.payload?.meetingId || event.payload.meetingId === current ? null : current, ); setMeetingStartMessage( event.payload?.error || "Could not start meeting notes.", ); }, ), ); track( listen<{ meetingId?: string | null; error?: string }>( "meetings:history-error", (event) => { setMeetingStartMessage( event.payload?.error || "Meeting notes are live, but the earlier local audio could not be included.", ); }, ), ); return () => { stopped = true; unlistens.forEach((unlisten) => unlisten()); unlistens.length = 0; }; }, []); useEffect(() => { if (!popoverVisible || !activeMeetingId) return; showActiveMeetingPill(activeMeetingId); }, [activeMeetingId, popoverVisible, showActiveMeetingPill]); useMeetingTranscription({ callClipsAction, serverUrl, selectedMicId, selectedMicLabel, }); // OAuth (Google) opens in the system browser — the popover WebView can't // share a cookie jar with a separate Tauri WebviewWindow, and the old // approach of opening a WebView at the server root produced a blank window. // Instead: fetch the Google auth URL, open it externally, then poll a // server-side exchange endpoint for the session token. async function signInExternal() { // Synchronous ref guard — prevents a double-click from opening two OAuth // tabs. State updates are async so `signInPending` alone isn't sufficient. if (signInInflightRef.current) return; signInInflightRef.current = true; function stopPolling() { if (pollIntervalRef.current !== null) { clearInterval(pollIntervalRef.current); pollIntervalRef.current = null; } } function finishWithError(message: string) { stopPolling(); signInInflightRef.current = false; setSignInPending(false); setSignInError(message); } try { setSignInError(null); const flowId = crypto.randomUUID?.() || Math.random().toString(36).slice(2) + Date.now().toString(36); const base = serverUrl.replace(/\/+$/, ""); // Open directly in the system browser — the server redirects (302) // to Google's OAuth page, avoiding any cross-origin fetch from // the Tauri WebView. await openExternal( `${base}/_agent-native/google/auth-url?desktop=1&flow_id=${flowId}&redirect=1`, ); setSignInPending(true); // Poll the exchange endpoint for the session token. const start = Date.now(); const TIMEOUT_MS = 180_000; // 3 minutes pollIntervalRef.current = setInterval(async () => { try { const xr = await fetch( `${base}/_agent-native/auth/desktop-exchange?flow_id=${flowId}`, { credentials: "include" }, ); if (!xr.ok) { if (Date.now() - start > TIMEOUT_MS) { stopPolling(); signInInflightRef.current = false; setSignInPending(false); } return; } const xd = await xr.json(); if (xd?.error) { finishWithError( typeof xd.error === "string" ? xd.error : "Google sign-in failed. Please try again.", ); return; } if (xd?.token) { stopPolling(); saveDesktopAuthToken(base, String(xd.token)); // Establish the session cookie when the WebView accepts it; the // bearer token above is the reliable desktop auth path. await fetch( `${base}/_agent-native/auth/session?_session=${xd.token}`, { credentials: "include" }, ); signInInflightRef.current = false; setSignInPending(false); const ok = await checkAuth(); if (!ok) { setSignInError( "Google sign-in completed, but Clips could not save the session. Please try again.", ); } } else if (Date.now() - start > TIMEOUT_MS) { finishWithError("Google sign-in timed out. Please try again."); } } catch { if (Date.now() - start > TIMEOUT_MS) { finishWithError("Google sign-in timed out. Please try again."); } } }, 1500); } catch (err) { console.error("[clips-tray] signInExternal failed:", err); signInInflightRef.current = false; setSignInPending(false); setSignInError( err instanceof Error ? err.message : "Could not open Google sign-in. Please try again.", ); } } // Sign out via the framework's logout endpoint. The cookie clears in the // same webview that will re-check `/auth/session`, so the popover flips // back to the inline sign-in form without a reload. async function signOut() { try { await fetch( `${serverUrl.replace(/\/+$/, "")}/_agent-native/auth/logout`, { method: "POST", credentials: "include" }, ); } catch { // ignore — we'll re-check session regardless } clearDesktopAuthToken(serverUrl); await checkAuth(); setPopoverView("recorder"); } // ---- Esc closes the popover -------------------------------------------- useEffect(() => { function onKey(e: KeyboardEvent) { if (e.key === "Escape") { // Don't close mid-recording — user would lose the recorder handle. if (isRecording) return; // Reset nested views before hide so the next tray open lands on the // main recorder UI instead of resuming scrolled settings/meetings. setPopoverView("recorder"); hidePopover(); } } window.addEventListener("keydown", onKey); return () => window.removeEventListener("keydown", onKey); }, [isRecording]); // ---- popover visibility tracking ---------------------------------------- // ONLY source of truth: explicit `clips:popover-visible` events from Rust, // which fire on every show/hide (including the blur-auto-hide path). // Focus events are NOT reliable here — opening devtools steals focus, // clicking inside the popover re-gains it, etc., which caused an // infinite show_bubble/hide flap when we listened to onFocusChanged. useEffect(() => { // Race-safe listen tracking. `listen()` is async — the unlisten fn // only exists AFTER the IPC round-trip resolves. If React cleanup // fires before that, the "fire-and-forget" `.then((u) => push(u))` // pattern never enqueues the unlisten and the listener leaks // forever. Each leaked listener closes over the effect scope + // React state, so every remount of this component grows heap. // Track `cancelled` and call the unlisten IMMEDIATELY if it arrives // after cleanup ran. let cancelled = false; const unlistens: Array<() => void> = []; const track = (p: Promise<() => void>) => { p.then((u) => { if (cancelled) { try { u(); } catch { // ignore } return; } unlistens.push(u); }).catch(() => { // ignore — best-effort }); }; track( listen("clips:popover-visible", (ev) => { console.log("[clips-popover] popover-visible =", ev.payload); const visible = !!ev.payload; setPopoverVisible(visible); // Leaving settings/meetings/dictation mid-scroll should not resume on // the next open — always return to the main recorder surface. if (!visible) setPopoverView("recorder"); }), ); // The bubble window emits `clips:bubble-closed` when the user clicks // the X on the hover controls. Treat that as "camera off": stop the // popover-owned camera track now so the hardware light goes off // immediately, and clear `cameraOn` so the bubble-session effect tears // down the rest (pump/window) and the toggle reflects the new state. track( listen("clips:bubble-closed", () => { console.log( "[clips-popover] bubble-closed received — stopping camera + clearing cameraOn", ); bubbleStreamRef.current?.getTracks().forEach((t) => t.stop()); setCameraOn(false); }), ); // Query the CURRENT visibility on mount in case the event already // fired before React subscribed. getCurrentWindow() .isVisible() .then((v) => { if (cancelled) return; console.log("[clips-popover] initial isVisible =", v); setPopoverVisible(!!v); }) .catch(() => {}); return () => { cancelled = true; unlistens.forEach((u) => { try { u(); } catch { // ignore } }); unlistens.length = 0; }; }, []); const speechPermissionChecked = useRef(false); useEffect(() => { if (!popoverVisible || !micOn || speechPermissionChecked.current) return; speechPermissionChecked.current = true; invoke("native_speech_request_permission").catch((err) => { const message = err instanceof Error ? err.message : String(err); console.warn("[clips-popover] speech permission preflight failed:", err); setRecError( /speech recognition|speech/i.test(message) ? MACOS_SPEECH_PERMISSION_MESSAGE : `Speech recognition unavailable: ${message}`, ); }); }, [micOn, popoverVisible]); // ---- camera bubble session --------------------------------------------- // The bubble overlay (small circular PiP in the bottom-left of the screen // showing the user's face) uses two paths. Browser capture keeps the camera // in this popover for the entire session because WebKit can mute capture // tracks across same-process webviews. Native full-screen capture uses a // local bubble camera because the native screen recorder captures that // overlay directly. // // Lifecycle: // - Popover visible + camera mode + cameraOn → acquire camera, call // show_bubble, then either start the WebRTC/canvas relay (browser // capture) or tell the bubble to start its local camera (native // full-screen capture). User sees their face in the bottom-left corner. // - User clicks Start Recording → popover hides, recording begins. // `isRecording` becomes true, so this effect's deps still say // "active" — the stream + bubble + pump keep running. The recorder // reuses the camera stream for the saved video composite (see // `preAcquiredCameraStream` in recorder.ts). Explicit native full-screen // mode leaves the bubble's local camera stream alone. // - Recording stops → `isRecording` flips back to false, popover // usually hides too, so the effect cleans up: stop tracks, hide // overlays (which closes the bubble window). // - User switches camera / turns camera off / closes popover (not // recording) → cleanup fires, bubble disappears. const bubbleStreamRef = useRef(null); // Set to true the instant handleStartRecording hands `bubbleStreamRef.current` // to `startRecording` as `preAcquiredCameraStream`. The recorder // then owns the track lifecycle — this effect's cleanup MUST NOT stop // the tracks or the MediaRecorder ends up with `readyState: "ended"` // tracks (which causes the laggy / black / silently-failing recording // symptoms). Reset to false once the recording is fully torn down. const bubbleStreamTransferredToRecorder = useRef(false); // Bumped when the native stop path releases the camera mid-session so the // bubble effect re-acquires even if bubbleActive/cameraId are unchanged // (post-stop reopen with a blank "Default Camera" preview). const [bubbleSessionEpoch, setBubbleSessionEpoch] = useState(0); const wantsCamera = mode !== "screen" && cameraOn; const nativeFullscreenRecordingActive = mode !== "camera" && shouldUseNativeFullscreenRecording(source); // Ref mirror of `isRecording || recordingFlowActive` so cleanup (which // captures the dep-snapshot value) can still see the CURRENT flow state // at the moment it actually runs. Without this, if `recordingFlowActive` // briefly flips false on a re-render mid-flow (e.g. finally-block // recovery path), the cleanup function snapshots `bubbleActive=false` // from THAT render and stops the camera stream even though recording is // still in flight. const recordingFlowGateRef = useRef(false); // Stop detaches the recorder state before optimization/upload finishes so a // fresh camera session can recover immediately. Keep that post-stop phase // separate so React cleanup does not close the finalizing progress window. const recordingStopFinalizingRef = useRef(false); useEffect(() => { recordingFlowGateRef.current = isRecording || recordingFlowActive; }, [isRecording, recordingFlowActive]); const bubbleActive = wantsCamera && (popoverVisible || isRecording || recordingFlowActive || recordingFlowGateRef.current); bubbleActiveRef.current = bubbleActive; // The toolbar is recording chrome, not pre-record chrome. Showing it while // the popover is merely open leaves a disabled 0:00 Stop/Pause pill on the // desktop, which reads as a stuck recorder and can trap accessibility clicks. const toolbarActive = isRecording || recordingFlowActive; useEffect(() => { if (!toolbarActive) return; let cancelled = false; (async () => { try { await invoke("show_toolbar"); if (cancelled) return; // Seed disabled — previous recordings may have latched it on in // the toolbar's React state (the window is destroyed on // `hide_overlays`, so this is mostly defensive, but free). emit("clips:toolbar-enabled", false).catch(() => {}); } catch (err) { console.error("[clips-popover] show_toolbar failed:", err); } })(); return () => { cancelled = true; // In screen-only mode the bubble effect never runs, so its // cleanup (which normally hides overlays) never fires either. // Hide them from here instead. Guard on !recordingInFlight so // we don't rip the toolbar out from under an active recording. if (!recordingFlowGateRef.current) { invoke("hide_overlays", { preserveFinalizing: recordingStopFinalizingRef.current, }).catch(() => {}); } }; }, [toolbarActive]); useEffect(() => { if (!bubbleActive) return; setCameraError(null); let cancelled = false; // Dual-transport bookkeeping. We try WebRTC first; if it fails or // times out, we fall back to the canvas pump. Only one should be // active at a time — the ref below guarantees we never double-start. let webrtcHandle: BubbleWebrtcHandle | null = null; let stopPump: (() => void) | null = null; let fellBackToPump = false; let stream: MediaStream | null = null; const startPump = (reason: string) => { if (cancelled || stopPump || !stream) return; fellBackToPump = true; console.log("[clips-popover] starting bubble canvas pump — %s", reason); stopPump = startBubbleFramePump(stream); }; console.log( "[clips-popover] bubble session start — acquiring camera + showing bubble", ); // The saved camera id can go stale (webcam unplugged since last launch). // The fallback helper retries once with the default camera on a // constraint failure instead of leaving the ghost id to fail with // OverconstrainedError; once `loadDevices()` refreshes the list below, // the stale selection itself is cleared by `useMediaDevices`. getCameraStreamWithFallback(cameraId, { width: { ideal: 1280 }, height: { ideal: 720 }, }) .then(async (s) => { if (cancelled) { // Effect re-ran before we resolved — throw this stream away. s.getTracks().forEach((t) => t.stop()); return; } await loadDevices(); stream = s; bubbleStreamRef.current = s; // Open the bubble window. It's a pure renderer — the bubble // itself creates an RTCPeerConnection receiver and emits // `clips:bubble-ready` once it's listening. We also keep the // legacy canvas-frame sink around so a WebRTC failure can // fall back to JPEG frames without a bubble reload. try { await invoke("show_bubble"); } catch (err) { console.error("[clips-popover] show_bubble failed:", err); } if (cancelled) { s.getTracks().forEach((t) => t.stop()); return; } // Preferred path: WebRTC. Starts listening for bubble-ready, // then kicks off an offer/answer/ICE dance. If ICE doesn't // connect within the timeout (or fails later) we start the // canvas pump in its place. The pump is our safety net — // proven to work, just slower. const startCanvasFallback = (reason: string) => { if (cancelled || fellBackToPump) return; fellBackToPump = true; console.warn( "[clips-popover] WebRTC bubble failed (%s) — starting canvas pump fallback", reason, ); webrtcHandle?.stop(); webrtcHandle = null; startPump(reason); }; webrtcHandle = startBubbleWebrtc({ stream: s, onConnected: () => { console.log( "[clips-popover] bubble WebRTC connected — video is live", ); }, onFailure: startCanvasFallback, }); }) .catch((err) => { if (cancelled) return; console.error("[clips-popover] camera acquisition failed:", err); const msg = err?.message ?? ""; if ( msg.includes("AVVideoCaptureSource") || msg.includes("sandbox") || err?.name === "NotAllowedError" ) { setCameraError( isMacPlatform() ? MACOS_CAPTURE_PERMISSION_MESSAGE : DESKTOP_CAPTURE_PERMISSION_MESSAGE, ); } else if (isMediaConstraintFailure(err)) { // Even the default-camera retry inside getCameraStreamWithFallback // failed, so no camera is usable right now. Say that plainly // instead of surfacing constraint jargon like "Invalid constraint". setCameraError( "No camera found. Connect a camera, or pick one from the camera menu.", ); } else { setCameraError(`Camera unavailable: ${msg}`); } }); return () => { cancelled = true; const transferred = bubbleStreamTransferredToRecorder.current; const recordingInFlight = recordingFlowGateRef.current; const trackCount = stream ? stream.getTracks().length : 0; console.log( "[clips-popover] bubble session end — transferred=%o recordingInFlight=%o tracks=%d hasWebrtc=%o hasPump=%o", transferred, recordingInFlight, trackCount, !!webrtcHandle, !!stopPump, ); if (webrtcHandle) { webrtcHandle.stop(); webrtcHandle = null; } if (stopPump) { stopPump(); stopPump = null; } // Critical: if the recorder borrowed this stream, it now owns the // track lifecycle. Stopping tracks here would end them out from // under `MediaRecorder`, producing the laggy-bubble / dead-track // bug. The recorder will stop them on `stop()` / `cancel()`. if (stream && !transferred) { stream.getTracks().forEach((t) => t.stop()); // Drop the local closure reference so nothing else pins the // (now-stopped) MediaStream. WebKit's MediaStream is backed by a // native track buffer that GC doesn't reclaim aggressively — any // dangling reference keeps it resident. stream = null; } // If the recorder owns the stream, keep `bubbleStreamRef` pointed // at it so the next re-entry of this effect (if any) doesn't try // to re-acquire while the recorder is still using it. if (!transferred) { bubbleStreamRef.current = null; } // Don't tear down overlays if a recording is still in flight (the // recorder's stop flow calls `hide_recording_chrome` which handles // the bubble correctly). Hiding here mid-flow would kill the // on-screen bubble window the user sees during the recording. // Also skip when the bubble is still wanted and only the capture // source changed (e.g. cameraId flip re-runs this effect): hiding // would race the re-run's show_bubble and close the window out from under it. if (!recordingInFlight && !bubbleActiveRef.current) { invoke("hide_overlays", { preserveFinalizing: recordingStopFinalizingRef.current, }).catch(() => {}); } }; }, [bubbleActive, cameraId, bubbleSessionEpoch]); useEffect(() => { let unlisten: (() => void) | null = null; let cancelled = false; listen("clips:release-camera", () => { console.log(`[popover] releasing camera`); // Native stop closes the bubble and kills tracks while React may still // hold a live RecorderHandle during finalize/upload. Clear ownership so // the bubble session can re-acquire a fresh preview, and so Start is not // stuck on an ended stream. bubbleStreamTransferredToRecorder.current = false; bubbleStreamRef.current?.getTracks().forEach((t) => t.stop()); bubbleStreamRef.current = null; setBubbleSessionEpoch((epoch) => epoch + 1); }) .then((u) => { if (cancelled) u(); else unlisten = u; }) .catch(() => {}); return () => { cancelled = true; unlisten?.(); }; }, []); // If the popover reopens while camera is still wanted, and the previous // bubble stream is gone or all tracks have ended (common after native stop // + mid-upload reopen), bump the session epoch so the bubble effect // re-acquires getUserMedia + WebRTC instead of showing a blank "Default // Camera" label with a silent Start. useEffect(() => { if (!popoverVisible || !wantsCamera) return; if (recordingFlowGateRef.current || recordingFlowActive) return; const tracks = bubbleStreamRef.current?.getTracks() ?? []; const needsFreshBubble = tracks.length === 0 || tracks.every((track) => track.readyState === "ended"); if (!needsFreshBubble) return; bubbleStreamTransferredToRecorder.current = false; bubbleStreamRef.current = null; setBubbleSessionEpoch((epoch) => epoch + 1); }, [popoverVisible, wantsCamera, recordingFlowActive]); // ---- auto-size popover to content -------------------------------------- // The Tauri window is fixed-size via tauri.conf.json, but our content // height varies (more rows when a camera is on, Recent list toggle, etc.). // A descendant-aware observer tells Rust what the current content height is // and we call `resize_popover` to match. const appRef = useRef(null); usePopoverAutoSize(appRef, { disabled: !popoverVisible || isRecording || recordingFlowActive, width: popoverView === "settings" || popoverView === "memory" ? 440 : 360, }); const loadPendingUploads = useCallback(async () => { const [nativeResult, browserResult] = await Promise.allSettled([ invoke[]>( "native_fullscreen_pending_uploads", ), listBrowserRecordingBackups(), ]); if (nativeResult.status === "rejected") { console.warn( "[clips-tray] native pending upload lookup failed:", nativeResult.reason, ); } if (browserResult.status === "rejected") { console.warn( "[clips-tray] browser pending upload lookup failed:", browserResult.reason, ); } const nativeUploads = nativeResult.status === "fulfilled" && Array.isArray(nativeResult.value) ? nativeResult.value.map((upload) => ({ ...upload, kind: "native" as const, })) : []; const browserUploads = browserResult.status === "fulfilled" ? browserResult.value : []; setPendingUploads( [...nativeUploads, ...browserUploads].sort((a, b) => b.savedAt.localeCompare(a.savedAt), ), ); }, []); useEffect(() => { let unlisten: (() => void) | null = null; let cancelled = false; listen("clips:pending-uploads-changed", () => { void loadPendingUploads(); }) .then((stop) => { if (cancelled) stop(); else unlisten = stop; }) .catch(() => {}); return () => { cancelled = true; unlisten?.(); }; }, [loadPendingUploads]); useEffect(() => { if (popoverView === "meetings" && popoverVisible) { void fetchUpcomingMeetings(); } }, [fetchUpcomingMeetings, popoverView, popoverVisible]); useEffect(() => { loadPendingUploads(); }, [loadPendingUploads, popoverVisible]); // ---- persist selections ------------------------------------------------- useEffect(() => saveString(MODE_KEY, mode), [mode]); useEffect( () => saveString(VOICE_SHORTCUT_KEY, voiceShortcut), [voiceShortcut], ); useEffect( () => saveString(VOICE_CUSTOM_SHORTCUT_KEY, voiceCustomShortcut), [voiceCustomShortcut], ); useEffect( () => saveString(POPOVER_CUSTOM_SHORTCUT_KEY, popoverCustomShortcut), [popoverCustomShortcut], ); useEffect( () => saveString(RECORD_CUSTOM_SHORTCUT_KEY, recordCustomShortcut), [recordCustomShortcut], ); useEffect(() => saveString(VOICE_MODE_KEY, voiceMode), [voiceMode]); useEffect( () => saveString(VOICE_PROVIDER_KEY, voiceProvider), [voiceProvider], ); useEffect( () => saveString(VOICE_INSTRUCTIONS_KEY, voiceInstructions), [voiceInstructions], ); useEffect(() => saveString(SOURCE_KEY, source), [source]); useEffect(() => saveBool(CAM_ON_KEY, cameraOn), [cameraOn]); useEffect(() => saveBool(MIC_ON_KEY, micOn), [micOn]); useEffect(() => saveBool(SYSTEM_AUDIO_KEY, systemAudioOn), [systemAudioOn]); // ---- actions ----------------------------------------------------------- function openInBrowser(path: string) { const href = `${serverUrl.replace(/\/+$/, "")}${path}`; openExternal(href).catch((err) => { console.error("[clips-tray] open failed:", err); }); } function openRewindDocs() { openExternal(REWIND_DOCS_URL).catch((err) => { console.error("[clips-tray] open Rewind docs failed:", err); }); } // Never rejects: a clipboard failure must not be mistaken for a failed // recording by the stop/retry callers that await this inside their try block. async function copyShareLink( recordingId: string, origin = serverUrl, { notify = true }: { notify?: boolean } = {}, ) { try { const url = recordingShareUrl(recordingId, origin); const copied = await copyRecordingShareLink(recordingId, origin); setShareLinkNotice({ recordingId, origin, url, copied }); // The popover hides right after an automatic copy, so the banner below is // only seen on the next open — the OS notification is the live signal. if (copied && notify) { await sendNativeNotification({ title: "Link copied", body: "Your clip link is ready to paste.", }); } return copied; } catch (err) { console.error("[clips-tray] copy share link failed:", err); return false; } } async function retryPendingUpload(upload: PendingDesktopUpload) { if (retryingUploadId || exportingUploadId || dismissingUploadId) return; const targetServerUrl = serverUrlForPendingUpload(upload, serverUrl); setRecError(null); setRetryingUploadId(upload.recordingId); try { const authToken = loadDesktopAuthToken(targetServerUrl); if (upload.kind === "native") { const result = await invoke<{ verificationPending?: boolean }>( "native_fullscreen_recording_retry_upload", { serverUrl: targetServerUrl, recordingId: upload.recordingId, authToken, cookie: typeof document !== "undefined" ? document.cookie || "" : "", }, ); if (result.verificationPending) { scheduleNativeBackupCleanupAfterProcessing({ serverUrl: targetServerUrl, recordingId: upload.recordingId, authToken, }); } } else { await retryBrowserRecordingBackup({ recordingId: upload.recordingId, serverUrl: targetServerUrl, authToken, }); } await loadPendingUploads(); await copyShareLink(upload.recordingId, targetServerUrl); await openExternal(`${targetServerUrl}/r/${upload.recordingId}`); getCurrentWindow() .hide() .catch(() => {}); emit("clips:popover-visible", false).catch(() => {}); } catch (err) { const message = err instanceof Error ? err.message : String(err); console.error("[clips-tray] retry saved upload failed:", err); setRecError( isStorageSetupFailureMessage(message) ? "Connect storage to finish uploading this saved clip: Builder.io (free tier storage + AI) or S3-compatible storage." : message, ); await loadPendingUploads(); } finally { setRetryingUploadId(null); } } async function exportPendingUpload(upload: PendingDesktopUpload) { if (retryingUploadId || exportingUploadId || dismissingUploadId) return; setRecError(null); if (upload.kind === "native") { openPendingUploadFolder(upload); return; } setExportingUploadId(upload.recordingId); try { const exportResult = await exportBrowserRecordingBackup( upload.recordingId, ); setLocalRecordingNotice({ folderPath: exportResult.folderPath, files: [exportResult.file], }); await invoke("open_local_recording_folder", { path: exportResult.folderPath, }); } catch (err) { const message = err instanceof Error ? err.message : String(err); console.error("[clips-tray] export saved upload failed:", err); setRecError(message); } finally { setExportingUploadId(null); } } async function dismissPendingUpload(upload: PendingDesktopUpload) { if (retryingUploadId || exportingUploadId || dismissingUploadId) return; setRecError(null); setDismissingUploadId(upload.recordingId); setPendingUploads((uploads) => uploads.filter((item) => item.recordingId !== upload.recordingId), ); try { if (upload.kind === "native") { await invoke("native_fullscreen_recording_dismiss_upload", { recordingId: upload.recordingId, }); } else { await dismissBrowserRecordingBackup(upload.recordingId); } await loadPendingUploads(); } catch (err) { const message = err instanceof Error ? err.message : String(err); console.error("[clips-tray] dismiss saved upload failed:", err); setRecError(message); await loadPendingUploads(); } finally { setDismissingUploadId(null); } } function openPendingUploadFolder(upload: PendingDesktopUpload) { if (upload.kind !== "native" || !upload.folderPath) { setRecError("This saved upload is stored in the browser backup cache."); return; } invoke("open_local_recording_folder", { path: upload.folderPath, }).catch((err) => { const message = err instanceof Error ? err.message : String(err); console.error("[clips-tray] open pending upload folder failed:", err); setRecError(message); }); } const openVideoStorageSetup = useCallback( (targetServerUrl?: string) => { const base = (targetServerUrl?.trim() || serverUrl).replace(/\/+$/, ""); setRecError(STORAGE_SETUP_HELP_TEXT); void openExternal(`${base}/record`).catch((err) => { setRecError( err instanceof Error ? err.message : "Could not open Clips storage setup.", ); }); void refreshVideoStorageStatus(); }, [refreshVideoStorageStatus, serverUrl], ); async function handleStartRecording(options?: { ignoreActiveRecorder?: boolean; }) { if (recorder && !options?.ignoreActiveRecorder) { console.warn( "[clips-popover] handleStartRecording ignored — recorder already active", ); setRecError( "Still finishing the last recording. Wait a moment, then try again.", ); return; } const bubbleTracks = bubbleStreamRef.current?.getTracks() ?? []; const bubbleStreamDead = bubbleTracks.length > 0 && bubbleTracks.every((track) => track.readyState === "ended"); if (bubbleStreamDead) { console.warn("[clips-popover] clearing ended bubble stream before start"); bubbleStreamTransferredToRecorder.current = false; bubbleStreamRef.current = null; setBubbleSessionEpoch((epoch) => epoch + 1); } if (localRecordingMode === "off") { if (videoStorageStatus === "checking") { setRecError("Checking video storage. Try again in a moment."); return; } if (videoStorageStatus === "missing") { openVideoStorageSetup(); return; } } setRecError(null); setLocalRecordingNotice(null); setShareLinkNotice(null); console.log("[clips-popover] handleStartRecording clicked", { serverUrl, mode, source, localRecordingMode, cameraOn, micOn, }); if (mode !== "camera" && nativeFullscreenRecordingActive) { try { const granted = await invoke( "request_macos_screen_recording_access", ); if (!granted) { setReadinessOpen(true); setRecError(MACOS_SCREEN_PERMISSION_MESSAGE); openPrivacySettings("screen"); return; } } catch (err) { setReadinessOpen(true); setRecError(err instanceof Error ? err.message : String(err)); return; } } stopAllMicMeters(); // Latch BEFORE the async work so the popover stays in "recording // flow" during the macOS screen-picker focus dance. The bubble // session effect also keys off this flag (via `bubbleActive`) so // the bubble + camera stream stay alive while the picker is up. recordingFlowGateRef.current = true; setRecordingFlowActive(true); // Tell Rust we're entering the recording flow NOW, not after the // handle arrives. The macOS screen-picker dialog steals focus from // the popover, which would otherwise trigger the blur-auto-hide // mid-setup — so the countdown and toolbar render behind a hidden // popover and the user sees nothing happen. invoke("set_recording_state", { active: true }).catch(() => {}); // Hand the live camera stream to the recorder so it doesn't // re-acquire the camera (which would trigger WebKit's // capture-exclusion mute bug — see `preAcquiredCameraStream` in // recorder.ts). The popover KEEPS ownership: the bubble session // effect's deps still include `isRecording`, so the stream + bubble // + pump stay alive for the entire recording. const preAcquiredCameraStream = mode !== "screen" && cameraOn ? bubbleStreamRef.current : null; // Flip the ownership flag BEFORE kicking off the recorder. Any // bubble-session cleanup that fires after this point must leave the // tracks alone — the recorder now owns them. Cleared in the stop / // cancel / failure paths below. if (preAcquiredCameraStream) { bubbleStreamTransferredToRecorder.current = true; } let handle: RecorderHandle | null = null; let startError: unknown = null; try { // Per Steve: "when we hit Start Recording the popover should disappear // BEFORE the screen picker shows up — otherwise you might accidentally // pick the popover itself." NSWindowSharingNone keeps the popover out // of the final recording, but on modern macOS the picker STILL lists // NSWindowSharingNone windows — only the actual capture is blocked. // So we have to visually hide it early. // // We can't hide() the popover — that suspends its JS and the bubble // frame pump dies. Instead we park it as a 2×2 pinhole on the primary // screen (AppKit sees the window as on-screen, no occlusion // throttling, pump keeps ticking). The pinhole is too small to show // up prominently in the picker and since NSWindowSharingNone is also // set the picker's thumbnail is empty anyway. // // USER ACTIVATION: WebKit requires `getDisplayMedia` to be called // from within a user gesture handler. The first `await` in a click // handler consumes user activation. `startRecording` kicks off // `getDisplayMedia` SYNCHRONOUSLY before its first `await`, so we // start the recording promise FIRST (capturing the gesture), then // park the popover in parallel via a fire-and-forget `invoke`. // `invoke` itself is async — but because `getDisplayMedia` was // already dispatched at that point, user activation has already been // consumed for the purpose that needs it. // // Set `clipsForceAlive` before parking so the bubble frame pump's // `document.hidden` early-out is bypassed even if WebKit flips // visibility=hidden on a pinhole-sized window. (window as unknown as { clipsForceAlive?: boolean }).clipsForceAlive = true; const recordingPromise = startRecording({ serverUrl, mode, source, cameraId, micId: selectedMicId || undefined, // Live label is empty when the stored hashed deviceId no longer // resolves in the current device list; fall back to the persisted label // so the native recorder always has a name to match. recorder.ts also // probes the live track.label at start as the authoritative source. micLabel: selectedMicLabel || micLabel || undefined, authToken: loadDesktopAuthToken(serverUrl), cookie: typeof document !== "undefined" ? document.cookie || "" : "", cameraOn, micOn, systemAudioOn, localRecordingMode, preAcquiredCameraStream, }); // macOS: park the popover to its 2×2 pinhole IMMEDIATELY so it // doesn't appear in the screen picker window list. The native // Rust recorder used for full-screen doesn't need getDisplayMedia // at all, so parking is always safe on macOS. // // Windows: do NOT park before getDisplayMedia resolves. On Windows, // the WebView2 screen picker UI renders within the popover webview — // shrinking the window to 2×2 makes the picker invisible and the // user can never select a screen. The recorder.ts code parks the // popover itself (line ~2165) AFTER the streams are acquired, which // is the correct time on Windows. if (isMacPlatform()) { invoke("park_popover_offscreen").catch(() => {}); emit("clips:popover-visible", false).catch(() => {}); } // No watchdog — the macOS screen picker can stay open indefinitely // (a user deciding which window to capture may take 20, 60, 180 // seconds). A false-positive timeout here fires recovery mid-setup, // which flips `recordingFlowActive` back to false → the bubble // session effect's cleanup runs and stops the popover-owned camera // stream → the recorder ends up with a dead track when the screen // picker finally resolves. If the user actually wants to abort, // canceling the picker throws NotAllowedError and we recover through // the normal error path. handle = await recordingPromise; console.log("[clips-popover] recorder handle received"); } catch (err) { startError = err; } finally { // If the recorder handle was NEVER set, ALWAYS run recovery here — // even if downstream code throws before reaching the failure // branch. This makes the tray-dead symptom impossible: regardless // of WHICH step failed (stream acquisition, countdown, createRecording, // MediaRecorder.start, watchdog, unexpected throw), is_recording_active // is flipped back to false and the popover is re-shown. if (!handle) { console.warn( "[clips-popover] handleStartRecording finally: no handle — running recovery", ); // Clear the force-alive flag if it was latched before the failure. (window as unknown as { clipsForceAlive?: boolean }).clipsForceAlive = false; // Hand the stream back to the popover session. The recorder // never got far enough to take ownership of the tracks, so the // bubble-session effect must be allowed to stop them again on // its next cleanup (e.g. if the user closes the popover). bubbleStreamTransferredToRecorder.current = false; recordingFlowGateRef.current = false; setRecordingFlowActive(false); try { await invoke("set_recording_state", { active: false }); } catch { // ignore — best-effort } try { await invoke("show_popover"); } catch { // ignore — best-effort } } } if (handle) { setRecorder(handle); return; } // Failure path — the recorder never came up. Side-effects (recording // flag + popover visibility) were already restored in the finally // block above. Now surface any non-cancel error to the UI. console.error("[clips-popover] handleStartRecording failed:", startError); // User cancelled the macOS screen-picker (or denied permission). WebKit // often reports both as NotAllowedError; only show the big permissions // banner when the message carries a hard macOS/privacy failure signal. const errName = startError instanceof DOMException || startError instanceof Error ? startError.name : ""; const message = startError instanceof Error ? startError.message : String(startError); if ( errName === "AbortError" || /was cancelled|dismissed|region selection cancelled/i.test(message) ) { return; } if ( errName === "NotAllowedError" && !isHardCapturePermissionError(message) ) { return; } if (isHardCapturePermissionError(message)) { // If an update has finished downloading and is waiting to install, the // safe next step is to restart (which applies the update and gives the // process a clean binary + permission state). Prefer that hint over the // "grant permissions" banner, which is misleading when the readiness // checkmarks are already green. setRecError( isUpdatePendingRestart() ? MACOS_UPDATE_RESTART_MESSAGE : isMacPlatform() ? MACOS_CAPTURE_PERMISSION_MESSAGE : DESKTOP_CAPTURE_PERMISSION_MESSAGE, ); return; } if (isStorageSetupFailureMessage(message)) { setRecError(STORAGE_SETUP_HELP_TEXT); openVideoStorageSetup(); return; } setRecError(message); } recordShortcutHandlerRef.current = () => { if (recorder) { emit("clips:recorder-stop").catch(() => {}); return; } if (recordingFlowGateRef.current || recordingFlowActive) { emit("clips:countdown-cancel").catch(() => {}); return; } setPopoverView("recorder"); if (authStatus === "anon" && localRecordingMode === "off") { setRecError("Sign in to Clips before using the recording shortcut."); invoke("show_popover").catch(() => {}); return; } const canStartFromGlobalShortcut = mode === "camera" || nativeFullscreenRecordingActive; if (!canStartFromGlobalShortcut) { setRecError( "Open Clips and click Start recording to use the selected source.", ); invoke("show_popover").catch(() => {}); return; } void handleStartRecording({ ignoreActiveRecorder: true }); }; useEffect(() => { let cancelled = false; let unlisten: (() => void) | null = null; listen("clips:record-shortcut", () => { recordShortcutHandlerRef.current(); }) .then((u) => { if (cancelled) { try { u(); } catch { // ignore } return; } unlisten = u; }) .catch(() => {}); return () => { cancelled = true; if (unlisten) { try { unlisten(); } catch { // ignore } } }; }, []); function updateReadinessOpen(next: boolean) { setReadinessOpen(next); if (!next) saveBool(READINESS_REVIEWED_KEY, true); } function retryCameraPreview() { setCameraError(null); if (!cameraOn) { setCameraOn(true); return; } setCameraOn(false); window.setTimeout(() => setCameraOn(true), 0); } // When the toolbar or countdown triggers stop/cancel the popover auto- // rehydrates into a "last recording" state so the user has a single-click // path to the playback page + knows the upload landed. useEffect(() => { if (!recorder) return; let cancelled = false; // Each Promise is still pending when this effect might // already be tearing down (a fast stop→cancel toggle, or the effect // re-running due to a new recorder). If the unlisten arrives after // cleanup ran, call it immediately — otherwise Tauri keeps the // listener registered for the lifetime of the webview, and each // orphaned closure pins `recorder` + its MediaStream graph. const unlisteners: Array<() => void> = []; const track = (p: Promise<() => void>) => { p.then((u) => { if (cancelled) { try { u(); } catch { // ignore } return; } unlisteners.push(u); }).catch(() => { // ignore — best-effort }); }; track( listen("clips:recorder-stop", async () => { // Detach the React Start/bubble gate immediately. The recorder keeps // Rust `is_recording_active` and the finalizing overlay guarded until // its durable backup/finalize boundary; keeping this React handle set // through the whole upload made reopen show a blank preview and made // Start a silent no-op. const handle = recorder; recordingStopFinalizingRef.current = true; setRecordingStopFinalizing(true); bubbleStreamTransferredToRecorder.current = false; bubbleStreamRef.current = null; recordingFlowGateRef.current = false; (window as unknown as { clipsForceAlive?: boolean }).clipsForceAlive = false; setRecordingFlowActive(false); setRecorder(null); // Force a fresh bubble session even when bubbleActive stays true // (popover still open / camera still on). Without this epoch bump the // effect does not re-run and reopen shows ended tracks until Start // discovers them. setBubbleSessionEpoch((epoch) => epoch + 1); let stopFailed = false; let stopResult: RecorderStopResult | null = null; try { stopResult = await handle.stop(); if (stopResult.localOnly) { setLocalRecordingNotice({ folderPath: stopResult.localFolder, files: stopResult.localFiles ?? [], }); } else { setLastRecordingId(stopResult.recordingId); // The browser opens `/r/` (the author's dashboard); what lands // on the clipboard must be the public `/share/` link, which is // the one a recipient can actually open. await copyShareLink(stopResult.recordingId); } } catch (err) { stopFailed = true; setRecError(err instanceof Error ? err.message : String(err)); await loadPendingUploads(); } finally { recordingStopFinalizingRef.current = false; setRecordingStopFinalizing(false); invoke("set_recording_state", { active: false }).catch(() => {}); if (stopFailed || stopResult?.localOnly) { invoke("show_popover").catch(() => {}); } else { // Close the popover — recorder.stop() already opened the // recording's page in the default browser. The popover doesn't // need to hang around. getCurrentWindow() .hide() .catch(() => {}); emit("clips:popover-visible", false).catch(() => {}); } } }), ); track( listen("clips:recorder-cancel", async () => { try { await recorder.cancel(); } finally { if (!cancelled) { ( window as unknown as { clipsForceAlive?: boolean } ).clipsForceAlive = false; bubbleStreamTransferredToRecorder.current = false; bubbleStreamRef.current = null; recordingFlowGateRef.current = false; setRecorder(null); setRecordingFlowActive(false); setBubbleSessionEpoch((epoch) => epoch + 1); invoke("set_recording_state", { active: false }).catch(() => {}); invoke("show_popover").catch(() => {}); } } }), ); track( listen("clips:recorder-restart", async () => { try { await recorder.cancel(); } finally { if (!cancelled) { ( window as unknown as { clipsForceAlive?: boolean } ).clipsForceAlive = false; bubbleStreamTransferredToRecorder.current = false; bubbleStreamRef.current = null; recordingFlowGateRef.current = false; setRecorder(null); setRecordingFlowActive(false); setBubbleSessionEpoch((epoch) => epoch + 1); invoke("set_recording_state", { active: false }).catch(() => {}); // Starting a new browser capture must come from a fresh click in // this webview. The toolbar click arrives here through async Tauri // IPC, so reopen the popover and let the next Start click provide // the required user activation. invoke("show_popover").catch(() => {}); } } }), ); return () => { cancelled = true; unlisteners.forEach((u) => { try { u(); } catch { // ignore } }); unlisteners.length = 0; }; }, [recorder, loadPendingUploads]); // Auto-hide on blur is handled on the Rust side (tauri::WindowEvent::Focused). const showCameraRow = mode !== "screen"; // screen-only has no camera const showSourceRow = mode !== "camera"; // camera-only has no screen source async function setHomeRewindRemembering(remembering: boolean) { if (homeScreenMemoryBusy) return; if (remembering && featureConfig?.screenMemory?.enabled !== true) { setRewindSettingsReturnView("recorder"); setPromptRewindEnable(true); setPopoverView("rewind-settings"); return; } setHomeScreenMemoryBusy(true); try { const current = await invoke("get_feature_config"); await invoke("set_feature_config", { config: { ...current, screenMemory: { ...DEFAULT_SCREEN_MEMORY_CONFIG, ...current.screenMemory, // Once Rewind has been set up, the everyday Home switch is a // pause/resume control. Full disable and capture permissions live // in Rewind Settings, so an accidental Home click never tears // down the configured memory system or asks for consent again. enabled: true, paused: !remembering, }, }, }); // The native producer can take a moment to finish pausing. Do not keep // the Home switch locked while that status request waits; the existing // change event and bounded poll will reconcile it, and this best-effort // refresh can do the same without blocking the next resume action. refreshHomeScreenMemoryStatus(); } catch (err) { console.error( "[clips-tray] update Rewind remembering state failed:", err, ); } finally { setHomeScreenMemoryBusy(false); } } const pendingUploadBanner = recordingStopFinalizing ? ( ) : pendingUploads.length > 0 ? ( openVideoStorageSetup(upload.serverUrl)} /> ) : null; async function copyRewindAgentPrompt() { try { await navigator.clipboard.writeText(REWIND_AGENT_PROMPT); setRewindAgentPromptCopied(true); window.setTimeout(() => setRewindAgentPromptCopied(false), 1_500); } catch (err) { console.error("[clips-tray] copy Rewind agent prompt failed:", err); } } if (agentHandoff) { const durationSeconds = Math.max( 1, Math.round( (new Date(agentHandoff.endAt).getTime() - new Date(agentHandoff.startAt).getTime()) / 1_000, ), ); return (

{agentHandoff.status === "pending" ? "Review before sending" : agentHandoff.status === "processing" ? "Making a private Clip" : agentHandoff.status === "ready" ? "Clip sent to your agent" : "Clip handoff needs attention"}

{agentHandoff.reason}

{new Date(agentHandoff.startAt).toLocaleString()} –{" "} {new Date(agentHandoff.endAt).toLocaleTimeString()} ·{" "} {durationSeconds} seconds

Only this interval becomes a private Clip. The rolling Rewind archive and its local paths stay on this Mac. {agentHandoff.agentClipRetention === "forever" ? " This Clip will be kept in your Library." : ` It uses your ${agentHandoff.agentClipRetention.replace("-", " ")} agent-Clip retention setting.`}

{agentHandoff.status === "pending" ? ( <>
Microphone setAgentHandoff({ ...agentHandoff, includeMicrophone }) } label="Include microphone audio" />
Mac audio setAgentHandoff({ ...agentHandoff, includeSystemAudio }) } label="Include Mac audio" />
{agentHandoffPreviewError ? (

{agentHandoffPreviewError}

) : null} ) : agentHandoff.status === "processing" ? (

Materializing the bounded range, uploading it privately, and preparing transcript and frame access for your agent…

) : agentHandoff.status === "ready" ? ( <>

The agent received a temporary access link. The private Clip itself remains in your Library according to your retention setting.

) : ( <>

{agentHandoff.error || "The bounded Clip could not be sent."}

)}
); } if (popoverView === "memory" || popoverView === "rewind-settings") { return (
{pendingUploadBanner} {isRecording ? : null} { saveString(STORAGE_KEY, url.replace(/\/+$/, "")); setServerUrl(url.replace(/\/+$/, "")); setPopoverView("recorder"); }} onOpenRewind={() => setPopoverView("rewind-settings")} onOpenMemory={() => setPopoverView("memory")} onCancel={() => { setPromptRewindEnable(false); setPopoverView( popoverView === "memory" ? "rewind-settings" : rewindSettingsReturnView, ); }} />
); } if (popoverView === "settings") { return (
{pendingUploadBanner} {isRecording ? : null} { saveString(STORAGE_KEY, url.replace(/\/+$/, "")); setServerUrl(url.replace(/\/+$/, "")); setPopoverView("recorder"); }} onOpenRewind={() => { setPromptRewindEnable(false); setRewindSettingsReturnView("settings"); setPopoverView("rewind-settings"); }} onCancel={() => setPopoverView("recorder")} />
); } if (popoverView === "meetings") { return (
{pendingUploadBanner} {isRecording ? : null} setPopoverView("recorder")} onRefresh={fetchUpcomingMeetings} onOpenMeetings={() => openInBrowser("/meetings")} onOpenMeeting={(meetingId) => openInBrowser(`/meetings/${encodeURIComponent(meetingId)}`) } onOpenSettings={() => setPopoverView("settings")} onStartNotes={startMeetingNotes} onStartNotesAndJoin={startMeetingNotesAndJoin} onShowActiveMeeting={showActiveMeetingPill} />
); } if (popoverView === "dictation") { return (
{pendingUploadBanner} {isRecording ? : null} setPopoverView("recorder")} onOpenDictate={() => openInBrowser("/dictate")} onOpenSettings={() => setPopoverView("settings")} />
); } // When unauthenticated, render the sign-in form INLINE in the popover // (not a separate Tauri window). This avoids Tauri 2's separate-WebKit- // data-store-per-WebviewWindow cookie-jar issue — the cookie is set in // the same webview that reads it on the next /auth/session poll. // OAuth (Google / Apple) still needs a browser, so we offer that as a // secondary link via signInExternal(). if (authStatus === "anon") { return (
{pendingUploadBanner} {signInPending ? (

Waiting for browser sign-in…

) : ( <> {signInError ? (
{signInError}
) : null} { setSignInError(null); await checkAuth(); }} onUseBrowser={signInExternal} /> )}
); } return (
{pendingUploadBanner} {isRecording ? : null} {localRecordingMode !== "off" ? ( ) : null} {localRecordingNotice ? ( setLocalRecordingNotice(null)} onOpenFolder={() => { if (!localRecordingNotice.folderPath) return; invoke("open_local_recording_folder", { path: localRecordingNotice.folderPath, }).catch((err) => { console.error("[clips-tray] open local folder failed:", err); }); }} /> ) : null} {shareLinkNotice ? ( { void copyShareLink( shareLinkNotice.recordingId, shareLinkNotice.origin, { notify: false }, ); }} onDismiss={() => setShareLinkNotice(null)} /> ) : null}
{showSourceRow ? ( ) : null} {showCameraRow ? ( { setCameraId(id); setCameraLabel(label); }} onRefresh={() => requestDeviceAccess("camera")} on={cameraOn} onToggle={setCameraOn} /> ) : null} { setMicId(id); setMicLabel(label); }} onRefresh={() => requestDeviceAccess("mic")} on={micOn} onToggle={setMicOn} systemAudio={systemAudioOn} onSystemAudioToggle={setSystemAudioOn} meterActive={popoverVisible && !isRecording && !recordingFlowActive} />
{rewindHomeOpen ? (

{homeRewindPresentation.detail}

{rewindAgentPromptCopied ? "Setup prompt copied" : "Copy setup prompt for your agent"} void setHomeRewindRemembering(remembering) } label={ featureConfig?.screenMemory?.enabled === true ? "Remember with Rewind" : "Set up Rewind" } />
) : null}
{!isRecording ? ( ) : null} {recError ? ( recError === MACOS_UPDATE_RESTART_MESSAGE ? ( ) : recError === MACOS_CAPTURE_PERMISSION_MESSAGE || recError === MACOS_SCREEN_PERMISSION_MESSAGE || recError === DESKTOP_CAPTURE_PERMISSION_MESSAGE ? ( ) : recError === MACOS_SPEECH_PERMISSION_MESSAGE ? ( ) : isStorageSetupFailureMessage(recError) ? ( openVideoStorageSetup()} /> ) : (
{recError}
) ) : null} {cameraError && !recError ? ( cameraError === MACOS_CAPTURE_PERMISSION_MESSAGE || cameraError === DESKTOP_CAPTURE_PERMISSION_MESSAGE ? ( ) : (
{cameraError}
) ) : null}
openInBrowser("/")} /> setPopoverView("meetings")} /> setPopoverView("dictation")} /> setPopoverView("settings")} />
); } // --------------------------------------------------------------------------- function hidePopover() { // Hide the Tauri window + tell Rust so it can broadcast the // popover-visible=false event (which in turn tears down the bubble). getCurrentWindow() .hide() .catch(() => {}); emit("clips:popover-visible", false).catch(() => {}); } function permissionPanesForRecording( mode: CaptureMode, cameraOn: boolean, micOn: boolean, ): MacosPrivacyPane[] { const panes: MacosPrivacyPane[] = []; if (mode !== "camera") panes.push("screen"); if (micOn) panes.push("microphone", "speech"); if (mode !== "screen" && cameraOn) panes.push("camera"); return Array.from(new Set(panes)); } function permissionPaneLabel(pane: MacosPrivacyPane): string { return { camera: "Camera", microphone: "Microphone", screen: "Screen", speech: "Speech", accessibility: "Accessibility", "input-monitoring": "Input Monitoring", }[pane]; } function PermissionRecoveryBanner({ kind, message, panes, onRetry, }: { kind: "recording" | "speech" | "camera"; message: string; panes: MacosPrivacyPane[]; onRetry: () => void; }) { const title = kind === "speech" ? "Transcript setup blocked" : kind === "camera" ? "Camera setup blocked" : "Recording setup blocked"; const uniquePanes = Array.from(new Set(panes)); const canOpenPrivacySettings = isMacPlatform() || isWindowsPlatform(); return (
{title}
{message}
{canOpenPrivacySettings ? uniquePanes.map((pane) => ( )) : null}
); } function UpdateRestartBanner({ message }: { message: string }) { return (
Restart to finish updating
{message}
); } function StorageConnectionBanner({ onConnect }: { onConnect: () => void }) { return (
Connect storage to keep recording
{STORAGE_SETUP_HELP_TEXT}
); } function PendingUploadBanner({ uploads, retryingUploadId, exportingUploadId, dismissingUploadId, onExport, onRetry, onDismiss, onOpenFolder, onConnectStorage, }: { uploads: PendingDesktopUpload[]; retryingUploadId: string | null; exportingUploadId: string | null; dismissingUploadId: string | null; onExport: (upload: PendingDesktopUpload) => void; onRetry: (upload: PendingDesktopUpload) => void; onDismiss: (upload: PendingDesktopUpload) => void; onOpenFolder: (upload: PendingDesktopUpload) => void; onConnectStorage: (upload: PendingDesktopUpload) => void; }) { const latest = uploads[0]; if (!latest) return null; const retrying = retryingUploadId === latest.recordingId; const storageSetupFailure = isStorageSetupFailureMessage(latest.lastError); const exporting = exportingUploadId === latest.recordingId; const canOpenFolder = latest.kind === "native" && !!latest.folderPath; const canExport = latest.kind === "browser"; const actionsDisabled = !!retryingUploadId || !!exportingUploadId || !!dismissingUploadId; const savedLabel = uploads.length === 1 ? "1 Clip saved locally" : `${uploads.length} Clips saved locally`; const nativeCorrupt = latest.kind === "native" && !!latest.corrupt; const title = nativeCorrupt ? uploads.length === 1 ? "Clip could not be finalized" : "Some Clips could not be finalized" : storageSetupFailure ? uploads.length === 1 ? "Connect storage to upload saved Clip" : "Connect storage to upload saved Clips" : savedLabel; const details = [ latest.savedAt ? `saved ${formatAgo(latest.savedAt)}` : null, formatFileSize(latest.bytes), ].filter(Boolean); const errorText = latest.lastError ? latest.lastError.replace(/\s+/g, " ").slice(0, 140) : null; return (
{title}
{nativeCorrupt ? `${details.join(" · ")} · file may be unusable` : storageSetupFailure ? `${details.join(" · ")} · your clip is safe locally` : `${details.join(" · ")}${errorText ? ` · ${errorText}` : ""}`}
{storageSetupFailure ? ( {STORAGE_SETUP_HELP_TEXT} ) : null}
{canOpenFolder ? ( ) : null} {canExport ? ( ) : null} {latest.kind === "native" && latest.corrupt ? null : ( <> {storageSetupFailure ? ( ) : null} )} Dismiss warning and keep the clip in Clip Drafts
); } function FinalizingUploadBanner() { return (
Still finishing your Clip
Recovery options will appear here if saving does not finish.
); } function localRecordingModeLabel(mode: Exclude) { return mode === "separate" ? "Local desktop + camera files" : "Local composed video"; } function LocalRecordingModeBanner({ mode, }: { mode: Exclude; }) { return (
{localRecordingModeLabel(mode)} is on. Clips will save to Movies/Clips and skip upload.
); } function localFileRoleLabel(role: LocalExportedFile["role"]) { return { composed: "Video", desktop: "Desktop", camera: "Camera", }[role]; } function LocalRecordingSavedBanner({ notice, onOpenFolder, onDismiss, }: { notice: LocalRecordingNotice; onOpenFolder: () => void; onDismiss: () => void; }) { const fileSummary = notice.files .map((file) => [localFileRoleLabel(file.role), formatFileSize(file.bytes)] .filter(Boolean) .join(" "), ) .join(" · "); return (
Saved locally
{fileSummary || "Recording saved to Movies/Clips"}
{notice.folderPath ? ( ) : null}
); } function ShareLinkBanner({ notice, onCopy, onDismiss, }: { notice: ShareLinkNotice; onCopy: () => void; onDismiss: () => void; }) { return (
{notice.copied ? "Share link copied" : "Share link ready"}
{notice.url}
{notice.copied ? null : ( )}
); } function Header({ mode, onModeChange, submitterEmail, }: { mode: CaptureMode; onModeChange: (m: CaptureMode) => void; submitterEmail?: string | null; }) { const [tooltipMode, setTooltipMode] = useState(null); const tooltipReadyAtRef = useRef(Date.now() + 600); const tooltipTimerRef = useRef | null>( null, ); const suppressTooltipRef = useRef(false); const clearModeTooltip = useCallback(() => { if (tooltipTimerRef.current) { window.clearTimeout(tooltipTimerRef.current); tooltipTimerRef.current = null; } setTooltipMode(null); }, []); const queueModeTooltip = useCallback( (nextMode: CaptureMode) => { if ( suppressTooltipRef.current || Date.now() < tooltipReadyAtRef.current || tooltipMode === nextMode || tooltipTimerRef.current ) { return; } tooltipTimerRef.current = window.setTimeout(() => { tooltipTimerRef.current = null; if (!suppressTooltipRef.current) { setTooltipMode(nextMode); } }, 350); }, [tooltipMode], ); const leaveModeButton = useCallback(() => { suppressTooltipRef.current = false; clearModeTooltip(); }, [clearModeTooltip]); const pressModeButton = useCallback(() => { suppressTooltipRef.current = true; clearModeTooltip(); }, [clearModeTooltip]); useEffect( () => () => { if (tooltipTimerRef.current) { window.clearTimeout(tooltipTimerRef.current); tooltipTimerRef.current = null; } }, [], ); // Mode-toggle is absolutely centered (visual center of the popover) and the // close button lives top-right as an absolute-positioned sibling, so the // tabs aren't offset by the close button's width. return (
); } function SignInForm({ serverUrl, onSignedIn, onUseBrowser, }: { serverUrl: string; onSignedIn: () => Promise | void; onUseBrowser: () => void; }) { const [email, setEmail] = useState(""); const [password, setPassword] = useState(""); const [submitting, setSubmitting] = useState(false); const [error, setError] = useState(null); const emailRef = useRef(null); useEffect(() => { emailRef.current?.focus(); }, []); async function onSubmit(e: React.FormEvent) { e.preventDefault(); if (submitting) return; setError(null); setSubmitting(true); try { // Post to the framework's Better Auth-backed email/password endpoint. // Production Tauri builds cannot rely on cross-origin cookies sticking, // so the desktop fetch interceptor stores the returned session token and // sends it as Authorization on later same-server requests. const res = await fetch( `${serverUrl.replace(/\/+$/, "")}/_agent-native/auth/login`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ email: email.trim(), password }), credentials: "include", }, ); const json = (await res.json().catch(() => null)) as { error?: string; token?: string; } | null; if (!res.ok) { throw new Error(json?.error || `Sign in failed (${res.status})`); } if (json?.token) saveDesktopAuthToken(serverUrl, json.token); await onSignedIn(); } catch (err) { setError(err instanceof Error ? err.message : String(err)); } finally { setSubmitting(false); } } return (
Sign in to Clips
setEmail(e.target.value)} required /> setPassword(e.target.value)} required /> {error ?
{error}
: null}
or
); } function PopoverSubViewHeader({ title, onBack, action, }: { title: string; onBack: () => void; action?: ReactNode; }) { return (

{title}

{action}
); } function MeetingsPopoverView({ meetings, loading, error, startMessage, activeMeetingId, meetingsEnabled, rewindHistoryAvailability, onBack, onRefresh, onOpenMeetings, onOpenMeeting, onOpenSettings, onStartNotes, onStartNotesAndJoin, onShowActiveMeeting, }: { meetings: PopoverMeeting[]; loading: boolean; error: string | null; startMessage: string | null; activeMeetingId: string | null; meetingsEnabled: boolean; rewindHistoryAvailability: Record; onBack: () => void; onRefresh: () => void; onOpenMeetings: () => void; onOpenMeeting: (meetingId: string) => void; onOpenSettings: () => void; onStartNotes: ( meeting: PopoverMeeting, includeFromMeetingStart?: boolean, ) => void; onStartNotesAndJoin: ( meeting: PopoverMeeting, includeFromMeetingStart?: boolean, ) => void; onShowActiveMeeting: (meetingId: string) => void; }) { const [includeHistoryFor, setIncludeHistoryFor] = useState>( () => new Set(), ); const consumeIncludeHistoryChoice = (meeting: PopoverMeeting): boolean => { const include = includeHistoryFor.has(meeting.id); // This is intentionally a one-shot choice. It never follows the next // meeting, an automatic start, or a tray action around like a lost duck. setIncludeHistoryFor((current) => { const next = new Set(current); next.delete(meeting.id); return next; }); return include; }; return (
Open web } />

Start Granola-style live notes from calendar meetings without hunting through Settings.

{!meetingsEnabled ? (
Meeting notes are off

Turn them on to show reminders and start live transcription.

) : error ? (
Could not load meetings

{error}

) : loading ? (
Loading meetings…

Checking your connected calendar.

) : meetings.length === 0 ? (
No meetings ready

Connect Google Calendar or open the Meetings page to see setup.

) : (
{meetings.map((meeting) => { const canStart = meetingCanStartNotes(meeting); const hasJoin = Boolean(meeting.joinUrl); const isActive = activeMeetingId === meeting.id; const rewindHistory = rewindHistoryAvailability[meeting.id]; const includeHistory = includeHistoryFor.has(meeting.id); return (
{meeting.title}
{formatMeetingWhen(meeting)} {meeting.platform ? ` · ${meeting.platform}` : ""}
{canStart && rewindHistory?.available ? ( ) : null}
{isActive ? ( ) : canStart ? ( ) : ( )}
); })}
)} {startMessage ?

{startMessage}

: null}
); } function DictationPopoverView({ voiceEnabled, voiceShortcut, voiceCustomShortcut, voiceMode, voiceProvider, onBack, onOpenDictate, onOpenSettings, }: { voiceEnabled: boolean; voiceShortcut: VoiceShortcutPreference; voiceCustomShortcut: string; voiceMode: VoiceMode; voiceProvider: VoiceProvider; onBack: () => void; onOpenDictate: () => void; onOpenSettings: () => void; }) { const shortcut = voiceShortcutLabel(voiceShortcut, voiceCustomShortcut); return (
Open web } />
{voiceEnabled ? "Ready to dictate" : "Dictation is off"}
{voiceEnabled ? ( <>

{voiceMode === "toggle" ? "Press once to start, then press again to stop." : "Hold the shortcut while speaking; release to paste."}

Shortcut {shortcut}
Provider {voiceProviderLabel(voiceProvider)}
) : (

Turn on voice dictation to speak to type anywhere on your Mac.

)}
); } function BottomButton({ icon, label, badge, onClick, }: { icon: "library" | "settings" | "meetings" | "dictation"; label: string; badge?: string; onClick: () => void; }) { return ( ); } function ActiveRecordingBanner() { return (
Recording in progress
); } // ---- inline icons (Tabler-style, monochrome, stroke=1.75) ----------------- // --------------------------------------------------------------------------- type VoiceProviderStatus = { browser: true; // Apple's SFSpeechRecognizer + AVAudioEngine driven from Rust. The // server reports `true` whenever it's available; the desktop client // additionally has it gated to macOS at the Tauri-command layer. "macos-native": boolean; builder: boolean; gemini: boolean; groq: boolean; }; function keyForByokProvider(provider: ByokVoiceProvider): string { return { gemini: "GEMINI_API_KEY", groq: "GROQ_API_KEY", }[provider]; } function labelForByokProvider(provider: ByokVoiceProvider): string { return { gemini: "Google Gemini", groq: "Groq", }[provider]; } function formatStorageBytes(bytes: number): string { if (!Number.isFinite(bytes) || bytes <= 0) return "0 MB"; const gb = bytes / (1024 * 1024 * 1024); if (gb >= 1) return `${gb.toFixed(gb >= 10 ? 0 : 1)} GB`; const mb = bytes / (1024 * 1024); return `${Math.max(1, Math.round(mb))} MB`; } function desktopUpdateStatusText(status: UpdateStatus): string { switch (status.state) { case "idle": return "Clips checks automatically after launch, every hour, and when you return."; case "checking": return "Checking for updates..."; case "not-available": return "Clips is up to date."; case "available": return `Update ${status.version} found. Downloading now...`; case "downloading": return `Downloading update ${status.version}... ${status.percent}%`; case "downloaded": return `Update ${status.version} is ready. Restart to install it.`; case "error": return `Update check failed: ${status.message}`; } } function Setup({ surface = "settings", promptRewindEnable = false, recordingActive = false, initial, serverUrl, signedInAs, voiceShortcut, voiceCustomShortcut, popoverCustomShortcut, recordCustomShortcut, voiceMode, voiceProvider, voiceInstructions, shortcutRegistrationError, onVoiceShortcutChange, onVoiceCustomShortcutChange, onPopoverCustomShortcutChange, onRecordCustomShortcutChange, onVoiceModeChange, onVoiceProviderChange, onVoiceInstructionsChange, onConnect, onOpenRewind, onOpenMemory, onCancel, onSignOut, }: { surface?: "settings" | "memory" | "rewind"; promptRewindEnable?: boolean; recordingActive?: boolean; initial?: string | null; serverUrl?: string; signedInAs?: string | null; voiceShortcut: VoiceShortcutPreference; voiceCustomShortcut: string; popoverCustomShortcut: string; recordCustomShortcut: string; voiceMode: VoiceMode; voiceProvider: VoiceProvider; voiceInstructions: string; shortcutRegistrationError: string | null; onVoiceShortcutChange: (value: VoiceShortcutPreference) => void; onVoiceCustomShortcutChange: (value: string) => void; onPopoverCustomShortcutChange: (value: string) => void; onRecordCustomShortcutChange: (value: string) => void; onVoiceModeChange: (value: VoiceMode) => void; onVoiceProviderChange: (value: VoiceProvider) => void; onVoiceInstructionsChange: (value: string) => void; onConnect: (url: string) => void; onOpenRewind?: () => void; onOpenMemory?: () => void; onCancel?: () => void; onSignOut?: () => void; }) { const [url, setUrl] = useState(initial ?? DEFAULT_URL); const [readinessOpen, setReadinessOpen] = useState(false); const featureConfig = useFeatureConfig(); const updateStatus = useUpdateStatus(); const voiceEnabled = featureConfig?.voiceEnabled !== false; const meetingsEnabled = featureConfig?.meetingsEnabled !== false; const launchAtLoginEnabled = featureConfig?.launchAtLoginEnabled !== false; const autoHidePopoverEnabled = featureConfig?.autoHidePopoverEnabled === true; const showInScreenCapture = featureConfig?.showInScreenCapture === true; const localRecordingMode = featureConfig?.localRecordingMode ?? "off"; const observedScreenMemory = featureConfig?.screenMemory ?? DEFAULT_SCREEN_MEMORY_CONFIG; const [screenMemory, setScreenMemory] = useState(observedScreenMemory); const [rewindConsentOpen, setRewindConsentOpen] = useState( promptRewindEnable && observedScreenMemory.enabled !== true, ); const [rewindConsentMode, setRewindConsentMode] = useState< "visuals" | "visuals-audio" >(observedScreenMemory.captureMode ?? "visuals"); const screenMemoryRef = useRef(observedScreenMemory); const screenMemoryMutationRef = useRef(0); const screenMemoryMutationVersionRef = useRef(0); const screenMemoryMutationTailRef = useRef>(Promise.resolve()); const [screenMemoryConfigBusy, setScreenMemoryConfigBusy] = useState(false); useEffect(() => { if (screenMemoryMutationRef.current > 0) return; screenMemoryRef.current = observedScreenMemory; setScreenMemory(observedScreenMemory); }, [observedScreenMemory]); const regionGuides = featureConfig?.regionGuides ?? { enabled: false, rects: [], alwaysVisible: false, }; const regionGuideRects = regionGuides.rects ?? []; const regionGuideCount = regionGuideRects.length; const regionGuidesAlwaysVisible = regionGuides.alwaysVisible === true; const meetingTranscriptionMode: MeetingTranscriptionMode = featureConfig?.meetingTranscriptionMode ?? "ask"; const showMeetingWidgetEnabled = featureConfig?.showMeetingWidgetEnabled !== false; const whisper = useWhisperSettings( featureConfig, voiceProvider, onVoiceProviderChange, nativeVoiceProvider, ); const { catalog: whisperModels, status: whisperStatus, enabled: whisperModelEnabled, modelId: whisperModelId, selectedModel: selectedWhisperModel, deletableModels, } = whisper; const [screenMemoryStatus, setScreenMemoryStatus] = useState(null); const screenMemoryStatusRefreshVersionRef = useRef(0); const [screenMemoryMessage, setScreenMemoryMessage] = useState<{ kind: "ok" | "error"; text: string; } | null>(null); const [screenMemoryExportResult, setScreenMemoryExportResult] = useState(null); const [clipDraftsError, setClipDraftsError] = useState(null); const [screenMemoryBusy, setScreenMemoryBusy] = useState(false); const [rewindEgressEvents, setRewindEgressEvents] = useState< RewindEgressEvent[] >([]); const [rewindEgressOpen, setRewindEgressOpen] = useState(false); const [rewindLocalQuery, setRewindLocalQuery] = useState(""); const [rewindLocalResult, setRewindLocalResult] = useState(null); const [rewindLocalBusy, setRewindLocalBusy] = useState(false); const [rewindLocalError, setRewindLocalError] = useState(null); const [rewindReplayId, setRewindReplayId] = useState(null); const [excludedBundleIdsInput, setExcludedBundleIdsInput] = useState(""); const [excludedApps, setExcludedApps] = useState( [], ); const [excludedAppsBusy, setExcludedAppsBusy] = useState(false); const [agentConnectionBusy, setAgentConnectionBusy] = useState< "codex" | "claude-code" | null >(null); const [agentConnectionMessage, setAgentConnectionMessage] = useState<{ kind: "ok" | "error"; text: string; } | null>(null); const screenMemorySegments = screenMemoryStatus?.recentSegments ?? []; const screenMemoryTotalBytes = screenMemorySegments.reduce( (sum, segment) => sum + segment.bytes, 0, ); const rewindStatusPresentation = getRewindStatusPresentation({ status: screenMemoryStatus, config: screenMemory, clipRecordingActive: recordingActive, }); const captureControlsLocked = recordingActive; useEffect(() => { setExcludedBundleIdsInput( (screenMemory.excludedBundleIds ?? []).join(", "), ); invoke("resolve_rewind_excluded_apps", { bundleIds: screenMemory.excludedBundleIds ?? [], }) .then(setExcludedApps) .catch(() => { setExcludedApps( (screenMemory.excludedBundleIds ?? []).map((bundleId) => ({ bundleId, name: bundleId.split(".").pop() || "Application", installed: false, })), ); }); }, [screenMemory.excludedBundleIds]); const excludedAppGroups = excludedApps.reduce< Array >((groups, app) => { const existing = groups.find( (candidate) => candidate.name.toLowerCase() === app.name.toLowerCase(), ); if (existing) { existing.bundleIds.push(app.bundleId); existing.installed ||= app.installed; existing.path ||= app.path; } else { groups.push({ ...app, bundleIds: [app.bundleId] }); } return groups; }, []); const [providerStatus, setProviderStatus] = useState(null); const [providerStatusLoading, setProviderStatusLoading] = useState(true); const [apiKeyValue, setApiKeyValue] = useState(""); const [apiKeySaving, setApiKeySaving] = useState(false); const [apiKeyMessage, setApiKeyMessage] = useState<{ kind: "ok" | "error"; text: string; } | null>(null); function setVoiceEnabled(enabled: boolean) { if (!featureConfig) return; invoke("set_feature_config", { config: { ...featureConfig, voiceEnabled: enabled }, }).catch((err) => console.error("[settings] set_feature_config failed", err), ); } function setMeetingsEnabled(enabled: boolean) { if (!featureConfig) return; invoke("set_feature_config", { config: { ...featureConfig, meetingsEnabled: enabled }, }).catch((err) => console.error("[settings] set_feature_config failed", err), ); } function setLaunchAtLoginEnabled(enabled: boolean) { if (!featureConfig) return; invoke("set_feature_config", { config: { ...featureConfig, launchAtLoginEnabled: enabled }, }).catch((err) => console.error("[settings] set_feature_config failed", err), ); } function setAutoHidePopoverEnabled(enabled: boolean) { if (!featureConfig) return; invoke("set_feature_config", { config: { ...featureConfig, autoHidePopoverEnabled: enabled }, }).catch((err) => console.error("[settings] set_feature_config failed", err), ); } function setShowInScreenCapture(enabled: boolean) { if (!featureConfig) return; invoke("set_feature_config", { config: { ...featureConfig, showInScreenCapture: enabled }, }).catch((err) => console.error("[settings] set_feature_config failed", err), ); } async function setScreenMemoryConfig( patch: Partial, ) { if (!featureConfig) return; setScreenMemoryMessage(null); const next = { ...DEFAULT_SCREEN_MEMORY_CONFIG, ...screenMemoryRef.current, ...patch, }; const version = ++screenMemoryMutationVersionRef.current; screenMemoryMutationRef.current += 1; screenMemoryRef.current = next; setScreenMemory(next); setScreenMemoryConfigBusy(true); let operation!: Promise; operation = screenMemoryMutationTailRef.current .catch(() => {}) .then(async () => { // Read the latest complete config at execution time so a queued Rewind // edit cannot overwrite an unrelated setting changed while it waited. const current = await invoke("get_feature_config"); await invoke("set_feature_config", { config: { ...current, screenMemory: next }, }); const committed = await invoke("get_feature_config"); if (version === screenMemoryMutationVersionRef.current) { screenMemoryRef.current = committed.screenMemory; setScreenMemory(committed.screenMemory); } }); screenMemoryMutationTailRef.current = operation; try { await operation; } catch (err) { console.error("[settings] set_feature_config failed", err); if (version === screenMemoryMutationVersionRef.current) { const committed = await invoke( "get_feature_config", ).catch(() => null); if (committed) { screenMemoryRef.current = committed.screenMemory; setScreenMemory(committed.screenMemory); } } setScreenMemoryMessage({ kind: "error", text: (err as Error)?.message ?? "Could not update Rewind.", }); } finally { screenMemoryMutationRef.current = Math.max( 0, screenMemoryMutationRef.current - 1, ); if (screenMemoryMutationRef.current === 0) { setScreenMemoryConfigBusy(false); } } } async function installRewindAgentConnection(client: "codex" | "claude-code") { setAgentConnectionBusy(client); setAgentConnectionMessage(null); try { const status = await invoke( "screen_memory_install_agent_connection", { client }, ); setAgentConnectionMessage({ kind: "ok", text: `${client === "codex" ? "Codex" : "Claude Code"} is connected to this Rewind store. Restart the agent app once to load it.`, }); console.info( `[clips-tray] configured ${status.client} Screen Memory MCP at ${status.configPath}`, ); } catch (err) { setAgentConnectionMessage({ kind: "error", text: err instanceof Error ? err.message : String(err), }); } finally { setAgentConnectionBusy(null); } } const refreshScreenMemoryStatus = useCallback(() => { const version = ++screenMemoryStatusRefreshVersionRef.current; invoke("screen_memory_status") .then((status) => { if (version === screenMemoryStatusRefreshVersionRef.current) { setScreenMemoryStatus(status); } }) .catch(() => {}); }, []); function refreshRewindEgressLog() { invoke("rewind_list_evidence_egress", { limit: 20 }) .then(setRewindEgressEvents) .catch((err) => { setScreenMemoryMessage({ kind: "error", text: (err as Error)?.message ?? "Could not read the Rewind access log.", }); }); } async function askRewindLocally() { const query = rewindLocalQuery.trim(); if (!query) return; setRewindLocalBusy(true); setRewindLocalError(null); try { const result = await invoke("rewind_local_ask", { query, limit: 12, }); setRewindLocalResult(result); } catch (err) { setRewindLocalError( (err as Error)?.message ?? "Could not search local Rewind evidence.", ); } finally { setRewindLocalBusy(false); } } async function replayRewindMoment( evidence: RewindLocalAskResult["evidence"][number], ) { setRewindReplayId(evidence.id); setRewindLocalError(null); try { await invoke("rewind_replay_moment", { segmentId: evidence.segmentId, offsetMs: evidence.offsetMs, }); } catch (err) { setRewindLocalError( (err as Error)?.message ?? "Could not replay this local moment.", ); } finally { setRewindReplayId(null); } } async function exportScreenMemoryRecent() { setScreenMemoryBusy(true); setScreenMemoryMessage(null); setScreenMemoryExportResult(null); try { const result = await invoke( "screen_memory_export_recent", { minutes: 5 }, ); setScreenMemoryExportResult(result); } catch (err) { setScreenMemoryMessage({ kind: "error", text: err instanceof Error ? err.message : String(err), }); } finally { setScreenMemoryBusy(false); refreshScreenMemoryStatus(); } } async function clearScreenMemory() { setScreenMemoryBusy(true); setScreenMemoryMessage(null); try { const status = await invoke( "screen_memory_delete_all", ); screenMemoryStatusRefreshVersionRef.current += 1; setScreenMemoryStatus(status); setScreenMemoryMessage({ kind: "ok", text: "Rewind cleared." }); } catch (err) { setScreenMemoryMessage({ kind: "error", text: (err as Error)?.message ?? "Could not clear Rewind.", }); } finally { setScreenMemoryBusy(false); } } function openScreenMemoryFolder() { invoke("screen_memory_open_folder").catch((err) => { setScreenMemoryMessage({ kind: "error", text: (err as Error)?.message ?? "Could not open Rewind folder.", }); }); } async function chooseExcludedApplications() { setExcludedAppsBusy(true); setScreenMemoryMessage(null); try { const chosen = await invoke( "choose_rewind_excluded_apps", ); if (chosen.length === 0) return; const bundleIds = [ ...new Set([ ...(screenMemory.excludedBundleIds ?? []), ...chosen.map((app) => app.bundleId), ]), ]; await setScreenMemoryConfig({ excludedBundleIds: bundleIds }); } catch (err) { setScreenMemoryMessage({ kind: "error", text: (err as Error)?.message ?? "Could not choose applications.", }); } finally { setExcludedAppsBusy(false); } } function removeExcludedApplications(bundleIds: string[]) { const removed = new Set(bundleIds); void setScreenMemoryConfig({ excludedBundleIds: (screenMemory.excludedBundleIds ?? []).filter( (candidate) => !removed.has(candidate), ), }); } function openClipDraftsFolder() { setClipDraftsError(null); invoke("native_fullscreen_open_drafts_folder").catch((err) => { setClipDraftsError( (err as Error)?.message ?? "Could not open Clip Drafts.", ); }); } function openRegionGuideEditor() { invoke("show_region_guide_editor").catch((err) => console.error("[settings] show_region_guide_editor failed", err), ); } function setRegionGuidesEnabled(enabled: boolean) { if (!featureConfig) return; if (enabled && regionGuideCount === 0) { openRegionGuideEditor(); return; } invoke("set_feature_config", { config: { ...featureConfig, regionGuides: { ...regionGuides, enabled, rects: regionGuideRects, ...(enabled ? {} : { alwaysVisible: false }), }, }, }).catch((err) => console.error("[settings] set_feature_config failed", err), ); } function setRegionGuidesAlwaysVisible(enabled: boolean) { if (!featureConfig) return; if (enabled && regionGuideCount === 0) { openRegionGuideEditor(); invoke("set_feature_config", { config: { ...featureConfig, regionGuides: { ...regionGuides, enabled: true, alwaysVisible: true, rects: regionGuideRects, }, }, }).catch((err) => console.error("[settings] set_feature_config failed", err), ); return; } invoke("set_feature_config", { config: { ...featureConfig, regionGuides: { ...regionGuides, alwaysVisible: enabled, enabled: regionGuides.enabled, rects: regionGuideRects, }, }, }).catch((err) => console.error("[settings] set_feature_config failed", err), ); } function clearRegionGuidePreset() { if (!featureConfig) return; invoke("set_feature_config", { config: { ...featureConfig, regionGuides: { enabled: false, rects: [], alwaysVisible: false }, }, }).catch((err) => console.error("[settings] set_feature_config failed", err), ); } function setLocalRecordingMode(mode: LocalRecordingMode) { if (!featureConfig) return; invoke("set_feature_config", { config: { ...featureConfig, localRecordingMode: mode }, }).catch((err) => console.error("[settings] set_feature_config failed", err), ); } function setMeetingTranscriptionMode(mode: MeetingTranscriptionMode) { if (!featureConfig) return; invoke("set_feature_config", { config: { ...featureConfig, meetingTranscriptionMode: mode }, }).catch((err) => console.error("[settings] set_feature_config failed", err), ); } function setShowMeetingWidgetEnabled(enabled: boolean) { if (!featureConfig) return; invoke("set_feature_config", { config: { ...featureConfig, showMeetingWidgetEnabled: enabled }, }).catch((err) => console.error("[settings] set_feature_config failed", err), ); } useEffect(() => { let cancelled = false; const refresh = () => { if (!cancelled) refreshScreenMemoryStatus(); }; refresh(); const timer = window.setInterval(refresh, 5_000); const unlistens: Array<() => void> = []; const track = (p: Promise<() => void>) => { p.then((u) => { if (cancelled) { try { u(); } catch { /* ignore */ } return; } unlistens.push(u); }).catch(() => {}); }; track(listen("clips:screen-memory-changed", refresh)); return () => { cancelled = true; screenMemoryStatusRefreshVersionRef.current += 1; window.clearInterval(timer); unlistens.forEach((u) => { try { u(); } catch { /* ignore */ } }); }; }, [refreshScreenMemoryStatus]); useEffect(() => { const base = (serverUrl ?? initial ?? DEFAULT_URL).replace(/\/+$/, ""); let cancelled = false; setProviderStatusLoading(true); (async () => { try { const res = await fetch( `${base}/_agent-native/voice-providers/status`, { credentials: "include" }, ); if (!res.ok) { if (!cancelled) { setProviderStatus(null); setProviderStatusLoading(false); } return; } // Server emits `native` (no namespace); the client uses // `"macos-native"` as the provider key throughout — remap on the // way in. const json = (await res.json().catch(() => null)) as | (Partial> & { native?: boolean; }) | null; if (cancelled) return; setProviderStatus({ browser: true, "macos-native": Boolean(json?.native), builder: Boolean(json?.builder), gemini: Boolean(json?.gemini), groq: Boolean(json?.groq), }); setProviderStatusLoading(false); } catch { if (!cancelled) { setProviderStatus(null); setProviderStatusLoading(false); } } })(); return () => { cancelled = true; }; }, [serverUrl, initial]); function handleConnect() { const trimmed = url.trim(); if (!trimmed) return; onConnect(trimmed); } const selectedMode = voiceProviderMode(voiceProvider); const byokProvider: ByokVoiceProvider = isByokVoiceProvider(voiceProvider) ? voiceProvider : "gemini"; const providerHint: Record = { native: isMacPlatform() ? "Uses macOS on-device speech recognition for the fastest free dictation." : "Uses the browser's built-in speech recognition when available.", whisper: "Uses the local Whisper model for offline AI transcription.", builder: "Uses Builder.io for fast cleanup. No separate provider key needed.", byok: "Use your own provider key for cleanup.", }; const shortcutHint: Record = { fn: "Press the Fn / globe key to dictate. macOS requires Input Monitoring for this one shortcut.", "cmd-shift-space": "Press Cmd+Shift+Space to dictate. This does not need Input Monitoring.", "ctrl-shift-space": "Press Ctrl+Shift+Space to dictate.", custom: `Press ${voiceCustomShortcut || "your recorded shortcut"} to dictate.`, both: "Any of Fn, Cmd+Shift+Space, or Ctrl+Shift+Space. Includes Fn, so macOS may ask for Input Monitoring.", }; const fnShortcutSelected = voiceShortcut === "fn" || voiceShortcut === "both"; const modeHint: Record = { "push-to-talk": "Hold the shortcut while speaking. Release to stop.", toggle: "Press once to start, again to stop.", }; function selectProviderMode(mode: VoiceProviderMode) { setApiKeyMessage(null); if (mode === "native") { onVoiceProviderChange(nativeVoiceProvider()); } else if (mode === "whisper") { onVoiceProviderChange("whisper"); if (!whisperModelEnabled) whisper.setEnabled(true); } else if (mode === "builder") { onVoiceProviderChange("builder-gemini"); } else { onVoiceProviderChange(byokProvider); } } async function saveApiKey() { const value = apiKeyValue.trim(); if (!value || apiKeySaving) return; const key = keyForByokProvider(byokProvider); const base = (serverUrl ?? initial ?? DEFAULT_URL).replace(/\/+$/, ""); setApiKeySaving(true); setApiKeyMessage(null); try { let res = await fetch( `${base}/_agent-native/secrets/${encodeURIComponent(key)}`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ value }), credentials: "include", }, ); // Some apps may not register every BYOK provider. Fall back to the // ad-hoc secret store so the tray can still wire user-scoped keys. if (res.status === 404) { res = await fetch(`${base}/_agent-native/secrets/adhoc`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ name: key, value, scope: "user", description: `${labelForByokProvider(byokProvider)} key for Clips voice transcription`, }), credentials: "include", }); } if (!res.ok) { const body = (await res.json().catch(() => null)) as { error?: string; } | null; throw new Error(body?.error || `Save failed (${res.status})`); } setProviderStatus((prev) => prev ? { ...prev, [byokProvider]: true } : { browser: true, "macos-native": true, builder: false, gemini: byokProvider === "gemini", groq: byokProvider === "groq", }, ); setApiKeyValue(""); setApiKeyMessage({ kind: "ok", text: `${labelForByokProvider(byokProvider)} key saved.`, }); } catch (err) { setApiKeyMessage({ kind: "error", text: (err as Error)?.message ?? "Could not save key.", }); } finally { setApiKeySaving(false); } } function connectBuilder() { const base = (serverUrl ?? initial ?? DEFAULT_URL).replace(/\/+$/, ""); openExternal(`${base}/_agent-native/builder/connect`).catch((err) => { setApiKeyMessage({ kind: "error", text: (err as Error)?.message ?? "Could not open Builder.io connect.", }); }); } // Only warn when the selected provider has no key/connection on the server. const providerWarning: string | null = (() => { if (providerStatusLoading || !providerStatus) return null; if (selectedMode === "native") return null; if (selectedMode === "whisper") return null; if (selectedMode === "builder") { return providerStatus.builder ? null : "Builder.io is not connected — cleanup will fail until connected."; } if (providerStatus[byokProvider]) return null; return `${keyForByokProvider(byokProvider)} is not set — cleanup will fail until configured.`; })(); const updateChecksSupported = canCheckForUpdates(); const updateBusy = updateStatus.state === "checking" || updateStatus.state === "available" || updateStatus.state === "downloading"; const updateReady = updateStatus.state === "downloaded"; const updateStatusClass = updateStatus.state === "error" ? "setup-warning" : updateStatus.state === "downloaded" || updateStatus.state === "not-available" ? "setup-success" : "setup-hint"; const updateCheckLabel = !updateChecksSupported ? "Release builds only" : updateStatus.state === "checking" ? "Checking..." : updateStatus.state === "downloading" ? `Downloading ${updateStatus.percent}%` : "Check now"; function checkForDesktopUpdate() { retryUpdateCheck().catch((err) => { console.error("[clips-updater] manual check failed:", err); }); } if (surface !== "settings" && rewindConsentOpen) { return (

Rewind

Turn on Rewind?

Rewind remembers recent moments so you can ask an agent about them or add earlier context to a Clip.

Raw Rewind recordings stay on this Mac. When you ask an agent to search, Clips returns bounded matching context. A media range uploads only through a private Clip handoff.

); } if (surface === "memory") { return (

Manual search

{screenMemory.enabled ? ( <>

This local fallback helps you verify what Rewind remembers. For everyday retrieval, ask Codex or your connected agent instead.

{ event.preventDefault(); void askRewindLocally(); }} > setRewindLocalQuery(event.target.value)} placeholder="Search words you saw or heard…" aria-label="Search memory" maxLength={500} /> {rewindLocalError ? (

{rewindLocalError}

) : null} {rewindLocalResult ? (
{rewindLocalResult.evidence.length} moment {rewindLocalResult.evidence.length === 1 ? "" : "s"} found {rewindLocalResult.coverage.segmentsConsidered} local segments searched
{rewindLocalResult.evidence.length === 0 ? (
No matching moments

Try an app name, a phrase you heard, or text that appeared on screen.

) : ( rewindLocalResult.evidence.map((evidence) => (
{new Date(evidence.capturedAt).toLocaleString()} ·{" "} {evidence.sourceType === "ocr" ? "On-screen text" : evidence.sourceType === "transcript" ? "Audio" : "App context"}

{evidence.excerpt}

)) )}
Coverage and gaps

{rewindLocalResult.coverage.transcriptIndexesReady} audio and {rewindLocalResult.coverage.ocrIndexesReady} visual indexes were ready. Confidence:{" "} {rewindLocalResult.confidence}.

{rewindLocalResult.coverage.gaps.length === 0 ? (

No known capture or index gaps.

) : ( rewindLocalResult.coverage.gaps.map((gap, index) => (

{gap.source} · {gap.detail}

)) )}
) : (
Find the source moment

Results stay grounded in retained local evidence and always lead back to Replay.

)} ) : (
Rewind is off

Turn on a private rolling memory before there is anything to search.

)}
); } if (surface === "rewind") { return (

Rewind settings

{ if (enabled) setRewindConsentOpen(true); else void setScreenMemoryConfig({ enabled: false, paused: false }); }} label="Enable Rewind" />
{captureControlsLocked ? (

Rewind capture settings unlock when this Clip ends. This keeps one screen and audio recorder running at a time.

) : null} {screenMemory.enabled ? ( <>
{rewindStatusPresentation.title} {rewindStatusPresentation.kind === "recording" && !rewindStatusPresentation.hasError ? `${screenMemorySegments.length} retained segment${screenMemorySegments.length === 1 ? "" : "s"} · ${formatStorageBytes(screenMemoryTotalBytes)}` : rewindStatusPresentation.detail}
Privacy
{excludedAppGroups.length > 0 ? (
{excludedAppGroups.map((app) => (
{app.name} {app.installed ? "Excluded" : "Not currently installed · still excluded"}
))}
) : (

No additional apps are excluded.

)}
Agent handoff
Set up your agent once

Copy the setup prompt into any compatible agent. It installs Rewind's reusable instructions and repairs the local connection, so later you can simply say “Look at Rewind.”

Repair an agent connection

Usually your agent can install Rewind from the copied prompt. These buttons are a manual repair for known clients.

{agentConnectionMessage ? (

{agentConnectionMessage.text}

) : null}
void setScreenMemoryConfig({ reviewBeforeSending: enabled, }) } label="Review visual and audio ranges before sending" />
void setScreenMemoryConfig({ autoPreviewBeforeSending: enabled, }) } label="Open a local preview automatically before sending" />

Asking your agent authorizes bounded matching text. Raw Rewind files stay local. If this review is off, an agent-requested media range becomes a private Clip immediately and leaves a receipt.

Ask your agent

Try “What was the Terminal error?” or “Replay Tuesday’s design review.”

{ const open = event.currentTarget.open; setRewindEgressOpen(open); if (open) refreshRewindEgressLog(); }} > Agent activity

See when an agent searched bounded local evidence. Raw Rewind media remains on this Mac unless you explicitly create a private Clip.

{rewindEgressEvents.length === 0 ? (

No matching-text requests yet.

) : ( rewindEgressEvents.slice(0, 10).map((event) => (

{new Date(event.occurredAt).toLocaleString()} {` · ${event.state} · ${event.evidenceCount} item${event.evidenceCount === 1 ? "" : "s"}`}

)) )}
Manage local memory
Save a local Clip

Export the previous five minutes as a video on this Mac. Nothing is uploaded.

{screenMemoryExportResult ? (
Saved locally
) : null}
View Rewind files

Open the private folder where Rewind keeps its temporary local memory.

Erase Rewind memory

Permanently delete all retained media and indexes from this Mac.

{screenMemoryMessage ? (

{screenMemoryMessage.text}

) : null} ) : (
Nothing is being remembered

Turning Rewind on begins a private rolling memory after you choose what it may remember.

)}
); } return (
{onCancel ? ( ) : null}

Settings

General
setUrl(e.target.value)} placeholder="http://localhost:8080" />
{updateReady ? ( ) : null}

{updateChecksSupported ? desktopUpdateStatusText(updateStatus) : "Update checks are available in signed release builds."}

Permissions
Recording
Rewind

{rewindStatusPresentation.title}

{clipDraftsError ? (

{clipDraftsError}

) : null}
void setScreenMemoryConfig({ enabled, paused: false }) } label="Enable Rewind" disabled={screenMemoryConfigBusy || captureControlsLocked} />

Local-only by default. Media uploads and sharing happen only when you make a normal Clip.

{screenMemory.enabled ? ( <>
void setScreenMemoryConfig({ paused })} label="Pause Rewind" disabled={screenMemoryConfigBusy || captureControlsLocked} />

{screenMemory.captureMode === "visuals-audio" ? "Visuals + audio is configured to retain microphone and system audio as separate local tracks." : "Visuals retains screen and app context without selecting audio capture."}

Agents receive bounded matching text when you ask. Raw Rewind media remains local unless a bounded range becomes a private Clip.

Bundle IDs, comma-separated. Password managers are excluded by default. Recognized bundle IDs stop media capture and discard the entire in-flight segment; apps that do not expose a recognized bundle ID cannot be detected.

{rewindStatusPresentation.isLive ? ( ) : ( )} {rewindStatusPresentation.kind === "recording" && !rewindStatusPresentation.hasError ? `Rewind is retaining local coverage: ${screenMemorySegments.length} segment${screenMemorySegments.length === 1 ? "" : "s"}, ${formatStorageBytes(screenMemoryTotalBytes)}.` : rewindStatusPresentation.detail}
{screenMemorySegments[0] ? (

Latest local coverage:{" "} {new Date(screenMemorySegments[0].endedAt).toLocaleTimeString()} .

) : null}
setRewindLocalQuery(event.target.value)} placeholder="What was Lilian saying about the audience?" aria-label="Ask Rewind locally" maxLength={500} onKeyDown={(event) => { if (event.key !== "Enter") return; event.preventDefault(); event.stopPropagation(); void askRewindLocally(); }} />
{rewindLocalError ? (

{rewindLocalError}

) : null} {rewindLocalResult ? (

Local answer:{" "} {rewindLocalResult.answerSummary}

Confidence: {rewindLocalResult.confidence}

Coverage:{" "} {rewindLocalResult.coverage.segmentsConsidered} retained segment {rewindLocalResult.coverage.segmentsConsidered === 1 ? "" : "s"} ; {rewindLocalResult.coverage.transcriptIndexesReady}{" "} transcript and {rewindLocalResult.coverage.ocrIndexesReady}{" "} visual indexes ready. {rewindLocalResult.coverage.gaps.length > 0 ? ` ${rewindLocalResult.coverage.gaps.length} capture or index gap${rewindLocalResult.coverage.gaps.length === 1 ? "" : "s"} may hide matches.` : " No known capture or index gaps."}

{rewindLocalResult.coverage.gaps.length > 0 ? (
Coverage gaps
{rewindLocalResult.coverage.gaps .slice(0, 10) .map((gap, index) => (

{gap.source} · {gap.detail}

))}
) : null} {rewindLocalResult.evidence.length === 0 ? (

No matching evidence to show.

) : ( rewindLocalResult.evidence.map((evidence) => (

{evidence.sourceType} ·{" "} {new Date(evidence.capturedAt).toLocaleString()} {typeof evidence.confidence === "number" ? ` · ${Math.round(evidence.confidence * 100)}% OCR confidence` : ""}
{evidence.excerpt}

)) )} {rewindLocalResult.truncated ? (

More local matches exist; results are bounded to the strongest 12.

) : null}
) : null}
{ const open = event.currentTarget.open; setRewindEgressOpen(open); if (open) refreshRewindEgressLog(); }} > Agent access log

Every bounded Rewind evidence request is recorded on this Mac before matching text is returned. Raw media appears only as a separate private Clip handoff.

{rewindEgressEvents.length === 0 ? (

No evidence requests yet.

) : ( rewindEgressEvents.slice(0, 10).map((event) => (
{new Date(event.occurredAt).toLocaleString()} ·{" "} {event.state} {event.evidenceCount} item {event.evidenceCount === 1 ? "" : "s"}

{event.operation ?? "Agent access"} {" · "}Request {event.requestId}

{event.receipt?.evidence?.map((evidence) => (

{evidence.sourceType} {evidence.capturedAt ? ` · ${new Date(evidence.capturedAt).toLocaleTimeString()}` : ""}
Evidence {evidence.id} · moment {evidence.momentId}

))} {event.receipt?.frames?.map((frame) => (

Local frame {` · ${new Date(frame.timestamp).toLocaleTimeString()}`}
Segment {frame.segmentId}

))} {event.receipt?.mediaInterval ? (

Private Clip range {` · ${new Date(event.receipt.mediaInterval.startAt).toLocaleTimeString()}–${new Date(event.receipt.mediaInterval.endAt).toLocaleTimeString()}`}

) : null} {!event.receipt ? (

This completion record refers to the prepared receipt with request ID {event.requestId}.

) : null} {event.error ? (

{event.error}

) : null}
)) )}
{screenMemoryMessage ? (

{screenMemoryMessage.text}

) : null} ) : null}
Advanced recording

Two-file mode records desktop with audio and a raw rectangular camera video with no audio.

{regionGuides.enabled && (
)}

{regionGuideCount === 0 ? "No guide preset saved yet." : `${regionGuideCount} ${regionGuideCount === 1 ? "rectangle" : "rectangles"} saved.`}

Window and browser-tab sources still open Clips first so the picker can use a click.

Use a modifier combination like Cmd+Shift+K. Leave empty to use only Cmd+Shift+L.

{shortcutRegistrationError ? (

{shortcutRegistrationError}

) : null}
Meetings
{meetingsEnabled ? ( <>

Auto-start still shows the notes pill while transcription is active.

) : null}
Whisper
{selectedWhisperModel ? (

{selectedWhisperModel.description}

) : null} {deletableModels.length > 0 ? (

Other downloaded models

{deletableModels.map((model) => (
{model.title} · {model.sizeMb} MB
))}
) : null}
Dictation
{voiceEnabled ? ( <>

{providerHint[selectedMode]}

{selectedMode === "whisper" && !whisperModelEnabled ? (

Whisper model is disabled. Enable it in the Whisper section above.

) : null} {providerWarning ? (

{providerWarning}

) : null} {selectedMode === "builder" && !providerStatus?.builder ? ( ) : null}
{selectedMode === "byok" ? (
setApiKeyValue(event.target.value)} onKeyDown={(event) => { if (event.key === "Enter") { event.preventDefault(); saveApiKey(); } }} placeholder={ providerStatus?.[byokProvider] ? "Key is saved — paste to rotate" : `Paste ${keyForByokProvider(byokProvider)} here` } className="setup-key-input" />
{providerStatus?.[byokProvider] ? (

{labelForByokProvider(byokProvider)} key is set.

) : null} {apiKeyMessage ? (

{apiKeyMessage.text}

) : null}
) : null} {selectedMode !== "native" && selectedMode !== "whisper" ? (