Bee Box documentation · directory: https://beebox.run/docs/concepts/ · index: https://beebox.run/docs/concepts/index.md · root: https://beebox.run/llms.txt
# 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:
- **Singular per directory.** One `*.landmark.card` per opted-in directory. Directories without one are invisible to the navigation surface — that's the point.
- **Thing-first, container-secondary.** The card itself is the widget. It can point at nearby cards, but it doesn't *contain* them — it references them.
- **Evergreen.** Content describes what the spot is and what's notable, long-term. Not "this week's top three." Permanence is implied by the metaphor.
- **Derived first, curated for the rest.** The list is mostly the cards under the directory that carry `prominence: entry-point` or `prominence: primary` (see "Derived links" below); `links:` is the curated exception for what a card cannot say about itself, and an `expand` entry is templated fan-out for "list everything matching X". Ordering is by tier, then by name; a fixed order is what `links:` is for.
- **A place marker, not a visitable file.** The landmark card is `background` by type: Browse folds it and draws the directory's identity from it. The place's entry point, if it has one, is a visitable card inside the directory. See `docs/implemented-plans/card-prominence.md`.
### 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:
| | Audience | Purpose |
|---|---|---|
| `briefing` | agents | context every agent needs to know about this spot |
| `landmark` | humans | "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.
```yaml
---
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:
```yaml
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:
- **`${path}`** — special-cased; resolves to the file path of the matched card, relative to the landmark's directory.
- **`${field}`** for any other name — reads that field from the matched card's **frontmatter**. So `${title}` grabs the card's `title:` field; dotted paths like `${exif.camera}` walk nested mappings. Missing or non-scalar values render as the empty string.
`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:
- `alphabetical` — by file path. Default; matches `bbx ls`.
- `modified-desc` — most-recently-edited first.
- `modified-asc` — oldest first.
The enum can grow without breaking existing cards.
### Group (collapsible submenu)
An `expand` carrying a `group:
` 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
`//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:
- **Header** — symbol + label, and the directory path as a link into Browse.
- **Chats** — the landmark's sessions as rows, an older-sessions disclosure
(`olderSessions` from `chat.byLandmark`), and a "New chat" action bound to
the landmark's directory.
- **Links** — resolved `links` + flat `expand` results as tiles, capped at
the first 6 with an inline "Show all N" disclosure. Grouped expands
(`group:`) stay collapsed count-chips beside them. A ref pointing at
nothing renders as "Missing" rather than vanishing.
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:
- **Other chats** — a trailing bucket for sessions bound to a directory with
no landmark card (the box root without a root landmark, or a
deleted-landmark directory). These used to be silently dropped.
- **Parse warnings** — a row naming any `**/*.landmark.card` whose
frontmatter failed to parse (`problems`, carried by both `landmarks.list`
and `chat.byLandmark`). A hand-edit that breaks a landmark must not make
an activity quietly disappear.
`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:
- **Switch menu** (the place pill's left half) lists every landmark as a row
— symbol, label, and its fresh-chat count — plus `All landmarks →` to the
page above. Tapping a row resumes the landmark's most recent chat or
starts one in its directory. The menu lists landmarks only; the "Other
chats" bucket is reachable through the page. Parse problems surface here
too. Data is fetched lazily on first open.
- **Here menu** (the place pill's right half) is the current directory's
landmark: Open `/`, its pinned links at root level (never in a
sub-panel), grouped expands as disclosures, and — on chat pages — Recent
files. Links open in the companion pane on chat, and navigate normally
elsewhere.
### 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 ` — ` 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
| Component | Location |
|---|---|
| Schema | `src/schemas/landmark.ts` |
| Schema registration | `src/schemas/registry.ts` |
| Expand evaluator | `src/core/landmark/` (resolves queries, applies templates, dedups, orders) |
| Merged activity surface | `src/frontend/src/components/landmarks/` (`LandmarksList`, `LandmarkSection`, `LandmarkSessions`) |
| Landmarks page | `src/frontend/src/pages/landmarks/LandmarksPage.tsx` |
| Chat buckets per landmark | `chat.byLandmark` (`src/webapp/trpc/routers/chat.ts`) |
| App-bar switch / here menus | `src/frontend/src/components/PlacePill.tsx` + the bar's chrome slots |
| API endpoint | tRPC procedure under `src/webapp/trpc/routers/` (lists landmark cards + resolves expands server-side) |
| Doctest coverage | `test/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
- **Order options beyond the v1 three.** By-attribute (`order="attr:priority"`) and by-XPath-value (`order="xpath:/yield/@amount"`) are obvious extensions if needed.
- **`` extensions.** Image variant and color/mood styling are deferred until there's a real case for them. The element shape leaves room.
- **Live fields.** `` or similar live-data slots are explicitly out of scope. The schema can absorb them later as new optional children without breaking existing cards.