From 9c0b7259b3867e5a858ece745d3eb72a20de3f43 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 5 May 2026 04:20:55 +0000 Subject: [PATCH 1/3] feat: Add shift+click to open links in split view Registers a ZenSplitView JSWindowActor pair (following the ZenGlance pattern) so shift+click on any web-content link opens the target URL in a new split-view tab alongside the current one. - ZenSplitViewChild: content-side actor registered for click and DOMContentLoaded events; fetches the glance activation method on load and skips if it is "shift" to avoid conflicting with Glance - ZenSplitViewParent: chrome-side actor that reads the glance pref and calls gZenViewSplitter.splitLinkFromURL(url) - ZenActorsManager: registers ZenSplitView in the static JSWINDOWACTORS map (same pattern as ZenGlance/ZenBoosts) - ZenViewSplitter: extracts splitLinkFromURL(url) from splitLinkInNewTab() so actors can call it directly without going through gContextMenu - split-view/moz.build: ships actor files to resource:///actors/ - zen/moz.build: adds split-view to DIRS so the build picks up the new moz.build https://claude.ai/code/session_01Mh21pBx11sPrnfbX7mECkt --- src/zen/common/sys/ZenActorsManager.sys.mjs | 16 ++++++++ src/zen/moz.build | 1 + src/zen/split-view/ZenViewSplitter.mjs | 9 +++++ .../actors/ZenSplitViewChild.sys.mjs | 39 +++++++++++++++++++ .../actors/ZenSplitViewParent.sys.mjs | 22 +++++++++++ src/zen/split-view/moz.build | 9 +++++ 6 files changed, 96 insertions(+) create mode 100644 src/zen/split-view/actors/ZenSplitViewChild.sys.mjs create mode 100644 src/zen/split-view/actors/ZenSplitViewParent.sys.mjs create mode 100644 src/zen/split-view/moz.build diff --git a/src/zen/common/sys/ZenActorsManager.sys.mjs b/src/zen/common/sys/ZenActorsManager.sys.mjs index 766193ad359..7f5c1a81250 100644 --- a/src/zen/common/sys/ZenActorsManager.sys.mjs +++ b/src/zen/common/sys/ZenActorsManager.sys.mjs @@ -57,6 +57,22 @@ let JSWINDOWACTORS = { remoteTypes: ["web", "file"], enablePreference: "zen.glance.enabled", }, + ZenSplitView: { + parent: { + esModuleURI: "resource:///actors/ZenSplitViewParent.sys.mjs", + }, + child: { + esModuleURI: "resource:///actors/ZenSplitViewChild.sys.mjs", + events: { + DOMContentLoaded: {}, + click: { + capture: true, + }, + }, + }, + allFrames: true, + remoteTypes: ["web", "file"], + }, }; if (!Services.appinfo.inSafeMode) { diff --git a/src/zen/moz.build b/src/zen/moz.build index e1efea20567..eabacf5a8e5 100644 --- a/src/zen/moz.build +++ b/src/zen/moz.build @@ -19,4 +19,5 @@ DIRS += [ "sessionstore", "share", "spaces", + "split-view", ] diff --git a/src/zen/split-view/ZenViewSplitter.mjs b/src/zen/split-view/ZenViewSplitter.mjs index 783f2646df9..174c1e47092 100644 --- a/src/zen/split-view/ZenViewSplitter.mjs +++ b/src/zen/split-view/ZenViewSplitter.mjs @@ -1228,6 +1228,15 @@ class nsZenViewSplitter extends nsZenDOMOperatedFeature { window.gContextMenu.mediaURL || window.gContextMenu.contentData.docLocation || window.gContextMenu.target.ownerDocument.location.href; + this.splitLinkFromURL(url); + } + + /** + * Opens a URL in a new tab and splits it with the current tab. + * + * @param {string} url - The URL to open in split view. + */ + splitLinkFromURL(url) { const currentTab = gZenGlanceManager.getTabOrGlanceParent( window.gBrowser.selectedTab ); diff --git a/src/zen/split-view/actors/ZenSplitViewChild.sys.mjs b/src/zen/split-view/actors/ZenSplitViewChild.sys.mjs new file mode 100644 index 00000000000..03c93b27a3e --- /dev/null +++ b/src/zen/split-view/actors/ZenSplitViewChild.sys.mjs @@ -0,0 +1,39 @@ +// 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/. + +export class ZenSplitViewChild extends JSWindowActorChild { + #glanceActivationMethod; + + async handleEvent(event) { + const handler = this[`on_${event.type}`]; + if (typeof handler === "function") { + await handler.call(this, event); + } + } + + async on_DOMContentLoaded() { + this.#glanceActivationMethod = await this.sendQuery( + "ZenSplitView:GetGlanceActivationMethod" + ); + } + + on_click(event) { + if (event.button !== 0 || event.defaultPrevented) { + return; + } + if (!event.shiftKey || event.ctrlKey || event.altKey || event.metaKey) { + return; + } + if (this.#glanceActivationMethod === "shift") { + return; + } + const anchor = event.target.closest("a[href]"); + if (!anchor) { + return; + } + event.preventDefault(); + event.stopPropagation(); + this.sendAsyncMessage("ZenSplitView:OpenInSplit", { url: anchor.href }); + } +} diff --git a/src/zen/split-view/actors/ZenSplitViewParent.sys.mjs b/src/zen/split-view/actors/ZenSplitViewParent.sys.mjs new file mode 100644 index 00000000000..0ce68aa4c4e --- /dev/null +++ b/src/zen/split-view/actors/ZenSplitViewParent.sys.mjs @@ -0,0 +1,22 @@ +// 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/. + +export class ZenSplitViewParent extends JSWindowActorParent { + receiveMessage(message) { + switch (message.name) { + case "ZenSplitView:GetGlanceActivationMethod": + return Services.prefs.getStringPref( + "zen.glance.activation-method", + "ctrl" + ); + case "ZenSplitView:OpenInSplit": + this.browsingContext.topChromeWindow.gZenViewSplitter?.splitLinkFromURL( + message.data.url + ); + break; + default: + console.warn(`[split-view]: Unknown message: ${message.name}`); + } + } +} diff --git a/src/zen/split-view/moz.build b/src/zen/split-view/moz.build new file mode 100644 index 00000000000..4e2e19750f3 --- /dev/null +++ b/src/zen/split-view/moz.build @@ -0,0 +1,9 @@ +# +# 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/. + +FINAL_TARGET_FILES.actors += [ + "actors/ZenSplitViewChild.sys.mjs", + "actors/ZenSplitViewParent.sys.mjs", +] From e9a082dc81d80e780909086aa7a5dfb01b92bd85 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 5 May 2026 04:29:43 +0000 Subject: [PATCH 2/3] fix: Use async receiveMessage to satisfy consistent-return lint rule https://claude.ai/code/session_01Mh21pBx11sPrnfbX7mECkt --- src/zen/split-view/actors/ZenSplitViewParent.sys.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/zen/split-view/actors/ZenSplitViewParent.sys.mjs b/src/zen/split-view/actors/ZenSplitViewParent.sys.mjs index 0ce68aa4c4e..ec906030fce 100644 --- a/src/zen/split-view/actors/ZenSplitViewParent.sys.mjs +++ b/src/zen/split-view/actors/ZenSplitViewParent.sys.mjs @@ -3,7 +3,7 @@ // file, You can obtain one at http://mozilla.org/MPL/2.0/. export class ZenSplitViewParent extends JSWindowActorParent { - receiveMessage(message) { + async receiveMessage(message) { switch (message.name) { case "ZenSplitView:GetGlanceActivationMethod": return Services.prefs.getStringPref( From ee3c5cdbd0efdf29c4e13bce981ab6ee5e666ad7 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 5 May 2026 04:38:33 +0000 Subject: [PATCH 3/3] fix: Add explicit return null to all receiveMessage branches MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Satisfies the consistent-return lint rule by ensuring every code path returns a value — the query case returns the pref string, action cases return null. https://claude.ai/code/session_01Mh21pBx11sPrnfbX7mECkt --- src/zen/split-view/actors/ZenSplitViewParent.sys.mjs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/zen/split-view/actors/ZenSplitViewParent.sys.mjs b/src/zen/split-view/actors/ZenSplitViewParent.sys.mjs index ec906030fce..a194fbd4acb 100644 --- a/src/zen/split-view/actors/ZenSplitViewParent.sys.mjs +++ b/src/zen/split-view/actors/ZenSplitViewParent.sys.mjs @@ -14,9 +14,10 @@ export class ZenSplitViewParent extends JSWindowActorParent { this.browsingContext.topChromeWindow.gZenViewSplitter?.splitLinkFromURL( message.data.url ); - break; + return null; default: console.warn(`[split-view]: Unknown message: ${message.name}`); + return null; } } }