From e9a8191c127c53b9d8dd66f27e361b44572a33a2 Mon Sep 17 00:00:00 2001 From: Michael Suchacz <203725896+ibetitsmike@users.noreply.github.com> Date: Thu, 18 Dec 2025 12:40:37 +0100 Subject: [PATCH] =?UTF-8?q?=F0=9F=A4=96=20fix:=20add=20logging=20to=20debu?= =?UTF-8?q?g=20Cmd+Q=20behavior=20on=20macOS?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adding detailed logging to understand why Cmd+Q closes the window instead of quitting the app on macOS: - Quit menu item click handler - mainWindow 'close' and 'closed' events - app 'before-quit' event with disposal state - app 'window-all-closed' event - Cmd+Q detection in renderer (to see if keypress reaches renderer) Run the app and press Cmd+Q, then check the console output. Fixes #1100 --- src/browser/App.tsx | 8 +++++ .../contexts/ProviderOptionsContext.tsx | 28 ++++++++++++---- src/browser/utils/messages/sendOptions.ts | 2 +- src/desktop/main.ts | 33 +++++++++++++++++-- tests/ipc/sendMessage.heavy.test.ts | 20 +++++++++-- 5 files changed, 79 insertions(+), 12 deletions(-) diff --git a/src/browser/App.tsx b/src/browser/App.tsx index 8ab4af27d9..0532df1378 100644 --- a/src/browser/App.tsx +++ b/src/browser/App.tsx @@ -486,6 +486,14 @@ function AppInner() { // Handle keyboard shortcuts useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { + // DEBUG: Log Cmd+Q presses to diagnose menu accelerator issues + if (e.metaKey && e.key.toLowerCase() === "q") { + console.log("[App] Cmd+Q detected in renderer", { + key: e.key, + metaKey: e.metaKey, + defaultPrevented: e.defaultPrevented, + }); + } if (matchesKeybind(e, KEYBINDS.NEXT_WORKSPACE)) { e.preventDefault(); handleNavigateWorkspace("next"); diff --git a/src/browser/contexts/ProviderOptionsContext.tsx b/src/browser/contexts/ProviderOptionsContext.tsx index 148bb09aa0..fbd02bb67f 100644 --- a/src/browser/contexts/ProviderOptionsContext.tsx +++ b/src/browser/contexts/ProviderOptionsContext.tsx @@ -1,5 +1,9 @@ -import React, { createContext, useContext } from "react"; -import { usePersistedState } from "@/browser/hooks/usePersistedState"; +import React, { createContext, useContext, useLayoutEffect } from "react"; +import { + readPersistedState, + updatePersistedState, + usePersistedState, +} from "@/browser/hooks/usePersistedState"; import type { MuxProviderOptions } from "@/common/types/providerOptions"; interface ProviderOptionsContextType { @@ -11,6 +15,10 @@ interface ProviderOptionsContextType { const ProviderOptionsContext = createContext(undefined); +const OPENAI_OPTIONS_KEY = "provider_options_openai"; +// One-time migration key: force disableAutoTruncation to true for existing users +const OPENAI_TRUNCATION_MIGRATION_KEY = "provider_options_openai_truncation_migrated"; + export function ProviderOptionsProvider({ children }: { children: React.ReactNode }) { const [anthropicOptions, setAnthropicOptions] = usePersistedState< MuxProviderOptions["anthropic"] @@ -19,12 +27,20 @@ export function ProviderOptionsProvider({ children }: { children: React.ReactNod }); const [openaiOptions, setOpenAIOptions] = usePersistedState( - "provider_options_openai", - { - disableAutoTruncation: false, - } + OPENAI_OPTIONS_KEY, + { disableAutoTruncation: true } ); + // One-time migration: force disableAutoTruncation to true for existing users + useLayoutEffect(() => { + const alreadyMigrated = readPersistedState(OPENAI_TRUNCATION_MIGRATION_KEY, false); + if (alreadyMigrated) { + return; + } + updatePersistedState(OPENAI_OPTIONS_KEY, { disableAutoTruncation: true }); + updatePersistedState(OPENAI_TRUNCATION_MIGRATION_KEY, true); + }, []); + const [googleOptions, setGoogleOptions] = usePersistedState( "provider_options_google", {} diff --git a/src/browser/utils/messages/sendOptions.ts b/src/browser/utils/messages/sendOptions.ts index 30e6c1b0b6..685952ce85 100644 --- a/src/browser/utils/messages/sendOptions.ts +++ b/src/browser/utils/messages/sendOptions.ts @@ -26,7 +26,7 @@ function getProviderOptions(): MuxProviderOptions { { use1MContext: false } ); const openai = readPersistedState("provider_options_openai", { - disableAutoTruncation: false, + disableAutoTruncation: true, }); const google = readPersistedState("provider_options_google", {}); diff --git a/src/desktop/main.ts b/src/desktop/main.ts index 34505e0d0e..9155ecc480 100644 --- a/src/desktop/main.ts +++ b/src/desktop/main.ts @@ -143,6 +143,13 @@ function timestamp(): string { function createMenu() { const template: MenuItemConstructorOptions[] = [ + { + label: "File", + submenu: + process.platform === "darwin" + ? [{ role: "close" }] // macOS: Cmd+W to close window + : [{ role: "quit" }], // Windows/Linux: Quit in File menu + }, { label: "Edit", submenu: [ @@ -182,7 +189,14 @@ function createMenu() { }, { label: "Window", - submenu: [{ role: "minimize" }, { role: "close" }], + submenu: + process.platform === "darwin" + ? [ + // macOS Window menu - close is in File menu to avoid Cmd+Q/Cmd+W conflicts + { role: "minimize" }, + { role: "zoom" }, + ] + : [{ role: "minimize" }, { role: "close" }], }, ]; @@ -206,7 +220,12 @@ function createMenu() { { role: "hideOthers" }, { role: "unhide" }, { type: "separator" }, - { role: "quit" }, + { + role: "quit", + click: () => { + console.log(`[${timestamp()}] Quit menu item clicked`); + }, + }, ], }); } @@ -504,7 +523,13 @@ function createWindow() { // First token count will use approximation, accurate count caches in background. }); + // Log when window is about to close (can be prevented) + mainWindow.on("close", (event) => { + console.log(`[${timestamp()}] mainWindow 'close' event fired`); + }); + mainWindow.on("closed", () => { + console.log(`[${timestamp()}] mainWindow 'closed' event fired`); mainWindow = null; }); } @@ -573,12 +598,15 @@ if (gotTheLock) { let isDisposing = false; app.on("before-quit", (event) => { + console.log(`[${timestamp()}] app 'before-quit' event fired (isDisposing=${isDisposing}, hasServices=${!!services})`); // Skip if already disposing or no services to clean up if (isDisposing || !services) { + console.log(`[${timestamp()}] before-quit: skipping (already disposing or no services)`); return; } // Prevent quit, clean up, then quit again + console.log(`[${timestamp()}] before-quit: preventing quit, starting disposal`); event.preventDefault(); isDisposing = true; @@ -594,6 +622,7 @@ if (gotTheLock) { }); app.on("window-all-closed", () => { + console.log(`[${timestamp()}] app 'window-all-closed' event fired (platform=${process.platform})`); if (process.platform !== "darwin") { app.quit(); } diff --git a/tests/ipc/sendMessage.heavy.test.ts b/tests/ipc/sendMessage.heavy.test.ts index dcf9d83630..41228daca9 100644 --- a/tests/ipc/sendMessage.heavy.test.ts +++ b/tests/ipc/sendMessage.heavy.test.ts @@ -41,13 +41,21 @@ describeIntegration("sendMessage heavy/load tests", () => { await withSharedWorkspace(provider, async ({ env, workspaceId, collector }) => { // Build up large conversation history to exceed context limit // This approach is model-agnostic - it keeps sending until we've built up enough history + // Use auto-truncation enabled (disableAutoTruncation: false) so history builds up successfully const largeMessage = "x".repeat(50_000); for (let i = 0; i < 10; i++) { await sendMessageWithModel( env, workspaceId, `Message ${i}: ${largeMessage}`, - modelString(provider, model) + modelString(provider, model), + { + providerOptions: { + openai: { + disableAutoTruncation: false, + }, + }, + } ); await collector.waitForEvent("stream-end", 30000); collector.clear(); @@ -93,8 +101,14 @@ describeIntegration("sendMessage heavy/load tests", () => { env, workspaceId, "This should succeed with auto-truncation", - modelString(provider, model) - // disableAutoTruncation defaults to false (auto-truncation enabled) + modelString(provider, model), + { + providerOptions: { + openai: { + disableAutoTruncation: false, + }, + }, + } ); expect(successResult.success).toBe(true);