Add per-tab NativeInputStateProvider with Room persistence#8488
Draft
malmstein wants to merge 28 commits into
Draft
Add per-tab NativeInputStateProvider with Room persistence#8488malmstein wants to merge 28 commits into
malmstein wants to merge 28 commits into
Conversation
malmstein
added a commit
that referenced
this pull request
May 7, 2026
Replace planned Voice/Image plugins with what actually shipped: StartChatNativeInputPlugin and ModelPickerNativeInputPlugin. NativeInputModeWidget now shown as implementing NativeInputHost (submit() / getInputState()). createView() arrows updated to show the host param. Containers renamed to startChatContainer / modelPickerContainer. Send phase updated: StartChat returns null, ModelPicker returns ModelSelection(modelId) | null. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
3 tasks
malmstein
added a commit
that referenced
this pull request
May 11, 2026
Task/Issue URL: https://app.asana.com/1/137249556945/project/1214157224317277/task/1214636594411870?focus=true ### Description Splits the interface part out of #8488 so it can land independently of the per-tab persistence work. `NativeInputPlugin.createView` previously took an opaque `(Action) -> Unit` callback whose only payload was `Action.StartChat`. That shape didn't extend cleanly: every new plugin → host signal needed another sealed subclass, and plugins had no way to read the host's state at all. This PR replaces the callback with a `NativeInputHost` interface exposing `submit()` and `getInputState()`. The host (`NativeInputModeWidget`) implements it and passes itself to plugins. - `StartChatNativeInputPlugin` now calls `host.submit()` instead of `onAction(Action.StartChat)`. - `ModelPickerNativeInputPlugin` takes the new param without using `host` yet — the persistence PR will use `host.getTabId()` once that field is added. - `Action` (sealed class) is removed; `PromptContribution` is unchanged. The persistence/provider track (#8488) will rebase on this and add `getTabId()` to `NativeInputHost`, deprecate `getPromptContribution()`, and wire `MutableNativeInputStateProvider`. ### Steps to test this PR _Start chat icon_ - [ ] Tap the start-chat icon with no text — should open a new chat session - [ ] Tap the start-chat icon with a query — should submit it as a chat message _Model picker_ - [ ] Open the model picker, select a model, send a chat — selected modelId should still be applied (no behaviour change vs develop) ### UI changes | Before | After | | ------ | ----- | | (No UI changes) | (No UI changes) | <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Medium risk because it changes the plugin↔host communication contract and rewires submit/attachment signals, which could affect core input/attachment UX if any plugin or host path is missed. > > **Overview** > Replaces the native input plugin callback-based `Action` API with a typed `NativeInputHost` interface, so plugins can invoke host behaviour (e.g. `submit`, attachment chooser/state updates) and query `getInputState()`. > > Updates `NativeInputModeWidget` to implement `NativeInputHost` and pass itself into `NativeInputPlugin.createView`, and migrates the start-chat and attachment flows (including `AttachmentView` notifications) to call host methods instead of emitting `Action` events. Tests and plugins are adjusted to the new signature; `PromptContribution` behaviour remains unchanged. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit a82e0ef. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY --> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
malmstein
added a commit
that referenced
this pull request
May 12, 2026
Replace planned Voice/Image plugins with what actually shipped: StartChatNativeInputPlugin and ModelPickerNativeInputPlugin. NativeInputModeWidget now shown as implementing NativeInputHost (submit() / getInputState()). createView() arrows updated to show the host param. Containers renamed to startChatContainer / modelPickerContainer. Send phase updated: StartChat returns null, ModelPicker returns ModelSelection(modelId) | null. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
69d29c9 to
fbbeff3
Compare
Replace planned Voice/Image plugins with what actually shipped: StartChatNativeInputPlugin and ModelPickerNativeInputPlugin. NativeInputModeWidget now shown as implementing NativeInputHost (submit() / getInputState()). createView() arrows updated to show the host param. Containers renamed to startChatContainer / modelPickerContainer. Send phase updated: StartChat returns null, ModelPicker returns ModelSelection(modelId) | null. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add 00-module-dependencies, 01-native-input-dataflow, 03-current-module-deps, and 04-state-management PNGs rendered at --force-device-scale-factor=2 for crisp, high-DPI output. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Docs stay gitignored and are maintained locally / updated by a scheduled task. Removed force-added files from the two previous diagram commits. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Designs a per-tab state provider for native input plugins: app-scoped provider with Room-backed persistence of selectedModelId, push-model replacing getPromptContribution() polling. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
10-task TDD plan covering: NativeInputState expansion, Room DB layer (migration 3→4), provider interfaces + implementation, widget tabId wiring, ModelPickerViewModel provider push, and getPromptContribution() deprecation. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add selectedModelId and attachedImages fields to track plugin contributions: - selectedModelId: the AI model the user selected for this tab - attachedImages: images attached to the current message Add zero() companion function for canonical initial state (SEARCH_ONLY, BROWSER). Both new fields have null/empty defaults to maintain compatibility with existing test code. Also fix unrelated issue in NativeInputModeWidgetViewModelTest where fakePlugin had incorrect createView signature (was using old Action parameter).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…bId, move assignment into coroutine - Add @volatile to activeTabId to ensure visibility across threads - Move activeTabId = tabId inside the IO coroutine so activeTabId and displayedState are always updated atomically on the same thread, eliminating the race window between the two assignments - Remove @Suppress("unused") from coroutineRule in test file; the @get:Rule annotation is sufficient for JUnit to discover the rule
…ure() - Add getTabId(): String to NativeInputHost interface - Update NativeInputWidget interface: configure() and configureContextual() now take tabId: String as first parameter - NativeInputModeWidget stores tabId in a private field and implements getTabId() to satisfy NativeInputHost - Thread tabId through ContextualNativeInputManager.init() and down to widget.configureContextual(); call site in DuckChatContextualFragment extracts tabId from fragment arguments before init() Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…figure() Adds tabId: String parameter to NativeInputManager.showNativeInput() interface and RealNativeInputManager implementation, threads it down through attachWidget(), and fixes the missed configure() call site that was previously passing only isDuckAiMode and isBottom.
…ctiveTab on configure Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…er on selection Inject NativeInputStateProvider and MutableNativeInputStateProvider. Add init(tabId) to bind the ViewModel to a specific tab and expose selectedModel as a per-tab StateFlow. selectModel() now also calls mutableNativeInputStateProvider.update() to persist the choice for the active tab, keeping global modelManager.selectModel() for backward compatibility.
…ead of reading empty state Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace first() with firstOrNull() in test to satisfy DenyListedApi lint rule. Apply spotless import ordering and trailing comma fixes. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
fbbeff3 to
1ed2f37
Compare
Persistence wasn't needed for NativeInputState — replace the Room entity, DAO, and v4 migration added on this branch with a plain in-memory tabId -> NativeInputState map in RealNativeInputStateProvider. Add chatId to NativeInputState so the widget can reflect the Duck.ai chat currently loaded in a tab. DuckAiChatStore.getChat(chatId) exposes a chat record by id so the cache can hydrate selectedModelId from the stored chat. BrowserTabViewModel.evaluateDuckAIPage now keeps the cache in sync with the tab URL: on a Duck.ai URL with chatID it calls loadChatState(tabId, chatId); otherwise chatId is nulled. The cache sync sits outside the showFullScreenMode gate so it stays correct regardless of the feature flag. NativeInputModeWidgetViewModel observes the per-tab provider state and folds selectedModelId, chatId and attachedImages into its emitted state. getSelectedModelId reads from that observed state instead of poking the provider directly. update() now lazily creates per-tab entries so a URL evaluation that lands before the widget attaches doesn't drop the chatId — the later setActiveTab merges its structural fields in without clobbering it. Also drop a stray logcat in toDuckAiChat that would have dumped chat JSON on every read. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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/1214157224317277/task/1214387687457001?focus=true
Description
Replaces the pull-based
NativeInputPlugin.getPromptContribution()model with a push-based, per-tabNativeInputStateProviderbacked by Room.NativeInputStateProvider(read) andMutableNativeInputStateProvider(write) interfaces, plus an app-scopedRealNativeInputStateProviderholdingMap<TabId, MutableStateFlow<NativeInputState>>in memory and persistingselectedModelIdper tab in a newnative_input_tab_statetable (DB migration 3 → 4).NativeInputModeWidgetViewModel.configure(tabId, ...)callssetActiveTabso the provider merges the persistedselectedModelIdfor that tab.getSelectedModelId()reads from the provider first, falling back to the legacy plugin chain during migration.ModelPickerViewModel.init(tabId)binds the picker to a tab. OnselectModel(...)it callsmutableProvider.update(tabId) { copy(selectedModelId = ...) }, which writes through to Room.NativeInputHostgainsgetTabId(), threaded fromBrowserTabFragment→NativeInputManager.showNativeInput→widget.configure(tabId, ...)and throughContextualNativeInputManager.NativeInputPlugin.getPromptContribution()keeps a defaultnullimpl and is marked@Deprecatedso existing plugins don't break while we migrate them off pull-based contributions.Steps to test this PR
Per-tab model selection
Send path
modelIdshould be the one you picked in that tab (not the global default)Contextual input
tabIdshould be threaded through and persisted state should still applyUI changes