From 8b879f2b542d3222e2f3c9cd34d782bca4cbdd0d Mon Sep 17 00:00:00 2001 From: Laney Pouzet Date: Tue, 3 Nov 2026 10:04:38 -0800 Subject: [PATCH 1/2] Fix Command-click browser opening for desktop message links MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Jello Canelé <417c271f0f8eb7d0c3eb9e706a364c84a9e938d7ac18621dc30185109fcaac49@buzz.block.builderlab.xyz> Signed-off-by: Laney Pouzet --- docs/buzz-help.md | 3 + .../conversation/MessageLink.test.tsx | 71 +++++++++++++++++++ src/features/conversation/MessageLink.tsx | 13 ++++ 3 files changed, 87 insertions(+) diff --git a/docs/buzz-help.md b/docs/buzz-help.md index 3d9ff88088..5389106e21 100644 --- a/docs/buzz-help.md +++ b/docs/buzz-help.md @@ -191,6 +191,9 @@ that agent in Buzz; they do not notify the external app through this connection. before allowing a fresh participation invitation; failed checks keep the original. If the original invitation was applied and the person was later removed, Retry clears the old row without re-adding them. Use **Add** in People to invite them again. +- **Open links in your browser**: on desktop, ⌘-click an HTTP(S) link in a + message to open it in your default browser instead of a side panel. You can + also right-click the link and choose **Open in browser**. - **Sidebar width**: drag the sidebar's right edge, or focus its resize handle and use the Left/Right arrow keys. Double-click the handle to reset the width. Messages and plugin pages share the saved width within the same community diff --git a/src/features/conversation/MessageLink.test.tsx b/src/features/conversation/MessageLink.test.tsx index ddf2bc19b8..b286022413 100644 --- a/src/features/conversation/MessageLink.test.tsx +++ b/src/features/conversation/MessageLink.test.tsx @@ -28,6 +28,7 @@ import { LinkThreadTabContext } from "./LinkLabelContext"; afterEach(() => { cleanup(); vi.restoreAllMocks(); + vi.unstubAllGlobals(); }); const entry: Contribution = { @@ -289,6 +290,76 @@ it("preserves ordinary pane interception and modified or middle-click fallback", expect(onOpenLink).not.toHaveBeenCalled(); }); +function desktopOpener() { + const invoke = vi.fn().mockResolvedValue(undefined); + vi.stubGlobal("isTauri", true); + vi.stubGlobal("__TAURI_INTERNALS__", { invoke }); + return invoke; +} + +it.each([ + "https://github.com/block/buzz-app/pull/1?view=all#discussion_r1", + "http://example.com/path?query=yes#section", +])( + "opens desktop Command-click in the browser with the exact URL: %s", + (target) => { + const invoke = desktopOpener(); + const { anchor, onOpenLink } = mount(target); + const event = new MouseEvent("click", { + bubbles: true, + cancelable: true, + metaKey: true, + }); + fireEvent(anchor, event); + expect(event.defaultPrevented).toBe(true); + expect(invoke).toHaveBeenCalledExactlyOnceWith( + "plugin:opener|open_url", + { + url: target, + }, + undefined, + ); + expect(onOpenLink).not.toHaveBeenCalled(); + }, +); + +it("keeps ordinary desktop clicks in the pane", () => { + const invoke = desktopOpener(); + const { anchor, onOpenLink } = mount(); + fireEvent.click(anchor); + expect(onOpenLink).toHaveBeenCalledExactlyOnceWith(url); + expect(invoke).not.toHaveBeenCalled(); +}); + +it("reports a failed desktop browser open and clears the error on retry", async () => { + const invoke = desktopOpener(); + invoke.mockRejectedValueOnce("Browser unavailable"); + const { anchor, onOpenLink } = mount(); + fireEvent.click(anchor, { metaKey: true }); + expect( + await screen.findByText("Couldn’t open the link in your browser."), + ).toBeInTheDocument(); + fireEvent.click(anchor, { metaKey: true }); + expect( + screen.queryByText("Couldn’t open the link in your browser."), + ).not.toBeInTheDocument(); + expect(invoke).toHaveBeenCalledTimes(2); + expect(onOpenLink).not.toHaveBeenCalled(); +}); + +it("does not open a desktop browser from an inactive conversation", () => { + const invoke = desktopOpener(); + const onOpenLink = vi.fn(() => true); + render( + + + , + ); + fireEvent.click(screen.getByRole("link"), { metaKey: true }); + expect(invoke).not.toHaveBeenCalled(); + expect(onOpenLink).not.toHaveBeenCalled(); +}); + it("retires an external menu when its retained conversation becomes inactive", async () => { const onOpenLink = vi.fn(() => true); const tree = (active: boolean) => ( diff --git a/src/features/conversation/MessageLink.tsx b/src/features/conversation/MessageLink.tsx index 9c9eb4a101..50f3bc6dd7 100644 --- a/src/features/conversation/MessageLink.tsx +++ b/src/features/conversation/MessageLink.tsx @@ -1,3 +1,4 @@ +import { invoke, isTauri } from "@tauri-apps/api/core"; import { useConversationPresentation } from "./ConversationPresentation"; import { useContext, @@ -226,6 +227,18 @@ export function MessageLink({ openInternal(); return; } + // Tauri's automatic external-link opener skips Command-clicks. + if (external && event.metaKey && event.button === 0 && isTauri()) { + event.preventDefault(); + setNotice(undefined); + void invoke("plugin:opener|open_url", { url }).catch(() => { + setNotice({ + text: "Couldn’t open the link in your browser.", + error: true, + }); + }); + return; + } if ( !event.metaKey && !event.ctrlKey && From f1cba37b66f5c12c8759a9b8ced4546eed3df0c0 Mon Sep 17 00:00:00 2001 From: Laney Pouzet Date: Tue, 3 Nov 2026 12:31:08 -0800 Subject: [PATCH 2/2] Document Ctrl-click browser shortcut on Windows and Linux MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Blue Raspberry Jello Canelé <417c271f0f8eb7d0c3eb9e706a364c84a9e938d7ac18621dc30185109fcaac49@buzz.block.builderlab.xyz> Signed-off-by: Laney Pouzet --- docs/buzz-help.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/docs/buzz-help.md b/docs/buzz-help.md index 5389106e21..c6bdc343a4 100644 --- a/docs/buzz-help.md +++ b/docs/buzz-help.md @@ -191,9 +191,10 @@ that agent in Buzz; they do not notify the external app through this connection. before allowing a fresh participation invitation; failed checks keep the original. If the original invitation was applied and the person was later removed, Retry clears the old row without re-adding them. Use **Add** in People to invite them again. -- **Open links in your browser**: on desktop, ⌘-click an HTTP(S) link in a - message to open it in your default browser instead of a side panel. You can - also right-click the link and choose **Open in browser**. +- **Open links in your browser**: on desktop, ⌘-click (Ctrl-click on Windows + and Linux) an HTTP(S) link in a message to open it in your default browser + instead of a side panel. You can also right-click the link and choose + **Open in browser**. - **Sidebar width**: drag the sidebar's right edge, or focus its resize handle and use the Left/Right arrow keys. Double-click the handle to reset the width. Messages and plugin pages share the saved width within the same community