Skip to content

Scope dialog dismissal to how the dialog was shown - #7

Merged
Frotty merged 3 commits into
masterfrom
agent/modal-dismiss-scope
Aug 17, 2026
Merged

Frotty merged 3 commits into
masterfrom
agent/modal-dismiss-scope

Conversation

@Frotty

@Frotty Frotty commented Aug 16, 2026

Copy link
Copy Markdown
Owner

Follow-up to #6. Closes the gap wurst-castle-fight had already worked around in RoundStatsBoard.wurst:

Not UIDialog.withModal(): that wires an outside-click handler calling the dialog's own global hide(), which would close the board for EVERY player when one of them clicks the dim.

That comment was accurate. withModal()'s dismiss handler, the confirm dialog's Yes/No buttons and UIDialog.closeButton() all called the global hide(), so one player dismissing closed the dialog for everyone — which made the show(p) overload added in #6 unusable together with a modal.

Approach

Rather than pick one scope for both cases, dismissal now follows how the dialog was shown:

  • shown globally with show() → dismissing closes it for everyone (unchanged);
  • shown per player with show(p) → dismissing closes it for that player alone.

setVisible(player, bool) sets an ownerScoped flag and dismissFor(p) branches on it. A shared prompt therefore keeps resolving once for all — correct when the dialog represents a single decision — while a private prompt cannot be closed out from under the other players. ModalDismissCallback already carries the acting player from #6, so no new plumbing was needed.

Applies to all three dismissal paths: the modal outside-click, UIConfirmDialog's Yes/No, and UIDialog.closeButton().

Compatibility

The only behaviour that changes is for dialogs shown per player, which was not expressible before #6, so nothing existing can depend on it. Verified against both consuming maps: neither uses withModal() or UIDialog.closeButton() — zombie-defense uses the free closeButton(frame, size) from TableUiButtons, which is untouched. Inert for them until they adopt it.

Validation

Stage 1 (this repo's gate):

  • grill typecheck --quiet — passes.
  • grill test — full suite green, zero compiler warnings.
  • git diff --check — clean.
  • The demo panel gained a row, so its fit was re-checked headlessly with a throwaway inspect() model before committing: 0.368 required of 0.380 available, empty overflow summary. Scratch file removed.

Stage 2 (engine evidence) — not run, and not runnable here. This repo has no e2e harness of its own; per AGENTS.md that gate belongs to a consuming map. TableLayoutTest.wurst gains a "Private" button that opens the confirm dialog for the clicking player only, so the scenario is ready to click, but the behaviour this PR changes is per-player dismissal — it cannot be observed with one client. The real check needs two clients: P1 opens via "Private", P2 must not see it; P1 clicks the dim, and the dialog must close for P1 only. The pre-existing "Confirm" button covers the global path in the same run.

Treat this as unverified against the engine until that run happens.

Closes the gap wurst-castle-fight worked around in RoundStatsBoard, which
carries the comment: "Not UIDialog.withModal(): that wires an outside-click
handler calling the dialog's own global hide(), which would close the board
for EVERY player when one of them clicks the dim."

That was accurate. withModal()'s dismiss handler, the confirm dialog's
Yes/No buttons and UIDialog.closeButton() all called the global hide(), so
one player dismissing a dialog closed it for everyone - which made the
per-player show(p) overload added in #6 unusable with a modal.

Dismissal now follows how the dialog was shown, rather than picking one
scope for both cases. setVisible(player, bool) marks the dialog owner-scoped,
and dismissFor(p) then hides for that player alone; a dialog only ever shown
globally still dismisses globally. So a shared prompt keeps resolving once
for all - the right behaviour when the dialog represents a single decision -
while a private prompt cannot be closed out from under the other players.
ModalDismissCallback already carries the acting player from #6, so no new
plumbing was needed.

Backward compatible: the only behaviour that changes is for dialogs shown
per player, which was not expressible before #6. Neither consuming map uses
withModal() or UIDialog.closeButton() today (zombie-defense uses the free
closeButton(frame, size), untouched), so this is inert for them until they
adopt it.

TableLayoutTest gains a "Private" button opening the confirm dialog for the
clicking player only. Panel fit re-checked headlessly before committing:
0.368 required of 0.380 available, no overflow.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 50c1bac2a4

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +73 to +76
if ownerScoped and p != null
hide(p)
else
hide()

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Set ownerScoped when changing dialog visibility

When either dialog is opened with show(p), ownerScoped is never assigned anywhere and therefore remains false; consequently every Yes/No, modal-backdrop, or close-button click reaches the global hide() branch and still closes the dialog for all players. Update the scope state in both the global and player-scoped visibility paths so it reflects the most recent way the dialog was shown.

AGENTS.md reference: AGENTS.md:L20-L23

Useful? React with 👍 / 👎.

Valid review finding, and the previous commit's feature did nothing without
it. ownerScoped was declared and read by dismissFor() but never assigned, so
it stayed false and every Yes/No, modal-backdrop and close-button click took
the global hide() branch - exactly the behaviour the commit set out to fix.
The intended assignment was lost to a silent no-op find/replace and the
grep that would have caught it was not read closely enough.

Both classes now update the flag in both directions, so it reflects the most
recent way the dialog was shown: setVisible(player, bool) sets it, and a
global show() clears it again. A dialog shown per player, then later shown to
everyone, correctly goes back to dismissing for everyone.

Verified by inspection this time rather than assumed: each class has exactly
one declaration, one read in dismissFor, one set-true in the player-scoped
path and one set-false in the global show, and all five dismissal paths
(modal outside-click x2, Yes, No, closeButton) route through dismissFor.
@Frotty

Frotty commented Aug 16, 2026

Copy link
Copy Markdown
Owner Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 5dbbe6cfec

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread wurst/components/TableUiDialog.wurst Outdated
function setVisible(player p, bool flag) returns thistype
// Records that this dialog is being driven per player, so dismissFor() closes it for the
// acting player instead of everyone. A later global show() clears it again.
ownerScoped = true

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve global dismissal after hiding one player

When a dialog opened with show() is subsequently hidden from one player via hide(p) or setVisible(p, false), this unconditional assignment changes its dismissal mode to owner-scoped even though the dialog was shown globally. A remaining player's Yes/No/outside click then hides only that player, leaving the dialog visible to others and allowing repeated result callbacks, contrary to the documented rule that globally shown dialogs dismiss for everyone; the mirrored assignment in UIDialog.setVisible has the same problem. Only a player-scoped show should switch the mode, while a scoped hide should preserve the mode of the active presentation.

AGENTS.md reference: AGENTS.md:L20-L23

Useful? React with 👍 / 👎.

Valid review finding. setVisible(player, bool) set ownerScoped
unconditionally, so a scoped HIDE flipped the mode too. Hiding a globally
shown dialog from one player - an observer, say - turned it into a
per-player prompt, and a remaining player's Yes/No/outside click then closed
only their own copy. The dialog stayed up for everyone else, who could
answer again, firing the result callback repeatedly. That contradicts the
documented rule that a globally shown dialog dismisses for everyone.

A scoped show is what establishes a per-player presentation, so only that
sets the mode now; a scoped hide preserves the mode of the active
presentation. Global show() still clears it.

Combinations checked: show() then hide(p) keeps global dismissal;
show(p) dismisses for p alone; show() after show(p) reverts to global;
hide(p) on a never-shown dialog leaves the mode untouched.
@Frotty

Frotty commented Aug 16, 2026

Copy link
Copy Markdown
Owner Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Bravo.

Reviewed commit: ef750790ff

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@Frotty
Frotty merged commit 0dcd360 into master Aug 17, 2026
2 checks passed
@Frotty
Frotty deleted the agent/modal-dismiss-scope branch August 17, 2026 11:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant