Skip to content
29 changes: 29 additions & 0 deletions Sources/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -72,8 +72,27 @@ final class ThemeReloadCoalescer {
/// `NSToolbar.delegate` is weak, so without this the delegate is deallocated
/// immediately and the toolbar silently loses its item provider.
private var mainWindowToolbarDelegateAssociationKey: UInt8 = 0
private var mainWindowIdentifierGuardAssociationKey: UInt8 = 0
let mainWindowToolbarIdentifier = NSToolbar.Identifier("programa.main.titlebar")

/// Keeps a main window's `programa.main.<id>` identifier in place. SwiftUI's
/// `AppWindowsController.makeMainWindow` assigns the scene identifier after it attaches the
/// content view, so it can land after `configureMainWindow` ran from the first `WindowAccessor`
/// callback and silently replace ours; `WindowAccessor` dedupes per window and never re-applies
/// it. Main-window routing and the UI test harnesses look windows up by this identifier.
private final class MainWindowIdentifierGuard {
var expected: NSUserInterfaceItemIdentifier
private var observation: NSKeyValueObservation?

init(window: NSWindow, expected: NSUserInterfaceItemIdentifier) {
self.expected = expected
observation = window.observe(\.identifier, options: [.new]) { [weak self] window, _ in
guard let self, window.identifier != self.expected else { return }
window.identifier = self.expected
}
}
}

/// Supplies a single system space item so the main window's otherwise-empty
/// unified toolbar reports a non-zero height, growing the titlebar so AppKit
/// re-centers the traffic lights on `WindowGlassEffect.sidebarHeaderCenterFromWindowTop`.
Expand Down Expand Up @@ -7975,6 +7994,16 @@ final class AppDelegate: NSObject, NSApplicationDelegate, @preconcurrency UNUser
// part of what feeds the safe-area invalidation storm in issue #307 — diffing
// stops the re-entry without changing what gets configured on first apply.
let nextIdentifier = NSUserInterfaceItemIdentifier(windowIdentifier)
if let identifierGuard = objc_getAssociatedObject(window, &mainWindowIdentifierGuardAssociationKey) as? MainWindowIdentifierGuard {
identifierGuard.expected = nextIdentifier
} else {
objc_setAssociatedObject(
window,
&mainWindowIdentifierGuardAssociationKey,
MainWindowIdentifierGuard(window: window, expected: nextIdentifier),
.OBJC_ASSOCIATION_RETAIN_NONATOMIC
)
}
if window.identifier != nextIdentifier {
window.identifier = nextIdentifier
}
Expand Down
7 changes: 6 additions & 1 deletion Sources/TabItemView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1212,8 +1212,13 @@ struct TabItemView: View, Equatable {
return tabManager.tabs[currentIndex - 1].id == indicator.tabId
}

/// `.combine` hides the description's own accessibility element, so the row label carries
/// the description (markdown rendered to plain text, line breaks kept) after the position.
private var accessibilityTitle: String {
String(localized: "accessibility.workspacePosition", defaultValue: "\(tab.title), workspace \(index + 1) of \(accessibilityWorkspaceCount)")
let position = String(localized: "accessibility.workspacePosition", defaultValue: "\(tab.title), workspace \(index + 1) of \(accessibilityWorkspaceCount)")
guard let description = tab.customDescription else { return position }
let plain = SidebarMarkdownRenderer.renderWorkspaceDescription(description).map { String($0.characters) } ?? description
return "\(position), \(plain)"
}

private func moveBy(_ delta: Int) {
Expand Down
10 changes: 5 additions & 5 deletions programaUITests/WorkspaceDescriptionUITests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -142,16 +142,16 @@ final class WorkspaceDescriptionUITests: XCTestCase {
"Expected Enter to save and dismiss the workspace description editor after multiline input"
)

let renderedDescription = app
.descendants(matching: .staticText)
.matching(NSPredicate(format: "label == %@", description))
let workspaceRow = app
.descendants(matching: .any)
.matching(NSPredicate(format: "identifier BEGINSWITH %@", "sidebarWorkspace."))
.firstMatch

XCTAssertTrue(
workspaceDescriptionPollUntil(timeout: 5.0) {
renderedDescription.exists
workspaceRow.exists && workspaceRow.label.contains(description)
},
"Expected the sidebar to render the saved multiline description with a newline-preserving label"
"Expected the sidebar workspace row to expose the saved multiline description, line break included, in its accessibility label. row=\(workspaceRow.debugDescription)"
)
}

Expand Down
3 changes: 0 additions & 3 deletions scripts/nightly-ui-classes.txt
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,6 @@ TitlebarShortcutHintsUITests
TerminalCmdClickUITests
# Raw CGEvent drags need the Accessibility (Events) grant that CI runners do not have; the recording shows the prompt and no drag.
!BonsplitTabDragUITests/testMinimalModeKeepsTabReorderWorking
# The second app launch of this class often has no configured main window (passes when run alone), so the second-launched method fails.
!BonsplitTabDragUITests/testMinimalModeCollapsedSidebarKeepsWorkspaceControlsSuppressed
!CommandPaletteAllSurfacesUITests/testCmdPSearchCanIncludeSurfacesFromOtherWorkspacesWhenEnabled
!CommandPaletteAllSurfacesUITests/testCommandPaletteCanEnableAndDisableMinimalMode
!CommandPaletteAllSurfacesUITests/testSwitcherEmptyStateDoesNotBlinkWhileRefiningNoMatchQuery
!WorkspaceDescriptionUITests/testSidebarRendersSavedDescriptionWithLineBreaks
Loading