From d4747ac9d75a8faf1210e90ae1c06007ffdb8a4e Mon Sep 17 00:00:00 2001 From: Leo Li Date: Tue, 6 Oct 2026 10:33:43 -0700 Subject: [PATCH 1/2] test(cmux-next): require Back to return to the New Tab page after a jump Co-Authored-By: Claude Opus 5.5 --- .../LocationTrailWiringTests.swift | 25 +++++++++++++++++++ .../LocationTrailStepTests.swift | 13 ++++++++++ 2 files changed, 38 insertions(+) diff --git a/Packages/macOS/CmuxNext/Tests/CmuxNextAppTests/LocationTrailWiringTests.swift b/Packages/macOS/CmuxNext/Tests/CmuxNextAppTests/LocationTrailWiringTests.swift index 0bd69b4d20c7..5c5f2d0c9786 100644 --- a/Packages/macOS/CmuxNext/Tests/CmuxNextAppTests/LocationTrailWiringTests.swift +++ b/Packages/macOS/CmuxNext/Tests/CmuxNextAppTests/LocationTrailWiringTests.swift @@ -1,5 +1,6 @@ import AppKit import CmuxNextActions +import CmuxNextAgentPane @testable import CmuxNextApp import CmuxNextDaemon import CmuxNextDesign @@ -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]) + } } diff --git a/Packages/macOS/CmuxNext/Tests/CmuxNextHistoryTests/LocationTrailStepTests.swift b/Packages/macOS/CmuxNext/Tests/CmuxNextHistoryTests/LocationTrailStepTests.swift index 4d4cc4ca31eb..68a2e4a289f3 100644 --- a/Packages/macOS/CmuxNext/Tests/CmuxNextHistoryTests/LocationTrailStepTests.swift +++ b/Packages/macOS/CmuxNext/Tests/CmuxNextHistoryTests/LocationTrailStepTests.swift @@ -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") + } +} From 0daf692fe297fa662d31671ef44caa8566e18239 Mon Sep 17 00:00:00 2001 From: Leo Li Date: Tue, 6 Oct 2026 10:34:36 -0700 Subject: [PATCH 2/2] fix(cmux-next): make a New Tab jump a Back step A tab picked from the New Tab page's location bar is now its own step on the location trail, even in the same workspace and right after the page opened, so Back returns to the New Tab page. Co-Authored-By: Claude Opus 5.5 --- .../History/LocationTrailService.swift | 17 +++++++++++++++++ .../Sources/CmuxNextApp/NewTabPage.swift | 2 ++ .../Sources/CmuxNextHistory/LocationTrail.swift | 9 +++++++++ 3 files changed, 28 insertions(+) diff --git a/Packages/macOS/CmuxNext/Sources/CmuxNextApp/History/LocationTrailService.swift b/Packages/macOS/CmuxNext/Sources/CmuxNextApp/History/LocationTrailService.swift index f37a18ac69a0..79550a63158c 100644 --- a/Packages/macOS/CmuxNext/Sources/CmuxNextApp/History/LocationTrailService.swift +++ b/Packages/macOS/CmuxNext/Sources/CmuxNextApp/History/LocationTrailService.swift @@ -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 diff --git a/Packages/macOS/CmuxNext/Sources/CmuxNextApp/NewTabPage.swift b/Packages/macOS/CmuxNext/Sources/CmuxNextApp/NewTabPage.swift index c48d717c0787..355ffbad1422 100644 --- a/Packages/macOS/CmuxNext/Sources/CmuxNextApp/NewTabPage.swift +++ b/Packages/macOS/CmuxNext/Sources/CmuxNextApp/NewTabPage.swift @@ -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) diff --git a/Packages/macOS/CmuxNext/Sources/CmuxNextHistory/LocationTrail.swift b/Packages/macOS/CmuxNext/Sources/CmuxNextHistory/LocationTrail.swift index cef5a09b172b..4d1d60c141a7 100644 --- a/Packages/macOS/CmuxNext/Sources/CmuxNextHistory/LocationTrail.swift +++ b/Packages/macOS/CmuxNext/Sources/CmuxNextHistory/LocationTrail.swift @@ -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? {