Compare commits

...

7 Commits

Author SHA1 Message Date
bincxz
df11beff8c fix: clear mainWindow reference on window destroy (#587)
Some checks failed
build-packages / build-macos (push) Has been cancelled
build-packages / build-windows (push) Has been cancelled
build-packages / build-linux-x64 (push) Has been cancelled
build-packages / build-linux-arm64 (push) Has been cancelled
build-packages / release (push) Has been cancelled
The mainWindow variable was never cleared when the window was destroyed,
unlike settingsWindow which had a proper 'closed' handler. This caused
getMainWindow() to return a destroyed window object, preventing the
activate handler from correctly detecting the main window was gone and
creating a new one.

Fixes #587

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 19:16:59 +08:00
陈大猫
c14da33e5b Merge pull request #588 from binaricat/fix/settings-window-title
fix: settings window title and dock reopen behavior
2026-03-31 19:11:37 +08:00
bincxz
f1ce541885 fix: dock click opens main window instead of settings window (#587)
On macOS, when the main window is closed but the settings window is
still open, clicking the Dock icon would focus the settings window
instead of re-creating the main window.

- focusMainWindow() now explicitly finds the main window via
  getWindowManager() instead of using getAllWindows()[0]
- activate handler creates a new main window even when other
  windows (settings) are still open

Fixes #587

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 19:05:13 +08:00
bincxz
07e003fe43 fix: distinguish settings window title from main window
Set the settings window title to "netcatty Settings" and prevent
the HTML <title> tag from overriding it, so macOS Dock menu and
Window menu can distinguish between the two windows.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 19:02:36 +08:00
陈大猫
81f53c9a7f Merge pull request #585 from binaricat/feat/always-immersive-mode
feat: enable immersive mode permanently
2026-03-31 16:25:57 +08:00
bincxz
2d8cea2e7d fix: remove stale immersive mode sync/rehydration handlers
Address Codex review: remove references to setImmersiveModeState
in rehydration, IPC sync, and cross-window storage handlers that
would throw after the state setter was removed.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 15:37:59 +08:00
bincxz
b724cfc775 feat: enable immersive mode permanently and remove settings toggle
Immersive mode is now always on — the UI chrome automatically adapts
to match the active terminal theme. The toggle in Appearance settings
has been removed and the TerminalLayer preview logic simplified.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 15:29:12 +08:00
5 changed files with 27 additions and 60 deletions

View File

@@ -33,7 +33,7 @@ import {
STORAGE_KEY_GLOBAL_HOTKEY_ENABLED,
STORAGE_KEY_AUTO_UPDATE_ENABLED,
STORAGE_KEY_WORKSPACE_FOCUS_STYLE,
STORAGE_KEY_IMMERSIVE_MODE,
} from '../../infrastructure/config/storageKeys';
import { DEFAULT_UI_LOCALE, resolveSupportedLocale } from '../../infrastructure/config/i18n';
import { TERMINAL_THEMES } from '../../infrastructure/config/terminalThemes';
@@ -340,20 +340,11 @@ export const useSettingsState = () => {
}
}, []);
const [immersiveMode, setImmersiveModeState] = useState<boolean>(() => {
const stored = localStorageAdapter.readString(STORAGE_KEY_IMMERSIVE_MODE);
if (stored === null || stored === '') {
// Persist default so collectSyncableSettings() can include it
localStorageAdapter.writeString(STORAGE_KEY_IMMERSIVE_MODE, 'true');
return true;
}
return stored === 'true';
});
const setImmersiveMode = useCallback((enabled: boolean) => {
setImmersiveModeState(enabled);
localStorageAdapter.writeString(STORAGE_KEY_IMMERSIVE_MODE, String(enabled));
notifySettingsChanged(STORAGE_KEY_IMMERSIVE_MODE, enabled);
}, [notifySettingsChanged]);
// Immersive mode is always enabled — the toggle has been removed from settings
const immersiveMode = true;
const setImmersiveMode = useCallback((_enabled: boolean) => {
// no-op: immersive mode is always on
}, []);
const setSftpTransferConcurrency = useCallback((value: number) => {
const clamped = Math.max(1, Math.min(16, Math.round(value)));
@@ -465,14 +456,6 @@ export const useSettingsState = () => {
const storedDefaultViewMode = readStoredString(STORAGE_KEY_SFTP_DEFAULT_VIEW_MODE);
if (storedDefaultViewMode === 'list' || storedDefaultViewMode === 'tree') setSftpDefaultViewMode(storedDefaultViewMode);
// Immersive mode
const storedImmersive = readStoredString(STORAGE_KEY_IMMERSIVE_MODE);
if (storedImmersive === 'true' || storedImmersive === 'false') {
const val = storedImmersive === 'true';
setImmersiveModeState(val);
notifySettingsChanged(STORAGE_KEY_IMMERSIVE_MODE, val);
}
// Workspace focus style
const storedFocusStyle = readStoredString(STORAGE_KEY_WORKSPACE_FOCUS_STYLE);
if (storedFocusStyle === 'dim' || storedFocusStyle === 'border') setWorkspaceFocusStyleState(storedFocusStyle);
@@ -625,9 +608,6 @@ export const useSettingsState = () => {
setSftpDefaultViewMode((prev) => (prev === value ? prev : value));
}
}
if (key === STORAGE_KEY_IMMERSIVE_MODE && typeof value === 'boolean') {
setImmersiveModeState((prev) => (prev === value ? prev : value));
}
if (key === STORAGE_KEY_WORKSPACE_FOCUS_STYLE && (value === 'dim' || value === 'border')) {
setWorkspaceFocusStyleState((prev) => (prev === value ? prev : value));
}
@@ -849,13 +829,6 @@ export const useSettingsState = () => {
setAutoUpdateEnabled(newValue);
}
}
// Sync immersive mode from other windows
if (e.key === STORAGE_KEY_IMMERSIVE_MODE && e.newValue !== null) {
const newValue = e.newValue === 'true';
if (newValue !== s.immersiveMode) {
setImmersiveModeState(newValue);
}
}
// Sync workspace focus style from other windows
if (e.key === STORAGE_KEY_WORKSPACE_FOCUS_STYLE && e.newValue !== null) {
if (e.newValue === 'dim' || e.newValue === 'border') {

View File

@@ -1452,13 +1452,12 @@ const TerminalLayerInner: React.FC<TerminalLayerProps> = ({
}, [applyTerminalPreviewVars, themePreview]);
useEffect(() => {
const themeToApply = activeTopTabsThemeId || (previewTargetSessionId ? focusedThemeId : null);
if (themeToApply) {
applyTopTabsPreviewVars(themeToApply);
} else {
clearTopTabsPreviewVars();
if (activeTopTabsThemeId) {
applyTopTabsPreviewVars(activeTopTabsThemeId);
return;
}
}, [activeTopTabsThemeId, focusedThemeId, previewTargetSessionId, applyTopTabsPreviewVars]);
clearTopTabsPreviewVars();
}, [activeTopTabsThemeId, applyTopTabsPreviewVars]);
useEffect(() => {
const panelOpen = activeSidePanelTab === 'theme' && !!previewTargetSessionId;

View File

@@ -258,19 +258,6 @@ export default function SettingsAppearanceTab(props: {
</SettingRow>
</div>
<SectionHeader title={t("settings.appearance.immersiveMode")} />
<div className="space-y-0 divide-y divide-border rounded-lg border bg-card px-4">
<SettingRow
label={t("settings.appearance.immersiveMode")}
description={t("settings.appearance.immersiveMode.desc")}
>
<Toggle
checked={!!isImmersive}
onChange={() => onToggleImmersive?.()}
/>
</SettingRow>
</div>
<SectionHeader title={t("settings.appearance.customCss")} />
<div className="space-y-2">
<p className="text-xs text-muted-foreground">

View File

@@ -675,6 +675,11 @@ async function createWindow(electronModule, options) {
mainWindow = win;
// Clear reference when the main window is destroyed
win.on('closed', () => {
if (mainWindow === win) mainWindow = null;
});
// Log renderer crashes for diagnostics (skip normal clean exits)
win.webContents.on("render-process-gone", (_event, details) => {
if (details?.reason === "clean-exit") return;
@@ -917,6 +922,7 @@ async function openSettingsWindow(electronModule, options, { showOnLoad = true }
}
const win = new BrowserWindow({
title: "netcatty Settings",
width: settingsWidth,
height: settingsHeight,
...(settingsX !== undefined && settingsY !== undefined ? { x: settingsX, y: settingsY } : {}),
@@ -1042,6 +1048,9 @@ async function openSettingsWindow(electronModule, options, { showOnLoad = true }
settingsWindow = null;
});
// Prevent HTML <title> from overriding the window title
win.on('page-title-updated', (e) => { e.preventDefault(); });
// Load the settings page
const settingsPath = '/#/settings';

View File

@@ -318,8 +318,8 @@ function registerAppProtocol() {
function focusMainWindow() {
try {
const wins = BrowserWindow.getAllWindows();
const win = wins && wins.length ? wins[0] : null;
const mainWin = getWindowManager().getMainWindow?.();
const win = mainWin && !mainWin.isDestroyed?.() ? mainWin : null;
if (!win) return false;
// Check if the webContents has crashed or been destroyed
@@ -1074,12 +1074,11 @@ if (!gotLock) {
} catch {}
if (focusMainWindow()) return;
if (BrowserWindow.getAllWindows().length === 0) {
void createWindow().catch((err) => {
console.error("[Main] Failed to create window on activate:", err);
showStartupError(err);
});
}
// Main window doesn't exist — create it even if other windows (e.g. settings) are open
void createWindow().catch((err) => {
console.error("[Main] Failed to create window on activate:", err);
showStartupError(err);
});
});
});