Skip to content
Open
Show file tree
Hide file tree
Changes from 20 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
a97293b
Add ZenWorkspacesSync engine for workspace syncing
kristijanribaric Feb 25, 2026
02870f1
Refactor workspace sync to apply workspace data immediately and pinne…
kristijanribaric Feb 26, 2026
d5c932e
Refactor control flow statements in ZenSessionManager
kristijanribaric Feb 26, 2026
e1497d8
Refactor arrow function formatting for consistency
kristijanribaric Feb 27, 2026
072c998
refactor: Implement conflict-resolution sync with syncMeta tracking
kristijanribaric Mar 11, 2026
095cc1c
refactor: Migrate to multi-record sync engine with per-item tracking
kristijanribaric Mar 12, 2026
03ae282
update session manager to build last snapshot after data cleanup in s…
kristijanribaric Apr 30, 2026
43b221f
move ZenWorkspacesSync.sys.mjs to zen/sync
kristijanribaric Apr 30, 2026
f3fe62a
ran lint fix
kristijanribaric Apr 30, 2026
f67ce1c
fix workspace reordering sync
kristijanribaric Apr 30, 2026
7e43e74
add force option to addToEssentials for immediate pinning in sync ope…
kristijanribaric Apr 30, 2026
883f513
update folder rename event to "TabGroupUpdate" and notify observers o…
kristijanribaric Apr 30, 2026
d0b6efc
ran lint fix
kristijanribaric Apr 30, 2026
b3ae110
implement syncDefaultUserContextId method in ZenPinnedTabManager to u…
kristijanribaric Apr 30, 2026
7c0ce26
notifications refactor, added zen sync manager, fixed zenDefaultUserC…
kristijanribaric May 1, 2026
a28c057
remove redundant await in ZenSpaceManager after propagateWorkspaceDat…
kristijanribaric May 1, 2026
2138903
reverted unnecessary assignment of canEssentialBeAdded call to a vari…
kristijanribaric May 1, 2026
83051fb
Ran lint fix
kristijanribaric May 1, 2026
689a2ab
Moved sync logic from ZenSessionManager.sys.mjs to ZenSyncManager.sys…
kristijanribaric May 1, 2026
8148035
Fixed a bug where reordering folders wouldn't notify the change to sy…
kristijanribaric May 1, 2026
f449897
update tab attribute check to 'zen-essential' in ZenSpaceManager.mjs;…
kristijanribaric May 1, 2026
bf1dd6c
fix zenDefaultUserContextId not being properly synced for essientials…
kristijanribaric May 2, 2026
43d200d
revert on_TabUngrouped in ZenWindowSync.sys.mjs
kristijanribaric May 2, 2026
603e585
simplified zenDefaultUserContextId assigning on tabs created by sync
kristijanribaric May 2, 2026
092b8e4
remove redundant observer notifications in folder and tab group event…
kristijanribaric May 2, 2026
1d8584b
remove redundant typeof check before calling setUserContextId
kristijanribaric May 2, 2026
73d846d
restored accidentally removed comment
kristijanribaric May 2, 2026
05b1188
Merge branch 'zen-browser:dev' into zen-sync
kristijanribaric May 7, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions src/browser/components/preferences/sync-js.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
diff --git a/browser/components/preferences/sync.js b/browser/components/preferences/sync.js
index dc89a9c41a0dbd44054ede0025d333773f0ae908..7fd91bd704b3b187277e4c8b076f990cb56ea8dc 100644
--- a/browser/components/preferences/sync.js
+++ b/browser/components/preferences/sync.js
@@ -40,6 +40,7 @@ Preferences.addAll([
{ id: "services.sync.engine.creditcards", type: "bool" },
{ id: "services.sync.engine.addons", type: "bool" },
{ id: "services.sync.engine.prefs", type: "bool" },
+ { id: "services.sync.engine.workspaces", type: "bool" },
]);

