@@ -36,9 +36,11 @@ define(function (require, exports, module) {
3636 FileSystem = require ( "filesystem/FileSystem" ) ,
3737 LiveDevProtocol = require ( "LiveDevelopment/MultiBrowserImpl/protocol/LiveDevProtocol" ) ,
3838 LiveDevMain = require ( "LiveDevelopment/main" ) ,
39+ LivePreviewConstants = require ( "LiveDevelopment/LivePreviewConstants" ) ,
3940 WorkspaceManager = require ( "view/WorkspaceManager" ) ,
4041 SnapshotStore = require ( "core-ai/AISnapshotStore" ) ,
4142 EventDispatcher = require ( "utils/EventDispatcher" ) ,
43+ StringUtils = require ( "utils/StringUtils" ) ,
4244 Strings = require ( "strings" ) ;
4345
4446 // filePath → previous content before edit, for undo/snapshot support
@@ -47,6 +49,134 @@ define(function (require, exports, module) {
4749 // Last screenshot base64 data, for displaying in tool indicators
4850 let _lastScreenshotBase64 = null ;
4951
52+ // Banner / live preview mode state
53+ let _activeExecJsCount = 0 ;
54+ let _savedLivePreviewMode = null ;
55+ let _bannerDismissed = false ;
56+ let _bannerEl = null ;
57+ let _bannerStyleInjected = false ;
58+ let _bannerAutoHideTimer = null ;
59+
60+ /**
61+ * Inject banner CSS once into the document head.
62+ */
63+ function _injectBannerStyles ( ) {
64+ if ( _bannerStyleInjected ) {
65+ return ;
66+ }
67+ _bannerStyleInjected = true ;
68+ const style = document . createElement ( "style" ) ;
69+ style . textContent =
70+ "@keyframes ai-banner-pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.4; } }" +
71+ ".ai-lp-banner {" +
72+ " position: absolute; top: 0; left: 0; right: 0; bottom: 0;" +
73+ " display: flex; align-items: center; justify-content: center; gap: 8px;" +
74+ " background: rgba(24,24,28,0.52);" +
75+ " backdrop-filter: blur(8px); -webkit-backdrop-filter: blur(8px);" +
76+ " z-index: 10; border-radius: 3px;" +
77+ " font-size: 12px; color: #e0e0e0; pointer-events: auto;" +
78+ " transition: opacity 0.3s ease;" +
79+ "}" +
80+ ".ai-lp-banner .ai-lp-banner-icon {" +
81+ " color: #66bb6a; animation: ai-banner-pulse 1.5s ease-in-out infinite;" +
82+ "}" +
83+ ".ai-lp-banner .ai-lp-banner-close {" +
84+ " position: absolute; right: 6px; top: 50%; transform: translateY(-50%);" +
85+ " background: none; border: none; color: #aaa; cursor: pointer;" +
86+ " font-size: 14px; padding: 2px 5px; line-height: 1;" +
87+ "}" +
88+ ".ai-lp-banner .ai-lp-banner-close:hover { color: #fff; }" ;
89+ document . head . appendChild ( style ) ;
90+ }
91+
92+ /**
93+ * Show a banner overlay on the live preview toolbar.
94+ * @param {string } text - Banner message text
95+ */
96+ function _showBanner ( text ) {
97+ if ( _bannerDismissed ) {
98+ return ;
99+ }
100+ _injectBannerStyles ( ) ;
101+ const toolbar = document . getElementById ( "live-preview-plugin-toolbar" ) ;
102+ if ( ! toolbar ) {
103+ return ;
104+ }
105+ // Ensure toolbar can host absolutely positioned children
106+ if ( getComputedStyle ( toolbar ) . position === "static" ) {
107+ toolbar . style . position = "relative" ;
108+ }
109+ if ( _bannerEl && _bannerEl . parentNode ) {
110+ // Update text on existing banner
111+ const textSpan = _bannerEl . querySelector ( ".ai-lp-banner-text" ) ;
112+ if ( textSpan ) {
113+ textSpan . textContent = text ;
114+ }
115+ _bannerEl . style . opacity = "1" ;
116+ return ;
117+ }
118+ const banner = document . createElement ( "div" ) ;
119+ banner . className = "ai-lp-banner" ;
120+ banner . innerHTML =
121+ '<i class="fa-solid fa-eye ai-lp-banner-icon"></i>' +
122+ '<span class="ai-lp-banner-text">' + text . replace ( / < / g, "<" ) + '</span>' +
123+ '<button class="ai-lp-banner-close" title="Dismiss">×</button>' ;
124+ banner . querySelector ( ".ai-lp-banner-close" ) . addEventListener ( "click" , function ( ) {
125+ _bannerDismissed = true ;
126+ _hideBanner ( ) ;
127+ } ) ;
128+ toolbar . appendChild ( banner ) ;
129+ _bannerEl = banner ;
130+ }
131+
132+ /**
133+ * Hide and remove the banner overlay with a fade-out transition.
134+ */
135+ function _hideBanner ( ) {
136+ if ( ! _bannerEl ) {
137+ return ;
138+ }
139+ _bannerEl . style . opacity = "0" ;
140+ const el = _bannerEl ;
141+ setTimeout ( function ( ) {
142+ if ( el . parentNode ) {
143+ el . parentNode . removeChild ( el ) ;
144+ }
145+ } , 300 ) ;
146+ _bannerEl = null ;
147+ }
148+
149+ /**
150+ * Called when an execJsInLivePreview call starts. Increments the active
151+ * count, saves mode and shows banner on first call.
152+ */
153+ function _onExecJsStart ( ) {
154+ _activeExecJsCount ++ ;
155+ if ( _activeExecJsCount === 1 ) {
156+ _savedLivePreviewMode = LiveDevMain . getCurrentMode ( ) ;
157+ if ( _savedLivePreviewMode !== LivePreviewConstants . LIVE_PREVIEW_MODE ) {
158+ LiveDevMain . setMode ( LivePreviewConstants . LIVE_PREVIEW_MODE ) ;
159+ }
160+ _bannerDismissed = false ;
161+ _showBanner ( Strings . AI_LIVE_PREVIEW_BANNER_TEXT ) ;
162+ }
163+ }
164+
165+ /**
166+ * Called when an execJsInLivePreview call finishes. Decrements the count
167+ * and restores mode / hides banner when all calls are done.
168+ */
169+ function _onExecJsDone ( ) {
170+ _activeExecJsCount = Math . max ( 0 , _activeExecJsCount - 1 ) ;
171+ if ( _activeExecJsCount === 0 ) {
172+ if ( _savedLivePreviewMode && _savedLivePreviewMode !== LivePreviewConstants . LIVE_PREVIEW_MODE ) {
173+ LiveDevMain . setMode ( _savedLivePreviewMode ) ;
174+ }
175+ _savedLivePreviewMode = null ;
176+ _hideBanner ( ) ;
177+ }
178+ }
179+
50180 // --- Editor state ---
51181
52182 /**
@@ -354,13 +484,16 @@ define(function (require, exports, module) {
354484 */
355485 function execJsInLivePreview ( params ) {
356486 const deferred = new $ . Deferred ( ) ;
487+ _onExecJsStart ( ) ;
357488
358489 function _evaluate ( ) {
359490 LiveDevProtocol . evaluate ( params . code )
360491 . done ( function ( evalResult ) {
492+ _onExecJsDone ( ) ;
361493 deferred . resolve ( { result : JSON . stringify ( evalResult ) } ) ;
362494 } )
363495 . fail ( function ( err ) {
496+ _onExecJsDone ( ) ;
364497 deferred . resolve ( { error : ( err && err . message ) || String ( err ) || "evaluate() failed" } ) ;
365498 } ) ;
366499 }
@@ -397,6 +530,7 @@ define(function (require, exports, module) {
397530 const timeoutTimer = setTimeout ( function ( ) {
398531 if ( settled ) { return ; }
399532 cleanup ( ) ;
533+ _onExecJsDone ( ) ;
400534 deferred . resolve ( { error : "Timed out waiting for live preview connection (30s)" } ) ;
401535 } , TIMEOUT ) ;
402536
@@ -495,6 +629,62 @@ define(function (require, exports, module) {
495629 return deferred . promise ( ) ;
496630 }
497631
632+ // --- Live preview resize ---
633+
634+ /**
635+ * Resize the live preview panel to a specific width in pixels.
636+ * @param {Object } params - { width: number }
637+ * @return {$.Promise } resolves with { actualWidth } or { error }
638+ */
639+ function resizeLivePreview ( params ) {
640+ const deferred = new $ . Deferred ( ) ;
641+
642+ if ( ! params . width ) {
643+ deferred . resolve ( { error : "Provide 'width' as a number in pixels" } ) ;
644+ return deferred . promise ( ) ;
645+ }
646+
647+ const targetWidth = params . width ;
648+ const label = targetWidth + "px" ;
649+
650+ // Ensure live preview panel is open
651+ const panel = WorkspaceManager . getPanelForID ( "live-preview-panel" ) ;
652+ if ( ! panel || ! panel . isVisible ( ) ) {
653+ CommandManager . execute ( "file.liveFilePreview" ) ;
654+ }
655+
656+ // Give the panel a moment to open, then resize
657+ setTimeout ( function ( ) {
658+ WorkspaceManager . setPluginPanelWidth ( targetWidth ) ;
659+
660+ // Read back actual width from the toolbar
661+ const toolbar = document . getElementById ( "live-preview-plugin-toolbar" ) ;
662+ const actualWidth = toolbar ? toolbar . offsetWidth : targetWidth ;
663+
664+ // Show brief banner
665+ _bannerDismissed = false ;
666+ _showBanner ( StringUtils . format ( Strings . AI_LIVE_PREVIEW_BANNER_RESIZE , label ) ) ;
667+ if ( _bannerAutoHideTimer ) {
668+ clearTimeout ( _bannerAutoHideTimer ) ;
669+ }
670+ _bannerAutoHideTimer = setTimeout ( function ( ) {
671+ _hideBanner ( ) ;
672+ _bannerAutoHideTimer = null ;
673+ } , 3000 ) ;
674+
675+ const result = { actualWidth : actualWidth } ;
676+ if ( actualWidth !== targetWidth ) {
677+ result . clamped = true ;
678+ result . note = "Requested " + targetWidth + "px but the editor window can only " +
679+ "accommodate " + actualWidth + "px. The user needs to increase the editor " +
680+ "window size to allow a wider preview." ;
681+ }
682+ deferred . resolve ( result ) ;
683+ } , 100 ) ;
684+
685+ return deferred . promise ( ) ;
686+ }
687+
498688 exports . getEditorState = getEditorState ;
499689 exports . takeScreenshot = takeScreenshot ;
500690 exports . getFileContent = getFileContent ;
@@ -504,6 +694,7 @@ define(function (require, exports, module) {
504694 exports . getLastScreenshot = getLastScreenshot ;
505695 exports . execJsInLivePreview = execJsInLivePreview ;
506696 exports . controlEditor = controlEditor ;
697+ exports . resizeLivePreview = resizeLivePreview ;
507698
508699 EventDispatcher . makeEventDispatcher ( exports ) ;
509700} ) ;
0 commit comments