# Proof audit, pass 1 — Android anchored menu

PR [#29263](https://git.cafe/pingdotgg/t3code/pull/29263), issue [#29201](https://git.cafe/pingdotgg/t3code/issues/29201).
Before: [47f2c8d29e](https://git.cafe/pingdotgg/t3code/commit/47f2c8d29e981c7723467efe137ee19e91df924e) (`47f2c8d29e981c7723467efe137ee19e91df924e`).
After: [3b020ab39b](https://git.cafe/pingdotgg/t3code/commit/3b020ab39b72d0a7be90f85515b41d22e5e7b87d) (`3b020ab39b72d0a7be90f85515b41d22e5e7b87d`).
Branch `fix/issue-29201`, target `main`; fixed requested comparison, not the local `main` ref. The before commit is the after commit's sole parent. GitCafe metadata confirms this head and one changed file. Audited read-only in `/home/ubuntu/wt/pr-29263` on 2026-11-03. No capture, client launch, test run, code edit, push, or PR comment in this pass.

## Findings first

1. **Proof remains incomplete.** No new evidence exists for this pass. The old PR body is a capture report, not an independently verified result. Its unexplained independent after-rebuild layout mismatch for F prevents accepting F's zero-diff pair. Retake F from the specified fixture and demonstrate a second independent after build/open has the same geometry.
2. **The actual affected branch is Android, keyboard visible at opening.** The change only removes the 120 ms entering fade from the in-window wrapper. It does not alter menu data, padding, width, placement, scrolling, native popup, or action dispatch. Every required row below is a **recording**: first measurement/opening, reopening, submenu changes, scrolling, and dismissal are temporal behavior. A settled still cannot prove this fix or the fade removal.
3. **iOS, web, and desktop can be excluded.** Android selects `Menu.android.tsx`; iOS selects `Menu.tsx` and its native `MenuView`. Direct callers are Android platform files or Android-gated render branches. `FilesHeader` is gated at its caller; `ThreadListToolbar` is reached through Android HomeHeader or the Android sidebar branch. `GitOverviewSheet` returns its iOS presentations before reaching the Android menu. Importing Android components into a cross-platform file does not mean iOS renders them. `SplitButton.android.tsx` uses its own Compose dropdown and only shares `MenuPopupItems`; it does not render this changed wrapper. Modal Android media menus explicitly use native `MenuView`, also outside scope.
4. **Fresh minimum: eight fixture checkpoints in five capture scenarios, each in explicit Light and Dark.** Merge old B and B-narrow into one narrow, large-text, reversed-order scenario. Separate root/submenu checkpoint rows so a good submenu cannot conceal a clipped root. Keep F separately identifiable. Native control G and synthetic edge fixtures H are documented exclusions from the minimum, not silently claimed as tested.
5. **No code correctness blocker was established by this source-only pass.** The native measurement explanation and resulting visible behavior still require capture. Mobile follow-up preferences are only `queue | steer`; the shared dispatch type's `restart` label is not a reachable mobile preference here and does not warrant another fixture.

## Source anchors

- [Changed component and complete branch logic](https://git.cafe/pingdotgg/t3code/blob/3b020ab39b72d0a7be90f85515b41d22e5e7b87d/apps/mobile/src/components/ui/AndroidAnchoredMenu.tsx#L43).
- [Compose inline/native presentations and menu item shape](https://git.cafe/pingdotgg/t3code/blob/3b020ab39b72d0a7be90f85515b41d22e5e7b87d/apps/mobile/src/components/ui/MenuPopup.android.tsx#L56).
- [Android Menu adapter](https://git.cafe/pingdotgg/t3code/blob/3b020ab39b72d0a7be90f85515b41d22e5e7b87d/apps/mobile/src/components/ui/Menu.android.tsx), [iOS/default Menu](https://git.cafe/pingdotgg/t3code/blob/3b020ab39b72d0a7be90f85515b41d22e5e7b87d/apps/mobile/src/components/ui/Menu.tsx), [iOS/default MenuPopup returns null](https://git.cafe/pingdotgg/t3code/blob/3b020ab39b72d0a7be90f85515b41d22e5e7b87d/apps/mobile/src/components/ui/MenuPopup.tsx).
- [Control sizing](https://git.cafe/pingdotgg/t3code/blob/3b020ab39b72d0a7be90f85515b41d22e5e7b87d/apps/mobile/src/lib/androidControlSizing.ts), [horizontal and hinge placement](https://git.cafe/pingdotgg/t3code/blob/3b020ab39b72d0a7be90f85515b41d22e5e7b87d/apps/mobile/src/lib/menu-placement.ts), [portal lifecycle](https://git.cafe/pingdotgg/t3code/blob/3b020ab39b72d0a7be90f85515b41d22e5e7b87d/apps/mobile/src/components/ui/OverlayPortal.tsx).
- [Theme host, including system-color branch](https://git.cafe/pingdotgg/t3code/blob/3b020ab39b72d0a7be90f85515b41d22e5e7b87d/apps/mobile/src/components/ui/NativeThemeHost.android.tsx), [appearance and readiness](https://git.cafe/pingdotgg/t3code/blob/3b020ab39b72d0a7be90f85515b41d22e5e7b87d/apps/mobile/src/features/settings/appearance/AppearancePreferencesProvider.tsx#L185).

## Complete changed-component input and branch inventory

| Input/branch | Render consequence and interaction | Fixture coverage / scope decision |
|---|---|---|
| `children` ReactNode vs function; `className`, `style` | ReactNode becomes a Pressable anchor with pointer-events-disabled child. Function receives `open`, may install a tap or long-press callback. Anchor dimensions/location feed placement. Android `Menu` clones valid long-press children and adds haptics; otherwise uses normal Pressable mode. | A/B long press, C normal Pressable tap, D/E render-prop icon tap. Styles that only move/resize the anchor collapse to placement/width cases. |
| `anchor === null`; missing node; pending overlay measurement | Closed: no portal. No measured node: `open` does nothing. After opening, backdrop exists but no menu until local frame and `rootHeight` are known. | Opening-to-first-layout recording in all required rows; explicit no-anchor/error simulation unnecessary because measurement code is unchanged. |
| `keyboardWasVisible` false vs true | Frozen at `open`. False uses native Compose DropdownMenu; true uses the changed View + RN ScrollView + inline Compose host. Later keyboard visibility does not switch presentation. | True in all required rows. G optional false control. A recording includes a keyboard hide/show while still open if supported; report it separately if Android Back is consumed by the IME. |
| Live keyboard visibility/height; overlay/root layout | Usable bottom subtracts IME height only while currently visible; overlay onLayout remeasures. Snapshot of anchor stays fixed. | Record exact IME rectangle; A/B keyboard-visible above-anchor, D/E below-anchor. Changed host settling is required evidence. Arbitrary rotation/split-screen reflows are unchanged layout behavior, not extra minimum rows. |
| Above vs below | `opensDown = spaceBelow >= 280 || spaceBelow >= spaceAbove`; 6 dp gap; 12 dp screen margins; maxHeight is min(available space on selected side, 480 dp). Bottom-positioned upward and top-positioned downward are distinct. | A/B/C above, D/D1/E/E1/F below. Log measured geometry; do not assume direction from route name. F must overflow actual maxHeight. |
| Horizontal placement / width | Desired width = round(250 × appFont/16). Left or right anchor alignment chosen by its center; clamped to viewport/pane minus 24 dp. Hinge restricts it to anchor's side. | C left, A/B/D/E right. B width-clamped. H3 optional hinge-left/right harness; same child measurement and changed wrapper, unchanged arithmetic. |
| `title` missing/empty vs nonempty | At root a truthy title adds caption with 16s/8s padding. At submenu, parent/back row replaces root caption. | D untitled root, E titled root, D1/E1/F parent rows. No need extra blank-string title row: same branch as absent. |
| Root `actions` vs `parent.subactions`; count; hidden | Each level filters hidden actions. Zero visible actions leaves only root title, back row, or empty padded host. Nonzero count changes height; sufficiently many rows scroll. A submenu exists only if raw subactions.length > 0, even when all are hidden. | Two-row root A/C/D/E; parent D1/E1; long overflowing F. H1 optional empty/hidden-only host. One/zero-project, zero-catalog, and no-legacy shapes are shorter versions listed below, not omitted by accident. |
| `action.title`, optional truthy `subtitle`; arbitrary lengths | Body label, optional caption; defaultMinSize instead of fixed item height allows wrapping. No numberOfLines truncation in this code. App text controls both width and font/line height. | A subtitles; B enlarged/clamped subtitles (do not claim wrap unless observed); F deliberately wrapped long label. This avoids relying on B subtitles to force wrapping. |
| `image`, valid AppSymbol vs unsupported image; optional string `imageColor` | Valid symbol adds RNHostView leading icon; no/unknown symbol produces no leading slot. Custom string color overrides ordinary/destructive icon color unless disabled. | C valid icons. A/D/E have none. Unsupported names collapse to no-icon layout. H2 optional colored/disabled/destructive composite; no new minimum for each glyph. |
| `subactions.length > 0`, `state` on vs off/undefined/other | Submenu chevron takes precedence over checked state. Leaf `on` shows checkmark; other states show none (there is no mixed-state glyph). Submenu press pushes path; back arrow/hardware Back pops it. | D/D1/E/E1/F cover push, back, checked and unchecked. Arbitrary deeper recursion repeats the same parent branch; H2 optional depth-two drill. |
| `attributes.disabled`, `destructive` | Disabled native row and icon Pressable cannot select; text/icon muted. Destructive enabled label/icon use danger color. Disabled text takes precedence over destructive. These change color/interaction, not wrapper lifecycle. | Enumerated, optional H2: renderer unchanged. No claim that the minimum certifies all menus' permissions/actions. |
| `id` and `onPressAction` optional | Submenu does not call callback. Leaf closes; calls callback only if id defined; missing callback still closes. Backdrop closes and resets path/overlay/rootHeight. | A Queue, B Steer, D1 environment, F project, E toggle/filter. Include first-tap response with IME retained. No-id synthetic leaf in optional H2. |
| Theme, text-size, loading/error | Light/Dark change RN surface and Compose text/icon palettes and native scheme: visibly different, retained for each required row. `system` that resolves to same appearance is identical and dropped. Other theme IDs/Material You supply different colors (and Material You takes a different colorOverrides branch), but add no wrapper/geometry logic; full palette certification is outside this fade fix. App font 11–22 scales fonts, icons, width, corner radius and padding; min row height remains at least 48 dp. No loading/error prop or spinner in AndroidAnchoredMenu/MenuPopup; caller loading/error may remove trigger, disable action or change strings/counts. | Explicit T3 Code Light and Dark at every required row. A/D/E/F 16, B22, C11: retain these sizes, drop full size Cartesian product. Wait for preferences/catalog/connection readiness; never capture a loading/error screen as a menu fixture. H2 can cover busy-disabled colors. |

## Caller inventory and collapse mapping

Paths in this table are relative to `apps/mobile/src/`. All are at the after commit above. The eight direct callers are listed first; the remaining rows exhaust runtime users of the shared Android `Menu` adapter. Duplicate call sites in a file are identified by role. An inventory entry does **not** imply a separate capture requirement: leaf content that shares title/icon/subtitle/check/overflow structure maps to the same fixture.

| Caller | Menu data / conditional trigger | Representative |
|---|---|---|
| [components/ui/Menu.android.tsx](https://git.cafe/pingdotgg/t3code/blob/3b020ab39b72d0a7be90f85515b41d22e5e7b87d/apps/mobile/src/components/ui/Menu.android.tsx) (two branches) | Long press + valid ReactElement vs ordinary tap; forwards actions/title/style/className/callback | A/B vs C |
| [components/ui/AndroidScreenHeader.tsx](https://git.cafe/pingdotgg/t3code/blob/3b020ab39b72d0a7be90f85515b41d22e5e7b87d/apps/mobile/src/components/ui/AndroidScreenHeader.tsx) | Overflow actions only; selected checks, subtitles, disabled/destructive flags. At >=600 dp uses wider inline-action budget; <=2 vs >2 actions changes whether/how many overflow items exist. Loading inline actions show spinner outside menu. | D/E/A shapes; no tablet capture required for unchanged action partition |
| [features/home/ThreadListToolbar.tsx](https://git.cafe/pingdotgg/t3code/blob/3b020ab39b72d0a7be90f85515b41d22e5e7b87d/apps/mobile/src/features/home/ThreadListToolbar.tsx) | Connected environments only; function-child Filter threads anchor, search-focused keyboard | D/D1/F |
| [features/pull-requests/PullRequestToolbar.android.tsx](https://git.cafe/pingdotgg/t3code/blob/3b020ab39b72d0a7be90f85515b41d22e5e7b87d/apps/mobile/src/features/pull-requests/PullRequestToolbar.android.tsx) | Filters nullable; Environment/State/Involvement/Labels/Author/Drafts/Reviews/Checks/Projects/Sort plus disabled Reset; up to 60 dynamic labels/authors, label-color icons, search actions | D/E/F shapes; colored glyph H2 optional |
| [features/settings/SettingsEnvironmentsRouteScreen.tsx](https://git.cafe/pingdotgg/t3code/blob/3b020ab39b72d0a7be90f85515b41d22e5e7b87d/apps/mobile/src/features/settings/SettingsEnvironmentsRouteScreen.tsx) | Android + relay session only; titled single refresh row, disabled while refreshing | E/H2 shapes |
| [features/threads/ThreadSettingsSheet.tsx](https://git.cafe/pingdotgg/t3code/blob/3b020ab39b72d0a7be90f85515b41d22e5e7b87d/apps/mobile/src/features/threads/ThreadSettingsSheet.tsx) | Android models screen; titled Provider submenu + optional Show legacy models; All providers/Favorites + dynamic provider groups; checked selection | E/E1 |
| [features/threads/git/GitOverviewSheet.tsx](https://git.cafe/pingdotgg/t3code/blob/3b020ab39b72d0a7be90f85515b41d22e5e7b87d/apps/mobile/src/features/threads/git/GitOverviewSheet.tsx) | After iOS early returns; Repository options title + single refresh disabled while busy/refreshing | E/H2 shapes |
| [features/files/FilesHeader.tsx](https://git.cafe/pingdotgg/t3code/blob/3b020ab39b72d0a7be90f85515b41d22e5e7b87d/apps/mobile/src/features/files/FilesHeader.tsx) | Android parent gate; File options title + Refresh files. Normal search replaces/hides header trigger, so cannot use focused file search to reach this menu | E shape; don't invent keyboard-visible file-search fixture |
| [components/ui/AppBar.android.tsx](https://git.cafe/pingdotgg/t3code/blob/3b020ab39b72d0a7be90f85515b41d22e5e7b87d/apps/mobile/src/components/ui/AppBar.android.tsx) | `menus` mapped through androidHeaderMenuActions: inline untitled groups flatten, other groups recurse; status/title caption, icon, subtitle, selected, disabled. Search replacement may hide menus; inline search retains them | E/E1/F shapes |
| [features/threads/ThreadComposer.tsx](https://git.cafe/pingdotgg/t3code/blob/3b020ab39b72d0a7be90f85515b41d22e5e7b87d/apps/mobile/src/features/threads/ThreadComposer.tsx) | SendActionButton only while running, steer-capable, valid action and enabled draft; exactly two subtitle leaves, default checked first. Idle, deferred send, editing queue, unsupported steering and disabled send do not offer this menu | A/B |
| [features/threads/ComposerAttachmentButton.tsx](https://git.cafe/pingdotgg/t3code/blob/3b020ab39b72d0a7be90f85515b41d22e5e7b87d/apps/mobile/src/features/threads/ComposerAttachmentButton.tsx) | Supports files and enabled: Photo Library + Choose Files icons; otherwise direct media button, no menu | C |
| [features/threads/ThreadFeed.tsx](https://git.cafe/pingdotgg/t3code/blob/3b020ab39b72d0a7be90f85515b41d22e5e7b87d/apps/mobile/src/features/threads/ThreadFeed.tsx) | Copy prompt link long-press only below prompt-length limit; one icon leaf | C/E shape |
| [features/threads/ThreadQueueControl.tsx](https://git.cafe/pingdotgg/t3code/blob/3b020ab39b72d0a7be90f85515b41d22e5e7b87d/apps/mobile/src/features/threads/ThreadQueueControl.tsx) | Queued-message long press: optional Send now, Edit, destructive Remove; permission/busy disables. Some Android image strings are unsupported and render without icons | A/C/H2 shapes |
| [features/threads/thread-list-v2-items.tsx](https://git.cafe/pingdotgg/t3code/blob/3b020ab39b72d0a7be90f85515b41d22e5e7b87d/apps/mobile/src/features/threads/thread-list-v2-items.tsx) (two menus) | Draft vs pending task long press; thread long press branches on branch, snooze/settlement/canUnsettle, legacy state; icon leaves and nested snooze choices | C/D1/F/H2 shapes |
| [features/home/thread-swipe-actions.tsx](https://git.cafe/pingdotgg/t3code/blob/3b020ab39b72d0a7be90f85515b41d22e5e7b87d/apps/mobile/src/features/home/thread-swipe-actions.tsx) | Swipe action menu optional, forwards title/actions/callback on full-height anchor | D/E shapes; swipe itself unchanged |
| [features/threads/ThreadNavigationSidebar.tsx](https://git.cafe/pingdotgg/t3code/blob/3b020ab39b72d0a7be90f85515b41d22e5e7b87d/apps/mobile/src/features/threads/ThreadNavigationSidebar.tsx) | Android uses ThreadListToolbar; iOS-only legacy header's Menu uses default native iOS implementation. Android submenu All entries additionally have descriptive subtitles; project item optional | D/D1/F with A subtitle structure |
| [features/threads/LinkedPullRequestList.tsx](https://git.cafe/pingdotgg/t3code/blob/3b020ab39b72d0a7be90f85515b41d22e5e7b87d/apps/mobile/src/features/threads/LinkedPullRequestList.tsx) | No actions => plain row; otherwise titled long-press link row with copy/browser/watch/unlink/dismiss, destructive variation | A/C/E/H2 shapes |
| [features/browser/BrowserTabMenu.tsx](https://git.cafe/pingdotgg/t3code/blob/3b020ab39b72d0a7be90f85515b41d22e5e7b87d/apps/mobile/src/features/browser/BrowserTabMenu.tsx) | Streaming remount: paste optional, reload, appearance/zoom/viewport/site-data submenus, destructive close; idle close-only; close disabled by grant | D/E/F/H2 shapes |
| [features/devices/DevicePreviewRouteScreen.tsx](https://git.cafe/pingdotgg/t3code/blob/3b020ab39b72d0a7be90f85515b41d22e5e7b87d/apps/mobile/src/features/devices/DevicePreviewRouteScreen.tsx) | DeviceOptionsMenu converts dynamic AppBarMenuItems, titled; controls/viewport/state determine rows | E/E1/F shapes |
| [features/files/AttachmentFileScreen.tsx](https://git.cafe/pingdotgg/t3code/blob/3b020ab39b72d0a7be90f85515b41d22e5e7b87d/apps/mobile/src/features/files/AttachmentFileScreen.tsx) | Android File actions; preview/source mode + save/remove/capability-dependent rows | C/E/H2 shapes |
| [features/cloud/T3ConnectProfilePage.tsx](https://git.cafe/pingdotgg/t3code/blob/3b020ab39b72d0a7be90f85515b41d22e5e7b87d/apps/mobile/src/features/cloud/T3ConnectProfilePage.tsx) | Environment destructive deregister menu replaced by spinner during operation; disables other triggers while busy | C/H2 shapes |
| [features/usage/UsageRouteScreen.tsx](https://git.cafe/pingdotgg/t3code/blob/3b020ab39b72d0a7be90f85515b41d22e5e7b87d/apps/mobile/src/features/usage/UsageRouteScreen.tsx) | Environment filter only when useful; All + environments, selection checks | E/E1/F shapes |
| [features/settings/SettingsProviderAccountsRouteScreen.tsx](https://git.cafe/pingdotgg/t3code/blob/3b020ab39b72d0a7be90f85515b41d22e5e7b87d/apps/mobile/src/features/settings/SettingsProviderAccountsRouteScreen.tsx) (two menus) | Signed-in account options (change account with optional method submenu, destructive sign out) vs multi-method Sign in with; active sign-in uses other controls, not menu | E/E1/H2 shapes |
| [features/settings/SettingsScheduledTasksRouteScreen.tsx](https://git.cafe/pingdotgg/t3code/blob/3b020ab39b72d0a7be90f85515b41d22e5e7b87d/apps/mobile/src/features/settings/SettingsScheduledTasksRouteScreen.tsx) (two menus) | Form selector action list; task Edit/Pause-or-Resume/optional Run now/Delete, policy disables, webhook omits Run now | D/E/F/H2 shapes |
| [features/settings/components/SettingsEnvironmentFilterHeader.tsx](https://git.cafe/pingdotgg/t3code/blob/3b020ab39b72d0a7be90f85515b41d22e5e7b87d/apps/mobile/src/features/settings/components/SettingsEnvironmentFilterHeader.tsx) | Settings scope title; All environments/projects + dynamic environment icon/subtitle/check rows + project leaves | A/C/E/F shapes |
| [features/workspace/ListFilterMenu.tsx](https://git.cafe/pingdotgg/t3code/blob/3b020ab39b72d0a7be90f85515b41d22e5e7b87d/apps/mobile/src/features/workspace/ListFilterMenu.tsx) | Titled filter chip; forwards list. Callers Home ThreadListFilters and PullRequestListFilters supply environments/projects, PR state/involvement/labels/authors and More submenus | E/E1/F shapes |
| [features/development/ComponentCatalogScreen.tsx](https://git.cafe/pingdotgg/t3code/blob/3b020ab39b72d0a7be90f85515b41d22e5e7b87d/apps/mobile/src/features/development/ComponentCatalogScreen.tsx) | Example menu: titled Copy icon + destructive Discard; development only | E/H2 shapes |
| [native/EnrichedMarkdownText.tsx](https://git.cafe/pingdotgg/t3code/blob/3b020ab39b72d0a7be90f85515b41d22e5e7b87d/apps/mobile/src/native/EnrichedMarkdownText.tsx) | Android file context menu only; MarkdownMenuAnchor forwards long press; title/actions from active link, disabled actions; actions default empty until activeMenu populated | E/H1/H2 shapes |
| [features/pull-requests/PullRequestActivityFilters.tsx](https://git.cafe/pingdotgg/t3code/blob/3b020ab39b72d0a7be90f85515b41d22e5e7b87d/apps/mobile/src/features/pull-requests/PullRequestActivityFilters.tsx) (two menus) | Activity kind 3 checks; Show in activity 2 toggle checks; counts affect trigger text outside menu | E/E1 shapes |
| [features/pull-requests/PullRequestAgentRow.tsx](https://git.cafe/pingdotgg/t3code/blob/3b020ab39b72d0a7be90f85515b41d22e5e7b87d/apps/mobile/src/features/pull-requests/PullRequestAgentRow.tsx) | Enabled agent with >1 thread only; titled thread choices | E/F shapes |
| [features/pull-requests/PullRequestControls.tsx](https://git.cafe/pingdotgg/t3code/blob/3b020ab39b72d0a7be90f85515b41d22e5e7b87d/apps/mobile/src/features/pull-requests/PullRequestControls.tsx) (two Android menus) | AI helpers/optional Open thread menus; permission, pending review and thread count choose controls; dynamic subtitle/icon/disabled/submenu actions | A/C/E/E1 shapes |
| [features/pull-requests/PullRequestFiles.tsx](https://git.cafe/pingdotgg/t3code/blob/3b020ab39b72d0a7be90f85515b41d22e5e7b87d/apps/mobile/src/features/pull-requests/PullRequestFiles.tsx) | Review files title + checked Hide test files icon leaf | C/E shape |
| [features/pull-requests/PullRequestListRow.tsx](https://git.cafe/pingdotgg/t3code/blob/3b020ab39b72d0a7be90f85515b41d22e5e7b87d/apps/mobile/src/features/pull-requests/PullRequestListRow.tsx) | Long-press PR row; dynamic title and quick actions; busy disables trigger | A/C/E/H2 shapes |
| [features/pull-requests/PullRequestReactions.tsx](https://git.cafe/pingdotgg/t3code/blob/3b020ab39b72d0a7be90f85515b41d22e5e7b87d/apps/mobile/src/features/pull-requests/PullRequestReactions.tsx) | Only when canReact; fixed emoji label list and checked viewer reactions | E/F shape |
| [features/pull-requests/PullRequestRouteScreen.tsx](https://git.cafe/pingdotgg/t3code/blob/3b020ab39b72d0a7be90f85515b41d22e5e7b87d/apps/mobile/src/features/pull-requests/PullRequestRouteScreen.tsx) | Android diff options: icon/check leaves and optional review/comment/copy actions depend on permission and selected line/file | C/E shape |
| [features/pull-requests/PullRequestStatusRows.tsx](https://git.cafe/pingdotgg/t3code/blob/3b020ab39b72d0a7be90f85515b41d22e5e7b87d/apps/mobile/src/features/pull-requests/PullRequestStatusRows.tsx) (two menus) | Status actions optional, icons/destructive close; Branch update methods and optional conflict helper/thread submenu | C/E/E1/H2 shapes |
| [features/pull-requests/guide/GuideOutline.tsx](https://git.cafe/pingdotgg/t3code/blob/3b020ab39b72d0a7be90f85515b41d22e5e7b87d/apps/mobile/src/features/pull-requests/guide/GuideOutline.tsx) | Guide options: word-wrap icon/check, optional Rewrite guide | C/E shape |
| [features/pull-requests/guide/PullRequestGuideScreen.tsx](https://git.cafe/pingdotgg/t3code/blob/3b020ab39b72d0a7be90f85515b41d22e5e7b87d/apps/mobile/src/features/pull-requests/guide/PullRequestGuideScreen.tsx) | Android guide word-wrap icon/check only | C/E shape |
| [components/MediaActionsMenu.tsx](https://git.cafe/pingdotgg/t3code/blob/3b020ab39b72d0a7be90f85515b41d22e5e7b87d/apps/mobile/src/components/MediaActionsMenu.tsx) (`MediaMenu` alias) | No actions => child/null; inModal Android => native MenuView excluded; otherwise titled optional long-press copy/share/open actions and disabled sharing state | A/E/H2 shapes |

AppBar menu providers are file browser/viewer (`ThreadFilesRouteScreen`), terminal (`ThreadTerminalRouteScreen`/terminalMenu), browser (`BrowserPreviewRouteScreen`), archive (`ArchivedThreadsScreen`), and review (`ReviewSheet`/useReviewHeaderPresentation). AppBar/AndroidScreenHeader are also used for thread/new-task/settings headers; their action overflow funnels through the single direct caller above. Nonmodal media callers are `ThreadMarkdownImage`, `WorkspaceFileImagePreview`, `VideoAttachmentTile`, and unexpanded `MediaVideoPlayer`; modal `MediaImagePreview`, `VideoPreviewModal`, expanded media player bypass the wrapper. `SplitButton.tsx` is not an Android caller: Metro selects `SplitButton.android.tsx` for PR merge controls.

## Minimum fixture matrix

**Every required checkpoint below is currently MISSING.** `L+D` means two explicit T3 Code appearance captures, not System mode. A normal viewport is one fixed Android portrait device around 411×923 dp (for example 1080×2424 px at 2.625); B uses one real 360×640 dp viewport. Use identical Android/API, density, system font scale 1.0, Gboard language/layout/version, navigation mode, safe areas, app navigation style, and keyboard geometry before and after per scenario. Required set = `(normal × L,D × A,C,D,D1,E,E1,F) + (narrow × L,D × B)` = **16 checkpoint pairs**, from **five scripted scenarios per appearance** (A, B, C, D→D1→F, E→E1). Recordings may cover multiple checkpoint rows; every checkpoint still gets its own four-column proof table.

| Row / status | Exact fixture and affected render path | Minimum viewport × theme × app text | Recording sequence and required observations | Seed through real app / harness boundary |
|---|---|---|---|---|
| **A — MISSING, required, recording** | Active steer-capable turn; short draft `Please keep waiting.`; default Steer. Untitled 2 subtitle leaves: checked Steer now / Interrupt what the agent is doing; unchecked Queue / Run after the current turn. Upward/right, not width-clamped. **Affected.** | Normal × L+D ×16. Both palettes visibly differ; retain both. Other text sizes dropped here because B/C cover scale extrema. | Start before long press → first open/host settling → outside dismiss → reopen → hardware Back/root dismissal → reopen → select Queue on first tap and show exactly 1 queued. Include keyboard-hide/show while open if supported, recording whether Back first dismisses IME or closes menu. Check no clipped subtitle/second row or transient frozen strip after. | Real thread composer. New isolated server home, one project, explicit model/provider supporting steering, active turn held at a known tool step, no existing queue. UI can use a long-running command, but that creates live timers; deterministic stills need a controlled fixture clock/provider or an explicitly labelled harness companion. Do not call an arbitrary live transcript pixel-identical. |
| **B — MISSING, required, recording** | Same 2-leaf structure, **Queue default first and checked**, Steer second. Exact three-line draft: `Please keep waiting.\nThis is a second line.\nAnd a third line.`; 22pt. Desired width344 dp, viewport360 minus margins24 => clamped336 dp. Up/right. **Affected.** Replaces both old B and B-narrow. | Narrow360×640dp × L+D ×22. Retain both palettes and large-text/clamped variant. Default size on this viewport would not exercise clamp and is dropped. | First open → outside dismiss → reopen → select Steer now on first tap → visible steer submission/draft outcome; reset to empty queue before each run. Prove actual width clamp from measured overlay/menu geometry. Subtitles may remain single-line; label wrap is F's job. | Real app: Appearance route `settings/appearance` text22; Chat route `settings/follow-ups` Queue; active turn as A. Use an actual supported small Android device/lease or viewport capability. If unavailable, mark **B missing**, or provide explicitly labelled harness geometry; a 411dp capture is not a substitute. |
| **C — MISSING, required, recording** | Idle/new draft with files-supported provider, keyboard focused. Tap plus. Two leading-icon leaves Photo Library/photo and Choose Files/folder, no subtitle/check/title. Upward/left. **Affected.** | Normal × L+D ×11. Both palettes retained. Small size changes icon17dp, width172dp, padding/radius; min row height48dp, so it is a distinct geometry. Other sizes dropped. | Tap open → both full rows/icons on first measured layout → outside dismiss → reopen → tap Choose Files and show picker then cancel. No upload required. Keyboard must not be dismissed merely by opening menu. | Real New task/idle thread composer. App Appearance text11, provider supports files, enabled attachment button. No running turn or seeded transcript needed. |
| **D — MISSING, required, recording** | Home Threads, search focused, one connected environment and exactly 13 projects as F. Untitled root: Environment + Project, both chevrons, no icons/subtitles. Down/right. **Affected.** | Normal × L+D ×16. Both palettes retained; font cross-product dropped because root structure is covered at default and extremes elsewhere. | First open → fully visible two-row root; dismiss/reopen. Record this checkpoint before drilling Environment and Project; do not substitute the submenu still for root. | Real Home route `''`, open Search threads then Filter threads. Create13 projects via authenticated project RPC/HTTP below; no active threads needed. If adaptive layout shows Android sidebar instead of HomeHeader, disclose it: same toolbar, but its All submenu rows have extra subtitles, a different fixture. Prefer phone HomeHeader for this specified row. |
| **D1 — MISSING, required, recording** | D → Environment submenu: back row Environment; checked All environments; exactly one unchecked environment `Proof environment`. Down/right, no root title caption. **Affected.** | Normal × L+D ×16. Both palettes retained; additional font variants dropped. | Drill → full back/all/environment rows → back arrow → root → drill → hardware Back → root → drill → select environment on first tap → reopen and show changed check. Continue or reset into F. | Same real Home fixture; label paired dev environment in app before capturing. This is the smallest real submenu, not an empty fake environment list. |
| **E — MISSING, required, recording** | Model settings/search focused, title **Model filters**, Provider chevron + unchecked Show legacy models. At least one actual legacy model in catalog; exactly two provider groups for E1. Down/right. **Affected.** | Normal × L+D ×16. Both palettes retained. Title/caption adds height; do not drop E in favor of untitled D. Size variants dropped. | First open → title and both root rows fully visible → toggle legacy → reopen and show checked legacy → drill Provider. Record original unchecked and changed-check checkpoints with matched state before/after. | Real composer model picker → focus Search models → Filter models. Enable/configure two provider instances with distinct groups and a catalog containing a legacy model via normal provider setup. Save actual catalog/group/legacy flags in fixture manifest. No generic RPC to inject this exact catalog was established in this pass: if installed drivers cannot supply it, use **labelled production-component harness** with the exact actions; don't claim an arbitrary synthetic model catalog was seeded through supported RPC. No-legacy real root is a shorter shape, not proof of legacy-row presence. |
| **E1 — MISSING, required, recording** | E → Provider submenu: back Provider; checked All providers; Favorites; exactly two actual provider labels (record their exact values). No caption. Down/right. **Affected.** | Normal × L+D ×16. Both palettes retained; other sizes dropped. | Drill → all 5 rows fully visible → arrow back → root → drill → hardware Back → root → select one provider → reopen and show moved check → outside dismiss. Root title must return on back. | Same real picker/catalog as E. If exact catalog unavailable, same explicitly labelled harness boundary. Provider model counts/loading/errors do not independently render inside this menu; wait for settled catalog. |
| **F — MISSING, required, recording; old evidence contradicted** | D → Project submenu: back Project; All projects; **13 project entries** = `Proof base` plus `Audit project 01`…`Audit project 12`, except 06 title exactly `Audit project with a deliberately long display name that wraps across multiple lines`. Initially All projects checked. At least one actual multiline label and content taller than maxHeight. Down/right. **Affected.** | Normal × L+D ×16. Both palettes retained. Default size already forces long-label wrap; extra 22pt version dropped because B covers scaling and this row covers wrapping/scroll. | Open root → Project → select 01 → reopen Project checked 01 → show long06 wrap and scroll to 12 → select 12 → reopen checked 12 → back/root → dismiss. Save top and scrolled checkpoints. Perform an independent Metro JS rebuild and fresh after launch at the same fixture to reproduce menu and header bounds + line breaks; unexplained F drift is a failure, not raster fuzz. | Real Home fixture. Create exactly 13 distinct projects via API; set project grouping to individual directories and leave all unarchived/unpinned; fixed labels/order/filters and no dynamic thread list. Capture actual visible project labels/order because app grouping can change them. If count, wrapping, or overflow is absent, fixture failed. Do not add threads midway or pair stills from a different take than recording. |

## Excluded rows and variants (explicit status)

| Row / status | Distinct state, availability and why it is outside the minimum |
|---|---|
| **G — DROPPED from minimum; optional recording control** | Keyboard hidden **before** opening A's same actions => native popup; unchanged branch. Seed A then dismiss IME before long press. One matched16pt palette is enough as a branch control; Light/Dark native popup colors are distinct but neither is changed. Record outside dismiss/reopen/select if captured. Do not confuse hiding IME after opening with this control. |
| **H1 — DROPPED; optional labelled harness, recording** | `actions=[]`, all hidden, titled empty, hidden-only submenu, missing anchor measurement. These are genuinely distinct empty host/back/title shapes, but no new empty-data logic was introduced and real listed normal menus provide minimum entries or hide their trigger. Android Markdown has default-empty actions before active link; no deterministic user-facing empty-menu route was established. A harness can supply exact actions/anchor lifecycle. No theme/size Cartesian product warranted for unchanged filtering. |
| **H2 — DROPPED; optional labelled harness or real PR filter, recording** | One composite menu can cover valid icon with string imageColor, unsupported icon, disabled+destructive precedence, checked+submenu precedence, no-id leaf, nested depth2; disabled tap must leave open, leaf closes, nested Back pops once. Real PR label filters supply colored circles; scheduled tasks/account menus supply destructive/disabled rows, but not all combinations. These item branches are unchanged; a full MenuPopup certification would include them, this fade-removal proof need not. Light/Dark colors are visibly distinct; size variants add no uncovered changed-wrapper branch. |
| **H3-L/H3-R — DROPPED; optional labelled hinge harness, recording** | Inject WindowHingeContext and anchors on each side; menu must stay in pane. Physical real app requires hinge hardware; no deterministic simulator hinge route was established. Placement arithmetic is unchanged and both directions/width clamp already exercised. No claim of foldable verification. |
| **H4 — DROPPED; optional geometry harness, recording** | Zero available space, landscape, rotation mid-open, keyboard resize and exact480dp cap boundary are different geometry inputs. The wrapper replacement does not alter their arithmetic. F proves a real scroll constraint and A covers IME interaction; synthetic boundary captures are not mandatory. |
| **Other themes / System — DROPPED** | System resolving to explicit Light/Dark is the same appearance. Built-in palette IDs and Material You are visibly different color variants, and Material You omits native colorOverrides when supported, but all keep the same changed View and matchContents measurement. Two explicit schemes on T3 Code are retained; exhaustive palette validation is outside this one-branch fix. Do not describe this as proof of all themes. |
| **Other providers, counts and labels — COLLAPSED** | AndroidAnchoredMenu receives MenuAction data, not provider kind. Provider-specific runtime errors/loading are outside its renderer. Mobile restart, iOS, web/desktop, native Toolbar/MenuView and Android native SplitButton are not added capture rows. Zero projects removes Project from root; no legacy removes legacy row; additional provider groups merely grow/scroll the submenu. D/E represent populated root structure; F represents growth/wrap/scroll. No separately required screenshot for every shorter subset. |

## Seeding and provenance instructions to capturer

- Each side runs from the exact commit above with its own Metro and isolated server home. Use synthetic data, not copied live conversations. No source edits in production renderer to manufacture a pass. Preserve exact fixture manifest, app preferences, installed native fingerprint, JS bundle revision, environment/project identifiers, menu actions, device dimensions/density/font scale/IME bounds, and screenshot/recording hashes. Keep branch display names/background content equal when possible; do not pair a detached checkout label against a named branch and call it solely a menu diff.
- Supported project route is `POST /api/projects/mutate`; [contract](https://git.cafe/pingdotgg/t3code/blob/3b020ab39b72d0a7be90f85515b41d22e5e7b87d/packages/contracts/src/environmentHttp.ts#L729). Driver creates each project with `type: project.create`, commandId, projectId, title, workspaceRoot and createWorkspaceRootIfMissing in an isolated home. Workspace roots and all driver output belong in a fresh timestamped run directory under the lead's scratch root. Start with no auto-seeded extra projects. Never send fixture mutations to live userdata. Normal navigation can also create these projects.
- Appearance is `settings/appearance`, follow-up default is `settings/follow-ups`; [actual routes](https://git.cafe/pingdotgg/t3code/blob/3b020ab39b72d0a7be90f85515b41d22e5e7b87d/apps/mobile/src/Stack.tsx#L331). Apply explicit T3 Code theme to **both** scheme preferences and wait for preference readiness. Do not infer Light from visible colors while System remains selected.
- E/E1 must use the actual model picker inside the thread settings presentation; `ThreadSettingsModels` is an internal navigator screen, not a claimed deep link. Synthetic actions mounted directly in the production component are a **harness**, even if they visually resemble that route. A real app route with normally configured synthetic provider instance data can count as real, but the driver and catalog response must show how it was seeded. No guarantee of such catalog injection is made here.
- First opening and reopening must be retained on both revisions. A before opening may intermittently render correctly: do not silently choose different actions or discard unsuccessful attempts. Label all takes; require at least one recorded baseline clipped failure for the reported bug and full after behavior across the scripted opens. Do not require every before open to fail.

## Required delivery format and stability gates

For **each required row and each retained theme**, provide one Markdown table with exactly these columns; put SHA labels under every column heading (diff columns identify both input SHAs):

| Before<br>47f2c8d29e | After<br>3b020ab39b | Diff0<br>47f2c8d29e → 3b020ab39b | DiffT<br>47f2c8d29e → 3b020ab39b |
|---|---|---|---|
| matched checkpoint image | matched checkpoint image | exact changed-pixel image | tolerant changed-pixel image, with declared threshold |

Use linked short SHAs in the eventual published headings. A/B opening failures and F top/scrolled states can have multiple labelled checkpoint tables under the row. A recording may be shared by D/D1/F or E/E1, but link its raw file and exact checkpoint times on each row. Keep original-speed opening/measurement windows and original recordings; independently shortened GIFs are not timing evidence. This change removes an entering animation, so stills alone are insufficient for every required row.

**Stability:** make status clock, visible conversation timestamps, live Working timers/counters, provider strings, caret and shimmer phases deterministic for proof stills. This source pass found no supported device-level application-clock freeze, and the old report explicitly says its clock was live; do not pretend `--normalize-status-bar` freezes the app. If the real app cannot be made stable without modification, retain the real interaction recordings and raw unmasked full-frame images, use a clearly labelled deterministic production-component harness for matched static menu proof, and disclose the boundary. Never erase menu pixels or represent masked/cropped images as zero-diff full-device evidence. A declared timer region is an explained residual difference, not an unchanged screenshot.

**Independent F check:** rebuild the after JS bundle with a separate Metro session and fresh launch, independently from the delivered pair using the saved manifest. Match viewport/IME/root/header/menu bounds, app text, project ordering, selected state, and label line counts. Report what changed and why before accepting any nonmatching layout. A before/after zero diff does not override a contradictory after/after rebuild. Do not lower DiffT to hide geometry drift.

No captures were assessed in this pass. Covered: complete requested diff, platform gates, direct and shared Menu callers, menu-item/placement/theme/size/lifecycle branches, real-app seed paths and harness limits, previous evidence claims/gaps. This pass completed within the 45-minute budget.

FRICTION: none

VERDICT: INSUFFICIENT A, B, C, D, D1, E, E1, F (Light and Dark recordings plus matched checkpoint tables; F independent after rebuild)
