Repository navigation
feat(desktop): add audio muting to browser pane #9395
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
a9091be
540490a
16c5d87
47c1536
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -260,6 +260,8 @@ class BrowserManager extends EventEmitter { | |
| private popupListeners = new Map<string, () => void>(); | ||
| private focusListeners = new Map<string, () => void>(); | ||
| private cdpDetachers = new Map<string, () => void>(); | ||
| private mutedPanes = new Map<string, boolean>(); | ||
| private popupWebContents = new Map<string, Set<Electron.WebContents>>(); | ||
| // Ref-count of in-flight agent work per pane (a live CDP session, a | ||
| // screenshot capture). While present the guest renderer stays | ||
| // un-throttled — see acquireAgentWake. The entry object's identity ties | ||
|
|
@@ -270,7 +272,36 @@ class BrowserManager extends EventEmitter { | |
| private forwardableChords = new Set<string>(); | ||
| private designMode = new DesignModeController(); | ||
|
|
||
| setForwardableChords(chords: string[]): void { | ||
| /** Sets audio muted state for the given pane and its child popups. */ | ||
| public setAudioMuted(paneId: string, muted: boolean): void { | ||
| this.mutedPanes.set(paneId, muted); | ||
| const entry = this.panes.get(paneId); | ||
| if (entry) { | ||
| const guest = webContents.fromId(entry.webContentsId); | ||
| if ( | ||
| guest && | ||
| !guest.isDestroyed() && | ||
| typeof guest.setAudioMuted === "function" | ||
| ) { | ||
| guest.setAudioMuted(muted); | ||
|
cubic-dev-ai[bot] marked this conversation as resolved.
Resolved
coderabbitai[bot] marked this conversation as resolved.
Resolved
|
||
| } | ||
| } | ||
| const popups = this.popupWebContents.get(paneId); | ||
| if (popups) { | ||
| for (const popup of popups) { | ||
| if (!popup.isDestroyed() && typeof popup.setAudioMuted === "function") { | ||
| popup.setAudioMuted(muted); | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| /** Returns true if the pane is currently set to mute audio. */ | ||
| public isAudioMuted(paneId: string): boolean { | ||
| return this.mutedPanes.get(paneId) ?? false; | ||
| } | ||
|
|
||
| public setForwardableChords(chords: string[]): void { | ||
| this.forwardableChords = new Set(chords); | ||
| } | ||
|
|
||
|
|
@@ -300,6 +331,12 @@ class BrowserManager extends EventEmitter { | |
| }); | ||
| const wc = webContents.fromId(webContentsId); | ||
| if (wc) { | ||
| // Apply persisted or default audio mute state | ||
| const isMuted = this.isAudioMuted(paneId); | ||
| if (typeof wc.setAudioMuted === "function") { | ||
| wc.setAudioMuted(isMuted); | ||
| } | ||
|
|
||
| // Throttling stays enabled by default so parked/offscreen persistent | ||
| // webviews don't run at full speed in the background — except while | ||
| // agent work is in flight on the pane (see acquireAgentWake), where a | ||
|
|
@@ -338,6 +375,8 @@ class BrowserManager extends EventEmitter { | |
| this.designMode.cancel(paneId, "destroyed"); | ||
| this.panes.delete(paneId); | ||
| this.consoleLogs.delete(paneId); | ||
| this.mutedPanes.delete(paneId); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P2: Prompt for AI agents
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Preserve mute state when a hidden webview is evicted. The renderer evicts hidden webviews beyond its three-webview cap and calls
|
||
| this.popupWebContents.delete(paneId); | ||
| // Tell subscribers when a live wake dies with the pane, so the renderer | ||
| // doesn't keep a stale pane id in its exemption set. | ||
| if (this.agentWakes.delete(paneId)) this.emitAgentActive(); | ||
|
|
@@ -1081,6 +1120,16 @@ class BrowserManager extends EventEmitter { | |
| ): void { | ||
| const wc = window.webContents; | ||
| markBrowserPanePopup(wc); | ||
| if (typeof wc.setAudioMuted === "function") { | ||
| wc.setAudioMuted(this.isAudioMuted(paneId)); | ||
| } | ||
| let popups = this.popupWebContents.get(paneId); | ||
| if (!popups) { | ||
| popups = new Set(); | ||
| this.popupWebContents.set(paneId, popups); | ||
| } | ||
| popups.add(wc); | ||
|
|
||
| const detachGuard = attachNavigationGuard(wc, (url) => | ||
| this.emit("deep-link", url), | ||
| ); | ||
|
|
@@ -1090,7 +1139,13 @@ class BrowserManager extends EventEmitter { | |
| wc.on("did-create-window", (child) => { | ||
| this.configurePopupWindow(paneId, child); | ||
| }); | ||
| window.on("closed", detachGuard); | ||
| window.once("closed", () => { | ||
| popups?.delete(wc); | ||
| if (popups?.size === 0) { | ||
| this.popupWebContents.delete(paneId); | ||
| } | ||
| detachGuard(); | ||
| }); | ||
| } | ||
|
|
||
| /** Items extensions add with `chrome.contextMenus`, for the pane's right-click menu. */ | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -8,7 +8,14 @@ import { | |
| } from "@superset/ui/dropdown-menu"; | ||
| import { toast } from "@superset/ui/sonner"; | ||
| import { useNavigate } from "@tanstack/react-router"; | ||
| import { CheckIcon, MinusIcon, PlusIcon, RotateCcwIcon } from "lucide-react"; | ||
| import { | ||
| CheckIcon, | ||
| MinusIcon, | ||
| PlusIcon, | ||
| RotateCcwIcon, | ||
| Volume2Icon, | ||
| VolumeXIcon, | ||
| } from "lucide-react"; | ||
| import { useEffect, useState } from "react"; | ||
| import { TbDots } from "react-icons/tb"; | ||
| import { ImportHistoryDialog } from "renderer/components/ImportHistoryDialog"; | ||
|
|
@@ -72,6 +79,48 @@ export function BrowserOverflowMenu({ | |
| const [isScreenshotsOpen, setIsScreenshotsOpen] = useState(false); | ||
| const [isClearDataOpen, setIsClearDataOpen] = useState(false); | ||
| const [isMenuOpen, setIsMenuOpen] = useState(false); | ||
| const [audioMuteState, setAudioMuteState] = useState<{ | ||
| paneId: string; | ||
| muted: boolean; | ||
| } | null>(null); | ||
|
|
||
| const isAudioMuteLoaded = audioMuteState?.paneId === paneId; | ||
| const isAudioMuted = isAudioMuteLoaded ? audioMuteState.muted : false; | ||
|
|
||
| useEffect(() => { | ||
| if (!paneId) return; | ||
| let cancelled = false; | ||
| electronTrpcClient.browser.isAudioMuted | ||
| .query({ paneId }) | ||
| .then((muted) => { | ||
| if (!cancelled) { | ||
| setAudioMuteState({ paneId, muted }); | ||
| } | ||
| }) | ||
| .catch((error) => { | ||
| console.warn("[BrowserOverflowMenu] could not read audio mute state", { | ||
| paneId, | ||
| error, | ||
| }); | ||
| }); | ||
| return () => { | ||
| cancelled = true; | ||
| }; | ||
| }, [paneId]); | ||
|
coderabbitai[bot] marked this conversation as resolved.
Resolved
|
||
|
|
||
| const handleToggleMute = () => { | ||
| if (!paneId || !isAudioMuteLoaded) return; | ||
| const next = !isAudioMuted; | ||
| electronTrpcClient.browser.setAudioMuted | ||
| .mutate({ paneId, muted: next }) | ||
| .then(() => setAudioMuteState({ paneId, muted: next })) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P2: A completed toggle for the previous pane can overwrite this component’s state after Prompt for AI agents
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ignore mutation results from a previous pane. If a mute mutation for pane A remains pending while the menu switches to pane B, pane B's query can finish first. The mutation then writes pane A's state over pane B's loaded state. The pane-id check disables B's mute action until another pane change triggers a query. Guard mutation completion with the current pane generation, as the query completion already does. (raw.githubusercontent.com)
|
||
| .catch((error) => { | ||
| console.warn("[BrowserOverflowMenu] could not set audio mute state", { | ||
| paneId, | ||
| error, | ||
| }); | ||
| }); | ||
| }; | ||
|
|
||
| // The webview swallows pointer events, so a click on the page would never | ||
| // reach the document listener Radix dismisses on; passing the click | ||
|
|
@@ -257,6 +306,18 @@ export function BrowserOverflowMenu({ | |
| <Trans>Take a screenshot</Trans> | ||
| </DropdownMenuItem> | ||
| <DropdownMenuSeparator /> | ||
| <DropdownMenuItem | ||
| onClick={handleToggleMute} | ||
| disabled={!hasPage || !isAudioMuteLoaded} | ||
| className="justify-between" | ||
| > | ||
| {isAudioMuted ? <Trans>Unmute</Trans> : <Trans>Mute</Trans>} | ||
| {isAudioMuted ? ( | ||
| <VolumeXIcon className="size-3.5" /> | ||
| ) : ( | ||
| <Volume2Icon className="size-3.5" /> | ||
| )} | ||
| </DropdownMenuItem> | ||
| <DropdownMenuItem onClick={handleHardReload} disabled={!hasPage}> | ||
| <Trans>Hard reload</Trans> | ||
| </DropdownMenuItem> | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
P3: This close-path test never verifies the popup it closes. Assert that
popupWcremains muted after unmuting the pane, then separately assert that the still-openpopupWc2is unmuted.Prompt for AI agents