[Init] Initial commit - NetMesh terminal manager
Some checks failed
build-packages / resolve bundled mosh-client (push) Has been cancelled
build-packages / resolve bundled et-client (push) Has been cancelled
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
build-packages / update Nix release metadata (push) Has been cancelled
build-packages / bump homebrew tap (push) Has been cancelled
test / lint-and-test (push) Has been cancelled
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled
build-et-binaries / build-linux-x64 (push) Has been cancelled
build-et-binaries / build-linux-arm64 (push) Has been cancelled
build-et-binaries / build-macos-universal (push) Has been cancelled
build-et-binaries / build-windows-x64 (push) Has been cancelled
build-et-binaries / release (push) Has been cancelled

This commit is contained in:
2026-09-13 18:24:01 +08:00
commit 3c72efcb7f
3255 changed files with 907009 additions and 0 deletions

View File

@@ -0,0 +1,588 @@
/* eslint-disable no-undef */
const {
windowsFramelessContentChromeOptions,
} = require("./windowsWindowChrome.cjs");
const { attachDisplayRecovery } = require("./displayRecovery.cjs");
const TERMINAL_KEYBOARD_FOCUS = Symbol("netcattyTerminalKeyboardFocus");
function setTerminalKeyboardFocusForWindow(win, focused) {
if (!win || win.isDestroyed?.() || !win.webContents) return false;
const isFocused = focused === true;
try {
win[TERMINAL_KEYBOARD_FOCUS] = isFocused;
win.webContents.setIgnoreMenuShortcuts?.(isFocused);
return true;
} catch {
return false;
}
}
function hasTerminalKeyboardFocus(win) {
return win?.[TERMINAL_KEYBOARD_FOCUS] === true;
}
function createMainWindowApi(ctx) {
with (ctx) {
async function createWindow(electronModule, options) {
const { BrowserWindow, nativeTheme, app, screen, shell } = electronModule;
const {
preload,
devServerUrl,
isDev,
appIcon,
isMac,
onRegisterBridge,
electronDir,
route,
onAppContentWindowClosed,
registerAsMainWindow = true,
persistWindowState = registerAsMainWindow,
registerAsAppContentWindow = true,
startHidden = false,
} = options;
const rendererHash = typeof route === "string" && route.trim()
? `#/${route.trim().replace(/^#?\/*/, "")}`
: "";
const CHROMIUM_ZOOM_FACTORS = [
0.25, 0.33, 0.5, 0.67, 0.75, 0.9, 1, 1.1, 1.25, 1.5, 1.75, 2, 2.5, 3, 4, 5,
];
const isPrimaryZoomInEqualInput = (input) => {
if (input?.type !== "keyDown") return false;
if (input.alt) return false;
const hasPrimaryModifier = isMac
? Boolean(input.meta) && !input.control
: Boolean(input.control) && !input.meta;
if (!hasPrimaryModifier || input.shift) return false;
return String(input.key || "") === "=";
};
const isPrimaryZoomOutMinusInput = (input) => {
if (input?.type !== "keyDown") return false;
if (input.alt) return false;
const hasPrimaryModifier = isMac
? Boolean(input.meta) && !input.control
: Boolean(input.control) && !input.meta;
if (!hasPrimaryModifier || input.shift) return false;
return String(input.key || "") === "-";
};
const isPrimaryResetZoomInput = (input) => {
if (input?.type !== "keyDown") return false;
if (input.alt) return false;
const hasPrimaryModifier = isMac
? Boolean(input.meta) && !input.control
: Boolean(input.control) && !input.meta;
if (!hasPrimaryModifier || input.shift) return false;
return String(input.key || "") === "0";
};
const adjustWindowZoom = (mode) => {
const webContents = win?.webContents;
if (!webContents || webContents.isDestroyed?.()) return false;
const currentFactor = Number(webContents.getZoomFactor?.());
const safeCurrentFactor = Number.isFinite(currentFactor) ? currentFactor : 1;
const nextFactor = mode === "in"
? CHROMIUM_ZOOM_FACTORS.find((factor) => factor > safeCurrentFactor + 0.0001)
: mode === "out"
? [...CHROMIUM_ZOOM_FACTORS].reverse().find((factor) => factor < safeCurrentFactor - 0.0001)
: 1;
if (!nextFactor) return false;
try {
webContents.setZoomFactor?.(nextFactor);
return true;
} catch {
return false;
}
};
// Store app reference for window state persistence
electronApp = app;
const osTheme = nativeTheme?.shouldUseDarkColors ? "dark" : "light";
const effectiveTheme = currentTheme === "dark" || currentTheme === "light" ? currentTheme : osTheme;
const frontendBackground = resolveFrontendBackgroundColor(electronDir || __dirname, effectiveTheme);
const backgroundColor = frontendBackground || "#1a1a1a";
const themeConfig = THEME_COLORS[effectiveTheme] || THEME_COLORS.light;
// Load saved window state
const savedState = persistWindowState ? loadWindowState() : null;
let windowBounds = {
width: DEFAULT_WINDOW_WIDTH,
height: DEFAULT_WINDOW_HEIGHT,
};
if (savedState) {
// Use saved dimensions, but clamp to the minimum so a previously
// shrunk window from an older build cannot start below the minimum.
windowBounds.width = Math.max(savedState.width, MIN_WINDOW_WIDTH);
windowBounds.height = Math.max(savedState.height, MIN_WINDOW_HEIGHT);
// Only use saved position if the screen is available at that location
if (typeof savedState.x === "number" && typeof savedState.y === "number") {
try {
// Check if the saved position is within any available display
const displays = screen?.getAllDisplays?.() || [];
const isPositionVisible = displays.some((display) => {
const { x, y, width, height } = display.bounds;
// Check if at least part of the window would be visible on this display
return (
savedState.x < x + width &&
savedState.x + savedState.width > x &&
savedState.y < y + height &&
savedState.y + savedState.height > y
);
});
if (isPositionVisible) {
windowBounds.x = savedState.x;
windowBounds.y = savedState.y;
}
} catch {
// Ignore screen check errors, just don't set position
}
}
}
const windowsChrome = !isMac ? windowsFramelessContentChromeOptions() : {};
const win = new BrowserWindow({
...windowBounds,
minWidth: MIN_WINDOW_WIDTH,
minHeight: MIN_WINDOW_HEIGHT,
backgroundColor,
icon: appIcon,
show: false,
frame: isMac,
titleBarStyle: isMac ? "hiddenInset" : undefined,
trafficLightPosition: isMac ? { x: 12, y: 12 } : undefined,
...windowsChrome,
webPreferences: {
preload,
contextIsolation: true,
nodeIntegration: false,
sandbox: false,
spellcheck: false,
backgroundThrottling: false,
v8CacheOptions: V8_CACHE_OPTIONS,
},
});
if (registerAsMainWindow) {
if (typeof registerMainWindow === "function") {
registerMainWindow(win);
} else {
mainWindow = win;
}
} else if (registerAsAppContentWindow && typeof registerAppContentWindow === "function") {
registerAppContentWindow(win, { queryDirtyEditors: true });
}
// Multi-monitor recovery (#3244): Windows lock/sleep can temporarily
// tear down a secondary display and the OS relocates the window onto
// the primary display. When the display comes back, put the window back
// where the user left it.
let detachDisplayRecovery = null;
try {
detachDisplayRecovery = attachDisplayRecovery({ win, screen });
} catch {
detachDisplayRecovery = null;
}
// Clear reference when the main window is destroyed
win.on('closed', () => {
try {
detachDisplayRecovery?.();
} catch {
// ignore
}
try {
if (win?.webContents?.id) {
unhealthyWebContentsIds.delete(win.webContents.id);
rendererReadySeenByWebContentsId.delete(win.webContents.id);
}
} catch {
// ignore
}
if (registerAsMainWindow) {
if (typeof unregisterMainWindow === "function") {
unregisterMainWindow(win);
} else if (mainWindow === win) {
mainWindow = null;
}
} else if (registerAsAppContentWindow && typeof unregisterAppContentWindow === "function") {
unregisterAppContentWindow(win);
}
if (registerAsAppContentWindow) {
try {
if (typeof notifyAppContentWindowClosed === "function") {
notifyAppContentWindowClosed(win);
} else {
onAppContentWindowClosed?.(win);
}
} catch {
// The application-level close fallback must not disrupt teardown.
}
}
});
// Log renderer crashes for diagnostics (skip normal clean exits)
win.webContents.on("render-process-gone", (_event, details) => {
if (details?.reason === "clean-exit") return;
try {
if (win.webContents?.id) {
unhealthyWebContentsIds.add(win.webContents.id);
}
} catch {
// ignore
}
try {
const crashLogBridge = require("./crashLogBridge.cjs");
crashLogBridge.captureError("render-process-gone", new Error(
`Renderer process gone: reason=${details?.reason}, exitCode=${details?.exitCode}`
), { reason: details?.reason, exitCode: details?.exitCode });
} catch {}
console.error("[WindowManager] Renderer process gone:", details);
});
// Prevent top-level navigation away from the app origin. If a remote origin ever
// loads in a privileged window (with preload), it can become an RCE vector.
const allowedOrigins = new Set(["app://netcatty"]);
if (isDev && devServerUrl) {
try {
allowedOrigins.add(new URL(getDevRendererBaseUrl(devServerUrl)).origin);
} catch {
// ignore invalid dev server URL
}
}
const isAllowedTopLevelUrl = (targetUrl) => {
try {
const parsedUrl = new URL(String(targetUrl));
if (parsedUrl.protocol === "app:" && parsedUrl.host === "netcatty") return true;
return allowedOrigins.has(parsedUrl.origin);
} catch {
return false;
}
};
win.webContents.on("did-start-navigation", (_event, targetUrl, isInPlace, isMainFrame) => {
if (isMainFrame === false || isInPlace === true || !isAllowedTopLevelUrl(targetUrl)) return;
try {
if (typeof clearRendererReadyForWebContents === "function") {
clearRendererReadyForWebContents(win.webContents?.id);
}
} catch {
// ignore
}
});
const blockUntrustedNavigation = (event, targetUrl) => {
if (isAllowedTopLevelUrl(targetUrl)) return;
try {
event.preventDefault();
} catch {
// ignore
}
debugLog("Blocked navigation to untrusted origin", { targetUrl });
};
win.webContents.on("will-navigate", blockUntrustedNavigation);
win.webContents.on("will-redirect", blockUntrustedNavigation);
// Prevent Chromium from consuming Alt+Arrow as browser back/forward navigation.
// Terminal apps need these keys to pass through to the remote shell (e.g., byobu, tmux).
// Using setIgnoreMenuShortcuts lets the keydown still reach the page (xterm.js)
// while preventing Chromium's built-in shortcuts from triggering.
win.webContents.on("before-input-event", (event, input) => {
if (isMac && shouldCloseWindowFromInput(input)) {
event.preventDefault();
requestWindowCommandClose(win);
return;
}
const isTerminalFontShortcut =
isPrimaryZoomInEqualInput(input)
|| isPrimaryZoomOutMinusInput(input)
|| isPrimaryResetZoomInput(input);
if (hasTerminalKeyboardFocus(win) && isTerminalFontShortcut) {
win.webContents.setIgnoreMenuShortcuts(true);
return;
}
if (isPrimaryZoomInEqualInput(input) && adjustWindowZoom("in")) {
event.preventDefault();
return;
}
if (isPrimaryZoomOutMinusInput(input) && adjustWindowZoom("out")) {
event.preventDefault();
return;
}
if (isPrimaryResetZoomInput(input) && adjustWindowZoom("reset")) {
event.preventDefault();
return;
}
if (input.alt && !input.control && !input.meta) {
if (input.key === "ArrowLeft" || input.key === "ArrowRight") {
win.webContents.setIgnoreMenuShortcuts(true);
return;
}
}
win.webContents.setIgnoreMenuShortcuts(false);
});
// Restore maximized state if it was saved
if (savedState?.isMaximized && !savedState?.isFullScreen) {
win.once("ready-to-show", () => {
try {
win.maximize();
} catch {
// ignore
}
});
}
// Track window bounds for saving (use last non-maximized/non-fullscreen bounds)
let lastNormalBounds = null;
let saveStateTimer = null;
let thisWindowCloseRequested = false;
let dirtyEditorCloseConfirmed = false;
const updateNormalBounds = () => {
if (!win.isDestroyed() && !win.isMaximized() && !win.isFullScreen()) {
lastNormalBounds = win.getBounds();
}
};
const scheduleSaveState = () => {
if (!persistWindowState) return;
if (saveStateTimer) clearTimeout(saveStateTimer);
saveStateTimer = setTimeout(() => {
const state = getWindowBoundsState(win, lastNormalBounds);
if (state) queueWindowStateSave(state);
}, 500);
};
// Update normal bounds on resize/move when not maximized/fullscreen
win.on("resize", () => {
updateNormalBounds();
scheduleSaveState();
});
win.on("move", () => {
updateNormalBounds();
scheduleSaveState();
});
win.on("maximize", scheduleSaveState);
win.on("unmaximize", () => {
updateNormalBounds();
scheduleSaveState();
});
const queryDirtyEditorsBeforeClose = (event, { markCloseRequested = false } = {}) => {
event.preventDefault();
if (markCloseRequested) {
thisWindowCloseRequested = true;
}
const dirtyEditorQuery = typeof queryDirtyEditors === "function"
? queryDirtyEditors(win.webContents, 5000, { ipcMain: electronModule.ipcMain })
: false;
Promise.resolve(dirtyEditorQuery)
.then((hasDirty) => {
if (hasDirty) {
thisWindowCloseRequested = false;
return;
}
dirtyEditorCloseConfirmed = true;
try {
win.close();
} catch {
// ignore
}
})
.catch(() => {
dirtyEditorCloseConfirmed = true;
try {
win.close();
} catch {
// ignore
}
});
};
// Save state when window is about to close
win.on("close", (event) => {
if (!registerAsMainWindow && registerAsAppContentWindow && !isQuitting && !dirtyEditorCloseConfirmed) {
queryDirtyEditorsBeforeClose(event, { markCloseRequested: true });
return;
}
// Check if close-to-tray is enabled
const trackedMainWindowCount = typeof getMainWindowCount === "function" ? getMainWindowCount() : 1;
if (registerAsMainWindow && trackedMainWindowCount <= 1 && !isQuitting && getGlobalShortcutBridge().handleWindowClose(event, win)) {
// Window was hidden to tray - save state before returning
if (saveStateTimer) clearTimeout(saveStateTimer);
const state = persistWindowState ? getWindowBoundsState(win, lastNormalBounds) : null;
if (state) saveWindowStateSync(state);
hideSettingsWindow();
return;
}
if (registerAsMainWindow && registerAsAppContentWindow && !isQuitting && !dirtyEditorCloseConfirmed) {
queryDirtyEditorsBeforeClose(event);
return;
}
if (thisWindowCloseRequested) {
return;
}
thisWindowCloseRequested = true;
if (saveStateTimer) clearTimeout(saveStateTimer);
const state = persistWindowState ? getWindowBoundsState(win, lastNormalBounds) : null;
if (pendingWindowStateWrite) {
event.preventDefault();
if (state) queuedWindowState = state;
pendingWindowStateWrite
.catch(() => {
// ignore async write errors before closing
})
.finally(() => {
const finalState = persistWindowState ? getWindowBoundsState(win, lastNormalBounds) : null;
if (finalState) saveWindowStateSync(finalState);
if (registerAsMainWindow) closeSettingsWindow();
try {
win.close();
} catch {
// ignore
}
});
return;
}
if (state) saveWindowStateSync(state);
if (registerAsMainWindow) closeSettingsWindow();
});
const safeSend = (channel, ...args) => {
try {
if (!win.isDestroyed() && win.webContents && !win.webContents.isDestroyed()) {
win.webContents.send(channel, ...args);
}
} catch {
// Render frame disposed during HMR / reload safe to ignore
}
};
win.on("enter-full-screen", () => {
safeSend("netcatty:window:fullscreen-changed", true);
scheduleSaveState();
});
win.on("leave-full-screen", () => {
safeSend("netcatty:window:fullscreen-changed", false);
updateNormalBounds();
scheduleSaveState();
});
win.on("show", () => {
safeSend("netcatty:window:shown");
if (startHidden) {
// The hidden-launch tray pin exists so a --hidden cold start is
// never a windowless, trayless zombie; once this window is
// actually visible that concern no longer applies, so hand tray
// lifetime back to the user's normal close-to-tray preference.
try {
getGlobalShortcutBridge().releaseHiddenLaunchTrayPin?.();
} catch (err) {
console.warn("[MainWindow] Failed to release hidden-launch tray pin:", err?.message || err);
}
}
});
// Ensure native background matches frontend background, even before first paint.
try {
win.setBackgroundColor(backgroundColor);
} catch {
// ignore
}
applyWindowOpacityToWindow(win);
// Defer show until renderer is ready; use fallback timeout to avoid keeping window hidden forever.
// Production gets a shorter timeout since the splash screen provides visual feedback.
setupDeferredShow(win, { timeoutMs: isDev ? 3000 : 1500, startHidden });
win.webContents.on("did-create-window", (childWindow) => {
try {
childWindow.setMenuBarVisibility(false);
childWindow.autoHideMenuBar = true;
childWindow.removeMenu();
} catch {
// ignore
}
try {
const iconPath = resolveLiveAppIcon(appIcon);
if (iconPath && childWindow.setIcon) childWindow.setIcon(iconPath);
} catch {
// ignore
}
// Never allow chained popups from remote content windows.
try {
childWindow.webContents?.setWindowOpenHandler?.(createExternalOnlyWindowOpenHandler(shell));
} catch {
// ignore
}
attachOAuthLoadingOverlay(childWindow);
});
win.webContents.setWindowOpenHandler(
createAppWindowOpenHandler(shell, {
backgroundColor,
appIcon,
getAppIcon: () => resolveLiveAppIcon(appIcon),
})
);
// Register window control handlers
registerWindowHandlers(electronModule.ipcMain, nativeTheme);
// Register IPC handlers BEFORE loading any URL so the renderer never
// calls a handler that hasn't been registered yet.
onRegisterBridge?.(win);
if (startHidden) {
// Belt-and-suspenders: guarantee a tray icon exists as soon as
// bridges (and electronModule) are ready, and keep it pinned
// (independent of the user's separate close-to-tray preference)
// until this window is actually shown — see releaseHiddenLaunchTrayPin
// in the "show" handler below.
try {
getGlobalShortcutBridge().pinTrayForHiddenLaunch?.();
} catch (err) {
console.warn("[MainWindow] Failed to create tray for hidden launch:", err?.message || err);
}
}
if (isDev) {
try {
await win.loadURL(`${getDevRendererBaseUrl(devServerUrl)}${rendererHash}`);
win.webContents.openDevTools({ mode: "detach" });
return win;
} catch (e) {
console.warn("Dev server not reachable, falling back to bundled dist.", e);
}
}
// Production mode - load via custom protocol.
await win.loadURL(`app://netcatty/index.html${rendererHash}`);
return win;
}
return { createWindow };
}
}
module.exports = {
createMainWindowApi,
setTerminalKeyboardFocusForWindow,
};