Skip to content

LGC-2026-06-16T045105

@YoussefKeyrouz YoussefKeyrouz tagged this 15 Jun 19:59
Task/Issue URL:
https://app.asana.com/1/137249556945/project/1212810093780571/task/1215535715356706?focus=true
Tech Design URL (if applicable):
https://app.asana.com/1/137249556945/project/1212810093780571/task/1215708562820722?focus=true

### Description

When an ongoing Duck.ai chat's selected model is no longer available to
the user (subscription lapsed, model retired, access lost), the web
frontend (FE) shows a recovery banner with two CTAs: **Activate
Subscription** and **Switch Model**. With `native-input` enabled, the
native Unified Input owns the submit button and the model picker, so the
FE can no longer drive these itself, it delegates to native over the JS
bridge.

This PR wires native to react to those FE bridge messages:

- **`disableChatInput` / `enableChatInput`**: disable/enable the native
submit button for that chat's tab.
- **`showModelPicker`**: open the native model picker on demand (the
"Switch Model" CTA).
- **`submitChangeModelAction(modelId)`**: native reports back the model
the user picked; the FE owns the chat's model and applies it.

All recovery state is **per-tab/per-chat** so a disabled input or an
open picker in one chat can't leak to another tab or chat.

**Scope: Unified Input only.** The contextual sheet is out of scope.
"Activate Subscription" remains an FE-driven action (native is not
involved); native only handles the three bridge messages above.

### High-level changes
- Native reacts to the three FE recovery bridge messages: disable/enable
the submit button, open the model picker, and report the user's picked
model back to the FE.
- The model picker is now **chat-aware**: its chip and selected
indicator reflect the current chat's model (or the just-picked one
during recovery), not only the global selection.
- During recovery, picking a model is reported to the FE for that chat
and does **not** change the user's global default.
- The submit button now visibly greys out when disabled.
- The picker opens reliably from the recovery banner even when the input
isn't focused (e.g. a chat opened from history).

### Known limitations

- After a recovery model switch, native may pass a model id that doesn't
match the recovered model on the *next* prompt. This is harmless today:
the FE ignores the per-prompt model id for ongoing chats and uses the
chat's own model (which it set when it received the recovery pick). It
is a pre-existing behaviour, not introduced by this PR. We need to
revisit this if we ever start submitting per chat model ID.


### Steps to test this PR

_Prerequisites for testing_

- [ ] Use an **internal build** with `native-input` enabled.
- [ ] To reproduce an unavailable model: open/resume an ongoing chat
whose stored model the current account can't access (e.g. a Plus/Pro
model on a Free account, or after signing out). The FE should render the
recovery banner.

_Happy path: Switch Model recovery_
- [ ] Open an ongoing chat whose model is unavailable for the current
account → FE recovery banner shows.
- [ ] Confirm the native **submit button is disabled**
(greyed/non-tappable) with text in the input.
- [ ] Tap **Switch Model** → the **native model picker opens**,
positioned correctly over the chip.
- [ ] Confirm the picker's selected **tick** is on the chat's
(now-unavailable) model, not the global default.
- [ ] Pick a supported model → the **chip label updates** to the picked
model, the **submit button re-enables**, and the FE hides the banner
confirming receipt of the message.
- [ ] Submit a prompt → recovery ends, chat continues with new model,
subsequent behaviour is normal.

_Activate Subscription CTA (FE-owned)_
- [ ] Tap **Activate Subscription** →. Confirm native handles it
correctly (existing behaviour).

_Dismiss picker without picking_
- [ ] Trigger recovery, tap **Switch Model**, then dismiss the picker
**without** selecting a model by tapping outside.
- [ ] Confirm recovery mode ends (chip returns to normal
enabled/disabled state) but the **submit button stays disabled** (the
model is still unavailable — FE hasn't enabled it).
- [ ] Tap Switch Model again → picker reopens correctly; pick a model →
resolves as in the happy path.

_Per-tab isolation_
- [ ] In tab A, trigger recovery (submit disabled / picker openable).
Switch to tab B (a normal new chat or NTP).
- [ ] Confirm tab B's **submit button is enabled** and its picker
behaves normally (changing the model in B changes the global default, no
recovery routing).
- [ ] Return to tab A → its recovery state is intact (still disabled /
still routes picks to the FE).

_Chat change_
- [ ] In a tab in recovery, navigate to a **different chat** (or start a
new chat) → submit re-enables and recovery clears.
- [ ] Re-open the **same** ongoing chat while it's still unavailable →
submit returns to disabled.

_NTP _
- [ ] Trigger recovery in a tab, then **open a brand-new tab** → the new
tab's submit button must be **enabled**.

_Recovery pick doesn't change the global default_
- [ ] Start a chat on any model.
- [ ] Switch to an ongoing chat that used a model no longer available.
- [ ] Complete a recovery pick in an ongoing chat. Then start a **new
chat** (NTP / fresh input) → it should use the **previous global default
model**, not the model just picked for recovery.

_Normal (non-recovery) picker behaviour unaffected_
- [ ] In a new chat (not in recovery), open the picker and pick a model
→ it selects normally (global default updates), submit behaves as before



<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Medium Risk**
> Touches native↔web bridge, per-tab input state, and model-picker
routing for ongoing chats; scope is Duck.ai unified input only with
solid test coverage, but regressions in normal picker/submit behavior
are possible.
> 
> **Overview**
> When the Duck.ai web UI hits an **unavailable model** on an ongoing
chat, it can no longer drive submit or the model picker; this PR wires
**native unified input** to the FE recovery bridge so Android owns those
controls per tab.
> 
> **Bridge handling:** New JS methods `disableChatInput`,
`enableChatInput`, and `showModelPicker` update per-tab `submitEnabled`
/ open the picker via `requestShowModelPicker` + tab-scoped events. User
picks in recovery emit **`submitChangeModelAction`** (`modelId`,
`platform: android`) from `BrowserTabFragment` instead of changing the
global default.
> 
> **Recovery UX:** `NativeInputState` gains `modelChangeMode` and
`submitEnabled`. During recovery the picker stays usable, taps route to
the FE, chip/tick/capabilities follow the **chat or just-picked** model
(`ModelPickerViewModel` + widget observers), submit greys out when
disabled, and `openPicker()` requests focus so the menu works from
history/unfocused chats. Recovery clears on prompt submit, chat change,
or picker dismiss without a selection.
> 
> **Tests** cover JS helper, picker VM, widget VM, and
`showModelPickerEvents`.
> 
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
52c7026fe877871833fe1ca7022ac6a05af2479e. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
Assets 2
Loading