@@ -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 ,
@@ -1966,15 +1968,17 @@ export function registerIpcHandlers(
19661968) {
19671969 /** The HUD is recording a take, the user's or an MCP client's. Set by `setAppRecording` only. */
19681970 let appRecording = false ;
1971+ const recordingCompletion = new RecordingCompletion ( ) ;
19691972 /**
19701973 * Every start and stop of a take goes through here: the browser path's `set-recording-state`
19711974 * and the native paths, which start and stop in this process. The MCP recorder waits on it
19721975 * for a client's take to start, and the tray shows it. A native path that called
19731976 * `onRecordingStateChange` itself left the MCP recorder waiting on a take already running.
19741977 */
1975- const setAppRecording = ( recording : boolean , sourceName : string ) => {
1978+ const setAppRecording = ( recording : boolean , sourceName : string , recordingId ?: number ) => {
1979+ if ( recording && recordingId !== undefined ) recordingCompletion . started ( recordingId ) ;
19761980 appRecording = recording ;
1977- mcpAppRecorder . recordingChanged ( recording ) ;
1981+ mcpAppRecorder . recordingChanged ( recording , recordingId ) ;
19781982 onRecordingStateChange ?.( recording , sourceName ) ;
19791983 } ;
19801984 const appSettings = new AppSettingsStore ( app . getPath ( "userData" ) ) ;
@@ -2760,7 +2764,7 @@ export function registerIpcHandlers(
27602764 // "Screen" }`, so the tray confidently displayed the name of a
27612765 // window the capture had never been told about.
27622766 linuxNativeCaptureSourceLabel = linuxSourceLabel ( session . grantedSourceKind ) ;
2763- setAppRecording ( true , linuxNativeCaptureSourceLabel ) ;
2767+ setAppRecording ( true , linuxNativeCaptureSourceLabel , recordingId ) ;
27642768
27652769 return { success : true , recordingId, path : outputPath } ;
27662770 } catch ( error ) {
@@ -3056,7 +3060,7 @@ export function registerIpcHandlers(
30563060 } ) ;
30573061
30583062 const source = selectedSource || { name : "Screen" } ;
3059- setAppRecording ( true , source . name ) ;
3063+ setAppRecording ( true , source . name , recordingId ) ;
30603064
30613065 // Reported at start, not at stop: the helper decides the camera is a
30623066 // lost cause during its own init — before it announces "Recording
@@ -3318,7 +3322,7 @@ export function registerIpcHandlers(
33183322 : 0 ;
33193323
33203324 const source = selectedSource || { name : "Screen" } ;
3321- setAppRecording ( true , source . name ) ;
3325+ setAppRecording ( true , source . name , recordingId ) ;
33223326
33233327 return {
33243328 success : true ,
@@ -4113,16 +4117,15 @@ export function registerIpcHandlers(
41134117 ipcMain . handle (
41144118 "set-recording-state" ,
41154119 async ( _ , recording : boolean , recordingId ?: number , cursorCaptureMode ?: CursorCaptureMode ) => {
4120+ const source = selectedSource || { name : "Screen" } ;
4121+ setAppRecording ( recording , source . name , recordingId ) ;
41164122 const normalizedCursorCaptureMode =
41174123 normalizeCursorCaptureMode ( cursorCaptureMode ) ?? "editable-overlay" ;
41184124 if ( recording && normalizedCursorCaptureMode === "editable-overlay" ) {
41194125 await startCursorRecording ( recordingId ) ;
41204126 } else {
41214127 await stopCursorRecording ( ) ;
41224128 }
4123-
4124- const source = selectedSource || { name : "Screen" } ;
4125- setAppRecording ( recording , source . name ) ;
41264129 } ,
41274130 ) ;
41284131
@@ -4864,10 +4867,43 @@ export function registerIpcHandlers(
48644867 setCurrentRecordingSessionState ( normalizedSession ) ;
48654868 currentVideoPath = normalizedSession ?. screenVideoPath ?? null ;
48664869 currentProjectPath = null ;
4867- if ( normalizedSession ) mcpAppRecorder . takeSaved ( normalizedSession . screenVideoPath ) ;
4870+ if ( normalizedSession ) {
4871+ mcpAppRecorder . takeSaved ( normalizedSession . screenVideoPath ) ;
4872+ recordingCompletion . saved ( normalizedSession . createdAt , normalizedSession . screenVideoPath ) ;
4873+ }
48684874 return { success : true , session : currentRecordingSession } ;
48694875 } ) ;
48704876
4877+ // A renderer acknowledgement of the actual hand-off, after the duration and
4878+ // automatic zoom saves. An imported/opened project never emits this signal.
4879+ const recordingOutcomeSchema = z . discriminatedUnion ( "status" , [
4880+ z . object ( {
4881+ recordingId : z . number ( ) . finite ( ) ,
4882+ status : z . literal ( "ready" ) ,
4883+ projectId : z . string ( ) ,
4884+ } ) ,
4885+ z . object ( {
4886+ recordingId : z . number ( ) . finite ( ) ,
4887+ status : z . enum ( [ "failed" , "cancelled" ] ) ,
4888+ message : z . string ( ) ,
4889+ } ) ,
4890+ ] ) ;
4891+ ipcMain . handle ( "recording-outcome" , async ( _ , input : unknown ) => {
4892+ const parsed = recordingOutcomeSchema . safeParse ( input ) ;
4893+ if ( ! parsed . success ) return { success : false } ;
4894+ const outcome = parsed . data ;
4895+ if ( outcome . status !== "ready" ) {
4896+ recordingCompletion . failed ( outcome . recordingId , outcome . message ) ;
4897+ mcpAppRecorder . recordingFinished ( outcome . recordingId ) ;
4898+ return { success : true } ;
4899+ }
4900+ await recordingCompletion . projectReady ( outcome . recordingId , outcome . projectId , ( id ) =>
4901+ aiEditionDocuments . getProject ( id ) ,
4902+ ) ;
4903+ mcpAppRecorder . recordingFinished ( outcome . recordingId ) ;
4904+ return { success : true } ;
4905+ } ) ;
4906+
48714907 ipcMain . handle ( "get-current-recording-session" , ( ) => {
48724908 if ( ! currentRecordingSession ) {
48734909 return { success : false } ;
@@ -5151,6 +5187,8 @@ export function registerIpcHandlers(
51515187 host : mcpToolDeps . host ,
51525188 } ) ;
51535189 const mcpMedia = createMediaTools ( {
5190+ recordingActive : ( ) => recordingCompletion . isActive ( ) ,
5191+ waitForRecording : ( timeoutMs , signal ) => recordingCompletion . wait ( timeoutMs , signal ) ,
51545192 transport : "http" ,
51555193 // Recording and exporting change things too: the same switch as edits.
51565194 editsAllowed : mcpToolDeps . editsAllowed ,
0 commit comments