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 @@ -94,9 +94,26 @@ final class LocationTrailService {
// the forward entries of a Back or Forward between workspaces.
if let shown = controller.state.workspaceID, state.topology.workspace != shown { return }
guard let location = location(of: state, in: controller) else { return }
if let jump = jumpNotedAt, now().timeIntervalSince(jump) < Self.jumpWindow, location.key != trail.current?.location.key {
jumpNotedAt = nil
if trail.recordJump(location, at: now()) { changed() }
return
}
if trail.record(location, at: now(), scope: stepScope) { changed() }
}

/// When the user last picked a jump (``noteJump()``).
private var jumpNotedAt: Date?
/// How long a noted jump waits for the focus it causes to settle.
private static let jumpWindow: TimeInterval = 2

/// The user picked a place to jump to (the New Tab page's location bar):
/// the next settled location elsewhere is its own step, even in the same
/// workspace, so Back returns to where they jumped from.
func noteJump() {
jumpNotedAt = now()
}

/// What a step is (`navigation.history.scope`, BACK-FORWARD-WORKSPACES-ONLY).
var stepScope: HistoryStepScope {
services.settings.flatMap { HistoryStepScope(rawValue: $0.snapshot.navigationHistorySteps) } ?? .default
Expand Down
2 changes: 2 additions & 0 deletions Packages/macOS/CmuxNext/Sources/CmuxNextApp/NewTabPage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,8 @@ enum NewTabPage {
/// Through the palette's switchers, the one path that reveals a tab's or
/// workspace's window and selects it.
static func jump(_ target: AgentPaneJumpTarget, id: String, services: AppServices) {
// Back returns to this page (Leo 2026-10-06).
services.locationTrail.noteJump()
switch target {
case .tab: PaletteSourcesBridge.TabSource(services: services).selectTab(id: id)
case .workspace: PaletteSourcesBridge.WorkspaceSource(services: services).selectWorkspace(id: id)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,15 @@ public nonisolated struct LocationTrail: Hashable, Sendable, Codable {
return true
}

/// Records a location the user jumped to on purpose (the New Tab page's
/// location bar): always its own step, never coalesced into the entry it
/// left, so Back returns there.
@discardableResult
public mutating func recordJump(_ location: HistoryLocation, at time: Date) -> Bool {
recordedAt = nil
return record(location, at: time)
}

/// Moves to the newest older entry that `isAvailable`, marks it pending,
/// and returns it (nil: nothing to go back to).
public mutating func back(isAvailable: (HistoryLocation) -> Bool = { _ in true }) -> Entry? {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import AppKit
import CmuxNextActions
import CmuxNextAgentPane
@testable import CmuxNextApp
import CmuxNextDaemon
import CmuxNextDesign
Expand Down Expand Up @@ -170,4 +171,28 @@ struct LocationTrailWiringTests {
return false
})
}

/// Leo (2026-10-06): a jump from the New Tab page's location bar to a tab
/// in the same workspace is a Back step under the default workspace steps.
@Test func aNewTabJumpInsideTheWorkspaceIsABackStep() async throws {
let services = ActionBindingCoverageTests.boundServices()
services.windows.ordersWindowsIn = false
services.daemon.store.apply(snapshot: try Self.tree(tabs: [7, 8]))
let controller = try #require(services.windows.openWindow(workspaces: [Self.key]))
services.windows.reconcileMembership()
defer { controller.window?.close() }
let pane = try #require(services.daemon.store.workspaces.first?.screens.first?.panes.first)
let (page, chat) = (pane.tabs[0].id, pane.tabs[1].id)
let trail = services.locationTrail
var clock = Date(timeIntervalSince1970: 1_800_000_000)
trail.now = { clock }

await Self.settle { controller.focus.state.topology.contains(pane: pane.id) }
controller.focus.send(.selectTab(pane: pane.id, tab: page, workspace: Self.key, source: .mouse))
await Self.settle { trail.trail.current?.location.key.tab == page }
clock += 0.2
NewTabPage.jump(.tab, id: chat, services: services)
await Self.settle { trail.trail.current?.location.key.tab == chat }
#expect(trail.trail.entries.map(\.location.key.tab) == [page, chat])
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -46,3 +46,16 @@ struct LocationTrailStepTests {
#expect(HistoryStepScope.default == .workspaces)
}
}

/// Leo (2026-10-06): after jumping from a New Tab page to a chat, Back
/// returns to the New Tab page. A jump the user picked is its own step,
/// even inside one workspace and right after the page opened.
struct LocationTrailJumpTests {
@Test func aJumpIsAStepInsideAWorkspaceAndBackReturnsToWhereItStarted() {
var trail = LocationTrailStepTests.trail([LocationTrailStepTests.tab("newtab", workspace: "A")], scope: .workspaces)
// Sooner than the coalesce interval, in the same workspace.
trail.recordJump(LocationTrailStepTests.tab("chat", workspace: "A"), at: LocationTrailStepTests.t0.addingTimeInterval(0.2))
#expect(trail.entries.map(\.location.key.tab) == ["newtab", "chat"])
#expect(trail.back(scope: .window)?.location.key.tab == "newtab")
}
}
Loading