fix(mobile): remount the sheet view on window hand-back so a rebuilt native view repaints

On-device confirmation showed the committed hand-back re-assert never
reaches the native view: progress already sits at 1 and translateY at 0,
so withTiming produces no style delta, and the dead screen stayed
reachable (1/25 on the committed build; 1/9 with a sub-pixel value
nudge, which lands on the stale native binding when the view was rebuilt
with a new tag). Remounting the sheet's Animated.View on an epoch keyed
to the hand-back mounts a fresh native view with the style computed from
the current shared values - progress is already 1, so it paints in place
with no visible animation. 0 dead in 50 attempts on the remount build
under the same churn condition that reproduced the dead screen on base.

LANE-REPORT.md carries the full confirmation evidence and limits.
This commit is contained in:
Brennan Benson
2026-08-28 01:32:17 -07:00
parent 28ac0caf95
commit 1c25b9334f
3 changed files with 117 additions and 3 deletions
+85
View File
@@ -300,3 +300,88 @@ window and no focus stealing. I did not fall back to any other interaction metho
before/after on a deterministic repro.
* The `-ss`-before-`-i` trap in the brief is real; every frame timestamp here used output
seeking.
---
# Confirmation round (2026-08-27, second agent)
A follow-up session on the same worktree and the same **iPhone 17 Pro Max**
(`CCA8A76F-77D1-4461-8318-63C7ED69D28E`, attached by UDID) re-verified the branch on device,
scripted the whole flow through `orca emulator tap`/`type` with a 3-band luma classifier over
frames from the helper's MJPEG stream, and ran attempt blocks against base, the committed fix,
and two candidate heals. Confirmed frames land in `lane1-evidence/07-13`.
## The dead screen was reproduced on base, with the full predicted fingerprint
Under a "commit churn" condition (open the picker over the pinned form, flip Smart↔GitHub tabs
twice — each flip re-runs the source fan-out and re-renders the whole modal content — then select
a row), base hit the dead screen live (`07-confirm-dead-screen-base-tab-churn.png`):
* dimmed workspace list, **no sheet anywhere** — the form's backdrop paints (so the form is
interactive with `progress = 1`) while the sheet itself does not;
* the accessibility tree collapses to the 6 status-bar nodes — the empty modal window owns the
screen (`09-confirm-dead-ax-status-bar-only.json`);
* every tap is swallowed; the only escape is a backdrop tap, which closes the whole modal
(`08-confirm-backdrop-tap-closed-modal.png`) — exactly the user's video.
**Determinism was not achieved.** ~99 valid base attempts across seven conditions (settled-list
selects, mid-search selects, fresh-modal-per-attempt, keystrokes inside the swap window, tab-flip
churn, churn + 4-6 CPU burners through the pinned window) produced exactly one dead hit, in the
churn condition during a spike of unrelated machine load. Commit pressure over the pinned sheet
plus machine load raises the odds; nothing tried forces it on demand. The last hop stays inside
the Fabric commit / Reanimated props race, not on an app-code line.
## The committed fix did NOT close the hole — A1's re-assert was a no-op
On the committed branch build (verified served: `hostEpochRef` in the bundle), the identical
churn block hit the **same dead state 1 in 25** (`12-committed-fix-still-dead-1-of-25.png`,
band-identical to the base dead frame). Mechanism: at hand-back `translateY` is already 0 and
`progress` already 1, so `withTiming(1)` produces no style delta and nothing reaches the native
view — the committed A1 cannot heal the state it targets.
A value-level strengthening (seed a sub-pixel `translateY` nudge so the style output changes)
was built, unit-tested, and **also died on device** (1 in 9, `13-nudge-fix-still-dead-1-of-9.png`):
when the native view is rebuilt with a new tag, shared-value writes land on the stale binding.
## The shipped heal: remount the sheet's view on window hand-back
`MountedBottomDrawer` now keys the sheet's `Animated.View` on a `windowEpoch` that increments
whenever a visible sheet takes the window back (`interactive` false → true). The fresh native
view mounts with the style computed from the **current** shared values — `progress` is already 1,
so it paints in place with no visible animation and no feel change. This is the in-app
equivalent of the tint/Fast-Refresh experiment above, the only intervention that provably
repaints. Cost: the sheet's native subtree is rebuilt once per picker close, and its scroll
position resets (the user is at the form top at that moment anyway).
Regression test: `bottom-drawer-window-handback.test.ts` now asserts the hand-back remount via
the epoch-carrying `nativeID` (fails against the committed code: `expected undefined to be
defined`) and keeps the no-remount-while-pinned guard.
## Rates, same device, same host, same steps (churn condition, fresh modal per attempt)
| build | attempts (valid) | dead |
|---|---|---|
| base `fecdf0bde8` | ~99 across all conditions | **1** (+ first agent's independent live repro) |
| committed fix `b99232f764` | 24 | **1** |
| nudge variant (discarded) | 9 | **1** |
| **remount build (this branch)** | **50 (44 valid selections)** | **0** |
`11-remount-fix-form-returns-with-pr.png` shows the form back on screen ~1.9 s after the row
tap, still holding the picked PR (#16908). One 25-attempt block that overlapped source edits
(Metro Fast Refresh mid-block) was discarded as mixed-build.
## Confirmation-round honest limits
* The dead state is rare at rest (~1-4 % per attempt under churn on this rig). 0/44 on the
remount build is strong but not proof; the mechanism argument (remount is the only heal that
rebinds a rebuilt native view, verified negatively twice) carries the rest of the weight.
* The software keyboard could not be exercised (`orca emulator type` injects HID events without
raising it), so keyboard-dismissal-during-swap remains untested as an amplifier.
* Bug B's reconnect-remount and Bug C's latency numbers were not re-measured; the first agent's
regression tests and measurements stand.
* Rig incidents, all recovered and none affecting results: the dev-client app terminated three
times during long tap-loops (no crash report — likely jetsam); one scripted recovery tap
accidentally submitted the create form up to the setup-trust sheet, which was cancelled with
nothing created (verified against the host's worktree list); a later stray tap opened one idle
Terminal tab in the "main" session of the **lane1 dev instance** — it runs no commands and
dies with that dev host.
@@ -127,6 +127,27 @@ describe('bottom drawer window hand-back', () => {
act(() => renderer.unmount())
})
it('remounts the sheet view when it takes the window back', () => {
const renderer = render(true)
update(renderer, false)
const drawerId = () =>
renderer.root
.findAll((node) => typeof node.props.nativeID === 'string')
.map((node) => node.props.nativeID as string)
.find((id) => id.startsWith('bottom-drawer-window-'))
const pinnedId = drawerId()
expect(pinnedId).toBeDefined()
update(renderer, true)
// Shared-value writes cannot heal a sheet whose native view was rebuilt
// under Reanimated (verified on device); only a remount repaints it. The
// epoch-keyed nativeID is the observable proof the remount happened.
expect(drawerId()).toBeDefined()
expect(drawerId()).not.toBe(pinnedId)
act(() => renderer.unmount())
})
it('does not re-assert while the sheet stays pinned', () => {
const renderer = render(true)
update(renderer, false)
@@ -92,9 +92,13 @@ export function MountedBottomDrawer({
// Why: a sheet pinned under a fill picker keeps progress at 1 while the picker
// owns the window, so nothing re-applies its transform when the picker leaves.
// If the native view was rebuilt underneath (shared modal host swaps the window
// contents), it is left with no transform and never paints — a dimmed, dead
// screen the user can only escape by dismissing the whole modal. Re-assert the
// enter transform whenever a visible sheet takes the window back.
// contents), it is left with a stale transform and never paints — a dimmed,
// dead screen the user can only escape by dismissing the whole modal. Shared-
// value writes cannot heal that (verified on device: an unchanged or nudged
// style lands on the stale native binding), so remount the sheet's view on
// every window hand-back: the fresh native view mounts with the style computed
// from the CURRENT progress (already 1), painting in place with no animation.
const [windowEpoch, setWindowEpoch] = useState(0)
const wasInteractiveRef = useRef(interactive)
useEffect(() => {
const tookWindowBack = visible && interactive && !wasInteractiveRef.current
@@ -104,6 +108,7 @@ export function MountedBottomDrawer({
}
translateY.value = 0
progress.value = withTiming(1, { duration: SHOW_DURATION })
setWindowEpoch((epoch) => epoch + 1)
}, [interactive, visible])
useEffect(() => {
@@ -375,6 +380,9 @@ export function MountedBottomDrawer({
<View style={[styles.anchor, isWideLayout && styles.anchorWide]} pointerEvents="box-none">
<Animated.View
// Why: remount per window hand-back — see the windowEpoch effect.
key={windowEpoch}
nativeID={`bottom-drawer-window-${windowEpoch}`}
style={[
styles.drawer,
fillAvailable ? styles.drawerFill : null,