Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
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
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ public enum CMUXSidebarAction: Codable, Equatable, Sendable {
case closeWorkspace(UUID)
case selectNextWorkspace
case selectPreviousWorkspace
case createTerminalSurface(workspaceID: UUID?, initialInput: String?)
case createTerminalSurface(workspaceID: UUID?)
case createBrowserSurface(workspaceID: UUID?, url: String?)
case selectSurface(workspaceID: UUID, surfaceID: UUID)
case selectNextSurface
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,11 +49,12 @@ public struct CmuxSidebarHost {
await perform(.openURL(url.absoluteString))
}

public func createTerminalSurface(
in workspaceID: UUID? = nil,
initialInput: String? = nil
) async -> CMUXExtensionActionResult {
await perform(.createTerminalSurface(workspaceID: workspaceID, initialInput: initialInput))
/// Requests that CMUX create a terminal surface.
///
/// Extensions can ask CMUX to create the surface, but cannot seed shell
/// input. This keeps `.createSurface` separate from command execution.
public func createTerminalSurface(in workspaceID: UUID? = nil) async -> CMUXExtensionActionResult {
await perform(.createTerminalSurface(workspaceID: workspaceID))
}

public func createBrowserSurface(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -254,15 +254,21 @@ struct CMUXExtensionKitTests {
host.refresh()
let selectResult = await host.selectWorkspace(workspaceID)
let closeResult = await host.closeWorkspace(workspaceID)
let terminalResult = await host.createTerminalSurface(in: workspaceID)
let browserResult = await host.createBrowserSurface(in: workspaceID, url: url)
let openResult = await host.openURL(url)

#expect(refreshCount == 1)
#expect(selectResult.accepted)
#expect(closeResult.accepted)
#expect(terminalResult.accepted)
#expect(browserResult.accepted)
#expect(openResult.accepted)
#expect(actions == [
.selectWorkspace(workspaceID),
.closeWorkspace(workspaceID),
.createTerminalSurface(workspaceID: workspaceID),
.createBrowserSurface(workspaceID: workspaceID, url: "https://example.com/pr/1"),
.openURL("https://example.com/pr/1"),
])
}
Expand Down
68 changes: 49 additions & 19 deletions Sources/ContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3191,6 +3191,12 @@ struct ContentView: View {
updateSidebarResizerBandState()
})

view = AnyView(view.onChange(of: titlebarControlsStyleRawValue) { _ in
clampSidebarWidthIfNeeded()
updateSidebarResizerBandState()
syncTitlebarControlsSidebarTrailingEdge()
})

view = AnyView(view.onChange(of: sidebarState.isVisible) { isVisible in
setMinimalModeSidebarTitlebarControlsAvailable(isVisible, in: observedWindow)
if let observedWindow {
Expand Down Expand Up @@ -10983,10 +10989,10 @@ struct VerticalTabsSidebar: View {
}
}

private func handleCMUXSidebarExtensionAction(
_ action: CMUXSidebarAction
) -> CMUXExtensionActionResult {
switch action {
private func handleCMUXSidebarExtensionAction(
_ action: CMUXSidebarAction
) -> CMUXExtensionActionResult {
switch action {
case .createWorkspace(let title, let workingDirectory, let select):
let workspace = tabManager.addWorkspace(
title: title,
Expand All @@ -10996,9 +11002,9 @@ struct VerticalTabsSidebar: View {
)
return CMUXExtensionActionResult(accepted: true, message: workspace.id.uuidString)

case .selectWorkspace(let workspaceId):
guard let workspace = tabManager.tabs.first(where: { $0.id == workspaceId }) else {
return CMUXExtensionActionResult(
case .selectWorkspace(let workspaceId):
guard let workspace = tabManager.tabs.first(where: { $0.id == workspaceId }) else {
return CMUXExtensionActionResult(
accepted: false,
message: String(localized: "sidebar.extensions.action.workspaceNotFound", defaultValue: "Workspace not found")
)
Expand All @@ -11012,8 +11018,8 @@ struct VerticalTabsSidebar: View {
accepted: false,
message: String(localized: "sidebar.extensions.action.closeRejected", defaultValue: "Workspace could not be closed")
)
}
return .accepted
}
return .accepted

case .selectNextWorkspace:
tabManager.selectNextTab()
Expand All @@ -11023,14 +11029,14 @@ struct VerticalTabsSidebar: View {
tabManager.selectPreviousTab()
return .accepted

case .createTerminalSurface(let workspaceId, let initialInput):
case .createTerminalSurface(let workspaceId):
guard let workspace = workspaceId.flatMap({ id in tabManager.tabs.first(where: { $0.id == id }) }) ?? tabManager.selectedWorkspace else {
return .rejected(String(localized: "sidebar.extensions.action.workspaceNotFound", defaultValue: "Workspace not found"))
}
if tabManager.selectedTabId != workspace.id {
tabManager.selectWorkspace(workspace)
}
let panel = workspace.newTerminalSurfaceInFocusedPane(focus: true, initialInput: initialInput)
let panel = workspace.newTerminalSurfaceInFocusedPane(focus: true, initialInput: nil)
return panel.map { CMUXExtensionActionResult(accepted: true, message: $0.id.uuidString) }
?? .rejected(String(localized: "sidebar.extensions.action.surfaceCreateRejected", defaultValue: "Surface could not be created"))

Expand All @@ -11041,8 +11047,11 @@ struct VerticalTabsSidebar: View {
if tabManager.selectedTabId != workspace.id {
tabManager.selectWorkspace(workspace)
}
let url = urlString.flatMap(URL.init(string:))
let panelId = tabManager.createBrowserSplit(direction: .right, url: url)
let validatedURL = cmuxSidebarExtensionOptionalHTTPURL(from: urlString)
guard validatedURL.accepted else {
return .rejected(String(localized: "sidebar.extensions.action.urlRejected", defaultValue: "URL could not be opened"))
}
let panelId = tabManager.createBrowserSplit(direction: .right, url: validatedURL.url)
return panelId.map { CMUXExtensionActionResult(accepted: true, message: $0.uuidString) }
?? .rejected(String(localized: "sidebar.extensions.action.surfaceCreateRejected", defaultValue: "Surface could not be created"))

Expand Down Expand Up @@ -11085,7 +11094,11 @@ struct VerticalTabsSidebar: View {
}
tabManager.selectWorkspace(tab)
tab.focusPanel(surfaceId)
let panelId = tabManager.createBrowserSplit(direction: splitDirection, url: urlString.flatMap(URL.init(string:)))
let validatedURL = cmuxSidebarExtensionOptionalHTTPURL(from: urlString)
guard validatedURL.accepted else {
return .rejected(String(localized: "sidebar.extensions.action.urlRejected", defaultValue: "URL could not be opened"))
}
let panelId = tabManager.createBrowserSplit(direction: splitDirection, url: validatedURL.url)
return panelId.map { CMUXExtensionActionResult(accepted: true, message: $0.uuidString) }
?? .rejected(String(localized: "sidebar.extensions.action.surfaceCreateRejected", defaultValue: "Surface could not be created"))

Expand All @@ -11096,18 +11109,35 @@ struct VerticalTabsSidebar: View {
return .accepted

case .openURL(let urlString):
guard let url = URL(string: urlString),
let scheme = url.scheme?.lowercased(),
["https", "http"].contains(scheme),
guard let url = cmuxSidebarExtensionRequiredHTTPURL(from: urlString),
NSWorkspace.shared.open(url) else {
return CMUXExtensionActionResult(
accepted: false,
message: String(localized: "sidebar.extensions.action.urlRejected", defaultValue: "URL could not be opened")
)
}
return .accepted
}
}
}
}

private func cmuxSidebarExtensionOptionalHTTPURL(from urlString: String?) -> (url: URL?, accepted: Bool) {
guard let urlString, !urlString.isEmpty else {
return (nil, true)
}
guard let url = cmuxSidebarExtensionRequiredHTTPURL(from: urlString) else {
return (nil, false)
}
return (url, true)
}

private func cmuxSidebarExtensionRequiredHTTPURL(from urlString: String) -> URL? {
guard let url = URL(string: urlString),
let scheme = url.scheme?.lowercased(),
scheme == "http" || scheme == "https" else {
return nil
}
return url
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

private func splitDirection(from direction: CMUXSplitDirection) -> SplitDirection? {
switch direction {
Expand Down
4 changes: 2 additions & 2 deletions Sources/SessionPersistence.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ enum SessionSnapshotSchema {
enum SessionPersistencePolicy {
static let sidebarMinimumWidthKey = "sidebarMinimumWidth"
static let defaultSidebarWidth: Double = 220
static let defaultMinimumSidebarWidth: Double = 190
static let minimumSidebarWidth: Double = 190
static let defaultMinimumSidebarWidth: Double = 171
static let minimumSidebarWidth: Double = 171
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 | ⚡ Quick win

Minimum width constant does not match the stated product target.

Lines 18–19 set the minimum to 171, but this PR’s objective says the default minimum should be reduced to 180 (from 190). This changes behavior more than intended and will clamp narrower than specified.

Proposed fix
-    static let defaultMinimumSidebarWidth: Double = 171
-    static let minimumSidebarWidth: Double = 171
+    static let defaultMinimumSidebarWidth: Double = 180
+    static let minimumSidebarWidth: Double = 180
🤖 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 `@Sources/SessionPersistence.swift` around lines 18 - 19, The constants
defaultMinimumSidebarWidth and minimumSidebarWidth in SessionPersistence.swift
are currently set to 171 but should reflect the PR objective of a 180px minimum;
update both static let defaultMinimumSidebarWidth and static let
minimumSidebarWidth to 180 so the default and enforced minimum match the
intended 180 value.

static let sidebarMinimumWidthRange: ClosedRange<Double> = 120...260
static let maximumSidebarWidth: Double = 600
static let minimumWindowWidth: Double = 300
Expand Down
2 changes: 1 addition & 1 deletion Sources/Update/MinimalModeSidebarControls.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ struct MinimalModeSidebarControlActionProxyView: NSViewRepresentable {
}

enum TitlebarControlsHitRegions {
static let outerLeadingPadding: CGFloat = 4
static let outerLeadingPadding: CGFloat = 0
static let buttonCount = MinimalModeSidebarControlActionSlot.allCases.count

static func buttonXRanges(config: TitlebarControlsStyleConfig) -> [ClosedRange<CGFloat>] {
Expand Down
21 changes: 13 additions & 8 deletions Sources/Update/UpdateTitlebarAccessory.swift
Original file line number Diff line number Diff line change
Expand Up @@ -453,8 +453,8 @@ enum TitlebarControlsLayoutMetrics {
static let hintRightSafetyShift: CGFloat = 6
static let hintTrailingBaseInset: CGFloat = 4
static let extraButtonCount = 0
static let trafficLightGap: CGFloat = 2
static let trafficLightClusterWidth: CGFloat = 48
static let trafficLightGap: CGFloat = 0
static let trafficLightClusterWidth: CGFloat = 45

static func hintTrailingInset(titlebarShortcutHintXOffset: Double = ShortcutHintDebugSettings.defaultTitlebarHintX) -> CGFloat {
max(0, ShortcutHintDebugSettings.clamped(titlebarShortcutHintXOffset))
Expand All @@ -468,15 +468,19 @@ enum TitlebarControlsLayoutMetrics {
return (buttonCount * config.buttonSize) + (gapCount * config.spacing)
}

static func visibleContentWidth(config: TitlebarControlsStyleConfig) -> CGFloat {
outerLeadingPadding
+ config.groupPadding.leading
+ buttonRowWidth(config: config)
+ config.groupPadding.trailing
}

static func contentSize(
config: TitlebarControlsStyleConfig,
titlebarShortcutHintXOffset: Double = ShortcutHintDebugSettings.defaultTitlebarHintX
) -> NSSize {
NSSize(
width: outerLeadingPadding
+ config.groupPadding.leading
+ buttonRowWidth(config: config)
+ config.groupPadding.trailing
width: visibleContentWidth(config: config)
+ hintTrailingInset(titlebarShortcutHintXOffset: titlebarShortcutHintXOffset),
height: max(
WindowChromeMetrics.appTitlebarHeight,
Expand Down Expand Up @@ -514,7 +518,7 @@ enum TitlebarControlsLayoutMetrics {
static func minimumSidebarWidth(config: TitlebarControlsStyleConfig) -> CGFloat {
trafficLightClusterWidth
+ trafficLightGap
+ contentSize(config: config).width
+ visibleContentWidth(config: config)
+ sidebarTrailingPadding
}

Expand Down Expand Up @@ -2010,6 +2014,7 @@ final class TitlebarControlsAccessoryViewController: NSTitlebarAccessoryViewCont
let styleRawValue = UserDefaults.standard.integer(forKey: "titlebarControlsStyle")
let style = TitlebarControlsStyle(rawValue: styleRawValue) ?? .classic
let contentSize = TitlebarControlsLayoutMetrics.contentSize(config: style.config)
let visibleContentWidth = TitlebarControlsLayoutMetrics.visibleContentWidth(config: style.config)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 visibleContentWidth is computed twice per updateSize() call — once inside contentSize(config:) (which calls visibleContentWidth internally) and once again as a separate local. Since this is not a hot path the impact is negligible, but the redundancy is easy to eliminate by computing visibleContentWidth first and threading it into contentSize.

Suggested change
let contentSize = TitlebarControlsLayoutMetrics.contentSize(config: style.config)
let visibleContentWidth = TitlebarControlsLayoutMetrics.visibleContentWidth(config: style.config)
let visibleContentWidth = TitlebarControlsLayoutMetrics.visibleContentWidth(config: style.config)
let contentSize = TitlebarControlsLayoutMetrics.contentSize(config: style.config)

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

if intrinsicSizeNeedsRefresh {
hostingView.invalidateIntrinsicContentSize()
intrinsicSizeNeedsRefresh = false
Expand Down Expand Up @@ -2038,7 +2043,7 @@ final class TitlebarControlsAccessoryViewController: NSTitlebarAccessoryViewCont
let accessoryOriginXInWindow = view.convert(view.bounds, to: nil).minX
let sidebarTrailingEdgeInAccessory = max(0, sidebarTrailingEdge - accessoryOriginXInWindow)
let xOffset = TitlebarControlsLayoutMetrics.leadingOffset(
contentWidth: contentSize.width,
contentWidth: visibleContentWidth,
trafficLightFrame: trafficLightFrame,
debugSnapshot: debugSnapshot,
sidebarTrailingEdge: sidebarTrailingEdgeInAccessory
Expand Down
6 changes: 3 additions & 3 deletions cmuxTests/SidebarWidthPolicyTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,19 +16,19 @@ final class SidebarWidthPolicyTests: XCTestCase {

XCTAssertEqual(
SessionPersistencePolicy.defaultMinimumSidebarWidth,
190,
171,
accuracy: 0.001
)
XCTAssertEqual(
SessionPersistencePolicy.resolvedMinimumSidebarWidth(defaults: defaults),
190,
171,
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major | 🏗️ Heavy lift

Convert touched XCTest tests to Swift Testing per repo policy.

This file is modified but still uses XCTestCase. The repo rule requires in-place migration when touching existing XCTest tests (unless UI tests).

As per coding guidelines: “When touching an existing XCTest test, convert in place: XCTestCase subclass becomes @Suite struct; func testFoo() becomes @Test func foo(); XCTAssert... becomes #expect(...) / try #require(...).”

Also applies to: 31-31

🤖 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 `@cmuxTests/SidebarWidthPolicyTests.swift` around lines 19 - 24, The tests in
SidebarWidthPolicyTests.swift still use XCTestCase and XCTAssert-style APIs;
convert the XCTestCase subclass (SidebarWidthPolicyTests) into an in-place
`@Suite` struct, change each func test...() into `@Test` func <name>() and replace
XCTAssertEqual / other XCTest assertions with Swift Testing expectations (e.g.
XCTAssertEqual(...) -> `#expect`(actual).toEqual(expected) or try `#require`(...)
where appropriate); update imports if needed (remove XCTest, add import
SwiftTesting) and ensure any setup/teardown methods are converted to
`@Before/`@After or equivalent Swift Testing hooks.

accuracy: 0.001
)
}

func testContentViewClampKeepsMinimumSidebarWidth() {
XCTAssertEqual(
ContentView.clampedSidebarWidth(184, maximumWidth: 600),
ContentView.clampedSidebarWidth(164, maximumWidth: 600),
CGFloat(SessionPersistencePolicy.minimumSidebarWidth),
accuracy: 0.001
)
Expand Down
2 changes: 1 addition & 1 deletion cmuxTests/UpdatePillReleaseVisibilityTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -230,7 +230,7 @@ final class TitlebarControlsSizingPolicyTests: XCTestCase {
func testTitlebarControlsMinimumSidebarWidthKeepsButtonsClearOfTrafficLights() {
XCTAssertEqual(
TitlebarControlsLayoutMetrics.minimumSidebarWidth(config: TitlebarControlsStyle.classic.config),
190,
171,
accuracy: 0.001
)
}
Expand Down
Loading