Skip to content

Fix iOS 27 keyboard dock tracking - #9958

Merged
azooz2003-bit merged 3 commits into
mainfrom
feat-ios27-keyboard-dock
Aug 11, 2026
Merged

azooz2003-bit merged 3 commits into
mainfrom
feat-ios27-keyboard-dock

Conversation

@azooz2003-bit

@azooz2003-bit azooz2003-bit commented Aug 11, 2026 •

Copy link
Copy Markdown
Collaborator

iOS 27 beta can leave the host keyboard layout guide at the screen bottom while the keyboard is visible. On OS major 27 only, the existing host-owned dock now uses keyboard frame notifications to update one bottom constraint with UIKit animation timing. Every other OS keeps the current layout-guide path. The focused UI test forces the iOS 27 policy on the hosted simulator, enters the production composer, and checks the dock and terminal viewport against the visible software keyboard.


Note

Medium Risk
Changes how keyboard height drives terminal chrome layout on iOS 27 only, with regression coverage; non-27 behavior remains on the layout-guide path.

Overview
On iOS 27, where UIKeyboardLayoutGuide can stay at the screen bottom while the software keyboard is visible, the terminal bottom dock (composer/toolbar) now follows keyboard frame notifications instead of the layout guide. A single manual bottom constraint is updated from notification overlap, animated with UIKit’s keyboard timing (including keyboardDidChangeFrame with zero duration when needed), and the terminal viewport stays aligned during the transition.

All other OS versions keep pinning the dock to keyboardLayoutGuide.topAnchor; layout-guide sync is skipped on the notification path so the stale guide does not compete.

DEBUG / tests: UITestConfig.forceIOS27KeyboardDockWorkaround (CMUX_UITEST_FORCE_IOS27_KEYBOARD_DOCK=1) forces the iOS 27 path on older simulators. Dock debug snapshots add keyboardDockSource and keyboardDockTargetTop; UI tests assert the dock edge against the active source, and a new test opens the production composer under the forced policy and checks alignment with the visible keyboard.

Reviewed by Cursor Bugbot for commit e99c3d8. Bugbot is set up for automated code reviews on this repo. Configure here.


Summary by cubic

Fixes keyboard dock tracking on iOS 27 by bypassing the broken UIKeyboardLayoutGuide and driving the dock with keyboard frame notifications. Other iOS versions keep the layout-guide path; animations and the terminal viewport stay in sync.

  • Bug Fixes
    • On iOS 27 only, drive the dock from keyboard frame notifications with UIKit-matched animations; keep the keyboardLayoutGuide path on other iOS.
    • Add a DEBUG-only toggle UITestConfig.forceIOS27KeyboardDockWorkaround (env: CMUX_UITEST_FORCE_IOS27_KEYBOARD_DOCK=1) to force the iOS 27 path in CI.
    • Extend the surface debug snapshot with keyboardDockSource and keyboardDockTargetTop to validate geometry in tests.
    • Update UI tests to assert against the selected geometry source (layout guide vs notification), pass launch env to the app, and add a focused test that forces the policy, opens the composer, and verifies the dock and terminal viewport follow the visible keyboard.

Written for commit e99c3d8. Summary will update on new commits.

Review in cubic

Summary by CodeRabbit

  • New Features

    • Improved keyboard and composer dock positioning on iOS 27.
    • Preserved compatible keyboard layout behavior across other supported iOS versions.
    • Improved alignment between the terminal viewport, composer, and toolbar when the keyboard appears.
  • Bug Fixes

    • Resolved keyboard-driven dock movement issues during transitions on iOS 27.
  • Testing

    • Added coverage for keyboard docking, terminal viewport alignment, and configurable test launch environments.

@coderabbitai

coderabbitai Bot commented Aug 11, 2026 •

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The PR adds an iOS 27 keyboard dock geometry path based on keyboard frame notifications. Older systems continue using keyboardLayoutGuide. Debug configuration, diagnostics, and UI coverage support the new path.

Changes

Keyboard dock geometry

Layer / File(s) Summary
Geometry policy and state
Packages/iOS/CmuxMobileSupport/Sources/CmuxMobileSupport/UITestConfig.swift, Packages/iOS/CmuxMobileTerminal/Sources/CmuxMobileTerminal/GhosttySurfaceView.swift, Packages/iOS/CmuxMobileTerminal/Sources/CmuxMobileTerminal/GhosttySurfaceHostView.swift
Adds the iOS 27 geometry policy, debug override, diagnostic fields, and manual dock constraint state.
Notification-driven dock flow
Packages/iOS/CmuxMobileTerminal/Sources/CmuxMobileTerminal/GhosttySurfaceView.swift
Uses keyboard frame notifications for iOS 27 dock positioning. Legacy systems use the system layout guide. Layout, safe-area, relocation, and debug override paths refresh the selected constraint.
Keyboard dock regression test
ios/cmuxUITests/cmuxUITests.swift
Adds launch environment overrides and verifies iOS 27 dock, keyboard, composer, toolbar, and terminal viewport alignment.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant iOSKeyboard
  participant GhosttySurfaceView
  participant GhosttySurfaceHostView
  iOSKeyboard->>GhosttySurfaceView: keyboardDidChangeFrameNotification
  GhosttySurfaceView->>GhosttySurfaceView: calculate keyboard overlap
  GhosttySurfaceView->>GhosttySurfaceHostView: update manual dock constraint
  GhosttySurfaceView->>GhosttySurfaceView: synchronize terminal viewport
Loading

Possibly related PRs

Suggested reviewers: lawrencecchen, austinywang


Important

Pre-merge checks failed

Please resolve all errors before merging. Addressing warnings is optional.

❌ Failed checks (2 errors, 1 warning)

Check name Status Explanation Resolution
Cmux No Test Or Debug Seam In Production Source ❌ Error GhosttySurfaceView.swift adds debugName and keyboardDockTargetTopInSurface solely to expose new keyboardDockSource/keyboardDockTargetTop values through the UI-test accessibility probe. Move keyboard observation into the test target via @testable import after private-to-internal widening, or isolate the diagnostic in a dedicated debug file/folder; see PR 6452.
Cmux No Ambient Global State ❌ Error UITestConfig.swift:84 adds public static var forceIOS27KeyboardDockWorkaround, a new ambient global flag read by KeyboardDockGeometrySource.current. Move the flag to constructable UITestEnvironmentConfig and inject the resulting keyboard-dock policy into GhosttySurfaceView from the app/test composition seam.
Description check ⚠️ Warning The description explains the change and rationale, but it omits the required Testing, Demo Video, Review Trigger, and Checklist sections. Add the required Testing, Demo Video, Review Trigger, and Checklist sections, including test details, a video link or attachment, and checklist status.
✅ Passed checks (22 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Cmux Swift Actor Isolation ✅ Passed Production additions stay in UIKit/MainActor UI flow; the new enum/config are value-only, no Sendable reference or service protocol was added, and the UI test is explicitly @MainActor.
Cmux Swift Blocking Runtime ✅ Passed The production diff uses UIKit UIView.animate with keyboard notification timing and completion callbacks; it adds no sleeps, delayed dispatch, sync waits, semaphores, locks, or polling loops. XCTes...
Cmux Browser Automation Off-Main ✅ Passed The PR changes only iOS keyboard and UI-test files; the scoped browser socket router files are unchanged, and no browser.* socket command was added or moved.
Cmux Expensive Synchronous Load ✅ Passed The aggregate production diff adds keyboard geometry and a DEBUG environment flag only; it adds no agent-history loader, file/JSON scan, transcript access, or synchronous workspace load on an inter...
Cmux Cache Substitution Correctness ✅ Passed Changed Swift drives transient keyboard-dock layout and DEBUG live UI-test diagnostics; no persistence, history, undo, or durable snapshot path replaces an authoritative read.
Cmux No Hacky Sleeps ✅ Passed The cumulative PR diff changes only four Swift files; it introduces no TypeScript, JavaScript, shell, or build/runtime-script sleeps or timers covered by this check.
Cmux Algorithmic Complexity ✅ Passed Production diff adds no scalable collection scans, nested rescans, sorting, or filtering; new keyboard handling uses scalar geometry and fixed dock constraints. UI-test collection logic is exempt.
Cmux Swift Concurrency ✅ Passed The diff adds only a UIKit keyboard observer and UIView animation callbacks, both allowed OS/UI boundaries; it adds no new Dispatch queues, Combine state, fire-and-forget Tasks, or internal complet...
Cmux Swift @Concurrent ✅ Passed The diff adds only an intentionally UI-bound @MainActor async UI test; its network helper already hops to a dedicated DispatchQueue and no @concurrent violation is introduced.
Cmux Swift Package Boundaries ✅ Passed Production changes stay in CmuxMobileSupport and CmuxMobileTerminal SwiftPM targets and implement UIKit/Ghostty dock glue; the remaining change is XCUITest code.
Cmux Swiftpm Lockfiles ✅ Passed The PR changes only Swift source and UI test files; no Package.swift, Package.resolved, .gitignore, workflow, or Xcode package-reference changes require lockfile updates.
Cmux Swift Logging ✅ Passed The complete PR diff adds no print, debugPrint, dump, NSLog, file/stdout logging, or Logger declaration; added dock diagnostics expose only source and numeric geometry, and test output is allowed.
Cmux User-Facing Error Privacy ✅ Passed The diff adds only DEBUG test configuration/diagnostics, comments, and XCTest assertions; it adds no production alerts, errors, recovery copy, raw upstream messages, or sensitive data.
Cmux Full Internationalization ✅ Passed The cumulative diff adds no user-facing copy. Production additions are comments, DEBUG geometry/config tokens, and test diagnostics; no localization catalogs or web locale files changed.
Cmux Swiftui State Layout ✅ Passed The PR adds only UIKit keyboard geometry, a debug config flag, and XCTest assertions; no new SwiftUI state, GeometryReader, lazy-row store reference, or render-time state mutation appears in the diff.
Cmux Architecture Rethink ✅ Passed The change uses a documented iOS keyboard notification bridge, one per-surface geometry owner, and UIKit timing; no new sleeps, polling, locks, side-channel state, or duplicate action paths were ad...
Cmux Swift Auxiliary Window Close Shortcuts ✅ Passed The PR changes UIKit terminal views, keyboard constraints, config, and an XCTest; it adds or modifies no NSWindow, NSPanel, Window, WindowGroup, or close-shortcut routing.
Cmux Source Artifacts ✅ Passed All four changed paths are tracked Swift source or UI-test files; the aggregate diff adds no artifact directories, binaries, logs, screenshots, caches, or build output.
Title check ✅ Passed The title clearly and concisely describes the main change: fixing iOS 27 keyboard dock tracking.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat-ios27-keyboard-dock

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@Packages/iOS/CmuxMobileTerminal/Sources/CmuxMobileTerminal/GhosttySurfaceView.swift`:
- Around line 22-30: Update the shared
assertTerminalDockPinnedToSoftwareKeyboard assertion to branch on
keyboardDockSource: for "notification", compare the terminal dock edge against
keyboardDockTargetTop; for "layoutGuide", preserve the existing keyboardGuideTop
assertion.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 8fbf6ff6-136a-4e8c-a720-b855779b6a08

📥 Commits

Reviewing files that changed from the base of the PR and between 61ca64f and f74fa7a.

📒 Files selected for processing (4)
  • Packages/iOS/CmuxMobileSupport/Sources/CmuxMobileSupport/UITestConfig.swift
  • Packages/iOS/CmuxMobileTerminal/Sources/CmuxMobileTerminal/GhosttySurfaceHostView.swift
  • Packages/iOS/CmuxMobileTerminal/Sources/CmuxMobileTerminal/GhosttySurfaceView.swift
  • ios/cmuxUITests/cmuxUITests.swift

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit f74fa7a. Configure here.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
ios/cmuxUITests/cmuxUITests.swift (1)

8548-8572: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Use the shared keyboard dock assertion.

Line 8548 through Line 8572 duplicates the target parsing and geometry checks in assertTerminalDockPinnedToSoftwareKeyboard. Keep the explicit "notification" source assertion. Then call the shared helper. This keeps all keyboard dock tests on one assertion contract.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@ios/cmuxUITests/cmuxUITests.swift` around lines 8548 - 8572, In the test
block around assertTerminalDockPinnedToSoftwareKeyboard, retain the explicit
notification source assertion, remove the duplicated
dockTargetTop/composerMinY/composerMaxY/toolbarMaxY parsing and geometry
assertions, and invoke the shared assertTerminalDockPinnedToSoftwareKeyboard
helper instead. Keep the existing notification-fallback context and helper
contract.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@Packages/iOS/CmuxMobileTerminal/Sources/CmuxMobileTerminal/GhosttySurfaceView.swift`:
- Around line 1005-1010: Remove keyboardHeightOverrideForTesting from the
production visibility decision in GhosttySurfaceView.swift:1005-1010, leaving
MobileKeyboardTransition as the sole source for willBeVisible. At
GhosttySurfaceView.swift:1062-1070, remove override-specific constraint updates
from the production notification path and relocate any unavoidable test forcing
to a dedicated debug-only facility outside Sources/**/*.swift.

---

Outside diff comments:
In `@ios/cmuxUITests/cmuxUITests.swift`:
- Around line 8548-8572: In the test block around
assertTerminalDockPinnedToSoftwareKeyboard, retain the explicit notification
source assertion, remove the duplicated
dockTargetTop/composerMinY/composerMaxY/toolbarMaxY parsing and geometry
assertions, and invoke the shared assertTerminalDockPinnedToSoftwareKeyboard
helper instead. Keep the existing notification-fallback context and helper
contract.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 41ee14f1-35dd-4152-9253-f3a3780d9739

📥 Commits

Reviewing files that changed from the base of the PR and between f74fa7a and e99c3d8.

📒 Files selected for processing (2)
  • Packages/iOS/CmuxMobileTerminal/Sources/CmuxMobileTerminal/GhosttySurfaceView.swift
  • ios/cmuxUITests/cmuxUITests.swift

@azooz2003-bit
azooz2003-bit merged commit cf291a4 into main Aug 11, 2026
17 checks passed
@azooz2003-bit
azooz2003-bit deleted the feat-ios27-keyboard-dock branch August 11, 2026 07:48
azooz2003-bit added a commit that referenced this pull request Aug 26, 2026
The guide-locked rewrite (#10594/#10687) left iOS 27 on a notification
seat that consumes the full keyboard notification stream: did-frame
disagreement reseats and steady-state tracker re-derivations. iOS 27
misreports keyboard frames outside the will transaction (#10518 recorded
this when it quarantined the rebuilt path away from that OS), so those
corrections moved a perfectly settled composer bar after toggles - the
regression against the #9958/#10006 path that shipped will-only and was
rated perfect in dogfood.

Restore that contract, scoped so iOS <= 26 behavior is untouched:
- iOS 27 seats ignore keyboardDidChangeFrame entirely and skip the
  steady-state tracker heal in layoutSubviews (attach recovery in
  didMoveToWindow stays - it fixes real workspace-switch wedges and did
  not exist on the old path).
- Interrupted legs rebase from live presentation frames before the next
  will leg (the #10006 reversal contract), folding the live dock bottom
  into the seat constraint and re-deriving clip and wrapper models from
  that edge before stripping animations.
- TerminalKeyboardSeatSelection (replacing the stale, unused
  TerminalKeyboardDockPathSelection) owns the seat decision table:
  guide seat on iOS <= 26, will-only notification seat on iOS 27+,
  full-stream notification seat behind the iOS <= 26 kill switch.
- CMUX_UITEST_FORCE_IOS27_KEYBOARD_SEAT (DEBUG) runs the exact iOS 27
  path on any simulator OS; the dock probe reports keyboardSeatWillOnly.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
azooz2003-bit added a commit that referenced this pull request Aug 26, 2026
The guide-locked rewrite (#10594/#10687) left iOS 27 on a notification
seat that consumes the full keyboard notification stream: did-frame
disagreement reseats and steady-state tracker re-derivations. iOS 27
misreports keyboard frames outside the will transaction (#10518 recorded
this when it quarantined the rebuilt path away from that OS), so those
corrections moved a perfectly settled composer bar after toggles - the
regression against the #9958/#10006 path that shipped will-only and was
rated perfect in dogfood.

Restore that contract, scoped so iOS <= 26 behavior is untouched:
- iOS 27 seats ignore keyboardDidChangeFrame entirely and skip the
  steady-state tracker heal in layoutSubviews (attach recovery in
  didMoveToWindow stays - it fixes real workspace-switch wedges and did
  not exist on the old path).
- Interrupted legs rebase from live presentation frames before the next
  will leg (the #10006 reversal contract), folding the live dock bottom
  into the seat constraint and re-deriving clip and wrapper models from
  that edge before stripping animations.
- TerminalKeyboardSeatSelection (replacing the stale, unused
  TerminalKeyboardDockPathSelection) owns the seat decision table:
  guide seat on iOS <= 26, will-only notification seat on iOS 27+,
  full-stream notification seat behind the iOS <= 26 kill switch.
- CMUX_UITEST_FORCE_IOS27_KEYBOARD_SEAT (DEBUG) runs the exact iOS 27
  path on any simulator OS; the dock probe reports keyboardSeatWillOnly.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
azooz2003-bit added a commit that referenced this pull request Aug 26, 2026
…10810)

* fix(ios): unbreak the iOS build after the workspace-groups merge

Two iOS-only compile errors from #10662 (the Mac target builds; both
break every iOS build, so main's iOS lane is red):

- MobileWorkspaceAggregation assigns a MobileWorkspaceGroupPreview.ID to
  anchorWorkspaceID, which is typed MobileWorkspacePreview.ID. Convert
  through rawValue, the same mapping the type's own initializer uses for
  exactly this empty-header fallback.
- WorkspaceListTableCoordinator.groupActionCapabilities(for:) is
  fileprivate but called from the context-menu actions extension in
  WorkspaceListTableCoordinator+Actions.swift, a different file. Make it
  internal.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test(ios): iOS 27 will-only keyboard seat must keep a settled dock still

Red half of the regression pair. Forces the iOS 27 keyboard seat via
CMUX_UITEST_FORCE_IOS27_KEYBOARD_SEAT and asserts the seat reports the
will-only contract, a settled composer bar does not move without a will
notification, and rapid reversals keep dock and render pinned. Red today
because the force env and the will-only seat do not exist: iOS 27 hosts
consume the full notification stream, whose non-will frames misreport on
that OS and hop the settled dock.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* iOS 27: restore the will-only keyboard seat contract

The guide-locked rewrite (#10594/#10687) left iOS 27 on a notification
seat that consumes the full keyboard notification stream: did-frame
disagreement reseats and steady-state tracker re-derivations. iOS 27
misreports keyboard frames outside the will transaction (#10518 recorded
this when it quarantined the rebuilt path away from that OS), so those
corrections moved a perfectly settled composer bar after toggles - the
regression against the #9958/#10006 path that shipped will-only and was
rated perfect in dogfood.

Restore that contract, scoped so iOS <= 26 behavior is untouched:
- iOS 27 seats ignore keyboardDidChangeFrame entirely and skip the
  steady-state tracker heal in layoutSubviews (attach recovery in
  didMoveToWindow stays - it fixes real workspace-switch wedges and did
  not exist on the old path).
- Interrupted legs rebase from live presentation frames before the next
  will leg (the #10006 reversal contract), folding the live dock bottom
  into the seat constraint and re-deriving clip and wrapper models from
  that edge before stripping animations.
- TerminalKeyboardSeatSelection (replacing the stale, unused
  TerminalKeyboardDockPathSelection) owns the seat decision table:
  guide seat on iOS <= 26, will-only notification seat on iOS 27+,
  full-stream notification seat behind the iOS <= 26 kill switch.
- CMUX_UITEST_FORCE_IOS27_KEYBOARD_SEAT (DEBUG) runs the exact iOS 27
  path on any simulator OS; the dock probe reports keyboardSeatWillOnly.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* review: poll the settled dock over a window; document the attach-heal trust tradeoff

CodeRabbit findings on #10810: the post-settle stability check now samples
the dock edge five times over 1.5s and requires every sample at the settled
edge (a transient hop between two single samples could previously pass), and
healKeyboardModelFromTracker documents why the will-only seat keeps ATTACH
recovery despite the tracker recording did frames iOS 27 can misreport
(settled end frame at attach, next will leg corrects a bad record, and
skipping recovery wedges the dock after workspace switches mid-transition).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant