The app concept: what a reader sees, and where the index lives
"systematically (default) would appreciate side menu rather than bottom nodes… we prefer index on side… and yes, there will be exceptions… try to work through app concept in general." — maintainer, 2026-09-17
Status 2026-10-11 — the missing rail is closed. Maintainer: "the default layout should NOT contain this kind of view of children at the bottom. we should use the more explorer-like navigation … combine this with mesh search … folder-like nav … don't eager load the whole thing … load max 50 items … and load more."
- Every node page has the left-hand index. Core wraps the typed default Overview with it (
MarkdownOverviewLayoutArea.WithIndexRail), and Space pages take it too (SpaceLayoutAreas).- It is a lazy explorer. It loads only the levels on the reader's path, at most 50 entries per level with a Load more… entry, and the reader drills in by opening a folder.
- It carries a search box.
NavMenuSkin.SearchNamespace, rendered byNavMenuView, opens the mesh search scoped to the space.- No bottom catalog. The Space page no longer appends a "Contents" catalog.
- The home is one flat list in location order. It no longer fans out per type.
The sections below are the analysis that led here. The app-root resolution (index root = nearest app root, not always the first path segment) is still open.
The immediate change (the index moves to the side rail, the bottom catalog goes) is simple. Applying it exposed something larger: an app has no index at all, and the contents catalogs scattered through this repo's plugin pages are the workaround. This page states the concept, names where the current surfaces disagree, and recommends the one rule that would make them agree.
What an app IS today
An app is a tile whose App.Plugin names the path of a node — "usually the Store plugin
cover (e.g. Chess, LinkedIn)". The record lives at {user}/_App/{appId} as an ordinary mesh
node; the grid is a single-partition query over {owner}/_App, and the tile's name and icon resolve
from the app's root node. Default apps are materialized write-behind from
Admin/HomeConfig.DefaultApps on home render.
So: an app is a node you open. There is no "app shell", no app-level container type, and nothing that says "these pages belong to this app" beyond the path prefix.
Three navigation models, and where they stop
| Surface | Who supplies it | What it answers |
|---|---|---|
| The launcher — home Apps grid, icon tiles grouped by category | InstalledApp records + Admin/HomeConfig.DefaultApps |
Which apps do I have? |
| A markdown tree — the left-hand rail | DefaultNodeNavigation, rooted at the page's SPACE |
Where am I in this document? |
Inside a typed app — a Store/Plugin root, a mailbox, a CRM client |
nothing | — |
The third row is the gap, and it is not cosmetic:
MarkdownOverviewLayoutAreawraps its page with the rail.MeshNodeLayoutAreas.AddDefaultLayoutAreas()— what every typed NodeType uses — contains no reference to the rail at all.App.Pluginpoints at aStore/Plugincover. So the node an app opens onto is exactly the kind of node that renders no index. A reader arrives from the launcher and the app has no visible structure.- The escape hatch is
INodeNavigationProvider, which core prefers over its default. Exactly one module uses it — Education, for course pages — and its doc comment explains why: core must not learn what a lesson is. That reasoning is right, and it is an argument for a seam, not for core having no default for typed nodes.
The measured consequence. 78 node bodies in this repo end in a contents catalog. 54 of them are
Store/Plugin roots — app front pages — and their catalog is the only child navigation they have.
Removing it would leave those apps with no way in. That is why the 2026-09-17 sweep touched only the
24 Markdown bodies (21 changed, 3 kept as genuine registers) and left the plugin roots alone.
The catalogs are a symptom; the missing rail is the defect.
Where the conventions disagree
- Two orderings. The rail orders by
MeshNode.Order, then name, recursively. The launcher orders byApp.Orderwith0meaning "never placed" (painting behind explicitly ordered tiles, most-recently-used first) and groups by category. A reader moving from grid to rail meets two different ideas of sequence. - Two ideas of "the root".
DefaultNodeNavigation.IndexRootis the first path segment — deliberately the space, so one index serves the whole tree. But an app installed into a user home lives at{user}/…, so its index root resolves to the user's home, not the app. An app's index would show the home's tree with the app as one entry inside it. - Embeds get no rail, correctly —
showHeader=falsesuppresses it and providers are not even asked. Worth keeping, and worth stating: an@@embed is content, never a page. - The user home is the one typed page that solved this, by embedding
@@("area/Catalog")— the Apps grid. That is the correct exception (the tiles are the content), and it is also evidence that the typed-page answer has so far been "hand-roll a region in the body".
The recommendation
One rule: the index is a property of the NODE, not of the markdown renderer.
- Give typed nodes the same default rail. Have
AddDefaultLayoutAreas()wrapOverviewwith the navigationMarkdownOverviewLayoutAreaalready builds — the sameSuppliedNavigationRail, the same supplied-then-default precedence, the same two guards (emit at once; a faulted query renders no index rather than holding the page). AStore/Plugin, a mailbox and a CRM client then each get an index for free. - Resolve the index root to the nearest APP ROOT, not the first path segment. An app root is a
node that owns a partition, or one an
InstalledApprecord points at.Chesskeeps today's behaviour; a course installed at{user}/AgenticEngineeringstarts its index at the course instead of the reader's whole home. This is the change that makes rule 1 useful rather than noisy. - Keep
INodeNavigationProvideras the override, unchanged. A module that knows its own semantics (a course lists the whole course, not the branch you stand in) still wins over the default. The default exists so that not implementing the seam is no longer the same as having no navigation. - Then retire the 54 catalogs in plugin bodies, leaving the genuine registers. Not before: the sweep is only safe once the rail is there, which is the ordering this page exists to record.
The author-facing rule, once that lands: would a reader look for this in the side rail? Then it
belongs in the rail. Embed a catalog only where the listing IS the content — a log, a register, a
store page, or a filtered view the rail cannot express (?groupBy=, ?subtree=true). Give it a
heading that says what it is (## Correspondence, ## Releases), never a bare ## Contents.
The exceptions, declared: what a Space page can turn off
One exception is DECLARED on the node — MeshNode.ExcludeFromContext, core's
MeshNodeVisibility.IsExcludedFromContext, the same mechanism a node already uses to stay out of
search or a create menu:
| Declaration | What the Space page drops | Read by |
|---|---|---|
ExcludeFromContext: ["header"] (MeshContexts.Header) |
the logo tile, the click-to-edit title, the description, the divider | SpaceLayoutAreas.BuildSpaceView |
The header additionally honours ?showHeader=false on the area reference — what an @@ embed and
the landing-page route (PublicPagePresentation) put there — because an embed is content, never a
page.
Author it wherever the node is authored. In a node JSON, "excludeFromContext": ["header"]; in
markdown front matter, ExcludeFromContext: [header]; in code, node.HideFrom(MeshContexts.Header).
The value is a lower-case wire identifier and is never translated.
🚨 Per INSTANCE only — a declaration on the NodeType does nothing here.
MeshNodeVisibility.IsExcludedFromContext reads the node's own ExcludeFromContext (plus the dotfile
rule for search); the TYPE-level exclusion is the query layer's, from
MeshConfiguration.GetExcludedNodeTypes, which a layout area never consults.
Retired: the contents opt-out
The Space page no longer appends a catalog, so there is nothing to opt out of. The switch
ExcludeFromContext: ["contents"], the body scan that looked for an existing catalog embed, and the
method that combined the two are removed with it. SpaceLayoutAreas.ContentsContext remains as a
constant so a node that still declares the value keeps loading; the declaration has no effect and
can be deleted from a node whenever it is next edited. An HTML comment carrying area/Search,
which some pages used to satisfy the old scan, is likewise inert and can go.
A page that wants a listing in its body embeds one explicitly, under a heading that says what it is.
Owed to the platform (core): SpaceNodeType.WelcomeMarkdown still ends with ## Contents and
an @@("area/Search") embed, which restates the rail on every new Space; it is removed in a core
change after the rail is released.
Follow-ups
| # | Item | Status |
|---|---|---|
| 1 | Typed nodes render the default rail | Done in core (MarkdownOverviewLayoutArea.WithIndexRail wraps the typed default Overview, MeshWeaver#6472). It reaches an instance with the platform release that contains it; until an instance runs that release its typed pages have no rail |
| 2 | IndexRoot → nearest app/partition root |
Changes the index of every nested page; wants tests over the installed-course and user-home shapes before it ships |
| 3 | Sweep the 54 Store/Plugin catalogs |
No longer blocked by design. Do it once item 1 is released to the instances the packages ship to — removing a catalog on an instance whose typed pages still have no rail strips that app's only navigation |
| 4 | Reconcile the two orderings (rail Order+name vs launcher App.Order+category) |
Design question: does an app declare its own section order, and does the rail honour categories? |
What changed on 2026-09-17
src/MeshWeaver.AI/Data/Skill/space.md§4 rewritten: the rail is the index; the exception test is stated; the typed-node caveat is explicit.src/MeshWeaver.AI/Data/Skill/markdown.mdandsrc/MeshWeaver.AI/Data/Agent/Assistant.md: the same rule, so the default agents stop teaching the old one.- 21
Markdownnode bodies lost their trailing catalog;Governance/Standards,Governance/ActivitiesandPublish/Deckskept theirs (the listing is the content). - Core: the Space welcome template no longer ends in
@@("area/Search")(MeshWeaver#4586) — 🚨 and that was REVERTED the same day by core69c23c62, "until the Plugins half can land", so the template embeds it again on today's core. The line above stood as fact here for four days; read the pin, never this page, for what the template currently says.