diff --git a/src/browser/app/profile/features.inc b/src/browser/app/profile/features.inc
index 83993709f1..4db348e569 100644
--- a/src/browser/app/profile/features.inc
+++ b/src/browser/app/profile/features.inc
@@ -78,6 +78,9 @@ pref('zen.glance.hold-duration', 300); // in ms
pref('zen.glance.open-essential-external-links', true);
pref('zen.glance.activation-method', 'alt'); // ctrl, alt, shift, none, hold
+pref('zen.tabsearch.enabled', true);
+pref('zen.tabsearch.auto-select-result', false);
+
pref('zen.view.sidebar-height-throttle', 200); // in ms
pref('zen.view.sidebar-expanded.max-width', 500);
diff --git a/src/browser/base/content/zen-assets.inc.xhtml b/src/browser/base/content/zen-assets.inc.xhtml
index a22c866350..acb2a69612 100644
--- a/src/browser/base/content/zen-assets.inc.xhtml
+++ b/src/browser/base/content/zen-assets.inc.xhtml
@@ -44,4 +44,5 @@ Services.scriptloader.loadSubScript("chrome://browser/content/zen-components/Zen
Services.scriptloader.loadSubScript("chrome://browser/content/zen-components/ZenGlanceManager.mjs", this);
Services.scriptloader.loadSubScript("chrome://browser/content/zen-components/ZenMediaController.mjs", this);
Services.scriptloader.loadSubScript("chrome://browser/content/zen-components/ZenDownloadAnimation.mjs", this);
+Services.scriptloader.loadSubScript("chrome://browser/content/zen-components/ZenTabSearch.mjs", this);
diff --git a/src/browser/base/content/zen-assets.jar.inc.mn b/src/browser/base/content/zen-assets.jar.inc.mn
index b083e20ef2..90ee0206a4 100644
--- a/src/browser/base/content/zen-assets.jar.inc.mn
+++ b/src/browser/base/content/zen-assets.jar.inc.mn
@@ -61,6 +61,8 @@
content/browser/zen-components/actors/ZenGlanceChild.sys.mjs (../../zen/glance/actors/ZenGlanceChild.sys.mjs)
content/browser/zen-components/actors/ZenGlanceParent.sys.mjs (../../zen/glance/actors/ZenGlanceParent.sys.mjs)
+ content/browser/zen-components/ZenTabSearch.mjs (../../zen/tabsearch/ZenTabSearch.mjs)
+
content/browser/zen-components/ZenFolders.mjs (../../zen/folders/ZenFolders.mjs)
content/browser/zen-styles/zen-folders.css (../../zen/folders/zen-folders.css)
diff --git a/src/browser/base/content/zen-keysets.inc.xhtml b/src/browser/base/content/zen-keysets.inc.xhtml
index 1b3c25f231..51f8147e77 100644
--- a/src/browser/base/content/zen-keysets.inc.xhtml
+++ b/src/browser/base/content/zen-keysets.inc.xhtml
@@ -47,6 +47,10 @@
+
+
+
+
diff --git a/src/browser/components/preferences/zen-settings.js b/src/browser/components/preferences/zen-settings.js
index 512d2e91d0..31ec22939b 100644
--- a/src/browser/components/preferences/zen-settings.js
+++ b/src/browser/components/preferences/zen-settings.js
@@ -777,6 +777,7 @@ var gZenWorkspacesSettings = {
};
Services.prefs.addObserver('zen.tab-unloader.enabled', tabsUnloaderPrefListener);
Services.prefs.addObserver('zen.glance.enabled', tabsUnloaderPrefListener); // We can use the same listener for both prefs
+ Services.prefs.addObserver('zen.tabsearch.enabled', tabsUnloaderPrefListener); // We can use the same listener for both prefs
Services.prefs.addObserver(
'zen.workspaces.container-specific-essentials-enabled',
tabsUnloaderPrefListener
@@ -1169,6 +1170,16 @@ Preferences.addAll([
type: 'bool',
default: true,
},
+ {
+ id: 'zen.tabsearch.enabled',
+ type: 'bool',
+ default: true,
+ },
+ {
+ id: 'zen.tabsearch.auto-select-result',
+ type: 'bool',
+ default: false,
+ },
{
id: 'zen.theme.color-prefs.use-workspace-colors',
type: 'bool',
diff --git a/src/browser/components/tabbrowser/content/tabbrowser-js.patch b/src/browser/components/tabbrowser/content/tabbrowser-js.patch
index dfaaacb142..2f7dd39338 100644
--- a/src/browser/components/tabbrowser/content/tabbrowser-js.patch
+++ b/src/browser/components/tabbrowser/content/tabbrowser-js.patch
@@ -2,7 +2,32 @@ diff --git a/browser/components/tabbrowser/content/tabbrowser.js b/browser/compo
index 6dece2b9d0462d90a28e75350ce983d87816ef73..5c49c43714b3914130f8d821d902f9f255c4ebc9 100644
--- a/browser/components/tabbrowser/content/tabbrowser.js
+++ b/browser/components/tabbrowser/content/tabbrowser.js
-@@ -415,11 +415,45 @@
+@@ -117,6 +117,24 @@
+ UrlbarProviderOpenTabs:
+ "resource:///modules/UrlbarProviderOpenTabs.sys.mjs",
+ });
++ // Add TabSelect event listener to update open tab information
++ this.tabContainer.addEventListener("TabSelect", async event => {
++ let tab = event.target;
++ let browser = tab.linkedBrowser;
++ let url = browser.currentURI.spec;
++ let userContextId = tab.getAttribute("usercontextid") || 0;
++ let isInPrivateWindow = PrivateBrowsingUtils.isWindowPrivate(window);
++
++ try {
++ await this.UrlbarProviderOpenTabs.updateOpenTab(
++ url,
++ parseInt(userContextId),
++ isInPrivateWindow
++ );
++ } catch (error) {
++ console.error("Failed to update open tab:", error);
++ }
++ });
+ ChromeUtils.defineLazyGetter(this, "tabLocalization", () => {
+ return new Localization(
+ ["browser/tabbrowser.ftl", "branding/brand.ftl"],
+@@ -415,11 +433,45 @@
return this.tabContainer.visibleTabs;
}
@@ -50,7 +75,7 @@ index 6dece2b9d0462d90a28e75350ce983d87816ef73..5c49c43714b3914130f8d821d902f9f2
}
return i;
}
-@@ -571,6 +605,7 @@
+@@ -571,6 +623,7 @@
this.tabpanels.appendChild(panel);
let tab = this.tabs[0];
@@ -58,7 +83,7 @@ index 6dece2b9d0462d90a28e75350ce983d87816ef73..5c49c43714b3914130f8d821d902f9f2
tab.linkedPanel = uniqueId;
this._selectedTab = tab;
this._selectedBrowser = browser;
-@@ -836,11 +871,13 @@
+@@ -836,11 +889,13 @@
}
this.showTab(aTab);
@@ -75,7 +100,7 @@ index 6dece2b9d0462d90a28e75350ce983d87816ef73..5c49c43714b3914130f8d821d902f9f2
this.moveTabTo(aTab, {
tabIndex: this.pinnedTabCount,
forceUngrouped: true,
-@@ -857,12 +894,15 @@
+@@ -857,12 +912,15 @@
}
if (this.tabContainer.verticalMode) {
@@ -92,7 +117,7 @@ index 6dece2b9d0462d90a28e75350ce983d87816ef73..5c49c43714b3914130f8d821d902f9f2
});
} else {
this.moveTabTo(aTab, {
-@@ -1046,6 +1086,8 @@
+@@ -1046,6 +1104,8 @@
let LOCAL_PROTOCOLS = ["chrome:", "about:", "resource:", "data:"];
@@ -101,7 +126,7 @@ index 6dece2b9d0462d90a28e75350ce983d87816ef73..5c49c43714b3914130f8d821d902f9f2
if (
aIconURL &&
!aLoadingPrincipal &&
-@@ -1056,6 +1098,9 @@
+@@ -1056,6 +1116,9 @@
);
return;
}
@@ -111,7 +136,7 @@ index 6dece2b9d0462d90a28e75350ce983d87816ef73..5c49c43714b3914130f8d821d902f9f2
let browser = this.getBrowserForTab(aTab);
browser.mIconURL = aIconURL;
-@@ -1305,6 +1350,7 @@
+@@ -1305,6 +1368,7 @@
if (!this._previewMode) {
newTab.recordTimeFromUnloadToReload();
newTab.updateLastAccessed();
@@ -119,7 +144,7 @@ index 6dece2b9d0462d90a28e75350ce983d87816ef73..5c49c43714b3914130f8d821d902f9f2
oldTab.updateLastAccessed();
// if this is the foreground window, update the last-seen timestamps.
if (this.ownerGlobal == BrowserWindowTracker.getTopWindow()) {
-@@ -1457,6 +1503,9 @@
+@@ -1457,6 +1521,9 @@
}
let activeEl = document.activeElement;
@@ -129,7 +154,7 @@ index 6dece2b9d0462d90a28e75350ce983d87816ef73..5c49c43714b3914130f8d821d902f9f2
// If focus is on the old tab, move it to the new tab.
if (activeEl == oldTab) {
newTab.focus();
-@@ -1780,7 +1829,8 @@
+@@ -1780,7 +1847,8 @@
}
_setTabLabel(aTab, aLabel, { beforeTabOpen, isContentTitle, isURL } = {}) {
@@ -139,7 +164,7 @@ index 6dece2b9d0462d90a28e75350ce983d87816ef73..5c49c43714b3914130f8d821d902f9f2
return false;
}
-@@ -1888,7 +1938,7 @@
+@@ -1888,7 +1956,7 @@
newIndex = this.selectedTab._tPos + 1;
}
@@ -148,7 +173,7 @@ index 6dece2b9d0462d90a28e75350ce983d87816ef73..5c49c43714b3914130f8d821d902f9f2
if (this.isTabGroupLabel(targetTab)) {
throw new Error(
"Replacing a tab group label with a tab is not supported"
-@@ -2152,6 +2202,7 @@
+@@ -2152,6 +2220,7 @@
uriIsAboutBlank,
userContextId,
skipLoad,
@@ -156,7 +181,7 @@ index 6dece2b9d0462d90a28e75350ce983d87816ef73..5c49c43714b3914130f8d821d902f9f2
} = {}) {
let b = document.createXULElement("browser");
// Use the JSM global to create the permanentKey, so that if the
-@@ -2225,8 +2276,7 @@
+@@ -2225,8 +2294,7 @@
// we use a different attribute name for this?
b.setAttribute("name", name);
}
@@ -166,7 +191,7 @@ index 6dece2b9d0462d90a28e75350ce983d87816ef73..5c49c43714b3914130f8d821d902f9f2
b.setAttribute("transparent", "true");
}
-@@ -2391,7 +2441,7 @@
+@@ -2391,7 +2459,7 @@
let panel = this.getPanel(browser);
let uniqueId = this._generateUniquePanelID();
@@ -175,7 +200,7 @@ index 6dece2b9d0462d90a28e75350ce983d87816ef73..5c49c43714b3914130f8d821d902f9f2
aTab.linkedPanel = uniqueId;
// Inject the into the DOM if necessary.
-@@ -2450,8 +2500,8 @@
+@@ -2450,8 +2518,8 @@
// If we transitioned from one browser to two browsers, we need to set
// hasSiblings=false on both the existing browser and the new browser.
if (this.tabs.length == 2) {
@@ -186,7 +211,7 @@ index 6dece2b9d0462d90a28e75350ce983d87816ef73..5c49c43714b3914130f8d821d902f9f2
} else {
aTab.linkedBrowser.browsingContext.hasSiblings = this.tabs.length > 1;
}
-@@ -2679,6 +2729,7 @@
+@@ -2679,6 +2747,7 @@
schemelessInput,
hasValidUserGestureActivation = false,
textDirectiveUserActivation = false,
@@ -194,7 +219,7 @@ index 6dece2b9d0462d90a28e75350ce983d87816ef73..5c49c43714b3914130f8d821d902f9f2
} = {}
) {
// all callers of addTab that pass a params object need to pass
-@@ -2689,6 +2740,12 @@
+@@ -2689,6 +2758,12 @@
);
}
@@ -207,7 +232,7 @@ index 6dece2b9d0462d90a28e75350ce983d87816ef73..5c49c43714b3914130f8d821d902f9f2
if (!UserInteraction.running("browser.tabs.opening", window)) {
UserInteraction.start("browser.tabs.opening", "initting", window);
}
-@@ -2752,6 +2809,16 @@
+@@ -2752,6 +2827,16 @@
noInitialLabel,
skipBackgroundNotify,
});
@@ -224,7 +249,7 @@ index 6dece2b9d0462d90a28e75350ce983d87816ef73..5c49c43714b3914130f8d821d902f9f2
if (insertTab) {
if (typeof index == "number") {
elementIndex = this.#tabIndexToElementIndex(index);
-@@ -2779,6 +2846,7 @@
+@@ -2779,6 +2864,7 @@
openWindowInfo,
skipLoad,
triggeringRemoteType,
@@ -232,7 +257,7 @@ index 6dece2b9d0462d90a28e75350ce983d87816ef73..5c49c43714b3914130f8d821d902f9f2
}));
if (focusUrlBar) {
-@@ -2898,6 +2966,12 @@
+@@ -2898,6 +2984,12 @@
}
}
@@ -245,7 +270,7 @@ index 6dece2b9d0462d90a28e75350ce983d87816ef73..5c49c43714b3914130f8d821d902f9f2
// Additionally send pinned tab events
if (pinned) {
this._notifyPinnedStatus(t);
-@@ -2945,12 +3019,15 @@
+@@ -2945,12 +3037,15 @@
* @param {string} [label=]
* @returns {MozTabbrowserTabGroup}
*/
diff --git a/src/browser/components/urlbar/UrlbarMuxerUnifiedComplete-sys-mjs.patch b/src/browser/components/urlbar/UrlbarMuxerUnifiedComplete-sys-mjs.patch
index 2d6f456e28..bcd54408c6 100644
--- a/src/browser/components/urlbar/UrlbarMuxerUnifiedComplete-sys-mjs.patch
+++ b/src/browser/components/urlbar/UrlbarMuxerUnifiedComplete-sys-mjs.patch
@@ -1,8 +1,28 @@
diff --git a/browser/components/urlbar/UrlbarMuxerUnifiedComplete.sys.mjs b/browser/components/urlbar/UrlbarMuxerUnifiedComplete.sys.mjs
-index dcf1af43d62979d3226d7f704c51a2f0bb935cc0..8879d657b99cb20cd657c2e4841738ffaa09c658 100644
+index dcf1af43d62979d3226d7f704c51a2f0bb935cc0..a8271ac083c54947756e6ef35a3b4eac407b03f9 100644
--- a/browser/components/urlbar/UrlbarMuxerUnifiedComplete.sys.mjs
+++ b/browser/components/urlbar/UrlbarMuxerUnifiedComplete.sys.mjs
-@@ -794,6 +794,7 @@ class MuxerUnifiedComplete extends UrlbarMuxer {
+@@ -117,6 +117,19 @@ class MuxerUnifiedComplete extends UrlbarMuxer {
+ // When you add state, update _copyState() as necessary.
+ };
+
++ let window = Services.wm.getMostRecentWindow("navigator:browser");
++ if (window && typeof window.gZenTabSearch !== "undefined" && window.gURLBar && window.gURLBar.searchMode && window.gURLBar.searchMode.source === 4) {
++ unsortedResults.sort((a, b) => {
++ const lastOpenedA = a.payload?.lastOpened || 0;
++ const lastOpenedB = b.payload?.lastOpened|| 0;
++ return lastOpenedB - lastOpenedA; // Descending order
++ });
++ if (window.gZenTabSearch.shouldInjectHeuristic()) {
++ unsortedResults[0].heuristic = true
++ unsortedResults[0].providerName = "HeuristicFallback"
++ }
++ }
++
+ // Do the first pass over all results to build some state.
+ for (let result of unsortedResults) {
+ // Add each result to the appropriate `resultsByGroup` map.
+@@ -794,6 +805,7 @@ class MuxerUnifiedComplete extends UrlbarMuxer {
}
if (result.providerName == lazy.UrlbarProviderTabToSearch.name) {
diff --git a/src/browser/components/urlbar/UrlbarProviderOpenTabs-sys-mjs.patch b/src/browser/components/urlbar/UrlbarProviderOpenTabs-sys-mjs.patch
new file mode 100644
index 0000000000..dbc5e21693
--- /dev/null
+++ b/src/browser/components/urlbar/UrlbarProviderOpenTabs-sys-mjs.patch
@@ -0,0 +1,120 @@
+diff --git a/browser/components/urlbar/UrlbarProviderOpenTabs.sys.mjs b/browser/components/urlbar/UrlbarProviderOpenTabs.sys.mjs
+index 8e76ad53dbc9af7a748b6eecf6ccf7984c61b0b6..a4a2663793a4dcd38fa88fa88c2f30993ba4e49f 100644
+--- a/browser/components/urlbar/UrlbarProviderOpenTabs.sys.mjs
++++ b/browser/components/urlbar/UrlbarProviderOpenTabs.sys.mjs
+@@ -241,6 +241,53 @@ export class UrlbarProviderOpenTabs extends UrlbarProvider {
+ await addToMemoryTable(url, userContextId).catch(console.error);
+ }
+
++ /**
++ * Updates the last_opened timestamp for an open tab and ensures it is registered.
++ *
++ * @param {string} url Address of the tab
++ * @param {integer|string} userContextId Containers user context id
++ * @param {boolean} isInPrivateWindow In private browsing window or not
++ * @returns {Promise} resolved after the update.
++ */
++ static async updateOpenTab(url, userContextId, isInPrivateWindow) {
++ // Ensure userContextId is consistently treated as an integer.
++ userContextId = parseInt(userContextId);
++ if (!Number.isInteger(userContextId)) {
++ lazy.logger.error("Invalid userContextId while updating openTab: ", {
++ url,
++ userContextId,
++ isInPrivateWindow,
++ });
++ return;
++ }
++
++ lazy.logger.info("Updating openTab: ", {
++ url,
++ userContextId,
++ isInPrivateWindow,
++ });
++
++ // Adjust userContextId for private windows.
++ userContextId = UrlbarProviderOpenTabs.getUserContextIdForOpenPagesTable(
++ userContextId,
++ isInPrivateWindow
++ );
++
++ // Ensure the tab is registered in the memory table.
++ let entries = gOpenTabUrls.get(userContextId);
++ if (!entries) {
++ entries = new Map();
++ gOpenTabUrls.set(userContextId, entries);
++ }
++ if (!entries.has(url)) {
++ entries.set(url, 1); // Register the tab if not already present.
++ await addToMemoryTable(url, userContextId).catch(console.error);
++ }
++
++ // Update the last_opened timestamp for the tab.
++ await updateLastOpened(url, userContextId).catch(console.error);
++ }
++
+ /**
+ * Unregisters a previously registered open tab.
+ *
+@@ -300,8 +347,9 @@ export class UrlbarProviderOpenTabs extends UrlbarProvider {
+ await UrlbarProviderOpenTabs.promiseDBPopulated;
+ await conn.executeCached(
+ `
+- SELECT url, userContextId
++ SELECT url, userContextId, last_opened
+ FROM moz_openpages_temp
++ ORDER BY last_opened DESC
+ `,
+ {},
+ (row, cancel) => {
+@@ -341,7 +389,7 @@ async function addToMemoryTable(url, userContextId, count = 1) {
+ let conn = await lazy.PlacesUtils.promiseLargeCacheDBConnection();
+ await conn.executeCached(
+ `
+- INSERT OR REPLACE INTO moz_openpages_temp (url, userContextId, open_count)
++ INSERT OR REPLACE INTO moz_openpages_temp (url, userContextId, open_count, last_opened)
+ VALUES ( :url,
+ :userContextId,
+ IFNULL( ( SELECT open_count + 1
+@@ -349,7 +397,8 @@ async function addToMemoryTable(url, userContextId, count = 1) {
+ WHERE url = :url
+ AND userContextId = :userContextId ),
+ :count
+- )
++ ),
++ strftime('%s', 'now') -- Update the last_opened timestamp
+ )
+ `,
+ { url, userContextId, count }
+@@ -357,6 +406,31 @@ async function addToMemoryTable(url, userContextId, count = 1) {
+ });
+ }
+
++/**
++ * Updates the last_opened timestamp for a tab in the memory table.
++ *
++ * @param {string} url Address of the tab
++ * @param {number} userContextId Containers user context id
++ * @returns {Promise} resolved after the update.
++ */
++async function updateLastOpened(url, userContextId) {
++ if (!UrlbarProviderOpenTabs.memoryTableInitialized) {
++ return;
++ }
++ await lazy.UrlbarProvidersManager.runInCriticalSection(async () => {
++ let conn = await lazy.PlacesUtils.promiseLargeCacheDBConnection();
++ await conn.executeCached(
++ `
++ UPDATE moz_openpages_temp
++ SET last_opened = strftime('%s', 'now') -- Update to current timestamp
++ WHERE url = :url
++ AND userContextId = :userContextId
++ `,
++ { url, userContextId }
++ );
++ });
++}
++
+ /**
+ * Removes an open page from the memory table.
+ *
diff --git a/src/browser/components/urlbar/UrlbarProviderPlaces-sys-mjs.patch b/src/browser/components/urlbar/UrlbarProviderPlaces-sys-mjs.patch
index 37ca86d1ee..c52cc2ad4c 100644
--- a/src/browser/components/urlbar/UrlbarProviderPlaces-sys-mjs.patch
+++ b/src/browser/components/urlbar/UrlbarProviderPlaces-sys-mjs.patch
@@ -2,22 +2,23 @@ diff --git a/browser/components/urlbar/UrlbarProviderPlaces.sys.mjs b/browser/co
index b1481a11ef38037bec13939928f72f9772e335a9..925f0dc34bf84bb9e0f143f5c1973a87e7b4f8ac 100644
--- a/browser/components/urlbar/UrlbarProviderPlaces.sys.mjs
+++ b/browser/components/urlbar/UrlbarProviderPlaces.sys.mjs
-@@ -35,6 +35,8 @@ const QUERYINDEX_SWITCHTAB = 9;
+@@ -35,6 +35,9 @@ const QUERYINDEX_SWITCHTAB = 9;
const QUERYINDEX_FRECENCY = 10;
const QUERYINDEX_USERCONTEXTID = 11;
const QUERYINDEX_LASTVIST = 12;
+const QUERYINDEX_PINNEDTITLE = 13;
+const QUERYINDEX_PINNEDURL = 14;
++const QUERYINDEX_LASTOPENED = 15;
// Constants to support an alternative frecency algorithm.
const PAGES_USE_ALT_FRECENCY = Services.prefs.getBoolPref(
-@@ -65,11 +67,14 @@ const SQL_BOOKMARK_TAGS_FRAGMENT = `EXISTS(SELECT 1 FROM moz_bookmarks WHERE fk
+@@ -65,11 +68,14 @@ const SQL_BOOKMARK_TAGS_FRAGMENT = `EXISTS(SELECT 1 FROM moz_bookmarks WHERE fk
// condition once, and avoid evaluating "btitle" and "tags" when it is false.
function defaultQuery(conditions = "") {
let query = `SELECT :query_type, h.url, h.title, ${SQL_BOOKMARK_TAGS_FRAGMENT},
- h.visit_count, h.typed, h.id, t.open_count, ${PAGES_FRECENCY_FIELD}, t.userContextId, h.last_visit_date
+ h.visit_count, h.typed, h.id, t.open_count, ${PAGES_FRECENCY_FIELD}, t.userContextId, h.last_visit_date,
-+ zp.title AS pinned_title, zp.url AS pinned_url
++ zp.title AS pinned_title, zp.url AS pinned_url, t.last_opened
FROM moz_places h
LEFT JOIN moz_openpages_temp t
ON t.url = h.url
@@ -27,7 +28,7 @@ index b1481a11ef38037bec13939928f72f9772e335a9..925f0dc34bf84bb9e0f143f5c1973a87
WHERE (
(:switchTabsEnabled AND t.open_count > 0) OR
${PAGES_FRECENCY_FIELD} <> 0
-@@ -83,7 +88,7 @@ function defaultQuery(conditions = "") {
+@@ -83,7 +89,7 @@ function defaultQuery(conditions = "") {
:matchBehavior, :searchBehavior, NULL)
ELSE
AUTOCOMPLETE_MATCH(:searchString, h.url,
@@ -36,14 +37,38 @@ index b1481a11ef38037bec13939928f72f9772e335a9..925f0dc34bf84bb9e0f143f5c1973a87
h.visit_count, h.typed,
0, t.open_count,
:matchBehavior, :searchBehavior, NULL)
-@@ -1132,11 +1137,14 @@ Search.prototype = {
+@@ -293,6 +299,7 @@ function convertLegacyMatches(context, matches, urls) {
+ firstToken: context.tokens[0],
+ userContextId: match.userContextId,
+ lastVisit: match.lastVisit,
++ lastOpened: match.lastOpened,
+ });
+ // Should not happen, but better safe than sorry.
+ if (!result) {
+@@ -342,6 +349,7 @@ function makeUrlbarResult(tokens, info) {
+ icon: info.icon,
+ userContextId: info.userContextId,
+ lastVisit: info.lastVisit,
++ lastOpened: info.lastOpened
+ });
+ if (lazy.UrlbarPrefs.get("secondaryActions.switchToTab")) {
+ payload[0].action = UrlbarUtils.createTabSwitchSecondaryAction(
+@@ -414,6 +422,7 @@ function makeUrlbarResult(tokens, info) {
+ blockL10n,
+ helpUrl,
+ lastVisit: info.lastVisit,
++ lastOpened: info.lastOpened,
+ })
+ );
+ }
+@@ -1132,15 +1141,19 @@ Search.prototype = {
let lastVisit = lastVisitPRTime
? lazy.PlacesUtils.toDate(lastVisitPRTime).getTime()
: undefined;
-
+ let pinnedTitle = row.getResultByIndex(QUERYINDEX_PINNEDTITLE);
+ let pinnedUrl = row.getResultByIndex(QUERYINDEX_PINNEDURL);
-+
++ let lastOpened = row.getResultByIndex(QUERYINDEX_LASTOPENED);
+
let match = {
placeId,
@@ -54,3 +79,8 @@ index b1481a11ef38037bec13939928f72f9772e335a9..925f0dc34bf84bb9e0f143f5c1973a87
icon: UrlbarUtils.getIconForUrl(url),
frecency: frecency || FRECENCY_DEFAULT,
userContextId,
+ lastVisit,
++ lastOpened: lastOpened || 0
+ };
+ if (openPageCount > 0 && this.hasBehavior("openpage")) {
+ if (
diff --git a/src/browser/components/urlbar/UrlbarUtils-sys-mjs.patch b/src/browser/components/urlbar/UrlbarUtils-sys-mjs.patch
new file mode 100644
index 0000000000..655523a708
--- /dev/null
+++ b/src/browser/components/urlbar/UrlbarUtils-sys-mjs.patch
@@ -0,0 +1,24 @@
+diff --git a/browser/components/urlbar/UrlbarUtils.sys.mjs b/browser/components/urlbar/UrlbarUtils.sys.mjs
+index 0968dfa885512096f1e58ec898eda139d9aea217..87974b95053bbb2b359bdcf6cf645104314abdca 100644
+--- a/browser/components/urlbar/UrlbarUtils.sys.mjs
++++ b/browser/components/urlbar/UrlbarUtils.sys.mjs
+@@ -1775,6 +1775,9 @@ UrlbarUtils.RESULT_PAYLOAD_SCHEMA = {
+ lastVisit: {
+ type: "number",
+ },
++ lastOpened: {
++ type: "number",
++ },
+ title: {
+ type: "string",
+ },
+@@ -1908,6 +1911,9 @@ UrlbarUtils.RESULT_PAYLOAD_SCHEMA = {
+ lastVisit: {
+ type: "number",
+ },
++ lastOpened: {
++ type: "number",
++ },
+ originalUrl: {
+ type: "string",
+ },
diff --git a/src/toolkit/components/places/PlacesUtils-sys-mjs.patch b/src/toolkit/components/places/PlacesUtils-sys-mjs.patch
new file mode 100644
index 0000000000..8e4e808cc6
--- /dev/null
+++ b/src/toolkit/components/places/PlacesUtils-sys-mjs.patch
@@ -0,0 +1,12 @@
+diff --git a/toolkit/components/places/PlacesUtils.sys.mjs b/toolkit/components/places/PlacesUtils.sys.mjs
+index 572d8678994b338e62f46869a2840948a74a6b1c..618878321bc605fb01d8caba466ff81ccc39fcb8 100644
+--- a/toolkit/components/places/PlacesUtils.sys.mjs
++++ b/toolkit/components/places/PlacesUtils.sys.mjs
+@@ -2248,6 +2248,7 @@ ChromeUtils.defineLazyGetter(lazy, "gAsyncDBLargeCacheConnPromised", () =>
+ url TEXT,
+ userContextId INTEGER,
+ open_count INTEGER,
++ last_opened INTEGER,
+ PRIMARY KEY (url, userContextId)
+ )`);
+ await conn.execute(`
diff --git a/src/toolkit/components/places/nsPlacesTables-h.patch b/src/toolkit/components/places/nsPlacesTables-h.patch
new file mode 100644
index 0000000000..34a01ed6fb
--- /dev/null
+++ b/src/toolkit/components/places/nsPlacesTables-h.patch
@@ -0,0 +1,12 @@
+diff --git a/toolkit/components/places/nsPlacesTables.h b/toolkit/components/places/nsPlacesTables.h
+index bfcc0c24aca4fcd951d66bd96c13ebe1a7df32eb..0ad78a3e1cd68b6efc3ea0adbb2553085fec1ef1 100644
+--- a/toolkit/components/places/nsPlacesTables.h
++++ b/toolkit/components/places/nsPlacesTables.h
+@@ -196,6 +196,7 @@
+ " url TEXT" \
+ ", userContextId INTEGER" \
+ ", open_count INTEGER" \
++ ", last_opened INTEGER" \
+ ", PRIMARY KEY (url, userContextId)" \
+ ")")
+
diff --git a/src/zen/common/zen-sets.js b/src/zen/common/zen-sets.js
index 1f73018f32..49670293c1 100644
--- a/src/zen/common/zen-sets.js
+++ b/src/zen/common/zen-sets.js
@@ -97,6 +97,11 @@ document.addEventListener(
case 'cmd_zenIgnoreUnloadTab':
gZenTabUnloader.ignoreUnloadTab();
break;
+ case 'cmd_zenSearchTabs':
+ gZenTabSearch.searchTabsShortcut();
+ break;
+ case 'cmd_zenBackwardSearchTabs':
+ gZenTabSearch.searchTabsShortcut(-1);
default:
if (event.target.id.startsWith('cmd_zenWorkspaceSwitch')) {
const index = parseInt(event.target.id.replace('cmd_zenWorkspaceSwitch', ''), 10) - 1;
diff --git a/src/zen/kbs/ZenKeyboardShortcuts.mjs b/src/zen/kbs/ZenKeyboardShortcuts.mjs
index 9feec1f7e2..ca088946f9 100644
--- a/src/zen/kbs/ZenKeyboardShortcuts.mjs
+++ b/src/zen/kbs/ZenKeyboardShortcuts.mjs
@@ -77,6 +77,7 @@ const defaultKeyboardGroups = {
],
searchAndFind: [
'zen-search-focus-shortcut',
+ 'zen-search-tabs-shortcut',
'zen-search-focus-shortcut-alt',
'zen-find-shortcut',
'zen-search-find-again-shortcut-2',
@@ -734,6 +735,31 @@ class ZenKeyboardShortcutsLoader {
)
);
+ //tabs search
+ newShortcutList.push(
+ new KeyShortcut(
+ 'zen-backward-search-tabs',
+ 'Q',
+ '',
+ 'searchAndFind',
+ KeyShortcutModifiers.fromObject({ accel: false, alt: true, shift: true }),
+ 'cmd_zenBackwardSearchTabs',
+ 'zen-backward-search-shortcut-tabs'
+ )
+ );
+
+ newShortcutList.push(
+ new KeyShortcut(
+ 'zen-search-tabs',
+ 'Q',
+ '',
+ 'searchAndFind',
+ KeyShortcutModifiers.fromObject({ accel: false, alt: true }),
+ 'cmd_zenSearchTabs',
+ 'zen-search-shortcut-tabs'
+ )
+ );
+
return newShortcutList;
}
diff --git a/src/zen/tabsearch/ZenTabSearch.mjs b/src/zen/tabsearch/ZenTabSearch.mjs
new file mode 100644
index 0000000000..d57fe56be8
--- /dev/null
+++ b/src/zen/tabsearch/ZenTabSearch.mjs
@@ -0,0 +1,117 @@
+{
+ class ZenTabSearch extends ZenDOMOperatedFeature {
+ init() {
+ this._heuristic = false;
+ this._updateAutoSelectResult = this._updateAutoSelectResult.bind(this);
+ this._handleKeyUp = this._handleKeyUp.bind(this);
+ Services.prefs.addObserver(
+ 'zen.tabsearch.auto-select-result',
+ this._updateAutoSelectResult,
+ false
+ );
+ this._updateAutoSelectResult();
+ window.setTimeout(() => {
+ this._setupListener();
+ }, 500);
+ }
+
+ _updateAutoSelectResult() {
+ this.autoSelectResult = Services.prefs.getBoolPref('zen.tabsearch.auto-select-result', false);
+ }
+
+ _setupListener() {
+ // Ensure gURLBar, its panel, and inputField are available
+ if (typeof gURLBar !== 'undefined') {
+ console.log('gURLBar or its components are not yet initialized');
+ // Bind the handlers to this instance for correct 'this' and removal
+ const self = this; // Capture the correct 'this' context
+ gURLBar.controller.addQueryListener({
+ onViewOpen() {
+ window.addEventListener('keyup', self._handleKeyUp, true);
+ self._heuristic = false;
+ },
+ onViewClose() {
+ window.removeEventListener('keyup', self._handleKeyUp, true);
+ self._heuristic = false;
+ },
+ onQueryStarted() {
+ window.removeEventListener('keyup', self._handleKeyUp, true);
+ },
+ onQueryFinished() {
+ self._heuristic = true;
+ },
+
+ });
+ } else {
+ // Retry if gURLBar or its components are not yet initialized
+ window.setTimeout(() => {
+ // 'this' here will refer to the ZenTabSearch instance because
+ // setTimeout is called on 'window', and the callback is an arrow function,
+ // which lexically captures 'this' from the surrounding scope (the catch block).
+ // In the catch block, 'this' is the ZenTabSearch instance.
+ this._setupListener();
+ }, 500);
+ }
+ }
+
+ _handleKeyUp(event) {
+ if (event.key === 'Alt' || event.key === 'Control') {
+ if (
+ this._heuristic &&
+ this.autoSelectResult &&
+ gURLBar.view.isOpen &&
+ gURLBar.searchMode?.source === 4 /* URLBarUtils.RESULT_SOURCE.TABS */
+ ) {
+ // simulate enter key press
+ if (this.autoSelectResult) {
+ const enterEvent = new KeyboardEvent('keydown', {
+ key: 'Enter',
+ code: 'Enter',
+ keyCode: 13,
+ which: 13,
+ bubbles: true,
+ cancelable: true,
+ });
+ gURLBar.handleCommand(enterEvent);
+ event.preventDefault();
+ event.stopImmediatePropagation();
+ }
+ } else {
+ // window.removeEventListener('keyup', this._handleKeyUp, true);
+ }
+ }
+ }
+
+ shouldInjectHeuristic() {
+ if (!this.autoSelectResult) return true;
+ if (this._heuristic) return true;
+ return false;
+ }
+
+ async searchTabsShortcut(offset = 1) {
+ // if the search is already open, we just need to select the next result
+ if (
+ gURLBar.view.isOpen &&
+ gURLBar.searchMode &&
+ gURLBar.searchMode.source === 4 /* URLBarUtils.RESULT_SOURCE.TABS */
+ ) {
+ if (gURLBar.view.visibleRowCount > 0) {
+ if (offset > 0) {
+ gURLBar.view.selectBy(offset);
+ } else if (offset < 0) {
+ gURLBar.view.selectBy(-offset, { reverse: true });
+ }
+ window.addEventListener('keyup', this._handleKeyUp, true);
+ this._heuristic = true;
+ }
+ } else {
+ gURLBar.search('% ');
+ this._heuristic = false;
+ }
+ }
+ }
+
+ if (Services.prefs.getBoolPref('zen.tabsearch.enabled', true)) {
+ window.gZenTabSearch = new ZenTabSearch();
+ }
+}
diff --git a/src/zen/workspaces/ZenWorkspaces.mjs b/src/zen/workspaces/ZenWorkspaces.mjs
index af67474e31..89f6bbb7c1 100644
--- a/src/zen/workspaces/ZenWorkspaces.mjs
+++ b/src/zen/workspaces/ZenWorkspaces.mjs
@@ -3137,7 +3137,7 @@ var gZenWorkspaces = new (class extends ZenMultiWindowFeature {
if (!this._hasInitializedTabsStrip) {
return gBrowser.browsers;
}
- const browsers = Array.from(gBrowser.tabpanels.querySelectorAll('browser'));
+ const browsers = Array.from(this.allStoredTabs).map(tab => tab.linkedBrowser);
// Sort browsers by making the current workspace first
const currentWorkspace = this.activeWorkspace;
const sortedBrowsers = browsers.sort((a, b) => {