commit 108746012f3a80eb57110fe8a5414d48d43e441c
parent c73a5034930ffb851950b1aa73d0115c15ece9d4
Author: Elissa Cha <echa@mozilla.com>
Date: Tue, 6 Jan 2026 18:30:06 +0000
Bug 2000982 - add message level config to allow showing message in AI Window. r=pdahiya,omc-reviewers,ngrato,aminomancer
Differential Revision: https://phabricator.services.mozilla.com/D274045
Diffstat:
7 files changed, 116 insertions(+), 16 deletions(-)
diff --git a/browser/components/asrouter/docs/targeting-attributes.md b/browser/components/asrouter/docs/targeting-attributes.md
@@ -48,6 +48,7 @@ Please note that some targeting attributes require stricter controls on the tele
* [hasSelectableProfiles](#hasselectableprofiles)
* [homePageSettings](#homepagesettings)
* [isBackgroundTaskMode](#isbackgroundtaskmode)
+* [isAIWindow] (#isaiwindow)
* [isChinaRepack](#ischinarepack)
* [isDefaultBrowser](#isdefaultbrowser)
* [isDefaultBrowserUncached](#isdefaultbrowseruncached)
@@ -855,6 +856,36 @@ actually emit from tabs, this is always true. For other triggers, like
declare const browserIsSelected: boolean;
```
+### `isAIWindow`
+
+A context property included for all triggers that evaluates to `true` when the
+message comes from an AI Window, and `false` otherwise.
+
+#### Definition
+
+```ts
+declare const isAIWindow: boolean;
+```
+
+#### Examples
+
+* Target AI Windows only:
+```javascript
+isAIWindow
+```
+
+* Target Classic Windows only:
+```javascript
+!isAIWindow
+```
+
+* Target both AI Windows and Classic Windows:
+```javascript
+isAIWindow == isAIWindow
+or equivalently
+(isAIWindow || !isAIWindow)
+```
+
### `isChinaRepack`
Does the user use [the partner repack distributed by Mozilla Online](https://github.com/mozilla-partners/mozillaonline),
diff --git a/browser/components/asrouter/modules/ASRouter.sys.mjs b/browser/components/asrouter/modules/ASRouter.sys.mjs
@@ -60,6 +60,8 @@ ChromeUtils.defineESModuleGetters(lazy, {
Spotlight: "resource:///modules/asrouter/Spotlight.sys.mjs",
ToastNotification: "resource:///modules/asrouter/ToastNotification.sys.mjs",
ToolbarBadgeHub: "resource:///modules/asrouter/ToolbarBadgeHub.sys.mjs",
+ AIWindow:
+ "moz-src:///browser/components/aiwindow/ui/modules/AIWindow.sys.mjs",
});
XPCOMUtils.defineLazyPreferenceGetter(
@@ -2342,6 +2344,9 @@ export class _ASRouter {
trigger.context = {};
}
if (typeof trigger.context === "object") {
+ trigger.context.isAIWindow = !!lazy.AIWindow?.isAIWindowActive?.(
+ browser.ownerGlobal
+ );
trigger.context.browserIsSelected =
trigger.context.browserIsSelected ||
browser === browser.ownerGlobal.gBrowser?.selectedBrowser;
diff --git a/browser/components/asrouter/modules/ASRouterTargeting.sys.mjs b/browser/components/asrouter/modules/ASRouterTargeting.sys.mjs
@@ -1390,6 +1390,19 @@ const TargetingGetters = {
},
};
+function addAIWindowTargeting(targeting) {
+ if (!targeting || targeting === "true") {
+ // Default behavior: Classic-only if no targeting is specified
+ return `!isAIWindow`;
+ }
+
+ if (/\bisAIWindow\b/.test(targeting)) {
+ return targeting;
+ }
+
+ return `((${targeting}) && !isAIWindow)`;
+}
+
export const ASRouterTargeting = {
Environment: TargetingGetters,
@@ -1531,14 +1544,13 @@ export const ASRouterTargeting = {
Array.from(arguments) // eslint-disable-line prefer-rest-params
);
- // If no targeting is specified,
- if (!message.targeting) {
- return true;
- }
+ let { targeting } = message;
+ targeting = addAIWindowTargeting(targeting);
+
let result;
try {
if (shouldCache) {
- result = this.getCachedEvaluation(message.targeting);
+ result = this.getCachedEvaluation(targeting);
if (result) {
return result.value;
}
@@ -1546,9 +1558,9 @@ export const ASRouterTargeting = {
// Used to report the source of the targeting error in the case of
// undesired events
targetingContext.setTelemetrySource(message.id);
- result = await targetingContext.evalWithDefault(message.targeting);
+ result = await targetingContext.evalWithDefault(targeting);
if (shouldCache) {
- jexlEvaluationCache.set(message.targeting, {
+ jexlEvaluationCache.set(targeting, {
timestamp: Date.now(),
value: result,
});
diff --git a/browser/components/asrouter/tests/browser/browser_asrouter_menu_messages.js b/browser/components/asrouter/tests/browser/browser_asrouter_menu_messages.js
@@ -486,6 +486,7 @@ add_task(async function test_trigger() {
context: {
source: MenuMessage.SOURCES.APP_MENU,
browserIsSelected: true,
+ isAIWindow: false,
},
}),
"sendTriggerMessage was called when opening the AppMenu panel."
@@ -501,6 +502,7 @@ add_task(async function test_trigger() {
context: {
source: MenuMessage.SOURCES.PXI_MENU,
browserIsSelected: true,
+ isAIWindow: false,
},
}),
"sendTriggerMessage was called when opening the PXI panel."
diff --git a/browser/components/asrouter/tests/unit/ASRouter.test.js b/browser/components/asrouter/tests/unit/ASRouter.test.js
@@ -1801,15 +1801,14 @@ describe("ASRouter", () => {
id: "firstRun",
});
+ const [{ trigger }] =
+ ASRouterTargeting.findMatchingMessage.firstCall.args;
+
assert.calledOnce(ASRouterTargeting.findMatchingMessage);
- assert.deepEqual(
- ASRouterTargeting.findMatchingMessage.firstCall.args[0].trigger,
- {
- id: "firstRun",
- param: undefined,
- context: { browserIsSelected: true },
- }
- );
+ assert.strictEqual(trigger.id, "firstRun");
+ assert.strictEqual(trigger.param, undefined);
+ assert.isObject(trigger.context);
+ assert.strictEqual(trigger.context.browserIsSelected, true);
});
it("should record telemetry information", async () => {
const fakeTimerId = 42;
diff --git a/browser/components/asrouter/tests/unit/ASRouterTargeting.test.js b/browser/components/asrouter/tests/unit/ASRouterTargeting.test.js
@@ -322,7 +322,10 @@ describe("ASRouterTargeting", () => {
false
);
assert.calledOnce(fakeTargetingContext.evalWithDefault);
- assert.calledWithExactly(fakeTargetingContext.evalWithDefault, "true");
+ assert.include(
+ fakeTargetingContext.evalWithDefault.firstCall.args[0],
+ "!isAIWindow"
+ );
assert.calledWithExactly(
fakeTargetingContext.setTelemetrySource,
"message"
@@ -404,6 +407,53 @@ describe("ASRouterTargeting", () => {
assert.calledTwice(evalStub);
});
+ it("defaults to Classic-only targeting when no targeting is specified", async () => {
+ evalStub.resolves(true);
+ const targetingContext = new global.TargetingContext();
+ const message = { id: "test-message" };
+
+ await ASRouterTargeting.checkMessageTargeting(
+ message,
+ targetingContext,
+ null,
+ false
+ );
+
+ assert.calledOnce(fakeTargetingContext.evalWithDefault);
+ assert.calledWith(fakeTargetingContext.evalWithDefault, "!isAIWindow");
+ });
+ it("blocks messages in AI windows by default via !isAIWindow", async () => {
+ evalStub.resolves(true);
+ const targetingContext = new global.TargetingContext();
+ targetingContext.isAIWindow = false;
+ const message = { id: "test-message" };
+
+ await ASRouterTargeting.checkMessageTargeting(
+ message,
+ targetingContext,
+ null,
+ false
+ );
+
+ assert.calledOnce(fakeTargetingContext.evalWithDefault);
+ assert.calledWith(fakeTargetingContext.evalWithDefault, "!isAIWindow");
+ });
+ it("does not modify targeting that explicitly references isAIWindow", async () => {
+ evalStub.resolves(true);
+ const targetingContext = new global.TargetingContext();
+ targetingContext.isAIWindow = true;
+ const message = { id: "test-message", targeting: "isAIWindow" };
+
+ await ASRouterTargeting.checkMessageTargeting(
+ message,
+ targetingContext,
+ null,
+ false
+ );
+
+ assert.calledOnce(fakeTargetingContext.evalWithDefault);
+ assert.calledWith(fakeTargetingContext.evalWithDefault, "isAIWindow");
+ });
describe("#findMatchingMessage", () => {
let matchStub;
diff --git a/browser/components/asrouter/tests/unit/TargetingDocs.test.js b/browser/components/asrouter/tests/unit/TargetingDocs.test.js
@@ -74,6 +74,7 @@ describe("ASRTargeting docs", () => {
"messageImpressions",
"screenImpressions",
"browserIsSelected",
+ "isAIWindow",
];
for (const targetingParam of DOCS_TARGETING_HEADINGS.filter(
doc => !allow.includes(doc)