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
Original file line number Diff line number Diff line change
Expand Up @@ -306,7 +306,7 @@ extension PaneController {
/// Selects `surface`'s tab in this pane once the store reports it, and
/// moves no keyboard focus: an agent's tab opened beside its chat.
func selectWhenReportedKeepingFocus(surface: SurfaceID) {
pendingSelectSurface = surface
pendingSelect.surface = surface
apply(snapshot())
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ struct NewTabSuccession {
handed = pane.pane.tabs.first { $0.id == arrived[0].id.rawValue }?.surface
// The selection goes with the cell, in the same apply: a selected stand-in never
// hands the pane to a neighbor for the frames until its creator hears the reply.
if pane.stripModel.selectedID == provisional, let handed { pane.pendingSelectSurface = handed }
if pane.stripModel.selectedID == provisional, let handed { pane.pendingSelect.surface = handed }
}
guard var succession = active[ObjectIdentifier(pane.pane)] else { return snapshot }
guard ContinuousClock.now < succession.deadline else {
Expand All @@ -119,12 +119,12 @@ struct NewTabSuccession {
var snapshot = snapshot
// The store's tab after a provisional one, the one made, else the tab being selected.
let tabs = pane.pane.tabs.filter { !succession.known.contains($0.id) }
let made = [handed, succession.surface, pane.pendingSelectSurface].lazy.compactMap { $0 }.first { surface in tabs.contains { $0.surface == surface } }
let made = [handed, succession.surface, pane.pendingSelect.surface].lazy.compactMap { $0 }.first { surface in tabs.contains { $0.surface == surface } }
guard let made, let tab = tabs.first(where: { $0.surface == made }), let item = snapshot.items.first(where: { $0.id.rawValue == tab.id }) else {
// Not answered yet: a new daemon tab of this pane waits for the answer naming this one
// (a reopened tab being selected never does). One the strip already shows stays: hidden,
// the selection would take it for closed and move to a neighbor (census #7).
let fresh = Set(tabs.map(\.id)).subtracting(shown.map(\.rawValue)).subtracting(pane.pendingSelectTab.map { [$0] } ?? [])
let fresh = Set(tabs.map(\.id)).subtracting(shown.map(\.rawValue)).subtracting(pane.pendingSelect.tab.map { [$0] } ?? [])
snapshot.items.removeAll { fresh.contains($0.id.rawValue) }
return snapshot
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ final class OutboundTab {
/// The tab `pane` shows or waits to select: never the open's (made unselected).
private func shownByPane(_ tab: TabModel) -> Bool {
guard let pane else { return false }
return tab.surface == pane.pendingSelectSurface || tab.id == pane.pendingSelectTab || tab.id == pane.stripModel.selectedID?.rawValue
return tab.surface == pane.pendingSelect.surface || tab.id == pane.pendingSelect.tab || tab.id == pane.stripModel.selectedID?.rawValue
}

/// Keeps the open's stand-ins listed now (provisional, new and not the pane's) out until they go.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -268,8 +268,8 @@ extension PaneBrowserTabOpener {
let surface = try await pending.send { surface in
if let tab = controller.daemon.store.tab(surface: surface) { _ = cache.moveBrowser(pending.tab, to: tab) }
}
let selected = controller.stripModel.selectedID?.rawValue
if selected == nil || selected == pending.tab || controller.pendingSelectSurface == pending.surface { controller.selectWhenReported(surface: surface) }
let selected = (controller.successor ?? controller).stripModel.selectedID?.rawValue // after Cmd-N's hand-over, the daemon pane's
if selected == nil || selected == pending.tab || (controller.successor ?? controller).pendingSelect.surface == pending.surface { controller.selectWhenReported(surface: surface) }
controller.workspace?.expectFocus(on: surface, target: focusesAddressBar ? .addressBar : .content, generation: intent)
landed = true
then?(surface)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -206,9 +206,9 @@ extension PaneController {
await previous?.value
do {
let created = try await pending.send(); BenchSpans.mark("daemon.newTab.returned")
// The swap moved the selection already (NewTabSuccession); a pane still on the stand-in follows it.
// The swap moved the selection already (NewTabSuccession); a pane still on the stand-in follows it (its `successor`'s).
if selectHere {
if stripModel.selectedID?.rawValue == pending.tab { selectWhenReported(surface: created.surface) }
if (successor ?? self).stripModel.selectedID?.rawValue == pending.tab { selectWhenReported(surface: created.surface) }
self.workspace?.expectFocus(on: created.surface, generation: intent)
}
landed = true
Expand Down
33 changes: 16 additions & 17 deletions Packages/macOS/CmuxNext/Sources/CmuxNextApp/PaneController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,8 @@ final class PaneController: SurfacePresenter, PresentablePane {
private(set) var presence: SurfacePresence = .hidden
var isVisible: Bool { presence == .visible }
var pendingDock = PendingDockChats() // chats bound for a new chat dock, never in this strip (NewChatPlacement)
/// A tab this app just created here; selected once the daemon reports it (`selectWhenReported`).
var pendingSelectSurface: SurfaceID? // set by selectWhenReported* (AgentBesidePlacement)
/// Same, named by tab resource id (a reopened tab's restored view).
private(set) var pendingSelectTab: String?
/// A tab this app just created or reopened here, selected once the daemon reports it (`selectWhenReported*`).
var pendingSelect = PendingTabSelect()
private var observation: Task<Void, Never>?
/// One observation per daemon tab's strip item and focus kind (`focusPane` reads it; I1, I2).
private lazy var tabItems = ObservedItems<TabModel, (StripTabItem, FocusTopology.Kind)>(
Expand Down Expand Up @@ -122,6 +120,10 @@ final class PaneController: SurfacePresenter, PresentablePane {
#if DEBUG
DebugLayoutCounters.note("pane mount", #fileID, #line)
#endif
// Cmd-N's stand-in this pane takes over (another layout id): what it waited to select, or selected, selects here now.
if !ProvisionalPane.isStandIn(pane.handle), let standIn = workspace?.panes.values.first(where: { daemon.store.daemonPane(takingOver: $0.pane.handle) == pane.handle }) {
pendingSelect.takeOver(standIn.pendingSelect, selected: standIn.stripModel.selectedID?.rawValue) { daemon.store.creations.replacement(ofStandIn: $0, in: pane)?.id }
}
observe()
}

Expand Down Expand Up @@ -340,16 +342,8 @@ final class PaneController: SurfacePresenter, PresentablePane {
}
if stripModel.showsNewTabButton != snapshot.takesNewTabs { stripModel.showsNewTabButton = snapshot.takesNewTabs }
if !snapshot.items.isEmpty { LaunchReveal.shared.markReady(.tabs) }
var selectNew = false
if let pending = pendingSelectSurface, let tab = snapshot.tabs.first(where: { $0.surface == pending }) { // once listed (OutboundTab)
state?.selection.select(tab.id, in: paneKey)
pendingSelectSurface = nil
selectNew = true
} else if let pending = pendingSelectTab, let tab = pane.tabs.first(where: { $0.id == pending }) {
state?.selection.select(tab.id, in: paneKey)
pendingSelectTab = nil
selectNew = true
}
let selectNew = pendingSelect.take(shown: snapshot.tabs, record: pane.tabs) // once listed (OutboundTab)
if let selectNew { state?.selection.select(selectNew, in: paneKey) }
// Members of a collapsed group are hidden: a closed selected tab's successor skips them while a shown tab survives (close-focus.md).
let collapsed = Set(snapshot.groups.filter(\.isCollapsed).map(\.id))
let hidden = Set(snapshot.items.filter { $0.groupID.map(collapsed.contains) ?? false }.map(\.id.rawValue))
Expand All @@ -358,7 +352,7 @@ final class PaneController: SurfacePresenter, PresentablePane {
setStripSelection(selected.map { StripTabID($0) })
let selectedID = stripModel.selectedID
sendTopologyIfChanged() // focus learns the selection before the show: the keyboard leaves the old tab first (showSelected)
if selectNew {
if selectNew != nil {
// A tab this window created: show it now (focus is the coordinator's expectation, not decided here).
showSelected()
} else if let selectedID, selectedID.rawValue != currentTabKey {
Expand Down Expand Up @@ -395,20 +389,25 @@ final class PaneController: SurfacePresenter, PresentablePane {
workspace?.setNeedsTopology()
}

/// The daemon pane's controller that took this stand-in over (Cmd-N's): a create made here that lands later selects there.
var successor: PaneController? { daemon.store.daemonPane(takingOver: pane.handle).flatMap { workspace?.pane(for: $0) } }

/// Selects the tab on `surface`, which this app just created here, once
/// the daemon reports it, and shows it now when it already does. An
/// action run without view-change permission (a CLI, script or agent
/// run without `focus: true`) creates the tab in the background.
func selectWhenReported(surface: SurfaceID) {
if let successor { return successor.selectWhenReported(surface: surface) }
guard ActionRunScope.viewChangeAllowed() else { return apply(snapshot()) }
pendingSelectSurface = surface
pendingSelect.surface = surface
apply(snapshot())
}

/// Same, for a tab named by its resource id (a reopened tab).
func selectWhenReported(tab: String) {
if let successor { return successor.selectWhenReported(tab: tab) }
guard ActionRunScope.viewChangeAllowed() else { return apply(snapshot()) }
pendingSelectTab = tab
pendingSelect.tab = tab
apply(snapshot())
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,4 +81,13 @@ extension DaemonStore {
func adoptedStandIn(_ handle: PaneID) -> WorkspaceKey? {
workspaces.first { $0.tookOverStandIn?.pane == handle }?.key
}

/// The daemon's pane that took stand-in pane `handle` over (Cmd-N's first pane, a split's) once the records hold it; nil
/// while the stand-in still shows, and for a pane that is no stand-in. A window shows Cmd-N's daemon pane under another
/// layout id than its stand-in, so it hands the stand-in's selection to that pane's new controller by this.
public func daemonPane(takingOver handle: PaneID) -> PaneID? {
guard ProvisionalPane.isStandIn(handle), let origin = standInTargets[handle] ?? ProvisionalSplit.standInOrigin(of: handle, in: self),
let pane = ProvisionalSplit.daemonPane(of: origin, showing: handle, in: self), !ProvisionalPane.isStandIn(pane) else { return nil }
return pane
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/// The tab a pane selects once the daemon reports it: one this app just created there (`surface`) or reopened there
/// (`tab`, its resource id). A pane's view state; its apply takes it the first time its tabs list it (``take(shown:record:)``).
public struct PendingTabSelect: Sendable {
/// A created tab's surface: selected once the pane's shown tabs list it.
public var surface: SurfaceID?
/// A tab named by its resource id (a reopened tab's restored view): selected once the pane's record lists it.
public var tab: String?

public init(surface: SurfaceID? = nil, tab: String? = nil) {
self.surface = surface
self.tab = tab
}

/// The tab to select now, which then waits no more: the surface's tab once `shown` lists it, else the tab once
/// `record` lists it. Nil while neither does; both stay as they were then.
@MainActor public mutating func take(shown: [TabModel], record: [TabModel]) -> String? {
if let surface, let made = shown.first(where: { $0.surface == surface }) {
self.surface = nil
return made.id
}
if let tab, record.contains(where: { $0.id == tab }) {
self.tab = nil
return tab
}
return nil
}

/// The hand-over of Cmd-N's stand-in pane to the daemon's pane that takes it over under another layout id, so under a
/// new pane controller (P0 STATUS 13438): what the stand-in waited to select (`standIn`), else the tab it showed
/// selected (`selected`, as the daemon's tab in its place once the records list one: `replacement`). One value, the
/// waiting surface first, as ``take(shown:record:)`` takes them: a second would select later what nobody chose then.
public mutating func takeOver(_ standIn: PendingTabSelect, selected: String?, replacement: (String) -> String?) {
if let surface = standIn.surface {
self = PendingTabSelect(surface: surface)
} else if let tab = standIn.tab ?? selected.map({ replacement($0) ?? $0 }) {
self = PendingTabSelect(tab: tab)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,7 @@ struct ClickedLinkFocusTests {
try await PinnedPageTests.until({ content.focusedPane === shown() && shown().stripModel.selectedID?.rawValue == opened },
report: { Self.describe(content, opened) })
agentPlaced(agents.surface)
try await PinnedPageTests.until({ shown().pendingSelectSurface == nil && right.pendingSelectSurface == nil })
try await PinnedPageTests.until({ shown().pendingSelect.surface == nil && right.pendingSelect.surface == nil })
#expect(content.focusedPane === shown(), "the agent's open took the focus from the clicked link")
#expect(shown().stripModel.selectedID?.rawValue == opened, "the agent's tab replaced the clicked link's tab: \(Self.describe(content, opened))")
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ struct CloudDiffPlacementTests {
// A late success after failure cannot select or move a tab.
opening?.created(1)
for task in work { #expect(await task.value?.message == "expected creation failure") }
#expect(pane.pendingSelectSurface == nil)
#expect(pane.pendingSelect.surface == nil)
}

@Test func unsupportedBesideDiffRefusesWithoutAnAppOnlyFallback() async throws {
Expand Down
Loading
Loading