[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
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:
321
electron/bridges/windowManager/terminalPopupWindow.cjs
Normal file
321
electron/bridges/windowManager/terminalPopupWindow.cjs
Normal file
@@ -0,0 +1,321 @@
|
||||
/* 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 };
|
||||
Reference in New Issue
Block a user