Skip to content

Add ActivePlugin point for Chat-tab contributions#8948

Open
malmstein wants to merge 6 commits into
developfrom
feature/david/chat_tab_item_plugin
Open

Add ActivePlugin point for Chat-tab contributions#8948
malmstein wants to merge 6 commits into
developfrom
feature/david/chat_tab_item_plugin

Conversation

@malmstein

@malmstein malmstein commented Jun 21, 2026

Copy link
Copy Markdown
Contributor

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, an ActivePluginPoint that lets modules outside duckchat-impl contribute 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 on duckchat-impl.

The full design lives in the Tech Design. Highlights:

  • API (duckchat-api): NativeInputChatTabItemPlugin is a factory for a per-binding NativeInputChatTabItem, which owns a RecyclerView.Adapter slotted into the existing Chat-tab ConcatAdapter. A plugin declares via supportsQuery whether it participates in filtering: false items are zero-state (shown only while the query is empty, hidden once the user types); true items stay and receive onQueryChanged. The interface lives in duckchat-api so it travels with native input when it moves to its own module.
  • Plugin point (duckchat-impl): pluginPointNativeInputChatTabItemPlugin (default enabled); each contributed plugin is additionally gated by its own @ContributesActivePlugin flag. Ordering comes from @ContributesActivePlugin(priority = …).
  • Host (duckchat-impl): NativeInputChatSuggestionsBinder loads the enabled plugins and inserts each item's adapter at the top of the ConcatAdapter in 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's hasContent. Loading is decoupled from submit, so the latest submit is replayed once plugins finish loading. NativeInputModeWidget owns a per-binding CoroutineScope handed 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 to supportsQuery == true items, non-query items hidden while typing and restored on clear, hasContent aggregation, and the load-vs-submit race (replay).
  • App builds and the Chat tab behaves exactly as before, since no module contributes a plugin yet.

UI changes

Before After
No UI changes — mechanism only, no contributors yet

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 NativeInputChatTabItemPlugin in duckchat-api so other modules can contribute rows to the native-input Chat tab suggestions list via their own RecyclerView.Adapter, without depending on duckchat-impl. Items declare supportsQuery for query forwarding vs empty-state-only behavior.

duckchat-impl registers the pluginPointNativeInputChatTabItemPlugin active plugin point (remote-gated). NativeInputChatSuggestionsBinder loads enabled plugins at the top of the existing ConcatAdapter, reconciles visibility for non-query items when typing, forwards queries to query-aware items, includes plugin rows in hasContent, and replays the last submit after async plugin load to avoid races. NativeInputModeWidget creates 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.

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>
malmstein and others added 2 commits June 21, 2026 22:54
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>

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ 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()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 01c30c8. Configure here.

malmstein and others added 2 commits June 22, 2026 17:02
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>
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.

1 participant