Skip to content

fix(wework): recover frozen webview after lock screen - #2216

Merged
qdaxb merged 2 commits into
mainfrom
fix/wework-lock-screen-recovery
Jul 25, 2026
Merged

qdaxb merged 2 commits into
mainfrom
fix/wework-lock-screen-recovery

Conversation

@qdaxb

@qdaxb qdaxb commented Jul 25, 2026 •

Copy link
Copy Markdown
Contributor

What changed

  • add a native/frontend resume probe for the Wework main WebView
  • check responsiveness when the window regains focus after remaining unfocused for at least one minute
  • require the frontend to advance two animation frames and acknowledge the probe within five seconds
  • recreate an unresponsive main WebView while preserving its position, size, maximized state, and fullscreen state
  • document the macOS lock-screen recovery behavior in Chinese and English

Why

After macOS remains locked for a while, the Wework WebView can occasionally stay on its last rendered frame and stop accepting input after unlock. The native application and local executor remain alive, and manually closing and reopening the window restores the interface.

The previous window lifecycle had no way to distinguish a healthy resumed WebView from a frozen one. This change verifies that rendering actually advances and rebuilds only the unresponsive interface process.

Impact

  • healthy windows resume without reloading
  • frozen windows recover automatically instead of requiring a manual close and reopen
  • running local tasks and the executor continue while the WebView is recreated
  • a recreated window restores persisted task state; unsent input that exists only in interface memory may be lost

Validation

  • pnpm --filter wework test — 218 test files, 2167 tests passed
  • cargo test --manifest-path wework/src-tauri/Cargo.toml — 91 tests passed
  • Wework ESLint and TypeScript checks passed in pre-push
  • isolated real-Tauri verification confirmed that a healthy WebView acknowledges the resume probe and is not recreated

Summary by CodeRabbit

  • New Features

    • Added automatic interface recovery for the macOS desktop app after returning from screen lock or prolonged inactivity.
    • The main window is recreated if the interface does not resume rendering, while preserving its size, position, maximized, and fullscreen states.
    • Running tasks continue uninterrupted during interface recovery.
  • Documentation

    • Added English and Chinese guidance covering recovery behavior and the potential loss of unsent interface-only input.
  • Tests

    • Added coverage for frontend recovery and inactivity threshold handling.

@coderabbitai

coderabbitai Bot commented Jul 25, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

The desktop app adds a frontend resume probe and Tauri recovery bridge. After prolonged window unfocus or application resume, the backend checks frontend rendering and recreates the main window on timeout while preserving its placement and keeping task execution active.

Changes

Frontend recovery bridge

Layer / File(s) Summary
Frontend probe acknowledgement
wework/src/lib/frontendRecovery.ts, wework/src/lib/frontendRecovery.test.ts, wework/src/main.tsx
The frontend registers the native probe callback in Tauri, acknowledges probes after two animation frames, tests both Tauri and non-Tauri behavior, and installs the bridge during normal app initialization.

Native recovery lifecycle

Layer / File(s) Summary
Probe scheduling and main-window recreation
wework/src-tauri/src/lib.rs
The Tauri backend tracks focus and probe state, registers recovery commands, schedules resume checks, preserves window placement, and recreates the main window when acknowledgement times out.

Lifecycle wiring and documentation

Layer / File(s) Summary
Resume events, threshold validation, and user guidance
wework/src-tauri/src/lib.rs, docs/en/wegent/user-guide/settings/desktop-app-preferences.md, docs/zh/wegent/user-guide/settings/desktop-app-preferences.md
Focus and resume events invoke recovery handling, a desktop test verifies the unfocus threshold, and English and Chinese documentation describe the recovery behavior.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant DesktopLifecycle
  participant FrontendBridge
  participant TauriCommands
  participant MainWindow
  DesktopLifecycle->>FrontendBridge: schedule frontend resume probe
  FrontendBridge->>TauriCommands: acknowledge probe after rendering frames
  TauriCommands-->>DesktopLifecycle: return acknowledged probe id
  DesktopLifecycle->>MainWindow: recreate window if acknowledgement times out
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: recovering a frozen WebView after the lock screen.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/wework-lock-screen-recovery

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@qdaxb
qdaxb marked this pull request as ready for review July 25, 2026 15:26

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (2)
wework/src-tauri/src/lib.rs (1)

2535-2549: 🩺 Stability & Availability | 🔵 Trivial | 💤 Low value

Ignored run_on_main_thread errors can latch frontend_probe_in_flight. Both worker threads discard the dispatch result with let _; if dispatch fails, the closure that resets frontend_probe_in_flight never runs and all future probes are skipped for the process lifetime.

  • wework/src-tauri/src/lib.rs#L2535-L2549: on Err from run_on_main_thread, reset frontend_probe_in_flight to false (and log) instead of dropping the result.
  • wework/src-tauri/src/lib.rs#L2492-L2505: apply the same fallback reset in the recreation thread.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@wework/src-tauri/src/lib.rs` around lines 2535 - 2549, The two worker threads
