@@ -683,10 +683,36 @@ class TimelineEditor {
683683 }
684684 } , { passive : false } ) ;
685685
686+ // Track pointer presence so a focus-independent Delete handler knows the
687+ // user is working inside the timeline (not the graph).
688+ this . _pointerInside = false ;
689+ this . container . addEventListener ( "mouseenter" , ( ) => { this . _pointerInside = true ; } ) ;
690+ this . container . addEventListener ( "mouseleave" , ( ) => { this . _pointerInside = false ; } ) ;
691+
692+ // THE FIX for "Delete wipes the whole node": ComfyUI has a global
693+ // keydown that deletes the selected node. When a clip/lane is selected in
694+ // the timeline, Delete must remove THAT item and never reach ComfyUI. A
695+ // window capture-phase listener runs BEFORE ComfyUI's, and when the
696+ // pointer is inside the widget (or focus is within it) with a selection,
697+ // it deletes the segment and stops the event dead.
698+ this . _onWinKeyDown = ( e ) => {
699+ if ( e . key !== "Delete" && e . key !== "Backspace" ) return ;
700+ const focusInside = this . container . contains ( document . activeElement ) ;
701+ const editing = / ^ ( I N P U T | T E X T A R E A | S E L E C T ) $ / . test ( document . activeElement ?. tagName || "" )
702+ && this . container . contains ( document . activeElement ) ;
703+ if ( editing ) return ; // let text fields handle it
704+ if ( ! ( this . _pointerInside || focusInside ) ) return ;
705+ if ( ! this . selection || this . selection . idx < 0 || ! this . selection . type ) return ;
706+ this . deleteSelected ( ) ;
707+ e . preventDefault ( ) ;
708+ e . stopImmediatePropagation ( ) ; // ComfyUI never sees it → node survives
709+ } ;
710+ window . addEventListener ( "keydown" , this . _onWinKeyDown , true ) ;
711+
686712 // Keyboard (canvas focused)
687713 this . cvs . addEventListener ( "keydown" , ( e ) => {
688714 const sel = this . selection ;
689- if ( e . key === "Delete" || e . key === "Backspace" ) { this . deleteSelected ( ) ; e . preventDefault ( ) ; }
715+ if ( e . key === "Delete" || e . key === "Backspace" ) { this . deleteSelected ( ) ; e . preventDefault ( ) ; e . stopPropagation ( ) ; }
690716 else if ( ( e . key === "c" || e . key === "C" ) && ( e . ctrlKey || e . metaKey ) ) {
691717 if ( sel . type && sel . idx >= 0 ) this . _copySegment ( { segType : sel . type , idx : sel . idx } ) ;
692718 e . preventDefault ( ) ;
@@ -1068,13 +1094,13 @@ class TimelineEditor {
10681094 ( seg . _thumbs || [ ] ) . forEach ( ( im ) => { im . onload = ( ) => this . render ( ) ; } ) ;
10691095 // LTX-style: a SCENE video that carries an audio track also drops a
10701096 // waveform on the AUDIO track (control videos don't). Silent if no audio.
1071- if ( ! toControl ) { try { await this . _autoExtractVideoAudio ( file , filename ) ; } catch ( _ ) { } }
1097+ if ( ! toControl ) { try { await this . _autoExtractVideoAudio ( file , filename , seg . id ) ; } catch ( _ ) { } }
10721098 }
10731099
10741100 /** Decode a video's embedded audio and add it as a waveform on the AUDIO
10751101 * track, referencing the same already-uploaded file (no re-upload). Skips
10761102 * silently when the video has no audio track. Mirrors LTX Director. */
1077- async _autoExtractVideoAudio ( file , videoFilename ) {
1103+ async _autoExtractVideoAudio ( file , videoFilename , sourceVideoId ) {
10781104 // If an audio clip for this exact file already exists, don't duplicate.
10791105 if ( this . audioSegments . some ( a => a . audioFile === videoFilename ) ) return ;
10801106 let peaksInfo = null ;
@@ -1088,6 +1114,9 @@ class TimelineEditor {
10881114 audioDurationFrames : peaksInfo . durFrames ,
10891115 audioFile : videoFilename , fileName : file . name ,
10901116 waveformPeaks : peaksInfo . peaks , fromVideo : true ,
1117+ // Linked to its parent SCENE video: deleting the video deletes this
1118+ // audio too (they're the same source), like LTX Director.
1119+ sourceVideoId : sourceVideoId || null ,
10911120 } ;
10921121 this . audioBuffers . set ( videoFilename , peaksInfo . audio ) ;
10931122 this . audioSegments . push ( seg ) ;
@@ -1126,7 +1155,13 @@ class TimelineEditor {
11261155 if ( idx < 0 ) return ;
11271156 const arr = this . _segArr ( type ) ;
11281157 if ( idx < arr . length ) {
1158+ const removed = arr [ idx ] ;
11291159 arr . splice ( idx , 1 ) ;
1160+ // Deleting a SCENE video also removes its linked auto-extracted audio
1161+ // (same source file), like LTX Director — video + its audio are one.
1162+ if ( removed && removed . type === "video" && removed . id ) {
1163+ this . audioSegments = this . audioSegments . filter ( a => a . sourceVideoId !== removed . id ) ;
1164+ }
11301165 this . selection = { type : null , idx : - 1 } ;
11311166 this . commitChanges ( ) ;
11321167 if ( this . _isV2 ( type ) ) this . _updateV2Props ( ) ; else this . _updatePropsPanel ( ) ;
@@ -2320,6 +2355,7 @@ class TimelineEditor {
23202355 destroy ( ) {
23212356 this . _stopAudio ( ) ;
23222357 this . _dismissAddMenu ( ) ;
2358+ try { if ( this . _onWinKeyDown ) window . removeEventListener ( "keydown" , this . _onWinKeyDown , true ) ; } catch { }
23232359 try { this . _resizeObs ?. disconnect ( ) ; } catch { }
23242360 if ( this . audioCtx ) try { this . audioCtx . close ( ) ; } catch { }
23252361 this . audioCtx = null ;
0 commit comments