From e2b6fe3723ef23c7baa6bd5ba1e2f6fb47227df7 Mon Sep 17 00:00:00 2001 From: lawrencecchen <54008264+lawrencecchen@users.noreply.github.com> Date: Mon, 2 Nov 2026 02:47:53 +0000 Subject: [PATCH 1/2] test(focus): a sidebar AppKit picks as a restored window comes forward never takes the restored terminal (REOPEN-F4, red on the stale CCmuxAppFFI pin) The Swift side of #30356: the app links cmux-focus-core only through the pinned CCmuxAppFFI (ae1499abb29), which predates the rule. On the feat tip, 10 of 15 Cmd-Q keep + reopen cycles on cmux21 lost the first key: the journal shows 'responder sidebar, programmatic' 41-55 ms after the window became key, and the focus then stayed on the sidebar. --- .../WindowFocusRestoreTests.swift | 56 +++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/Packages/macOS/CmuxNext/Tests/CmuxNextAppTests/WindowFocusRestoreTests.swift b/Packages/macOS/CmuxNext/Tests/CmuxNextAppTests/WindowFocusRestoreTests.swift index 0a3844dd1e5f..264ef6146d14 100644 --- a/Packages/macOS/CmuxNext/Tests/CmuxNextAppTests/WindowFocusRestoreTests.swift +++ b/Packages/macOS/CmuxNext/Tests/CmuxNextAppTests/WindowFocusRestoreTests.swift @@ -25,6 +25,62 @@ struct WindowFocusRestoreTests { #expect(state.focus.state.pane == "b2", "the focused pane of a workspace the window did not show was lost") } + /// REOPEN-F4 (STATUS 4618, 15 Cmd-Q keep + reopen cycles on cmux21): the restored window comes + /// forward before its focused terminal's surface is shown, and AppKit makes the sidebar list its + /// first key view. The app reports that as a programmatic responder change; the reducer took it as + /// the person's choice, so the focus stayed on the sidebar (`keyboard: false`) and the first key a + /// person typed went nowhere, in 10 of 15 cycles. The rule is cmux-focus-core's (#30356); the app + /// gets it only through the pinned CCmuxAppFFI, so this test is red on a pin without it. A + /// person's click on the sidebar still moves the focus there. + @Test func theSidebarAppKitPicksAsARestoredWindowComesForwardNeverTakesTheRestoredTerminal() { + var record = WindowRecord(id: "w1", workspaceKey: WorkspaceKey(rawValue: "ws-a"), workspaceKeys: [WorkspaceKey(rawValue: "ws-a")]) + record.focusedPane = "a2" + let state = WindowState(record: record) + let focused = FocusState.Resolved.terminal(pane: "a2", tab: "a2-tab") + + // The journal's order in the lost cycles: the topology, the window becomes key, then AppKit's first key view. + state.focus.send(.topology(Self.topology("ws-a", panes: ["a1", "a2", "a3"]))) + state.focus.send(.windowKey(true)) + #expect(state.focus.state.resolved == focused) + state.focus.responderDidChange(.sidebar, source: .programmatic) + #expect(state.focus.state.resolved == focused, + "the sidebar list AppKit picked took the restored terminal's keyboard: \(state.focus.state.resolved), target \(state.focus.state.target)") + #expect(state.focus.state.target == .content) + // The terminal's surface shows a frame later: the focus is still the restored terminal's. + state.focus.send(.contentPresented(pane: "a2")) + #expect(state.focus.state.resolved == focused) + + // The person's own click on the sidebar list still moves the focus there. + state.focus.responderDidChange(.sidebar, source: .mouse) + #expect(state.focus.state.resolved == .sidebar) + } + + /// REOPEN-F5 (STATUS 4618, journey reopen-kill-all-scrollback, 2 of 3 on cmux21): after a kill of + /// the app and every process of its bundle, the window reopens with its Home page column and one + /// terminal; the line typed about 2 s after the terminals settled never reached the new shell. The + /// same cause as F4: the sidebar AppKit picked as the window came forward kept the focus for good, + /// so a key long after the terminal's surface showed still went to the sidebar list. + @Test func aKillAllReopenWithAHomeColumnKeepsTheTerminalFocusedLongAfterItsSurfaceShows() { + var record = WindowRecord(id: "w1", workspaceKey: WorkspaceKey(rawValue: "ws-h"), workspaceKeys: [WorkspaceKey(rawValue: "ws-h")]) + record.focusedPane = "term" + let state = WindowState(record: record) + let home = FocusTopology.Pane(id: "home", tabs: [.init(id: "home-tab", kind: .page)], selected: "home-tab") + let terminal = FocusTopology.Pane(id: "term", tabs: [.init(id: "term-tab", kind: .terminal)], selected: "term-tab") + let focused = FocusState.Resolved.terminal(pane: "term", tab: "term-tab") + + state.focus.send(.topology(FocusTopology(workspace: "ws-h", panes: [home, terminal], + screens: [[.init(id: "column:1", panes: ["home"]), .init(id: "column:2", panes: ["term"])]]))) + state.focus.send(.windowKey(true)) + state.focus.responderDidChange(.sidebar, source: .programmatic) + state.focus.send(.contentPresented(pane: "home")) + state.focus.send(.contentPresented(pane: "term")) + // Nothing else happens until the typed line, about 2 s later (the lost cycles have no other focus event). + state.focus.send(.appActive(true)) + #expect(state.focus.state.resolved == focused, + "the typed line after a kill-all reopen goes to \(state.focus.state.resolved), not the restored terminal") + #expect(state.focus.state.context.terminal, "the window does not publish a terminal focus: terminal shortcuts and typing miss it") + } + private static func topology(_ workspace: String, panes: [String]) -> FocusTopology { FocusTopology(workspace: workspace, panes: panes.map { pane in FocusTopology.Pane(id: pane, tabs: [.init(id: "\(pane)-tab", kind: .terminal)], selected: "\(pane)-tab") From 124b6680c406c7e00fd6b85e539549d33e3a5e1c Mon Sep 17 00:00:00 2001 From: lawrencecchen <54008264+lawrencecchen@users.noreply.github.com> Date: Mon, 2 Nov 2026 02:49:09 +0000 Subject: [PATCH 2/2] fix(focus): pin CCmuxAppFFI to cmux-app-ffi-382e54b7 (#30356's release): a sidebar AppKit picks never takes a restored terminal's keyboard (REOPEN-F4, REOPEN-F5) check-app-ffi-pin.sh failed on every feat-cmux-next push since #30356 changed cmux-focus-core: app-ffi-release published cmux-app-ffi-382e54b7 (2026-11-01 07:40Z), and its bot repin #26434 stayed red on base reds of that day. HEAD's FFI sources match 382e54b7; --verify-release passes. With the stale pin the app never got #30356's rule: at a relaunch AppKit makes the sidebar list the first key view before the restored pane's surface shows, and the focus stayed on the sidebar. The first key after a Cmd-Q keep + reopen was lost in 10 of 15 cycles (REOPEN-F4), and the line typed after a kill-all reopen never reached the shell (REOPEN-F5). --- Packages/macOS/CmuxNext/Package.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Packages/macOS/CmuxNext/Package.swift b/Packages/macOS/CmuxNext/Package.swift index bff24d0fb132..e18695f15247 100644 --- a/Packages/macOS/CmuxNext/Package.swift +++ b/Packages/macOS/CmuxNext/Package.swift @@ -173,8 +173,8 @@ let daemonSwiftSettings: [SwiftSetting] = [ /// when the FFI sources differ from the pinned source sha. let appFFI: Target = .binaryTarget( name: "CCmuxAppFFI", - url: "https://github.com/manaflow-ai/cmux/releases/download/cmux-app-ffi-ae1499abb29f7c7fb179e4691e76072161788d58/CCmuxAppFFI.xcframework.zip", - checksum: "80eca4b06ee28c58070679a6b50946a2a2bae724eb97b98aadf488aca7de37ef" + url: "https://github.com/manaflow-ai/cmux/releases/download/cmux-app-ffi-382e54b7d1408493c7a13c09bffedee20cfdcc97/CCmuxAppFFI.xcframework.zip", + checksum: "40d3608d2f5a666802f5b5d0b59698665cb4278a14cf0a2044962510f3c370db" ) // Debug-only product consumed by Prototypes/UiLayerHost. Keeping this leaf