diff --git a/AGENTS.md b/AGENTS.md index 7c157d0633a..c76bc41f78e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -5,7 +5,7 @@ These rules are enforced by CI (`cargo clippy -D warnings`, Biome). Fixing them afterwards is wasted effort — emit code in the correct shape the FIRST time. Every CI failure caused by one of these rules means the agent didn't read this section. ### Zero-tolerance rules -- **No code comments anywhere.** Not `//`, `/* */`, `///`, `//!`, `#`, JSDoc, nor doc-strings injected into new code. Code must be self-explanatory via naming and types. This applies to every language: Rust, TS, JS, Python, shell, SQL, TOML, etc. +- **Default to no code comments. Add a comment only after solving a bug or working through a complex issue, and only when it captures non-obvious context that a future investigator or reviewer genuinely needs** — e.g. why the fix looks the way it does, the upstream/platform bug being worked around, a non-obvious invariant or trade-off chosen after investigation, or a link to the PR/issue that explains the decision. Bad cases that remain banned: narrating what the code does, restating types, JSDoc that paraphrases parameter names, "TODO: refactor" or "this should be cleaner" notes, and any comment that just describes the change you are currently making. When in doubt, prefer better naming/types over a comment. Applies to every language: Rust, TS, JS, Python, shell, SQL, TOML, etc. - **Never edit generated files**: `**/tauri.ts`, `**/queries.ts`, `apps/desktop/src-tauri/gen/**`, `packages/ui-solid/src/auto-imports.d.ts`, Drizzle migration SQL under `packages/database/migrations/`. - **Never start additional dev servers** (`pnpm dev`, `pnpm dev:web`, `pnpm dev:desktop`, Docker services). Assume they are already running. @@ -69,7 +69,7 @@ Additionally, `unused_must_use = "deny"` applies to all Rust code: every `Result - Naming: files kebab‑case (`user-menu.tsx`); React/Solid components PascalCase; hooks `useX`; Rust modules snake_case; crates kebab‑case. - Runtime: Node 20, pnpm 10.5.2, Rust 1.88+, Docker for MySQL/MinIO. -(See **Pre-Generation Invariants** at the top of this file for the zero-comments rule and the denied clippy/Biome patterns. Those are the source of truth — do not duplicate or weaken them here.) +(See **Pre-Generation Invariants** at the top of this file for the comments policy and the denied clippy/Biome patterns. Those are the source of truth — do not duplicate or weaken them here.) ## Testing - TS/JS: Vitest where present (e.g., desktop). Name tests `*.test.ts(x)` near sources. @@ -86,7 +86,7 @@ Additionally, `unused_must_use = "deny"` applies to all Rust code: every `Result - Database flow: always `db:generate` → `db:push` before relying on new schema. - Keep secrets out of VCS; configure via `.env` from `pnpm env-setup`. - macOS note: desktop permissions (screen/mic) apply to the terminal running `pnpm dev:desktop`. -- All other agent-facing rules (no comments, no editing generated files, clippy/Biome shape, post-edit gates) live in **Pre-Generation Invariants** at the top of this file. +- All other agent-facing rules (comments policy, no editing generated files, clippy/Biome shape, post-edit gates) live in **Pre-Generation Invariants** at the top of this file. ## Effect Usage - Next.js API routes in `apps/web/app/api/*` are built with `@effect/platform`'s `HttpApi` builder; copy the existing class/group/endpoint pattern instead of ad-hoc handlers. diff --git a/CLAUDE.md b/CLAUDE.md index ab2526c458c..4a502d89197 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -7,7 +7,7 @@ This file provides comprehensive guidance to Claude Code when working with code These rules are enforced by CI (`cargo clippy -D warnings`, Biome). Fixing violations after the fact is wasted effort — emit code in the correct shape the FIRST time. Every CI failure tied to a rule below means this section was not respected. ### Zero-tolerance rules -- **No code comments anywhere.** Not `//`, `/* */`, `///`, `//!`, `#`, JSDoc, nor doc strings injected into new code. Applies to Rust, TS, JS, Python, shell, SQL, TOML — every language. Code must explain itself through naming and types. +- **Default to no code comments. Add a comment only after solving a bug or working through a complex issue, and only when it captures non-obvious context that a future investigator or reviewer genuinely needs.** Good cases: why a fix looks the way it does, the upstream/platform bug being worked around, a non-obvious invariant or trade-off chosen after investigation, a link to the PR/issue that explains the decision. Bad cases that remain banned: narrating what the code does, restating types, JSDoc that paraphrases parameter names, "TODO: refactor" or "this should be cleaner" notes, and any comment explaining the change you are currently making. When in doubt, prefer better naming/types over a comment. Applies to every language: Rust, TS, JS, Python, shell, SQL, TOML, etc. - **Never edit generated files**: `**/tauri.ts`, `**/queries.ts`, `apps/desktop/src-tauri/gen/**`, `packages/ui-solid/src/auto-imports.d.ts`. - **Never start additional dev servers** (`pnpm dev`, `pnpm dev:web`, `pnpm dev:desktop`, Docker). Assume the developer has them running. @@ -412,7 +412,7 @@ Minimize `useEffect` usage: compute during render, handle logic in event handler - Components: PascalCase; hooks: camelCase starting with `use`; Rust modules snake_case; crates kebab-case. - Biome formats and lints TS/JS/JSON/CSS (tab indent, double quotes, organizeImports). rustfmt + the workspace clippy lints handle Rust. -The zero-comment rule, the denied clippy patterns, and the Biome style invariants all live in **Pre-Generation Invariants** at the top of this file — that section is authoritative. +The comments policy, the denied clippy patterns, and the Biome style invariants all live in **Pre-Generation Invariants** at the top of this file — that section is authoritative. ## Rust Clippy Rules (Workspace Lints) diff --git a/apps/desktop/src-tauri/src/export.rs b/apps/desktop/src-tauri/src/export.rs index 5e49712348c..09d13739f63 100644 --- a/apps/desktop/src-tauri/src/export.rs +++ b/apps/desktop/src-tauri/src/export.rs @@ -6,9 +6,12 @@ use cap_rendering::{ FrameRenderer, ProjectRecordingsMeta, ProjectUniforms, RenderSegment, RenderVideoConstants, RendererLayers, ZoomFocusInterpolator, spring_mass_damper::SpringMassDamperSimulationConfig, }; +use futures::FutureExt; use image::codecs::jpeg::JpegEncoder; use serde::{Deserialize, Serialize}; use specta::Type; +use std::any::Any; +use std::panic::AssertUnwindSafe; use std::{ path::{Path, PathBuf}, sync::{ @@ -16,7 +19,44 @@ use std::{ atomic::{AtomicBool, Ordering}, }, }; -use tracing::{info, instrument}; +use tracing::{error, info, instrument}; + +fn panic_message(panic: Box) -> String { + if let Some(msg) = panic.downcast_ref::<&str>() { + msg.to_string() + } else if let Some(msg) = panic.downcast_ref::() { + msg.clone() + } else { + "unknown panic".to_string() + } +} + +async fn run_protected_export( + project_path: &Path, + settings: &ExportSettings, + progress: &tauri::ipc::Channel, + force_ffmpeg: bool, +) -> Result { + match AssertUnwindSafe(do_export(project_path, settings, progress, force_ffmpeg)) + .catch_unwind() + .await + { + Ok(result) => result, + Err(panic) => { + let panic_msg = panic_message(panic); + error!( + target: "cap_desktop_export", + panic = %panic_msg, + "export task panicked" + ); + sentry::capture_message( + &format!("Export task panicked: {panic_msg}"), + sentry::Level::Error, + ); + Err("Export failed unexpectedly".to_string()) + } + } +} struct ExportActiveGuard<'a>(&'a AtomicBool); @@ -193,7 +233,7 @@ pub async fn export_video( wait_for_export_preview_idle(&ed.export_preview_active).await; } - let result = do_export(&project_path, &settings, &progress, force_ffmpeg).await; + let result = run_protected_export(&project_path, &settings, &progress, force_ffmpeg).await; match result { Ok(path) => { @@ -206,7 +246,8 @@ pub async fn export_video( e ); - let retry_result = do_export(&project_path, &settings, &progress, true).await; + let retry_result = + run_protected_export(&project_path, &settings, &progress, true).await; match retry_result { Ok(path) => { @@ -351,6 +392,37 @@ pub async fn generate_export_preview( project_path: PathBuf, frame_time: f64, settings: ExportPreviewSettings, +) -> Result { + match AssertUnwindSafe(generate_export_preview_inner( + project_path, + frame_time, + settings, + )) + .catch_unwind() + .await + { + Ok(result) => result, + Err(panic) => { + let panic_msg = panic_message(panic); + error!( + target: "cap_desktop_export", + panic = %panic_msg, + "generate_export_preview panicked" + ); + sentry::capture_message( + &format!("Export preview panicked: {panic_msg}"), + sentry::Level::Error, + ); + Err("Export preview failed unexpectedly".to_string()) + } + } +} + +#[instrument(skip_all)] +async fn generate_export_preview_inner( + project_path: PathBuf, + frame_time: f64, + settings: ExportPreviewSettings, ) -> Result { use base64::{Engine, engine::general_purpose::STANDARD}; use cap_editor::create_segments; @@ -593,6 +665,35 @@ pub async fn generate_export_preview_fast( editor: WindowEditorInstance, frame_time: f64, settings: ExportPreviewSettings, +) -> Result { + match AssertUnwindSafe(generate_export_preview_fast_inner( + editor, frame_time, settings, + )) + .catch_unwind() + .await + { + Ok(result) => result, + Err(panic) => { + let panic_msg = panic_message(panic); + error!( + target: "cap_desktop_export", + panic = %panic_msg, + "generate_export_preview_fast panicked" + ); + sentry::capture_message( + &format!("Export preview panicked: {panic_msg}"), + sentry::Level::Error, + ); + Err("Export preview failed unexpectedly".to_string()) + } + } +} + +#[instrument(skip_all)] +async fn generate_export_preview_fast_inner( + editor: WindowEditorInstance, + frame_time: f64, + settings: ExportPreviewSettings, ) -> Result { use base64::{Engine, engine::general_purpose::STANDARD}; use std::time::Instant; diff --git a/apps/desktop/src-tauri/src/main.rs b/apps/desktop/src-tauri/src/main.rs index 75dfffbdb9a..4de5aa7b60b 100644 --- a/apps/desktop/src-tauri/src/main.rs +++ b/apps/desktop/src-tauri/src/main.rs @@ -4,7 +4,7 @@ use std::sync::Arc; use cap_desktop_lib::DynLoggingLayer; -use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; +use tracing_subscriber::{Layer, layer::SubscriberExt, util::SubscriberInitExt}; fn main() { #[cfg(debug_assertions)] @@ -71,8 +71,11 @@ fn main() { eprintln!("Failed to create logs directory: {e}"); }); - let file_appender = tracing_appender::rolling::daily(&logs_dir, "cap-desktop.log"); - let (non_blocking, _logger_guard) = tracing_appender::non_blocking(file_appender); + let info_file_appender = tracing_appender::rolling::daily(&logs_dir, "cap-desktop.log"); + let (info_file_writer, _info_logger_guard) = tracing_appender::non_blocking(info_file_appender); + + let errors_file_appender = + tracing_appender::rolling::daily(&logs_dir, "cap-desktop-errors.log"); let (otel_layer, _tracer) = if cfg!(debug_assertions) { use opentelemetry::trace::TracerProvider; @@ -125,10 +128,19 @@ fn main() { tracing_subscriber::fmt::layer() .with_ansi(false) .with_target(true) - .with_writer(non_blocking), + .with_writer(info_file_writer), + ) + .with( + tracing_subscriber::fmt::layer() + .with_ansi(false) + .with_target(true) + .with_writer(errors_file_appender) + .with_filter(tracing_subscriber::filter::LevelFilter::WARN), ) .init(); + install_panic_hook(logs_dir.clone()); + #[cfg(debug_assertions)] sentry::configure_scope(|scope| { scope.set_user(Some(sentry::User { @@ -143,3 +155,72 @@ fn main() { .expect("Failed to build multi threaded tokio runtime") .block_on(cap_desktop_lib::run(handle, logs_dir)); } + +fn install_panic_hook(logs_dir: std::path::PathBuf) { + let prev = std::panic::take_hook(); + let panics_log = logs_dir.join("panics.log"); + std::panic::set_hook(Box::new(move |info| { + let location = info + .location() + .map(|l| format!("{}:{}:{}", l.file(), l.line(), l.column())) + .unwrap_or_else(|| "".to_string()); + let message = info + .payload() + .downcast_ref::<&str>() + .map(|s| (*s).to_string()) + .or_else(|| info.payload().downcast_ref::().cloned()) + .unwrap_or_else(|| "".to_string()); + let backtrace = std::backtrace::Backtrace::force_capture(); + let thread = std::thread::current(); + let thread_name = thread.name().unwrap_or("").to_string(); + let timestamp = chrono::Utc::now().to_rfc3339(); + let pid = std::process::id(); + + write_panic_record( + &panics_log, + ×tamp, + pid, + &thread_name, + &location, + &message, + &backtrace, + ); + + tracing::error!( + target: "cap_desktop_panic", + location = %location, + thread = %thread_name, + message = %message, + backtrace = %backtrace, + "panic" + ); + eprintln!( + "[cap-desktop panic] thread '{thread_name}' at {location}: {message}\nbacktrace:\n{backtrace}" + ); + prev(info); + })); +} + +fn write_panic_record( + path: &std::path::Path, + timestamp: &str, + pid: u32, + thread_name: &str, + location: &str, + message: &str, + backtrace: &std::backtrace::Backtrace, +) { + use std::io::Write; + let Ok(mut file) = std::fs::OpenOptions::new() + .create(true) + .append(true) + .open(path) + else { + return; + }; + let _ = writeln!( + file, + "[{timestamp}] pid={pid} thread='{thread_name}' at {location}: {message}\n{backtrace}\n----" + ); + let _ = file.flush(); +} diff --git a/apps/desktop/src/components/ModeSelect.tsx b/apps/desktop/src/components/ModeSelect.tsx index db8445a5f2b..9af75e16061 100644 --- a/apps/desktop/src/components/ModeSelect.tsx +++ b/apps/desktop/src/components/ModeSelect.tsx @@ -15,7 +15,7 @@ interface ModeOptionProps { const ModeOption = (props: ModeOptionProps) => { return (
props.onSelect(props.mode)} class={cx( "relative flex flex-col items-center rounded-xl border-2 transition-all duration-200 cursor-pointer overflow-hidden group", @@ -84,7 +84,7 @@ const ModeSelect = (props: { onClose?: () => void; standalone?: boolean }) => { return (
{props.children} diff --git a/apps/desktop/src/routes/(window-chrome)/onboarding.tsx b/apps/desktop/src/routes/(window-chrome)/onboarding.tsx index 0f761e6de2b..6d741d097c2 100644 --- a/apps/desktop/src/routes/(window-chrome)/onboarding.tsx +++ b/apps/desktop/src/routes/(window-chrome)/onboarding.tsx @@ -303,7 +303,10 @@ function OnboardingAmbientBackdrop() { } export default function OnboardingPage() { - const [step, setStep] = createSignal(0); + const isMacOS = createMemo(() => ostype() === "macos"); + const minStep = createMemo(() => (isMacOS() ? 0 : 1)); + + const [step, setStep] = createSignal(minStep()); const [showStartupOverlay, setShowStartupOverlay] = createSignal(true); const [isExiting, setIsExiting] = createSignal(false); const [permissionsNeeded, setPermissionsNeeded] = createSignal(false); @@ -322,7 +325,6 @@ export default function OnboardingPage() { } }); - const isMacOS = createMemo(() => ostype() === "macos"); const permissionsOnly = createMemo( () => isMacOS() && isRevisit() && permissionsNeeded(), ); @@ -335,6 +337,12 @@ export default function OnboardingPage() { } }); + createEffect(() => { + if (step() < minStep()) { + setStep(minStep()); + } + }); + const totalSteps = createMemo(() => { if (permissionsOnly()) return 1; return 8; @@ -356,7 +364,7 @@ export default function OnboardingPage() { }); const goToStep = (target: number) => { - if (target < 0 || target >= totalSteps()) return; + if (target < minStep() || target >= totalSteps()) return; setStep(target); }; @@ -372,9 +380,6 @@ export default function OnboardingPage() { setIsExiting(true); await generalSettingsStore.set({ hasCompletedStartup: true }); setTimeout(() => { - if (!isMacOS()) { - goToStep(1); - } setShowStartupOverlay(false); setIsExiting(false); }, 600); @@ -397,7 +402,7 @@ export default function OnboardingPage() { if (!nextDisabled() && step() < totalSteps() - 1) goToStep(step() + 1); } else if (e.key === "ArrowLeft") { e.preventDefault(); - if (step() > 0) goToStep(step() - 1); + if (step() > minStep()) goToStep(step() - 1); } else if (e.key === "Enter") { e.preventDefault(); if (!nextDisabled()) handleNext(); @@ -485,7 +490,7 @@ export default function OnboardingPage() { `}
@@ -531,12 +536,12 @@ export default function OnboardingPage() {
goToStep(step() - 1)} onNext={handleNext} nextLabel={nextLabel()} - showBack={step() > 0} + showBack={step() > minStep()} nextDisabled={nextDisabled()} showSkipOnboarding={ corePermsGranted() && @@ -571,14 +576,14 @@ function StepNavigation(props: { }) { return (
-
+
diff --git a/apps/desktop/src/utils/tauri.ts b/apps/desktop/src/utils/tauri.ts index 22b6d2acf8f..e0c1904d9f5 100644 --- a/apps/desktop/src/utils/tauri.ts +++ b/apps/desktop/src/utils/tauri.ts @@ -11,6 +11,9 @@ async setMicInput(label: string | null) : Promise { async setCameraInput(id: DeviceOrModelID | null, skipCameraWindow: boolean | null) : Promise { return await TAURI_INVOKE("set_camera_input", { id, skipCameraWindow }); }, +async setNativeCameraPreviewEnabled(enabled: boolean) : Promise { + return await TAURI_INVOKE("set_native_camera_preview_enabled", { enabled }); +}, async setRecordingMode(mode: RecordingMode) : Promise { return await TAURI_INVOKE("set_recording_mode", { mode }); }, @@ -104,6 +107,9 @@ async generateExportPreviewFast(frameTime: number, settings: ExportPreviewSettin async startVideoImport(sourcePath: string) : Promise { return await TAURI_INVOKE("start_video_import", { sourcePath }); }, +async startImageImport(sourcePath: string) : Promise { + return await TAURI_INVOKE("start_image_import", { sourcePath }); +}, async checkImportReady(projectPath: string) : Promise { return await TAURI_INVOKE("check_import_ready", { projectPath }); }, @@ -429,6 +435,7 @@ videoImportProgress: "video-import-progress" /** user-defined types **/ +export type AllGpusInfo = { gpus: GpuInfoDiag[]; primaryGpuIndex: number | null; isMultiGpuSystem: boolean; hasDiscreteGpu: boolean } export type Annotation = { id: string; type: AnnotationType; x: number; y: number; width: number; height: number; strokeColor: string; strokeWidth: number; fillColor: string; opacity: number; rotation: number; text: string | null; maskType?: MaskType | null; maskLevel?: number | null } export type AnnotationType = "arrow" | "circle" | "rectangle" | "text" | "mask" export type AppTheme = "system" | "light" | "dark" @@ -438,7 +445,7 @@ export type AudioConfiguration = { mute: boolean; improve: boolean; micVolumeDb: export type AudioInputLevelChange = number export type AudioMeta = { path: string; start_time?: number | null; device_id?: string | null } export type AuthSecret = { api_key: string } | { token: string; expires: number } -export type AuthStore = { secret: AuthSecret; user_id: string | null; plan: Plan | null; organizations?: Organization[] } +export type AuthStore = { secret: AuthSecret; user_id: string | null; plan: Plan | null; organizations?: Organization[]; organizations_updated_at?: number | null } export type BackgroundBlurConfig = { mode: BackgroundBlurMode } export type BackgroundBlurMode = "off" | "light" | "heavy" export type BackgroundConfiguration = { source: BackgroundSource; blur: number; padding: number; rounding: number; roundingType: CornerStyle; inset: number; crop: Crop | null; shadow: number; advancedShadow: ShadowConfiguration | null; border: BorderConfiguration | null } @@ -506,6 +513,7 @@ quality: number | null; */ fast: boolean | null } export type GlideDirection = "none" | "left" | "right" | "up" | "down" +export type GpuInfoDiag = { vendor: string; description: string; dedicatedVideoMemoryMb: number; adapterIndex: number; isSoftwareAdapter: boolean; isBasicRenderDriver: boolean; supportsHardwareEncoding: boolean } export type HapticPattern = "alignment" | "levelChange" | "generic" export type HapticPerformanceTime = "default" | "now" | "drawCompleted" export type Hotkey = { code: string; meta: boolean; ctrl: boolean; alt: boolean; shift: boolean } @@ -523,7 +531,6 @@ export type KeyboardTrackSegment = { id: string; start: number; end: number; dis export type LogicalBounds = { position: LogicalPosition; size: LogicalSize } export type LogicalPosition = { x: number; y: number } export type LogicalSize = { width: number; height: number } -export type MacOSVersionInfo = { major: number; minor: number; patch: number; displayName: string; buildNumber: string; isAppleSilicon: boolean } export type MainWindowRecordingStartBehaviour = "close" | "minimise" export type MaskKeyframes = { position?: MaskVectorKeyframe[]; size?: MaskVectorKeyframe[]; intensity?: MaskScalarKeyframe[] } export type MaskKind = "sensitive" | "highlight" @@ -546,7 +553,8 @@ export type OSPermission = "screenRecording" | "camera" | "microphone" | "access export type OSPermissionStatus = "notNeeded" | "empty" | "granted" | "denied" export type OSPermissionsCheck = { screenRecording: OSPermissionStatus; microphone: OSPermissionStatus; camera: OSPermissionStatus; accessibility: OSPermissionStatus } export type OnEscapePress = null -export type Organization = { id: string; name: string; ownerId: string } +export type Organization = { id: string; name: string; ownerId: string; role?: string; canEditBrand?: boolean; iconUrl?: string | null; brandColors?: OrganizationBrandColors } +export type OrganizationBrandColors = { primary: string | null; secondary: string | null; accent: string | null; background: string | null } export type PhysicalSize = { width: number; height: number } export type Plan = { upgraded: boolean; manual: boolean; last_checked: number } export type Platform = "MacOS" | "Windows" @@ -570,6 +578,7 @@ export type RecordingStatus = "pending" | "recording" export type RecordingStopped = null export type RecordingTargetMode = "display" | "window" | "area" | "camera" export type RenderFrameEvent = { frame_number: number; fps: number; resolution_base: XY } +export type RenderingStatus = { isUsingSoftwareRendering: boolean; isUsingBasicRenderDriver: boolean; hardwareEncodingAvailable: boolean; warningMessage: string | null } export type RequestOpenRecordingPicker = { target_mode: RecordingTargetMode | null } export type RequestOpenSettings = { page: string } export type RequestScreenCapturePrewarm = { force?: boolean } @@ -594,7 +603,7 @@ export type StereoMode = "stereo" | "monoL" | "monoR" export type StudioRecordingMeta = { segment: SingleSegment } | { inner: MultipleSegments } export type StudioRecordingQuality = "compatibility" | "balanced" | "ultra" export type StudioRecordingStatus = { status: "InProgress" } | { status: "NeedsRemux" } | { status: "Failed"; error: string } | { status: "Complete" } -export type SystemDiagnostics = { macosVersion: MacOSVersionInfo | null; availableEncoders: string[]; screenCaptureSupported: boolean; metalSupported: boolean; gpuName: string | null } +export type SystemDiagnostics = { windowsVersion: WindowsVersionInfo | null; gpuInfo: GpuInfoDiag | null; allGpus: AllGpusInfo | null; renderingStatus: RenderingStatus; availableEncoders: string[]; graphicsCaptureSupported: boolean; d3D11VideoProcessorAvailable: boolean } export type TargetUnderCursor = { display_id: DisplayId | null; window: WindowUnderCursor | null } export type TextSegment = { start: number; end: number; track?: number; enabled?: boolean; content?: string; center?: XY; size?: XY; fontFamily?: string; fontSize?: number; fontWeight?: number; italic?: boolean; color?: string; fadeDuration?: number } export type TimelineConfiguration = { segments: TimelineSegment[]; zoomSegments: ZoomSegment[]; sceneSegments?: SceneSegment[]; maskSegments?: MaskSegment[]; textSegments?: TextSegment[]; captionSegments?: CaptionTrackSegment[]; keyboardSegments?: KeyboardTrackSegment[] } @@ -614,6 +623,7 @@ export type WindowExclusion = { bundleIdentifier?: string | null; ownerName?: st export type WindowId = string export type WindowPosition = { x: number; y: number; displayId?: DisplayId | null } export type WindowUnderCursor = { id: WindowId; app_name: string; bounds: LogicalBounds } +export type WindowsVersionInfo = { major: number; minor: number; build: number; displayName: string; meetsRequirements: boolean; isWindows11: boolean } export type XY = { x: T; y: T } export type ZoomMode = "auto" | { manual: { x: number; y: number } } export type ZoomSegment = { start: number; end: number; amount: number; mode: ZoomMode; glideDirection?: GlideDirection; glideSpeed?: number; instantAnimation?: boolean; edgeSnapRatio?: number } diff --git a/apps/web/public/.well-known/workflow/v1/manifest.json b/apps/web/public/.well-known/workflow/v1/manifest.json index f5da0be7f7a..6c8d916ac00 100644 --- a/apps/web/public/.well-known/workflow/v1/manifest.json +++ b/apps/web/public/.well-known/workflow/v1/manifest.json @@ -1,18 +1,7 @@ { "version": "1.0.0", "steps": { - "node_modules/.pnpm/workflow@4.2.0-beta.73_@nestjs+common@11.1.17_reflect-metadata@0.2.2_rxjs@7.8.2__@nestj_2a6f07c63175c34d8d104665b34a32ad/node_modules/workflow/dist/internal/builtins.js": { - "__builtin_response_array_buffer": { - "stepId": "__builtin_response_array_buffer" - }, - "__builtin_response_json": { - "stepId": "__builtin_response_json" - }, - "__builtin_response_text": { - "stepId": "__builtin_response_text" - } - }, - "node_modules/.pnpm/workflow@4.2.0-beta.73_@nestjs+common@11.1.17_reflect-metadata@0.2.2_rxjs@7.8.2__@nestj_2a6f07c63175c34d8d104665b34a32ad/node_modules/workflow/dist/stdlib.js": { + "node_modules/.pnpm/workflow@4.2.0-beta.73_@nes_2a6f07c63175c34d8d104665b34a32ad/node_modules/workflow/dist/stdlib.js": { "fetch": { "stepId": "step//workflow@4.2.0-beta.73//fetch" } @@ -34,6 +23,37 @@ "stepId": "step//./workflows/process-video//validateProcessingRequest" } }, + "workflows/import-loom-video.ts": { + "downloadLoomToS3": { + "stepId": "step//./workflows/import-loom-video//downloadLoomToS3" + }, + "processVideoOnMediaServer": { + "stepId": "step//./workflows/import-loom-video//processVideoOnMediaServer" + }, + "saveMetadataAndComplete": { + "stepId": "step//./workflows/import-loom-video//saveMetadataAndComplete" + }, + "setProcessingError": { + "stepId": "step//./workflows/import-loom-video//setProcessingError" + } + }, + "workflows/generate-ai.ts": { + "fetchTranscript": { + "stepId": "step//./workflows/generate-ai//fetchTranscript" + }, + "generateWithAi": { + "stepId": "step//./workflows/generate-ai//generateWithAi" + }, + "markSkipped": { + "stepId": "step//./workflows/generate-ai//markSkipped" + }, + "saveResults": { + "stepId": "step//./workflows/generate-ai//saveResults" + }, + "validateAndSetProcessing": { + "stepId": "step//./workflows/generate-ai//validateAndSetProcessing" + } + }, "workflows/transcribe.ts": { "_enhanceAndSaveAudio": { "stepId": "step//./workflows/transcribe//_enhanceAndSaveAudio" @@ -66,35 +86,15 @@ "stepId": "step//./workflows/transcribe//validateVideo" } }, - "workflows/import-loom-video.ts": { - "downloadLoomToS3": { - "stepId": "step//./workflows/import-loom-video//downloadLoomToS3" - }, - "processVideoOnMediaServer": { - "stepId": "step//./workflows/import-loom-video//processVideoOnMediaServer" - }, - "saveMetadataAndComplete": { - "stepId": "step//./workflows/import-loom-video//saveMetadataAndComplete" - }, - "setProcessingError": { - "stepId": "step//./workflows/import-loom-video//setProcessingError" - } - }, - "workflows/generate-ai.ts": { - "fetchTranscript": { - "stepId": "step//./workflows/generate-ai//fetchTranscript" - }, - "generateWithAi": { - "stepId": "step//./workflows/generate-ai//generateWithAi" - }, - "markSkipped": { - "stepId": "step//./workflows/generate-ai//markSkipped" + "node_modules/.pnpm/workflow@4.2.0-beta.73_@nes_2a6f07c63175c34d8d104665b34a32ad/node_modules/workflow/dist/internal/builtins.js": { + "__builtin_response_array_buffer": { + "stepId": "__builtin_response_array_buffer" }, - "saveResults": { - "stepId": "step//./workflows/generate-ai//saveResults" + "__builtin_response_json": { + "stepId": "__builtin_response_json" }, - "validateAndSetProcessing": { - "stepId": "step//./workflows/generate-ai//validateAndSetProcessing" + "__builtin_response_text": { + "stepId": "__builtin_response_text" } } }, @@ -132,16 +132,16 @@ } } }, - "workflows/transcribe.ts": { - "transcribeVideoWorkflow": { - "workflowId": "workflow//./workflows/transcribe//transcribeVideoWorkflow", + "workflows/import-loom-video.ts": { + "importLoomVideoWorkflow": { + "workflowId": "workflow//./workflows/import-loom-video//importLoomVideoWorkflow", "graph": { "nodes": [ { "id": "start", "type": "workflowStart", "data": { - "label": "Start: transcribeVideoWorkflow", + "label": "Start: importLoomVideoWorkflow", "nodeKind": "workflow_start" } }, @@ -165,16 +165,16 @@ } } }, - "workflows/import-loom-video.ts": { - "importLoomVideoWorkflow": { - "workflowId": "workflow//./workflows/import-loom-video//importLoomVideoWorkflow", + "workflows/generate-ai.ts": { + "generateAiWorkflow": { + "workflowId": "workflow//./workflows/generate-ai//generateAiWorkflow", "graph": { "nodes": [ { "id": "start", "type": "workflowStart", "data": { - "label": "Start: importLoomVideoWorkflow", + "label": "Start: generateAiWorkflow", "nodeKind": "workflow_start" } }, @@ -198,16 +198,16 @@ } } }, - "workflows/generate-ai.ts": { - "generateAiWorkflow": { - "workflowId": "workflow//./workflows/generate-ai//generateAiWorkflow", + "workflows/transcribe.ts": { + "transcribeVideoWorkflow": { + "workflowId": "workflow//./workflows/transcribe//transcribeVideoWorkflow", "graph": { "nodes": [ { "id": "start", "type": "workflowStart", "data": { - "label": "Start: generateAiWorkflow", + "label": "Start: transcribeVideoWorkflow", "nodeKind": "workflow_start" } }, diff --git a/crates/enc-ffmpeg/src/remux.rs b/crates/enc-ffmpeg/src/remux.rs index 25d57d22486..a9fa2fe6597 100644 --- a/crates/enc-ffmpeg/src/remux.rs +++ b/crates/enc-ffmpeg/src/remux.rs @@ -478,8 +478,7 @@ fn probe_video_seek_point_with( if packets_tried >= SEEK_PROBE_PACKET_LIMIT { return Err(format!( - "No decodable frames found within {} packets after seeking to {position_us}us", - SEEK_PROBE_PACKET_LIMIT + "No decodable frames found within {SEEK_PROBE_PACKET_LIMIT} packets after seeking to {position_us}us" )); } } diff --git a/crates/recording/src/output_validation.rs b/crates/recording/src/output_validation.rs index 40c1c17f35a..72da56a4d9d 100644 --- a/crates/recording/src/output_validation.rs +++ b/crates/recording/src/output_validation.rs @@ -81,8 +81,7 @@ pub fn validate_instant_recording( issues.push(issue); } else if ratio < 0.9 { let issue = format!( - "Output duration ({:.1}s) is shorter than expected ({:.1}s)", - actual_secs, expected_secs, + "Output duration ({actual_secs:.1}s) is shorter than expected ({expected_secs:.1}s)" ); info!("{issue}"); issues.push(issue); diff --git a/crates/recording/src/recovery.rs b/crates/recording/src/recovery.rs index d6f793c3c2c..37afb1a090f 100644 --- a/crates/recording/src/recovery.rs +++ b/crates/recording/src/recovery.rs @@ -1103,12 +1103,10 @@ impl RecoveryManager { match probe_video_can_decode(path) { Ok(true) => Ok(()), Ok(false) => Err(RecoveryError::UnplayableVideo(format!( - "{} video has no decodable frames: {path:?}", - label + "{label} video has no decodable frames: {path:?}" ))), Err(e) => Err(RecoveryError::UnplayableVideo(format!( - "{} video validation failed for {path:?}: {e}", - label + "{label} video validation failed for {path:?}: {e}" ))), } } @@ -1124,8 +1122,7 @@ impl RecoveryManager { probe_video_seek_points(path, EXPORT_SEEK_PROBE_SAMPLE_COUNT).map_err(|e| { RecoveryError::UnplayableVideo(format!( - "{} video seek validation failed for {path:?}: {e}", - label + "{label} video seek validation failed for {path:?}: {e}" )) })?; diff --git a/crates/recording/src/sources/microphone.rs b/crates/recording/src/sources/microphone.rs index 90332aeff54..145cb140207 100644 --- a/crates/recording/src/sources/microphone.rs +++ b/crates/recording/src/sources/microphone.rs @@ -573,7 +573,8 @@ mod tests { #[test] fn upsampling_preserves_total_duration() { - use cap_timestamp::{MachAbsoluteTimestamp, Timestamp}; + use cap_timestamp::Timestamp; + use std::time::Instant; let target = AudioInfo::new_raw(Sample::F32(Type::Packed), 48000, 1); let mut resampler = @@ -583,7 +584,7 @@ mod tests { const FRAME_COUNT: usize = 64; const CHANNELS: usize = 2; - let ts = Timestamp::MachAbsoluteTime(MachAbsoluteTimestamp::now()); + let ts = Timestamp::Instant(Instant::now()); let payload_len_bytes = FRAME_SAMPLES * CHANNELS * std::mem::size_of::(); let payload = vec![0u8; payload_len_bytes]; @@ -608,7 +609,8 @@ mod tests { #[test] fn downsampling_preserves_total_duration() { - use cap_timestamp::{MachAbsoluteTimestamp, Timestamp}; + use cap_timestamp::Timestamp; + use std::time::Instant; let target = AudioInfo::new_raw(Sample::F32(Type::Packed), 48000, 1); let mut resampler = @@ -618,7 +620,7 @@ mod tests { const FRAME_COUNT: usize = 64; const CHANNELS: usize = 2; - let ts = Timestamp::MachAbsoluteTime(MachAbsoluteTimestamp::now()); + let ts = Timestamp::Instant(Instant::now()); let payload_len_bytes = FRAME_SAMPLES * CHANNELS * std::mem::size_of::(); let payload = vec![0u8; payload_len_bytes];