OpenScreen is a free, open-source screen recorder and video editor (Electron + React + TypeScript + Pixi.js) maintained as a continuation of the original v1.5.0 release. This file is the canonical guide for any AI coding agent working in this repo.
- Install deps:
npm install(Node 22.22.1, npm 10.9.4 — seepackage.json#engines) - Start dev:
npm run dev(Vite dev server; Electron window opens viavite-plugin-electron) - Build:
npm run build(TypeScript check + Vite build + electron-builder) - Typecheck:
npx tsc --noEmit(CI runs this; no standalone script) - Test (unit):
npm run test(Vitest, jsdom env) - Test (browser):
npm run test:browser(Vitest + Playwright, requiresnpm run test:browser:installfirst) - Test (e2e):
npm run test:e2e(Playwright) - Lint:
npm run lint(Biome 2.4) - Format:
npm run format(Biome, tabs, double quotes, 100-col) - i18n check:
npm run i18n:check(validates the 13 locale files)
src/— React app: UI, editor components, timeline, i18n, captioning/cursor/exporter libselectron/— main process, IPC, recording orchestrationelectron/native/— native capture helpers:screencapturekit/(Swift, macOS) andwgc-capture/(C++/Win32, Windows). These are built and shipped with the app, not loaded from npmdocs/— architecture, engineering roadmaps, testing guidestests/— Playwright e2e specs + fixturesscripts/— native build scripts, diagnostic toolsnix/,flake.nix— Linux packagingrelease/,dist-electron/— build artifacts (gitignored)
- TypeScript strict mode (
tsconfig.json). Noany(BiomenoExplicitAnyiswarn— don't add newany). - Biome handles lint AND format. Tabs, double quotes, 100-col width, LF line endings. Run
npm run lint:fixbefore committing. - React functional components only. Hooks at top level (Biome
useHookAtTopLeveliserror). - Imports: use the
useImportTypediscipline (Biome organizes them). - Husky + lint-staged runs Biome on staged
*.{ts,tsx,js,jsx,mts,cts,json}. - The repo is pre-1.x and not production-grade — rough edges are expected, but new code should be clean.
- Unit tests live next to source as
*.test.ts/*.test.tsx(Vitest, jsdom). - Browser tests use
vitest.browser.config.ts(Playwright headless) — only run when DOM/Pixi rendering matters. - E2E tests are in
tests/e2e/(Playwright). Some specs are platform-specific (e.g.windows-native-checklist.spec.ts). - Add a test for every new behavior in the same package as the code under test.
- All tests must pass before opening a PR. CI runs
npm run testandnpm run test:browseron every PR.
Unit/browser tests can't exercise real capture (native screen recording, a physical webcam, the tray). To verify a recording/editor feature end to end, drive the actual Electron app with the computer-use MCP (screenshot + click/type on the desktop). This is the required "manual smoke test on real Windows/macOS" for native changes.
Launch the app
- Normal:
npm run dev— Vite serves the renderer andvite-plugin-electronopens the Electron window. The main process logsGlobal shortcut registered: CommandOrControl+Shift+Owhen ready (Ctrl/Cmd+Shift+O toggles the HUD). - The app is single-instance: a lock dir at
%TEMP%/openscreen-single-instance-<user>.lock(macOS:$TMPDIR). If a stale Electron process holds it, a new launch quits silently (exit 0, no window). Kill leftoverelectronprocesses and delete that lock dir before relaunching. - From a git worktree (no
node_modules/native binaries): junction/symlinknode_modulesfrom the main checkout (deps are usually identical — checkpackage-lock.json), and copy the prebuilt native capture binaries fromelectron/native/bin/<platform>/(gitignored — rebuilding needs the full VS/Xcode toolchain). Thennpm run devworks normally.
Granting access
request_accessresolves names against installed apps. A dev build runs aselectron.exe(orElectron.app), not the installedOpenscreen— grantelectron.exeor the dev window stays masked in screenshots. Non-allowlisted windows are masked (solid rectangles); the screenshot note lists their process names to add.
The HUD widget (recording controller)
- Frameless, transparent, always-on-top,
skipTaskbar, centered at the bottom of the primary display (createHudOverlayWindow, 600×160). It is click-through (setIgnoreMouseEvents(true, { forward: true })): moving the real cursor over an interactive control makes that region clickable and shows its tooltip, somouse_move→ screenshot →left_clickworks; a blind click on empty HUD area passes through to the desktop. - Control row (left→right): layout preset, source button (
Screen/Window→ label becomes the picked source), system-audio toggle, mic toggle, webcam toggle (shows the detected camera name), cursor-highlight toggle, record, notes, open-editor, language, minimize, close. The record button is disabled until a source is chosen (tooltip: "Please select a source to record").
The tray icon (bottom-right notification area)
- Because the HUD skips the taskbar and can be minimized/hidden, the system-tray icon is the reliable way to refocus the app: left-click or double-click reopens/focuses the HUD (
showMainWindow). Its icon swaps to a red dot while recording. - Right-click → context menu: Open / Quit when idle, or Stop Recording while recording (mirrors the HUD's stop). Tooltip shows
OpenScreenorRecording: <source>. Use this to stop a recording if the HUD isn't reachable.
End-to-end flow (record → edit)
- On the HUD: click the webcam toggle to enable the camera, then the source button → pick the Screens/Windows tab → select a thumbnail → Share.
- Click record; the HUD switches to a red stop button with a running timer (a countdown overlay may show first).
- Stop via the HUD's red button (or tray → Stop Recording). The editor window opens with the screen recording and the webcam PiP.
- Exercise the feature in the editor (e.g. Full Camera: press C to add a segment on the timeline, scrub to see the webcam grow to fullscreen and ease back; Ctrl+Z / Ctrl+Shift+Z undo/redo).
- Capture a screenshot as proof. Clean up: stop
npm run dev, remove temporary worktree junctions/lock.
- Branch from
main; never push to it directly. - Commit messages: short imperative summary, optional body. Recent style mixes conventional-ish prefixes (
ci:,chore:,fix:) with plain messages — either is fine, just be consistent within a PR. - PR titles must follow Conventional Commits (
feat:,fix:,chore:,refactor:,perf:,docs:,test:,build:,ci:,style:,revert:). Enforced by thesemantic-prjob inci.yml. This feeds GitHub's auto-generated release notes with clean categories. - Open PR via
gh pr createonce CI is green. - PR template is in
.github/pull_request_template.md.
Two workflow_dispatch workflows cut a release with a pre-release candidate (RC) first, then promote to stable. Trunk-based, no extra branch. Full operational guide in .harness/docs/git-workflow.md § Release flow.
- Cut RC: Actions → "Cut a release candidate" → Run workflow. Inputs:
bump(patch|minor|major),rc_number(default 1), optionaltarget_versionoverride. Snaps issues out of the rollingNext Releasemilestone into a versionedvX.Y.Zmilestone, bumpspackage.json, pushes thevX.Y.Z-rc.Ntag, which triggers the existingbuild.ymlto publish a GitHub pre-release. Notarization is skipped on RCs. Notifies#rc-testingon Discord. - Promote RC: Actions → "Promote RC to stable release" → Run workflow. Input:
rc_tag(e.g.v1.5.0-rc.2), optionalrelease_notes_extra. Closes thevX.Y.Zmilestone, strips-rc.Nfrompackage.json, pushesvX.Y.Ztag, which triggersbuild.ymlto publish a stable release (full notarization, Tier 3 homebrew/winget/nix/aur fires). Notifies#announcementson Discord. - Manual fallback:
git tag vX.Y.Z-rc.N <sha> && git push origin vX.Y.Z-rc.Ndoes the same as Cut RC (minus the milestone migration and Discord announce) — useful for emergency cuts.
Both workflows require the OPENSCREEN_RELEASE_TOKEN secret (a fine-grained PAT with contents: write + issues: write). This is the standard fix for release: published not triggering downstream workflows when the release is created by GITHUB_TOKEN. See docs/secrets.md.
Release branches freeze the build between cut and promote. Every RC cut creates release/vX.Y.Z-rc.N. The branch is not merged into main until the stable tag is published; only cherry-picks of bugfixes land on the release branch during the RC window. The stable tag points at the branch tip (RC + cherry-picks), then promote.yml opens a release/vX.Y.Z-sync → main PR to bring main into line. This contract exists because of the v1.6.0 incident (2026-07-05) where the original promote workflow tagged main instead of the RC snapshot, causing 23 unreleased commits to ship in v1.6.0. Full rules in .harness/docs/git-workflow.md § Release branches.
- Never commit secrets.
.env.exampleexists; real.envis gitignored. macos.entitlementscontrols macOS permissions — review when touching native recorder.- Native helpers run with elevated privileges on user systems; treat code in
electron/*-helper/as security-sensitive.
- Native capture is platform-fragile: macOS uses ScreenCaptureKit (Swift), Windows uses WGC (C++/Win32). CI runs on Linux only — manual smoke test on real macOS/Windows is required for native changes.
- Pixi.js v8 is the rendering engine. Filters come from
pixi-filtersand@pixi/filter-drop-shadow. GSAP +motionfor animation. - i18n: 13 locales in
src/i18n/locales/<locale>/(e.g.src/i18n/locales/en/settings.json). Thei18n:checkscript validates them — run it after touching translation files. - Build pipeline:
npm run buildis full electron-builder. For iterating on renderer only, usenpm run build-vite(Vite + tsc, no packaging). - README tone: the project is explicitly "not production-grade" and free forever — don't add paywalls, premium tiers, or upsell language to UI/copy.