Add ActivePlugin point for Chat-tab contributions#8948
Conversation
Introduce NativeInputChatTabItemPlugin so modules outside duckchat-impl can contribute their own item(s) to the native-input Chat-tab list, gated by remote config, without depending on duckchat-impl. The API lives in duckchat-api (it travels with native input when it moves to its own module). Each plugin is a factory for a per-binding NativeInputChatTabItem that owns a RecyclerView.Adapter slotted into the existing Chat-tab ConcatAdapter. A plugin declares via supportsQuery whether the host should forward query updates; static items render once. Contributions are inserted at the top of the list in plugin-point (priority) order; the built-in sections are left untouched. Ordering and gating come from the @ContributesActivePlugin annotation. This is the first step toward migrating the existing sections onto the same point. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Showcase a NativeInputChatTabItemPlugin contribution: a dismissible MessageCta pinned to the top of the Chat tab, gated INTERNAL so it only appears in internal/dev builds. It demonstrates the contract end to end — a singleton factory creating a per-binding item, a query-independent item the host never refreshes on keystrokes, and an item that drives its own content (dismissing empties the adapter, which the host folds into the overlay's hasContent). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A non-query item (supportsQuery = false) is a zero-state element: the host now shows it only while the query is empty and removes it as soon as the user starts typing, re-adding it at its original position when the query is cleared. Query-aware items are untouched and keep receiving onQueryChanged. hasContent now counts only visible plugin items. This makes the example message card disappear when filtering suggestions, matching the intended behaviour. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
loadPluginItems runs in its own coroutine while submit runs in another, so a submit can apply before the contributed items exist. Two effects, both flagged by Bugbot on PR 8948: - A non-empty-query submit set the visibility "last state" while there were no plugins yet, and the early-return guard then permanently skipped the zero-state items once loaded, leaving a card visible while typing. - Query-aware items missed the first query and hasContent was computed without the plugin rows, so the overlay could hide before adapters were inserted. Make reconcilePluginVisibility idempotent (per-item membership check already prevents churn, so the stale guard wasn't needed) and replay the latest submit once loadPluginItems finishes, so late-loaded items catch up to the current query, visibility and hasContent. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 01c30c8. Configure here.
| concatAdapter.addAdapter(pluginItems.size, item.adapter) | ||
| pluginItems += item | ||
| } | ||
| replayLastSubmit?.invoke() |
There was a problem hiding this comment.
Plugin load replay uses stale query
High Severity
When loadPluginItems finishes, it invokes replayLastSubmit, which re-runs applySubmit with the query captured on the last completed fetch—not the text currently in the input. If the initial empty-query fetch completes and the user types before plugins load or before the next fetch submits, replay treats the query as empty, keeps non-query plugin rows visible while typing, and can fire a stale onCommit.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit 01c30c8. Configure here.
The example card is an internal-only showcase that ships to no users, so its strings should not enter the localization pipeline. Marking them translatable="false" fixes the MissingTranslation lint errors that broke the Lint CI check. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The example contribution served its purpose validating the plugin point end to end. Remove it (plugin, adapter, and its strings); the mechanism and the public API stay. No user-facing change — the example was internal-only and there are no other contributors yet. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>


Task/Issue URL: https://app.asana.com/1/137249556945/project/1214157224317277/task/1215897994835019
Tech Design URL (if applicable): https://app.asana.com/1/137249556945/task/1215906519502283
Description
Introduces
NativeInputChatTabItemPlugin, anActivePluginPointthat lets modules outsideduckchat-implcontribute their own item(s) to the native-input Chat-tab suggestions list (the list shown under the Chat tab when the input is focused in Search & Duck.ai mode), gated by remote config and without depending onduckchat-impl.The full design lives in the Tech Design. Highlights:
duckchat-api):NativeInputChatTabItemPluginis a factory for a per-bindingNativeInputChatTabItem, which owns aRecyclerView.Adapterslotted into the existing Chat-tabConcatAdapter. A plugin declares viasupportsQuerywhether it participates in filtering:falseitems are zero-state (shown only while the query is empty, hidden once the user types);trueitems stay and receiveonQueryChanged. The interface lives induckchat-apiso it travels with native input when it moves to its own module.duckchat-impl):pluginPointNativeInputChatTabItemPlugin(default enabled); each contributed plugin is additionally gated by its own@ContributesActivePluginflag. Ordering comes from@ContributesActivePlugin(priority = …).duckchat-impl):NativeInputChatSuggestionsBinderloads the enabled plugins and inserts each item's adapter at the top of theConcatAdapterin plugin-point order, above the built-in sections (which are left untouched).submit()forwards the query only to query-aware items and folds visible plugin content into the overlay'shasContent. Loading is decoupled fromsubmit, so the latestsubmitis replayed once plugins finish loading.NativeInputModeWidgetowns a per-bindingCoroutineScopehanded to each item, cancelled at teardown.This is the first step toward later migrating the existing sections (chat history, "View all Chats", URL suggestions, "Search for …") onto the same plugin point — see the Tech Design.
No user-facing change: this PR adds the mechanism only; there are no contributors yet (an internal-only example card was used to validate the contract during development and has been removed).
Steps to test this PR
Plugin mechanism (no user-visible change)
./gradlew :duckchat-impl:testDebugUnitTest --tests "com.duckduckgo.duckchat.impl.ui.nativeinput.views.NativeInputChatSuggestionsBinderTest"passes — covers top insertion in priority order, query forwarding only tosupportsQuery == trueitems, non-query items hidden while typing and restored on clear,hasContentaggregation, and the load-vs-submit race (replay).UI changes
Note
Medium Risk
Touches native input UI assembly and overlay visibility logic with async plugin loading; mistakes could show wrong suggestions or flicker the overlay, but changes are isolated behind feature flags and well-covered by tests.
Overview
Adds
NativeInputChatTabItemPlugininduckchat-apiso other modules can contribute rows to the native-input Chat tab suggestions list via their ownRecyclerView.Adapter, without depending onduckchat-impl. Items declaresupportsQueryfor query forwarding vs empty-state-only behavior.duckchat-implregisters thepluginPointNativeInputChatTabItemPluginactive plugin point (remote-gated).NativeInputChatSuggestionsBinderloads enabled plugins at the top of the existingConcatAdapter, reconciles visibility for non-query items when typing, forwards queries to query-aware items, includes plugin rows inhasContent, and replays the lastsubmitafter async plugin load to avoid races.NativeInputModeWidgetcreates a dedicated coroutine scope for plugins and cancels it on teardown.Unit tests cover ordering, query forwarding,
hasContent, visibility toggling, and submit/load race handling.Reviewed by Cursor Bugbot for commit e41ff60. Bugbot is set up for automated code reviews on this repo. Configure here.