Repository navigation
DESIGN WS7: menus and standard views as plugin content — the UiContribution mechanism #1645
Description
The composition-first end state ("deployment = base runtime + plugin composition; ALL menus + ALL standard views become content of a pre-installed platform-UI plugin"). Wave 2–4 modularized the compiled packs; this designs the CONTENT lane for the UI shell itself.
Inventory (measured on current main)
- Node/mesh menus: a reactive provider pipeline (
INodeMenuProviderDI +AddNodeMenuItemsdelegates per context) with the defaults compiled inDefaultNodeMenuProvider/DefaultMeshMenuProvider; presentation is already editable data (MenuPresentationcatalog) but deliberately cosmetic/subtractive only. - Settings tabs:
GlobalSettingsMenuItemProvidercollections; ~20 compiledAdd*SettingsTabcalls in the portal composition (the ContentIndex tab already rides its module). - Top-bar AI menu / side panel: compiled
AddNodeMenuItems(AiMenuContext…)/("SidePanel"…). - Home tabs:
HomeTabNODES — already pure data; the precedent this design generalizes. - Per-NodeType menu items: plugins already have FULL power via in-mesh
AddNodeMenuItemsdelegates in their NodeTypes' configuration lambdas (plugin-gate CI'd). Not a gap. - The gaps: plugins cannot contribute (a) every-node menu items, (b) settings tabs, (c) shell menus — and the defaults are compiled.
The crux: the security boundary
MenuPresentationOverlay is cosmetic-only for a documented reason: permission filtering happens by providers not emitting an item, so an item conjured from data would carry no permission gate. The mechanism therefore moves enforcement into the compiled aggregator with a CLOSED declarative gate vocabulary:
RequiredPermission— enforced by the aggregator against the live permission stream (per-viewer reactive, exactly like today);- node-shape gates:
nodeTypes(suffix-awareMatchessemantics),syncedOnly,excludePartitionRoot,excludeViewerHome,adminOnly(IsGlobalAdmin).
Anything beyond the vocabulary stays in code (in-mesh per-NodeType delegates, or compiled providers). A contribution can only ever NARROW visibility relative to what the aggregator enforces — never widen it.
The contribution node
nodeType UiContribution (lives in the contributing plugin's partition):
{ Context: Node|Mesh|Settings|AiMenu|SidePanel, Title (localizable per Systemorph/MeshWeaver.Plugins#978's translations model), Icon, Area, Order, Group, RequiredPermission, Gates{...} }
A contribution only adds a MENU ENTRY pointing at a layout area the plugin already ships — rendering stays the existing layout pipeline; no new render surface.
Read-at-hub-build + generation bump
Aggregators run ONE nodeType:UiContribution snapshot query per hub build (never N live streams per hub — cost control; per-viewer permission reactivity is separate and stays live). An Admin/UiGeneration counter bumps on plugin install/update/uninstall (the installer knows); hubs lazily recycle on generation mismatch — the existing recycle machinery.
The PlatformUI pack
A preInstalled plugin carries the DEFAULT item set (the Edit/Pin/Move/Copy/Delete/Files/Data/Versions/StopSync/Recycle table, the mesh menu, the standard settings tabs) as UiContribution nodes. The compiled defaults retire to a MINIMAL fallback (Data + Versions + Delete — enough to operate a zero-plugin mesh; the fallback-slot philosophy). Phase 2 moves standard AREAS into the pack's in-mesh source where feasible.
Migration order (each shippable)
- Mechanism:
UiContributiontype + gated aggregation + generation bump — additive, nothing moves. - Settings tabs first: lowest risk, biggest decongestion (~20 portal calls become PlatformUI content).
- AI menu + side panel.
- Node/mesh menu defaults LAST, once the fallback is proven.
Risks
- Gate-vocabulary creep — keep it CLOSED; code remains the escape hatch.
- i18n:
Titlemust ride the same translations model Systemorph/MeshWeaver.Plugins#978 defines for skills/agents — one localization story, not two. - A malicious/broken pack flooding menus — contributions are per-partition, so the existing install/grant model bounds who can contribute; the aggregator caps per-context contributions and logs the source partition per item.
- Query cost at hub build — one indexed snapshot query; measure on the batch-bake-scale meshes before step 4.
Per the standing WS7 directive this design leads; implementation starts with step 1 + the settings-tab slice.
Activity
rbuergi commentedon Aug 15, 2026
Implementation note (slice 1, in progress): the read-at-hub-build + generation-bump + hub-recycle mechanic is superseded by something simpler and strictly better — a mesh-scoped
UiContributionCatalogsingleton holding ONE livenodeType:UiContributionquery subscription per silo (theDeckSlidesCachesharing pattern), which every hub's menu aggregation maps. Initial/Reset carry the full set and Added/Updated/Removed fold as deltas, so a plugin installing or updating contributions changes every open menu reactively — no generation counter, no recycle machinery, no watchdog, and it can never go stale. Cost stays bounded: one query subscription per silo, not per hub.Also narrowed the gate vocabulary:
SyncedOnlyis dropped — there is no clean node-level predicate for it (even the compiled StopSync item gates on permission, not sync state). Shipping vocabulary:RequiredPermission(floored at Read, enforced against the viewer's live effective permission; anonymous arrives asPermission.Noneand gets nothing),AdminOnly(reactiveIsGlobalAdmin),ExcludePartitionRoot(the sharedPartitionRootDeletionGuardpredicate),NodeTypes(suffix-awareMatchessemantics). The projection is a PURE internal class (UiContributionProjection) so the security-relevant logic is unit-tested the wayMenuPresentationOverlayis — 9 pins landing with the PR.rbuergi commentedon Aug 21, 2026
Verdict: PARTLY DONE — mechanism + migration steps 1–3 shipped; step 4 remains
Verified against
origin/main.Shipped
UiContributionnode typesrc/MeshWeaver.Graph/Configuration/UiContributionNodeType.cs. ContextsNode/Mesh/Settings/AiMenu/SidePanelplusTopBar— a contribution there declares a whole NEW top-bar dropdown whoseAreais the context key its entries target (#1652). Fields: Title/LabelKey, Icon, Area, Order, Group/GroupKey/GroupIcon, RequiredPermission, Gates.RequiredPermissionenforced against the LIVE permission streamPermission.Read; "contributions can never demand less than Read", checked in the compiled aggregator, never trusted from data.memex/Memex.Portal.Shared/Settings/PlatformSettingsTabAreas.cs): What's New, About, Invitations, Inbox, Updates, Published to the web, Token Usage, Privacy, … Seven compiledAdd*SettingsTabcalls remain inMemexConfiguration.cs:945–989, down from the ~20 the inventory counted.memex/Memex.Portal.Shared/AiMenuContributions.cs(Threads, Models, Tiers, Providers, Agents, Skills, …).Superseded — and by something better
The design specified "ONE
nodeType:UiContributionsnapshot query per hub build" plus anAdmin/UiGenerationcounter bumped on install/update/uninstall, with hubs lazily recycling on mismatch. That counter does not exist, and should not:UiContributionCatalogis a mesh-wide singleton holding ONE shared, replayed live query (GraphConfigurationExtensions.cs:90→AddSingleton<UiContributionCatalog>; the query isnodeType:UiContribution, subscribed lazily on first read).That satisfies the design's actual cost constraint — "never N live streams per hub" — with one stream for the whole mesh rather than N snapshots, and it makes the generation bump unnecessary: contributions update live, so nothing has to be recycled to see them. Consider that sub-item closed by a better answer, not outstanding.
What remains
Step 4 — node/mesh menu DEFAULTS as content of a
preInstalledPlatformUI pack. Not started, and the design put it last deliberately ("once the fallback is proven"):PlatformUIinsrc,memex,content, or MeshWeaver.Plugins.AddNodeMenuItemsdelegates rather than theDefaultNodeMenuProvider/DefaultMeshMenuProviderclasses the inventory named (neither type exists today):MeshNodeLayoutAreas.cs:624(Edit),CopyLayoutArea.cs:30,MoveLayoutArea.cs:31,CreateLayoutArea.cs:45,ImportLayoutArea.cs:33,ExportLayoutArea.cs:31, …Two of the five node-shape gates are missing, and step 4 is what needs them.
UiContributionGatestoday carriesNodeTypes(suffix-aware),ExcludePartitionRootandAdminOnly; the design also namedsyncedOnlyandexcludeViewerHome. They are exactly the predicates the compiled defaults use — StopSync appears only on synced nodes, Edit/Move/Copy/Delete are suppressed on a viewer's home — so the default table cannot be expressed as data without them.I deliberately did not add the two gates now: unused vocabulary on a CLOSED vocabulary is the "gate-vocabulary creep" this design warns against, and they should land with the step-4 contributions that consume them, where their semantics can be asserted against the behaviour they replace.
Not superseded
Nothing in the
modules/lane, generation (#1954) or the Store package flow overtakes this. The endpoint lane (#1655, now closed) is the deliberately-different sibling: trusted compiled code, no closed vocabulary. This is still the DATA lane, and its remaining tranche is real work rather than a premise that dissolved.rbuergi commentedon Aug 22, 2026
Sequencing decided (maintainer, 2026-08-22): #1580 (import) goes first.
The reasoning is about cost, not importance: #1576 has just modularized export, so mirroring that pattern onto import is cheapest while it is fresh. This design then starts from a codebase where both content lanes already declare their own behaviour, rather than designing the UI-shell lane against a half-modularized base.
rbuergi commentedon Aug 25, 2026
Re-measured on
origin/main(ed45882) — step 4 is still the whole of what's left, and it is BLOCKED by this issue's own sequencing decisionTwo corrections to how this issue is being described elsewhere, both measured rather than assumed.
1. The "remaining compiled settings tabs" count is stale — and the remainder is not a tranche you can just move
The 2026-08-21 verdict said "Seven compiled
Add*SettingsTabcalls remain inMemexConfiguration.cs:945–989". Those line numbers no longer point at settings tabs at all, and the real remainder is thirteen, in two places:memex/Memex.Portal.Shared/MemexConfiguration.cs(per-node-hub composition, ~:1048–1099):AddApiTokensSettingsTab,AddInstancesSettingsTab,AddNotificationsSettingsTab,AddGitHubSyncSettingsTab,AddGitHubIssuesTab,AddCouponAdminSettingsTab,AddInstanceGrantAdminSettingsTab,AddCompositionAdminSettingsTab,AddWorkingTreeTab,AddGitHistoryTab.src/MeshWeaver.Graph/Configuration/UserNodeType.cs:134–136— on theUsertype's ownHubConfiguration:AddGlobalAdminSettingsTab,AddPartitionSyncSettingsTab,AddUserPreferencesSettingsTab.The eight that DID move (
memex/Memex.Portal.Shared/Settings/PlatformSettingsTabAreas.cs) share one property: their visibility isAdminOnlyor ungated, which the closed vocabulary already expresses. The thirteen above mostly do not —AddGitHubSyncSettingsTabis self-filtered to Space nodes, the three onUserNodeTypeare scoped by riding a specific type's hub configuration rather than by a gate. Moving them is a vocabulary question, not a mechanical one, which is the same wall step 4 hits.2. Step 4 is sequenced BEHIND #1580, by a decision recorded on this issue
That is still the live instruction, and #1580 has not shipped (see my comment there). So step 4 is not "next up but unstarted" — it is correctly waiting.
What step 4 still needs, unchanged from the 2026-08-21 verdict
preInstalledPlatformUI pack to carry the default item set. Still noPlatformUIanywhere insrc,memex,content, or MeshWeaver.Plugins — and since it is a pack, this is a cross-repo change.UiContributionGatesdoes not have:syncedOnly(StopSync appears only on synced nodes) andexcludeViewerHome(Edit/Move/Copy/Delete are suppressed on a viewer's own home). The earlier decision to land them WITH the contributions that consume them still looks right — unused vocabulary on a closed vocabulary is the creep this design warns about.DefaultNodeMenuProvider/DefaultMeshMenuProvider; neither type exists. The defaults areAddNodeMenuItemsdelegates scattered across the layout areas that own each operation (MeshNodeLayoutAreas.cs,CopyLayoutArea.cs,MoveLayoutArea.cs,CreateLayoutArea.cs,ImportLayoutArea.cs,ExportLayoutArea.cs), so "retire the two provider classes" is not the shape of the work — collecting a scattered set into a fallback is.The
Admin/UiGenerationbump: superseded, not skippedRecording this against the design text so nobody re-adds it.
UiContributionCatalog(src/MeshWeaver.Graph/Configuration/UiContributionCatalog.cs:16-22, registered atGraphConfigurationExtensions.cs:90) is a mesh-scoped singleton holding ONE shared livenodeType:UiContributionquery for the whole silo. It satisfies the design's actual cost constraint ("never N live streams per hub") with one stream rather than N snapshots, and it makes a generation counter unnecessary: contributions update reactively, so nothing has to be recycled to see them. The counter should not be built.No code change in this pass — reporting measured state only.
rbuergi commentedon Aug 30, 2026
Closed per the maintainer (2026-08-30): the UI-shell-as-content work belongs in MeshWeaver.Plugins now that the portal shell lives there. The measured residual (PlatformUI pack, two gate words, the thirteen compiled settings tabs, the fallback) is carried verbatim to https://github.com/Systemorph/MeshWeaver.Plugins/issues/960. Nothing else remains under this number in core.