Compare commits

...
Author SHA1 Message Date
netcatty-bot bb54d10df6 fix(#3553): automated AI fix 2026-09-29 07:56:13 +00:00
15 changed files with 172 additions and 16 deletions

No files matched your search

+2
View File
@@ -314,6 +314,8 @@ export const enCoreMessages: Messages = {
'settings.globalHotkey.reset': 'Reset to default',
'settings.globalHotkey.closeToTray': 'Close to System Tray',
'settings.globalHotkey.closeToTrayDesc': 'When enabled, closing the window will minimize to the system tray instead of quitting.',
'settings.globalHotkey.showTrayIcon': 'Show System Tray Icon',
'settings.globalHotkey.showTrayIconDesc': 'When disabled, the tray icon is hidden but the app keeps running in the background.',
'settings.globalHotkey.enabled': 'Enable Global Hotkey',
'settings.globalHotkey.enabledDesc': 'Register system-wide keyboard shortcuts. When disabled, all global hotkeys are unregistered.',
'settings.globalHotkey.hint': 'Global hotkey works system-wide to quickly show or hide the window (Quake-style terminal).',
+2
View File
@@ -229,6 +229,8 @@ export const esCoreMessages: Messages = {
'settings.globalHotkey.reset': 'Restablecer al valor predeterminado',
'settings.globalHotkey.closeToTray': 'Cerrar a la bandeja del sistema',
'settings.globalHotkey.closeToTrayDesc': 'Cuando está activado, cerrar la ventana la minimiza a la bandeja del sistema en lugar de salir.',
'settings.globalHotkey.showTrayIcon': 'Mostrar el icono de la bandeja del sistema',
'settings.globalHotkey.showTrayIconDesc': 'Cuando está desactivado, el icono de la bandeja se oculta pero la aplicación sigue ejecutándose en segundo plano.',
'settings.globalHotkey.enabled': 'Activar tecla de acceso global',
'settings.globalHotkey.enabledDesc': 'Registra atajos de teclado a nivel de todo el sistema. Cuando está desactivado, todas las teclas de acceso global se desregistran.',
'settings.globalHotkey.hint': 'La tecla de acceso global funciona en todo el sistema para mostrar u ocultar la ventana rápidamente (terminal estilo Quake).',
+2
View File
@@ -298,6 +298,8 @@ export const ruCoreMessages: Messages = {
'settings.globalHotkey.reset': 'Сбросить по умолчанию',
'settings.globalHotkey.closeToTray': 'Сворачивать в системный трей',
'settings.globalHotkey.closeToTrayDesc': 'Если включено, при закрытии окно будет сворачиваться в системный трей вместо выхода из приложения.',
'settings.globalHotkey.showTrayIcon': 'Показывать значок в системном трее',
'settings.globalHotkey.showTrayIconDesc': "Если отключено, значок в трее скрывается, но приложение продолжает работать в фоне.",
'settings.globalHotkey.enabled': 'Включить глобальную горячую клавишу',
'settings.globalHotkey.enabledDesc': 'Регистрировать системные сочетания клавиш. Когда отключено, все глобальные горячие клавиши снимаются с регистрации.',
'settings.globalHotkey.hint': 'Глобальная горячая клавиша работает на уровне всей системы и позволяет быстро показывать или скрывать окно (терминал в стиле Quake).',
+2
View File
@@ -318,6 +318,8 @@ export const zhCNCoreMessages: Messages = {
'settings.globalHotkey.reset': '恢复默认',
'settings.globalHotkey.closeToTray': '关闭时最小化到托盘',
'settings.globalHotkey.closeToTrayDesc': '启用后,关闭窗口将最小化到系统托盘而不是退出程序。',
'settings.globalHotkey.showTrayIcon': '显示系统托盘图标',
'settings.globalHotkey.showTrayIconDesc': '关闭后,将隐藏系统托盘中的应用图标,应用仍会在后台正常运行。',
'settings.globalHotkey.enabled': '启用全局快捷键',
'settings.globalHotkey.enabledDesc': '注册系统级键盘快捷键。禁用后将取消所有全局快捷键注册。',
'settings.globalHotkey.hint': '全局快捷键在系统范围内工作,可快速显示或隐藏窗口(下拉式终端风格)。',
+2
View File
@@ -320,6 +320,8 @@ export const zhTWCoreMessages: Messages = {
'settings.globalHotkey.reset': '恢復預設',
'settings.globalHotkey.closeToTray': '關閉時最小化到系統匣',
'settings.globalHotkey.closeToTrayDesc': '啟用後,關閉視窗將最小化到系統匣而不是結束程式。',
'settings.globalHotkey.showTrayIcon': '顯示系統匣圖示',
'settings.globalHotkey.showTrayIconDesc': '停用後,將隱藏系統匣中的應用圖示,應用程式仍會在背景正常執行。',
'settings.globalHotkey.enabled': '啟用全域快速鍵',
'settings.globalHotkey.enabledDesc': '註冊系統級鍵盤快速鍵。停用後將取消所有全域快速鍵註冊。',
'settings.globalHotkey.hint': '全域快速鍵在系統範圍內工作,可快速顯示或隱藏視窗(下拉式終端風格)。',
@@ -2,6 +2,7 @@ import { useEffect, useRef, type MutableRefObject } from 'react';
import {
STORAGE_KEY_AUTO_UPDATE_ENABLED,
STORAGE_KEY_CLOSE_TO_TRAY,
STORAGE_KEY_SHOW_TRAY_ICON,
STORAGE_KEY_AUTO_LAUNCH_ENABLED,
STORAGE_KEY_GLOBAL_HOTKEY_ENABLED,
STORAGE_KEY_TOGGLE_WINDOW_HOTKEY,
@@ -42,6 +43,7 @@ interface UseSystemSettingsEffectsParams {
toggleWindowHotkey: string;
globalHotkeyEnabled: boolean;
closeToTray: boolean;
showTrayIcon: boolean;
autoLaunchEnabled: boolean;
windowOpacityRecord: WindowOpacityRecord;
windowOpacityMutationSourceRef: MutableRefObject<WindowOpacityMutationSource>;
@@ -62,6 +64,7 @@ export function useSystemSettingsEffects({
toggleWindowHotkey,
globalHotkeyEnabled,
closeToTray,
showTrayIcon,
autoLaunchEnabled,
windowOpacityRecord,
windowOpacityMutationSourceRef,
@@ -157,6 +160,23 @@ export function useSystemSettingsEffects({
notifySettingsChanged(STORAGE_KEY_GLOBAL_HOTKEY_ENABLED, globalHotkeyEnabled);
}, [enabled, globalHotkeyEnabled, notifySettingsChanged, persistMountedRef]);
// Persist and sync the show-tray-icon setting. The tray itself is managed
// in the main process: hiding the icon keeps the app running in the
// background, only the icon's visibility changes.
useEffect(() => {
if (!enabled) return;
const bridge = netcattyBridge.get();
if (bridge?.setShowTrayIcon) {
bridge.setShowTrayIcon(showTrayIcon).catch((err) => {
console.warn('[SystemTray] Failed to set show-tray-icon:', err);
});
}
localStorageAdapter.writeString(STORAGE_KEY_SHOW_TRAY_ICON, showTrayIcon ? 'true' : 'false');
// Skip IPC on initial mount
if (!persistMountedRef.current) return;
notifySettingsChanged(STORAGE_KEY_SHOW_TRAY_ICON, showTrayIcon);
}, [enabled, showTrayIcon, notifySettingsChanged, persistMountedRef]);
// Persist and sync close to tray setting
useEffect(() => {
if (!enabled) return;
+10
View File
@@ -67,6 +67,7 @@ import {
STORAGE_KEY_EXPLORER_CONTEXT_MENU_ENABLED,
STORAGE_KEY_TOGGLE_WINDOW_HOTKEY,
STORAGE_KEY_CLOSE_TO_TRAY,
STORAGE_KEY_SHOW_TRAY_ICON,
STORAGE_KEY_AUTO_LAUNCH_ENABLED,
STORAGE_KEY_HTTP_NETWORK_PROXY,
STORAGE_KEY_GLOBAL_HOTKEY_ENABLED,
@@ -550,6 +551,12 @@ export const useSettingsState = (options: { enableSettingsSync?: boolean; enable
if (stored === null) return true;
return stored === 'true';
});
const [showTrayIcon, setShowTrayIcon] = useState<boolean>(() => {
const stored = readStoredString(STORAGE_KEY_SHOW_TRAY_ICON);
// Default to true so the tray icon keeps its current always-visible behavior
if (stored === null) return true;
return stored === 'true';
});
// Optimistic cache for first paint; useSystemSettingsEffects hydrates this
// from app.getLoginItemSettings() (the real source of truth) on mount.
const [autoLaunchEnabled, setAutoLaunchEnabled] = useState<boolean>(() => {
@@ -1791,6 +1798,7 @@ export const useSettingsState = (options: { enableSettingsSync?: boolean; enable
toggleWindowHotkey,
globalHotkeyEnabled,
closeToTray,
showTrayIcon,
autoLaunchEnabled,
windowOpacityRecord,
windowOpacityMutationSourceRef,
@@ -2156,6 +2164,8 @@ export const useSettingsState = (options: { enableSettingsSync?: boolean; enable
setToggleWindowHotkey,
closeToTray,
setCloseToTray,
showTrayIcon,
setShowTrayIcon,
autoLaunchEnabled,
setAutoLaunchEnabled,
autoLaunchSupported,
+2
View File
@@ -677,6 +677,8 @@ const SettingsPageContent: React.FC<{ settings: SettingsState; appLock?: AppLock
setToggleWindowHotkey={settings.setToggleWindowHotkey}
closeToTray={settings.closeToTray}
setCloseToTray={settings.setCloseToTray}
showTrayIcon={settings.showTrayIcon}
setShowTrayIcon={settings.setShowTrayIcon}
autoLaunchEnabled={settings.autoLaunchEnabled}
setAutoLaunchEnabled={settings.setAutoLaunchEnabled}
autoLaunchSupported={settings.autoLaunchSupported}
@@ -121,6 +121,8 @@ interface SettingsSystemTabProps {
setToggleWindowHotkey: (hotkey: string) => void;
closeToTray: boolean;
setCloseToTray: (enabled: boolean) => void;
showTrayIcon: boolean;
setShowTrayIcon: (enabled: boolean) => void;
autoLaunchEnabled: boolean;
setAutoLaunchEnabled: (enabled: boolean) => void;
autoLaunchSupported: boolean;
@@ -173,6 +175,8 @@ const SettingsSystemTab: React.FC<SettingsSystemTabProps> = ({
setToggleWindowHotkey,
closeToTray,
setCloseToTray,
showTrayIcon,
setShowTrayIcon,
autoLaunchEnabled,
setAutoLaunchEnabled,
autoLaunchSupported,
@@ -1302,6 +1306,18 @@ const SettingsSystemTab: React.FC<SettingsSystemTabProps> = ({
)}
</div>
{/* Show Tray Icon */}
<SettingRow
anchorId="system-show-tray-icon"
label={t("settings.globalHotkey.showTrayIcon")}
description={t("settings.globalHotkey.showTrayIconDesc")}
>
<Toggle
checked={showTrayIcon}
onChange={setShowTrayIcon}
/>
</SettingRow>
{/* Close to Tray */}
<SettingRow
anchorId="system-close-to-tray"
+7
View File
@@ -954,6 +954,13 @@ export const SETTINGS_SEARCH_CATALOG: readonly SettingsSearchEntry[] = [
descriptionKey: "settings.globalHotkey.toggleWindowDesc",
sectionKey: "settings.globalHotkey.title",
},
{
id: "system-show-tray-icon",
tab: "system",
labelKey: "settings.globalHotkey.showTrayIcon",
descriptionKey: "settings.globalHotkey.showTrayIconDesc",
sectionKey: "settings.globalHotkey.title",
},
{
id: "system-close-to-tray",
tab: "system",
+54 -16
View File
@@ -19,6 +19,9 @@ let sendWhenRendererReady = null;
let getSystemMenuMainWindow = null;
let tray = null;
let closeToTray = false;
// Separate user preference: whether the tray icon itself is shown at all.
// Defaults to true so existing behavior (a visible tray icon) is unchanged.
let showTrayIcon = true;
let currentHotkey = null;
let hotkeyEnabled = false;
// True while a hidden auto-launch cold start has no visible window yet.
@@ -1133,50 +1136,79 @@ function destroyTray() {
}
}
/**
* Reconcile the tray's existence with the current preferences:
* - showTrayIcon=false always wins: the user asked for no tray icon, so the
* tray is destroyed even if close-to-tray or the hidden-launch pin is on.
* - Otherwise the tray exists when close-to-tray is enabled, or while a
* hidden auto-launch cold start has pinned it.
*/
function syncTrayVisibility() {
if (!showTrayIcon) {
destroyTray();
return;
}
if (closeToTray) {
if (!tray) {
createTray();
}
return;
}
// A hidden auto-launch cold start pins the tray regardless of this
// preference until its window is actually shown once — otherwise a user
// with close-to-tray off would get a windowless, trayless zombie process.
if (!hiddenLaunchTrayPinned) {
destroyTray();
}
}
/**
* Set close-to-tray behavior
*/
function setCloseToTray(enabled) {
closeToTray = !!enabled;
if (closeToTray) {
// Create tray if it doesn't exist
if (!tray) {
createTray();
}
} else {
if (!closeToTray) {
clearPendingFullscreenHide(getMainWindow());
// A hidden auto-launch cold start pins the tray regardless of this
// preference until its window is actually shown once — otherwise a user
// with close-to-tray off would get a windowless, trayless zombie process.
if (!hiddenLaunchTrayPinned) {
destroyTray();
}
}
syncTrayVisibility();
return { success: true, enabled: closeToTray };
}
/**
* Set whether the system tray icon is shown at all. The app keeps running
* in the background either way; only the icon's visibility changes.
*/
function setShowTrayIcon(enabled) {
showTrayIcon = !!enabled;
syncTrayVisibility();
return { success: true, enabled: showTrayIcon };
}
/**
* Force-create the tray for a hidden auto-launch cold start and keep it
* alive even if close-to-tray is later disabled, until the pin is released.
* An explicit "hide tray icon" preference still wins — the pin only exists
* to avoid an unreachable zombie, not to override the user's choice.
*/
function pinTrayForHiddenLaunch() {
hiddenLaunchTrayPinned = true;
if (!tray) {
if (!tray && showTrayIcon) {
createTray();
}
}
/**
* Release the hidden-launch tray pin once its window has been shown. If the
* user's close-to-tray preference is off, the tray is destroyed now instead
* of lingering until the next close-to-tray toggle.
* user's close-to-tray preference is off (or they opted out of the tray
* icon entirely), the tray is destroyed now instead of lingering until the
* next close-to-tray toggle.
*/
function releaseHiddenLaunchTrayPin() {
if (!hiddenLaunchTrayPinned) return;
hiddenLaunchTrayPinned = false;
if (!closeToTray) {
if (!closeToTray || !showTrayIcon) {
destroyTray();
}
}
@@ -1240,6 +1272,11 @@ function registerHandlers(ipcMain) {
return { enabled: closeToTray };
});
// Set whether the system tray icon is shown at all
ipcMain.handle("netcatty:tray:setShowTrayIcon", async (_event, { enabled }) => {
return setShowTrayIcon(enabled);
});
// Update tray menu data
ipcMain.handle("netcatty:tray:updateMenuData", async (_event, data) => {
setTrayMenuData(data);
@@ -1357,6 +1394,7 @@ module.exports = {
pinTrayForHiddenLaunch,
releaseHiddenLaunchTrayPin,
getTray: () => tray,
setShowTrayIcon,
getTrayPanelWindow: () => trayPanelWindow,
// Test helpers
__flushPendingPortForwardTogglesForTests: flushPendingPortForwardToggles,
@@ -1771,6 +1771,55 @@ test("setCloseToTray(false) still destroys an unpinned tray as before", async ()
}
});
test("setShowTrayIcon(false) hides the tray even with close-to-tray on", async () => {
const bridge = loadBridge();
const electronModule = createElectronStub();
const { ipcMain } = await enableCloseToTray(bridge, electronModule);
try {
assert.notEqual(bridge.getTray(), null);
const result = await ipcMain.handlers.get("netcatty:tray:setShowTrayIcon")(null, { enabled: false });
assert.deepEqual(result, { success: true, enabled: false });
assert.equal(bridge.getTray(), null, "the hide-tray-icon preference must win over close-to-tray");
} finally {
bridge.cleanup();
}
});
test("setShowTrayIcon(true) recreates the tray when close-to-tray is on", async () => {
const bridge = loadBridge();
const electronModule = createElectronStub();
const { ipcMain } = await enableCloseToTray(bridge, electronModule);
try {
await ipcMain.handlers.get("netcatty:tray:setShowTrayIcon")(null, { enabled: false });
assert.equal(bridge.getTray(), null);
await ipcMain.handlers.get("netcatty:tray:setShowTrayIcon")(null, { enabled: true });
assert.notEqual(bridge.getTray(), null, "re-enabling the icon must restore the tray");
} finally {
bridge.cleanup();
}
});
test("pinTrayForHiddenLaunch respects the hidden tray icon preference", () => {
const bridge = loadBridge();
const electronModule = createElectronStub();
bridge.init({ electronModule, getMainWindow: () => null });
const ipcMain = createIpcMainStub();
bridge.registerHandlers(ipcMain);
return ipcMain.handlers.get("netcatty:tray:setShowTrayIcon")(null, { enabled: false }).then(async () => {
try {
bridge.pinTrayForHiddenLaunch();
assert.equal(bridge.getTray(), null, "an explicit hide-tray-icon choice must not be overridden by the hidden-launch pin");
bridge.releaseHiddenLaunchTrayPin();
assert.equal(bridge.getTray(), null);
} finally {
bridge.cleanup();
}
});
});
test("tray panel forwarding start reaches the main renderer coordinator", async () => {
await withPlatform("darwin", async () => {
const bridge = loadBridge();
+2
View File
@@ -1557,6 +1557,8 @@ function createPreloadApi(ctx) {
ipcRenderer.invoke("netcatty:tray:setCloseToTray", { enabled }),
isCloseToTray: () =>
ipcRenderer.invoke("netcatty:tray:isCloseToTray"),
setShowTrayIcon: (enabled) =>
ipcRenderer.invoke("netcatty:tray:setShowTrayIcon", { enabled }),
// Auto Launch at system login (hidden to tray)
getAutoLaunch: () =>
+1
View File
@@ -171,6 +171,7 @@ export const STORAGE_KEY_MANAGED_SOURCES = 'netcatty_managed_sources_v1';
// Global Toggle Window Settings (Quake Mode)
export const STORAGE_KEY_TOGGLE_WINDOW_HOTKEY = 'netcatty_toggle_window_hotkey_v1';
export const STORAGE_KEY_CLOSE_TO_TRAY = 'netcatty_close_to_tray_v1';
export const STORAGE_KEY_SHOW_TRAY_ICON = 'netcatty_show_tray_icon_v1';
/** Optimistic cache only — the source of truth is app.getLoginItemSettings() in the main process. */
export const STORAGE_KEY_AUTO_LAUNCH_ENABLED = 'netcatty_auto_launch_enabled_v1';
/** App-level HTTP(S) proxy for cloud sync / AI (not SSH ProxyJump). */
+1
View File
@@ -65,6 +65,7 @@ declare global {
// System Tray / Close to Tray
setCloseToTray?(enabled: boolean): Promise<{ success: boolean; enabled: boolean }>;
isCloseToTray?(): Promise<{ enabled: boolean }>;
setShowTrayIcon?(enabled: boolean): Promise<{ success: boolean; enabled: boolean }>;
// Auto Launch at system login (hidden to tray)
getAutoLaunch?(): Promise<{ success: boolean; enabled: boolean; supported: boolean }>;