Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
34 changes: 34 additions & 0 deletions Resources/Localizable.xcstrings
Original file line number Diff line number Diff line change
Expand Up @@ -100537,6 +100537,40 @@
}
}
},
"featureFlags.appKitSidebarList.description": {
"extractionState": "manual",
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "Renders the workspace sidebar with a native AppKit list and divider for smoother scrolling and resizing with many workspaces."
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "ワークスペースサイドバーをネイティブの AppKit リストとディバイダーで描画し、多数のワークスペースでもスクロールとリサイズを滑らかにします。"
}
}
}
},
"featureFlags.appKitSidebarList.title": {
"extractionState": "manual",
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "Lawrence Sidebar"
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "Lawrence サイドバー"
}
}
}
},
"featureFlags.clearAll": {
"extractionState": "manual",
"localizations": {
Expand Down
1 change: 0 additions & 1 deletion Sources/App/WorkspaceRuntimeSettings.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ enum WorkspaceTitlebarSettings {
}
enum WorkspacePresentationModeSettings {
static let modeKey = "workspacePresentationMode"

enum Mode: String {
case standard
case minimal
Expand Down
678 changes: 589 additions & 89 deletions Sources/ContentView.swift

Large diffs are not rendered by default.

13 changes: 5 additions & 8 deletions Sources/Debug/SidebarLazyContractProbe.swift
Original file line number Diff line number Diff line change
@@ -1,21 +1,18 @@
import SwiftUI

#if DEBUG
/// Test-only body-evaluation probe for the workspace sidebar's lazy-layout
/// contract: sidebar layout/diff work must stay O(visible rows), never
/// O(all workspaces). The contract has regressed five times through five
/// different mechanisms (#2586, #5764, #5845, #6210, #6556), each shipping to
/// stable before being detected at scale. `SidebarLazyLayoutScaleTests` mounts
/// the sidebar with hundreds of workspaces, injects these closures, and fails
/// if row bodies are realized without bound or keep re-evaluating after
/// updates settle — regardless of which mechanism defeats laziness next.
/// Test-only probe for the workspace sidebar virtualization contract: AppKit
/// must materialize viewport-many cells and reconfigure only changed hosted
/// roots, never all workspaces. `SidebarLazyLayoutScaleTests` mounts hundreds
/// of workspaces and fails on realization or reconfiguration churn.
///
/// Same pattern as `MinimalModeInvalidationProbe`; compiled out of Release.
struct SidebarLazyContractProbe {
var workspaceRowBody: (() -> Void)?
var workspaceRowBodyEnd: (() -> Void)?
var groupHeaderRowBody: (() -> Void)?
var workspaceSnapshotBuild: (() -> Void)?
var tableRootViewReconfigure: (() -> Void)?
var workspaceRowInputProjection: (() -> Void)?
}
#endif
23 changes: 23 additions & 0 deletions Sources/FeatureFlags.swift
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ final class CmuxFeatureFlags {
private static let agentChatUIDefault = false
private static let sidebarWorkspaceAgentSpinnerDefault = false
private static let workspaceTodoControlsDefault = false
private static let appKitSidebarListDefault = false

private static let overrideKeyPrefix = "cmux.flags.override."

Expand Down Expand Up @@ -151,6 +152,24 @@ final class CmuxFeatureFlags {
),
defaultWhenUnavailable: CmuxFeatureFlags.workspaceTodoControlsDefault
),

// FLAG(key: sidebar-appkit-list-experiment, owner: lawrencecchen,
// reviewBy: 2026-10-01, defaultWhenUnavailable: false)
// Renders the workspace sidebar with the AppKit NSTableView list
// (virtualized rows, measured-once heights) instead of the SwiftUI
// LazyVStack. Off by default while the rewrite soaks.
CmuxFeatureFlagDefinition(
key: "sidebar-appkit-list-experiment",
title: String(
localized: "featureFlags.appKitSidebarList.title",
defaultValue: "Lawrence Sidebar"
),
flagDescription: String(
localized: "featureFlags.appKitSidebarList.description",
defaultValue: "Renders the workspace sidebar with a native AppKit list and divider for smoother scrolling and resizing with many workspaces."
),
defaultWhenUnavailable: CmuxFeatureFlags.appKitSidebarListDefault
),
]
}()

Expand Down Expand Up @@ -178,6 +197,10 @@ final class CmuxFeatureFlags {
effectiveValue(for: Self.allFlags[5])
}

var isAppKitSidebarListEnabled: Bool {
effectiveValue(for: Self.allFlags[6])
}

@ObservationIgnored
private let defaults: UserDefaults
@ObservationIgnored
Expand Down
13 changes: 11 additions & 2 deletions Sources/GhosttyTerminalView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9113,13 +9113,22 @@ final class GhosttySurfaceScrollView: NSView {
}

private func logLayoutDuringActiveDrag(targetSize: CGSize) {
// Cheap app-owned gates first. The NSPasteboard(name: .drag) read
// below is a synchronous XPC round trip to the pasteboard daemon;
// unguarded it ran on every pane layout of every width commit during
// sidebar divider drags (5% of main-thread time in a Time Profiler
// capture) because divider drags also deliver drag mouse events.
let dropZoneActive = activeDropZone != nil || pendingDropZone != nil
let appOwnedDragActive =
AppDelegate.shared?.sidebarWorkspaceDragRegistry.currentWorkspaceId != nil
guard dropZoneActive || appOwnedDragActive else { return }

let pasteboardTypes = NSPasteboard(name: .drag).types
let hasTabDrag = pasteboardTypes?.contains(Self.tabTransferPasteboardType) == true
let hasSidebarDrag = pasteboardTypes?.contains(Self.sidebarTabReorderPasteboardType) == true
let eventType = NSApp.currentEvent?.type
let hasActiveDrag =
activeDropZone != nil ||
pendingDropZone != nil ||
dropZoneActive ||
((hasTabDrag || hasSidebarDrag) && Self.isDragMouseEvent(eventType))
guard hasActiveDrag else { return }

Expand Down
58 changes: 58 additions & 0 deletions Sources/Sidebar/AppKitList/Cells/SidebarGroupHeaderRowModel.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import CoreGraphics
import Foundation

/// Immutable render input for one pure-AppKit sidebar group header row.
///
/// Value fields only: action closures live in ``SidebarGroupHeaderRowActions``
/// and are excluded from equality so recycled cells can reconfigure cheaply
/// (same discipline as the hosted rows' Equatable snapshot contract).
struct SidebarGroupHeaderRowModel: Equatable {
let groupId: UUID
let anchorWorkspaceId: UUID
let name: String
let iconSymbol: String
let tintHex: String?
let isCollapsed: Bool
let isPinned: Bool
let isAnchorActive: Bool
let memberCount: Int
let anchorUnreadCount: Int
let canMarkRead: Bool
let canMarkUnread: Bool
let hasLatestNotifications: Bool
let canMarkAllRead: Bool
let canMarkAllUnread: Bool
/// Resolved modifier-hold hint (for example "⌘3"); nil hides the pill.
let shortcutHintText: String?
let shortcutHintXOffset: Double
let shortcutHintYOffset: Double
let fontScale: CGFloat
let globalFontMagnificationPercent: Int
let cwdContextMenuItems: [CmuxResolvedConfigContextMenuItem]
let rowSpacing: CGFloat
let isFirstRow: Bool
let isBeingDragged: Bool
let topDropIndicatorVisible: Bool
let bottomDropIndicatorVisible: Bool
}

/// Behavior bundle for one group header row; recreated per apply and excluded
/// from model equality.
@MainActor
struct SidebarGroupHeaderRowActions {
let onToggleCollapsed: () -> Void
let onFocusAnchor: () -> Void
let onTapPlus: () -> Void
let onRunResolvedItem: (CmuxResolvedConfigMenuAction) -> Void
let onRename: () -> Void
let onTogglePinned: () -> Void
let onMarkRead: () -> Void
let onMarkUnread: () -> Void
let onClearLatestNotifications: () -> Void
let onMarkAllRead: () -> Void
let onMarkAllUnread: () -> Void
let onUngroup: () -> Void
let onDelete: () -> Void
let onEditConfig: () -> Void
let onOpenDocs: () -> Void
}
Loading