From 74c1e53e86fbb8e51470ed6c14109b2092a18a9d Mon Sep 17 00:00:00 2001 From: Andrey Zinovyev Date: Fri, 6 Feb 2026 15:10:02 +0000 Subject: [PATCH 01/93] Bug 2013322 - Remove (activity as HomeActivity) casts in TopSiteController.kt r=android-reviewers,jonalmeida Differential Revision: https://phabricator.services.mozilla.com/D280980 --- .../main/java/org/mozilla/fenix/home/HomeFragment.kt | 2 +- .../mozilla/fenix/home/topsites/ShortcutsFragment.kt | 3 +-- .../home/topsites/controller/TopSiteController.kt | 11 +++++++---- .../controller/DefaultTopSiteControllerTest.kt | 4 ++-- 4 files changed, 11 insertions(+), 9 deletions(-) diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/HomeFragment.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/HomeFragment.kt index 29d4b26938d85..7356107eec503 100644 --- a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/HomeFragment.kt +++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/HomeFragment.kt @@ -621,7 +621,7 @@ class HomeFragment : Fragment() { appStore = components.appStore, ), topSiteController = DefaultTopSiteController( - activityRef = WeakReference(activity), + activityRef = WeakReference(requireActivity()), store = store, navControllerRef = WeakReference(findNavController()), settings = components.settings, diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/topsites/ShortcutsFragment.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/topsites/ShortcutsFragment.kt index b1189549ce38d..b305cb917e57f 100644 --- a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/topsites/ShortcutsFragment.kt +++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/topsites/ShortcutsFragment.kt @@ -18,7 +18,6 @@ import androidx.navigation.fragment.findNavController import kotlinx.coroutines.flow.map import mozilla.components.feature.top.sites.presenter.DefaultTopSitesPresenter import mozilla.components.support.base.feature.ViewBoundFeatureWrapper -import org.mozilla.fenix.HomeActivity import org.mozilla.fenix.components.components import org.mozilla.fenix.ext.requireComponents import org.mozilla.fenix.home.topsites.controller.DefaultTopSiteController @@ -44,7 +43,7 @@ class ShortcutsFragment : Fragment() { super.onViewCreated(view, savedInstanceState) controller = DefaultTopSiteController( - activityRef = WeakReference(activity as HomeActivity), + activityRef = WeakReference(requireActivity()), store = requireComponents.core.store, navControllerRef = WeakReference(findNavController()), settings = requireComponents.settings, diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/topsites/controller/TopSiteController.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/topsites/controller/TopSiteController.kt index c201933b0c2af..04a6052aaacae 100644 --- a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/topsites/controller/TopSiteController.kt +++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/topsites/controller/TopSiteController.kt @@ -5,6 +5,7 @@ package org.mozilla.fenix.home.topsites.controller import android.annotation.SuppressLint +import android.app.Activity import android.content.res.ColorStateList import android.view.LayoutInflater import android.widget.EditText @@ -36,9 +37,9 @@ import mozilla.telemetry.glean.private.NoExtras import org.mozilla.fenix.GleanMetrics.Pings import org.mozilla.fenix.GleanMetrics.ShortcutsLibrary import org.mozilla.fenix.GleanMetrics.TopSites -import org.mozilla.fenix.HomeActivity import org.mozilla.fenix.R import org.mozilla.fenix.browser.browsingmode.BrowsingMode +import org.mozilla.fenix.components.appstate.AppAction import org.mozilla.fenix.components.metrics.MetricsUtils import org.mozilla.fenix.components.usecases.FenixBrowserUseCases import org.mozilla.fenix.ext.components @@ -112,7 +113,7 @@ interface TopSiteController { */ @Suppress("LongParameterList") class DefaultTopSiteController( - private val activityRef: WeakReference, + private val activityRef: WeakReference, private val navControllerRef: WeakReference, private val store: BrowserStore, private val settings: Settings, @@ -125,7 +126,7 @@ class DefaultTopSiteController( private val viewLifecycleScope: CoroutineScope, ) : TopSiteController { - private val activity: HomeActivity + private val activity: Activity get() = requireNotNull(activityRef.get()) private val navController: NavController @@ -138,7 +139,9 @@ class DefaultTopSiteController( TopSites.openInPrivateTab.record(NoExtras()) } - activity.browsingModeManager.mode = BrowsingMode.Private + activity.components.appStore.dispatch( + AppAction.BrowsingModeManagerModeChanged(BrowsingMode.Private), + ) if (navController.currentDestination?.id == R.id.shortcutsFragment) { navController.navigate(ShortcutsFragmentDirections.actionShortcutsFragmentToBrowserFragment()) diff --git a/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/home/topsites/controller/DefaultTopSiteControllerTest.kt b/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/home/topsites/controller/DefaultTopSiteControllerTest.kt index 271a29f068b54..ffd75e8d995c5 100644 --- a/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/home/topsites/controller/DefaultTopSiteControllerTest.kt +++ b/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/home/topsites/controller/DefaultTopSiteControllerTest.kt @@ -4,6 +4,7 @@ package org.mozilla.fenix.home.topsites.controller +import android.app.Activity import androidx.navigation.NavController import androidx.test.ext.junit.runners.AndroidJUnit4 import io.mockk.coVerify @@ -37,7 +38,6 @@ import org.mozilla.fenix.GleanMetrics.Events import org.mozilla.fenix.GleanMetrics.Pings import org.mozilla.fenix.GleanMetrics.ShortcutsLibrary import org.mozilla.fenix.GleanMetrics.TopSites -import org.mozilla.fenix.HomeActivity import org.mozilla.fenix.R import org.mozilla.fenix.components.Analytics import org.mozilla.fenix.components.usecases.FenixBrowserUseCases @@ -55,7 +55,7 @@ class DefaultTopSiteControllerTest { @get:Rule val gleanTestRule = FenixGleanTestRule(testContext) - private val activity: HomeActivity = mockk(relaxed = true) + private val activity: Activity = mockk(relaxed = true) private val navController: NavController = mockk(relaxed = true) private val tabsUseCases: TabsUseCases = mockk(relaxed = true) private val selectTabUseCase: TabsUseCases = mockk(relaxed = true) From 2ef746250dc8efe9568c1d262b3a1182f61bc7ee Mon Sep 17 00:00:00 2001 From: Greg Stoll Date: Fri, 6 Feb 2026 15:12:14 +0000 Subject: [PATCH 02/93] Bug 1994918 part 2 - improve windows message debug output for WM_NCHITTEST r=win-reviewers,handyman Differential Revision: https://phabricator.services.mozilla.com/D274823 --- widget/windows/nsWindowDbg.cpp | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/widget/windows/nsWindowDbg.cpp b/widget/windows/nsWindowDbg.cpp index 6552fb8103271..0055396b59570 100644 --- a/widget/windows/nsWindowDbg.cpp +++ b/widget/windows/nsWindowDbg.cpp @@ -377,6 +377,7 @@ bool NativeEventLogger::NativeEventLoggerInternal() { } const char* resultMsg = [&]() { if (!mResult.isSome()) return "initial call"; + if (!mResult.value()) return "false"; if (mMsg == WM_NCHITTEST) { auto const& htr = HitTestResults(); if (auto const it = htr.find(mRetValue); it != htr.end()) { @@ -384,7 +385,7 @@ bool NativeEventLogger::NativeEventLoggerInternal() { } return "undocumented value?"; } - return mResult.value() ? "true" : "false"; + return "true"; }(); nsAutoCString logMessage; @@ -561,9 +562,12 @@ void VirtualKeyParamInfo(nsCString& result, uint64_t param, const char* name, bool /* isPreCall */) { // check that `name` is of length 2 constexpr static const auto ASCII_KEY_ENTRY_HELPER = - [](const char(&name)[2]) -> uint64_t { return name[0]; }; + [](const char (&name)[2]) -> uint64_t { return name[0]; }; -#define ASCII_KEY_ENTRY(name) {ASCII_KEY_ENTRY_HELPER(name), name} +#define ASCII_KEY_ENTRY(name) \ + { \ + ASCII_KEY_ENTRY_HELPER(name), name \ + } const static std::unordered_map virtualKeys{ VALANDNAME_ENTRY(VK_LBUTTON), From c5868a99b4c0710d430673891631d650af9ac936 Mon Sep 17 00:00:00 2001 From: Rebecca King Date: Fri, 6 Feb 2026 15:20:35 +0000 Subject: [PATCH 03/93] Bug 2000718 - Trigger in-panel bandwidth limit warning messages at 75 and 90 percent usage - r=ip-protection-reviewers,fluent-reviewers,bolsson,kpatenio Differential Revision: https://phabricator.services.mozilla.com/D281691 --- .../ipprotection/IPProtectionPanel.sys.mjs | 47 +++++- .../content/ipprotection-content.css | 6 + .../content/ipprotection-content.mjs | 15 +- .../browser_ipprotection_message_bar.js | 138 +++++++++++++++++- .../browser_ipprotection_status_card.js | 1 + .../ipprotection/tests/browser/head.js | 1 - 6 files changed, 198 insertions(+), 10 deletions(-) diff --git a/browser/components/ipprotection/IPProtectionPanel.sys.mjs b/browser/components/ipprotection/IPProtectionPanel.sys.mjs index 6c5f0c941638e..c5887c3859345 100644 --- a/browser/components/ipprotection/IPProtectionPanel.sys.mjs +++ b/browser/components/ipprotection/IPProtectionPanel.sys.mjs @@ -35,6 +35,7 @@ import { } from "chrome://browser/content/ipprotection/ipprotection-constants.mjs"; const EGRESS_LOCATION_PREF = "browser.ipProtection.egressLocationEnabled"; +const BANDWIDTH_THRESHOLD_PREF = "browser.ipProtection.bandwidthThreshold"; const DEFAULT_EGRESS_LOCATION = { name: "United States", code: "us" }; XPCOMUtils.defineLazyPreferenceGetter( @@ -51,6 +52,13 @@ XPCOMUtils.defineLazyPreferenceGetter( false ); +XPCOMUtils.defineLazyPreferenceGetter( + lazy, + "BANDWIDTH_THRESHOLD", + "browser.ipProtection.bandwidthThreshold", + 0 +); + let hasCustomElements = new WeakSet(); /** @@ -129,6 +137,7 @@ export class IPProtectionPanel { panel = null; initiatedUpgrade = false; #window = null; + #lastBandwidthWarningMessageDismissed = 0; /** * Gets the gBrowser from the weak reference to the window. @@ -195,7 +204,7 @@ export class IPProtectionPanel { isSiteExceptionsEnabled: this.isExceptionsFeatureEnabled, siteData: this.#getSiteData(), bandwidthUsage: lazy.BANDWIDTH_USAGE_ENABLED - ? { currentBandwidthUsage: 0, maxBandwidth: 150 } + ? { currentBandwidthUsage: 0, maxBandwidth: 50 } : null, isActivating: lazy.IPPProxyManager.state === lazy.IPPProxyStates.ACTIVATING, @@ -505,6 +514,10 @@ export class IPProtectionPanel { "IPProtection:UserDisableVPNForSite", this.handleEvent ); + doc.addEventListener( + "IPProtection:DismissBandwidthWarning", + this.handleEvent + ); } #removePanelListeners(doc) { @@ -522,6 +535,10 @@ export class IPProtectionPanel { "IPProtection:UserDisableVPNForSite", this.handleEvent ); + doc.removeEventListener( + "IPProtection:DismissBandwidthWarning", + this.handleEvent + ); } #addProxyListeners() { @@ -576,10 +593,15 @@ export class IPProtectionPanel { #addPrefObserver() { Services.prefs.addObserver(EGRESS_LOCATION_PREF, this.handlePrefChange); + Services.prefs.addObserver(BANDWIDTH_THRESHOLD_PREF, this.handlePrefChange); } #removePrefObserver() { Services.prefs.removeObserver(EGRESS_LOCATION_PREF, this.handlePrefChange); + Services.prefs.removeObserver( + BANDWIDTH_THRESHOLD_PREF, + this.handlePrefChange + ); } #handlePrefChange(subject, topic, data) { @@ -588,6 +610,25 @@ export class IPProtectionPanel { this.setState({ location: isEnabled ? DEFAULT_EGRESS_LOCATION : null, }); + } else if (data === BANDWIDTH_THRESHOLD_PREF) { + const threshold = lazy.BANDWIDTH_THRESHOLD; + + // Reset dismissed warnings when threshold is cleared (e.g., new month) + if (threshold === 0) { + this.#lastBandwidthWarningMessageDismissed = 0; + return; + } + + // Only show warning if threshold is 75 or 90 + if (threshold !== 75 && threshold !== 90) { + return; + } + + // Show warning only if current threshold is higher than dismissed threshold + // This allows warning to reappear when going from 75% → 90% + if (threshold > this.#lastBandwidthWarningMessageDismissed) { + this.setState({ bandwidthWarning: true }); + } } } @@ -711,6 +752,10 @@ export class IPProtectionPanel { lazy.IPPExceptionsManager.setExclusion(principal, true); this.#reloadCurrentTab(win); + } else if (event.type == "IPProtection:DismissBandwidthWarning") { + // Store the dismissed threshold level + this.#lastBandwidthWarningMessageDismissed = event.detail.threshold; + this.setState({ bandwidthWarning: false }); } } } diff --git a/browser/components/ipprotection/content/ipprotection-content.css b/browser/components/ipprotection/content/ipprotection-content.css index a23ddf6187629..78ca565ff9725 100644 --- a/browser/components/ipprotection/content/ipprotection-content.css +++ b/browser/components/ipprotection/content/ipprotection-content.css @@ -92,6 +92,12 @@ hr { margin: 0; } +ipprotection-message-bar.vpn-top-content { + margin-inline: var(--space-small); + margin-block-start: 0; + margin-block-end: var(--space-small); +} + @media (prefers-color-scheme: dark) { #unauthenticated-vpn-img { content: url("chrome://browser/content/ipprotection/assets/vpn-panel-get-started-dark.svg"); diff --git a/browser/components/ipprotection/content/ipprotection-content.mjs b/browser/components/ipprotection/content/ipprotection-content.mjs index a3c310be8893f..d1299dffe7780 100644 --- a/browser/components/ipprotection/content/ipprotection-content.mjs +++ b/browser/components/ipprotection/content/ipprotection-content.mjs @@ -177,7 +177,14 @@ export default class IPProtectionContentElement extends MozLitElement { this._showMessageBar = false; this._messageDismissed = true; this.state.error = ""; - this.state.bandwidthWarning = false; + + if (this.state.bandwidthWarning) { + this.dispatchEvent( + new CustomEvent("IPProtection:DismissBandwidthWarning", { + bubbles: true, + }) + ); + } } } @@ -232,9 +239,10 @@ export default class IPProtectionContentElement extends MozLitElement { messageId = "ipprotection-message-bandwidth-warning"; messageType = "warning"; messageLinkL10nArgs = JSON.stringify({ - usageLeft: + usageLeft: ( this.state.bandwidthUsage.maxBandwidth - - this.state.bandwidthUsage.currentBandwidthUsage, + this.state.bandwidthUsage.currentBandwidthUsage + ).toFixed(0), maxUsage: this.state.bandwidthUsage.maxBandwidth, }); } else if (this.state.onboardingMessage) { @@ -263,6 +271,7 @@ export default class IPProtectionContentElement extends MozLitElement { .messageLink=${ifDefined(messageLink)} .messageLinkl10nId=${ifDefined(messageLinkl10nId)} .messageLinkL10nArgs=${ifDefined(messageLinkL10nArgs)} + .bandwidthUsage=${ifDefined(this.state.bandwidthUsage)} > `; } diff --git a/browser/components/ipprotection/tests/browser/browser_ipprotection_message_bar.js b/browser/components/ipprotection/tests/browser/browser_ipprotection_message_bar.js index 365e98742e32c..d39802f3bf4ae 100644 --- a/browser/components/ipprotection/tests/browser/browser_ipprotection_message_bar.js +++ b/browser/components/ipprotection/tests/browser/browser_ipprotection_message_bar.js @@ -50,17 +50,29 @@ add_task(async function test_generic_error() { }); /** - * Tests the warning message bar + * Tests the warning message bar triggered by bandwidth threshold preference */ add_task(async function test_warning_message() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.ipProtection.bandwidth.enabled", true], + ["browser.ipProtection.bandwidthThreshold", 0], + ], + }); + + // Start with no bandwidth usage let content = await openPanel({ isSignedOut: false, error: "", + bandwidthUsage: { + currentBandwidthUsage: 0, + maxBandwidth: 50, + }, }); let messageBar = content.shadowRoot.querySelector("ipprotection-message-bar"); - Assert.ok(!messageBar, "Message bar should not be present"); + Assert.ok(!messageBar, "Message bar should not be present initially"); let messageBarLoadedPromise = BrowserTestUtils.waitForMutationCondition( content.shadowRoot, @@ -68,17 +80,26 @@ add_task(async function test_warning_message() { () => content.shadowRoot.querySelector("ipprotection-message-bar") ); + // Simulate bandwidth usage increasing to 75% (37.5 GB used, 12.5 GB remaining) await setPanelState({ isSignedOut: false, error: "", - bandwidthWarning: true, - bandwidthUsage: { currentBandwidthUsage: 55, maxBandwidth: 150 }, + bandwidthUsage: { + currentBandwidthUsage: 37.5, + maxBandwidth: 50, + }, + }); + + // Set threshold to 75% to trigger warning + await SpecialPowers.pushPrefEnv({ + set: [["browser.ipProtection.bandwidthThreshold", 75]], }); + await messageBarLoadedPromise; messageBar = content.shadowRoot.querySelector("ipprotection-message-bar"); - Assert.ok(messageBar, "Message bar should be present"); + Assert.ok(messageBar, "Message bar should be present after threshold change"); Assert.ok( messageBar.mozMessageBarEl, "Wrapped moz-message-bar should be present" @@ -90,7 +111,113 @@ add_task(async function test_warning_message() { "Warning message id should match" ); + // Verify bandwidth data is passed to the message bar + Assert.ok( + messageBar.bandwidthUsage, + "Bandwidth usage data should be passed to message bar" + ); + Assert.equal( + messageBar.bandwidthUsage.currentBandwidthUsage, + 37.5, + "Current bandwidth usage should match (37.5 GB used at 75% threshold)" + ); + Assert.equal( + messageBar.bandwidthUsage.maxBandwidth, + 50, + "Max bandwidth should match (50 GB limit)" + ); + + // Dismiss the 75% warning + let closeButton = messageBar.mozMessageBarEl.closeButton; + Assert.ok(closeButton, "Message bar should have close button"); + + let dismissEvent = BrowserTestUtils.waitForEvent( + document, + messageBar.DISMISS_EVENT + ); + let messageBarUnloadedPromise = BrowserTestUtils.waitForMutationCondition( + content.shadowRoot, + { childList: true, subtree: true }, + () => !content.shadowRoot.querySelector("ipprotection-message-bar") + ); + + closeButton.click(); + + await dismissEvent; + await messageBarUnloadedPromise; + + Assert.ok( + !content.shadowRoot.querySelector("ipprotection-message-bar"), + "Message bar should be dismissed after clicking close button" + ); + + await closePanel(); + + // Simulate bandwidth usage increasing to 90% (45 GB used, 5 GB remaining) + await SpecialPowers.pushPrefEnv({ + set: [["browser.ipProtection.bandwidthThreshold", 90]], + }); + // Simulate bandwidth usage increasing to 90% (45 GB used, 5 GB remaining) + content = await openPanel({ + isSignedOut: false, + error: "", + bandwidthUsage: { + currentBandwidthUsage: 45, + maxBandwidth: 50, + }, + }); + + messageBarLoadedPromise = BrowserTestUtils.waitForMutationCondition( + content.shadowRoot, + { childList: true, subtree: true }, + () => content.shadowRoot.querySelector("ipprotection-message-bar") + ); + + // The 90% warning should appear + await messageBarLoadedPromise; + + messageBar = content.shadowRoot.querySelector("ipprotection-message-bar"); + + Assert.ok( + messageBar, + "Message bar should reappear at 90% threshold after 75% was dismissed" + ); + Assert.equal(messageBar.type, "warning", "Message bar should be warning"); + Assert.equal( + messageBar.messageId, + "ipprotection-message-bandwidth-warning", + "Warning message id should match" + ); + + // Verify updated bandwidth data + Assert.equal( + messageBar.bandwidthUsage.currentBandwidthUsage, + 45, + "Current bandwidth usage should be updated (45 GB used at 90% threshold)" + ); + Assert.equal( + messageBar.bandwidthUsage.maxBandwidth, + 50, + "Max bandwidth should match (50 GB limit)" + ); + + dismissEvent = BrowserTestUtils.waitForEvent( + document, + messageBar.DISMISS_EVENT + ); + messageBarUnloadedPromise = BrowserTestUtils.waitForMutationCondition( + content.shadowRoot, + { childList: true, subtree: true }, + () => !content.shadowRoot.querySelector("ipprotection-message-bar") + ); + closeButton = messageBar.mozMessageBarEl.closeButton; + closeButton.click(); + + await dismissEvent; + await messageBarUnloadedPromise; + await closePanel(); + await SpecialPowers.popPrefEnv(); }); /** @@ -101,6 +228,7 @@ add_task(async function test_dismiss() { let content = await openPanel({ isSignedOut: false, error: "", + bandwidthWarning: false, }); let messageBar = content.shadowRoot.querySelector("ipprotection-message-bar"); diff --git a/browser/components/ipprotection/tests/browser/browser_ipprotection_status_card.js b/browser/components/ipprotection/tests/browser/browser_ipprotection_status_card.js index 2db6851bb5755..342340371d3f3 100644 --- a/browser/components/ipprotection/tests/browser/browser_ipprotection_status_card.js +++ b/browser/components/ipprotection/tests/browser/browser_ipprotection_status_card.js @@ -46,6 +46,7 @@ async function setupStatusCardTest( } async function cleanupStatusCardTest() { + await SpecialPowers.popPrefEnv(); cleanupService(); } diff --git a/browser/components/ipprotection/tests/browser/head.js b/browser/components/ipprotection/tests/browser/head.js index c58203080b1c5..0242157641acf 100644 --- a/browser/components/ipprotection/tests/browser/head.js +++ b/browser/components/ipprotection/tests/browser/head.js @@ -312,7 +312,6 @@ add_setup(async function setupVPN() { Services.prefs.clearUserPref("browser.ipProtection.entitlementCache"); Services.prefs.clearUserPref("browser.ipProtection.locationListCache"); Services.prefs.clearUserPref("browser.ipProtection.onboardingMessageMask"); - Services.prefs.clearUserPref("browser.ipProtection.bandwidth.enabled"); Services.prefs.clearUserPref("browser.ipProtection.egressLocationEnabled"); }); }); From 136b2ccc2c80f8964a5516955d04a02e9525729a Mon Sep 17 00:00:00 2001 From: Giulia Cardieri Date: Fri, 6 Feb 2026 15:31:46 +0000 Subject: [PATCH 04/93] Bug 2010422 - Add loader for chat assistant r=ai-frontend-reviewers,Mardak [[ https://www.figma.com/design/5KuePTGmOEUFyCHBHCsGim/AI-Mode-%E2%80%94%C2%A0MVP-Scope-Design?node-id=16507-34414&m=dev | Figma example (no text for now) ]] **Goal**: add a loader element, it is visible after the user sends a message and before the assistant responds. Based on the back-end, the analyzing search text might appear after the search page is visible and while the assistant is loading their summary of it. Differential Revision: https://phabricator.services.mozilla.com/D281502 --- .../components/aiwindow/ui/assets/loader.svg | 5 ++ .../ai-chat-content/ai-chat-content.css | 32 ------------ .../ai-chat-content/ai-chat-content.mjs | 29 +++++------ .../chat-assistant-loader.css | 41 ++++++++++++++++ .../chat-assistant-loader.mjs | 49 +++++++++++++++++++ browser/components/aiwindow/ui/jar.mn | 3 ++ .../test/browser/browser_actor_user_prompt.js | 46 +++++++++++++++++ 7 files changed, 156 insertions(+), 49 deletions(-) create mode 100644 browser/components/aiwindow/ui/assets/loader.svg create mode 100644 browser/components/aiwindow/ui/components/ai-chat-content/chat-assistant-loader/chat-assistant-loader.css create mode 100644 browser/components/aiwindow/ui/components/ai-chat-content/chat-assistant-loader/chat-assistant-loader.mjs diff --git a/browser/components/aiwindow/ui/assets/loader.svg b/browser/components/aiwindow/ui/assets/loader.svg new file mode 100644 index 0000000000000..c063a09187c32 --- /dev/null +++ b/browser/components/aiwindow/ui/assets/loader.svg @@ -0,0 +1,5 @@ + + +
\ No newline at end of file diff --git a/browser/components/aiwindow/ui/components/ai-chat-content/ai-chat-content.css b/browser/components/aiwindow/ui/components/ai-chat-content/ai-chat-content.css index 2db95c66c7497..3b334e651f086 100644 --- a/browser/components/aiwindow/ui/components/ai-chat-content/ai-chat-content.css +++ b/browser/components/aiwindow/ui/components/ai-chat-content/ai-chat-content.css @@ -66,35 +66,3 @@ gap: var(--space-small); width: 100%; } - -.searching-indicator { - align-items: center; - flex-direction: row; - gap: var(--space-small); -} - -.searching-text { - color: var(--text-color-deemphasized); - font-style: italic; -} - -.searching-indicator::before { - animation: pulse 1.5s ease-in-out infinite; - background: var(--color-accent-primary); - border-radius: 50%; - content: ""; - height: 8px; - width: 8px; -} - -@keyframes pulse { - 0%, - 100% { - opacity: 0.4; - transform: scale(0.8); - } - 50% { - opacity: 1; - transform: scale(1.2); - } -} diff --git a/browser/components/aiwindow/ui/components/ai-chat-content/ai-chat-content.mjs b/browser/components/aiwindow/ui/components/ai-chat-content/ai-chat-content.mjs index 413d95a3c57b9..9fec20b632120 100644 --- a/browser/components/aiwindow/ui/components/ai-chat-content/ai-chat-content.mjs +++ b/browser/components/aiwindow/ui/components/ai-chat-content/ai-chat-content.mjs @@ -6,6 +6,8 @@ import { html, nothing } from "chrome://global/content/vendor/lit.all.mjs"; import { MozLitElement } from "chrome://global/content/lit-utils.mjs"; // eslint-disable-next-line import/no-unassigned-import import "chrome://browser/content/aiwindow/components/assistant-message-footer.mjs"; +// eslint-disable-next-line import/no-unassigned-import +import "chrome://browser/content/aiwindow/components/chat-assistant-loader.mjs"; /** * A custom element for managing AI Chat Content @@ -15,14 +17,14 @@ export class AIChatContent extends MozLitElement { conversationState: { type: Array }, tokens: { type: Object }, isSearching: { type: Boolean }, - searchQuery: { type: String }, + assistantIsLoading: { type: Boolean }, }; constructor() { super(); this.conversationState = []; this.isSearching = false; - this.searchQuery = null; + this.assistantIsLoading = false; } connectedCallback() { @@ -145,9 +147,9 @@ export class AIChatContent extends MozLitElement { } handleLoadingEvent(event) { - const { isSearching, searchQuery } = event.detail; + const { isSearching } = event.detail; this.isSearching = !!isSearching; - this.searchQuery = searchQuery || null; + this.assistantIsLoading = true; this.requestUpdate(); this.#scrollToBottom(); } @@ -160,6 +162,7 @@ export class AIChatContent extends MozLitElement { handleUserPromptEvent(event) { const { convId, content, ordinal } = event.detail; + this.assistantIsLoading = true; this.conversationState[ordinal] = { role: "user", body: content.body, @@ -178,7 +181,7 @@ export class AIChatContent extends MozLitElement { handleAIResponseEvent(event) { this.isSearching = false; - this.searchQuery = null; + this.assistantIsLoading = false; const { convId, @@ -277,18 +280,10 @@ export class AIChatContent extends MozLitElement {
`; })} - ${this.isSearching - ? html` -
- - ${this.searchQuery - ? `Searching for: "${this.searchQuery}"` - : "Searching the web..."} - -
- ` + ${this.assistantIsLoading + ? html`` : nothing} `; diff --git a/browser/components/aiwindow/ui/components/ai-chat-content/chat-assistant-loader/chat-assistant-loader.css b/browser/components/aiwindow/ui/components/ai-chat-content/chat-assistant-loader/chat-assistant-loader.css new file mode 100644 index 0000000000000..6376c07977670 --- /dev/null +++ b/browser/components/aiwindow/ui/components/ai-chat-content/chat-assistant-loader/chat-assistant-loader.css @@ -0,0 +1,41 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +:host { + --chat-assistant-loader-size: var(--size-item-small); + --chat-assistant-loader-light-pink: color-mix(in srgb, var(--color-blue-40) 38.74%, var(--color-purple-10) 61.26%); + --chat-assistant-loader-dark-pink: var(--color-purple-60); +} + +.chat-assistant-loader { + display: flex; + gap: calc(var(--space-xxsmall) + var(--space-xsmall)); +} + +.chat-assistant-loader__spinner { + animation: 0.9s spinLoader infinite linear; + background-image: url("chrome://browser/content/aiwindow/assets/loader.svg"); + background-size: var(--chat-assistant-loader-size); + height: var(--chat-assistant-loader-size); + width: var(--chat-assistant-loader-size); + will-change: transform; +} + +@keyframes spinLoader { + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(360deg); + } +} + +.chat-assistant-loader__text { + background: linear-gradient(90deg, var(--chat-assistant-loader-light-pink) 14.17%, var(--chat-assistant-loader-dark-pink) 91.21%); + background-clip: text; + /* stylelint-disable-next-line stylelint-plugin-mozilla/use-design-tokens */ + color: transparent; + font-weight: var(--font-weight-semibold); + margin: 0; +} diff --git a/browser/components/aiwindow/ui/components/ai-chat-content/chat-assistant-loader/chat-assistant-loader.mjs b/browser/components/aiwindow/ui/components/ai-chat-content/chat-assistant-loader/chat-assistant-loader.mjs new file mode 100644 index 0000000000000..48be3c156f981 --- /dev/null +++ b/browser/components/aiwindow/ui/components/ai-chat-content/chat-assistant-loader/chat-assistant-loader.mjs @@ -0,0 +1,49 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +import { MozLitElement } from "chrome://global/content/lit-utils.mjs"; +import { html, nothing } from "chrome://global/content/vendor/lit.all.mjs"; + +/** + * Loader/spinner visible while the assistant is thinking + * + * isSearch - true when this component is being used for loading a search handoff action + */ +export class ChatAssistantLoader extends MozLitElement { + static properties = { + isSearch: { type: Boolean }, + }; + + constructor() { + super(); + this.isSearch = false; + } + + connectedCallback() { + super.connectedCallback(); + } + + render() { + return html` + + +
+ + ${this.isSearch + ? html` +

Analyzing web search

+ ` + : nothing} +
+ `; + } +} + +customElements.define("chat-assistant-loader", ChatAssistantLoader); diff --git a/browser/components/aiwindow/ui/jar.mn b/browser/components/aiwindow/ui/jar.mn index d0f22bcb03619..1217776c10c1e 100644 --- a/browser/components/aiwindow/ui/jar.mn +++ b/browser/components/aiwindow/ui/jar.mn @@ -9,6 +9,9 @@ browser.jar: content/browser/aiwindow/assets/input-cta-arrow-icon.svg (assets/input-cta-arrow-icon.svg) content/browser/aiwindow/components/ai-chat-content.mjs (components/ai-chat-content/ai-chat-content.mjs) content/browser/aiwindow/components/ai-chat-content.css (components/ai-chat-content/ai-chat-content.css) + content/browser/aiwindow/components/chat-assistant-loader.mjs (components/ai-chat-content/chat-assistant-loader/chat-assistant-loader.mjs) + content/browser/aiwindow/components/chat-assistant-loader.css (components/ai-chat-content/chat-assistant-loader/chat-assistant-loader.css) + content/browser/aiwindow/assets/loader.svg (assets/loader.svg) content/browser/aiwindow/components/ai-chat-message.mjs (components/ai-chat-message/ai-chat-message.mjs) content/browser/aiwindow/components/ai-chat-message.css (components/ai-chat-message/ai-chat-message.css) content/browser/aiwindow/components/ai-chat-search-button.mjs (components/ai-chat-search-button/ai-chat-search-button.mjs) diff --git a/browser/components/aiwindow/ui/test/browser/browser_actor_user_prompt.js b/browser/components/aiwindow/ui/test/browser/browser_actor_user_prompt.js index fe26e5c88426e..5f0653efc8d02 100644 --- a/browser/components/aiwindow/ui/test/browser/browser_actor_user_prompt.js +++ b/browser/components/aiwindow/ui/test/browser/browser_actor_user_prompt.js @@ -56,3 +56,49 @@ add_task(async function test_streaming_ai_response() { await SpecialPowers.popPrefEnv(); }); + +/** + * Test if the loader shows after the user prompt is submitted + */ +add_task(async function test_loader_shows_on_user_submit() { + await SpecialPowers.pushPrefEnv({ + set: [["browser.smartwindow.enabled", true]], + }); + + await BrowserTestUtils.withNewTab("about:aichatcontent", async browser => { + const actor = + browser.browsingContext.currentWindowGlobal.getActor("AIChatContent"); + + const userPrompt = { + role: "user", + content: { body: "Show loader please" }, + }; + + await actor.dispatchMessageToChatContent(userPrompt); + + await SpecialPowers.spawn(browser, [], async () => { + const contentEl = content.document.querySelector("ai-chat-content"); + await contentEl.updateComplete; + + let loaderEl; + await ContentTaskUtils.waitForMutationCondition( + contentEl.shadowRoot, + { childList: true, subtree: true }, + () => { + loaderEl = contentEl.shadowRoot.querySelector( + "chat-assistant-loader" + ); + return loaderEl; + } + ); + Assert.ok(loaderEl, "Loader element exists"); + + const inner = loaderEl.shadowRoot?.querySelector( + ".chat-assistant-loader" + ); + Assert.ok(inner, "Loader has the correct content"); + }); + }); + + await SpecialPowers.popPrefEnv(); +}); From 40d36ba49cf6d720479d6ce8821d7025f4646a1b Mon Sep 17 00:00:00 2001 From: Alex Hochheiden Date: Fri, 6 Feb 2026 15:34:57 +0000 Subject: [PATCH 05/93] Bug 2010200 - Add default `distribution.ini` for official Linux builds r=firefox-build-system-reviewers,glandium,omc-reviewers,mimi Differential Revision: https://phabricator.services.mozilla.com/D281651 --- browser/app/distribution/distribution.ini | 4 ++++ browser/app/distribution/moz.build | 12 ++++++++++ browser/app/moz.build | 2 ++ .../about/browser_aboutDialog_distribution.js | 1 + .../browser/browser_asrouter_targeting.js | 20 +++++++++++----- .../components/uitour/test/browser_UITour.js | 21 +++++++++++------ browser/config/mozconfigs/linux64/common-opt | 2 ++ browser/installer/package-manifest.in | 4 ++++ .../test/browser/browser_ClientEnvironment.js | 23 ++++++++++++------- toolkit/modules/AppConstants.sys.mjs | 7 ++++++ toolkit/moz.configure | 6 +++++ tools/update-packaging/common.sh | 3 ++- 12 files changed, 83 insertions(+), 22 deletions(-) create mode 100644 browser/app/distribution/distribution.ini create mode 100644 browser/app/distribution/moz.build diff --git a/browser/app/distribution/distribution.ini b/browser/app/distribution/distribution.ini new file mode 100644 index 0000000000000..5c16c3bab161e --- /dev/null +++ b/browser/app/distribution/distribution.ini @@ -0,0 +1,4 @@ +[Global] +id=mozilla-official +version=1.0 +about=Mozilla Firefox Official Build \ No newline at end of file diff --git a/browser/app/distribution/moz.build b/browser/app/distribution/moz.build new file mode 100644 index 0000000000000..a97ea6cce4bff --- /dev/null +++ b/browser/app/distribution/moz.build @@ -0,0 +1,12 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +DIST_SUBDIR = "" + +if CONFIG["BUILT_BY_MOZILLA"]: + FINAL_TARGET_FILES.distribution += [ + "distribution.ini", + ] diff --git a/browser/app/moz.build b/browser/app/moz.build index a23bb7812a03b..b5f5461fd0835 100644 --- a/browser/app/moz.build +++ b/browser/app/moz.build @@ -29,6 +29,8 @@ with Files("profile/channel-prefs.js"): with Files("profile/firefox.js"): BUG_COMPONENT = ("Firefox", "General") +DIRS += ["distribution"] + if CONFIG["MOZ_MACBUNDLE_NAME"]: DIRS += ["macbuild/Contents"] diff --git a/browser/base/content/test/about/browser_aboutDialog_distribution.js b/browser/base/content/test/about/browser_aboutDialog_distribution.js index 8f52533bbc52c..2b9c41b2f6fd9 100644 --- a/browser/base/content/test/about/browser_aboutDialog_distribution.js +++ b/browser/base/content/test/about/browser_aboutDialog_distribution.js @@ -13,6 +13,7 @@ add_task(async function verify_distribution_info_hides() { defaultBranch.setCharPref("distribution.id", "mozilla-test-distribution-id"); defaultBranch.setCharPref("distribution.version", "1.0"); + defaultBranch.setCharPref("distribution.about", ""); let aboutDialog = await waitForAboutDialog(); diff --git a/browser/components/asrouter/tests/browser/browser_asrouter_targeting.js b/browser/components/asrouter/tests/browser/browser_asrouter_targeting.js index 2fd4dfde9584b..5fbcd72df48d5 100644 --- a/browser/components/asrouter/tests/browser/browser_asrouter_targeting.js +++ b/browser/components/asrouter/tests/browser/browser_asrouter_targeting.js @@ -1212,6 +1212,7 @@ add_task(async function checkPatternsValid() { add_task(async function check_isChinaRepack() { const prefDefaultBranch = Services.prefs.getDefaultBranch("distribution."); + const originalDistributionId = prefDefaultBranch.getCharPref("id", ""); const messages = [ { id: "msg_for_china_repack", targeting: "isChinaRepack == true" }, { id: "msg_for_everyone_else", targeting: "isChinaRepack == false" }, @@ -1254,7 +1255,7 @@ add_task(async function check_isChinaRepack() { "should select the message for non China repack users" ); - prefDefaultBranch.deleteBranch(""); + prefDefaultBranch.setCharPref("id", originalDistributionId); }); add_task(async function check_userId() { @@ -1513,22 +1514,29 @@ add_task(async function check_userPrefersReducedMotion() { }); add_task(async function test_distributionId() { + let expectedDefault = Services.prefs + .getDefaultBranch(null) + .getCharPref("distribution.id", ""); is( ASRouterTargeting.Environment.distributionId, - "", - "Should return an empty distribution Id" + expectedDefault, + "Should return the expected default distribution Id" ); Services.prefs.getDefaultBranch(null).setCharPref("distribution.id", "test"); - is( ASRouterTargeting.Environment.distributionId, "test", "Should return the correct distribution Id" ); - // clean up default branch distribution.id - Services.prefs.getDefaultBranch(null).deleteBranch("distribution.id"); + if (expectedDefault) { + Services.prefs + .getDefaultBranch(null) + .setCharPref("distribution.id", expectedDefault); + } else { + Services.prefs.getDefaultBranch(null).deleteBranch("distribution.id"); + } }); add_task(async function test_fxViewButtonAreaType_default() { diff --git a/browser/components/uitour/test/browser_UITour.js b/browser/components/uitour/test/browser_UITour.js index 662a58dff1503..4e5867b3726f0 100644 --- a/browser/components/uitour/test/browser_UITour.js +++ b/browser/components/uitour/test/browser_UITour.js @@ -509,15 +509,22 @@ var tests = [ "undefined", "Check distribution isn't undefined." ); - // distribution id defaults to "default" for most builds, and - // "mozilla-MSIX" for MSIX builds. + // distribution id defaults to "default" for most builds, + // "mozilla-MSIX" for MSIX builds, and "mozilla-official" for + // official Mozilla builds. + let expectedDistribution = "default"; + if ( + AppConstants.platform === "win" && + Services.sysinfo.getProperty("hasWinPackageId") + ) { + expectedDistribution = "mozilla-MSIX"; + } else if (AppConstants.BUILT_BY_MOZILLA) { + expectedDistribution = "mozilla-official"; + } is( result.distribution, - AppConstants.platform === "win" && - Services.sysinfo.getProperty("hasWinPackageId") - ? "mozilla-MSIX" - : "default", - 'Should be "default" without preference set.' + expectedDistribution, + "Should have expected distribution value." ); let defaults = Services.prefs.getDefaultBranch("distribution."); diff --git a/browser/config/mozconfigs/linux64/common-opt b/browser/config/mozconfigs/linux64/common-opt index 7181c511ba8b7..179a31324acc6 100644 --- a/browser/config/mozconfigs/linux64/common-opt +++ b/browser/config/mozconfigs/linux64/common-opt @@ -10,5 +10,7 @@ ac_add_options --with-mozilla-api-keyfile=/builds/mozilla-desktop-geoloc-api.key # Needed to enable breakpad in application.ini export MOZILLA_OFFICIAL=1 +ac_add_options --built-by-mozilla + # Package js shell. export MOZ_PACKAGE_JSSHELL=1 diff --git a/browser/installer/package-manifest.in b/browser/installer/package-manifest.in index d3ef68f4c184a..7eb90525444dc 100644 --- a/browser/installer/package-manifest.in +++ b/browser/installer/package-manifest.in @@ -449,3 +449,7 @@ bin/libfreebl_64int_3.so #if defined(DESKTOP_LAUNCHER_ENABLED) @BINPATH@/desktop-launcher/desktop-launcher@BIN_SUFFIX@ #endif + +#if defined(BUILT_BY_MOZILLA) +@RESPATH@/distribution/* +#endif diff --git a/toolkit/components/normandy/test/browser/browser_ClientEnvironment.js b/toolkit/components/normandy/test/browser/browser_ClientEnvironment.js index f1322cffae248..76ba82d9a5e27 100644 --- a/toolkit/components/normandy/test/browser/browser_ClientEnvironment.js +++ b/toolkit/components/normandy/test/browser/browser_ClientEnvironment.js @@ -62,21 +62,28 @@ add_task(async function testUserId() { }); add_task(async function testDistribution() { - // distribution id defaults to "default" for most builds, and - // "mozilla-MSIX" for MSIX builds. + // distribution id defaults to "default" for most builds, + // "mozilla-MSIX" for MSIX builds, and "mozilla-official" for + // official Mozilla builds. + let expectedDistribution = "default"; + if ( + AppConstants.platform === "win" && + Services.sysinfo.getProperty("hasWinPackageId") + ) { + expectedDistribution = "mozilla-MSIX"; + } else if (AppConstants.BUILT_BY_MOZILLA) { + expectedDistribution = "mozilla-official"; + } is( ClientEnvironment.distribution, - AppConstants.platform === "win" && - Services.sysinfo.getProperty("hasWinPackageId") - ? "mozilla-MSIX" - : "default", + expectedDistribution, "distribution has a default value" ); // distribution id is read from a preference Services.prefs .getDefaultBranch(null) - .setStringPref("distribution.id", "funnelcake"); + .setCharPref("distribution.id", "funnelcake"); is( ClientEnvironment.distribution, "funnelcake", @@ -84,7 +91,7 @@ add_task(async function testDistribution() { ); Services.prefs .getDefaultBranch(null) - .setStringPref("distribution.id", "default"); + .setCharPref("distribution.id", "default"); }); const mockClassify = { country: "FR", request_time: new Date(2017, 1, 1) }; diff --git a/toolkit/modules/AppConstants.sys.mjs b/toolkit/modules/AppConstants.sys.mjs index 7c57a768e679a..a51aa0459c5c4 100644 --- a/toolkit/modules/AppConstants.sys.mjs +++ b/toolkit/modules/AppConstants.sys.mjs @@ -31,6 +31,13 @@ export var AppConstants = Object.freeze({ MOZ_OFFICIAL_BRANDING: @MOZ_OFFICIAL_BRANDING_BOOL@, + BUILT_BY_MOZILLA: +#ifdef BUILT_BY_MOZILLA + true, +#else + false, +#endif + MOZ_DEV_EDITION: @MOZ_DEV_EDITION_BOOL@, MOZ_SERVICES_SYNC: @MOZ_SERVICES_SYNC_BOOL@, diff --git a/toolkit/moz.configure b/toolkit/moz.configure index 64f527d9e8053..c301e8ab4e5dd 100644 --- a/toolkit/moz.configure +++ b/toolkit/moz.configure @@ -94,6 +94,12 @@ option( ) set_config("MOZ_DISTRIBUTION_ID", depends("--with-distribution-id")(lambda v: v[0])) +option( + "--built-by-mozilla", + help="Enable Mozilla official build designation", +) +set_config("BUILT_BY_MOZILLA", depends("--built-by-mozilla")(lambda v: bool(v) or None)) +set_define("BUILT_BY_MOZILLA", depends("--built-by-mozilla")(lambda v: bool(v) or None)) @depends("MOZ_APP_VENDOR", build_project) def check_moz_app_vendor(moz_app_vendor, build_project): diff --git a/tools/update-packaging/common.sh b/tools/update-packaging/common.sh index e055b1c24efd5..be2061b98cbba 100755 --- a/tools/update-packaging/common.sh +++ b/tools/update-packaging/common.sh @@ -95,7 +95,8 @@ check_for_add_if_not_update() { if [[ "$(basename "$add_if_not_file_chk")" = "channel-prefs.js" || \ "$add_if_not_file_chk" =~ (^|/)ChannelPrefs\.framework/ || \ "$(basename "$add_if_not_file_chk")" = "update-settings.ini" || \ - "$add_if_not_file_chk" =~ (^|/)UpdateSettings\.framework/ ]]; then + "$add_if_not_file_chk" =~ (^|/)UpdateSettings\.framework/ || \ + "$(basename "$add_if_not_file_chk")" = "distribution.ini" ]]; then ## "true" return 0; fi From 214a01a4ec0caf0edbe53c50e34dd98f48832650 Mon Sep 17 00:00:00 2001 From: Dave Townsend Date: Fri, 6 Feb 2026 15:35:46 +0000 Subject: [PATCH 06/93] Bug 2014616: Always create the Profile Groups folder in case it is missing. r=profiles-reviewers,jhirsch Differential Revision: https://phabricator.services.mozilla.com/D282027 --- .../profiles/tests/unit/test_fail_recover_storeID.js | 4 ++-- toolkit/profile/ProfilesDatastoreService.sys.mjs | 12 ++++++------ 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/browser/components/profiles/tests/unit/test_fail_recover_storeID.js b/browser/components/profiles/tests/unit/test_fail_recover_storeID.js index edd980221b17c..4d4f4c8a9e873 100644 --- a/browser/components/profiles/tests/unit/test_fail_recover_storeID.js +++ b/browser/components/profiles/tests/unit/test_fail_recover_storeID.js @@ -14,8 +14,8 @@ add_task(async function test_recover_storeID() { await SelectableProfileService.init(); Assert.ok( - !ProfilesDatastoreService.initialized, - "Didn't initialize the datastore service" + ProfilesDatastoreService.initialized, + "Initialized the datastore service" ); Assert.ok( !SelectableProfileService.initialized, diff --git a/toolkit/profile/ProfilesDatastoreService.sys.mjs b/toolkit/profile/ProfilesDatastoreService.sys.mjs index e572969143f62..4a5e912b0ea54 100644 --- a/toolkit/profile/ProfilesDatastoreService.sys.mjs +++ b/toolkit/profile/ProfilesDatastoreService.sys.mjs @@ -521,15 +521,11 @@ class ProfilesDatastoreServiceClass { this.#connection = null; } - async maybeCreateProfilesStorePath() { + maybeCreateStoreID() { if (this.#storeID) { return; } - await IOUtils.makeDirectory( - ProfilesDatastoreServiceClass.PROFILE_GROUPS_DIR - ); - const storageID = Services.uuid .generateUUID() .toString() @@ -541,7 +537,7 @@ class ProfilesDatastoreServiceClass { } async getProfilesStorePath() { - await this.maybeCreateProfilesStorePath(); + this.maybeCreateStoreID(); // If we are not running in a named nsIToolkitProfile, the datastore path // should be in the profile directory. This is true in a local build or a @@ -553,6 +549,10 @@ class ProfilesDatastoreServiceClass { ); } + await IOUtils.makeDirectory( + ProfilesDatastoreServiceClass.PROFILE_GROUPS_DIR + ); + return getSharedProfilesStorePath(this.#storeID); } } From 1a313f1fa95c3738754c4097e9d4f2ec0db54b87 Mon Sep 17 00:00:00 2001 From: AndiAJ Date: Fri, 6 Feb 2026 15:35:52 +0000 Subject: [PATCH 07/93] Bug 2015047 - Fix disabled UI tests that leave the Tabs Tray/Tab manager via Close all tabs or closing the last tab r=aaronmt MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The UI tests were [[ https://bugzilla.mozilla.org/show_bug.cgi?id=1989405 | disabled ]] by Noah a while ago because after some changes he made, if the last tab was closed or all tabs were closed VIA the "Close all tabs" button the UI remained stuck. Managed to re-enable 3 UI tests. All 3 successfully passed 50x on Firebase ✅ Differential Revision: https://phabricator.services.mozilla.com/D282176 --- .../androidTest/java/org/mozilla/fenix/ui/BookmarksTest.kt | 4 ++-- .../androidTest/java/org/mozilla/fenix/ui/HomeScreenTest.kt | 1 - .../java/org/mozilla/fenix/ui/TabbedBrowsingTest.kt | 3 +-- 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/BookmarksTest.kt b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/BookmarksTest.kt index 3730ebbeb9b75..b66005cacd228 100644 --- a/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/BookmarksTest.kt +++ b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/BookmarksTest.kt @@ -20,6 +20,7 @@ import org.mozilla.fenix.helpers.TestAssetHelper.htmlControlsFormAsset import org.mozilla.fenix.helpers.TestHelper.clickSnackbarButton import org.mozilla.fenix.helpers.TestHelper.exitMenu import org.mozilla.fenix.helpers.TestHelper.verifySnackBarText +import org.mozilla.fenix.helpers.TestHelper.waitForAppWindowToBeUpdated import org.mozilla.fenix.helpers.TestSetup import org.mozilla.fenix.helpers.perf.DetectMemoryLeaksRule import org.mozilla.fenix.ui.robots.browserScreen @@ -143,7 +144,6 @@ class BookmarksTest : TestSetup() { // TestRail link: https://mozilla.testrail.io/index.php?/cases/view/2833702 @SmokeTest - @Ignore("disabled - https://bugzilla.mozilla.org/show_bug.cgi?id=1989405") @Test fun openMultipleSelectedBookmarksInANewTabTest() { val webPages = listOf( @@ -168,7 +168,7 @@ class BookmarksTest : TestSetup() { verifyTabTrayIsOpen() verifyNormalBrowsingButtonIsSelected() verifyNormalTabsList() - verifyExistingOpenTabs(webPages[0].title, webPages[1].title) + verifyExistingOpenTabs(webPages[0].url.toString(), webPages[1].url.toString()) } } diff --git a/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/HomeScreenTest.kt b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/HomeScreenTest.kt index d49dacff2be69..226699c14eb10 100644 --- a/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/HomeScreenTest.kt +++ b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/HomeScreenTest.kt @@ -75,7 +75,6 @@ class HomeScreenTest : TestSetup() { // TestRail link: https://mozilla.testrail.io/index.php?/cases/view/1364362 @SmokeTest - @Ignore("disabled - https://bugzilla.mozilla.org/show_bug.cgi?id=1989405") @Test fun verifyJumpBackInSectionTest() { composeTestRule.activityRule.applySettingsExceptions { diff --git a/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/TabbedBrowsingTest.kt b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/TabbedBrowsingTest.kt index 10611e1ae401d..e2c09267a823d 100644 --- a/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/TabbedBrowsingTest.kt +++ b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/TabbedBrowsingTest.kt @@ -62,7 +62,6 @@ class TabbedBrowsingTest : TestSetup() { // val retryTestRule = RetryTestRule(3) // TestRail link: https://mozilla.testrail.io/index.php?/cases/view/903599 - @Ignore("disabled - https://bugzilla.mozilla.org/show_bug.cgi?id=1989405") @Test fun closeAllTabsTest() { val defaultWebPage = mockWebServer.getGenericAsset(1) @@ -89,7 +88,7 @@ class TabbedBrowsingTest : TestSetup() { }.openThreeDotMenu { verifyCloseAllTabsButton() }.closeAllTabs { - verifyTabCounter("0") + verifyTabCounter("0", isPrivateBrowsingEnabled = true) } } From a595f56a6e1fbaaff00d0f37fceccd5f7fcbf18c Mon Sep 17 00:00:00 2001 From: Julien Cristau Date: Fri, 6 Feb 2026 15:44:45 +0000 Subject: [PATCH 08/93] Bug 2015073 - don't submit a repo-update patch to phab when running from try. r=RyanVM DONTBUILD Differential Revision: https://phabricator.services.mozilla.com/D282193 --- taskcluster/docker/periodic-updates/runme.sh | 3 +++ 1 file changed, 3 insertions(+) diff --git a/taskcluster/docker/periodic-updates/runme.sh b/taskcluster/docker/periodic-updates/runme.sh index dd6054b120664..ffdf00da4fea2 100755 --- a/taskcluster/docker/periodic-updates/runme.sh +++ b/taskcluster/docker/periodic-updates/runme.sh @@ -57,6 +57,9 @@ then PARAMS="${PARAMS} -d" fi +if [ "${BRANCH}" = try ]; then + PARAMS="${PARAMS} --skip-push" +fi export ARTIFACTS_DIR="/home/worker/artifacts" mkdir -p "$ARTIFACTS_DIR" From 51835764ccd26f9baa32f86e17d38103c7a70bd0 Mon Sep 17 00:00:00 2001 From: Sandor Molnar Date: Fri, 6 Feb 2026 18:19:46 +0200 Subject: [PATCH 09/93] Revert "Bug 2010422 - Add loader for chat assistant r=ai-frontend-reviewers,Mardak" for causing bc failures @ browser_parsable_css.js This reverts commit 136b2ccc2c80f8964a5516955d04a02e9525729a. --- .../components/aiwindow/ui/assets/loader.svg | 5 -- .../ai-chat-content/ai-chat-content.css | 32 ++++++++++++ .../ai-chat-content/ai-chat-content.mjs | 29 ++++++----- .../chat-assistant-loader.css | 41 ---------------- .../chat-assistant-loader.mjs | 49 ------------------- browser/components/aiwindow/ui/jar.mn | 3 -- .../test/browser/browser_actor_user_prompt.js | 46 ----------------- 7 files changed, 49 insertions(+), 156 deletions(-) delete mode 100644 browser/components/aiwindow/ui/assets/loader.svg delete mode 100644 browser/components/aiwindow/ui/components/ai-chat-content/chat-assistant-loader/chat-assistant-loader.css delete mode 100644 browser/components/aiwindow/ui/components/ai-chat-content/chat-assistant-loader/chat-assistant-loader.mjs diff --git a/browser/components/aiwindow/ui/assets/loader.svg b/browser/components/aiwindow/ui/assets/loader.svg deleted file mode 100644 index c063a09187c32..0000000000000 --- a/browser/components/aiwindow/ui/assets/loader.svg +++ /dev/null @@ -1,5 +0,0 @@ - - -
\ No newline at end of file diff --git a/browser/components/aiwindow/ui/components/ai-chat-content/ai-chat-content.css b/browser/components/aiwindow/ui/components/ai-chat-content/ai-chat-content.css index 3b334e651f086..2db95c66c7497 100644 --- a/browser/components/aiwindow/ui/components/ai-chat-content/ai-chat-content.css +++ b/browser/components/aiwindow/ui/components/ai-chat-content/ai-chat-content.css @@ -66,3 +66,35 @@ gap: var(--space-small); width: 100%; } + +.searching-indicator { + align-items: center; + flex-direction: row; + gap: var(--space-small); +} + +.searching-text { + color: var(--text-color-deemphasized); + font-style: italic; +} + +.searching-indicator::before { + animation: pulse 1.5s ease-in-out infinite; + background: var(--color-accent-primary); + border-radius: 50%; + content: ""; + height: 8px; + width: 8px; +} + +@keyframes pulse { + 0%, + 100% { + opacity: 0.4; + transform: scale(0.8); + } + 50% { + opacity: 1; + transform: scale(1.2); + } +} diff --git a/browser/components/aiwindow/ui/components/ai-chat-content/ai-chat-content.mjs b/browser/components/aiwindow/ui/components/ai-chat-content/ai-chat-content.mjs index 9fec20b632120..413d95a3c57b9 100644 --- a/browser/components/aiwindow/ui/components/ai-chat-content/ai-chat-content.mjs +++ b/browser/components/aiwindow/ui/components/ai-chat-content/ai-chat-content.mjs @@ -6,8 +6,6 @@ import { html, nothing } from "chrome://global/content/vendor/lit.all.mjs"; import { MozLitElement } from "chrome://global/content/lit-utils.mjs"; // eslint-disable-next-line import/no-unassigned-import import "chrome://browser/content/aiwindow/components/assistant-message-footer.mjs"; -// eslint-disable-next-line import/no-unassigned-import -import "chrome://browser/content/aiwindow/components/chat-assistant-loader.mjs"; /** * A custom element for managing AI Chat Content @@ -17,14 +15,14 @@ export class AIChatContent extends MozLitElement { conversationState: { type: Array }, tokens: { type: Object }, isSearching: { type: Boolean }, - assistantIsLoading: { type: Boolean }, + searchQuery: { type: String }, }; constructor() { super(); this.conversationState = []; this.isSearching = false; - this.assistantIsLoading = false; + this.searchQuery = null; } connectedCallback() { @@ -147,9 +145,9 @@ export class AIChatContent extends MozLitElement { } handleLoadingEvent(event) { - const { isSearching } = event.detail; + const { isSearching, searchQuery } = event.detail; this.isSearching = !!isSearching; - this.assistantIsLoading = true; + this.searchQuery = searchQuery || null; this.requestUpdate(); this.#scrollToBottom(); } @@ -162,7 +160,6 @@ export class AIChatContent extends MozLitElement { handleUserPromptEvent(event) { const { convId, content, ordinal } = event.detail; - this.assistantIsLoading = true; this.conversationState[ordinal] = { role: "user", body: content.body, @@ -181,7 +178,7 @@ export class AIChatContent extends MozLitElement { handleAIResponseEvent(event) { this.isSearching = false; - this.assistantIsLoading = false; + this.searchQuery = null; const { convId, @@ -280,10 +277,18 @@ export class AIChatContent extends MozLitElement {
`; })} - ${this.assistantIsLoading - ? html`` + ${this.isSearching + ? html` +
+ + ${this.searchQuery + ? `Searching for: "${this.searchQuery}"` + : "Searching the web..."} + +
+ ` : nothing} `; diff --git a/browser/components/aiwindow/ui/components/ai-chat-content/chat-assistant-loader/chat-assistant-loader.css b/browser/components/aiwindow/ui/components/ai-chat-content/chat-assistant-loader/chat-assistant-loader.css deleted file mode 100644 index 6376c07977670..0000000000000 --- a/browser/components/aiwindow/ui/components/ai-chat-content/chat-assistant-loader/chat-assistant-loader.css +++ /dev/null @@ -1,41 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ - -:host { - --chat-assistant-loader-size: var(--size-item-small); - --chat-assistant-loader-light-pink: color-mix(in srgb, var(--color-blue-40) 38.74%, var(--color-purple-10) 61.26%); - --chat-assistant-loader-dark-pink: var(--color-purple-60); -} - -.chat-assistant-loader { - display: flex; - gap: calc(var(--space-xxsmall) + var(--space-xsmall)); -} - -.chat-assistant-loader__spinner { - animation: 0.9s spinLoader infinite linear; - background-image: url("chrome://browser/content/aiwindow/assets/loader.svg"); - background-size: var(--chat-assistant-loader-size); - height: var(--chat-assistant-loader-size); - width: var(--chat-assistant-loader-size); - will-change: transform; -} - -@keyframes spinLoader { - 0% { - transform: rotate(0deg); - } - 100% { - transform: rotate(360deg); - } -} - -.chat-assistant-loader__text { - background: linear-gradient(90deg, var(--chat-assistant-loader-light-pink) 14.17%, var(--chat-assistant-loader-dark-pink) 91.21%); - background-clip: text; - /* stylelint-disable-next-line stylelint-plugin-mozilla/use-design-tokens */ - color: transparent; - font-weight: var(--font-weight-semibold); - margin: 0; -} diff --git a/browser/components/aiwindow/ui/components/ai-chat-content/chat-assistant-loader/chat-assistant-loader.mjs b/browser/components/aiwindow/ui/components/ai-chat-content/chat-assistant-loader/chat-assistant-loader.mjs deleted file mode 100644 index 48be3c156f981..0000000000000 --- a/browser/components/aiwindow/ui/components/ai-chat-content/chat-assistant-loader/chat-assistant-loader.mjs +++ /dev/null @@ -1,49 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ - -import { MozLitElement } from "chrome://global/content/lit-utils.mjs"; -import { html, nothing } from "chrome://global/content/vendor/lit.all.mjs"; - -/** - * Loader/spinner visible while the assistant is thinking - * - * isSearch - true when this component is being used for loading a search handoff action - */ -export class ChatAssistantLoader extends MozLitElement { - static properties = { - isSearch: { type: Boolean }, - }; - - constructor() { - super(); - this.isSearch = false; - } - - connectedCallback() { - super.connectedCallback(); - } - - render() { - return html` - - -
- - ${this.isSearch - ? html` -

Analyzing web search

- ` - : nothing} -
- `; - } -} - -customElements.define("chat-assistant-loader", ChatAssistantLoader); diff --git a/browser/components/aiwindow/ui/jar.mn b/browser/components/aiwindow/ui/jar.mn index 1217776c10c1e..d0f22bcb03619 100644 --- a/browser/components/aiwindow/ui/jar.mn +++ b/browser/components/aiwindow/ui/jar.mn @@ -9,9 +9,6 @@ browser.jar: content/browser/aiwindow/assets/input-cta-arrow-icon.svg (assets/input-cta-arrow-icon.svg) content/browser/aiwindow/components/ai-chat-content.mjs (components/ai-chat-content/ai-chat-content.mjs) content/browser/aiwindow/components/ai-chat-content.css (components/ai-chat-content/ai-chat-content.css) - content/browser/aiwindow/components/chat-assistant-loader.mjs (components/ai-chat-content/chat-assistant-loader/chat-assistant-loader.mjs) - content/browser/aiwindow/components/chat-assistant-loader.css (components/ai-chat-content/chat-assistant-loader/chat-assistant-loader.css) - content/browser/aiwindow/assets/loader.svg (assets/loader.svg) content/browser/aiwindow/components/ai-chat-message.mjs (components/ai-chat-message/ai-chat-message.mjs) content/browser/aiwindow/components/ai-chat-message.css (components/ai-chat-message/ai-chat-message.css) content/browser/aiwindow/components/ai-chat-search-button.mjs (components/ai-chat-search-button/ai-chat-search-button.mjs) diff --git a/browser/components/aiwindow/ui/test/browser/browser_actor_user_prompt.js b/browser/components/aiwindow/ui/test/browser/browser_actor_user_prompt.js index 5f0653efc8d02..fe26e5c88426e 100644 --- a/browser/components/aiwindow/ui/test/browser/browser_actor_user_prompt.js +++ b/browser/components/aiwindow/ui/test/browser/browser_actor_user_prompt.js @@ -56,49 +56,3 @@ add_task(async function test_streaming_ai_response() { await SpecialPowers.popPrefEnv(); }); - -/** - * Test if the loader shows after the user prompt is submitted - */ -add_task(async function test_loader_shows_on_user_submit() { - await SpecialPowers.pushPrefEnv({ - set: [["browser.smartwindow.enabled", true]], - }); - - await BrowserTestUtils.withNewTab("about:aichatcontent", async browser => { - const actor = - browser.browsingContext.currentWindowGlobal.getActor("AIChatContent"); - - const userPrompt = { - role: "user", - content: { body: "Show loader please" }, - }; - - await actor.dispatchMessageToChatContent(userPrompt); - - await SpecialPowers.spawn(browser, [], async () => { - const contentEl = content.document.querySelector("ai-chat-content"); - await contentEl.updateComplete; - - let loaderEl; - await ContentTaskUtils.waitForMutationCondition( - contentEl.shadowRoot, - { childList: true, subtree: true }, - () => { - loaderEl = contentEl.shadowRoot.querySelector( - "chat-assistant-loader" - ); - return loaderEl; - } - ); - Assert.ok(loaderEl, "Loader element exists"); - - const inner = loaderEl.shadowRoot?.querySelector( - ".chat-assistant-loader" - ); - Assert.ok(inner, "Loader has the correct content"); - }); - }); - - await SpecialPowers.popPrefEnv(); -}); From a3689c31dc3174bc4c93872a89d4b0634a96c7cd Mon Sep 17 00:00:00 2001 From: Sandor Molnar Date: Fri, 6 Feb 2026 18:44:07 +0200 Subject: [PATCH 10/93] Revert "Bug 2014616: Always create the Profile Groups folder in case it is missing. r=profiles-reviewers,jhirsch" for causing multiple failures This reverts commit 214a01a4ec0caf0edbe53c50e34dd98f48832650. --- .../profiles/tests/unit/test_fail_recover_storeID.js | 4 ++-- toolkit/profile/ProfilesDatastoreService.sys.mjs | 12 ++++++------ 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/browser/components/profiles/tests/unit/test_fail_recover_storeID.js b/browser/components/profiles/tests/unit/test_fail_recover_storeID.js index 4d4f4c8a9e873..edd980221b17c 100644 --- a/browser/components/profiles/tests/unit/test_fail_recover_storeID.js +++ b/browser/components/profiles/tests/unit/test_fail_recover_storeID.js @@ -14,8 +14,8 @@ add_task(async function test_recover_storeID() { await SelectableProfileService.init(); Assert.ok( - ProfilesDatastoreService.initialized, - "Initialized the datastore service" + !ProfilesDatastoreService.initialized, + "Didn't initialize the datastore service" ); Assert.ok( !SelectableProfileService.initialized, diff --git a/toolkit/profile/ProfilesDatastoreService.sys.mjs b/toolkit/profile/ProfilesDatastoreService.sys.mjs index 4a5e912b0ea54..e572969143f62 100644 --- a/toolkit/profile/ProfilesDatastoreService.sys.mjs +++ b/toolkit/profile/ProfilesDatastoreService.sys.mjs @@ -521,11 +521,15 @@ class ProfilesDatastoreServiceClass { this.#connection = null; } - maybeCreateStoreID() { + async maybeCreateProfilesStorePath() { if (this.#storeID) { return; } + await IOUtils.makeDirectory( + ProfilesDatastoreServiceClass.PROFILE_GROUPS_DIR + ); + const storageID = Services.uuid .generateUUID() .toString() @@ -537,7 +541,7 @@ class ProfilesDatastoreServiceClass { } async getProfilesStorePath() { - this.maybeCreateStoreID(); + await this.maybeCreateProfilesStorePath(); // If we are not running in a named nsIToolkitProfile, the datastore path // should be in the profile directory. This is true in a local build or a @@ -549,10 +553,6 @@ class ProfilesDatastoreServiceClass { ); } - await IOUtils.makeDirectory( - ProfilesDatastoreServiceClass.PROFILE_GROUPS_DIR - ); - return getSharedProfilesStorePath(this.#storeID); } } From 93dfeeec2fac835cfc496b85216cdf192728a0bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Emilio=20Cobos=20=C3=81lvarez?= Date: Fri, 6 Feb 2026 16:02:46 +0000 Subject: [PATCH 11/93] Bug 2013011 - Check implicit anchor validity. r=layout-anchor-positioning-reviewers,layout-reviewers,dshin The invoker can be anywhere in the dom, have to confirm that it's an acceptable anchor. I _think_ pseudo elements should always pass this check but worth doing regardless. This also fixes bug 2013896, while at it, gotta add a test for that, if this doesn't cause any progression. Differential Revision: https://phabricator.services.mozilla.com/D282173 --- layout/base/AnchorPositioningUtils.cpp | 80 ++++++++++--------- ...self-referencing-invoker-anchor-crash.html | 17 ++++ 2 files changed, 61 insertions(+), 36 deletions(-) create mode 100644 testing/web-platform/tests/css/css-anchor-position/self-referencing-invoker-anchor-crash.html diff --git a/layout/base/AnchorPositioningUtils.cpp b/layout/base/AnchorPositioningUtils.cpp index 09da39cbba1fa..73eedd7b7e61b 100644 --- a/layout/base/AnchorPositioningUtils.cpp +++ b/layout/base/AnchorPositioningUtils.cpp @@ -457,7 +457,7 @@ class LazyAncestorHolder { }; bool IsAcceptableAnchorElement( - const nsIFrame* aPossibleAnchorFrame, const ScopedNameRef& aName, + const nsIFrame* aPossibleAnchorFrame, const ScopedNameRef* aName, const nsIFrame* aPositionedFrame, LazyAncestorHolder& aPositionedFrameAncestorHolder) { MOZ_ASSERT(aPossibleAnchorFrame); @@ -476,14 +476,23 @@ bool IsAcceptableAnchorElement( // The phrase "element or a fully styleable tree-abiding pseudo-element" // used by the spec is taken to mean // "either not a pseudo-element or a pseudo-element of a specific kind". - return (IsFullyStyleableTreeAbidingOrNotPseudoElement(aPossibleAnchorFrame) && - IsAnchorLaidOutStrictlyBeforeElement( - aPossibleAnchorFrame, aPositionedFrame, - aPositionedFrameAncestorHolder.GetAncestors()) && - IsAnchorInScopeForPositionedElement(aName, aPossibleAnchorFrame, - aPositionedFrame) && - IsPositionedElementAlsoSkippedWhenAnchorIsSkipped( - aPossibleAnchorFrame, aPositionedFrame)); + if (!IsFullyStyleableTreeAbidingOrNotPseudoElement(aPossibleAnchorFrame)) { + return false; + } + if (!IsAnchorLaidOutStrictlyBeforeElement( + aPossibleAnchorFrame, aPositionedFrame, + aPositionedFrameAncestorHolder.GetAncestors())) { + return false; + } + if (aName && !IsAnchorInScopeForPositionedElement( + *aName, aPossibleAnchorFrame, aPositionedFrame)) { + return false; + } + if (!IsPositionedElementAlsoSkippedWhenAnchorIsSkipped(aPossibleAnchorFrame, + aPositionedFrame)) { + return false; + } + return true; } } // namespace @@ -548,7 +557,7 @@ nsIFrame* AnchorPositioningUtils::FindFirstAcceptableAnchor( } // Check if the possible anchor is an acceptable anchor element. - if (IsAcceptableAnchorElement(*it, aName, aPositionedFrame, + if (IsAcceptableAnchorElement(*it, &aName, aPositionedFrame, positionedFrameAncestorHolder)) { return *it; } @@ -942,42 +951,41 @@ Maybe AnchorPositioningUtils::GetUsedAnchorName( return Nothing{}; } -auto AnchorPositioningUtils::GetAnchorPosImplicitAnchor(const nsIFrame* aFrame) - -> ImplicitAnchorResult { +static std::pair +GetImplicitAnchorContent(const nsIFrame* aFrame) { const auto* element = dom::Element::FromNodeOrNull(aFrame->GetContent()); - if (!aFrame->Style()->IsPseudoElement() && !element) { + if (!element) [[unlikely]] { return {}; } - - if (element) [[likely]] { - if (const dom::PopoverData* popoverData = element->GetPopoverData()) - [[unlikely]] { - if (const RefPtr& invoker = popoverData->GetInvoker()) { - return {invoker->GetPrimaryFrame(), ImplicitAnchorKind::Popover}; - } + if (const auto* popoverData = element->GetPopoverData()) [[unlikely]] { + if (RefPtr invoker = popoverData->GetInvoker()) { + return {invoker.get(), + AnchorPositioningUtils::ImplicitAnchorKind::Popover}; } } - - const auto* pseudoRoot = aFrame->GetClosestNativeAnonymousSubtreeRoot(); - if (!pseudoRoot) { + if (!aFrame->Style()->IsPseudoElement()) { return {}; } + return {element->GetClosestNativeAnonymousSubtreeRootParentOrHost(), + AnchorPositioningUtils::ImplicitAnchorKind::PseudoElement}; +} - auto* pseudoRootFrame = pseudoRoot->GetPrimaryFrame(); - if (!pseudoRootFrame) { +auto AnchorPositioningUtils::GetAnchorPosImplicitAnchor(const nsIFrame* aFrame) + -> ImplicitAnchorResult { + auto [implicitAnchor, kind] = GetImplicitAnchorContent(aFrame); + if (!implicitAnchor) { return {}; } - - // FIXME(emilio, bug 2013896): Is this really right? It's just returning the - // in-flow parent of the pseudo-element, but - // GetClosestNativeAnonymousSubtreeRootParent()'s primary frame seems most - // likely to be the intended thing in presence of anonymous boxes like - // fieldsets and so on... - auto* implicitAnchor = - pseudoRootFrame->HasAnyStateBits(NS_FRAME_OUT_OF_FLOW) - ? pseudoRootFrame->GetPlaceholderFrame()->GetParent() - : pseudoRootFrame->GetParent(); - return {implicitAnchor, ImplicitAnchorKind::PseudoElement}; + auto* anchorFrame = implicitAnchor->GetPrimaryFrame(); + if (!anchorFrame) { + return {}; + } + LazyAncestorHolder ancestorHolder(aFrame); + if (!IsAcceptableAnchorElement(anchorFrame, /* aName = */ nullptr, aFrame, + ancestorHolder)) { + return {}; + } + return {anchorFrame, kind}; } AnchorPositioningUtils::ContainingBlockInfo diff --git a/testing/web-platform/tests/css/css-anchor-position/self-referencing-invoker-anchor-crash.html b/testing/web-platform/tests/css/css-anchor-position/self-referencing-invoker-anchor-crash.html new file mode 100644 index 0000000000000..d8e028e19f84f --- /dev/null +++ b/testing/web-platform/tests/css/css-anchor-position/self-referencing-invoker-anchor-crash.html @@ -0,0 +1,17 @@ + + + + From c77441ec61a03fc1b77e4c1044b8eda6f95101bb Mon Sep 17 00:00:00 2001 From: Diego Escalante Date: Fri, 6 Feb 2026 16:09:11 +0000 Subject: [PATCH 12/93] Bug 1986639 - Invalidate elements with styles that reference custom attributes and update wpt expectations. r=emilio,firefox-style-system-reviewers Differential Revision: https://phabricator.services.mozilla.com/D281793 --- layout/style/RestyleManager.cpp | 3 +++ servo/ports/geckolib/glue.rs | 16 ++++++++++++++++ .../css/css-values/attr-invalidation.html.ini | 3 --- .../attr-pseudo-elem-invalidation.html.ini | 12 ------------ 4 files changed, 19 insertions(+), 15 deletions(-) delete mode 100644 testing/web-platform/meta/css/css-values/attr-invalidation.html.ini delete mode 100644 testing/web-platform/meta/css/css-values/attr-pseudo-elem-invalidation.html.ini diff --git a/layout/style/RestyleManager.cpp b/layout/style/RestyleManager.cpp index 3c74b95b176cc..fb31c7268fb56 100644 --- a/layout/style/RestyleManager.cpp +++ b/layout/style/RestyleManager.cpp @@ -3489,6 +3489,9 @@ void RestyleManager::AttributeWillChange(Element* aElement, int32_t aNameSpaceID, nsAtom* aAttribute, AttrModType aModType) { + if (Servo_Element_ReferencesAttribute(aElement, aAttribute)) { + PostRestyleEvent(aElement, RestyleHint::RECASCADE_SELF, nsChangeHint(0)); + } TakeSnapshotForAttributeChange(*aElement, aNameSpaceID, aAttribute); } diff --git a/servo/ports/geckolib/glue.rs b/servo/ports/geckolib/glue.rs index 2142bc34ed095..23479b822cb8c 100644 --- a/servo/ports/geckolib/glue.rs +++ b/servo/ports/geckolib/glue.rs @@ -1586,6 +1586,22 @@ pub extern "C" fn Servo_Element_MayHaveStartingStyle(element: &RawGeckoElement) .contains(data::ElementDataFlags::MAY_HAVE_STARTING_STYLE) } +#[no_mangle] +pub extern "C" fn Servo_Element_ReferencesAttribute( + element: &RawGeckoElement, + attr: *const nsAtom, +) -> bool { + let element = GeckoElement(element); + let Some(data) = element.borrow_data() else { + return false; + }; + let Some(ref attrs) = data.styles.primary().attribute_references else { + return false; + }; + + unsafe { Atom::with(attr, |attr| attrs.contains(AtomIdent::cast(attr))) } +} + fn mode_to_origin(mode: SheetParsingMode) -> Origin { match mode { SheetParsingMode::eAuthorSheetFeatures => Origin::Author, diff --git a/testing/web-platform/meta/css/css-values/attr-invalidation.html.ini b/testing/web-platform/meta/css/css-values/attr-invalidation.html.ini deleted file mode 100644 index 111ec1b397568..0000000000000 --- a/testing/web-platform/meta/css/css-values/attr-invalidation.html.ini +++ /dev/null @@ -1,3 +0,0 @@ -[attr-invalidation.html] - [CSS Values and Units Test: attr() invalidation] - expected: FAIL diff --git a/testing/web-platform/meta/css/css-values/attr-pseudo-elem-invalidation.html.ini b/testing/web-platform/meta/css/css-values/attr-pseudo-elem-invalidation.html.ini deleted file mode 100644 index 167cc146d9a2d..0000000000000 --- a/testing/web-platform/meta/css/css-values/attr-pseudo-elem-invalidation.html.ini +++ /dev/null @@ -1,12 +0,0 @@ -[attr-pseudo-elem-invalidation.html] - [CSS Values and Units Test: attr() invalidation of pseudo elements 1] - expected: FAIL - - [CSS Values and Units Test: attr() invalidation of pseudo elements 3] - expected: FAIL - - [CSS Values and Units Test: attr() invalidation of pseudo elements 4] - expected: FAIL - - [CSS Values and Units Test: attr() invalidation of pseudo elements 5] - expected: FAIL From c4a43cb42238b866ad5aa0ba5107f4e537b8cff3 Mon Sep 17 00:00:00 2001 From: Chidam Gopal Date: Fri, 6 Feb 2026 16:18:14 +0000 Subject: [PATCH 13/93] Bug 2010062 - set message length threshold for memory generation from conversation r=cdipersio,ai-models-reviewers Differential Revision: https://phabricator.services.mozilla.com/D282061 --- .../aiwindow/models/memories/MemoriesChatSource.sys.mjs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/browser/components/aiwindow/models/memories/MemoriesChatSource.sys.mjs b/browser/components/aiwindow/models/memories/MemoriesChatSource.sys.mjs index 36cbb5166a854..0eb8098f4188c 100644 --- a/browser/components/aiwindow/models/memories/MemoriesChatSource.sys.mjs +++ b/browser/components/aiwindow/models/memories/MemoriesChatSource.sys.mjs @@ -15,6 +15,7 @@ import { BlockListManager } from "chrome://global/content/ml/Utils.sys.mjs"; // Chat fetch defaults const DEFAULT_MAX_RESULTS = 50; const DEFAULT_HALF_LIFE_DAYS = 7; +const MESSAGE_LENGTH_THRESHOLD = 1000; const MS_PER_SEC = 1_000; const SEC_PER_MIN = 60; const MINS_PER_HOUR = 60; @@ -71,10 +72,14 @@ export async function getRecentChats( const chatMessages = filtered.map(msg => { const createdDate = msg.createdDate; const freshness_score = computeFreshnessScore(createdDate, halfLifeDays); + let content = msg.content?.body ?? null; + if (content && content.length > MESSAGE_LENGTH_THRESHOLD) { + content = content.substring(0, MESSAGE_LENGTH_THRESHOLD); + } return { createdDate, role: msg.role, - content: msg.content?.body ?? null, + content, pageUrl: msg.pageUrl, freshness_score, }; From 8522c4315d082e05f4f71fb8644957f6e6b373b9 Mon Sep 17 00:00:00 2001 From: Chidam Gopal Date: Fri, 6 Feb 2026 16:19:10 +0000 Subject: [PATCH 14/93] Bug 2014320 - Memories generation to handle bigger inputs r=cdipersio,ai-models-reviewers Differential Revision: https://phabricator.services.mozilla.com/D281706 --- .../memories/MemoriesHistoryScheduler.sys.mjs | 2 +- .../memories/MemoriesHistorySource.sys.mjs | 31 ++++- .../models/memories/MemoriesManager.sys.mjs | 121 +++++++++++++++++- .../xpcshell/test_MemoriesHistorySource.js | 118 +++++++++++++++++ 4 files changed, 266 insertions(+), 6 deletions(-) diff --git a/browser/components/aiwindow/models/memories/MemoriesHistoryScheduler.sys.mjs b/browser/components/aiwindow/models/memories/MemoriesHistoryScheduler.sys.mjs index 37b2770cbfb31..93d1e0e3c3442 100644 --- a/browser/components/aiwindow/models/memories/MemoriesHistoryScheduler.sys.mjs +++ b/browser/components/aiwindow/models/memories/MemoriesHistoryScheduler.sys.mjs @@ -29,7 +29,7 @@ ChromeUtils.defineLazyGetter(lazy, "console", function () { const INITIAL_MEMORIES_PAGES_THRESHOLD = 0; // Only run if at least this many pages have been visited. -const MEMORIES_SCHEDULER_PAGES_THRESHOLD = 2; +const MEMORIES_SCHEDULER_PAGES_THRESHOLD = 30; // Memories history schedule every 2 mins const MEMORIES_SCHEDULER_INTERVAL_MS = 2 * 60 * 1000; diff --git a/browser/components/aiwindow/models/memories/MemoriesHistorySource.sys.mjs b/browser/components/aiwindow/models/memories/MemoriesHistorySource.sys.mjs index 00dcd5f21b479..eb57cd1611962 100644 --- a/browser/components/aiwindow/models/memories/MemoriesHistorySource.sys.mjs +++ b/browser/components/aiwindow/models/memories/MemoriesHistorySource.sys.mjs @@ -212,7 +212,8 @@ export async function getRecentHistory(opts = {}) { const onlyTitle = row.getResultByName("title") || ""; let title; if (onlyTitle) { - title = onlyTitle + " | " + host; + const sanitizedTitle = sanitizeTitle(onlyTitle); + title = sanitizedTitle + " | " + host; } else { title = onlyTitle; } @@ -761,11 +762,39 @@ function round2(x) { return Math.round(Number(x) * 100) / 100; } +/** + * Sanitize title text to prevent JSON parsing issues in LLM outputs. + * Removes/replaces characters that commonly cause problems: + * - Backslashes (replaced with forward slashes) + * - Control characters (replaced with spaces) + * + * @param {string} title - Raw title from Places database + * @returns {string} - Sanitized title + */ +function sanitizeTitle(title) { + if (typeof title !== "string") { + return ""; + } + + return ( + title + .replace(/\\/g, "/") // Replace backslash with forward slash + // eslint-disable-next-line no-control-regex + .replace(/[\x00-\x1F\x7F]/g, " ") // Replace control chars (0-31, 127) with space + .replace(/\s+/g, " ") // Collapse multiple spaces + .trim() + ); +} + // for tests only export function _setBlockListManagerForTesting(mgr) { _mgr = mgr; } +export function _sanitizeTitleForTesting(title) { + return sanitizeTitle(title); +} + /** * Return the number of history visits since `days` ago. * diff --git a/browser/components/aiwindow/models/memories/MemoriesManager.sys.mjs b/browser/components/aiwindow/models/memories/MemoriesManager.sys.mjs index 0af1f751202ce..ef8bae1eae226 100644 --- a/browser/components/aiwindow/models/memories/MemoriesManager.sys.mjs +++ b/browser/components/aiwindow/models/memories/MemoriesManager.sys.mjs @@ -37,12 +37,16 @@ import { EveryWindow } from "resource:///modules/EveryWindow.sys.mjs"; import { AIWindowAccountAuth } from "moz-src:///browser/components/aiwindow/ui/modules/AIWindowAccountAuth.sys.mjs"; const K_DOMAINS_FULL = 100; -const K_TITLES_FULL = 60; +const K_TITLES_FULL = 100; const K_SEARCHES_FULL = 10; + const K_DOMAINS_DELTA = 30; const K_TITLES_DELTA = 60; const K_SEARCHES_DELTA = 10; +// for initial memory generation batches +const TOKEN_BUDGET = 2000; + const DEFAULT_HISTORY_FULL_LOOKUP_DAYS = 60; const DEFAULT_HISTORY_FULL_MAX_RESULTS = 3000; const DEFAULT_HISTORY_DELTA_MAX_RESULTS = 500; @@ -240,10 +244,24 @@ export class MemoriesManager { return []; } - return await this.generateAndSaveMemoriesFromSources( - sources, - SOURCE_HISTORY + const batches = this._createHistoryBatches( + domainItems, + titleItems, + searchItems, + TOKEN_BUDGET ); + + const allGeneratedMemories = []; + for (let i = 0; i < batches.length; i++) { + const batchSources = { history: batches[i] }; + const batchMemories = await this.generateAndSaveMemoriesFromSources( + batchSources, + SOURCE_HISTORY + ); + allGeneratedMemories.push(...batchMemories); + } + + return allGeneratedMemories; } /** @@ -600,4 +618,99 @@ export class MemoriesManager { static async countRecentVisits(opts = {}) { return await countRecentVisits(opts); } + + // Helper: Estimate token count for history items + static _estimateHistoryTokens(domainItems, titleItems, searchItems) { + let chars = 0; + + // Domains: "domain.com,99.5\n" + chars += domainItems.reduce( + (sum, [domain, _score]) => sum + domain.length + 10, + 0 + ); + + // Titles: "Long Title | domain.com,99.5\n" + chars += titleItems.reduce( + (sum, [title, _score]) => sum + title.length + 10, + 0 + ); + + // Searches: can have multiple queries per item + chars += searchItems.reduce( + (sum, item) => sum + (item.q || []).join(",").length + 20, + 0 + ); + + // CSV headers and formatting overhead + chars += 1000; + + // Rough conversion: 1 token ≈ 4 characters + return Math.ceil(chars / 4); + } + + // Helper: Split history items into token-budget-compliant batches + static _createHistoryBatches( + domainItems, + titleItems, + searchItems, + tokenBudget + ) { + const batches = []; + + // Calculate how many items per batch based on average item size + const totalItems = + domainItems.length + titleItems.length + searchItems.length; + const avgTokensPerItem = + this._estimateHistoryTokens(domainItems, titleItems, searchItems) / + totalItems; + + const itemsPerBatch = Math.max( + 10, // Minimum batch size + Math.floor((tokenBudget * 0.9) / avgTokensPerItem) // 0.9 for safety margin + ); + + // Calculate proportional splits + const domainRatio = domainItems.length / totalItems; + const titleRatio = titleItems.length / totalItems; + const searchRatio = searchItems.length / totalItems; + + const domainsPerBatch = Math.ceil(itemsPerBatch * domainRatio); + const titlesPerBatch = Math.ceil(itemsPerBatch * titleRatio); + const searchesPerBatch = Math.ceil(itemsPerBatch * searchRatio); + + let domainIdx = 0; + let titleIdx = 0; + let searchIdx = 0; + + while ( + domainIdx < domainItems.length || + titleIdx < titleItems.length || + searchIdx < searchItems.length + ) { + const batchDomains = domainItems.slice( + domainIdx, + domainIdx + domainsPerBatch + ); + const batchTitles = titleItems.slice(titleIdx, titleIdx + titlesPerBatch); + const batchSearches = searchItems.slice( + searchIdx, + searchIdx + searchesPerBatch + ); + + // Only add batch if it has content + if ( + !!batchDomains.length || + !!batchTitles.length || + batchSearches.length + ) { + batches.push([batchDomains, batchTitles, batchSearches]); + } + + domainIdx += domainsPerBatch; + titleIdx += titlesPerBatch; + searchIdx += searchesPerBatch; + } + + return batches; + } } diff --git a/browser/components/aiwindow/models/tests/xpcshell/test_MemoriesHistorySource.js b/browser/components/aiwindow/models/tests/xpcshell/test_MemoriesHistorySource.js index 9dd64df1df4e2..5c96a2b776b79 100644 --- a/browser/components/aiwindow/models/tests/xpcshell/test_MemoriesHistorySource.js +++ b/browser/components/aiwindow/models/tests/xpcshell/test_MemoriesHistorySource.js @@ -8,6 +8,7 @@ const { generateProfileInputs, aggregateSessions, topkAggregates, + _sanitizeTitleForTesting, } = ChromeUtils.importESModule( "moz-src:///browser/components/aiwindow/models/memories/MemoriesHistorySource.sys.mjs" ); @@ -759,3 +760,120 @@ add_task(function test_topkAggregates_recency_and_ranking() { ); Assert.equal(secondDomain, "old.com", "Older domain comes second"); }); + +add_task(function test_sanitizeTitle_basic() { + // Normal title - should pass through unchanged + const normal = "Example Page Title"; + Assert.equal( + _sanitizeTitleForTesting(normal), + normal, + "Normal title unchanged" + ); +}); + +add_task(function test_sanitizeTitle_backslash() { + // Backslash - primary issue that caused JSON parse failures + const withBackslash = "Claude's new constitution \\ Anthropic"; + const expected = "Claude's new constitution / Anthropic"; + Assert.equal( + _sanitizeTitleForTesting(withBackslash), + expected, + "Backslash replaced with forward slash" + ); + + // Multiple backslashes + const multipleBackslashes = "Path\\to\\file\\name.txt"; + const expectedMultiple = "Path/to/file/name.txt"; + Assert.equal( + _sanitizeTitleForTesting(multipleBackslashes), + expectedMultiple, + "Multiple backslashes replaced" + ); + + // Windows path + const windowsPath = "C:\\Users\\Documents\\file.txt"; + const expectedPath = "C:/Users/Documents/file.txt"; + Assert.equal( + _sanitizeTitleForTesting(windowsPath), + expectedPath, + "Windows path backslashes replaced" + ); +}); + +add_task(function test_sanitizeTitle_control_characters() { + // Newline + const withNewline = "Title\nwith\nnewlines"; + Assert.equal( + _sanitizeTitleForTesting(withNewline), + "Title with newlines", + "Newlines replaced with spaces" + ); + + // Tab + const withTab = "Title\twith\ttabs"; + Assert.equal( + _sanitizeTitleForTesting(withTab), + "Title with tabs", + "Tabs replaced with spaces" + ); + + // Carriage return + const withCarriageReturn = "Title\rwith\rCR"; + Assert.equal( + _sanitizeTitleForTesting(withCarriageReturn), + "Title with CR", + "Carriage returns replaced with spaces" + ); + + // Mixed control characters + const mixed = "Title\n\t\rwith\x00mixed\x1Fcontrols"; + Assert.equal( + _sanitizeTitleForTesting(mixed), + "Title with mixed controls", + "Mixed control characters replaced and collapsed" + ); +}); + +add_task(function test_sanitizeTitle_multiple_spaces() { + // Multiple spaces should collapse to single space + const multipleSpaces = "Title with many spaces"; + Assert.equal( + _sanitizeTitleForTesting(multipleSpaces), + "Title with many spaces", + "Multiple spaces collapsed to single space" + ); + + // Mixed spacing + const mixedSpacing = "Title \t\n with \r mixed spacing"; + Assert.equal( + _sanitizeTitleForTesting(mixedSpacing), + "Title with mixed spacing", + "Mixed spacing collapsed" + ); +}); + +add_task(function test_sanitizeTitle_whitespace_trim() { + // Leading whitespace + const leading = " Title with leading spaces"; + Assert.equal( + _sanitizeTitleForTesting(leading), + "Title with leading spaces", + "Leading whitespace trimmed" + ); + + // Trailing whitespace + const trailing = "Title with trailing spaces "; + Assert.equal( + _sanitizeTitleForTesting(trailing), + "Title with trailing spaces", + "Trailing whitespace trimmed" + ); + + // Both + const both = " Title with both "; + Assert.equal( + _sanitizeTitleForTesting(both), + "Title with both", + "Both leading and trailing whitespace trimmed" + ); +}); From 98c974ba80ae88a5340806defcd85066924847aa Mon Sep 17 00:00:00 2001 From: Christopher DiPersio Date: Fri, 6 Feb 2026 16:20:12 +0000 Subject: [PATCH 15/93] Bug 2014279 - Add new tool to return all saved memories to the assistant r=cgopal,ai-models-reviewers Differential Revision: https://phabricator.services.mozilla.com/D281678 --- .../components/aiwindow/models/Chat.sys.mjs | 2 + .../components/aiwindow/models/Tools.sys.mjs | 22 +++++ .../browser/browser_conversation_stream.js | 90 +++++++++++++++++++ .../models/tests/xpcshell/test_Chat.js | 5 ++ 4 files changed, 119 insertions(+) diff --git a/browser/components/aiwindow/models/Chat.sys.mjs b/browser/components/aiwindow/models/Chat.sys.mjs index bdf04fee9c3ed..be1a82626a656 100644 --- a/browser/components/aiwindow/models/Chat.sys.mjs +++ b/browser/components/aiwindow/models/Chat.sys.mjs @@ -17,6 +17,7 @@ import { searchBrowsingHistory, GetPageContent, RunSearch, + getUserMemories, } from "moz-src:///browser/components/aiwindow/models/Tools.sys.mjs"; const lazy = {}; @@ -44,6 +45,7 @@ Object.assign(Chat, { search_browsing_history: searchBrowsingHistory, get_page_content: GetPageContent.getPageContent, run_search: RunSearch.runSearch.bind(RunSearch), + get_user_memories: getUserMemories, }, /** diff --git a/browser/components/aiwindow/models/Tools.sys.mjs b/browser/components/aiwindow/models/Tools.sys.mjs index 1943ffb16eb7c..2dbe2adbd61e8 100644 --- a/browser/components/aiwindow/models/Tools.sys.mjs +++ b/browser/components/aiwindow/models/Tools.sys.mjs @@ -18,6 +18,8 @@ ChromeUtils.defineESModuleGetters(lazy, { BrowserWindowTracker: "resource:///modules/BrowserWindowTracker.sys.mjs", clearTimeout: "resource://gre/modules/Timer.sys.mjs", setTimeout: "resource://gre/modules/Timer.sys.mjs", + MemoriesManager: + "moz-src:///browser/components/aiwindow/models/memories/MemoriesManager.sys.mjs", // @todo Bug 2009194 // PageDataService: // "moz-src:///browser/components/pagedata/PageDataService.sys.mjs", @@ -27,12 +29,14 @@ const GET_OPEN_TABS = "get_open_tabs"; const SEARCH_BROWSING_HISTORY = "search_browsing_history"; const GET_PAGE_CONTENT = "get_page_content"; const RUN_SEARCH = "run_search"; +const GET_USER_MEMORIES = "get_user_memories"; export const TOOLS = [ GET_OPEN_TABS, SEARCH_BROWSING_HISTORY, GET_PAGE_CONTENT, RUN_SEARCH, + GET_USER_MEMORIES, ]; export const toolsConfig = [ @@ -129,6 +133,18 @@ export const toolsConfig = [ }, }, }, + { + type: "function", + function: { + name: GET_USER_MEMORIES, + description: + 'Retrieves all memories saved about the user to answer questions like "What do you know about me?", "What memories have you saved?", "What do you remember about me?", etc. Respond to the user that these are memories.', + parameters: { + type: "object", + properties: {}, + }, + }, + }, ]; /** @@ -665,3 +681,9 @@ export class GetPageContent { return `Content (${modeLabel}) from ${label}:\n\n${cleanContent}`; } } + +export async function getUserMemories() { + const memories = await lazy.MemoriesManager.getAllMemories(); + + return memories.map(memory => memory.memory_summary); +} diff --git a/browser/components/aiwindow/models/tests/browser/browser_conversation_stream.js b/browser/components/aiwindow/models/tests/browser/browser_conversation_stream.js index e6220324ce3d3..7d69a37e05afa 100644 --- a/browser/components/aiwindow/models/tests/browser/browser_conversation_stream.js +++ b/browser/components/aiwindow/models/tests/browser/browser_conversation_stream.js @@ -12,6 +12,9 @@ const { MESSAGE_ROLE } = ChromeUtils.importESModule( const { Chat } = ChromeUtils.importESModule( "moz-src:///browser/components/aiwindow/models/Chat.sys.mjs" ); +const { MemoryStore } = ChromeUtils.importESModule( + "moz-src:///browser/components/aiwindow/services/MemoryStore.sys.mjs" +); const { PlacesTestUtils } = ChromeUtils.importESModule( "resource://testing-common/PlacesTestUtils.sys.mjs" @@ -249,3 +252,90 @@ add_task(async function test_chat_tool_call_get_page_content() { await new Promise(resolve => pageServer.stop(resolve)); } }); + +add_task(async function test_chat_tool_call_get_user_memories() { + // Clear existing memories + const preTestMemories = await MemoryStore.getMemories({ + includeSoftDeleted: true, + }); + for (const memory of preTestMemories) { + await MemoryStore.hardDeleteMemory(memory.id); + } + + // Add temp test memories + const testMemories = [ + { + memory_summary: "Loves drinking coffee", + category: "Food & Drink", + intent: "Plan / Organize", + score: 3, + }, + { + memory_summary: "Buys dog food online", + category: "Pets & Animals", + intent: "Buy / Acquire", + score: 4, + }, + ]; + for (const memory of testMemories) { + await MemoryStore.addMemory(memory); + } + + try { + await withServer( + { + toolCall: { + name: "get_user_memories", + args: "{}", + }, + followupChunks: ["Memories ready."], + }, + async () => { + const conversation = new ChatConversation({ + title: "chat title", + description: "chat desc", + pageUrl: new URL("https://example.com"), + pageMeta: {}, + }); + conversation.addUserMessage( + "Tell me everything you know about me", + "https://example.com", + 0 + ); + + let responseText = ""; + for await (const chunk of Chat.fetchWithHistory(conversation)) { + if (typeof chunk === "string") { + responseText += chunk; + } + } + + Assert.equal( + responseText, + "Memories ready.", + "Assistant should stream follow-up text" + ); + + const toolMessages = conversation.messages.filter( + message => message.role === MESSAGE_ROLE.TOOL + ); + Assert.equal(toolMessages.length, 1, "Tool result recorded"); + const returnedMemories = toolMessages[0].content.body; + info("got memories: " + returnedMemories); + Assert.deepEqual( + returnedMemories, + ["Loves drinking coffee", "Buys dog food online"], + "Memories tool returns should match the expected set" + ); + } + ); + } finally { + // Clear temp memories + const postTestMemories = await MemoryStore.getMemories({ + includeSoftDeleted: true, + }); + for (const memory of postTestMemories) { + await MemoryStore.hardDeleteMemory(memory.id); + } + } +}); diff --git a/browser/components/aiwindow/models/tests/xpcshell/test_Chat.js b/browser/components/aiwindow/models/tests/xpcshell/test_Chat.js index f8ca82a4af83b..08742d389fb4a 100644 --- a/browser/components/aiwindow/models/tests/xpcshell/test_Chat.js +++ b/browser/components/aiwindow/models/tests/xpcshell/test_Chat.js @@ -50,6 +50,11 @@ add_task(async function test_Chat_real_tools_are_registered() { "function", "get_page_content should be registered in toolMap" ); + Assert.strictEqual( + typeof Chat.toolMap.get_user_memories, + "function", + "get_user_memories should be registered in the toolMap" + ); }); add_task( From 56c551217d512335adda6486c06b558510293cc2 Mon Sep 17 00:00:00 2001 From: Maxx Crawford Date: Fri, 6 Feb 2026 17:07:44 +0000 Subject: [PATCH 16/93] Bug 2010597 - Add unified widget telemetry metrics. r=home-newtab-reviewers,reemhamz,nina-py Adds four new unified Glean metrics to replace per-widget telemetry: - widgets_impression: Records when any widget is visible - widgets_user_event: Records user interactions with any widget - widgets_enabled: Records when widgets are enabled/disabled - widgets_error: Records when widgets encounter an error Differential Revision: https://phabricator.services.mozilla.com/D281043 --- browser/components/newtab/metrics.yaml | 116 +++++++++++++++++++++++++ 1 file changed, 116 insertions(+) diff --git a/browser/components/newtab/metrics.yaml b/browser/components/newtab/metrics.yaml index 71e087872772a..db379b4d25447 100644 --- a/browser/components/newtab/metrics.yaml +++ b/browser/components/newtab/metrics.yaml @@ -1169,6 +1169,122 @@ newtab: send_in_pings: - newtab + widgets_impression: + type: event + description: > + Recorded when any widget is viewable on a user's screen. + This metric replaces widgets_lists_impression, widgets_timer_impression, + and weather_impression. + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=2010597 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=2010597 + data_sensitivity: + - interaction + notification_emails: + - mcrawford@mozilla.com + expires: never + extra_keys: + newtab_visit_id: *newtab_visit_id + widget_name: &widget_name + description: > + Name of the widget that emits the event (e.g., lists, focus_timer, weather). + type: string + widget_size: &widget_size + description: > + Size of widget (e.g., mini, small, medium). + type: string + send_in_pings: + - newtab + + widgets_user_event: + type: event + description: > + Recorded when user interacts with any widget. + This metric replaces widgets_lists_user_event, widgets_timer_user_event, + weather_detect_location, weather_opt_in_selection, weather_location_selected, + weather_open_provider_url and weather_change_display. + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=2010597 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=2010597 + data_sensitivity: + - interaction + notification_emails: + - mcrawford@mozilla.com + expires: never + extra_keys: + newtab_visit_id: *newtab_visit_id + widget_name: *widget_name + widget_source: &widget_source + description: > + Where the event originated (e.g., widget, context_menu, customize_panel). + type: string + widget_size: *widget_size + user_action: + description: > + Specific action taken by different widgets (e.g., list_copy, list_create, + timer_set, timer_play, timer_pause, change_location, detect_location). + type: string + action_value: + description: > + Optional value associated with action (string representation). + type: string + send_in_pings: + - newtab + + widgets_enabled: + type: event + description: > + Recorded when a widget is enabled or disabled. + This metric replaces widgets_lists_change_display, widgets_timer_change_display, + and weather_enabled. + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=2010597 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=2010597 + data_sensitivity: + - technical + notification_emails: + - mcrawford@mozilla.com + expires: never + extra_keys: + newtab_visit_id: *newtab_visit_id + widget_name: *widget_name + widget_source: *widget_source + widget_size: *widget_size + enabled: + description: > + Whether widget is now enabled (true) or disabled (false). + type: boolean + send_in_pings: + - newtab + + widgets_error: + type: event + description: > + Recorded when a widget encounters an error state (e.g., failed to load data). + This metric replaces weather_load_error. + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=2010597 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=2010597 + data_sensitivity: + - technical + notification_emails: + - mcrawford@mozilla.com + expires: never + extra_keys: + newtab_visit_id: *newtab_visit_id + widget_name: *widget_name + widget_size: *widget_size + error_type: + description: > + Type of error encountered (e.g., load_error, network_error, timeout). + type: string + send_in_pings: + - newtab + spons_nav_traffic_sent: type: memory_distribution memory_unit: kilobyte From 14f98623dbdbdb5a372b5699a0eafa22fa6bfcb7 Mon Sep 17 00:00:00 2001 From: Maxx Crawford Date: Fri, 6 Feb 2026 17:07:44 +0000 Subject: [PATCH 17/93] Bug 2012824 - Update widget telemetry events to pass the correct value to newtab_visit_id. r=home-newtab-reviewers,nbarrett The handlePromoCardUserEvent and handleWidgetsUserEvent methods were incorrectly accessing session.visit_id, which doesn't exist. The correct property is session.session_id. This bug caused these telemetry events to send undefined for the newtab_visit_id field. Differential Revision: https://phabricator.services.mozilla.com/D281044 --- browser/extensions/newtab/lib/TelemetryFeed.sys.mjs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/browser/extensions/newtab/lib/TelemetryFeed.sys.mjs b/browser/extensions/newtab/lib/TelemetryFeed.sys.mjs index 2d2f226aa740f..16203457d176b 100644 --- a/browser/extensions/newtab/lib/TelemetryFeed.sys.mjs +++ b/browser/extensions/newtab/lib/TelemetryFeed.sys.mjs @@ -1597,7 +1597,7 @@ export class TelemetryFeed { const session = this.sessions.get(au.getPortIdOfSender(action)); if (session) { const payload = { - newtab_visit_id: session.visit_id, + newtab_visit_id: session.session_id, }; switch (action.type) { @@ -1618,7 +1618,7 @@ export class TelemetryFeed { const session = this.sessions.get(au.getPortIdOfSender(action)); if (session) { const payload = { - newtab_visit_id: session.visit_id, + newtab_visit_id: session.session_id, }; switch (action.type) { case "WIDGETS_LISTS_USER_EVENT": From 3010d7e98f604aa06c3350e5dab01ccb4d0a8aa6 Mon Sep 17 00:00:00 2001 From: Maxx Crawford Date: Fri, 6 Feb 2026 17:07:44 +0000 Subject: [PATCH 18/93] Bug 2012838 - Fix data-eventSource attributes in ContentSection to use kebab-case. r=home-newtab-reviewers,reemhamz Differential Revision: https://phabricator.services.mozilla.com/D281045 --- .../ContentSection/ContentSection.jsx | 14 +++++++------- .../newtab/data/content/activity-stream.bundle.js | 14 +++++++------- .../CustomizeMenu/ContentSection.test.jsx | 10 +++++----- 3 files changed, 19 insertions(+), 19 deletions(-) diff --git a/browser/extensions/newtab/content-src/components/CustomizeMenu/ContentSection/ContentSection.jsx b/browser/extensions/newtab/content-src/components/CustomizeMenu/ContentSection/ContentSection.jsx index 8ed0141597d52..9d6cc90de4105 100644 --- a/browser/extensions/newtab/content-src/components/CustomizeMenu/ContentSection/ContentSection.jsx +++ b/browser/extensions/newtab/content-src/components/CustomizeMenu/ContentSection/ContentSection.jsx @@ -152,7 +152,7 @@ export class ContentSection extends React.PureComponent { pressed={weatherEnabled || null} onToggle={this.onPreferenceSelect} data-preference="showWeather" - data-eventSource="WEATHER" + data-event-source="WEATHER" data-l10n-id="newtab-custom-widget-weather-toggle" /> @@ -166,7 +166,7 @@ export class ContentSection extends React.PureComponent { pressed={listsEnabled || null} onToggle={this.onPreferenceSelect} data-preference="widgets.lists.enabled" - data-eventSource="WIDGET_LISTS" + data-event-source="WIDGET_LISTS" data-l10n-id="newtab-custom-widget-lists-toggle" /> @@ -180,7 +180,7 @@ export class ContentSection extends React.PureComponent { pressed={timerEnabled || null} onToggle={this.onPreferenceSelect} data-preference="widgets.focusTimer.enabled" - data-eventSource="WIDGET_TIMER" + data-event-source="WIDGET_TIMER" data-l10n-id="newtab-custom-widget-timer-toggle" /> @@ -198,7 +198,7 @@ export class ContentSection extends React.PureComponent { pressed={weatherEnabled || null} onToggle={this.onPreferenceSelect} data-preference="showWeather" - data-eventSource="WEATHER" + data-event-source="WEATHER" data-l10n-id="newtab-custom-weather-toggle" /> @@ -210,7 +210,7 @@ export class ContentSection extends React.PureComponent { pressed={topSitesEnabled || null} onToggle={this.onPreferenceSelect} data-preference="feeds.topsites" - data-eventSource="TOP_SITES" + data-event-source="TOP_SITES" data-l10n-id="newtab-custom-shortcuts-toggle" >
@@ -264,7 +264,7 @@ export class ContentSection extends React.PureComponent { onToggle={this.onPreferenceSelect} aria-describedby="custom-pocket-subtitle" data-preference="feeds.section.topstories" - data-eventSource="TOP_STORIES" + data-event-source="TOP_STORIES" {...(mayHaveInferredPersonalization ? { "data-l10n-id": @@ -291,7 +291,7 @@ export class ContentSection extends React.PureComponent { type="checkbox" onChange={this.onPreferenceSelect} data-preference="discoverystream.sections.personalization.inferred.user.enabled" - data-eventSource="INFERRED_PERSONALIZATION" + data-event-source="INFERRED_PERSONALIZATION" />