Skip to content

Add per-tab NativeInputStateProvider with Room persistence#8488

Draft
malmstein wants to merge 28 commits into
developfrom
feature/david/05-07-unified_input_better_nativeactiveplugin
Draft

Add per-tab NativeInputStateProvider with Room persistence#8488
malmstein wants to merge 28 commits into
developfrom
feature/david/05-07-unified_input_better_nativeactiveplugin

Conversation

@malmstein
Copy link
Copy Markdown
Contributor

@malmstein malmstein commented May 7, 2026

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-tab NativeInputStateProvider backed by Room.

  • Adds NativeInputStateProvider (read) and MutableNativeInputStateProvider (write) interfaces, plus an app-scoped RealNativeInputStateProvider holding Map<TabId, MutableStateFlow<NativeInputState>> in memory and persisting selectedModelId per tab in a new native_input_tab_state table (DB migration 3 → 4).
  • NativeInputModeWidgetViewModel.configure(tabId, ...) calls setActiveTab so the provider merges the persisted selectedModelId for 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. On selectModel(...) it calls mutableProvider.update(tabId) { copy(selectedModelId = ...) }, which writes through to Room.
  • NativeInputHost gains getTabId(), threaded from BrowserTabFragmentNativeInputManager.showNativeInputwidget.configure(tabId, ...) and through ContextualNativeInputManager.
  • NativeInputPlugin.getPromptContribution() keeps a default null impl and is marked @Deprecated so existing plugins don't break while we migrate them off pull-based contributions.

Steps to test this PR

Per-tab model selection

  • Open a tab, switch to chat, pick a non-default model in the model picker
  • Open a second tab, verify the model picker resets (or shows global default), pick a different model
  • Switch back to the first tab — the model you picked there should still be selected
  • Kill the app and reopen — both tabs should restore their per-tab model selection

Send path

  • On a tab with a model selected, send a chat — the selected modelId should be the one you picked in that tab (not the global default)

Contextual input

  • Open Duck.ai contextual mode — tabId should be threaded through and persisted state should still apply

UI changes

Before After
(No UI changes) (No UI changes)

Copy link
Copy Markdown
Contributor Author

This stack of pull requests is managed by Graphite. Learn more about stacking.

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>
@malmstein malmstein changed the title Unified Input: Better NativeActivePlugin Add per-tab NativeInputStateProvider with Room persistence May 7, 2026
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>
@malmstein malmstein force-pushed the feature/david/05-07-unified_input_better_nativeactiveplugin branch from 69d29c9 to fbbeff3 Compare May 12, 2026 15:19
malmstein and others added 22 commits May 12, 2026 22:16
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>
malmstein and others added 5 commits May 12, 2026 22:16
@malmstein malmstein force-pushed the feature/david/05-07-unified_input_better_nativeactiveplugin branch from fbbeff3 to 1ed2f37 Compare May 12, 2026 20:55
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>
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