From c2dd4599faff6694c4cec24719ee31d2faf1624e Mon Sep 17 00:00:00 2001 From: LeonardW-sl <105418399+LeonardW-sl@users.noreply.github.com> Date: Fri, 7 Aug 2026 08:51:21 +0800 Subject: [PATCH 1/2] fix(linux): allow hide-to-tray when StatusNotifierWatcher is available MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit On Linux, Tauri's tray build() succeeds even when the desktop session does not provide a StatusNotifierWatcher (notably GNOME 45+ without an AppIndicator extension). In that case the tray icon is silently invisible and hiding the main window would leave the user with no way to recover it. Previously codeg avoided this by unconditionally returning false from can_hide_to_tray() on Linux, which prevented hide-to-tray even on KDE, XFCE, Cinnamon, Budgie, and GNOME-with-AppIndicator — all of which have a working tray. Fix: detect the actual tray availability at install_tray_icon() time by querying D-Bus for org.kde.StatusNotifierWatcher. Only set TRAY_AVAILABLE when the service is present, so the close handler hides the window on fully capable desktops and exits otherwise. --- src-tauri/src/commands/windows.rs | 58 +++++++++++++++++++++++-------- 1 file changed, 44 insertions(+), 14 deletions(-) diff --git a/src-tauri/src/commands/windows.rs b/src-tauri/src/commands/windows.rs index 171dbe8b1..e2ceebca3 100644 --- a/src-tauri/src/commands/windows.rs +++ b/src-tauri/src/commands/windows.rs @@ -1915,29 +1915,44 @@ pub const TRAY_MENU_ID_SHOW: &str = "tray:show"; pub const TRAY_MENU_ID_QUIT: &str = "tray:quit"; pub const TRAY_ICON_ID: &str = "codeg-tray"; -/// True after `install_tray_icon` returns `Ok`. The hide-on-close path -/// in `lib.rs` consults this so we don't strand the user on systems -/// where the tray failed to install (Windows tray refused, etc.). On -/// Linux this is necessary-but-not-sufficient: the StatusNotifierWatcher -/// may be missing and the icon invisible even when build() returns Ok, -/// which is why `can_hide_to_tray()` reports false on Linux regardless. +/// True after `install_tray_icon` finishes successfully and the tray is +/// actually usable (on Linux, that requires a running +/// StatusNotifierWatcher — see `linux_status_notifier_available`). +/// The hide-on-close path in `lib.rs` consults this to avoid stranding +/// the user when the tray is unavailable. #[cfg(feature = "tauri-runtime")] static TRAY_AVAILABLE: AtomicBool = AtomicBool::new(false); +/// On Linux, Tauri's tray `build()` can succeed even when no +/// StatusNotifierWatcher is available (notably GNOME 45+ without an +/// AppIndicator extension). In that case the icon is silently invisible +/// and hiding the window would strand the user, so only treat the tray +/// as available when the desktop actually provides the D-Bus service. +#[cfg(all(target_os = "linux", feature = "tauri-runtime"))] +fn linux_status_notifier_available() -> bool { + crate::process::std_command("gdbus") + .args([ + "call", + "--session", + "--dest", + "org.freedesktop.DBus", + "--object-path", + "/org/freedesktop/DBus", + "--method", + "org.freedesktop.DBus.NameHasOwner", + "org.kde.StatusNotifierWatcher", + ]) + .output() + .map(|out| out.status.success() && String::from_utf8_lossy(&out.stdout).contains("true")) + .unwrap_or(false) +} + /// Whether hide-on-close is safe on this platform/session. When false, /// the close handler in `lib.rs` forces a real app exit instead — both /// `hide()` and `minimize()` would leave aux windows (pet, settings) /// running without a recoverable workspace. #[cfg(feature = "tauri-runtime")] pub fn can_hide_to_tray() -> bool { - // Linux: even with a successfully installed tray icon, modern GNOME - // (45+) defaults ship without a StatusNotifierWatcher and the icon - // is silently invisible. Refusing here forces the close to pass - // through to a real exit on Linux — preferable to a phantom process - // with no UI surface. - if cfg!(target_os = "linux") { - return false; - } TRAY_AVAILABLE.load(AtomicOrdering::Relaxed) } @@ -2082,6 +2097,21 @@ pub fn install_tray_icon( }) .build(app)?; + // On Linux, verify the tray is actually visible before trusting it. + // Tauri's tray build() succeeds even when StatusNotifierWatcher is + // absent (GNOME 45+), leaving the icon invisible. Detect that case + // and leave TRAY_AVAILABLE false so the close handler exits instead + // of hiding the window with no way to bring it back. + #[cfg(target_os = "linux")] + if !linux_status_notifier_available() { + tracing::warn!( + "[Tray] StatusNotifierWatcher not found — hiding on close will be disabled" + ); + } else { + TRAY_AVAILABLE.store(true, AtomicOrdering::Relaxed); + } + + #[cfg(not(target_os = "linux"))] TRAY_AVAILABLE.store(true, AtomicOrdering::Relaxed); Ok(()) } From 867365a9737e283f8ca9abb4e8c223752b34e4f4 Mon Sep 17 00:00:00 2001 From: LeonardW-sl <105418399+LeonardW-sl@users.noreply.github.com> Date: Fri, 7 Aug 2026 15:18:26 +0800 Subject: [PATCH 2/2] feat: add configurable close behavior (hide to tray / exit) with settings UI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a new setting to let users choose what happens when the main window close button is clicked: - Hide to tray (background) — default. Window hides to system tray if available; falls back to exit if no tray is present. - Exit application — always exits on close, regardless of tray. Backend changes: - New CloseAction enum + SystemCloseSettings model - load_system_close_settings/get_system_close_settings/update_system_close_settings - Close button handler reads stored setting instead of checking can_hide_to_tray() alone Frontend changes: - CloseBehaviorSettings component with radio-button UI - Integrated into GeneralSettings page - All 10 locales updated with 4 new strings - 3 new unit tests covering load, save, and save-failure revert All new items gated behind tauri-runtime feature to keep sidecar builds clean. 3482 existing tests pass, no lint warnings. --- PR_BODY.md | 37 +++++++ src-tauri/src/commands/system_settings.rs | 48 +++++++++ src-tauri/src/lib.rs | 22 ++++- src-tauri/src/models/mod.rs | 4 +- src-tauri/src/models/system.rs | 16 +++ .../settings/close-behavior-settings.test.tsx | 74 ++++++++++++++ .../settings/close-behavior-settings.tsx | 97 +++++++++++++++++++ src/components/settings/general-settings.tsx | 3 + src/i18n/messages/ar.json | 9 +- src/i18n/messages/de.json | 9 +- src/i18n/messages/en.json | 9 +- src/i18n/messages/es.json | 9 +- src/i18n/messages/fr.json | 9 +- src/i18n/messages/ja.json | 9 +- src/i18n/messages/ko.json | 9 +- src/i18n/messages/pt.json | 9 +- src/i18n/messages/zh-CN.json | 9 +- src/i18n/messages/zh-TW.json | 9 +- src/lib/api.ts | 11 +++ src/lib/types.ts | 6 ++ 20 files changed, 385 insertions(+), 23 deletions(-) create mode 100644 PR_BODY.md create mode 100644 src/components/settings/close-behavior-settings.test.tsx create mode 100644 src/components/settings/close-behavior-settings.tsx diff --git a/PR_BODY.md b/PR_BODY.md new file mode 100644 index 000000000..a68c7c286 --- /dev/null +++ b/PR_BODY.md @@ -0,0 +1,37 @@ +## Problem + +On Linux (and other platforms), clicking the main window's close button always exits the entire application. There is no way to minimize to the system tray and keep the app running in the background. + +The existing `can_hide_to_tray()` check was already able to detect tray availability, but the close button simply checked `can_hide_to_tray()` without consulting any user preference — if the tray was available, it always hid; if not, it always exited. There was no UI for the user to choose their preferred behavior. + +## Solution + +Add a configurable close-behavior setting with two options: + +1. **Hide to tray (background)** — default. When the close button is clicked and tray is available, the window hides to the system tray. The app keeps running, and the tray icon restores the window. When tray is not available (e.g., GNOME 45+ without AppIndicator), this falls back to exiting. +2. **Exit application** — always exits the app on close button click, regardless of tray availability. + +### Changes + +**Backend (Rust):** +- `models/system.rs`: New `CloseAction` enum (`HideToTray` / `Exit`) and `SystemCloseSettings` struct, persisted via `app_metadata_service`. +- `commands/system_settings.rs`: `load_system_close_settings`, `get_system_close_settings`, `update_system_close_settings` — all gated behind `tauri-runtime` to avoid dead_code warnings in sidecar builds. +- `lib.rs`: Close button handler reads the stored setting and uses `CloseAction::HideToTray && can_hide_to_tray()` instead of `can_hide_to_tray()` alone. + +**Frontend (TypeScript/React):** +- `lib/types.ts`: `CloseAction` type and `SystemCloseSettings` interface. +- `lib/api.ts`: `getSystemCloseSettings()` / `updateSystemCloseSettings()` transport wrappers. +- `components/settings/close-behavior-settings.tsx`: Radio-button UI with loading/saving states and error toast. +- `components/settings/general-settings.tsx`: Integrates the new section. +- `i18n/messages/*.json`: All 10 locales updated with the 4 new strings. + +## Testing + +- [x] 3,479 existing frontend tests pass (no regression). +- [x] Sidecar compiles cleanly (`cargo build --no-default-features --bin codeg-mcp`). +- [x] Main binary compiles cleanly (`cargo build --release --bin codeg`). +- [x] Setting persists across app restarts. +- [x] "Hide to tray" → close button hides window; tray icon restores it. +- [x] "Exit" → close button exits the app. +- [x] Default is "Hide to tray" (backward-compatible with existing behavior on tray-capable platforms). +- [x] Linux without tray: `can_hide_to_tray()` returns false, so both settings exit the app (no stranded process). diff --git a/src-tauri/src/commands/system_settings.rs b/src-tauri/src/commands/system_settings.rs index 1ecf48af1..a71b2c18f 100644 --- a/src-tauri/src/commands/system_settings.rs +++ b/src-tauri/src/commands/system_settings.rs @@ -8,6 +8,8 @@ use crate::db::service::app_metadata_service; use crate::db::AppDatabase; #[cfg(feature = "tauri-runtime")] use crate::models::SystemRenderingSettings; +#[cfg(feature = "tauri-runtime")] +use crate::models::SystemCloseSettings; use crate::models::{ AvailableTerminalShells, SystemLanguageSettings, SystemProxySettings, SystemTerminalSettings, TerminalShellOption, @@ -21,6 +23,8 @@ use crate::terminal::manager::resolve_shell; pub(crate) const SYSTEM_PROXY_SETTINGS_KEY: &str = "system_proxy_settings"; pub(crate) const SYSTEM_LANGUAGE_SETTINGS_KEY: &str = "system_language_settings"; pub(crate) const SYSTEM_TERMINAL_SETTINGS_KEY: &str = "system_terminal_settings"; +#[cfg(feature = "tauri-runtime")] +pub(crate) const SYSTEM_CLOSE_SETTINGS_KEY: &str = "system_close_settings"; pub(crate) const LANGUAGE_SETTINGS_UPDATED_EVENT: &str = "app://language-settings-updated"; pub(crate) const TERMINAL_SETTINGS_UPDATED_EVENT: &str = "app://terminal-settings-updated"; @@ -210,6 +214,24 @@ pub(crate) async fn load_system_terminal_settings( Ok(normalize_terminal_settings(parsed)) } +#[cfg(feature = "tauri-runtime")] +pub(crate) async fn load_system_close_settings( + conn: &DatabaseConnection, +) -> Result { + let raw = app_metadata_service::get_value(conn, SYSTEM_CLOSE_SETTINGS_KEY) + .await + .map_err(AppCommandError::from)?; + + let Some(raw) = raw else { + return Ok(SystemCloseSettings::default()); + }; + + serde_json::from_str::(&raw).map_err(|e| { + AppCommandError::configuration_invalid("Failed to parse stored close settings") + .with_detail(e.to_string()) + }) +} + #[cfg(feature = "tauri-runtime")] #[cfg_attr(feature = "tauri-runtime", tauri::command)] pub async fn get_system_proxy_settings( @@ -254,6 +276,32 @@ pub async fn get_system_terminal_settings( load_system_terminal_settings(&db.conn).await } +#[cfg(feature = "tauri-runtime")] +#[cfg_attr(feature = "tauri-runtime", tauri::command)] +pub async fn get_system_close_settings( + db: State<'_, AppDatabase>, +) -> Result { + load_system_close_settings(&db.conn).await +} + +#[cfg(feature = "tauri-runtime")] +#[cfg_attr(feature = "tauri-runtime", tauri::command)] +pub async fn update_system_close_settings( + settings: SystemCloseSettings, + db: State<'_, AppDatabase>, +) -> Result { + let serialized = serde_json::to_string(&settings).map_err(|e| { + AppCommandError::invalid_input("Failed to serialize close settings") + .with_detail(e.to_string()) + })?; + + app_metadata_service::upsert_value(&db.conn, SYSTEM_CLOSE_SETTINGS_KEY, &serialized) + .await + .map_err(AppCommandError::from)?; + + Ok(settings) +} + #[cfg(feature = "tauri-runtime")] #[cfg_attr(feature = "tauri-runtime", tauri::command)] pub async fn get_available_terminal_shells() -> Result { diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 14b9fbb3c..d6e64fc40 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -883,7 +883,25 @@ mod tauri_app { // that should fall through to the cleanup below. if !APP_QUITTING.load(Ordering::Relaxed) { api.prevent_close(); - if windows::can_hide_to_tray() { + let close_action = window.app_handle() + .try_state::() + .map(|db| { + tauri::async_runtime::block_on( + crate::commands::system_settings::load_system_close_settings( + &db.conn, + ), + ) + }) + .transpose() + .ok() + .flatten() + .map(|settings| settings.action) + .unwrap_or_default(); + + let should_hide = + close_action == crate::models::CloseAction::HideToTray + && windows::can_hide_to_tray(); + if should_hide { let _ = window.hide(); } else { window.app_handle().exit(0); @@ -1098,6 +1116,8 @@ mod tauri_app { system_settings::probe_terminal_shell_path, system_settings::get_system_rendering_settings, system_settings::update_system_rendering_settings, + system_settings::get_system_close_settings, + system_settings::update_system_close_settings, logging_commands::get_log_settings, logging_commands::set_log_settings, logging_commands::get_recent_logs, diff --git a/src-tauri/src/models/mod.rs b/src-tauri/src/models/mod.rs index 23d69dfbd..1ddcd8894 100644 --- a/src-tauri/src/models/mod.rs +++ b/src-tauri/src/models/mod.rs @@ -50,7 +50,7 @@ pub use work_task::{ #[cfg(feature = "tauri-runtime")] pub use system::SystemRenderingSettings; pub use system::{ - AvailableTerminalShells, GitCredentials, GitDetectResult, GitHubAccountsSettings, - GitHubTokenValidation, GitSettings, SystemLanguageSettings, SystemProxySettings, + AvailableTerminalShells, CloseAction, GitCredentials, GitDetectResult, GitHubAccountsSettings, + GitHubTokenValidation, GitSettings, SystemCloseSettings, SystemLanguageSettings, SystemProxySettings, SystemTerminalSettings, TerminalShellOption, }; diff --git a/src-tauri/src/models/system.rs b/src-tauri/src/models/system.rs index 4842ac6d2..05d48122e 100644 --- a/src-tauri/src/models/system.rs +++ b/src-tauri/src/models/system.rs @@ -43,6 +43,22 @@ pub struct SystemTerminalSettings { pub default_shell: Option, } +#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)] +#[serde(rename_all = "snake_case")] +pub enum CloseAction { + /// Hide to tray when a tray is available; otherwise exit. + #[default] + HideToTray, + /// Always exit when the main window is closed. + Exit, +} + +#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)] +#[serde(default)] +pub struct SystemCloseSettings { + pub action: CloseAction, +} + /// One row in the "default shell" picker. Backend owns the option list so the /// frontend doesn't have to know which shells are available on which platform. /// Labels are not localized server-side: `label_key` points at a frontend i18n diff --git a/src/components/settings/close-behavior-settings.test.tsx b/src/components/settings/close-behavior-settings.test.tsx new file mode 100644 index 000000000..b0c158795 --- /dev/null +++ b/src/components/settings/close-behavior-settings.test.tsx @@ -0,0 +1,74 @@ +import { fireEvent, render, screen, waitFor } from "@testing-library/react" +import { NextIntlClientProvider } from "next-intl" +import { beforeEach, describe, expect, it, vi } from "vitest" + +vi.mock("@/lib/api", () => ({ + getSystemCloseSettings: vi.fn(), + updateSystemCloseSettings: vi.fn(), +})) + +vi.mock("sonner", () => ({ + toast: { error: vi.fn() }, +})) + +import { CloseBehaviorSettings } from "./close-behavior-settings" +import enMessages from "@/i18n/messages/en.json" +import { getSystemCloseSettings, updateSystemCloseSettings } from "@/lib/api" + +const mockGet = vi.mocked(getSystemCloseSettings) +const mockSet = vi.mocked(updateSystemCloseSettings) + +function renderWithIntl() { + return render( + + + + ) +} + +beforeEach(() => { + mockGet.mockReset() + mockSet.mockReset() +}) + +describe("CloseBehaviorSettings", () => { + it("loads the backend default and selects hide-to-tray", async () => { + mockGet.mockResolvedValue({ action: "hide_to_tray" }) + renderWithIntl() + const hide = (await screen.findByLabelText( + "Hide to tray (background)" + )) as HTMLInputElement + expect(hide.checked).toBe(true) + }) + + it("switches to exit and persists the choice", async () => { + mockGet.mockResolvedValue({ action: "hide_to_tray" }) + mockSet.mockImplementation(async (next) => next) + renderWithIntl() + + const exit = await screen.findByLabelText("Exit application") + fireEvent.click(exit) + + await waitFor(() => { + expect(mockSet).toHaveBeenCalledWith({ action: "exit" }) + }) + expect((exit as HTMLInputElement).checked).toBe(true) + }) + + it("reverts the radio when saving fails", async () => { + mockGet.mockResolvedValue({ action: "hide_to_tray" }) + mockSet.mockRejectedValue(new Error("boom")) + renderWithIntl() + + const exit = await screen.findByLabelText("Exit application") + fireEvent.click(exit) + + await waitFor(() => { + expect(mockSet).toHaveBeenCalledWith({ action: "exit" }) + }) + const hide = screen.getByLabelText( + "Hide to tray (background)" + ) as HTMLInputElement + expect(hide.checked).toBe(true) + }) +}) diff --git a/src/components/settings/close-behavior-settings.tsx b/src/components/settings/close-behavior-settings.tsx new file mode 100644 index 000000000..72163840d --- /dev/null +++ b/src/components/settings/close-behavior-settings.tsx @@ -0,0 +1,97 @@ +"use client" + +import { useCallback, useEffect, useState } from "react" +import { PanelBottomClose } from "lucide-react" +import { useTranslations } from "next-intl" +import { toast } from "sonner" + +import { getSystemCloseSettings, updateSystemCloseSettings } from "@/lib/api" +import { toErrorMessage } from "@/lib/app-error" +import type { CloseAction } from "@/lib/types" + +export function CloseBehaviorSettings() { + const t = useTranslations("GeneralSettings") + const tDynamic = t as unknown as ( + key: string, + values?: Record + ) => string + + const [loading, setLoading] = useState(true) + const [action, setAction] = useState("hide_to_tray") + const [saving, setSaving] = useState(false) + + const loadSettings = useCallback(async () => { + setLoading(true) + try { + const settings = await getSystemCloseSettings() + setAction(settings.action) + } catch (err) { + console.error("[Settings] load close behavior failed:", err) + } finally { + setLoading(false) + } + }, []) + + useEffect(() => { + loadSettings().catch((err) => { + console.error("[Settings] load close behavior failed:", err) + }) + }, [loadSettings]) + + const save = useCallback( + async (next: CloseAction) => { + setSaving(true) + setAction(next) + try { + await updateSystemCloseSettings({ action: next }) + } catch (err) { + setAction(action) + const message = toErrorMessage(err) + toast.error(tDynamic("closeActionSaveFailed", { message })) + } finally { + setSaving(false) + } + }, + [action, tDynamic] + ) + + if (loading) return null + + return ( +
+
+ +

{t("closeActionTitle")}

+
+ +

+ {t("closeActionDescription")} +

+ +
+ + +
+
+ ) +} diff --git a/src/components/settings/general-settings.tsx b/src/components/settings/general-settings.tsx index eaf4ffa2e..a4ed8ada3 100644 --- a/src/components/settings/general-settings.tsx +++ b/src/components/settings/general-settings.tsx @@ -33,6 +33,7 @@ import { DelegationSettingsSection } from "@/components/settings/delegation-sett import { SessionFeedbackSettingsSection } from "@/components/settings/session-feedback-settings" import { AskQuestionSettingsSection } from "@/components/settings/ask-question-settings" import { SessionInfoSettingsSection } from "@/components/settings/session-info-settings" +import { CloseBehaviorSettings } from "@/components/settings/close-behavior-settings" const TERMINAL_SHELL_OPTION_SYSTEM = "system" const TERMINAL_SHELL_OPTION_CUSTOM = "custom" @@ -388,6 +389,8 @@ export function GeneralSettings() { )} + + diff --git a/src/i18n/messages/ar.json b/src/i18n/messages/ar.json index e0e39ad05..82f20a492 100644 --- a/src/i18n/messages/ar.json +++ b/src/i18n/messages/ar.json @@ -1288,7 +1288,12 @@ "restartRequired": "تم الحفظ. أعد تشغيل التطبيق ليصبح التغيير نافذًا.", "restartNow": "إعادة التشغيل الآن", "restartFailed": "فشل إعادة التشغيل: {message}", - "loadFailed": "فشل التحميل: {message}" + "loadFailed": "فشل التحميل: {message}", + "closeActionTitle": "سلوك الإغلاق", + "closeActionDescription": "اختر ما يحدث عند النقر على زر إغلاق النافذة الرئيسية.", + "closeActionHideToTray": "إخفاء في شريط النظام (خلفية)", + "closeActionExit": "إنهاء التطبيق", + "closeActionSaveFailed": "فشل حفظ سلوك الإغلاق:{message}" }, "LoginPage": { "documentTitle": "تسجيل الدخول - codeg", @@ -4666,4 +4671,4 @@ "loadFailed": "تعذّر تحميل بيانات الاستهلاك", "truncatedNotice": "هذا النطاق واسع جدًا — الأرقام تغطي أحدث جزء منه فقط." } -} +} \ No newline at end of file diff --git a/src/i18n/messages/de.json b/src/i18n/messages/de.json index 2cd752ee7..a1694878f 100644 --- a/src/i18n/messages/de.json +++ b/src/i18n/messages/de.json @@ -1288,7 +1288,12 @@ "restartRequired": "Gespeichert. Starten Sie die App neu, damit die Änderung wirksam wird.", "restartNow": "Jetzt neu starten", "restartFailed": "Neustart fehlgeschlagen: {message}", - "loadFailed": "Laden fehlgeschlagen: {message}" + "loadFailed": "Laden fehlgeschlagen: {message}", + "closeActionTitle": "Schließen-Verhalten", + "closeActionDescription": "Wählen Sie, was beim Klick auf den Schließen-Button des Hauptfensters passiert.", + "closeActionHideToTray": "In Tray ausblenden (Hintergrund)", + "closeActionExit": "Anwendung beenden", + "closeActionSaveFailed": "Fehler beim Speichern des Schließen-Verhaltens:{message}" }, "LoginPage": { "documentTitle": "Anmelden - codeg", @@ -4666,4 +4671,4 @@ "loadFailed": "Verbrauch konnte nicht geladen werden", "truncatedNotice": "Dieser Zeitraum ist sehr groß — die Zahlen decken nur seinen jüngsten Abschnitt ab." } -} +} \ No newline at end of file diff --git a/src/i18n/messages/en.json b/src/i18n/messages/en.json index 158af682b..c6ffacca7 100644 --- a/src/i18n/messages/en.json +++ b/src/i18n/messages/en.json @@ -1288,7 +1288,12 @@ "restartRequired": "Saved. Restart the app for the change to take effect.", "restartNow": "Restart now", "restartFailed": "Failed to restart: {message}", - "loadFailed": "Load failed: {message}" + "loadFailed": "Load failed: {message}", + "closeActionTitle": "Close Behavior", + "closeActionDescription": "Choose what happens when the main window's close button is clicked.", + "closeActionHideToTray": "Hide to tray (background)", + "closeActionExit": "Exit application", + "closeActionSaveFailed": "Failed to save close behavior: {message}" }, "LoginPage": { "documentTitle": "Login - codeg", @@ -4666,4 +4671,4 @@ "loadFailed": "Could not load usage", "truncatedNotice": "This range is very large — the numbers cover only the most recent slice of it." } -} +} \ No newline at end of file diff --git a/src/i18n/messages/es.json b/src/i18n/messages/es.json index 7a659cb6f..6185c9cbf 100644 --- a/src/i18n/messages/es.json +++ b/src/i18n/messages/es.json @@ -1288,7 +1288,12 @@ "restartRequired": "Guardado. Reinicia la aplicación para aplicar el cambio.", "restartNow": "Reiniciar ahora", "restartFailed": "No se pudo reiniciar: {message}", - "loadFailed": "Error al cargar: {message}" + "loadFailed": "Error al cargar: {message}", + "closeActionTitle": "Comportamiento al cerrar", + "closeActionDescription": "Elija qué sucede al hacer clic en el botón de cerrar de la ventana principal.", + "closeActionHideToTray": "Ocultar en la bandeja (fondo)", + "closeActionExit": "Salir de la aplicación", + "closeActionSaveFailed": "Error al guardar comportamiento:{message}" }, "LoginPage": { "documentTitle": "Iniciar sesión - codeg", @@ -4666,4 +4671,4 @@ "loadFailed": "No se pudo cargar el uso", "truncatedNotice": "Este periodo es muy amplio: las cifras cubren solo su tramo más reciente." } -} +} \ No newline at end of file diff --git a/src/i18n/messages/fr.json b/src/i18n/messages/fr.json index a39e80fcf..6471b5468 100644 --- a/src/i18n/messages/fr.json +++ b/src/i18n/messages/fr.json @@ -1288,7 +1288,12 @@ "restartRequired": "Enregistré. Redémarrez l’application pour appliquer la modification.", "restartNow": "Redémarrer maintenant", "restartFailed": "Échec du redémarrage : {message}", - "loadFailed": "Échec du chargement : {message}" + "loadFailed": "Échec du chargement : {message}", + "closeActionTitle": "Comportement de fermeture", + "closeActionDescription": "Choisissez ce qui se passe lorsque vous cliquez sur le bouton de fermeture de la fenêtre principale.", + "closeActionHideToTray": "Masquer dans la barre d'état (arrière-plan)", + "closeActionExit": "Quitter l'application", + "closeActionSaveFailed": "Échec de l'enregistrement du comportement:{message}" }, "LoginPage": { "documentTitle": "Connexion - codeg", @@ -4666,4 +4671,4 @@ "loadFailed": "Impossible de charger la consommation", "truncatedNotice": "Cette période est très large — les chiffres ne couvrent que sa portion la plus récente." } -} +} \ No newline at end of file diff --git a/src/i18n/messages/ja.json b/src/i18n/messages/ja.json index 6febebabc..c2b462af8 100644 --- a/src/i18n/messages/ja.json +++ b/src/i18n/messages/ja.json @@ -1288,7 +1288,12 @@ "restartRequired": "保存しました。再起動後に反映されます。", "restartNow": "今すぐ再起動", "restartFailed": "再起動に失敗しました: {message}", - "loadFailed": "読み込みに失敗しました: {message}" + "loadFailed": "読み込みに失敗しました: {message}", + "closeActionTitle": "閉じる動作", + "closeActionDescription": "メインウィンドウの閉じるボタンをクリックしたときの動作を選択します。", + "closeActionHideToTray": "トレイに隠す(バックグラウンド)", + "closeActionExit": "アプリケーションを終了", + "closeActionSaveFailed": "閉じる動作の保存に失敗:{message}" }, "LoginPage": { "documentTitle": "ログイン - codeg", @@ -4666,4 +4671,4 @@ "loadFailed": "使用量を読み込めませんでした", "truncatedNotice": "期間が非常に長いため、表示中の数値は直近の一部のみを対象としています。" } -} +} \ No newline at end of file diff --git a/src/i18n/messages/ko.json b/src/i18n/messages/ko.json index 66db87b7d..6d4cc4b35 100644 --- a/src/i18n/messages/ko.json +++ b/src/i18n/messages/ko.json @@ -1288,7 +1288,12 @@ "restartRequired": "저장되었습니다. 변경 사항을 적용하려면 앱을 다시 시작하세요.", "restartNow": "지금 다시 시작", "restartFailed": "다시 시작 실패: {message}", - "loadFailed": "불러오기 실패: {message}" + "loadFailed": "불러오기 실패: {message}", + "closeActionTitle": "닫기 동작", + "closeActionDescription": "메인 창 닫기 버튼을 클릭할 때의 동작을 선택합니다.", + "closeActionHideToTray": "트레이로 숨기기(백그라운드)", + "closeActionExit": "애플리케이션 종료", + "closeActionSaveFailed": "닫기 동작 저장 실패:{message}" }, "LoginPage": { "documentTitle": "로그인 - codeg", @@ -4666,4 +4671,4 @@ "loadFailed": "사용량을 불러오지 못했습니다", "truncatedNotice": "기간이 매우 길어 표시된 수치는 가장 최근 구간만 반영합니다." } -} +} \ No newline at end of file diff --git a/src/i18n/messages/pt.json b/src/i18n/messages/pt.json index 7604d1c8f..4aab710ab 100644 --- a/src/i18n/messages/pt.json +++ b/src/i18n/messages/pt.json @@ -1288,7 +1288,12 @@ "restartRequired": "Salvo. Reinicie o aplicativo para aplicar a alteração.", "restartNow": "Reiniciar agora", "restartFailed": "Falha ao reiniciar: {message}", - "loadFailed": "Falha ao carregar: {message}" + "loadFailed": "Falha ao carregar: {message}", + "closeActionTitle": "Comportamento ao fechar", + "closeActionDescription": "Escolha o que acontece quando o botão de fechar da janela principal é clicado.", + "closeActionHideToTray": "Ocultar na bandeja (segundo plano)", + "closeActionExit": "Sair do aplicativo", + "closeActionSaveFailed": "Falha ao salvar comportamento:{message}" }, "LoginPage": { "documentTitle": "Entrar - codeg", @@ -4666,4 +4671,4 @@ "loadFailed": "Não foi possível carregar o uso", "truncatedNotice": "Este período é muito amplo — os números cobrem apenas o trecho mais recente." } -} +} \ No newline at end of file diff --git a/src/i18n/messages/zh-CN.json b/src/i18n/messages/zh-CN.json index f73133564..72ad5c7b5 100644 --- a/src/i18n/messages/zh-CN.json +++ b/src/i18n/messages/zh-CN.json @@ -1288,7 +1288,12 @@ "restartRequired": "已保存,重启应用后生效。", "restartNow": "立即重启", "restartFailed": "重启失败:{message}", - "loadFailed": "加载失败:{message}" + "loadFailed": "加载失败:{message}", + "closeActionTitle": "关闭行为", + "closeActionDescription": "选择点击主窗口关闭按钮时的行为。", + "closeActionHideToTray": "隐藏到系统托盘(后台运行)", + "closeActionExit": "退出应用", + "closeActionSaveFailed": "保存关闭行为设置失败:{message}" }, "LoginPage": { "documentTitle": "登录 - codeg", @@ -4666,4 +4671,4 @@ "loadFailed": "用量加载失败", "truncatedNotice": "该范围过大 —— 下面的数字只覆盖了其中最近的一段。" } -} +} \ No newline at end of file diff --git a/src/i18n/messages/zh-TW.json b/src/i18n/messages/zh-TW.json index 4f2c0dc62..d00691193 100644 --- a/src/i18n/messages/zh-TW.json +++ b/src/i18n/messages/zh-TW.json @@ -1288,7 +1288,12 @@ "restartRequired": "已儲存,需重新啟動應用程式才會生效。", "restartNow": "立即重新啟動", "restartFailed": "重新啟動失敗:{message}", - "loadFailed": "載入失敗:{message}" + "loadFailed": "載入失敗:{message}", + "closeActionTitle": "關閉行為", + "closeActionDescription": "選擇點擊主視窗關閉按鈕時的行為。", + "closeActionHideToTray": "隱藏到系統托盤(背景執行)", + "closeActionExit": "退出應用", + "closeActionSaveFailed": "儲存關閉行為設定失敗:{message}" }, "LoginPage": { "documentTitle": "登入 - codeg", @@ -4666,4 +4671,4 @@ "loadFailed": "用量載入失敗", "truncatedNotice": "此範圍過大 —— 下面的數字只涵蓋其中最近的一段。" } -} +} \ No newline at end of file diff --git a/src/lib/api.ts b/src/lib/api.ts index 06bb2ba0e..0158369be 100644 --- a/src/lib/api.ts +++ b/src/lib/api.ts @@ -107,6 +107,7 @@ import type { AvailableTerminalShells, SystemLanguageSettings, SystemProxySettings, + SystemCloseSettings, SystemRenderingSettings, SystemTerminalSettings, LogSettings, @@ -1409,6 +1410,16 @@ export async function updateSystemRenderingSettings( return getTransport().call("update_system_rendering_settings", { settings }) } +export async function getSystemCloseSettings(): Promise { + return getTransport().call("get_system_close_settings") +} + +export async function updateSystemCloseSettings( + settings: SystemCloseSettings +): Promise { + return getTransport().call("update_system_close_settings", { settings }) +} + // --- Logging --- /** Live-tail channel: one event per appended log record. */ diff --git a/src/lib/types.ts b/src/lib/types.ts index 4c41d84cc..35fb5855a 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -2640,6 +2640,12 @@ export interface SystemRenderingSettings { disable_hardware_acceleration: boolean } +export type CloseAction = "hide_to_tray" | "exit" + +export interface SystemCloseSettings { + action: CloseAction +} + // --- Logging --- export type LogLevel = "off" | "error" | "warn" | "info" | "debug" | "trace"