Skip to content

DESIGN WS7: menus and standard views as plugin content — the UiContribution mechanism #1645

Closed

Description

@rbuergi

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 (INodeMenuProvider DI + AddNodeMenuItems delegates per context) with the defaults compiled in DefaultNodeMenuProvider/DefaultMeshMenuProvider; presentation is already editable data (MenuPresentation catalog) but deliberately cosmetic/subtractive only.
  • Settings tabs: GlobalSettingsMenuItemProvider collections; ~20 compiled Add*SettingsTab calls in the portal composition (the ContentIndex tab already rides its module).
  • Top-bar AI menu / side panel: compiled AddNodeMenuItems(AiMenuContext…) / ("SidePanel"…).
  • Home tabs: HomeTab NODES — already pure data; the precedent this design generalizes.
  • Per-NodeType menu items: plugins already have FULL power via in-mesh AddNodeMenuItems delegates 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-aware Matches semantics), 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)

  1. Mechanism: UiContribution type + gated aggregation + generation bump — additive, nothing moves.
  2. Settings tabs first: lowest risk, biggest decongestion (~20 portal calls become PlatformUI content).
  3. AI menu + side panel.
  4. Node/mesh menu defaults LAST, once the fallback is proven.

Risks

  • Gate-vocabulary creep — keep it CLOSED; code remains the escape hatch.
  • i18n: Title must 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

  1. rbuergi commented on Aug 15, 2026

    @rbuergi
    ContributorAuthor

    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 UiContributionCatalog singleton holding ONE live nodeType:UiContribution query subscription per silo (the DeckSlidesCache sharing 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: SyncedOnly is 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 as Permission.None and gets nothing), AdminOnly (reactive IsGlobalAdmin), ExcludePartitionRoot (the shared PartitionRootDeletionGuard predicate), NodeTypes (suffix-aware Matches semantics). The projection is a PURE internal class (UiContributionProjection) so the security-relevant logic is unit-tested the way MenuPresentationOverlay is — 9 pins landing with the PR.

  2. rbuergi commented on Aug 21, 2026

    @rbuergi
    ContributorAuthor

    Verdict: PARTLY DONE — mechanism + migration steps 1–3 shipped; step 4 remains

    Verified against origin/main.

    Shipped

    design item state evidence
    UiContribution node type ✅ and wider than designed src/MeshWeaver.Graph/Configuration/UiContributionNodeType.cs. Contexts Node/Mesh/Settings/AiMenu/SidePanel plus TopBar — a contribution there declares a whole NEW top-bar dropdown whose Area is the context key its entries target (#1652). Fields: Title/LabelKey, Icon, Area, Order, Group/GroupKey/GroupIcon, RequiredPermission, Gates.
    RequiredPermission enforced against the LIVE permission stream ✅ defaults to Permission.Read; "contributions can never demand less than Read", checked in the compiled aggregator, never trusted from data.
    Step 2 — settings tabs ✅ 9 tabs ride the lane (memex/Memex.Portal.Shared/Settings/PlatformSettingsTabAreas.cs): What's New, About, Invitations, Inbox, Updates, Published to the web, Token Usage, Privacy, … Seven compiled Add*SettingsTab calls remain in MemexConfiguration.cs:945–989, down from the ~20 the inventory counted.
    Step 3 — AI menu + side panel ✅ 7 contributions in memex/Memex.Portal.Shared/AiMenuContributions.cs (Threads, Models, Tiers, Providers, Agents, Skills, …).

    Superseded — and by something better

    The design specified "ONE nodeType:UiContribution snapshot query per hub build" plus an Admin/UiGeneration counter bumped on install/update/uninstall, with hubs lazily recycling on mismatch. That counter does not exist, and should not: UiContributionCatalog is a mesh-wide singleton holding ONE shared, replayed live query (GraphConfigurationExtensions.cs:90 → AddSingleton<UiContributionCatalog>; the query is nodeType: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 preInstalled PlatformUI pack. Not started, and the design put it last deliberately ("once the fallback is proven"):

    • There is no PlatformUI pack — no hit for PlatformUI in src, memex, content, or MeshWeaver.Plugins.
    • The default table is still compiled, as AddNodeMenuItems delegates rather than the DefaultNodeMenuProvider/DefaultMeshMenuProvider classes 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, …
    • The minimal fallback (Data + Versions + Delete — enough to operate a zero-plugin mesh) does not exist.

    Two of the five node-shape gates are missing, and step 4 is what needs them. UiContributionGates today carries NodeTypes (suffix-aware), ExcludePartitionRoot and AdminOnly; the design also named syncedOnly and excludeViewerHome. 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.

  3. rbuergi commented on Aug 22, 2026

    @rbuergi
    ContributorAuthor

    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.

  4. rbuergi commented on Aug 25, 2026

    @rbuergi
    ContributorAuthor

    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 decision

    Two 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*SettingsTab calls remain in MemexConfiguration.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 the User type's own HubConfiguration:
    AddGlobalAdminSettingsTab, AddPartitionSyncSettingsTab, AddUserPreferencesSettingsTab.

    The eight that DID move (memex/Memex.Portal.Shared/Settings/PlatformSettingsTabAreas.cs) share one property: their visibility is AdminOnly or ungated, which the closed vocabulary already expresses. The thirteen above mostly do not — AddGitHubSyncSettingsTab is self-filtered to Space nodes, the three on UserNodeType are 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

    Sequencing decided (maintainer, 2026-08-22): #1580 (import) goes first.

    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

    • A preInstalled PlatformUI pack to carry the default item set. Still no PlatformUI anywhere in src, memex, content, or MeshWeaver.Plugins — and since it is a pack, this is a cross-repo change.
    • Two gate-vocabulary additions the compiled defaults need and UiContributionGates does not have: syncedOnly (StopSync appears only on synced nodes) and excludeViewerHome (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.
    • The minimal compiled fallback (Data + Versions + Delete). Note the design text names DefaultNodeMenuProvider / DefaultMeshMenuProvider; neither type exists. The defaults are AddNodeMenuItems delegates 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/UiGeneration bump: superseded, not skipped

    Recording this against the design text so nobody re-adds it. UiContributionCatalog (src/MeshWeaver.Graph/Configuration/UiContributionCatalog.cs:16-22, registered at GraphConfigurationExtensions.cs:90) is a mesh-scoped singleton holding ONE shared live nodeType:UiContribution query 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.

  5. rbuergi commented on Aug 30, 2026

    @rbuergi
    ContributorAuthor

    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.

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions