Add Customize Responses to NTP omnibar Tools menu - #2865
Conversation
Bring the New Tab Page omnibar Tools-menu "Customize responses" row to parity with the address bar: show the customization summary as the row sub-heading and an on/off toggle once responses have been customized. - omnibar-config: add hasCustomization + customizationActive - new message omnibar_setCustomizeResponsesActive to apply/unapply the stored customization - DropdownItem gains an interactive trailingControl slot; ToolsMenu renders the shared Switch bound to the active state - service/provider wiring, strings, mock-transport support
Match the icon-only trigger state used when image or web search tools are selected.
Add disabled dropdown item support and remove dev-only mock query param overrides for customize config.
Use control-fill-primary when icon-only; hover and active states stay aligned with the expanded Tools button.
Only collapse when a stored customization exists and is active, not when customizationActive is stale after reset.
Replace the NTP customizer sliders glyph with the design-system Glasses-16 icon.
Only show the divider when customize responses is enabled and tool rows are present above it.
[Beta] Generated file diffTime updated: Thu, 16 Jul 2026 17:38:03 GMT Apple
File has changed Integration
File has changed Windows
File has changed |
|
This PR requires a manual review and approval from a member of one of the following teams:
|
Build Branch
Static preview entry points
QR codes (mobile preview)
Integration commandsnpm (Android / Extension): Swift Package Manager (Apple): .package(url: "https://github.com/duckduckgo/content-scope-scripts.git", branch: "pr-releases/feature/anpete/ntp-customize-responses")git submodule (Windows): git -C submodules/content-scope-scripts fetch origin pr-releases/feature/anpete/ntp-customize-responses
git -C submodules/content-scope-scripts checkout origin/pr-releases/feature/anpete/ntp-customize-responsesPin to exact commitnpm (Android / Extension): Swift Package Manager (Apple): .package(url: "https://github.com/duckduckgo/content-scope-scripts.git", revision: "ff92067bf643ad3d189a0cbab69f77e280aa56cc")git submodule (Windows): git -C submodules/content-scope-scripts fetch origin pr-releases/feature/anpete/ntp-customize-responses
git -C submodules/content-scope-scripts checkout ff92067bf643ad3d189a0cbab69f77e280aa56cc |
Align generated types with the updated type generator output on CI.
There was a problem hiding this comment.
Thanks @anpete for working on this 🙏
Overall it works as expected. One thing I'd suggest changing is how we render the "Customize responses" dropdown item.
Right now we pass a batch of props specific to customize responses to ToolsMenu (props): showCustomizeResponses, customizeResponsesSubLabel, hasCustomization, customizeResponsesActive, onSetCustomizeResponsesActive, and onOpenCustomizeResponses. These get threaded down through OmnibarConsumer → Omnibar → AiChatContent → ToolsMenu. That doesn't scale: every new menu entry of this kind adds another batch of props through the same three layers, and ToolsMenu's API keeps growing.
Worth noting we already avoid this for tools. useActiveTools reads its config straight from OmnibarContext, so ToolsMenu only takes tools, activeTool, and onToggle. Customize responses is the odd one out.
I'd refactor it so the config comes from OmnibarContext instead of props, the way useActiveTools already feeds the tools. Concretely: a useCustomizeResponses hook for the state and actions, and the row resolved to a uniform config rendered from the same .map the tools use. ToolsMenu then drops the six props and stays a 3-prop shell, so the next menu entry is another config source instead of more props and JSX.
I've prepared a commit that should give you some ideas: 0a0982b.
Happy to sync if needed.
The "Customize responses" row was passed into ToolsMenu as six feature-specific props threaded through OmnibarConsumer, Omnibar, and AiChatContent, which does not scale as more menu entries are added. Read the customize config and actions from OmnibarContext via a useCustomizeResponses hook, mirroring how useActiveTools feeds the tools, and render every row (tools and customize) from one uniform config list. Extract the customize row into useCustomizeResponsesItem so the feature stays self-contained and ToolsMenu keeps a three-prop surface (tools, activeTool, onToggle). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Stale comment
Web Compatibility Assessment
File Lines Severity Finding special-pages/pages/new-tab/app/omnibar/components/chat-tools/dropdown/Dropdown.js65–66, 132–139 warning DropdownSeparatorchildren return{}fromgetItemProps()(notnull), so separators are included innavigableIndices. Arrow/Home/End can land on a separator;aria-activedescendantmay reference anidinjected viacloneElementthatDropdownSeparatordoes not forward to its<li>, producing a phantom focus target. Filter separators out ofgetNavigableIndices()(e.g. treatrole="separator"or returnnullfromgetItemPropsfor non-item children).special-pages/pages/new-tab/app/omnibar/components/chat-tools/tools-menu/useCustomizeResponsesItem.js51–64 warning Keyboard Space/Enter on the customize-responses row invokes onSelect(opens modal) viaDropdown.handleKeyDown; the embeddedSwitchonly receives mouse clicks (stopPropagationon the trailing span). Keyboard users cannot toggle the switch without Tab-stopping into it. Considerrole="menuitemcheckbox"with row-level toggle behavior, or intercept Space when focus is on a row withtrailingControl.special-pages/pages/new-tab/app/omnibar/components/chat-tools/dropdown/Dropdown.js65–66 info Disabled rows (e.g. customize-responses when image-generation is active) remain in navigableIndices; keyboard can highlight them butselectAtno-ops. Acceptable givenaria-disabled+pointer-events: none, but skipping disabled indices would be cleaner.All special-pages/changes— info No injected/changes. This code runs in the embedded first-party NTP, not in third-party page context — no browser API fidelity, prototype-chain, or third-party script detection concerns apply.Security Assessment
File Lines Severity Finding special-pages/pages/new-tab/app/omnibar/omnibar.service.js214–224 info New notify()calls send fixed payloads ({}and{ active }). No object spreading; nonativeDataleakage risk.special-pages/pages/new-tab/app/omnibar/components/chat-tools/useCustomizeResponses.js14 info Feature visibility gated with strict config?.enableCustomizeResponses === true(defaultfalsein schema). Native can remotely disable per window.special-pages/pages/new-tab/messages/omnibar_openCustomizeResponses.notify.json1–7 info Schema lacks additionalProperties: false(consistent with most omnibar notifies likeomnibar_viewAllAIChats; onlyomnibar_submitChatenforces it). Low risk given empty fixed payload.special-pages/pages/new-tab/messages/omnibar_setCustomizeResponsesActive.notify.json1–15 info Same additionalPropertiesnote;activeis a required boolean — no injection surface.special-pages/pages/new-tab/app/omnibar/components/chat-tools/tools-menu/useCustomizeResponsesItem.js49 info customizeSubLabelfrom native config is rendered as Preact text content (auto-escaped). NodangerouslySetInnerHTML.Risk Level
Low Risk — special-pages NTP omnibar UI only; no injected runtime, API overrides, messaging transport changes, or security-boundary modifications.
Recommendations
- (warning) Exclude
DropdownSeparatorfromgetNavigableIndices()and ensure separator<li>elements are notaria-activedescendanttargets.- (warning) Align keyboard behavior for the customize-responses toggle with mouse behavior (row Space/Enter toggles when
hasCustomization, or document Tab-to-switch as intentional).- (info) Add integration tests for the customize-responses row (visibility gating, toggle notify, disabled-when-image-gen, separator placement) — mock transport already handles both notifies.
- (info) Consider
additionalProperties: falseon new notify schemas for consistency with stricter messages.Synchronize note (
cab5a3b)Latest commit refactors customize-responses into
useCustomizeResponses/useCustomizeResponsesItemhooks (mirrorsuseActiveToolspattern), removing prop drilling throughOmnibar→ToolsMenu. Separator now renders only whenindex > 0. No new compatibility or security surface introduced by the refactor.Sent by Cursor Automation: Web compat and sec
Wire up omnibar.enableCustomizeResponses, omnibar.customizeSubLabel, omnibar.hasCustomization, and omnibar.customizationActive in the new-tab mock transport, matching the existing omnibar.* overrides, so the Customize responses row and its states can be exercised from the URL in local dev. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Skip separators and disabled rows in Dropdown keyboard navigation so arrow/Home/End never land on a non-actionable element or point aria-activedescendant at a separator. - Drop native notify method names from omnibar config type descriptions. - Add additionalProperties: false to the new customize-responses notify schemas, matching stricter omnibar messages.
There was a problem hiding this comment.
Stale comment
Injected PR Evaluation — Web Compatibility & Security
Re-assessed on synchronize (
0fdc9c75f). Latest commit addresses prior review feedback (separator keyboard nav,additionalProperties: falseon notify schemas).Web Compatibility Assessment
File Lines Severity Finding (none — injected/)— — No changes to injected runtime code. This PR is confined to special-pages/pages/new-tab/(first-party NTP omnibar UI). Standard web-compat categories (API overrides, prototype patching, third-party script detection) do not apply.Dropdown.js71–77, 143–154 info getNavigableIndices()correctly skipsDropdownSeparatorand disabled rows — prior separator keyboard-nav concern is resolved in0fdc9c75f.Dropdown.js+useCustomizeResponsesItem.js143–154 / 64 warning When the Customize Responses row is keyboard-focused, Space/Enter invoke onSelect(open modal) rather than toggling the embeddedSwitch. Mouse users can click the switch (stopPropagationon trailing control), but keyboard-only users cannot toggle without opening the modal first. Consider intercepting Space when focus is on the trailing control, or making the switch independently focusable within the menu.useCustomizeResponsesItem.js42, 50 info Row is disabled when activeTool === 'image-generation'— sensible mutual-exclusion with image-gen mode.pointer-events: noneon disabled rows also blocks mouse access to the switch.ToolsMenu.js486–488 info Separator inserted only when customize-responses is not the first item — correct grouping UX. (tests) — info No dedicated integration tests for the customize-responses Tools menu row or toggle notify. Mock transport handles both new notifies; consider Playwright coverage for keyboard nav + toggle. Security Assessment
File Lines Severity Finding (none — injected/)— — No changes to captured-globals.js, message bridge, wrapper utilities, or injected features.useCustomizeResponses.js605 info Feature visibility gated on enableCustomizeResponses === true(schema defaultfalse) — supports remote rollback without deploy.omnibar.service.js666–675 info New notifies use explicit, minimal payloads: omnibar_openCustomizeResponses→{};omnibar_setCustomizeResponsesActive→{ active: boolean }. No object spreading; nonativeDataleakage risk.omnibar_openCustomizeResponses.notify.json6 info additionalProperties: false— added in0fdc9c75f, matching stricter omnibar message schemas.omnibar_setCustomizeResponsesActive.notify.json6, 10–12 info additionalProperties: false+ requiredactive: boolean— typed, closed schema.useCustomizeResponsesItem.js49 info customizeSubLabelfrom native config rendered as Preact text node — auto-escaped; no HTML injection vector.Risk Level
Low Risk — Special-pages-only UI change with config-gated visibility, closed notify schemas, and no injected/API-override/messaging-transport modifications.
Recommendations
- (warning) Improve keyboard a11y for the embedded
Switch: either focus-trap into the trailing control on ArrowRight, or suppress row-level Space/Enter when focus is within the switch.- (info) Add integration tests covering: customize-responses row visibility (
enableCustomizeResponses), toggle notify (omnibar_setCustomizeResponsesActive), separator keyboard nav, and disabled state during image-generation.- (info) Confirm native platforms handle both new notifies before shipping (expected coordination).
Sent by Cursor Automation: Web compat and sec
Replace the numeric 1/-1 direction argument to focusNavigable with 'up'/'down' so call sites read clearly; the step is derived internally.
Move the menu-building logic out of ToolsMenu into a useToolsMenuItems
hook that composes all menu sources (tools + Customize responses) into a
single view model: { items, activeItem, isCollapsed }. ToolsMenu becomes
render-only, and Omnibar gates the menu on items.length instead of
checking availableTools and showCustomizeResponses separately, making the
data flow explicit and scalable as more menu entries are added.
Add a separatorBefore flag to the menu item config and set it on the "Customize responses" row when tool rows precede it, so ToolsMenu renders the divider from config instead of a customize-responses-specific index check.
Replace the nested ternary for the start position with an explicit if/else, and use modulo for the wrap-around step.
Dismissing stale approval — new commits pushed, awaiting Cursor re-review.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit ff7dbb2. Configure here.
There was a problem hiding this comment.
Stale comment
Injected PR Evaluation: Web Compatibility & Security
PR: Add Customize Responses to NTP omnibar Tools menu (
ff7dbb2a)
Scope:special-pages/pages/new-tab/only — noinjected/,messaging/, orcaptured-globalschanges. This UI runs in the first-party NTP embedded page, not in third-party page injection.
Web Compatibility Assessment
File Lines Severity Finding (none in injected scope) — info No browser API overrides, prototype patches, or DOM patterns that affect third-party sites. dropdown/Dropdown.js149–160 warning Keyboard vs trailing toggle: Space/Enteron the focused row invokeonSelect(opens Customize Responses modal) rather than toggling the embeddedSwitch. Mouse users can click the switch (stopPropagationontrailingControl), but keyboard-only users cannot operate the toggle from the menu.dropdown/Dropdown.js165–167 warning Tab closes menu: TabschedulesonClose({ restoreFocus: false }), so focus cannot move into the trailingSwitchwhile the menu stays open.dropdown/Dropdown.js71–111 info Positive: getNavigableIndices()/focusNavigable('up'|'down')correctly skipDropdownSeparatorrows and disabled items;aria-activedescendantwon't point at separators.tools-menu/useCustomizeResponsesItem.js42, 49 info Customize Responses row is disabled when image-generationis active — avoids conflicting tool states.tools-menu/useToolsMenu.js74 info Tools button collapses to icon-only when a tool chip or active customization is shown — localized NTP layout change only. omnibar/mocks/omnibar.mock-transport.js203–208 warning Missing break:omnibar_setCustomizeResponsesActivefalls through intoomnibar_removeSuggestion, loggingmsg.params.urlasundefinedin local dev mocks.integration-tests/— info No dedicated integration tests for the Customize Responses row or its notifies; existing tools-menu tests cover Create Image / Web Search only.
Security Assessment
File Lines Severity Finding (scope) — info Changes are confined to the NTP special page. Standard injected threat model (prototype tampering, captured-globals, message-bridge spoofing) does not apply.omnibar.service.js218–227 info Explicit notify payloads: notify('omnibar_openCustomizeResponses', {})andnotify('omnibar_setCustomizeResponsesActive', { active })— no object spreading; nonativeDataleakage risk.messages/omnibar_*.notify.json— info Both schemas set additionalProperties: false; toggle schema requires booleanactive.useCustomizeResponses.js16 info Config-gated: showCustomizeResponsesrequiresenableCustomizeResponses === true(schema defaultfalse) — remotely disableable without deploy.useCustomizeResponsesItem.js49 info customizeSubLabelfrom native config is rendered as Preact text (descriptionprop) — auto-escaped; nodangerouslySetInnerHTML.messages/types/omnibar-config.json189–212 info New config fields are typed booleans/strings with defaults; no executable config surface.
Risk Level
Low Risk — Special-pages NTP UI only; no injected runtime, API shims, or messaging transport changes; feature is config-gated with tightly scoped notify schemas.
Recommendations
- (warning) Add
break;after theomnibar_setCustomizeResponsesActivecase inomnibar.mock-transport.jsto prevent fall-through.- (warning) Consider keyboard accessibility for the trailing
Switch: e.g. don'tpreventDefaultSpace when focus is insidetrailingControl, or expose a separate toggle action via keyboard.- (info) Add integration tests asserting
omnibar_openCustomizeResponses/omnibar_setCustomizeResponsesActivenotifies when the row is selected and the toggle is clicked.- (info) Coordinate native handler rollout for the two new notify methods before enabling
enableCustomizeResponsesin production config.Sent by Cursor Automation: Web compat and sec
There was a problem hiding this comment.
Injected PR Evaluation: Web Compatibility & Security
Re-assessed on synchronize (5b0be2087). Latest commit fixes the mock-transport break fallthrough flagged in the prior review.
Web Compatibility Assessment
| File | Lines | Severity | Finding |
|---|---|---|---|
special-pages/pages/new-tab/app/omnibar/components/chat-tools/dropdown/Dropdown.js |
149–160 | warning | Space/Enter on the highlighted row always invokes onSelect (opens Customize Responses modal). When trailingControl embeds a Switch, keyboard users cannot toggle active state from the row — only mouse clicks on the switch work (stopPropagation on click, not keydown). Consider forwarding Space/Enter to the focused trailing control or using a roving-tabindex pattern. |
special-pages/pages/new-tab/app/omnibar/components/chat-tools/dropdown/Dropdown.js |
165–167 | warning | Tab closes the menu (onClose({ restoreFocus: false })) before focus can move into the embedded Switch, so keyboard users cannot reach the toggle without the menu closing first. |
special-pages/pages/new-tab/app/omnibar/components/chat-tools/tools-menu/useCustomizeResponsesItem.js |
42, 50 | info | Row is disabled when image-generation is active (pointer-events: none via .itemDisabled). Consistent with product intent; disabled row is excluded from getNavigableIndices(). |
special-pages/pages/new-tab/app/omnibar/components/chat-tools/dropdown/Dropdown.js |
71–79 | info | getNavigableIndices() / focusNavigable('up'/'down') correctly skip separators and disabled rows — avoids aria-activedescendant pointing at non-actionable elements. |
| (no injected/ changes) | — | info | No browser API overrides, prototype patches, or third-party page interaction. Web-compat evaluation categories (API fidelity, prototype chain, DOM on hostile pages) do not apply. |
Security Assessment
| File | Lines | Severity | Finding |
|---|---|---|---|
special-pages/pages/new-tab/app/omnibar/omnibar.service.js |
218–227 | info | New notifies use explicit literal payloads ({}, { active }). No object spreading — nativeData leakage risk is low. |
special-pages/pages/new-tab/messages/omnibar_openCustomizeResponses.notify.json |
1–8 | info | Empty object schema with additionalProperties: false — page cannot inject extra fields. |
special-pages/pages/new-tab/messages/omnibar_setCustomizeResponsesActive.notify.json |
1–16 | info | Required boolean active with additionalProperties: false — well-bounded payload. |
special-pages/pages/new-tab/app/omnibar/components/chat-tools/useCustomizeResponses.js |
16 | info | Feature gated via config?.enableCustomizeResponses === true (schema default false). Native can disable per rollout without code deploy. |
special-pages/pages/new-tab/app/omnibar/components/chat-tools/tools-menu/useCustomizeResponsesItem.js |
49 | info | customizeSubLabel from native config is rendered as Preact text content ({description} in DropdownItem) — auto-escaped, no dangerouslySetInnerHTML. Trust boundary is native→NTP (first-party). |
special-pages/pages/new-tab/app/omnibar/mocks/omnibar.mock-transport.js |
203–207 | info | Fixed in 5b0be2087: omnibar_setCustomizeResponsesActive case now has break; — no longer falls through to omnibar_removeSuggestion. |
| (no injected/ changes) | — | info | No changes to captured-globals.js, message-bridge trust boundary, origin validation, or postMessage usage. |
Risk Level
Low Risk — Changes are confined to the first-party NTP special-page (special-pages/pages/new-tab/); no injected content-script code, API shims, or messaging transport changes that affect third-party web pages.
Recommendations
- (warning) Improve keyboard a11y for the embedded
Switch: either handle Space/Enter at theDropdownlevel whentrailingControlis present, or allow Tab to move focus into the switch without closing the menu. - (info) Add an integration test for the customize-responses row (toggle visibility, disabled state during image generation,
omnibar_openCustomizeResponses/omnibar_setCustomizeResponsesActivenotifies) — mock transport and query-param overrides are already wired. - (info) Coordinate native handling for the two new notifies and four config fields before enabling
enableCustomizeResponsesin production.
Positive notes
- Config-gated with safe defaults (
enableCustomizeResponses: false). - Message schemas use
additionalProperties: falseand explicit payloads. - Dropdown keyboard-nav refactor (
getNavigableIndices, separator support, disabled rows) is a solid improvement shared by all Tools-menu rows. - Mock transport fallthrough from prior review is resolved.
Sent by Cursor Automation: Web compat and sec



Asana Task/Github Issue: https://app.asana.com/1/137249556945/project/72649045549333/task/1215104074883750?focus=true
Description
Adds the Customize Responses entry to the NTP AI chat Tools menu, wired through omnibar config and native messaging.
enableCustomizeResponses,customizeSubLabel,hasCustomization,customizationActiveomnibar_openCustomizeResponses,omnibar_setCustomizeResponsesActivehasCustomizationis truecustomizationActive && hasCustomization)Testing Steps
special-pagesnpm run watch -- --page=new-tabCustomize Responseitem renders.Checklist
Please tick all that apply:
Note
Medium Risk
New native messaging and config drive AI response customization UX; changes are config-gated but depend on correct native handling of open/toggle notifies.
Overview
Adds a Customize Responses row to the NTP AI chat Tools menu when
enableCustomizeResponsesis on, gated by omnibar config (customizeSubLabel,hasCustomization,customizationActive).Menu construction moves into
useToolsMenu/useCustomizeResponsesItem: the row opens the native modal viaomnibar_openCustomizeResponses, and when the user has saved settings it shows an inline Switch that callsomnibar_setCustomizeResponsesActive. The Tools trigger collapses to icon-only when a tool chip is active or customization is applied; the row is disabled during image generation and separated from tool checkboxes when both are shown.Shared Dropdown support grows for this pattern: separators, disabled rows, trailing controls (toggle clicks don’t fire the row), and keyboard navigation that skips non-actionable items.
Reviewed by Cursor Bugbot for commit 5b0be20. Bugbot is set up for automated code reviews on this repo. Configure here.