Guidance for AI assistants working in this repository. Read this before making changes.
vidcord is a cross-platform desktop app that compresses video files under Discord's size limits. It is built with Tauri 2 (Rust backend + React/TypeScript frontend) and shells out to the system FFmpeg binary for all video work. It does not bundle FFmpeg — the system ffmpeg/ffprobe must be on PATH.
- App version: kept in sync across
package.json,src-tauri/Cargo.toml,src-tauri/tauri.conf.json, and anyvX.Yreferences in source/docs (see "Bumping the version" below) - Window: fixed 460×690, user non-resizable/non-maximizable, opaque window background with macOS Tahoe "liquid glass" styling inside the app surface
- Supported OS/arch: Windows (x86_64 + aarch64), macOS (universal), Linux (x86_64 + aarch64)
- Node:
^20.19.0 || >=22.13.0(seepackage.jsonengines) - Rust: stable toolchain, edition 2021
.
├── src/ # React frontend (TypeScript)
│ ├── App.tsx # Root component — wires hooks, UI, trim timeline, compression
│ ├── App.css # App styling (dark/light via prefers-color-scheme)
│ ├── ErrorBoundary.tsx # Top-level crash recovery UI
│ ├── ipc.ts # Typed wrappers around Tauri invoke() commands
│ ├── main.tsx # ReactDOM entry + native macOS title-bar theme sync
│ ├── index.css # Global CSS vars (--accent, --surface, --blur…)
│ ├── ffmpegErrors.ts # Shared FFmpeg-missing error detection/copy
│ ├── previewScrub.ts # Pure preview-seek and native-context-menu helpers
│ ├── timelineZoom.ts # Pure trim-timeline zoom/view calculations
│ ├── videoMetadata.ts # Pure source-metadata formatting helpers
│ ├── losslessTrim.ts # Pure lossless-fit, keyframe-snap, and keyframe-normalization helpers
│ ├── settingsPresets.ts # Preset schema, normalization, equality, and parsing helpers
│ ├── components/
│ │ ├── PreviewPane.tsx # Video preview + scrub thumbnail + filmstrip
│ │ ├── ProgressSection.tsx# Compression progress bar + ETA
│ │ ├── TrimTimeline.tsx # Memoized trim controls, shortcuts, zoom, playhead UI
│ │ ├── Toast.tsx # Single toast row
│ │ ├── SettingsPresets.tsx # Autosave and named preset save/restore/delete controls
│ │ └── EncodersDialog.tsx # Lazy-loaded FFmpeg encoder list dialog
│ ├── hooks/
│ │ ├── useCompression.ts # Event listeners + pure bitrate/dimension helpers (tested)
│ │ ├── useEncoders.ts # detect_encoders + FFmpeg-missing tracking
│ │ ├── useSettings.ts # persisted compression/output prefs, presets, and debounced saves
│ │ └── useToasts.ts # Toast queue with per-id timer cleanup
│ ├── __tests__/ # Vitest tests (node env, Tauri APIs mocked)
│ └── __mocks__/@tauri-apps/ # Invoke/listen stubs so pure helpers run in Node
├── src-tauri/ # Rust backend
│ ├── src/
│ │ ├── main.rs # Thin entry — calls lib::run()
│ │ ├── lib.rs # Tauri builder, platform shims/theme, file-open routing
│ │ ├── ffmpeg.rs # probe / preview / filmstrip / VAAPI discovery + caches
│ │ ├── ffmpeg/
│ │ │ └── encoders.rs # FFmpeg encoder detection + encoder cache invalidation
│ │ ├── gpu.rs # Vendor detection (lspci / system_profiler / Get-CimInstance)
│ │ ├── log.rs # Rotating ~/…/vidcord/vidcord.log (5 MB cap)
│ │ ├── settings.rs # Typed settings persisted via atomic rename
│ │ └── commands/ # #[tauri::command] handlers
│ │ ├── compression.rs # compression jobs/retries/reservations + preview/probe commands
│ │ ├── encoders.rs # detect/install/list FFmpeg; platform install flows
│ │ ├── files.rs # output locations/staging, file reveal/clipboard, get_os, PendingFile
│ │ └── updates.rs # Semver checks + verified installer download/publication
│ ├── capabilities/default.json # Tauri permissions (dialog, opener, core)
│ ├── windows/
│ │ └── ffmpeg-hooks.nsh # Interactive NSIS post-install FFmpeg/winget offer
│ ├── build.rs # Tauri build entry
│ ├── tauri.conf.json # Product config, CSP, associations, targets, installer hook
│ ├── Cargo.toml # release/ci/dev profiles (see "Build profiles")
│ └── .cargo/audit.toml # RUSTSEC ignore list for Tauri upstream advisories
├── public/
│ └── icon.png # Vite-served app logo
├── site/ # Static marketing/download website for vidcord.app
│ ├── index.html # Crawlable landing page, metadata, JSON-LD, app download UI
│ ├── styles.css # Dark blue responsive site styling
│ ├── script.js # Platform detection + latest GitHub release asset selection
│ ├── robots.txt # Allows search crawlers and AI agents
│ ├── sitemap.xml # Canonical sitemap for vidcord.app
│ ├── llms.txt # Short AI-agent grounding summary
│ ├── llms-full.txt # Expanded AI-agent grounding context
│ ├── site.webmanifest # Site/app manifest
│ └── assets/ # Canonical logo and uncropped product / file-manager screenshots
├── wrangler.jsonc # Cloudflare Worker static-assets deployment config
├── .github/workflows/build.yml# Multi-platform CI + release workflow
├── .github/workflows/site.yml # Site-only validation workflow
├── scripts/ # Bundle, version, asset, sitemap, and structured-data checks
├── index.html # Vite entry
├── vite.config.ts # React plugin, manual chunks (react / tauri / tauri-opener)
├── vitest.config.ts # Node env + Tauri-api mock aliases
├── eslint.config.js # ESLint flat config (TS + react-hooks)
├── .prettierrc # 100-col, 2-space, double-quote, ES5 trailing commas
├── tsconfig.json # Strict TS, ES2021 target, react-jsx
├── FFMPEG_SETUP.md # End-user FFmpeg install guide (per platform)
├── CHANGELOG.md # User-facing release notes
├── .env.example # Optional Vite/Tauri development build variables
└── README.md # Public overview
npm install
npm run tauri dev # starts Vite on :5173, launches the Tauri windowFFmpeg must be on PATH for the app to probe videos or compress.
npm run lint # eslint src
npm run typecheck # tsc --noEmit
npm test # vitest run
npm run build # production frontend bundle
npm run bundle:check # enforce JS/CSS bundle-size budgets after build
npm run version:check # align package/locks/Tauri config/site version references
npm run assets:check # enforce canonical app/site asset organization
npm run format # prettier --write src
cargo fmt --check --manifest-path src-tauri/Cargo.toml
cargo clippy --manifest-path src-tauri/Cargo.toml --tests -- -D warnings
cargo test --manifest-path src-tauri/Cargo.toml
(cd src-tauri && cargo audit) # respects .cargo/audit.tomlCI treats any clippy warning as an error and also runs npm/Rust audit, version, asset, lint, typecheck, test, and build checks — keep new Rust code warning-clean.
npm run tauri build # outputs to src-tauri/target/release/bundle/The NSIS bundle loads src-tauri/windows/ffmpeg-hooks.nsh through
bundle.windows.nsis.installerHooks. On an interactive install, the post-install hook checks for
both ffmpeg and ffprobe; if either is missing and winget exists, it asks the user before
installing the exact Gyan.FFmpeg package. Silent installs never prompt, declining is supported,
and failure falls back to the app's first-launch FFmpeg setup UI. Keep this explicitly opt-in and
do not describe FFmpeg as bundled with vidcord.
The public website lives in site/ and deploys from GitHub when changes are pushed to main.
Do not run Wrangler for normal site deploys.
- Production domain:
https://vidcord.app/ - Workers.dev URL:
https://vidcord-site.cyrz.workers.dev/ - Cloudflare Worker name:
vidcord-site - Legacy/manual deployment config:
wrangler.jsonc→assets.directory = "./site",workers_dev = true - Custom domain route:
vidcord.app - Deployment trigger: push the committed site changes to
origin/main; the GitHub-connected deployment handles publishing.
Site behavior and content:
site/index.htmlis static, crawlable HTML. Keep important product claims visible in HTML, not only in JavaScript.site/script.jsdetects Windows/macOS/Linux, calls GitHub's latest-release API, and links download buttons directly to matching binary assets when the platform and architecture are known. If x64 vs ARM64 cannot be determined with high confidence, prompt the user to choose an architecture; each architecture option should link directly to the matching latest-release binary. It falls back tohttps://github.com/cyroz1/vidcord/releases/latestonly when release metadata cannot be fetched. It also fetches the aggregate release download count from Shields.io with a bounded timeout and strict response validation; failure must leave the count unavailable without affecting download links.- The social/link embed image intentionally uses the logo:
https://vidcord.app/assets/icon.pngviaog:imageandtwitter:image. - Discord and other chat clients may cache old embeds. Use a temporary query string such as
https://vidcord.app/?v=2when checking a changed preview image. - Product screenshots should not be cropped in CSS. Keep
width: 100%andheight: autofor screenshot images unless the user explicitly asks for a cropped composition. - The website documents local processing, FFmpeg as a required system dependency, Discord target sizes, Open With integration, and selectable output destinations (Downloads by default).
SEO and crawler/agent files:
robots.txtshould allow normal web crawlers and AI agents, and point athttps://vidcord.app/sitemap.xml.sitemap.xmlshould include the home page plusllms.txtandllms-full.txt. Update each changed public URL'slastmoddate when its page or grounding content changes.llms.txtis the concise grounding file for AI agents.llms-full.txtis the expanded grounding context. Keep it factual and aligned with the app and README; do not invent hosted compression, bundled FFmpeg, accounts, or telemetry.index.htmlcontains JSON-LD forWebSite,SoftwareApplication, andFAQPage. If site facts change, update visible copy, JSON-LD,llms.txt, andllms-full.txttogether.
Local website preview:
python3 -m http.server 4174 --bind 127.0.0.1 -d siteWebsite validation:
npm run site:checkThe site check runs Prettier, node --check, JSON-LD / manifest parsing, semantic sitemap validation, asset organization checks, and version alignment. If you need to run the structured-data or sitemap parsers directly:
node scripts/check-site-structured-data.mjs
node scripts/check-sitemap.mjsDeploy the website:
git push origin mainPost-deploy checks:
curl -I https://vidcord.app/
curl -I https://vidcord.app/assets/icon.png
curl -L https://vidcord.app/robots.txt
curl -L https://vidcord.app/sitemap.xml
curl -L https://vidcord.app/llms.txt
curl -L https://vidcord.app/llms-full.txt
curl -L https://vidcord.app/site.webmanifestFor browser QA, use the in-app browser when available and check:
- page title and canonical URL
- no blank page or framework overlay
- no relevant console warnings/errors
- JSON-LD types are present
- FAQ and download sections render on desktop and mobile
- screenshot aspect ratios remain uncropped
Site-only changes should not trigger the multi-platform app CI: .github/workflows/build.yml ignores site/**, wrangler.jsonc, the Site Checks workflow, and site-only validator scripts for push and pull_request.
release— LTO,codegen-units=1,strip,panic=abort. Used for tagged releases.ci— inherits release withlto=false,codegen-units=4,opt-level=1. Used by non-tag CI builds for speed.dev.package."*"— deps built atopt-level=1so FFmpeg stderr parsing / regex stay responsive intauri devwhile our crate stays unoptimised for fast incremental compiles.
CI picks the profile based on whether the ref is a v* tag (see build.yml).
All Rust→Frontend IO flows through #[tauri::command] functions registered in src-tauri/src/lib.rs's invoke_handler! macro. When you add a new command:
- Define it in the appropriate file under
src-tauri/src/commands/(or a new module). - Re-export it from
commands/mod.rsif needed and import it inlib.rs. - Add it to the
tauri::generate_handler![…]list inlib.rs— otherwiseinvoke()from the frontend throws at runtime. - Heavy/blocking work (
ffmpeg,ffprobe,lspci,winget, etc.) must run insidetokio::task::spawn_blocking— Tauri's command runtime uses a small async pool and blocking work there stalls the UI.
Events flow the other direction via AppHandle::emit → listen() in the frontend:
compress-progress— percent, eta, status, attempt number, encoder, bitrate (emitted per FFmpeg stderrtime=line and at retry boundaries)compress-done— success/cancelled/message/output_path plus input/output/target size metadata when availableopen-file— path from single-instance forwarding, macOS Apple Events, or CLI argstauri://drag-drop— built-in Tauri event for drops on the window
System notifications are initiated by useToasts: every in-app toast queues the exact same title
and body for the backend send_system_notification command. The backend rechecks main-window focus
immediately before delivery and suppresses the OS notification while focused. All three platforms
use the synchronous notify-rust API so delivery acceptance or failure is returned to the
frontend; macOS uses its modern UNUserNotificationCenter backend. A detached response listener
waits for activation without blocking later notification delivery, then shows, restores, and
focuses the main window when the notification is clicked. Dismissal is a no-op, and macOS response
waiting is bounded because Notification Center's "Clear All" does not yield a response. Delivery
failures are logged without logging notification contents, and the in-app toast remains visible as
the fallback. macOS bundles use Tauri's "-" ad-hoc signing identity so Notification Center can
associate permission with the app's bundle identifier; this is not authenticated Developer ID
signing or notarization. Unbundled tauri dev processes have no bundle identifier and use the
bounded AppleScript notification fallback instead. Keep notification delivery serialized so related
banners retain their in-app order, preserve the non-blocking response listener, respect an explicit
macOS notification-permission denial, register Linux's default action, and escape Linux notification
markup so errors render as literal text.
Compression uses a single owned backend job ID. Cancellation keeps that job active until its FFmpeg
process exits; do not clear frontend compression state before the matching compress-done event or
replace the job-specific state with an unowned global PID. Cancellation gives the owned process a
short graceful-exit window, then force-terminates it only while the same job still owns that PID.
Lossless Trim is a dedicated mode beside Advanced. Its settings area hides unrelated controls while
keeping audio-track removal available; removing audio uses stream-copy with -an and does not
re-encode the video. Size-based compression also compares the selected duration and source average
bitrate against the target capacity and offers the same stream-copy path when it is estimated to
fit. Accepting that offer must explain that keyframe trimming is less precise, switch the UI into
Lossless Trim, and let the user choose the trim points again before export. It discovers source
keyframes through a bounded, generation-owned FFprobe command, snaps visible trim boundaries outward
(start backward and end forward), and attempts -map 0 -c copy without media filters or injected
metadata. It prefers an MP4 stream-copy output, then the validated source container, and finally
falls back to normal compression if both copy attempts are incompatible. Keyframe discovery failure
blocks the selected Lossless Trim export; hidden resolution, crop, FPS, encoder, and audio
normalization controls must remain non-destructive to their saved values.
FFprobe returns source codec, frame rate, bitrate, dimensions, display dimensions, and duration.
videoMetadata.ts formats those values for the import summary. The frontend derives the initial
target bitrate from the selected size and trimmed duration, then caps it at the probed source
bitrate so compression does not request a higher bitrate than the input. On success, the backend
includes the input file size in compress-done so the frontend can report the actual percentage
reduction.
Output handling has two paths:
- Downloads / clip folder / custom folder:
resolve_output_pathchooses a collision-free<stem>-vidcord[-N].<extension>candidate (.mp4for compression, or the validated source video extension for Lossless Trim fallback). Before FFmpeg starts,OutputReservation::createatomically creates that exact path and owns it across every adaptive retry. Treat path resolution as advisory; do not replace the create-new reservation with a check-then-write flow or allow FFmpeg's-yto overwrite an unrelated file. - Ask when done: compression writes only beneath
%TEMP%/vidcord/staged-output(the platform temp equivalent), then the frontend opens a save dialog.publish_staged_outputcanonicalizes and accepts only a direct file child of that staging directory, first tries a same-filesystem hard link to a unique temporary path beside the destination, falls back to a create-new copy when linking is unavailable, syncs the temporary file, atomically publishes it viasettings::replace_file, and removes the staged file. Cancelling the save dialog or failing finalization must calldiscard_staged_output; retain the backend validation and partial-file cleanup.
The persisted completion action is either copy (default) or reveal. Clipboard copy validates
that the output is still a file, then uses CF_HDROP on Windows, AppleScript on macOS, and
wl-copy/xclip on Linux. A copy failure falls back to revealing the saved file; a completion
action failure must not be reported as a compression failure.
useSettings persists output_destination (downloads, source, ask, or custom), the custom
directory, and completion_action (copy or reveal). Keep the frontend's allowlist validation
when loading these string values; invalid or older persisted values must fall back to safe defaults.
Open-with / right-click → Open must work across three delivery mechanisms:
- Windows/Linux CLI arg: handled in
setup()→ stored inPendingFile. - macOS Apple Events:
RunEvent::Openedin the top-level.run(|app, event| …)handler — fires aftersetup(), may fire hot or cold. - Second instance launched while running:
tauri_plugin_single_instance::initfocuses the existing window and emitsopen-filedirectly.
The frontend registers its open-file listener and then invokes frontend_ready; that command marks the listener ready and drains PendingFile under the same mutex used by event delivery. on_page_load resets readiness at PageLoadEvent::Started so a WebView reload cannot emit into a stale React listener. Do not collapse these paths into one — each handles a real race that exists on at least one platform.
The app checks the fixed cyroz1/vidcord GitHub Releases API endpoint and only downloads an
installer after explicit user approval. Preserve all of these controls when changing the updater:
- Select only the expected
vidcord_asset suffix for the current OS/architecture and validate the asset filename before using it locally. - Require GitHub's
sha256:release-asset digest, hash the response incrementally, and reject missing, malformed, or mismatched digests before the file becomes visible or executable. - Enforce the 512 MB limit and response completeness while streaming to a create-new temporary file; never buffer a full installer in memory.
- Publish under a collision-safe Downloads filename without replacing an existing file. Keep the hard-link path and create-new copy fallback for FAT, exFAT, and network filesystems.
- Open the installer only after verification and publication, using
spawn_blockingfor the platform opener. Remove temporary or partial files on every failure path.
The repository does not configure platform signing credentials. Documentation may say downloads are verified against GitHub's published SHA-256 digest, but must not claim code signing or notarization unless the release workflow actually adds and verifies those controls.
Every std::process::Command::new("ffmpeg"|"ffprobe") in Rust must:
- Call
configure_ffmpeg_command(&mut cmd)after setting its arguments and stdio. On Linux this setsLIBVA_DRIVER_NAME=radeonsifor AMD systems and removes AppImage-internalLD_LIBRARY_PATHentries that would make the system FFmpeg load incompatible bundled libraries. - On Windows, set
creation_flags(0x08000000)(CREATE_NO_WINDOW) to avoid a console flash. The pattern in use:#[allow(unused_mut)] let mut cmd = std::process::Command::new("ffmpeg"); cmd.args([…]); #[cfg(target_os = "windows")] { use std::os::windows::process::CommandExt; cmd.creation_flags(0x08000000); }
- Validate any string interpolated into arguments. Encoder names are validated with
c.is_ascii_alphanumeric() || c == '_'before being passed as-c:v; extend that pattern when adding new user-string args. - Short discovery/version commands must use
spawn_captured_command()pluswait_for_output()with a finite deadline, output cap, and kill/reap cleanup. Do not use unbounded.status()or.output()on startup-facing FFmpeg, FFprobe, GPU, or package-manager probes. Long preview/compression jobs keep their existing generation/job cancellation paths instead.
ENCODER_CACHE(src-tauri/src/ffmpeg/encoders.rs) — memoised encoder list plus the H.264 hardware encoders that passed a bounded one-frame initialization test. Only validated hardware is eligible for first-run auto-selection; listed-but-unusable encoders remain available for an explicit choice. Callinvalidate_encoder_cache()after a successful FFmpeg install. The frontend also persists the last validated capability list inencoder_capabilitiesso startup can restore it immediately, then refreshes discovery after a quiet idle window that resets while a video probe is active.VAAPI_CACHE(src-tauri/src/ffmpeg.rs) — probes/dev/dri/renderD*once per session.PREVIEW_FRAME_CACHE(src-tauri/src/ffmpeg.rs) — 60-entry / 16 MB LRU, keyed by(path_hash, time_100ms, preview_width, preview_height).FFMPEG_AVAIL_CACHE(src-tauri/src/commands/encoders.rs) — 30 s TTL onffmpeg/ffprobe -versionprobes, withffmpeg_available_fresh()for post-install bypass.
On Windows, encoder detection and fresh availability probes also recover a standard WinGet
Gyan.FFmpeg install from its portable aliases or package directory and prepend that directory to
the current process PATH. This handles PATH registry updates that cannot propagate into an already
running vidcord process. Keep the lookup constrained to WinGet roots and require both
ffmpeg.exe and ffprobe.exe before using a directory.
Call clear_preview_caches_for_path(path) when the frontend loads a new file
(already done in probe) so preview reuse survives switching sources without
retaining stale entries for the file being re-probed.
FFprobe child PIDs have a separate generation token and a six-second deadline. Starting a newer
import must terminate the older probe so stale metadata work cannot consume the full timeout.
Preview FFmpeg child PIDs are tracked by a generation token. Call cancel_preview_jobs() before
starting work that should supersede previews; probe and compress_video already do this, and the
frontend invokes cancel_preview_generation when the preview unmounts.
Preview frame and filmstrip IPC accepts optional preview dimensions; the backend clamps them to even values before building FFmpeg scale filters. Filmstrips are a fallback: they start after the first exact frame on Linux or when native media preview loading fails, decode keyframes into at most 30 lower-resolution frames (eight for videos of 3+ minutes), and are skipped when direct seeking works. If a source has too few keyframes, scrubbing continues requesting exact frames instead of relying on the sparse strip. Videos of 3+ minutes use sparse seeks for filmstrip generation instead of a dense single-pass fps filter. Preview frame, filmstrip, and generated-clip commands have bounded deadlines and output sizes. A failed hardware preview decode disables further hardware-decode attempts for that imported source. Generated preview clips are bounded to a short playhead-relative window, try platform H.264 hardware encoders first, then fall back through software libx264. Generated clips are not cached in Rust because returning cached owned IPC bytes requires another large copy; the frontend retains and revokes one Blob URL keyed by import generation, path, and clip range instead.
The frontend keeps only the newest fallback-frame target and actively cancels an older in-flight preview generation before launching it; preserve that cancellation handshake so slow stale seeks cannot block the released scrub position.
settings.rs deserializes into a typed Settings struct and re-serializes to drop unknown keys — this silently migrates away from removed fields. When adding a field:
- Add an
Option<T>field toSettings(always optional for forward/backward compatibility). - Read it in the frontend's
useSettings.ts. - Call
saveSettings({ your_key: … }); writes are debounced 250 ms and flushed on unmount.
Persisted to ~/.local/share/vidcord/settings.json (Linux), ~/Library/Application Support/vidcord/settings.json (macOS), or %LOCALAPPDATA%\vidcord\settings.json (Windows) via dirs::data_local_dir().
Writes use a flushed temporary file plus atomic replacement. Windows must use MoveFileExW with replace/write-through flags because std::fs::rename cannot replace an existing destination there; do not regress repeated settings saves to a plain rename.
Named presets are stored in the optional presets array as { id, name, settings } records. The
frontend parses and normalizes them through settingsPresets.ts, accepts at most 20 presets, limits
names to 40 characters, replaces an existing preset case-insensitively when the name matches, and
ignores malformed or duplicate records on load. Presets capture compression and mode settings,
including the selected encoder identity, but intentionally do not capture output destination,
custom output folder, or completion action. The footer shows Autosave whenever the current captured
settings no longer equal the selected preset; keep that comparison behavior when adding a new
preset-backed setting.
main.tsx watches prefers-color-scheme and invokes sync_native_window_theme. The command is a
no-op outside macOS; on macOS it sets the native NSWindow background behind the transparent title
bar. Its light/dark RGB values must stay aligned with the corresponding opaque --bg values in
src/index.css, or the title bar and web content visibly split at the seam.
The app re-renders on every trim-slider move. Established patterns:
- Module-scope style objects in components (
PreviewPane,Toast,ProgressSection) — do not re-createReact.CSSPropertiesliterals per render. memo()on leaf components that receive many prop updates.- Refs for callbacks when a hook needs an empty dependency array but must call the latest version of a caller-provided function (see
useEncoders,loadVideoRefpattern inApp.tsx). useMemo/useCallbackon anything used by the trim timeline.- The import/settings subtree is memoized behind an explicit dependency list so trim-only updates do not rebuild it. Keep that dependency list complete when adding values captured by the subtree render callback.
- Playhead updates come from the
<video>element'stimeupdateevent viaonTimeUpdate, not asetInterval. The callback updates the compositor-owned playhead transform through a ref; only derived button-enabled booleans enter React state.
- Prettier: double quotes, 2-space indent, semicolons, 100-column print width, ES5 trailing commas.
- Text source files are normalized to LF by
.gitattributesso Prettier checks behave consistently on Windows and CI. - Strict TS — no
anywithout a// eslint-disable-next-line @typescript-eslint/no-explicit-anyand a reason. - Unused args must be prefixed with
_(seeuseCompression.ts's_onToast). - No comment churn for removed code — delete it. Don't add "// removed" breadcrumbs.
rustfmtdefault config; CI runscargo fmt --check.- Prefer
OnceLock<Mutex<…>>overlazy_static/once_cell— the codebase uses stdlib sync primitives exclusively. - When locking a mutex that might be poisoned by a previous panic:
.lock().unwrap_or_else(|e| e.into_inner()). This pattern is used consistently for cache mutexes; keep it. - Platform-specific code goes behind
#[cfg(target_os = "…")]. The Linux env setup inlib.rs::run()sets a dozen GTK/GIO/Wayland shims — do not reorder or remove without replicating on every DE listed in comments (KDE Plasma, LXQt, Sway, Hyprland). - Logging: call
vidcord_log("…")(writes to the rotating log). Never logPATHorSHELL— they can contain sensitive substrings.
- Branching and worktree strategy: Application development for an active WIP release must use
a branch named exactly
X.Y(for example,7.0or7.1). The version branch itself is the WIP branch;CHANGELOG.mdmay use either a top-level## WIPsection or the matching## vX.Ysection while that release is under development. Do not develop application code onmain, a detached HEAD,feature/..., orvX.Ybranches. Site-only changes may be made directly onmainwhen they are limited tosite/**,wrangler.jsonc, the site validator scripts, or.github/workflows/site.yml; runnpm run site:checkfor those changes. For application work, verify the branch name and that the changelog's leading release section matches the active work; if either check fails, stop and ask the user to switch to or create the appropriateX.Ybranch/worktree. Merge the completedX.Ybranch intomainonly after all quality gates pass and version references are aligned. This keeps application WIP commits isolated while allowing the GitHub-connected site deployment to continue frommain. - Run the relevant quality gates before every commit. If Rust changed:
cargo fmt --check --manifest-path src-tauri/Cargo.toml,cargo clippy --manifest-path src-tauri/Cargo.toml --tests -- -D warnings,cargo test --manifest-path src-tauri/Cargo.toml. If frontend changed:npm run lint,npm run typecheck,npm test. Fix failures before committing — never push and let CI catch it. - Document significant changes where future users and agents will look for them. Update
AGENTS.mdfor workflow, architecture, release, or repository-practice changes; updateREADME.mdfor public product behavior, install/setup, supported-platform, or development changes; and update the website (site/index.html, JSON-LD,llms.txt,llms-full.txt, and related site assets) when public-facing product facts or download behavior change. - Keep a WIP changelog in
CHANGELOG.mdfor meaningful user-visible changes made after the latest release. Use the latestvX.Ytag as the baseline, keep notes concise and release-note-ready, and exclude pure refactors, tests, chores, internal-only work, and iterative refinements of an already-documented change within the current WIP version. Consolidate those iterations into the broader release note when appropriate. - Don't bump the version casually. A version bump implies a release; only do it when explicitly requested. Follow the "Bumping the version" steps below — partial bumps cause CI/release mismatches.
- Add CHANGELOG entries under a new
## vX.Yheading — the release workflow extracts that section as the GitHub release body.
When the user asks to change the version, update every reference in one commit so semver and vX.Y references stay aligned. There is no single source of truth — these all need to match:
- Packaging / source:
package.json(version, full semver)package-lock.json(runnpm installafter editingpackage.jsonso the lockfile picks up the new version — don't hand-edit)src-tauri/Cargo.toml(version, full semver)src-tauri/Cargo.lock(runcargo check --manifest-path src-tauri/Cargo.tomlso the lockfile updates)src-tauri/tauri.conf.json(version, full semver)
- Source code: grep for the previous full semver and
vX.Yshort form across the repo (README.md,src/**,src-tauri/src/**, docs). Update inline copy and any hard-coded version strings that declare or display the current app version. The frontend'sDISPLAY_VERSIONis derived frompackage.jsonand does not need a manual edit. Skip test fixtures that use semver strings as arbitrary inputs (e.g.src-tauri/src/commands/updates.rssemver-comparison tests) — those exercise comparison invariants, not the current version. - Verify: run
npm run version:check, thengit grep -E "<old-semver>|v<old-major>\.<old-minor>"should return zero hits before committing (excludingCargo.lock/package-lock.jsonentries for unrelated dependencies that share the version string — read each match before assuming). - Run the relevant quality gates (above) before committing.
When the user asks to tag and push vX.Y:
- Confirm version alignment: every reference listed under "Bumping the version" must already match the requested version. If anything lags, fix it in a preparatory commit first — never tag a tree where the source disagrees with the tag.
- Update
CHANGELOG.md: convert the WIP notes into a## vX.Ysection at the top with all user-visible changes since the previous tag. Cross-check againstgit log <previous-tag>..HEAD --no-merges --pretty=format:"%s"and rewrite as user-facing release notes (drop refactor/chore/test-only commits unless they affect behaviour). The release workflow extracts this section verbatim as the GitHub release body, so it is the public changelog. - Commit the CHANGELOG (and any version edits, if step 1 needed them) on the matching
X.Ybranch. - Tag with the
vX.Yshort form (matching existing tags — seegit tag --list):git tag vX.Y. Do not usevX.Y.Z— the existing tag history is short-form and the release workflow's CHANGELOG extraction matches## vX.Y. - Merge and push the release commit to
main, then push the tag:git push origin mainfollowed bygit push origin vX.Y. Pushing the tag triggers the release workflow (build.yml→releasejob), which builds thereleaseprofile and drafts a GitHub release. - Start the next changelog on the next version branch: create or switch to the next planned
X.Ybranch/worktree, add a fresh## WIPsection at the top ofCHANGELOG.md, commit it as the first post-release commit, and push that version branch. Do not put the new WIP commit directly onmain. If the next version is not established, ask the user before creating it.
Confirm with the user before pushing the tag — tag pushes are hard to reverse and trigger the public release pipeline.
.github/workflows/build.yml has four jobs:
- frontend (ubuntu-latest) —
npm ci, audit (high), version and asset checks, lint, typecheck, test, build, and bundle-size budget; uploadsdist/as an artifact for every platform matrix job to download. - rust-compile-checks (ubuntu-22.04) —
cargo fmt --check,cargo audit,cargo clippy -D warnings, andcargo test. Clippy and tests use theciprofile and explicit Linux x86_64 target, sharing therust-linux-ubuntu-22.04-v1Swatinem/rust-cachekey and compatible artifacts with the Linux x86_64 build job. - build (5-way matrix) — Windows x86_64/aarch64, macOS universal, Linux x86_64/aarch64. Branch and pull-request runs compile every target with the
ciprofile but skip installer bundling. Taggedv*refs build release-profile installers; explicit manual runs build CI-profile installers for testing. - release (ubuntu-latest, only on tags) — extracts the matching CHANGELOG section, downloads artifacts, creates a draft GitHub release.
Concurrency groups branch pushes and pull-request synchronization events by head commit so the same revision is not built twice. Release tags and explicit manual packaging runs use isolated groups and are never cancelled.
The app workflow ignores README.md, CHANGELOG.md, .gitignore, site/**, wrangler.jsonc,
the Site Checks workflow, and the site-only sitemap/structured-data validators. Site changes run the
separate Site Checks workflow.
npm auditis run in CI; new high-severity advisories fail non-PR builds. Add a frontend advisory to an ignore-list only when upstream is the blocker.- Rust audit ignores live in
src-tauri/.cargo/audit.tomland are almost all Tauri/GTK transitives. If a new advisory appears for code we actually control, fix it. - The Tauri window is opaque (
transparent: false), while inner app surfaces still use glass-style translucency. Keep--bgfully opaque so WebView/Desktop compositor quirks cannot bleed through. convertFileSrcis required for local file URLs in the video preview; paths fed directly to<video src>will fail under the asset-protocol CSP. The CSP lives insrc-tauri/tauri.conf.json→app.security.csp.- Linux live
<video>scrubbing and Play/Stop trim controls are deliberately disabled because WebKitGTK's GStreamer playback path can crash the renderer on systems without a usable audio sink. Keep theget_osguard and FFmpeg-generated filmstrip/frame fallback unless live playback is validated across the supported Linux desktop environments and AppImage packaging. - CSP also gates drive roots on Windows (
C:/**…Z:/**). If a user reports a path refused by the asset protocol, checkassetProtocol.scope. - EncodersDialog is
React.lazy+Suspense— don't import it eagerly inApp.tsx, that re-grows the entry bundle. - Tests run in a Node environment, and the Tauri runtime APIs are mocked in
src/__mocks__/@tauri-apps/api/*. Write tests against pure helpers (useCompression.ts,ffmpegErrors.ts,losslessTrim.ts,previewScrub.ts,settingsPresets.ts,timelineZoom.ts,videoMetadata.ts) or as Rust unit tests. Component integration tests are not currently wired up; don't invent a jsdom setup unless asked.