From e0b74cecaeb8c1eb95f5867a5927ff362ef2bd6a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A8=8B=E5=BA=8F=E5=91=98=E9=98=BF=E6=B1=9F=28Relakkes?= =?UTF-8?q?=29?= Date: Sun, 19 Apr 2026 13:59:31 +0800 Subject: [PATCH] Restore Windows custom titlebar controls so desktop builds can manage the window again The Windows desktop shell was rendering custom minimize, maximize, and close buttons without the Tauri capabilities those commands require. The app was also starting with decorated windows and then trying to disable decorations at runtime, which is a weaker setup than the platform-specific config Tauri expects for custom chrome. This change grants the missing window permissions, moves Windows and macOS window chrome settings into platform-specific Tauri config files, removes the runtime decoration toggle, and adds a regression test that proves the custom controls invoke the Tauri window API. Constraint: Windows build validation will happen on the remote branch instead of this macOS workspace Rejected: Keep runtime set_decorations(false) on Windows | leaves window chrome behavior dependent on post-start native mutation Rejected: Ship only the capability fix | lower-risk hotfix, but it preserves the fragile cross-platform window config Confidence: medium Scope-risk: narrow Reversibility: clean Directive: Keep custom titlebar behavior in config and capabilities; avoid moving Windows decoration changes back into runtime setup Tested: cd desktop && bun run lint Tested: cd desktop && bun run test Not-tested: Native Windows click/drag behavior on an actual packaged build from this local machine Related: GitHub issue #62 --- desktop/src-tauri/capabilities/default.json | 3 + desktop/src-tauri/src/lib.rs | 6 -- desktop/src-tauri/tauri.conf.json | 2 - desktop/src-tauri/tauri.macos.conf.json | 18 +++++ desktop/src-tauri/tauri.windows.conf.json | 16 +++++ .../components/layout/WindowControls.test.tsx | 69 +++++++++++++++++++ .../src/components/layout/WindowControls.tsx | 15 +++- 7 files changed, 118 insertions(+), 11 deletions(-) create mode 100644 desktop/src-tauri/tauri.macos.conf.json create mode 100644 desktop/src-tauri/tauri.windows.conf.json create mode 100644 desktop/src/components/layout/WindowControls.test.tsx diff --git a/desktop/src-tauri/capabilities/default.json b/desktop/src-tauri/capabilities/default.json index 0e039140..b532e822 100644 --- a/desktop/src-tauri/capabilities/default.json +++ b/desktop/src-tauri/capabilities/default.json @@ -5,7 +5,10 @@ "windows": ["main"], "permissions": [ "core:default", + "core:window:allow-close", + "core:window:allow-minimize", "core:window:allow-start-dragging", + "core:window:allow-toggle-maximize", "shell:allow-open", { "identifier": "shell:allow-execute", diff --git a/desktop/src-tauri/src/lib.rs b/desktop/src-tauri/src/lib.rs index 8a15c4f7..7671fce4 100644 --- a/desktop/src-tauri/src/lib.rs +++ b/desktop/src-tauri/src/lib.rs @@ -394,12 +394,6 @@ pub fn run() { // 让 adapter 连上动态端口。 spawn_and_track_adapters_sidecar(&app.handle()); - let _window = app.get_webview_window("main").unwrap(); - - // Windows: hide native decorations — frontend renders custom titlebar - #[cfg(target_os = "windows")] - let _ = _window.set_decorations(false); - Ok(()) }) .build(tauri::generate_context!()) diff --git a/desktop/src-tauri/tauri.conf.json b/desktop/src-tauri/tauri.conf.json index 75b079a6..57b69a2c 100644 --- a/desktop/src-tauri/tauri.conf.json +++ b/desktop/src-tauri/tauri.conf.json @@ -18,8 +18,6 @@ "minWidth": 960, "minHeight": 640, "decorations": true, - "titleBarStyle": "Overlay", - "hiddenTitle": true, "transparent": false, "acceptFirstMouse": true } diff --git a/desktop/src-tauri/tauri.macos.conf.json b/desktop/src-tauri/tauri.macos.conf.json new file mode 100644 index 00000000..9405fbbc --- /dev/null +++ b/desktop/src-tauri/tauri.macos.conf.json @@ -0,0 +1,18 @@ +{ + "app": { + "windows": [ + { + "title": "Claude Code Haha", + "width": 1440, + "height": 960, + "minWidth": 960, + "minHeight": 640, + "decorations": true, + "titleBarStyle": "Overlay", + "hiddenTitle": true, + "transparent": false, + "acceptFirstMouse": true + } + ] + } +} diff --git a/desktop/src-tauri/tauri.windows.conf.json b/desktop/src-tauri/tauri.windows.conf.json new file mode 100644 index 00000000..ba908a16 --- /dev/null +++ b/desktop/src-tauri/tauri.windows.conf.json @@ -0,0 +1,16 @@ +{ + "app": { + "windows": [ + { + "title": "Claude Code Haha", + "width": 1440, + "height": 960, + "minWidth": 960, + "minHeight": 640, + "decorations": false, + "transparent": false, + "acceptFirstMouse": true + } + ] + } +} diff --git a/desktop/src/components/layout/WindowControls.test.tsx b/desktop/src/components/layout/WindowControls.test.tsx new file mode 100644 index 00000000..cf29ada5 --- /dev/null +++ b/desktop/src/components/layout/WindowControls.test.tsx @@ -0,0 +1,69 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import { fireEvent, render, screen, waitFor } from '@testing-library/react' +import '@testing-library/jest-dom' + +const minimize = vi.fn().mockResolvedValue(undefined) +const toggleMaximize = vi.fn().mockResolvedValue(undefined) +const close = vi.fn().mockResolvedValue(undefined) +const isMaximized = vi.fn().mockResolvedValue(false) +const onResized = vi.fn().mockResolvedValue(() => {}) + +vi.mock('@tauri-apps/api/window', () => ({ + getCurrentWindow: () => ({ + minimize, + toggleMaximize, + close, + isMaximized, + onResized, + }), +})) + +describe('WindowControls', () => { + const originalPlatform = navigator.platform + + beforeEach(async () => { + minimize.mockClear() + toggleMaximize.mockClear() + close.mockClear() + isMaximized.mockClear() + onResized.mockClear() + + Object.defineProperty(window, '__TAURI_INTERNALS__', { + configurable: true, + value: {}, + }) + Object.defineProperty(navigator, 'platform', { + configurable: true, + value: 'Win32', + }) + vi.resetModules() + }) + + afterEach(() => { + Reflect.deleteProperty(window, '__TAURI_INTERNALS__') + Object.defineProperty(navigator, 'platform', { + configurable: true, + value: originalPlatform, + }) + }) + + it('invokes Tauri window APIs for custom controls on Windows', async () => { + const { WindowControls } = await import('./WindowControls') + + render() + + await waitFor(() => { + expect(screen.getByRole('button', { name: 'Minimize window' })).toBeInTheDocument() + }) + + fireEvent.click(screen.getByRole('button', { name: 'Minimize window' })) + fireEvent.click(screen.getByRole('button', { name: 'Maximize window' })) + fireEvent.click(screen.getByRole('button', { name: 'Close window' })) + + await waitFor(() => { + expect(minimize).toHaveBeenCalledTimes(1) + expect(toggleMaximize).toHaveBeenCalledTimes(1) + expect(close).toHaveBeenCalledTimes(1) + }) + }) +}) diff --git a/desktop/src/components/layout/WindowControls.tsx b/desktop/src/components/layout/WindowControls.tsx index 35f4706d..2825e80a 100644 --- a/desktop/src/components/layout/WindowControls.tsx +++ b/desktop/src/components/layout/WindowControls.tsx @@ -34,13 +34,20 @@ export function WindowControls() { return () => { unlisten?.() } }, []) + const runWindowAction = (action: () => Promise) => { + void action().catch((error) => { + console.error('Window control action failed', error) + }) + } + if (!showWindowControls || !win) return null return (
{/* Minimize */}