Onboarding · Updated 2026-06-29

Feature hints (onboarding coachmarks)

First-run coachmarks for Organic LLM. Hints register anchors only in the existing layout — a global layer renders overlays without shifting page structure.

Presentation modes

ModeDesktopMobileUse when
spotlightSpotlight ring + anchored popover + click-away dismissBottom sheet drawerUI has a clear anchor (rail, button, composer chip)
toastSonner toast (bottom-right)SameContextual message with no stable anchor (empty states)

Only one hint shows at a time. The priority queue in lib/onboarding/feature-hint-priority.ts picks the next eligible hint; when more are waiting, the popover shows a compact page index (e.g. *1* | · | 3).

Guide pacing (hard rule)

Never auto-chain more than 4 consecutive coachmarks without unobstructed UI time.

RuleDetail
CapMAX_CONSECUTIVE_AUTO_FEATURE_HINTS = 4 in lib/onboarding/feature-hint-guide-policy.ts
Breath pauseAfter 4 dismissals on the same surface, auto hints pause until the user clicks/types outside overlays or switches surfaces
Beginning exceptionFirst browser session with no prior dismissals — no forced breath pause (first-steps sheet / welcome toast are not coachmarks)
Explicit requestHelp → Show next tip, opening first-steps guide, or Settings → Tips & coachmarks bypasses the pause
Surface navigationMoving to another surface (Arcadia → Noesis, etc.) resets the consecutive counter

Session state lives in sessionStorage under organic-llm-feature-hint-session. Per-tab dismissals (until replay is toggled on) also live in organic-llm-feature-hint-session-dismissed. Dismiss records remain in localStorage.

Nav/composer hints (experience-rail, composer-search-memory, composer-auto-model) set respectDismissInReplay: true so replay mode re-shows surface tips without re-opening the sidebar/composer chrome on every navigation.

Developer controls

ControlLocationEffect
Global kill switchFEATURE_HINTS_MASTER_SWITCH in lib/onboarding/feature-hints.tsDisables every hint
Per-hint switchenabled: false on a hint definitionDisables one hint without removing wiring
Copy refreshBump hint versionRe-shows for users who dismissed an older version
Presentationpresentation: "spotlight" | "toast"Spotlight vs toast-only
UI wiringfeatureHints={false} on CoreInputSkips composer coachmarks for that surface

Adding a hint

  1. Add an id to FEATURE_HINT_IDS and a definition in FEATURE_HINTS (lib/onboarding/feature-hints.ts).
  2. Choose presentation and optional side / align for spotlight hints.
  3. Wrap the anchor element (layout unchanged — ref registration only):
import { FeatureHint } from "@/components/onboarding/feature-hint";

<FeatureHint id="my-new-hint" showWhen={someProductCondition}>
  <Button>...</Button>
</FeatureHint>
  1. Optional: gate with showWhen so the callout only appears when the feature is relevant (e.g. after the first assistant turn).
  2. Add priority in lib/onboarding/feature-hint-priority.ts if order matters.
  3. Add or extend tests in tests/unit/feature-hint-storage.test.ts and tests/unit/feature-hint-priority.test.ts.

Architecture

OnboardingHost (app/layout.tsx)
├── FeatureHintRegistryProvider
├── children (unchanged layout)
├── FeatureHintLayer — spotlight / popover / drawer / toast
└── FirstSessionSheet — floating pill + bottom drawer
AreaPath
Registrylib/onboarding/feature-hints.ts
Priority queuelib/onboarding/feature-hint-priority.ts
Queue + pagerlib/onboarding/feature-hint-queue.ts, components/onboarding/feature-hint-queue-indicator.tsx
Session pacinglib/onboarding/feature-hint-guide-policy.ts
Session counterlib/onboarding/feature-hint-session.ts
Explicit resumelib/onboarding/feature-hint-explicit-request.ts
Anchor registrylib/onboarding/feature-hint-context.tsx
Storagelib/onboarding/feature-hint-storage.ts
Anchor wrappercomponents/onboarding/feature-hint.tsx
Global layercomponents/onboarding/feature-hint-layer.tsx
Hostcomponents/onboarding/onboarding-host.tsx
Teststests/unit/feature-hint-storage.test.ts, tests/unit/feature-hint-priority.test.ts

Persistence

Dismissals are stored in localStorage under organic-llm-feature-hints-dismissed as { [hintId]: dismissedVersion }.

Reset locally when testing:

localStorage.removeItem("organic-llm-feature-hints-dismissed");

Or import resetFeatureHintDismissals from lib/onboarding/feature-hint-storage.ts.

Current hints

IdSurfacePresentationAnchor
experience-railSidebarspotlightExperience rail
chat-empty-stateMain chattoast—
noesis-sparksNoesis empty statespotlightSparks header
noesis-suggest-replyNoesisspotlightSuggest my reply button
noesis-steer-assistNoesis composerspotlightSteer assist button
noesis-thread-titleNoesis (sidebar collapsed)spotlightThread title pill
composer-search-memoryShared composerspotlightSearch + Memory chips
composer-auto-modelShared composerspotlightModel picker
rabbit-holes-focusRabbit Holes empty statetoast—
arcadia-startersArcadia empty statespotlight (blur)Full style picker + starters
arcadia-style-standardArcadia (Standard selected)spotlight (blur)Standard style card
arcadia-style-ergonArcadia (Ergon board selected)spotlight (blur)Ergon style card
arcadia-style-scribeArcadia (Scribe selected)spotlight (blur)Scribe style card
noesis-sparksNoesis empty statespotlight (blur)Sparks header + grid

Surface entry hints wait ~420ms after navigation and take priority over the experience rail and composer hints on that route.

Composer hints apply across chat surfaces using CoreInput until dismissed once per browser origin.

Related NUX

  • Organic Help — components/onboarding/organic-help-dialog.tsx (control cluster ?)
  • First session — components/onboarding/first-session-sheet.tsx (floating pill + drawer, no home layout changes)
  • Keyboard shortcuts registry — lib/onboarding/keyboard-shortcuts.ts

Roadmap

  • Server-synced dismiss state for signed-in users
  • Settings → “Reset tips” for support and QA