mirror of
https://github.com/stablyai/orca.git
synced 2026-10-24 16:03:45 +00:00
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:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user