Skip to content
Closed
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
54243c6
Add workspace pages
austinywang May 26, 2026
70fd0f3
Merge remote-tracking branch 'origin/main' into issue-3297-allow-a-su…
austinywang May 26, 2026
efadafb
Fix page reorder after anchor
austinywang May 26, 2026
101600a
Fix page surface restore startup call
austinywang May 26, 2026
3f99182
Route surface and pane commands through requested page
austinywang May 26, 2026
94d5dd6
Remove stale handoff policy tests
austinywang May 26, 2026
d7916bc
Restore notifications for inactive pages
austinywang May 26, 2026
4e756c2
Merge remote-tracking branch 'origin/main' into issue-3297-allow-a-su…
austinywang May 26, 2026
43d75ce
Address page PR review feedback
austinywang May 26, 2026
a4462b8
Replace split tree when restoring pages
austinywang May 26, 2026
f3b5818
Fix titlebar page strip return
austinywang May 26, 2026
aeec2af
Sync page notifications on runtime restore
austinywang May 26, 2026
35577ea
Fix page socket payload isolation
austinywang May 26, 2026
b885a44
Merge remote-tracking branch 'origin/main' into issue-3297-allow-a-su…
austinywang May 26, 2026
c0d41a5
Preserve page switch notifications
austinywang May 26, 2026
2ce3531
Fix runtime page layout restore
austinywang May 26, 2026
57a1f4f
Merge remote-tracking branch 'origin/main' into issue-3297-allow-a-su…
austinywang May 26, 2026
be282c7
Merge remote-tracking branch 'origin/main' into issue-3297-allow-a-su…
austinywang Jun 6, 2026
09af328
Test socket close current page
austinywang Jun 6, 2026
5ee3d6e
Allow socket page close to use current page
austinywang Jun 6, 2026
c06ec55
Test page restore closed history suppression
austinywang Jun 6, 2026
1bdda58
Suppress page restore placeholder history
austinywang Jun 6, 2026
65da8a1
Localize workspace page strings
austinywang Jun 6, 2026
255a185
Test socket rename current page
austinywang Jun 6, 2026
9be7e4f
Test stale socket page duplicate id
austinywang Jun 6, 2026
6926cbe
Default page rename to current page
austinywang Jun 6, 2026
c3d503c
Test close page workspace default
austinywang Jun 6, 2026
4bacd69
Parse close-page workspace option before page
austinywang Jun 6, 2026
630809f
Test page CLI window routing
austinywang Jun 6, 2026
2598118
Route page CLI through window context
austinywang Jun 6, 2026
198b5e3
Test page CLI command registry
austinywang Jun 6, 2026
af5d481
Register page CLI verbs
austinywang Jun 6, 2026
05c7821
Test duplicate-page positional handle
austinywang Jun 6, 2026
ad22cef
Allow duplicate-page positional handles
austinywang Jun 6, 2026
ea9c0a9
Test page CLI terminator positional handles
austinywang Jun 6, 2026
8762ef7
Allow page CLI terminator positional handles
austinywang Jun 6, 2026
c215790
Address page review preflight findings
austinywang Jun 6, 2026
89a3f0a
Test page CLI numeric index handles
austinywang Jun 6, 2026
b9bfbf7
Allow page CLI one-based numeric indexes
austinywang Jun 6, 2026
d7b7e39
Address page review maintenance notes
austinywang Jun 6, 2026
3fbf192
Test rename page positional handle
austinywang Jun 6, 2026
24e2276
Allow rename page positional handles
austinywang Jun 6, 2026
4109da9
Test numeric page titles
austinywang Jun 6, 2026
41d5c5b
Disambiguate numeric page titles
austinywang Jun 6, 2026
0d05ebd
Test page CLI terminator title options
austinywang Jun 6, 2026
ab96f90
Honor page CLI terminator titles
austinywang Jun 6, 2026
12f9a9a
Migrate page lifecycle tests to Swift Testing
austinywang Jun 6, 2026
4d21ad3
Test runtime page restore selection fallback
austinywang Jun 6, 2026
98c82de
Select attached panel on page restore
austinywang Jun 6, 2026
c4446ab
Test in-range numeric page titles
austinywang Jun 6, 2026
9a84dda
Fix page reorder workspace isolation
austinywang Jun 6, 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
490 changes: 459 additions & 31 deletions CLI/cmux.swift

