Skip to content

feat(cli): headless record / export / info commands - #176

Open
PeterTakahashi wants to merge 2 commits into
getopenscreen:mainfrom
PeterTakahashi:feat/headless-cli
Open

feat(cli): headless record / export / info commands#176
PeterTakahashi wants to merge 2 commits into
getopenscreen:mainfrom
PeterTakahashi:feat/headless-cli

Conversation

@PeterTakahashi

@PeterTakahashi PeterTakahashi commented Jul 27, 2026

Copy link
Copy Markdown

Summary

This PR adds a headless CLI to OpenScreen so scripts, CI pipelines, and AI coding agents can produce polished demos end-to-end — record, edit the project JSON programmatically, render — with no visible windows and machine-readable output:

# record the screen (native ScreenCaptureKit/WGC pipeline, cursor telemetry included)
openscreen record --duration 20 --project demo.openscreen --json

# agents can add zooms/annotations/trims by editing the .openscreen JSON directly

# render with the real export pipeline, optionally mixing in a TTS voiceover
openscreen export demo.openscreen -o demo.mp4 --audio voice.m4a --json

Full docs in docs/cli.md (commands, NDJSON protocol, exit codes, an end-to-end agent example).

Why

.openscreen projects are plain JSON and the export pipeline is already proven headless by the HEADLESS=true e2e test — the missing piece was a way to drive both without the GUI. With this CLI, an AI coding agent can demo the product it just built: record it, zoom into the interesting parts, narrate with TTS, and ship an MP4.

Design — deliberately minimal footprint

The CLI reuses the existing pipelines unchanged via the hidden-window pattern your tests/e2e/gif-export.spec.ts already established:

  • electron/cli/args.ts — pure argv parser (14 unit tests). Skips leading Chromium switches so Openscreen.AppImage --no-sandbox export … works.
  • electron/cli/cliMain.ts — headless boot: no HUD/tray/menu/dock, reuses registerIpcHandlers with inert window callbacks (the big handler module is untouched), NDJSON/--json protocol on stdout, diagnostics on stderr, SIGINT/SIGTERM/stdin stop, EPIPE-safe writes, exit codes 0/1/2, SwiftShader fallback for GPU-less environments.
  • src/cli/CliExportRunner.tsx — loads the project via the existing load-project-file-from-path bridge action, rebuilds the same exporter config the editor's dialog builds, runs VideoExporter/GifExporter. DOM-derived preview dimensions are replaced by a deterministic 1280×720 reference box (--preview-size to override).
  • src/cli/CliRecordRunner.tsx — drives useScreenRecorder (source by display index or window-title substring, mic by device label, system audio, cursor modes, --duration), then optionally writes a ready-to-export project file.
  • --audio voiceover mixing (src/lib/exporter/voiceoverMix.ts): copies the exported video packets without re-encoding (mediabunny Input/EncodedPacketSinkEncodedVideoPacketSource) and renders the audio offline with OfflineAudioContext (mix or replace, --audio-offset).

Only three GUI-side touches: the main.ts CLI branch, new preload IPC methods, and exposing startRecordingImmediately() (countdown-less start) from useScreenRecorder.

Tested (macOS 26.6, Apple Silicon)

  • record --duration 4/5 → MP4 + .cursor.json + session manifest; stop via SIGINT and via --duration; --project output loads in the GUI editor
  • export MP4 (zooms, annotations, padding, multi-track audio fixture) and GIF (magic bytes + dimensions verified); output verified with ffprobe
  • Full agent pipeline: record → programmatic JSON edit → export; and --audio with a real say-generated TTS track (volumedetect confirms audible narration)
  • openscreen export | head (EPIPE) no longer crashes or shows Electron's error dialog; renderer console errors are forwarded to stderr
  • npm run lint clean, tsc clean, all 441 vitest unit tests pass (14 new), GUI mode boots unaffected

Platform caveats (honest disclosure)

Windows and Linux paths were statically reviewed, not device-tested (I only have macOS hardware). The review-driven fixes are included: leading-switch subcommand detection, stdin guard moved inside try (Windows GUI-subsystem stdio edge case), and platform notes in docs/cli.md (Windows: no SIGTERM — use stdin stop/--duration; Linux Wayland: PipeWire portal may prompt and headless sessions can't record). Happy to iterate if maintainers can test on those platforms.

🤖 Generated with Claude Code

Summary by CodeRabbit

Summary by CodeRabbit

  • New Features
    • Added a headless CLI with record, export, and info, including NDJSON/TTY progress output and scripted stop controls.
    • Introduced a shared CLI request/response contract powering hidden runner flows for recording and exporting.
    • Added voiceover post-processing support for CLI exports (mix/replace).
  • Documentation
    • Updated README and added CLI documentation with commands, options, examples, and exit-code behavior.
  • Tests
    • Added argument-parsing tests covering validation, defaults, path handling, and error cases.

Adds a CLI so scripts, CI pipelines, and AI coding agents can drive
OpenScreen end-to-end without a visible window:

  openscreen record --duration 20 --project demo.openscreen --json
  openscreen export demo.openscreen -o demo.mp4 --audio voice.m4a --json
  openscreen info demo.openscreen --json

Design: CLI mode boots Electron without HUD/tray/menu and drives a hidden
runner window (windowType=cli-export|cli-record) that reuses the existing
pipelines unchanged — VideoExporter/GifExporter for export (the approach
already proven by the HEADLESS=true e2e test) and useScreenRecorder for
recording (native SCK/WGC helpers, browser fallback). registerIpcHandlers
is reused with inert window callbacks, so the 2900-line handler module is
untouched.

- electron/cli/args.ts: pure argv parser + unit tests; skips leading
  Chromium switches (AppImage --no-sandbox) before subcommand detection
- electron/cli/cliMain.ts: stdio protocol (NDJSON with --json, TTY-aware
  progress otherwise), SIGINT/SIGTERM/stdin stop, EPIPE-safe writes,
  exit codes 0/1/2, console rerouted to stderr, SwiftShader fallback
- src/cli/CliExportRunner.tsx: loads .openscreen via the native bridge,
  rebuilds the editor's exporter config deterministically (1280x720
  reference preview box, overridable via --preview-size), optional
  voiceover mix (--audio/--audio-mode/--audio-offset) that copies video
  packets and re-renders audio offline via OfflineAudioContext+mediabunny
- src/cli/CliRecordRunner.tsx: source pick by display index or window
  title, mic device by label, --duration/signal/stdin stop, optional
  ready-to-export --project output
- useScreenRecorder: expose startRecordingImmediately() (skips countdown)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019ceTJjoPJYa5BdmXZdDc5J
@coderabbitai

coderabbitai Bot commented Jul 27, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 9ec84f7f-5d4e-4018-aab5-e277f2ddf3d4

📥 Commits

Reviewing files that changed from the base of the PR and between 37c173f and cc64b11.

📒 Files selected for processing (8)
  • docs/cli.md
  • electron/cli/args.test.ts
  • electron/cli/args.ts
  • electron/cli/cliMain.ts
  • electron/preload.ts
  • src/cli/CliExportRunner.tsx
  • src/cli/CliRecordRunner.tsx
  • src/lib/exporter/voiceoverMix.ts
🚧 Files skipped from review as they are similar to previous changes (8)
  • electron/cli/args.ts
  • electron/cli/args.test.ts
  • docs/cli.md
  • electron/preload.ts
  • electron/cli/cliMain.ts
  • src/cli/CliExportRunner.tsx
  • src/cli/CliRecordRunner.tsx
  • src/lib/exporter/voiceoverMix.ts

📝 Walkthrough

Walkthrough

Adds a headless Electron CLI for recording, inspecting projects, and exporting to GIF or MP4 with NDJSON output and optional voiceover mixing. It includes argument parsing, hidden renderer runners, IPC contracts, recording controls, export integration, tests, and documentation.

Changes

Headless CLI

Layer / File(s) Summary
CLI contracts and argument parsing
src/lib/cliContracts.ts, electron/cli/args.ts, electron/cli/args.test.ts
Defines typed CLI requests and results, parses command options, validates cross-option constraints, and tests export, record, info, and help behavior.
Electron CLI lifecycle and IPC
electron/main.ts, electron/cli/cliMain.ts, electron/preload.ts, electron/electron-env.d.ts, electron/windows.ts, package.json
Adds CLI startup branching, hidden runner windows, stdout/stderr protocols, IPC handlers, signal handling, project inspection, and the CLI npm script.
Headless recording workflow
src/cli/CliRecordRunner.tsx, src/hooks/useScreenRecorder.ts
Selects capture sources and microphones, starts recording without a countdown, handles duration and stop signals, and returns recording artifacts or project data.
Headless export and voiceover processing
src/App.tsx, src/cli/CliExportRunner.tsx, src/lib/exporter/voiceoverMix.ts
Runs GIF and MP4 exports in a hidden renderer, applies project media and settings, optionally remuxes mixed voiceover audio, and writes output files.
CLI usage documentation
README.md, docs/cli.md
Documents CLI commands, options, NDJSON events, exit codes, platform behavior, scripted workflows, and architecture.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant CLI
  participant ElectronMain
  participant HiddenRunner
  participant RecorderOrExporter
  participant FileSystem
  CLI->>ElectronMain: invoke record or export
  ElectronMain->>HiddenRunner: load cli-record or cli-export window
  HiddenRunner->>RecorderOrExporter: request and execute workflow
  RecorderOrExporter-->>ElectronMain: progress, logs, and completion
  ElectronMain->>FileSystem: write project or export output
  ElectronMain-->>CLI: NDJSON events and exit status
Loading

Possibly related PRs

Suggested reviewers: etiennelescot

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description is detailed, but it omits several required template sections like Related issue, Type of change, Release impact, and Desktop impact. Add the missing template sections, especially Related issue, Type of change, Release impact, Desktop impact, and Screenshots/video, even if marked N/A.
Docstring Coverage ⚠️ Warning Docstring coverage is 22.58% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title is concise and accurately summarizes the main headless CLI record/export/info change.
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

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: 8

🧹 Nitpick comments (7)
src/cli/CliExportRunner.tsx (1)

42-57: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

No timeout on metadata probe — a stalled load hangs the CLI forever.

onerror covers decode failures, but a media element that never fires either event (unreachable/locked file, stalled reader) leaves the promise pending, and the CLI has no export-side deadline to fall back on. A bounded wait keeps openscreen export from hanging headlessly.

♻️ Proposed fix
 function probeVideoDimensions(url: string): Promise<{ width: number; height: number }> {
 	return new Promise((resolve, reject) => {
 		const video = document.createElement("video");
 		video.preload = "metadata";
 		video.muted = true;
+		const timer = setTimeout(() => {
+			video.removeAttribute("src");
+			video.load();
+			reject(new Error(`Timed out reading video metadata: ${url}`));
+		}, 30_000);
 		video.onloadedmetadata = () => {
+			clearTimeout(timer);
 			const width = video.videoWidth;
 			const height = video.videoHeight;
 			video.removeAttribute("src");
 			video.load();
 			resolve({ width, height });
 		};
-		video.onerror = () => reject(new Error(`Failed to load video metadata: ${url}`));
+		video.onerror = () => {
+			clearTimeout(timer);
+			reject(new Error(`Failed to load video metadata: ${url}`));
+		};
 		video.src = url;
 	});
 }
🤖 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/cli/CliExportRunner.tsx` around lines 42 - 57, Update
probeVideoDimensions to enforce a bounded timeout for video metadata loading,
rejecting the promise when neither onloadedmetadata nor onerror fires before the
deadline. Ensure successful metadata loads and errors clear the timeout and
perform the existing video cleanup so the CLI cannot remain pending
indefinitely.
electron/cli/args.test.ts (1)

4-8: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Path assertions are POSIX-only.

resolvePath delegates to path.resolve, so on a Windows runner parse(["export","demo.openscreen"]) yields \work\demo.openscreen and every hard-coded /work/... expectation fails. Build expectations with path.resolve(CWD, …) (or path.join) so the suite is platform-agnostic.

♻️ Suggested approach
+import path from "node:path";
 import { describe, expect, it } from "vitest";
 import { parseCliArgs } from "./args";

 const CWD = "/work";
+const at = (name: string) => path.resolve(CWD, name);

Then use projectPath: at("demo.openscreen") instead of the literal.

Also applies to: 17-20

🤖 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/cli/args.test.ts` around lines 4 - 8, Update path expectations in
the tests around parse and the projectPath assertions to use a platform-aware
helper such as at, backed by path.resolve(CWD, ...) or path.join(CWD, ...),
instead of hard-coded /work/... strings. Ensure all affected expected paths,
including demo.openscreen, match resolvePath on Windows and POSIX systems.
src/lib/exporter/voiceoverMix.ts (1)

105-135: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Cancel the partially started Output if remux fails.

After output.start() fails once adding packets or audioSource.add(mixedAudio) throws, the finally only disposes the input; output remains started and unfinalized, and BufferTarget.buffer will not be the remux output. Track whether output.start() has resolved and await output.cancel() in the catch path before rethrowing.

🤖 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/lib/exporter/voiceoverMix.ts` around lines 105 - 135, The remux cleanup
must cancel a started but unfinalized Output when packet or audio processing
fails. In the surrounding flow that creates output and calls output.start(),
track successful startup, catch subsequent errors, await output.cancel() when
startup completed, then rethrow; retain input.dispose() in finally and avoid
canceling if output.start() never resolved.
electron/preload.ts (1)

309-317: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Type these against the CLI contracts instead of unknown.

electron-env.d.ts already declares CliProgressEvent / CliDoneResult; typing the preload the same way keeps the two sides from drifting.

🤖 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/preload.ts` around lines 309 - 317, Update the cliProgress and
cliDone methods in the preload API to use the existing CliProgressEvent and
CliDoneResult types from electron-env.d.ts instead of unknown, while preserving
their current IPC behavior and cliLog signature.
electron/cli/cliMain.ts (1)

245-252: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

JSON.stringify on a circular arg throws outside safeWrite.

The args.map(...) runs before safeWrite, so any log call carrying a circular object (common with Electron/DOM-ish objects) throws from console.log, escaping into the uncaughtException handler and killing the CLI run.

♻️ Proposed fix
+	const stringify = (a: unknown) => {
+		if (typeof a === "string") return a;
+		try {
+			return JSON.stringify(a);
+		} catch {
+			return String(a);
+		}
+	};
 	for (const level of ["log", "info", "warn", "error", "debug"] as const) {
 		console[level] = (...args: unknown[]) => {
-			safeWrite(
-				process.stderr,
-				`${args.map((a) => (typeof a === "string" ? a : JSON.stringify(a))).join(" ")}\n`,
-			);
+			safeWrite(process.stderr, `${args.map(stringify).join(" ")}\n`);
 		};
 	}
🤖 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/cli/cliMain.ts` around lines 245 - 252, Update the console method
overrides in the level loop so argument serialization cannot throw before
safeWrite, including for circular or otherwise non-serializable objects.
Preserve string arguments and the existing space-separated output, while using a
safe fallback representation for values that JSON.stringify cannot handle.
src/cli/CliRecordRunner.tsx (2)

122-126: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Ref writes during render.

toggleRecordingRef/recordingRef are mutated in the render body; React may discard or replay that work. Move both into an effect.

♻️ Proposed fix
-	const toggleRecordingRef = useRef(toggleRecording);
-	toggleRecordingRef.current = toggleRecording;
-	const recordingRef = useRef(recording);
-	recordingRef.current = recording;
+	const toggleRecordingRef = useRef(toggleRecording);
+	const recordingRef = useRef(recording);
+	useEffect(() => {
+		toggleRecordingRef.current = toggleRecording;
+		recordingRef.current = recording;
+	});
🤖 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/cli/CliRecordRunner.tsx` around lines 122 - 126, Move the current-value
assignments for toggleRecordingRef and recordingRef out of the render body and
into a React effect in the surrounding component. Keep both refs initialized
with their corresponding values and update their .current fields after committed
renders so the stop/finish effects continue using the latest values.

Source: Linters/SAST tools


98-108: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

No test accompanies this runner.

The cohort only adds electron/cli/args.test.ts; the record runner's bootstrap/stop/completion state machine is untested. As per coding guidelines, "Add a test for every new behavior in the same package as the code under test." Happy to draft a test that stubs window.electronAPI and asserts the start/stop/cliDone transitions.

