---
title: App Tour
---

# App Tour

## What it is

The App Tour is Omniscio's built-in guided-onboarding system — full-screen overlays and spotlight tooltips that walk you through a feature step by step, highlighting the actual element you'd click and explaining what it does. Tours are **opt-in walkthroughs** — no tour ever force-runs; you start them yourself. A few also surface a one-time **offer toast** you can accept or dismiss ("Never show again"): when you first turn on a feature (Gmail, Voice, Screen Capture), and — for the **Start a New Hub** tour — right after you finish onboarding. You start a tour from **Settings → Setup Wizards**, which lists every available tour with a checkmark next to the ones you've completed. Picking a tour dims the rest of the app, cuts a spotlight hole around the relevant element, and shows a tooltip card next to it; an overlay step (welcome / completion cards) replaces the spotlight with a centered card. Use **Next / Back** to step through, **Esc** to exit, and **Skip** to mark the tour as completed without finishing it.

A second entry point — the **onboarding demo tour** — runs once during first-time setup. It's a sandboxed walkthrough of the app inside a fake project with fake sessions and messages (the `scene` blocks on most steps inject the demo state on the fly so you can see how the inbox, sessions list, and conversation view behave without ever touching real data; the steps that carry a real action or a full-screen overlay do not need one). It is NOT advertised in the Setup Wizards tour list; it's surfaced through the dedicated **Get Started** entry point on first launch and via Settings → Setup Wizards → **Show me around the app again**. A few of the demo's steps are **hands-on** — instead of pressing Next you perform the real action (add a project, start a session, open the session that needs you) and the tour advances when you do; because the demo is sandboxed, nothing real is created (see _Hands-on (interactive) steps_ below).

A third variant — the **keyboard demo** (`onboarding-demo-hotkeys`, dev-gated) — is a keystroke-driven copy of the onboarding demo. It reuses the same 34 steps and the same `scene` blocks (same sandboxed demo data), but each step advances on a **key** instead of Next/click: the tooltip shows a key-cap and you press it. The keys are the app's REAL shortcuts (`N` new session, `Enter` send, `R` reply, `E` archive, `I` inbox, `H` snooze; `Space` for explainer steps), resolved from your effective keybindings so they stay correct even if you rebind. Those keys normally fire real, sometimes-paid actions, so the safety model is a **hard gate**: while this tour is active, `AppTour` calls `useHardGate(...)`, and the global shortcut dispatch (`useKeyboardShortcuts`) early-returns on `isHardGateOpen()` — every real shortcut stands down wholesale, so pressing `N` only advances the tour (the next step's scene simulates the result) and can never spawn a session. See `app-tour-hotkey-demo-contract.md`. It's hidden from the Setup Wizards tour list and launched from its own dev-gated Setup Wizards card.

A fourth variant — the **Managing Your Sessions** practice tour (`manage-sessions`) — is an on-demand demo tour launched from the toolbar **Tours menu** (not first-run). Like the onboarding demo it runs on a sandboxed sample workspace: its `scene` blocks inject a fake project + sessions, and because it's a demo tour (in `DEMO_TOUR_IDS`), the demo controller (`useDemoTour`) **hides your real sessions and restores them on exit** — nothing real is touched. It walks through pausing/resuming, snoozing (opening the snooze picker), undo, and archiving on the sample session — each beat showing the result plus a key-cap (`P` / `H` / `Ctrl+Z` / `E`) — then narrates the right-click actions (move / rename / color / pin) and the Stop-vs-Pause distinction. It is **scene-driven and advances with Next** (the key-caps teach the shortcuts; it is NOT keystroke-gated like the keyboard demo, so it needs no hard gate). It launches through its own `DemoTourRunner` flag (`manageSessionsTourActive`), strictly parallel to the keyboard demo and off the first-run chain, and is hidden from the plain Setup-Wizards list so it can only run through that demo runner (the plain `startTour` path would paint its cards over the real app with no sandbox behind them).

Tour authoring is **pure JSON**. Every tour lives in `src/renderer/src/features/app-tour/tours/<slug>.json`, gets Zod-validated at app boot via `loadAllTours()` in `tour-loader.ts`, and hydrates into the runtime `TourDefinition` shape. There are no JavaScript helpers (`spotlight()` / `overlay()`) any more — you write JSON, the loader does the rest, and if the file fails the schema the app refuses to boot (so a bad tour file is impossible to ship by accident).

## Where to find it

### How to use it (end-user)

1. **Open Settings → Setup Wizards** — every available tour is listed with a checkmark for the ones you've completed.
2. **Click a tour to start it** — the rest of the app dims, the relevant element gets a spotlight, and a tooltip appears next to it (or a centered overlay card for welcome / completion steps).
3. **Step through with Next / Back** — phase dots at the top of the tooltip show your progress; phases with an empty `label` (typically the final "completion" phase) are hidden from the dots.
4. **Exit with Esc or Skip** — both mark the tour as completed in `tourCompletions` so the next visit shows a checkmark.
5. **Some tours suggest a follow-up** — if the tour's JSON declares a `nextTourSuggestion`, finishing it pops a toast 1 second later inviting you into the next tour (suppressed for experienced users with ≥ 10 sessions).

The progress dots, Skip button, and Esc handler are all part of the shared `<AppTour />` overlay component (it portals into `document.body`, so it sits above every modal). The actual highlighted element comes from a CSS-mask cutout — the page itself is not modified, so links and buttons under the dim layer remain non-interactive until the tour exits.

### The tours

| Slug                      | Label                 | Where it surfaces                                     |
| ------------------------- | --------------------- | ----------------------------------------------------- |
| `core-workflow`           | Your First Session    | Setup Wizards                                       |
| `sessions-productivity`   | Productivity Power-Ups | Setup Wizards                                      |
| `recipes`                 | Recipes & Filters      | Setup Wizards                                      |
| `gmail`                   | Gmail                 | Setup Wizards                                       |
| `rss`                     | RSS & Webhooks         | Setup Wizards                                       |
| `automations`             | Filters               | Setup Wizards                                       |
| `archive-search`          | Archive & Search      | Setup Wizards                                       |
| `onboarding-tour`         | Welcome to Omniscio   | Setup Wizards — replay button (behind the Onboarding Tour flag); auto-runs after first-run onboarding |
| `voice-commands`          | Voice Commands        | Setup Wizards                                       |
| `settings-tour`           | Settings Deep Dive    | Setup Wizards                                       |
| `start-next-project`      | Start a New Hub       | Setup Wizards · one-time offer right after onboarding |
| `mindmap`                 | Mind Map              | Gated — Tours menu · in-Map button · first-open offer |
| `writer-onboarding`       | Writer Studio Tour    | Writer Studio first-open offer (Writer-gated)         |
| `sms`                     | SMS                   | SMS onboarding flow only                              |
| `screen-recorder`         | Screen Capture        | Screen Capture settings — "Take the tour"             |
| `onboarding-demo`         | Interactive Demo      | First-time setup / Get Started                        |
| `onboarding-demo-hotkeys` | Keyboard Demo         | Setup Wizards (dev-gated)                           |
| `manage-sessions`         | Managing Your Sessions | Tours menu — dedicated button → demo runner          |
| `tasks-v2`                | Tasks                 | Tasks MORE menu — "Guided Tour" entry                 |
| `vault-kms`               | Vault                 | Vault toolbar — "Guided Tour" button                  |
| `__example__`             | Placeholder example tour | Hidden — bootstrap template                        |

> **Retired — `welcome` ("Getting Started").** Removed 2026-07. It spotlighted session-only UI (message box, quick replies, file explorer) that doesn't exist for a brand-new user with nothing set up, so it narrated at empty space; launched from the toolbar over Settings it could also strand the user (its Escape handler swallowed Settings' own Esc-to-close). The sandboxed **onboarding demo** — surfaced as **Interactive Demo**, the new-user **"Take the tour"** CTA, and the first-run walkthrough — is now the single intro tour: it stands up its own sample projects/sessions beneath the walkthrough, so it works with nothing set up. Relatedly, every tour launch now navigates to its home view (dashboard, or the Mind Map) BEFORE the overlay starts, so a tour never renders on top of another screen — see `src/renderer/src/app/tour-launch.ts`. Guard: `welcome-tour-retired.test.ts`.

