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 @@ -55,12 +55,32 @@ extension PaneHandlers {
let controller = ctx.services.paneController(for: pane)
// The pane's own daemon decides: a remote pane's close waits for no local connection.
let shown = ctx.services.daemon(for: pane).connection == nil ? nil : controller?.showPaneClose()
let store = ctx.services.daemon(for: pane).store
// A stand-in pane still opening (a dock's, Cmd-N's first pane) closes too, never refused ("This pane is
// still opening", Leo 2026-11-03: "i should be able to just close immedeiately"): it leaves the records in
// this turn (and the daemon's pane that takes it over stays out), and its close waits, off the key's turn,
// for that daemon pane, as a create on it does (`DaemonStore.daemonTarget`). One never made has nothing to close.
let closing = store.intendStandInPaneClose(handle)
ctx.send("close-pane") { connection in
do {
try await connection.closePane(handle)
try await connection.closePane(store.daemonTarget(handle))
if let closing {
let sequence = await connection.eventSequence()
await MainActor.run {
if let sequence { store.noteSettled(closing, at: sequence) } else { store.noteSettledAtNextSnapshot(closing) }
}
}
await MainActor.run { controller?.finishPaneClose(shown, failed: false) }
} catch is PendingPaneNotCreated {
await MainActor.run {
if let closing { store.rejectIntent(closing) }
controller?.finishPaneClose(shown, failed: false)
}
} catch {
await MainActor.run { controller?.finishPaneClose(shown, failed: true) }
await MainActor.run {
if let closing { store.rejectIntent(closing) }
controller?.finishPaneClose(shown, failed: true)
}
throw error
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,52 @@ import Foundation
switch intent {
case .createTab(_, let provisional): provisional.surface
case .splitPane(_, _, _, let provisional): provisional.surface
case .dockTab(_, _, _, _, let standIn): standIn?.surface
default: nil
}
}

/// Whether `transaction`'s creation goes on when its stand-in tab closes (a dock: the chat still docks; only its
/// fresh tab is closed, ``closeStandIn(_:in:)``), rather than being cancelled whole.
static func keepsCreation(_ transaction: ClientTransactionID, in store: DaemonStore) -> Bool {
guard case .dockTab? = store.intentLog.entries.first(where: { $0.transaction == transaction })?.kind else { return false }
return true
}

/// The person closed the stand-in tab of `transaction` while its creation goes on (Leo 2026-11-03: "i should be
/// able to just close immedeiately"; #33829): the stand-in leaves the visible state now, and the daemon's fresh tab
/// is hidden when it arrives and closed as soon as it is known (``requestCloses(in:)``), never shown.
static func closeStandIn(_ transaction: ClientTransactionID, in store: DaemonStore) {
guard store.intentLog.markStandInClosed(transaction) else { return }
store.withOverlayLifted(writer: "stand-in closed") {}
requestCloses(in: store)
}

/// The daemon's tab that replaces a closed stand-in, in the records: for a dock, the tab of its pane that runs
/// the stand-in's terminal (the client minted that id, so no other tab has it).
static func freshTab(of entry: PendingIntent, in store: DaemonStore) -> TabModel? {
guard case .dockTab(_, let source, _, _, let standIn?) = entry.kind, let terminal = standIn.terminalID,
let pane = store.panesByHandle[source] else { return nil }
return pane.tabs.first { $0.terminalID == terminal && !ProvisionalTab.isProvisional(surface: $0.surface) }
}

/// The overlay of a creation whose stand-in was closed: the creation's own overlay without the stand-in, then
/// the daemon's fresh tab taken out of the visible records once they hold it.
static func restoreClosedStandIn(_ entry: PendingIntent, in store: DaemonStore) -> IntentUndo? {
guard case .dockTab(let surface, let source, let provisional, let dock, _) = entry.kind else { return nil }
let overlay = ProvisionalSplit.paneGone(entry, in: store) ? nil
: ProvisionalDock.apply(surface: surface, from: source, provisional: provisional, dock: dock, standIn: nil, to: store)
let hidden = freshTab(of: entry, in: store).flatMap { hideTab($0.surface, in: store) }
guard overlay != nil || hidden != nil else { return nil }
return .closedStandIn(overlay: overlay, hidden: hidden)
}

/// Whether the confirmed records still hold the daemon's fresh tab of a closed stand-in (its close not committed).
static func freshTabHeld(_ entry: PendingIntent, in store: DaemonStore) -> Bool {
if let fresh = freshTab(of: entry, in: store) { return store.tabsBySurface[fresh.surface] != nil }
return false
}

/// Whether the confirmed records hold what the cancelled creation made (the overlay is lifted).
static func daemonHolds(_ entry: PendingIntent, in store: DaemonStore) -> Bool {
if let surface = entry.createdSurface ?? entry.replySurface, store.tabsBySurface[surface] != nil { return true }
Expand All @@ -43,6 +85,7 @@ import Foundation
switch entry.undo {
case .hiddenTab(let hidden)?: hidden.tab.surface
case .hiddenPane(let hidden)?: hidden.pane.tabs.first?.surface ?? entry.createdSurface ?? entry.replySurface
case .closedStandIn(_, let hidden)?: hidden?.tab.surface
default: entry.createdSurface ?? entry.replySurface
}
}
Expand All @@ -59,38 +102,65 @@ import Foundation
guard let surface = entry.createdSurface ?? entry.replySurface, let hidden = hideTab(surface, in: store) else { return nil }
return .hiddenTab(hidden)
case .splitPane(_, _, _, let provisional):
guard let pane = daemonPane(provisional, in: store), let screen = store.screen(containing: pane.handle),
let index = screen.panes.firstIndex(where: { $0 === pane }) else { return nil }
let layout = screen.layout, columns = screen.columns
// A tree the pane is the whole of (it would leave no pane) keeps it shown until the close.
guard let unsplit = removing(pane.handle, from: layout) else { return nil }
var reached = true
// A column that is only this pane (New Column's) goes with it.
let unsplitColumns = columns.filter { $0.layout != .leaf(pane.handle) }.map { column -> ColumnSnapshot in
guard column.layout.paneIDs.contains(pane.handle) else { return column }
var column = column
guard let tree = removing(pane.handle, from: column.layout) else { reached = false; return column }
column.layout = tree
// A row that is only this pane (New Row's) goes with it.
column.rows = column.rows.filter { $0.layout != .leaf(pane.handle) }.map { row in
guard row.layout.paneIDs.contains(pane.handle) else { return row }
var row = row
if let tree = removing(pane.handle, from: row.layout) { row.layout = tree } else { reached = false }
return row
}
return column
}
guard reached else { return nil }
store.index.remove(pane)
screen.panes.remove(at: index)
screen.layout = unsplit
if unsplitColumns != columns { screen.columns = unsplitColumns }
return .hiddenPane(HiddenPane(pane: pane, screen: screen.handle, index: index, layout: layout, columns: columns))
guard let pane = daemonPane(provisional, in: store), let hidden = hidePane(pane, in: store) else { return nil }
return .hiddenPane(hidden)
default:
return nil
}
}

/// The pane a ``Intent/closeStandInPane(pane:paneID:workspace:)`` takes out: the stand-in while it shows, else
/// the daemon's pane that took it over.
static func closingPane(_ intent: Intent, in store: DaemonStore) -> PaneModel? {
guard case .closeStandInPane(let handle, let paneID, let workspace) = intent else { return nil }
if let pane = store.panesByHandle[handle] { return pane }
if let paneID, let pane = store.index.panesByResource[ResourceID(rawValue: paneID)], pane.handle != handle { return pane }
if let workspace, let adopted = ProvisionalWorkspaceOverlay.adoptedPane(of: workspace, in: store) { return store.panesByHandle[adopted] }
return nil
}

/// The daemon's pane that a pending close of stand-in `handle` hides, once the daemon made it.
static func hiddenDaemonPane(closing handle: PaneID, in store: DaemonStore) -> PaneID? {
for entry in store.intentLog.entries {
guard case .closeStandInPane(let pane, _, _) = entry.kind, pane == handle,
case .hiddenPane(let hidden)? = entry.undo, hidden.pane.handle.rawValue < ProvisionalPane.firstHandle else { continue }
return hidden.pane.handle
}
return nil
}

/// Takes `pane` out of the visible records with its screen's trees (its split collapses; a column or row that is
/// only this pane goes with it). Nil when it is its tree's only pane (it would leave no pane): it stays shown.
static func hidePane(_ pane: PaneModel, in store: DaemonStore) -> HiddenPane? {
guard let screen = store.screen(containing: pane.handle),
let index = screen.panes.firstIndex(where: { $0 === pane }) else { return nil }
let layout = screen.layout, columns = screen.columns
// A tree the pane is the whole of (it would leave no pane) keeps it shown until the close.
guard let unsplit = removing(pane.handle, from: layout) else { return nil }
var reached = true
// A column that is only this pane (New Column's) goes with it.
let unsplitColumns = columns.filter { $0.layout != .leaf(pane.handle) }.map { column -> ColumnSnapshot in
guard column.layout.paneIDs.contains(pane.handle) else { return column }
var column = column
guard let tree = removing(pane.handle, from: column.layout) else { reached = false; return column }
column.layout = tree
// A row that is only this pane (New Row's) goes with it.
column.rows = column.rows.filter { $0.layout != .leaf(pane.handle) }.map { row in
guard row.layout.paneIDs.contains(pane.handle) else { return row }
var row = row
if let tree = removing(pane.handle, from: row.layout) { row.layout = tree } else { reached = false }
return row
}
return column
}
guard reached else { return nil }
store.index.remove(pane)
screen.panes.remove(at: index)
screen.layout = unsplit
if unsplitColumns != columns { screen.columns = unsplitColumns }
return HiddenPane(pane: pane, screen: screen.handle, index: index, layout: layout, columns: columns)
}

/// Takes the daemon's tab on `surface` out of the visible records, with its place.
static func hideTab(_ surface: SurfaceID, in store: DaemonStore) -> HiddenTab? {
guard let tab = store.tabsBySurface[surface], let pane = store.index.pane(holding: tab),
Expand Down Expand Up @@ -128,9 +198,10 @@ import Foundation
private static func hiddenTabs(_ undo: IntentUndo?) -> Int {
switch undo {
case .hiddenTab?: 1
case .hiddenPane(let hidden)?: hidden.pane.tabs.count
case .hiddenPane(let hidden)?: hidden.pane.tabs.count { !ProvisionalTab.isProvisional(surface: $0.surface) }
case .closedTabs(let hidden)?: hidden.count
case .guessedTab?: 1
case .closedStandIn(let overlay, let hidden)?: hiddenTabs(overlay) + (hidden == nil ? 0 : 1)
default: 0
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,11 @@ import Foundation
/// state now and the daemon's tab never shows. False for any other tab (it closes as usual).
public func cancel(tab id: String) -> Bool {
guard let transaction = CancelledCreation.transaction(showingTab: id, in: store) else { return false }
CancelledCreation.cancel(transaction, in: store)
if CancelledCreation.keepsCreation(transaction, in: store) {
CancelledCreation.closeStandIn(transaction, in: store)
} else {
CancelledCreation.cancel(transaction, in: store)
}
return true
}

Expand Down Expand Up @@ -74,7 +78,27 @@ import Foundation
}
}

extension DaemonStore { public var creations: DaemonCreations { DaemonCreations(store: self) } }
extension DaemonStore {
public var creations: DaemonCreations { DaemonCreations(store: self) }

/// A close of pane `handle` while it is still opening (a stand-in no cancel took, e.g. a dock's pane): it leaves
/// the visible records now, and the daemon's pane that takes it over stays out until the close commits. The
/// caller sends `close-pane` for ``daemonTarget(_:)``, then settles (`noteSettled`) or rejects the transaction.
/// Nil for a daemon pane (its close needs no intent).
public func intendStandInPaneClose(_ handle: PaneID) -> ClientTransactionID? {
guard handle.rawValue >= ProvisionalPane.firstHandle else { return nil }
let origin = standInTargets[handle] ?? ProvisionalSplit.standInOrigin(of: handle, in: self)
var paneID = panesByHandle[handle]?.resourceID?.rawValue, workspace: WorkspaceKey?
switch origin {
case .pane(let id)?: paneID = id
case .workspace(let key)?: workspace = key
case nil: break
}
let transaction = ClientTransactionID.generate()
intend(.closeStandInPane(pane: handle, paneID: paneID, workspace: workspace), transaction: transaction)
return transaction
}
}

extension CancelledCreation {
/// Marks `transaction`'s creation cancelled (once): the lift undoes its provisional tab or
Expand All @@ -88,7 +112,7 @@ extension CancelledCreation {
/// Hands each cancelled creation's daemon surface to the close once, as soon as it is known.
static func requestCloses(in store: DaemonStore) {
guard !store.overlayLifted else { return }
for entry in store.intentLog.entries where entry.closeWhenCreated && !entry.closeRequested {
for entry in store.intentLog.entries where (entry.closeWhenCreated || entry.standInClosed) && !entry.closeRequested {
// A create's tab paired by a guess (``ProvisionalTab/matched(_:in:)``) is hidden but
// closed only once the daemon names it for this transaction: a wrong guess never
// closes another create's tab.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ extension DaemonStore {
overlayLifted = true
liftOverlay()
let result = body()
for intent in intentLog.removeDue(appliedSequence: appliedSequence, snapshot: snapshot, awaiting: { ProvisionalSplit.awaitsRecords($0, in: self) }) {
for intent in intentLog.removeDue(appliedSequence: appliedSequence, snapshot: snapshot, awaiting: { ProvisionalSplit.awaitsRecords($0, snapshot: snapshot, in: self) }) {
intentSettlements.append((intent.transaction, .applied))
noteIntentLeft(intent, rejected: false)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ extension DaemonStore {
case .renameScreen(let screen, _), .setScreenColor(let screen, _), .setScreenIcon(let screen, _), .setScreenPinned(let screen, _):
return self.screen(screen).flatMap { index.workspace(holding: $0) }.map { [$0.handle] }
case .setWorkspaceGroupCollapsed, .setTabGroupCollapsed, .setRowHeights, .createTab, .bindAgentSession, .splitPane,
.createWorkspace, .closeTabs, .dockTab:
.createWorkspace, .closeTabs, .dockTab, .closeStandInPane:
return nil
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ struct StandInWait {

/// Where stand-in `handle` (made by `origin`) stands in `store` now.
@MainActor static func state(of handle: PaneID, origin: StandInOrigin?, in store: DaemonStore) -> State {
// The daemon's pane a pending close of this stand-in hides (``Intent/closeStandInPane(pane:paneID:workspace:)``)
// is out of the visible records, yet it is what that close must name.
if let hidden = CancelledCreation.hiddenDaemonPane(closing: handle, in: store) { return .adopted(hidden) }
guard let origin else { return .notCreated }
if let pane = ProvisionalSplit.daemonPane(of: origin, showing: handle, in: store), pane.rawValue < ProvisionalPane.firstHandle {
return .adopted(pane)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,11 @@ public enum Intent: Sendable, Hashable {
/// shows in the input's turn until the daemon's layout places the pane with that id or the
/// move is refused (``ProvisionalDock``).
case dockTab(surface: SurfaceID, pane: PaneID, provisional: ProvisionalPane, dock: DockSnapshot, standIn: TabSnapshot?)
/// `close-pane` of a pane still opening (a dock's stand-in pane; ``DaemonStore/intendStandInPaneClose(_:)``): the
/// pane leaves the visible records in the input's turn, and so does the daemon's pane that takes it over (the same
/// public `paneID`, or the pane of Cmd-N's workspace `workspace` that holds its first tab), until the daemon's
/// records drop it or the close fails. Leo 2026-11-03: "i should be able to just close immedeiately".
case closeStandInPane(pane: PaneID, paneID: String?, workspace: WorkspaceKey?)

/// The provisional pane a split or a dock shows until the daemon's pane with its public id.
var provisionalPane: ProvisionalPane? {
Expand Down
Loading
Loading