From 1343b20bd64cdc68333ad4dbd445445e5fbde63f Mon Sep 17 00:00:00 2001 From: "Mr. M" Date: Fri, 26 Sep 2025 17:47:44 +0200 Subject: [PATCH] feat: Add learning for omnibox commands, b=no-bug, c=common --- .../urlbar/UrlbarPrefs-sys-mjs.patch | 4 +- .../urlbar/UrlbarUtils-sys-mjs.patch | 20 ++-- .../urlbar/UrlbarView-sys-mjs.patch | 38 +----- src/zen/common/styles/zen-omnibox.css | 1 + src/zen/urlbar/ZenUBActionsProvider.sys.mjs | 40 +++++-- src/zen/urlbar/ZenUBGlobalActions.sys.mjs | 14 +-- src/zen/urlbar/ZenUBResultsLearner.sys.mjs | 108 ++++++++++++++++++ src/zen/urlbar/moz.build | 1 + surfer.json | 2 +- 9 files changed, 162 insertions(+), 66 deletions(-) create mode 100644 src/zen/urlbar/ZenUBResultsLearner.sys.mjs diff --git a/src/browser/components/urlbar/UrlbarPrefs-sys-mjs.patch b/src/browser/components/urlbar/UrlbarPrefs-sys-mjs.patch index 6cc0c51c1..2b93e360f 100644 --- a/src/browser/components/urlbar/UrlbarPrefs-sys-mjs.patch +++ b/src/browser/components/urlbar/UrlbarPrefs-sys-mjs.patch @@ -1,12 +1,12 @@ diff --git a/browser/components/urlbar/UrlbarPrefs.sys.mjs b/browser/components/urlbar/UrlbarPrefs.sys.mjs -index 3c179db3b310c43f8c6c06b1ecbcf5ed59feefe6..693bef15401cd4428c8a0222de57b83b78564194 100644 +index 3c179db3b310c43f8c6c06b1ecbcf5ed59feefe6..d9d2ce116ebcee8d403e165066c3a569bb952cd2 100644 --- a/browser/components/urlbar/UrlbarPrefs.sys.mjs +++ b/browser/components/urlbar/UrlbarPrefs.sys.mjs @@ -719,6 +719,7 @@ function makeResultGroups({ showSearchSuggestionsFirst }) { */ let rootGroup = { children: [ -+ { group: lazy.UrlbarUtils.RESULT_GROUP.ZEN_ACTION }, ++ { children: [{ group: lazy.UrlbarUtils.RESULT_GROUP.ZEN_ACTION }] }, // heuristic { maxResultCount: 1, diff --git a/src/browser/components/urlbar/UrlbarUtils-sys-mjs.patch b/src/browser/components/urlbar/UrlbarUtils-sys-mjs.patch index cc0b30de1..81a9ac12a 100644 --- a/src/browser/components/urlbar/UrlbarUtils-sys-mjs.patch +++ b/src/browser/components/urlbar/UrlbarUtils-sys-mjs.patch @@ -1,5 +1,5 @@ diff --git a/browser/components/urlbar/UrlbarUtils.sys.mjs b/browser/components/urlbar/UrlbarUtils.sys.mjs -index 0bc15c02f56dd8f46a21fed02b4e21a741f27f41..b69a4d34f700637bd352620c520b989cf00fa39f 100644 +index 0bc15c02f56dd8f46a21fed02b4e21a741f27f41..3b2c1218532dcbd79d5c970ab9fd075d4e58d1ff 100644 --- a/browser/components/urlbar/UrlbarUtils.sys.mjs +++ b/browser/components/urlbar/UrlbarUtils.sys.mjs @@ -75,6 +75,7 @@ export var UrlbarUtils = { @@ -10,12 +10,12 @@ index 0bc15c02f56dd8f46a21fed02b4e21a741f27f41..b69a4d34f700637bd352620c520b989c }), // Defines provider types. -@@ -576,6 +577,8 @@ export var UrlbarUtils = { - return this.RESULT_GROUP.INPUT_HISTORY; - case "UrlbarProviderQuickSuggest": - return this.RESULT_GROUP.GENERAL_PARENT; -+ case "ZenUrlbarProviderGlobalActions": -+ return this.RESULT_GROUP.ZEN_ACTION; - default: - break; - } +@@ -553,6 +554,8 @@ export var UrlbarUtils = { + return this.RESULT_GROUP.HEURISTIC_SEARCH_TIP; + case "HistoryUrlHeuristic": + return this.RESULT_GROUP.HEURISTIC_HISTORY_URL; ++ case "ZenUrlbarProviderGlobalActions": ++ return this.RESULT_GROUP.ZEN_ACTION; + default: + if (result.providerName.startsWith("TestProvider")) { + return this.RESULT_GROUP.HEURISTIC_TEST; diff --git a/src/browser/components/urlbar/UrlbarView-sys-mjs.patch b/src/browser/components/urlbar/UrlbarView-sys-mjs.patch index 15c3687e2..0e0812bd7 100644 --- a/src/browser/components/urlbar/UrlbarView-sys-mjs.patch +++ b/src/browser/components/urlbar/UrlbarView-sys-mjs.patch @@ -1,5 +1,5 @@ diff --git a/browser/components/urlbar/UrlbarView.sys.mjs b/browser/components/urlbar/UrlbarView.sys.mjs -index fdbab8806fd320f4aacec46a42c8ef953580d00c..e23fae0d7e0b71d74899c11c229359864cd7e427 100644 +index fdbab8806fd320f4aacec46a42c8ef953580d00c..031a615ad09274c578184b129434bbd93b49353d 100644 --- a/browser/components/urlbar/UrlbarView.sys.mjs +++ b/browser/components/urlbar/UrlbarView.sys.mjs @@ -613,7 +613,7 @@ export class UrlbarView { @@ -11,39 +11,7 @@ index fdbab8806fd320f4aacec46a42c8ef953580d00c..e23fae0d7e0b71d74899c11c22935986 // Try to reuse the cached top-sites context. If it's not cached, then // there will be a gap of time between when the input is focused and // when the view opens that can be perceived as flicker. -@@ -824,6 +824,31 @@ export class UrlbarView { - // still associated with the first result. - this.input.setResultForCurrentValue(firstResult); - } -+ if (queryContext.results[0].payload.zenAction) { -+ this.#selectElement(this.getFirstSelectableElement(), { -+ updateInput: false, -+ setAccessibleFocus: -+ this.controller._userSelectionBehavior == "arrow", -+ }); -+ this.window.setTimeout(() => { -+ this.window.setTimeout(() => { -+ this.#selectElement(this.getFirstSelectableElement(), { -+ updateInput: false, -+ setAccessibleFocus: -+ this.controller._userSelectionBehavior == "arrow", -+ }); -+ }); -+ }); -+ } -+ this.window.setTimeout(() => { -+ if (queryContext.results[0].payload.zenAction) { -+ this.#selectElement(this.getFirstSelectableElement(), { -+ updateInput: false, -+ setAccessibleFocus: -+ this.controller._userSelectionBehavior == "arrow", -+ }); -+ } -+ }, 220); - } - - // Announce tab-to-search results to screen readers as the user types. -@@ -2706,6 +2731,8 @@ export class UrlbarView { +@@ -2706,6 +2706,8 @@ export class UrlbarView { if (row?.hasAttribute("row-selectable")) { row?.toggleAttribute("selected", true); } @@ -52,7 +20,7 @@ index fdbab8806fd320f4aacec46a42c8ef953580d00c..e23fae0d7e0b71d74899c11c22935986 if (element != row) { row?.toggleAttribute("descendant-selected", true); } -@@ -3189,7 +3216,7 @@ export class UrlbarView { +@@ -3189,7 +3191,7 @@ export class UrlbarView { } #enableOrDisableRowWrap() { diff --git a/src/zen/common/styles/zen-omnibox.css b/src/zen/common/styles/zen-omnibox.css index 90d675f82..ef8356c24 100644 --- a/src/zen/common/styles/zen-omnibox.css +++ b/src/zen/common/styles/zen-omnibox.css @@ -627,6 +627,7 @@ & .urlbarView-favicon { fill: currentColor !important; + stroke: currentColor !important; } & .urlbarView-shortcutContent { diff --git a/src/zen/urlbar/ZenUBActionsProvider.sys.mjs b/src/zen/urlbar/ZenUBActionsProvider.sys.mjs index e8f3b2ff1..f675ef223 100644 --- a/src/zen/urlbar/ZenUBActionsProvider.sys.mjs +++ b/src/zen/urlbar/ZenUBActionsProvider.sys.mjs @@ -5,6 +5,7 @@ import { XPCOMUtils } from 'resource://gre/modules/XPCOMUtils.sys.mjs'; import { UrlbarProvider, UrlbarUtils } from 'resource:///modules/UrlbarUtils.sys.mjs'; import { globalActions } from 'resource:///modules/ZenUBGlobalActions.sys.mjs'; +import { zenUrlbarResultsLearner } from './ZenUBResultsLearner.sys.mjs'; const lazy = {}; @@ -14,14 +15,13 @@ const DYNAMIC_TYPE_NAME = 'zen-actions'; const MAX_RECENT_ACTIONS = 5; const MINIMUM_QUERY_SCORE = 92; -const EN_LOCALE_MATCH = /^en(-.*)$/; - ChromeUtils.defineESModuleGetters(lazy, { UrlbarResult: 'resource:///modules/UrlbarResult.sys.mjs', UrlbarTokenizer: 'resource:///modules/UrlbarTokenizer.sys.mjs', QueryScorer: 'resource:///modules/UrlbarProviderInterventions.sys.mjs', BrowserWindowTracker: 'resource:///modules/BrowserWindowTracker.sys.mjs', AddonManager: 'resource://gre/modules/AddonManager.sys.mjs', + zenUrlbarResultsLearner: 'resource:///modules/ZenUBResultsLearner.sys.mjs', }); XPCOMUtils.defineLazyPreferenceGetter( @@ -35,6 +35,8 @@ XPCOMUtils.defineLazyPreferenceGetter( * A provider that lets the user view all available global actions for a query. */ export class ZenUrlbarProviderGlobalActions extends UrlbarProvider { + #seenCommands = new Set(); + constructor() { super(); lazy.UrlbarResult.addDynamicResultType(DYNAMIC_TYPE_NAME); @@ -64,8 +66,7 @@ export class ZenUrlbarProviderGlobalActions extends UrlbarProvider { queryContext.searchString && queryContext.searchString.length < UrlbarUtils.MAX_TEXT_LENGTH && queryContext.searchString.length > 2 && - !lazy.UrlbarTokenizer.REGEXP_LIKE_PROTOCOL.test(queryContext.searchString) && - EN_LOCALE_MATCH.test(Services.locale.appLocaleAsBCP47) + !lazy.UrlbarTokenizer.REGEXP_LIKE_PROTOCOL.test(queryContext.searchString) ); } @@ -92,6 +93,7 @@ export class ZenUrlbarProviderGlobalActions extends UrlbarProvider { prettyIcon: workspace.icon, accentColor, }, + commandId: `zen:workspace-${workspace.uuid}`, icon: 'chrome://browser/skin/zen-icons/forward.svg', }); } @@ -116,6 +118,7 @@ export class ZenUrlbarProviderGlobalActions extends UrlbarProvider { return { icon: 'chrome://browser/skin/zen-icons/extension.svg', label: 'Extension', + commandId: `zen:extension-${addon.id}`, extraPayload: { extensionId: addon.id, prettyName: addon.name, @@ -227,6 +230,7 @@ export class ZenUrlbarProviderGlobalActions extends UrlbarProvider { } const ownerGlobal = lazy.BrowserWindowTracker.getTopWindow(); + const finalResults = []; for (const action of actionsResults) { const [payload, payloadHighlights] = lazy.UrlbarResult.payloadAndSimpleHighlights([], { suggestion: action.label, @@ -235,7 +239,7 @@ export class ZenUrlbarProviderGlobalActions extends UrlbarProvider { zenCommand: action.command, dynamicType: DYNAMIC_TYPE_NAME, zenAction: true, - icon: action.icon || 'chrome://browser/skin/trending.svg', + icon: action.icon, shortcutContent: ownerGlobal.gZenKeyboardShortcutsManager.getShortcutDisplayFromCommand( action.command ), @@ -249,11 +253,18 @@ export class ZenUrlbarProviderGlobalActions extends UrlbarProvider { payload, payloadHighlights ); - if (typeof action.suggestedIndex === 'number') { - result.suggestedIndex = action.suggestedIndex; + if (zenUrlbarResultsLearner.shouldPrioritize(action.commandId)) { + result.heuristic = true; + } else { + result.suggestedIndex = zenUrlbarResultsLearner.getDeprioritizeIndex(action.commandId); } - addCallback(this, result); + result.commandId = action.commandId; + this.#seenCommands.add(action.commandId); + finalResults.push(result); } + zenUrlbarResultsLearner.sortCommandsByPriority(finalResults).forEach((result) => { + addCallback(this, result); + }); } /** @@ -362,6 +373,19 @@ export class ZenUrlbarProviderGlobalActions extends UrlbarProvider { }; } + onSearchSessionEnd(_queryContext, _controller, details) { + // We should only record the execution if a result was actually used. + // Otherwise we would start de-prioritizing commands that were never used. + if (details?.result) { + let usedCommand = null; + if (details?.provider === this.name) { + usedCommand = details.result?.commandId; + } + zenUrlbarResultsLearner.recordExecution(usedCommand, [...this.#seenCommands]); + } + this.#seenCommands = new Set(); + } + onEngagement(queryContext, controller, details) { const result = details.result; const payload = result.payload; diff --git a/src/zen/urlbar/ZenUBGlobalActions.sys.mjs b/src/zen/urlbar/ZenUBGlobalActions.sys.mjs index 2fdb25324..ba5a88abe 100644 --- a/src/zen/urlbar/ZenUBGlobalActions.sys.mjs +++ b/src/zen/urlbar/ZenUBGlobalActions.sys.mjs @@ -11,7 +11,6 @@ const globalActionsTemplate = [ label: 'Toggle Compact Mode', command: 'cmd_zenCompactModeToggle', icon: 'chrome://browser/skin/zen-icons/sidebar.svg', - suggestedIndex: 0, }, { label: 'Open Theme Picker', @@ -22,7 +21,6 @@ const globalActionsTemplate = [ label: 'New Split View', command: 'cmd_zenNewEmptySplit', icon: 'chrome://browser/skin/zen-icons/split.svg', - suggestedIndex: 0, }, { label: 'New Folder', @@ -33,7 +31,6 @@ const globalActionsTemplate = [ label: 'Copy Current URL', command: 'cmd_zenCopyCurrentURL', icon: 'chrome://browser/skin/zen-icons/edit-copy.svg', - suggestedIndex: 0, }, { label: 'Settings', @@ -89,7 +86,6 @@ const globalActionsTemplate = [ label: 'Close Tab', command: 'cmd_close', icon: 'chrome://browser/skin/zen-icons/close.svg', - suggestedIndex: 1, isAvailable: (window) => { return isNotEmptyTab(window); }, @@ -121,13 +117,11 @@ const globalActionsTemplate = [ isAvailable: (window) => { return isNotEmptyTab(window); }, - suggestedIndex: 1, }, { label: 'Toggle Tabs on right', command: 'cmd_zenToggleTabsOnRight', icon: 'chrome://browser/skin/zen-icons/sidebars-right.svg', - suggestedIndex: 1, }, { label: 'Add to Essentials', @@ -139,14 +133,12 @@ const globalActionsTemplate = [ ); }, icon: 'chrome://browser/skin/zen-icons/essential-add.svg', - suggestedIndex: 1, }, { label: 'Remove from Essentials', command: (window) => window.gZenPinnedTabManager.removeEssentials(window.gBrowser.selectedTab), isAvailable: (window) => window.gBrowser.selectedTab.hasAttribute('zen-essential'), icon: 'chrome://browser/skin/zen-icons/essential-remove.svg', - suggestedIndex: 1, }, { label: 'Find in Page', @@ -155,13 +147,11 @@ const globalActionsTemplate = [ isAvailable: (window) => { return isNotEmptyTab(window); }, - suggestedIndex: 1, }, { label: 'Manage Extensions', command: 'Tools:Addons', icon: 'chrome://browser/skin/zen-icons/extension.svg', - suggestedIndex: 1, }, ]; @@ -169,6 +159,10 @@ export const globalActions = globalActionsTemplate.map((action) => ({ isAvailable: (window) => { return window.document.getElementById(action.command)?.getAttribute('disabled') !== 'true'; }, + commandId: + typeof action.command === 'string' + ? action.command + : `zen:global-action-${action.label.toLowerCase().replace(/\s+/g, '-')}`, extraPayload: {}, ...action, })); diff --git a/src/zen/urlbar/ZenUBResultsLearner.sys.mjs b/src/zen/urlbar/ZenUBResultsLearner.sys.mjs new file mode 100644 index 000000000..99165f85f --- /dev/null +++ b/src/zen/urlbar/ZenUBResultsLearner.sys.mjs @@ -0,0 +1,108 @@ +/* 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/. */ + +import { XPCOMUtils } from 'resource://gre/modules/XPCOMUtils.sys.mjs'; + +const lazy = {}; + +const DEFAULT_DB_DATA = '{}'; +const DEPRIORITIZE_MAX = -4; +const PRIORITIZE_MAX = 5; + +XPCOMUtils.defineLazyPreferenceGetter( + lazy, + 'database', + 'zen.urlbar.suggestions-learner', + 'DEFAULT_DB_DATA' +); + +/** + * A class that manages the learning of URL bar results for commands, + * can be used for any ID that can be executed in the URL bar. + * + * The schema would be something like: + * { + * "": , + * } + * + * If the current command is not on the list is because the user + * has *seen* the command but never executed it. If the number is + * less than -2, that means the user will most likely never use it. + * + * The priority number is incremented each time the command is executed. + */ +class ZenUrlbarResultsLearner { + constructor() {} + + get database() { + try { + return JSON.parse(lazy.database); + } catch { + return {}; + } + } + + saveDatabase(db) { + Services.prefs.setStringPref( + 'zen.urlbar.suggestions-learner', + JSON.stringify(db || DEFAULT_DB_DATA) + ); + } + + recordExecution(commandId, seenCommands = []) { + const db = this.database; + if (commandId) { + const numberOfUsages = Math.min((db[commandId] || 0) + 1, PRIORITIZE_MAX); + db[commandId] = numberOfUsages; + } + for (const cmd of seenCommands) { + if (cmd !== commandId) { + if (!db[cmd]) { + db[cmd] = -1; + } else { + const newIndex = Math.max(db[cmd] - 1, DEPRIORITIZE_MAX); + db[cmd] = newIndex; + if (newIndex === 0) { + // Save some space by deleting commands that are not used + // and have a neutral score. + delete db[cmd]; + } + } + } + } + this.saveDatabase(db); + } + + shouldPrioritize(commandId) { + if (!commandId) { + return false; + } + const db = this.database; + return !!db[commandId] && db[commandId] > 0; + } + + getDeprioritizeIndex(commandId) { + if (!commandId) { + return 1; + } + const db = this.database; + if (db[commandId] < 0) { + return Math.abs(db[commandId]); + } + // This will most likely never run, since + // positive commands are prioritized. + return 1; + } + + /** + * Sorts the given commands by their priority in the database. + * @param {*} commands + */ + sortCommandsByPriority(commands) { + const db = this.database; + return commands.sort((a, b) => (db[b.commandId] || 0) - (db[a.commandId] || 0)); + } +} + +export const zenUrlbarResultsLearner = new ZenUrlbarResultsLearner(); diff --git a/src/zen/urlbar/moz.build b/src/zen/urlbar/moz.build index 306a7c08b..1e331db90 100644 --- a/src/zen/urlbar/moz.build +++ b/src/zen/urlbar/moz.build @@ -6,4 +6,5 @@ EXTRA_JS_MODULES += [ "ZenUBActionsProvider.sys.mjs", "ZenUBGlobalActions.sys.mjs", "ZenUBProvider.sys.mjs", + "ZenUBResultsLearner.sys.mjs", ] diff --git a/surfer.json b/surfer.json index 64ca34198..f408413bf 100644 --- a/surfer.json +++ b/surfer.json @@ -19,7 +19,7 @@ "brandShortName": "Zen", "brandFullName": "Zen Browser", "release": { - "displayVersion": "1.16.1b", + "displayVersion": "1.16.2b", "github": { "repo": "zen-browser/desktop" },