The tours hidden from Setup Wizards (`sms`, `onboarding-demo`, `onboarding-demo-hotkeys`, `manage-sessions`, `__example__`, `mindmap`, `writer-onboarding`, `start-next-project`, `onboarding-tour`, `tasks-v2`, `vault-kms`) live in the `SETTINGS_MENU_HIDDEN_TOUR_IDS` set in `tour-steps.ts`. They're still resolvable via `getTourById()` so the surfaces that own them can start them directly. **`writer-onboarding`** (Writer Studio Tour) is hidden because Writer Studio is a gated feature — it launches as a first-open offer from the Writer view (`WriterView.tsx`) only for users with Writer enabled. **`mindmap`** is hidden because the Mind Map is a gated (unreleased) feature — its tour is surfaced only through feature-gated entry points (a conditional row in the Tours dropdown, shown when the feature is on, and a "Take a tour" button in the Mind Map toolbar) plus a first-open auto-offer (Gmail-style). Keeping it out of the ungated `ALL_TOURS` list is what stops it leaking into the Setup Wizards list for users without the feature. See `app-tour-mindmap-tour-contract.md`. **`onboarding-demo-hotkeys`** (the keyboard demo) is likewise dev-gated and launched from its own Setup Wizards card — see `app-tour-hotkey-demo-contract.md`. **`manage-sessions`** (Managing Your Sessions) is hidden for the same structural reason as a demo tour: it must run through its own `DemoTourRunner` (which stands up the sandbox and hides/restores real sessions), so it's launched from a dedicated Tours-menu button, never the plain `startTour` path that the ungated list uses. **`tasks-v2`** (Tasks) is hidden because it launches from the Tasks MORE menu's "Guided Tour" entry — it belongs to the Tasks feature, not the general Setup Wizards. **`vault-kms`** (Vault) is hidden for the same reason: it launches from a "Guided Tour" button in the Vault's editor toolbar (`KmsEditorToolbarStrip.tsx`), not the general Setup Wizards — an 8-step spotlight tour covering note creation, formatting, backlinks/tags, Vault Overview, AI agent tools, and appearance settings.

## How it behaves

### Hands-on (interactive) steps

A spotlight step can carry an `interactive` block to make it **hands-on** — the tour hides Next and advances only when the user performs the highlighted action (the active-learning counterpart to read-and-Next). Invariants are locked by `app-tour-interactive-contract.md`.

```json
{
  "kind": "spotlight",
  "anchor": "sidebar-add-project-button",
  "title": "Adding Projects",
  "description": [{ "type": "text", "text": "This is where you add project folders." }],
  "interactive": { "advanceOn": { "type": "click" } }
}
```

**Two modes:**

- **Scripted (the onboarding demo, and the default).** The tour's full-screen click-catcher stays on top; a click over the highlighted element is _detected_ via `document.elementsFromPoint` (so it sees the real target beneath the catcher) and the tour advances — **the real handler never fires**, so clicking the demo's "Start a session" costs nothing and creates nothing. The next step's scene shows the result. The onboarding demo is ALWAYS scripted, regardless of any flag.
- **Live (`"live": true`, for real feature tours).** The catcher drops pointer-events so the real action runs, and the tour watches the declared `advanceOn` outcome. Honored ONLY in a non-demo tour on an anchor flagged `interactiveSafe` in the registry — the guard `tests/unit/lint/tour-interactive-safe-anchor.test.ts` fails the build otherwise. One tour ships with `live` today: **Welcome to Omniscio** (`onboarding-tour`) runs two hands-on steps, `ot-03-open-session` and `ot-06-open-inbox`, where you click the real session row and the real inbox button to advance. The seam exists so other real tours can opt in as their anchors are flagged.

**`advanceOn`** — what counts as "done": `{ "type": "click" }` (clicked the highlighted anchor) or `{ "type": "anchor-appears", "anchor": "<name>" }` (a named element mounted — used by live mode to watch an outcome).

**Guided-but-rescuable.** Next is hidden (you advance by doing), but a **"Show me"** escape appears if the user stalls (~8s), clicks the wrong spot (with a corrective nudge), or the target never shows — and a missing target degrades to a normal Next step, so nobody is ever stuck.

**Authoring one:** add `"interactive": { "advanceOn": { "type": "click" } }` to a spotlight step whose anchor is a clickable control, and make sure the NEXT step's `scene` depicts the result of the action (in the demo, advancing replays the scripted result).

## For agents

### JSON shape

Every tour file is one JSON document validated against `tourFileSchema` in `src/renderer/src/features/app-tour/tour-schema.ts`. Top-level shape:

```json
{
  "id": "core-workflow",
  "label": "Your First Session",
  "description": "The complete cycle: project to finished work",
  "nextTourSuggestion": {
    "tourId": "sessions-productivity",
    "message": "Nice! Want to learn the power features? The Productivity tour covers quick replies and shortcuts."
  },
  "phaseLabels": [
    { "phase": "welcome", "label": "" },
    { "phase": "projects", "label": "Projects" },
    { "phase": "completion", "label": "" }
  ],
  "steps": [ ... ]
}
```

- **`id`** — must be one of the TourId union literals in `tour-types.ts`. Adding a brand-new tour means extending that union.
- **`label`** — human-readable title shown in Setup Wizards and in the tooltip header.
- **`description`** — one-line subtitle for the Setup Wizards card; not shown inside the running tour.
- **`nextTourSuggestion`** (optional) — fires a follow-up toast on completion. Skipped if the suggested tour is already completed, or if the user has ≥ 10 sessions (treated as "experienced, don't nag").
- **`phaseLabels`** — drives the phase dots. Empty `label` hides the dot for that phase.
- **`steps`** — non-empty array, each step is either `spotlight` or `overlay`.

#### Spotlight steps

```json
{
  "kind": "spotlight",
  "anchor": "sidebar-projects-list",
  "title": "Your Projects",
  "placement": "right",
  "padding": 4,
  "borderRadius": 12,
  "phase": "workspace",
  "description": [
    { "type": "text", "text": "Each project maps to a local folder on your machine." },
    { "type": "bullets", "items": ["Add folders", "Organize with dividers", "Color-code"] },
    { "type": "tip", "text": "The Inbox at the top collects sessions needing attention." }
  ]
}
```

- **Target — exactly ONE of `anchor` / `selector` / `text`** (a spotlight step must set precisely one; the schema rejects zero or two). This is the "highlight literally anything" seam — see _Flexible targeting_ below.
  - **`anchor`** — names a DOM element via `data-ui-anchor="<anchor>"`. When set, it MUST be registered: the CI gate (`tests/unit/lint/tour-anchor-coverage.test.ts`) fails the build if any spotlight anchor is not in the anchor registry. Anchors are declared in a feature's co-located `*.ui-anchors.ts` file and assembled into `STATIC_UI_ANCHORS` (exported from `src/shared/ui-anchor-registry.ts`, backed by the generated `ui-anchor-registry.generated.ts`) by `npm run ui-anchors:reindex` — see `ui-anchor-colocation-contract.md`. This is the same registry that powers `GET /ui/snapshot` for AI agents — so tour anchors are _also_ AI-pointable, no duplicated wiring.
  - **`selector`** — any CSS selector (e.g. `"[data-setting-id=\"font-size\"]"`, `".my-widget"`). No registry requirement — this is how a tour highlights an element that has no anchor.
  - **`text`** — visible text of the element to highlight (case-insensitive substring; resolves to the leaf-most element that owns the string).
- **`reveal`** (optional, spotlight OR overlay) — bring the target on screen BEFORE spotlighting: `{ "view"?: "<top-level view>", "settingsSection"?: "<section id>", "open"?: "<modal id>" }`. This is what lets one tour walk across many screens and open modals — see _Flexible targeting_ below.
- **`placement`** — `'top' | 'bottom' | 'left' | 'right' | 'auto'`. Default `'auto'` lets the tooltip pick a side that fits in the viewport.
- **`padding`** — extra space (px) around the cutout hole. Default 8.
- **`borderRadius`** — cutout corner radius (px). Default 12; set to 0 for sharp-cornered targets like the full-width header.
- **`phase`** — must match one of the `phaseLabels[].phase` strings on the parent tour; drives which dot is highlighted.
- **`id`** (optional) — falls back to the anchor string. Tests sometimes need stable step ids that don't collide with the anchor name.
- **`buttonLabel`** (optional) — overrides the default "Next" button text on this step.
- **`interactive`** (optional) — makes the step **hands-on**: the tour hides Next and advances only when the user performs the action. Shape: `{ "advanceOn": { "type": "click" }, "live"?: boolean }`. See _Hands-on (interactive) steps_ below. Hands-on steps require an `anchor` (selector/text steps are read-and-highlight only).

#### Flexible targeting — highlight anything + cross-screen reveal

Two powers let a tour highlight **any** element and walk the user **across many screens**, opening modals along the way. Both reuse the app's existing spotlight + navigation primitives; invariants are locked by `app-tour-reveal-targeting-contract.md`.

**Highlight anything** — instead of `anchor`, a spotlight step can target by raw CSS `selector` or visible `text`. These carry no registry requirement, so a tour can point at an element that was never given a `data-ui-anchor`. Resolution runs through the SAME shared picker as anchors (`pickSpotlightTarget`), so selector/text targets get the identical 0×0 guard, scroll-into-view, late-mount retry, and centered-fallback safety.

**Reveal (cross screens + open modals)** — any step (spotlight or overlay) can carry a `reveal` that fires ONCE on step entry, before the spotlight resolves:

- `"view"` — switch to a top-level view (validated ∈ `TOP_LEVEL_VIEWS`).
- `"settingsSection"` — open Settings scrolled to that section.
- `"open"` — open a modal/overlay by its reveal-registry id.

After the reveal fires, the tour's existing late-mount resolution waits for the target to appear on the new screen / in the modal, then spotlights it (or shows the centered fallback if it never appears). A build gate (`tests/unit/lint/tour-reveal-target-coverage.test.ts`) fails if a `reveal.view` isn't a real view or a `reveal.open` isn't registered — so a typo can't silently strand the tour on the wrong screen.

```json
{
  "kind": "spotlight",
  "reveal": { "view": "archive" },
  "selector": "[data-ui-anchor=\"archive-search-input\"]",
  "title": "Search your archive",
  "phase": "archive",
  "description": "Jump straight to the archive and find any past session."
}
```

The openable modal ids live in `src/renderer/src/features/app-tour/reveal-targets.ts` (currently `mind-map`, `flowchart`, `bake-off`, `presentation-staging`). There is no universal "open any modal" API in the app, so this registry is the curated, extensible seam — add a new openable surface by adding ONE entry to `reveal-targets.ts` (id + description) and its idempotent opener to `src/renderer/src/features/app-tour/reveal-registry.ts`.

#### Overlay steps

```json
{
  "kind": "overlay",
  "id": "welcome",
  "phase": "welcome",
  "title": "Welcome to Omniscio",
  "buttonLabel": "Show Me Around",
  "icon": "Sparkles",
  "description": [
    { "type": "text", "text": "Your air traffic control tower for AI-powered development." },
    {
      "type": "bullets",
      "items": ["Manage multiple AI sessions", "Review code changes", "Automate workflows"]
    }
  ]
}
```

- **`id`** — required (overlay steps have no anchor to fall back to).
- **`icon`** (optional) — name of a lucide-react icon (e.g. `"Sparkles"`, `"Rocket"`). Resolved through `resolveTourIcon()` in `tour-icon-map.ts`; unknown names silently fall back to no icon.
- No `anchor` / `placement` / `padding` / `borderRadius` — overlay cards are full-screen centered, not anchored to a DOM element.

### Description: markdown OR rich block array

The `description` field accepts either a plain markdown string or a typed-block array. The loader's `parseDescription()` (in `markdown-to-rich.ts`) handles both — string descriptions get parsed once at hydration; array descriptions pass through unchanged.

#### Markdown shortcuts

When you write a string description, these tokens parse into typed blocks:

- `[[shortcut:Esc:Interrupt Claude]]` → `{ type: 'shortcut', keys: 'Esc', label: 'Interrupt Claude' }`
- `[[prereq:Create snippets first.]]` → `{ type: 'prereq', text: 'Create snippets first.' }`
- `[[tryit:Click + to add a project.]]` → `{ type: 'tryit', text: 'Click + to add a project.' }`
- `> a blockquote line` → `{ type: 'tip', text: 'a blockquote line' }`
- `- bullet 1\n- bullet 2` → `{ type: 'bullets', items: ['bullet 1', 'bullet 2'] }`
- `1. step\n2. step` → `{ type: 'ordered', items: ['step', 'step'] }`
- Anything else → `{ type: 'text', text: '...' }`

#### Rich blocks (array form)

For finer control, write the array directly. Block types from `tour-types.ts`:

- `TextBlock` — `{ type: 'text', text }`
- `BulletBlock` — `{ type: 'bullets', items[] }`
- `OrderedListBlock` — `{ type: 'ordered', items[] }`
- `TipBlock` — `{ type: 'tip', text }` (rendered as an info callout)
- `PrereqBlock` — `{ type: 'prereq', text }` (rendered as "Before you start: …")
- `TryItBlock` — `{ type: 'tryit', text }` (rendered as a "Try it" call-out)
- `ShortcutBlock` — `{ type: 'shortcut', keys, label }` (rendered as a kbd-style chip)

The two forms are equivalent — they hydrate into the same `RichDescription` shape. Pick whichever reads better for the content.

### Scene blocks (demo tour only)

A demo tour needs to _show_ the app in different states (a project added, sessions running, an inbox alert) without those states being real. Each demo step can carry a `scene` block that mutates a sandboxed demo state when you arrive at the step:

```json
{
  "kind": "spotlight",
  "anchor": "sidebar-sessions-list",
  "title": "Sessions list",
  "scene": {
    "injectProjects": ["demo-project"],
    "injectSessions": ["demo-session-running", "demo-session-needs-you"],
    "activeProjectId": "demo-project",
    "messageOverrides": { "demo-session-running": "demo-running-messages" }
  },
  "description": "..."
}
```

Scenes are hydrated by `resolveSceneRefs()` in `src/renderer/src/features/onboarding/steps/demo-scenes.ts`, which converts string identifiers into the actual demo Project / Session / Message records (referencing the shared `DEMO_REGISTRY`, so a new demo tour can reuse the existing sample project/sessions). The scene block is consumed by the demo-tour controller (`useDemoTour`), which drives **every** tour in `DEMO_TOUR_IDS` — the onboarding demo, the keyboard demo, and the Managing Your Sessions practice tour. A NON-demo tour can technically include a `scene` block, but nothing reads it.

### How loading works

`tour-loader.ts` uses Vite's `import.meta.glob` to **eagerly** import every `tours/*.json` file at app boot. Each file is `safeParse`-d through `tourFileSchema`. A validation failure throws — the app refuses to start rather than silently dropping a broken tour. The loader then hydrates each `TourFile` into the runtime `TourDefinition` shape: strings become parsed `RichDescription` arrays, icon names become lucide components, scene refs become resolved records.

Tour resolution is cached per process — `getAllTours()` and `getTourById()` in `tour-steps.ts` lazy-build a module-level cache on first call.

### Files at a glance

- `tour-types.ts` — pure type module. `TooltipPlacement`, `TourId` union, the block-type interfaces, `OverlayTourStep`, `SpotlightTourStep`, `TourDefinition`, `TourPhaseLabel`. No runtime code.
- `tour-schema.ts` — Zod schemas matching every type. Single source of truth for what's valid JSON.
- `tour-loader.ts` — eager glob + per-file `safeParse` + hydration to runtime shape.
- `tour-steps.ts` — registry (`getAllTours`, `getTourById`, `ALL_TOURS` back-compat array) + `SETTINGS_MENU_HIDDEN_TOUR_IDS` filter list + the eleven `*_TOUR` const aliases kept for older imports.
- `markdown-to-rich.ts` — string-to-blocks parser (`parseDescription`).
- `tour-icon-map.ts` — `resolveTourIcon(name)` mapping from icon-name strings to lucide components.
- `tours/*.json` — one JSON file per tour slug (the source of truth for content).
- `AppTour.tsx` — the runtime overlay component. Reads `useAppTourStore` for current tour + step index, looks up the tour via `getTourById`, renders `<TourSpotlight>` + `<TourTooltip>` (or `<TourOverlayStep>`).
- `TourSpotlight.tsx` / `TourTooltip.tsx` — the glow ring + the info card, both SHARED with the AI/CLI highlight so the tour looks identical to `POST /ui/highlight`. `TourSpotlight` is the one full glow (3 px ring + halo); a `pulse` prop distinguishes read-only steps (steady) from action-required ones (pulse). `TourTooltip` renders through the shared `components/ui/SpotlightCard`, which owns the position, the fade + scale **materialize-in-place** entrance (no fly-in from the edge), the glass shell, and the close-X. See [ui-anchor-highlight-contract.md](../../.claude/memory/contracts/ui-anchor-highlight-contract.md) + [app-tour-x-hit-target-contract.md](../../.claude/memory/contracts/app-tour-x-hit-target-contract.md).
- `useTourTargetRect.ts` — measures the spotlighted element via `document.querySelector('[data-ui-anchor="<anchor>"]')`, polls up to 5 × 30 ms (`TOUR_TARGET_RECT_RETRY_COUNT` / `TOUR_TARGET_RECT_RETRY_INTERVAL_MS`) while the target paints in, returns the bounding rect, and reports "missing" after that ~150 ms window — a `MutationObserver` then keeps watching for a late mount and attaches the instant the element appears.
- `tests/unit/lint/tour-anchor-coverage.test.ts` — CI gate that scans every JSON tour file and fails the build if any spotlight `anchor` is missing from `STATIC_UI_ANCHORS`.

### To author a new tour

1. **Pick a slug** (kebab-case) and add it to the `TourId` union in `tour-types.ts`.
2. **Create `tours/<slug>.json`** with at least one step. Spotlight steps must reference real `data-ui-anchor` names — see the next section for adding a new anchor.
3. **Add phase labels** for every distinct `phase` string you use in the steps; the final completion phase typically has an empty `label` so its dot is hidden.
4. **If the tour should appear in Setup Wizards**, the JSON-loaded registry picks it up automatically.
5. **If the tour should be hidden from Setup Wizards** (surfaced via a custom entry point instead), add its id to `SETTINGS_MENU_HIDDEN_TOUR_IDS` in `tour-steps.ts`.
6. **If the tour suggests a follow-up**, add a `nextTourSuggestion` block.
7. **Verify** — `node scripts/vitest.js run tests/unit/lint/tour-anchor-coverage.test.ts` to catch unregistered anchors, then run the tour interactively from Settings → Setup Wizards.

**Authoring a demo (sandboxed) tour** — if the tour needs to _show_ actions happening on fake data (like Managing Your Sessions), it is a demo tour, not a plain one: (a) add its id to `DEMO_TOUR_IDS` in `tour-hotkey.ts` so `useDemoTour` snapshots/hides/restores the user's real sessions; (b) drive states with `scene` blocks referencing `DEMO_REGISTRY` ids (reuse the existing sample project/sessions to avoid registering new anchors); (c) add it to `SETTINGS_MENU_HIDDEN_TOUR_IDS` and launch it from a dedicated entry that mounts a `<DemoTourRunner tourId="…">` (mirror `manageSessionsTourActive` in `App.tsx` / `GlobalModals.tsx` / the Tours menu) — never the plain `startTour` path, which has no sandbox behind it. Behavior is locked by `tests/unit/features/app-tour/manage-sessions-tour.test.ts`.

### To add a new tour anchor

If your tour needs to highlight an element that isn't yet registered:

1. **Add `data-ui-anchor="<kebab-name>"`** to the JSX of the element you want to highlight. Names are kebab-case and globally unique.
2. **Declare it in your feature's co-located `*.ui-anchors.ts`** file (beside the component), with `{ description, kind, when?, related? }` — NEVER hand-edit `STATIC_UI_ANCHORS` / `ui-anchor-registry.generated.ts` (those are generated and a reindex overwrites any hand edit). Then run **`npm run ui-anchors:reindex`** to assemble the co-located entries into the generated registry. See `ui-anchor-colocation-contract.md`. The same registry powers `GET /ui/snapshot` for external AI agents — declaring once buys you both the tour spotlight AND AI-discoverability for free.
3. **Run the lint test** — `tests/unit/lint/tour-anchor-coverage.test.ts` will pass as soon as the anchor is in the (reindexed) registry.

The registry-only model means you never query the DOM for a tour anchor — if it's not in the registry, the CI gate fails before merge. No silent "the tour points at nothing" failures.

## Related

Related pages: [first-time-setup.md](first-time-setup.md) covers the first-run setup the onboarding demo runs inside, [mindmap.md](mindmap.md) and [kms.md](kms.md) are the gated features whose guided tours launch from their own toolbars rather than the Setup Wizards section, and [keyboard-shortcuts.md](keyboard-shortcuts.md) documents the shortcuts the keyboard demo teaches.
