Skip to content

Commit 7409da9

Browse files
authored
fix(autostart): defer prompt until tutorial settles (#2885)
* fix(autostart): defer prompt until tutorial settles * fix(autostart): close tutorial gate races * fix(autostart): cancel prompt when tutorial starts * fix(tutorial): rearm autostart prompt races * fix(autostart): close prompt when tutorial rearms * fix(tutorial): defer resize init for active prompt
1 parent 77c45ca commit 7409da9

5 files changed

Lines changed: 644 additions & 20 deletions

File tree

static/app/app-autostart-prompt.js

Lines changed: 117 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33

44
const HEARTBEAT_INTERVAL_MS = 15000;
55
const FAST_HEARTBEAT_DELAY_MS = 1200;
6+
const TUTORIAL_GATE_FALLBACK_MS = 15000;
7+
const TUTORIAL_GATE_QUIET_MS = 200;
68
const AUTOSTART_STATUS_MAX_AGE_MS = HEARTBEAT_INTERVAL_MS;
79
const AUTOSTART_PROMPT_COORDINATION_KEY = 'home-autostart-prompt';
810
const AUTOSTART_PROMPT_PRIORITY = 100;
@@ -567,29 +569,113 @@
567569
}
568570
}
569571

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() {
571650
const AUTOSTART_GATE_FALLBACK_MS = 15000;
572651
if (typeof window.waitForStorageLocationStartupBarrier === 'function') {
573652
await window.waitForStorageLocationStartupBarrier();
574653
}
575654

576655
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+
}
587668
}
669+
}
588670

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;
593679
}
594680
}
595681

@@ -598,7 +684,7 @@
598684
return;
599685
}
600686
state.foregroundGateStarted = true;
601-
void waitForAutostartPromptGate()
687+
void waitForAutostartPromptPrerequisites()
602688
.catch(function (error) {
603689
console.warn('[AutostartPrompt] foreground gate failed, starting timer anyway:', error);
604690
})
@@ -682,6 +768,9 @@
682768
if (state.promptOpen) {
683769
return false;
684770
}
771+
if (isTutorialBusy()) {
772+
return false;
773+
}
685774
if (!promptToken) {
686775
return false;
687776
}
@@ -696,7 +785,7 @@
696785
} catch (_) {
697786
return false;
698787
}
699-
if (!state.autostartSupported || isPromptSuppressedLocally()) {
788+
if (!state.autostartSupported || isPromptSuppressedLocally() || isTutorialBusy()) {
700789
return false;
701790
}
702791
return typeof window.showDecisionPrompt === 'function';
@@ -784,7 +873,6 @@
784873

785874
async function showPrompt(promptToken) {
786875
state.promptOpen = true;
787-
state.lastPromptTokenSeen = promptToken;
788876
let promptVoice = null;
789877
const buttons = [
790878
{
@@ -823,8 +911,15 @@
823911
dismissValue: null,
824912
closeOnClickOutside: false,
825913
closeOnEscape: false,
826-
onShown: function () {
914+
onShown: function (modal) {
827915
stopPromptVoice();
916+
if (isTutorialBusy()) {
917+
if (modal && typeof modal.close === 'function') {
918+
modal.close(null);
919+
}
920+
return;
921+
}
922+
state.lastPromptTokenSeen = promptToken;
828923
promptVoice = startAutostartPromptVoice();
829924
return postShownAck(promptToken);
830925
},
@@ -861,6 +956,9 @@
861956
return canShowPrompt(promptToken);
862957
},
863958
display: function () {
959+
if (isTutorialBusy()) {
960+
return null;
961+
}
864962
return showPrompt(promptToken);
865963
},
866964
});

static/common_dialogs.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1329,6 +1329,7 @@
13291329
return modalConfig.onShown({
13301330
overlay: overlay,
13311331
dialog: dialog,
1332+
close: finish,
13321333
});
13331334
})
13341335
.catch(function (error) {

static/tutorial/core/universal-manager.js

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4390,6 +4390,52 @@ function dispatchStartupGreetingReleaseWithoutManager(reason, detail = {}) {
43904390
return releaseDetail;
43914391
}
43924392

4393+
function rearmStartupGreetingWithoutManager(reason, detail = {}) {
4394+
window.isNekoHomeTutorialPending = true;
4395+
const rearmDetail = Object.assign({
4396+
released: false,
4397+
page: 'unknown',
4398+
reason: reason || 'tutorial-manager-initializing',
4399+
timestamp: Date.now()
4400+
}, detail || {});
4401+
try {
4402+
window.__NEKO_TUTORIAL_STARTUP_SETTLED__ = false;
4403+
delete window.__NEKO_STARTUP_GREETING_RELEASED__;
4404+
window.dispatchEvent(new CustomEvent(STARTUP_GREETING_RELEASE_EVENT, {
4405+
detail: rearmDetail
4406+
}));
4407+
} catch (error) {
4408+
console.warn('[Tutorial] 无管理器启动问候重新上锁失败:', error);
4409+
}
4410+
return rearmDetail;
4411+
}
4412+
4413+
function waitForActiveAutostartPromptClosed() {
4414+
const selector = '.modal-overlay-autostart-retention';
4415+
if (!document.querySelector(selector)) {
4416+
return Promise.resolve();
4417+
}
4418+
return new Promise((resolve) => {
4419+
let done = false;
4420+
const finish = function () {
4421+
if (done) return;
4422+
done = true;
4423+
window.removeEventListener('neko:decision-prompt-closed', onPromptClosed);
4424+
resolve();
4425+
};
4426+
const onPromptClosed = function (event) {
4427+
const detail = event && event.detail ? event.detail : {};
4428+
if (detail.skin === 'autostart-retention') {
4429+
finish();
4430+
}
4431+
};
4432+
window.addEventListener('neko:decision-prompt-closed', onPromptClosed);
4433+
if (!document.querySelector(selector)) {
4434+
finish();
4435+
}
4436+
});
4437+
}
4438+
43934439
async function destroyUniversalTutorialManagerInstance(reason = 'destroy') {
43944440
const manager = window.universalTutorialManager;
43954441
if (!manager) return;
@@ -4415,7 +4461,12 @@ function bindUniversalTutorialManagerResizeRetry() {
44154461
window.removeEventListener('resize', retryUniversalTutorialManagerInit);
44164462
window.__universalTutorialManagerResizeRetryBound = false;
44174463
if (window.__universalTutorialManagerInitialized) return;
4418-
initUniversalTutorialManager().then(function (initialized) {
4464+
rearmStartupGreetingWithoutManager('tutorial-manager-resize-init', {
4465+
viewportWidth: window.innerWidth
4466+
});
4467+
waitForActiveAutostartPromptClosed().then(function () {
4468+
return initUniversalTutorialManager();
4469+
}).then(function (initialized) {
44194470
if (initialized !== false) {
44204471
window.__universalTutorialManagerInitialized = true;
44214472
}

0 commit comments

Comments
 (0)