Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
a8543f1
ContentView drain: lift command-palette domain into CmuxCommandPalett…
azooz2003-bit Jun 13, 2026
7bb426d
ContentView drain: complete command-palette test cutover (stack E)
azooz2003-bit Jun 13, 2026
d017279
ContentView drain: remove leftover duplicate overlay-promotion policy…
azooz2003-bit Jun 13, 2026
521da34
ContentView drain: lift AppKit-support primitives into CmuxAppKitSupp…
azooz2003-bit Jun 13, 2026
b9d9911
ContentView drain: lift NSColor.hexString into CmuxFoundation (stack …
azooz2003-bit Jun 13, 2026
cd036a7
ContentView drain: lift String.nilIfEmpty into CmuxFoundation (stack …
azooz2003-bit Jun 13, 2026
cffc1f3
ContentView drain: import CmuxFoundation in cmuxTests callers of NSCo…
azooz2003-bit Jun 13, 2026
6f0aadd
ContentView drain: lift sidebar scroll-view resolver cluster into Cmu…
azooz2003-bit Jun 13, 2026
ff864cf
ContentView drain: retarget SidebarScrollViewConfiguratorTests to Cmu…
azooz2003-bit Jun 13, 2026
fdc6420
ContentView drain: lift feedback composer domain into CmuxFeedback (s…
azooz2003-bit Jun 13, 2026
06ff565
ContentView drain: lift feedback composer message-editor views into C…
azooz2003-bit Jun 13, 2026
c9ac520
ContentView drain: link CmuxAppKitSupportUI into cmuxTests target (st…
azooz2003-bit Jun 13, 2026
656773c
ContentView drain: retarget app-host palette tests to CmuxCommandPale…
azooz2003-bit Jun 13, 2026
48f9614
ContentView drain: make SidebarScrollViewResolverView.resolveScrollVi…
azooz2003-bit Jun 13, 2026
8373ccc
ContentView drain: link CmuxCommandPalette into cmuxTests target (sta…
azooz2003-bit Jun 13, 2026
fc6d171
ContentView drain: fix CmuxAppKitSupportUI strict-concurrency errors …
azooz2003-bit Jun 13, 2026
b561670
ContentView drain: refresh file-length budget for palette-test import…
azooz2003-bit Jun 13, 2026
48bcea5
ContentView drain: import CmuxCommandPalette in CommandPaletteNucleoF…
azooz2003-bit Jun 13, 2026
393fe30
ContentView drain: retarget palette fingerprint test calls to package…
azooz2003-bit Jun 13, 2026
5cdf4bd
ContentView drain: lift sidebar drag auto-scroll domain to CmuxAppKit…
azooz2003-bit Jun 13, 2026
b0c1ca6
ContentView drain: lift sidebar drop planner to CmuxFoundation (stack E)
azooz2003-bit Jun 13, 2026
6720428
ContentView drain: lift sidebar tab drop-indicator predicate to CmuxF…
azooz2003-bit Jun 13, 2026
ec97502
ContentView drain: lift browser-stack drop planner to CmuxSidebarProv…
azooz2003-bit Jun 13, 2026
307fcfa
ContentView drain: lift sidebar drag-lifecycle policies to CmuxFounda…
azooz2003-bit Jun 13, 2026
cec2995
ContentView drain: wire CmuxSidebarProviderKit into cmuxTests target …
azooz2003-bit Jun 13, 2026
f36cdfb
Merge remote-tracking branch 'origin/main' into feat-contentview-drain
azooz2003-bit Jun 13, 2026
57df083
ContentView drain: import CmuxFoundation in two lifted-type test cons…
azooz2003-bit Jun 13, 2026
1675a29
ContentView drain: lift SidebarFeedbackComposerSheet to CmuxFeedbackUI
azooz2003-bit Jun 13, 2026
547fd6a
ContentView drain: lift shortcut-hint + dev-banner + markdown leaves …
azooz2003-bit Jun 13, 2026
2196d23
ContentView drain: lift SidebarWorkspaceSelectionSyncPolicy to CmuxFo…
azooz2003-bit Jun 13, 2026
68a2620
ContentView drain: lift SidebarDragLifecycleNotification to CmuxFound…
azooz2003-bit Jun 13, 2026
743c2ae
Merge remote-tracking branch 'origin/main' into feat-contentview-drain
azooz2003-bit Jun 13, 2026
de016c5
Merge origin/main into feat-contentview-drain (re-sync after Wave-3 m…
azooz2003-bit Jun 14, 2026
b0f340a
ContentView drain: mark lifted policy/value namespace enums lint:allow
azooz2003-bit Jun 14, 2026
7978cb2
Merge origin/main into feat-contentview-drain (re-sync #2 after #6067)
azooz2003-bit Jun 14, 2026
e878eb0
Merge remote-tracking branch 'origin/main' into feat-contentview-drain
azooz2003-bit Jun 14, 2026
3e88c09
Merge remote-tracking branch 'origin/main' into feat-contentview-drain
azooz2003-bit Jun 14, 2026
a271bd9
Merge remote-tracking branch 'origin/main' into feat-contentview-drain
azooz2003-bit Jun 14, 2026
f2bd672
Merge remote-tracking branch 'origin/main' into feat-contentview-drain
azooz2003-bit Jun 14, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 11 additions & 9 deletions .github/swift-file-length-budget.tsv
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
# Reduce counts as files shrink. CI fails if tracked files exceed this budget.
33285 CLI/cmux.swift
19990 Sources/Workspace.swift
19265 Sources/ContentView.swift
18699 Sources/ContentView.swift
18118 Sources/AppDelegate.swift
16674 Sources/GhosttyTerminalView.swift
14622 Sources/TerminalController.swift
Expand Down Expand Up @@ -33,19 +33,19 @@
3396 Sources/CmuxConfig.swift
3316 cmuxTests/TabManagerSessionSnapshotTests.swift
3202 Sources/Update/UpdateTitlebarAccessory.swift
2877 Sources/SessionIndexView.swift
2878 Sources/SessionIndexView.swift
2871 cmuxTests/CMUXOpenCommandTests.swift
2565 Sources/Panels/CmuxWebView.swift
2545 cmuxTests/WorkspaceManualUnreadTests.swift
2544 cmuxTests/CommandPaletteSearchEngineTests.swift
2546 cmuxTests/WorkspaceManualUnreadTests.swift
2516 Sources/KeyboardShortcutSettings.swift
2449 cmuxTests/CommandPaletteSearchEngineTests.swift
2327 cmuxTests/CJKIMEInputTests.swift
2322 Sources/Mobile/MobileHostService.swift
2314 Sources/FileExplorerView.swift
2260 Sources/TerminalWindowPortal.swift
2198 Sources/SessionPersistence.swift
2123 cmuxTests/ShortcutAndCommandPaletteTests.swift
2117 cmuxTests/CmuxConfigTests.swift
2087 cmuxTests/ShortcutAndCommandPaletteTests.swift
2030 Sources/KeyboardShortcutSettingsFileStore.swift
1949 Sources/Panels/BrowserWebAuthnSupport.swift
1941 Sources/TerminalNotificationStore.swift
Expand All @@ -62,7 +62,6 @@
1498 cmuxTests/OmnibarAndToolsTests.swift
1496 cmuxUITests/MultiWindowNotificationsUITests.swift
1448 Sources/FileExplorerStore.swift
1410 Sources/CommandPalette/CommandPaletteSearch.swift
1380 cmuxUITests/MenuKeyEquivalentRoutingUITests.swift
1376 cmuxTests/KeyboardShortcutSettingsFileStoreStartupTests.swift
1372 cmuxTests/AppDelegateIssue2907RoutingTests.swift
Expand All @@ -71,6 +70,7 @@
1313 cmuxTests/MobileHostAuthorizationTests.swift
1285 cmuxUITests/SidebarHelpMenuUITests.swift
1255 Sources/Feed/FeedCoordinator.swift
1216 Packages/CmuxCommandPalette/Tests/CmuxCommandPaletteTests/CommandPaletteSearchEngineTests.swift
1205 Packages/CmuxMobileTerminal/Sources/CmuxMobileTerminal/TerminalInputTextView.swift
1197 cmuxTests/CodexAppServerSessionTests.swift
1156 cmuxTests/SidebarOrderingTests.swift
Expand All @@ -82,18 +82,19 @@
1093 cmuxUITests/BonsplitTabDragUITests.swift
1084 cmuxTests/AgentHibernationTests.swift
1084 cmuxTests/RestorableAgentSessionIndexTests.swift
1047 Packages/CmuxCommandPalette/Sources/CmuxCommandPalette/Search/CommandPaletteFuzzyMatcher.swift
1021 cmuxUITests/TerminalCmdClickUITests.swift
1006 cmuxTests/CmuxSSHURLRequestTests.swift
1000 cmuxTests/CmuxTopSnapshotScopeTests.swift
949 Sources/App/TerminalDirectoryOpenSupport.swift
947 Sources/TerminalNotificationPolicy.swift
945 Sources/SessionIndexRegisteredAgents.swift
944 Sources/App/ShortcutRoutingSupport.swift
939 Sources/App/TerminalDirectoryOpenSupport.swift
937 Sources/TextBoxMentionIndexStore.swift
924 Sources/DockPanelView.swift
913 cmuxTests/WorkspaceGroupTests.swift
905 Sources/CmuxSSHURLRequest.swift
896 Sources/CommandPalette/CommandPaletteSettingsToggle.swift
897 Sources/CommandPalette/CommandPaletteSettingsToggle.swift
878 Sources/WorkspaceContentView.swift
868 Sources/Panels/BrowserScreenshotSnapshotter.swift
864 Sources/Panels/TerminalPanel.swift
Expand Down Expand Up @@ -137,7 +138,7 @@
654 Packages/CmuxSettingsUI/Sources/CmuxSettingsUI/Sections/KeyboardShortcutsSection.swift
650 Sources/Panels/MarkdownRemoteImageLoader.swift
649 Sources/CmuxTopSnapshot.swift
640 cmuxTests/CommandPaletteNucleoFFITests.swift
641 cmuxTests/CommandPaletteNucleoFFITests.swift
630 Packages/CmuxSettings/Sources/CmuxSettings/Values/ShortcutWhenClause.swift
627 Sources/WorkspaceRemoteConfiguration.swift
621 cmuxUITests/RightSidebarChromeHeightUITests.swift
Expand All @@ -146,6 +147,7 @@
614 cmuxTests/SessionIndexViewTests.swift
613 Sources/PortScanner.swift
611 Sources/TerminalController+ControlPaneContext.swift
604 Packages/CmuxCommandPalette/Tests/CmuxCommandPaletteTests/CommandPaletteNucleoFFITests.swift
603 Sources/SettingsNavigation.swift
599 Packages/CMUXAgentLaunch/Sources/CMUXAgentLaunch/AgentLaunchSanitizerPrimaryPolicies.swift
596 cmuxTests/CmuxEventBusTests.swift
Expand Down
6 changes: 6 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -429,6 +429,11 @@ jobs:
- name: Run Swift package unit tests
run: |
set -euo pipefail
# CmuxCommandPalette's nucleo FFI tests load the Rust dylib through
# CMUX_NUCLEO_FFI_LIB (they skip when it is absent, so build it here
# to keep the FFI parity suite a real gate).
cargo build --manifest-path Native/CommandPaletteNucleoFFI/Cargo.toml --release
export CMUX_NUCLEO_FFI_LIB="$PWD/Native/CommandPaletteNucleoFFI/target/release/libcmux_command_palette_nucleo_ffi.dylib"
# The cmux-unit scheme only runs the cmuxTests app-host suite; it does
# not execute the SPM package test targets. Run them here so package
# tests (settings stores, secret-file migration, socket-control
Expand All @@ -439,6 +444,7 @@ jobs:
PACKAGES=(
CMUXAuthCore
CmuxAuthRuntime
CmuxCommandPalette
CmuxControlSocket
CmuxFileWatch
CmuxFoundation
Expand Down
35 changes: 35 additions & 0 deletions Packages/CmuxAppKitSupportUI/Package.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
// swift-tools-version: 6.0

import PackageDescription

let package = Package(
name: "CmuxAppKitSupportUI",
platforms: [
.macOS(.v14),
],
products: [
.library(
name: "CmuxAppKitSupportUI",
targets: ["CmuxAppKitSupportUI"]
),
],
targets: [
.target(
name: "CmuxAppKitSupportUI",
swiftSettings: [
.swiftLanguageMode(.v6),
.enableUpcomingFeature("ExistentialAny"),
.enableUpcomingFeature("InternalImportsByDefault"),
]
),
.testTarget(
name: "CmuxAppKitSupportUITests",
dependencies: ["CmuxAppKitSupportUI"],
swiftSettings: [
.swiftLanguageMode(.v6),
.enableUpcomingFeature("ExistentialAny"),
.enableUpcomingFeature("InternalImportsByDefault"),
]
),
]
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import AppKit
public import SwiftUI

/// A transparent overlay that intercepts only middle-mouse clicks, letting left-click
/// selection and right-click context menus hit-test through to the underlying view tree.
public struct MiddleClickCapture: NSViewRepresentable {
public let onMiddleClick: () -> Void

/// Creates a middle-click capture overlay.
/// - Parameter onMiddleClick: Invoked when a middle (button 2) click lands on the overlay.
public init(onMiddleClick: @escaping () -> Void) {
self.onMiddleClick = onMiddleClick
}

public func makeNSView(context: Context) -> MiddleClickCaptureView {
let view = MiddleClickCaptureView()
view.onMiddleClick = onMiddleClick
return view
}

public func updateNSView(_ nsView: MiddleClickCaptureView, context: Context) {
nsView.onMiddleClick = onMiddleClick
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
public import AppKit

/// Backing `NSView` for ``MiddleClickCapture`` that hit-tests only middle-clicks.
public final class MiddleClickCaptureView: NSView {
/// Invoked when a middle (button 2) mouse-down lands on this view.
public var onMiddleClick: (() -> Void)?

public override func hitTest(_ point: NSPoint) -> NSView? {
// Only intercept middle-click so left-click selection and right-click context menus
// continue to hit-test through to SwiftUI/AppKit normally.
guard let event = NSApp.currentEvent,
event.type == .otherMouseDown,
event.buttonNumber == 2 else {
return nil
}
return self
}

public override func otherMouseDown(with event: NSEvent) {
guard event.buttonNumber == 2 else {
super.otherMouseDown(with: event)
return
}
onMiddleClick?()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
public import AppKit
public import SwiftUI

/// An `NSViewRepresentable` that presents SwiftUI content in an `NSPopover` with the
/// popover arrow hidden, anchored to an invisible SwiftUI-backed view.
///
/// The popover is positioned relative to a synthetic rect inset toward the anchor so the
/// detached content sits a fixed gap from the anchoring edge while the arrow stays hidden.
public struct ArrowlessPopoverAnchor<PopoverContent: View>: NSViewRepresentable {
@Binding public var isPresented: Bool
public let preferredEdge: NSRectEdge
public let detachedGap: CGFloat
@ViewBuilder public let content: () -> PopoverContent

/// Creates an arrowless popover anchor.
/// - Parameters:
/// - isPresented: Binding driving popover presentation.
/// - preferredEdge: The edge of the anchor the popover prefers to appear from.
/// - detachedGap: The gap, in points, between the anchor edge and the popover.
/// - content: The SwiftUI content rendered inside the popover.
public init(
isPresented: Binding<Bool>,
preferredEdge: NSRectEdge,
detachedGap: CGFloat,
@ViewBuilder content: @escaping () -> PopoverContent
) {
self._isPresented = isPresented
self.preferredEdge = preferredEdge
self.detachedGap = detachedGap
self.content = content
}

public func makeNSView(context: Context) -> NSView {
let view = NSView()
context.coordinator.anchorView = view
return view
}

public func updateNSView(_ nsView: NSView, context: Context) {
context.coordinator.anchorView = nsView
context.coordinator.updateRootView(AnyView(content()))

if isPresented {
context.coordinator.present(
preferredEdge: preferredEdge,
detachedGap: detachedGap
)
} else {
context.coordinator.dismiss()
}
}

public func makeCoordinator() -> Coordinator {
Coordinator(isPresented: $isPresented)
}

/// Bridges popover lifecycle between AppKit's `NSPopover` and the SwiftUI binding.
@MainActor
public final class Coordinator: NSObject, NSPopoverDelegate {
@Binding var isPresented: Bool

weak var anchorView: NSView?
private let hostingController = NSHostingController(rootView: AnyView(EmptyView()))
private var popover: NSPopover?

init(isPresented: Binding<Bool>) {
_isPresented = isPresented
}

func updateRootView(_ rootView: AnyView) {
hostingController.rootView = AnyView(rootView.fixedSize())
hostingController.view.invalidateIntrinsicContentSize()
hostingController.view.layoutSubtreeIfNeeded()
}

func present(preferredEdge: NSRectEdge, detachedGap: CGFloat) {
guard let anchorView else {
isPresented = false
dismiss()
return
}

let popover = popover ?? makePopover()
if popover.isShown {
return
}

hostingController.view.invalidateIntrinsicContentSize()
hostingController.view.layoutSubtreeIfNeeded()
let fittingSize = hostingController.view.fittingSize
if fittingSize.width > 0, fittingSize.height > 0 {
popover.contentSize = NSSize(
width: ceil(fittingSize.width),
height: ceil(fittingSize.height)
)
}

popover.show(
relativeTo: positioningRect(
for: anchorView.bounds,
preferredEdge: preferredEdge,
detachedGap: detachedGap
),
of: anchorView,
preferredEdge: preferredEdge
)
}

func dismiss() {
popover?.performClose(nil)
popover = nil
}

public func popoverDidClose(_ notification: Notification) {
popover = nil
if isPresented {
isPresented = false
}
}

private func makePopover() -> NSPopover {
let popover = NSPopover()
popover.behavior = .semitransient
popover.animates = true
popover.setValue(true, forKeyPath: "shouldHideAnchor")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

Is shouldHideAnchora documented public API/property forNSPopover in AppKit (macOS 14 through latest)? If not, what stable/public alternative exists to hide a popover arrow?

💡 Result:

The property shouldHideAnchor is not a documented public API for NSPopover in AppKit [1][2]. While it appears in some runtime headers [3][4] and has been discussed in developer forums as a potential private or internal flag that can be accessed via Key-Value Coding (e.g., popover.setValue(true, forKeyPath: "shouldHideAnchor")) [5], using private APIs is discouraged and may lead to app rejection or unexpected behavior in future macOS versions. There is no native, stable, or public public API to remove or hide the popover arrow in AppKit [6][7]. The most reliable, non-private "stable" workaround used by developers is to exploit the system's built-in behavior: the popover automatically hides its arrow when the positioning view is moved outside the visible rect of its window [8][9]. You can achieve this by: 1. Creating a secondary, transparent positioning view or using a placeholder view. 2. Presenting the popover relative to this view [8]. 3. Immediately moving the positioning view outside the visible bounds of the screen or window (e.g., by adjusting its frame to a negative coordinate), which triggers the system to hide the anchor arrow [8][9]. Alternatively, for more robust or custom control over the popover appearance and behavior, many developers choose to implement a custom popover using an NSWindow or utilize established, well-maintained third-party libraries [6].

Citations:


Avoid KVC against NSPopover private key (shouldHideAnchor).
ArrowlessPopoverAnchor.swift line 125 uses popover.setValue(true, forKeyPath: "shouldHideAnchor"), but shouldHideAnchor is not a documented public NSPopover API; relying on this private KVC contract risks runtime failures or broken popover presentation if AppKit changes.

  • Use a non-private arrow-hiding approach instead (e.g., present relative to a temporary/transparent positioning view and move that view outside the visible window/screen bounds so AppKit hides the arrow), or switch to a custom popover implementation if you need deterministic arrow control.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@Packages/CmuxAppKitSupportUI/Sources/CmuxAppKitSupportUI/Popover/ArrowlessPopoverAnchor.swift`
at line 125, The code uses KVC on NSPopover (popover.setValue(true, forKeyPath:
"shouldHideAnchor")) which targets a private API; remove that KVC usage in
ArrowlessPopoverAnchor (or whatever type/method contains that line) and
implement a supported arrow-hiding strategy instead — for example, present the
NSPopover relative to a temporary transparent positioning view (create a
transient NSView, position it off-screen or outside the visible window bounds,
and call popover.show(relativeTo:of:preferredEdge:) using that view) or replace
the popover usage with a custom popover/NSWindow-based implementation if
deterministic arrow control is required; update any methods that reference
popover.setValue(...keyPath: "shouldHideAnchor") to use the new positioning-view
or custom popover approach.

popover.contentViewController = hostingController
popover.delegate = self
self.popover = popover
return popover
}

private func positioningRect(
for bounds: CGRect,
preferredEdge: NSRectEdge,
detachedGap: CGFloat
) -> CGRect {
let hiddenArrowInset: CGFloat = 13
let compensation = max(hiddenArrowInset - detachedGap, 0)

switch preferredEdge {
case .maxY:
return NSRect(
x: bounds.minX,
y: bounds.maxY - compensation,
width: bounds.width,
height: compensation
)
case .minY:
return NSRect(
x: bounds.minX,
y: bounds.minY,
width: bounds.width,
height: compensation
)
case .maxX:
return NSRect(
x: bounds.maxX - compensation,
y: bounds.minY,
width: compensation,
height: bounds.height
)
case .minX:
return NSRect(
x: bounds.minX,
y: bounds.minY,
width: compensation,
height: bounds.height
)
@unknown default:
return bounds
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import AppKit
public import SwiftUI

/// A `ViewModifier` that makes the enclosing `NSScrollView` fully transparent, hiding the
/// SwiftUI scroll content background and clearing the AppKit scroll-view layer chain.
public struct ClearScrollBackground: ViewModifier {
/// Creates the clear-scroll-background modifier.
public init() {}

public func body(content: Content) -> some View {
if #available(macOS 13.0, *) {
content
.scrollContentBackground(.hidden)
.background(ScrollBackgroundClearer())
} else {
content
.background(ScrollBackgroundClearer())
}
}
}
Loading
Loading