Skip to content

Claim the prompt surface only when a modal will show - #9387

Draft
catalinradoiu wants to merge 1 commit into
developfrom
feature/cradoiu/improve-modal-evaluator-promots-coordination
Draft

Claim the prompt surface only when a modal will show#9387
catalinradoiu wants to merge 1 commit into
developfrom
feature/cradoiu/improve-modal-evaluator-promots-coordination

Conversation

@catalinradoiu

@catalinradoiu catalinradoiu commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Task/Issue URL:
Tech Design URL (if applicable):
API Proposals URL(s) (if applicable):

Description

Introduces a decide-then-show contract for modal evaluators. Previously, evaluate() was responsible for both deciding eligibility and performing the side effects of showing a modal (marking surveys consumed, recording messages shown, advancing prompt stages, starting activities). This meant that side effects could fire before the shared prompt surface was actually claimed, and a refused surface could silently burn a prompt or advance state without anything ever appearing on screen.

The ModalEvaluator.EvaluationResult.ModalShown result type is replaced by WantsToShow(show: suspend () -> Boolean). Evaluators now return a deferred action: evaluate() is side-effect free with respect to showing, and the coordinator invokes show() only after successfully claiming the shared prompt surface. The show() lambda returns true when the modal actually reached the user and false when showing fell through (e.g. no visible host), allowing the coordinator to cancel the claim and continue down the priority list rather than recording a completion.

The PromptsCoordinator claim model is simplified accordingly. Because claims are now never speculative — a claim is taken only when a modal is definitely about to show — a busy surface is a final answer rather than something to wait out. The 1-second wait-and-retry loop in RealPromptsCoordinator.tryClaim is removed, along with the MutableStateFlow owner and the ClaimOutcome sealed interface. The owner is now a plain nullable field guarded by the existing mutex.

Stage transitions in AdditionalDefaultBrowserPromptsImpl are deferred into the show action so that a refused prompt surface cannot advance the stage without its dialog ever appearing. Similarly, survey consumption in DefaultBrowserChangedSurveyEvaluator and message recording in RemoteMessageModalSurfaceEvaluator are moved inside their respective show actions.

Tests are updated throughout to reflect the two-phase contract: assertions verify that deciding alone produces no side effects, and side effects are confirmed only after explicitly invoking the returned show() action.

Steps to test this PR

Modal prompt coordination

  • Trigger an app resume with multiple eligible modal evaluators and confirm only the highest-priority one shows.
  • Confirm that when the prompt surface is held by another prompt, the evaluator's show() action is never invoked and no state is persisted.
  • Confirm that when show() returns false (presenter not available), the coordinator cancels the claim, records no completion, and continues to the next evaluator.
  • Confirm that when show() throws, the claim is cancelled and the surface is not stranded for the rest of the process.

Stage transitions

  • Trigger the default browser prompts flow to a stage that shows a dialog and confirm the stage is not stored until the dialog actually appears.

Survey / remote message

  • Trigger the default-browser-changed survey and confirm markSurveyShown is not called until the survey activity starts.
  • Trigger a remote message modal and confirm recordLastShownRemoteMessage is not called until the activity starts.

UI changes

Before After
N/A N/A

Evaluators decided and showed in a single evaluate() call, so the Modal
Coordinator had to claim the shared prompt surface speculatively, before
knowing whether any modal would show. Most passes showed nothing and
released the claim milliseconds later, but an NTP card claiming in that
window was refused, and tryClaim papered over it by waiting a second and
retrying.

Split the two: evaluate() now returns WantsToShow with a deferred show
action, evaluation runs with no claim held, and the surface is claimed
only once an evaluator is about to show. A claim now always means a
prompt is genuinely appearing, so a busy surface is a final answer and
the wait/retry in tryClaim is gone.

Side effects that mark a prompt as consumed (RMF message recorded,
survey marked shown, default browser stage advanced) moved into the show
action, so a refused claim can no longer burn a prompt nobody saw. The
two promo evaluators resolve their display delay and presenter lookup
during evaluation, since both can still decline.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

Copy link
Copy Markdown
Contributor Author

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

@github-actions

github-actions Bot commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

⚠️ API Proposal reminder

This PR changes the public surface of one or more -api modules, but the API Proposals field of the PR description is empty. This is a non-blocking reminder — please double-check whether a proposal is needed.

Changed -api source files:

  • prompts-coordinator/prompts-coordinator-api/src/main/java/com/duckduckgo/promptscoordinator/api/ModalEvaluator.kt
  • prompts-coordinator/prompts-coordinator-api/src/main/java/com/duckduckgo/promptscoordinator/api/PromptsCoordinator.kt

To clear this reminder, edit the PR description and either:

  • add the approved API Proposal link(s) in the API Proposals field (one per line), or
  • write None there if this PR does not change the public API (e.g. KDoc, comments, tests).

See the API Proposals section in .cursor/rules/contributions.mdc for details.

@aibrahim-

Copy link
Copy Markdown
Contributor

Code looks good to me.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants