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 src/apps/cli/src/peer_host/deny.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ static LOCAL_ONLY_COMMANDS: &[&str] = &[
"initialize_tray_after_startup",
"startup_window_control",
"toggle_main_window_fullscreen",
"set_main_window_transient_geometry",
"get_prevent_sleep_enabled",
"set_prevent_sleep_enabled",
"restart_app",
Expand Down
1 change: 1 addition & 0 deletions src/apps/desktop/src/api/peer_host_invoke.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ static LOCAL_ONLY_COMMANDS: &[&str] = &[
"initialize_tray_after_startup",
"startup_window_control",
"toggle_main_window_fullscreen",
"set_main_window_transient_geometry",
"get_prevent_sleep_enabled",
"set_prevent_sleep_enabled",
"restart_app",
Expand Down
4 changes: 4 additions & 0 deletions src/apps/desktop/src/api/remote_workspace_policy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1516,6 +1516,10 @@ pub const REMOTE_WORKSPACE_COMMAND_POLICIES: &[(&str, RemoteWorkspacePolicy)] =
RemoteWorkspacePolicy::RemoteUnsupported,
),
("set_macos_edit_menu_mode", RemoteWorkspacePolicy::LocalOnly),
(
"set_main_window_transient_geometry",
RemoteWorkspacePolicy::LocalOnly,
),
(
"set_prevent_sleep_enabled",
RemoteWorkspacePolicy::LocalOnly,
Expand Down
37 changes: 34 additions & 3 deletions src/apps/desktop/src/api/system_api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,10 @@ async fn probe_endpoint_throughput(client: &reqwest::Client, url: &str) -> u64 {
let started = std::time::Instant::now();
let request = client
.get(url)
.header(reqwest::header::RANGE, format!("bytes=0-{}", PROBE_BYTES - 1))
.header(
reqwest::header::RANGE,
format!("bytes=0-{}", PROBE_BYTES - 1),
)
.send();
let Ok(Ok(response)) = tokio::time::timeout(PROBE_WINDOW, request).await else {
return 0;
Expand Down Expand Up @@ -168,7 +171,10 @@ async fn ranked_updater(app: &AppHandle) -> Result<tauri_plugin_updater::Updater
let builder = match builder.endpoints(endpoints) {
Ok(builder) => builder,
Err(error) => {
log::warn!("Updater endpoint ranking rejected, using bundled order: {}", error);
log::warn!(
"Updater endpoint ranking rejected, using bundled order: {}",
error
);
app.updater_builder()
}
};
Expand Down Expand Up @@ -590,12 +596,33 @@ fn read_main_window_fullscreen_response(

// ─── Window / Tray behavior commands ─────────────────────────────────────────

#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SetMainWindowTransientGeometryRequest {
pub transient: bool,
}

/// Mark whether the shared main window currently uses toolbar-mode geometry.
///
/// Entering captures the latest normal bounds before the frontend resizes the
/// native window. Leaving persists the restored normal bounds. While transient
/// geometry is active, all process-exit save paths retain the captured normal
/// state instead of the floating-window state.
#[tauri::command]
pub async fn set_main_window_transient_geometry(
app: tauri::AppHandle,
request: SetMainWindowTransientGeometryRequest,
) -> Result<(), String> {
crate::set_main_window_transient_geometry(&app, request.transient)
}

/// Immediately exit the application (used by the "ask" dialog when the user
/// chooses to quit rather than minimize to tray).
#[tauri::command]
pub async fn quit_app(app: tauri::AppHandle) -> Result<(), String> {
log::info!("Quit requested via quit_app command");
crate::crash_diagnostics::mark_clean_shutdown("quit_app_command");
crate::save_main_window_state(&app);
crate::perform_process_exit_cleanup();
app.exit(0);
Ok(())
Expand Down Expand Up @@ -667,6 +694,7 @@ pub async fn startup_window_control(
if behavior == "quit" {
log::info!("Quit requested from startup window control");
crate::crash_diagnostics::mark_clean_shutdown("startup_window_control");
crate::save_main_window_state(&app);
crate::perform_process_exit_cleanup();
app.exit(0);
} else {
Expand Down Expand Up @@ -843,7 +871,10 @@ mod tests {
"unexpected updater arch segment: {arch}"
);
#[cfg(target_os = "macos")]
assert!(key.starts_with("darwin-"), "macOS must map to darwin, got {key}");
assert!(
key.starts_with("darwin-"),
"macOS must map to darwin, got {key}"
);
}

use super::*;
Expand Down
139 changes: 136 additions & 3 deletions src/apps/desktop/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ use std::sync::{
use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
use tauri::Emitter;
use tauri::Manager;
use tauri_plugin_window_state::{AppHandleExt, StateFlags};
use tauri_plugin_window_state::{AppHandleExt, StateFlags, WindowExt};

// Re-export API
pub use api::*;
Expand Down Expand Up @@ -113,6 +113,15 @@ static MAIN_WINDOW_CLOSE_PENDING_ON_MACOS: AtomicBool = AtomicBool::new(false);
const MAIN_WINDOW_CLOSE_REQUESTED_EVENT: &str = "bitfun_main_window_close_requested";
const BROWSER_WEBVIEW_PAGE_LOAD_EVENT: &str = "browser-webview-page-load";
const CRON_DESKTOP_START_FALLBACK_DELAY: Duration = Duration::from_secs(120);
pub(crate) const MAIN_WINDOW_DEFAULT_WIDTH: f64 = 1200.0;
pub(crate) const MAIN_WINDOW_DEFAULT_HEIGHT: f64 = 800.0;
pub(crate) const MAIN_WINDOW_MIN_WIDTH: f64 = 800.0;
pub(crate) const MAIN_WINDOW_MIN_HEIGHT: f64 = 600.0;

// Toolbar mode temporarily morphs the main window into a compact floating
// surface. Its geometry must never replace the normal main-window geometry
// restored on the next process start.
static MAIN_WINDOW_USES_TRANSIENT_GEOMETRY: AtomicBool = AtomicBool::new(false);

#[cfg(target_os = "macos")]
const MAIN_WINDOW_CLOSE_FALLBACK_HIDE_MS: u64 = 2_500;
Expand Down Expand Up @@ -277,12 +286,129 @@ fn main_window_state_flags() -> StateFlags {
StateFlags::SIZE | StateFlags::POSITION | StateFlags::MAXIMIZED | StateFlags::FULLSCREEN
}

fn persist_main_window_state(app: &tauri::AppHandle) -> Result<(), String> {
app.save_window_state(main_window_state_flags())
.map_err(|error| error.to_string())
}

pub(crate) fn save_main_window_state(app: &tauri::AppHandle) {
if let Err(error) = app.save_window_state(main_window_state_flags()) {
if MAIN_WINDOW_USES_TRANSIENT_GEOMETRY.load(Ordering::SeqCst) {
log::debug!("Skipped saving transient main window geometry");
return;
}

if let Err(error) = persist_main_window_state(app) {
log::warn!("Failed to save main window state: {}", error);
}
}

pub(crate) fn set_main_window_transient_geometry(
app: &tauri::AppHandle,
transient: bool,
) -> Result<(), String> {
if transient {
if MAIN_WINDOW_USES_TRANSIENT_GEOMETRY.load(Ordering::SeqCst) {
return Ok(());
}

// Capture the latest normal bounds before toolbar mode starts resizing
// the shared native window.
persist_main_window_state(app).map_err(|error| {
format!(
"Failed to save main window state before transient geometry: {}",
error
)
})?;
MAIN_WINDOW_USES_TRANSIENT_GEOMETRY.store(true, Ordering::SeqCst);
return Ok(());
}

MAIN_WINDOW_USES_TRANSIENT_GEOMETRY.store(false, Ordering::SeqCst);
persist_main_window_state(app).map_err(|error| {
format!(
"Failed to save restored main window state after transient geometry: {}",
error
)
})
}

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) {
if let Err(error) = window.restore_state(main_window_state_flags()) {
log::warn!("Failed to restore main window state: {}", error);
}

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_state(window.app_handle()) {
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);
}
}

#[cfg(test)]
mod main_window_geometry_tests {
use super::has_standard_main_window_size;

#[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));
}
}

#[tauri::command]
async fn webdriver_bridge_result(request: WebdriverBridgeResultRequest) -> Result<(), String> {
log::debug!("webdriver_bridge_result command invoked");
Expand Down Expand Up @@ -510,7 +636,12 @@ pub async fn run() {
.plugin(tauri_plugin_updater::Builder::new().build())
.plugin(
tauri_plugin_window_state::Builder::default()
.with_state_flags(main_window_state_flags())
// 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(),
)
Expand Down Expand Up @@ -1406,6 +1537,7 @@ pub async fn run() {
api::system_api::minimize_to_tray,
api::system_api::initialize_tray_after_startup,
api::system_api::startup_window_control,
api::system_api::set_main_window_transient_geometry,
api::system_api::toggle_main_window_fullscreen,
sleep_prevention::get_prevent_sleep_enabled,
sleep_prevention::set_prevent_sleep_enabled,
Expand Down Expand Up @@ -1603,6 +1735,7 @@ pub async fn run() {
app.run(|_app_handle, event| match event {
tauri::RunEvent::ExitRequested { .. } | tauri::RunEvent::Exit => {
crash_diagnostics::mark_clean_shutdown("tauri_run_exit");
save_main_window_state(_app_handle);
perform_process_exit_cleanup();
}
#[cfg(target_os = "macos")]
Expand Down
6 changes: 5 additions & 1 deletion src/apps/desktop/src/theme.rs
Original file line number Diff line number Diff line change
Expand Up @@ -476,7 +476,10 @@ pub fn create_main_window(
#[allow(unused_mut)]
let mut builder = tauri::WebviewWindowBuilder::new(app_handle, "main", main_url)
.title("BitFun")
.inner_size(1200.0, 800.0)
.inner_size(
crate::MAIN_WINDOW_DEFAULT_WIDTH,
crate::MAIN_WINDOW_DEFAULT_HEIGHT,
)
.center()
.resizable(true)
.fullscreen(false)
Expand Down Expand Up @@ -529,6 +532,7 @@ pub fn create_main_window(
let build_started_at = Instant::now();
match builder.build() {
Ok(window) => {
crate::restore_main_window_state(&window);
startup_trace.record_elapsed_step("native_window", "webview_build", build_started_at);
debug!(
"Main window creation step completed: step=build url_kind={} duration_ms={} total_duration_ms={}",
Expand Down
1 change: 1 addition & 0 deletions src/apps/desktop/src/tray.rs
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,7 @@ pub fn setup_tray(
} else if id == "quit" {
log::info!("Quit requested from tray menu");
crate::crash_diagnostics::mark_clean_shutdown("tray_quit");
crate::save_main_window_state(app);
crate::perform_process_exit_cleanup();
app.exit(0);
} else if id == "toggle_desktop_pet" {
Expand Down
30 changes: 28 additions & 2 deletions src/web-ui/src/app/startup/startupPerformanceContract.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -168,21 +168,47 @@ describe('startup performance contract', () => {
);
});

it('centers the first main window and persists geometry before close handling', () => {
it('restores only normal main-window geometry and repairs legacy floating sizes', () => {
const desktopThemeSource = readSource('../../../../apps/desktop/src/theme.rs');
const desktopLibSource = readSource('../../../../apps/desktop/src/lib.rs');
const toolbarModeProviderSource = readSource(
'../../flow_chat/components/toolbar-mode/ToolbarModeProvider.tsx'
);
const windowEventStart = desktopLibSource.indexOf('.on_window_event({');
const invokeHandlerStart = desktopLibSource.indexOf('.invoke_handler(', windowEventStart);
const windowEventSource = desktopLibSource.slice(windowEventStart, invokeHandlerStart);

expect(desktopThemeSource).toContain('.inner_size(1200.0, 800.0)\n .center()');
expect(desktopThemeSource).toContain('crate::MAIN_WINDOW_DEFAULT_WIDTH');
expect(desktopThemeSource).toContain('crate::restore_main_window_state(&window)');
expect(desktopThemeSource).not.toContain('windows_maximize_show_wait_action');
expect(desktopLibSource).toContain('tauri_plugin_window_state::Builder::default()');
expect(desktopLibSource).toContain('.with_state_flags(StateFlags::empty())');
expect(desktopLibSource).toContain('.with_filter(|label| label == "main")');
expect(desktopLibSource).toContain('Resetting undersized main window state');
expect(desktopLibSource).toContain('MAIN_WINDOW_USES_TRANSIENT_GEOMETRY');
expect(toolbarModeProviderSource).not.toContain(
"import { systemAPI } from '@/infrastructure/api/service-api/SystemAPI'"
);
expect(toolbarModeProviderSource).toContain(
"await import('@/infrastructure/api/service-api/SystemAPI')"
);
expect(toolbarModeProviderSource).toContain('win.innerSize()');
expect(toolbarModeProviderSource).not.toContain('win.outerSize(),');
expect(windowEventStart).toBeGreaterThan(-1);
expect(invokeHandlerStart).toBeGreaterThan(windowEventStart);
expect(windowEventSource).toContain('matches!(event, tauri::WindowEvent::CloseRequested { .. })');
expect(windowEventSource).toContain('save_main_window_state(window.app_handle())');
expect(toolbarModeProviderSource).toContain(
'setMainWindowTransientGeometry(true)'
);
expect(toolbarModeProviderSource).toContain(
'setMainWindowTransientGeometry(false)'
);
expect(
toolbarModeProviderSource.indexOf('setMainWindowTransientGeometry(true)')
).toBeLessThan(
toolbarModeProviderSource.indexOf('win.setSize(new PhysicalSize(geometry.width')
);
});

it('keeps system tray creation out of the synchronous Tauri setup path', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,8 @@ export const TOOLBAR_COMPACT_MIN = { width: 400, height: 100 };
/** Matches the floating chat bubble panel ($panel-width/$panel-height). */
export const TOOLBAR_EXPANDED_SIZE = { width: 440, height: 680 };
export const TOOLBAR_EXPANDED_MIN = { width: 400, height: 500 };
export const MAIN_WINDOW_DEFAULT_SIZE = { width: 1200, height: 800 };
export const MAIN_WINDOW_MIN_SIZE = { width: 800, height: 600 };

export const ToolbarModeContext = createContext<ToolbarModeContextType | undefined>(undefined);

Expand Down
Loading