Removed obsolete migration logic that forced distribution language packs to be reinstalled when upgrading from Firefox versions older than 67 - Bug 2000797
Thanks to Aloys for contributing the changes needed to cleanup this old XPIProvider migration logic
WebExtensions Framework
Fixed a regression where WebRTC permission popups were queued and suppressed while an extension popup was open - Bug 1982832
WebExtension APIs
Fixed an edge case where tabs.move would revert splitview tabs order while moving splitview tab to a new window - Bug 2028832
Fixed windows API reporting window type normal instead of popup for windows opened via window.open() - Bug 2030631
Thanks to Brandon Lucier for contributing this small but very much appreciated fix to the windows WebExtensions API!
Henrik Skupin landed support for browser.setClientWindowState, originally started by Dan and continued by Liam DeBeasi, with final fixes and improvements completed after earlier contributors were unable to continue.
This also updated some global styling for card, message bar, text input border radiuses/colors and the moz-promo component got a refactor with better support for image styling by default
Jules Simplicio has been updating variable names in the Figma Nova Components/Styles files so our design token naming is consistent between Figma and central (so we can run our import script for more Nova automation)
UX Fundamentals
Felt Privacy error pages now support more NSS errors instead of falling through to the legacy page. Updated introductory text for the denied-port-access error. - 2024150
Fixed a test in browser_aboutCertError.js that was failing on Linux opt standalone and removed the platform skip. - 2028651
Added clock skew detection to the Felt Privacy error pages. When a certificate error is caused by a wrong system clock, the Felt Privacy error pages now show the same dedicated clock-skew message that the legacy error pages had, and helps guide users to correct their system time. - 2025049
Fixed misaligned bullet points in the "What can you do?" section of Felt Privacy network error pages, restoring correct visual indentation for that list. - 2028632
We're delighted that Abigail Besdin has joined Mozilla as our new Chief Operating Officer.
This is an incredibly exciting time for Mozilla. Our focus is to become the world's most trusted software company by building products that let people use the internet openly, safely, and on their terms. As technology changes rapidly, we are working to strengthen the business foundation and infrastructure that champions our mission. Delivering on that ambition takes more than great products; it demands operational rigor. Abigail will lead this effort, demonstrating how values-driven organizations can scale with discipline, speed, and trust in the AI era.
As COO, Abigail will drive company strategy and oversee Mozilla's Core Services teams: Business Operations, Data, Infrastructure, IT, Legal, People, Security, and Strategy. These are the functions that enable us to move quickly and scale with focus. Abigail will sharpen how we plan, prioritize, and execute across the company.
Abigail brings more than 18 years of experience building and scaling high-impact platforms. She co-founded Great Jones, a venture-backed property management startup where she raised $30M, reached $10M in ARR, and led a successful acquisition by Roofstock. At Roofstock, she served as Chief of Staff to the CEO - functioning as an internal COO - where she launched new product lines, closed and integrated two acquisitions, and led the company's strategic planning process.
Earlier in her career, she spent six years at Skillshare, where she launched the company's online learning platform and built its growth and content engines from the ground up.
That combination of founder's instinct and operator's discipline is exactly what Mozilla needs right now. Abigail will report directly to our CEO and join the executive team.
I've learned firsthand that ambitious product goals are only as effective as the operations underpinning them. Mozilla's mission is as big as it gets, and I'm thrilled to lead our Core Services organization to enable rigorous, smart, and quick decision-making across the business. With a powerful execution engine, we can make sure the best of Mozilla's mission materializes.
Abigail Besdin, Chief Operating Officer
Abigail studied Philosophy at NYU, with a focus on Ethics and Mathematical Logic. Born and raised in New York City, she still lives there with her husband and three kids.
Please note some of the information provided in this report may be subject to change as we are sometimes sharing information about projects that are still in early stages and are not final yet.
Welcome!
Are you a locale leader and want us to include new members in our upcoming reports? Contact us!
What's new or coming up in Firefox desktop
Firefox string deadline changes
Starting with 149, some changes in developer deadlines relating to Nightly and Beta have resulted in a slight shift in string translation deadlines, giving us 2 extra days to land strings. Previously deadlines in Pontoon were set to the Sunday ahead of the final Release Candidate but going forward they will be set to a Tuesday. For example the upcoming deadline for Firefox 151 is Tuesday, May 12.
If you're interested to see more details on upcoming Firefox releases and milestones, https://whattrainisitnow.com has all the latest details.
UI Refresh
Behind the scenes a refresh on the visual look of Firefox has been ongoing using the internal name "Nova". You may have seen some blog reports recently on this, or perhaps have been seeing bugs in Bugzilla with this in the title. We will start seeing new strings related to these changes here and there as development work progresses, however we don't expect a large number of string changes stemming from this work.
That being said, these updates also bring some changes in how we communicate directly to our users within Firefox. One of these changes you may have already met: our new mascot Kit. If you missed the announcement give it a read here. You may also notice a shift voice for user directed messages - with source strings becoming more Genuine, Fiery, and Playful. See this recent update in Firefox's brand voice for more details.
Settings redesign
Localization for the update to about:settings has been going on for some time (starting early this year) and the bulk of the translation work is behind us at this point. You may see some new strings (particularly around Privacy & Security) but many of the strings are in a viewable/testable state in Nightly 152. You can check your translations and test out the redesign by typing about:config into your URL bar, proceeding past the warning message, and searching for browser.settings-redesign.enabled and setting the value to true.
What's new or coming up in mobile
Things have been particularly busy on mobile over the past couple of months. For example, Firefox for Android saw a significant spike in April, with the number of new strings increasing to over 200 compared to fewer than 50 in March - more than eight times the typical monthly volume*.
There are two main drivers behind this increase. First, Firefox for Android is introducing a built-in VPN feature, bringing it in line with the functionality already available in Firefox. Second, both iOS and Android teams are working on a new widget for the upcoming 2026 World Cup, allowing users to follow their team directly from the browser.
Given the short turnaround time for this feature, you will notice that many strings are intentionally kept consistent across platforms - and started landing on Desktop as well. We're also pre-landing as many strings as possible, ahead of implementation, to give localizers more time to complete translations.
* Did you know that you can track the number of new strings in a project from the Insights page in Pontoon? Check for example Firefox for Android. In the Translation activity chart, click on New source strings in the legend to display this data. Given the difference in scale, it can also help to hide other metrics to make the chart easier to read.
What's new or coming up in Pontoon
New documentation system. Pontoon now features a brand-new, unified documentation system. This new hub brings together previously scattered resources into a single, streamlined experience, consolidating developer, localizer, and admin documentation from three separate sites into one cohesive platform. By centralizing content, the new system makes it easier to find, navigate, and maintain documentation, ensuring contributors of all roles have quick access to up-to-date and consistent guidance.
Search. You can now set default search options directly in your profile. This allows you to tailor your search without having to adjust filters each time.
The same settings are also applied when using the recently introduced global search page, which brings a major step forward in unifying localization across Mozilla by allowing users to search for strings across all projects and locales in one place. Inspired by Transvision and designed as its successor, the feature integrates deeply with Pontoon, making it easy to filter results, compare translations across languages, and jump directly into the translation workflow.
AI integration. We've also refined the prompt used by the LLM-powered translation feature. The goal is not to change how the feature works, but to make its output more consistent and better aligned with the context available in Pontoon. For example, the updated prompt improves how punctuation is handled, reducing variability in suggestions.
In addition, the prompt now includes more contextual data:
String ID.
Comments, including pinned comments from project managers.
Matches from terminology.
This additional context helps the model generate more relevant suggestions. It also represents a first step toward making LLM suggestions more useful, ahead of potential experiments with displaying them by default alongside suggestions from traditional machine translation.
New contributors. We're also excited to welcome a group of new contributors who have started making an impact on Pontoon over the past few months. MundiaNderi, nishitmistry, dannycolin, first-afk, wassafshahzad, huseynovvusal, and Peacanduck have all contributed valuable improvements across different parts of the project, helping us move faster and improve the overall experience.
A special shoutout goes to Serah (MundiaNderi), who not only made significant contributions but also shared insights into her work in a recent blog post about enhancing comment management in Pontoon-an excellent example of the kind of collaboration and knowledge sharing we love to see in the community.
Newly published localizer facing documentation
As part of the recent documentation update for Pontoon, we've reorganized the content around pretranslation to make it clearer and easier to navigate. There is now a dedicated page outlining the criteria required to enable pretranslation for a locale, along with guidance on how to monitor its effectiveness over time (for example, by tracking metrics like acceptance rate or time to review). If you're a locale manager and want to try pretranslation for your locale, you can request it directly from Pontoon.
Over the past 12 months, we also ran a limited experiment using paid translation agencies for two locales. The goal was to restore the localization level of Firefox for Android in cases where the community was inactive - situations that have since improved, with both communities now active again.
Because volunteer communities remain the foundation of Mozilla's localization model, we wanted to be transparent about when and why this approach was used, and what it means in practice. This includes clarifying how external support fits within a community-driven ecosystem, where localizers retain ownership and responsibility for quality and direction. You can find more details in this page.
Friends of the Lion
Image by Elio Qoshi
We continue the localizer spotlight series this year.
Meet Oliver from China Firefox localizer, accounting student, former Minecraft translator, and Bocchi the Rock! fan He talks about starting with a single typo, why Firefox's independence matters to him, and how the Simplified Chinese community keeps quality high with cross-review and shared responsibility.
Marcelo from Argentina needs no introduction to the localization communities. From Phoenix 0.3 to 24 years later, he shares how he got started, what it meant to be part of the Firefox 1.0 release, his experience as an l10n manager, and why using Mozilla products in his own language - Spanish (Argentina) - continues to motivate him.
What does 18 years of volunteer localization look like? From discovering Firefox and Linux out of curiosity to leading the Portuguese translation team, Cláudio from Portugal reflects on why localization is a form of digital activism, and how every translated word helps build a more inclusive internet.
Baurzhan from Kazakhstan began his localization journey with a simple question: why wasn't Kazakh available in widely used software? That curiosity grew into a long-term commitment to localization, leading to the successful translation of Firefox and many other open source projects. His work demonstrates the power of perseverance in making technology accessible to all.
If you enjoy the series, please help us identify the localizers you'd like to see featured filling out this nomination form. If you have stories to share, tell us in your own words.
Know someone in your l10n community who's been doing a great job and should appear here? Contact us and we'll make sure they get a shout-out!
servoshell is now installed as servoshell or servoshell.exe, rather than servo or servo.exe (@jschwe, @mrobinson, #42958). --userscripts has been removed for now, but anyone who uses it is welcome to reinstate it as a wrapper around UserContentManager::add_script (@jschwe, #43573). We've fixed a bug where link hover status lines are sometimes not legible (@simartin, #43320), and we're working on getting servoshell signed for macOS to avoid getting blocked by Gatekeeper (@jschwe, #42912).
crypto.subtle.deriveBits() for X25519 checking for all-zero secrets, and verify() for HMAC comparing signatures, are now done in constant time (@kkoyung, #43775, #43773).
'Content-Security-Policy' now handles redirects correctly (@TimvdLippe, #43438), and sends violation reports with the correct blockedURI and referrer (@TimvdLippe, #43367, #43645, #43483). The policy in <meta> now combines with the policy sent in HTTP headers, rather than overriding it (@TimvdLippe, @elomscansio, #43063). When checking nonces, we now reject elements with duplicate attributes (@dyegoaurelio, #43216).
The document containing an <iframe> can no longer access the contents of error pages (@TimvdLippe, #43539), and CSP violations inside an <iframe> are now correctly reported (@TimvdLippe, #43652).
We're continuing to implement document.execCommand() for rich text editing (@TimvdLippe, #43177), under --pref dom_exec_command_enabled. 'beforeinput' and 'input' events are now fired when executing supported and enabled commands (@TimvdLippe, #43087), the 'defaultParagraphSeparator' and 'styleWithCSS' commands are now supported (@TimvdLippe, #43028), and the 'delete' command is partially supported (@TimvdLippe, #43016, #43082).
All of the features above are enabled in servoshell's experimental mode.
Work on accessibility support for web contents continues under --pref accessibility_enabled. There was a breaking change in the embedding API (@delan, @alice, #43029), and we've landed support for "grafting" the accessibility tree of a document into that of its containing webview (@delan, @alice, #43012, #43013, #43556). As a result, when you navigate, separate documents can have separate accessibility trees without complicating the embedder.
<link rel=modulepreload> is now partially supported (@Gae24, #42964), though recursive fetching of descendants is gated by --pref dom_allow_preloading_module_descendants (@Gae24, #43353).
For a long time, Servo has had some support for the Web Bluetooth API under --pref dom_bluetooth_enabled. We've recently reworked our implementation to adopt btleplug, the cross-platform Rust-native Bluetooth LE library (@webbeef, #43529, #43581).
We've landed more fixes to Servo's async parser (@simonwuelker, #42930, #42959), under --pref dom_servoparser_async_html_tokenizer_enabled. If we can get the feature working more reliably (#37418), it could halve the energy Servo spends on parsing, lower latency for pages that don't use document.write(), and even improve the html5ever API for the ecosystem.
For developers
Servo's DevTools feature now has partial support for inspecting service workers (@CynthiaOketch, #43659), as well as using the navigation controls along the top of the UI (@brentschroeter, @eerii, #43026).
In the Inspector tab, we've fixed a bug where the UI stops updating when navigating to a new page (@brentschroeter, #43153).
In the Console tab, you can now evaluate JavaScript in web workers and service workers (@SharanRP, #43361, #43492).
We've fixed some long-outstanding bugs where the DevTools UI may stop responding due to protocol desyncs (@brentschroeter, @eerii, #43230, #43236), or due to messages from multiple Servo threads being interleaved (@brentschroeter, @eerii, #43472).
For developers of Servo itself, mach can be a bit opaque at times. To make mach more transparent and composable, we've added mach print-env and mach exec commands (@jschwe, #42888).
The empty default implementation of EventLoopWaker::wake has been removed, because it almost never makes sense for a new custom impl to leave the method empty (@chrisduerr, @mrobinson, #43250).
Add Cookie is now more conformant (@yezhizhen, #43690), which led to Servo developers landing a spec patch. 'pause' actions are now slightly more efficient (@yezhizhen, #43014), and we've fixed a bug where 'wheel' actions fail to interleave with other actions (@yezhizhen, #43126).
More on the web platform
Carets now blink in text fields (@mrobinson, #43128). You can configure or disable blinking carets with --pref editing_caret_blink_time=0 or a duration in milliseconds. Clicking to move the caret is more forgiving now (@mrobinson, #43238), and moving the caret by a word at a time is more conventional on Windows and Linux, with Ctrl instead of Alt (@mrobinson, #43436). We've also fixed a bug where pressing the arrow keys in text fields both moves the caret (good) and scrolls the page (bad), and fixed a bug where the caret fails to render on empty lines (@mrobinson, @freyacodes, #43247, #42218).
Input has improved, with more responsive touchpad scrolling on Linux (@mrobinson, @chrisduerr, #43350). Pointer events and mouse events can now be captured across shadow DOM boundaries (@simonwuelker, #42987), and we've now started working towards shadow-DOM-compatible focus (@mrobinson, #43811). Pressing Space or Enter inside text fields no longer causes them to be clicked (@mrobinson, #43343).
The lang attribute is now taken into account when shaping, which is important for the correct rendering of Chinese and Japanese text (@RichardTjokroutomo, @mrobinson, #43447). 'font-weight' is now matched more accurately when no available font is an exact match (@shubhamg13, #43125).
Navigation is one of the most complicated parts of HTML: navigating can run some JavaScript that replaces the page, just run some JavaScript, or depending on the response, do nothing at all. <iframe> makes navigation doubly complicated: the document containing an <iframe> can observe and interact with the document inside the <iframe> in various ways, often synchronously. This has been the source of manybugsovertheyears, but we've recently fixed one of those major issues (@jdm, #43496).
new Worker() now supports JS modules (@pylbrecht, @Gae24, #40365), and CanvasRenderingContext2D now supports drawing text with Variation Selectors, allowing you to control things like emoji presentation and CJK shaping (@yezhizhen, #43449).
Servo now fires 'pointerover', 'pointerout', 'pointerenter', and 'pointerleave' events on web content (@webbeef, #42736), 'scroll' events on VisualViewport (@stevennovaryo, #42771), and 'scrollend' events on Document, Element, and VisualViewport (@abdelrahman1234567, @mrobinson, #38773). We also fire 'error' events when event handler attributes contain syntax errors (@simonwuelker, #43178).
'direction' now works on grid containers (@nicoburns, #42118), SVG images can now be used in 'border-image' (@shubhamg13, #42566), 'linear-gradient()' now dithers to reduce banding (@Messi002, #43603), 'letter-spacing' no longer applies to invisible zero-width formatting characters (@simonwuelker, #42961), and ':active' now matches disabled or non-focusable elements too, as long as they are being clicked (@webbeef, #42935).
DOMContentLoaded timings in PerformanceNavigationTiming are more accurate (@simonwuelker, #43151). PerformancePaintTiming and LargestContentfulPaint are more accurate too, taking <iframe> into account (@shubhamg13, #42149), and checking for and ignoring things like broken images and transparent backgrounds (@shubhamg13, #42833, #42975, #43475).
We've landed partial support for using CSS counters in 'list-style-type' on 'display: list-item' and 'content' on '::marker', but the counter values themselves are not calculated yet, so all list items still read as 0. or similar. In any case, you can use a <counter-style-name> or 'symbols()' in 'list-style-type', and 'counter()' and 'counters()' in 'content' (@Loirooriol, #43111).
We've also landed partial support for <marquee> and the HTMLMarqueeElement interface, including basic layout, but the contents are not animated yet (@mrobinson, @lukewarlow, #43520, #43610).
Servo now exposes several attributes that have no direct effect, but are needed for web compatibility (@lukewarlow, #43500, #43499, #43502, #43518):
noHref on HTMLAreaElement
hreflang, type, charset on HTMLAnchorElement
useMap on HTMLInputElement and HTMLObjectElement
longDesc on HTMLIFrameElement and HTMLFrameElement
Web fonts are no longer fetched more than once, and they no longer cause reflow when they fail to load (@minghuaw, #43382, #43595). We're also working towards better caching for shaping results (@mrobinson, @lukewarlow, @Loirooriol, #43653). Event handler attribute lookup is more efficient now (@Narfinger, #43337), and we've made DOM tree walking more efficient in many cases (@Narfinger, #42781, #42978, #43476).
crypto.subtle.encrypt(), decrypt(), sign(), verify(), digest(), importKey(), unwrapKey(), decapsulateKey(), and decapsulateBits() are more efficient now (@kkoyung, #42927), thanks to a recent specupdate.
DOM data structures (#[dom_struct]) can refer to one another, with the help of garbage collection. But when DOM objects are being destroyed, those references can become invalid for a brief moment, depending on the order the GC finalizers run in. This can be unsound if those references are accessed, which is a very easy mistake to make if the type has an impl Drop. To help prevent that class of bug, we're reworking our DOM types so that none of them have #[dom_struct] and impl Drop at the same time (@willypuzzle, #42937, #42982, #43018, #43071, #43222, #43288, #43544, #43563, #43631).
Thanks again for your generous support! We are now receiving 7167 USD/month (+2.6% from February) in recurring donations. This helps us cover the cost of our speedyCIandbenchmarkingservers, one of our latest Outreachy interns, and funding maintainer work that helps more people contribute to Servo.
Servo is also on thanks.dev, and already 37 GitHub users (+5 from February) that depend on Servo are sponsoring us there. If you use Servo libraries like url, html5ever, selectors, or cssparser, signing up for thanks.dev could be a good way for you (or your employer) to give back to the community.
We now have sponsorship tiers that allow you or your organisation to donate to the Servo project with public acknowlegement of your support. If you're interested in this kind of sponsorship, please contact us at join@servo.org.
Hello and welcome to another issue of This Week in Rust! Rust is a programming language empowering everyone to build reliable and efficient software. This is a weekly summary of its progress and community. Want something mentioned? Tag us at @thisweekinrust.bsky.social on Bluesky or @ThisWeekinRust on mastodon.social, or send us a pull request. Want to get involved? We love contributions.
An important step for RFC implementation is for people to experiment with the implementation and give feedback, especially before stabilization.
If you are a feature implementer and would like your RFC to appear in this list, add a call-for-testing label to your RFC along with a comment providing testing instructions and/or guidance on which aspect(s) of the feature need testing.
Always wanted to contribute to open-source projects but did not know where to start? Every week we highlight some tasks from the Rust community for you to pick and get started!
Some of these tasks may also have mentors available, visit the task page for more information.
If you are a Rust project owner and are looking for contributors, please submit tasks here or through a PR to TWiR or by reaching out on Bluesky or Mastodon!
Are you a new or experienced speaker looking for a place to share something cool? This section highlights events that are being planned and are accepting submissions to join their event as a speaker.
If you are an event organizer hoping to expand the reach of your event, please submit a link to the website through a PR to TWiR or by reaching out on Bluesky or Mastodon!
Relatively few perf-affecting changes this week. Perf report is more positive than users should see due to the -Zincremental-verify-ich related improvements in #155473.
No Items entered Final Comment Period this week for Language Reference, Language Team or Leadership Council. Let us know if you would like your PRs, Tracking Issues or RFCs to be tracked as a part of this list.
If you are running a Rust event please add it to the calendar to get it mentioned here. Please remember to add a link to the event too. Email the Rust Community Team for access.
A small pet-peeve with fetching the latest main on jujutsu is that I like to move all my WIP patches to the new one. That's also nice because jj doesn't make me fix the conflicts immediately!
The solution from a co-worker (kudos to skippyhammond!) is to query all immediate decendants of the previous main after the fetch.
jj git fetch
# assuming 'z' is the rev-id of the previous main.
jj rebase -s "mutable()&z+" -d main
I haven't learnt how to make aliases accept params with it yet, so this will have to do for now.
Update 2: After some months of usage across multiple repositories, I've found it better to be clear with the destination since main, trunk or others can be tracked with a combination of repository aliases too.
[aliases]
# Update all revs to the latest main; point to the previous one.
hoist = ["util", "exec", "--", "bash", "-c", """
set -euo pipefail
jj rebase -s "mutable()&$1+" -d "$2"
""", ""]
You can use this to rebase all your WIPs like so:
$ jj hoist <prev_main> <current_main>
If my previous main revision was kz, this is what I would end up doing:
One of the most exciting aspects of bringing Thunderbird Pro to life is the opportunity to build an email service from Thunderbird together with our community, giving users the control and freedom they expect without relying on third party email service providers.
Over the past few months, we've been checking in with our community through quick surveys, and the feedback is clear: people care most about Thundermail. We're listening and working to deliver what you expect as quickly as possible, focusing our resources on building a great Thundermail experience first, with Appointment and Send as power features alongside that foundation. We're also adjusting the initial price to better align with your expectations.
We'll be sending out the first wave of Early Bird Beta invites next month. If you haven't already, please join the waitlist HERE and keep an eye on your inbox. We're excited to get Thundermail into your hands and continue building it together.
Latest Thundermail Developments
Our work right now is focused on making Thundermail reliable, easy to set up, and ensuring a smooth onboarding experience with an intuitive design, both visually and functionally.
Sign-in and Setup
A new connection flow is in development that will make it much easier to add a Thundermail account to Thunderbird, including options like QR code setup and deeper integration within the app. We have also fixed a range of sign in issues, improved domain setup, and made it easier to move from account creation to actually using the service.
The account dashboard has been updated for a cleaner look, smoother onboarding, and easier access to the key details our users care about. Configuring settings like app passwords, custom domains and aliases are now front and center when you first sign in.
Infrastructure
On the infrastructure side, we're continuing to improve stability and performance. This includes completed work on upgrading Stalwart to strengthen spam detection so legitimate emails are far less likely to end up in spam, along with improvements to how we monitor the services so problems are easier to catch and less likely to affect users. Everyday actions like archiving and managing settings should feel more intuitive for users, and the web app, add-ons, and related services now work together more smoothly.
April Onward
Next up for the account experience is better alias and custom-domain handling, and even better integration between Thunderbird and the web account flow.
The dashboard is also getting another round of refinement so settings, account details, and subscription information are easier to understand at a glance.
Thundermail work continues by focusing on reliability and security, including aliases, delivery, transport security, and admin access controls.
There will also be a final layer of polish across the entire experience between the web app, add-on, and desktop flows.
Finally: Webmail is moving up our priority list. While still early, development is actively progressing and we're aiming to bring a usable experience much sooner than originally planned.
Progress on Appointment and Send
While Thundermail is our primary focus, work on other Thunderbird Pro services is continuing.
For Appointment, we've made progress on reliability and backend performance, including improvements to how calendar tasks are processed and fixes to event handling. Our priorities heading up to the release are also focused on reliability, with refinement on calendar connections, event syncing, Zoom access, and a simpler first-time setup flow.
For Send, we've made substantial visual improvement so that it feels like a more natural part of Thunderbird Pro. We've also made a number of security improvements and are continuing to evaluate infrastructure choices to ensure long term reliability. Our priorities for Send in the coming months include better encryption-key handling and clearer password-protected downloads.
What's Next
We'll begin inviting people from the waitlist into the Early Bird beta shortly. If you haven't signed up yet, now's the time. Your feedback will directly shape how Thundermail evolves.
Improvements to tabs.move() split views support: extensions can now swap tabs within a split view, and passing a list of tabs that explicitly separates split-view members will properly unsplit the view to honor the requested ordering - Bug 2016762 / Bug 2022549
Alexandre Poirot [:ochameau] improved performance of the Inspector Rules view by supporting incremental updates, making some perf test 2 times faster (#2018538)
The Tab Notes feature is now live in Firefox 149 in Firefox Labs! We have a few tweaks and fixes coming in Firefox 150 and 151, but we're mostly collecting feedback from users.
Fixed a long-standing issue where extension paths stored in extensions.json (and addonStartup.json.lz4) became incorrect after restoring a Firefox profile to a different location and causing all previously installed add-ons to fail to load - Bug 1429838
DevTools
geppy renamed some of our CSS variables to be more explicit (#1767617)
Sebastian Zartner [:sebo] move the :visited pseudo-class to element-specific section in the Inspector pseudo-class (:hov) panel (#2017985)
Ben improved the RDM "Add Custom Device" form by making sure it worked for any kind of locale (#1705177)
Nicolas Chevobbe [:nchevobbe] added a <global> node on objects which are not from the top-level debugged page (it's always visible in the browser console/browser toolbox) (#1962343)
Nicolas Chevobbe [:nchevobbe] added proper autocomplete (including things like interpolation/color space) for linear-gradient() (and repeating-linear-gradient()) (#2025761)
The main impact of this is that when changing globals in head.js files, VS Code'S ESLint reporting will see the changes when you have the test files open that use the head.js file.
New Tab Page
Irene Ni updated the Shortcuts UI to Nova, refreshing about:newtab shortcuts with the new design system and improved focus states.
The Firefox for Android app has always had a complicated build process - we're cramping a complex cross-platform browser engine and all the related components that make it work on Android into one package. In its current form, it lives in the Firefox mono-repo at mozilla-central (now mozilla-firefox using the git repository).
I wanted to document my "artifact-mode" environment here since it's worked quite successfully for me for many years with minor changes.
NOTE: After a fresh clone of the mono-repo, don't forget to first run and follow the prompts of ./mach bootstrap .
mozconfig
My mozconfig below is enabled for artifact mode, but occasionally I switch between various configurations. You can see those commented out, with these few extra notes:
I like to separate out my objdirs to avoid cache pollution between the different build types. I think you can get away without needing to specify this and an objdir for your build type and arch will be generated.
sccache speeds up the native portion of full builds after the first slow one, but it's a hit or miss if you fetch from the remote repository but don't need to rebuild as often.
I don't care to manually run the clobber step, and I don't truly appreciate why that isn't always automatically done.
# Build GeckoView/Firefox for Android:
ac_add_options --enable-application=mobile/android
# Targeting the following architecture.
# For regular phones, no --target is needed.
# For x86 emulators (and x86 devices, which are uncommon):
# ac_add_options --target=i686
# For newer phones or Apple silicon
ac_add_options --target=aarch64
# For x86_64 emulators (and x86_64 devices, which are even less common):
# ac_add_options --target=x86_64
# sccache will significantly speed up your builds by caching
# compilation results. The Firefox build system will download
# sccache automatically.
# This only works for non-artifact builds.
#ac_add_options --with-ccache=sccache
# Enable artifact builds; manager-mode.
ac_add_options --enable-artifact-builds
# Write build artifacts to..
## Full build dir
#mk_add_options MOZ_OBJDIR=./objdir-droid
#mk_add_options MOZ_OBJDIR=./objdir-desktop
## Artifact builds
mk_add_options MOZ_OBJDIR=./objdir-frontend
# Automatic clobbering; don't ask me.
mk_add_options AUTOCLOBBER=1
JAVA_HOME
Sometimes you might find yourself needing to run a (non-mach) command in the terminal. Those typically will need to invoke some parts of gradle for an Android build, so it's best to make sure those are using the same JDK as the bootstrapped one in the mono-repo. This avoids weird build errors where something that compiles in one place isn't working in another (like Android Studio).
The location for the JDKs are typically in ~/.mozbuild/jdk/, and if you've between around for ~6 months you end up with multiple versions after every JDK bump:
$ ls -l ~/.mozbuild/jdk/
drwxr-xr-x@ - jalmeida 15 Apr 2025 jdk-17.0.15+6
drwxr-xr-x@ - jalmeida 15 Jul 2025 jdk-17.0.16+8
drwxr-xr-x@ - jalmeida 21 Oct 2025 jdk-17.0.17+10
drwxr-xr-x@ - jalmeida 20 Jan 09:00 jdk-17.0.18+8
drwxr-xr-x@ - jalmeida 26 Feb 15:04 mozboot
You can find some way to point your latest JDK to one location or you can be lazy like me and pick the latest version to assign as your JAVA_HOME property by adding this to your shell's RC file:
export JAVA_HOME="$(ls -1dr -- $HOME/.mozbuild/jdk/jdk-* | head -n 1)/Contents/Home"
Android Studio
Similarly for Android Studio, let's do the same so that environment is identical. Head to, Settings | Build, Execution, Deployment | Build Tools | Gradle, and ensure that "Gradle JDK" path is set to JAVA_HOME.
Lately, the default seems to be for it to follow GRADLE_LOCAL_JAVA_HOME which is a property we can't easily override, so we have to manually set this ourselves.
Using the same Android SDK also helps speed things up and avoids source confusion. You can typically find it in ~/.mozbuild/android-sdk-macosx and update it at Settings | Languages & Frameworks | Android SDK.
Debugging
This section is for miscellaneous build error situations that come-up, but assuming mach build work and there are no known Android build changes, my solution has typically always been the same.
For example, the other day I fetched another engineers patch to test out locally1 as part of reviewing it where I faced the error message below:
Execution failed for task ':components:feature-pwa:compileDebugKotlin'.
FAILURE: Build failed with an exception.
* What went wrong:
Execution failed for task ':components:feature-pwa:compileDebugKotlin'.
> A failure occurred while executing org.jetbrains.kotlin.compilerRunner.GradleCompilerRunnerWithWorkers$GradleKotlinCompilerWorkAction
> Internal compiler error. See log for more details
* Try:
> Run with --info or --debug option to get more log output.
> Run with --scan to generate a Build Scan (powered by Develocity).
> Get more help at https://help.gradle.org.
* Exception is:
org.gradle.api.tasks.TaskExecutionException: Execution failed for task ':components:feature-pwa:compileDebugKotlin'.
at org.gradle.api.internal.tasks.execution.ExecuteActionsTaskExecuter.lambda$executeIfValid$1(ExecuteActionsTaskExecuter.java:135)
at org.gradle.internal.Try$Failure.ifSuccessfulOrElse(Try.java:288)
at org.gradle.api.internal.tasks.execution.ExecuteActionsTaskExecuter.executeIfValid(ExecuteActionsTaskExecuter.java:133)
at org.gradle.api.internal.tasks.execution.ExecuteActionsTaskExecuter.execute(ExecuteActionsTaskExecuter.java:121)
at org.gradle.api.internal.tasks.execution.ProblemsTaskPathTrackingTaskExecuter.execute(ProblemsTaskPathTrackingTaskExecuter.java:41)
at org.gradle.api.internal.tasks.execution.FinalizePropertiesTaskExecuter.execute(FinalizePropertiesTaskExecuter.java:46)
at org.gradle.api.internal.tasks.execution.ResolveTaskExecutionModeExecuter.execute(ResolveTaskExecutionModeExecuter.java:51)
at org.gradle.api.internal.tasks.execution.SkipTaskWithNoActionsExecuter.execute(SkipTaskWithNoActionsExecuter.java:57)
at org.gradle.api.internal.tasks.execution.SkipOnlyIfTaskExecuter.execute(SkipOnlyIfTaskExecuter.java:74)
at org.gradle.api.internal.tasks.execution.CatchExceptionTaskExecuter.execute(CatchExceptionTaskExecuter.java:36)
at org.gradle.api.internal.tasks.execution.EventFiringTaskExecuter$1.executeTask(EventFiringTaskExecuter.java:77)
at org.gradle.api.internal.tasks.execution.EventFiringTaskExecuter$1.call(EventFiringTaskExecuter.java:55)
at org.gradle.api.internal.tasks.execution.EventFiringTaskExecuter$1.call(EventFiringTaskExecuter.java:52)
at org.gradle.internal.operations.DefaultBuildOperationRunner$CallableBuildOperationWorker.execute(DefaultBuildOperationRunner.java:209)
at org.gradle.internal.operations.DefaultBuildOperationRunner$CallableBuildOperationWorker.execute(DefaultBuildOperationRunner.java:204)
at org.gradle.internal.operations.DefaultBuildOperationRunner$2.execute(DefaultBuildOperationRunner.java:66)
at org.gradle.internal.operations.DefaultBuildOperationRunner$2.execute(DefaultBuildOperationRunner.java:59)
at org.gradle.internal.operations.DefaultBuildOperationRunner.execute(DefaultBuildOperationRunner.java:166)
at org.gradle.internal.operations.DefaultBuildOperationRunner.execute(DefaultBuildOperationRunner.java:59)
at org.gradle.internal.operations.DefaultBuildOperationRunner.call(DefaultBuildOperationRunner.java:53)
at org.gradle.api.internal.tasks.execution.EventFiringTaskExecuter.execute(EventFiringTaskExecuter.java:52)
at org.gradle.execution.plan.DefaultNodeExecutor.executeLocalTaskNode(DefaultNodeExecutor.java:55)
at org.gradle.execution.plan.DefaultNodeExecutor.execute(DefaultNodeExecutor.java:34)
at org.gradle.execution.taskgraph.DefaultTaskExecutionGraph$InvokeNodeExecutorsAction.execute(DefaultTaskExecutionGraph.java:355)
at org.gradle.execution.taskgraph.DefaultTaskExecutionGraph$InvokeNodeExecutorsAction.execute(DefaultTaskExecutionGraph.java:343)
at org.gradle.execution.taskgraph.DefaultTaskExecutionGraph$BuildOperationAwareExecutionAction.lambda$execute$0(DefaultTaskExecutionGraph.java:339)
at org.gradle.internal.operations.CurrentBuildOperationRef.with(CurrentBuildOperationRef.java:84)
at org.gradle.execution.taskgraph.DefaultTaskExecutionGraph$BuildOperationAwareExecutionAction.execute(DefaultTaskExecutionGraph.java:339)
at org.gradle.execution.taskgraph.DefaultTaskExecutionGraph$BuildOperationAwareExecutionAction.execute(DefaultTaskExecutionGraph.java:328)
at org.gradle.execution.plan.DefaultPlanExecutor$ExecutorWorker.execute(DefaultPlanExecutor.java:459)
at org.gradle.execution.plan.DefaultPlanExecutor$ExecutorWorker.run(DefaultPlanExecutor.java:376)
at org.gradle.internal.concurrent.ExecutorPolicy$CatchAndRecordFailures.onExecute(ExecutorPolicy.java:64)
at org.gradle.internal.concurrent.AbstractManagedExecutor$1.run(AbstractManagedExecutor.java:47)
Caused by: org.gradle.workers.internal.DefaultWorkerExecutor$WorkExecutionException: A failure occurred while executing org.jetbrains.kotlin.compilerRunner.GradleCompilerRunnerWithWorkers$GradleKotlinCompilerWorkAction
at org.gradle.workers.internal.DefaultWorkerExecutor$WorkItemExecution.waitForCompletion(DefaultWorkerExecutor.java:289)
at org.gradle.internal.work.DefaultAsyncWorkTracker.lambda$waitForItemsAndGatherFailures$2(DefaultAsyncWorkTracker.java:130)
at org.gradle.internal.Factories$1.create(Factories.java:33)
at org.gradle.internal.work.DefaultWorkerLeaseService.lambda$withoutLocks$2(DefaultWorkerLeaseService.java:344)
at org.gradle.internal.work.ResourceLockStatistics$1.measure(ResourceLockStatistics.java:42)
at org.gradle.internal.work.DefaultWorkerLeaseService.withoutLocks(DefaultWorkerLeaseService.java:342)
at org.gradle.internal.work.DefaultWorkerLeaseService.withoutLocks(DefaultWorkerLeaseService.java:326)
at org.gradle.internal.work.DefaultWorkerLeaseService.withoutLock(DefaultWorkerLeaseService.java:331)
at org.gradle.internal.work.DefaultAsyncWorkTracker.waitForItemsAndGatherFailures(DefaultAsyncWorkTracker.java:126)
at org.gradle.internal.work.DefaultAsyncWorkTracker.waitForItemsAndGatherFailures(DefaultAsyncWorkTracker.java:92)
at org.gradle.internal.work.DefaultAsyncWorkTracker.waitForAll(DefaultAsyncWorkTracker.java:78)
at org.gradle.internal.work.DefaultAsyncWorkTracker.waitForCompletion(DefaultAsyncWorkTracker.java:66)
at org.gradle.api.internal.tasks.execution.TaskExecution$3.run(TaskExecution.java:260)
at org.gradle.internal.operations.DefaultBuildOperationRunner$1.execute(DefaultBuildOperationRunner.java:29)
at org.gradle.internal.operations.DefaultBuildOperationRunner$1.execute(DefaultBuildOperationRunner.java:26)
at org.gradle.internal.operations.DefaultBuildOperationRunner$2.execute(DefaultBuildOperationRunner.java:66)
at org.gradle.internal.operations.DefaultBuildOperationRunner$2.execute(DefaultBuildOperationRunner.java:59)
at org.gradle.internal.operations.DefaultBuildOperationRunner.execute(DefaultBuildOperationRunner.java:166)
at org.gradle.internal.operations.DefaultBuildOperationRunner.execute(DefaultBuildOperationRunner.java:59)
at org.gradle.internal.operations.DefaultBuildOperationRunner.run(DefaultBuildOperationRunner.java:47)
at org.gradle.api.internal.tasks.execution.TaskExecution.executeAction(TaskExecution.java:237)
at org.gradle.api.internal.tasks.execution.TaskExecution.executeActions(TaskExecution.java:220)
at org.gradle.api.internal.tasks.execution.TaskExecution.executeWithPreviousOutputFiles(TaskExecution.java:203)
at org.gradle.api.internal.tasks.execution.TaskExecution.execute(TaskExecution.java:170)
at org.gradle.internal.execution.steps.ExecuteStep.executeInternal(ExecuteStep.java:105)
at org.gradle.internal.execution.steps.ExecuteStep.access$000(ExecuteStep.java:44)
at org.gradle.internal.execution.steps.ExecuteStep$1.call(ExecuteStep.java:59)
at org.gradle.internal.execution.steps.ExecuteStep$1.call(ExecuteStep.java:56)
at org.gradle.internal.operations.DefaultBuildOperationRunner$CallableBuildOperationWorker.execute(DefaultBuildOperationRunner.java:209)
at org.gradle.internal.operations.DefaultBuildOperationRunner$CallableBuildOperationWorker.execute(DefaultBuildOperationRunner.java:204)
at org.gradle.internal.operations.DefaultBuildOperationRunner$2.execute(DefaultBuildOperationRunner.java:66)
at org.gradle.internal.operations.DefaultBuildOperationRunner$2.execute(DefaultBuildOperationRunner.java:59)
at org.gradle.internal.operations.DefaultBuildOperationRunner.execute(DefaultBuildOperationRunner.java:166)
at org.gradle.internal.operations.DefaultBuildOperationRunner.execute(DefaultBuildOperationRunner.java:59)
at org.gradle.internal.operations.DefaultBuildOperationRunner.call(DefaultBuildOperationRunner.java:53)
at org.gradle.internal.execution.steps.ExecuteStep.execute(ExecuteStep.java:56)
at org.gradle.internal.execution.steps.ExecuteStep.execute(ExecuteStep.java:44)
at org.gradle.internal.execution.steps.CancelExecutionStep.execute(CancelExecutionStep.java:42)
at org.gradle.internal.execution.steps.TimeoutStep.executeWithoutTimeout(TimeoutStep.java:75)
at org.gradle.internal.execution.steps.TimeoutStep.execute(TimeoutStep.java:55)
at org.gradle.internal.execution.steps.PreCreateOutputParentsStep.execute(PreCreateOutputParentsStep.java:50)
at org.gradle.internal.execution.steps.PreCreateOutputParentsStep.execute(PreCreateOutputParentsStep.java:28)
at org.gradle.internal.execution.steps.RemovePreviousOutputsStep.execute(RemovePreviousOutputsStep.java:68)
at org.gradle.internal.execution.steps.RemovePreviousOutputsStep.execute(RemovePreviousOutputsStep.java:38)
at org.gradle.internal.execution.steps.BroadcastChangingOutputsStep.execute(BroadcastChangingOutputsStep.java:61)
at org.gradle.internal.execution.steps.BroadcastChangingOutputsStep.execute(BroadcastChangingOutputsStep.java:26)
at org.gradle.internal.execution.steps.CaptureOutputsAfterExecutionStep.execute(CaptureOutputsAfterExecutionStep.java:69)
at org.gradle.internal.execution.steps.CaptureOutputsAfterExecutionStep.execute(CaptureOutputsAfterExecutionStep.java:46)
at org.gradle.internal.execution.steps.ResolveInputChangesStep.execute(ResolveInputChangesStep.java:39)
at org.gradle.internal.execution.steps.ResolveInputChangesStep.execute(ResolveInputChangesStep.java:28)
at org.gradle.internal.execution.steps.BuildCacheStep.executeWithoutCache(BuildCacheStep.java:189)
at org.gradle.internal.execution.steps.BuildCacheStep.lambda$execute$1(BuildCacheStep.java:75)
at org.gradle.internal.Either$Right.fold(Either.java:176)
at org.gradle.internal.execution.caching.CachingState.fold(CachingState.java:62)
at org.gradle.internal.execution.steps.BuildCacheStep.execute(BuildCacheStep.java:73)
at org.gradle.internal.execution.steps.BuildCacheStep.execute(BuildCacheStep.java:48)
at org.gradle.internal.execution.steps.StoreExecutionStateStep.execute(StoreExecutionStateStep.java:46)
at org.gradle.internal.execution.steps.StoreExecutionStateStep.execute(StoreExecutionStateStep.java:35)
at org.gradle.internal.execution.steps.SkipUpToDateStep.executeBecause(SkipUpToDateStep.java:75)
at org.gradle.internal.execution.steps.SkipUpToDateStep.lambda$execute$2(SkipUpToDateStep.java:53)
at org.gradle.internal.execution.steps.SkipUpToDateStep.execute(SkipUpToDateStep.java:53)
at org.gradle.internal.execution.steps.SkipUpToDateStep.execute(SkipUpToDateStep.java:35)
at org.gradle.internal.execution.steps.legacy.MarkSnapshottingInputsFinishedStep.execute(MarkSnapshottingInputsFinishedStep.java:37)
at org.gradle.internal.execution.steps.legacy.MarkSnapshottingInputsFinishedStep.execute(MarkSnapshottingInputsFinishedStep.java:27)
at org.gradle.internal.execution.steps.ResolveIncrementalCachingStateStep.executeDelegate(ResolveIncrementalCachingStateStep.java:49)
at org.gradle.internal.execution.steps.ResolveIncrementalCachingStateStep.executeDelegate(ResolveIncrementalCachingStateStep.java:27)
at org.gradle.internal.execution.steps.AbstractResolveCachingStateStep.execute(AbstractResolveCachingStateStep.java:71)
at org.gradle.internal.execution.steps.AbstractResolveCachingStateStep.execute(AbstractResolveCachingStateStep.java:39)
at org.gradle.internal.execution.steps.ResolveChangesStep.execute(ResolveChangesStep.java:64)
at org.gradle.internal.execution.steps.ResolveChangesStep.execute(ResolveChangesStep.java:35)
at org.gradle.internal.execution.steps.ValidateStep.execute(ValidateStep.java:62)
at org.gradle.internal.execution.steps.ValidateStep.execute(ValidateStep.java:40)
at org.gradle.internal.execution.steps.AbstractCaptureStateBeforeExecutionStep.execute(AbstractCaptureStateBeforeExecutionStep.java:76)
at org.gradle.internal.execution.steps.AbstractCaptureStateBeforeExecutionStep.execute(AbstractCaptureStateBeforeExecutionStep.java:45)
at org.gradle.internal.execution.steps.AbstractSkipEmptyWorkStep.executeWithNonEmptySources(AbstractSkipEmptyWorkStep.java:136)
at org.gradle.internal.execution.steps.AbstractSkipEmptyWorkStep.execute(AbstractSkipEmptyWorkStep.java:66)
at org.gradle.internal.execution.steps.AbstractSkipEmptyWorkStep.execute(AbstractSkipEmptyWorkStep.java:38)
at org.gradle.internal.execution.steps.legacy.MarkSnapshottingInputsStartedStep.execute(MarkSnapshottingInputsStartedStep.java:38)
at org.gradle.internal.execution.steps.LoadPreviousExecutionStateStep.execute(LoadPreviousExecutionStateStep.java:36)
at org.gradle.internal.execution.steps.LoadPreviousExecutionStateStep.execute(LoadPreviousExecutionStateStep.java:23)
at org.gradle.internal.execution.steps.HandleStaleOutputsStep.execute(HandleStaleOutputsStep.java:75)
at org.gradle.internal.execution.steps.HandleStaleOutputsStep.execute(HandleStaleOutputsStep.java:41)
at org.gradle.internal.execution.steps.AssignMutableWorkspaceStep.lambda$execute$0(AssignMutableWorkspaceStep.java:35)
at org.gradle.api.internal.tasks.execution.TaskExecution$4.withWorkspace(TaskExecution.java:297)
at org.gradle.internal.execution.steps.AssignMutableWorkspaceStep.execute(AssignMutableWorkspaceStep.java:31)
at org.gradle.internal.execution.steps.AssignMutableWorkspaceStep.execute(AssignMutableWorkspaceStep.java:22)
at org.gradle.internal.execution.steps.ChoosePipelineStep.execute(ChoosePipelineStep.java:40)
at org.gradle.internal.execution.steps.ChoosePipelineStep.execute(ChoosePipelineStep.java:23)
at org.gradle.internal.execution.steps.ExecuteWorkBuildOperationFiringStep.lambda$execute$2(ExecuteWorkBuildOperationFiringStep.java:67)
at org.gradle.internal.execution.steps.ExecuteWorkBuildOperationFiringStep.execute(ExecuteWorkBuildOperationFiringStep.java:67)
at org.gradle.internal.execution.steps.ExecuteWorkBuildOperationFiringStep.execute(ExecuteWorkBuildOperationFiringStep.java:39)
at org.gradle.internal.execution.steps.IdentityCacheStep.execute(IdentityCacheStep.java:46)
at org.gradle.internal.execution.steps.IdentityCacheStep.execute(IdentityCacheStep.java:34)
at org.gradle.internal.execution.steps.IdentifyStep.execute(IdentifyStep.java:44)
at org.gradle.internal.execution.steps.IdentifyStep.execute(IdentifyStep.java:31)
at org.gradle.internal.execution.impl.DefaultExecutionEngine$1.execute(DefaultExecutionEngine.java:64)
at org.gradle.api.internal.tasks.execution.ExecuteActionsTaskExecuter.executeIfValid(ExecuteActionsTaskExecuter.java:132)
... 30 more
Caused by: org.jetbrains.kotlin.gradle.tasks.FailedCompilationException: Internal compiler error. See log for more details
at org.jetbrains.kotlin.gradle.tasks.TasksUtilsKt.throwExceptionIfCompilationFailed(tasksUtils.kt:22)
at org.jetbrains.kotlin.compilerRunner.GradleKotlinCompilerWork.run(GradleKotlinCompilerWork.kt:112)
at org.jetbrains.kotlin.compilerRunner.GradleCompilerRunnerWithWorkers$GradleKotlinCompilerWorkAction.execute(GradleCompilerRunnerWithWorkers.kt:75)
at org.gradle.workers.internal.DefaultWorkerServer.execute(DefaultWorkerServer.java:68)
at org.gradle.workers.internal.NoIsolationWorkerFactory$1$1.create(NoIsolationWorkerFactory.java:64)
at org.gradle.workers.internal.NoIsolationWorkerFactory$1$1.create(NoIsolationWorkerFactory.java:61)
at org.gradle.internal.classloader.ClassLoaderUtils.executeInClassloader(ClassLoaderUtils.java:100)
at org.gradle.workers.internal.NoIsolationWorkerFactory$1.lambda$execute$0(NoIsolationWorkerFactory.java:61)
at org.gradle.workers.internal.AbstractWorker$1.call(AbstractWorker.java:44)
at org.gradle.workers.internal.AbstractWorker$1.call(AbstractWorker.java:41)
at org.gradle.internal.operations.DefaultBuildOperationRunner$CallableBuildOperationWorker.execute(DefaultBuildOperationRunner.java:209)
at org.gradle.internal.operations.DefaultBuildOperationRunner$CallableBuildOperationWorker.execute(DefaultBuildOperationRunner.java:204)
at org.gradle.internal.operations.DefaultBuildOperationRunner$2.execute(DefaultBuildOperationRunner.java:66)
at org.gradle.internal.operations.DefaultBuildOperationRunner$2.execute(DefaultBuildOperationRunner.java:59)
at org.gradle.internal.operations.DefaultBuildOperationRunner.execute(DefaultBuildOperationRunner.java:166)
at org.gradle.internal.operations.DefaultBuildOperationRunner.execute(DefaultBuildOperationRunner.java:59)
at org.gradle.internal.operations.DefaultBuildOperationRunner.call(DefaultBuildOperationRunner.java:53)
at org.gradle.workers.internal.AbstractWorker.executeWrappedInBuildOperation(AbstractWorker.java:41)
at org.gradle.workers.internal.NoIsolationWorkerFactory$1.execute(NoIsolationWorkerFactory.java:58)
at org.gradle.workers.internal.DefaultWorkerExecutor.lambda$submitWork$0(DefaultWorkerExecutor.java:176)
at org.gradle.internal.work.DefaultConditionalExecutionQueue$ExecutionRunner.runExecution(DefaultConditionalExecutionQueue.java:194)
at org.gradle.internal.work.DefaultConditionalExecutionQueue$ExecutionRunner.access$700(DefaultConditionalExecutionQueue.java:127)
at org.gradle.internal.work.DefaultConditionalExecutionQueue$ExecutionRunner$1.run(DefaultConditionalExecutionQueue.java:169)
at org.gradle.internal.Factories$1.create(Factories.java:33)
at org.gradle.internal.work.DefaultWorkerLeaseService.lambda$withLocksAcquired$0(DefaultWorkerLeaseService.java:269)
at org.gradle.internal.work.ResourceLockStatistics$1.measure(ResourceLockStatistics.java:42)
at org.gradle.internal.work.DefaultWorkerLeaseService.withLocksAcquired(DefaultWorkerLeaseService.java:267)
at org.gradle.internal.work.DefaultWorkerLeaseService.withLocks(DefaultWorkerLeaseService.java:259)
at org.gradle.internal.work.DefaultWorkerLeaseService.runAsWorkerThread(DefaultWorkerLeaseService.java:127)
at org.gradle.internal.work.DefaultWorkerLeaseService.runAsWorkerThread(DefaultWorkerLeaseService.java:132)
at org.gradle.internal.work.DefaultConditionalExecutionQueue$ExecutionRunner.runBatch(DefaultConditionalExecutionQueue.java:164)
at org.gradle.internal.work.DefaultConditionalExecutionQueue$ExecutionRunner.run(DefaultConditionalExecutionQueue.java:133)
... 2 more
The full trace was long and didn't seem related to a code failure in the module itself. So I employed the solution, which is always the same:
./mach build
In Android Studio, File > Sync Project with Gradle Files.
Yup, that's all. Very simple and boring.
1
With Jujutsu, this is the moz-phab command I use which has made it easier to manage review patches: moz-phab patch <patch-id> --no-branch --apply-to main@origin
Comments
With an account on the Fediverse or Mastodon, you can respond to this post. Since Mastodon is decentralized, you can use your existing account hosted by another Mastodon server or compatible platform if you don't have an account on this one. Known non-private replies are displayed below.
Learn how this was implemented from the original source here.
<noscript><p>Loading comments relies on JavaScript. Try enabling JavaScript and reloading, or visit <a href="https://mindly.social/@jonalmeida/116197244320129422">the original post</a> on Mastodon.</p></noscript>
<noscript>You need JavaScript to view the comments.</noscript> &>"'
Hey everyone, we've been working on some exciting changes, and want to share them with you.
But first, let me introduce myself. I am Christos, the new Sr. Developer Relations engineer in Add-ons, and I'm excited to write my first post on the Add-ons engineering blog.
Deprecations and changes
To start, I'm looking at a couple of features that are going away: avoiding content script execution in extension contexts, decoupling file access from host permissions, and improving the display of pageAction SVG icon.
executeScript / registerContentScript in moz-extension documents
Deprecated: Firefox 149 Removed: Firefox 152
Starting in Firefox Nightly 149 and scheduled for Firefox 152, the scripting and tabs injection APIs no longer inject into moz-extension://documents. This change brings the API in line with broader efforts to discourage string-based code execution in extension contexts, alongside the default CSP that restricts script-src to extension URLs and the removal of remote source allowlisting in MV3 (bug 1581608).
Firefox emits a warning when this restriction is met, so you are aware of and can address any use of this process in your extensions. This is an example of the warning message:
Content Script execution in moz-extension document has been deprecated and it has been blocked
To work around this change, you can:
Import scripts directly in the extension page's HTML.
Use module imports or standard <script> tags in extension documents.
Restructure code to avoid dynamic code execution patterns. An extension can run code in its documents dynamically by registering a runtime.onMessage listener in the document's script, then sending a message to trigger execution of the required code.
File access becomes opt-in
Target: Firefox 152
Extensions requesting file://*/ or <all_urls> currently trigger the "Access your data for all websites" permission message, and when granted, can run content scripts in file:-URLs. From Firefox 152, file access in extensions requires an opt-in for all extensions, including those already installed (bug 2034168).
pageAction SVG icon CSS filter (automatic color scheme)
Removed: Firefox 152
Firefox has been automatically applying a greyscale and brightness CSS filter to pageAction (address bar button) SVG icons when a dark theme is active. This was intended to improve contrast, but it actually reduced contrast for multi-color icons and caused poor visibility for some extensions, such as Firefox Multi-Account Containers.
For icons that adapt to light and dark color schemes, you can now use @media (prefers-color-scheme: dark) in the SVG icon, or the MV3 action manifest key, and specify theme_icons.
Here is an example of how to use a `prefers-color-scheme` media query in a pageAction SVG icon to control how the icon adapts to dark mode:
Use of prefers-color-scheme media queries is also allowed in MV2 browserAction and MV3 action SVG icons as an alternative to the theme_icons manifest properties.
Now to the new stuff. Here, you get the ability to use popups without user activation, initial support for the new tab split view feature, and WebAuthn RP ID assertion.
openPopup without user activation (Firefox Desktop)
Available: Firefox 149 Desktop
action.openPopup() and browserAction.openPopup() no longer require a user gesture on Firefox Desktop. You can open your extension's popup programmatically, e.g., in response to a native-messaging event, an alarm, or a background-script condition.
This change is part of the ongoing cross-browser alignment work in the WebExtensions Community Group to harmonize popup behavior across engines.
Example
Before (Firefox < 149): must hang off a user gesture, e.g., a context menu click:
browser.menus.create({
id: "nudge",
title: "Open popup",
contexts: ["all"],
});
browser.menus.onClicked.addListener((info) => {
if (info.menuItemId === "nudge") {
browser.action.openPopup(); // user clicked the menu → allowed
}
});
After (Firefox ≥ 149) - same intent, no user gesture needed, fires from a timer:
browser.alarms.create("nudge", { delayInMinutes: 1 });
browser.alarms.onAlarm.addListener((alarm) => {
if (alarm.name === "nudge") {
browser.action.openPopup(); // works without a click
}
});
It's the same call with the same result, but only the trigger changes from a user-action handler to any background event.
It's the same call with the same result, but only the trigger changes from a user-action handler to any background event.
splitViewId in the tabs API
Available: Firefox 149
Firefox 149 introduces a new read-onlysplitViewIdproperty on the tabs.Tab object to expose Firefox's new split view feature (where two tabs are displayed side-by-side in one window). Split views are treated as one unit, and Web Extensions treat them the same way.
In Firefox 150, extensions can swap tabs within a split view. This update also resolves a confusing issue where using the user interface to reverse tab order incorrectly reports the tabs.onMoved event with inaccurate values. Additionally, Firefox introduces unsplitting behavior for web extensions: when tabs.move() is called with split-view tabs positioned separately (non-adjacently) in the array. Now, after the call, Firefox removes the split view rather than keeping the tabs locked together.
Here is an example of using the new splitViewId property.
// Log whenever a tab joins or leaves a split view.
browser.tabs.onUpdated.addListener((tabId, changeInfo) => {
if (!("splitViewId" in changeInfo)) return;
if (changeInfo.splitViewId === browser.tabs.SPLIT_VIEW_ID_NONE) {
console.log(`Tab ${tabId} left its split view`);
} else {
console.log(`Tab ${tabId} joined split view ${changeInfo.splitViewId}`);
}
});
// Firefox desktop also supports a filter to limite onUpdated events:
// }, { properties: ["splitViewId"] });
Firefox 151 enables extensions to move split views in tab groups. More improvements are coming, such as the ability to create split views from extensions (bug 2016928).
WebAuthn RP ID assertion
Available: Firefox 150
Previously, web extensions couldn't use WebAuthn credentials registered on their company's website or mobile apps. When extensions tried to set a custom Relying Party ID (RP ID) in navigator.credentials.create() or navigator.credentials.get(), Firefox rejected it with "SecurityError: The operation is insecure."
With Firefox 150, Extensions can now assert aWebAuthn RP IDfor any domain they have host permissions for
when calling navigator.credentials.create() or navigator.credentials.get(). This applies to both the publicKey.rp.id field during credential creation and the publicKey.rpId field during authentication.
A critical detail for server-side validation: When relying party servers validate credentials created by extensions, they must account for different origin formats across browsers. In Chrome, the origin follows the pattern chrome-extension://extensionid, which matches the extension's location.origin. Firefox 150 introduces a new stable origin format: moz-extension://hash, where the hash is a 64-character SHA-256 representation of the extension ID (using characters a-p to represent hex values). Importantly, this hash-based origin is the same all users, unlike Firefox's existing UUID-based moz-extension:// URLs used for extension documents.
To extract the origin from a credential for validation:
let clientData = JSON.parse(new TextDecoder().decode(
publicKeyCredential.response.clientDataJSON
));
console.log(clientData.origin);
It's been a very busy couple of months as we've reworked processes & priorities and established a roadmap for both iOS and Android. We are determining how best we can coordinate with the community, and think that our roadmap for the year has a good balance of fixes and features. Today, I want to talk about our contributors and pull requests, Notifications in the Android app, progress in the iOS app, and an overview of our roadmap for both apps this year.
Contributors & Pull Requests
We are so grateful for the support and code contributions of many members, whether building items on our roadmap, improving the user experience, or, of course, translating. As we work on our roadmap priorities, we will make time to review PRs and will discuss them weekly, and prioritize those that help solve issues and bugs or align with our roadmap items. Please be patient with our Pull Request pipeline. Typically, in working with the community, we try to react very quickly.
Roadmap
For Android, we've chosen the items on our roadmap because we think these will be the highest-impact features and bring the most value to everyone. Our focus this year is to simplify and modernize the Android codebase. This means reworking some of the architecture. This will be super helpful for us to move more quickly and will reduce complex bugs. The app has an older codebase, and like many older ones, it has its challenges. We have three full-time Android engineers and several community contributors, and we hope to better position ourselves to move quickly. At a high level, Android is focusing on the rearchitecture, a better Message List experience, and Message Reader screens. We are also simplifying how users can connect to Thunder Mail as we open it up.
Notifications
One thing that is at the top of my mind right now, too, is Push Notifications, specifically changes that Google has made to background processes, which affect our Notifications. We are looking into what we can do to solve this, so know that it has become a top priority for us. I've been asked, "Why is it so hard for Thunderbird to get Push Notifications right?" and I wanted to speak to some of the challenges we have. Most apps' Notifications are triggered by their own web services, which then send Notifications through Apple or Google, who pass them to users. But email is different. In an email client, we typically don't own our own backend services, but other companies do (Microsoft, Google, Hotmail, Yahoo, Proton, etc.). And they can have their own flavors of SMTP - how we get the emails, and no specific Push Notification implementation.
So we have a work around: polling those providers ever X minutes asking for new emails, and triggering local notifications - but we can't hook into a native Push Notification process like your banking app for example. This is under the IMAP implementation. The JMAP implementation (think modern email protocols) has something in place we can more readily consume. Another challenge is how the battery is affected by how often we poll the providers, and we need specific permissions from Google to run this process in the background. Those permissions changed recently which is why Notifications are having issues.
I've simplified some pieces here, but hopefully that gives you an idea of some of the complexity and tradeoffs that we are working with. With all of that said, this is veryimportant to us, and is our users' biggest pain point. It is becoming our biggest need for a fix. I'll give an update on where that sits within the roadmap next progress report when we have explored what solutions we can provide.
iOS Progress
For the iOS roadmap, everything is moving along well. We have been wrapping up most of our IMAP & SMTP tickets, and we are moving into the Account Data pieces to manage accounts and authorizations. We will also be having a new member join us in the next couple of weeks. This will add some speed, but we've made good progress in getting the inner pieces together - what I consider the most complex parts. As we move to more standard mobile backend pieces and more standard UI, we leave the world of unknown unknowns, and will be picking up steam.
At a high level our iOS roadmap is build out these screens:
Account Setup and Drawer
Messages: List, Reader, Compose, Search
And have these pieces in place:
IMAP
SMTP
MIME
OAuth
Encryption
Email Composition
And our target is still end of the year for the iOS release.
Thank You!
Again we are so grateful to you, our community, for your support, and we are excited for this next quarter as we start to see the fruits of our labors.
The Sync Storage team has landed official PostgreSQL support for Firefox Sync.
Historically, Sync has only officially supported Google Spanner as a storage backend, with MySQL working unofficially. That has been a pretty high barrier to entry for people self-hosting their own services.
With PostgreSQL support, we hope to make self-hosting more approachable and continue supporting people who want the agency of hosting their data on infrastructure they control.
There is updated documentation for running it with Docker, including a one-shot docker compose setup:
If you've been interested in self-hosting Sync but were put off by the storage requirements, take another look. If you run into bugs or have feedback, please file issues here:
I want Phabricator emails to have a Gmail label so I can know which patches had me as a reviewer that then had follow-up comments from other folks.
This is useful for me when I review a patch and then I need to respond back to discussions in a more timely manner in comment threads that I've created.
It's difficult to do this today similar to Bugzilla Gmail filters because there are fewer identifiers that the more simplistic Gmail filter parameters can help with.
Today I learnt that there is an X-Phabricator-Stamps header in those Phabricator emails that let's you identify you as a the reviewer in a patch. So using that information, I wrote the Google script below to run every minute and avoid re-processing the same email twice.
A couple variables were added to the top and some console.logs are sprinkled around for my own debugging.
Code
var REVIEWER = "jonalmeida";
var LABEL_NAME = "Phabricator/Comments";
var BODY_MATCH = "commented on this revision.";
var SENDER = "phabricator@mozilla.com";
/**
* Run once manually to install the per-minute trigger.
*/
function install() {
uninstall();
ScriptApp.newTrigger('processInbox')
.timeBased()
.everyMinutes(1)
.create();
}
/**
* Run once manually to remove the trigger.
*/
function uninstall() {
ScriptApp.getProjectTriggers().forEach(function(t) {
ScriptApp.deleteTrigger(t);
});
PropertiesService.getScriptProperties().deleteProperty('lastRun');
}
/**
* Every run, we try to avoid processing the same email twice because
* there is no API trigger to run a script on every new email received.
*/
function processInbox() {
var props = PropertiesService.getScriptProperties();
var lastRun = parseInt(props.getProperty('lastRun') || '0');
var now = Math.floor(Date.now() / 1000);
// On first run, look back 2 minutes
if (lastRun === 0) {
lastRun = now - 120;
}
var label = GmailApp.getUserLabelByName(LABEL_NAME);
if (!label) {
label = GmailApp.createLabel(LABEL_NAME);
}
console.log("last run: " + lastRun);
var threads = GmailApp.search("from:" + SENDER + " after:" + lastRun);
console.log("threads to process: " + threads.length);
for (var i = 0; i < threads.length; i++) {
var thread = threads[i];
var messages = thread.getMessages();
console.log("messages to process: " + messages.length);
for (var j = 0; j < messages.length; j++) {
if (hasReviewerStamp(messages[j])) {
thread.addLabel(label);
console.log(thread.getFirstMessageSubject());
break;
}
}
}
props.setProperty('lastRun', String(now));
}
function hasReviewerStamp(message) {
var raw = message.getRawContent();
var match = raw.match(/^X-Phabricator-Stamps:\s*(.+)$/m);
if (!match) {
return false;
}
var stamps = match[1].trim().split(/\s+/);
return (stamps.indexOf("reviewer(@" + REVIEWER + ")") > -1) && raw.indexOf(BODY_MATCH) > -1;
}
/**
* For debugging - see the list of labels you can search which
* differs from what is used in the Gmail UI filter.
*/
function listAllLabels() {
console.log("All labels");
var labels = GmailApp.getUserLabels();
for (var i = 0; i < labels.length; i++) {
console.log(labels[i].getName());
}
}