Skip to content

Duck.ai Contextual: Multiple page contents#7814

Open
malmstein wants to merge 20 commits intodevelopfrom
feature/david/02-24-duck.ai_contextual_multiple_page_contents
Open

Duck.ai Contextual: Multiple page contents#7814
malmstein wants to merge 20 commits intodevelopfrom
feature/david/02-24-duck.ai_contextual_multiple_page_contents

Conversation

@malmstein
Copy link
Contributor

@malmstein malmstein commented Feb 25, 2026

Task/Issue URL: https://app.asana.com/1/137249556945/project/1174433894299346/task/1213030322231769?focus=true

Description

This PR enables multiple page content attachments to Duck.ai

Steps to test this PR

Automatic Page Page Content Enabled

  • Open Settings / AI Features / Automatic Page Page Content
  • Open a Contextual Sheet
  • Verify Context is attached in Native view
  • Send a prompt
  • Verify context was attached
  • Close the sheet and navigate to a new site
  • Open the sheet again
  • Verify context is pre-attached to the FE input field

Automatic Page Page Content Disabled

  • Open Settings / AI Features / Automatic Page Page Content is disabled
  • Open a Contextual Sheet
  • Verify Context is not attached in Native view
  • Send a prompt
  • Verify context was not attached
  • Attach the context via “Attach Page Content"
  • Enter any prompt so that the content is attached to the chat
  • Close the sheet and navigate to a new site
  • Open the sheet again
  • Verify context is not pre-attached to the FE input field
  • Attach the context via “Attach Page Content”
  • Verify the new site content is in the input field

Note

Medium Risk
Changes Duck.ai contextual page-context collection/attachment flow and JS messaging, which could affect when/what page data is sent to the chat and introduce edge cases around sheet lifecycle timing. Feature behavior is gated via new config flags but touches core browser↔webview integration paths.

Overview
Enables multiple page content attachments for Duck.ai contextual mode by introducing a new capability toggle (supportsMultipleContexts) and exposing it to the web client via getAIChatNativeConfigValues.

Updates the contextual sheet lifecycle to request/collect page context on open/reopen and after the chat finishes loading, routing requests through DuckChatContextualSharedViewModel and moving collection initiation to BrowserTabViewModel.collectPageContext.

Enhances page-context payloads by optionally embedding the page favicon as base64 (new Bitmap.encodeBitmapToBase64 extension) and, when in webview mode with auto-attach or multi-attach enabled, emits submitAIChatPageContext subscription events to attach refreshed context to an existing chat. Tests are expanded/updated for the renamed PageContextJSHelper.collectPageContext, the new toggle, and the new attachment/event behavior.

Written by Cursor Bugbot for commit 00a31f3. This will update automatically on new commits. Configure here.

Copy link
Contributor Author

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

Copy link
Collaborator

@aitorvs aitorvs left a comment

Choose a reason for hiding this comment

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

Reviewing the multiple page contents feature — a few things to address before this is ready to merge.

Copy link
Contributor Author

@malmstein malmstein left a comment

Choose a reason for hiding this comment

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

Replying inline to review feedback.

@malmstein
Copy link
Contributor Author

Thanks again for the review, Aitor! I’ve addressed your feedback; could you please take another look when you get a chance?

@malmstein malmstein force-pushed the feature/david/02-24-duck.ai_contextual_multiple_page_contents branch 7 times, most recently from d0e764c to 08d8391 Compare March 3, 2026 12:10
@malmstein malmstein marked this pull request as ready for review March 4, 2026 10:53
@malmstein malmstein requested a review from mikescamell as a code owner March 4, 2026 10:53
@malmstein malmstein force-pushed the feature/david/02-24-duck.ai_contextual_multiple_page_contents branch from 991bb2a to 00a31f3 Compare March 4, 2026 10:55
Copy link

@cursor cursor bot left a comment

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 3 potential issues.

