* fix(windows): restore frameless rounded corners and tray transparency (#2505) Windows frameless windows used a solid host backgroundColor under DWM rounding, which showed as a dark rim, while the tray popup kept the default opaque backdrop under CSS border-radius (square tips / 冒尖). Use a clear transparent host with native roundedCorners for app windows, and disable OS rounding for the CSS-shaped tray overlay. Keep html/body transparent on the tray route so corner pixels stay see-through. Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com> * fix(windows): require chrome helpers outside with(ctx) createMainWindowApi injects require from windowManager.cjs, so a relative require inside with(ctx) resolved to electron/bridges/windowsWindowChrome.cjs and crashed every main-window create (MODULE_NOT_FOUND). Load the helpers at module scope in each factory so paths stay file-local. Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com> * fix(windows): keep resizable app windows opaque Codex P2: Electron documents transparent BrowserWindows as not resizable, and enabling resizable can break them. App content windows now only set roundedCorners: true with a solid theme backdrop; the tray overlay stays transparent because it is already non-resizable. Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com> * fix(tray): hide opaque splash on tray overlay route Codex P2: the shared splash screen still painted an opaque full-window fill when the tray panel showed before React mounted, recreating the square-corner flash. Mark tray-window early, CSS-hide the splash, and remove the splash node on DOM ready / tray render. Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com> * fix(tray): defer first show until ready; keep chrome comments ASCII Wait to show the tray BrowserWindow until did-finish-load so the shared opaque splash cannot flash as a square underlay. Also rewrite the new helper file header in ASCII-only English per AGENTS.md. Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com> * fix(tray): cancel pending first-load show on second toggle If the user toggles the tray again while the first renderer load is still pending, clear trayPanelShowWhenReady so did-finish-load cannot open the panel after an intentional dismiss. Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com> * fix(tray): keep shared splash transparent by default Move the opaque splash fill behind html:not(.tray-window) so the tray overlay route never inherits a solid full-window background from the shared splash markup. Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com> --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
318 lines
10 KiB
JavaScript
318 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,
|
|
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,
|
|
};
|
|
}
|
|
}
|
|
|
|
module.exports = { createTerminalPopupWindowApi };
|