Skip to content

fix(app): restore the invisible settings/model/effort controls on Android - #1598

Open
chphch wants to merge 1 commit into
slopus:mainfrom
chphch:fix/android-native-settings-menu-invisible
Open

fix(app): restore the invisible settings/model/effort controls on Android#1598
chphch wants to merge 1 commit into
slopus:mainfrom
chphch:fix/android-native-settings-menu-invisible

Conversation

@chphch

@chphch chphch commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

On Android the session composer's permission, model and effort controls do not paint: the action row shows the photo, mic and send buttons with an empty gap where the three controls belong, so the model and effort pickers are unreachable on a phone. The controls are still laid out and tappable, which makes it look like the row is simply missing options.

The cause is the Compose → React Native boundary. Each trigger is a React Native subtree handed to a Jetpack Compose DropdownMenu.Trigger, and expo-modules-core wraps such a child in ExpoComposeAndroidView, which renders it with Modifier.size(view.width, view.height) sampled once at composition:

// expo-modules-core/android/src/compose/.../ExpoComposeAndroidView.kt
AndroidView(
  factory = { view },
  modifier = Modifier.size(
    view.width.toFloat().pxToDp().dp,
    view.height.toFloat().pxToDp().dp
  )
)

There is no layout listener, so a trigger composed before React Native has measured it stays pinned at 0x0 for the rest of its life. AgentInput already ships popup pickers for the non-native path that render the same three controls, so Android now uses those and iOS keeps its native menus.

Two smaller fixes in the same area:

  • DropdownMenuItem.Text is a native slot view (SlotView), so the bare string child threw Text strings must be rendered within a <Text> component once per option on every menu render — nine per render on the rig preview. Compose's own Text turns children into the native text prop instead. Same fix in NativeOptionsPicker.android, which has the identical slot usage.
  • The composer passed NativeSettingsMenu the full button style for both the wrapper and the child inside it, so padding applied twice once the controls became visible. The wrapper now carries only the slot's size, matching how HomeDock already splits it.

Verified on an Android emulator (Pixel 5, API 30, arm64) against happy://dev/rig-preview: before the change the row renders only the send control; after it, the gear and the GPT Shared · high model/effort control render and tapping the latter opens the picker.

…roid

Upstream's mobile UI refresh routes the session composer's permission, model
and effort triggers through a Jetpack Compose DropdownMenu. On Android nothing
painted: the row showed the photo, mic and send buttons with an empty gap where
the three controls belong, so the model and effort pickers were unreachable on
a phone.

The trigger content is a React Native subtree hosted inside a Compose view.
expo-modules-core wraps such a child in ExpoComposeAndroidView, which renders it
with `Modifier.size(view.width, view.height)` sampled once at composition and
never re-read — there is no layout listener. Composed before React Native has
measured the child, the trigger is pinned at 0x0 for the rest of its life: laid
out, tappable, invisible. The composer already ships its own popup pickers for
the non-native path, and they render the same controls, so Android now uses
those and iOS keeps its native menus.

Two smaller fixes in the same area:

- `DropdownMenuItem.Text` is a native slot view, so the bare string child threw
  "Text strings must be rendered within a <Text> component" once per option on
  every menu render. Compose's own Text turns children into the native `text`
  prop.
- The composer handed `NativeSettingsMenu` the full button style for both the
  wrapper and the child inside it, which insets the content twice. The wrapper
  now carries only the slot's size.

Verified on an Android emulator (Pixel 5, API 30 arm64) against
`happy://dev/rig-preview`: the gear and the "GPT Shared · high" model/effort
control render, and tapping the latter opens the picker.

Generated with [Claude Code](https://claude.ai/code)
via [Happy](https://happy.engineering)

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>
@chphch

chphch commented Jul 27, 2026

Copy link
Copy Markdown
Contributor Author

Device capture — Android emulator (Pixel 5, API 30, arm64, 393dp wide so compactMobileComposer is on), driven against happy://dev/rig-preview, which mounts a real AgentInput with permission modes, models and effort levels wired up.

Before — the action row paints only the send/stop control; the gear and the model/effort control occupy their slots but render nothing:

before

After — the gear and the GPT Shared · high model/effort control render:

after

After, tapped — the model picker opens:

after, picker open

One caveat on the "before" frame so it is not overstated: it was captured on the same debug build with an earlier, ineffective attempt at the fix in place (wrapping the trigger in RNHostView), not on a separately built pristine main APK. The trigger still rendered nothing, so the frame shows the same behaviour main produces, but it is not a matched-pair capture from two builds. The "after" frames are the shipped patch.

For what it is worth, RNHostView — the expo-ui component whose whole purpose is this Compose → RN boundary — did not rescue it either: with Host + RNHostView the controls painted once and then the entire row, send button included, collapsed after the first tap. That is why this patch routes Android to the composer's existing popup pickers rather than trying to keep the Compose menu.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant