Platform UI โ the shell's menus as data
A menu entry in the MeshWeaver top bar can be mesh data: a UiContribution node naming a layout
area, contributed by the package that owns it (design MeshWeaver#1645). Install the package and the
entry appears; remove it and the entry goes. No platform build either way.
This pack ships the other half โ the menus those entries land in.
Two halves, and only one of them is decentral
| Lives where | Why | |
|---|---|---|
| The entry | in the contributing package's own partition (Edu/CoursesMenu, Approvals/ApprovalsMenu) |
placement IS the targeting: the entry arrives and leaves with its package |
| The menu | here, PlatformUI/AppsMenu |
one dropdown, contributed once; a package must not declare the shell's furniture |
A UiContribution with context: "TopBar" declares a dropdown, and its area is the new
menu's context key. Entries join by naming that key as their own context. So PlatformUI/AppsMenu
declares Apps, and the six package front doors below set "context": "Apps".
The dropdown is hidden while it has no visible entries, so a mesh carrying none of those packages sees no extra button.
๐จ The failure mode is SILENCE โ read this before adding an entry
An entry whose context nobody declares renders nowhere, and nothing says so. The node is valid,
it imports, validate-repos.py is green, it is indexed and searchable, and the compiled aggregator
even builds a stream for its context โ but no dropdown subscribes that stream. There is no error, no
warning, and not even the area-not-found placeholder a dangling area link would give you.
Measured on 2026-09-02, six entries had been shipping exactly that way against a context named
Admin that has never existed in either repo:
| entry | shipped | opens |
|---|---|---|
Providers/ProvidersMenu ยท Providers/ProvidersSetupMenu |
2026-08-24 | the AI provider overview and setup |
Approvals/ApprovalsMenu ยท Feedback/FeedbackMenu ยท Edu/CoursesMenu ยท Training/ToursMenu |
2026-08-28 | the approval desk, the feedback inbox, the course catalog, the tour player |
Every one of them was reviewed, merged, published and installed. The bug is invisible to every gate that existed, and to a person reading the diff, because the entry is correct โ it is the menu that is missing.
scripts/check-menu-contexts.py now fails a PR that adds a seventh; run against the pre-fix
tree it names all six, and its --self-test drives every rule from both sides.
Adding a front door to your package
Ship a
UiContributionnode in your own partition, not here:// {Package}/{Name}Menu.json โ nodeType "UiContribution" "content": { "$type": "UiContribution", "context": "Apps", // the menu key PlatformUI declares "label": "Approvals", "labelKey": "home.apps", // ONLY a key the platform catalog carries "icon": "โ ", "area": "Approvals", // an area YOUR package already ships "href": "/Approvals/Workspace/area/Approvals", "order": 42, "requiredPermission": "Read" }Run
python3 scripts/check-menu-contexts.py. It also catches the three other ways a contribution goes quiet: an emptyarea(the projection drops it before any gate runs), an externalhref(rejected as not portal-internal, so the entry silently opens the derived area URL instead of the page you wrote), and alabelKeythat is not in the platform catalog.That last one bit all six of the entries above.
NodeMenuItemDefinition.Localizedprefers the key over the literal, andLocalizationCatalogreturns the key itself when it is missing โ so an unregistered key does not fall back to your English label, it rendersmenu.coursesin the menu. Five of the six carried a key that has never existed; they now ship the literal until the keys land in the platform'sstrings.en.json/strings.de.json(Systemorph/MeshWeaver#3055).hrefmay carry the one token{node}โ the path of the node whose menu is being rendered, URL-escaped. That is how a node-anchored surface served from your OWN workspace names the current node (/Approvals/Workspace/RequestApproval?doc={node}).
Who enforces visibility โ never the data
The compiled aggregator, against the viewer's live effective permissions and a closed gate
vocabulary (UiContributionGates: nodeTypes, excludePartitionRoot, adminOnly). A contribution
can only ever NARROW its own visibility; it cannot widen anything, and a rule the vocabulary cannot
express stays in code. That is the whole reason menu entries may be data at all, where
MenuPresentationOverlay is deliberately cosmetic-only.
So an admin-only entry declares gates.adminOnly on itself. Gating the whole Apps menu would
also hide the course catalog and the tour player, which are for every viewer.
What is NOT here yet
The design's larger cargo โ the default node-menu table (Edit / Pin / Move / Copy / Delete / Files /
Data / Versions / StopSync / Recycle), the mesh menu, and the remaining compiled settings tabs โ
cannot move until two gate words exist in core's UiContributionGates (syncedOnly,
excludeViewerHome) and the compiled minimal fallback lands. Both are MeshWeaver.Graph changes;
see Systemorph/MeshWeaver.Plugins#960.