Bugbot Autofix prepared fixes for all 3 issues found in the latest run.

  • ✅ Fixed: Dead code: safe parsing result silently discarded
    • Removed the dead safe-parsing chain and redundant JSONObject creation by using the safe-parsed result directly with .also{} for logging, matching the pattern in generateContextPrompt().
  • ✅ Fixed: Missing mode check for multiple context config value
    • Added mode == Mode.CONTEXTUAL guard to SUPPORTS_MULTIPLE_PAGE_CONTEXT to match the sibling SUPPORTS_PAGE_CONTEXT check and prevent inconsistent capability advertisement in Mode.FULL.
  • ✅ Fixed: Test fake returns wrong flow for native input
    • Changed observeNativeInputFieldUserSettingEnabled() to return nativeInputFieldUserSettingEnabled instead of the incorrect inputScreenUserSettingEnabled flow.

Create PR

Or push these changes by commenting:

@cursor push 1c7b0bea75
Preview (1c7b0bea75)
diff --git a/duckchat/duckchat-impl/src/main/java/com/duckduckgo/duckchat/impl/contextual/DuckChatContextualViewModel.kt b/duckchat/duckchat-impl/src/main/java/com/duckduckgo/duckchat/impl/contextual/DuckChatContextualViewModel.kt
--- a/duckchat/duckchat-impl/src/main/java/com/duckduckgo/duckchat/impl/contextual/DuckChatContextualViewModel.kt
+++ b/duckchat/duckchat-impl/src/main/java/com/duckduckgo/duckchat/impl/contextual/DuckChatContextualViewModel.kt
@@ -319,15 +319,14 @@
             updatedPageContext
                 .takeIf { it.isNotBlank() }
                 ?.let { runCatching { JSONObject(it) }.getOrNull() }
+                ?.also { json ->
+                    val url = json.optString("url")
+                    logcat { "Duck.ai: generatePageContextEventData for url $url" }
+                }
                 ?: run {
                     logcat { "Duck.ai: no pageContext available" }
                     null
                 }
-
-            val json = JSONObject(updatedPageContext)
-            val url = json.optString("url")
-            logcat { "Duck.ai: generatePageContextEventData for url $url" }
-            json
         } else {
             logcat { "Duck.ai: pageContext is not valid" }
             null

diff --git a/duckchat/duckchat-impl/src/main/java/com/duckduckgo/duckchat/impl/helper/DuckChatJSHelper.kt b/duckchat/duckchat-impl/src/main/java/com/duckduckgo/duckchat/impl/helper/DuckChatJSHelper.kt
--- a/duckchat/duckchat-impl/src/main/java/com/duckduckgo/duckchat/impl/helper/DuckChatJSHelper.kt
+++ b/duckchat/duckchat-impl/src/main/java/com/duckduckgo/duckchat/impl/helper/DuckChatJSHelper.kt
@@ -251,7 +251,8 @@
                 put(
                     SUPPORTS_MULTIPLE_PAGE_CONTEXT,
                     duckChat.isDuckChatContextualModeEnabled() &&
-                        duckChat.areMultipleContentAttachmentsEnabled(),
+                        duckChat.areMultipleContentAttachmentsEnabled() &&
+                        mode == Mode.CONTEXTUAL,
                 )
             }.also { logcat { "DuckChat-Sync: getAIChatNativeConfigValues $it" } }
         return JsCallbackData(jsonPayload, featureName, method, id)

diff --git a/duckchat/duckchat-impl/src/test/kotlin/com/duckduckgo/duckchat/impl/messaging/fakes/FakeDuckChatInternal.kt b/duckchat/duckchat-impl/src/test/kotlin/com/duckduckgo/duckchat/impl/messaging/fakes/FakeDuckChatInternal.kt
--- a/duckchat/duckchat-impl/src/test/kotlin/com/duckduckgo/duckchat/impl/messaging/fakes/FakeDuckChatInternal.kt
+++ b/duckchat/duckchat-impl/src/test/kotlin/com/duckduckgo/duckchat/impl/messaging/fakes/FakeDuckChatInternal.kt
@@ -78,7 +78,7 @@
 
     override fun observeCosmeticInputScreenUserSettingEnabled(): Flow<Boolean?> = cosmeticInputScreenUserSettingEnabled
     override fun observeAutomaticContextAttachmentUserSettingEnabled(): Flow<Boolean> = automaticContextAttachmentUserSettingEnabled
-    override fun observeNativeInputFieldUserSettingEnabled(): Flow<Boolean> = inputScreenUserSettingEnabled
+    override fun observeNativeInputFieldUserSettingEnabled(): Flow<Boolean> = nativeInputFieldUserSettingEnabled
 
     override suspend fun isStandaloneMigrationCompleted(): Boolean = standaloneMigrationCompleted.value

} else {
logcat { "Duck.ai: pageContext is not valid" }
null
}
Copy link

Choose a reason for hiding this comment

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

Dead code: safe parsing result silently discarded

Medium Severity

In generatePageContextEventData(), the takeIf/let/runCatching/getOrNull chain (lines 319–325) evaluates to a JSONObject? but its result is discarded — it's not the last expression in the if block. Instead, the block always evaluates to json from val json = JSONObject(updatedPageContext) on line 327, which unconditionally creates a new JSONObject without the safe runCatching protection. Compare with the identical pattern in generateContextPrompt() where the chain's result IS used as the pageContext value. This appears to be a copy-paste error where extra lines were added after the chain, making the safe-parsing logic dead code and the pageContext variable never null in the valid branch.


Please tell me if this was useful or not with a 👍 or 👎.

Fix in Cursor Fix in Web

SUPPORTS_MULTIPLE_PAGE_CONTEXT,
duckChat.isDuckChatContextualModeEnabled() &&
duckChat.areMultipleContentAttachmentsEnabled(),
)
Copy link

Choose a reason for hiding this comment

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

Missing mode check for multiple context config value

Medium Severity

SUPPORTS_MULTIPLE_PAGE_CONTEXT is missing the mode == Mode.CONTEXTUAL check that its sibling SUPPORTS_PAGE_CONTEXT includes. This means in Mode.FULL, supportsPageContext will be false but supportsMultipleContexts could be true — an inconsistent capability advertisement to the JS frontend. The condition likely needs && mode == Mode.CONTEXTUAL to match the pattern used by SUPPORTS_PAGE_CONTEXT on the preceding line.


Please tell me if this was useful or not with a 👍 or 👎.

Fix in Cursor Fix in Web

override fun observeCosmeticInputScreenUserSettingEnabled(): Flow<Boolean?> = cosmeticInputScreenUserSettingEnabled
override fun observeAutomaticContextAttachmentUserSettingEnabled(): Flow<Boolean> = automaticContextAttachmentUserSettingEnabled
override fun observeNativeInputFieldUserSettingEnabled(): Flow<Boolean> = nativeInputFieldUserSettingEnabled
override fun observeNativeInputFieldUserSettingEnabled(): Flow<Boolean> = inputScreenUserSettingEnabled
Copy link

Choose a reason for hiding this comment

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

Test fake returns wrong flow for native input

Low Severity

observeNativeInputFieldUserSettingEnabled() now returns inputScreenUserSettingEnabled instead of nativeInputFieldUserSettingEnabled. These are two separate MutableStateFlow fields in the fake. This means calling setNativeInputFieldUserSetting() won't affect the flow returned by observeNativeInputFieldUserSettingEnabled(), and tests relying on this fake could silently produce incorrect results.


Please tell me if this was useful or not with a 👍 or 👎.

Fix in Cursor Fix in Web

params = params,
)
} else {
val params =
Copy link
Contributor Author

Choose a reason for hiding this comment

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

we do this to show the “Attach Page Context” button again. Ideally the message should be explicit, like “showAttachPageContextCTA” but the FE decided to send an empty pageContext for this 🤷

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.

3 participants