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 */} win.minimize()} + onClick={() => runWindowAction(() => win.minimize())} + aria-label="Minimize window" className="w-[46px] h-full flex items-center justify-center text-[var(--color-text-secondary)] hover:bg-[var(--color-surface-hover)] transition-colors" > @@ -50,7 +57,8 @@ export function WindowControls() { {/* Maximize / Restore */} win.toggleMaximize()} + onClick={() => runWindowAction(() => win.toggleMaximize())} + aria-label={maximized ? 'Restore window' : 'Maximize window'} className="w-[46px] h-full flex items-center justify-center text-[var(--color-text-secondary)] hover:bg-[var(--color-surface-hover)] transition-colors" > {maximized ? ( @@ -67,7 +75,8 @@ export function WindowControls() { {/* Close */} win.close()} + onClick={() => runWindowAction(() => win.close())} + aria-label="Close window" className="w-[46px] h-full flex items-center justify-center text-[var(--color-text-secondary)] hover:bg-[#e81123] hover:text-white transition-colors" >