Skip to content

Migrate StartChat plugin to NativeInputStateProvider#8570

Open
malmstein wants to merge 9 commits into
developfrom
feature/david/native_input_startchat_migration
Open

Migrate StartChat plugin to NativeInputStateProvider#8570
malmstein wants to merge 9 commits into
developfrom
feature/david/native_input_startchat_migration

Conversation

@malmstein
Copy link
Copy Markdown
Contributor

@malmstein malmstein commented May 14, 2026

Task/Issue URL: https://app.asana.com/1/137249556945/project/1174433894299346/task/1214802413219916?focus=true

Stacked on #8556 — base is feature/david/native_input_tabid_non_null until that merges.

Description

First plugin migration onto the per-tab push model. StartChatViewModel drops DuckChatInputModeState and observes the tab's NativeInputState via NativeInputStateProvider.

  • NativeInputState gets a stored toggleSelection: ToggleSelection field (defaults to today's context-derived value via NativeInputState.defaultToggleFor). toggleSelection is distinct from inputContext: the context says where the widget lives, the selection says which tab the user tapped — they can diverge inside a browser omnibar.
  • NativeInputHost exposes val tabId: String. The widget implements it from activeTabId; reads pre-configure throw, mirroring getInputState().
  • Widget's configureMainButtonsVisibility calls viewModel.setToggleSelection(...) on every onTabSelected / onTabReselected, before the existing updatePluginContainerVisibility call. WidgetConfig carries a nullable toggleSelection override; configure() resets it (null = follow context default).
  • StartChatViewModel injects NativeInputStateProvider and exposes bindTabId(tabId); the combined isVisible flow flatMapLatests on stateForTab(tabId) and gates on toggleSelection == SEARCH.
  • StartChatView stashes the host's tabId before attach and forwards it to the VM from onAttachedToWindow; the plugin's createView calls bindTabId(host.tabId) alongside the existing onIconClicked wiring.

DuckChatInputModeState stays in place — still consumed by ambient UI (NTP background logo). Removal happens once nothing reads it. updatePluginContainerVisibility carries a TODO: it becomes redundant once every plugin reads toggleSelection from NativeInputStateProvider and decides its own visibility.

Steps to test this PR

StartChat icon visibility

  • Enable Duck.ai feature + user setting on; disable input-screen setting (search omnibar mode). On a website, focus the omnibar — StartChat icon visible.
  • Same setup, tap the Duck.ai tab in the toggle — icon hides.
  • Tap back to the Search tab — icon reappears.
  • Disable Duck.ai user setting — icon hides regardless of tab.
  • Enable input-screen setting (SEARCH_AND_DUCK_AI mode) — icon hides.

Toggle / tab switching across tabs

  • Open a website on tab A, tap the Duck.ai toggle. Switch to a fresh tab B in the browser. The widget should reflect tab B's selection independently from tab A.

UI changes

No visual change to existing UI; refactor of how the StartChat icon's visibility is wired.


Note

Medium Risk
Updates native input state modeling and plugin/widget wiring around per-tab state, which could regress toggle selection behavior and StartChat visibility across tabs. No auth, network, or data persistence changes are introduced beyond UI state propagation.

Overview
StartChat plugin visibility is migrated to the per-tab push model: StartChatViewModel now binds a tabId and observes NativeInputStateProvider.stateForTab(tabId) (dropping DuckChatInputModeState) to decide visibility based on inputMode and the selected toggle.

NativeInputState now includes a persisted toggleSelection (defaulted via defaultToggleFor), the widget ViewModel can explicitly setToggleSelection, and NativeInputModeWidget pushes the current selection on tab (re)select and exposes NativeInputHost.tabId so plugins can subscribe to the correct tab state. Tests are updated and a new StartChatViewModelTest is added to cover the new visibility rules.

Reviewed by Cursor Bugbot for commit cebd418. Bugbot is set up for automated code reviews on this repo. Configure here.

@malmstein malmstein marked this pull request as draft May 14, 2026 21:09
@malmstein malmstein force-pushed the feature/david/native_input_tabid_non_null branch 2 times, most recently from ba04601 to 1803308 Compare May 15, 2026 15:49
@malmstein malmstein force-pushed the feature/david/native_input_startchat_migration branch from d0cc5a0 to 29a236a Compare May 15, 2026 15:57
@malmstein malmstein force-pushed the feature/david/native_input_tabid_non_null branch from 1803308 to 96d5ee1 Compare May 15, 2026 20:11
@malmstein malmstein force-pushed the feature/david/native_input_startchat_migration branch from 29a236a to 99723a2 Compare May 15, 2026 20:16
Base automatically changed from feature/david/native_input_tabid_non_null to develop May 15, 2026 20:25
@malmstein malmstein force-pushed the feature/david/native_input_startchat_migration branch from 99723a2 to 5f0da07 Compare May 15, 2026 20:30
malmstein and others added 9 commits May 16, 2026 20:38
NativeInputState gets a stored toggleSelection: ToggleSelection that
defaults to today's context-derived value (the old defaultToggleSelection
getter is now NativeInputState.defaultToggleFor and feeds the default).
toggleSelection is distinct from inputContext: the context says where
the widget lives, the selection says which tab the user tapped — these
can diverge inside a browser omnibar.

Plugins now need a way to read the per-tab state, so add tabId: String
to NativeInputHost. The widget implements it from activeTabId; reading
it pre-configure throws, mirroring getInputState().

Widget's configureMainButtonsVisibility pushes the new selection through
the viewmodel's setToggleSelection on every onTabSelected /
onTabReselected. WidgetConfig carries a nullable toggleSelection
override; configure() resets it (null = follow context default), and
the listener writes the user's explicit pick.

TODO on updatePluginContainerVisibility — it becomes redundant once
every plugin reads toggleSelection from NativeInputStateProvider for
its own visibility. Leaving the call in place for now.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
StartChatViewModel drops DuckChatInputModeState and instead injects
NativeInputStateProvider. It exposes bindTabId(tabId) so the View can
hand it the tabId before isVisible is first observed; the combine
flatMapLatest-es on stateForTab(tabId) and gates on toggleSelection ==
SEARCH (matching the old "showing search tab" check via displayedMode).

StartChatView holds the pending tabId and forwards it to the VM from
onAttachedToWindow. The plugin's createView calls bindTabId(host.tabId)
alongside the existing onIconClicked wiring; host.tabId is now
available on the new NativeInputHost surface added in the previous
commit.

DuckChatInputModeState itself stays in place — it's still consumed by
ambient UI (NTP background logo). Removal happens once nothing reads
it.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
StartChatViewModelTest covers the four-source visibility combine:
feature flag, user setting, input-screen setting, and the bound tab's
toggleSelection from the provider. Also verifies the VM reacts to
later state-flow emissions on the bound tab (later commits to the
provider feeding through the flatMapLatest path).

Two new cases in NativeInputModeWidgetViewModelTest:
- setToggleSelection updates state.toggleSelection.
- configure() resets the override so selection falls back to the
  context-derived default.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
StartChatViewModel was combining
observeInputScreenUserSettingEnabled directly to compute the
"is the input-screen toggle off?" condition, but the per-tab
NativeInputState already carries inputMode derived from the same
source. Drop the extra source and read state.inputMode == SEARCH_ONLY
instead, leaving feature + user-setting flows in place to disambiguate
the "Duck.ai entirely disabled" case (which also produces SEARCH_ONLY).

One fewer source in the combine; the test gains coverage of the
SEARCH_AND_DUCK_AI case.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
setupPlugins() ran from onAttachedToWindow, but NativeInputManager.
attachWidget calls addView before configure() — so plugin createView()
fired while activeTabId was still null. StartChat's createView reads
host.tabId, which now throws on pre-configure access.

Move setupPlugins() into configure()/configureContextual()'s
doOnAttach block so plugins are created only after activeTabId is set.
onAttachedToWindow still re-runs setupPlugins on re-attach when
activeTabId is cached from a previous configure (matches the existing
guard around observeNativeInputState).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The widget's observeNativeInputState was subscribed to
NativeInputStateProvider.stateForTab(tabId), which is seeded with
NativeInputState.zero() — default SEARCH_AND_DUCK_AI. Subscribers that
attach before the widget VM's init block publishes the real state
briefly receive the placeholder. For users with the input-screen
setting off (SEARCH_ONLY mode), this means the toggle row renders on
first focus and only corrects on the next state emission.

The widget already has access to viewModel.state, which combines
activeTabId.filterNotNull() so it doesn't emit until configure() runs.
Its first emission is the real state, not the placeholder. Switch the
widget's observer to viewModel.state for its own UI; plugins continue
to read from the provider for cross-plugin consistency.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
applyDefaultTogglePosition() selects the chat tab when the user's
default toggle pref is DUCK_AI, regardless of whether the toggle row
is actually rendered. For SEARCH_ONLY users (input-screen setting off)
the toggle row is hidden, but the TabLayout's selectedTabPosition
still moves to 1 — which then drives applyTabUi() into chat styling.

Pre-PR 3 the next applyState() reverted that via updateSelectedTab(),
which used the context-derived defaultToggleSelection (SEARCH for
BROWSER). PR 3 made the chat-tab selection sticky in state because the
new onTabSelected listener calls setToggleSelection(DUCK_AI), so
state.toggleSelection then matches the programmatically-selected tab
and updateSelectedTab no longer reverts.

Gate setToggleSelection on nativeInputState.toggleVisible: only treat
the TabLayout selection as user-driven when the toggle row is shown.
For SEARCH_ONLY users the listener stops pushing DUCK_AI into state,
so applyState() reverts the selection back to tab 0 the way it did
before.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
applyDefaultTogglePosition fires on NTP and flips the TabLayout's
selectedTabPosition to 1 if the user's pref is DUCK_AI — without
checking whether the toggle row is actually shown. For SEARCH_ONLY
users the toggle is hidden, but the programmatic tab flip still
triggers the onTabSelected listener, which runs applyTabUi (chat
styling) and updatePluginContainerVisibility(true) — leaking the
Duck.ai controls (model picker, attachments, etc.) into the
search-only UI.

Wait for the first state emission and bail out unless toggleVisible
is true. The default-position concept only applies when the toggle
exists for the user to interact with.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@malmstein malmstein force-pushed the feature/david/native_input_startchat_migration branch from 5f0da07 to 241d545 Compare May 16, 2026 18:45
@malmstein malmstein marked this pull request as ready for review May 16, 2026 18:58
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.

2 participants