Migrate StartChat plugin to NativeInputStateProvider#8570
Open
malmstein wants to merge 9 commits into
Open
Conversation
ba04601 to
1803308
Compare
d0cc5a0 to
29a236a
Compare
1803308 to
96d5ee1
Compare
29a236a to
99723a2
Compare
Base automatically changed from
feature/david/native_input_tabid_non_null
to
develop
May 15, 2026 20:25
99723a2 to
5f0da07
Compare
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>
5f0da07 to
241d545
Compare
This was referenced May 17, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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_nulluntil that merges.Description
First plugin migration onto the per-tab push model.
StartChatViewModeldropsDuckChatInputModeStateand observes the tab'sNativeInputStateviaNativeInputStateProvider.NativeInputStategets a storedtoggleSelection: ToggleSelectionfield (defaults to today's context-derived value viaNativeInputState.defaultToggleFor).toggleSelectionis distinct frominputContext: the context says where the widget lives, the selection says which tab the user tapped — they can diverge inside a browser omnibar.NativeInputHostexposesval tabId: String. The widget implements it fromactiveTabId; reads pre-configure throw, mirroringgetInputState().configureMainButtonsVisibilitycallsviewModel.setToggleSelection(...)on everyonTabSelected/onTabReselected, before the existingupdatePluginContainerVisibilitycall.WidgetConfigcarries a nullabletoggleSelectionoverride;configure()resets it (null = follow context default).StartChatViewModelinjectsNativeInputStateProviderand exposesbindTabId(tabId); the combinedisVisibleflowflatMapLatests onstateForTab(tabId)and gates ontoggleSelection == SEARCH.StartChatViewstashes the host's tabId before attach and forwards it to the VM fromonAttachedToWindow; the plugin'screateViewcallsbindTabId(host.tabId)alongside the existingonIconClickedwiring.DuckChatInputModeStatestays in place — still consumed by ambient UI (NTP background logo). Removal happens once nothing reads it.updatePluginContainerVisibilitycarries a TODO: it becomes redundant once every plugin readstoggleSelectionfromNativeInputStateProviderand decides its own visibility.Steps to test this PR
StartChat icon visibility
Toggle / tab switching across tabs
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:
StartChatViewModelnow binds atabIdand observesNativeInputStateProvider.stateForTab(tabId)(droppingDuckChatInputModeState) to decide visibility based oninputModeand the selected toggle.NativeInputStatenow includes a persistedtoggleSelection(defaulted viadefaultToggleFor), the widget ViewModel can explicitlysetToggleSelection, andNativeInputModeWidgetpushes the current selection on tab (re)select and exposesNativeInputHost.tabIdso plugins can subscribe to the correct tab state. Tests are updated and a newStartChatViewModelTestis 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.