@@ -18,6 +18,7 @@ import {
1818 shell ,
1919 systemPreferences ,
2020} from "electron" ;
21+ import { z } from "zod" ;
2122import { DEFAULT_WEBCAM_QUALITY , type WebcamQualityId } from "../../src/hooks/webcamCaptureTarget" ;
2223import {
2324 type AxcutDocument ,
@@ -88,6 +89,7 @@ import { McpController } from "../mcp/mcp-controller";
8889import { McpSettingsStore } from "../mcp/mcp-settings-store" ;
8990import { createMediaTools , type MediaToolDeps , type RecordRequest } from "../mcp/media-tools" ;
9091import { createToolRunner , type McpToolDeps } from "../mcp/openscreen-mcp-server" ;
92+ import { RecordingCompletion } from "../mcp/recording-completion" ;
9193import { type AudioPeaksResult , getAudioPeaks } from "../media/audioPeaks" ;
9294import {
9395 readCursorRecordingFile as readCursorRecordingFileFrom ,
@@ -1985,15 +1987,17 @@ export function registerIpcHandlers(
19851987) {
19861988 /** The HUD is recording a take, the user's or an MCP client's. Set by `setAppRecording` only. */
19871989 let appRecording = false ;
1990+ const recordingCompletion = new RecordingCompletion ( ) ;
19881991 /**
19891992 * Every start and stop of a take goes through here: the browser path's `set-recording-state`
19901993 * and the native paths, which start and stop in this process. The MCP recorder waits on it
19911994 * for a client's take to start, and the tray shows it. A native path that called
19921995 * `onRecordingStateChange` itself left the MCP recorder waiting on a take already running.
19931996 */
1994- const setAppRecording = ( recording : boolean , sourceName : string ) => {
1997+ const setAppRecording = ( recording : boolean , sourceName : string , recordingId ?: number ) => {
1998+ if ( recording && recordingId !== undefined ) recordingCompletion . started ( recordingId ) ;
19951999 appRecording = recording ;
1996- mcpAppRecorder . recordingChanged ( recording ) ;
2000+ mcpAppRecorder . recordingChanged ( recording , recordingId ) ;
19972001 onRecordingStateChange ?.( recording , sourceName ) ;
19982002 } ;
19992003 const appSettings = new AppSettingsStore ( app . getPath ( "userData" ) ) ;
@@ -2782,7 +2786,7 @@ export function registerIpcHandlers(
27822786 // "Screen" }`, so the tray confidently displayed the name of a
27832787 // window the capture had never been told about.
27842788 linuxNativeCaptureSourceLabel = linuxSourceLabel ( session . grantedSourceKind ) ;
2785- setAppRecording ( true , linuxNativeCaptureSourceLabel ) ;
2789+ setAppRecording ( true , linuxNativeCaptureSourceLabel , recordingId ) ;
27862790
27872791 return { success : true , recordingId, path : outputPath } ;
27882792 } catch ( error ) {
@@ -3078,7 +3082,7 @@ export function registerIpcHandlers(
30783082 } ) ;
30793083
30803084 const source = selectedSource || { name : "Screen" } ;
3081- setAppRecording ( true , source . name ) ;
3085+ setAppRecording ( true , source . name , recordingId ) ;
30823086
30833087 // Reported at start, not at stop: the helper decides the camera is a
30843088 // lost cause during its own init — before it announces "Recording
@@ -3350,7 +3354,7 @@ export function registerIpcHandlers(
33503354 : 0 ;
33513355
33523356 const source = selectedSource || { name : "Screen" } ;
3353- setAppRecording ( true , source . name ) ;
3357+ setAppRecording ( true , source . name , recordingId ) ;
33543358
33553359 return {
33563360 success : true ,
@@ -4145,16 +4149,15 @@ export function registerIpcHandlers(
41454149 ipcMain . handle (
41464150 "set-recording-state" ,
41474151 async ( _ , recording : boolean , recordingId ?: number , cursorCaptureMode ?: CursorCaptureMode ) => {
4152+ const source = selectedSource || { name : "Screen" } ;
4153+ setAppRecording ( recording , source . name , recordingId ) ;
41484154 const normalizedCursorCaptureMode =
41494155 normalizeCursorCaptureMode ( cursorCaptureMode ) ?? "editable-overlay" ;
41504156 if ( recording && normalizedCursorCaptureMode === "editable-overlay" ) {
41514157 await startCursorRecording ( recordingId ) ;
41524158 } else {
41534159 await stopCursorRecording ( ) ;
41544160 }
4155-
4156- const source = selectedSource || { name : "Screen" } ;
4157- setAppRecording ( recording , source . name ) ;
41584161 } ,
41594162 ) ;
41604163
@@ -4896,10 +4899,43 @@ export function registerIpcHandlers(
48964899 setCurrentRecordingSessionState ( normalizedSession ) ;
48974900 currentVideoPath = normalizedSession ?. screenVideoPath ?? null ;
48984901 currentProjectPath = null ;
4899- if ( normalizedSession ) mcpAppRecorder . takeSaved ( normalizedSession . screenVideoPath ) ;
4902+ if ( normalizedSession ) {
4903+ mcpAppRecorder . takeSaved ( normalizedSession . screenVideoPath ) ;
4904+ recordingCompletion . saved ( normalizedSession . createdAt , normalizedSession . screenVideoPath ) ;
4905+ }
49004906 return { success : true , session : currentRecordingSession } ;
49014907 } ) ;
49024908
4909+ // A renderer acknowledgement of the actual hand-off, after the duration and
4910+ // automatic zoom saves. An imported/opened project never emits this signal.
4911+ const recordingOutcomeSchema = z . discriminatedUnion ( "status" , [
4912+ z . object ( {
4913+ recordingId : z . number ( ) . finite ( ) ,
4914+ status : z . literal ( "ready" ) ,
4915+ projectId : z . string ( ) ,
4916+ } ) ,
4917+ z . object ( {
4918+ recordingId : z . number ( ) . finite ( ) ,
4919+ status : z . enum ( [ "failed" , "cancelled" ] ) ,
4920+ message : z . string ( ) ,
4921+ } ) ,
4922+ ] ) ;
4923+ ipcMain . handle ( "recording-outcome" , async ( _ , input : unknown ) => {
4924+ const parsed = recordingOutcomeSchema . safeParse ( input ) ;
4925+ if ( ! parsed . success ) return { success : false } ;
4926+ const outcome = parsed . data ;
4927+ if ( outcome . status !== "ready" ) {
4928+ recordingCompletion . failed ( outcome . recordingId , outcome . message ) ;
4929+ mcpAppRecorder . recordingFinished ( outcome . recordingId ) ;
4930+ return { success : true } ;
4931+ }
4932+ await recordingCompletion . projectReady ( outcome . recordingId , outcome . projectId , ( id ) =>
4933+ aiEditionDocuments . getProject ( id ) ,
4934+ ) ;
4935+ mcpAppRecorder . recordingFinished ( outcome . recordingId ) ;
4936+ return { success : true } ;
4937+ } ) ;
4938+
49034939 ipcMain . handle ( "get-current-recording-session" , ( ) => {
49044940 if ( ! currentRecordingSession ) {
49054941 return { success : false } ;
@@ -5183,6 +5219,8 @@ export function registerIpcHandlers(
51835219 host : mcpToolDeps . host ,
51845220 } ) ;
51855221 const mcpMedia = createMediaTools ( {
5222+ recordingActive : ( ) => recordingCompletion . isActive ( ) ,
5223+ waitForRecording : ( timeoutMs , signal ) => recordingCompletion . wait ( timeoutMs , signal ) ,
51865224 transport : "http" ,
51875225 // Recording and exporting change things too: the same switch as edits.
51885226 editsAllowed : mcpToolDeps . editsAllowed ,
0 commit comments