Skip to content
Closed
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
1 change: 1 addition & 0 deletions Sources/CmuxConfig.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1614,6 +1614,7 @@ struct CmuxSurfaceDefinition: Codable, Sendable, Hashable {
var cwd: String?
var env: [String: String]?
var url: String?
var selected: Bool?
Comment thread
austinywang marked this conversation as resolved.
var focus: Bool?
}

Expand Down
43 changes: 39 additions & 4 deletions Sources/Workspace+CustomLayout.swift
Original file line number Diff line number Diff line change
Expand Up @@ -111,15 +111,19 @@ extension Workspace {
guard !surfaces.isEmpty else { return }

let firstSurface = surfaces[0]
var selectedPanelId: UUID?
if let placeholderPanelId = existingPanelIds.first {
configureExistingSurface(
let configuredPanelId = configureExistingSurface(
panelId: placeholderPanelId,
inPane: paneId,
surface: firstSurface,
baseCwd: baseCwd,
focusPanelId: &focusPanelId,
pendingSetup: &pendingSetup
)
if firstSurface.selected == true {
selectedPanelId = configuredPanelId
}
}

// The first surface either reuses or replaces the pane's placeholder.
Expand All @@ -130,16 +134,38 @@ extension Workspace {
defer { bonsplitController.configuration.newTabPosition = interactiveNewTabPosition }

for surfaceIndex in 1..<surfaces.count {
createNewSurface(
let createdPanelId = createNewSurface(
inPane: paneId,
surface: surfaces[surfaceIndex],
baseCwd: baseCwd,
focusPanelId: &focusPanelId,
pendingSetup: &pendingSetup
)
if surfaces[surfaceIndex].selected == true {
selectedPanelId = createdPanelId
}
}
if let selectedPanelId,
let tabId = surfaceIdFromPanelId(selectedPanelId) {
restoreCustomLayoutSelection(tabId)
}
}

private func restoreCustomLayoutSelection(_ tabId: TabID) {
let focusedPaneId = bonsplitController.focusedPaneId
let wasSuppressingSelectionActivation = suppressesCustomLayoutSelectionActivation
suppressesCustomLayoutSelectionActivation = true
defer {
if let focusedPaneId,
bonsplitController.focusedPaneId != focusedPaneId,
bonsplitController.allPaneIds.contains(focusedPaneId) {
bonsplitController.focusPane(focusedPaneId)
}
suppressesCustomLayoutSelectionActivation = wasSuppressingSelectionActivation
}
bonsplitController.selectTab(tabId)
}

/// Consumes the workspace-level setup command on the first terminal surface it
/// reaches, sequencing it ahead of that surface's own `command`.
private static func dequeueInitialTerminalInput(
Expand All @@ -165,7 +191,7 @@ extension Workspace {
baseCwd: String,
focusPanelId: inout UUID?,
pendingSetup: inout String?
) {
) -> UUID? {
switch surface.type {
case .terminal where surface.cwd != nil || surface.env != nil:
// Placeholder can't change cwd/env — replace it
Expand All @@ -182,6 +208,7 @@ extension Workspace {
if let input = Self.dequeueInitialTerminalInput(pendingSetup: &pendingSetup, command: surface.command) {
sendInputWhenReady(input, to: panel)
}
return panel.id
}

case .terminal:
Expand All @@ -191,6 +218,7 @@ extension Workspace {
let terminal = terminalPanel(for: panelId) {
sendInputWhenReady(input, to: terminal)
}
return panelId

case .browser:
let url = surface.url.flatMap { URL(string: $0) }
Expand All @@ -203,6 +231,7 @@ extension Workspace {
_ = closePanel(panelId, force: true)
if let name = surface.name { setPanelCustomTitle(panelId: panel.id, title: name) }
if surface.focus == true { focusPanelId = panel.id }
return panel.id
}

case .project:
Expand All @@ -214,8 +243,10 @@ extension Workspace {
_ = closePanel(panelId, force: true)
if let name = surface.name { setPanelCustomTitle(panelId: panel.id, title: name) }
if surface.focus == true { focusPanelId = panel.id }
return panel.id
}
}
return nil
}

private func createNewSurface(
Expand All @@ -224,7 +255,7 @@ extension Workspace {
baseCwd: String,
focusPanelId: inout UUID?,
pendingSetup: inout String?
) {
) -> UUID? {
switch surface.type {
case .terminal:
let resolvedCwd = CmuxConfigStore.resolveCwd(surface.cwd, relativeTo: baseCwd)
Expand All @@ -239,6 +270,7 @@ extension Workspace {
if let input = Self.dequeueInitialTerminalInput(pendingSetup: &pendingSetup, command: surface.command) {
sendInputWhenReady(input, to: panel)
}
return panel.id
}

case .browser:
Expand All @@ -251,6 +283,7 @@ extension Workspace {
) {
if let name = surface.name { setPanelCustomTitle(panelId: panel.id, title: name) }
if surface.focus == true { focusPanelId = panel.id }
return panel.id
}

case .project:
Expand All @@ -261,8 +294,10 @@ extension Workspace {
) {
if let name = surface.name { setPanelCustomTitle(panelId: panel.id, title: name) }
if surface.focus == true { focusPanelId = panel.id }
return panel.id
}
}
return nil
}

private func applyCustomDividerPositions(
Expand Down
32 changes: 18 additions & 14 deletions Sources/Workspace+LayoutCapture.swift
Original file line number Diff line number Diff line change
Expand Up @@ -84,23 +84,32 @@ extension Workspace {
unsupportedSurfaceCount += pane.tabs.count
return []
}
let paneId = PaneID(id: paneUUID)
let selectedTabId = bonsplitController.selectedTab(inPane: paneId)?.id
var surfaces: [CmuxSurfaceDefinition] = []
surfaces.reserveCapacity(max(pane.tabs.count, 1))
for tab in bonsplitController.tabs(inPane: PaneID(id: paneUUID)) {
for tab in bonsplitController.tabs(inPane: paneId) {
var definition: CmuxSurfaceDefinition
guard let panelId = panelIdFromSurfaceId(tab.id),
let panel = panels[panelId] else {
unsupportedSurfaceCount += 1
surfaces.append(CmuxSurfaceDefinition(type: .terminal))
definition = CmuxSurfaceDefinition(type: .terminal)
if tab.id == selectedTabId {
definition.selected = true
}
surfaces.append(definition)
continue
}
surfaces.append(
captureSurfaceDefinition(
panelId: panelId,
panel: panel,
baseCwd: baseCwd,
unsupportedSurfaceCount: &unsupportedSurfaceCount
)
definition = captureSurfaceDefinition(
panelId: panelId,
panel: panel,
baseCwd: baseCwd,
unsupportedSurfaceCount: &unsupportedSurfaceCount
)
if tab.id == selectedTabId {
definition.selected = true
}
surfaces.append(definition)
}
return surfaces
}
Expand Down Expand Up @@ -156,11 +165,6 @@ extension Workspace {
unsupportedSurfaceCount += 1
definition = CmuxSurfaceDefinition(type: .terminal)
}
// The declarative schema models only the single focused surface
// (`focus`); per-pane tab selection is not representable without a
// schema extension, so non-focused multi-tab panes reopen with their
// first tab selected (tracked in
// https://github.com/manaflow-ai/cmux/issues/7444).
if focusedPanelId == panelId {
definition.focus = true
}
Expand Down
12 changes: 8 additions & 4 deletions Sources/Workspace.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2186,6 +2186,8 @@ final class Workspace: Identifiable, ObservableObject {

/// The bonsplit controller managing the split panes for this workspace
let bonsplitController: BonsplitController
/// Per-pane selection restoration must not run the global panel-activation path.
var suppressesCustomLayoutSelectionActivation = false

/// Backing store for `dockSplit`, created on first access. Kept optional so
/// workspace teardown can tear down the Dock only when it was actually used
Expand Down Expand Up @@ -12670,8 +12672,9 @@ extension Workspace: BonsplitDelegate {
}

func splitTabBar(_ controller: BonsplitController, didSelectTab tab: Bonsplit.Tab, inPane pane: PaneID) {
// Mirror bookkeeping restores selection from its transaction snapshot.
guard !remoteTmuxMirrorMutations.suppressesFocusActivation else { return }
// Focus-neutral restoration mutates Bonsplit selection without activating panels.
guard !remoteTmuxMirrorMutations.suppressesFocusActivation,
!suppressesCustomLayoutSelectionActivation else { return }
applyTabSelection(tabId: tab.id, inPane: pane)
}

Expand Down Expand Up @@ -12749,8 +12752,9 @@ extension Workspace: BonsplitDelegate {
}

func splitTabBar(_ controller: BonsplitController, didFocusPane pane: PaneID) {
// Mirror bookkeeping restores pane focus without re-running activation.
guard !remoteTmuxMirrorMutations.suppressesFocusActivation else { return }
// Focus-neutral restoration mutates Bonsplit focus without activating panels.
guard !remoteTmuxMirrorMutations.suppressesFocusActivation,
!suppressesCustomLayoutSelectionActivation else { return }
// When a pane is focused, focus its selected tab's panel
guard let tab = controller.selectedTab(inPane: pane) else { return }
#if DEBUG
Expand Down
11 changes: 9 additions & 2 deletions Sources/WorkspaceConfigActionCapture.swift
Original file line number Diff line number Diff line change
Expand Up @@ -162,14 +162,21 @@ extension Workspace {
guard let paneId = bonsplitController.allPaneIds.first(where: { $0.id.uuidString == pane.id }) else {
return nil
}
let selectedTabId = bonsplitController.selectedTab(inPane: paneId)?.id
let surfaces = bonsplitController.tabs(inPane: paneId).compactMap { tab -> CmuxSurfaceDefinition? in
guard let panelId = panelIdFromSurfaceId(tab.id) else { return nil }
return configCaptureSurfaceDefinition(
guard var definition = configCaptureSurfaceDefinition(
panelId: panelId,
workspaceCwd: workspaceCwd,
liveCommands: liveCommands,
skippedPanelCount: &skippedPanelCount
)
) else {
return nil
}
if tab.id == selectedTabId {
definition.selected = true
}
return definition
}
guard !surfaces.isEmpty else { return nil }
return .pane(CmuxPaneDefinition(surfaces: surfaces))
Expand Down
Loading