feat(ui): right-click context bundle (SD-2945)#3169
Conversation
Adds ui.viewport.contextAt({ x, y }) returning the full
{ point, entities, position, selection, insideSelection } bundle every
right-click consumer ends up assembling by hand. insideSelection is an
AABB hit-test against the current selection rects so predicates can
distinguish "right-clicked the selection" from "right-clicked elsewhere"
without re-running geometry.
Widens ContextMenuWhenInput with optional point / position /
insideSelection (additive, old { entities, selection } predicates keep
working). getContextMenuItems(input) now accepts a ViewportContext OR
the legacy { entities } shape; bundle inputs make the same context
available on ContextMenuItem.invoke() and pass it into the registered
execute({ context }) so handlers act on the click target without
threading payloads. Direct ui.commands.execute / commands.get(id).execute
calls leave context undefined.
Tests cover the AABB helper boundaries, contextAt composition,
predicate input shape for both call styles, invoke() forwarding to
execute, and the legacy path keeping invoke absent.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 48d983d38c
ℹ️ About Codex in GitHub
Codex has been enabled to automatically 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 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| const whenInput = context | ||
| ? { | ||
| entities, | ||
| selection: state.selection, |
There was a problem hiding this comment.
Use bundled selection when evaluating context-menu predicates
When getContextMenuItems is called with a ViewportContext bundle, the predicate input still uses state.selection instead of context.selection. This means predicates can be evaluated against one selection while item.invoke() later executes with a different selection snapshot from the bound bundle, so menu visibility and command behavior can diverge if callers cache/forward a bundle (the exact stale-selection hazard this API is trying to avoid).
Useful? React with 👍 / 👎.
… (SD-2945)
isViewportContextBundle is the single guard both the controller proxy
and the registry route through, so the two layers can't disagree on
ambiguous inputs ({ point: null }, undefined, partial bundles). Closes
the typeof-null trap in the proxy and the !Array.isArray-on-undefined
trap in the registry.
ContextMenuItem.invoke() now mirrors the captured-handle pattern at
buildHandle.execute: a closure captures the entry that produced the
menu item and refuses to dispatch when a later register({ id }) has
replaced it. A stale menu item returns false instead of firing the
new owner's handler with the old item's label/predicate/bundle.
Weakens the contextAt JSDoc claim about deterministic empty-bundle
defaults: non-numeric coords coerce to (0, 0), which is itself a
valid viewport point.
Three regression tests: invoke-after-replacement, partial-input
legacy routing ({ entities, point: null }), and isViewportContextBundle
boundaries (null point, missing x/y, non-object).
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
|
🎉 This PR is included in @superdoc-dev/react v1.2.0-next.99 The release is available on GitHub release |
|
🎉 This PR is included in vscode-ext v2.3.0-next.101 |
|
🎉 This PR is included in @superdoc-dev/mcp v0.3.0-next.57 The release is available on GitHub release |
|
🎉 This PR is included in superdoc v1.30.0-next.57 The release is available on GitHub release |
|
🎉 This PR is included in superdoc-cli v0.8.0-next.74 The release is available on GitHub release |
|
🎉 This PR is included in superdoc-sdk v1.8.0-next.57 |
Lifts the small bundle every right-click consumer keeps reassembling onto the controller.
ui.viewport.contextAt({ x, y })returns the full shape:```ts
{ point, entities, position, selection, insideSelection }
```
`insideSelection` is an AABB hit-test against the live selection rects, so predicates can distinguish 'right-clicked the selection' from 'right-clicked elsewhere' without re-running geometry.
`getContextMenuItems(input)` accepts the full bundle (or the legacy `{ entities }` shape). Bundle inputs make `point` / `position` / `insideSelection` available on `ContextMenuContribution.when`, and each returned item carries an `invoke()` closure that fires `execute({ context })` with the bundle bound. Direct `ui.commands.execute` and `commands.get(id).execute()` calls still pass `context: undefined`, so handlers that don't care about clicks keep working unchanged.
Closes the gap consumers hit on every register site: today they thread the same shape (entity + point + position + selection containment) by hand and unpack it inside the handler. The bundle removes the boilerplate and keeps predicate filtering and execute dispatch on the same shape, so 'Paste here' / 'Comment here' / 'Accept this change' stop being stale-selection hazards.
Out of scope: the demo update consuming this surface lives in PR #3152 (will rebase once this lands).
Verified: `pnpm exec vitest run src/ui` -> 276 passed (17 files, +15 new); `pnpm exec tsc -b tsconfig.references.json` -> clean.