Large diffs are not rendered by default.

755 changes: 755 additions & 0 deletions Resources/Localizable.xcstrings

Large diffs are not rendered by default.

8 changes: 8 additions & 0 deletions Sources/App/TerminalDirectoryOpenSupport.swift
Original file line number Diff line number Diff line change
Expand Up @@ -782,4 +782,12 @@ enum WorkspaceShortcutMapper {
}
return nil
}

static func pageIndex(forOptionDigit digit: Int, pageCount: Int) -> Int? {
workspaceIndex(forDigit: digit, workspaceCount: pageCount)
}

static func optionDigitForPage(at index: Int, pageCount: Int) -> Int? {
digitForWorkspace(at: index, workspaceCount: pageCount)
}
}
59 changes: 59 additions & 0 deletions Sources/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12336,6 +12336,65 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent
return handled
}

if matchConfiguredShortcut(event: event, action: .newPage) {
let routedManager = preferredMainWindowContextForShortcutRouting(event: event)?.tabManager ?? tabManager
_ = routedManager?.selectedWorkspace?.newPage(select: true)
return true
}

if matchConfiguredShortcut(event: event, action: .renamePage) {
let routedManager = preferredMainWindowContextForShortcutRouting(event: event)?.tabManager ?? tabManager
guard let workspace = routedManager?.selectedWorkspace,
let pageId = workspace.activePage?.id else {
return false
}
workspace.promptRenamePage(pageId: pageId)
return true
}

if matchConfiguredShortcut(event: event, action: .closePage) {
let routedManager = preferredMainWindowContextForShortcutRouting(event: event)?.tabManager ?? tabManager
guard let workspace = routedManager?.selectedWorkspace,
let pageId = workspace.activePage?.id else {
return false
}
workspace.closePage(pageId)
return true
}

if matchConfiguredShortcut(event: event, action: .nextPage) {
let routedManager = preferredMainWindowContextForShortcutRouting(event: event)?.tabManager ?? tabManager
routedManager?.selectedWorkspace?.selectNextPage()
return true
}

if matchConfiguredShortcut(event: event, action: .previousPage) {
let routedManager = preferredMainWindowContextForShortcutRouting(event: event)?.tabManager ?? tabManager
routedManager?.selectedWorkspace?.selectPreviousPage()
return true
}

let pageSelectionShortcuts: [(KeyboardShortcutSettings.Action, Int?)] = [
(.selectPage1, 0),
(.selectPage2, 1),
(.selectPage3, 2),
(.selectPage4, 3),
(.selectPage5, 4),
(.selectPage6, 5),
(.selectPage7, 6),
(.selectPage8, 7),
(.selectLastPage, nil),
]
for (action, pageIndex) in pageSelectionShortcuts where matchConfiguredShortcut(event: event, action: action) {
let routedManager = preferredMainWindowContextForShortcutRouting(event: event)?.tabManager ?? tabManager
if let pageIndex {
routedManager?.selectedWorkspace?.selectPage(at: pageIndex)
} else {
routedManager?.selectedWorkspace?.selectLastPage()
}
return true
}

// Workspace navigation: Cmd+Ctrl+] / Cmd+Ctrl+[
if matchConfiguredShortcut(event: event, action: .nextSidebarTab) {
#if DEBUG
Expand Down
109 changes: 104 additions & 5 deletions Sources/ContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2358,11 +2358,15 @@ struct ContentView: View {
.padding(.leading, -6)
}

Text(titlebarText)
.font(.system(size: 13, weight: .bold))
.foregroundColor(fakeTitlebarTextColor(appearance: appearance))
.lineLimit(1)
.allowsHitTesting(false)
if let workspace = tabManager.selectedWorkspace {
titlebarPageStrip(workspace: workspace, textColor: fakeTitlebarTextColor(appearance: appearance))
} else {
Text(titlebarText)
.font(.system(size: 13, weight: .bold))
.foregroundColor(fakeTitlebarTextColor(appearance: appearance))
.lineLimit(1)
.allowsHitTesting(false)
}

Spacer()

Expand All @@ -2381,6 +2385,101 @@ struct ContentView: View {
}
}