/**
@@ -512,6 +513,7 @@ const SYNC_ENGINE_SETTINGS = [
},
{ id: "syncAddons", pref: "services.sync.engine.addons", type: "addons" },
{ id: "syncSettings", pref: "services.sync.engine.prefs", type: "settings" },
+ { id: "syncWorkspaces", pref: "services.sync.engine.workspaces", type: "workspaces" },
];

SYNC_ENGINE_SETTINGS.forEach(({ id, pref }) => {
15 changes: 15 additions & 0 deletions src/services/sync/modules/service-sys-mjs.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
diff --git a/services/sync/modules/service.sys.mjs b/services/sync/modules/service.sys.mjs
index c873293871ffaba305bc1bf41730d79c13546b85..0e0171cec13dfcbb296ec7bf03628370ce2fa93f 100644
--- a/services/sync/modules/service.sys.mjs
+++ b/services/sync/modules/service.sys.mjs
@@ -99,6 +99,10 @@ function getEngineModules() {
whenTrue: "ExtensionStorageEngineKinto",
whenFalse: "ExtensionStorageEngineBridge",
};
+ result.Workspaces = {
+ module: "resource:///modules/zen/ZenWorkspacesSync.sys.mjs",
+ symbol: "ZenWorkspacesEngine",
+ };
return result;
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
diff --git a/toolkit/components/contextualidentity/ContextualIdentityService.sys.mjs b/toolkit/components/contextualidentity/ContextualIdentityService.sys.mjs
index 5702ff28cc22206f5ce16584dac8a78d816562ce..2132ee9ad8f553b3effeb7c4386e5fae46b80507 100644
--- a/toolkit/components/contextualidentity/ContextualIdentityService.sys.mjs
+++ b/toolkit/components/contextualidentity/ContextualIdentityService.sys.mjs
@@ -270,11 +270,11 @@ _ContextualIdentityService.prototype = {
});
},

- create(name, icon, color) {
+ create(name, icon, color, id = null) {
this.ensureDataReady();

- // Retrieve the next userContextId available.
- let userContextId = ++this._lastUserContextId;
+ // If explicit ID is provided, use it if it's not already in use, otherwise use the next available ID.
+ let userContextId = id !== null && !this._identities.some(i => i.userContextId === id) ? id : ++this._lastUserContextId;

// Throw an error if the next userContextId available is invalid (the one associated to
// MAX_USER_CONTEXT_ID is already reserved to "userContextIdInternal.webextStorageLocal", which
3 changes: 3 additions & 0 deletions src/zen/common/modules/ZenSessionStore.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ class ZenSessionStore extends nsZenPreloadedFeature {
});

restoreInitialTabData(tab, tabData) {
if (typeof tab.setUserContextId === "function") {
Comment thread
kristijanribaric marked this conversation as resolved.
Outdated
tab.setUserContextId(tabData.userContextId || 0);
}
if (tabData.zenWorkspace) {
tab.setAttribute("zen-workspace-id", tabData.zenWorkspace);
}
Expand Down
7 changes: 6 additions & 1 deletion src/zen/folders/ZenFolder.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ export class nsZenFolder extends MozTabbrowserTabGroup {

this.labelElement.onRenameFinished = newLabel => {
this.name = newLabel.trim() || "Folder";
const event = new CustomEvent("ZenFolderRenamed", {
const event = new CustomEvent("TabGroupUpdate", {
bubbles: true,
});
this.dispatchEvent(event);
Expand Down Expand Up @@ -172,6 +172,11 @@ export class nsZenFolder extends MozTabbrowserTabGroup {
}

async delete() {
Services.obs.notifyObservers(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not needed

null,
"zen-workspace-item-changed",
`f~${this.id}`
);
for (const tab of this.allItemsRecursive) {
if (tab.hasAttribute("zen-empty-tab")) {
// Manually remove the empty tabs as removeTabs() inside removeTabGroup
Expand Down
31 changes: 22 additions & 9 deletions src/zen/folders/ZenFolders.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -641,16 +641,22 @@ class nsZenFolders extends nsZenDOMOperatedFeature {
const insertBefore =
options.insertBefore ||
pinnedContainer.querySelector(".pinned-tabs-container-separator");
const emptyTab = gBrowser.addTab("about:blank", {
skipAnimation: true,
pinned: true,
triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
_forZenEmptyTab: true,
createLazyBrowser: true,
});

gBrowser.pinTab(emptyTab);
tabs = [emptyTab, ...filteredTabs];
if (!options.skipEmptyTab) {
const emptyTab = gBrowser.addTab("about:blank", {
skipAnimation: true,
pinned: true,
triggeringPrincipal:
Services.scriptSecurityManager.getSystemPrincipal(),
_forZenEmptyTab: true,
createLazyBrowser: true,
});

gBrowser.pinTab(emptyTab);
tabs = [emptyTab, ...filteredTabs];
} else {
tabs = filteredTabs;
}

const folder = this._createFolderNode(options);

Expand Down Expand Up @@ -680,6 +686,13 @@ class nsZenFolders extends nsZenDOMOperatedFeature {
}

this.#groupInit(folder);
if (folder.id) {
Services.obs.notifyObservers(
Comment thread
kristijanribaric marked this conversation as resolved.
Outdated
null,
"zen-workspace-item-changed",
`f~${folder.id}`
);
}
return folder;
}

Expand Down
6 changes: 3 additions & 3 deletions src/zen/live-folders/ZenLiveFoldersManager.sys.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -467,9 +467,6 @@ class nsZenLiveFoldersManager {
});
// createLazyBrowser can't be pinned by default
this.window.gBrowser.pinTab(tab);
if (userContextId) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why?

tab.setAttribute("zenDefaultUserContextId", "true");
}
if (item.icon) {
this.window.gBrowser.setIcon(tab, item.icon);
if (tab.linkedBrowser) {
Expand All @@ -496,6 +493,9 @@ class nsZenLiveFoldersManager {
// Wait for tabs to (hopefully) be initialized on all windows
lazy.setTimeout(() => {
folder.addTabs(newItems);
for (const tab of newItems) {
this.window.gZenPinnedTabManager?.syncDefaultUserContextId(tab);
}
this.saveState();
}, 0);
}
Expand Down
1 change: 1 addition & 0 deletions src/zen/moz.build
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,5 @@ DIRS += [
"sessionstore",
"share",
"spaces",
"sync"
]
42 changes: 41 additions & 1 deletion src/zen/sessionstore/ZenSessionManager.sys.mjs
Comment thread
kristijanribaric marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
ZenLiveFoldersManager:
"resource:///modules/zen/ZenLiveFoldersManager.sys.mjs",
ZenSyncStore: "resource:///modules/zen/ZenSyncManager.sys.mjs",
PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs",
SessionStore: "resource:///modules/sessionstore/SessionStore.sys.mjs",
SessionStartup: "resource:///modules/sessionstore/SessionStartup.sys.mjs",
Expand Down Expand Up @@ -312,6 +313,7 @@ export class nsZenSessionManager {
}
}
delete this._dataFromFile;
lazy.ZenSyncStore.seedSnapshot(this.#sidebar);
}

get #shouldRestoreOnlyPinned() {
Expand Down Expand Up @@ -609,7 +611,7 @@ export class nsZenSessionManager {
}
);
this.#collectWindowData(windows);
// This would save the data to disk asynchronously or when quitting the app.
Comment thread
kristijanribaric marked this conversation as resolved.
lazy.ZenSyncStore.noteSidebarDataChanged(this.#sidebar);
let sidebar = this.#sidebarWithoutCloning;
this.#file.data = sidebar;
if (soon) {
Expand Down Expand Up @@ -724,6 +726,7 @@ export class nsZenSessionManager {

sidebarData.lastCollected = Date.now();
this.#collectTabsData(sidebarData, aStateWindows);

this.#sidebar = sidebarData;
}

Expand Down Expand Up @@ -933,6 +936,43 @@ export class nsZenSessionManager {
}
return Cu.cloneInto(sidebar.spaces, {});
}

/**
* Returns a deep clone of the full sidebar object (spaces, tabs, folders, etc.).
* Used by the ZenWorkspacesSync engine to build the sync payload.
*
* @returns {object} A deep clone of the sidebar data.
*/
getSidebarData() {
const sidebar = this.#sidebar;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this.#sidebar is already deep cloned

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Has this been fixed? @kristijanribaric I see you are still doing the JSON thingy

if (!sidebar) {
return {};
}
return JSON.parse(JSON.stringify(sidebar));
}

replaceSidebarData(sidebar, soon = true) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just call saveState?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I kept this separate intentionally.

saveState() rebuilds sidebar/session data from window state, runs the normal save pipeline, and is meant for local persistence.

Here I only want a raw replace-and-persist helper for sync apply, because incoming sync already produced the sidebar state I want to write. Calling saveState() here would immediately re-collect from the current windows and could overwrite or reshape the freshly merged sync result before the UI/apply path finishes, it would be inpredictable

So replaceSidebarData() exists to do the narrower thing, accept already prepared sidebar data, store it, and persist it, without another live collection step.

this.#sidebar = sidebar || {};
this.#file.data = this.#sidebarWithoutCloning;
if (soon) {
this.#file.saveSoon();
} else {
this.#file._save();
}
}

getCurrentSidebarData() {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should just be getSidebarData, not sure why you would call this instead of getSidebarData.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@kristijanribaric in case you missed it ^

const state = lazy.SessionStore.getCurrentState(true);
let windows = state?.windows || [];
windows = windows.filter(win => this.#isWindowSaveable(win));
if (!windows.length) {
return this.getSidebarData();
}

const sidebarData = { lastCollected: Date.now() };
this.#collectTabsData(sidebarData, windows);
return JSON.parse(JSON.stringify(sidebarData));
}
}

export const ZenSessionStore = new nsZenSessionManager();
Loading
Loading