Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions openspec/changes/add-virtual-office-preferences/design.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
1 change: 1 addition & 0 deletions openspec/changes/add-virtual-office-preferences/tasks.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
9 changes: 7 additions & 2 deletions web/src/App.jsx
Original file line number Diff line number Diff line change
@@ -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)+ 主壳。
//
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -2467,7 +2472,7 @@ export function App() {
onDismiss={dismissGuidedTour}
onAbort={abortGuidedTour}
/>
{officeWelcomeOpen && !officeInvitationBlocked && !guidedTourPreparing && !guidedTourRun && (
{officeWelcomeVisible && (
<VirtualOfficeWelcome office={office} onClose={() => setOfficeWelcomeOpen(false)} />
)}
<SessionNavigationMask
Expand Down
4 changes: 4 additions & 0 deletions web/src/lib/desktopOfficePreferences.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
export const OFFICE_PREFERENCES_EVENT = 'ace-desktop-office-preferences';

export function isOfficeWelcomeVisible({ requested, blocked, tourPreparing, tourRunning }) {
return requested && !blocked && !tourPreparing && !tourRunning;
}

export function parseOfficePreferences(value) {
const result = typeof value === 'string' ? JSON.parse(value) : value;
if (!result || typeof result.available !== 'boolean' || typeof result.enabled !== 'boolean') {
Expand Down
19 changes: 18 additions & 1 deletion web/src/lib/desktopOfficePreferences.test.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,22 @@
import assert from 'node:assert/strict';
import { createDesktopOfficePreferences, OFFICE_PREFERENCES_EVENT } from './desktopOfficePreferences.js';
import { createDesktopOfficePreferences, isOfficeWelcomeVisible, OFFICE_PREFERENCES_EVENT } from './desktopOfficePreferences.js';
import { shouldPrepareDesktopGuidedTour } from './desktopGuidedTour.js';

// Native claiming may finish after the ordinary first-run tour starts preparing.
// A queued invitation must let that tour finish before taking its turn.
{
const tour = { mode: 'shell', authState: 'ok', startupNavigationSettled: true, hasActiveSession: false };
const queued = { requested: true, blocked: false, tourPreparing: true, tourRunning: false };
assert.equal(isOfficeWelcomeVisible(queued), false);
assert.equal(shouldPrepareDesktopGuidedTour({ ...tour, blocked: isOfficeWelcomeVisible(queued) }), true,
'a welcome claimed during preparation cannot deadlock the guided tour');
assert.equal(isOfficeWelcomeVisible({ ...queued, tourPreparing: false, tourRunning: true }), false);
const visible = isOfficeWelcomeVisible({ ...queued, tourPreparing: false });
assert.equal(visible, true, 'welcome appears once the tour finishes or is dismissed');
assert.equal(shouldPrepareDesktopGuidedTour({ ...tour, blocked: visible }), false,
'a visible invitation protects its focus from the tour');
assert.equal(isOfficeWelcomeVisible({ ...queued, tourPreparing: false, blocked: true }), false);
}

const flush = () => new Promise(resolve => setImmediate(resolve));
const initial = { ok: true, available: true, enabled: false, welcomePending: true };
Expand Down
Loading