🤖 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/cli/CliRecordRunner.tsx` around lines 98 - 108, Add tests in the same
package for CliRecordRunner’s bootstrap and recording state machine, stubbing
window.electronAPI as needed. Cover recorder start, stop, and cliDone completion
transitions, including the requestReady bootstrap path.

Source: Coding guidelines

🤖 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 `@docs/cli.md`:
- Around line 7-9: Add a language identifier to the fenced code block containing
the record-to-export workflow in docs/cli.md, using text for descriptive content
or bash only if it is intended to be executable.

In `@electron/cli/args.ts`:
- Around line 183-191: Update the --preview-size parsing in the argument switch
to reject zero-valued width or height after matching the dimensions. Preserve
the existing format validation and assignment for positive dimensions, while
throwing the same style of descriptive error when either parsed value is zero.

In `@electron/cli/cliMain.ts`:
- Around line 212-225: Update the JSON and human-readable output branches in the
CLI summary writer to use the existing safeWrite helper instead of direct
process.stdout.write calls. Ensure both writes handle closed-pipe EPIPE errors
before runCli installs its stdout error guards, preserving the current output
content and formatting.
- Around line 283-287: Hoist the finished flag to the surrounding function scope
and remove the inner declaration near the deferred exit logic. In the
app.on("window-all-closed") handler, return immediately when finished is set
before logging the unexpected closure or calling app.exit(1), while preserving
the existing failure behavior for unfinished runs.

In `@src/cli/CliRecordRunner.tsx`:
- Around line 226-234: Update the stop-signal effect around onCliStopRecording
to retain a stop request received during initialization instead of ignoring it.
Use the existing stopRequestedRef, then check it immediately after
startRecordingImmediately() resolves and abort or stop recording so the CLI
always reaches completion; preserve the current stopping behavior when recording
is already active.

In `@src/hooks/useScreenRecorder.ts`:
- Around line 57-58: Update startRecordingImmediately and the underlying
startRecording flow to report startup success or failure instead of always
resolving; return true only after recording is enabled and false from catch or
early-return paths. In CliRecordRunner, inspect that result and call fail(...)
when startup returns false so CLI startup failures terminate rather than waiting
for completion.

In `@src/lib/exporter/voiceoverMix.ts`:
- Around line 53-73: Update the mix path around voiceoverNode and gainNode so
the default mix mode attenuates the original audio below unity before combining
it with the voiceover, preventing both sources from playing at full gain.
Preserve explicitly supplied originalGain values and replace-mode behavior.
- Around line 100-103: Materialize the video data lazily in the voiceover mixing
flow: avoid calling videoBlob.arrayBuffer() before renderMixedAudio when replace
mode does not use it. Update renderMixedAudio or its caller so the ArrayBuffer
is obtained only inside the mix branch, while preserving existing replace and
mix behavior.

---

Nitpick comments:
In `@electron/cli/args.test.ts`:
- Around line 4-8: Update path expectations in the tests around parse and the
projectPath assertions to use a platform-aware helper such as at, backed by
path.resolve(CWD, ...) or path.join(CWD, ...), instead of hard-coded /work/...
strings. Ensure all affected expected paths, including demo.openscreen, match
resolvePath on Windows and POSIX systems.

In `@electron/cli/cliMain.ts`:
- Around line 245-252: Update the console method overrides in the level loop so
argument serialization cannot throw before safeWrite, including for circular or
otherwise non-serializable objects. Preserve string arguments and the existing
space-separated output, while using a safe fallback representation for values
that JSON.stringify cannot handle.

In `@electron/preload.ts`:
- Around line 309-317: Update the cliProgress and cliDone methods in the preload
API to use the existing CliProgressEvent and CliDoneResult types from
electron-env.d.ts instead of unknown, while preserving their current IPC
behavior and cliLog signature.

In `@src/cli/CliExportRunner.tsx`:
- Around line 42-57: Update probeVideoDimensions to enforce a bounded timeout
for video metadata loading, rejecting the promise when neither onloadedmetadata
nor onerror fires before the deadline. Ensure successful metadata loads and
errors clear the timeout and perform the existing video cleanup so the CLI
cannot remain pending indefinitely.

In `@src/cli/CliRecordRunner.tsx`:
- Around line 122-126: Move the current-value assignments for toggleRecordingRef
and recordingRef out of the render body and into a React effect in the
surrounding component. Keep both refs initialized with their corresponding
values and update their .current fields after committed renders so the
stop/finish effects continue using the latest values.
- Around line 98-108: Add tests in the same package for CliRecordRunner’s
bootstrap and recording state machine, stubbing window.electronAPI as needed.
Cover recorder start, stop, and cliDone completion transitions, including the
requestReady bootstrap path.

In `@src/lib/exporter/voiceoverMix.ts`:
- Around line 105-135: The remux cleanup must cancel a started but unfinalized
Output when packet or audio processing fails. In the surrounding flow that
creates output and calls output.start(), track successful startup, catch
subsequent errors, await output.cancel() when startup completed, then rethrow;
retain input.dispose() in finally and avoid canceling if output.start() never
resolved.
🪄 Autofix (Beta)

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: d3dba1c5-2e28-4090-8d2f-2e52c6aea3d2

📥 Commits

Reviewing files that changed from the base of the PR and between f508a21 and 37c173f.

📒 Files selected for processing (16)
  • README.md
  • docs/cli.md
  • electron/cli/args.test.ts
  • electron/cli/args.ts
  • electron/cli/cliMain.ts
  • electron/electron-env.d.ts
  • electron/main.ts
  • electron/preload.ts
  • electron/windows.ts
  • package.json
  • src/App.tsx
  • src/cli/CliExportRunner.tsx
  • src/cli/CliRecordRunner.tsx
  • src/hooks/useScreenRecorder.ts
  • src/lib/cliContracts.ts
  • src/lib/exporter/voiceoverMix.ts

Comment thread docs/cli.md Outdated
Comment thread electron/cli/args.ts
Comment thread electron/cli/cliMain.ts
Comment thread electron/cli/cliMain.ts
Comment thread src/cli/CliRecordRunner.tsx
Comment on lines +57 to +58
/** Starts recording with no countdown overlay. Used by the headless CLI runner. */
startRecordingImmediately: () => Promise<void>;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

startRecordingImmediately never rejects, so CLI start failures hang the run.

startRecording catches everything internally (toast + state reset, Lines 1449-1465). The CLI runner awaits this in a try/catch (src/cli/CliRecordRunner.tsx Lines 190-196) expecting a throw; instead it resolves, recording stays false, recordingStartedAtRef stays null, the completion effect early-returns, and cli-done is never sent — the CLI blocks until the user kills it.

Return a success signal (or rethrow) from this entrypoint and have the runner fail on it.

🔧 Sketch
-	/** Starts recording with no countdown overlay. Used by the headless CLI runner. */
-	startRecordingImmediately: () => Promise<void>;
+	/** Starts recording with no countdown overlay. Resolves false when start failed. Used by the headless CLI runner. */
+	startRecordingImmediately: () => Promise<boolean>;

Have startRecording return false from its catch/early-return paths and true once setRecording(true) has run, then in CliRecordRunner treat false as a failure and call fail(...).

Also applies to: 1733-1733

🤖 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 57 - 58, Update
startRecordingImmediately and the underlying startRecording flow to report
startup success or failure instead of always resolving; return true only after
recording is enabled and false from catch or early-return paths. In
CliRecordRunner, inspect that result and call fail(...) when startup returns
false so CLI startup failures terminate rather than waiting for completion.

Comment thread src/lib/exporter/voiceoverMix.ts
Comment thread src/lib/exporter/voiceoverMix.ts
Actionable findings:
- voiceoverMix: duck the original bed to 0.4 gain by default in "mix" mode
  so the unity-gain sum cannot hard-clip; skip buffering the whole MP4 in
  "replace" mode; cancel the mediabunny Output on remux failure
- CliRecordRunner: a stop signal arriving before capture starts is now
  remembered and applied the moment recording begins; add a 30s watchdog
  so a silently failed start no longer hangs the CLI; move ref sync out
  of render into an effect
- cliMain: guard window-all-closed with the finished flag so teardown
  after a successful run can't report a false failure; route info-command
  output through safeWrite; harden console rerouting against circular args
- args: reject zero dimensions in --preview-size
- CliExportRunner: 30s timeout on the video metadata probe
- preload: type the CLI bridge methods against cliContracts
- args.test: platform-agnostic path assertions (win32-safe)
- docs: fence language, document mix-mode ducking

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019ceTJjoPJYa5BdmXZdDc5J
@PeterTakahashi

PeterTakahashi commented Jul 27, 2026

Copy link
Copy Markdown
Author

Addressed the CodeRabbit review in cc64b11 — all 8 actionable findings plus 6 of the nitpicks:

Actionable

  • voiceoverMix clipping: mix mode now ducks the original bed to 0.4 gain by default (voiceover stays at unity), so the sum can't hard-clip; documented in docs/cli.md. Verified with volumedetect: max -2.5 dB on a mixed export.
  • voiceoverMix buffering: the full-MP4 arrayBuffer() copy is now only taken in mix mode; replace skips it.
  • stop-before-start race: an early SIGINT/stdin stop is remembered in stopRequestedRef and applied the moment recording flips on.
  • silent start failure: since useScreenRecorder reports start failures via toast/console rather than rejecting, the record runner arms a 30s watchdog after startRecordingImmediately() and fails the run with a pointer to stderr (where renderer console errors are already forwarded). I kept the hook's semantics unchanged to avoid altering GUI behavior — happy to make it reject instead if you'd prefer that contract.
  • false failure on teardown: window-all-closed now checks the finished flag before reporting.
  • info-command writes now go through safeWrite; console rerouting stringification is guarded against circular args.
  • --preview-size 0x0 rejected.
  • docs fence language added.

Nitpicks taken: metadata-probe 30s timeout, Output.cancel() on remux failure, typed preload CLI methods, win32-safe test path assertions, ref sync moved out of render.

Re-verified after the changes: full unit suite (439 tests), repo lint, tsc, and E2E smoke on macOS (record 3s + export with --audio-mode mix).

🤖 Generated with Claude Code

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