diff --git a/GhosttyTabs.xcodeproj/project.pbxproj b/GhosttyTabs.xcodeproj/project.pbxproj index 1710f987..9b663742 100644 --- a/GhosttyTabs.xcodeproj/project.pbxproj +++ b/GhosttyTabs.xcodeproj/project.pbxproj @@ -31,6 +31,7 @@ A5001400 /* Panel.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5001410 /* Panel.swift */; }; A5001401 /* TerminalPanel.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5001411 /* TerminalPanel.swift */; }; A5001402 /* BrowserPanel.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5001412 /* BrowserPanel.swift */; }; + A500RG01 /* ReactGrab.swift in Sources */ = {isa = PBXBuildFile; fileRef = A500RG00 /* ReactGrab.swift */; }; A5001403 /* TerminalPanelView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5001413 /* TerminalPanelView.swift */; }; A5001404 /* BrowserPanelView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5001414 /* BrowserPanelView.swift */; }; A5007420 /* BrowserPopupWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5007421 /* BrowserPopupWindowController.swift */; }; @@ -230,6 +231,7 @@ A5001410 /* Panel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Panels/Panel.swift; sourceTree = ""; }; A5001411 /* TerminalPanel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Panels/TerminalPanel.swift; sourceTree = ""; }; A5001412 /* BrowserPanel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Panels/BrowserPanel.swift; sourceTree = ""; }; + A500RG00 /* ReactGrab.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Panels/ReactGrab.swift; sourceTree = ""; }; A5001413 /* TerminalPanelView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Panels/TerminalPanelView.swift; sourceTree = ""; }; A5001414 /* BrowserPanelView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Panels/BrowserPanelView.swift; sourceTree = ""; }; A5007421 /* BrowserPopupWindowController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Panels/BrowserPopupWindowController.swift; sourceTree = ""; }; @@ -481,6 +483,7 @@ A5001410 /* Panel.swift */, A5001411 /* TerminalPanel.swift */, A5001412 /* BrowserPanel.swift */, + A500RG00 /* ReactGrab.swift */, A5001413 /* TerminalPanelView.swift */, A5001414 /* BrowserPanelView.swift */, A5007421 /* BrowserPopupWindowController.swift */, @@ -802,6 +805,7 @@ A5001400 /* Panel.swift in Sources */, A5001401 /* TerminalPanel.swift in Sources */, A5001402 /* BrowserPanel.swift in Sources */, + A500RG01 /* ReactGrab.swift in Sources */, A5001403 /* TerminalPanelView.swift in Sources */, A5001404 /* BrowserPanelView.swift in Sources */, A5007420 /* BrowserPopupWindowController.swift in Sources */, diff --git a/Resources/Localizable.xcstrings b/Resources/Localizable.xcstrings index 15a9dba4..923b3d4a 100644 --- a/Resources/Localizable.xcstrings +++ b/Resources/Localizable.xcstrings @@ -10404,6 +10404,59 @@ } } }, + "browser.reactGrab": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Inject React Grab" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "React Grabを注入" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "注入 React Grab" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "注入 React Grab" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "React Grab 주입" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "React Grab einfügen" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Inyectar React Grab" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Injecter React Grab" + } + } + } + }, "browser.search.placeholder": { "extractionState": "manual", "localizations": { @@ -43562,6 +43615,41 @@ } } }, + "menu.view.toggleReactGrab": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Toggle React Grab" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "React Grabの切り替え" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "切换 React Grab" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "切換 React Grab" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "React Grab 전환" + } + } + } + }, "menu.view.showNotifications": { "extractionState": "manual", "localizations": { @@ -56855,6 +56943,40 @@ } } }, + "settings.browser.reactGrabVersion": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "React Grab Version" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "React Grabバージョン" + } + } + } + }, + "settings.browser.reactGrabVersion.subtitle": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Pinned npm version of react-grab injected by the toolbar button (Cmd+Shift+G). Only versions with a known integrity hash are accepted." + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "ツールバーボタン(Cmd+Shift+G)で注入されるreact-grabのnpmバージョン。既知の整合性ハッシュを持つバージョンのみ使用可能です。" + } + } + } + }, "settings.browser.history": { "extractionState": "manual", "localizations": { @@ -67653,6 +67775,41 @@ } } }, + "shortcut.toggleReactGrab.label": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Toggle React Grab" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "React Grabの切り替え" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "切换 React Grab" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "切換 React Grab" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "React Grab 전환" + } + } + } + }, "shortcut.showNotifications.label": { "extractionState": "manual", "localizations": { diff --git a/Sources/ContentView.swift b/Sources/ContentView.swift index dd56fe6b..8145ea58 100644 --- a/Sources/ContentView.swift +++ b/Sources/ContentView.swift @@ -5164,6 +5164,8 @@ struct ContentView: View { return .toggleBrowserDeveloperTools case "palette.browserConsole": return .showBrowserJavaScriptConsole + case "palette.browserReactGrab": + return .toggleReactGrab case "palette.browserSplitRight", "palette.terminalSplitBrowserRight": return .splitBrowserRight case "palette.browserSplitDown", "palette.terminalSplitBrowserDown": @@ -5790,6 +5792,15 @@ struct ContentView: View { when: { $0.bool(CommandPaletteContextKeys.panelIsBrowser) } ) ) + contributions.append( + CommandPaletteCommandContribution( + commandId: "palette.browserReactGrab", + title: constant(String(localized: "command.browserReactGrab.title", defaultValue: "Toggle React Grab")), + subtitle: browserPanelSubtitle, + keywords: ["browser", "react", "grab", "inspect", "element"], + when: { $0.bool(CommandPaletteContextKeys.panelIsBrowser) } + ) + ) contributions.append( CommandPaletteCommandContribution( commandId: "palette.browserZoomIn", @@ -6274,6 +6285,9 @@ struct ContentView: View { NSSound.beep() } } + registry.register(commandId: "palette.browserReactGrab") { + tabManager.toggleReactGrabFocusedBrowser() + } registry.register(commandId: "palette.browserZoomIn") { if !tabManager.zoomInFocusedBrowser() { NSSound.beep() diff --git a/Sources/KeyboardShortcutSettings.swift b/Sources/KeyboardShortcutSettings.swift index b044c5c7..601ed71c 100644 --- a/Sources/KeyboardShortcutSettings.swift +++ b/Sources/KeyboardShortcutSettings.swift @@ -46,6 +46,7 @@ enum KeyboardShortcutSettings { case openBrowser case toggleBrowserDeveloperTools case showBrowserJavaScriptConsole + case toggleReactGrab var id: String { rawValue } @@ -83,6 +84,7 @@ enum KeyboardShortcutSettings { case .openBrowser: return String(localized: "shortcut.openBrowser.label", defaultValue: "Open Browser") case .toggleBrowserDeveloperTools: return String(localized: "shortcut.toggleBrowserDevTools.label", defaultValue: "Toggle Browser Developer Tools") case .showBrowserJavaScriptConsole: return String(localized: "shortcut.showBrowserJSConsole.label", defaultValue: "Show Browser JavaScript Console") + case .toggleReactGrab: return String(localized: "shortcut.toggleReactGrab.label", defaultValue: "Toggle React Grab") } } @@ -120,6 +122,7 @@ enum KeyboardShortcutSettings { case .openBrowser: return "shortcut.openBrowser" case .toggleBrowserDeveloperTools: return "shortcut.toggleBrowserDeveloperTools" case .showBrowserJavaScriptConsole: return "shortcut.showBrowserJavaScriptConsole" + case .toggleReactGrab: return "shortcut.toggleReactGrab" } } @@ -191,6 +194,8 @@ enum KeyboardShortcutSettings { case .showBrowserJavaScriptConsole: // Safari default: Show JavaScript Console. return StoredShortcut(key: "c", command: true, shift: false, option: true, control: false) + case .toggleReactGrab: + return StoredShortcut(key: "g", command: true, shift: true, option: false, control: false) } } diff --git a/Sources/Panels/BrowserPanel.swift b/Sources/Panels/BrowserPanel.swift index dfbd8a9c..ea3a8a70 100644 --- a/Sources/Panels/BrowserPanel.swift +++ b/Sources/Panels/BrowserPanel.swift @@ -2258,6 +2258,8 @@ final class BrowserPanel: Panel, ObservableObject { private var insecureHTTPAlertWindowProvider: () -> NSWindow? = { NSApp.keyWindow ?? NSApp.mainWindow } // Persist user intent across WebKit detach/reattach churn (split/layout updates). @Published private(set) var preferredDeveloperToolsVisible: Bool = false + @Published var isReactGrabActive: Bool = false + var reactGrabMessageHandler: ReactGrabMessageHandler? private var preferredDeveloperToolsPresentation: DeveloperToolsPresentation = .unknown private var forceDeveloperToolsRefreshOnNextAttach: Bool = false private var developerToolsRestoreRetryWorkItem: DispatchWorkItem? @@ -2547,6 +2549,7 @@ final class BrowserPanel: Panel, ObservableObject { webView.navigationDelegate = navigationDelegate webView.uiDelegate = uiDelegate setupObservers(for: webView) + setupReactGrabMessageHandler(for: webView) } private func configureNavigationDelegateCallbacks() { @@ -2704,6 +2707,7 @@ final class BrowserPanel: Panel, ObservableObject { bindWebView(webView) installDetachedDeveloperToolsWindowCloseObserver() applyBrowserThemeModeIfNeeded() + ReactGrabScriptLoader.prefetch() insecureHTTPAlertWindowProvider = { [weak self] in self?.webView.window ?? NSApp.keyWindow ?? NSApp.mainWindow } diff --git a/Sources/Panels/BrowserPanelView.swift b/Sources/Panels/BrowserPanelView.swift index 0624c149..ead597d4 100644 --- a/Sources/Panels/BrowserPanelView.swift +++ b/Sources/Panels/BrowserPanelView.swift @@ -588,6 +588,7 @@ struct BrowserPanelView: View { if isWebViewBlank() { refreshEmptyStateImportBrowsers() } + panel.resetReactGrabState() } .onChange(of: browserThemeModeRaw) { _ in let normalizedMode = BrowserThemeSettings.mode(for: browserThemeModeRaw) @@ -732,6 +733,7 @@ struct BrowserPanelView: View { if shouldShowToolbarImportHintChip { browserImportHintToolbarChip } + reactGrabButton browserProfileButton browserThemeModeButton developerToolsButton @@ -823,6 +825,23 @@ struct BrowserPanelView: View { } } + private var reactGrabButton: some View { + Button(action: { + Task { await panel.toggleOrInjectReactGrab() } + }) { + Image(systemName: "cursorarrow.click.2") + .symbolRenderingMode(.monochrome) + .cmuxFlatSymbolColorRendering() + .font(.system(size: devToolsButtonIconSize, weight: .medium)) + .foregroundStyle(panel.isReactGrabActive ? Color.accentColor : Color.secondary) + .frame(width: addressBarButtonSize, height: addressBarButtonSize, alignment: .center) + } + .buttonStyle(OmnibarAddressButtonStyle()) + .frame(width: addressBarButtonSize, height: addressBarButtonSize, alignment: .center) + .safeHelp(String(localized: "browser.reactGrab", defaultValue: "Inject React Grab")) + .accessibilityIdentifier("BrowserReactGrabButton") + } + private var developerToolsButton: some View { Button(action: { openDevTools() diff --git a/Sources/Panels/ReactGrab.swift b/Sources/Panels/ReactGrab.swift new file mode 100644 index 00000000..86e86da3 --- /dev/null +++ b/Sources/Panels/ReactGrab.swift @@ -0,0 +1,205 @@ +import CryptoKit +import Foundation +import WebKit + +#if DEBUG +import Bonsplit +#endif + +// MARK: - Settings + +enum ReactGrabSettings { + static let versionKey = "reactGrabVersion" + static let defaultVersion = "0.1.29" + + /// Known versions and their SHA-256 integrity hashes. + /// Add new entries when bumping the default or to allow user-selected versions. + static let knownHashes: [String: String] = [ + "0.1.29": "4a1e71090e8ad8bb6049de80ccccdc0f5bb147b9f8fb88886d871612ac7ca04b", + ] + + static func scriptURL(for version: String) -> URL { + URL(string: "https://unpkg.com/react-grab@\(version)/dist/index.global.js")! + } + + static var configuredVersion: String { + let stored = UserDefaults.standard.string(forKey: versionKey)?.trimmingCharacters(in: .whitespacesAndNewlines) ?? "" + return stored.isEmpty ? defaultVersion : stored + } +} + +// MARK: - Script Loader + +/// Fetches, integrity-checks, and caches the react-grab script. +/// Shared across all BrowserPanel instances. +enum ReactGrabScriptLoader { + private static var cachedScript: String? + private static var cachedVersion: String? + private static var prefetchTask: Task? + + static func prefetch() { + let version = ReactGrabSettings.configuredVersion + // Invalidate cache if version changed. + if cachedVersion != version { + cachedScript = nil + cachedVersion = nil + } + guard cachedScript == nil else { return } + guard prefetchTask == nil else { return } + prefetchTask = Task.detached(priority: .low) { + let result = await doFetch(version: version) + await MainActor.run { prefetchTask = nil } + return result + } + } + + static func fetch() async -> String? { + let version = ReactGrabSettings.configuredVersion + if cachedVersion == version, let cached = cachedScript { return cached } + prefetch() + return await prefetchTask?.value + } + + private static func doFetch(version: String) async -> String? { + let url = ReactGrabSettings.scriptURL(for: version) + do { + let (data, _) = try await URLSession.shared.data(from: url) + if let expectedHash = ReactGrabSettings.knownHashes[version] { + let hash = SHA256.hash(data: data) + let hex = hash.compactMap { String(format: "%02x", $0) }.joined() + guard hex == expectedHash else { + NSLog("ReactGrab: integrity mismatch for v%@ (got %@)", version, hex) + return nil + } + } + guard let script = String(data: data, encoding: .utf8) else { return nil } + await MainActor.run { + cachedScript = script + cachedVersion = version + } + return script + } catch { + NSLog("ReactGrab: fetch failed for v%@: %@", version, error.localizedDescription) + return nil + } + } +} + +// MARK: - WKScriptMessageHandler + +private let reactGrabMessageHandlerName = "cmuxReactGrab" + +class ReactGrabMessageHandler: NSObject, WKScriptMessageHandler { + private let onStateChange: @MainActor (Bool) -> Void + + init(onStateChange: @escaping @MainActor (Bool) -> Void) { + self.onStateChange = onStateChange + } + + func userContentController( + _ userContentController: WKUserContentController, + didReceive message: WKScriptMessage + ) { + guard let body = message.body as? [String: Any], + let isActive = body["isActive"] as? Bool else { return } + #if DEBUG + dlog("reactGrab.messageHandler isActive=\(isActive)") + #endif + Task { @MainActor in + #if DEBUG + dlog("reactGrab.messageHandler.mainActor isActive=\(isActive)") + #endif + onStateChange(isActive) + } + } +} + +// MARK: - BrowserPanel extension + +extension BrowserPanel { + func setupReactGrabMessageHandler(for webView: WKWebView) { + let handler = ReactGrabMessageHandler { [weak self] isActive in + self?.isReactGrabActive = isActive + } + reactGrabMessageHandler = handler + webView.configuration.userContentController.add(handler, name: reactGrabMessageHandlerName) + } + + func injectReactGrab() async { + #if DEBUG + dlog("reactGrab.inject.start") + #endif + guard let scriptSource = await ReactGrabScriptLoader.fetch() else { + #if DEBUG + dlog("reactGrab.inject.fetchFailed") + #endif + return + } + #if DEBUG + dlog("reactGrab.inject.fetched len=\(scriptSource.count)") + #endif + + let handlerName = reactGrabMessageHandlerName + let combined = """ + (function() { + if (window.__REACT_GRAB__) { window.__REACT_GRAB__.activate(); return; } + window.addEventListener('react-grab:init', function(e) { + var api = e.detail; + if (!api) return; + api.activate(); + var lastActive; + api.registerPlugin({ + name: 'cmux-bridge', + hooks: { + onStateChange: function(state) { + if (state.isActive === lastActive) return; + lastActive = state.isActive; + var h = window.webkit && window.webkit.messageHandlers && window.webkit.messageHandlers.\(handlerName); + if (h) h.postMessage({ isActive: state.isActive }); + } + } + }); + }, { once: true }); + })(); + \(scriptSource) + """ + #if DEBUG + dlog("reactGrab.inject.evalJS len=\(combined.count)") + #endif + webView.evaluateJavaScript(combined) { [weak self] _, error in + #if DEBUG + dlog("reactGrab.inject.evalJS.done error=\(error?.localizedDescription ?? "none")") + #endif + if let error { + NSLog("ReactGrab: injection failed: %@", error.localizedDescription) + Task { @MainActor in self?.isReactGrabActive = false } + } + } + #if DEBUG + dlog("reactGrab.inject.end") + #endif + } + + func toggleReactGrab() { + #if DEBUG + dlog("reactGrab.toggle.start") + #endif + let script = "window.__REACT_GRAB__?.toggle()" + webView.evaluateJavaScript(script, completionHandler: nil) + #if DEBUG + dlog("reactGrab.toggle.end") + #endif + } + + func toggleOrInjectReactGrab() async { + if isReactGrabActive { + toggleReactGrab() + } else { + await injectReactGrab() + } + } + + func resetReactGrabState() { + isReactGrabActive = false + } +} diff --git a/Sources/TabManager.swift b/Sources/TabManager.swift index c60a20f9..4e69d49e 100644 --- a/Sources/TabManager.swift +++ b/Sources/TabManager.swift @@ -3159,6 +3159,11 @@ class TabManager: ObservableObject { focusedBrowserPanel?.showDeveloperToolsConsole() ?? false } + func toggleReactGrabFocusedBrowser() { + guard let panel = focusedBrowserPanel else { return } + Task { await panel.toggleOrInjectReactGrab() } + } + /// Backwards compatibility: returns the focused surface ID func focusedSurfaceId(for tabId: UUID) -> UUID? { focusedPanelId(for: tabId) diff --git a/Sources/cmuxApp.swift b/Sources/cmuxApp.swift index 8dffff53..701f8970 100644 --- a/Sources/cmuxApp.swift +++ b/Sources/cmuxApp.swift @@ -164,6 +164,8 @@ struct cmuxApp: App { private var toggleBrowserDeveloperToolsShortcutData = Data() @AppStorage(KeyboardShortcutSettings.Action.showBrowserJavaScriptConsole.defaultsKey) private var showBrowserJavaScriptConsoleShortcutData = Data() + @AppStorage(KeyboardShortcutSettings.Action.toggleReactGrab.defaultsKey) + private var toggleReactGrabShortcutData = Data() @AppStorage(KeyboardShortcutSettings.Action.splitBrowserRight.defaultsKey) private var splitBrowserRightShortcutData = Data() @AppStorage(KeyboardShortcutSettings.Action.splitBrowserDown.defaultsKey) private var splitBrowserDownShortcutData = Data() @AppStorage(KeyboardShortcutSettings.Action.renameWorkspace.defaultsKey) private var renameWorkspaceShortcutData = Data() @@ -722,6 +724,10 @@ struct cmuxApp: App { } } + splitCommandButton(title: String(localized: "menu.view.toggleReactGrab", defaultValue: "Toggle React Grab"), shortcut: toggleReactGrabMenuShortcut) { + activeTabManager.toggleReactGrabFocusedBrowser() + } + Button(String(localized: "menu.view.zoomIn", defaultValue: "Zoom In")) { _ = activeTabManager.zoomInFocusedBrowser() } @@ -930,6 +936,13 @@ struct cmuxApp: App { ) } + private var toggleReactGrabMenuShortcut: StoredShortcut { + decodeShortcut( + from: toggleReactGrabShortcutData, + fallback: KeyboardShortcutSettings.Action.toggleReactGrab.defaultShortcut + ) + } + private var splitBrowserRightMenuShortcut: StoredShortcut { decodeShortcut( from: splitBrowserRightShortcutData, @@ -4010,6 +4023,7 @@ struct SettingsView: View { @AppStorage(BrowserImportHintSettings.variantKey) private var browserImportHintVariantRaw = BrowserImportHintSettings.defaultVariant.rawValue @AppStorage(BrowserImportHintSettings.showOnBlankTabsKey) private var showBrowserImportHintOnBlankTabs = BrowserImportHintSettings.defaultShowOnBlankTabs @AppStorage(BrowserImportHintSettings.dismissedKey) private var isBrowserImportHintDismissed = BrowserImportHintSettings.defaultDismissed + @AppStorage(ReactGrabSettings.versionKey) private var reactGrabVersion = ReactGrabSettings.defaultVersion @AppStorage(BrowserLinkOpenSettings.openTerminalLinksInCmuxBrowserKey) private var openTerminalLinksInCmuxBrowser = BrowserLinkOpenSettings.defaultOpenTerminalLinksInCmuxBrowser @AppStorage(BrowserLinkOpenSettings.interceptTerminalOpenCommandInCmuxBrowserKey) private var interceptTerminalOpenCommandInCmuxBrowser = BrowserLinkOpenSettings.initialInterceptTerminalOpenCommandInCmuxBrowserValue() @@ -5703,6 +5717,19 @@ struct SettingsView: View { SettingsCardDivider() + SettingsCardRow( + String(localized: "settings.browser.reactGrabVersion", defaultValue: "React Grab Version"), + subtitle: String(localized: "settings.browser.reactGrabVersion.subtitle", defaultValue: "Pinned npm version of react-grab injected by the toolbar button (Cmd+Shift+G). Only versions with a known integrity hash are accepted.") + ) { + TextField("", text: $reactGrabVersion) + .textFieldStyle(.roundedBorder) + .frame(width: 100) + .font(.system(.body, design: .monospaced)) + .accessibilityIdentifier("SettingsReactGrabVersionField") + } + + SettingsCardDivider() + SettingsCardRow(String(localized: "settings.browser.history", defaultValue: "Browsing History"), subtitle: browserHistorySubtitle) { Button(String(localized: "settings.browser.history.clearButton", defaultValue: "Clear History…")) { showClearBrowserHistoryConfirmation = true