Landmarks

A navigation surface for the box: a hand-curated, short list of widgets pointing at the most-used spots. Each landmark is a card living in the directory it represents.

The problem

_content/ and _bookkeeping/ accumulate directories at different levels of importance. Some are well-trod (recipes, todos, calendar); others are housekeeping (trash, usage). The Browse page shows everything with equal weight — a flat file tree, no editorial layer. There's no way to say "these few directories are the main pathways into the system; the rest are just files." Landmarks are that editorial layer.

What a Landmark is

A landmark is a single card inside a directory that says "this directory is a notable spot." It's a bookmark, not a museum plaque. Most appearances are tiles in a list, and the iconic form (symbol + label) is the entire content most of the time — landmarks earn their compactness by being seen many times.

Key properties:

Distinct from briefing

briefing.briefing.card already exists for per-directory context aimed at agents. Landmarks are aimed at humans navigating the UI. They occupy adjacent but distinct roles:

AudiencePurpose
briefingagentscontext every agent needs to know about this spot
landmarkhumans"here's a bookmark to this spot, with a few pinned items"

Both can coexist in the same directory.

Card schema

A landmark is pure YAML frontmatter (no body) with one or more roles. The navigation role carries the bookmark fields; each destinations entry carries category rules and a handler procedure (its for list names the kinds it accepts, e.g. triage). A landmark can carry one or both; everything below describes the navigation role. See docs/triage.md for the destination role.

---
navigation:
  label: Recipes
  symbol: 🍳
  links:
    - { ref: /_content/recipes/Bread.recipe.card, label: the bread }
    - { ref: /_content/recipes/techniques/Knife_Skills.doc.card }
  expand:
    - query: "*.recipe.card"
      order: modified-desc
      template-ref: "${path}"
      template-label: "${title}"
---

Fields

All of these live under navigation.

label (one) — short bookmark name. Displayed prominently on the tile. Not a sentence; treat it like a tab name.

symbol (one) — the iconic mark. Two forms:

symbol: 🍳                          # emoji or short text
symbol: { src: /_content/recipes/images/portrait.webp }   # image

