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
16 changes: 0 additions & 16 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 0 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -214,7 +214,6 @@ tauri-plugin-autostart = "2.5"
tauri-plugin-notification = "2.3"
tauri-plugin-updater = "2.10"
tauri-plugin-single-instance = "2.4"
tauri-plugin-window-state = "2.4"
notify-rust = { version = "4.18", default-features = false }
tauri-build = { version = "2.6", features = [] }
keepawake = "0.6.0"
Expand Down
9 changes: 9 additions & 0 deletions src/apps/desktop/AGENTS-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ crate;`src/crates/assembly/core` 只保留产品装配与兼容桥接。

- 桌面端专属集成留在这里,不要下沉到共享 core
- 窗口 lifecycle 行为(包括 close/minimize-to-tray 默认值)属于桌面端 surface;修改时必须保留用户已保存偏好。
- `window_state_support` 负责主窗口布局校验,并沿用旧 `.window-state.json` 格式原子保存。
不要同时注册 window-state 插件,其退出时写入的缓存可能覆盖修复结果。

## 命令

Expand Down Expand Up @@ -78,6 +80,13 @@ pnpm run prepare:dsh-profile # 可选:本地 DeepSeek Harness 会话
cargo check -p openbitfun-desktop && cargo test -p openbitfun-desktop
```

窗口布局恢复、旧状态兼容和快照保存使用:

```bash
cargo test -p openbitfun-desktop --lib window_state_support::tests
pnpm --dir src/web-ui run test:run src/app/startup/startupPerformanceContract.test.ts
```

如果改动影响启动、WebDriver、browser/computer-use 或打包行为,还需要运行:

```bash
Expand Down
7 changes: 7 additions & 0 deletions src/apps/desktop/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,9 @@ product wiring and compatibility bridges in `src/crates/assembly/core`.
- Keep desktop-only integrations here; do not move them into shared core
- Window lifecycle behavior, including close/minimize-to-tray defaults, is a
desktop surface concern. Preserve saved user preferences when changing it.
- `window_state_support` owns main-window geometry validation and atomic
persistence in the legacy `.window-state.json` format. Do not reinstall the
window-state plugin alongside it: the plugin's exit cache can overwrite repairs.

## Commands

Expand Down Expand Up @@ -105,6 +108,10 @@ For staged application-update cache and signature behavior, use
`cargo test -p openbitfun-desktop --lib api::update_api::tests`.
For peer system-info response compatibility, run
`cargo test -p openbitfun-desktop --lib system_info_home_contract`.
For window geometry recovery, legacy state compatibility, and snapshot persistence,
run `cargo test -p openbitfun-desktop --lib window_state_support::tests`.
For the matching startup wiring contract, run
`pnpm --dir src/web-ui run test:run src/app/startup/startupPerformanceContract.test.ts`.
After changing updater command registration, also run
`cargo test -p openbitfun-desktop --lib remote_workspace_policy`.

Expand Down
1 change: 0 additions & 1 deletion src/apps/desktop/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,6 @@ tauri-plugin-autostart = { workspace = true }
tauri-plugin-notification = { workspace = true }
tauri-plugin-updater = { workspace = true }
tauri-plugin-single-instance = { workspace = true }
tauri-plugin-window-state = { workspace = true }
keepawake = { workspace = true }
# Keep Tauri's transitive time resolution on the known-good release.
time = { workspace = true }
Expand Down
25 changes: 17 additions & 8 deletions src/apps/desktop/src/appearance.rs
Original file line number Diff line number Diff line change
Expand Up @@ -683,18 +683,18 @@ fn show_main_window_for_startup(
let focus_started_at = Instant::now();
if let Err(error) = window.set_focus() {
warn!("Failed to focus main window during startup: {}", error);
return;
} else {
startup_trace.record_elapsed_step("native_window", "focus_window", focus_started_at);
debug!(
"Main window startup show step completed: step=focus duration_ms={} since_create_start_ms={}",
focus_started_at.elapsed().as_millis(),
total_started_at.elapsed().as_millis()
);
}
startup_trace.record_elapsed_step("native_window", "focus_window", focus_started_at);
debug!(
"Main window startup show step completed: step=focus duration_ms={} since_create_start_ms={}",
focus_started_at.elapsed().as_millis(),
total_started_at.elapsed().as_millis()
);

// Maximize only after the window is visible: maximizing a hidden
// undecorated window on Windows is dropped on show and leaves a bogus
// normal-placement rect behind (see `main_window_restore_flags`).
// normal-placement rect behind (see `window_state_support`).
if reapply_maximized {
match window.is_maximized() {
Ok(true) => {}
Expand Down Expand Up @@ -1024,6 +1024,15 @@ pub async fn hide_agent_companion_desktop_pet(app: tauri::AppHandle) -> Result<(
pub async fn show_main_window(app: tauri::AppHandle) -> Result<(), String> {
let total_started_at = Instant::now();
if let Some(main_window) = app.get_webview_window("main") {
main_window
.unminimize()
.map_err(|error| error.to_string())?;
if let Err(error) = crate::window_state_support::repair_for_activation(&main_window) {
warn!(
"Failed to repair main window geometry during activation: {}",
error
);
}
let step_started_at = Instant::now();
main_window.show().map_err(|e| {
error!("Failed to show main window: {}", e);
Expand Down
189 changes: 17 additions & 172 deletions src/apps/desktop/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,6 @@ use std::sync::{
use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
use tauri::Emitter;
use tauri::Manager;
use tauri_plugin_window_state::{AppHandleExt, StateFlags, WindowExt};

// Re-export API
pub use api::*;
Expand Down Expand Up @@ -288,6 +287,12 @@ fn show_main_window_for_secondary_launch(
main_window
.unminimize()
.map_err(|error| format!("failed to unminimize main window: {}", error))?;
if let Err(error) = window_state_support::repair_for_activation(&main_window) {
log::warn!(
"Failed to repair main window geometry from secondary launch: {}",
error
);
}
main_window
.show()
.map_err(|error| format!("failed to show main window: {}", error))?;
Expand Down Expand Up @@ -329,59 +334,8 @@ pub(crate) fn e2e_storage_guard_enabled() -> bool {
.is_some_and(|value| value == "1" || value.eq_ignore_ascii_case("true"))
}

fn main_window_state_flags() -> StateFlags {
main_window_geometry_state_flags() | StateFlags::MAXIMIZED
}

fn main_window_geometry_state_flags() -> StateFlags {
StateFlags::SIZE | StateFlags::POSITION | StateFlags::FULLSCREEN
}

/// Restore deliberately excludes `MAXIMIZED` on Windows: maximizing a hidden
/// undecorated window does not survive `show()` and leaves Windows tracking a
/// bogus normal-placement rect. Other platforms use the plugin's complete
/// restore behavior.
#[cfg(target_os = "windows")]
fn main_window_restore_flags() -> StateFlags {
main_window_geometry_state_flags()
}

#[cfg(not(target_os = "windows"))]
fn main_window_restore_flags() -> StateFlags {
main_window_state_flags()
}

fn persist_main_window_state(app: &tauri::AppHandle, reason: &str) -> Result<(), String> {
persist_main_window_state_with_flags(app, reason, main_window_state_flags())
}

fn persist_main_window_geometry_state(app: &tauri::AppHandle, reason: &str) -> Result<(), String> {
persist_main_window_state_with_flags(app, reason, main_window_geometry_state_flags())
}

fn persist_main_window_state_with_flags(
app: &tauri::AppHandle,
reason: &str,
flags: StateFlags,
) -> Result<(), String> {
let result = app
.save_window_state(flags)
.map_err(|error| error.to_string());
if let Err(error) = &result {
log::warn!(
"Failed to save main window state: reason={}, error={}",
reason,
error
);
return result;
}

#[cfg(target_os = "windows")]
if flags.contains(StateFlags::MAXIMIZED) {
window_state_support::correct_saved_main_window_state(app);
}

Ok(())
window_state_support::save(app, reason)
}

pub(crate) fn save_main_window_state(app: &tauri::AppHandle, reason: &str) {
Expand Down Expand Up @@ -432,115 +386,8 @@ pub(crate) fn set_main_window_transient_geometry(
})
}

fn has_standard_main_window_size(width: f64, height: f64) -> bool {
width >= MAIN_WINDOW_MIN_WIDTH && height >= MAIN_WINDOW_MIN_HEIGHT
}

pub(crate) fn restore_main_window_state(window: &tauri::WebviewWindow) -> bool {
if let Err(error) = window.restore_state(main_window_restore_flags()) {
log::warn!("Failed to restore main window state: {}", error);
}

#[cfg(target_os = "windows")]
let reapply_maximized =
window_state_support::read_persisted_main_maximized(window.app_handle()).unwrap_or(false);

#[cfg(not(target_os = "windows"))]
let reapply_maximized = false;

let is_maximized = window.is_maximized().unwrap_or(false);
let is_fullscreen = window.is_fullscreen().unwrap_or(false);
if !is_maximized && !is_fullscreen {
match (window.inner_size(), window.scale_factor()) {
(Ok(size), Ok(scale_factor)) => {
let logical_size = size.to_logical::<f64>(scale_factor);
if !has_standard_main_window_size(logical_size.width, logical_size.height) {
log::info!(
"Resetting undersized main window state: width={}, height={}",
logical_size.width,
logical_size.height
);

let resize_result = window.set_size(tauri::LogicalSize::new(
MAIN_WINDOW_DEFAULT_WIDTH,
MAIN_WINDOW_DEFAULT_HEIGHT,
));
let center_result = window.center();
let resize_succeeded = match resize_result {
Ok(()) => true,
Err(error) => {
log::warn!("Failed to reset main window size: {}", error);
false
}
};
if let Err(error) = center_result {
log::warn!("Failed to center reset main window: {}", error);
}
if resize_succeeded {
if let Err(error) = persist_main_window_geometry_state(
window.app_handle(),
"startup_geometry_repair",
) {
log::warn!("Failed to persist repaired main window state: {}", error);
}
}
}
}
(Err(error), _) => {
log::warn!("Failed to read restored main window size: {}", error);
}
(_, Err(error)) => {
log::warn!("Failed to read main window scale factor: {}", error);
}
}
}

if let Err(error) = window.set_min_size(Some(tauri::LogicalSize::new(
MAIN_WINDOW_MIN_WIDTH,
MAIN_WINDOW_MIN_HEIGHT,
))) {
log::warn!("Failed to set main window minimum size: {}", error);
}

reapply_maximized
}

#[cfg(test)]
mod main_window_geometry_tests {
use super::{
has_standard_main_window_size, main_window_geometry_state_flags, main_window_restore_flags,
main_window_state_flags,
};
use tauri_plugin_window_state::StateFlags;

#[test]
fn floating_toolbar_sizes_are_not_valid_main_window_sizes() {
assert!(!has_standard_main_window_size(440.0, 680.0));
assert!(!has_standard_main_window_size(700.0, 140.0));
}

#[test]
fn default_client_size_is_a_valid_main_window_size() {
assert!(has_standard_main_window_size(1200.0, 800.0));
}

#[test]
fn geometry_saves_do_not_overwrite_maximized_state() {
assert!(!main_window_geometry_state_flags().contains(StateFlags::MAXIMIZED));
assert!(main_window_state_flags().contains(StateFlags::MAXIMIZED));
}

#[cfg(target_os = "windows")]
#[test]
fn windows_restore_defers_maximized_state_until_after_show() {
assert!(!main_window_restore_flags().contains(StateFlags::MAXIMIZED));
}

#[cfg(not(target_os = "windows"))]
#[test]
fn non_windows_restore_keeps_plugin_maximized_behavior() {
assert!(main_window_restore_flags().contains(StateFlags::MAXIMIZED));
}
window_state_support::restore(window)
}

#[tauri::command]
Expand Down Expand Up @@ -860,17 +707,9 @@ pub async fn run() {
)
.plugin(tauri_plugin_notification::init())
.plugin(tauri_plugin_updater::Builder::new().build())
.plugin(
tauri_plugin_window_state::Builder::default()
// Restore explicitly after the main window is built, and save
// explicitly at normal-geometry boundaries. Empty automatic
// flags keep toolbar-mode resize/move events out of the
// plugin cache and prevent its exit hook from overwriting the
// last normal main-window geometry.
.with_state_flags(StateFlags::empty())
.with_filter(|label| label == "main")
.build(),
)
// The desktop owns validated snapshots and atomic writes. Do not install
// window-state: its exit hook can overwrite repairs with stale cached data.
.manage(window_state_support::MainWindowState::default())
.manage(app_state)
.manage(sleep_prevention::SleepPreventionState::default())
.manage(desktop_runtime)
Expand Down Expand Up @@ -1344,6 +1183,12 @@ pub async fn run() {
})
.on_window_event({
move |window, event| {
if window.label() == "main"
&& !MAIN_WINDOW_USES_TRANSIENT_GEOMETRY.load(Ordering::SeqCst)
&& matches!(event, tauri::WindowEvent::Moved(_) | tauri::WindowEvent::Resized(_))
{
window_state_support::remember_normal(window);
}
if window.label() == "main"
&& matches!(event, tauri::WindowEvent::CloseRequested { .. })
{
Expand Down
3 changes: 3 additions & 0 deletions src/apps/desktop/src/tray.rs
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,9 @@ pub fn show_main_window(app: &tauri::AppHandle) {
log::warn!("Failed to unminimize main window via tray: {}", error);
return;
}
if let Err(error) = crate::window_state_support::repair_for_activation(&window) {
log::warn!("Failed to repair main window geometry via tray: {}", error);
}
if let Err(error) = window.show() {
log::warn!("Failed to show main window via tray: {}", error);
return;
Expand Down
Loading
Loading