From f10c419854c5dc05217891d9101106cf79675d8c Mon Sep 17 00:00:00 2001 From: tmoonlight Date: Wed, 7 Oct 2026 15:58:06 +0800 Subject: [PATCH] fix: Avoid startup deadlock between office welcome and guided tour --- .../add-virtual-office-preferences/design.md | 1 + .../add-virtual-office-preferences/tasks.md | 1 + .../verification.md | 6 ++++++ web/src/App.jsx | 9 +++++++-- web/src/lib/desktopOfficePreferences.js | 4 ++++ web/src/lib/desktopOfficePreferences.test.js | 19 ++++++++++++++++++- 6 files changed, 37 insertions(+), 3 deletions(-) diff --git a/openspec/changes/add-virtual-office-preferences/design.md b/openspec/changes/add-virtual-office-preferences/design.md index 667e6a3f..4d7e134c 100644 --- a/openspec/changes/add-virtual-office-preferences/design.md +++ b/openspec/changes/add-virtual-office-preferences/design.md @@ -10,6 +10,7 @@ The office is a Windows WebView2 or macOS WKWebView window driven by Web UI sess 4. One frontend owner manages preference state and live controller lifetime. Menu/settings receive that state; native close events update it. Stop polling and subscriptions when disabled. Unsupported Web/Linux shows a disabled settings switch with a short explanation and no actionable quick-menu entry or invitation. 5. Only while the invitation is open, request the already embedded HTML over the native bridge and render it in an `allow-scripts` sandboxed iframe with local demo snapshots. No video/GIF, network, real sessions, duplicated HTML bundle, or surviving animation after dismissal. Honor reduced motion and provide pause/play. 6. Invitation uses shared Modal, exact requested description, explicit close/later/enable buttons, no backdrop dismissal, and avoids other startup blocking dialogs. Existing settings tokens and a simple 20-unit desk/room SVG are reused. +7. A native welcome claim can resolve after the ordinary guided tour starts preparing. Only a visible office invitation blocks that tour: a queued invitation waits for an already preparing/running tour to finish. This avoids both startup surfaces waiting for the other to close. ## Validation diff --git a/openspec/changes/add-virtual-office-preferences/tasks.md b/openspec/changes/add-virtual-office-preferences/tasks.md index 3c206bd0..c3d41187 100644 --- a/openspec/changes/add-virtual-office-preferences/tasks.md +++ b/openspec/changes/add-virtual-office-preferences/tasks.md @@ -3,6 +3,7 @@ - [x] 2. Add gear action, SVG, settings page and unified frontend state with hidden-session teardown. - [x] 3. Add version-gated once-only invitation and lazy offline animated preview. ## Validation and delivery +- [x] 6. Fix the native welcome-claim/guided-tour preparation race found during 0.9.37 release validation and verify the real first-run flow. - [x] 4. Add meaningful service/UI coverage; run focused native and Web tests, build, browser checks, strict spec and diff validation. - [x] 5. Document macOS handoff and verification limits. diff --git a/openspec/changes/add-virtual-office-preferences/verification.md b/openspec/changes/add-virtual-office-preferences/verification.md index eaa0a9d5..2f4feb38 100644 --- a/openspec/changes/add-virtual-office-preferences/verification.md +++ b/openspec/changes/add-virtual-office-preferences/verification.md @@ -12,3 +12,9 @@ macOS was source-reviewed only: shared lifecycle/preferences and early bridge registration are applied to the WKWebView host. A Mac build/runtime is not claimed. Follow the handoff in `docs/desktop-agent-office.md` for session linkage, close/reopen, restart and first-run invitation verification. Current project version remains 0.9.36. The invitation gate intentionally activates at 0.9.37 and later stable versions; version thresholds are covered by native tests. This change requests code delivery only, with no release tag or package publication. + +## Follow-up during 0.9.37 release validation + +With the native version gate active, a clean-profile startup exposed a race: the welcome claim could finish while the existing guided tour was preparing, and the queued welcome then blocked that tour while waiting for it. The invitation had been acknowledged but neither surface appeared. The provisional update entry was withdrawn and the package job cancelled before GitHub Release publication. + +The fix distinguishes queued and visible invitations; an already preparing/running tour can finish first. Regression tests check this interaction with `shouldPrepareDesktopGuidedTour`. Full Web tests/build passed again. An isolated native v0.9.37 process using the corrected production Web build passed automatic invitation display, dismissal without enabling, once-only acknowledgement, snapshot/title delivery, native close persistence, same-process reopening and disabling. Native evidence: `C:/Users/shao/AppData/Local/Temp/ace-office-optin-native-xv7o4_q8/result.json`. Source version is now 0.9.37; publication recovery is tracked separately in the release evidence. diff --git a/web/src/App.jsx b/web/src/App.jsx index b276ae2b..156c4e4a 100644 --- a/web/src/App.jsx +++ b/web/src/App.jsx @@ -1,4 +1,5 @@ import { useDesktopOffice } from './lib/useDesktopOffice.js'; +import { isOfficeWelcomeVisible } from './lib/desktopOfficePreferences.js'; import { VirtualOfficeWelcome } from './components/VirtualOfficeWelcome.jsx'; // 顶层 App:鉴权 gate(401 → TokenPrompt)+ 主壳。 // @@ -510,7 +511,11 @@ export function App() { || desktopCloseDialogOpen || configRecoveryBlocking || questionReqs.length > 0; - const guidedTourBlocked = officeInvitationBlocked || officeWelcomeOpen; + // A tour that started while the native welcome claim was pending finishes first. + // Blocking it with the queued invitation would make both surfaces wait forever. + const officeWelcomeVisible = isOfficeWelcomeVisible({ requested: officeWelcomeOpen, + blocked: officeInvitationBlocked, tourPreparing: guidedTourPreparing, tourRunning: guidedTourRun }); + const guidedTourBlocked = officeInvitationBlocked || officeWelcomeVisible; useEffect(() => { if (officeWelcomeAttempted.current || !office.welcomePending || authState !== 'ok' || !health @@ -2467,7 +2472,7 @@ export function App() { onDismiss={dismissGuidedTour} onAbort={abortGuidedTour} /> - {officeWelcomeOpen && !officeInvitationBlocked && !guidedTourPreparing && !guidedTourRun && ( + {officeWelcomeVisible && ( setOfficeWelcomeOpen(false)} /> )} new Promise(resolve => setImmediate(resolve)); const initial = { ok: true, available: true, enabled: false, welcomePending: true };