Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
125 commits
Select commit Hold shift + click to select a range
2aa7e74
Add focus history
lawrencecchen May 14, 2026
331eefd
Extract workspace attention flash ring view
lawrencecchen May 14, 2026
e9b6ada
Polish titlebar control interactions
lawrencecchen May 15, 2026
c8f8761
Wire focus history titlebar controls
lawrencecchen May 15, 2026
5c47853
Refresh focus history menu state
lawrencecchen May 15, 2026
506b10b
Disable unavailable titlebar focus history buttons
lawrencecchen May 15, 2026
bde6077
Refresh focus history after index updates
lawrencecchen May 15, 2026
bedd785
Tighten focus history edge cases
lawrencecchen May 15, 2026
e7851b7
Polish titlebar control chrome
lawrencecchen May 16, 2026
6615de8
Cover minimal titlebar control hit isolation
lawrencecchen May 16, 2026
5134f63
Fix minimal titlebar control chrome
lawrencecchen May 16, 2026
db54cce
Raise sidebar minimum width
lawrencecchen May 16, 2026
b264706
Fix notification popover titlebar anchor
lawrencecchen May 16, 2026
4f8fac7
Add focus history context menu
lawrencecchen May 16, 2026
0b9bb4d
Remove focus history context menu icons
lawrencecchen May 16, 2026
d5bca64
Shorten titlebar notification bell
lawrencecchen May 16, 2026
d200c1a
Revert "Shorten titlebar notification bell"
lawrencecchen May 16, 2026
162fbd2
Split focus history menus by direction
lawrencecchen May 16, 2026
97c608a
Merge remote-tracking branch 'origin/main' into feature-focus-workspa…
lawrencecchen May 17, 2026
d475e80
Extract focus history value types
lawrencecchen May 17, 2026
fd3dfe7
Remove unused focus history suppression state
lawrencecchen May 17, 2026
5e08323
Keep notification popover anchors in window
lawrencecchen May 17, 2026
791eb8d
Fix focus history removal edge cases
lawrencecchen May 17, 2026
bb159ce
Show focus history shortcuts in titlebar hints
lawrencecchen May 18, 2026
3b399d6
Add workspace and window history
lawrencecchen May 14, 2026
ef1fbd8
Fix workspace history restore focus
lawrencecchen May 15, 2026
14eedb2
Preserve closed window fallback history
lawrencecchen May 15, 2026
d2d7c4c
Preserve focus state on failed closed-panel restore
lawrencecchen May 17, 2026
9b81380
Fix closed item history regressions
lawrencecchen May 17, 2026
a50a7d5
Fix closed item history restoration anchors
lawrencecchen May 17, 2026
df56eb2
Add recently closed history list
lawrencecchen May 18, 2026
a5dbeb5
Register recently closed history panel
lawrencecchen May 18, 2026
185e0e0
Avoid deprecated recently closed activation
lawrencecchen May 18, 2026
ed8c692
Add searchable history pane
lawrencecchen May 18, 2026
b1ac783
Preserve skipped recently closed history entries
lawrencecchen May 18, 2026
c341e5b
Tighten recently closed restore paths
lawrencecchen May 18, 2026
dcbafef
Suppress recently closed cleanup records
lawrencecchen May 18, 2026
a5e9cd1
Keep skipped recently closed rows remappable
lawrencecchen May 18, 2026
b3f81e8
Show recent history previews inline
lawrencecchen May 18, 2026
7490126
Merge remote-tracking branch 'origin/main' into feature-focus-workspa…
lawrencecchen May 18, 2026
576755e
Merge branch 'feature-focus-workspace-history' into feature-workspace…
lawrencecchen May 18, 2026
2b07bef
Stabilize titlebar control positioning
lawrencecchen May 18, 2026
53d7ccd
Merge branch 'feature-focus-workspace-history' into feature-workspace…
lawrencecchen May 18, 2026
22702b7
Clarify titlebar sizing state
lawrencecchen May 18, 2026
151f2b2
Merge branch 'feature-focus-workspace-history' into feature-workspace…
lawrencecchen May 18, 2026
abb9e52
Align titlebar controls to traffic light bounds
lawrencecchen May 18, 2026
1525af2
Merge branch 'feature-focus-workspace-history' into feature-workspace…
lawrencecchen May 18, 2026
f78a7d3
Stabilize titlebar geometry updates
lawrencecchen May 18, 2026
c03425f
Merge branch 'feature-focus-workspace-history' into feature-workspace…
lawrencecchen May 18, 2026
b5fc0ed
Localize titlebar layout debug controls
lawrencecchen May 18, 2026
f61439e
Merge branch 'feature-focus-workspace-history' into feature-workspace…
lawrencecchen May 18, 2026
48a1927
Stop repositioning native titlebar buttons
lawrencecchen May 18, 2026
5c2092f
Merge remote-tracking branch 'origin/feature-focus-workspace-history'…
lawrencecchen May 18, 2026
40e2583
Fix plus menu icons and sidebar minimum default
lawrencecchen May 19, 2026
624f058
Merge branch 'feature-focus-workspace-history' into feature-workspace…
lawrencecchen May 19, 2026
d421f54
Merge remote-tracking branch 'origin/main' into feature-focus-workspa…
lawrencecchen May 19, 2026
5e788cf
Merge branch 'feature-focus-workspace-history' into feature-workspace…
lawrencecchen May 19, 2026
d75d89e
Merge remote-tracking branch 'origin/main' into feature-focus-workspa…
lawrencecchen May 19, 2026
04c0a27
Merge branch 'feature-focus-workspace-history' into feature-workspace…
lawrencecchen May 19, 2026
7bbc915
Fix focus history invalidation and header icon chrome
lawrencecchen May 19, 2026
c2dd289
Merge branch 'feature-focus-workspace-history' into feature-workspace…
lawrencecchen May 19, 2026
c4a9c3a
Register titlebar layout debug window
lawrencecchen May 19, 2026
f5c92b5
Merge branch 'feature-focus-workspace-history' into feature-workspace…
lawrencecchen May 19, 2026
bbd3883
Reject empty workspace history restores
lawrencecchen May 19, 2026
b8c99f6
Move titlebar shortcut hints below controls
lawrencecchen May 19, 2026
d4ef96c
Merge branch 'feature-focus-workspace-history' into feature-workspace…
lawrencecchen May 19, 2026
11fd84d
Fix focus review followups
lawrencecchen May 19, 2026
15ced20
Merge remote-tracking branch 'origin/feature-focus-workspace-history'…
lawrencecchen May 19, 2026
5d25179
Record context tab closes in history
lawrencecchen May 19, 2026
6b4e145
Record drawn titlebar geometry
lawrencecchen May 19, 2026
e87371d
Merge remote-tracking branch 'origin/feature-focus-workspace-history'…
lawrencecchen May 19, 2026
c93e6d7
Record minimal mode traffic lights
lawrencecchen May 19, 2026
256dde4
Merge remote-tracking branch 'origin/feature-focus-workspace-history'…
lawrencecchen May 19, 2026
bcc5460
Keep titlebar shortcut hints off controls
lawrencecchen May 19, 2026
8a66b2d
Merge remote-tracking branch 'origin/feature-focus-workspace-history'…
lawrencecchen May 19, 2026
1e35b11
Match titlebar chrome icon sizing
lawrencecchen May 19, 2026
75c2dc7
Merge remote-tracking branch 'origin/feature-focus-workspace-history'…
lawrencecchen May 19, 2026
4e2e19c
Align right sidebar header controls
lawrencecchen May 19, 2026
9397566
Merge remote-tracking branch 'origin/feature-focus-workspace-history'…
lawrencecchen May 19, 2026
17f7089
Tighten titlebar shortcut hint placement
lawrencecchen May 19, 2026
5fb4fc8
Merge remote-tracking branch 'origin/feature-focus-workspace-history'…
lawrencecchen May 19, 2026
918e8e6
Center right sidebar header controls
lawrencecchen May 19, 2026
3ae62bf
Merge remote-tracking branch 'origin/feature-focus-workspace-history'…
lawrencecchen May 19, 2026
283cc81
Match header icon button metrics
lawrencecchen May 19, 2026
362afee
Merge remote-tracking branch 'origin/feature-focus-workspace-history'…
lawrencecchen May 19, 2026
e5b8713
Record header icon geometry
lawrencecchen May 19, 2026
42c022c
Merge remote-tracking branch 'origin/feature-focus-workspace-history'…
lawrencecchen May 19, 2026
c6e928c
Tune right header icons and titlebar hints
lawrencecchen May 19, 2026
05d6f3a
Merge remote-tracking branch 'origin/feature-focus-workspace-history'…
lawrencecchen May 19, 2026
51a4c3f
Use compact right sidebar header icons
lawrencecchen May 19, 2026
86b39a9
Align right sidebar chrome button styling
lawrencecchen May 19, 2026
17441f6
Record right sidebar mode icon geometry
lawrencecchen May 20, 2026
cadc726
Restore minimal titlebar button hover
lawrencecchen May 20, 2026
624685f
Merge origin/main into feature-workspace-window-history
lawrencecchen May 20, 2026
8baf43a
Resolve history review feedback
lawrencecchen May 21, 2026
5814b19
Fix right sidebar test harness
lawrencecchen May 21, 2026
74d0b84
Resolve remaining history review comments
lawrencecchen May 21, 2026
0206a62
Fix history test compile error
lawrencecchen May 21, 2026
b199c24
Merge remote-tracking branch 'origin/main' into feature-workspace-win…
lawrencecchen May 21, 2026
9e4dec9
Fix recently closed history edge cases
lawrencecchen May 21, 2026
0c8ec40
Fix history shortcut hints and drag handling
lawrencecchen May 21, 2026
6c0ebd8
Fix history menu title fallbacks
lawrencecchen May 21, 2026
49d9052
Fix recently closed recency ordering
lawrencecchen May 21, 2026
1ef92d2
Fix stale recently closed history paths
lawrencecchen May 21, 2026
fc26673
Fix history sidebar focus and palette entry
lawrencecchen May 21, 2026
3b1ed70
Preserve pending close history
lawrencecchen May 21, 2026
d323e1c
Localize titlebar layout debug button
lawrencecchen May 21, 2026
e371809
ci: allow longer unit test run
lawrencecchen May 21, 2026
129f423
Merge remote-tracking branch 'origin/main' into feature-workspace-win…
lawrencecchen May 21, 2026
5f22153
Merge remote-tracking branch 'origin/main' into feature-workspace-win…
lawrencecchen May 22, 2026
f9bdb77
Tighten history restore closeout
lawrencecchen May 22, 2026
d939746
Merge remote-tracking branch 'origin/main' into feature-workspace-win…
lawrencecchen May 22, 2026
641c2f7
Cover explicit last-panel close history
lawrencecchen May 22, 2026
7d02e44
Consume unavailable focus history shortcuts
lawrencecchen May 22, 2026
35b1a84
Beep on unavailable focus history shortcuts
lawrencecchen May 22, 2026
05a4804
Skip failed closed history records per restore
lawrencecchen May 22, 2026
e20b9a1
Merge remote-tracking branch 'origin/main' into feature-workspace-win…
lawrencecchen May 22, 2026
95d8866
Merge remote-tracking branch 'origin/main' into feature-workspace-win…
lawrencecchen May 23, 2026
d59e754
fix: preserve empty session windows in autosave
lawrencecchen May 23, 2026
86dffe1
fix: remap closed workspace windows after restore
lawrencecchen May 23, 2026
9a28543
Merge remote-tracking branch 'origin/main' into feature-workspace-win…
lawrencecchen May 23, 2026
ae78f6d
fix: wire minimal sidebar focus history after merge
lawrencecchen May 23, 2026
fa91aaa
fix: sync extension sidebar titlebar debug insets
lawrencecchen May 23, 2026
ae8ffa5
Merge remote-tracking branch 'origin/main' into feature-workspace-win…
lawrencecchen May 23, 2026
e6dce82
Merge remote-tracking branch 'origin/main' into feature-workspace-win…
lawrencecchen May 26, 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
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@ jobs:

tests:
runs-on: warp-macos-15-arm64-6x
timeout-minutes: 45
timeout-minutes: 75
env:
CMUX_SKIP_ZIG_BUILD: "1"
# XCTest app-host crashes can leave xcodebuild waiting in Swift's crash
Expand Down
105 changes: 97 additions & 8 deletions Resources/Localizable.xcstrings

Large diffs are not rendered by default.

171 changes: 171 additions & 0 deletions Sources/AppDelegate+ClosedItemHistory.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
import AppKit

extension AppDelegate {
func clearRecentlyClosedHistory(preferredTabManager: TabManager? = nil) {
ClosedItemHistoryStore.shared.removeAll()

var clearedManagers: Set<ObjectIdentifier> = []
func clear(_ manager: TabManager?) {
guard let manager else { return }
guard clearedManagers.insert(ObjectIdentifier(manager)).inserted else { return }
manager.clearRecentlyClosedBrowserPanelHistory()
}

clear(preferredTabManager)
clear(tabManager)
for context in mainWindowContexts.values {
clear(context.tabManager)
}
for route in recoverableMainWindowRoutes() {
clear(route.tabManager)
}
}

@discardableResult
func reopenMostRecentlyClosedItem(
preferredTabManager: TabManager? = nil,
shouldActivate: Bool = true
) -> Bool {
var failedStoreRecordIds: Set<UUID> = []
let restoreStoreItem: (Date?) -> Bool = { cutoff in
ClosedItemHistoryStore.shared.restoreFirstRestorable(
newerThan: cutoff,
excluding: failedStoreRecordIds,
onFailure: { failedStoreRecordIds.insert($0) },
using: { entry in
self.restoreClosedItem(
entry,
preferredTabManager: preferredTabManager,
shouldActivate: shouldActivate
)
}
)
}

for manager in recentlyClosedLegacyBrowserManagers(preferredTabManager: preferredTabManager) {
guard let closedAt = manager.mostRecentLegacyClosedBrowserPanelClosedAt() else {
continue
}
if restoreStoreItem(closedAt) {
return true
}
if manager.reopenMostRecentlyClosedBrowserPanelFromLegacyStack() {
return true
}
}

return restoreStoreItem(nil)
}
Comment thread
cursor[bot] marked this conversation as resolved.

private func recentlyClosedLegacyBrowserManagers(preferredTabManager: TabManager?) -> [TabManager] {
var managers: [TabManager] = []
var seen: Set<ObjectIdentifier> = []

func append(_ manager: TabManager?) {
guard let manager else { return }
guard manager.mostRecentLegacyClosedBrowserPanelClosedAt() != nil else { return }
guard seen.insert(ObjectIdentifier(manager)).inserted else { return }
managers.append(manager)
}

append(preferredTabManager)
append(tabManager)
for context in mainWindowContexts.values {
append(context.tabManager)
}
for route in recoverableMainWindowRoutes() {
append(route.tabManager)
}

return managers.sorted { lhs, rhs in
let lhsDate = lhs.mostRecentLegacyClosedBrowserPanelClosedAt() ?? .distantPast
let rhsDate = rhs.mostRecentLegacyClosedBrowserPanelClosedAt() ?? .distantPast
return lhsDate > rhsDate
}
}

@discardableResult
private func restoreClosedItem(
_ entry: ClosedItemHistoryEntry,
preferredTabManager: TabManager? = nil,
shouldActivate: Bool
) -> Bool {
switch entry {
case .panel(let panelEntry):
let manager =
tabManagerFor(tabId: panelEntry.workspaceId)
?? preferredTabManager
?? tabManager
guard let manager, manager.restoreClosedPanel(panelEntry) else {
return false
}
activateMainWindowIfNeeded(for: manager, shouldActivate: shouldActivate)
return true
case .workspace(let workspaceEntry):
let manager =
workspaceEntry.windowId.flatMap { tabManagerFor(windowId: $0) }
?? preferredTabManager
?? tabManager
guard let manager, manager.restoreClosedWorkspace(workspaceEntry) else {
return false
}
activateMainWindowIfNeeded(for: manager, shouldActivate: shouldActivate)
return true
case .window(let windowEntry):
var restoredPanelIdsByWorkspaceIndex: [[UUID: UUID]] = []
var restoredTabManager: TabManager?
let windowId = createMainWindow(
sessionWindowSnapshot: windowEntry.snapshot,
shouldActivate: shouldActivate,
closedWindowHistoryWorkspaceIds: windowEntry.workspaceIds,
restoredSessionSnapshotHandler: { panelIdsByWorkspaceIndex, tabManager in
restoredPanelIdsByWorkspaceIndex = panelIdsByWorkspaceIndex
restoredTabManager = tabManager
}
)
let hasLivePanels = restoredTabManager?.tabs.contains { !$0.panels.isEmpty } == true
guard ClosedWindowRestoreValidation.hasUsableRestoredContent(
snapshot: windowEntry.snapshot,
restoredPanelIdsByWorkspaceIndex: restoredPanelIdsByWorkspaceIndex,
hasLivePanels: hasLivePanels
) else {
discardMainWindowWithoutClosedHistory(windowId: windowId)
return false
}
if let oldWindowId = windowEntry.windowId {
ClosedItemHistoryStore.shared.remapWorkspaceWindowIds(from: oldWindowId, to: windowId)
}
return true
}
Comment thread
cursor[bot] marked this conversation as resolved.
}

@discardableResult
func reopenClosedHistoryItem(
id: UUID,
preferredTabManager: TabManager? = nil,
shouldActivate: Bool = true
) -> Bool {
guard let removed = ClosedItemHistoryStore.shared.removeRecord(id: id) else {
return false
}

if restoreClosedItem(
removed.record.entry,
preferredTabManager: preferredTabManager,
shouldActivate: shouldActivate
) {
return true
}

ClosedItemHistoryStore.shared.insert(removed.record, at: removed.index)
return false
}

private func activateMainWindowIfNeeded(for manager: TabManager, shouldActivate: Bool) {
guard shouldActivate,
let windowId = windowId(for: manager) else {
return
}
_ = focusMainWindow(windowId: windowId)
}
}
139 changes: 139 additions & 0 deletions Sources/AppDelegate+FocusHistoryContextMenu.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
import AppKit

private let focusHistoryContextMenuPreviewLimit = 12

private final class FocusHistoryContextMenuItemBox: NSObject {
weak var tabManager: TabManager?
let item: FocusHistoryMenuItem

init(tabManager: TabManager, item: FocusHistoryMenuItem) {
self.tabManager = tabManager
self.item = item
}
}

private final class FocusHistoryShowFullContextMenuBox: NSObject {
weak var tabManager: TabManager?
weak var anchorView: NSView?
let direction: FocusHistoryMenuDirection

init(tabManager: TabManager, anchorView: NSView, direction: FocusHistoryMenuDirection) {
self.tabManager = tabManager
self.anchorView = anchorView
self.direction = direction
}
}

extension AppDelegate {
@discardableResult
func showFocusHistoryContextMenu(
anchorView: NSView,
event: NSEvent,
direction: FocusHistoryMenuDirection,
showFullHistory: Bool = false,
debugSource: String = "titlebar.focusHistory.contextMenu"
) -> Bool {
let context = contextForMainWindow(anchorView.window)
?? mainWindowContext(forShortcutEvent: event, debugSource: debugSource)
guard let context else { return false }

let menu = makeFocusHistoryContextMenu(
tabManager: context.tabManager,
anchorView: anchorView,
direction: direction,
showFullHistory: showFullHistory
)
guard menu.items.contains(where: { !$0.isSeparatorItem }) else { return false }

NSMenu.popUpContextMenu(menu, with: event, for: anchorView)
return true
}

private func makeFocusHistoryContextMenu(
tabManager: TabManager,
anchorView: NSView,
direction: FocusHistoryMenuDirection,
showFullHistory: Bool
) -> NSMenu {
let snapshot = tabManager.focusHistoryMenuSnapshot(
direction: direction,
maxItemCount: showFullHistory ? nil : focusHistoryContextMenuPreviewLimit
)
let menu = NSMenu(title: String(localized: "menu.history.focusHistory", defaultValue: "Focus History"))

if snapshot.items.isEmpty {
let item = NSMenuItem(
title: String(localized: "menu.history.noFocusHistory", defaultValue: "No Focus History"),
action: nil,
keyEquivalent: ""
)
item.isEnabled = false
menu.addItem(item)
return menu
}

for itemSnapshot in snapshot.items {
let item = NSMenuItem(
title: focusHistoryContextMenuTitle(for: itemSnapshot),
action: #selector(performFocusHistoryContextMenuItem(_:)),
keyEquivalent: ""
)
item.target = self
item.representedObject = FocusHistoryContextMenuItemBox(tabManager: tabManager, item: itemSnapshot)
item.isEnabled = itemSnapshot.isNavigable
menu.addItem(item)
}

if snapshot.isLimited {
menu.addItem(.separator())
let item = NSMenuItem(
title: String(localized: "menu.history.showFullFocusHistory", defaultValue: "Show Full History"),
action: #selector(showFullFocusHistoryContextMenu(_:)),
keyEquivalent: ""
)
item.target = self
item.representedObject = FocusHistoryShowFullContextMenuBox(
tabManager: tabManager,
anchorView: anchorView,
direction: direction
)
menu.addItem(item)
}

return menu
}

private func focusHistoryContextMenuTitle(for item: FocusHistoryMenuItem) -> String {
FocusHistoryMenuFormatter.title(for: item)
}

@objc private func performFocusHistoryContextMenuItem(_ sender: NSMenuItem) {
guard let box = sender.representedObject as? FocusHistoryContextMenuItemBox,
let tabManager = box.tabManager,
tabManager.navigateToFocusHistoryMenuItem(box.item) else {
NSSound.beep()
return
}
}

@objc private func showFullFocusHistoryContextMenu(_ sender: NSMenuItem) {
guard let box = sender.representedObject as? FocusHistoryShowFullContextMenuBox,
let tabManager = box.tabManager,
let anchorView = box.anchorView else {
NSSound.beep()
return
}

let menu = makeFocusHistoryContextMenu(
tabManager: tabManager,
anchorView: anchorView,
direction: box.direction,
showFullHistory: true
)
guard menu.items.contains(where: { !$0.isSeparatorItem }) else {
NSSound.beep()
return
}
menu.popUp(positioning: nil, at: NSPoint(x: 0, y: anchorView.bounds.height), in: anchorView)
}
}
23 changes: 23 additions & 0 deletions Sources/AppDelegate+HistoryPane.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import AppKit

extension AppDelegate {
@discardableResult
func openHistoryPane(
preferredTabManager: TabManager? = nil,
preferredWindow: NSWindow? = nil
) -> Bool {
let manager = preferredTabManager
?? activeTabManagerForCommands(preferredWindow: preferredWindow ?? NSApp.keyWindow ?? NSApp.mainWindow)
guard let workspace = manager?.selectedWorkspace,
let paneId = workspace.bonsplitController.focusedPaneId ?? workspace.bonsplitController.allPaneIds.first else {
return false
}

workspace.clearSplitZoom()
return workspace.openOrFocusRightSidebarToolSurface(
inPane: paneId,
mode: .history,
focus: true
) != nil
}
}
Loading
Loading