For character-driven scenarios where the face is the bookmark, the image form makes the Landmarks page look like a real launcher rather than an emoji grid. Image src is a box path — write it with a leading /, from the box root (a path relative to the landmark's directory still resolves). It is validated: a src pointing at nothing is a broken-ref warning at bbx validate. The symbol carries most of the "iconic and unique expression" weight — pick well.

links (zero or more { ref, label? }) — pinned references to other cards. ref is a box path to the target — leading /, from the box root (a path relative to the landmark's directory still resolves). It's validated like any other ref — it must point at a real file. Optional label is a per-landmark contextual label — call this card "the bread" here even if its real title is "Bread Basics." When omitted, the renderer falls back to the target's own title.

expand (zero or more) — templated fan-out. Runs a query, applies a template per match, generates links. See below.

Why no description / purpose / intent

Earlier sketches included prose fields. Removed: a bookmark seen hundreds of times shouldn't carry a paragraph explaining itself. If a landmark genuinely needs a written rationale, write a doc card and add it to links as the first reference. That keeps the schema honest about its job.

The expand entry

A landmark like Recipes naturally wants to surface "all recipe cards in this directory" without listing them by hand. expand is the editorial way to opt into that.

Query

query is a glob pattern, matching bbx ls conventions (*.recipe.card, **/*.doc.card, etc.). Resolved relative to the landmark's directory.

Template

template-ref / template-label are placeholder strings applied to each matched card. When template-ref is omitted, each match links by its box path (the canonical leading-/ form, resolved as a literal path).

${...} placeholders interpolate at expand time:

template-label derives the per-match label the same way (template-label: "${title}").

The ${} syntax differs from bbx ls's {...} deliberately — it avoids interpolating literal braces that appear in card body text.

Order

Optional order on an expand entry:

The enum can grow without breaking existing cards.

Group (collapsible submenu)

An expand carrying a group: <title> keeps its matches grouped under that title instead of flattening them into the flat link list. Collapsed, the group shows its title and a child count (e.g. Images · 134); expanded, it reveals the matched links. Use it for broad "all the X" globs (e.g. group: Images over **/*.image.card) that would otherwise flood the flat grid. An expand without group flattens inline as before.

Both surfaces — the Landmarks page grid and the chat-header landmark menu — render a group as a collapsed-by-default disclosure; on the header menu the children open in the companion sidebar just like flat links. Group children resolve server-side, capped at 50 (the collapsed count stays exact); a larger group renders its first 50 with a "+N more" note.

Dedup

A card appearing both in a hand-listed links entry and in an unnamed expand result shows once: hand-listed links come first and win. This lets a landmark hoist a few items to the top with custom labels and let the rest fill in via expand below, without doubling. Named group expands are independent — they dedup within themselves only, not against the flat list or each other.

Derived links

A landmark's flat list is assembled in tiers (src/core/landmark/resolve.ts, derived-links.ts):

  1. hand-listed links: (first, and winning dedup, with their labels);
  2. derived entry-point cards, then derived primary cards, from the landmark's pruned subtree: its directory and every descendant directory that has no landmark of its own, never entering an owned .attach/ scope unless that scope holds its own landmark;
  3. nested landmarks, one entry each (label, symbol), except those written prominence: background;
  4. unnamed expand results.

Within a derived tier the order is by box path. A card's level is its written prominence, else its type's default (defaultProminence on the schema: background for category: "system" types and for landmarks, ordinary otherwise). The walk is fresh on every call and parses are cached per file by identity (prominence-index.ts, prominence-cache.ts), the same shape as card-cache.ts; landmarks.identity serves the mount-path callers (place pill, tab title) with no resolution at all, and landmarks.forDir resolves on demand.

A landmark written prominence: background is a housekeeping place: off the Landmarks page and the switch menu (cascade.ts, isListedLandmark), and every card under it is background for folding and derivation, whatever the card says. bbx validate warns on entry-point/primary written on a landmark, on a prominent card under a background place, and on too many entry points or primary cards in one directory (lint-prominence.ts).

Existing boxes were migrated by landmark-links-prominence: every in-subtree links: target got prominence: primary; no link was removed.

Rendering

Landmarks have three rendering surfaces: the Landmarks page (the full picture), and the app bar's two menus (the compact, always-reachable forms). A fourth surface is the browser tab, below.

Landmarks page — the merged activity surface

/<box>/landmarks is one section per landmark, each carrying both halves of the landmark's activity: its chats and its links. It absorbed the former Chats page (/chats redirects here) — the two were the same landmark-keyed page projected twice (docs/implemented-plans/top-nav-ia.md Track D). Implementation: LandmarksList + LandmarkSection + LandmarkSessions in src/frontend/src/components/landmarks/.

A section renders:

There is no landmark full form and no click-through to one: a tile links straight to its target card. The caps above are inline disclosures for exactly that reason — the section already is the landmark's full picture. (A tile/full-form renderer pair was designed early on and never built; the design is dropped, not deferred.)

Ordering comes from chat.byLandmark (latest session activity first, then chat-less landmarks with the box root ahead of alphabetical), and the page does not re-sort — so the page and the app bar's switch menu agree.

Two things render outside the per-landmark sections:

view: landmarks cards render this same component, sessions included.

App bar — switch menu and here menu

The unified app bar (docs/implemented-plans/top-nav-ia.md) is the compact surface:

The browser tab, and everywhere else a box is drawn

A landmark's symbol for the directory you are in leads the tab title (frontend/src/components/DocumentPlace.tsx), while the tab icon is the box's own mark. Splitting them that way lets a tab say both which box it belongs to and which place inside it you are looking at; an icon that followed the landmark instead meant two boxes' tabs could wear the same mark.

The box root's landmark is the box's own identity. Its label is the box's display name in every /api/boxes listing — the box switcher, the dashboard header, and the <page> — <box> tab title — and its symbol is the box's mark — drawn on the tab, on the box selector, on a notification, and on the installed app's icon. The box server stamps it into the served document so a tab is identifiable before the app boots, and renders it as a PNG for the surfaces that require one (core/landmark/box-identity.ts, webapp/index-html.ts, webapp/routes/box-identity-assets.ts). A box had no display name before this; it answered with its slug. Renaming a box is editing that card, which is why there is no box-name setting anywhere.

Implementation outline

ComponentLocation
Schemasrc/schemas/landmark.ts
Schema registrationsrc/schemas/registry.ts
Expand evaluatorsrc/core/landmark/ (resolves queries, applies templates, dedups, orders)
Merged activity surfacesrc/frontend/src/components/landmarks/ (LandmarksList, LandmarkSection, LandmarkSessions)
Landmarks pagesrc/frontend/src/pages/landmarks/LandmarksPage.tsx
Chat buckets per landmarkchat.byLandmark (src/webapp/trpc/routers/chat.ts)
App-bar switch / here menussrc/frontend/src/components/PlacePill.tsx + the bar's chrome slots
API endpointtRPC procedure under src/webapp/trpc/routers/ (lists landmark cards + resolves expands server-side)
Doctest coveragetest/core/landmark/landmark-schema.doctest.md (schema validation, expand semantics, dedup, order)

The expand evaluator runs server-side at fetch time so the wire response is a fully-resolved list of links (no client-side glob or field lookup).

Open questions