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/chat/MessageList.test.tsx b/desktop/src/components/chat/MessageList.test.tsx index 603ee9a2..ca1e83d2 100644 --- a/desktop/src/components/chat/MessageList.test.tsx +++ b/desktop/src/components/chat/MessageList.test.tsx @@ -281,4 +281,32 @@ describe('MessageList nested tool calls', () => { '先看 CLI 和服务端入口。\n再看 desktop 前后端边界。' ) }) + + it('shows raw startup details under translated CLI startup errors', () => { + useChatStore.setState({ + sessions: { + [ACTIVE_TAB]: makeSessionState({ + messages: [ + { + id: 'error-1', + type: 'error', + code: 'CLI_START_FAILED', + message: + 'CLI exited during startup (code 1): Claude Code on Windows requires git-bash (https://git-scm.com/downloads/win).', + timestamp: 1, + }, + ], + }), + }, + }) + + render() + + expect(screen.getByText('Failed to start CLI process.')).toBeTruthy() + expect( + screen.getByText( + 'CLI exited during startup (code 1): Claude Code on Windows requires git-bash (https://git-scm.com/downloads/win).', + ), + ).toBeTruthy() + }) }) diff --git a/desktop/src/components/chat/MessageList.tsx b/desktop/src/components/chat/MessageList.tsx index 4ee13c6b..a50286ea 100644 --- a/desktop/src/components/chat/MessageList.tsx +++ b/desktop/src/components/chat/MessageList.tsx @@ -222,9 +222,18 @@ export const MessageBlock = memo(function MessageBlock({ const errorKey = message.code ? `error.${message.code}` as TranslationKey : null const errorText = errorKey ? t(errorKey) : null const displayMessage = (errorText && errorText !== errorKey) ? errorText : message.message + const showRawDetail = + Boolean(message.message) && + message.message.trim() !== '' && + message.message !== displayMessage return (
Error: {displayMessage} + {showRawDetail && ( +
+ {message.message} +
+ )}
) } 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 */}