---
name: rnx-debug
description: Debug rendering, animation, gesture, performance, and accessibility issues in a React Native app running under Peach. Reach for this whenever an animation looks wrong (janky, stutters, snaps, only updates once, or doesn't move at all), a swipe/transition/gesture misbehaves or feels broken, scrolling drops frames or feels slow, the UI renders the wrong tree/props, or a screen reader can't reach something — and drive the rnx CLI debug tooling first, not screenshots or ad-hoc logging.
---

# Peach debug

the foundational skill for "something is wrong with my app in Peach".
covers debugging, performance, and accessibility — three branches that
share the same CLI primitives, the same auto-settling contract, and the
same diff/snapshot workflow. start here for any rendering, interaction,
event-order, or correctness question. for pixel-level rendering fidelity
load `/rnx-visual` instead.

> needs a connected, pinned sim. if `rnx describe` errors, load
> `/rnx-setup` first.

## principle — drive the CLI, don't probe by hand

for any rendering, animation, gesture, or interaction bug, reach for the
rnx CLI debug tooling **first** — `describe` and `find` to read the
tree, `debug snapshot` + `debug diff` to capture before/after, `debug
recent` on a channel and `perf shell` for frame-by-frame stats, and the
timeline for "what just happened". **do not** debug these by eye with
screenshots, sprinkle `console.log` through the render path, or spin up a
one-off browser-automation script. the canvas has no DOM to inspect, and a
CLI capture is reproducible, diffable, and shareable in a way a screenshot
never is.

if the existing CLI tools don't capture the issue cleanly — the animation
is too fast to read, the signal you need isn't on any channel, the
interaction spans a boundary the timeline doesn't section — the right move
is to **improve the debug tooling**, not to paper over it with throwaway
probes. add a durable, channel-gated, start/stop instrument that's off by
default (so baseline performance is untouched) and emits exactly the signal
you're chasing. a good capture command is one-shot reproducible, every
agent after you benefits from it, and the same capture can become a
regression test. a pile of ad-hoc logs helps no one and gets deleted.

## route in

- "my app renders wrong / wrong props / wrong tree" → [debugging](#branch--debugging)
- "feels slow / drops frames / janks on scroll" → [performance](#branch--performance)
- "screen reader can't find this / wrong label / tap target too small" → [accessibility](#branch--accessibility)
- "I want to see what just happened" (ephemeral toast, async navigation,
  fetch + alert sequence) → [the timeline](#the-timeline-what-happened)
- "I want pixel-level fidelity to iOS" → load `/rnx-visual`
- "I want to author a test" → load `/rnx-test`

## anti-patterns (apply to every branch)

- **`a1`/`a2`/`b0` style ids mean Peach sims, never browser tabs.** when a user
  says "look at a3", that's the `rnx list` sim id. do not open chrome
  MCP unless the user explicitly says "use chrome" — the canvas has no
  DOM to inspect anyway.
- **the CLI auto-settles. don't add `sleep`.** every read waits up to
  1800 ms for in-flight transitions to start; every write polls layout
  hash until stable. if a read keeps racing its write, the fix is **never**
  a blind `sleep` — it's `wait selector`, `wait ready`, `wait idle`, or
  fixing the underlying animation-completion signal.
- **prefer `--testid` over text matching.** text drifts under i18n, copy
  changes, and duplicate strings. add `testID` during implementation and
  query against it.
- **pin a sim once.** if more than one is live, an unpinned command
  refuses to choose. run `rnx use <id>` at the start of the workflow.
- **keep one sim and reuse its browser tab.** before `rnx open`, run
  `rnx list --all`. pin the target and use `rnx do reload`; a plain
  `rnx open <target>` also navigates the saved sim. reserve `--new` for a
  genuinely concurrent sim. repeated browser trees consume memory and CPU on a
  shared machine.
- **`debug eval` is an escape hatch.** if a top-level command feels
  broken or missing, file an issue — every agent after you benefits from
  a better CLI, not a pile of ad-hoc scripts.
- **sweep `get errors` and `get requests` after every repro.** the
  one-line summary tells you the count grew; you still have to *look at
  them*. a "fixed" behavior with a new console error is not fixed.
- **"found a testID" is not done.** done is: reliable repro + identified
  root cause (specific node + prop, or event + handler) + verified fix.
- **an empty `find --pressable` is not proof there is no affordance.** it
  lists registered touch responders, and a tamagui `styled()` view whose
  `onPress` comes from the call site is tappable without ever appearing there.
  before concluding a screen has no way out, corroborate with `get a11y` or an
  actual `do tap-text`.
- **after an unexplained restart, re-establish which sim you are on.** a sim
  that reconnected, or a dev stack that came back on a port another process now
  holds, is a fresh instrument; a port answering 200 says something is
  listening, not that it is yours. every Peach command prints its own `→ <sim>`
  line, so reading that line is the check.

## the timeline ("what happened")

between two CLI calls, things happen invisibly — toasts flash, keyboards
open and close, screens push and pop, alerts fire and dismiss, async
fetches resolve. the timeline records them so you can ask "what
happened" instead of racing the UI with `find`.

```sh
rnx timeline start                       # arm recording (cheap kinds default-on)
# reproduce the flow
rnx what-happened                        # events since the last CLI call
rnx what-happened --since 10s            # absolute window
rnx what-happened --kinds toast,fetch    # filter to a few kinds
rnx what-happened --summary              # one-line counts
rnx what-happened --flow                 # section events by screen/route
```

semantic kinds the timeline understands: `keyboard`, `screen`, `route`,
`alert`, `actionsheet`, `picker`, `notification`, `toast`, `scroll`,
`gesture`, `text-input`, `reanimated`, `animation`, `fetch`, `console`,
`app-launch`, `shell`. cheap kinds record by default; heavier kinds
(scroll, gesture, text-input, reanimated, animation) opt in via
`rnx timeline start scroll,gesture,…`.

every other CLI command also prints a one-line footer for events since
the last call — that's the headline indicator. when the footer is empty,
there's nothing new to look at; when it says `since last: 1 error · 1
toast · 1 screen push`, run `rnx what-happened` to see the detail.

> reach for the timeline first when the question involves *something
> that appeared briefly and disappeared*. it's faster than a screenshot
> and tells you the duration too.

## automatic settling — when you actually need a manual wait

every CLI command already participates in a two-phase settle. you only
need a manual wait when:

1. the CLI explicitly printed `⚠ auto-wait timed out after Nms — next
   command may see mid-animation state` *and* your next read depends on
   the animation finishing.
2. the transition is longer than ~400 ms (full stack navigation,
   crossfades, video first-frame, network-backed render).
3. you want `--strict` settle, which also requires animation flags to
   clear — only safe for apps with no perpetual background animation
   (no pulsing dots, no shimmer, no looping Lottie).

```sh
rnx do settle 5                # wait up to 5s for layout stability
rnx wait idle --max-ms 5000    # same thing under the `wait` verb
rnx wait idle --strict         # also require animation flags clear
rnx do tap-id play --no-wait   # skip auto-settle on one command
RNX_NO_AUTO_WAIT=1 rnx ... # disable auto-settle for a lane
```

## branch — debugging

the playbook for "wrong tree / wrong props / wrong handler":

```sh
rnx describe                         # what's on screen now
rnx get errors 5                     # any console smoke?
rnx find --testid <node>             # locate the suspect
rnx debug snapshot before
# reproduce
rnx debug snapshot after
rnx debug diff before after          # structural diff: what changed?
rnx what-happened                    # timeline events during the repro
```

when the repro needs several closely timed inputs, replace the individual
write calls with one inline batch so WebSocket scheduling cannot change the
gesture:

```sh
rnx do touch down 200 600 --then sleep 0.03 --then touch move 200 250 --then touch up 200 250
```

for nested scroll ownership or momentum transfer, compare the shell's resolved
gesture chain with the published scroll registry:

```sh
rnx debug state scroll-input
rnx debug state scroll-mirror
```

reading `describe` output:

```
[button] "Sign in" #loginButton @(0,347) 402x44 [bg:#006AFF fg:#fff] (tap)
<text> "Forgot?" @(341,287) 55x28 [fg:#666 fontSize:14]
```

format: `[type] "text" #testID @(x,y) WxH [styles] (flags)`. coordinates
are points (CSS pixels), origin top-left, parent-relative.

for transitions, alerts, native UI, or anything spanning the shell ↔
tenant boundary, lean on the timeline before reaching for traces:

```sh
rnx timeline start
# reproduce the launch / dismiss / alert
rnx what-happened --kinds shell,alert,screen,animation
```

you're done when:

1. repro is reliable (same input → same output)
2. root cause is identified (specific node + prop, or event + handler,
   not "something in the navigation stack")
3. fix re-verified through the same `snapshot → reproduce → diff` loop

## branch — performance

```sh
rnx perf shell start                  # arm capture (also clears prior frames)
# reproduce the slow interaction
rnx perf shell stop                   # full report, including worst frames
rnx perf shell stop --json            # machine-readable summary
```

`perf shell` output:

```
frames: 847
total: 14123ms
avg: 16.7ms
max: 42ms (⚠ dropped frames)
p95: 22.3ms
layout frames: 123
layout avg: 2.1ms
```

frame budget: target **8 ms / frame (120 fps)** — render <4 ms, layout
<2 ms, JS/react <2 ms. heavier paints can fall back to the 16 ms / 60 fps
budget. **p95 >20 ms is visible jank for a scrolling surface.**

profiling a whole flow:

```sh
rnx maestro test .maestro/scroll-feed.yaml --profile # per-step + frame stats
rnx perf cpu --duration 5 --output /tmp/trace.cpuprofile  # sampled CPU
```

common patterns → first probe:

| symptom | likely | next |
| --- | --- | --- |
| scroll jank | layout firing during scroll | `debug enable layout` + `debug recent layout 40` |
| anim stutter | `Animated.Value` on `width`/`height` | switch to `transform: [{ scale }]` or `translate` |
| first-tap lag | font / image still loading | `get requests` + `debug state image` |
| memory growth | live animation drivers not cleaned up | `debug state animations` |
| slow initial render | many text measurements | `debug recent text` on first paint |

debug channels that help, but cost 1–3 ms/frame on their own — enable
only what you're investigating:

```sh
rnx debug enable layout,onlayout,render,animated
# reproduce
rnx debug recent layout 20            # last 20 layout events
rnx debug recent render 20            # last 20 draw passes
rnx debug disable layout,onlayout,render,animated
```

you're done when: a p95 number for the interaction + named hot path +
remeasured (a fresh `perf start` between runs clears the buffer) and the
number moved.

watch out for:

- **dynamic format strings bust the text-measurement cache.** `${count}
  messages` re-measures every change — render the number and the word as
  separate `<Text>` nodes.
- **`--strict` settle hangs forever on perpetual animation.** anything
  with a pulsing dot or looping Lottie will never go quiet under
  `--strict`. use plain `wait idle` there.
- **p95 over a single run is noise.** take three runs minimum.

## branch — accessibility

```sh
rnx get a11y                          # roles, labels, hints, and target geometry
rnx get layout --styling              # boxes, styles, and native pixel contrast
rnx describe --a11y                   # inline a11y info on every node
```

native styled layout reads rendered pixels rather than inferring contrast from
tokens. it compares the normal frame with one CanvasKit capture where text is
suppressed and adds each text score to the existing layout row and issue list.

contrast summary format:

```
#loginButton @(24,347) 120x24 font:16 color:#777 contrast:1.4/4.5 FAIL on:#888888 "Sign in"
```

finding offenders fast:

```sh
rnx find --no-label                   # interactive nodes missing label
rnx find --no-role                    # interactive nodes missing role
rnx find --pressable --max-dim 44     # undersized tap targets
```

what gets checked:

- **missing labels** — interactive nodes without `accessibilityLabel`
- **missing roles** — `Pressable`/`Touchable` without `accessibilityRole`
- **missing hints** — complex interactions without `accessibilityHint`
- **touch target size** — tappable nodes with hitSlop-expanded rect <44×44
- **contrast** — run `rnx get layout --styling`; it samples final text and
  text-suppressed background pixels, including gradients and composited opacity
- **label / visible-text mismatch** — label that reads differently from
  the text VoiceOver would fall back to (often a translation bug)
- **duplicate labels** — two reachable nodes with identical labels on
  one screen ("Open" + "Open" leaves no way to disambiguate)

triage order:

- **P0** — interactive node with no label or no role (blocks screen
  reader entirely)
- **P1** — tap target <44×44, duplicate labels
- **P2** — contrast, label/visible-text mismatch

fix → verify loop (the same shape as debugging):

```sh
rnx get a11y > before-a11y.txt
rnx get layout --styling --json > before-layout.json
# apply the fix
rnx get a11y > after-a11y.txt
rnx get layout --styling --json > after-layout.json
diff before-a11y.txt after-a11y.txt
diff before-layout.json after-layout.json
```

fixing common cases:

```tsx
// bad: icon button with no accessible name
<Pressable onPress={onBack}>
  <ChevronLeftIcon />
</Pressable>

// good: labeled + roled + hinted + hitSlop'd
<Pressable
  onPress={onBack}
  accessibilityRole="button"
  accessibilityLabel="Back"
  accessibilityHint="Returns to the previous screen"
  hitSlop={12}    // bumps tap target to 44×44 minimum
>
  <ChevronLeftIcon />
</Pressable>

// bad: text input with no programmatic label
<TextInput placeholder="Email" onChangeText={setEmail} />

// good: label survives even when placeholder is hidden
<TextInput
  placeholder="Email"
  accessibilityLabel="Email address"
  onChangeText={setEmail}
/>
```

watch out for:

- **placeholders are not labels.** they disappear after the user types.
- **hitSlop expands the a11y rect too.** rarely need to make the
  rendered button bigger — `hitSlop={12}` is usually enough.
- **`accessibilityRole="image"` is not an `alt` tag.** pair it with
  `accessibilityLabel` or VoiceOver reads "image" and nothing else.
- **`importantForAccessibility="no-hide-descendants"` hides an entire
  subtree.** useful for decorative chrome, dangerous if you forget it.

run the screen reader on a real device at least once before shipping —
labels that read fine on paper often sound robotic ("delete_message_btn_v2"
type names sneak in).

## recovery — common failure modes

- **`describe` returns nothing** — no sim pinned, or the app isn't
  loaded. load `/rnx-setup` and re-confirm.
- **`find` returns multiple matches** — narrow with `--testid`,
  `--visible`, or both.
- **`perf shell` reports zero frames** — you didn't run `perf shell start`
  first, or the capture window closed before any frame committed.
- **`get a11y` returns 100+ items** — narrow with `find --role`,
  `find --pressable`, or `find --no-label`; the a11y tree is descriptive and
  intentionally does not invent an `--issues` grading mode.
- **styled layout cannot capture pixels** — open a tenant app, dismiss system
  overlays, and let the launch transition finish before retrying.
- **timeline footer keeps showing 1 error but `get errors` is empty** —
  the footer reads "since last cursor advance"; `get errors` is absolute.
  the error already scrolled out of the absolute ring buffer.

## reference

### finding and interacting

```sh
rnx describe                          # all visible elements with styles
rnx describe button                   # filter by text/role/label/testID
rnx describe --testid foo             # only the subtree under a testID
rnx describe --verbose                # include every style property

rnx find "Sign in"                    # by text content
rnx find --testid loginButton
rnx find --role button
rnx find --pressable
rnx find --visible                    # only on-screen nodes

rnx do tap-id loginButton             # tap by testID (preferred)
rnx do tap-text "Submit"              # tap by text
rnx do tap 196 400                    # tap at coordinates
rnx do type "hello world"             # type into focused input
rnx do type-into emailInput "a@b"     # focus then type
rnx do key return                     # special key (return, backspace, …)
rnx do scroll feed-list 0 500         # scroll a testID to absolute x/y offsets
rnx do swipe feed-list left           # directional swipe
rnx do long-press messageRow 800      # ms hold
```

### errors and requests

```sh
rnx get errors 5                      # last 5 console errors
rnx get warnings 5
rnx get requests 5                    # recent failed requests
rnx logs --since 30s                  # recent console output
rnx network --since 30s               # recent network traffic
```

### snapshots and diff

```sh
rnx debug snapshot before
# interact
rnx debug snapshot after
rnx debug diff before after           # added / removed / changed nodes
```

### debug channels

useful: `animated`, `layout`, `onlayout`, `render`, `sheets`, `portals`,
`gesture`, `scroll`, `text`, `image`.

```sh
rnx debug enable animated,layout,sheets,portals
rnx debug recent layout 20            # last 20 events on the channel
rnx debug disable animated,layout,sheets,portals
```