private func titlebarPageStrip(workspace: Workspace, textColor: Color) -> some View {
let pages = workspace.pages
let activePageId = workspace.activePageId
let canCloseOthers = pages.count > 1

return ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: 4) {
ForEach(pages) { page in
let pageId = page.id
titlebarPageButton(
title: page.title,
isActive: pageId == activePageId,
canClose: workspace.canClosePage(pageId),
canCloseOthers: canCloseOthers,
textColor: textColor,
selectAction: { workspace.selectPage(pageId) },
renameAction: { workspace.promptRenamePage(pageId: pageId) },
duplicateAction: { _ = workspace.duplicatePage(sourcePageId: pageId, select: true) },
closeOthersAction: { workspace.closeOtherPages(keeping: pageId) },
closeAction: { workspace.closePage(pageId) }
)
}

Button {
_ = workspace.newPage(select: true)
} label: {
Image(systemName: "plus")
.font(.system(size: 11, weight: .semibold))
.frame(width: 22, height: 22)
.contentShape(Rectangle())
}
.buttonStyle(.plain)
.foregroundColor(textColor.opacity(0.8))
.help(String(localized: "menu.view.newPage", defaultValue: "New Page"))
}
.frame(maxHeight: .infinity, alignment: .center)
}
.frame(maxWidth: .infinity, alignment: .leading)
}

private func titlebarPageButton(
title: String,
isActive: Bool,
canClose: Bool,
canCloseOthers: Bool,
textColor: Color,
selectAction: @escaping () -> Void,
renameAction: @escaping () -> Void,
duplicateAction: @escaping () -> Void,
closeOthersAction: @escaping () -> Void,
closeAction: @escaping () -> Void
) -> some View {
Button {
selectAction()
} label: {
HStack(spacing: 4) {
Text(title)
.font(.system(size: 12, weight: isActive ? .semibold : .regular))
.lineLimit(1)
.truncationMode(.tail)
}
.padding(.horizontal, 8)
.frame(height: 22)
.background(
RoundedRectangle(cornerRadius: 5, style: .continuous)
.fill(isActive ? textColor.opacity(0.16) : Color.clear)
)
.overlay(
RoundedRectangle(cornerRadius: 5, style: .continuous)
.stroke(isActive ? textColor.opacity(0.20) : Color.clear, lineWidth: 1)
)
.contentShape(RoundedRectangle(cornerRadius: 5, style: .continuous))
}
.buttonStyle(.plain)
.foregroundColor(isActive ? textColor : textColor.opacity(0.72))
.help(title)
.contextMenu {
Button(String(localized: "workspace.page.context.rename", defaultValue: "Rename Page…")) {
renameAction()
}
Button(String(localized: "workspace.page.context.duplicate", defaultValue: "Duplicate Page")) {
duplicateAction()
}
Divider()
Button(String(localized: "workspace.page.context.closeOthers", defaultValue: "Close Other Pages")) {
closeOthersAction()
}
.disabled(!canCloseOthers)
Button(String(localized: "workspace.page.context.close", defaultValue: "Close Page")) {
closeAction()
}
.disabled(!canClose)
}
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved

private func syncTrafficLightInset() {
let inset: CGFloat = (isMinimalMode && !sidebarState.isVisible && !isFullScreen)
? CGFloat(titlebarDebugChromeSnapshot.trafficLightTabBarLeadingInset)
Expand Down
74 changes: 74 additions & 0 deletions Sources/KeyboardShortcutSettings.swift
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,20 @@ enum KeyboardShortcutSettings {
case toggleTerminalCopyMode
case focusTextBoxInput
case attachTextBoxFile
case newPage
case renamePage
case closePage
case nextPage
case previousPage
case selectPage1
case selectPage2
case selectPage3
case selectPage4
case selectPage5
case selectPage6
case selectPage7
case selectPage8
case selectLastPage

// Panes / splits
case focusLeft
Expand Down Expand Up @@ -199,6 +213,20 @@ enum KeyboardShortcutSettings {
case .toggleTerminalCopyMode: return String(localized: "shortcut.toggleTerminalCopyMode.label", defaultValue: "Toggle Terminal Copy Mode")
case .focusTextBoxInput: return String(localized: "shortcut.focusTextBoxInput.label", defaultValue: "Focus TextBox Input")
case .attachTextBoxFile: return String(localized: "shortcut.attachTextBoxFile.label", defaultValue: "Attach File to TextBox Input")
case .newPage: return String(localized: "shortcut.newPage.label", defaultValue: "New Page")
case .renamePage: return String(localized: "shortcut.renamePage.label", defaultValue: "Rename Page")
case .closePage: return String(localized: "shortcut.closePage.label", defaultValue: "Close Page")
case .nextPage: return String(localized: "shortcut.nextPage.label", defaultValue: "Next Page")
case .previousPage: return String(localized: "shortcut.previousPage.label", defaultValue: "Previous Page")
case .selectPage1: return String(localized: "shortcut.selectPage1.label", defaultValue: "Select Page 1")
case .selectPage2: return String(localized: "shortcut.selectPage2.label", defaultValue: "Select Page 2")
case .selectPage3: return String(localized: "shortcut.selectPage3.label", defaultValue: "Select Page 3")
case .selectPage4: return String(localized: "shortcut.selectPage4.label", defaultValue: "Select Page 4")
case .selectPage5: return String(localized: "shortcut.selectPage5.label", defaultValue: "Select Page 5")
case .selectPage6: return String(localized: "shortcut.selectPage6.label", defaultValue: "Select Page 6")
case .selectPage7: return String(localized: "shortcut.selectPage7.label", defaultValue: "Select Page 7")
case .selectPage8: return String(localized: "shortcut.selectPage8.label", defaultValue: "Select Page 8")
case .selectLastPage: return String(localized: "shortcut.selectLastPage.label", defaultValue: "Select Last Page")
case .focusLeft: return String(localized: "shortcut.focusPaneLeft.label", defaultValue: "Focus Pane Left")
case .focusRight: return String(localized: "shortcut.focusPaneRight.label", defaultValue: "Focus Pane Right")
case .focusUp: return String(localized: "shortcut.focusPaneUp.label", defaultValue: "Focus Pane Up")
Expand Down Expand Up @@ -333,6 +361,34 @@ enum KeyboardShortcutSettings {
return StoredShortcut(key: "w", command: true, shift: true, option: false, control: false)
case .reopenClosedBrowserPanel:
return StoredShortcut(key: "t", command: true, shift: true, option: false, control: false)
case .newPage:
return StoredShortcut(key: "n", command: true, shift: false, option: true, control: false)
case .renamePage:
return StoredShortcut(key: "r", command: true, shift: false, option: true, control: false)
case .closePage:
return StoredShortcut(key: "w", command: true, shift: false, option: true, control: false)
case .nextPage:
return StoredShortcut(key: "]", command: false, shift: false, option: true, control: false)
case .previousPage:
return StoredShortcut(key: "[", command: false, shift: false, option: true, control: false)
case .selectPage1:
return StoredShortcut(key: "1", command: false, shift: false, option: true, control: false)
case .selectPage2:
return StoredShortcut(key: "2", command: false, shift: false, option: true, control: false)
case .selectPage3:
return StoredShortcut(key: "3", command: false, shift: false, option: true, control: false)
case .selectPage4:
return StoredShortcut(key: "4", command: false, shift: false, option: true, control: false)
case .selectPage5:
return StoredShortcut(key: "5", command: false, shift: false, option: true, control: false)
case .selectPage6:
return StoredShortcut(key: "6", command: false, shift: false, option: true, control: false)
case .selectPage7:
return StoredShortcut(key: "7", command: false, shift: false, option: true, control: false)
case .selectPage8:
return StoredShortcut(key: "8", command: false, shift: false, option: true, control: false)
case .selectLastPage:
return StoredShortcut(key: "9", command: false, shift: false, option: true, control: false)
case .focusLeft:
return StoredShortcut(key: "←", command: true, shift: false, option: true, control: false)
case .focusRight:
Expand Down Expand Up @@ -876,6 +932,20 @@ enum KeyboardShortcutSettings {
static func prevSidebarTabShortcut() -> StoredShortcut { shortcut(for: .prevSidebarTab) }
static func renameWorkspaceShortcut() -> StoredShortcut { shortcut(for: .renameWorkspace) }
static func closeWorkspaceShortcut() -> StoredShortcut { shortcut(for: .closeWorkspace) }
static func newPageShortcut() -> StoredShortcut { shortcut(for: .newPage) }
static func renamePageShortcut() -> StoredShortcut { shortcut(for: .renamePage) }
static func closePageShortcut() -> StoredShortcut { shortcut(for: .closePage) }
static func nextPageShortcut() -> StoredShortcut { shortcut(for: .nextPage) }
static func previousPageShortcut() -> StoredShortcut { shortcut(for: .previousPage) }
static func selectPage1Shortcut() -> StoredShortcut { shortcut(for: .selectPage1) }
static func selectPage2Shortcut() -> StoredShortcut { shortcut(for: .selectPage2) }
static func selectPage3Shortcut() -> StoredShortcut { shortcut(for: .selectPage3) }
static func selectPage4Shortcut() -> StoredShortcut { shortcut(for: .selectPage4) }
static func selectPage5Shortcut() -> StoredShortcut { shortcut(for: .selectPage5) }
static func selectPage6Shortcut() -> StoredShortcut { shortcut(for: .selectPage6) }
static func selectPage7Shortcut() -> StoredShortcut { shortcut(for: .selectPage7) }
static func selectPage8Shortcut() -> StoredShortcut { shortcut(for: .selectPage8) }
static func selectLastPageShortcut() -> StoredShortcut { shortcut(for: .selectLastPage) }

static func focusLeftShortcut() -> StoredShortcut { shortcut(for: .focusLeft) }
static func focusRightShortcut() -> StoredShortcut { shortcut(for: .focusRight) }
Expand Down Expand Up @@ -1562,6 +1632,10 @@ struct ShortcutStroke: Equatable, Hashable {
}

let allowANSIKeyCodeFallback = flags.contains(.control)
|| (!flags.contains(.command)
&& flags.contains(.option)
&& !flags.contains(.control)
&& !Self.shouldRequireCharacterMatchForCommandShortcut(shortcutKey: shortcutKey))
|| (flags.contains(.command)
&& !flags.contains(.control)
&& (
Expand Down
19 changes: 19 additions & 0 deletions Sources/SessionPersistence.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1572,6 +1572,25 @@ struct SessionWorkspaceSnapshot: Codable, Sendable {
var progress: SessionProgressSnapshot?
var gitBranch: SessionGitBranchSnapshot?
var remote: SessionRemoteWorkspaceSnapshot?
var activePageId: UUID?
var pages: [SessionWorkspacePageSnapshot]?
}

struct SessionWorkspacePageStateSnapshot: Codable, Sendable {
var currentDirectory: String
var focusedPanelId: UUID?
var layout: SessionWorkspaceLayoutSnapshot
var panels: [SessionPanelSnapshot]
var statusEntries: [SessionStatusEntrySnapshot]
var logEntries: [SessionLogEntrySnapshot]
var progress: SessionProgressSnapshot?
var gitBranch: SessionGitBranchSnapshot?
}

struct SessionWorkspacePageSnapshot: Codable, Sendable {
var id: UUID
var title: String
var state: SessionWorkspacePageStateSnapshot
}

extension SessionWorkspaceSnapshot {
Expand Down
Loading
Loading