[pull] develop from duckduckgo:develop#350
Open
pull[bot] wants to merge 6167 commits into
Open
Conversation
3bc8c57 to
ceb6fa4
Compare
Task/Issue URL: https://app.asana.com/1/137249556945/project/488551667048375/task/1213890328647785?focus=true ### Description - Removes the intermediate unfocussed state of the native input ### Steps to test this PR _With native input enabled_ - [ ] Focus the omnibar - [ ] Verify that the native input is shown - [ ] Go back or hide the keyboard - [ ] Verify that the unfocussed omnibar is shown Repeat for top, bottom and split configurations <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Behavior and animation changes in keyboard hide/show flows can cause regressions in native-input layout, focus, and visual transitions across top/bottom omnibar modes. > > **Overview** > Removes the “intermediate” native-input UI state by deleting card width/margin animations and related layout tweaking. > > `NativeInputManager` now skips bottom-card expansion and end-margin adjustments on keyboard visibility changes; on keyboard hide (non-DuckAI) it shows a transparent omnibar and then schedules `hideNativeInput()` instead of keeping a rounded/inset widget card visible. Related API surface is simplified by removing `animateCardWidth`, `applyRoundedCardShape`, and `getButtonsWidth`, and `cancelAnimation()` no longer manages a separate card-width animator. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 34c2172. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY -->
Task/Issue URL: https://app.asana.com/1/137249556945/project/488551667048375/task/1214047603542125?focus=true ### Description - Fixes a bug where the omnibar buttons weren’t shown in Duck.ai split mode ### Steps to test this PR _With native input enabled_ - [ ] Enable split mode - [ ] Got to Duck.ai - [ ] Verify that the omnibar buttons are visible <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Low Risk** > Small, view-visibility-only change scoped to split-mode Duck.ai omnibar transitions; low likelihood of impacting data or security. > > **Overview** > When entering Duck.ai *native input* with the omnibar background hidden, the split-omnibar toolbar buttons (`fireIconMenu`, `tabsMenu`, `browserMenu`) are now explicitly shown so the top bar retains its controls. > > On `restore`, those buttons are hidden again in split mode to return the omnibar to its normal state. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit a375172. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY -->
Task/Issue URL: https://app.asana.com/1/137249556945/project/488551667048375/task/1213890416683396?focus=true ### Description - Fixes an issue where URLs were submitted to Duck.ai when the native input was toggled to Duck.ai ### Steps to test this PR _With the native input enabled_ - [ ] Toggle to Duck.ai and submit a URL - [ ] Verify that the URL is opened in the browser not Duck.ai <!-- CURSOR_SUMMARY --> --- > [!NOTE] > <sup>[Cursor Bugbot](https://cursor.com/bugbot) is generating a summary for commit 8afa80f. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY -->
Task/Issue URL: https://app.asana.com/1/137249556945/project/1174433894299346/task/1213831723087162 ### Description Adds a new `errorCodePixel` remote feature flag (defaulting to `false`) and `m_errorpageshown_code` pixel that fires for **all** main-frame WebView errors — including those previously silently discarded as `OMITTED`. The pixel sends the raw error code string as an `error_code` parameter. This gives the team data on which error codes occur in the wild before expanding Yeti error page coverage to more error types. The existing `errorPagePixel` / `ERROR_PAGE_SHOWN` behaviour is completely unchanged. OMITTED errors continue to suppress the error page UI and `WebViewError` command. Changes are confined to `:app`: - `AndroidBrowserConfigFeature` — new `errorCodePixel()` toggle - `AppPixelName` — new `ERROR_CODE_PIXEL("m_errorpageshown_code")` entry - `WebViewClientListener` — `onReceivedError` gains a `rawErrorCode: String` param - `BrowserWebViewClient` — calls listener for all main-frame errors (not just non-OMITTED) - `BrowserTabViewModel` — gates UI/command on `errorType != OMITTED`; fires new pixel when flag enabled - Tests updated and 5 new tests added ### Steps to test this PR _Error code pixel (flag disabled by default)_ - [x] Enable the `errorCodePixel` sub-feature under `androidBrowserConfig` via remote config override - [x] Navigate to an unreachable host — verify `m_errorpageshown_code` pixel fires with `error_code=ERROR_HOST_LOOKUP` - [ ] Navigate to a URL with a bad SSL handshake — verify pixel fires with `error_code=ERROR_FAILED_SSL_HANDSHAKE` - [ ] Navigate to a URL that returns an OMITTED error — verify pixel fires but Yeti error page is NOT shown - [x] Disable the flag — verify pixel does not fire _Existing error page behaviour (unchanged)_ - [x] Navigate to an unreachable host — verify Yeti error page still appears - [x] Verify `m_errorpageshown` pixel still fires for BAD_URL / CONNECTION / SSL_PROTOCOL_ERROR errors ### UI changes No UI changes. <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Touches core WebView error handling and pixel firing paths (though gated by a default-off flag), so regressions could affect error UI signaling or telemetry if miswired. > > **Overview** > Introduces a new pixel definition `m_errorpageshown_code` (and `AppPixelName.ERROR_CODE_PIXEL`) to collect the *raw* main-frame WebView error code via an `error_code` parameter. > > Plumbs the raw error code through `WebViewClientListener.onReceivedError` and updates `BrowserTabViewModel` to **keep existing error page/command behavior** (still suppressed for `OMITTED`), while optionally firing the new pixel behind the new `androidBrowserConfig.errorCodePixel` remote flag (default `false`). `PixelParamRemovalInterceptor` is updated to strip ATB from the new pixels, and tests are updated/added to cover the new instrumentation paths. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit ed35d9b. 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 Sonnet 4.6 <noreply@anthropic.com>
Task/Issue URL: https://app.asana.com/1/137249556945/project/1174433894299346/task/1213883965463238?focus=true ### Description Updated the maintenance worker workflow (android-maintenance-worker.md) to unblock Asana access from the GitHub Actions sandbox: - Allowlisted `app.asana.com` in the network config so the agent can reach the Asana API - Replaced the Asana MCP server with `mcp-scripts` that expose two read-only Asana tools (`asana_get_section_tasks`, `asana_get_task`) — lighter weight and no extra dependency - Removed the `ddg-ai-config` APM dependency that was no longer needed - Added GitHub App auth for `ddg-ai-config` APM package access - Auto-labels PRs created by the maintenance worker with `agentic-maintenance` - Added a new workflow (`action-agentic-maintenance-pr.yaml`) that moves the Asana task to "In Review" when a PR is opened or labeled with `agentic-maintenance` — this overcomes the agent's lack of Asana write permissions ### Steps to test this PR Worked in https://github.com/duckduckgo/Android/actions/runs/23839305109 ### UI changes | Before | After | | ------ | ----- | | No UI changes | No UI changes | <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Modifies GitHub Actions agent sandbox/networking and introduces new Asana-integrated automation using `ASANA_ACCESS_TOKEN`, which can affect CI behavior and external side effects if misconfigured. > > **Overview** > Unblocks the Android agentic maintenance worker from reading Asana by allowlisting `app.asana.com` and wiring in a lightweight `mcp-scripts` HTTP server that exposes two read-only Asana tools (`asana_get_section_tasks`, `asana_get_task`) through the MCP gateway. > > Maintenance PRs created by the worker are now automatically labeled `agentic-maintenance`, and a new `action-agentic-maintenance-pr` workflow reacts to that label/PR open to move the referenced Asana task to *In Review* using `duckduckgo/native-github-asana-sync`. > > Updates the compiled `android-maintenance-worker.lock.yml` accordingly (MCP scripts startup, env/secret redaction, artifact upload) and adds `microsoft/apm-action@v1.4.1` to the actions lockfile. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit cd99791. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY --> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Task/Issue URL: https://app.asana.com/1/137249556945/project/1208671677432066/task/1214087464606240?focus=true ### Description Enables Duck.ai voice mode to retain microphone access when the app is backgrounded. Previously, Android would cut off microphone access as soon as the app left the foreground, interrupting any active voice session. This change introduces a foreground service (DuckChatVoiceMicrophoneService) that is started when a voice session begins and stopped when it ends, signalling to Android that microphone use is intentional and keeping the process alive in the background. Changes - DuckChatVoiceMicrophoneService — new foreground service that displays a persistent notification while a voice session is active, satisfying Android's background microphone requirements - VoiceSessionStateManager — new component that tracks the active voice session and its associated tab. Automatically ends the session (and stops the service) when: - The Duck.ai tab is closed - The app fully exits (swipe from recents) - The app is fresh-launched after a process kill - The Duck.ai frontend sends a voiceSessionEnded message - BrowserTabViewModel — passes tabId into processJsCallbackMessage so the session manager can correctly identify which tab owns the voice session and avoid ending the session when an unrelated tab is closed - AndroidManifest — adds FOREGROUND_SERVICE, FOREGROUND_SERVICE_MICROPHONE, and RECORD_AUDIO permissions, and registers the service with foregroundServiceType="microphone" NOTE: This does not consider multiple duck.ai voice sessions. ### Steps to test this PR Prerequisites: Enable Search & Duck.Ai _Background app — microphone stays active_ - [ ] Open Duck.ai and start a voice session - [ ] Press the Home button to background the app - [ ] Expected: A persistent notification appears ("Duck.ai Voice" or similar). Voice continues picking up audio in the background. _Return from background — session intact_ - [ ] Complete step 1–2 above - [ ] Re-open the app from the Recent Apps switcher or tap the notification - [ ] Expected: The notification disappears. The voice session is still active in Duck.ai. _Swipe app away from recents — session ends_ - [ ] Open Duck.ai and start a voice session - [ ] Open the Recent Apps switcher and swipe the app away - [ ] Expected: The notification is dismissed. The foreground service stops. _Close the Duck.ai tab — session ends_ - [ ] Open Duck.ai and start a voice session - [ ] Close the Duck.ai tab (via the tab switcher) - [ ] Expected: The notification disappears immediately. The voice session ends. _Closing a different tab — session unaffected_ - [ ] Open Duck.ai and start a voice session, plus at least one other tab - [ ] Close any tab that is NOT the Duck.ai tab - [ ] Expected: The notification remains. The voice session continues. _Voice session ended by Duck.ai UI — service stops_ - [ ] Open Duck.ai and start a voice session - [ ] End the voice session within Duck.ai (e.g. tap stop/end) - [ ] Expected: The notification disappears. _Fresh app launch — no stale session_ - [ ] Force-stop the app while a voice session notification is visible - [ ] Re-launch the app - [ ] Expected: No notification appears. No stale session is active. <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Introduces a new microphone foreground service and additional permissions, which can impact privacy expectations, battery, and lifecycle correctness if mis-triggered. Risk is moderated by being scoped to voice sessions and gated behind a feature toggle. > > **Overview** > Enables Duck.ai voice chat to continue while the app is backgrounded by introducing `DuckChatVoiceMicrophoneService`, a microphone foreground service that shows a persistent notification during an active voice session. > > Voice session tracking is expanded to be *tab-aware* (`VoiceSessionStateManager.onVoiceSessionStarted(tabId)`), optionally starts/stops the foreground service behind a new `duckAiVoiceChatService` feature toggle, and automatically ends the session when the owning tab is closed or on app fresh launch/exit. `BrowserTabViewModel` now passes `tabId` through JS callbacks so the voice session can be associated with the correct tab, and the DuckChat manifest adds the required foreground-service/microphone/audio permissions plus registers the new service. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 8046599. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY -->
Task/Issue URL: https://app.asana.com/1/137249556945/task/1214188036606639 Autoconsent Release: https://github.com/duckduckgo/autoconsent/releases/tag/v14.74.0 ## Description Updates Autoconsent to version [v14.74.0](https://github.com/duckduckgo/autoconsent/releases/tag/v14.74.0). ### Autoconsent v14.74.0 release notes See release notes [here](https://github.com/duckduckgo/autoconsent/blob/v14.74.0/CHANGELOG.md) ## Steps to test This release has been tested during Autoconsent development. You can check the release notes for more information. 1. Make sure that there's no unexpected failures in CI checks 2. (optional) smoke test some of the sites mentioned in the release notes 3. If there are problems, reach out to a CPM DRI <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Updates the consent automation bundle used on real pages; upstream rule changes can alter CMP detection/opt-out behavior across sites even though the change is largely a version bump. > > **Overview** > Bumps `@duckduckgo/autoconsent` from `14.71.x/14.72.0` to `14.74.0` and refreshes `package-lock.json` accordingly. > > Regenerates the shipped `autoconsent-bundle.js` to the new upstream version, including updated CMP detection/interaction logic and minor robustness tweaks in button text handling and Sourcepoint opt-out flow. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit fe24da2. 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: muodov <2726132+muodov@users.noreply.github.com>
Task/Issue URL: https://app.asana.com/1/137249556945/project/1174433894299346/task/1214050958318612?focus=true ### Description Adds a new pixel (`m_ntp_after_idle_autofill_after_idle_return`) to measure when an external password manager fills credentials via Android system autofill after the idle-return (Hatch) feature has navigated the user away from their login page. The pixel fires only when all of the following are true: - The `showNTPAfterIdleReturn` feature flag is enabled - The idle threshold is set to "Always" (0 seconds) - The app launch option is "New Tab Page" or "Specific Page" - The idle return handler executes and navigates the user - The system autofill callback fires in the same session The pixel includes an `appLaunchOption` parameter (`new_tab_page` or `specific_page`). **Implementation:** - **ShowOnAppLaunchOptionHandler** sets a flag on `SystemAutofillEngagement` when the idle return conditions are met - **SystemAutofillEngagement** checks the flag when the autofill callback fires, sends the pixel, and clears it - **FirstScreenHandlerImpl** clears the flag on `onClose()` to prevent stale flags across sessions ### Steps to test this PR _Pixel fires when autofill occurs after idle return with "Always" threshold_ - [x] Enable the `showNTPAfterIdleReturn` feature flag - [x] Set idle threshold to "Always" (0 seconds) - [x] Set app launch option to "New Tab Page" - [x] Navigate to a login page and background the app - [x] Return to the app (idle return triggers, NTP is shown) - [x] Use an external password manager to fill credentials - [x] Verify `m_ntp_after_idle_autofill_after_idle_return` pixel fires with `appLaunchOption=new_tab_page` _Pixel does not fire when threshold is not "Always"_ - [x] Set idle threshold to 60 seconds or higher - [x] Repeat the steps above - [x] Verify the pixel does not fire _Pixel does not fire when option is "Last Opened Tab"_ - [x] Set idle threshold to "Always" - [x] Set app launch option to "Last Opened Tab" - [x] Repeat the autofill steps - [x] Verify the pixel does not fire ### UI changes No UI changes. <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Low Risk** > Adds analytics/pixel tracking with simple state gating and no user data handling beyond a small string parameter; main risk is misfiring or missed events due to lifecycle/flag clearing. > > **Overview** > Adds a new `m_ntp_after_idle_autofill_after_idle_return` pixel definition (with `appLaunchOption` param) to track system autofill usage immediately after the NTP idle-return flow navigates users away from a login page. > > Implements a session-scoped “idle return triggered” flag in `SystemAutofillEngagement`: `ShowOnAppLaunchOptionHandler` sets it only for *Always (0s)* threshold and launch options `new_tab_page`/`specific_page`, `RealSystemAutofillEngagement` fires the count pixel on the next system autofill event and clears it, and `FirstScreenHandlerImpl` clears the flag on app close; unit tests were expanded to cover firing/clearing behavior and parameter correctness. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 52fb309. 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.6 (1M context) <noreply@anthropic.com>
Task/Issue URL: https://app.asana.com/1/137249556945/project/72649045549333/task/1214087740593971?focus=true ### Description Add the following methods to the duck.ai native store Native<>JS API ``` Request getChat { "context": "contentScopeScripts", "featureName": "duckAiNativeStorage", "method": "getChat", "params": { "chatId": "chat-1" } "id": "abc123" } Response { "context": "contentScopeScripts", "featureName": "duckAiNativeStorage", "id": "abc123", "result": { "chat": { "chatId": "chat-1", "messages": [], ... } } // or "result": { "chat": null } when not found } Notification deleteFiles { "context": "contentScopeScripts", "featureName": "duckAiNativeStorage", "method": "deleteFile", "params": { "chatId": "chat-1" } } ``` ### Steps to test this PR Code review and added test cases <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Touches Duck.ai native storage persistence, DB migrations, and the JS↔native bridge contract, so regressions could affect chat history/files and telemetry naming. Changes are mostly additive and covered by new/updated tests, but require careful validation across upgrade paths and frontend expectations. > > **Overview** > Adds `getChat(chatId)` and `deleteFiles(chatId)` to the Duck.ai native storage JS bridge, and wraps native storage operations in error handling that returns safe defaults while emitting new failure/migration pixels. > > Introduces a dedicated `duckAiNativeStorage` feature toggle surfaced to the web layer via `supportsNativeStorage`, renames existing native-storage pixel names to the `m_duck-ai_native-storage_*` namespace, and expands pixel coverage for migration and CRUD exceptions. > > Improves native storage persistence by indexing file metadata by `chatId` (Room DB v3 migration) and adding DAO APIs for per-chat file cleanup; removes the `MessageBridge` gating from `RealDuckAiChatStore` so migration state is purely preference-based. Also updates the native storage debug page with collapsible sections and a one-click “nuke everything” action, plus adds/updates unit and integration tests accordingly. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 67ad6b4. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY -->
Task/Issue URL: https://app.asana.com/1/137249556945/project/1174433894299346/task/1214155101201365?focus=true ### Description When the app is opened via a Custom Tab intent, the idle-return (Hatch) logic should not redirect the user to the NTP or their configured "show on app launch" screen. This PR makes two changes: - **IntentDispatcherViewModel**: `customTabDetector.setCustomTab()` now passes the actual `customTabRequested` value instead of always passing `false`, so the detector correctly tracks whether the current session is a Custom Tab. - **FirstScreenHandlerImpl**: The idle-return path now checks `customTabDetector.isCustomTab()` alongside the existing voice session check. If the active tab is a Custom Tab, `handleAfterInactivityOption` is skipped. ### Steps to test this PR _Custom Tab does not trigger idle return_ - [x] Enable the `showNTPAfterIdleReturn` feature flag - [x] Set the idle threshold to a low value (e.g. 0 seconds / "Always") - [x] Open a Custom Tab from another app (e.g. long-press a link in a messaging app and choose DuckDuckGo) - [x] Background the app, wait for the idle threshold to elapse, then return - [x] Verify the Custom Tab content is still displayed (not replaced by NTP) _Regular tabs still trigger idle return_ - [x] Open DuckDuckGo normally (not via Custom Tab) - [x] Navigate to any website - [x] Background the app, wait for the idle threshold to elapse, then return - [x] Verify the NTP or configured launch screen is shown as expected _Voice session check still works_ - [x] Start a voice chat session on duck.ai - [x] Background the app, wait for the idle threshold to elapse, then return - [x] Verify the voice session is preserved (not replaced by NTP) ### UI changes No UI changes. <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Changes app launch/idle-return navigation behavior by suppressing redirects when a Custom Tab session is active, which could affect first-screen routing in edge cases. Scope is limited and covered by new unit tests for both custom-tab and non-custom-tab paths. > > **Overview** > Prevents *idle-return (Hatch)* from redirecting to NTP / configured first screen when the current session was launched as a **Custom Tab**. > > `IntentDispatcherViewModel` now updates `CustomTabDetector` with the computed `customTabRequested` value (instead of always `false`), and `FirstScreenHandlerImpl` gates `handleAfterInactivityOption` on `!customTabDetector.isCustomTab()` (alongside the existing voice-session check). Tests were expanded to assert detector updates and the new custom-tab idle-return suppression behavior. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit f2fa033. 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.6 (1M context) <noreply@anthropic.com>
…)" (#8339) Task/Issue URL: https://app.asana.com/1/137249556945/project/488551667048375/task/1214187547786034?focus=true ### Description - Reverts #8317 ### Steps to test this PR - [ ] Code review <!-- CURSOR_SUMMARY --> --- > [!NOTE] > <sup>[Cursor Bugbot](https://cursor.com/bugbot) is generating a summary for commit fd84aed. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY -->
Task/Issue URL: https://app.asana.com/1/137249556945/task/1213910337470393?focus=true ### Description Ensure E2E tests pass when the new Hatch is visible ### Steps to test this PR Full test suite passes in https://github.com/duckduckgo/Android/actions/runs/24806057191 <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Low Risk** > Test-only change limited to a single Maestro YAML flow; no production logic or data handling is affected. > > **Overview** > Updates the Maestro privacy E2E flow `7_-_Browser_restart_mid-session.yaml` to handle the new Hatch “Return to” UI after an app kill/relaunch by switching the conditional visibility check and tap target from text-based matching to the `newTabReturnHatchView` accessibility id. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 93f3b5d. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY -->
Task/Issue URL: https://app.asana.com/1/137249556945/project/1174433894299346/task/1214181027808336?focus=true ### Description The return-to-page Hatch card on the NTP was previously shown whenever a last-accessed tab existed and the `showNTPAfterIdleReturn` feature flag was enabled. This meant it appeared even when the user manually opened a new tab, which is incorrect — the Hatch should only appear when the NTP was shown as a result of an idle return. Changes: - Added `isAfterIdleReturn` `StateFlow<Boolean>` to the `NtpAfterIdleManager` API, replacing the internal `currentAfterIdle` `AtomicBoolean` - `NewTabReturnHatchViewModel` now `combine`s `flowLastAccessedTab` with `isAfterIdleReturn` to reactively show/hide the Hatch - Added `:new-tab-page-api` dependency to `:browser-ui` module ### Steps to test this PR _Hatch appears after idle return_ - [x] Enable the `showNTPAfterIdleReturn` feature flag - [x] Set idle threshold to "Always" (0 seconds) - [x] Navigate to any website - [x] Background the app, then return - [x] Verify the NTP is shown with the Hatch card (showing the previous page) - [x] Tap the Hatch card and verify it returns to the previous page _Hatch does not appear on user-initiated NTP_ - [x] With the feature flag enabled, open a new tab manually (tap the + button or similar) - [x] Verify the Hatch card is NOT shown on the new tab page _NTP shown pixels fire correctly after idle return_ - [x] Enable the feature flag and set idle threshold to "Always" - [x] Navigate to a website, background the app, then return - [x] Verify `m_ntp_after_idle_ntp_shown_after_idle` pixel fires - [x] Open a new tab manually from the menu - [x] Verify `m_ntp_after_idle_ntp_shown_user_initiated` pixel fires (not the after_idle variant) _Return-to-page and search pixels classify correctly_ - [x] Trigger an idle return (NTP shown after idle) - [x] Tap the Hatch card to return to the previous page - [x] Verify `m_ntp_after_idle_return_to_page_tapped_after_idle` pixel fires - [x] Trigger another idle return, then use the search bar on the NTP - [x] Verify `m_ntp_after_idle_bar_used_from_ntp_after_idle` pixel fires - [x] Open a new tab manually, use the search bar - [x] Verify `m_ntp_after_idle_bar_used_from_ntp_user_initiated` pixel fires ### UI changes No UI changes — the Hatch card appearance is unchanged, only when it appears has changed. <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Changes idle-return classification and lifecycle ordering for NTP state/pixeling, which can affect when the hatch appears and how metrics are recorded across app recreation/backgrounding. > > **Overview** > Ensures the NTP “return-to-page” hatch only appears when the NTP was shown due to an *idle return*, not when the user manually opens a new tab. > > This introduces `NtpAfterIdleManager.isAfterIdleReturn` (a `StateFlow`) and updates `NtpAfterIdleManagerImpl` to track/clear after-idle state safely across `onOpen`/`onClose` ordering, with tests updated accordingly. > > Idle-return triggering is tightened: `FirstScreenHandlerImpl` can now synchronously call `onIdleReturnTriggered()` when reopening directly onto an NTP, `ShowOnAppLaunchOptionHandler` always marks after-idle when the inactivity option resolves to NTP even if no new tab is added, and `BrowserTabViewModel` avoids firing NTP-search-submitted pixels during restore flows by requiring `url == null`. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 55a2251. 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.6 (1M context) <noreply@anthropic.com>
Task/Issue URL: https://app.asana.com/1/137249556945/project/488551667048375/task/1214187547786023?focus=true ### Description - The menu icon was recently updated but the mock omnibar/layout XML still use the old icon, which means it shows briefly on startup. **Solution** - When we `bindMockupToolbars` then the icon should be set based on the menu feature flag. - The initial `BrowserMenuDisplayState` should be the actual state based on the feature flag instead of false. ### Steps to test this PR - [ ] Kill the app and reopen - [ ] Verify that the correct menu icon is shown on startup ### UI changes | Before | After | | ------ | ----- | <img width="1080" height="2340" alt="Screenshot_20260422_215028" src="https://github.com/user-attachments/assets/942940c0-289c-46e4-a010-c2023a88dc94" />|<img width="1080" height="2340" alt="Screenshot_20260422_214713" src="https://github.com/user-attachments/assets/1d68ce3f-8862-4641-9bb2-c393e73581f9" /> <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Low Risk** > Low risk UI-state change: adds a synchronous check for the bottom-sheet menu flag to avoid incorrect initial state before flows emit, with unit tests covering the new logic. > > **Overview** > Ensures the browser menu icon is correct immediately on startup by introducing `BrowserMenuDisplayRepository.isBottomSheetMenuEnabled()` and using it to set initial `useBottomSheetMenu`/menu icon state before reactive flows emit. > > `BrowserActivity` now picks the mock toolbar menu icon (hamburger vs vertical) based on this synchronous check, and `BrowserTabViewModel` initializes `browserMenuState`/`BrowserViewState` with the same derived value. Adds unit tests for the new enablement computation across rollout/experimental flags and user preference. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 6152118. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY -->
Task/Issue URL: https://app.asana.com/1/137249556945/project/1200204095367872/task/1214039037878462?focus=true ### Description - Updates the model picker UI and adds icons for each model ### Steps to test this PR _With the native input enabled_ - [x] Tap the model picker - [x] Verify that the correct icons are shown next to each model ### UI changes | Before | After | | ------ | ----- | <img width="1080" height="2340" alt="Screenshot_20260423_212611" src="https://github.com/user-attachments/assets/91bc014b-0005-482a-9d18-050ff4eea0e6" />|<img width="1080" height="2340" alt="Screenshot_20260423_212512" src="https://github.com/user-attachments/assets/c76501db-d6fa-44f4-a8ed-12c1097f5295" /> <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Low Risk** > Low risk UI/data-model enhancement: adds a new `provider` field and derived enum used only for display, with tests covering provider resolution and icon mapping. > > **Overview** > Adds model *provider* metadata end-to-end: `RemoteAIChatModel` now parses a `provider` field, `AIChatModel` carries a derived `ModelProvider`, and `RealDuckAiModelManager` resolves it via `ModelProvider.from(...)`. > > Updates the model picker UI to show a provider icon next to each model and tweaks layout (single-line/ellipsis label, trailing checkmark margin). For free users, removes the separate premium section header and deletes the unused `duckAiModelPickerPremiumModels` string. > > Includes new vector drawables for provider icons (OpenAI, Claude/Anthropic, Mistral, Llama/Meta, OSS) plus new/updated unit tests verifying provider resolution and icon selection. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit ef66aac. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY -->
Task/Issue URL: https://app.asana.com/1/137249556945/project/1200204095367872/task/1213433888294717?focus=true ### Description - Updates the input Ui when Duck.ai is disabled ### Steps to test this PR _With native input enabled_ - [ ] Disable Duck.ai - [ ] Focus the input - [ ] Verify that the input is the correct size <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Touches native input sizing/animation and runtime margin adjustments, which can cause visual regressions across device sizes and top/bottom widget configurations. > > **Overview** > Improves native input UI consistency when Duck.ai is disabled by **matching the widget card’s shape/margins to the omnibar** and adding an explicit trailing margin for the widget layout. > > Adjusts the enter animation sizing to compute the expanded width *excluding* card side margins, and updates the Duck.ai-disabled widget to **collapse extra spacers/top margin and enforce omnibar-like minimum height** when the toggle is hidden. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit ec2c36a. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY -->
Task/Issue URL: https://app.asana.com/1/137249556945/project/1200204095367872/task/1213821485629415?focus=true ### Description - Hides favorites when toggling to search on a Duck.ai tab ### Steps to test this PR _With native input enabled_ - [x] Add a favorite - [x] Go to Duck.ai - [x] Toggle to search - [x] Verify that the favorite does not show - [x] Type something - [x] Clear it - [x] Verify that the favorite does not show <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Low Risk** > Low risk: small, localized callback changes in `NativeInputManager` affecting autocomplete clearing behavior, with minimal impact outside Duck.ai mode. > > **Overview** > Adjusts native input callback handling so that in **Duck.ai mode** clearing the search field triggers `onClearAutocomplete` (instead of propagating an empty `onSearchTextChanged`). > > Also updates search-tab selection handling to clear autocomplete and **short-circuit further selection behavior** when the text is blank in Duck.ai mode. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 4a14205. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY -->
Task/Issue URL: https://app.asana.com/1/137249556945/project/1200204095367872/task/1213890328647790?focus=true ### Description - Hides the toggle when AI Features are set to “Search Only" ### Steps to test this PR _With native input enabled_ - [ ] Go to AI Features - [ ] Set to "Search Only" - [ ] Verify that the toggle is hidden <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Low Risk** > Low risk UI/state wiring change limited to toggle visibility and initial tab selection based on an existing user setting. > > **Overview** > **Native input now reacts to the “Input Screen” (AI features) user setting to hide the Search/Chat toggle in “Search Only” mode.** `NativeInputModeWidget` observes the setting, centralizes toggle visibility logic (`applyToggleVisibility`/`shouldShowToggle`), and gates `setToggleVisible` so the toggle can’t reappear during keyboard transitions when disabled. > > In Duck AI mode, `RealNativeInputManager` also listens for that setting and re-selects the Chat tab (and on initial show) to keep the UI in the expected state when the toggle is suppressed. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 60d6ccc. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY -->
Task/Issue URL: https://app.asana.com/1/137249556945/project/1200204095367872/task/1214271710998174?focus=true ### Description - Fixes a regression where the native input is the wrong height ### Steps to test this PR _With native input enabled_ - [ ] Focus the native input - [ ] Verify that the input is the correct height - [ ] In AI Features select “Search Only" - [ ] Verify that the input resizes correctly <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Low Risk** > Low risk UI/layout-only changes that adjust native input widget sizing/shape based on DuckChat and input-screen user settings; main risk is minor visual regressions across mode/layout combinations. > > **Overview** > Fixes native input sizing/shape inconsistencies when the input-screen toggle is disabled. > > `RealNativeInputManager` now tracks `observeInputScreenUserSettingEnabled()` and uses it (via `shouldMatchOmnibarShape`) to decide when to match the omnibar card shape. `NativeInputModeWidget` now explicitly applies `matchOmnibarHeight()` whenever the toggle is hidden (DuckChat disabled or user setting off), ensuring the widget height stays aligned with the omnibar. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 0037e94. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY -->
Task/Issue URL: https://app.asana.com/1/137249556945/project/1212227266948491/task/1214039006242282?focus=true ### Description Add autofillService to pixel params in SecureStorageKeyStore ### Steps to test this PR _Feature 1_ - [ ] - [ ] ### UI changes | Before | After | | ------ | ----- | !(Upload before screenshot)|(Upload after screenshot)| <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Low Risk** > Low risk: changes only analytics/pixel parameter wiring and definitions, without altering secure storage read/write behavior. > > **Overview** > Adds a new `autofillService` parameter to all relevant Autofill secure-storage pixel definitions in `autofill.json5` to capture whether multi-process (autofill service) mode is enabled. > > Updates `RealSecureStorageKeyStore` to pass a single `HarmonyFlags` snapshot into `getPixelParams` and includes `harmonyFlags.multiProcess` as `autofillService` on all pixel fires, replacing the previous `useHarmony`/`readFromHarmony` argument threading. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit cd0feb3. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY -->
Task/Issue URL: https://app.asana.com/1/137249556945/project/1212810093780571/task/1213381906205784?focus=true ### Description - Remove the aiChatSuggestions feature flag and treat the AI chat suggestions feature (pinned and recent chats) as always enabled. - Remove DuckChat.isChatSuggestionsFeatureAvailable() API and inline the always-true branches at every call site (InputScreenFragment, ChatTabFragment, InputScreenViewModel, NativeInputManager, GeneralSettingsViewModel). - Remove dead helpers (hideAutoCompleteIfOnChatTab, enableViewPagerInputIfNoFavorites) and feature flag related tests. ### Steps to test this PR - Open the input screen, type in chat mode, confirm pinned/recent chats and chat URL suggestions still render - General Settings shows the chat suggestions toggle when Duck.ai + input screen are enabled. Setting should work as expected when turned on/off. - Switching between Search and Chat tabs hides/shows overlays as expected - Enable native input and confirm pinned/recent chat suggestions render under the input <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Removes a remote-config gate around chat suggestions, so pinned/recent chat suggestions will now activate wherever the input screen/native input supports them; this is user-visible and could affect overlay/focus behavior and suggestion fetching frequency. > > **Overview** > Chat suggestions (pinned/recent chats and related overlays) are now treated as *always available* by removing the `aiChatSuggestions` feature flag and the `DuckChat.isChatSuggestionsFeatureAvailable()` API. > > Call sites in the input screen and native input (`InputScreenFragment`, `ChatTabFragment`, `InputScreenViewModel`, `NativeInputManager`, `GeneralSettingsViewModel`) were simplified to always run the chat-suggestions code paths, and related dead helper methods/branches were deleted. > > Tests were updated accordingly by removing feature-flag expectations and dropping coverage for the “feature not available” scenario. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 5a3e99b. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY -->
Task/Issue URL: https://app.asana.com/1/137249556945/project/1207418217763355/task/1214579513091724?focus=true ### Description This PR adds the new `BrowserMode` and the related interfaces and their concrete implementations: - `BrowserModeStateHolder` that exposes enum class `BrowserMode { REGULAR, FIRE }`, which is the single source of truth for current mode. Theming overlay, tab switcher VM, mode-toggle pill, sync entry points, repository providers — all observe this Flow. - Adds `FireModeAvailability` that checks the feature flag and the WebView feature support - Adds `BrowserMode` plumbing to the main entry points Related API proposal: https://app.asana.com/1/137249556945/project/1207418217763355/task/1214579944546013?focus=true ### Steps to test this PR Smoke testing that the app is launched correctly and opening an external link works as expected is sufficient. <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Touches core `BrowserActivity` intent processing and tab creation, adding mode-based recreation and deferred-intent handling which could affect external link/widget/shortcut launches. New mode plumbing is guarded by availability checks but has broad surface area across entry points. > > **Overview** > Introduces a new cross-module `BrowserMode` concept (`REGULAR`/`FIRE`) with a `BrowserModeStateHolder` source-of-truth and an app implementation (`RealBrowserModeStateHolder`). > > Updates `BrowserActivity` and tab creation to be mode-aware (adapter/`BrowserTabFragment` arguments, DuckChat fragment args), and recreates the activity on mode changes; external entry-point intents can now be stamped as `LAUNCH_REQUIRES_REGULAR_MODE` and, if received while in `FIRE`, are deferred across a FIRE→REGULAR switch before processing. > > Adds a new `fire-mode-api`/`fire-mode-impl` with `FireModeAvailability` (feature flag + WebView MultiProfile capability), wires it into `BrowserViewModel.switchToMode`, and updates numerous launch points (widgets, shortcuts, onboarding/settings/internal screens, tests) to pass a `BrowserLaunchSource` and correctly stamp intents. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 2f0669e. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY -->
Task/Issue URL: https://app.asana.com/1/137249556945/project/488551667048375/task/1214799610249611?focus=true ### Description Updates Metro to 1.0.1 ### Steps to test this PR - [ ] Verify CI checks pass - [ ] (optional) Run locally with `./gradlew clean installID -Pddg.di=Metro --no-configuration-cache` ### UI changes No UI changes <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Low Risk** > Low risk version bump limited to dependency coordinates in `versions.properties`, with behavior changes (if any) coming only from Metro itself. > > **Overview** > Updates Metro dependency versions in `versions.properties`, switching both `dev.zacsweers.metro` `gradle-plugin` and `runtime` from a `1.1.0-SNAPSHOT` to the released `1.0.1`. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit c2f02be. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY -->
Task/Issue URL: https://app.asana.com/1/137249556945/task/1214749839646931?focus=true ### Description Makes internal releases use Metro as the default DI framework. ### Steps to test this PR Will be tested when we trigger the internal release workflow ### UI changes No UI changes <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Low Risk** > Low risk workflow-only change; main risk is misconfiguration causing internal release builds to fail if the `-Pddg.di` flag or input value is incorrect. > > **Overview** > Internal release uploads now accept a `ddg_di` input (for `workflow_call` and `workflow_dispatch`) and default it to **Metro**. > > The workflow passes the selected DI framework through `DDG_DI` into Gradle via `-Pddg.di=...` for both `bundleInternalRelease` and `getBuildVersionName`, ensuring internal builds/versioning align with the chosen DI implementation. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit ad75d18. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY -->
Task/Issue URL: https://app.asana.com/1/137249556945/project/1200204095367872/task/1214803420088478?focus=true ### Description - Places the top left hand corner of the reasoning dialog to the button position (to match the options dialog) ### Steps to test this PR _With native input enabled_ - [ ] Tap the reasoning button - [ ] Verify that the dialog is properly positioned ### UI changes | Before | After | | ------ | ----- | <img width="1080" height="594" alt="Screenshot_20260515_110826" src="https://github.com/user-attachments/assets/473e36f1-6834-49e2-8846-8dd3d83ee465" />|<img width="1080" height="670" alt="Screenshot_20260515_133852" src="https://github.com/user-attachments/assets/812e8311-6a58-47e9-b6e2-097e0e2c4bea" /> <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Low Risk** > Low risk UI-only change that adjusts `PopupWindow` positioning; potential issues limited to layout/coordinates across screen sizes and RTL. > > **Overview** > Updates the reasoning mode picker popup positioning to align the menu with the triggering button, matching the behavior of the options dialog. > > Replaces the previous bottom/end offset calculation with a screen-coordinate based placement using `button.getLocationOnScreen` and the configured `reasoningModePickerMenuWidth` to right-align the popup to the button. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit f24cd89. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY -->
Task/Issue URL: https://app.asana.com/1/137249556945/project/1207418217763355/task/1214778997953222?focus=true ### Description This PR replaces the `fire-mode-*` modules with `browser-mode-*` modules so that the everything related to the Browser modes can be moved there. The `browser-mode-api` is added to the exception list for api->api dependence, which has already been been approved in the API proposal/tech design. ### Steps to test this PR QA-optional <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Mostly a module/package rename, but it touches Gradle dependency wiring and DI bindings across the app; build/merge issues or missing imports are the main risk rather than behavioral changes. > > **Overview** > Renames the `fire-mode-*` modules to `browser-mode-*` and moves the public APIs (`BrowserMode`, `BrowserModeStateHolder`, `FireModeAvailability`, etc.) under the new `com.duckduckgo.browsermode.api` namespace. > > Updates app and feature modules (e.g., `BrowserActivity`, `BrowserViewModel`, DuckChat) to depend on and import from `browser-mode-*`, and adjusts Gradle rules to allow `:browser-mode-api` as an approved `api`→`api` dependency. The `browser-mode-impl` build config is updated with additional AndroidX dependencies and unit-test (Robolectric/resource) support. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 19ab411. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY -->
Task/Issue URL: https://app.asana.com/1/137249556945/project/1212608036467427/task/1214598987399330?focus=true, https://app.asana.com/1/137249556945/project/1212608036467427/task/1214707942438120?focus=true ### Description Widget and duck.ai webview now don't have an extra overlap. ### Steps to test this PR - open duck.ai in native-input widget mode. - observe terms and conditions are shown without a cutoff. - observe a long conversation not cutoff at the end. ### UI changes <img width="400" height="900" alt="test" src="https://github.com/user-attachments/assets/1879ff7f-1868-4612-8438-a400045bab72" /> <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Low Risk** > Low risk UI-only change that adjusts padding/offset calculations; main risk is minor layout regressions on bottom-positioned widget modes across devices. > > **Overview** > Fixes an incorrect extra overlap beneath the native-input DuckAI widget by removing the hardcoded `overlap` dimension and simplifying `computeDeltaBottom()` to use the current `widgetView.height` when the widget is bottom-aligned. > > This changes how bottom padding is applied to `browserLayout`/NTP content so web content (e.g., DuckAI WebView terms and long conversations) is no longer cut off under the widget. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit f175d6c. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY -->
Task/Issue URL: https://app.asana.com/1/137249556945/project/1200204095367872/task/1214832730289236?focus=true ### Description - Fixes a crash when the native input is showing and you change to dark/light theme. ### Steps to test this PR _With native input enabled_ - [ ] Tap on the omnibar to show the native input - [ ] Change the system theme - [ ] Verify that the app does not crash <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Low Risk** > Low risk lifecycle change that delays applying the remembered toggle state until `doOnAttach`, reducing chances of accessing uninitialized/inactive view/lifecycle state during configuration changes (e.g., theme switch). > > **Overview** > Defers `applyDefaultTogglePosition` execution by wrapping the remembered-toggle lookup and tab selection in `doOnAttach`, so the coroutine only runs once the widget is attached to a window. > > This avoids applying the default/last-used toggle state during transient view recreation (e.g., light/dark theme changes), preventing crashes from accessing lifecycle/view model dependencies too early. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit b06dad7. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY -->
Task/Issue URL: https://app.asana.com/1/137249556945/project/1200581511062568/task/1213808460359637?focus=true ### Description Pre-compiles the regex patterns for TDS tracker rules once at TdsClient construction time, instead of recompiling them on every URL match. On a typical browse session this is a large amount of redundant Pattern.compile() work — every request that hits a tracker domain currently recompiles every rule's regex until one matches (or all are exhausted). The new path: - TdsClient builds CompiledTracker / CompiledRule wrappers up front, compiling each rule's regex once. - Invalid regex patterns are caught at construction (logged and skipped) rather than throwing per-call from inside matchesTrackerEntry. - Gated by a new precompileTdsRegex sub-feature on AndroidBrowserConfigFeature (default INTERNAL), exposed via PrecompileTdsRegexRCWrapper and read by TrackerDataLoader when constructing the client. - Also gated on optimizeTrackerEvaluationV3 — precompileRegex only takes effect when V3 is on, since they share the same fast-path code. With V3 off, the legacy per-call compile path runs unchanged. Existing tracker tests (TdsClientTest, reference tests) were extended to run both precompile=false and precompile=true so all behaviour is covered. A new on-device microbenchmark, TdsClientPerfAndroidTest, mirrors the JVM TdsClientPerfTest and is annotated @ignore (manual run only) so CI doesn't execute it — kdoc on the class explains how to run it and capture logcat. ### Steps to test this PR 1/ `precompileRegex` flag ON (internal build, default INTERNAL) - install from this branch (the flag is ON for internal) - browse a few tracker-heavy sites (e.g. news / shopping pages). - confirm tracker blocking and the privacy dashboard counts match what you see in Prod. - confirm no crashes on launch: the precompile step now runs eagerly when the TDS client is built during TrackerDataLoader. 2/ `precompileRegex` flag OFF - install from this branch and ensure you have the `precompileRegex`flag OFF - browse a few tracker-heavy sites (e.g. news / shopping pages). - confirm tracker blocking and the privacy dashboard counts match what you see in Prod. ### NO UI changes <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Changes core tracker-matching behavior in `TdsClient` and adds a new remote-config-controlled optimization path; while gated and covered by tests, mistakes could impact tracker blocking accuracy or startup cost due to eager regex compilation. > > **Overview** > Introduces an optional **regex precompilation** path in `TdsClient` that compiles each tracker rule’s regex once during client construction and reuses it during `matches`, skipping (and logging) rules that fail to compile. > > Wires this behavior behind a new `androidBrowserConfig.precompileTdsRegex` toggle via `PrecompileTdsRegexRCWrapper`, and passes the flag from `TrackerDataLoader` when building the TDS client (*effective only when `optimizeTrackerEvaluationV3` is enabled*). > > Updates unit/reference tests to exercise both `precompileRegex` modes and adds an `@Ignore`d on-device microbenchmark (`TdsClientPerfAndroidTest`) to compare construction and match throughput. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 48da3db. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY -->
Task/Issue URL: https://app.asana.com/1/137249556945/project/1174433894299346/task/1214788735007211?focus=true Stacked on top of #8549. ### Description PR 2 of the native input state refactor — see the parent task for the full plan. The native input widget is now the single publisher of `NativeInputState` and reads its own state from the per-tab store (`NativeInputStateProvider`) instead of keeping it as a widget-local field. Plugins still won't see this change until PR 3. Concretely: - `NativeInputModeWidgetViewModel` injects `NativeInputStatePublisher`. A new `init { … }` block collects `combine(state, activeTabId)` and pushes each value through `publisher.publish(tabId, state.copy(tabId = tabId))` whenever a tabId is set. - `NativeInputModeWidget` injects `NativeInputStateProvider`. `observeNativeInputState()` now subscribes to `provider.stateForTab(tabId)` instead of `viewModel.state`. `getInputState()` reads `provider.stateForTab(activeTabId).value`. - `configure()` and `configureContextual()` take a `tabId: String` parameter and start the observation once the tabId is known. - `tabId` is threaded: - `BrowserTabFragment.tabId` → `NativeInputManager.showNativeInput(tabId, …)` → `attachWidget(widgetView, isBottom, tabId)` → `widget.configure(tabId, …)`. - `DuckChatContextualFragment` reads its `KEY_DUCK_AI_CONTEXTUAL_TAB_ID` argument once and passes it to `ContextualNativeInputManager.init(tabId, …)` → `widget.configureContextual(tabId)`. - `viewModel.state` stays as a `SharedFlow<NativeInputState>` so existing ViewModel-level tests can keep asserting on its values; the widget no longer consumes it. What this PR intentionally does **not** do: - `NativeInputState.tabId` stays nullable. Tightening to non-null will follow once the publish-on-configure invariant is verified in practice (and we can drop the `zero("__init__")`-style placeholder). - `NativeInputHost.getInputState()` is still exposed — its implementation now reads from the provider, but the surface remains until plugins are migrated in PR 3 and it's removed in PR 4. - Plugins still receive the widget as their `NativeInputHost`; nothing about their behaviour changes here. ### Steps to test this PR - [x] CI green: `:duckchat-impl:testDebugUnitTest`, `:app:testPlayDebugUnitTest`, `spotlessCheck`. - [x] Existing `NativeInputModeWidgetViewModelTest` cases still pass after the `configure`/`configureContextual` signature changes and the added publisher mock. - [x] Smoke-test on device: - Open Duck.ai input from the NTP, type, switch to the chat tab, submit — behaviour matches develop. - Open the omnibar on a browser tab, switch between Search and Duck.ai modes — toggle/position/context behave as before. - Open Duck.ai contextual chat from a browser tab — keyboard, attachments, and submit all still work. - Background a tab and reopen it — input state for that tab is restored correctly. ### UI changes None. <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Changes how the native input widget publishes/consumes state by keying everything on `tabId`, which could cause cross-tab state leaks or missing updates if tab IDs aren’t consistently threaded through all entry points. > > **Overview** > The native input widget now **publishes `NativeInputState` per tab** and **reads state from `NativeInputStateProvider`** instead of relying on widget-local/ViewModel flow state. > > This threads `tabId` through `BrowserTabFragment` → `NativeInputManager.showNativeInput`/`attachWidget` → `NativeInputModeWidget.configure`, and through contextual chat via `DuckChatContextualFragment`/`ContextualNativeInputManager.init` → `configureContextual`. The ViewModel now injects `NativeInputStatePublisher` to push updates for the active tab, and tests are updated for the new `tabId`-required `configure` APIs. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 9dc03e8. 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>
Task/Issue URL: https://app.asana.com/1/137249556945/project/1200581511062568/task/1213808460359640?focus=true ### Description Adds an opt-in optimized read path for RealTrackerAllowlist, gated behind a new optimizeTrackerAllowList.precompileRegexAndCacheDomains remote feature (default INTERNAL). When the toggle is off, behavior is unchanged: linear filter over exceptions + regex compiled on every isAnException call. When the toggle is on: - TrackerAllowlistRepository now exposes a rulesByDomain: Map\<String, List\<CompiledRule>> view, built once per privacy-config refresh. Regexes are precompiled at build time; rules whose regex fails to compile are kept with regex = null and skipped at match time. Keys are www-stripped so entries stored as [www.foo.com](http://www.foo.com) are reachable from UriString.host() lookups. - The repository swaps its CopyOnWriteArrayList for an immutable Snapshot. - RealTrackerAllowlist.isAnException does an O(1) host lookup with a subdomain fallback walk ([a.b.tracker.com](http://a.b.tracker.com) → [b.tracker.com](http://b.tracker.com) → [tracker.com](http://tracker.com)) instead of scanning the full list, and reuses precompiled regexes. Other changes: - OptimizeTrackerAllowListRCWrapper wraps the toggle check behind by lazy so the feature flag is read at most once per app process. - RealTrackerAllowlist constructor gains the wrapper as a third parameter — all callers/tests updated. - New on-device benchmark RealTrackerAllowlistPerfAndroidTest (@ignored in CI) compares the two paths against the bundled R.raw.privacy_config allowlist on a real device. Run manually per the class kdoc. ### Steps to test this PR Smoke test with flag ON / OFF. ### NO UI changes <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Adds a new, toggle-gated matching path for tracker allowlisting and changes the repository API/storage shape, which could affect tracking exceptions behavior when enabled or during config refreshes. Default gating reduces blast radius, but correctness/performance depends on the new domain-indexing and regex compilation logic. > > **Overview** > Introduces an **opt-in optimized read path** for `RealTrackerAllowlist`, gated by the new remote feature `optimizeTrackerAllowList.precompileRegexAndCacheDomains` (via `OptimizeTrackerAllowListRCWrapper`). When enabled, allowlist lookups use a per-domain `rulesByDomain` index with **precompiled regex** and subdomain fallback instead of scanning all exceptions and compiling regex on each call. > > Updates `TrackerAllowlistRepository` to expose `rulesByDomain` and replaces the in-memory `CopyOnWriteArrayList` with an immutable, volatile `Snapshot` built on each refresh; invalid regex rules are retained with `regex=null` and skipped. Call sites and reference/unit tests are updated for the new constructor/dependencies, and an `@Ignore`d on-device microbenchmark (`RealTrackerAllowlistPerfAndroidTest`) is added to compare the two paths. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 55df6cf. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY -->
Task/Issue URL: https://app.asana.com/1/137249556945/project/1210594645151737/task/1211659112661276?focus=true ### Description Adds `DaxRadioButton` to the Compose design system ### Steps to test this PR _ADS Preview_ - [x] Open ADS Preview screen - [x] Go to Interactive tab - [x] Compare XML and Compose versions ### UI changes See ADS Preview screen <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Introduces a new shared Compose component plus a new **error-level** lint rule that will fail builds where `androidx.compose.material3.RadioButton` is used outside the design-system modules. > > **Overview** > Adds a new Compose design-system component, `DaxRadioButton`, which wraps Material3 `RadioButton` with DuckDuckGo theme colors. > > Updates `DaxRadioOptions` and the ADS preview (`ComponentViewHolder`/`component_radio_button.xml`) to use and showcase the new Compose radio button alongside the existing XML version. > > Adds `NoMaterial3RadioButtonUsageDetector` (registered in `DuckDuckGoIssueRegistry` with tests) to enforce `DaxRadioButton` usage by flagging Material3 `RadioButton` calls outside `design-system`/`design-system-internal`, and tweaks the Maestro ADS preview flow to scroll to the next target element. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 7b77faa. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY -->
Task/Issue URL: https://app.asana.com/1/137249556945/task/1214808964012146?focus=true ### Description Adds `Subscriptions.getEntitlements(): Flow<Set<Entitlement>>` to the public API so consumers can read the live entitlement set (each carrying both `name` and `product`). Changes: - New `getEntitlements()` method on the `Subscriptions` interface, backed by a new `entitlementSet: Flow<Set<Entitlement>>` on `SubscriptionsManager`. Emits alongside the existing `entitlements: Flow<List<Product>>` from the same trigger points (`emitEntitlementsValues()`). - `Entitlement` data class moved from `subscriptions-impl/model/` to `subscriptions-api/model/` so it's part of the public surface. All impl-side imports repointed. - `getEntitlementStatus()` annotated `@Deprecated`. - Unit tests added in `RealSubscriptionsManagerTest` covering the active-subscription and inactive-subscription emissions. Design and API proposal: - [Tech Design](https://app.asana.com/1/137249556945/project/481882893211075/task/1214784714729690) - [API Proposal](https://app.asana.com/1/137249556945/project/1202552961248957/task/1214804968952054?focus=true) ### Steps to test this PR - [ ] Smoke: Sign in with a Plus or Pro account, confirm subscription status and features renders as before (existing `getEntitlementStatus()` callers unchanged). - [ ] Sign out, confirm no regressions on the subscription settings screen. ### UI changes No UI changes - public API only. <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Medium risk because it expands the public `Subscriptions` API (interface method addition + deprecation) and changes entitlement emission plumbing, which could affect downstream implementers/consumers and flow behavior if mismatched. > > **Overview** > Adds a new public `Subscriptions.getEntitlements(): Flow<Set<Entitlement>>` API that exposes the live *raw* entitlement set (including both `name` and `product`) and deprecates `getEntitlementStatus()`. > > Moves the `Entitlement` model into `subscriptions-api` and wires a new `SubscriptionsManager.entitlementSet` flow through `RealSubscriptions`/`SubscriptionsDummy`, emitting alongside the existing `entitlements` list and resetting to empty on sign-out; updates imports across impl/services/auth and adds unit tests covering entitlement-set emission scenarios. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit ab125ce. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY -->
Task/Issue URL: https://app.asana.com/1/137249556945/task/1214837464091621 ### Description Merges `ClearableData.DuckChats.Single(chatUrl: String)` into a new `Selected(chatUrls: Set<String>)` variant in `:data-clearing-api`. ### Steps to test this PR _Existing in-chat fire flow is unchanged_ - [ ] Open a Duck.ai chat tab, send a message so it has a `chatID=` URL. - [ ] Tap the fire button → confirm. The tab should be replaced with a fresh new-chat URL, and the deleted chat should no longer appear in chat history. _Contextual chat clear is unchanged_ - [ ] Open an input-screen contextual chat tied to a tab, then trigger a single-tab fire on that tab. Both the tab's Duck.ai URL chat and the contextual chat should be deleted. ### UI changes n/a
Task/Issue URL: https://app.asana.com/1/137249556945/project/1214157224317277/task/1214802223978853?focus=true Stacked on #8550. Until that merges, the diff here shows PR 2's commits too; review the top commit (`7d57719`) for the PR 2.5 change in isolation. ### Description Small follow-up to PR 2. Now that the widget publishes on configure, every `NativeInputState` in the per-tab store is provably associated with a `tabId`, so we tighten the type. - Drop the `String? = null` default on `NativeInputState.tabId` and `NativeInputState.zero()`. - ViewModel `state` flow combines `activeTabId.filterNotNull()` so the emitted state carries the active tabId. The publish init block simplifies to a direct collect-and-publish — the separate combine that re-attached the tabId is gone. - Widget local cache becomes nullable (`NativeInputState?`). Read sites use null-safe access with the same defaults the previous field initializer carried. - `getInputState()` throws if called pre-configure instead of returning a stale placeholder; the publish-on-configure invariant ensures plugins only reach it after configure. - Tests: `@Before` calls `configure()` so the state flow has a tabId; the back-buttons test passes an explicit tabId. ### Steps to test this PR _Native input widget regressions_ - [x] Open the app on a NTP, type a search query, submit — search works as before - [x] Open a website, focus the omnibar, switch between Search and Duck.ai tabs — toggle works, no flicker - [x] Open a Duck.ai contextual session from a long-press menu — contextual UI renders correctly - [x] Toggle "Address bar position" between top and bottom in Settings — widget reshapes correctly on next omnibar focus - [x] Delete a tab, then the fire button — no leaked state, next tab renders fresh ### UI changes No UI changes — refactor only. <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Tightens a core UI/state model contract and changes initialization/placeholder behavior; mis-ordered widget configuration could now throw or suppress UI updates until `tabId` is set. > > **Overview** > Makes `NativeInputState.tabId` mandatory by removing the nullable/default value and updating `zero(tabId)` accordingly. > > Updates `NativeInputModeWidgetViewModel` to only emit `state` once `activeTabId` is non-null (via `activeTabId.filterNotNull()`), embedding the `tabId` in every emitted `NativeInputState` and simplifying publishing to a direct collect-and-publish. > > Adjusts `NativeInputModeWidget` to treat its cached `NativeInputState` as nullable (null-safe reads with safe defaults) and makes `getInputState()` fail fast if called before `configure()`. Tests are updated to pass explicit `tabId`/call `configure()` up front. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 96d5ee1. 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>
Task/Issue URL: https://app.asana.com/1/137249556945/project/1214157224317277/task/1214577731172288?focus=true ### Description Fixes the Duck.ai model picker for Pro subscribers, who were incorrectly seeing only Plus/Free models. Migrates `DuckAiModelManager` from the old `getEntitlementStatus()` + `getAvailableProducts()` combination to the new `Subscriptions.getEntitlements()` flow added in the previous PR. The new API exposes the per-entitlement `name` (`"plus"` / `"pro"`), which is what distinguishes the two paid tiers. Changes: - `DuckAiModelManager.init` now collects `subscriptions.getEntitlements()` for the "entitlements changed → refresh models" trigger (same semantics as before, just the new flow). - `resolveUserTier()` reads the current entitlement snapshot via `.first()`, finds the entitlement whose `product == Product.DuckAiPlus.value`, and resolves its `name` to a `UserTier` via `UserTier.from(raw)`. - Tests updated Depends on the previous PR being merged: #8566 ### Steps to test this PR Pro subscriber path (the bug being fixed) - [x] Sign into a Pro account that has Duck.ai Pro entitlement. - [x] Open the Duck.ai model picker. - [x] Confirm Pro-tier models are listed and selectable (previously they were not). - [x] Confirm a Plus-tier model is still selectable (Plus access should not regress). - [x] Select Pro model (Opus 4.6 and start chat. Plus and Free subscriber paths (regression checks)_ - [x] Sign in with a Plus account, confirm Plus models are shown and Pro-only models are gated. - [x] Sign out, confirm the picker shows only Free-tier models. Reactivity - [x] While Duck.ai is open, sign out from settings and return to Duck.ai. - [x] Start a new chat. - [x] The picker should refresh to free-tier-only without restarting the app. ### UI changes | Before | After | | ------ | ----- | |<img width="1080" height="2410" alt="rn_image_picker_lib_temp_e529b4d9-6b7d-4970-8d21-49786c0a958a" src="https://github.com/user-attachments/assets/e41efd5b-b205-4fee-bdc2-2c75a895f790" />|<img width="1080" height="2410" alt="rn_image_picker_lib_temp_e34e70af-f0e7-41d1-acc8-fe6682118a24" src="https://github.com/user-attachments/assets/bdb5cf12-f65c-4044-9b79-4c415623fefc" />| <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Updates tier resolution and refresh triggers to use the new `getEntitlements()` flow; mistakes here could mis-gate paid models/limits for subscribers but scope is limited to Duck.ai model selection logic. > > **Overview** > Fixes Duck.ai tier detection so **Pro subscribers** are correctly treated as `UserTier.PRO` (and thus see Pro-tier models). > > `RealDuckAiModelManager` now listens to `subscriptions.getEntitlements()` for reactive model refresh, and `resolveUserTier()` derives the tier from the Duck.ai entitlement’s `name` (e.g., `plus`/`pro`) instead of inferring from available products. > > Adds `UserTier.from(raw)` and updates tests to the new entitlements API, including new coverage for Pro, unknown tier names, empty/multiple entitlements, and the updated entitlement-change trigger. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 16175cf. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY -->
Task/Issue URL: https://app.asana.com/1/137249556945/project/488551667048375/task/1214776295472425?focus=true ### Description - Adds create image and web search tools to the native input ### Steps to test this PR _With the native input enabled_ - [x] Tap the options button - [x] Select create image - [x] Verify that the pickers are hidden - [x] Toggle to Search and back to Duck.ai - [x] Verify that the option stays selected - [x] Type something and submit - [x] Verify that the image generated - [x] Do the same in Duck.ai - [x] Go back - [x] Tap the options button - [x] Select web search - [x] Type something and submit - [x] Verify that a web search is completed in Duck.ai - [x] Do the same in Duck.ai <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Adds a new tool-selection path that alters AI chat prompt payloads and UI state across native input, which could affect prompt handling and model capability gating if mismatched with backend/JS expectations. > > **Overview** > Adds native input *tool selection* for Duck.ai prompts (e.g. **Create Image** and **Web Search**) and threads it through submission and pending-prompt flows. > > The selected tool is contributed via the options plugin/UI, persisted while navigating, cleared after submit/store, and sent to JS as a new `toolChoice` array in the `submitAIChatNativePrompt`/pending prompt payloads. > > Model capabilities are extended with `supportedTools` (new `Tool` enum) and used to show/hide available options; selecting image generation also hides the model/reasoning pickers. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 4a6e867. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY -->
Task/Issue URL: https://app.asana.com/1/137249556945/task/1214837570101572 ### Description When the contextual Duck.ai sheet opens with the native input widget enabled, the Search/Duck.ai toggle row briefly shows before being hidden — and on some users (typically `SEARCH_ONLY` mode) it stays visible. Regression after the per-tab `NativeInputStateProvider` landed: the contextual widget subscribes to the same `tabId` slot as the main browser widget, so it observes a state with `inputContext = BROWSER` and `toggleVisible = true/false` published by the main widget. This change adds a `private var isContextualWidget: Boolean` on `NativeInputModeWidget`, set from `configureContextual()`. It is then used to: - Force `applyToggleVisibility` to GONE, regardless of the observed state. - Short-circuit `applyOmnibarShape` so it does not mutate the parent `contextualNativeInputCard`'s shape (otherwise a `BROWSER`/`toggleVisible=false` emission would override `ContextualNativeInputManager.applyCardShape`'s top-only rounded corners). The underlying shared-store race and the contextual sheet's outer visual treatment are not in scope here — this is the minimal scoped fix to get the toggle hidden. ### Steps to test this PR _Toggle hidden in contextual sheet_ - [x] Install internal build with native input enabled. - [x] Open the browser, navigate to a page. - [x] Open the Duck.ai contextual sheet (e.g. from the page-context entry point). - [x] Verify the Search/Duck.ai toggle row is **not** visible inside the sheet. - [x] Switch the input-screen setting off (`SEARCH_ONLY` mode) and repeat. Toggle still hidden. - [x] Close the sheet and reopen it. Toggle still hidden. _No regression in browser / full-screen Duck.ai_ - [x] In the main browser omnibar with native input, verify the toggle still appears as before (when applicable). - [ ] Open full-screen Duck.ai mode, verify the toggle still appears as before. ### UI changes | Before | After | | ------ | ----- | | Toggle row briefly/persistently visible inside the contextual sheet | Toggle row hidden, sheet shows only the Duck.ai input | <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Low Risk** > Low-risk UI-only change that gates toggle visibility and shape mutations when the widget is used in the contextual sheet; main risk is unintended suppression of the toggle if the contextual flag is set incorrectly. > > **Overview** > Prevents the Search/Duck.ai toggle row from appearing in the Duck.ai contextual sheet by marking `NativeInputModeWidget` instances created via `configureContextual()` and forcing `applyToggleVisibility` to hide the toggle regardless of per-tab state emissions. > > Also skips `applyOmnibarShape()` for contextual widgets so shared `NativeInputStateProvider` updates from the main browser widget can’t override the contextual sheet card’s rounded-corner styling. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 8b5465a. 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>
Task/Issue URL: https://app.asana.com/1/137249556945/project/1214403976670255/task/1214808360974304?focus=true ### Description Expands current wide event purchase with metadata for missing product error. ### Steps to test this PR QA-Optional: this is adding metadata to existing pixel. ### UI changes | Before | After | | ------ | ----- | !(Upload before screenshot)|(Upload after screenshot)| <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Low Risk** > Low risk: this only enriches wide-event telemetry for billing-flow init failures and updates tests/schema; it does not change the purchase flow control path beyond additional metadata generation. > > **Overview** > Enhances the `wide_subscription-purchase` wide event to include **structured metadata** when `billing_flow_init` fails due to missing Play product/offer details. > > `RealPlayBillingManager` now tracks the outcome of the last `loadProducts()` call and, on missing product details, emits a `BillingFlowInitFailureContext` (reason, requested IDs, loaded count, billing-client readiness, last load outcome) that `SubscriptionPurchaseWideEventImpl` serializes into wide-event `flowStep` metadata. Pixel schema (`wide_subscription-purchase.json5`) is updated to define the new keys, and unit tests are expanded to validate the new metadata and serialization behavior. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit fe38e88. 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 Sonnet 4.6 <noreply@anthropic.com>
…View (#8589) Task/Issue URL: https://app.asana.com/1/137249556945/project/608920331025315/task/1214875487371350?focus=true ### Description Makes RequestBlocklistTest obtain the WebView via an Espresso ViewAction (WebViewGrabber) instead of grabbing it from ActivityScenario.onActivity, reducing flakiness from timing/race conditions. ### Steps to test this PR - QA optional - https://github.com/duckduckgo/Android/actions/runs/26003060252/job/76429699672 passed already <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Low Risk** > Low risk: test-only change that alters how the WebView instance is obtained to avoid a timing/race issue in instrumentation runs. > > **Overview** > Makes `RequestBlocklistTest` obtain the `WebView` via an Espresso `ViewAction` (`WebViewGrabber`) instead of grabbing it from `ActivityScenario.onActivity`, reducing flakiness from timing/race conditions. > > Updates idling-resource tracking calls to use the non-null grabbed `WebView` reference and fails fast if the view is not present. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 2fa6df8. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY --> Co-authored-by: Your Name <1336281+CDRussell@users.noreply.github.com>
Task/Issue URL: https://app.asana.com/1/137249556945/project/414730916066338/task/1214874062712778?focus=true ## PIR Broker Bundle Update Automated update of PIR broker JSON files from the remote bundle. ### Changes **Added (4):** - publicdatacheck.com.json - publicrecordreports.com.json - searchpublicrecords.com.json - vehiclerelatedrecords.com.json **Updated (1):** - spyfly.com.json (0.7.0 → 0.8.0) ### Checklist - [x] Verify broker changes look correct - [x] All tests must pass <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Medium risk because it changes broker automation configurations (selectors/flows) and adds new broker definitions, which can break scanning/opt-out runs if site markup differs from assumptions. > > **Overview** > Updates the PIR broker bundle with **four new broker definitions** (`publicdatacheck.com`, `publicrecordreports.com`, `searchpublicrecords.com`, and `vehiclerelatedrecords.com`), all modeled as SpyFly children (three with full form-based scan/opt-out flows; one delegating opt-out to the parent site). > > Bumps `spyfly.com.json` from `0.7.0` to `0.8.0` and tweaks multiple page-header expectations to use the more specific selector `.section__head h1` for results/submission/verification steps. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit bc71945. 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: daxmobile <daxmobile@users.noreply.github.com>
…#8592) Task/Issue URL: https://app.asana.com/1/137249556945/project/1211760946270935/task/1214893969211117?focus=true ### Description Add an additional message posting during Play Store release process when app building is starting. Also fixes - missing app version in the other messages due to incorrect reference - incorrect title (referencing Internal track) ### Steps to test this PR - qa optional <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Low Risk** > Low risk workflow-only change that adds an extra notification step and corrects variables used in Mattermost messages; main risk is incorrect messaging or minor workflow YAML issues, not app behavior. > > **Overview** > Adds a new Mattermost notification at the start of the Play Store release workflow to announce the build has begun and will be uploaded when complete. > > Updates existing Mattermost messages in `release_upload_play_store.yml` to consistently use `inputs.ref` (instead of `github.event.inputs.ref`) and adjusts the job name wording to reflect the “for review” Play Store upload. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 1cc153f. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY --> Co-authored-by: Your Name <1336281+CDRussell@users.noreply.github.com>
Task/Issue URL: https://app.asana.com/1/137249556945/project/1211850753229323/task/1214864368580602?focus=true ### Description Resolves two pixel validation errors reported by the automated live-validation report: `wide_page-load` and `m_app_startup_time` ### Steps to test this PR n/a ### UI changes n/a
Task/Issue URL: https://app.asana.com/1/137249556945/task/1214864368587046?focus=true ### Description Removed unnecessary atb param from the below pixels: `m_duckai_only_widget_added` `m_search_only_widget_added` `m_search_only_widget_deleted` ### Steps to test this PR - No QA, code review only. ### NO UI changes <!-- CURSOR_SUMMARY --> --- > [!NOTE] > <sup>[Cursor Bugbot](https://cursor.com/bugbot) is generating a summary for commit e7a2007. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY -->
Task/Issue URL: https://app.asana.com/1/137249556945/project/488551667048375/task/1214899543892734?focus=true ### Description - When the native input is enabled, adds a new parameter to Duck.ai URL → `?native-input=true` ### Steps to test this PR _With the native input enabled_ - [ ] Point at the Duck.ai URL linked in the task - [ ] Tap on Duck.ai - [ ] Verify that the initial loading glitch is gone <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Low Risk** > Low risk: only adjusts Duck.ai URL construction to optionally add a query parameter, with unit tests covering the new cases. > > **Overview** > Duck.ai navigation now conditionally appends `native-input=true` to all constructed/opened Duck.ai chat URLs when the native input field user setting is enabled. > > This is centralized via a new `nativeInputParameters()` helper in `RealDuckChat`, and is covered by new tests validating URL output for standard, prefilled, voice, and `getDuckChatUrl` flows. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 3c47d1a. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY -->
Task/Issue URL: https://app.asana.com/1/137249556945/project/1209121419454298/task/1214253991604420?focus=true ### Description Implements the Android side of the cross-platform "opt-out submitted" unification design. Adds a new `optOutFormSubmittedDate` field to the per-match payload sent to the PIR Web dashboard so the Activity card can count submissions made by us, not broker acknowledgments. - Persists `optOutFormSubmittedDate` on a new nullable column on `OptOutJobRecordEntity` (DB v16, destructive migration via existing fallback). - Stamps the timestamp on first successful form submission and never overwrites on retries: - non-email brokers → set in `JobRecordUpdater.updateOptOutRequested` - email-confirming brokers → set in `JobRecordUpdater.markOptOutAsWaitingForEmailConfirmation` (the form-submission moment for that flow) - Surfaces the field through `DashboardExtractedProfileResult` and `PirWebMessageResponse.ScanResult` (in seconds, like the sibling timestamps). - Child brokers (`broker.parent != null`) inherit the most recent `optOutFormSubmittedDate` across any opt-out on the parent broker per design note [3]. Mirror sites copy the value from the parent broker's match. - No backfill — existing records have null until their next form submission. ### Steps to test this PR _Form submission stamping (non-email broker)_ - [x] Run an initial scan against a non-email-confirming broker, then trigger an opt-out for a found profile. - [x] Verify in the DB (`pir_optout_job_record` table) that `optOutFormSubmittedDate` and `optOutRequestedDate` are equal and non-null. - [x] Re-trigger the opt-out (forced retry) and confirm `optOutFormSubmittedDate` is unchanged while `optOutRequestedDate` updates. _Form submission stamping (email-confirming broker)_ - [x] Run an opt-out against an email-confirming broker. - [x] Verify `optOutFormSubmittedDate` is set as soon as the form is submitted (status = PENDING_EMAIL_CONFIRMATION), before email confirmation completes. - [x] Complete the email confirmation; verify `optOutRequestedDate` is set later than `optOutFormSubmittedDate`. _Web contract_ - [x] Open the PIR Web dashboard. In WebView devtools, inspect the `initialScanStatus` / `maintenanceScanStatus` response payloads and confirm each `ScanResult` includes `optOutFormSubmittedDate` (Unix seconds, nullable). _Child broker propagation_ - [x] With opt-outs run against a parent broker, verify that child broker matches in the dashboard payload carry the parent's most recent `optOutFormSubmittedDate`, even when the child profile doesn't strictly match the parent. ### UI changes N/A — native↔WebView contract change only; no native UI changes. <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Adds a new persisted timestamp to the opt-out job record (Room DB v16) and threads it through scheduling and dashboard state into the WebView JS contract; errors in migration or propagation logic could affect opt-out status reporting. > > **Overview** > **Adds a new `optOutFormSubmittedDate` timestamp to PIR opt-out reporting.** A nullable `optOutFormSubmittedDate` column is added to `pir_optout_job_record` (Room schema v16 w/ auto-migration) and mapped through `OptOutJobRecordEntity` � `OptOutJobRecord`. > > `JobRecordUpdater` now stamps `optOutFormSubmittedDateInMillis` on the *first* successful form submission (both direct `REQUESTED` flows and `PENDING_EMAIL_CONFIRMATION` flows) and preserves the original value on retries. The dashboard state and WebView message payloads (`initialScanStatus` and `maintenanceScanStatus`) now include `optOutFormSubmittedDate` (seconds, nullable), with child brokers inheriting the most recent parent-broker submission timestamp and mirror-site results copying the parent match. > > Tests are updated/added to validate the new field in message payloads, retry preservation behavior, and parent�child propagation rules. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit c1b4eb9. 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: Robert Anderson <randerson@duckduckgo.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Task/Issue URL: https://app.asana.com/1/137249556945/project/1207908166761516/task/1212699268790172?focus=true ### Description Adds a new brand-design **Default Browser** page to the onboarding flow, gated behind the `onboardingBrandDesignUpdate` feature flag. The page is only inserted into the flow when `shouldShowDefaultBrowserPage()` returns true — i.e. on devices where the system role-manager / default-browser chooser isn't available and the user has to navigate to system settings manually. This mirrors the gating of the legacy `DefaultBrowserPage`; it's just the surface that's new. **With the flag on:** - New `BrandDesignUpdateDefaultBrowserPage` fragment with the 2026 brand-design layout — new header illustration, restyled title / subtitle, primary "Set as default browser" + secondary "Continue" buttons. - Portrait and landscape layouts wired up; header illustration adapts to available space (caps at design width on tablets, shrinks to a minimum height when content doesn't fit). - New illustration drawables shipped as WebP at every density. - `BrandDesignUpdateDefaultBrowserPageBlueprint` is appended to `buildBrandDesignUpdatePageBlueprints()` whenever `shouldShowDefaultBrowserPage()` is true. **With the flag off:** - No change to the legacy path — `buildPageBlueprints()` still emits the existing `DefaultBrowserBlueprint`, and the original `DefaultBrowserPage` renders exactly as before. ### Steps to test this PR [Designs](https://www.figma.com/design/YPE94Xkcrk2uqiF2l4VmSv/Onboarding--2026-?node-id=20832-106044&m=dev) >⚠️ This page only renders on devices where the system role-manager isn't used (API ≤ 28). Use a Pie (API 28) or older emulator / device to exercise it. To see these changes, patch: ``` Index: app/src/main/java/com/duckduckgo/app/onboardingbranddesignupdate/OnboardingBrandDesignUpdateToggles.kt IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/app/src/main/java/com/duckduckgo/app/onboardingbranddesignupdate/OnboardingBrandDesignUpdateToggles.kt b/app/src/main/java/com/duckduckgo/app/onboardingbranddesignupdate/OnboardingBrandDesignUpdateToggles.kt --- a/app/src/main/java/com/duckduckgo/app/onboardingbranddesignupdate/OnboardingBrandDesignUpdateToggles.kt (revision 6fd565c) +++ b/app/src/main/java/com/duckduckgo/app/onboardingbranddesignupdate/OnboardingBrandDesignUpdateToggles.kt (date 1777967047929) @@ -34,13 +34,13 @@ * Main toggle for the onboarding brand design update feature. * Default value: false (disabled). */ - @Toggle.DefaultValue(DefaultFeatureValue.FALSE) + @Toggle.DefaultValue(DefaultFeatureValue.TRUE) fun self(): Toggle /** * Toggle for the brand design update variant. * Default value: false (disabled). */ - @Toggle.DefaultValue(DefaultFeatureValue.FALSE) + @Toggle.DefaultValue(DefaultFeatureValue.TRUE) fun brandDesignUpdate(): Toggle } Index: app/src/main/java/com/duckduckgo/app/onboarding/ui/OnboardingViewModel.kt IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/app/src/main/java/com/duckduckgo/app/onboarding/ui/OnboardingViewModel.kt b/app/src/main/java/com/duckduckgo/app/onboarding/ui/OnboardingViewModel.kt --- a/app/src/main/java/com/duckduckgo/app/onboarding/ui/OnboardingViewModel.kt (revision 6fd565c) +++ b/app/src/main/java/com/duckduckgo/app/onboarding/ui/OnboardingViewModel.kt (date 1777657893440) @@ -45,7 +45,7 @@ val viewState = _viewState.asStateFlow() fun initializePages() { - pageLayoutManager.buildPageBlueprints() + pageLayoutManager.buildBrandDesignUpdatePageBlueprints() } fun pageCount(): Int { @@ -69,8 +69,8 @@ } } - fun initializeOnboardingSkipper() { - if (!appBuildConfig.canSkipOnboarding) return + fun initializeOnboardingSkipper() { + return // delay showing skip button until privacy config downloaded viewModelScope.launch { ``` _Flag on (API ≤ 28)_ - [x] Fresh install on an API ≤ 28 emulator / device. - [x] Step through onboarding until the default-browser page appears. - [x] Verify the new brand-design layout renders: new illustration, updated title / subtitle, restyled primary + secondary buttons. - [x] Tap the primary "Set as default browser" button — verify it deep-links into the default-browser system settings as before. - [x] Return to the app without setting DDG as default — verify the instructions card (toast) is shown. - [x] Repeat, this time setting DDG as default in system settings — verify onboarding continues to the next page. - [x] Tap the secondary "Continue" button instead — verify onboarding continues to the next page without prompting for default. - [x] Rotate the device on the page — verify the landscape layout renders and the header illustration adapts (caps width on tablets / shrinks height when content doesn't fit). _Flag off_ - [x] Without the patch above (or with `onboardingBrandDesignUpdate` toggled off via internal settings), repeat the fresh install on an API ≤ 28 device. - [x] Step through onboarding to the default-browser page — verify the **legacy** `DefaultBrowserPage` renders unchanged (no new illustration, original copy, original buttons). - [x] Verify the rest of the onboarding flow is unchanged. _Flag on, API ≥ 29_ - [x] Fresh install on an API 29+ emulator / device with the flag on. - [x] Verify the default-browser page is **not shown** (system role-manager path is used instead) — same behaviour as legacy. ### UI changes Screenshots: see the [Screenshots subtask](https://app.asana.com/1/137249556945/project/1202552961248957/task/1214448081619520?focus=true). <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Moderate risk: introduces a new onboarding fragment and page selection path that affects navigation/intent flows and activity result handling on supported devices. > > **Overview** > Adds a new `BrandDesignUpdateDefaultBrowserPage` onboarding screen (plus portrait/land layouts) that reuses the existing default-browser view model/commands but presents updated brand-design UI and responsive header image behavior. > > Extends `OnboardingPageBuilder`/`OnboardingPageManager` with a new `BrandDesignUpdateDefaultBrowserPageBlueprint`, and appends it to `buildBrandDesignUpdatePageBlueprints()` when `shouldShowDefaultBrowserPage()` is true. Updates unit tests to cover the new brand-design blueprint/page-count behavior under different default-browser gating conditions. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 5269feb. 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 Sonnet 4.6 <noreply@anthropic.com>
Task/Issue URL: https://app.asana.com/1/137249556945/project/488551667048375/task/1214907772007655?focus=true ### Description - Updates the attachment chooser to show a similar dialog to the options and reasoning dialogs - Adds an additional option if files are supported ### Steps to test this PR _With the native input enabled_ - [x] Select GPT-5 - [x] Verify that take photo, attach photo and attach file are available - [x] Select 4o-mini - [x] Verify that only take photo and attach photo are available - [x] Verify that the options work as expected ### UI changes <img width="1080" height="822" alt="Screenshot_20260518_182532" src="https://github.com/user-attachments/assets/af87fd47-915c-42f4-b25f-50aeb755b9fe" /> <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Low Risk** > Low risk UI refactor limited to the attachment picker surface; main risk is regressions in picker launch/dismiss behavior and supported type gating. > > **Overview** > Switches the attachment button in `AttachmentView` from an `ActionBottomSheetDialog` chooser to a `PopupWindow` menu styled like other native-input menus, including proper dismissal handling on detach. > > The menu now conditionally shows **Take Photo**, **Attach Photo**, and a new **Attach File** option based on `supportsImageUpload` and `supportedFileTypes`, and adds new strings plus a new `ic_folder_24` icon for the file option. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit d7a9413. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY -->
Task/Issue URL: https://app.asana.com/1/137249556945/task/1213363333711266?focus=true ### Description Add pixels for VPN revoked message and separate promo modal pixels depending on the origin ### Steps to test this PR _Pre steps_ - [x] Apply patch on https://app.asana.com/1/137249556945/project/1209991789468715/task/1210448620621729?focus=true _VPN Revoked alert_ - [x] Install from branch - [x] Purchase a test subscription - [x] Cancel it and wait until it expires - [x] Open the app - [x] VPN Revoked alert should appear and pixel ` m_netp_ev_vpn_access_revoked_dialog_show` is fired - [x] Tap on Subscribe and verify pixel `m_netp_ev_vpn_access_revoked_dialog_subscribe_clicked` is fired - [x] Once in the paywall check the origin is `funnel_alert_android__subscriptionvpnrevoked` _Skipped onboarding modal_ - [x] Fresh install - [x] Skip onboarding tapping on "I've been here before" > "Start browsing" - [x] Close the app and set the date to 7 days from today - [x] Open app - [x] Check half-sheet promo dialog shows correctly - [x] Verify pixel ` m_subscription_promo_modal_skipped_onboarding_shown` is fired - [x] Tap on _Learn More_ and verify pixel ` m_subscription_promo_modal_skipped_onboarding_subscribe_clicked` is fired - [x] Once in the paywall check the origin is `funnel_modal_android__skippedonboardingupsell` ### No UI changes <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Low Risk** > Low risk: primarily adds/updates analytics pixels and origin strings, with a small change to the subscription purchase navigation from the VPN access-revoked dialog. > > **Overview** > Adds new pixel definitions and firing points for the VPN access-revoked dialog, including *Subscribe* and *Dismiss* click events, and passes a dedicated purchase `origin` when launching the subscription paywall. > > Refactors `SubscriptionPromoModalCta` to use a `SubscriptionPromoFlow` enum (origin + per-flow pixels) and fires distinct *shown*/*subscribe clicked* pixels for skipped-onboarding vs nudge promo modals; updates the subscription launch URL and related tests accordingly. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit a4205b9. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY -->
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.
See Commits and Changes for more details.
Created by
pull[bot]
Can you help keep this open source service alive? 💖 Please sponsor : )