Skip to content

chore(release): release v1.9.0 into main - #288

Merged
EtienneLescot merged 5 commits into
mainfrom
release/v1.9.0-sync
Aug 5, 2026
Merged

chore(release): release v1.9.0 into main#288
EtienneLescot merged 5 commits into
mainfrom
release/v1.9.0-sync

Conversation

@EtienneLescot

@EtienneLescot EtienneLescot commented Aug 5, 2026

Copy link
Copy Markdown
Collaborator

Sync main with the released snapshot (RC + cherry-picked bugfixes + version bump). Rebase-merged via PAT; bypass applies because EtienneLescot is a ruleset bypass actor.

Summary by CodeRabbit

  • Chores
    • Updated the package to version 1.9.0.

@coderabbitai

coderabbitai Bot commented Aug 5, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The package version in package.json was updated from 1.8.0 to 1.9.0.

Changes

Package release metadata

Layer / File(s) Summary
Package version update
package.json
The package version changed from 1.8.0 to 1.9.0.

Estimated code review effort: 1 (Trivial) | ~2 minutes

Possibly related PRs

Suggested reviewers: siddharthvaddem

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description is relevant but omits nearly all required template sections, including change type, release impact, platform impact, and testing. Complete the template with a summary, related issue status, selected change type, release impact, platform impact, and testing details.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly identifies the release version and the target branch, matching the package version bump and release objective.
Docstring Coverage ✅ Passed Docstring coverage is 85.92% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch release/v1.9.0-sync

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 11

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/components/ai-edition/NewEditorShell.tsx (1)

686-701: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Start recording in one control path.

If the user selects Save or Discard, Line 686 starts recording and resolves the prompt. Line 699 then starts recording again. One user action can send two startNewRecording IPC requests.

Remove the action === "record" start from handleConfirmUnsaved. Keep recording startup in handleNewRecording after promptUnsaved resolves.

Proposed fix
-			const { action, resolve } = unsavedPrompt;
+			const { resolve } = unsavedPrompt;
...
-				if (action === "record") {
-					void window.electronAPI?.startNewRecording?.().catch((err) => {
-						console.warn("[editor] failed to start a new recording:", err);
-					});
-				}
 				resolve(choice);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/ai-edition/NewEditorShell.tsx` around lines 686 - 701, Remove
the action === "record" startNewRecording call from handleConfirmUnsaved,
leaving it responsible only for resolving the prompt. Keep handleNewRecording's
startNewRecording call after promptUnsaved resolves for non-cancel choices,
ensuring each recording action sends only one IPC request.
electron/ipc/handlers.ts (1)

2801-2812: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Do not attach webcam files before the helper finalizes them.

waitForNativeWindowsCaptureStop can return ok: true after emitting recording-stopped with webcamPath, even when the helper later exits 1 from ERROR: Failed to finalize the webcam recording or is terminated during webcam-encoder-finalize. The current parent-side logic treats any readable preferredWebcamPath as good, so an unindexed webcam MP4 can be stored in the session manifest and media links while the screen video is correctly retained.

Gate webcamVideoPath by the salvageable-output threshold before registering media links, or have the helper emit webcamPath only after webcamEncoder.finalize() succeeds. Keep the stricter contract change focused on the webcam emission so the screen file is not dropped.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@electron/ipc/handlers.ts` around lines 2801 - 2812, The webcam path in the
recording-session setup must not be accepted solely because it is readable.
Update the flow around waitForNativeWindowsCaptureStop and the webcamPath
emission so webcamVideoPath is only attached after webcamEncoder.finalize()
succeeds or the helper explicitly reports a salvageable finalized output;
preserve the screenVideoPath and session creation when webcam finalization fails
or the helper is terminated.
🧹 Nitpick comments (4)
AGENTS.md (1)

50-52: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Align the final full-suite policy.

The canonical guidance conflicts with the developer-agent guidance. The release workflow guide requires one final npm run test before pushing. Keep that requirement consistent.

  • AGENTS.md#L50-L52: remove the exception that permits targeted tests plus CI instead of the final full run.
  • .harness/reins/openscreen-dev/agent.md#L29-L31: replace “if at all” with the final full-suite requirement.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@AGENTS.md` around lines 50 - 52, Align the final test policy across both
guidance files: in AGENTS.md lines 50-52, require one final npm run test and
remove the exception allowing targeted tests plus CI; in
.harness/reins/openscreen-dev/agent.md lines 29-31, replace “if at all” with the
same final full-suite requirement.
electron/ipc/handlers.ts (1)

2446-2453: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider removing the partial output when the start fails.

The discard path and the stop-failure path both call removeNativeWindowsCaptureOutputs. This path does not. A helper that spawned, created the MP4, and then failed before Recording started leaves an unindexed stub in RECORDINGS_DIR that no session manifest references. The size gate already classifies such a file as unusable, so the same call is safe here.

Note the ordering constraint: read the paths into locals before resetNativeWindowsCaptureState() clears them.

♻️ Proposed refactor
 			} catch (error) {
 				console.error("Failed to start native Windows recording:", error);
+				const failedScreenPath = nativeWindowsCaptureTargetPath;
+				const failedWebcamPath = nativeWindowsCaptureWebcamTargetPath;
 				nativeWindowsCaptureProcess?.kill();
 				detachNativeWindowsCaptureOutputDrain();
 				resetNativeWindowsCaptureState();
 				await stopCursorRecording();
+				await removeNativeWindowsCaptureOutputs(failedScreenPath, failedWebcamPath, {
+					onlyIfUnusable: true,
+				});
 				return { success: false, error: String(error) };
 			}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@electron/ipc/handlers.ts` around lines 2446 - 2453, Update the start-failure
catch block in the native Windows recording flow to capture the current output
paths in locals before calling resetNativeWindowsCaptureState(), then invoke
removeNativeWindowsCaptureOutputs with those paths. Preserve the existing
cleanup and failure response while ensuring any partially created MP4 output is
discarded.
scripts/test-windows-wgc-helper.mjs (1)

44-61: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Guard the numeric env values against NaN.

Number(process.env[...]) returns NaN for a malformed value. A typo in OPENSCREEN_WGC_STOP_BUDGET_MS makes STOP_BUDGET_MS NaN, so STOP_HANG_LIMIT_MS is NaN, setTimeout(..., NaN) fires on the next tick, and the harness kills the helper and reports the issue #252 hang it exists to detect. String(NaN) also reaches the child environment, where readEnvInt discards it and silently uses its own default — so the harness and the helper no longer agree, which is the drift this constant was added to prevent.

Parse with a finite-value check and fall back to the documented default.

♻️ Proposed refactor
+const readEnvMs = (name, fallback) => {
+	const parsed = Number(process.env[name]);
+	return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
+};
-const STALL_READBACK_MS = Number(process.env[STALL_READBACK_ENV] ?? 60_000);
+const STALL_READBACK_MS = readEnvMs(STALL_READBACK_ENV, 60_000);
 const STOP_BUDGET_ENV = "OPENSCREEN_WGC_STOP_BUDGET_MS";
@@
-const STOP_BUDGET_MS = Number(process.env[STOP_BUDGET_ENV] ?? 50_000);
+const STOP_BUDGET_MS = readEnvMs(STOP_BUDGET_ENV, 50_000);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/test-windows-wgc-helper.mjs` around lines 44 - 61, Guard the numeric
environment parsing for STALL_READBACK_MS and STOP_BUDGET_MS with a finite-value
check, falling back to their documented defaults when parsing yields NaN or
another non-finite value. Ensure the validated STOP_BUDGET_MS is used
consistently to derive STOP_HANG_LIMIT_MS and pass the child environment value.
vitest.config.ts (1)

23-23: 🩺 Stability & Availability | 🔵 Trivial | 🏗️ Heavy lift

Scope the 15-second timeout to slow tests.

testTimeout is a global per-test timeout. Raising it from Vitest’s 5-second default to 15 seconds also delays failure reporting for unrelated hangs. Keep the suite default lower and apply 15 seconds only to the measured slow files or tests, unless the full-suite failure budget is an explicit release requirement. Vitest defines testTimeout as the default timeout of a test in milliseconds. (v4.vitest.dev)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@vitest.config.ts` at line 23, Keep the global testTimeout at Vitest’s default
or a lower suite-wide value, and move the 15-second timeout to only the specific
slow test files or tests that require it. Update the Vitest configuration around
testTimeout without changing unrelated test settings.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.github/workflows/build.yml:
- Around line 533-542: Update the prerelease previous-tag search in the workflow
around IS_PRERELEASE and RC_NUMBER to derive and reuse the current prerelease
kind, rather than hardcoding “rc” in CANDIDATE. Preserve the existing numeric
countdown and tag-existence check so alpha, beta, and rc releases select the
preceding tag of the same kind.

In `@electron/ai-edition/agent-tools.ts`:
- Around line 373-375: Bound the batch sizes accepted by addTrimsArgs and the
corresponding addZoom batch schema, and update the iteration flow around the
unitary executor at the 710-735 range to enforce that limit while preserving
sequential semantics. Avoid repeatedly copying the accumulated ranges on each
successful addTrim or addZoom operation by using an internal accumulator or
equivalent linear-time approach.

In `@electron/media/mediaLinksRegistry.test.ts`:
- Around line 280-297: Update the “survives the directory disappearing while the
refresh is queued” test to instrument the refresh write and wait until that
write is pending before removing tempDir. Assert the findMediaLinksByFingerprint
lookup result while the write barrier remains held, then release the barrier and
verify withoutUnhandledRejections still reports no rejections; keep the existing
warning handling and cleanup.

In `@electron/native/wgc-capture/src/main.cpp`:
- Around line 1052-1060: Update the beginStopStep lambda so it assigns
currentStepDeadlineMs before publishing the new currentStopStep value. Preserve
the existing deadline calculation and timing log, ensuring the watchdog cannot
observe a new step name paired with the previous expired deadline.

In `@electron/native/wgc-capture/src/wgc_session.cpp`:
- Around line 278-282: Replace the single quiesced_ state in WgcSession with
separate closed_ and handlerRevoked_ flags. Update quiesceCapture so retries
after a drain timeout repeat the drain and close sequence, while the early
return only reports success when closed_ is true; set closed_ immediately before
the successful return. Guard callback-handler revocation and frameCallback_
clearing with handlerRevoked_ to avoid repeating those operations.

In `@scripts/test-windows-wgc-helper.mjs`:
- Around line 60-61: Update STOP_LATENCY_BUDGET_MS and its comment in the test
harness so a legitimate encoder-finalize lasting up to the configured
OPENSCREEN_WGC_STOP_BUDGET_MS does not fail the helper; either set the overall
budget above STOP_BUDGET_MS or limit this timeout to pre-finalize steps.
Preserve the existing stopHung and required-step assertions as the checks for
hung or incomplete helpers.

In `@src/hooks/useScreenRecorder.nativeStopFailure.test.tsx`:
- Around line 20-41: Pin process.platform to "win32" in the native Windows test
suite’s beforeEach and restore the original platform value in afterEach. Keep
stubElectronAPI focused on Electron API mocks, and ensure cleanup runs after
every test so the host platform is not altered for other suites.

In `@src/hooks/useScreenRecorder.ts`:
- Around line 1200-1211: Track ownership transfer for the local recorder created
in the macOS path around createRecorderHandle, and when startup fails or the
countdown becomes inactive before webcamRecorder.current receives it, stop and
drain the recorder before calling discard(). Apply the same ownership tracking
and cleanup to the Linux path around its createRecorderHandle call in
src/hooks/useScreenRecorder.ts:1392-1401; ensure both sites preserve the
existing cleanup when ownership is transferred.

In `@src/native/sceneDescription.test.ts`:
- Line 1347: In the affected test scope, remove the duplicate const area
declarations and retain a single area helper declaration for the test’s
calculations. Ensure all existing usages continue referencing that remaining
declaration without changing its behavior.

In `@technical-documentation/testing/writing-tests.md`:
- Around line 21-42: Update the testing guidance to state that jsdom is required
only for tests using DOM APIs, including applicable *.test.tsx files, rather
than every TSX test. Revise the documented file pattern to match
vitest.config.ts by covering test and spec files across JavaScript and
TypeScript extensions, while retaining node as the default environment and the
explicit jsdom opt-in guidance.

In `@workbench/lib/oracles.ts`:
- Around line 297-305: Update the array handling in the result oracle so every
applied entry must be an object containing an identifier recognized by ID_KEYS;
return false for non-object entries or objects without such an identifier.
Preserve claimSurvives validation for entries that pass this structural check
and for non-array results.

---

Outside diff comments:
In `@electron/ipc/handlers.ts`:
- Around line 2801-2812: The webcam path in the recording-session setup must not
be accepted solely because it is readable. Update the flow around
waitForNativeWindowsCaptureStop and the webcamPath emission so webcamVideoPath
is only attached after webcamEncoder.finalize() succeeds or the helper
explicitly reports a salvageable finalized output; preserve the screenVideoPath
and session creation when webcam finalization fails or the helper is terminated.

In `@src/components/ai-edition/NewEditorShell.tsx`:
- Around line 686-701: Remove the action === "record" startNewRecording call
from handleConfirmUnsaved, leaving it responsible only for resolving the prompt.
Keep handleNewRecording's startNewRecording call after promptUnsaved resolves
for non-cancel choices, ensuring each recording action sends only one IPC
request.

---

Nitpick comments:
In `@AGENTS.md`:
- Around line 50-52: Align the final test policy across both guidance files: in
AGENTS.md lines 50-52, require one final npm run test and remove the exception
allowing targeted tests plus CI; in .harness/reins/openscreen-dev/agent.md lines
29-31, replace “if at all” with the same final full-suite requirement.

In `@electron/ipc/handlers.ts`:
- Around line 2446-2453: Update the start-failure catch block in the native
Windows recording flow to capture the current output paths in locals before
calling resetNativeWindowsCaptureState(), then invoke
removeNativeWindowsCaptureOutputs with those paths. Preserve the existing
cleanup and failure response while ensuring any partially created MP4 output is
discarded.

In `@scripts/test-windows-wgc-helper.mjs`:
- Around line 44-61: Guard the numeric environment parsing for STALL_READBACK_MS
and STOP_BUDGET_MS with a finite-value check, falling back to their documented
defaults when parsing yields NaN or another non-finite value. Ensure the
validated STOP_BUDGET_MS is used consistently to derive STOP_HANG_LIMIT_MS and
pass the child environment value.

In `@vitest.config.ts`:
- Line 23: Keep the global testTimeout at Vitest’s default or a lower suite-wide
value, and move the 15-second timeout to only the specific slow test files or
tests that require it. Update the Vitest configuration around testTimeout
without changing unrelated test settings.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 8d9eeb2f-e20a-4ad0-9a4e-f39d57ee17c7

📥 Commits

Reviewing files that changed from the base of the PR and between 90ae294 and 2879746.

📒 Files selected for processing (106)
  • .github/scripts/discord-thread-validator.mjs
  • .github/scripts/discord-thread-validator.test.mjs
  • .github/workflows/build.yml
  • .github/workflows/diagnostic-artifact.yml
  • .harness/docs/git-workflow.md
  • .harness/reins/openscreen-dev/agent.md
  • .harness/reins/openscreen-tester/agent.md
  • AGENTS.md
  • crates/compositor/src/compositor_linux.rs
  • crates/compositor/src/compositor_macos.rs
  • crates/compositor/src/compositor_windows.rs
  • crates/compositor/src/frame_geometry.rs
  • crates/compositor/src/live.rs
  • crates/compositor/src/timeline_walk.rs
  • crates/poc-d3d/src/bench.rs
  • electron-builder.json5
  • electron/ai-edition/agent-tools.test.ts
  • electron/ai-edition/agent-tools.ts
  • electron/ai-edition/chat-compaction.test.ts
  • electron/ai-edition/chat-compaction.ts
  • electron/ai-edition/chat-service.compaction.test.ts
  • electron/ai-edition/chat-service.ts
  • electron/ai-edition/deep-agent/service.test.ts
  • electron/ai-edition/deep-agent/service.ts
  • electron/ai-edition/document-service.test.ts
  • electron/electron-env.d.ts
  • electron/ipc/handlers.ts
  • electron/media/mediaLinksRegistry.test.ts
  • electron/media/mediaLinksRegistry.ts
  • electron/native-bridge/cursor/recording/windowsNativeRecordingSession.ts
  • electron/native/wgc-capture/src/cursor-sampler.cpp
  • electron/native/wgc-capture/src/main.cpp
  • electron/native/wgc-capture/src/wgc_session.cpp
  • electron/native/wgc-capture/src/wgc_session.h
  • electron/preload.ts
  • electron/recording/nativeWindowsCaptureStop.test.ts
  • electron/recording/nativeWindowsCaptureStop.ts
  • electron/recording/webm-seek-index.test.ts
  • electron/windows.ts
  • package.json
  • scripts/diagnostic-tool/diagnostic.mjs
  • scripts/test-windows-wgc-helper.mjs
  • src/components/ai-edition/CaptionsPane.gating.test.tsx
  • src/components/ai-edition/ChatWelcome.test.tsx
  • src/components/ai-edition/ColorField.test.tsx
  • src/components/ai-edition/EditorEmptyState.test.tsx
  • src/components/ai-edition/ExportDialog.tsx
  • src/components/ai-edition/Modals.tsx
  • src/components/ai-edition/NewEditorShell.tsx
  • src/components/ai-edition/NewProjectModal.test.tsx
  • src/components/ai-edition/Preview.test.tsx
  • src/components/ai-edition/PreviewCanvas.tsx
  • src/components/ai-edition/RightPanes.i18n.test.tsx
  • src/components/ai-edition/TranscriptPane.gating.test.tsx
  • src/components/ai-edition/TranscriptPane.keyboardCut.test.tsx
  • src/components/ai-edition/TranscriptPane.sharedMedia.test.tsx
  • src/components/ai-edition/TransportBar.test.tsx
  • src/components/ai-edition/VirtualPreview.playback.test.tsx
  • src/components/ai-edition/WebcamOverlay.test.tsx
  • src/components/ai-edition/backgroundImageUpload.test.tsx
  • src/components/ai-edition/chatBudget.ts
  • src/components/ai-edition/v4/EditorTopBar.test.tsx
  • src/components/ai-edition/v4/RecStage.tsx
  • src/components/ai-edition/v4/SpeedControl.test.tsx
  • src/components/ai-edition/v4/V4Timeline.geometry.test.tsx
  • src/components/launch/LaunchWindow.test.tsx
  • src/components/launch/NotesToolbar.test.tsx
  • src/components/launch/NotesWindow.css
  • src/components/launch/NotesWindow.editable.test.tsx
  • src/components/launch/NotesWindow.test.tsx
  • src/components/launch/NotesWindow.tsx
  • src/components/launch/SourceSelector.test.tsx
  • src/components/ui/gradient-editor.test.tsx
  • src/components/video-editor/editorDefaults.ts
  • src/hooks/recorderHandle.test.ts
  • src/hooks/useAudioPeaks.test.ts
  • src/hooks/useCameraDevices.test.ts
  • src/hooks/useScreenRecorder.nativeStopFailure.test.tsx
  • src/hooks/useScreenRecorder.ts
  • src/hooks/webcamAsset.test.ts
  • src/lib/ai-edition/store/editorSettings.test.ts
  • src/lib/ai-edition/store/editorSettings.ts
  • src/lib/ai-edition/store/projectStore.test.ts
  • src/lib/ai-edition/store/transcriptionStore.test.ts
  • src/lib/ai-edition/store/useSequentialTimelineOps.test.ts
  • src/lib/ai-edition/store/useTimeline.test.ts
  • src/lib/ai-edition/store/useTimeline.ts
  • src/lib/ai-edition/timeline/duration.test.ts
  • src/lib/ai-edition/timeline/pointer-drag.test.tsx
  • src/lib/captioning/transcribe.test.ts
  • src/native/hooks/useCompositorBackend.test.ts
  • src/native/hooks/useNativeCompositorView.test.ts
  • src/native/sceneDescription.test.ts
  • src/native/sceneDescription.ts
  • src/utils/platformUtils.test.ts
  • technical-documentation/architecture/ai-agent.md
  • technical-documentation/architecture/recording.md
  • technical-documentation/engineering/build-and-packaging.md
  • technical-documentation/engineering/ci-workflows.md
  • technical-documentation/engineering/release-and-secrets.md
  • technical-documentation/testing/writing-tests.md
  • tests/e2e/windows-native-checklist.spec.ts
  • vitest.config.ts
  • workbench/l0/oracles.wb.ts
  • workbench/lib/fixtures.ts
  • workbench/lib/oracles.ts

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Inline review comments failed to post. This is likely due to GitHub's internal server error or limits when posting large numbers of comments. If you are seeing this consistently it is likely a permissions issue. Please check "Moderation" -> "Code review limits" under your organization settings.

Actionable comments posted: 11

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/components/ai-edition/NewEditorShell.tsx (1)

686-701: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Start recording in one control path.

If the user selects Save or Discard, Line 686 starts recording and resolves the prompt. Line 699 then starts recording again. One user action can send two startNewRecording IPC requests.

Remove the action === "record" start from handleConfirmUnsaved. Keep recording startup in handleNewRecording after promptUnsaved resolves.

Proposed fix
-			const { action, resolve } = unsavedPrompt;
+			const { resolve } = unsavedPrompt;
...
-				if (action === "record") {
-					void window.electronAPI?.startNewRecording?.().catch((err) => {
-						console.warn("[editor] failed to start a new recording:", err);
-					});
-				}
 				resolve(choice);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/ai-edition/NewEditorShell.tsx` around lines 686 - 701, Remove
the action === "record" startNewRecording call from handleConfirmUnsaved,
leaving it responsible only for resolving the prompt. Keep handleNewRecording's
startNewRecording call after promptUnsaved resolves for non-cancel choices,
ensuring each recording action sends only one IPC request.
electron/ipc/handlers.ts (1)

2801-2812: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Do not attach webcam files before the helper finalizes them.

waitForNativeWindowsCaptureStop can return ok: true after emitting recording-stopped with webcamPath, even when the helper later exits 1 from ERROR: Failed to finalize the webcam recording or is terminated during webcam-encoder-finalize. The current parent-side logic treats any readable preferredWebcamPath as good, so an unindexed webcam MP4 can be stored in the session manifest and media links while the screen video is correctly retained.

Gate webcamVideoPath by the salvageable-output threshold before registering media links, or have the helper emit webcamPath only after webcamEncoder.finalize() succeeds. Keep the stricter contract change focused on the webcam emission so the screen file is not dropped.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@electron/ipc/handlers.ts` around lines 2801 - 2812, The webcam path in the
recording-session setup must not be accepted solely because it is readable.
Update the flow around waitForNativeWindowsCaptureStop and the webcamPath
emission so webcamVideoPath is only attached after webcamEncoder.finalize()
succeeds or the helper explicitly reports a salvageable finalized output;
preserve the screenVideoPath and session creation when webcam finalization fails
or the helper is terminated.
🧹 Nitpick comments (4)
AGENTS.md (1)

50-52: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Align the final full-suite policy.

The canonical guidance conflicts with the developer-agent guidance. The release workflow guide requires one final npm run test before pushing. Keep that requirement consistent.

  • AGENTS.md#L50-L52: remove the exception that permits targeted tests plus CI instead of the final full run.
  • .harness/reins/openscreen-dev/agent.md#L29-L31: replace “if at all” with the final full-suite requirement.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@AGENTS.md` around lines 50 - 52, Align the final test policy across both
guidance files: in AGENTS.md lines 50-52, require one final npm run test and
remove the exception allowing targeted tests plus CI; in
.harness/reins/openscreen-dev/agent.md lines 29-31, replace “if at all” with the
same final full-suite requirement.
electron/ipc/handlers.ts (1)

2446-2453: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider removing the partial output when the start fails.

The discard path and the stop-failure path both call removeNativeWindowsCaptureOutputs. This path does not. A helper that spawned, created the MP4, and then failed before Recording started leaves an unindexed stub in RECORDINGS_DIR that no session manifest references. The size gate already classifies such a file as unusable, so the same call is safe here.

Note the ordering constraint: read the paths into locals before resetNativeWindowsCaptureState() clears them.

♻️ Proposed refactor
 			} catch (error) {
 				console.error("Failed to start native Windows recording:", error);
+				const failedScreenPath = nativeWindowsCaptureTargetPath;
+				const failedWebcamPath = nativeWindowsCaptureWebcamTargetPath;
 				nativeWindowsCaptureProcess?.kill();
 				detachNativeWindowsCaptureOutputDrain();
 				resetNativeWindowsCaptureState();
 				await stopCursorRecording();
+				await removeNativeWindowsCaptureOutputs(failedScreenPath, failedWebcamPath, {
+					onlyIfUnusable: true,
+				});
 				return { success: false, error: String(error) };
 			}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@electron/ipc/handlers.ts` around lines 2446 - 2453, Update the start-failure
catch block in the native Windows recording flow to capture the current output
paths in locals before calling resetNativeWindowsCaptureState(), then invoke
removeNativeWindowsCaptureOutputs with those paths. Preserve the existing
cleanup and failure response while ensuring any partially created MP4 output is
discarded.
scripts/test-windows-wgc-helper.mjs (1)

44-61: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Guard the numeric env values against NaN.

Number(process.env[...]) returns NaN for a malformed value. A typo in OPENSCREEN_WGC_STOP_BUDGET_MS makes STOP_BUDGET_MS NaN, so STOP_HANG_LIMIT_MS is NaN, setTimeout(..., NaN) fires on the next tick, and the harness kills the helper and reports the issue #252 hang it exists to detect. String(NaN) also reaches the child environment, where readEnvInt discards it and silently uses its own default — so the harness and the helper no longer agree, which is the drift this constant was added to prevent.

Parse with a finite-value check and fall back to the documented default.

♻️ Proposed refactor
+const readEnvMs = (name, fallback) => {
+	const parsed = Number(process.env[name]);
+	return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
+};
-const STALL_READBACK_MS = Number(process.env[STALL_READBACK_ENV] ?? 60_000);
+const STALL_READBACK_MS = readEnvMs(STALL_READBACK_ENV, 60_000);
 const STOP_BUDGET_ENV = "OPENSCREEN_WGC_STOP_BUDGET_MS";
@@
-const STOP_BUDGET_MS = Number(process.env[STOP_BUDGET_ENV] ?? 50_000);
+const STOP_BUDGET_MS = readEnvMs(STOP_BUDGET_ENV, 50_000);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/test-windows-wgc-helper.mjs` around lines 44 - 61, Guard the numeric
environment parsing for STALL_READBACK_MS and STOP_BUDGET_MS with a finite-value
check, falling back to their documented defaults when parsing yields NaN or
another non-finite value. Ensure the validated STOP_BUDGET_MS is used
consistently to derive STOP_HANG_LIMIT_MS and pass the child environment value.
vitest.config.ts (1)

23-23: 🩺 Stability & Availability | 🔵 Trivial | 🏗️ Heavy lift

Scope the 15-second timeout to slow tests.

testTimeout is a global per-test timeout. Raising it from Vitest’s 5-second default to 15 seconds also delays failure reporting for unrelated hangs. Keep the suite default lower and apply 15 seconds only to the measured slow files or tests, unless the full-suite failure budget is an explicit release requirement. Vitest defines testTimeout as the default timeout of a test in milliseconds. (v4.vitest.dev)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@vitest.config.ts` at line 23, Keep the global testTimeout at Vitest’s default
or a lower suite-wide value, and move the 15-second timeout to only the specific
slow test files or tests that require it. Update the Vitest configuration around
testTimeout without changing unrelated test settings.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.github/workflows/build.yml:
- Around line 533-542: Update the prerelease previous-tag search in the workflow
around IS_PRERELEASE and RC_NUMBER to derive and reuse the current prerelease
kind, rather than hardcoding “rc” in CANDIDATE. Preserve the existing numeric
countdown and tag-existence check so alpha, beta, and rc releases select the
preceding tag of the same kind.

In `@electron/ai-edition/agent-tools.ts`:
- Around line 373-375: Bound the batch sizes accepted by addTrimsArgs and the
corresponding addZoom batch schema, and update the iteration flow around the
unitary executor at the 710-735 range to enforce that limit while preserving
sequential semantics. Avoid repeatedly copying the accumulated ranges on each
successful addTrim or addZoom operation by using an internal accumulator or
equivalent linear-time approach.

In `@electron/media/mediaLinksRegistry.test.ts`:
- Around line 280-297: Update the “survives the directory disappearing while the
refresh is queued” test to instrument the refresh write and wait until that
write is pending before removing tempDir. Assert the findMediaLinksByFingerprint
lookup result while the write barrier remains held, then release the barrier and
verify withoutUnhandledRejections still reports no rejections; keep the existing
warning handling and cleanup.

In `@electron/native/wgc-capture/src/main.cpp`:
- Around line 1052-1060: Update the beginStopStep lambda so it assigns
currentStepDeadlineMs before publishing the new currentStopStep value. Preserve
the existing deadline calculation and timing log, ensuring the watchdog cannot
observe a new step name paired with the previous expired deadline.

In `@electron/native/wgc-capture/src/wgc_session.cpp`:
- Around line 278-282: Replace the single quiesced_ state in WgcSession with
separate closed_ and handlerRevoked_ flags. Update quiesceCapture so retries
after a drain timeout repeat the drain and close sequence, while the early
return only reports success when closed_ is true; set closed_ immediately before
the successful return. Guard callback-handler revocation and frameCallback_
clearing with handlerRevoked_ to avoid repeating those operations.

In `@scripts/test-windows-wgc-helper.mjs`:
- Around line 60-61: Update STOP_LATENCY_BUDGET_MS and its comment in the test
harness so a legitimate encoder-finalize lasting up to the configured
OPENSCREEN_WGC_STOP_BUDGET_MS does not fail the helper; either set the overall
budget above STOP_BUDGET_MS or limit this timeout to pre-finalize steps.
Preserve the existing stopHung and required-step assertions as the checks for
hung or incomplete helpers.

In `@src/hooks/useScreenRecorder.nativeStopFailure.test.tsx`:
- Around line 20-41: Pin process.platform to "win32" in the native Windows test
suite’s beforeEach and restore the original platform value in afterEach. Keep
stubElectronAPI focused on Electron API mocks, and ensure cleanup runs after
every test so the host platform is not altered for other suites.

In `@src/hooks/useScreenRecorder.ts`:
- Around line 1200-1211: Track ownership transfer for the local recorder created
in the macOS path around createRecorderHandle, and when startup fails or the
countdown becomes inactive before webcamRecorder.current receives it, stop and
drain the recorder before calling discard(). Apply the same ownership tracking
and cleanup to the Linux path around its createRecorderHandle call in
src/hooks/useScreenRecorder.ts:1392-1401; ensure both sites preserve the
existing cleanup when ownership is transferred.

In `@src/native/sceneDescription.test.ts`:
- Line 1347: In the affected test scope, remove the duplicate const area
declarations and retain a single area helper declaration for the test’s
calculations. Ensure all existing usages continue referencing that remaining
declaration without changing its behavior.

In `@technical-documentation/testing/writing-tests.md`:
- Around line 21-42: Update the testing guidance to state that jsdom is required
only for tests using DOM APIs, including applicable *.test.tsx files, rather
than every TSX test. Revise the documented file pattern to match
vitest.config.ts by covering test and spec files across JavaScript and
TypeScript extensions, while retaining node as the default environment and the
explicit jsdom opt-in guidance.

In `@workbench/lib/oracles.ts`:
- Around line 297-305: Update the array handling in the result oracle so every
applied entry must be an object containing an identifier recognized by ID_KEYS;
return false for non-object entries or objects without such an identifier.
Preserve claimSurvives validation for entries that pass this structural check
and for non-array results.

---

Outside diff comments:
In `@electron/ipc/handlers.ts`:
- Around line 2801-2812: The webcam path in the recording-session setup must not
be accepted solely because it is readable. Update the flow around
waitForNativeWindowsCaptureStop and the webcamPath emission so webcamVideoPath
is only attached after webcamEncoder.finalize() succeeds or the helper
explicitly reports a salvageable finalized output; preserve the screenVideoPath
and session creation when webcam finalization fails or the helper is terminated.

In `@src/components/ai-edition/NewEditorShell.tsx`:
- Around line 686-701: Remove the action === "record" startNewRecording call
from handleConfirmUnsaved, leaving it responsible only for resolving the prompt.
Keep handleNewRecording's startNewRecording call after promptUnsaved resolves
for non-cancel choices, ensuring each recording action sends only one IPC
request.

---

Nitpick comments:
In `@AGENTS.md`:
- Around line 50-52: Align the final test policy across both guidance files: in
AGENTS.md lines 50-52, require one final npm run test and remove the exception
allowing targeted tests plus CI; in .harness/reins/openscreen-dev/agent.md lines
29-31, replace “if at all” with the same final full-suite requirement.

In `@electron/ipc/handlers.ts`:
- Around line 2446-2453: Update the start-failure catch block in the native
Windows recording flow to capture the current output paths in locals before
calling resetNativeWindowsCaptureState(), then invoke
removeNativeWindowsCaptureOutputs with those paths. Preserve the existing
cleanup and failure response while ensuring any partially created MP4 output is
discarded.

In `@scripts/test-windows-wgc-helper.mjs`:
- Around line 44-61: Guard the numeric environment parsing for STALL_READBACK_MS
and STOP_BUDGET_MS with a finite-value check, falling back to their documented
defaults when parsing yields NaN or another non-finite value. Ensure the
validated STOP_BUDGET_MS is used consistently to derive STOP_HANG_LIMIT_MS and
pass the child environment value.

In `@vitest.config.ts`:
- Line 23: Keep the global testTimeout at Vitest’s default or a lower suite-wide
value, and move the 15-second timeout to only the specific slow test files or
tests that require it. Update the Vitest configuration around testTimeout
without changing unrelated test settings.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 8d9eeb2f-e20a-4ad0-9a4e-f39d57ee17c7

📥 Commits

Reviewing files that changed from the base of the PR and between 90ae294 and 2879746.

📒 Files selected for processing (106)
  • .github/scripts/discord-thread-validator.mjs
  • .github/scripts/discord-thread-validator.test.mjs
  • .github/workflows/build.yml
  • .github/workflows/diagnostic-artifact.yml
  • .harness/docs/git-workflow.md
  • .harness/reins/openscreen-dev/agent.md
  • .harness/reins/openscreen-tester/agent.md
  • AGENTS.md
  • crates/compositor/src/compositor_linux.rs
  • crates/compositor/src/compositor_macos.rs
  • crates/compositor/src/compositor_windows.rs
  • crates/compositor/src/frame_geometry.rs
  • crates/compositor/src/live.rs
  • crates/compositor/src/timeline_walk.rs
  • crates/poc-d3d/src/bench.rs
  • electron-builder.json5
  • electron/ai-edition/agent-tools.test.ts
  • electron/ai-edition/agent-tools.ts
  • electron/ai-edition/chat-compaction.test.ts
  • electron/ai-edition/chat-compaction.ts
  • electron/ai-edition/chat-service.compaction.test.ts
  • electron/ai-edition/chat-service.ts
  • electron/ai-edition/deep-agent/service.test.ts
  • electron/ai-edition/deep-agent/service.ts
  • electron/ai-edition/document-service.test.ts
  • electron/electron-env.d.ts
  • electron/ipc/handlers.ts
  • electron/media/mediaLinksRegistry.test.ts
  • electron/media/mediaLinksRegistry.ts
  • electron/native-bridge/cursor/recording/windowsNativeRecordingSession.ts
  • electron/native/wgc-capture/src/cursor-sampler.cpp
  • electron/native/wgc-capture/src/main.cpp
  • electron/native/wgc-capture/src/wgc_session.cpp
  • electron/native/wgc-capture/src/wgc_session.h
  • electron/preload.ts
  • electron/recording/nativeWindowsCaptureStop.test.ts
  • electron/recording/nativeWindowsCaptureStop.ts
  • electron/recording/webm-seek-index.test.ts
  • electron/windows.ts
  • package.json
  • scripts/diagnostic-tool/diagnostic.mjs
  • scripts/test-windows-wgc-helper.mjs
  • src/components/ai-edition/CaptionsPane.gating.test.tsx
  • src/components/ai-edition/ChatWelcome.test.tsx
  • src/components/ai-edition/ColorField.test.tsx
  • src/components/ai-edition/EditorEmptyState.test.tsx
  • src/components/ai-edition/ExportDialog.tsx
  • src/components/ai-edition/Modals.tsx
  • src/components/ai-edition/NewEditorShell.tsx
  • src/components/ai-edition/NewProjectModal.test.tsx
  • src/components/ai-edition/Preview.test.tsx
  • src/components/ai-edition/PreviewCanvas.tsx
  • src/components/ai-edition/RightPanes.i18n.test.tsx
  • src/components/ai-edition/TranscriptPane.gating.test.tsx
  • src/components/ai-edition/TranscriptPane.keyboardCut.test.tsx
  • src/components/ai-edition/TranscriptPane.sharedMedia.test.tsx
  • src/components/ai-edition/TransportBar.test.tsx
  • src/components/ai-edition/VirtualPreview.playback.test.tsx
  • src/components/ai-edition/WebcamOverlay.test.tsx
  • src/components/ai-edition/backgroundImageUpload.test.tsx
  • src/components/ai-edition/chatBudget.ts
  • src/components/ai-edition/v4/EditorTopBar.test.tsx
  • src/components/ai-edition/v4/RecStage.tsx
  • src/components/ai-edition/v4/SpeedControl.test.tsx
  • src/components/ai-edition/v4/V4Timeline.geometry.test.tsx
  • src/components/launch/LaunchWindow.test.tsx
  • src/components/launch/NotesToolbar.test.tsx
  • src/components/launch/NotesWindow.css
  • src/components/launch/NotesWindow.editable.test.tsx
  • src/components/launch/NotesWindow.test.tsx
  • src/components/launch/NotesWindow.tsx
  • src/components/launch/SourceSelector.test.tsx
  • src/components/ui/gradient-editor.test.tsx
  • src/components/video-editor/editorDefaults.ts
  • src/hooks/recorderHandle.test.ts
  • src/hooks/useAudioPeaks.test.ts
  • src/hooks/useCameraDevices.test.ts
  • src/hooks/useScreenRecorder.nativeStopFailure.test.tsx
  • src/hooks/useScreenRecorder.ts
  • src/hooks/webcamAsset.test.ts
  • src/lib/ai-edition/store/editorSettings.test.ts
  • src/lib/ai-edition/store/editorSettings.ts
  • src/lib/ai-edition/store/projectStore.test.ts
  • src/lib/ai-edition/store/transcriptionStore.test.ts
  • src/lib/ai-edition/store/useSequentialTimelineOps.test.ts
  • src/lib/ai-edition/store/useTimeline.test.ts
  • src/lib/ai-edition/store/useTimeline.ts
  • src/lib/ai-edition/timeline/duration.test.ts
  • src/lib/ai-edition/timeline/pointer-drag.test.tsx
  • src/lib/captioning/transcribe.test.ts
  • src/native/hooks/useCompositorBackend.test.ts
  • src/native/hooks/useNativeCompositorView.test.ts
  • src/native/sceneDescription.test.ts
  • src/native/sceneDescription.ts
  • src/utils/platformUtils.test.ts
  • technical-documentation/architecture/ai-agent.md
  • technical-documentation/architecture/recording.md
  • technical-documentation/engineering/build-and-packaging.md
  • technical-documentation/engineering/ci-workflows.md
  • technical-documentation/engineering/release-and-secrets.md
  • technical-documentation/testing/writing-tests.md
  • tests/e2e/windows-native-checklist.spec.ts
  • vitest.config.ts
  • workbench/l0/oracles.wb.ts
  • workbench/lib/fixtures.ts
  • workbench/lib/oracles.ts
🛑 Comments failed to post (11)
.github/workflows/build.yml (1)

533-542: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Use the current prerelease kind when selecting the previous tag.

Line 536 always searches for -rc.N. The validation at Line 493 also accepts -beta.N and -alpha.N. A v1.9.0-beta.2 release therefore skips v1.9.0-beta.1 and can generate notes from the wrong range.

Proposed fix
 if [[ "$IS_PRERELEASE" == "true" ]]; then
+  PRERELEASE_KIND="${VERSION#*-}"
+  PRERELEASE_KIND="${PRERELEASE_KIND%%.*}"
   RC_NUMBER="${VERSION##*.}"
   for (( n = RC_NUMBER - 1; n >= 1; n-- )); do
-    CANDIDATE="v${STABLE_VERSION}-rc.${n}"
+    CANDIDATE="v${STABLE_VERSION}-${PRERELEASE_KIND}.${n}"
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

          if [[ "$IS_PRERELEASE" == "true" ]]; then
            PRERELEASE_KIND="${VERSION#*-}"
            PRERELEASE_KIND="${PRERELEASE_KIND%%.*}"
            RC_NUMBER="${VERSION##*.}"
            for (( n = RC_NUMBER - 1; n >= 1; n-- )); do
              CANDIDATE="v${STABLE_VERSION}-${PRERELEASE_KIND}.${n}"
              if git rev-parse -q --verify "refs/tags/${CANDIDATE}" >/dev/null; then
                NOTES_START_TAG="$CANDIDATE"
                break
              fi
            done
          fi
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/build.yml around lines 533 - 542, Update the prerelease
previous-tag search in the workflow around IS_PRERELEASE and RC_NUMBER to derive
and reuse the current prerelease kind, rather than hardcoding “rc” in CANDIDATE.
Preserve the existing numeric countdown and tag-existence check so alpha, beta,
and rc releases select the preceding tag of the same kind.
electron/ai-edition/agent-tools.ts (1)

373-375: 🚀 Performance & Scalability | 🟠 Major | 🏗️ Heavy lift

Bound or linearize batch execution.

Lines 373-375 and 441-443 accept unbounded lists. Each successful iteration at Line 721 replays a unitary executor against current. addTrim and addZoom then copy the accumulated range array on every append.

A large model-generated batch therefore has quadratic array-copy cost and can monopolize the agent tool loop. Preserve sequential semantics, but use a bounded work budget or an internal accumulator that avoids copying all prior successful ranges for each item.

Also applies to: 441-443, 710-735

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@electron/ai-edition/agent-tools.ts` around lines 373 - 375, Bound the batch
sizes accepted by addTrimsArgs and the corresponding addZoom batch schema, and
update the iteration flow around the unitary executor at the 710-735 range to
enforce that limit while preserving sequential semantics. Avoid repeatedly
copying the accumulated ranges on each successful addTrim or addZoom operation
by using an internal accumulator or equivalent linear-time approach.
electron/media/mediaLinksRegistry.test.ts (1)

280-297: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Synchronize the directory-removal race.

findMediaLinksByFingerprint first awaits fingerprint and registry reads. Line 291 can remove moved or the registry before the background refresh is queued. The test can then fail from the lookup itself instead of testing a detached refresh failure.

Wait for an instrumented refresh write to become pending before removing tempDir. Also assert the lookup result before releasing the write barrier.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@electron/media/mediaLinksRegistry.test.ts` around lines 280 - 297, Update the
“survives the directory disappearing while the refresh is queued” test to
instrument the refresh write and wait until that write is pending before
removing tempDir. Assert the findMediaLinksByFingerprint lookup result while the
write barrier remains held, then release the barrier and verify
withoutUnhandledRejections still reports no rejections; keep the existing
warning handling and cleanup.
electron/native/wgc-capture/src/main.cpp (1)

1052-1060: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Publish the new deadline before the new step name.

beginStopStep writes currentStopStep first and currentStepDeadlineMs second. The watchdog reads both independently. If the previous step returned just after its deadline passed — the watchdog polls only every 50 ms, so a step can finish at 8005 ms against an 8000 ms deadline — the watchdog can observe the new step name together with the old, already-expired deadline and call TerminateProcess at the very start of a step that had its full budget. For encoder-finalize that kills the process before the MP4 index is written.

Store the deadline first. The watchdog then never sees a name paired with a stale deadline.

🔧 Proposed fix
     auto beginStopStep = [&](const char* step, int budgetMs) {
-        currentStopStep = step;
         // Clamped to the ceiling: no sequence of individually-patient steps can
         // add up to a shutdown the app has already given up on.
         currentStepDeadlineMs =
             std::min<int64_t>(stopElapsedMs() + budgetMs, shutdownBudgetMs);
+        // After the deadline, never before: the watchdog reads the two
+        // separately, and a new name paired with the previous step's expired
+        // deadline would abandon a step that still had its full budget.
+        currentStopStep = step;
         std::cerr << "[stop-timing] step=" << step << " elapsed_ms=" << stopElapsedMs()
                   << " phase=begin" << std::endl;
     };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

    auto beginStopStep = [&](const char* step, int budgetMs) {
        // Clamped to the ceiling: no sequence of individually-patient steps can
        // add up to a shutdown the app has already given up on.
        currentStepDeadlineMs =
            std::min<int64_t>(stopElapsedMs() + budgetMs, shutdownBudgetMs);
        // After the deadline, never before: the watchdog reads the two
        // separately, and a new name paired with the previous step's expired
        // deadline would abandon a step that still had its full budget.
        currentStopStep = step;
        std::cerr << "[stop-timing] step=" << step << " elapsed_ms=" << stopElapsedMs()
                  << " phase=begin" << std::endl;
    };
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@electron/native/wgc-capture/src/main.cpp` around lines 1052 - 1060, Update
the beginStopStep lambda so it assigns currentStepDeadlineMs before publishing
the new currentStopStep value. Preserve the existing deadline calculation and
timing log, ensuring the watchdog cannot observe a new step name paired with the
previous expired deadline.
electron/native/wgc-capture/src/wgc_session.cpp (1)

278-282: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

A retry after a failed drain returns true without closing the session.

quiesced_ is set before the drain. If the drain times out, the function returns false with session_ and framePool_ still open. A later call — stop(), or ~WgcSession() via stop() — takes the early return at Line 279. If the callback has finished by then, it returns true, and stop() releases the D3D device while the capture session and frame pool were never closed. The header documents quiesceCapture as idempotent, so a second call that reports success must have completed the close.

Latch the flag only once the close block has run. Then a retry redoes the drain and the close.

🔧 Proposed fix
 bool WgcSession::quiesceCapture(int drainTimeoutMs) {
-    if (quiesced_) {
+    // Only a call that got all the way through the close below counts as
+    // quiesced. A drain that timed out left session_/framePool_ open, so a
+    // retry has to run the close rather than report success on its behalf.
+    if (closed_) {
         return callbacksInFlight_.load() == 0;
     }
-    quiesced_ = true;
+    if (!handlerRevoked_) {
+        handlerRevoked_ = true;
+        // ... revoke + clear frameCallback_ (once is enough) ...
+    }

Add the two flags to wgc_session.h in place of quiesced_, and set closed_ = true immediately before return true; at Line 339. Keep the revoke and the frameCallback_ clear guarded by handlerRevoked_ so a retry does not revoke a token twice.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@electron/native/wgc-capture/src/wgc_session.cpp` around lines 278 - 282,
Replace the single quiesced_ state in WgcSession with separate closed_ and
handlerRevoked_ flags. Update quiesceCapture so retries after a drain timeout
repeat the drain and close sequence, while the early return only reports success
when closed_ is true; set closed_ immediately before the successful return.
Guard callback-handler revocation and frameCallback_ clearing with
handlerRevoked_ to avoid repeating those operations.
scripts/test-windows-wgc-helper.mjs (1)

60-61: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

The stop latency budget can fail a helper that is still working.

The comment says a healthy stop is well under a second, and the value is 15 s. Neither matches the helper's contract. main.cpp gives encoder-finalize the whole OPENSCREEN_WGC_STOP_BUDGET_MS ceiling on purpose, because a long recording through the software encoder legitimately spends seconds in IMFSinkWriter::Finalize — that is issue #34, cited in the STOP_BUDGET_MS comment directly above. A run that finalizes for 20 s therefore stops cleanly and still throws here.

The harness already asserts the real invariant: stopHung catches a helper that never ended itself, and the required steps catch an incomplete sequence. Either raise this budget above STOP_BUDGET_MS, or apply it to the pre-finalize steps only and correct the comment to state the value it enforces.

🔧 Proposed fix
-/** A healthy stop is well under a second. */
-const STOP_LATENCY_BUDGET_MS = 15_000;
+/**
+ * A stop that finished but took longer than the helper's own ceiling means the
+ * ceiling was not enforced. Kept above STOP_BUDGET_MS so a legitimate slow
+ * `encoder-finalize` (issue `#34`) is not reported as a failure.
+ */
+const STOP_LATENCY_BUDGET_MS = STOP_BUDGET_MS;

Also applies to: 168-172

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/test-windows-wgc-helper.mjs` around lines 60 - 61, Update
STOP_LATENCY_BUDGET_MS and its comment in the test harness so a legitimate
encoder-finalize lasting up to the configured OPENSCREEN_WGC_STOP_BUDGET_MS does
not fail the helper; either set the overall budget above STOP_BUDGET_MS or limit
this timeout to pre-finalize steps. Preserve the existing stopHung and
required-step assertions as the checks for hung or incomplete helpers.
src/hooks/useScreenRecorder.nativeStopFailure.test.tsx (1)

20-41: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Pin process.platform to win32 in this Windows-specific suite.

stubElectronAPI only controls window.electronAPI.getPlatform(). Direct platform checks in the hook dependency chain can still use the host platform. Set process.platform in beforeEach and restore it in afterEach.

Proposed fix
 const SOURCE = { id: "screen:0:0", name: "Screen 1", display_id: "1", thumbnail: "" };
+const REAL_PLATFORM = process.platform;
+
+const setPlatform = (value: NodeJS.Platform) =>
+	Object.defineProperty(process, "platform", { value, configurable: true });
 
 let api: Record<string, ReturnType<typeof vi.fn>>;
 
 beforeEach(() => {
 	vi.useFakeTimers();
+	setPlatform("win32");
 	stubElectronAPI();
 });
 
 afterEach(() => {
+	setPlatform(REAL_PLATFORM);
 	vi.useRealTimers();
 	vi.restoreAllMocks();
 });

As per coding guidelines, “Pin process.platform in platform-conditional tests so CI does not hide Windows/macOS failures.”

Also applies to: 72-80

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/hooks/useScreenRecorder.nativeStopFailure.test.tsx` around lines 20 - 41,
Pin process.platform to "win32" in the native Windows test suite’s beforeEach
and restore the original platform value in afterEach. Keep stubElectronAPI
focused on Electron API mocks, and ensure cleanup runs after every test so the
host platform is not altered for other suites.

Source: Coding guidelines

src/hooks/useScreenRecorder.ts (1)

1200-1211: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Clean up the local webcam recorder when native startup does not transfer ownership.

Both paths create nativeWebcamRecorder before native startup completes. If startup throws, or the countdown becomes inactive after startup, the function returns before assigning that recorder to webcamRecorder.current. The outer cleanup only clears refs and media tracks. It cannot call RecorderHandle.discard(). With streaming enabled, this can leave a partial webcam file and an active recorder.

  • src/hooks/useScreenRecorder.ts#L1200-L1211: Track whether the macOS path transfers ownership. If it does not, stop and drain the local recorder, then call discard().
  • src/hooks/useScreenRecorder.ts#L1392-L1401: Apply the same cleanup for the Linux path.
📍 Affects 1 file
  • src/hooks/useScreenRecorder.ts#L1200-L1211 (this comment)
  • src/hooks/useScreenRecorder.ts#L1392-L1401
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/hooks/useScreenRecorder.ts` around lines 1200 - 1211, Track ownership
transfer for the local recorder created in the macOS path around
createRecorderHandle, and when startup fails or the countdown becomes inactive
before webcamRecorder.current receives it, stop and drain the recorder before
calling discard(). Apply the same ownership tracking and cleanup to the Linux
path around its createRecorderHandle call in
src/hooks/useScreenRecorder.ts:1392-1401; ensure both sites preserve the
existing cleanup when ownership is transferred.
src/native/sceneDescription.test.ts (1)

1347-1347: 🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win

Remove the duplicate area declarations.

Line 1347 declares the same const area three times in one test scope. TypeScript reports a block-scoped redeclaration error. Keep one declaration.

Proposed fix
 		const blocked = byClip[0]?.screenRect;
 		const alone = byClip[1]?.screenRect;
 		if (!blocked || !alone) throw new Error("entree manquante");
 		// Meme source (1920x1080) des deux cotes : la seule difference est la camera.
 		const area = (r: { width: number; height: number }) => r.width * r.height;
-		const area = (r: { width: number; height: number }) => r.width * r.height;
-		const area = (r: { width: number; height: number }) => r.width * r.height;
 		expect(area(alone)).toBeGreaterThan(area(blocked) * 1.2);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/native/sceneDescription.test.ts` at line 1347, In the affected test
scope, remove the duplicate const area declarations and retain a single area
helper declaration for the test’s calculations. Ensure all existing usages
continue referencing that remaining declaration without changing its behavior.
technical-documentation/testing/writing-tests.md (1)

21-42: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Correct the unit-test environment and file-pattern guidance.

Do not require jsdom for every *.test.tsx file. Use jsdom only when the test needs DOM APIs. Match the documented pattern to vitest.config.ts, which includes test and spec files across JavaScript and TypeScript extensions.

Proposed fix
-**File pattern:** `{src,electron,.github}/**/*.test.{ts,tsx}`
+**File pattern:** `{src,electron,.github}/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}`

-That is also the fix when a new test dies on `document is not defined` or
-`ReferenceError: window is not defined`. Every `*.test.tsx` needs it; a `*.test.ts` needs
-it only if it renders a component, uses `renderHook`, or reaches for a browser global.
+Use the docblock when a test needs `document`, `window`, Testing Library rendering,
+`renderHook`, or another browser API. File extension alone does not require jsdom.

As per coding guidelines, “Use the default node Vitest environment unless the test explicitly needs the DOM.”

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

**Config:** `vitest.config.ts`  
**Runs in:** Node by default; jsdom only for files that ask for it  
**File pattern:** `{src,electron,.github}/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}`  
**CI command:** `npm run test`

Use unit tests for pure logic, utility functions, data transformations, and React behavior.

### Environment: node by default, jsdom on request

Building a jsdom for a test that never touches the DOM was this suite's single largest
cost — 719s of cumulative environment setup against 89s of actual test time. So
`vitest.config.ts` sets `environment: "node"`, and the 37 files that genuinely need a DOM
opt in with a docblock on line 1:

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@technical-documentation/testing/writing-tests.md` around lines 21 - 42,
Update the testing guidance to state that jsdom is required only for tests using
DOM APIs, including applicable *.test.tsx files, rather than every TSX test.
Revise the documented file pattern to match vitest.config.ts by covering test
and spec files across JavaScript and TypeScript extensions, while retaining node
as the default environment and the explicit jsdom opt-in guidance.

Source: Coding guidelines

workbench/lib/oracles.ts (1)

297-305: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Reject malformed applied entries.

The new batch branch returns true for a non-object entry. It also returns true for {} because claimSurvives accepts results without an ID key.

A successful addTrims or addZooms result must contain a document-backed identifier for every applied entry. Return false when an entry is not an object or has no ID_KEYS identifier. Otherwise this oracle accepts malformed batch results and cannot detect a broken tool response.

Proposed fix
 if (Array.isArray(applied)) {
-	return applied.every((entry) =>
-		entry && typeof entry === "object"
-			? claimSurvives(after, entry as Record<string, unknown>)
-			: true,
-	);
+	return (
+		applied.length > 0 &&
+		applied.every((entry) => {
+			if (!entry || typeof entry !== "object") return false;
+			const claim = entry as Record<string, unknown>;
+			return ID_KEYS.some((key) => typeof claim[key] === "string") && claimSurvives(after, claim);
+		})
+	);
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

	const applied = result.applied;
	if (Array.isArray(applied)) {
		return (
			applied.length > 0 &&
			applied.every((entry) => {
				if (!entry || typeof entry !== "object") return false;
				const claim = entry as Record<string, unknown>;
				return ID_KEYS.some((key) => typeof claim[key] === "string") && claimSurvives(after, claim);
			})
		);
	}
	return claimSurvives(after, result);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@workbench/lib/oracles.ts` around lines 297 - 305, Update the array handling
in the result oracle so every applied entry must be an object containing an
identifier recognized by ID_KEYS; return false for non-object entries or objects
without such an identifier. Preserve claimSurvives validation for entries that
pass this structural check and for non-array results.

@coderabbitai

coderabbitai Bot commented Aug 5, 2026

Copy link
Copy Markdown

Note

GitHub couldn't provide a complete incremental comparison for this pull request, so CodeRabbit is performing a full review instead. This review may take a little longer.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@package.json`:
- Line 4: Synchronize the root package metadata in package-lock.json with the
1.9.0 version declared by package.json, either by regenerating the lockfile or
updating its root version fields while preserving all dependency data.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 5f3e3633-b69e-4014-a716-72946c09a2d9

📥 Commits

Reviewing files that changed from the base of the PR and between 90ae294 and 7385656.

📒 Files selected for processing (1)
  • package.json

Comment thread package.json
"name": "openscreen",
"private": true,
"version": "1.8.0",
"version": "1.9.0",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Synchronize package-lock.json with the release version.

package.json declares 1.9.0, but package-lock.json still declares root version 1.8.0 at Lines 1-10. Regenerate the lockfile or update its root metadata before merging so package metadata remains consistent.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@package.json` at line 4, Synchronize the root package metadata in
package-lock.json with the 1.9.0 version declared by package.json, either by
regenerating the lockfile or updating its root version fields while preserving
all dependency data.

@EtienneLescot
EtienneLescot merged commit af1ff35 into main Aug 5, 2026
5 of 6 checks passed
@EtienneLescot
EtienneLescot deleted the release/v1.9.0-sync branch August 5, 2026 17:47
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.

1 participant