@@ -41,6 +41,9 @@ define(function (require, exports, module) {
4141 StringUtils = require ( "utils/StringUtils" ) ,
4242 marked = require ( "thirdparty/marked.min" ) ;
4343
44+ // Capture at module load time — window.KernalModeTrust is deleted before extensions load
45+ const _KernalModeTrust = window . KernalModeTrust ;
46+
4447 let _nodeConnector = null ;
4548 let _isStreaming = false ;
4649 let _queuedMessage = null ; // text queued by user while AI is streaming
@@ -86,6 +89,7 @@ define(function (require, exports, module) {
8689
8790 // DOM references
8891 let $panel , $messages , $status , $statusText , $textarea , $sendBtn , $stopBtn , $imagePreview ;
92+ let $aiTabContainer = null ;
8993
9094 // Live DOM query for $messages — the cached $messages reference can become stale
9195 // after SidebarTabs reparents the panel. Use this for any deferred operations
@@ -148,6 +152,7 @@ define(function (require, exports, module) {
148152 '<div class="ai-unavailable-message">' +
149153 Strings . AI_CHAT_DESKTOP_ONLY +
150154 '</div>' +
155+ '<button class="ai-upsell-btn ai-download-btn">' + Strings . AI_CHAT_DOWNLOAD_BTN + '</button>' +
151156 '</div>' +
152157 '</div>' ;
153158
@@ -185,16 +190,138 @@ define(function (require, exports, module) {
185190 }
186191 } ) ;
187192
188- // Check availability and render appropriate UI
189- _checkAvailability ( ) ;
193+ // Create container once, add to AI tab
194+ $aiTabContainer = $ ( '<div class="ai-tab-container"></div>' ) ;
195+ SidebarTabs . addToTab ( "ai" , $aiTabContainer ) ;
196+
197+ // Listen for entitlement changes to refresh UI on login/logout
198+ const EntitlementsManager = _KernalModeTrust && _KernalModeTrust . EntitlementsManager ;
199+ if ( EntitlementsManager ) {
200+ EntitlementsManager . on ( EntitlementsManager . EVENT_ENTITLEMENTS_CHANGED , _checkEntitlementAndInit ) ;
201+ }
202+
203+ // Check entitlements and render appropriate UI
204+ _checkEntitlementAndInit ( ) ;
190205 }
191206
192207 /**
193208 * Show placeholder UI for non-native (browser) builds.
194209 */
195210 function initPlaceholder ( ) {
211+ $aiTabContainer = $ ( '<div class="ai-tab-container"></div>' ) ;
212+ SidebarTabs . addToTab ( "ai" , $aiTabContainer ) ;
196213 const $placeholder = $ ( PLACEHOLDER_HTML ) ;
197- SidebarTabs . addToTab ( "ai" , $placeholder ) ;
214+ $placeholder . find ( ".ai-download-btn" ) . on ( "click" , function ( ) {
215+ window . open ( "https://phcode.io" , "_blank" ) ;
216+ } ) ;
217+ $aiTabContainer . empty ( ) . append ( $placeholder ) ;
218+ }
219+
220+ /**
221+ * Remove any existing panel content from the AI tab container.
222+ */
223+ function _removeCurrentPanel ( ) {
224+ if ( $aiTabContainer ) {
225+ $aiTabContainer . empty ( ) ;
226+ }
227+ // Clear cached DOM references so stale jQuery objects aren't reused
228+ $panel = null ;
229+ $messages = null ;
230+ $status = null ;
231+ $statusText = null ;
232+ $textarea = null ;
233+ $sendBtn = null ;
234+ $stopBtn = null ;
235+ $imagePreview = null ;
236+ }
237+
238+ /**
239+ * Gate AI UI behind entitlement checks. Shows login screen if not logged in,
240+ * upsell screen if no AI plan, or proceeds to CLI availability check if entitled.
241+ */
242+ function _checkEntitlementAndInit ( ) {
243+ _removeCurrentPanel ( ) ;
244+ const EntitlementsManager = _KernalModeTrust && _KernalModeTrust . EntitlementsManager ;
245+ if ( ! EntitlementsManager ) {
246+ // No entitlement system (test env or dev) — skip straight to CLI check
247+ _checkAvailability ( ) ;
248+ return ;
249+ }
250+ EntitlementsManager . getAIEntitlement ( ) . then ( function ( entitlement ) {
251+ if ( entitlement . aiDisabledByAdmin ) {
252+ _renderAdminDisabledUI ( ) ;
253+ } else if ( entitlement . activated ) {
254+ _checkAvailability ( ) ;
255+ } else if ( entitlement . needsLogin ) {
256+ _renderLoginUI ( ) ;
257+ } else {
258+ _renderUpsellUI ( entitlement ) ;
259+ }
260+ } ) . catch ( function ( ) {
261+ _checkAvailability ( ) ; // fallback on error
262+ } ) ;
263+ }
264+
265+ /**
266+ * Render the login prompt UI (user not signed in).
267+ */
268+ function _renderLoginUI ( ) {
269+ const html =
270+ '<div class="ai-chat-panel">' +
271+ '<div class="ai-unavailable">' +
272+ '<div class="ai-unavailable-icon"><i class="fa-solid fa-wand-magic-sparkles"></i></div>' +
273+ '<div class="ai-unavailable-title">' + Strings . AI_CHAT_LOGIN_TITLE + '</div>' +
274+ '<div class="ai-unavailable-message">' +
275+ Strings . AI_CHAT_LOGIN_MESSAGE +
276+ '</div>' +
277+ '<button class="ai-retry-btn ai-login-btn">' + Strings . AI_CHAT_LOGIN_BTN + '</button>' +
278+ '</div>' +
279+ '</div>' ;
280+ const $login = $ ( html ) ;
281+ $login . find ( ".ai-login-btn" ) . on ( "click" , function ( ) {
282+ _KernalModeTrust . EntitlementsManager . loginToAccount ( ) ;
283+ } ) ;
284+ $aiTabContainer . empty ( ) . append ( $login ) ;
285+ }
286+
287+ /**
288+ * Render the upsell UI (user logged in but no AI plan).
289+ */
290+ function _renderUpsellUI ( entitlement ) {
291+ const html =
292+ '<div class="ai-chat-panel">' +
293+ '<div class="ai-unavailable">' +
294+ '<div class="ai-unavailable-icon"><i class="fa-solid fa-wand-magic-sparkles"></i></div>' +
295+ '<div class="ai-unavailable-title">' + Strings . AI_CHAT_UPSELL_TITLE + '</div>' +
296+ '<div class="ai-unavailable-message">' +
297+ Strings . AI_CHAT_UPSELL_MESSAGE +
298+ '</div>' +
299+ '<button class="ai-upsell-btn">' + Strings . AI_CHAT_UPSELL_BTN + '</button>' +
300+ '</div>' +
301+ '</div>' ;
302+ const $upsell = $ ( html ) ;
303+ $upsell . find ( ".ai-upsell-btn" ) . on ( "click" , function ( ) {
304+ const url = ( entitlement && entitlement . buyURL ) || brackets . config . purchase_url ;
305+ Phoenix . app . openURLInDefaultBrowser ( url ) ;
306+ } ) ;
307+ $aiTabContainer . empty ( ) . append ( $upsell ) ;
308+ }
309+
310+ /**
311+ * Render the admin-disabled UI (AI turned off by system administrator).
312+ */
313+ function _renderAdminDisabledUI ( ) {
314+ const html =
315+ '<div class="ai-chat-panel">' +
316+ '<div class="ai-unavailable">' +
317+ '<div class="ai-unavailable-icon"><i class="fa-solid fa-wand-magic-sparkles"></i></div>' +
318+ '<div class="ai-unavailable-title">' + Strings . AI_CHAT_ADMIN_DISABLED_TITLE + '</div>' +
319+ '<div class="ai-unavailable-message">' +
320+ Strings . AI_CHAT_ADMIN_DISABLED_MESSAGE +
321+ '</div>' +
322+ '</div>' +
323+ '</div>' ;
324+ $aiTabContainer . empty ( ) . append ( $ ( html ) ) ;
198325 }
199326
200327 /**
@@ -419,7 +546,7 @@ define(function (require, exports, module) {
419546 _newSession ( ) ;
420547 } ) ;
421548
422- SidebarTabs . addToTab ( "ai" , $panel ) ;
549+ $aiTabContainer . empty ( ) . append ( $panel ) ;
423550 }
424551
425552 /**
@@ -428,10 +555,9 @@ define(function (require, exports, module) {
428555 function _renderUnavailableUI ( error ) {
429556 const $unavailable = $ ( UNAVAILABLE_HTML ) ;
430557 $unavailable . find ( ".ai-retry-btn" ) . on ( "click" , function ( ) {
431- $unavailable . remove ( ) ;
432558 _checkAvailability ( ) ;
433559 } ) ;
434- SidebarTabs . addToTab ( "ai" , $unavailable ) ;
560+ $aiTabContainer . empty ( ) . append ( $unavailable ) ;
435561 }
436562
437563 // --- Context bar chip management ---
0 commit comments