diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index bc1a32d..a63f2ab 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -9,6 +9,8 @@ mod local_proxy_gateway; mod tray; use tauri::{Emitter, Listener, Manager}; +// UX-P2 走查⑥:首次关窗托盘化提示走 OS 系统通知(应用内气泡窗口即隐看不见),需 NotificationExt +use tauri_plugin_notification::NotificationExt; use state::AppState; @@ -31,6 +33,11 @@ use df_tunnel::TunnelClient; /// 关窗进行中提示哨兵:tray-hide-notice 每进程只发一次(反复关窗不重复弹气泡)。 static CLOSE_NOTICE_SENT: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false); +/// 首次关窗托盘化说明的持久化标记文件名(app_data_dir 下;存在 = 历史上已提示过,不再弹)。 +/// 用文件而非进程哨兵:解释一次即够,重启后不必再打扰;写入与通知成败解耦(先占坑语义), +/// 防通知通道异常(如无系统通知权限)导致每次关窗都尝试弹。 +const TRAY_NOTICE_SENT_MARKER: &str = "tray-hide-notice-sent"; + #[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { // 初始化文件日志:追加到 %TEMP%/devflow-trace.log,RUST_LOG 控制级别(默认 info) @@ -521,6 +528,9 @@ pub fn run() { // 真正关闭,否则 close 被吞成隐藏僵尸窗,浮窗「正在关闭…」卡死、新审批推给不可见窗口。 // P1 bg 化:有 AI 会话运行中时,首次关窗 emit tray-hide-notice 提示后台仍在跑(每进程一次), // 异步等 2 秒再 hide(同步 sleep 卡主线程渲染,气泡画不出来);无运行会话保持立即 hide 零回归。 + // UX-P2 走查⑥:无会话关窗历史上零提示,用户误以为已退出。窗口随即隐藏,应用内气泡画了也 + // 看不见,且前端气泡通道仅 running>0 才渲染,故走 OS 系统通知(托盘旁 toast)补位, + // 以持久化标记全生命周期只解释一次(解释性提示,与上方每进程一次的会话提醒互不替代)。 .on_window_event(|window, event| { if let tauri::WindowEvent::CloseRequested { api, .. } = event { if window.label() != "main" { @@ -530,6 +540,24 @@ pub fn run() { let app = window.app_handle().clone(); let active = app.state::().conv_states.active_convs().len(); if active == 0 { + // 首次(全历史一次)关窗托盘化:弹一次系统通知说明「已最小化到托盘」 + match app.path().app_data_dir() { + Ok(dir) => { + let marker = dir.join(TRAY_NOTICE_SENT_MARKER); + if !marker.exists() { + let _ = app + .notification() + .builder() + .title("DevFlow") + .body("已最小化到系统托盘,程序仍在后台运行;托盘图标可随时唤起,托盘菜单「退出」才真正结束") + .show(); + let _ = std::fs::write(&marker, b"1"); + } + } + Err(e) => { + tracing::warn!("[tray] app_data_dir 解析失败,跳过首次关窗托盘提示: {}", e); + } + } let _ = window.hide(); // 无运行会话:隐藏到托盘(原行为) return; }