|
3 | 3 |
|
4 | 4 | const HEARTBEAT_INTERVAL_MS = 15000; |
5 | 5 | const FAST_HEARTBEAT_DELAY_MS = 1200; |
| 6 | + const TUTORIAL_GATE_FALLBACK_MS = 15000; |
| 7 | + const TUTORIAL_GATE_QUIET_MS = 200; |
6 | 8 | const AUTOSTART_STATUS_MAX_AGE_MS = HEARTBEAT_INTERVAL_MS; |
7 | 9 | const AUTOSTART_PROMPT_COORDINATION_KEY = 'home-autostart-prompt'; |
8 | 10 | const AUTOSTART_PROMPT_PRIORITY = 100; |
|
567 | 569 | } |
568 | 570 | } |
569 | 571 |
|
570 | | - async function waitForAutostartPromptGate() { |
| 572 | + function hasActiveTutorialSurface() { |
| 573 | + return !!document.querySelector([ |
| 574 | + '.yui-guide-overlay', |
| 575 | + '.yui-guide-stage', |
| 576 | + '.driver-overlay', |
| 577 | + '.driver-popover', |
| 578 | + '#neko-day1-systray-intro-modal', |
| 579 | + ].join(', ')); |
| 580 | + } |
| 581 | + |
| 582 | + function isTutorialBusy() { |
| 583 | + const manager = window.universalTutorialManager || null; |
| 584 | + return window.isNekoHomeTutorialPending === true |
| 585 | + || window.isInTutorial === true |
| 586 | + || !!(manager && ( |
| 587 | + manager.isTutorialRunning |
| 588 | + || manager.activeAvatarFloatingGuideRound |
| 589 | + || manager.pendingTutorialStartSource |
| 590 | + || manager._pendingI18nStart |
| 591 | + || manager._teardownPromise |
| 592 | + )) |
| 593 | + || hasActiveTutorialSurface(); |
| 594 | + } |
| 595 | + |
| 596 | + function waitForTutorialStartupBarrier() { |
| 597 | + return new Promise((resolve) => { |
| 598 | + let moduleUnavailableFallbackExpired = false; |
| 599 | + let quietSince = 0; |
| 600 | + let pollTimer = null; |
| 601 | + let fallbackTimer = null; |
| 602 | + let done = false; |
| 603 | + |
| 604 | + const finish = function () { |
| 605 | + if (done) { |
| 606 | + return; |
| 607 | + } |
| 608 | + done = true; |
| 609 | + clearInterval(pollTimer); |
| 610 | + clearTimeout(fallbackTimer); |
| 611 | + window.removeEventListener('neko:startup-greeting-release', check); |
| 612 | + window.removeEventListener('neko:day1-systray-intro-closed', check); |
| 613 | + resolve(); |
| 614 | + }; |
| 615 | + const check = function () { |
| 616 | + if (done) { |
| 617 | + return; |
| 618 | + } |
| 619 | + const startupState = window.__NEKO_TUTORIAL_STARTUP_SETTLED__; |
| 620 | + const tutorialModuleLoaded = typeof startupState === 'boolean'; |
| 621 | + const tutorialDecisionPending = tutorialModuleLoaded |
| 622 | + ? startupState !== true |
| 623 | + : !moduleUnavailableFallbackExpired; |
| 624 | + if (tutorialDecisionPending || isTutorialBusy()) { |
| 625 | + quietSince = 0; |
| 626 | + return; |
| 627 | + } |
| 628 | + const now = Date.now(); |
| 629 | + if (!quietSince) { |
| 630 | + quietSince = now; |
| 631 | + return; |
| 632 | + } |
| 633 | + if (now - quietSince >= TUTORIAL_GATE_QUIET_MS) { |
| 634 | + finish(); |
| 635 | + } |
| 636 | + }; |
| 637 | + |
| 638 | + window.addEventListener('neko:startup-greeting-release', check); |
| 639 | + window.addEventListener('neko:day1-systray-intro-closed', check); |
| 640 | + pollTimer = setInterval(check, 50); |
| 641 | + fallbackTimer = setTimeout(function () { |
| 642 | + moduleUnavailableFallbackExpired = true; |
| 643 | + check(); |
| 644 | + }, TUTORIAL_GATE_FALLBACK_MS); |
| 645 | + check(); |
| 646 | + }); |
| 647 | + } |
| 648 | + |
| 649 | + async function waitForAutostartPromptPrerequisites() { |
571 | 650 | const AUTOSTART_GATE_FALLBACK_MS = 15000; |
572 | 651 | if (typeof window.waitForStorageLocationStartupBarrier === 'function') { |
573 | 652 | await window.waitForStorageLocationStartupBarrier(); |
574 | 653 | } |
575 | 654 |
|
576 | 655 | const onboarding = window.CharacterPersonalityOnboarding; |
577 | | - if (!onboarding || typeof onboarding.whenSettled !== 'function') { |
578 | | - return; |
579 | | - } |
580 | | - |
581 | | - const settled = await Promise.race([ |
582 | | - onboarding.whenSettled().then(() => true), |
583 | | - new Promise((resolve) => setTimeout(() => resolve(false), AUTOSTART_GATE_FALLBACK_MS)), |
584 | | - ]); |
585 | | - if (settled) { |
586 | | - return; |
| 656 | + if (onboarding && typeof onboarding.whenSettled === 'function') { |
| 657 | + const settled = await Promise.race([ |
| 658 | + onboarding.whenSettled().then(() => true), |
| 659 | + new Promise((resolve) => setTimeout(() => resolve(false), AUTOSTART_GATE_FALLBACK_MS)), |
| 660 | + ]); |
| 661 | + if (!settled) { |
| 662 | + const overlayActive = (onboarding.overlay && !onboarding.overlay.hidden) |
| 663 | + || onboarding.pendingResumeAfterTutorial; |
| 664 | + if (overlayActive) { |
| 665 | + await onboarding.whenSettled(); |
| 666 | + } |
| 667 | + } |
587 | 668 | } |
| 669 | + } |
588 | 670 |
|
589 | | - const overlayActive = (onboarding.overlay && !onboarding.overlay.hidden) |
590 | | - || onboarding.pendingResumeAfterTutorial; |
591 | | - if (overlayActive) { |
592 | | - await onboarding.whenSettled(); |
| 671 | + async function waitForAutostartPromptGate() { |
| 672 | + // Register first so a release emitted while another startup gate is |
| 673 | + // pending cannot be missed. |
| 674 | + const tutorialStartupBarrier = waitForTutorialStartupBarrier(); |
| 675 | + try { |
| 676 | + await waitForAutostartPromptPrerequisites(); |
| 677 | + } finally { |
| 678 | + await tutorialStartupBarrier; |
593 | 679 | } |
594 | 680 | } |
595 | 681 |
|
|
598 | 684 | return; |
599 | 685 | } |
600 | 686 | state.foregroundGateStarted = true; |
601 | | - void waitForAutostartPromptGate() |
| 687 | + void waitForAutostartPromptPrerequisites() |
602 | 688 | .catch(function (error) { |
603 | 689 | console.warn('[AutostartPrompt] foreground gate failed, starting timer anyway:', error); |
604 | 690 | }) |
|
682 | 768 | if (state.promptOpen) { |
683 | 769 | return false; |
684 | 770 | } |
| 771 | + if (isTutorialBusy()) { |
| 772 | + return false; |
| 773 | + } |
685 | 774 | if (!promptToken) { |
686 | 775 | return false; |
687 | 776 | } |
|
696 | 785 | } catch (_) { |
697 | 786 | return false; |
698 | 787 | } |
699 | | - if (!state.autostartSupported || isPromptSuppressedLocally()) { |
| 788 | + if (!state.autostartSupported || isPromptSuppressedLocally() || isTutorialBusy()) { |
700 | 789 | return false; |
701 | 790 | } |
702 | 791 | return typeof window.showDecisionPrompt === 'function'; |
|
784 | 873 |
|
785 | 874 | async function showPrompt(promptToken) { |
786 | 875 | state.promptOpen = true; |
787 | | - state.lastPromptTokenSeen = promptToken; |
788 | 876 | let promptVoice = null; |
789 | 877 | const buttons = [ |
790 | 878 | { |
|
823 | 911 | dismissValue: null, |
824 | 912 | closeOnClickOutside: false, |
825 | 913 | closeOnEscape: false, |
826 | | - onShown: function () { |
| 914 | + onShown: function (modal) { |
827 | 915 | stopPromptVoice(); |
| 916 | + if (isTutorialBusy()) { |
| 917 | + if (modal && typeof modal.close === 'function') { |
| 918 | + modal.close(null); |
| 919 | + } |
| 920 | + return; |
| 921 | + } |
| 922 | + state.lastPromptTokenSeen = promptToken; |
828 | 923 | promptVoice = startAutostartPromptVoice(); |
829 | 924 | return postShownAck(promptToken); |
830 | 925 | }, |
|
861 | 956 | return canShowPrompt(promptToken); |
862 | 957 | }, |
863 | 958 | display: function () { |
| 959 | + if (isTutorialBusy()) { |
| 960 | + return null; |
| 961 | + } |
864 | 962 | return showPrompt(promptToken); |
865 | 963 | }, |
866 | 964 | }); |
|
0 commit comments