diff --git a/Sources/Panels/BrowserPanelView.swift b/Sources/Panels/BrowserPanelView.swift index 5c9c780a..d5433771 100644 --- a/Sources/Panels/BrowserPanelView.swift +++ b/Sources/Panels/BrowserPanelView.swift @@ -203,6 +203,35 @@ func resolvedBrowserOmnibarPillBackgroundColor( return themeBackgroundColor.blended(withFraction: darkenMix, of: .black) ?? themeBackgroundColor } +private struct BrowserChromeStyle { + let backgroundColor: NSColor + let colorScheme: ColorScheme + let omnibarPillBackgroundColor: NSColor + + static func resolve( + for colorScheme: ColorScheme, + themeBackgroundColor: NSColor + ) -> BrowserChromeStyle { + let backgroundColor = resolvedBrowserChromeBackgroundColor( + for: colorScheme, + themeBackgroundColor: themeBackgroundColor + ) + let chromeColorScheme = resolvedBrowserChromeColorScheme( + for: colorScheme, + themeBackgroundColor: backgroundColor + ) + let omnibarPillBackgroundColor = resolvedBrowserOmnibarPillBackgroundColor( + for: chromeColorScheme, + themeBackgroundColor: backgroundColor + ) + return BrowserChromeStyle( + backgroundColor: backgroundColor, + colorScheme: chromeColorScheme, + omnibarPillBackgroundColor: omnibarPillBackgroundColor + ) + } +} + /// View for rendering a browser panel with address bar struct BrowserPanelView: View { @ObservedObject var panel: BrowserPanel @@ -220,6 +249,8 @@ struct BrowserPanelView: View { @AppStorage(BrowserDevToolsButtonDebugSettings.iconNameKey) private var devToolsIconNameRaw = BrowserDevToolsButtonDebugSettings.defaultIcon.rawValue @AppStorage(BrowserDevToolsButtonDebugSettings.iconColorKey) private var devToolsIconColorRaw = BrowserDevToolsButtonDebugSettings.defaultColor.rawValue @AppStorage(BrowserThemeSettings.modeKey) private var browserThemeModeRaw = BrowserThemeSettings.defaultMode.rawValue + @AppStorage(KeyboardShortcutSettings.Action.toggleBrowserDeveloperTools.defaultsKey) + private var toggleBrowserDeveloperToolsShortcutData = Data() @State private var suggestionTask: Task? @State private var isLoadingRemoteSuggestions: Bool = false @State private var latestRemoteSuggestionQuery: String = "" @@ -236,7 +267,11 @@ struct BrowserPanelView: View { @State private var pendingAddressBarFocusRetryRequestId: UUID? @State private var pendingAddressBarFocusRetryGeneration: UInt64 = 0 @State private var isBrowserThemeMenuPresented = false - @State private var ghosttyBackgroundGeneration: Int = 0 + @State private var browserChromeStyle = BrowserChromeStyle.resolve( + for: .light, + themeBackgroundColor: GhosttyBackgroundTheme.currentColor() + ) + @State private var toggleBrowserDeveloperToolsShortcut = KeyboardShortcutSettings.Action.toggleBrowserDeveloperTools.defaultShortcut // Keep this below half of the compact omnibar height so it reads as a squircle, // not a capsule. private let omnibarPillCornerRadius: CGFloat = 10 @@ -282,24 +317,15 @@ struct BrowserPanelView: View { } private var browserChromeBackground: Color { - _ = ghosttyBackgroundGeneration - return Color(nsColor: GhosttyBackgroundTheme.currentColor()) + Color(nsColor: browserChromeStyle.backgroundColor) } private var browserChromeBackgroundColor: NSColor { - _ = ghosttyBackgroundGeneration - return resolvedBrowserChromeBackgroundColor( - for: colorScheme, - themeBackgroundColor: GhosttyBackgroundTheme.currentColor() - ) + browserChromeStyle.backgroundColor } private var browserChromeColorScheme: ColorScheme { - _ = ghosttyBackgroundGeneration - return resolvedBrowserChromeColorScheme( - for: colorScheme, - themeBackgroundColor: GhosttyBackgroundTheme.currentColor() - ) + browserChromeStyle.colorScheme } private var browserContentAccessibilityIdentifier: String { @@ -307,10 +333,12 @@ struct BrowserPanelView: View { } private var omnibarPillBackgroundColor: NSColor { - resolvedBrowserOmnibarPillBackgroundColor( - for: browserChromeColorScheme, - themeBackgroundColor: browserChromeBackgroundColor - ) + browserChromeStyle.omnibarPillBackgroundColor + } + + private var developerToolsButtonHelp: String { + let base = String(localized: "browser.toggleDevTools", defaultValue: "Toggle Developer Tools") + return "\(base) (\(toggleBrowserDeveloperToolsShortcut.displayString))" } private var owningWorkspace: Workspace? { @@ -420,6 +448,8 @@ struct BrowserPanelView: View { BrowserSearchSettings.searchSuggestionsEnabledKey: BrowserSearchSettings.defaultSearchSuggestionsEnabled, BrowserThemeSettings.modeKey: BrowserThemeSettings.defaultMode.rawValue, ]) + refreshBrowserChromeStyle() + refreshToggleBrowserDeveloperToolsShortcut() let resolvedThemeMode = BrowserThemeSettings.mode(defaults: .standard) if browserThemeModeRaw != resolvedThemeMode.rawValue { browserThemeModeRaw = resolvedThemeMode.rawValue @@ -459,8 +489,12 @@ struct BrowserPanelView: View { panel.setBrowserThemeMode(normalizedMode) } .onChange(of: colorScheme) { _ in + refreshBrowserChromeStyle() panel.refreshAppearanceDrivenColors() } + .onChange(of: toggleBrowserDeveloperToolsShortcutData) { _ in + refreshToggleBrowserDeveloperToolsShortcut() + } .onChange(of: panel.pendingAddressBarFocusRequestId) { _ in applyPendingAddressBarFocusRequestIfNeeded() } @@ -552,7 +586,7 @@ struct BrowserPanelView: View { } } .onReceive(NotificationCenter.default.publisher(for: .ghosttyDefaultBackgroundDidChange)) { _ in - ghosttyBackgroundGeneration &+= 1 + refreshBrowserChromeStyle() } } @@ -668,7 +702,7 @@ struct BrowserPanelView: View { } .buttonStyle(OmnibarAddressButtonStyle()) .frame(width: addressBarButtonSize, height: addressBarButtonSize, alignment: .center) - .safeHelp(KeyboardShortcutSettings.Action.toggleBrowserDeveloperTools.tooltip(String(localized: "browser.toggleDevTools", defaultValue: "Toggle Developer Tools"))) + .safeHelp(developerToolsButtonHelp) .accessibilityIdentifier("BrowserToggleDevToolsButton") } @@ -907,6 +941,28 @@ struct BrowserPanelView: View { } } + private func refreshBrowserChromeStyle() { + browserChromeStyle = BrowserChromeStyle.resolve( + for: colorScheme, + themeBackgroundColor: GhosttyBackgroundTheme.currentColor() + ) + } + + private func refreshToggleBrowserDeveloperToolsShortcut() { + toggleBrowserDeveloperToolsShortcut = decodeShortcut( + from: toggleBrowserDeveloperToolsShortcutData, + fallback: KeyboardShortcutSettings.Action.toggleBrowserDeveloperTools.defaultShortcut + ) + } + + private func decodeShortcut(from data: Data, fallback: StoredShortcut) -> StoredShortcut { + guard !data.isEmpty, + let shortcut = try? JSONDecoder().decode(StoredShortcut.self, from: data) else { + return fallback + } + return shortcut + } + private func syncWebViewResponderPolicyWithViewState( reason: String, isPanelFocusedOverride: Bool? = nil diff --git a/Sources/TerminalController.swift b/Sources/TerminalController.swift index dccd9cb1..4515805a 100644 --- a/Sources/TerminalController.swift +++ b/Sources/TerminalController.swift @@ -13025,6 +13025,61 @@ class TerminalController { return trimmed.isEmpty ? nil : trimmed } + private func schedulePanelMetadataMutation( + args: String, + options: [String: String], + missingPanelUsage: String, + mutation: @escaping (Tab, UUID) -> Void + ) -> String { + let rawPanelArg = options["panel"] ?? options["surface"] + let surfaceIdFromOptions: UUID? + if let rawPanelArg { + if rawPanelArg.isEmpty { + return "ERROR: Missing panel id — usage: \(missingPanelUsage)" + } + guard let surfaceId = UUID(uuidString: rawPanelArg) else { + return "ERROR: Invalid panel id '\(rawPanelArg)'" + } + surfaceIdFromOptions = surfaceId + } else { + surfaceIdFromOptions = nil + } + + if let tabArg = options["tab"]?.trimmingCharacters(in: .whitespacesAndNewlines), + !tabArg.isEmpty, + UUID(uuidString: tabArg) == nil, + Int(tabArg) == nil { + return "ERROR: Tab not found" + } + + if let scope = Self.explicitSocketScope(options: options) { + DispatchQueue.main.async { [weak self] in + guard let self, + let tab = self.tabForSidebarMutation(id: scope.workspaceId) else { + return + } + let validSurfaceIds = Set(tab.panels.keys) + tab.pruneSurfaceMetadata(validSurfaceIds: validSurfaceIds) + guard validSurfaceIds.contains(scope.panelId) else { return } + mutation(tab, scope.panelId) + } + return "OK" + } + + DispatchQueue.main.async { [weak self] in + guard let self, + let tab = self.resolveTabForReport(args) else { + return + } + let validSurfaceIds = Set(tab.panels.keys) + tab.pruneSurfaceMetadata(validSurfaceIds: validSurfaceIds) + guard let surfaceId = surfaceIdFromOptions ?? tab.focusedPanelId else { return } + guard validSurfaceIds.contains(surfaceId) else { return } + mutation(tab, surfaceId) + } + return "OK" + } + private func upsertSidebarMetadata(_ args: String, missingError: String) -> String { guard tabManager != nil else { return "ERROR: TabManager not available" } let parsed = parseOptionsNoStop(args) @@ -13611,40 +13666,13 @@ class TerminalController { } let label = String(labelRaw.prefix(16)) - var result = "OK" - DispatchQueue.main.sync { - guard let tab = resolveTabForReport(args) else { - result = parsed.options["tab"] != nil ? "ERROR: Tab not found" : "ERROR: No tab selected" - return - } - let validSurfaceIds = Set(tab.panels.keys) - tab.pruneSurfaceMetadata(validSurfaceIds: validSurfaceIds) - - let panelArg = parsed.options["panel"] ?? parsed.options["surface"] - let surfaceId: UUID - if let panelArg { - if panelArg.isEmpty { - result = "ERROR: Missing panel id — usage: report_pr [--label=PR] [--state=open|merged|closed] [--tab=X] [--panel=Y]" - return - } - guard let parsedId = UUID(uuidString: panelArg) else { - result = "ERROR: Invalid panel id '\(panelArg)'" - return - } - surfaceId = parsedId - } else { - guard let focused = tab.focusedPanelId else { - result = "ERROR: Missing panel id (no focused surface)" - return - } - surfaceId = focused - } - - guard validSurfaceIds.contains(surfaceId) else { - result = "ERROR: Panel not found '\(surfaceId.uuidString)'" - return - } - + // Shell integration provides explicit workspace/panel UUIDs for browser metadata. + // Keep this telemetry path off-main so SwiftUI render passes can't deadlock the socket handler. + return schedulePanelMetadataMutation( + args: args, + options: parsed.options, + missingPanelUsage: "report_pr [--label=PR] [--state=open|merged|closed] [--tab=X] [--panel=Y]" + ) { tab, surfaceId in guard Self.shouldReplacePullRequest( current: tab.panelPullRequests[surfaceId], number: number, @@ -13663,48 +13691,17 @@ class TerminalController { status: status ) } - return result } private func clearPullRequest(_ args: String) -> String { let parsed = parseOptions(args) - var result = "OK" - DispatchQueue.main.sync { - guard let tab = resolveTabForReport(args) else { - result = parsed.options["tab"] != nil ? "ERROR: Tab not found" : "ERROR: No tab selected" - return - } - let validSurfaceIds = Set(tab.panels.keys) - tab.pruneSurfaceMetadata(validSurfaceIds: validSurfaceIds) - - let panelArg = parsed.options["panel"] ?? parsed.options["surface"] - let surfaceId: UUID - if let panelArg { - if panelArg.isEmpty { - result = "ERROR: Missing panel id — usage: clear_pr [--tab=X] [--panel=Y]" - return - } - guard let parsedId = UUID(uuidString: panelArg) else { - result = "ERROR: Invalid panel id '\(panelArg)'" - return - } - surfaceId = parsedId - } else { - guard let focused = tab.focusedPanelId else { - result = "ERROR: Missing panel id (no focused surface)" - return - } - surfaceId = focused - } - - guard validSurfaceIds.contains(surfaceId) else { - result = "ERROR: Panel not found '\(surfaceId.uuidString)'" - return - } - + return schedulePanelMetadataMutation( + args: args, + options: parsed.options, + missingPanelUsage: "clear_pr [--tab=X] [--panel=Y]" + ) { tab, surfaceId in tab.clearPanelPullRequest(panelId: surfaceId) } - return result } private func reportPorts(_ args: String) -> String {