[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,296 @@
"use strict";
function emitAppLockReopen(windows) {
const seen = new Set();
for (const win of Array.isArray(windows) ? windows : []) {
try {
if (!win || win.isDestroyed?.()) continue;
const id = win.webContents?.id;
if (id && seen.has(id)) continue;
if (id) seen.add(id);
win.webContents?.send?.("netcatty:app-lock:reopen");
} catch {
// ignore
}
}
}
function shouldBackgroundLockOnHide(appLockController) {
return Boolean(appLockController && typeof appLockController.setLocked === "function");
}
function handleAppHide(appLockController) {
if (!shouldBackgroundLockOnHide(appLockController)) return false;
try {
appLockController.setLocked("background");
return true;
} catch {
return false;
}
}
/**
* macOS keeps the process alive after the last BrowserWindow closes
* (`window-all-closed` only quits on non-darwin). App-lock runtime is shared
* for the process lifetime, so an unlock can otherwise survive a full
* close → Dock reopen cycle. Re-apply lock with `startup` (same reason as
* cold start) so a fresh renderer withholds content until unlock. Prefer this
* over `background`, which waits on a reopen signal that a brand-new window
* never receives.
*/
function ensureAppLockForFreshSession(appLockController, reason = "startup") {
if (!shouldBackgroundLockOnHide(appLockController)) return false;
const nextReason = typeof reason === "string" && reason.trim() !== "" ? reason : "startup";
try {
appLockController.setLocked(nextReason);
return true;
} catch {
return false;
}
}
/**
* True when no usable app-content windows remain (main / settings / etc.).
* Used to decide whether recreating a main window is a "fresh session" reopen
* after the last window was closed, vs. adding another window while one is open.
*/
function hasNoUsableAppContentWindows(appContentWindows) {
if (!Array.isArray(appContentWindows) || appContentWindows.length === 0) return true;
return !appContentWindows.some((win) => {
try {
if (!win || (typeof win.isDestroyed === "function" && win.isDestroyed())) {
return false;
}
// Hidden prewarm windows (e.g. Settings) keep the process alive but are
// not a user session — do not block fresh-session re-lock (Codex P2).
// Minimized windows also report !isVisible but are still a live session
// (Codex P2 on 4e6c3235).
if (typeof win.isVisible === "function" && !win.isVisible()) {
if (typeof win.isMinimized === "function" && win.isMinimized()) {
return true;
}
return false;
}
return true;
} catch {
return false;
}
});
}
function handleActivateWithMainWindow({
app,
mainWindow,
globalShortcutBridge,
windowManager,
reopenWindows,
}) {
if (!mainWindow || mainWindow.isDestroyed?.()) return false;
try {
if (mainWindow.webContents?.isCrashed?.()) {
mainWindow.destroy?.();
return false;
}
} catch {
// ignore
}
try {
globalShortcutBridge?.clearPendingFullscreenHide?.(mainWindow);
} catch {
// ignore
}
if (typeof windowManager?.showAndFocusMainWindow === "function") {
windowManager.showAndFocusMainWindow(mainWindow);
} else {
try {
if (mainWindow.isMinimized?.()) mainWindow.restore?.();
} catch {
// ignore
}
try {
mainWindow.show?.();
} catch {
// ignore
}
try {
mainWindow.focus?.();
} catch {
// ignore
}
}
emitAppLockReopen(reopenWindows);
try {
app?.focus?.({ steal: true });
} catch {
// ignore
}
return true;
}
function shouldCommitQuitWithoutDirtyCheck({
reachableMainWindows,
queryableWebContents,
}) {
const hasReachableMainWindows = Array.isArray(reachableMainWindows) && reachableMainWindows.length > 0;
if (!hasReachableMainWindows) return true;
const hasQueryableWebContents = Array.isArray(queryableWebContents) && queryableWebContents.length > 0;
return !hasQueryableWebContents;
}
async function handleBeforeQuit({
event,
mainWindows,
queryDirtyEditors,
appLockController,
windowManager,
app,
ipcMain,
quitConfirmed,
quitGuardChannelBusy,
timeoutMs,
setQuitGuardChannelBusy,
setQuitConfirmed,
commitQuit,
cancelPendingUpdateInstall,
}) {
if (quitConfirmed) return { committed: false, skipped: "quit-confirmed" };
if (quitGuardChannelBusy) {
event?.preventDefault?.();
return { committed: false, skipped: "busy" };
}
// When the caller provides commitQuit (async plugin shutdown before the real
// quit), the original quit event must stay cancelled until commitQuit
// re-enters app.quit(). commitQuit owns background-lock, isQuitting and
// quitConfirmed in that case.
const commit = () => {
if (typeof commitQuit === "function") {
event?.preventDefault?.();
commitQuit();
return;
}
if (shouldBackgroundLockOnHide(appLockController)) {
appLockController.setLocked("background");
}
windowManager?.setIsQuitting?.(true);
setQuitConfirmed?.(true);
app?.quit?.();
};
const reachableMainWindows = (Array.isArray(mainWindows) ? mainWindows : []).filter((candidate) => (
candidate && !candidate.isDestroyed?.()
));
// Keep the window alongside webContents so a dirty result can bring the
// owning window forward (hidden-to-tray / unfocused) before aborting quit.
const queryableWindows = reachableMainWindows.filter((candidate) => {
const wc = candidate.webContents;
return wc && !wc.isDestroyed?.() && !wc.isCrashed?.();
});
const queryableWebContents = queryableWindows
.map((candidate) => candidate.webContents)
.filter(Boolean);
if (shouldCommitQuitWithoutDirtyCheck({ reachableMainWindows, queryableWebContents })) {
commit();
return { committed: true, skipped: "fast-path" };
}
setQuitGuardChannelBusy?.(true);
event?.preventDefault?.();
try {
const dirtyResults = await Promise.all(
queryableWindows.map((win) =>
queryDirtyEditors(win.webContents, timeoutMs, { ipcMain }).then((hasDirty) => ({
win,
hasDirty: Boolean(hasDirty),
})),
),
);
setQuitGuardChannelBusy?.(false);
const dirtyWindows = dirtyResults.filter((result) => result.hasDirty).map((result) => result.win);
if (dirtyWindows.length === 0) {
commit();
return { committed: true, skipped: null };
}
// Renderer only surfaces the dirty toast in its own window. Focus every
// dirty owner so a tray-hidden or backgrounded window is visible before
// we refuse to quit.
for (const win of dirtyWindows) {
try {
if (typeof windowManager?.showAndFocusMainWindow === "function") {
windowManager.showAndFocusMainWindow(win);
} else {
try {
if (win.isMinimized?.()) win.restore?.();
} catch {
// ignore
}
try {
win.show?.();
} catch {
// ignore
}
try {
win.focus?.();
} catch {
// ignore
}
}
} catch {
// ignore
}
}
// App Lock overlay sits above renderer toasts (z-index). When the app is
// already locked, focusing the dirty window does not make the unsaved
// warning visible — surface a native dialog above the lock screen (Codex P2).
try {
const { dialog } = require("electron");
dialog.showMessageBoxSync({
type: "warning",
buttons: ["OK"],
defaultId: 0,
message: "Unsaved changes",
detail:
"One or more editors have unsaved changes. If the app is locked, unlock it, then save or discard before quitting.",
});
} catch {
// ignore missing dialog in tests
}
if (windowManager?.isQuittingForUpdate?.()) {
// Cancel the install bridge's in-flight state as well as the
// window-manager flag so a cancelled update can be retried immediately
// instead of waiting for its watchdog (#1215 review).
try {
cancelPendingUpdateInstall?.();
} catch {
// ignore
}
if (windowManager.isQuittingForUpdate?.()) {
windowManager.setQuittingForUpdate?.(false);
}
}
return { committed: false, skipped: "dirty" };
} catch {
setQuitGuardChannelBusy?.(false);
commit();
return { committed: true, skipped: "error" };
}
}
module.exports = {
emitAppLockReopen,
ensureAppLockForFreshSession,
handleAppHide,
handleActivateWithMainWindow,
handleBeforeQuit,
hasNoUsableAppContentWindows,
shouldBackgroundLockOnHide,
shouldCommitQuitWithoutDirtyCheck,
};