Files
NetMesh/electron/bridges/windowManager/terminalPopupWindow.cjs
zhaolei 3c72efcb7f
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
[Init] Initial commit - NetMesh terminal manager
2026-09-13 18:24:01 +08:00

322 lines
10 KiB
JavaScript

/* eslint-disable no-undef */
const { randomUUID } = require("node:crypto");
const crashLogBridge = require("../crashLogBridge.cjs");
const {
isAttachPopupClosePrepared,
registerAttachPopupAuthorization,
releaseAttachPopupAuthorization,
restoreAttachedSessionOutput,
} = require("../terminalAttachRestore.cjs");
const {
windowsFramelessContentChromeOptions,
} = require("./windowsWindowChrome.cjs");
const ATTACH_CLOSE_PREPARE_TIMEOUT_MS = 2000;
function createTerminalPopupWindowApi(ctx) {
with (ctx) {
const terminalPopupWindows = new Map();
/** attachSessionId -> popupId for AI silent-session observe windows */
const attachSessionPopups = new Map();
function isLiveWindow(win) {
return Boolean(win && typeof win.isDestroyed === "function" && !win.isDestroyed());
}
async function openTerminalPopupWindow(electronModule, options, payload) {
const { BrowserWindow, shell } = electronModule;
const { preload, devServerUrl, isDev, appIcon, isMac, electronDir, sourceWindow } = options;
const attachSessionId = typeof payload?.attachSessionId === "string" && payload.attachSessionId
? payload.attachSessionId
: null;
const attachAuthorization = attachSessionId ? randomUUID() : null;
if (attachSessionId) {
const existingPopupId = attachSessionPopups.get(attachSessionId);
const existing = existingPopupId ? terminalPopupWindows.get(existingPopupId) : null;
if (existing && isLiveWindow(existing.win)) {
try {
showAndFocusWindow(existing.win);
} catch {
// ignore focus races
}
return { success: true, popupId: existingPopupId, reused: true };
}
if (existingPopupId) attachSessionPopups.delete(attachSessionId);
}
const osTheme = electronModule?.nativeTheme?.shouldUseDarkColors ? "dark" : "light";
const effectiveTheme = currentTheme === "dark" || currentTheme === "light" ? currentTheme : osTheme;
const frontendBackground = resolveFrontendBackgroundColor(electronDir || __dirname, effectiveTheme);
const backgroundColor = frontendBackground || "#1a1a1a";
const popupWidth = 920;
const popupHeight = 580;
const { x: popupX, y: popupY } = resolveSettingsWindowBounds(electronModule, {
sourceWindow: sourceWindow || mainWindow,
settingsWidth: popupWidth,
settingsHeight: popupHeight,
});
const title = typeof payload?.title === "string" && payload.title.trim()
? payload.title.trim()
: "Terminal";
const popupId = String(payload?.popupId || randomUUID());
if (terminalPopupWindows.has(popupId)) {
throw new Error(`Terminal popup ID is already active: ${popupId}`);
}
crashLogBridge.captureDiagnostic("terminal-popup", "creating popup window", {
popupId,
title,
isDev,
popupX,
popupY,
});
const windowsChrome = windowsFramelessContentChromeOptions();
const win = new BrowserWindow({
title,
width: popupWidth,
height: popupHeight,
...(popupX !== undefined && popupY !== undefined ? { x: popupX, y: popupY } : {}),
minWidth: 480,
minHeight: 320,
backgroundColor,
icon: appIcon,
show: false,
frame: false,
...(isMac ? { trafficLightPosition: { x: 12, y: 12 } } : {}),
...windowsChrome,
webPreferences: {
preload,
contextIsolation: true,
nodeIntegration: false,
sandbox: false,
spellcheck: false,
backgroundThrottling: false,
v8CacheOptions: V8_CACHE_OPTIONS,
},
});
let lifecycleReleased = false;
let closePreparationRequested = false;
let closePreparationTimer = null;
const releaseLifecycle = () => {
if (lifecycleReleased) return;
lifecycleReleased = true;
if (closePreparationTimer) clearTimeout(closePreparationTimer);
terminalPopupWindows.delete(popupId);
if (attachSessionId && attachSessionPopups.get(attachSessionId) === popupId) {
attachSessionPopups.delete(attachSessionId);
// Window may be destroyed before React cleanup runs; always restore
// the display route from the main-process lifecycle.
try {
restoreAttachedSessionOutput(attachSessionId);
} catch (err) {
crashLogBridge.captureError?.("terminal-popup", err, {
popupId,
attachSessionId,
step: "restore attach output on close",
});
}
}
releaseAttachPopupAuthorization(attachAuthorization);
unregisterAppContentWindow(win);
notifyAppContentWindowClosed(win);
};
terminalPopupWindows.set(popupId, { releaseLifecycle, win });
if (attachSessionId) {
attachSessionPopups.set(attachSessionId, popupId);
registerAttachPopupAuthorization(
attachAuthorization,
attachSessionId,
win.webContents.id,
);
}
registerAppContentWindow(win);
crashLogBridge.captureDiagnostic("terminal-popup", "popup BrowserWindow created", {
popupId,
title,
webContentsId: win.webContents?.id,
});
try {
win.webContents?.setWindowOpenHandler?.(
createExternalOnlyWindowOpenHandler(shell),
);
} catch {
// ignore
}
win.on("close", (event) => {
if (!attachSessionId || isAttachPopupClosePrepared(attachAuthorization)) return;
event?.preventDefault?.();
if (closePreparationRequested) return;
closePreparationRequested = true;
try {
win.webContents.send("netcatty:terminal-popup:prepare-close", {
sessionId: attachSessionId,
authorization: attachAuthorization,
});
} catch {
// Timeout below force-closes and restores the route.
}
closePreparationTimer = setTimeout(() => {
closePreparationTimer = null;
try {
if (isLiveWindow(win)) win.destroy();
} catch {
releaseLifecycle();
}
}, ATTACH_CLOSE_PREPARE_TIMEOUT_MS);
});
win.on("closed", releaseLifecycle);
try {
win.webContents?.on?.("did-fail-load", (_event, errorCode, errorDescription, validatedURL) => {
console.warn("[TerminalPopup] Failed to load renderer", {
popupId,
errorCode,
errorDescription,
validatedURL,
});
});
win.webContents?.on?.("render-process-gone", (_event, details) => {
console.warn("[TerminalPopup] Renderer process gone", { popupId, details });
if (attachSessionId) {
restoreAttachedSessionOutput(attachSessionId);
}
try {
if (isLiveWindow(win)) win.destroy();
} catch {
releaseLifecycle();
}
});
win.webContents?.on?.("console-message", (_event, level, message, line, sourceId) => {
crashLogBridge.captureDiagnostic("terminal-popup-console", message, {
popupId,
level,
line,
sourceId,
});
});
} catch {
// ignore diagnostics wiring failures
}
win.on("page-title-updated", (e) => { e.preventDefault(); });
try {
win.setBackgroundColor(backgroundColor);
} catch {
// ignore
}
applyWindowOpacityToWindow(win);
if (isMac) {
try {
win.setWindowButtonVisibility(true);
} catch {
// ignore
}
try {
win.setWindowButtonPosition({ x: 12, y: 12 });
} catch {
// ignore
}
}
const popupPath = "#/terminal-popup";
try {
if (isDev) {
try {
const baseUrl = getDevRendererBaseUrl(devServerUrl);
crashLogBridge.captureDiagnostic("terminal-popup", "loading dev popup URL", {
popupId,
url: `${baseUrl}${popupPath}`,
});
await win.loadURL(`${baseUrl}${popupPath}`);
} catch (e) {
console.warn("[TerminalPopup] Dev server not reachable", e);
crashLogBridge.captureError("terminal-popup", e, {
popupId,
step: "load dev popup URL",
});
await win.loadURL(`app://netcatty/index.html${popupPath}`);
}
} else {
crashLogBridge.captureDiagnostic("terminal-popup", "loading packaged popup URL", {
popupId,
url: `app://netcatty/index.html${popupPath}`,
});
await win.loadURL(`app://netcatty/index.html${popupPath}`);
}
win.webContents.send("netcatty:window:terminalPopupConfig", {
...payload,
popupId,
...(attachAuthorization ? { attachAuthorization } : {}),
});
crashLogBridge.captureDiagnostic("terminal-popup", "popup config delivered", {
popupId,
title,
});
showAndFocusWindow(win);
crashLogBridge.captureDiagnostic("terminal-popup", "popup window shown", {
popupId,
title,
visible: typeof win.isVisible === "function" ? win.isVisible() : undefined,
});
return { success: true, popupId };
} catch (error) {
try {
if (isLiveWindow(win)) {
if (typeof win.destroy === "function") win.destroy();
else win.close();
}
} catch {
// Lifecycle cleanup below remains fail-safe if Electron cleanup throws.
}
releaseLifecycle();
throw error;
}
}
function closeTerminalPopupWindow(popupId) {
const record = terminalPopupWindows.get(popupId);
if (!record) return;
if (!isLiveWindow(record.win)) {
record.releaseLifecycle();
return;
}
try {
record.win.close();
} catch {
try {
record.win.destroy?.();
} catch {
// The lifecycle registry must still be released below.
} finally {
record.releaseLifecycle();
}
}
}
return {
openTerminalPopupWindow,
closeTerminalPopupWindow,
getTerminalPopupWindows() {
return Array.from(terminalPopupWindows.values());
},
};
}
}
module.exports = { createTerminalPopupWindowApi };