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
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.
Follow-ups
| # | Item | Why it is not done here |
|---|---|---|
| 1 | Typed nodes render the default rail (AddDefaultLayoutAreas) |
Core change touching every typed page in the fleet; needs visual verification, which this repo has no local path for (the portal ships in the image) |
| 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 |
Blocked on 1 — removing them today strips an 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).