Skip to content

fix(focus): after a relaunch the saved pane of the shown workspace gets the keyboard, and Home does not take it (QA v2.2.2 bug 1) - #31852

Open
lawrencecchen wants to merge 10 commits into
feat-cmux-nextfrom
fix/dogfood-port-restored-pane
Open

lawrencecchen wants to merge 10 commits into
feat-cmux-nextfrom
fix/dogfood-port-restored-pane

Conversation

@lawrencecchen

@lawrencecchen lawrencecchen commented Nov 2, 2026 •

Copy link
Copy Markdown
Contributor

Summary

After Cmd-Q with a browser tab focused in the pane beside Home and a relaunch, the keys a person types go into Home's Chief message box (text for an agent), not to the browser page that had the keyboard (QA v2.2.2 bug 1).

Ports 77c9594 and 47d6a3f from cp/dogfood-v2-2-12-opt (with their companions 92f8d64, bde3363, 4033999 and c93f03a). On the dogfood line the restore rule ended in the app's FocusCoordinator only because that build could not take a reducer change; on feat the rule belongs to cmux-focus-core (plans/cmux-next/layer-ownership.md), so this PR puts it there.

Two causes, two fixes:

  1. cmux-focus-core (reducer.rs, RestoredPane). The launch window shows the home workspace before it adopts the saved window record. WindowState.adopt then sends the record's focused pane as restoredPane for the workspace the window already shows, and the rule dropped it (it only remembered a restore for a workspace the window does not show). Now, for the shown workspace, the saved pane becomes the focus (target content, the responder re-applied) while no user intent happened in the window (generation 0). A view that took the keyboard by itself (a programmatic report: Home's box, AppKit's own pick) does not keep it; a person's click or key does. The recorded Swift focus corpus has 4 restores for the shown workspace, all after an intent (generation 4 or 9), so no recorded step changes.
  2. Home (Swift). Home's unasked keyboard grab (TopHomePageView.show of a conversation nobody opened, and the message box's later install in MessagesLabHomeView.finishOpenKeyboard) read "the window itself is first responder" as "nothing has the keyboard". A Chromium page that has the keys leaves the cmux window itself first responder. Now the unasked path also asks the focus model: for a view in a pane the model lists, the model must keep the keyboard in that pane's content (FocusState.keepsKeyboard(inContentOf:), allowsUnaskedKeyboard(inContentOf:)). A conversation the person opens (a click, Return, a switch key, an action) takes the keyboard as before.

To keep CmuxNextApp from growing (arch ratchet app_lines), KeyRouter.mayHavePrimaryInput(_:) moves to FocusState.Resolved.mayHavePrimaryInput in CmuxNextFocus (same rule, two callers).

Repin: BLOCKED (do not land yet)

The app links cmux-focus-core only through the prebuilt CCmuxAppFFI pin. The R3b rule says the repin goes in this same PR, but STATUS 5013 (rust-migration) found that a PR cannot repin itself today: app-ffi-release.yml publishes only from feat-cmux-next, and the ffi-release environment admits only that ref. Decision 5013 waits for cmux-ad. Until a pre-merge release exists, this PR has no ready-to-land. Check the app FFI pin is red on this PR because of that (the reducer changed with no repin). Two Swift tests stay red on this head until the repin: theSavedPaneOfTheShownWorkspaceGetsTheKeyboard and aViewThatTookTheKeyboardByItselfDoesNotKeepTheRestore. The repin PR #31684 (REOPEN-F4) also changes the pin line; this PR does not touch it.

Testing

Swift (RestoredFocusTests, through the real relaunch path WindowState.adopt; fleet Mac lane, compile-checks passed first: 1b269db218a7d8e055324007 at the red ref, 8defc5b7b0aeaf74e15df608 at 56aa6b2):

  • Red at 27b2a65 (test only, base FFI), step ae90896f420025a69f24e491 on aws-m4pro-1: 2 failed on outcome (resolved stays page(home-pane), no moveResponder): theSavedPaneOfTheShownWorkspaceGetsTheKeyboard, aViewThatTookTheKeyboardByItselfDoesNotKeepTheRestore. 2 guard tests passed (thePersonsChoiceBeforeTheRecordStays, aRecordBeforeTheWorkspaceShowsStillLandsOnItsPane), as they do on the base rule too.
  • Head 56aa6b2, step 22aa69c245a7f9208f571c89 on aws-m4pro-7: homeTakesTheKeyboardUnaskedOnlyWhileTheModelKeepsItInHomesPane and the 2 guard tests pass; the same 2 restore tests fail the same way, as expected with the old pin. HomeTypeToFocusTests (9) and HomeOpenFocusesComposerTests (2) pass: the Home keyboard paths a person uses still work.

Rust (cmux-layout-reducer-ffi/tests/focus_rules.rs: a_record_for_the_shown_workspace_focuses_its_saved_pane, a_record_before_the_workspace_shows_still_lands_on_its_pane). The crate is its own Cargo workspace, so the fleet cmux-tui-rust-check.sh test steps (572f40404ba2, bbfa6a835f69) and the test (linux) dispatch jobs do not reach it; their green says nothing. The lint (linux) job runs it, but on every dispatch so far (runs 41394713812, 41394716337, 41397167791, 41397170316) a base clippy red stopped the job before that step (#31462's suspicious_open_options, fixed by #31697; then three clippy errors in cmux-tui-core on feat 947bb5e). The red and green go through a Testbox run of cargo test --test focus_rules in that crate; results are added here.

Observed / Not run

  • Observed: the Swift red and head results above; the CmuxNext compile at both refs.
  • Read only: the focus corpus has 4 restoredPane steps for the shown workspace, all at generation 4 or 9, so no recorded step changes (a script over tests/focus/corpus.json, repeated by the reviewer).
  • Not covered by a failing test: the Home gate itself (TopHomePageView.focusPrimaryInput(unasked:), MessagesLabHomeView openKeyboardAllowed). The new test checks only the focus-model predicate it asks; a revert of the gate keeps every test green. A gate test needs a real WindowController with a Home tab beside a browser page that has the keys; not written.
  • Known limit (same as the dogfood fix): a click into the pane that already has the focus, or keys typed before the record arrives, do not bump the generation, so a late record can still move focus once. The window is the short time before the first restore.
  • Not run: the QA journey (Cmd-Q with a browser tab beside Home, relaunch, type) on a tagged build. It needs the repinned FFI.

Rust red/green (cmux-focus-core, fleet, Linux)

cmux-tui-rust-check.sh test restored_pane runs cmux-tui/crates/cmux-focus-core/tests/restored_pane.rs (3 of the 5 restored-pane cases of focus_rules.rs, in the cmux-tui workspace; commit 77e15d9; the addressBar case stays only in focus_rules.rs, which the fleet step does not run):

  • Red at 9c816b5 (feat tip + that test only, branch fix/dogfood-port-restored-pane-focuscore-red, never merge): step e3e9ae38fc72f7510047dc53 FAILED as expected: restored_pane_for_the_shown_workspace_focuses_its_saved_pane panics at restored_pane.rs:41 (resolved stays Home's page); the guard restored_pane_never_overrides_the_persons_choice passes.
  • Green at 77e15d9: step f16e672983292f101e8385a0 PASSED, 2 of 2.

Changelog

Fixed: After Cmd-Q with a browser tab focused beside Home and a relaunch, typed keys go to the browser page again, not into Home's Chief message box.

Proof

No capture: a focus-routing fix with no visual change; the proof is the red/green tests above.

…ses its saved pane (QA v2.2.2 bug 1, red)

Ports the Rust half of 92f8d64 from cp/dogfood-v2-2-12-opt.

The launch window shows the home workspace (Home's page first, the pane
beside it with a terminal and the selected browser tab) before it adopts
the saved window record. The record's focused pane then comes as
restoredPane for the shown workspace, and cmux-focus-core drops it: the
first pane (Home) keeps the focus and the typed keys go to Home's Chief
message box.

Expected red on the base: resolved stays page(home).
…record focuses the saved pane (QA v2.2.2 bug 1, red)

Ports the Swift test of 92f8d64 from cp/dogfood-v2-2-12-opt, through
the real relaunch path (WindowState.adopt) instead of a bare send.

RestoredFocusTests: the launch window shows the home workspace (Home's
page, the pane beside it with a terminal and the selected browser tab),
then adopts the saved record whose focused pane is the browser pane.
Expected red on the base: resolved stays page(home-pane). It stays red
until CCmuxAppFFI is repinned to a release with the cmux-focus-core fix:
the app links the reducer only through that pin.
…focuses its saved pane (QA v2.2.2 bug 1)

Ports bde3363 from cp/dogfood-v2-2-12-opt (the dogfood line later
moved the rule into the app's FocusCoordinator, 77c9594, only
because its pinned FFI could not take a reducer change; on feat the
rule belongs to cmux-focus-core).

After Cmd-Q with a browser tab focused in the pane beside Home and a
relaunch, typed keys went to Home's Chief message box. The launch window
showed the home workspace before WindowManager adopted the saved window
record; WindowState.adopt then sent the record's focused pane as
restoredPane for the shown workspace, and the rule only remembered a
restore for a workspace the window does not show. The first pane (Home)
kept the focus.

Now restoredPane for the shown workspace makes the saved pane the focus
(target content, the responder re-applied) while no user intent happened
in the window (generation 0). A view that took the keyboard by itself (a
programmatic report: Home's box, AppKit's own pick) does not keep it; a
person's click or key does. The recorded Swift corpus has 4 restores for
the shown workspace; all come after an intent (generation 4 or 9), so no
recorded step changes.

The app links this rule only through the CCmuxAppFFI pin: the Swift
RestoredFocusTests go green with the repin.
…l keeps it in Home's pane (QA v2.2.2 bug 1, red)

Ports 4033999 from cp/dogfood-v2-2-12-opt. Home's message box took
the keyboard from a restored browser page: a Chromium page that has the
keys leaves the cmux window itself first responder, so Home read
'nothing has the keyboard'. FocusState.keepsKeyboard(inContentOf:) is the
model's answer Home must ask. Red on the base: it does not exist (the
test target does not compile).
… the focus model keeps it in Home's pane (QA v2.2.2 bug 1)

Ports 47d6a3f and its follow-up c93f03a from
cp/dogfood-v2-2-12-opt.

Home took the keyboard from a restored browser page: when it shows a
conversation nobody opened (TopHomePageView.show), it gave the message
box the keyboard whenever the window's first responder was the window or
nil, now or when the box is installed later
(MessagesLabHomeView.finishOpenKeyboard). A Chromium page that has the
keys keeps them in its own page window and leaves the cmux window itself
first responder, so that read 'nothing has the keyboard', and the focus
model then took the programmatic report as a move to Home's pane.

Fix: the focus model answers. NSWindow.keyboardIsFree(for:) is true only
when no view of the window holds the keyboard and, for a view in a pane
the focus model lists, the model keeps the keyboard in that pane's
content (FocusState.keepsKeyboard(inContentOf:)). Outside every pane, or
in a pane the model does not list yet (a first launch before the first
topology), the first-responder rule alone decides, as before. Home's
unasked path asks it when the box takes the keyboard (now, or at its
install: takeKeyboardOnOpen(while:)). A conversation the person opens (a
click, Return, a switch key, an action) takes the keyboard as before.

Test: RestoredFocusTests.homeTakesTheKeyboardUnaskedOnlyWhileTheModelKeepsItInHomesPane.
@lawrencecchen

Copy link
Copy Markdown
Contributor Author

Review (independent review subagent): SHIP at 43d699e

Read-only review of 9ce095b907c6..43d699e07970, done by reading the code. I compiled nothing and ran no tests. I found no correctness defect. The missing CCmuxAppFFI repin still holds this PR: Check the app FFI pin is red on this head, as the body says.

What I checked

  • Reducer, other restore paths. The only sender of restoredPane is WindowState.adopt (WindowState.swift:138-141). It has three callers:

    • WindowState(record:) starts with an empty topology, so the restore is remembered as before.
    • LaunchSnapshotWindow.showSaved (LaunchSnapshotWindow.swift:28) adopts before any topology arrives, which is the same case.
    • adoptLaunchWindow (WindowManager.swift:352) is the bug path. restore() runs once (guard !restored), so the new branch can only fire at launch. In the snapshot path, the second adopt finds the pane already focused with target content, so nothing changes.
    • If the launch window shows workspace X but the record shows W, and focusedPanes names X: X's saved pane briefly takes focus, then the switch to W stores it as X's remembered pane. The end state is correct. There is one extra RevealPane/MoveResponder pair, which does no harm.
  • Corpus test. I replayed the recorded state of tests/focus/corpus.json with a script (no build). It has 24 restoredPane steps; 4 are for the shown workspace (seeds 30, 34, 70, 105), with generation 9, 4, 4, 4. None is at generation 0, so the_rust_focus_rules_reproduce_every_recorded_swift_step cannot change. This matches the commit message. The corpus therefore does not exercise the new branch; only the two new tests do.

  • Effects and force_responder. finish remembers the new pane, emits RevealPane because the pane changed, and emits MoveResponder because force_responder is set. This is the same pattern as the FocusPane arm. When the saved pane is already focused with target content, the branch does nothing and emits no effects.

  • Swift compile, by reading. These all resolve:

    • FocusState.Target.content, Resolved.pane, FocusTopology.Pane.id
    • FocusResponderClassifier.classify(_:in:), which returns FocusEvent.Responder with .content(pane:)
    • FocusCoordinator.currentState()
    • WindowController.focus, and the import CmuxNextFocus in TopHomePageView
    • the @MainActor closure with [weak self] and guard let self, let window
    • the { true } defaults
    • every test API (FocusCoordinator(), WindowState(id:workspaceID:), WindowRecord keys workspace_key / focused_pane, .focusPane(_:source:))

    No reference to KeyRouter.mayHavePrimaryInput is left. The stored closure captures TopHomePageView weakly, so there is no retain cycle.

  • Person-opened conversations. open(_:) (TopHomePageView.swift:446-449) and the pending-selection path (:291-299) call show first, then host?.takeKeyboardOnOpen() with the default { true }. In the deferred branch, that call overwrites openKeyboardAllowed in the same turn. In the immediate branch, it grabs directly. All other focusPrimaryInput() callers default to unasked: false. So the person's open is not gated.

  • First launch. allowsUnaskedKeyboard returns true while the topology does not list the pane, and also for views outside any pane (top page, dock, which classify as .windowOrNone). In those cases the old first-responder rule alone decides. This is the same as the old check, which ran at show time, because the closure checks first responder == window or nil before it asks the model.

Findings (none blocking)

  1. PR body is wrong about which tests are red. It says "the four restore tests in RestoredFocusTests stay red on this head". By reading, only theSavedPaneOfTheShownWorkspaceGetsTheKeyboard (RestoredFocusTests.swift:49) and aViewThatTookTheKeyboardByItselfDoesNotKeepTheRestore (:60) fail with the pinned FFI.

    • thePersonsChoiceBeforeTheRecordStays (:68) passes under the old rule, because the old rule drops the restore, so Home stays.
    • aRecordBeforeTheWorkspaceShowsStillLandsOnItsPane (:76) passes under the old rule, because the record comes first and is remembered.

    These two are guards, not red-first tests. Fix the count. The "Swift red at 27b2a65: pending" and "results are filled in below" lines are still unfilled.

  2. The Home gate has no behavioral regression test. homeTakesTheKeyboardUnaskedOnlyWhileTheModelKeepsItInHomesPane (:86) tests only the new FocusState.keepsKeyboard predicate. Its red is a compile failure. If you revert TopHomePageView.swift:438 and :469-475 and MessagesLabHomeView.swift:467/:482, every test stays green. The repo rule asks for a regression test that fails without the fix. Add a follow-up test that drives TopHomePageView.show / finishOpenKeyboard with the window as first responder and the model on another pane.

  3. "Programmatic reports do not count as intent" is not proven by the tests. In home_took_it (focus_rules.rs:356-358) and the Swift twin (:60), the report content(home) names the pane that already has focus. accepted returns Same, and is_noop_responder returns before any work. Neither case would fail if a programmatic Move bumped the generation. The case that would prove it: focus starts on another pane, and a programmatic content(home) report moves it.

  4. A click on the pane that already has focus does not keep the person's choice (reducer.rs:52). Example: the person clicks into Home's message box when Home's pane already has focus, before adoptLaunchWindow runs. No makeFirstResponder report is sent, or the report is Same, so the generation stays 0, and the restore still moves focus to the saved pane. Keys typed before the restore also do not bump the generation. The window is short (only until the first restore) and this matches what a restore is for. I note it because the comment "unless the person chose since the window opened" is slightly wider than what the code does.

  5. Doc drift. FocusEvent.swift:70-72 ("the pane that workspace focuses when it first shows, unless focus there already moved") and event.rs:152 do not describe the new rule for the shown workspace at generation 0.

  6. Layering (minor). keepsKeyboard / allowsUnaskedKeyboard (FocusState.swift:225-236) are small focus rules on the Swift read view. layer-ownership.md:29 puts window-focus rules in cmux-focus-core. They only read target, overlays and resolved, like the moved mayHavePrimaryInput, so this is acceptable for now. A later port could expose them from the reducer.

  7. One unasked grab is still ungated (existing code, not added by this PR). At TopHomePageView.swift:426, inside show (which the self-opening observations also call), if leftNewMessage { focusPrimaryInput() } uses unasked: false. It is only reachable while the New Message page is up, so it is outside this bug.

CI on this head (classified)

  • Check the app FFI pin: red because of this PR, as expected. The reducer changed with no repin. This is the hold the body discloses.
  • pin-cmux-tui-windows-fetch.test.sh: tmp cleanup failure (rm: ... Directory not empty). This is infrastructure, not this change.
  • cmux-next swift test: still pending. Once it finishes, the two restore tests above should be red until the repin.

…rkspace's restore off; doc the new restoredPane rule

Review of #31852: the 'Home took it' case reported the pane that already
had the focus, a no-op, so it did not prove that a programmatic report is
no person's choice. The new case moves the target (the saved pane's
address bar, programmatic) before the record comes; the restore still
puts the keyboard on the page. FocusEvent.restoredPane (Swift) and
FocusEvent::RestoredPane (Rust) now say what the rule does for the shown
workspace.
@github-actions

github-actions Bot commented Nov 2, 2026 •

Copy link
Copy Markdown
Contributor

cmux status (one comment, edited in place; each line is updated by the job that owns it)

Review 56aa6b2: HQ feed card · gallery before/after runs · checks

App of this head (app.zip, kept 1 day)

gh workflow run reload-build.yml -R manaflow-ai/cmux --ref feat-cmux-next -f tag=pr31852 -f ref=fix/dogfood-port-restored-pane -f platform=macos -f backend_mode=local

red: cmux-next checks (god files, concurrency, crash safety, l10n); NEW: green on the base e34fecc; do not merge until green; see lane-rules

@blacksmith-sh

blacksmith-sh Bot commented Nov 2, 2026 •

Copy link
Copy Markdown

Found 6 test failures on Blacksmith runners:

Failures

Test View Logs
/tmp/tmp3vxr2dp1/summary-fail.json (live):/summary-fail.json (live): View Logs
/tmp/tmp9pnrkmn7/summary-missing.json (live):/summary-missing.json (live): View Logs
[new_workspaces] debug.new_workspace timed 0 of 50 new workspace rows/
[new_workspaces] debug.new_workspace timed 0 of 50 new workspace rows
View Logs
[new_workspaces] the window draws no frames/[new_workspaces] the window draws no frames View Logs
[tabs] debug.tab_switch timed 0 of 40 switches/
[tabs] debug.tab_switch timed 0 of 40 switches
View Logs
[tabs] the window draws no frames/[tabs] the window draws no frames View Logs

Fix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need.

@lawrencecchen

Copy link
Copy Markdown
Contributor Author

Review (independent delta review subagent): SHIP at a1ef7df

Scope: the delta since the SHIP at 43d699e (56aa6b2, plus the merge a1ef7df of feat-cmux-next 947bb5e). Read-only review with git; nothing compiled or run.

New test case (cmux-tui/crates/cmux-layout-reducer-ffi/tests/focus_rules.rs:360-365): correct, and it can fail.

  • Trace: the topology at restore_after_the_workspace_shows (line 328) sets pane home, target content, generation 0 (reducer.rs:269-286; a topology does not bump). The programmatic responder addressBar {pane: right} goes to accepted() (reducer.rs:483-509): right is in the topology and differs from home, so it returns Move(Some(right), AddressBar). It is not Same, so is_noop_responder does not drop it. accept() (reducer.rs:525-537) bumps only when source.is_user_intent(), and Programmatic is not a user intent (event.rs:22-24). The state is now pane right, target address bar, generation 0. finish keeps the address bar because right selects the browser tab web.
  • Then restoredPane right (reducer.rs:45-58): generation is 0, the pane exists, the pane is the same but the target is not Content, so it sets target Content and forces the responder. The result is browserPage {right, web}, which is what the assertion expects.
  • If programmatic reports bumped the generation, the restore would be skipped. The state would then resolve to addressBar {right, web} and the assertion would fail. So the case guards that rule.
  • The case also covers the next.target != Target::Content {} half of the condition at reducer.rs:54. The older home_took_it case covers only the pane half, so this case is the only one that fails if that half is removed. Good addition.

Docs: accurate.

  • Packages/macOS/CmuxNext/Sources/CmuxNextFocus/FocusEvent.swift:70-73 and cmux-tui/crates/cmux-focus-core/src/event.rs:152-155 match reducer.rs:45-58 (generation 0 = no user intent; bump is called only for user-intent sources and BeginIntent).
  • Nit, does not block: event.rs:153 says a record for a workspace the window does not show "is remembered for when it shows". The reducer does that only when that workspace has no remembered pane yet (reducer.rs:59, !next.remembered.contains_key(workspace)). You can add "unless a pane is already remembered for it" in a later change.

Merge a1ef7df: clean.

  • Parents are 56aa6b2 and 947bb5e, and 947bb5e is on origin/feat-cmux-next.
  • git diff 947bb5e00d79 a1ef7dfb818 --stat shows 13 files (+244/-25). All 13 are in this PR's file list.
  • git diff 56aa6b27bb91 a1ef7dfb818 on those files is empty, so the merge did not change this PR's content.

Not run: no Rust or Swift test was run for this review. The red/green evidence for the new case must come from the fleet run named in the PR.

…ns in the cmux-tui workspace)

The cases of focus_rules.rs (cmux-layout-reducer-ffi, its own Cargo
workspace) that cover QA v2.2.2 bug 1, in the crate that owns the rule,
so the fleet Rust test step (cmux-tui-rust-check.sh test) runs them.
Red on the base (the rule dropped restoredPane for the shown workspace),
green with e4a5c7a.
@lawrencecchen

Copy link
Copy Markdown
Contributor Author

Review (independent delta review subagent): SHIP at 77e15d9

Scope: delta a1ef7df..77e15d9 (one new file, cmux-tui/crates/cmux-focus-core/tests/restored_pane.rs). I read the code only. I did not build or run anything.

Correct

  • The wire shapes agree with the serde in event.rs and topology.rs: topology._0 (the TopologyWire workspace, panes, tabs {id, kind}, selected), restoredPane {_0, workspace}, responder {_0: {content: {pane}}, source: "programmatic"}, focusPane {_0, source: "mouse"}. The serialized Resolved shapes browserPage and page {pane, tab} match state.rs.
  • The assertions test the rule. With no intent, the first topology puts the focus on home (apply_topology, panes.first()). restoredPane for the shown workspace w must then move the focus to right, which resolves to its selected browser tab web, and emit MoveResponder (force_responder). The guard test's mouse focusPane bumps the generation, so the restore must leave Home's page. A fix with no generation check fails the guard.
  • It fails without the fix for the right reason. The red branch 9c816b5 has the same test file byte for byte; the only difference from the head is reducer.rs and event.rs. On the base rule, a restore for the shown workspace is dropped, so resolved stays page(home). The assertion that fails is restored_pane.rs:41, which agrees with step e3e9ae38fc72f7510047dc53. The guard passes on the base, as expected.
  • Conventions: [lints] workspace = true applies. The workspace lints do not deny unwrap_used, and the panic ratchet covers only --lib --bins. .unwrap() in integration tests is the usual pattern in this repo. serde_json is a normal dependency of the crate, so no dev-dependency is necessary. The import order {Value, json} is the edition 2024 order. The code lines are 100 characters or fewer (rustfmt.toml: max_width = 100). I found nothing that clippy -D warnings would flag.

Nits (do not block)

  1. Line 46, the comment // Home's message box took the keyboard by itself first ..., is 104 characters. max_width is 100. cargo fmt --check does not fail on it, because wrap_comments and error_on_line_overflow are off. The FFI copy wraps the same comment into two lines.
  2. The PR body says "the same cases as focus_rules.rs", but this file has 3 of the 5 sub-cases. It does not have these two:
    • The programmatic addressBar pick on right. This is the only case that exercises the next.target != Target::Content {} half of the new condition. If that half is removed, this file stays green, and only focus_rules.rs catches the change, which the fleet test step does not run.
    • The record before the workspace shows (the old remember path).
      I recommend adding the addressBar sub-case to this file (3 lines), or changing the body to say "a subset".
  3. In this core, the home_took_it sub-case has no effect. After the topology event, the focus is already home with the target Content, so is_noop_responder returns before any work and the state is the same as in the first sub-case. It is not harmful and records the intent, but it does not prove more.

Duplication: The cases are copied on purpose from cmux-layout-reducer-ffi/tests/focus_rules.rs, because that crate is in a different Cargo workspace that the fleet test step does not reach. This copy is not harmful. The topology() helper keeps the copy small.

@teamleaderleo

Copy link
Copy Markdown
Collaborator

Review (next-review patch-id; carried): SHIP at 20ab387

The independent SHIP at 77e15d9 carries to this head: the PR's own change (git diff $(git merge-base feat-cmux-next HEAD) HEAD) has the same git patch-id --verbatim (721c13fdd0cfd311) at both heads, and no merge between them was resolved by hand. Nothing new to review. A newer BLOCK, or any change to the PR's own diff, ends the carry.

@lawrencecchen

Copy link
Copy Markdown
Contributor Author

Split per lead decision (STATUS 5388, two-step FFI move). Step A, the Rust reducer change alone: #32055 (branch fix/dogfood-port-restored-pane-rust). This PR (#31852) is now step B: the Swift half (Home's unasked keyboard gate, 47d6a3f) and the 4 RestoredFocusTests. It lands after #32055 lands and the bot's CCmuxAppFFI repin is on feat; I then merge feat here (the Rust commits become no-ops) and run the Mac suites. No ready-to-land until then.

@teamleaderleo

Copy link
Copy Markdown
Collaborator

Review (next-review patch-id; carried): SHIP at bf6bcba

The independent SHIP at 77e15d9 carries to this head: the PR's own change (git diff $(git merge-base feat-cmux-next HEAD) HEAD) has the same git patch-id --verbatim (721c13fdd0cfd311) at both heads, and no merge between them was resolved by hand. Nothing new to review. A newer BLOCK, or any change to the PR's own diff, ends the carry.

lawrencecchen added a commit that referenced this pull request Nov 2, 2026
…focuses its saved pane (QA v2.2.2 bug 1, step A) (#32055)

* test(focus): a record for the workspace the window already shows focuses its saved pane (QA v2.2.2 bug 1, red)

Ports the Rust half of 92f8d64 from cp/dogfood-v2-2-12-opt.

The launch window shows the home workspace (Home's page first, the pane
beside it with a terminal and the selected browser tab) before it adopts
the saved window record. The record's focused pane then comes as
restoredPane for the shown workspace, and cmux-focus-core drops it: the
first pane (Home) keeps the focus and the typed keys go to Home's Chief
message box.

Expected red on the base: resolved stays page(home).

(cherry picked from commit a52678b)

* test(focus): cmux-focus-core's own test of the restored-pane rule (runs in the cmux-tui workspace)

The cases of focus_rules.rs (cmux-layout-reducer-ffi, its own Cargo
workspace) that cover QA v2.2.2 bug 1, in the crate that owns the rule,
so the fleet Rust test step (cmux-tui-rust-check.sh test) runs them.
Red on the base (the rule dropped restoredPane for the shown workspace),
green with e4a5c7a.

(cherry picked from commit 77e15d9)

* fix(focus-core): a record for the workspace the window already shows focuses its saved pane (QA v2.2.2 bug 1)

Ports bde3363 from cp/dogfood-v2-2-12-opt (the dogfood line later
moved the rule into the app's FocusCoordinator, 77c9594, only
because its pinned FFI could not take a reducer change; on feat the
rule belongs to cmux-focus-core).

After Cmd-Q with a browser tab focused in the pane beside Home and a
relaunch, typed keys went to Home's Chief message box. The launch window
showed the home workspace before WindowManager adopted the saved window
record; WindowState.adopt then sent the record's focused pane as
restoredPane for the shown workspace, and the rule only remembered a
restore for a workspace the window does not show. The first pane (Home)
kept the focus.

Now restoredPane for the shown workspace makes the saved pane the focus
(target content, the responder re-applied) while no user intent happened
in the window (generation 0). A view that took the keyboard by itself (a
programmatic report: Home's box, AppKit's own pick) does not keep it; a
person's click or key does. The recorded Swift corpus has 4 restores for
the shown workspace; all come after an intent (generation 4 or 9), so no
recorded step changes.

The app links this rule only through the CCmuxAppFFI pin: the Swift
RestoredFocusTests go green with the repin.

(cherry picked from commit e4a5c7a)

* test(focus-core): a programmatic target move does not keep a shown workspace's restore off; doc the new restoredPane rule

The Rust part of 56aa6b2 on fix/dogfood-port-restored-pane (#31852):
the focus_rules.rs case and the event.rs doc comment. The matching Swift
doc comment (FocusEvent.swift) lands with the Swift half after the repin.
@teamleaderleo

Copy link
Copy Markdown
Collaborator

For awareness: this PR conflicts with feat-cmux-next, so it can't land until it's rebased or base is merged in. Its current review verdict stays as is; a hand-resolved merge gets a fresh review at the new head.

This branch was previously deployed

3 inactive deployments
ci-routing-agent — bf6bcbad Deployed Nov 2, 2026 by github-actions[bot] via cmux-next macOS placement #62216
artifacts — bf6bcbad Deployed Nov 2, 2026 by cmux-bot[bot] via publish tree-addressed cmux-tui #100379
sentry-dsyms — bf6bcbad Deployed Nov 2, 2026 by cmux-bot[bot] via upload cmux-tui dSYMs to Sentry #100379
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

risk:normal Risk tier (docs/ci/risk-tiers.md)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants