Skip to content

ci: skip Discord PR sync when webhook secrets are unset - #2

Merged
cursor[bot] merged 1 commit into
mainfrom
cursor/fix-discord-ci-skip-8440
Jul 11, 2026
Merged

ci: skip Discord PR sync when webhook secrets are unset#2
cursor[bot] merged 1 commit into
mainfrom
cursor/fix-discord-ci-skip-8440

Conversation

@m8i-51

@m8i-51 m8i-51 commented Jul 11, 2026

Copy link
Copy Markdown
Owner

Summary

Fork では Discord webhook シークレットが未設定のため、PR 同期ワークフローが失敗していました。シークレット未設定時は warning でスキップするよう変更します。

Changes

  • discord.yaml: webhook 未設定時の core.setFailedcore.warning に変更
  • 同期ステップに continue-on-error: true を追加(二重の安全策)

Why

pull_request_target ワークフローは base ブランチ(main) の定義を使うため、upstream sync PR の CI が Discord 通知で落ちていました。

Open in Web Open in Cursor 

@cursor
cursor Bot marked this pull request as ready for review July 11, 2026 06:41
@cursor
cursor Bot merged commit 98daf19 into main Jul 11, 2026
5 of 6 checks passed
m8i-51 added a commit that referenced this pull request Jul 11, 2026
* feat: add video transcriptions (in-browser auto captions)

Decode mono 16k audio from the editor video, run Whisper via Transformers.js, and insert linked text annotations with timing and layout helpers.

Adds Vite resolve shims for Node-only imports used by the model stack, optional leading-silence trim for the caption buffer, timeline gap reconciliation for auto-caption regions, and editor i18n. Raises Select content z-index so caption controls stay usable over the video surface.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix: improve auto-caption timing and pause handling

* fix: address PR review for auto captions and locales

- Gate trimSec segment shift when transcription used full buffer retry
- Restore UTF-8 autoCaptions strings (ar, es, fr, ja-JP, ko-KR, tr, zh-CN, zh-TW)
- Dedupe caption segments after grouping only; stricter chunk dedupe in transcribe
- Post-truncate duration, explicit consume merge for web demuxer, trim region shift cleanup
- Tests for caption annotation pipeline

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix: time-slice single-group phrase captions into lines

Phrase mode with one merged span now splits wrapCaptionTextByWordBounds
lines into separate CaptionSegments with even time allocation across
the phrase span (fallback when duration is too short for min spans).
Update unit test accordingly.

Co-authored-by: Cursor <cursoragent@cursor.com>

* feat(editor): polish auto-caption loading toasts

Use one Sonner id for the full caption flow, show a distinct transcribing step, yield before Whisper so updates paint, match editor dark chrome on toasts, and keep pointer-events only on toast bodies. Add transcribing strings across locales.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(captioning): narrow ORT node shim, trim retries, and abort checks

Scope withoutNodeVersion to Transformers import/pipeline only; reapply trim-region filtering after ignore-trims retries; check AbortSignal after each slice inference before chunk processing.

Co-authored-by: Cursor <cursoragent@cursor.com>

* test: expose source selector hooks

* feat: add webcam mirror toggle

* fix: polish webcam mirror toggle

* fix: tidy webcam mirror settings

* feat: Add projectFolder to user preferences

Adds `projectFolder` as a key to user preferences, stores the location of
the most recently opened project, and prefills it in the next File → Open
action. Mirrors the pattern from #512 (`exportFolder`).

Closes #668

* docs: add macOS native cursor capture test pipeline

Documents the cursor helper binary, permissions, manual test checklist,
expected sidecar shape, and known limitations for the darwin native
cursor path.

* docs: fix three inaccuracies flagged by bot reviews

- Unify sidecar key name to camelCase cursorRecordingData (was mixing
  cursor-recording-data and cursorRecordingData in the same checklist)
- Graceful-degradation test now removes both binary copies so the
  missing-helper fallback is actually exercised (build/ and bin/darwin-*)
- Healthy recording section points to <videoPath>.cursor.json sidecar
  instead of the .openscreen project file, which does not embed cursor data

* fix: pass getProjectFolder to loadProjectFile in EditorEmptyState

The Studio Dashboard's Load Project button was calling loadProjectFile()
without the remembered folder, always defaulting to RECORDINGS_DIR.
Also save projectFolder preference after a successful load from this path.

* captions: run Whisper in a Web Worker, move auto-captions button to timeline

Move model load and transcription off the renderer main thread into a
dedicated worker (transcribe.worker.ts + transcribeCore.ts) so the editor
UI no longer freezes during captioning. Relocate the auto-captions action
from the editor header into the timeline toolbar, and add the missing
it/pt-BR/ru/vi autoCaptions translations.

* fix: resolve cursor-sampler telemetry not working

* fix vertical layout

* fix ci check

* record permissions prompt

* clip tooltio

* global auto toggle

* fix variant class

* rm blur

* improve waveform

* fix export compoisted shadow

* custom cursors

* reactive zoom

* improved cursor smoothing

* cleanup follow effect

* remove oversmooth

* fix toggles

* version bump

* fix(build): use sharp prebuilt instead of compiling from source (fixes macOS CI)

* bundle tts

* fix auto focus export sync

* cleanup

* helper fix

* fix 2

* final readme

* --

* chore: bump nix package to v1.5.0

* Update repository links for maintained fork

* ci: prepare v1.5.0 release pipeline

* Add merged PR issue bookkeeping

* Chain recording after source selection

* Address merged issue bookkeeping review

* Add official OpenScreen links

* Refine official download guidance

* Move official links near installation

* Make Discord PR sync optional

* Avoid failing PRs on Discord sync errors

* Fix issue 3 app launch and UI bugs

* Guard duplicate HUD creation

* Fix duplicate app instances

* Address source selector review feedback

* Harden source selector rejection test

* Fix video export stall when trim regions cause long decoder gaps

When a recording has a large trim region (e.g. 400s–828s removed),
the decoder must sequentially decode and discard all frames in that
region to maintain P/B-frame state. On a 3320x2160 source this can
take 40–50 seconds of wall time.

During that decode pass the encoder queue drains to empty and
lastEncoderOutputAt stops updating. When the next segment's frames
arrive and fill the encode queue, the stall detector would compare
Date.now() against the stale lastEncoderOutputAt (~50 s ago) and
incorrectly throw a stall error, aborting the export.

Fix: measure stall timeout from when the queue-full while-loop is
entered (stallWaitStartAt), not from the last global encoder output.
This gives the encoder a fresh 15 s window to produce output each
time the queue fills up, regardless of how long the decoder spent
on trimmed frames.

Also remove VideoFrame leak-tracker debug code added during diagnosis,
and switch latencyMode to "realtime" with a smaller maxEncodeQueue
to reduce encoder internal buffering depth.

* Add regression test for encoder stall timeout

Extracts the queue-full wait loop into waitForEncoderQueueSpace() so
the timing logic can be unit tested without real WebCodecs. Covers
the original false-positive: a long gap before the call (e.g. decoder
discarding frames in a trim region) must not count against the
15s timeout, since the timer starts at call time, not from the
encoder's last output.

* Remove redundant stall-timeout test, clarify regression rationale

The "long gap before this call" case was mathematically identical to
the queue-drain test once now()/sleep() are injected — shifting the
fake clock's epoch doesn't change now() - stallWaitStartAt. Replaced
with a comment on why the bug can't recur: the function takes no
external "last output" timestamp to go stale in the first place.

* chore: bootstrap Mavis agent team for the repo

Add a root AGENTS.md (open agents.md standard) and a .harness/
Mavis multi-agent team so any AI coding agent opening this repo
gets canonical setup / layout / style / test / PR conventions, and
the orchestrator can route work to the right specialist.

AGENTS.md
  - canonical commands (install, dev, build, test, lint, typecheck,
    i18n check)
  - project layout (src/, electron/, native helpers, docs/, tests/)
  - code style (TS strict, Biome 2.4, tabs, 100-col, double quotes)
  - testing (Vitest + Playwright)
  - PR/commit conventions, security notes

.harness/
  - agent.md           orchestrator (routes incoming work)
  - reins/
    openscreen-dev     generalist implementer
    openscreen-tester  Vitest + Playwright specialist
    openscreen-reviewer PR quality gate
  - docs/              architecture overview, git workflow
  - hooks/             pre-commit (Biome + tsc + Vitest),
                       post-commit (push/review reminder)
  - memory/            durable team facts

* chore: address CodeRabbit review feedback on agent bootstrap

- MEMORY.md: drop heading level one step (### -> ##) so subsections
  follow the h1 title correctly; update the format example to match.
- openscreen-dev/agent.md: replace non-existent .harness/docs/architecture/
  and .harness/docs/engineering/ subdir refs with the actual files
  (.harness/docs/architecture-overview.md, docs/architecture/native-bridge.md,
  docs/engineering/).

Skipping AGENTS.md line 32 wording nitpick: the original phrasing is
accurate and consistent with the rest of the file.

* fix(harness): correct native helper and i18n locale paths in agent docs

The bootstrap commit referenced paths that don't exist in the actual repo:
- electron/macos-helper/ and electron/windows-helper/ — actual layout is
  electron/native/screencapturekit/ (Swift) and electron/native/wgc-capture/
  (C++/Win32). Confirmed by directory listing and upstream main commits.
- src/locales/ — actual layout is src/i18n/locales/<locale>/. Confirmed by
  scripts/i18n-check.mjs (LOCALES_DIR = path.resolve('src/i18n/locales'))
  and the 13 locale subdirectories.

Fix all 6 occurrences across AGENTS.md, openscreen-dev agent.md,
openscreen-tester agent.md, and shared MEMORY.md. Also bump MEMORY.md
entry dates from 2026-06-21 (placeholder) to 2026-06-22 (actual).

* chore(agents): remove Mavis-specific 'Agent team' section from AGENTS.md

The '## Agent team' section leaked a runtime-specific name (Mavis) into
the otherwise agent-agnostic AGENTS.md standard. AGENTS.md is meant to
be consumed by any AI coding agent (OpenCode, Codex, Cursor, Aider, Devin,
Gemini CLI) — naming one specific runtime breaks that contract.

The Mavis-side discovery still works: the reins (.harness/reins/*/agent.md)
already say 'Read AGENTS.md at the repo root' as their first action, so
Mavis finds AGENTS.md without needing AGENTS.md to mention Mavis back.

* Keep HUD interactive on Linux so the drag handle can receive pointer events

Fixes #12

* fix(wgc-capture): pre-flight H.264 MFT check + actionable error

Fixes #15 (items 1 + 2 only; software H.264 fallback tracked in #18 is
intentionally out of scope).

When MFCreateSinkWriterFromURL fails on Windows with hr=0x80070003, the
user sees a wall of HRESULT noise with no next step. The actual cause is
typically that no H.264 encoder MFT is registered on the system (missing
Media Feature Pack, empty HKLM:\\SOFTWARE\\Microsoft\\Windows Media
Foundation\\Transforms, or a GPU driver that did not register its hardware
encoder). This change makes the helper diagnose the situation:

  1. Before MFStartup, count H.264 video encoder MFTs via MFTEnumEx. If
     zero, fail fast with a clear actionable message listing the four
     concrete fixes from the issue.
  2. On MFCreateSinkWriterFromURL failure, log how many H.264 encoder
     MFTs are registered, how many AAC encoder MFTs are registered
     (audio only), and the hex HRESULT. This distinguishes 'no H.264
     encoder' from 'encoder exists but sink cannot wire it' from 'AAC
     missing'.

Implementation notes:
- Uses MFTEnumEx with MFT_REGISTER_TYPE_INFO input/output type blobs
  (the universally supported signature); MFTEnum2 would also work but
  MFTEnumEx is enough for the count-only diagnostic we need.
- Uses MFT_ENUM_FLAG_ALL so both synchronous and async hardware MFTs
  (e.g. AMD AMF, NVIDIA NVENC) are counted.
- AAC count is computed lazily only when audio is requested, so a
  screen-only recording does not pay for an unnecessary enumeration.
- All new logic is in the anonymous namespace of mf_encoder.cpp per the
  task scope; mf_encoder.h is unchanged.
- Native helper compiles cleanly with the existing CMake configuration
  (no new warnings).

Manual smoke test status: not performed on a real Windows box by the
author (no Windows VM available). Compile-tested with Visual Studio 2022
via 'npm run build:native:win' on Windows 11; wgc-capture.exe and
cursor-sampler.exe both link successfully.

* refactor(wgc-capture): log MFTEnumEx HRESULT and rename mediaType param

Follow-ups from PR #20 review (both non-blocking):

  1. countRegisteredMfts previously swallowed MFTEnumEx HRESULTs and
     returned 0 silently. Future bug reports could not distinguish
     'zero encoders registered' (the MFP-missing case issue #15 is
     filed on) from 'MFTEnumEx itself failed' (e.g. COM not
     initialized). Log the HRESULT on the FAILED(hr) branch so the
     two paths are greppable.
  2. Rename countRegisteredMfts's second parameter from
     'inputMajorType' to 'mediaType'. The value is used as both the
     input and output major type because an encoder's input and
     output streams share the same major type by definition; the
     old name was misleading. Update the docblock to make that
     explicit.

Not folded: the MFTEnumEx-before-MFStartup ordering in
MFEncoder::initialize was explicitly called out as the right
trade-off for the MFP-missing diagnostic the issue was filed on,
so it stays as-is.

Native helper compile-tested with Visual Studio 2022 via
'npm run build:native:win'; wgc-capture.exe and cursor-sampler.exe
both link cleanly with no new warnings.

* fix(wgc-capture): drop pre-flight H.264 MFT check, keep sink-failure diagnostic

The pre-flight H.264 encoder MFT check from PR #20 fires a false negative in non-interactive / Session 0 contexts: MFTEnumEx can report zero H.264 encoders (because their COM server fails to fully activate in a service session) while MFCreateSinkWriterFromURL still finds a usable encoder via its Preferred / PreferredByOutputType registry walk.

With the pre-flight gate, MP4 recording broke on systems where it had worked before. Verified by comparing 8ced98d (recording produced a valid 3.84 MB MP4 on the affected hardware) against HEAD with PR #20 (recording failed with hr=0x80070003 from the pre-flight path on the same hardware).

Drop the pre-flight count and the early return. Keep the diagnostic dump on the MFCreateSinkWriterFromURL failure path: the helper still logs the H.264 and AAC encoder counts, plus the actionable four-bullet error when no H.264 encoder is registered. Healthy recordings no longer pay for an MFTEnumEx call; the diagnostic only runs on the failure path.

Also update electron/native/README.md to describe the diagnostic-only behavior.

Verified:

- npm run build:native:win: clean build, both wgc-capture.exe and cursor-sampler.exe link with no new warnings.

- npm run test: 241/241 pass.

- npm run lint: same 312-error baseline as 1a17f8b (pre-existing CRLF format issues in TS files, unrelated to this change).

- End-to-end recording on the affected system produces a valid 3.87 MB MP4 with 242 cursor samples; stderr is empty.

Follow-up to PR #20.

* docs: add Discord badge, tech/platform badges, and Need help link

* docs: move Need help link into a Community section at the bottom

* docs: link GitHub Issues in Community section

* docs+ci: add public ROADMAP.md, README link, and #🗺️・roadmap auto-post

- Add ROADMAP.md at repo root for top-level discoverability.
- Stability queue pulled from open issues / PRs on getopenscreen/openscreen (#8, #21, #22, #19, #18, #24).
- AI direction framed as opt-in / off by default, with per-feature tags.
- Link the new roadmap from the README Community section.
- Extend .github/workflows/discord.yaml with a roadmap-notify job that posts an embed to the #🗺️・roadmap Discord channel whenever a PR changes ROADMAP.md (or docs/roadmap.md). Mirrors the existing PR -> #pr-reviews sync pattern. Activated by setting the DISCORD_ROADMAP_WEBHOOK_URL repo secret; silently skipped otherwise so CI never breaks.

* ci(discord): post roadmap changes via openscreen_etienne bot instead of webhook

Replaces DISCORD_ROADMAP_WEBHOOK_URL with DISCORD_BOT_TOKEN +
DISCORD_ROADMAP_CHANNEL_ID so roadmap notifications use the same
openscreen_etienne bot as PR thread sync. Avoids accumulating one
channel-scoped webhook per channel and preserves the bot's identity
when posting to #🗺️・roadmap.

Channel ID is public info, so it lives in a GitHub Actions var
(not a secret). Bot token is the existing repo secret.

* docs(roadmap): link #🗺️・roadmap to the Discord channel

Fixes empty markdown link in ROADMAP.md line 44 (was rendering as
'#🗺️・roadmap' with no href). Uses the standard discord://channel
URL format with the guild + channel ids.

* ci(discord): sync ROADMAP.md to a pinned message in #🗺️・roadmap

Replaces the previous per-PR notification design with a true
mirror: the bot keeps a single pinned message in #🗺️・roadmap
in sync with the content of ./ROADMAP.md.

Behavior change:
- Triggers: pull_request_target (closed+merged to main) AND push to main
- The bot fetches the current ROADMAP.md and either PATCHes the
  existing pinned message or POSTs a new one (first run), then pins it
- No more per-PR notification spam — the channel is a 1:1 mirror of the file

Required config:
- DISCORD_BOT_TOKEN (secret, existing)
- DISCORD_ROADMAP_CHANNEL_ID (var, existing: 1493586210675884265)
- DISCORD_ROADMAP_MESSAGE_ID (var, new — the bot logs the id on first
  run; the maintainer sets it so future runs update the same message)

Per-channel perms on #🗺️・roadmap for openscreen_etienne:
- Send Messages
- Embed Links
- Manage Messages (new — required for pinning)

Also tightens the notify job's trigger filter to avoid wasted no-op
runs on direct push events (it was running for all events except
schedule, which would have been a free wasted run on every push).

* fix(ci): use context.payload.after for push event SHA in roadmap-notify

* docs(roadmap): smoke test for discord pinned-message sync

* fix(ci): account for truncation note length when slicing ROADMAP.md content

* docs(roadmap): second smoke test after fixing embed truncation

* docs(roadmap): third smoke test - verify PATCH path

* fix(ci): make pin attempt self-healing on every sync (idempotent)

* chore(ci): drop unused didPost variable

* refactor(ci): use Discord pins as the persistent state, not a variable

The bot now looks up the existing roadmap message by listing the
channel's pinned messages and matching the embed title. The pin
itself becomes the state.

This makes the DISCORD_ROADMAP_MESSAGE_ID variable optional (kept
as an escape hatch for manual recovery scenarios) instead of
required. Self-healing by construction: if a moderator unpins the
message, the next sync re-creates and re-pins it.

* docs(roadmap): fourth smoke test - pin-as-state refactor

* docs(roadmap): fifth smoke test - pin permission granted

* docs(roadmap): sixth smoke test - variable deleted, pin-as-state pure

* docs(roadmap): seventh smoke test via PR merge

* docs(roadmap): add 'Site & documentation' tier; clean smoke-test noise

Adds the Docusaurus + GitHub Pages tier between Stability & quality and How to influence. Theme direction recorded as 'Bespoke (TBD)' — to be decided when the site moves out of scaffold phase.

While here, removes the seven internal smoke-test entries from the changelog. Those validated the Discord pin-as-state sync mechanism and belong in CI logs, not in a user-facing changelog.

* feat(docs): scaffold Docusaurus 3 site + GitHub Pages CI

First of three PRs for the docs site effort tracked in ROADMAP.md
(tier 'Site & documentation', see PR #32).

- website/: minimal Docusaurus 3 site (TS config, Infima-tweaked theme,
  hero landing with WIP badge, intro doc only).
- .github/workflows/docs.yml: build on PR, deploy to GitHub Pages on
  push to main. Concurrency group, minimal permissions.
- website/postcss.config.cjs: overrides the monorepo Tailwind config
  so Docusaurus CSS pipeline doesn't try to load Tailwind.

PR #2 will polish the landing (full bento, demo, footer polish).
PR #3 will migrate docs/ -> website/docs/.

* fix(wgc): capture per-step stop timing, bump timeout, add diagnostic tool

Issue #34: Windows 10 recording stop times out at 15s with no useful
data. Three changes:

1. The stop timeout (electron/ipc/handlers.ts:415) was 15s, shorter
   than the macOS equivalent at 30s. Bumped to 60s so the Media
   Foundation SinkWriter::Finalize has room to drain on slow encoders.

2. Added per-step elapsed-time logs ([stop-timing] step=... elapsed_ms=...)
   around the cleanup chain in main.cpp so future stop hangs pinpoint
   which step is slow. These go to stderr, which Electron already
   captures into nativeWindowsCaptureOutput.

3. Save Diagnostics (electron/ipc/handlers.ts) previously hardcoded
   logs: [] in the renderer; now includes helperOutput (helper stdout +
   stderr, capped at 64KB) and mainProcessLogs (a console ring buffer
   populated when OPENSCREEN_DIAGNOSTIC=1).

4. New scripts/diagnostic-tool/ runs the helper outside the Electron
   app, captures [stop-timing] lines, writes a structured JSON report.
   Used as a standalone diagnostic bundle attached to bug reports.

5. .github/workflows/diagnostic-artifact.yml builds per-platform
   diagnostic zips on every push to main, retained 14 days. Windows
   job smoke-tests the bundle before upload.

Replaces scripts/repro-stop-hang.mjs and scripts/wgc-diagnostic-bundle/
which were scratch tools; the new diagnostic.mjs subsumes both.

* fix(diagnostic-tool): address CodeRabbit review

- ci: Windows smoke test now validates bundle structure and the
  diagnostic.bat --help exit code instead of running capture. The
  non-interactive runner has no display, so WGC cannot acquire frames
  and the helper exits before [stop-timing] lines appear (verified
  locally). Real users exercise the capture path on their own boxes.
- tool: --duration / --output / --source now validate their values
  and exit with a clear error on missing or non-numeric input, rather
  than silently turning into NaN and firing the stop timer immediately.
- tool: diagnostic.bat is now CRLF-terminated so Windows cmd parses
  it reliably.
- docs: tag fenced code blocks in the diagnostic-tool README with
  'text' to satisfy markdownlint MD040.

* style(website): biome auto-format for cce4524 website scaffold

Mechanical biome --write pass on the Docusaurus scaffolding files
introduced in cce4524. No behavior change; resolves CodeRabbit
formatting comments and unblocks the lint job.

* ci(docs): pin actions to commit SHAs in docs.yml

Resolves the last remaining CodeRabbit review comment on PR #36.
Pins actions/checkout, actions/setup-node, actions/upload-pages-artifact,
and actions/deploy-pages to their current v4/v3 commit SHAs with inline
version comments for grep-ability.

* Revert "ci(docs): pin actions to commit SHAs in docs.yml"

This reverts commit 8f1134e59d1b6027326d6ef7ec7592e81635454a.

* Revert "style(website): biome auto-format for cce4524 website scaffold"

This reverts commit a6d8af38d6a09979282187895e4214157615ca70.

* Revert "feat(docs): scaffold Docusaurus 3 site + GitHub Pages CI"

This reverts commit 8176f476863c596b752eaf33ebe013eb1b403604.

* fix(macos): correct cursor offset in single-window capture

When recording a single window on macOS, the cursor in the exported
video was offset by a fixed translation: clicks landed in the wrong
place. Full-screen capture was correct.

Root cause: the cursor sampler normalized the global cursor position
against the selected display's bounds, never against the captured
window's region. `getSelectedSourceBounds()` resolved a window source
to its display (the window origin was never subtracted), so the
normalized coordinates carried a constant offset equal to the window
origin.

Fix: the ScreenCaptureKit helper now reports the captured region's
global frame (the window frame for window captures, the display frame
for display captures) via a `captureBounds` field on its `ready` and
`recording-started` events. The main process stores it and
`getSelectedSourceBounds()` returns the window frame for window
sources, so the cursor is normalized into the captured window's
coordinate space. Display capture is unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013iccbhodxNjMBraYPXpX6q

* refactor(macos): address review — drop captureBounds validator and ready-event field

- inline the captureBounds check in dispatchNativeMacHelperEvent and remove
  parseCaptureBounds; we control both ends of this IPC
- drop captureBounds from the `ready` event (Swift + TS): cursor polling only
  starts after `recording-started` resolves, so it was read by nothing
- trim the duplicated/self-explanatory comments around activeMacCaptureBounds

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S4UdyNkRoEn2B8JaG5YdLK

* First implementation of copy

* feat(video-editor): region copy/paste and shared text-editing guard

Add copy/paste of region attributes (zoom/speed/annotation) with region
placement helpers, and lift isTextEditingTarget into src/lib/shortcuts.ts
so VideoEditor and TimelineEditor share one keyboard guard. Includes i18n
strings for the new actions.

* Update blur handling

* fix(video-editor): address CodeRabbit review of region clipboard

- Fix handleCopySelected loop so stale ids fall through to the
  'nothing to copy' toast instead of silently returning.
- Use regionClipboard.kinds.blur label when copying a blur region.
- Add 'blur' to regionClipboard.kinds in all 13 locales; broaden
  the 'annotation' label to neutral terms in es/fr/it/ja-JP/ko-KR/
  pt-BR/ru/tr/zh-CN so the toast no longer reads 'Text' for blurs.
- Cover the placement-at-region-start boundary in regionPlacement
  tests.

* fix(launch): resize HUD overlay to fit system language prompt

The system-language suggestion prompt is anchored at fixed top-8 of the
renderer, but measureHudSize only sized the OS overlay to fit the bottom
HUD bar. On a non-English OS that triggered the prompt for the first
time, the window was too short and the prompt's buttons were clipped
above the visible area, making the app appear unusable (issue #30).

Include the prompt in the size measurement and observe it via the
existing ResizeObserver so the bottom-anchored window grows upward to
contain the prompt's full extent. Add a test asserting the overlay
height covers the prompt plus top margin.

* fix(launch): backfill prompt observer and make sizing test deterministic

Two follow-ups from the CodeRabbit review on #37:

* measureHudSize's ResizeObserver effect only observed hudBarRef and
  deviceSelectorRef. If the system-language prompt (or language menu)
  mounted before the effect ran, their reflows would never trigger
  another resize. Backfill both refs in the effect.
* The size-assertion test was stubbing getBoundingClientRect after
  render but never re-fired the observer, so it read the mount-time
  call (with height: 0 from jsdom) and passed for the wrong reason.
  Capture the ResizeObserver callback, fire it after stubbing, and
  also stub the bar's box to mimic a real layout — verified the test
  now fails (height 68 vs expected 186) when the prompt-sizing path
  is disabled.

* refactor(launch): drop hardcoded top-8 and trim prompt test bloat

- Use rect.top + promptHeight in measureHudSize so the prompt's
  measured bottom drives overlay height instead of a magic 32
  that would silently drift from the Tailwind class.
- Hoist StubResizeObserver / CapturingResizeObserver to module
  scope and factor resetLaunchMocks() so the two describes share
  setup. Drop the trivial button-routing test (the click handlers
  are direct i18n callbacks in the JSX).

* refactor(ci): Global refactoring of pipeline workflows while addressing current technical debt

* fix(ci): address all 11 CodeRabbit review findings

- discord-pr-sync: validate thread channel via Discord API before acting
  on body-extracted threadId to prevent attacker-controlled redirects;
  cap appliedTags at 5 to match Discord forum limit
- discord-weekly-leaderboard: paginate search results via octokit.paginate;
  catch API errors gracefully instead of crashing
- aur-publish: gate all publish steps on key-check output; replace
  ssh-keyscan with pinned known_hosts from repo variable; install
  arch-install-scripts for makepkg on ubuntu-latest
- build.yml: tolerate missing per-arch macOS downloads via
  continue-on-error for single-arch workflow_dispatch
- update-homebrew-cask: match exact DMG asset names in poll instead of
  counting any two .dmg files
- discord-roadmap-sync: migrate from deprecated /pins to /messages/pins
  endpoints with new response shape
- docs: correct workflow count from 13 to 12
- build-native-mac.yml: remove unused reusable workflow (single npm run
  does not justify a separate job with artifact orchestration)

* fix(security): harden thread channel validation in discord-pr-sync

CodeRabbit identified two remaining weaknesses:
- Validation failed open when DISCORD_BOT_TOKEN was unset (returned
  true, trusting any marker)
- Parent-ID-only check allowed forged markers pointing to any thread
  in the same forum

Changes:
- Fail closed: return false when botToken is missing so markers are
  never trusted without the ability to validate
- Verify thread name matches the bot's deterministic naming convention
  (PR # - *) so only threads the bot itself created for this
  specific PR pass validation
- Forum channel ID check is now optional (thread name alone is a
  sufficient identity when parent cannot be verified)

* fix(ci): address maintainer review feedback

- discord-weekly-leaderboard: replace setFailed with warning for
  non-critical Discord webhook failure; drop unused import
- docs: remove stale build-native-mac.yml node and edges from
  mermaid graph; delete orphaned Tier 2 subsection
- discord-pr-sync: extract validateThreadChannel into standalone
  module (discord-thread-validator.mjs) with dependency injection
  for botToken/forumChannelId so it is independently testable
- test: add unit tests for validateThreadChannel covering:
  fail-closed without token, wrong parent rejection, sibling PR
  name mismatch, valid thread acceptance, API errors, and network
  failures
- vitest.config: add .github/ to test include pattern

* fix(security): add AbortController timeout to Discord thread validation

Without a timeout, a stalled network path during the best-effort
validation step can hang the job indefinitely. continue-on-error
only helps after the request completes or the job-level timeout
(360 min default) fires. Add a 5-second AbortController abort so
the workflow moves on quickly when Discord is unreachable.

- Fetch call now passes an AbortSignal with 5s timeout
- Test verifies the signal is wired into fetch arguments

* fix(ci): install makepkg package, not arch-install-scripts

arch-install-scripts only provides pacstrap/arch-chroot/genfstab; it does
NOT ship the makepkg binary. If pacman-package-manager failed to install
and arch-install-scripts succeeded, the step exited 0 and the later
makepkg --printsrcinfo call failed downstream with 'command not found'.

- Try makepkg package directly in the fallback chain (the explicit
  provider per packages.ubuntu.com).
- Verify with command -v makepkg after install so a silent failure
  fails the job instead of leaking past the 'Install makepkg' step.

Addresses CodeRabbit comment r3484700062.

* feat(ci): release candidate pipeline (prerelease + promote workflows)

- .github/workflows/prerelease.yml: cut vX.Y.Z-rc.N from rolling Next Release milestone
- .github/workflows/promote.yml: promote RC tag to stable release
- .github/scripts/release-milestone-migrate.mjs: snapshot Next Release -> vX.Y.Z
- .github/scripts/release-milestone-close.mjs: close vX.Y.Z milestone on promote
- .github/scripts/discord-release-announce.mjs: announce RC and stable
- build.yml: tag regex accepts pre-release, --prerelease, skip notarization, use OPENSCREEN_RELEASE_TOKEN
- ci.yml: semantic-pr job validates PR titles for clean release notes
- docs: AGENTS.md, git-workflow.md, github-actions-workflows.md, secrets.md

* fix(ci): use OPENSCREEN_RELEASE_TOKEN for git push to bypass repo ruleset

actions/checkout defaults to GITHUB_TOKEN (github-actions[bot]), but the repo ruleset bypass is set on EtienneLescot. Pushing via the PAT makes git act as EtienneLescot, so the bypass applies and the push succeeds.

* fix(ci): add origin argument to git remote set-url

* fix(ci): drop git remote set-url workaround now that github-actions[bot] is a bypass actor

Added github-actions[bot] (id 41898282) as a bypass actor on the main-protection ruleset, so the default GITHUB_TOKEN auth from actions/checkout@v4 can push to main. Fine-grained PATs are deliberately excluded from ruleset bypasses by GitHub, so the previous git remote set-url workaround would still fail.

* fix(ci): push to release branch and rebase-merge via PR

The main-protection ruleset bypass actors (github-actions[bot] / EtienneLescot) do not apply to direct pushes from workflow tokens or PATs. Open a PR on a release branch and rebase-merge it via the PAT instead; the merge respects the ruleset while the bypass for EtienneLescot satisfies the review requirement.

* fix(ci): pass token via GH_TOKEN env var, not --token flag

* fix(ci): delete remote release branch first for idempotent reruns

* fix(ci): use GITHUB_TOKEN for PR create (has pull_requests write), PAT for merge (bypass)

* fix(ci): grant pull-requests: write to workflow so GITHUB_TOKEN can create PRs

* fix(ci): use PAT (EtienneLescot) for both PR create and merge

Org-level policy disables GITHUB_TOKEN write, so PR creation with GITHUB_TOKEN fails. PAT is unaffected by the org policy; it just needs the right permissions.

* docs(secrets): update PAT perms and ruleset section

* fix(ci): --delete-branch-remote is --delete-branch

* fix(ci): add --admin flag to gh pr merge to bypass branch protection

* chore(release): bump to 1.5.1-rc.99 [skip ci]

* fix(ci): fetch main and tag the merged commit, not the local branch tip

* Revert "chore(release): bump to 1.5.1-rc.99 [skip ci]"

This reverts commit 3d2e4b02da04e13db9862f28770f3ab1e2750911.

* chore(release): bump to 1.5.1-rc.99 [skip ci]

* fix(ci): compare package.json to full tag version, not just the stable part

For RC tags, package.json is at e.g. 1.5.0-rc.1, not 1.5.0. The previous check rejected those tags.

* fix(ci): use GITHUB_TOKEN for tag push to trigger build.yml downstream

PAT tag pushes silently do not trigger downstream workflows in this setup. GITHUB_TOKEN pushes do, and a tag is just a ref (no file changes), so the workflows:write permission isn't needed.

* Revert "chore(release): bump to 1.5.1-rc.99 [skip ci]"

This reverts commit 5f772b9b79875d0d791e4c9843f18b6cba2a0fac.

* chore(release): bump to 1.5.1-rc.99 [skip ci]

* fix(ci): delete remote tag first for idempotent reruns

* fix(ci): use Discord webhooks for release announce (works with forum channels)

Forum channels don't accept bot messages, but webhooks create threads. The existing PR forum sync uses the same pattern.

* fix(ci): use bot API (POST /channels/{forum}/threads) for forum channels

Drops the webhook fallback. The bot detects the channel type and either posts a message in text channels or creates a thread in forum channels. Consistent with the existing discord-pr-sync.mjs design (one bot identity) and the user's preference for proper OAuth apps over webhook shortcuts.

* refactor(ci): use shared discord-bot-api helper for release announce

Adopted from PR #42. The helper wraps createForumThread, postChannelMessage, patchChannel with consistent 429 detection. discord-release-announce.mjs now uses it and auto-detects forum (15) vs media (16) vs text (0) channels. Added vitest coverage: 7/7 new tests + 9/9 from PR #42 = 23/23 green.

* Revert "chore(release): bump to 1.5.1-rc.99 [skip ci]"

This reverts commit dcd98444f72f1513343b5ae9a705b73625e71912.

* chore(release): bump to 1.5.1-rc.88 [skip ci]

* fix(ci): trigger build.yml explicitly after tag push

GITHUB_TOKEN tag pushes don't fire downstream workflows in this org. Add an explicit gh workflow run step so the release actually gets built without a manual trigger.

* Revert "chore(release): bump to 1.5.1-rc.88 [skip ci]"

This reverts commit 95177570ea97fe1789e478a58440e393458b7f71.

* chore(release): bump to 1.5.1-rc.77 [skip ci]

* docs(secrets): document Actions and Workflows PAT permissions

* Revert "chore(release): bump to 1.5.1-rc.77 [skip ci]"

This reverts commit 56250edcb19a40e4e7547b2be13a931899d18254.

* chore(release): bump to 1.5.1-rc.66 [skip ci]

* fix(ci): use --notes-start-tag to anchor auto-notes to the SemVer previous

The fork's v1.4.0 release was re-published after v1.5.0, which made --generate-notes pick v1.4.0 as the previous for any v1.5.x release. Explicitly pass the SemVer previous (e.g. v1.5.0 for v1.5.1-rc.N or v1.5.1) as --notes-start-tag so the auto-notes show the real v1.5.0..v1.5.1 diff.

* Revert "chore(release): bump to 1.5.1-rc.66 [skip ci]"

This reverts commit 996524de43e5e114f3c4be2125a2f42fd6ee6c51.

* chore(release): bump to 1.5.1-rc.55 [skip ci]

* Revert "chore(release): bump to 1.5.1-rc.55 [skip ci]"

This reverts commit 4ee17bdba2552969d9ba4287149014431b148d68.

* chore(release): bump to 1.6.0-rc.1 [skip ci]

* refactor(ci): switch Discord automation from webhooks to single bot

All four PR-review automations (intro post, thread updates, status
mutations, weekly spotlight) now flow through one Discord bot identity
(DISCORD_BOT_TOKEN) instead of three separate webhook URLs. Removes
the per-channel webhook secret rotation surface and consolidates rate
limits, audit logs, and permissions under one app.

Behavioural changes:
- Intro post + forum thread creation: was POST to webhook with
  thread_name, now POST /channels/{forumChannelId}/threads via bot.
- Per-event message in thread: was POST to webhook with thread_id, now
  POST /channels/{threadId}/messages via bot.
- Thread status (tags, archive, lock): unchanged, already used bot.
- Weekly leaderboard: was POST to spotlight webhook, now POST to
  spotlight channel via bot.
- Failure alerts: optional, was alert webhook, now alert channel via
  bot (DISCORD_ALERT_CHANNEL_ID); unset to silence.

Display name and avatar for bot messages are now the bot's configured
profile in the Discord developer portal rather than per-webhook env
vars. Remove the old secrets after migration:
DISCORD_WEBHOOK_URL, DISCORD_PR_FORUM_WEBHOOK, DISCORD_SPOTLIGHT_WEBHOOK_URL,
DISCORD_ALERT_WEBHOOK_URL, DISCORD_WEBHOOK_USERNAME, DISCORD_WEBHOOK_AVATAR_URL.

Add as variables (channel ids, not secrets):
DISCORD_PR_FORUM_CHANNEL_ID, DISCORD_SPOTLIGHT_CHANNEL_ID,
DISCORD_ALERT_CHANNEL_ID (optional).

Bot permissions required on each channel:
- PR forum: View, Send Messages, Embed Links, Create Public Threads,
  Manage Threads.
- Spotlight channel: View, Send Messages, Embed Links.
- Alert channel (optional): View, Send Messages.

* fix(ci): restore mention suppression and patch resilience after webhook migration

Three regressions from the bot-only migration, caught on self-review:

1. Webhook POSTs defaulted to allowed_mentions: { parse: [] }; bot POSTs
   default to allowing all mentions. Without restoring it, the
   <@&{reviewerRoleId}> embed in ready_for_review and changes_requested
   review posts would start pinging the reviewer role, spamming
   maintainers.

2. patchChannel now throws on non-ok (was: warn-and-return in the
   inline patchDiscordThread). That made the user-facing message after
   each tag/archive patch (PR merged, review state, etc.) skipped when
   the patch failed, because the throw bubbled to the outer catch.
   Added safePatchChannel that logs and continues so the message still
   posts even if tagging fails.

3. Move THREAD_MARKER_REGEX back to the top of the file (the rewrite
   dropped it below the function that uses it — TDZ-safe but ugly).

Also: drop the unused rateLimited flag from the bot-api error (no
consumer read it; the message is enough), and restore trailing
newlines on the two yml files.

* test(ci): collapse discord-bot-api tests via describe.each

5 near-duplicate tests (3 happy-path + 2 error, only on createForumThread)
became 9 parameterized tests (3 cases × 3 paths) covering all 3 helpers
uniformly, in 39 fewer lines.

Side benefit: postChannelMessage and patchChannel now also have explicit
rate-limit and error-path coverage.

* fix(ci): address CodeRabbit review on discord-bot-api

1. discord-bot-api.mjs: add AbortController-based 5s timeout to all bot calls (matches the discord-thread-validator pattern). Configurable per-call via timeoutMs option.
2. discord-bot-api.test.mjs: add test for timeout abort, assert signal is passed to fetch.
3. docs/github-actions-workflows.md: list Send Messages in Threads alongside Create Public Threads in the PR forum bot permissions.

* docs(ai-edition): add HANDOFF.md with implementation summary, file map, deferred work, and continuation guide

* Feat: Added lint Button

* Notes is working, and not showing in the screen recorder

* Refactor: Update URL parameters for notes window

Changed the query parameter from "windowType=notes" to "showNotes=true" in both the Electron window creation and the React app. This enhances clarity and consistency in how the notes window is displayed based on URL parameters.

* Refactor: Update NotesWindow and createNotesWindow for improved UI

Modified the Electron notes window to enhance its appearance and functionality by adjusting dimensions, background color, and removing unnecessary properties. Updated the React NotesWindow component to ensure it occupies the full screen height and width, improving user experience for note-taking.

* Refactor: Adjust dimensions and local storage handling in NotesWindow

Updated the dimensions of the Electron notes window for better usability and added max width and height constraints. Enhanced the React NotesWindow component to store and retrieve notes from local storage, improving the user experience by preserving notes across sessions.

* Refactor: Update window handling and title for NotesWindow

Rearranged the order of window creation to ensure the countdown overlay is displayed correctly. Updated the title of the NotesWindow to "OpenScreen - Notes" for better branding and clarity.

* Refactor: Replace useEffect with useLayoutEffect in NotesWindow

Updated the NotesWindow component to utilize useLayoutEffect instead of useEffect for setting initial notes from local storage, improving performance and ensuring the notes are rendered correctly before the browser paints the UI.

* Refactor: Remove unused allowScripts section from package.json

Eliminated the allowScripts configuration for specific dependencies in package.json, streamlining the file and removing unnecessary entries.

* Refactor: Update NotesWindow integration and improve accessibility

Removed the conditional rendering of NotesWindow from the main App component and integrated it directly within the return statement when showNotes is true. Added aria-label for the open notes button in LaunchWindow for better accessibility. Updated the placeholder text in NotesWindow to utilize localized strings for improved user experience.

* Refactor: Simplify NotesWindow rendering in App component

Removed the conditional block for rendering NotesWindow and integrated it directly within the return statement, improving code clarity. This change ensures that NotesWindow is displayed when showNotes is true, streamlining the component structure.

* Chore: Removed redundant code for the app.tsx resolving coderabbit's issues

* Chore: Upgrade to Tiptap for enhanced note-taking functionality

Updated package versions to 1.6.0-rc.1 and integrated Tiptap for the NotesWindow component, replacing the previous textarea with a rich text editor. Improved local storage handling for notes and added styles for better UI. Enhanced CSS to hide scrollbars for a cleaner appearance.

* Refactor: Adjust NotesWindow dimensions and update styling

Modified the minimum width of the Electron notes window for improved usability. Removed unused CSS styles from NotesWindow.module.css and updated the NotesWindow component to utilize Tailwind CSS for layout and styling, enhancing the overall user interface.

* Enhance NotesToolbar with tooltips and localization support

Updated the NotesToolbar component to include tooltips for each formatting button, improving user experience. Integrated localization for toolbar button labels using the useScopedT hook, ensuring accessibility for multiple languages. Added corresponding translations for toolbar actions in various locale files.

* Refactor: Clean up NotesWindow content handling

Simplified the notes content escaping logic in the NotesWindow component by formatting the code for better readability. This change enhances maintainability while ensuring that notes saved as plain text are correctly wrapped for Tiptap parsing.

* fix: restore README screenshots and localize image read errors

- Restore demo screenshot height to 320px (0.2467 made images invisible)
- Align macOS permission instructions with System Settings naming
- Reuse settings.imageUpload.errorReading in AnnotationSettingsPanel

* refactor(i18n): drop duplicate annotation.failedImageUpload key

annotation.failedImageUpload is identical (modulo one Arabic preposition)
to imageUpload.failedToUpload. Use the latter so the upload error toast
in AnnotationSettingsPanel reuses the same namespace as SettingsPanel.

# Conflicts:
#	src/components/video-editor/AnnotationSettingsPanel.tsx

* chore(release): bump to 1.6.0 [skip ci]

* chore: bump nix package to v1.6.0

* ci(release): freeze release branches between RC cut and stable promote

prerelease.yml: the version bump now lives on release/vX.Y.Z-rc.N only and
is NOT merged into main. The RC tag points at the release branch tip, so
anything merged into main after the RC cut stays out of the build.

promote.yml: checks out the frozen release branch, bumps to the stable
version, tags its tip, publishes the release, then opens a release-sync
PR into main so the released snapshot eventually lands on main without
polluting it during QA.

Fixes the v1.6.0 incident where 23 commits (Tiptap, NotesWindow, lint
button, AI handoff) landed on main between RC cut (5d7248cb) and the
stable promote, so the published v1.6.0 stable included untested
features.

* docs(release): document frozen release branches and the v1.6.0 incident

- git-workflow.md: rewrite the § Release flow section to describe the
  release/vX.Y.Z-rc.N freeze contract, cherry-pick rules during the RC
  window, and the post-promote release-sync PR. Add a § Release branches
  section that lists the three branches per version (rc, sync, stable)
  and the rules each one follows.
- AGENTS.md § Release flow: one-paragraph pointer to the freeze contract.
- MEMORY.md: log the v1.6.0 incident + the fix so future agents don't
  re-introduce the 'tag main instead of release branch' bug.

* fix(recording): pause/resume webcam recorder on native Windows (#45)

The native Windows pause/resume branches added in ca826d9 only paused the
screen capture. The webcam MediaRecorder kept recording during a pause,
so the editor saw a gap in the screen track and continuous webcam footage
through the pause window.

Mirror the native macOS pattern: after a successful native pause/resume,
call MediaRecorder.pause()/.resume() on the webcam recorder if it's in
the matching state. The browser path and the native macOS path already
did this correctly.

* docs(roadmap): add blur regions restoration to stability tier

* fix: stream >2GB recordings into OPFS so long videos can export

Exporting a recording larger than ~2 GiB failed with "Failed to read
binary file". The renderer loaded the whole source into memory via the
read-binary-file IPC (Node `fs.readFile`), which throws
ERR_FS_FILE_TOO_LARGE above 2 GiB, so any long recording (e.g. a 2h
1080p60 capture at ~6.6 GB) could never be exported. Even under the cap,
a multi-GB ArrayBuffer/Blob would exhaust memory on a typical machine.

web-demuxer reads a File on demand, so it never needs the bytes up front.
Add a chunked range-read IPC (`get-readable-file-info` + `read-file-chunk`)
and a renderer helper that streams large recordings into an OPFS-backed
File, handing web-demuxer a disk-backed File instead of an in-memory one.
Memory stays flat regardless of length. Small recordings keep the
existing single-shot path.

Wire the streaming loader into the export decoder and the captions audio
extractor; skip the in-memory source-copy fast path and the waveform
ArrayBuffer read for oversized files so they degrade gracefully.

Verified against a real 6.6 GB / 2h11m recording: OPFS copy peaked at
~137 MB heap, web-demuxer parsed metadata and decoded 1920x1080 frames
with heap staying ~27 MB.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix: guard OPFS source cache and surface copy progress

Address review feedback on the >2GB export fix:

- OPFS cache pruning now reference-counts live sources and never deletes
  an entry a demuxer is still reading. releaseLocalSourceFile() is called
  from StreamingVideoDecoder.destroy() and the captions extractor's
  finally. Previously pruning kept only the newest entry, which could
  remove a file still in use by a concurrent export/caption pass.
- Extract the shared in-memory size threshold into sourceFileLimits.ts
  (was duplicated across localSourceFile, extractMono16k, videoExporter).
- Report OPFS copy progress as a "preparing" export phase so the dialog
  reflects the multi-GB copy instead of sitting at 0%.
- Add unit coverage for the OPFS streaming/eviction path: chunked copy,
  cache reuse, and the in-use pruning guard.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix: key OPFS cache refcount by cache entry, not source URL

Address the follow-up review on the OPFS cache guard:

- Reference-count cache entries by cache-entry name (the returned File's
  .name) instead of by source URL. Keying by URL discarded an older
  revision's refcount when a new revision (size/mtime change) superseded
  it, so releasing the old revision could decrement the wrong entry and
  prematurely drop a still-in-use copy. Callers now release with the
  File's name (StreamingVideoDecoder.destroy and the captions extractor).
- Add a test that a failed chunk read mid-copy throws and removes the
  partial cache entry, exercising the catch-block cleanup.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix: lower in-memory source threshold and skip waveform for huge files

The 1.5 GiB threshold was too high. read-binary-file returns the whole
file over IPC, which Electron structured-clones (copies) in the main
process, so a ~1 GB recording transiently needs ~2x its size there and
hard-crashes a memory-constrained machine (observed on a 16 GB Mac) —
well below the 2 GiB fs.readFile cap. Two paths hit this:

- Export/demux source load and captions now stream via OPFS above
  256 MiB (lowered MAX_IN_MEMORY_SOURCE_BYTES), so moderate recordings no
  longer ship a giant buffer over IPC.
- The trim waveform (loadFileAsArrayBuffer) cannot stream — decodeAudioData
  needs the whole file — so it now skips recordings above the limit;
  useAudioPeaks already degrades to no waveform on throw. Fixes an
  editor-entry crash when opening a ~1 GB recording.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix: address review — prune race, chunk cap, MIME, streaming waveform

Review feedback from #74:

- Fix the concurrent-prune race: retain the OPFS cache entry BEFORE
  pruning/writing, so a concurrent materialization's prune pass can no
  longer delete an entry that is still mid-write. On failure the
  reference is released and the partial copy removed only if no other
  in-flight operation still references it. Regression test included.
- Cap read-file-chunk requests at 64 MiB on the main-process side so a
  buggy or compromised renderer cannot force an arbitrarily large
  Buffer.allocUnsafe.
- Infer the small-file MIME type from the extension (mp4/webm/mov/...)
  instead of hardcoding video/mp4.
- Restore the trim waveform for large recordings instead of skipping it:
  new streaming peaks path demuxes the audio track and folds each
  decoded AudioData frame straight into min/max buckets (same output
  format as audioPeaksWorker), so memory stays flat. Verified on a
  288 MB file: all 24000 blocks populated, renderer heap 25→27 MB.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix: abortable, deduplicated OPFS copies; harden waveform and caption paths

Findings from an adversarial multi-agent cross-review of this branch:

- Cancelling an export during the "preparing" phase used to leak the OPFS
  cache reference forever (destroy() ran before sourceCacheName was set)
  while the multi-GB copy kept streaming in the background. Copies are now
  abortable: materializeLocalSourceFile takes an AbortSignal, the decoder's
  cancel()/destroy() and the load timeout abort it, and references are only
  taken when a caller actually receives the File — no success, no retain,
  nothing to leak. GifExporter benefits via the same decoder hook.
- Concurrent materializations of the same recording (waveform + export)
  used to race two writables on one OPFS handle and could invalidate the
  File another consumer was reading. In-flight copies are now deduplicated
  per cache entry: joiners share one stream (and its progress events), and
  the underlying copy aborts only when every joined caller has aborted.
- The streaming waveform now falls back to a demux-only packet-timestamp
  scan when the container duration is missing/bogus (MediaRecorder WebM),
  and always closes its AudioDecoder on error/abort paths.
- The caption demuxer path buffers decoded PCM in memory, so oversized
  sources now cap decoded audio at 30 min and surface `truncated` instead
  of exhausting the renderer heap on multi-hour recordings.
- Stale multi-GB OPFS copies from previous sessions are reclaimed at app
  startup (previously only pruned during the next large-file load).

Tests: in-flight dedup, mid-copy abort + cleanup, and shared-copy survival
when one of two joined callers aborts (307 total passing).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat: implement optimized webm patching and saving state

* fix: address CodeRabbit review feedback

- Hoist WebmElementMock interface to describe scope in webm-duration.test.ts
- Wrap ws.write inside try/finally to ensure streams are always destroyed on failure
- Disable pause/restart/cancel HUD buttons while saving to prevent race conditions
- Skip setSaving(true) for cancelled/restarted recordings (discardRecordingId guard)
- Extract hudDisabledClasses constant to reduce repetition in LaunchWindow.tsx

* fix(export): project native cursor onto cropped rect, not mask rect

After cropping, the exported cursor drifted because the recorded sample
was projected onto the screen mask rect instead of the rectangle the
cropped video is actually painted on. In cover layouts where the
cropped video letterboxes inside screenRect (coverOffset != 0), the
cursor offset was proportional to the letterbox margin.

Track the painted croppedRect in FrameRenderer layoutCache and pass it
to projectNativeCursorToLocal so the cursor overlays the cropped video
pixels exactly. Adds regression tests for projectNativeCursorToLocal.

Fixes #64

* fix(export): address review feedback

- Rename cover-letterboxed test to cover-overflowing (fixture is cover, not letterbox)
- Update sizeNorm comment to acknowledge the export/preview asymmetry introduced
  by the croppedRect field: export now uses croppedRect.width, preview still
  uses screenRect.width. They agree in cover mode but differ in fit-to-height
  letterbox layouts.

* fix(cursor): normalize sampler position against physical bounds

The Windows cursor-sampler (Win32 GetCursorInfo) reports raw x/y in
physical screen pixels. For display captures, normalizeSample was
dividing by bounds from Electron's \screen\ API, which are in logical
pixels (DIP). On a 100% DPI display these coincide and the bug is
invisible, but on any high-DPI display (125%, 150%, 200%) the normalized
cursor position is wrong by the scale factor. The error then compounds
through projectNativeCursorToLocal in the export, producing a visible
cursor offset proportional to the DPI ratio.

Fix by converting the logical bounds to physical via the display's
scaleFactor before normalizing. payload.bounds (from the sampler's
GetWindowRect, used for window captures) is already physical and is
left as-is.

Pre-existing since 1.5.0; never caught because CI is Linux-only and
manual Windows smoke tests ran on 100% DPI displays.

* fix(cursor): use screen.dipToScreenRect for multi-monitor origin

The previous fix multiplied bounds.x/y/width/height by a single
scaleFactor to convert from DIPs to physical pixels. That works for the
primary display (origin at 0,0 in the virtual screen) but misplaces the
origin on non-primary or mixed-DPI displays: a secondary 200% DPI
monitor to the right of a 100% primary has DIP bounds {x:1920, y:0,
w:1920, h:1080} but physical bounds {x:1920, y:0, w:3840, h:2160} —
multiplying the origin by 2 would push it to 3840.

Use Electron's \screen.dipToScreenRect(null, bounds)\ instead, which
picks the correct display from the rect's center and handles the
virtual-screen origin correctly across multi-monitor and mixed-DPI
setups. payload.bounds (from the sampler's GetWindowRect) is already
physical and is left as-is.

* chore: retrigger CI after workflow fixes

---------

Co-authored-by: parse-nip <152457438+parse-nip@users.noreply.github.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: AjTheSpidey <aj.gupta0911@gmail.com>
Co-authored-by: Paulo Henrique Garcia <paulohenriquesg@gmail.com>
Co-authored-by: Siddharth <siddharthvaddem@yahoo.com>
Co-authored-by: Kelly Yang <124ykl@gmail.com>
Co-authored-by: Sid <70214527+siddharthvaddem@users.noreply.github.com>
Co-authored-by: Jodélcio Luz <jodelcioluzfilho@gmail.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: EtienneLescot <etiennelescot@gmail.com>
Co-authored-by: EtienneLescot <215859519+EtienneLescot@users.noreply.github.com>
Co-authored-by: Etienne Lescot <etienne@openscreen.local>
Co-authored-by: giulio333 <digiagiulio@gmail.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: 446f6e6e79 <66618414+446f6e6e79@users.noreply.github.com>
Co-authored-by: psychosomat <hello@ddark.dev>
Co-authored-by: itzadetunji <adetunjiadeyinka29@gmail.com>
Co-authored-by: Tamsi <tamsi.besson@gmail.com>
Co-authored-by: my_mac <1317811579@qq.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants