Skip to content

iOS: keep the bottom bars docked to the keyboard when the layout guide misses a transition - #9663

Closed
azooz2003-bit wants to merge 14 commits into
mainfrom
feat-ios-kb-dock-attach
Closed

azooz2003-bit wants to merge 14 commits into
mainfrom
feat-ios-kb-dock-attach

Conversation

@azooz2003-bit

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

Copy link
Copy Markdown
Collaborator

Bug

Keyboard rises but the bottom bars (Tab/Esc accessory toolbar + Message composer) stay seated at the screen bottom behind it. Field recording repro: keyboard up in workspace A, back to the list, open workspace B, focus input — the keyboard covers the bars and the terminal grid keeps keyboard-down geometry until the keyboard is dismissed and re-raised.

Root cause

Since #9371 the dock is constrained to UIView.keyboardLayoutGuide, which only reflects keyboard transitions UIKit routed to the view's window while the view was installed. A surface (re)attached around a workspace switch can miss the transition and stay seated on the guide's bottom-safe-area fallback while the keyboard is up. Nothing re-derives keyboard geometry after attach, so both the constraint-positioned bars and the keyboardOverlapFromLayoutGuide viewport model stay at keyboard-down layout.

Fix

Keyboard notifications are posted process-wide regardless of any view's attachment, so:

  • MobileKeyboardFrameTracker (CmuxMobileSupport) records the latest keyboard end frame from keyboardWillChangeFrame, clearing on keyboardDidHide and backgrounding.
  • The dock gains a REQUIRED inequality floor composer.bottom <= view.bottom - trackedOverlap beneath the guide equality, which drops one priority notch. The guide remains the movement engine; the floor only stops the dock from sitting below the real keyboard. It animates on the notification's own curve on the notification path and catches up from the tracker on layout after a late attach.
  • The viewport model takes max(guide, floor) so the terminal grid reserves exactly the space the lifted bars occupy.

A stale notification frame can never pin the dock below wherever the guide places it (inequality, not a second equality authority), and MobileKeyboardReservation keeps floating/split iPad keyboards at zero overlap.

Tests

Commit 1 (red): dock regression tests in a test window, where the system guide never observes a keyboard — deterministically the same wedge as the missed live transition. Covers the notification path, the attach-after-transition path, and dismissal release. Plus MobileKeyboardFrameTracker unit tests (notification-center-isolated).
Commit 2 (green): the fix.

Localization audit: no user-facing strings added or changed.

🤖 Generated with Claude Code


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


Summary by cubic

Reworked the iOS bottom dock so the toolbar and composer are a system keyboard accessory that rides the keyboard’s animation and stays docked at the bottom when the keyboard is down. This removes guide/floor glitches, stabilizes transitions, and keeps the grid reservation accurate from a tracker-derived model.

  • Bug Fixes
    • Replaced custom constraints with a self-sizing KeyboardDockAccessoryView returned as the inputAccessoryView of both the terminal input proxy and the surface; the OS now positions the dock in all states.
    • The surface becomes first responder when the keyboard is down so the dock stays seated; chrome hide/show now reloads input views to withhold/restore the accessory.
    • The keyboard toggle now branches on actual first-responder ownership (input proxy or composer) instead of the tracked visibility bit, preventing desync on rapid taps.
    • MobileKeyboardFrameTracker now feeds only the terminal grid model using window-space overlap (with the accessory’s height subtracted), clears on hide/background, and reconciles visibility on remount so the toggle starts as “Show Keyboard.”
    • Pinned the surface under the home indicator in all states (.ignoresSafeArea(.container, edges: .bottom)) to keep frame conversions stable mid-animation.
    • Added dock-accessory tests (responder wiring, chrome withholding, model derivation, glyph state) and fixed/updated injected-attach startup tests.
    • iPhone install queue: during a drain pass, only the newest queued build is launched; older ones install without launch to avoid foreground fights.

Written for commit 3fab1b1. Summary will update on new commits.

Review in cubic

Summary by CodeRabbit

  • Bug Fixes
    • Improved composer positioning when the on-screen keyboard appears, including late or incomplete layout updates.
    • Ensured the composer remains above the keyboard and returns to the bottom of the window after dismissal.
    • Cleared keyboard positioning state when the app moves to the background, preventing stale offsets.
    • Improved keyboard overlap handling for views attached after the keyboard is already visible.
    • Improved handling of keyboard transitions when layout notifications are delayed or incomplete.

Note

Medium Risk
Large change to terminal input, first-responder, and keyboard geometry on a user-critical path; mitigated by extensive new tests and unchanged grid math semantics aside from the positioning model shift.

Overview
Fixes iOS terminal bottom chrome (toolbar + composer) lagging or sitting behind the keyboard after workspace switches by moving dock positioning to the OS keyboard system instead of keyboardLayoutGuide constraints on the surface.

The toolbar and composer band now live in a self-sizing KeyboardDockAccessoryView returned as inputAccessoryView from the surface and the hidden typing proxy, with the surface holding first responder when the keyboard is down so the dock stays at the screen bottom. Grid reservation still follows keyboard height via a new process-wide MobileKeyboardFrameTracker (notification-based, catch-up on late attach) and subtracts the accessory footprint so the dock does not double-count.

WorkspaceDetailView adds .ignoresSafeArea(.container, edges: .bottom) so the terminal surface frame does not breathe with the home indicator mid-transition (which skewed keyboard conversions and caused visible bar pops).

Tests: MobileInjectedAttachStartupTests is rewritten for the shipped startup coordinator API (restores a compile-broken target). New tracker and keyboard-dock contract tests cover accessory wiring, overlap model, and reattach visibility.

Dev tooling: iphone-install-queue.sh installs all pending builds but launches only the newest per drain to avoid foreground fights on a single device.

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

@coderabbitai

coderabbitai Bot commented Aug 5, 2026 •

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds a process-wide UIKit keyboard frame tracker and uses its overlap data to maintain GhosttySurfaceView composer docking. Replaces an asynchronous injected-attach transport test with synchronous startup admission tests.

Changes

Keyboard dock floor

Layer / File(s) Summary
Keyboard transition tracker
Packages/iOS/CmuxMobileSupport/Sources/CmuxMobileSupport/MobileKeyboardFrameTracker.swift, Packages/iOS/CmuxMobileSupport/Tests/CmuxMobileSupportTests/MobileKeyboardFrameTrackerTests.swift
Adds notification-based keyboard transition tracking, lifecycle clearing, view overlap calculation, observer cleanup, and isolated tests.
Composer keyboard floor
Packages/iOS/CmuxMobileTerminal/Sources/CmuxMobileTerminal/GhosttySurfaceView.swift
Applies tracker-derived overlap through a floor constraint, combines it with keyboard-guide geometry, supports late attachment, and clears the floor for debug overrides.
Keyboard docking validation
Packages/iOS/CmuxMobileTerminal/Tests/CmuxMobileTerminalTests/GhosttySurfaceKeyboardDockFloorTests.swift
Tests keyboard rise, late attachment, and composer restoration after dismissal.

Injected attach startup admission

Layer / File(s) Summary
Startup admission validation
Packages/iOS/CmuxMobileShellUI/Tests/CmuxMobileShellUITests/MobileInjectedAttachStartupTests.swift
Replaces the asynchronous URL transport test with synchronous tests for successful admission, fallback after failure, and single-claim exclusivity.

Estimated code review effort: 3 (Moderate) | ~30 minutes

Sequence Diagram(s)

sequenceDiagram
  participant KeyboardNotifications
  participant MobileKeyboardFrameTracker
  participant GhosttySurfaceView
  participant ComposerConstraints
  KeyboardNotifications->>MobileKeyboardFrameTracker: record keyboard frame transition
  KeyboardNotifications->>GhosttySurfaceView: handle keyboard frame change
  GhosttySurfaceView->>MobileKeyboardFrameTracker: read overlap
  GhosttySurfaceView->>ComposerConstraints: apply overlap floor
  ComposerConstraints->>GhosttySurfaceView: reserve maximum guide or floor overlap
Loading

Possibly related PRs

  • manaflow-ai/cmux#9668: Both changes replace obsolete injected-attach transport tests with coordinator admission tests.
  • manaflow-ai/cmux#8899: Both changes modify GhosttySurfaceView keyboard-frame handling and terminal dock positioning.
  • manaflow-ai/cmux#8907: Both changes coordinate keyboard-transition geometry with docking behavior.

Important

Pre-merge checks failed

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

❌ Failed checks (3 errors)

Check name Status Explanation Resolution
Cmux Architecture Rethink ❌ Error The PR adds a process-wide singleton observer/cache and a per-view floor beside keyboardLayoutGuide, creating parallel keyboard-state owners and notification entry points. Move notification handling and the keyboard snapshot into one injected MainActor scene or surface coordinator; make the guide, floor, and viewport consume that snapshot, then remove the singleton observer.
Cmux No Test Or Debug Seam In Production Source ❌ Error GhosttySurfaceView.swift adds #if DEBUG test seams setKeyboardFrameTrackerForTesting and handleKeyboardWillChangeFrameForTesting; tests call them, and no production callers exist. Remove the DEBUG wrapper seams from Sources. Use @testable import with narrowly internal production members, or move test-only support to a dedicated debug/test-support file or target.
Cmux No Ambient Global State ❌ Error MobileKeyboardFrameTracker.swift:25 adds public static let shared runtime state; GhosttySurfaceView.swift:530,1053 consume it, while injection exists only under DEBUG. Remove MobileKeyboardFrameTracker.shared; construct one in the app composition seam and inject it through GhosttySurfaceView's production initializer or scoped owner.
✅ 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 The new tracker is explicitly @MainActor, observer callbacks use a MainActor boundary, and GhosttySurfaceView is UIKit-bound; no new implicit value models, async service protocols, or unisolated Se...
Cmux Swift Blocking Runtime ✅ Passed PR production Swift adds no semaphores, waits, sleeps, delayed dispatch, polling, sync dispatch, or locks; it only uses existing keyboard-curve UIView animation for visible dock movement.
Cmux Browser Automation Off-Main ✅ Passed The full PR diff contains only five Packages/iOS files and no browser.* commands, socket-worker policy changes, WebKit waits, or browser automation routing changes.
Cmux Expensive Synchronous Load ✅ Passed The production diff only adds keyboard notification tracking and layout constraints; it adds no agent-history loader, filesystem/JSON parsing, directory scan, or synchronous heavy load.
Cmux Cache Substitution Correctness ✅ Passed The diff adds only transient keyboard-layout state; it does not replace a persistence, history, undo, or durable snapshot read. The tracker is notification-driven, and UIKit's guide remains the pri...
Cmux No Hacky Sleeps ✅ Passed The PR diff contains only five Swift files and no TypeScript, JavaScript, shell, or build/runtime script changes; this check is not applicable.
Cmux Algorithmic Complexity ✅ Passed The production diff uses scalar keyboard geometry and constraints; its only loop removes a fixed three-observer token array. No scalable collection scans, sorting, joins, or per-target rescans were...
Cmux Swift Concurrency ✅ Passed The feature diff adds no DispatchQueue, Combine, completion-handler, or fire-and-forget Task pattern; NotificationCenter observers use the allowed UIKit boundary and main-actor hop, while tests rem...
Cmux Swift @Concurrent ✅ Passed The PR adds no nonisolated async work or @concurrent annotations; new tracker and UI helpers are synchronous and @MainActor-bound, while the only async test was removed.
Cmux Swift Package Boundaries ✅ Passed The reusable keyboard tracker is in the CmuxMobileSupport SwiftPM target; GhosttySurfaceView changes are UIKit/Ghostty docking glue, an allowed case.
Cmux Swiftpm Lockfiles ✅ Passed PR changes only Swift source/tests; no Package.swift, Package.resolved, .gitignore, workflow, or Xcode package-reference files changed. Package .gitignores contain only .build/.
Cmux Swift Logging ✅ Passed The full PR diff adds no print, debugPrint, dump, NSLog, ad hoc file/stdout logging, Logger constants, or sensitive-data logs.
Cmux User-Facing Error Privacy ✅ Passed Changed production code adds keyboard tracking and layout constraints only; the diff adds no alerts, user-facing errors, command output, or sensitive text. Other additions are tests/comments.
Cmux Full Internationalization ✅ Passed The three PR commits add no user-facing Swift text, catalog entries, or web messages; production additions are keyboard/layout logic, while literals are confined to tests and developer comments.
Cmux Swiftui State Layout ✅ Passed The PR changes only UIKit and test code; the diff adds no SwiftUI, ObservableObject, GeometryReader, lazy-row store references, or render-time state mutation.
Cmux Swift Auxiliary Window Close Shortcuts ✅ Passed PR changes iOS UIView keyboard handling and test UIWindow fixtures only; it adds no user-visible NSWindow, NSPanel, controller, SwiftUI Window, or auxiliary identifier assignment.
Cmux Source Artifacts ✅ Passed The PR changes five tracked Swift source/test files only; all have textual diffs and no artifact directories, generated outputs, binaries, logs, screenshots, or caches.
Title check ✅ Passed The title clearly identifies the iOS keyboard docking fix and the missed layout-guide transition scenario.
Description check ✅ Passed The description clearly explains the bug, root cause, fix, testing, and scope, although several template sections are absent.
✨ 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-ios-kb-dock-attach

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 525-529: Remove production test seams: make
GhosttySurfaceView.keyboardFrameTracker a private let injected through its
normal initializer with .shared as the default, and make
handleKeyboardWillChangeFrame internal while deleting
handleKeyboardWillChangeFrameForTesting. In
Packages/iOS/CmuxMobileTerminal/Tests/CmuxMobileTerminalTests/GhosttySurfaceKeyboardDockFloorTests.swift
lines 95-97 and 159-161, call the internal handler directly; at lines 112-116,
pass the isolated tracker through the initializer.
🪄 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: 8e4a00c4-e250-4da2-80e4-9fcc7ac297c9

📥 Commits

Reviewing files that changed from the base of the PR and between 2d9ba4b and f41d974.

📒 Files selected for processing (4)
  • Packages/iOS/CmuxMobileSupport/Sources/CmuxMobileSupport/MobileKeyboardFrameTracker.swift
  • Packages/iOS/CmuxMobileSupport/Tests/CmuxMobileSupportTests/MobileKeyboardFrameTrackerTests.swift
  • Packages/iOS/CmuxMobileTerminal/Sources/CmuxMobileTerminal/GhosttySurfaceView.swift
  • Packages/iOS/CmuxMobileTerminal/Tests/CmuxMobileTerminalTests/GhosttySurfaceKeyboardDockFloorTests.swift

@azooz2003-bit
azooz2003-bit force-pushed the feat-ios-kb-dock-attach branch from f41d974 to 2298d2a Compare August 5, 2026 20:23
@coderabbitai

coderabbitai Bot commented Aug 5, 2026

Copy link
Copy Markdown

Note

GitHub couldn't provide a complete incremental comparison for this pull request, so CodeRabbit is performing a full review instead. This review may take a little longer.

@azooz2003-bit
azooz2003-bit force-pushed the feat-ios-kb-dock-attach branch from 307427e to 1863c2c Compare August 5, 2026 21:49
@coderabbitai

coderabbitai Bot commented Aug 5, 2026

Copy link
Copy Markdown

Note

GitHub couldn't provide a complete incremental comparison for this pull request, so CodeRabbit is performing a full review instead. This review may take a little longer.

@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: 2

🤖 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/CmuxMobileSupport/Sources/CmuxMobileSupport/MobileKeyboardFrameTracker.swift`:
- Around line 17-25: Remove the process-wide MobileKeyboardFrameTracker.shared
singleton and its singleton-specific documentation. Instantiate the tracker from
the app or scene lifecycle owner, then pass the immutable tracker reference into
each GhosttySurfaceView through its existing injection path, preserving shared
observation without ambient global state.
- Around line 47-52: The MobileKeyboardFrameTracker must clear stale keyboard
overlap when a notification lacks a usable end frame. In
MobileKeyboardFrameTracker.swift lines 47-52, set latestTransition to nil before
returning from the failed transition parse; in
MobileKeyboardFrameTrackerTests.swift lines 112-125, update the regression test
to expect a cleared transition.
🪄 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: 2e3bfd50-646c-45d2-9cb5-21d0563dcc9f

📥 Commits

Reviewing files that changed from the base of the PR and between 2d9ba4b and 1863c2c.

📒 Files selected for processing (5)
  • Packages/iOS/CmuxMobileShellUI/Tests/CmuxMobileShellUITests/MobileInjectedAttachStartupTests.swift
  • Packages/iOS/CmuxMobileSupport/Sources/CmuxMobileSupport/MobileKeyboardFrameTracker.swift
  • Packages/iOS/CmuxMobileSupport/Tests/CmuxMobileSupportTests/MobileKeyboardFrameTrackerTests.swift
  • Packages/iOS/CmuxMobileTerminal/Sources/CmuxMobileTerminal/GhosttySurfaceView.swift
  • Packages/iOS/CmuxMobileTerminal/Tests/CmuxMobileTerminalTests/GhosttySurfaceKeyboardDockFloorTests.swift

azooz2003-bit and others added 3 commits August 5, 2026 15:44
Field repro (IMG_6733.mov): open a workspace with the keyboard recently up,
focus input, the keyboard rises but the accessory toolbar and composer band
stay seated at the screen bottom behind it. The dock is constrained to
UIView.keyboardLayoutGuide, which only reflects transitions UIKit routed to
the view's window while it was installed; a test window's guide never moves,
which reproduces the missed-transition wedge deterministically.

Adds MobileKeyboardFrameTracker (inert in this commit), a DEBUG-only
tracker-injection seam, and a per-view notification test seam. The dock
tests are red until the dock stops depending on the guide alone.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The dock (accessory toolbar + composer band) follows
UIView.keyboardLayoutGuide. The guide only observes keyboard transitions
UIKit routed to the view's window while the view was installed, so a surface
(re)attached around a workspace switch can miss the rise entirely and stay
seated on the guide's bottom-safe-area fallback while the keyboard covers
it — the bars vanish behind the keyboard and the grid keeps keyboard-down
geometry (IMG_6733.mov).

Keyboard notifications are posted process-wide regardless of attachment, so
MobileKeyboardFrameTracker records the latest keyboard end frame as the
floor's single data source, and the dock gains a REQUIRED inequality floor
(composer.bottom <= view.bottom - tracked overlap) beneath the guide
equality, which drops one priority notch. The guide remains the movement
engine; the floor only stops the dock from sitting below the real keyboard.
The per-view notification handler re-reads the tracker on the notification's
own animation curve, and layout passes catch late-attached views up from the
same tracker, so the two paths can never disagree. The viewport model takes
max(guide, floor) so the terminal grid reserves exactly the space the lifted
bars occupy.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The suite was written against a connectInjectedAttach(_:attachURL:_:)
coordinator API that was reworked before #9252 merged, so the file has never
compiled. cmux.xctestplan builds every member test target even under
-only-testing, so this one broken target has been failing the whole
'iOS simulator tests' iphone lane for every dispatch since.

Rewrite the suite against the coordinator API that shipped, preserving the
admission contract: a connected injected attach consumes startup and blocks
the saved-Mac reconnect, a failed one releases startup to it, and only one
startup source can claim admission. The original's URL-connect side-effect
assertion lives in CMUXMobileRootView's path and was never compilable here.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@azooz2003-bit
azooz2003-bit force-pushed the feat-ios-kb-dock-attach branch from 1863c2c to b74ab85 Compare August 5, 2026 22:46
@coderabbitai

coderabbitai Bot commented Aug 5, 2026

Copy link
Copy Markdown

Note

GitHub couldn't provide a complete incremental comparison for this pull request, so CodeRabbit is performing a full review instead. This review may take a little longer.

@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/CmuxMobileShellUI/Tests/CmuxMobileShellUITests/MobileInjectedAttachStartupTests.swift`:
- Around line 14-29: Add a focused test alongside
connectedInjectedAttachConsumesStartupWithoutFallback that claims an injected
attach and finishes it with outcome .awaitingUserApproval. Assert it returns
false, leaves shouldFallBackFromInjectedAttach false, and makes
claimStoredReconnect() return nil, matching the behavior in
MobileStartupConnectionCoordinator.finishInjectedAttach.
🪄 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: 528cf274-b540-44df-ad20-e70c8c55d6b2

📥 Commits

Reviewing files that changed from the base of the PR and between 4b7796a and b74ab85.

📒 Files selected for processing (5)
  • Packages/iOS/CmuxMobileShellUI/Tests/CmuxMobileShellUITests/MobileInjectedAttachStartupTests.swift
  • Packages/iOS/CmuxMobileSupport/Sources/CmuxMobileSupport/MobileKeyboardFrameTracker.swift
  • Packages/iOS/CmuxMobileSupport/Tests/CmuxMobileSupportTests/MobileKeyboardFrameTrackerTests.swift
  • Packages/iOS/CmuxMobileTerminal/Sources/CmuxMobileTerminal/GhosttySurfaceView.swift
  • Packages/iOS/CmuxMobileTerminal/Tests/CmuxMobileTerminalTests/GhosttySurfaceKeyboardDockFloorTests.swift

azooz2003-bit and others added 2 commits August 5, 2026 15:52
Review feedback (CodeRabbit): drop the DEBUG-only tracker swap and the
ForTesting notification wrapper. The surface now takes the tracker as an
immutable init dependency defaulting to the shared process-wide instance,
tests inject a notification-center-isolated tracker through the initializer
and call the internal handler via @testable import. A keyboardWillChangeFrame
without a readable end frame now clears the tracked transition (fail closed)
instead of preserving a stale floor.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Review feedback (CodeRabbit): finishInjectedAttach treats .awaitingUserApproval
like .connected — startup stays consumed and the saved-Mac reconnect must not
dial under the approval prompt. Only .connected was covered, so a regression
in the approval arm could pass.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
azooz2003-bit and others added 2 commits August 5, 2026 20:20
Leaving the workspace detail with the keyboard up and re-entering shows the
hide-keyboard glyph (and keyboardUp=1) although the keyboard is down: the
toolbar button is constructed in the keyboard-up state and nothing reconciles
the visibility bit when a surface (re)mounts without a keyboard event. Red
until the visibility state catches up from the tracked keyboard frame.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The toolbar's keyboard-toggle glyph and the surface's keyboardVisible bit
were only updated from keyboard notifications, so a surface (re)mounted
after the keyboard changed kept the stale state: re-entering a workspace
left with the keyboard up showed the hide-keyboard glyph over a dismissed
keyboard, and the toggle resigned a keyboard that was not there.

The dismiss button is now born in the keyboard-down state, and the layout
catch-up that already re-derives the dock floor from
MobileKeyboardFrameTracker also reconciles keyboardVisible (change-guarded
so the glyph cross-dissolve only runs on real flips). The tracker gains
isVisible(in:), mirroring the notification path's floating-keyboard
semantics.

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

Copy link
Copy Markdown
Collaborator Author

Second fix on this branch (same state-ownership family, reported during dogfood): leaving the workspace detail with the keyboard up and re-entering showed the stale hide-keyboard glyph over a dismissed keyboard, and the toggle resigned a keyboard that was not there. Cause: the toolbar's keyboard button is constructed in the keyboard-up state and keyboardVisible only updated from keyboard notifications, which a remounting surface misses. Fix (5ecfb17 red -> 984c585 green): the button is born keyboard-down, and the same layout catch-up that re-derives the dock floor from MobileKeyboardFrameTracker now reconciles keyboardVisible (change-guarded so the glyph cross-dissolve only runs on real flips). isVisible(in:) added to the tracker with the notification path's floating-keyboard semantics. Red run: https://github.com/manaflow-ai/cmux/actions/runs/31068251560 (keyboardUp=1 + "Hide Keyboard" on re-entry, 4 issues) / green run: https://github.com/manaflow-ai/cmux/actions/runs/31068252978 (all 5 dock tests pass, TEST SUCCEEDED; job badge red is the known zero-match filter guard).

azooz2003-bit and others added 6 commits August 5, 2026 21:06
Dogfood recording (2026-08-05 20:48): the keyboard rose over the bars,
which snapped up only after it settled — on every rise, including rapid
toggles. With usesBottomSafeArea=true the keyboard guide's position is
coupled to bottom-safe-area propagation, which lands at the END of a
keyboard transition, so the guide-constrained dock moved late; the same
coupling explains the settled guide reading the bottom inset below the
notification frame on the simulator.

Switch the guide to pure keyboard tracking (usesBottomSafeArea=false) and
express the keyboard-down seat explicitly: a required cap keeps the dock
above the bottom safe area, decisive only while the keyboard is down (the
guide equality and notification floor are stricter when it is up). The
existing dismissal test covers the cap (it fails without it in the new
guide mode).

DEBUG forensics for this class of bug: kb.willChange logs the notification
frame, tracker/guide/floor trio, and duration; kb.floor marks late floor
application, so a recording can be lined up against which source moved the
dock and when.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Device forensics (kb.willChange/kb.floor, 2026-08-06): every keyboard rise
applied the dock floor at willChangeFrame with an overlap 34pt short of the
settled truth, corrected ~1s later by a layout catch-up — the bars visibly
popped by the home-indicator height after each rise. Cause: the surface
ignored only the KEYBOARD safe area, so its bottom edge respected the home
indicator while the keyboard was down but extended to the window bottom
while it was up (the keyboard region subsumes the indicator inset). The
frame breathed by 34pt on every transition, and converting the (final)
notification end frame through the mid-animation frame under-measured.

Extend the surface under the home indicator in ALL states so its frame is
keyboard-invariant; the dock's required safe-area cap (previous commit)
keeps the bars clear of the indicator while the keyboard is down, and the
grid already reserves the bottom safe area. The tracker also observes
keyboardDidChangeFrameNotification so any consumer converting through a
view that DID move mid-transition converges on settled geometry.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
When the phone reappears after being away, the queue drained every pending
tag with a signed foreground launch, each suspending the previously
launched app seconds after it paired — its stream dropped and the user
found their dogfood app disconnected (2026-08-06 13:41-13:43: four tags
launched back to back). Launch only the newest queued build per drain
pass; older ones install fresh but stay unlaunched and re-pair with stored
credentials on their next manual open.

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

Device forensics: the host resizes the surface by the home-indicator height
across every keyboard transition (bounds flipping 836<->802), so a floor
seeded from a mid-transition view-space conversion lands 34pt shy and pops
into place a second later. Red until the floor is window-anchored.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The surface's frame is not keyboard-invariant and cannot be made so at the
leaf: safe-area propagation resizes it by the home-indicator height across
every keyboard transition (device bounds flipping 836<->802 even with the
container-bottom ignore), so a floor expressed against the surface's bottom
was seeded from a mid-transition conversion 34pt shy of the settled truth
and popped into place when a later layout pass re-derived it.

Anchor the floor constraint to the WINDOW — the only frame that never moves
— installed per window attach and dropped on detach. Its constant is the
notification's window-space overlap (MobileKeyboardTransition.
overlapInWindow(of:)), final on first application, so the bars ride the
keyboard's animation curve with no late correction. The viewport model
converts the window floor into view space per layout pass, converging with
the surface's settled frame.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Rounds of custom dock geometry all failed the same way: some input lied
mid-transition (keyboardLayoutGuide frozen or late on device, the surface's
frame breathing by the home-indicator height, notification frames converted
through that moving frame). Change the owner instead of the inputs: the
toolbar row + composer band now live in one self-sizing UIInputView
(KeyboardDockAccessoryView) that every cmux keyboard owner returns as its
inputAccessoryView — the terminal input proxy while typing, and the surface
itself (canBecomeFirstResponder) in the keyboard-down state, so the system
docks the bar at the screen bottom and carries it on the keyboard's own
animation in every transition, interactive gestures included.

Deleted for the bars: the guide equality and configuration, the
notification floor constraint, the safe-area cap, and all dock position
math. The tracker now feeds only the terminal grid reservation, with the
accessory's own footprint subtracted (keyboard end frames include the
accessory, which would otherwise double-reserve the dock and make the
docked-accessory state read as a visible keyboard).

Dock tests rewritten to the accessory contract: responder wiring, chrome
withholding, docked-state first responder, model derivation, and the glyph
reconcile coverage carried over.

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

@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 2 potential issues.

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 670b0dd. Configure here.

let keyboardUp = try #require(probeValue(of: harness.view, key: "keyboardUp"))
#expect(abs(modelKeyboardHeight - Self.keyboardHeight) <= 1)
// The visibility bit catches up too: the toggle must read hide-keyboard.
#expect(keyboardUp == 1)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Tests assert full keyboard overlap

Medium Severity

The dock regression tests expect keyboardHeight to equal the full tracked window overlap, but production derives that model by subtracting keyboardDockAccessory.contentHeight so the grid can reserve the toolbar and composer separately. With a non-zero accessory footprint those assertions cannot pass, so the rise and late-attach coverage does not validate the real contract.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 670b0dd. Configure here.

)
let viewMaxYInWindow = convert(bounds, to: window).maxY
let belowView = max(0, window.bounds.maxY - viewMaxYInWindow)
return max(0, keyboardOnly - belowView)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Stale floor under-reserves composer growth

Medium Severity

keyboardHeight is computed as tracked overlap minus the live accessory contentHeight, while setComposerBandHeight updates that content height without refreshing the tracked frame. If the accessory grows and UIKit does not immediately post a new keyboard frame, total grid reservation stays at the old overlap even though the OS-hosted dock has moved farther up over the terminal.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 670b0dd. Configure here.

Rapid successive toggles (Aziz's overnight repro) desynced: keyboardVisible
is reconciled from keyboard notifications and lags during back-to-back
transitions, so a quick re-tap re-focused when it should resign until the
keyboard wedged against the button. Branch on the actual first responder
instead; the tracked bit remains display-only (glyph).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@lawrencecchen lawrencecchen added the stale-revisit Closed after 30+ days without activity; preserved for possible revisit or reopening. label Sep 23, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

stale-revisit Closed after 30+ days without activity; preserved for possible revisit or reopening.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants