Avoid blocking browser PR metadata updates (#1564)

This commit is contained in:
Austin Wang 2026-03-16 22:10:15 -07:00 committed by GitHub
parent 7f220dc8e4
commit 9bf6ad9457
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 142 additions and 89 deletions

View file

@ -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<Void, Never>?
@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

View file

@ -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 <number> <url> [--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 <number> <url> [--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 {