From b83bc4f5e54ca91ecffaf70f7e0679784e19d45c Mon Sep 17 00:00:00 2001 From: Tyler South Date: Sun, 23 Aug 2026 14:32:02 -0400 Subject: [PATCH 1/2] Persist settings window geometry --- CHANGELOG.md | 2 +- apps/desktop-tauri/src-tauri/src/main.rs | 3 + .../src-tauri/src/shell/settings_window.rs | 57 +++++++++++++++---- .../src-tauri/src/shell/tests.rs | 45 +++++++++++++++ .../src/surfaces/Settings.test.ts | 8 +++ apps/desktop-tauri/src/surfaces/Settings.tsx | 43 +------------- 6 files changed, 105 insertions(+), 53 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6af902add..c5c7789b6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ - **A leaked `serve.token` is rotated on Windows, not just Unix.** After SBS-953, a world-readable token was replaced on Unix, but Windows still tightened the DACL and reused the same secret. The ACL is now inspected before tightening; if anyone other than the current user, SYSTEM, or Administrators can read the file, the token is replaced. Closes SBS-1043. ### Fixed +- **The detached Settings window now reopens where you left it.** Its saved size and position are restored and clamped on screen instead of being overwritten by a second frontend resize on every open. Closes #275. - **Loading settings no longer rewrites another install's start-at-login command.** Every `Settings::load` repaired `HKCU\...\Run\Ceiling` whenever the value was not the quoted path of this process. A portable CLI or a second tree therefore replaced the installed desktop's startup entry, and any extra arguments were stripped. Repair now runs only when this process owns that entry — the same intended exe, or a stale `codexbar-cli.exe` / `codexbar-desktop.exe` sibling in the same directory — and leaves custom arguments and other trees alone. Closes SBS-1053. - **MCP `get_status` no longer hides an exhausted Weekly behind a healthy session.** Top-level `remaining_percent` copied only `usage.primary`, so a Claude/Codex 5-hour window with room made the advertised cap-check sink look fine while Weekly was already at 100%. It now uses the same constraining-window ranking as the desktop strip across primary, secondary, and tertiary (exhausted first, then highest used %). Closes SBS-1055. - **Remembered window positions no longer drop a sibling when two surfaces save at once.** `window_geometry.json` was updated with an unlocked read-modify-write, so moving Settings while the float bar or Pop Out also wrote could replace the file with a snapshot that had never seen the other key. Geometry persist now holds the same cross-process state lock as settings and credentials. Closes SBS-1024. @@ -1561,4 +1562,3 @@ First stable release of Ceiling for Windows. - Async off-main log parsing for responsiveness; strict-concurrency build flags enabled. - Packaging + signing/notarization scripts (arm64); build scripts convert `.icon` bundle to `.icns`. - diff --git a/apps/desktop-tauri/src-tauri/src/main.rs b/apps/desktop-tauri/src-tauri/src/main.rs index 2b98b036c..2d21aef7c 100644 --- a/apps/desktop-tauri/src-tauri/src/main.rs +++ b/apps/desktop-tauri/src-tauri/src/main.rs @@ -298,6 +298,9 @@ fn main() { if shell::flyout_window::handle_window_event(window, event) { return; } + if shell::settings_window::handle_window_event(window, event) { + return; + } // Only the main window participates in blur-dismiss and close-to-hide. // The detached settings window uses normal OS close behavior. if window.label() != "main" { diff --git a/apps/desktop-tauri/src-tauri/src/shell/settings_window.rs b/apps/desktop-tauri/src-tauri/src/shell/settings_window.rs index 89ce28573..22df7170a 100644 --- a/apps/desktop-tauri/src-tauri/src/shell/settings_window.rs +++ b/apps/desktop-tauri/src-tauri/src/shell/settings_window.rs @@ -3,6 +3,8 @@ use tauri::{Emitter, Manager, PhysicalPosition, WebviewUrl}; +use crate::surface::SurfaceMode; + const SETTINGS_LABEL: &str = "settings"; const SETTINGS_WIDTH: f64 = 720.0; const SETTINGS_HEIGHT: f64 = 580.0; @@ -22,9 +24,17 @@ pub fn open_or_focus(app: &tauri::AppHandle, tab: &str) -> Result<(), String> { let url = WebviewUrl::App(format!("index.html?window=settings&tab={tab}").into()); + let stored = crate::geometry_store::load(SurfaceMode::Settings); + let width = stored + .and_then(|geometry| geometry.width) + .map_or(SETTINGS_WIDTH, f64::from); + let height = stored + .and_then(|geometry| geometry.height) + .map_or(SETTINGS_HEIGHT, f64::from); + let win = tauri::WebviewWindowBuilder::new(app, SETTINGS_LABEL, url) .title("Ceiling Settings") - .inner_size(SETTINGS_WIDTH, SETTINGS_HEIGHT) + .inner_size(width, height) .decorations(false) .shadow(false) .theme(Some(tauri::Theme::Dark)) @@ -35,22 +45,47 @@ pub fn open_or_focus(app: &tauri::AppHandle, tab: &str) -> Result<(), String> { // Force DWM caption to dark; keep WS_THICKFRAME since window is resizable super::dwm::force_dark_caption_resizable(&win); - // Manually center: Tauri's .center() is unreliable on Windows when - // called from async commands. Compute position from the primary monitor. - if let Ok(Some(monitor)) = win.primary_monitor() { - let pos = monitor.position(); - let size = monitor.size(); - let scale = win.scale_factor().unwrap_or(1.0); - let win_w = (SETTINGS_WIDTH * scale) as i32; - let win_h = (SETTINGS_HEIGHT * scale) as i32; - let x = pos.x + (size.width as i32 - win_w) / 2; - let y = pos.y + (size.height as i32 - win_h) / 2; + if let Some((x, y)) = super::position::default_surface_position(app, SurfaceMode::Settings) { let _ = win.set_position(PhysicalPosition::new(x, y)); } Ok(()) } +/// Persist move and resize events for the detached Settings window. +pub fn handle_window_event(window: &tauri::Window, event: &tauri::WindowEvent) -> bool { + if window.label() != SETTINGS_LABEL { + return false; + } + + if matches!( + event, + tauri::WindowEvent::Moved(_) | tauri::WindowEvent::Resized(_) + ) && !window.is_maximized().unwrap_or(false) + && !window.is_minimized().unwrap_or(false) + && let Ok(position) = window.outer_position() + { + let scale = window.scale_factor().unwrap_or(1.0).max(1.0); + let logical_size = window.outer_size().ok().map(|size| { + ( + (size.width as f64 / scale).round().max(1.0) as u32, + (size.height as f64 / scale).round().max(1.0) as u32, + ) + }); + crate::geometry_store::save( + SurfaceMode::Settings, + crate::geometry_store::StoredGeometry { + x: position.x, + y: position.y, + width: logical_size.map(|size| size.0), + height: logical_size.map(|size| size.1), + }, + ); + } + + true +} + /// Dismiss Settings without exiting CodexBar. /// /// The detached Settings window is hidden instead of closed so Tauri's diff --git a/apps/desktop-tauri/src-tauri/src/shell/tests.rs b/apps/desktop-tauri/src-tauri/src/shell/tests.rs index 43609da65..2daeecb27 100644 --- a/apps/desktop-tauri/src-tauri/src/shell/tests.rs +++ b/apps/desktop-tauri/src-tauri/src/shell/tests.rs @@ -895,6 +895,51 @@ fn remembered_popout_position_clamps_using_stored_size() { assert_eq!(position, Some((392, 292))); } +#[test] +fn remembered_settings_position_clamps_using_stored_size() { + let monitor = MonitorPlacement { + bounds: Rect { + x: 0, + y: 0, + width: 1000, + height: 800, + }, + work_area: Rect { + x: 0, + y: 0, + width: 1000, + height: 760, + }, + scale_factor: 1.0, + }; + let stored = crate::geometry_store::StoredGeometry { + x: 900, + y: 700, + width: Some(720), + height: Some(580), + }; + + let position = + remembered_surface_position_with_monitors(SurfaceMode::Settings, stored, &[monitor], None); + + assert_eq!(position, Some((272, 172))); +} + +#[test] +fn remembered_panel_size_uses_stored_settings_size() { + let stored = crate::geometry_store::StoredGeometry { + x: 0, + y: 0, + width: Some(840), + height: Some(680), + }; + + let size = remembered_panel_size(SurfaceMode::Settings, stored); + + assert_eq!(size.width, 840); + assert_eq!(size.height, 680); +} + #[test] fn remembered_panel_size_uses_stored_popout_size() { let stored = crate::geometry_store::StoredGeometry { diff --git a/apps/desktop-tauri/src/surfaces/Settings.test.ts b/apps/desktop-tauri/src/surfaces/Settings.test.ts index dec41d93e..0d8f6b1cd 100644 --- a/apps/desktop-tauri/src/surfaces/Settings.test.ts +++ b/apps/desktop-tauri/src/surfaces/Settings.test.ts @@ -1,4 +1,6 @@ import { describe, expect, it } from "vitest"; +import { readFileSync } from "node:fs"; +import { resolve } from "node:path"; import { TAB_META } from "./Settings"; /** Live Settings shell tabs, in render order. SBS-872. */ @@ -27,4 +29,10 @@ describe("Settings navigation", () => { // to the Rust allowlist and the SettingsTabId union. expect(TAB_META.map((tab) => tab.id)).toEqual([...LIVE_SETTINGS_TABS]); }); + + it("does not overwrite restored window geometry on mount", () => { + const source = readFileSync(resolve(process.cwd(), "src/surfaces/Settings.tsx"), "utf8"); + expect(source).not.toContain("setSize("); + expect(source).not.toContain("setPosition("); + }); }); diff --git a/apps/desktop-tauri/src/surfaces/Settings.tsx b/apps/desktop-tauri/src/surfaces/Settings.tsx index 5974ec77a..10dc6ac7e 100644 --- a/apps/desktop-tauri/src/surfaces/Settings.tsx +++ b/apps/desktop-tauri/src/surfaces/Settings.tsx @@ -1,5 +1,5 @@ import { useCallback, useEffect, useState, type ReactElement, type ReactNode } from "react"; -import { getCurrentWindow, LogicalPosition, LogicalSize } from "@tauri-apps/api/window"; +import { getCurrentWindow } from "@tauri-apps/api/window"; import { getCurrentWebviewWindow } from "@tauri-apps/api/webviewWindow"; import type { BootstrapState, @@ -11,7 +11,7 @@ import { useSurfaceTarget } from "../hooks/useSurfaceMode"; import { useLocale } from "../hooks/useLocale"; import { useTabListKeyboard } from "../hooks/useTabListKeyboard"; import type { LocaleKey } from "../i18n/keys"; -import { closeSettingsWindow, getWorkAreaRect, setSurfaceMode } from "../lib/tauri"; +import { closeSettingsWindow, setSurfaceMode } from "../lib/tauri"; import GeneralTab from "./settings/tabs/GeneralTab"; import DisplayTab from "./settings/tabs/DisplayTab"; import AdvancedTab from "./settings/tabs/AdvancedTab"; @@ -117,41 +117,6 @@ function isSettingsTab(value: string): value is SettingsTab { return TAB_META.some((t) => t.id === value); } -const SETTINGS_WINDOW_HEIGHT = 580; -const SETTINGS_WINDOW_WIDTH = 600; - -async function applySettingsWindowSize() { - const workArea = await getWorkAreaRect().catch(() => null); - const screenWidth = window.screen.availWidth || window.innerWidth || SETTINGS_WINDOW_WIDTH; - const screenHeight = window.screen.availHeight || window.innerHeight || SETTINGS_WINDOW_HEIGHT; - const maxWidth = Math.min(workArea?.width ?? screenWidth, screenWidth); - const maxHeight = Math.min(workArea?.height ?? screenHeight, screenHeight); - const width = Math.max( - 360, - Math.min(SETTINGS_WINDOW_WIDTH, maxWidth - 16), - ); - const height = Math.max( - 360, - Math.min(SETTINGS_WINDOW_HEIGHT, maxHeight - 16), - ); - const win = getCurrentWindow(); - await win.setSize(new LogicalSize(width, height)).catch(() => {}); - const screenOrigin = window.screen as Screen & { - availLeft?: number; - availTop?: number; - }; - const left = screenOrigin.availLeft ?? workArea?.x ?? 0; - const top = screenOrigin.availTop ?? workArea?.y ?? 0; - await win - .setPosition( - new LogicalPosition( - left + Math.max(8, Math.round((screenWidth - width) / 2)), - top + Math.max(8, Math.round((screenHeight - height) / 2)), - ), - ) - .catch(() => {}); -} - export default function Settings({ state, initialTab: propTab }: { state: BootstrapState; initialTab?: string }) { const { settings, saving, error, update } = useSettings(state.settings); const { t } = useLocale(); @@ -164,10 +129,6 @@ export default function Settings({ state, initialTab: propTab }: { state: Bootst : "general"; const [activeTab, setActiveTab] = useState(initialTab); - useEffect(() => { - void applySettingsWindowSize(); - }, []); - // Respond to prop-driven tab changes (detached window re-focus events). useEffect(() => { if (propTab && isSettingsTab(propTab)) { From fc4b9dc9017f060b7734b9f1255eea18be4b4948 Mon Sep 17 00:00:00 2001 From: Tyler South Date: Sun, 23 Aug 2026 14:43:19 -0400 Subject: [PATCH 2/2] Keep settings geometry test browser-compatible --- apps/desktop-tauri/src/surfaces/Settings.test.ts | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/apps/desktop-tauri/src/surfaces/Settings.test.ts b/apps/desktop-tauri/src/surfaces/Settings.test.ts index 0d8f6b1cd..eef01d99c 100644 --- a/apps/desktop-tauri/src/surfaces/Settings.test.ts +++ b/apps/desktop-tauri/src/surfaces/Settings.test.ts @@ -1,7 +1,6 @@ import { describe, expect, it } from "vitest"; -import { readFileSync } from "node:fs"; -import { resolve } from "node:path"; import { TAB_META } from "./Settings"; +import settingsSource from "./Settings.tsx?raw"; /** Live Settings shell tabs, in render order. SBS-872. */ const LIVE_SETTINGS_TABS = [ @@ -31,8 +30,7 @@ describe("Settings navigation", () => { }); it("does not overwrite restored window geometry on mount", () => { - const source = readFileSync(resolve(process.cwd(), "src/surfaces/Settings.tsx"), "utf8"); - expect(source).not.toContain("setSize("); - expect(source).not.toContain("setPosition("); + expect(settingsSource).not.toContain("setSize("); + expect(settingsSource).not.toContain("setPosition("); }); });