@@ -47,6 +47,7 @@ import { DEMO_EVENTS } from './components/tour/demoData';
4747import {
4848 createDashboardTabHref ,
4949 getDashboardTabAndSubFromHref ,
50+ getDashboardTabFromHref ,
5051 type DashboardTab ,
5152} from './tab-routing' ;
5253import styles from './App.module.scss' ;
@@ -425,12 +426,45 @@ function AppContent() {
425426 // expensive when real run data is loaded).
426427 } , [ activeTab ] ) ;
427428
428- // The GuidedTour is only ever started by an explicit HOW IT WORKS
429- // click. The prior auto-start fired on first visit, set tourActive
430- // true, painted a 55%-black SVG scrim over the whole viewport, and
431- // then survived any tab navigation the user made before reaching the
432- // first dismissal control — so a user who clicked VIZ before reading
433- // the tour card saw a permanently dimmed page with no obvious cause.
429+ // Auto-start the GuidedTour on the user's FIRST visit to the sim
430+ // page so new viewers get oriented without having to find the
431+ // HOW IT WORKS button. Gated on a localStorage flag
432+ // (`paracosm:tourSeen`) so returning users don't get the tour
433+ // replayed every time they open the app.
434+ //
435+ // We set the flag IMMEDIATELY on auto-start fire (not just when
436+ // the tour ends). Reason: React 19's StrictMode double-runs
437+ // effects in dev, SPA navigations / query-param changes can
438+ // remount AppContent, and various dismissal paths (click-away,
439+ // Escape, browser back) don't all reliably call handleTourEnd
440+ // before the component unmounts. Pinning the flag at fire-time
441+ // guarantees once-ever auto-start behavior regardless of how
442+ // the user exits the tour. Manual re-play via HOW IT WORKS still
443+ // works since that path bypasses this effect.
444+ //
445+ // Mobile gate: skip auto-start below 640px.
446+ useEffect ( ( ) => {
447+ try {
448+ if ( localStorage . getItem ( 'paracosm:tourSeen' ) === '1' ) return ;
449+ localStorage . setItem ( 'paracosm:tourSeen' , '1' ) ;
450+ } catch {
451+ return ;
452+ }
453+ if ( typeof window !== 'undefined' && window . innerWidth < 640 ) {
454+ return ;
455+ }
456+ const timer = setTimeout ( ( ) => {
457+ const currentTab = getDashboardTabFromHref ( window . location . href ) ;
458+ if ( currentTab !== 'quickstart' ) {
459+ return ;
460+ }
461+ preTourTabRef . current = currentTab ;
462+ setTourActive ( true ) ;
463+ } , 600 ) ;
464+ return ( ) => clearTimeout ( timer ) ;
465+ // eslint-disable-next-line react-hooks/exhaustive-deps
466+ } , [ ] ) ;
467+
434468
435469 // Chat handoff from the VIZ drilldown. Sets the URL hash so
436470 // ChatPanel can read it on mount or on hashchange, then switches
0 commit comments