must handle failed run_on_main_thread dispatches so frontend_probe_in_flight
cannot remain latched. In wework/src-tauri/src/lib.rs:2535-2549, update the
probe thread around run_on_main_thread to reset frontend_probe_in_flight to
false and log the failure when dispatch returns Err; apply the same fallback in
the recreation thread at wework/src-tauri/src/lib.rs:2492-2505, while preserving
the existing successful closure behavior.
wework/src/lib/frontendRecovery.ts (1)

14-30: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider an install-once guard for consistency.

installAppLogging / installDebugPanelLogCapture both guard with an installed flag; a second installFrontendRecoveryBridge() call would re-register the bridge and re-invoke the native command. Cheap to align.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@wework/src/lib/frontendRecovery.ts` around lines 14 - 30, Add an install-once
guard to installFrontendRecoveryBridge so repeated calls return without
replacing the probe handler or re-invoking REGISTER_FRONTEND_RECOVERY_COMMAND.
Set the guard only when installation proceeds, while preserving the existing
isTauriRuntime check and recovery behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/zh/wegent/user-guide/settings/desktop-app-preferences.md`:
- Line 68: Update the recovery description at
docs/zh/wegent/user-guide/settings/desktop-app-preferences.md:68-68 to describe
the behavior as applying to all desktop platforms rather than only macOS, with
screen locking presented as a typical trigger. Apply the equivalent wording
change at docs/en/wegent/user-guide/settings/desktop-app-preferences.md:68-68,
replacing “On macOS” with desktop-wide wording; no code changes are needed.

In `@wework/src-tauri/src/lib.rs`:
- Around line 2474-2505: Clear destroy_to_tray_in_progress when the delayed
recreation flow completes successfully or fails, after create_main_window is
attempted in the run_on_main_thread closure. Update the recovery path around
create_main_window and retain the existing cleanup for destroy errors, ensuring
the flag cannot remain latched when no ExitRequested event is emitted.

---

Nitpick comments:
In `@wework/src-tauri/src/lib.rs`:
- Around line 2535-2549: The two worker threads must handle failed
run_on_main_thread dispatches so frontend_probe_in_flight cannot remain latched.
In wework/src-tauri/src/lib.rs:2535-2549, update the probe thread around
run_on_main_thread to reset frontend_probe_in_flight to false and log the
failure when dispatch returns Err; apply the same fallback in the recreation
thread at wework/src-tauri/src/lib.rs:2492-2505, while preserving the existing
successful closure behavior.

In `@wework/src/lib/frontendRecovery.ts`:
- Around line 14-30: Add an install-once guard to installFrontendRecoveryBridge
so repeated calls return without replacing the probe handler or re-invoking
REGISTER_FRONTEND_RECOVERY_COMMAND. Set the guard only when installation
proceeds, while preserving the existing isTauriRuntime check and recovery
behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: fbce04f3-ba25-48ab-b335-dd60872ed69c

📥 Commits

Reviewing files that changed from the base of the PR and between bd0686d and e3e1c4c.

📒 Files selected for processing (6)
  • docs/en/wegent/user-guide/settings/desktop-app-preferences.md
  • docs/zh/wegent/user-guide/settings/desktop-app-preferences.md
  • wework/src-tauri/src/lib.rs
  • wework/src/lib/frontendRecovery.test.ts
  • wework/src/lib/frontendRecovery.ts
  • wework/src/main.tsx


## 锁屏后的界面恢复

在 macOS 上,如果 Wework 主窗口连续失去焦点超过 1 分钟,例如电脑长时间锁屏,窗口重新获得焦点时会检查 WebView 是否已经恢复绘制。正常恢复的界面不会重新加载;如果 WebView 在 5 秒内仍无法完成绘制回执,Wework 会自动重建主窗口,并保留原来的窗口位置、大小、最大化和全屏状态。

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Recovery is #[cfg(desktop)], not macOS-only. Both docs scope the behavior to macOS, but the probe/recreate path in wework/src-tauri/src/lib.rs is gated on desktop, so Windows and Linux builds behave the same way.

  • docs/zh/wegent/user-guide/settings/desktop-app-preferences.md#L68-L68: 将“在 macOS 上”改为桌面版通用描述(或说明该行为在所有桌面平台生效,锁屏是最常见触发场景)。
  • docs/en/wegent/user-guide/settings/desktop-app-preferences.md#L68-L68: replace "On macOS" with desktop-wide wording, mentioning screen lock only as the typical trigger.
📍 Affects 2 files
  • docs/zh/wegent/user-guide/settings/desktop-app-preferences.md#L68-L68 (this comment)
  • docs/en/wegent/user-guide/settings/desktop-app-preferences.md#L68-L68
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/zh/wegent/user-guide/settings/desktop-app-preferences.md` at line 68,
Update the recovery description at
docs/zh/wegent/user-guide/settings/desktop-app-preferences.md:68-68 to describe
the behavior as applying to all desktop platforms rather than only macOS, with
screen locking presented as a typical trigger. Apply the equivalent wording
change at docs/en/wegent/user-guide/settings/desktop-app-preferences.md:68-68,
replacing “On macOS” with desktop-wide wording; no code changes are needed.

Comment on lines +2474 to +2505
state.frontend_recovery_ready.store(false, Ordering::SeqCst);
state
.destroy_to_tray_in_progress
.store(true, Ordering::SeqCst);

log::warn!("Recreating unresponsive main WebView after resume probe timed out");
if let Err(error) = window.destroy() {
state
.destroy_to_tray_in_progress
.store(false, Ordering::SeqCst);
state
.frontend_probe_in_flight
.store(false, Ordering::SeqCst);
state.frontend_recovery_ready.store(true, Ordering::SeqCst);
log::warn!("Failed to destroy unresponsive main WebView: {error}");
return;
}

std::thread::spawn(move || {
std::thread::sleep(MAIN_WINDOW_RECREATE_DELAY);
let app_for_create = app.clone();
let _ = app.run_on_main_thread(move || {
let state = app_for_create.state::<MainWindowLifecycleState>();
if let Err(error) = create_main_window(&app_for_create, None, Some(placement)) {
log::warn!("Failed to recreate unresponsive main WebView: {error}");
set_dock_icon_visible(&app_for_create, true);
}
state
.frontend_probe_in_flight
.store(false, Ordering::SeqCst);
});
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

destroy_to_tray_in_progress can stay latched after a successful recovery.

The flag is set before window.destroy() and is only cleared in the destroy-error branch or by the RunEvent::ExitRequested handler (Line 4279). If no ExitRequested fires — e.g. another webview window (embedded browser / appshot) is still open, so destroying the main window is not an app-exit trigger — the flag remains true and the user's next real quit is silently prevented once. Clear it once the recreation attempt completes.

🐛 Proposed fix
             if let Err(error) = create_main_window(&app_for_create, None, Some(placement)) {
                 log::warn!("Failed to recreate unresponsive main WebView: {error}");
                 set_dock_icon_visible(&app_for_create, true);
             }
+            state
+                .destroy_to_tray_in_progress
+                .store(false, Ordering::SeqCst);
             state
                 .frontend_probe_in_flight
                 .store(false, Ordering::SeqCst);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
state.frontend_recovery_ready.store(false, Ordering::SeqCst);
state
.destroy_to_tray_in_progress
.store(true, Ordering::SeqCst);
log::warn!("Recreating unresponsive main WebView after resume probe timed out");
if let Err(error) = window.destroy() {
state
.destroy_to_tray_in_progress
.store(false, Ordering::SeqCst);
state
.frontend_probe_in_flight
.store(false, Ordering::SeqCst);
state.frontend_recovery_ready.store(true, Ordering::SeqCst);
log::warn!("Failed to destroy unresponsive main WebView: {error}");
return;
}
std::thread::spawn(move || {
std::thread::sleep(MAIN_WINDOW_RECREATE_DELAY);
let app_for_create = app.clone();
let _ = app.run_on_main_thread(move || {
let state = app_for_create.state::<MainWindowLifecycleState>();
if let Err(error) = create_main_window(&app_for_create, None, Some(placement)) {
log::warn!("Failed to recreate unresponsive main WebView: {error}");
set_dock_icon_visible(&app_for_create, true);
}
state
.frontend_probe_in_flight
.store(false, Ordering::SeqCst);
});
});
state.frontend_recovery_ready.store(false, Ordering::SeqCst);
state
.destroy_to_tray_in_progress
.store(true, Ordering::SeqCst);
log::warn!("Recreating unresponsive main WebView after resume probe timed out");
if let Err(error) = window.destroy() {
state
.destroy_to_tray_in_progress
.store(false, Ordering::SeqCst);
state
.frontend_probe_in_flight
.store(false, Ordering::SeqCst);
state.frontend_recovery_ready.store(true, Ordering::SeqCst);
log::warn!("Failed to destroy unresponsive main WebView: {error}");
return;
}
std::thread::spawn(move || {
std::thread::sleep(MAIN_WINDOW_RECREATE_DELAY);
let app_for_create = app.clone();
let _ = app.run_on_main_thread(move || {
let state = app_for_create.state::<MainWindowLifecycleState>();
if let Err(error) = create_main_window(&app_for_create, None, Some(placement)) {
log::warn!("Failed to recreate unresponsive main WebView: {error}");
set_dock_icon_visible(&app_for_create, true);
}
state
.destroy_to_tray_in_progress
.store(false, Ordering::SeqCst);
state
.frontend_probe_in_flight
.store(false, Ordering::SeqCst);
});
});
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@wework/src-tauri/src/lib.rs` around lines 2474 - 2505, Clear
destroy_to_tray_in_progress when the delayed recreation flow completes
successfully or fails, after create_main_window is attempted in the
run_on_main_thread closure. Update the recovery path around create_main_window
and retain the existing cleanup for destroy errors, ensuring the flag cannot
remain latched when no ExitRequested event is emitted.

@qdaxb
qdaxb merged commit 4cffb1f into main Jul 25, 2026
33 of 34 checks passed
@qdaxb
qdaxb deleted the fix/wework-lock-screen-recovery branch July 25, 2026 16:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant