AIWindow.sys.mjs (8473B)
1 /** 2 * This Source Code Form is subject to the terms of the Mozilla Public 3 * License, v. 2.0. If a copy of the MPL was not distributed with this 4 * file, You can obtain one at http://mozilla.org/MPL/2.0/. 5 */ 6 import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs"; 7 8 export const AIWINDOW_URL = "chrome://browser/content/aiwindow/aiWindow.html"; 9 const AIWINDOW_URI = Services.io.newURI(AIWINDOW_URL); 10 const FIRSTRUN_URL = "chrome://browser/content/aiwindow/firstrun.html"; 11 const FIRSTRUN_URI = Services.io.newURI(FIRSTRUN_URL); 12 13 const lazy = {}; 14 ChromeUtils.defineESModuleGetters(lazy, { 15 AIWindowMenu: 16 "moz-src:///browser/components/aiwindow/ui/modules/AIWindowMenu.sys.mjs", 17 18 SearchUIUtils: "moz-src:///browser/components/search/SearchUIUtils.sys.mjs", 19 ChatStore: 20 "moz-src:///browser/components/aiwindow/ui/modules/ChatStore.sys.mjs", 21 PanelMultiView: 22 "moz-src:///browser/components/customizableui/PanelMultiView.sys.mjs", 23 PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs", 24 }); 25 26 /** 27 * AI Window Service 28 */ 29 30 export const AIWindow = { 31 _initialized: false, 32 _windowStates: new WeakMap(), 33 _aiWindowMenu: null, 34 35 /** 36 * Handles startup tasks 37 */ 38 39 init(win) { 40 if (!this._windowStates.has(win)) { 41 this._windowStates.set(win, {}); 42 this.initializeAITabsToolbar(win); 43 } 44 45 if (this._initialized) { 46 return; 47 } 48 49 ChromeUtils.defineLazyGetter( 50 AIWindow, 51 "chatStore", 52 () => new lazy.ChatStore() 53 ); 54 this._initialized = true; 55 }, 56 57 _onAIWindowEnabledPrefChange() { 58 ChromeUtils.nondeterministicGetWeakMapKeys(this._windowStates).forEach( 59 win => { 60 this._updateButtonVisibility(win); 61 } 62 ); 63 }, 64 65 _updateButtonVisibility(win) { 66 const isPrivateWindow = lazy.PrivateBrowsingUtils.isWindowPrivate(win); 67 const modeSwitcherButton = win.document.getElementById("ai-window-toggle"); 68 if (modeSwitcherButton) { 69 modeSwitcherButton.hidden = !this.isAIWindowEnabled() || isPrivateWindow; 70 } 71 }, 72 73 /** 74 * Sets options for new AI Window if new or inherited conditions are met 75 * 76 * @param {object} options Used in BrowserWindowTracker.openWindow 77 * @param {object} options.openerWindow Window making the BrowserWindowTracker.openWindow call 78 * @param {object} options.args Array of arguments to pass to new window 79 * @param {boolean} [options.aiWindow] Should new window be AI Window (true), Classic Window (false), or inherited from opener (undefined, default) 80 * @param {boolean} [options.private] Should new window be Private Window 81 * @param {boolean} [options.restoreSession] Should previous AI Window session be restored 82 * 83 * @returns {object} Modified arguments appended to the options object 84 */ 85 handleAIWindowOptions({ 86 openerWindow, 87 args, 88 aiWindow = undefined, 89 private: isPrivate = false, 90 restoreSession = false, 91 } = {}) { 92 // Indicates whether the new window should inherit AI Window state from opener window 93 const canInheritAIWindow = 94 this.isAIWindowActiveAndEnabled(openerWindow) && 95 !isPrivate && 96 typeof aiWindow === "undefined"; 97 98 const willOpenAIWindow = 99 (aiWindow && this.isAIWindowEnabled()) || canInheritAIWindow; 100 101 if (!willOpenAIWindow) { 102 return args; 103 } 104 105 args ??= Cc["@mozilla.org/array;1"].createInstance(Ci.nsIMutableArray); 106 107 if (!args.length) { 108 const aiWindowURI = Cc["@mozilla.org/supports-string;1"].createInstance( 109 Ci.nsISupportsString 110 ); 111 aiWindowURI.data = restoreSession ? "" : AIWINDOW_URL; 112 args.appendElement(aiWindowURI); 113 114 const aiOption = Cc["@mozilla.org/hash-property-bag;1"].createInstance( 115 Ci.nsIWritablePropertyBag2 116 ); 117 aiOption.setPropertyAsBool("ai-window", aiWindow); 118 args.appendElement(aiOption); 119 } 120 121 return args; 122 }, 123 124 /** 125 * Show Window Switcher button in tabs toolbar 126 * 127 * @param {object} win caller window 128 */ 129 handleAIWindowSwitcher(win) { 130 let view = lazy.PanelMultiView.getViewNode( 131 win.document, 132 "ai-window-toggle-view" 133 ); 134 135 const isPrivateWindow = lazy.PrivateBrowsingUtils.isWindowPrivate(win); 136 137 if (!isPrivateWindow) { 138 view.querySelector("#ai-window-switch-classic").hidden = false; 139 view.querySelector("#ai-window-switch-ai").hidden = false; 140 } 141 142 let windowState = this._windowStates.get(win); 143 if (!windowState) { 144 windowState = {}; 145 this._windowStates.set(win, windowState); 146 } 147 148 if (windowState.viewInitialized) { 149 return; 150 } 151 152 view.addEventListener("command", event => { 153 switch (event.target.id) { 154 case "ai-window-switch-classic": 155 this.toggleAIWindow(win, false); 156 break; 157 case "ai-window-switch-ai": 158 this.toggleAIWindow(win, true); 159 break; 160 } 161 }); 162 163 windowState.viewInitialized = true; 164 }, 165 166 /** 167 * Show Window Switcher button in tabs toolbar 168 * 169 * @param {Window} win caller window 170 */ 171 initializeAITabsToolbar(win) { 172 const modeSwitcherButton = win.document.getElementById("ai-window-toggle"); 173 if (!modeSwitcherButton) { 174 return; 175 } 176 177 this._updateButtonVisibility(win); 178 179 modeSwitcherButton.addEventListener("command", event => { 180 if (win.PanelUI.panel.state == "open") { 181 win.PanelUI.hide(); 182 } else if (win.PanelUI.panel.state == "closed") { 183 this.handleAIWindowSwitcher(win); 184 win.PanelUI.showSubView("ai-window-toggle-view", event.target, event); 185 } 186 }); 187 }, 188 189 /** 190 * Is current window an AI Window 191 * 192 * @param {Window} win current Window 193 * @returns {boolean} whether current Window is an AI Window 194 */ 195 isAIWindowActive(win) { 196 return !!win && win.document.documentElement.hasAttribute("ai-window"); 197 }, 198 199 /** 200 * Is AI Window enabled 201 * 202 * @returns {boolean} whether AI Window is enabled 203 */ 204 isAIWindowEnabled() { 205 return this.AIWindowEnabled; 206 }, 207 208 isAIWindowActiveAndEnabled(win) { 209 return this.isAIWindowActive(win) && this.AIWindowEnabled; 210 }, 211 212 /** 213 * Check if window is being opened as an AI Window. 214 * 215 * @param {Window} win - The window to check 216 * @returns {boolean} whether the window is being opened as an AI Window 217 */ 218 isOpeningAIWindow(win) { 219 const windowArgs = win?.arguments?.[1]; 220 if (!(windowArgs instanceof Ci.nsIPropertyBag2)) { 221 return false; 222 } 223 224 return windowArgs.hasKey("ai-window"); 225 }, 226 227 /** 228 * Is AI Window content page active 229 * 230 * @param {nsIURI} uri current URI 231 * @returns {boolean} whether AI Window content page is active 232 */ 233 isAIWindowContentPage(uri) { 234 return ( 235 AIWINDOW_URI.equalsExceptRef(uri) || FIRSTRUN_URI.equalsExceptRef(uri) 236 ); 237 }, 238 239 /** 240 * Adds the AI Window app menu options 241 * 242 * @param {Event} event - History menu click event 243 * @param {Window} win - current Window reference 244 * 245 * @returns {Promise} - Resolves when menu is done being added 246 */ 247 appMenu(event, win) { 248 if (!this._aiWindowMenu) { 249 this._aiWindowMenu = new lazy.AIWindowMenu(); 250 } 251 252 return this._aiWindowMenu.addMenuitems(event, win); 253 }, 254 255 get newTabURL() { 256 return AIWINDOW_URL; 257 }, 258 259 /** 260 * Performs a search in the default search engine with 261 * passed query in the current tab. 262 * 263 * @param {string} query 264 * @param {Window} window 265 */ 266 async performSearch(query, window) { 267 let engine = null; 268 try { 269 engine = await Services.search.getDefault(); 270 } catch (error) { 271 console.error(`Failed to get default search engine:`, error); 272 } 273 274 const triggeringPrincipal = 275 Services.scriptSecurityManager.getSystemPrincipal(); 276 277 await lazy.SearchUIUtils.loadSearch({ 278 window, 279 searchText: query, 280 where: "current", 281 usePrivate: false, 282 triggeringPrincipal, 283 policyContainer: null, 284 engine, 285 searchUrlType: null, 286 sapSource: "aiwindow_assistant", 287 }); 288 }, 289 290 async toggleAIWindow(win, isTogglingToAIWindow) { 291 let isActive = this.isAIWindowActive(win); 292 if (isActive != isTogglingToAIWindow) { 293 win.document.documentElement.toggleAttribute("ai-window"); 294 Services.obs.notifyObservers(win, "ai-window-state-changed"); 295 } 296 }, 297 }; 298 299 XPCOMUtils.defineLazyPreferenceGetter( 300 AIWindow, 301 "AIWindowEnabled", 302 "browser.aiwindow.enabled", 303 false, 304 AIWindow._onAIWindowEnabledPrefChange.bind(AIWindow) 305 );