import * as ChildProcess from "node:child_process"; import * as Crypto from "node:crypto"; import * as FS from "node:fs"; import * as OS from "node:os"; import * as Path from "node:path"; import { app, BrowserWindow, clipboard, dialog, ipcMain, Menu, nativeImage, nativeTheme, protocol, safeStorage, shell, } from "electron"; import type { MenuItemConstructorOptions } from "electron"; import type { ClientSettings, DesktopTheme, DesktopServerExposureMode, DesktopServerExposureState, PersistedSavedEnvironmentRecord, DesktopUpdateActionResult, DesktopUpdateCheckResult, DesktopUpdateState, } from "@t3tools/contracts"; import { autoUpdater } from "electron-updater"; import type { ContextMenuItem } from "@t3tools/contracts"; import { RotatingFileSink } from "@t3tools/shared/logging"; import { parsePersistedServerObservabilitySettings } from "@t3tools/shared/serverSettings"; import { DEFAULT_DESKTOP_BACKEND_PORT, resolveDesktopBackendPort } from "./backendPort"; import { DEFAULT_DESKTOP_SETTINGS, readDesktopSettings, setDesktopServerExposurePreference, writeDesktopSettings, } from "./desktopSettings"; import { readClientSettings, readSavedEnvironmentRegistry, readSavedEnvironmentSecret, removeSavedEnvironmentSecret, writeClientSettings, writeSavedEnvironmentRegistry, writeSavedEnvironmentSecret, } from "./clientPersistence"; import { isBackendReadinessAborted, waitForHttpReady } from "./backendReadiness"; import { showDesktopConfirmDialog } from "./confirmDialog"; import { resolveDesktopServerExposure } from "./serverExposure"; import { syncShellEnvironment } from "./syncShellEnvironment"; import { getAutoUpdateDisabledReason, shouldBroadcastDownloadProgress } from "./updateState"; import { createInitialDesktopUpdateState, reduceDesktopUpdateStateOnCheckFailure, reduceDesktopUpdateStateOnCheckStart, reduceDesktopUpdateStateOnDownloadComplete, reduceDesktopUpdateStateOnDownloadFailure, reduceDesktopUpdateStateOnDownloadProgress, reduceDesktopUpdateStateOnDownloadStart, reduceDesktopUpdateStateOnInstallFailure, reduceDesktopUpdateStateOnNoUpdate, reduceDesktopUpdateStateOnUpdateAvailable, } from "./updateMachine"; import { isArm64HostRunningIntelBuild, resolveDesktopRuntimeInfo } from "./runtimeArch"; syncShellEnvironment(); const PICK_FOLDER_CHANNEL = "desktop:pick-folder"; const CONFIRM_CHANNEL = "desktop:confirm"; const SET_THEME_CHANNEL = "desktop:set-theme"; const CONTEXT_MENU_CHANNEL = "desktop:context-menu"; const OPEN_EXTERNAL_CHANNEL = "desktop:open-external"; const MENU_ACTION_CHANNEL = "desktop:menu-action"; const UPDATE_STATE_CHANNEL = "desktop:update-state"; const UPDATE_GET_STATE_CHANNEL = "desktop:update-get-state"; const UPDATE_DOWNLOAD_CHANNEL = "desktop:update-download"; const UPDATE_INSTALL_CHANNEL = "desktop:update-install"; const UPDATE_CHECK_CHANNEL = "desktop:update-check"; const GET_LOCAL_ENVIRONMENT_BOOTSTRAP_CHANNEL = "desktop:get-local-environment-bootstrap"; const GET_CLIENT_SETTINGS_CHANNEL = "desktop:get-client-settings"; const SET_CLIENT_SETTINGS_CHANNEL = "desktop:set-client-settings"; const GET_SAVED_ENVIRONMENT_REGISTRY_CHANNEL = "desktop:get-saved-environment-registry"; const SET_SAVED_ENVIRONMENT_REGISTRY_CHANNEL = "desktop:set-saved-environment-registry"; const GET_SAVED_ENVIRONMENT_SECRET_CHANNEL = "desktop:get-saved-environment-secret"; const SET_SAVED_ENVIRONMENT_SECRET_CHANNEL = "desktop:set-saved-environment-secret"; const REMOVE_SAVED_ENVIRONMENT_SECRET_CHANNEL = "desktop:remove-saved-environment-secret"; const GET_SERVER_EXPOSURE_STATE_CHANNEL = "desktop:get-server-exposure-state"; const SET_SERVER_EXPOSURE_MODE_CHANNEL = "desktop:set-server-exposure-mode"; const BASE_DIR = process.env.T3CODE_HOME?.trim() || Path.join(OS.homedir(), ".t3"); const STATE_DIR = Path.join(BASE_DIR, "userdata"); const DESKTOP_SETTINGS_PATH = Path.join(STATE_DIR, "desktop-settings.json"); const CLIENT_SETTINGS_PATH = Path.join(STATE_DIR, "client-settings.json"); const SAVED_ENVIRONMENT_REGISTRY_PATH = Path.join(STATE_DIR, "saved-environments.json"); const DESKTOP_SCHEME = "t3"; const ROOT_DIR = Path.resolve(__dirname, "../../.."); const isDevelopment = Boolean(process.env.VITE_DEV_SERVER_URL); const APP_DISPLAY_NAME = isDevelopment ? "T3 Code (Dev)" : "T3 Code (Alpha)"; const APP_USER_MODEL_ID = "com.t3tools.t3code"; const LINUX_DESKTOP_ENTRY_NAME = isDevelopment ? "t3code-dev.desktop" : "t3code.desktop"; const LINUX_WM_CLASS = isDevelopment ? "t3code-dev" : "t3code"; const USER_DATA_DIR_NAME = isDevelopment ? "t3code-dev" : "t3code"; const LEGACY_USER_DATA_DIR_NAME = isDevelopment ? "T3 Code (Dev)" : "T3 Code (Alpha)"; const COMMIT_HASH_PATTERN = /^[0-9a-f]{7,40}$/i; const COMMIT_HASH_DISPLAY_LENGTH = 12; const LOG_DIR = Path.join(STATE_DIR, "logs"); const LOG_FILE_MAX_BYTES = 10 * 1024 * 1024; const LOG_FILE_MAX_FILES = 10; const APP_RUN_ID = Crypto.randomBytes(6).toString("hex"); const SERVER_SETTINGS_PATH = Path.join(STATE_DIR, "settings.json"); const AUTO_UPDATE_STARTUP_DELAY_MS = 15_000; const AUTO_UPDATE_POLL_INTERVAL_MS = 4 * 60 * 60 * 1000; const DESKTOP_UPDATE_CHANNEL = "latest"; const DESKTOP_UPDATE_ALLOW_PRERELEASE = false; const DESKTOP_LOOPBACK_HOST = "127.0.0.1"; type DesktopUpdateErrorContext = DesktopUpdateState["errorContext"]; type LinuxDesktopNamedApp = Electron.App & { setDesktopName?: (desktopName: string) => void; }; let mainWindow: BrowserWindow | null = null; let backendProcess: ChildProcess.ChildProcess | null = null; let backendPort = 0; let backendBindHost = DESKTOP_LOOPBACK_HOST; let backendBootstrapToken = ""; let backendHttpUrl = ""; let backendWsUrl = ""; let backendEndpointUrl: string | null = null; let backendAdvertisedHost: string | null = null; let backendReadinessAbortController: AbortController | null = null; let restartAttempt = 0; let restartTimer: ReturnType | null = null; let isQuitting = false; let desktopProtocolRegistered = false; let aboutCommitHashCache: string | null | undefined; let desktopLogSink: RotatingFileSink | null = null; let backendLogSink: RotatingFileSink | null = null; let restoreStdIoCapture: (() => void) | null = null; let backendObservabilitySettings = readPersistedBackendObservabilitySettings(); let desktopSettings = readDesktopSettings(DESKTOP_SETTINGS_PATH); let desktopServerExposureMode: DesktopServerExposureMode = desktopSettings.serverExposureMode; let destructiveMenuIconCache: Electron.NativeImage | null | undefined; const expectedBackendExitChildren = new WeakSet(); const desktopRuntimeInfo = resolveDesktopRuntimeInfo({ platform: process.platform, processArch: process.arch, runningUnderArm64Translation: app.runningUnderARM64Translation === true, }); const initialUpdateState = (): DesktopUpdateState => createInitialDesktopUpdateState(app.getVersion(), desktopRuntimeInfo); function logTimestamp(): string { return new Date().toISOString(); } function logScope(scope: string): string { return `${scope} run=${APP_RUN_ID}`; } function sanitizeLogValue(value: string): string { return value.replace(/\s+/g, " ").trim(); } function readPersistedBackendObservabilitySettings(): { readonly otlpTracesUrl: string | undefined; readonly otlpMetricsUrl: string | undefined; } { try { if (!FS.existsSync(SERVER_SETTINGS_PATH)) { return { otlpTracesUrl: undefined, otlpMetricsUrl: undefined }; } return parsePersistedServerObservabilitySettings(FS.readFileSync(SERVER_SETTINGS_PATH, "utf8")); } catch (error) { console.warn("[desktop] failed to read persisted backend observability settings", error); return { otlpTracesUrl: undefined, otlpMetricsUrl: undefined }; } } function resolveConfiguredDesktopBackendPort(rawPort: string | undefined): number | undefined { if (!rawPort) { return undefined; } const parsedPort = Number.parseInt(rawPort, 10); if (!Number.isInteger(parsedPort) || parsedPort < 1 || parsedPort > 65_535) { return undefined; } return parsedPort; } function resolveDesktopDevServerUrl(): string { const devServerUrl = process.env.VITE_DEV_SERVER_URL?.trim(); if (!devServerUrl) { throw new Error("VITE_DEV_SERVER_URL is required in desktop development."); } return devServerUrl; } function backendChildEnv(): NodeJS.ProcessEnv { const env = { ...process.env }; delete env.T3CODE_PORT; delete env.T3CODE_MODE; delete env.T3CODE_NO_BROWSER; delete env.T3CODE_HOST; delete env.T3CODE_DESKTOP_WS_URL; delete env.T3CODE_DESKTOP_LAN_ACCESS; delete env.T3CODE_DESKTOP_LAN_HOST; return env; } function getDesktopServerExposureState(): DesktopServerExposureState { return { mode: desktopServerExposureMode, endpointUrl: backendEndpointUrl, advertisedHost: backendAdvertisedHost, }; } function getDesktopSecretStorage() { return { isEncryptionAvailable: () => safeStorage.isEncryptionAvailable(), encryptString: (value: string) => safeStorage.encryptString(value), decryptString: (value: Buffer) => safeStorage.decryptString(value), } as const; } function resolveAdvertisedHostOverride(): string | undefined { const override = process.env.T3CODE_DESKTOP_LAN_HOST?.trim(); return override && override.length > 0 ? override : undefined; } async function applyDesktopServerExposureMode( mode: DesktopServerExposureMode, options?: { readonly persist?: boolean; readonly rejectIfUnavailable?: boolean }, ): Promise { const advertisedHostOverride = resolveAdvertisedHostOverride(); const requestedMode = mode; let exposure = resolveDesktopServerExposure({ mode, port: backendPort, networkInterfaces: OS.networkInterfaces(), ...(advertisedHostOverride ? { advertisedHostOverride } : {}), }); if (requestedMode === "network-accessible" && exposure.endpointUrl === null) { if (options?.rejectIfUnavailable) { throw new Error("No reachable network address is available for this desktop right now."); } exposure = resolveDesktopServerExposure({ mode: "local-only", port: backendPort, networkInterfaces: OS.networkInterfaces(), ...(advertisedHostOverride ? { advertisedHostOverride } : {}), }); } desktopServerExposureMode = exposure.mode; desktopSettings = setDesktopServerExposurePreference(desktopSettings, requestedMode); backendBindHost = exposure.bindHost; backendHttpUrl = exposure.localHttpUrl; backendWsUrl = exposure.localWsUrl; backendEndpointUrl = exposure.endpointUrl; backendAdvertisedHost = exposure.advertisedHost; if (options?.persist) { writeDesktopSettings(DESKTOP_SETTINGS_PATH, desktopSettings); } return getDesktopServerExposureState(); } function relaunchDesktopApp(reason: string): void { writeDesktopLogHeader(`desktop relaunch requested reason=${reason}`); setImmediate(() => { isQuitting = true; clearUpdatePollTimer(); cancelBackendReadinessWait(); void stopBackendAndWaitForExit() .catch((error) => { writeDesktopLogHeader( `desktop relaunch backend shutdown warning message=${formatErrorMessage(error)}`, ); }) .finally(() => { restoreStdIoCapture?.(); if (isDevelopment) { app.exit(75); return; } app.relaunch({ execPath: process.execPath, args: process.argv.slice(1), }); app.exit(0); }); }); } function writeDesktopLogHeader(message: string): void { if (!desktopLogSink) return; desktopLogSink.write(`[${logTimestamp()}] [${logScope("desktop")}] ${message}\n`); } function writeBackendSessionBoundary(phase: "START" | "END", details: string): void { if (!backendLogSink) return; const normalizedDetails = sanitizeLogValue(details); backendLogSink.write( `[${logTimestamp()}] ---- APP SESSION ${phase} run=${APP_RUN_ID} ${normalizedDetails} ----\n`, ); } function formatErrorMessage(error: unknown): string { if (error instanceof Error) { return error.message; } return String(error); } function getSafeExternalUrl(rawUrl: unknown): string | null { if (typeof rawUrl !== "string" || rawUrl.length === 0) { return null; } let parsedUrl: URL; try { parsedUrl = new URL(rawUrl); } catch { return null; } if (parsedUrl.protocol !== "https:" && parsedUrl.protocol !== "http:") { return null; } return parsedUrl.toString(); } function getSafeTheme(rawTheme: unknown): DesktopTheme | null { if (rawTheme === "light" || rawTheme === "dark" || rawTheme === "system") { return rawTheme; } return null; } async function waitForBackendHttpReady(baseUrl: string): Promise { cancelBackendReadinessWait(); const controller = new AbortController(); backendReadinessAbortController = controller; try { await waitForHttpReady(baseUrl, { signal: controller.signal, }); } finally { if (backendReadinessAbortController === controller) { backendReadinessAbortController = null; } } } function cancelBackendReadinessWait(): void { backendReadinessAbortController?.abort(); backendReadinessAbortController = null; } function writeDesktopStreamChunk( streamName: "stdout" | "stderr", chunk: unknown, encoding: BufferEncoding | undefined, ): void { if (!desktopLogSink) return; const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk), typeof chunk === "string" ? encoding : undefined); desktopLogSink.write(`[${logTimestamp()}] [${logScope(streamName)}] `); desktopLogSink.write(buffer); if (buffer.length === 0 || buffer[buffer.length - 1] !== 0x0a) { desktopLogSink.write("\n"); } } function installStdIoCapture(): void { if (!app.isPackaged || desktopLogSink === null || restoreStdIoCapture !== null) { return; } const originalStdoutWrite = process.stdout.write.bind(process.stdout); const originalStderrWrite = process.stderr.write.bind(process.stderr); const patchWrite = (streamName: "stdout" | "stderr", originalWrite: typeof process.stdout.write) => ( chunk: string | Uint8Array, encodingOrCallback?: BufferEncoding | ((error?: Error | null) => void), callback?: (error?: Error | null) => void, ): boolean => { const encoding = typeof encodingOrCallback === "string" ? encodingOrCallback : undefined; writeDesktopStreamChunk(streamName, chunk, encoding); if (typeof encodingOrCallback === "function") { return originalWrite(chunk, encodingOrCallback); } if (callback !== undefined) { return originalWrite(chunk, encoding, callback); } if (encoding !== undefined) { return originalWrite(chunk, encoding); } return originalWrite(chunk); }; process.stdout.write = patchWrite("stdout", originalStdoutWrite); process.stderr.write = patchWrite("stderr", originalStderrWrite); restoreStdIoCapture = () => { process.stdout.write = originalStdoutWrite; process.stderr.write = originalStderrWrite; restoreStdIoCapture = null; }; } function initializePackagedLogging(): void { if (!app.isPackaged) return; try { desktopLogSink = new RotatingFileSink({ filePath: Path.join(LOG_DIR, "desktop-main.log"), maxBytes: LOG_FILE_MAX_BYTES, maxFiles: LOG_FILE_MAX_FILES, }); backendLogSink = new RotatingFileSink({ filePath: Path.join(LOG_DIR, "server-child.log"), maxBytes: LOG_FILE_MAX_BYTES, maxFiles: LOG_FILE_MAX_FILES, }); installStdIoCapture(); writeDesktopLogHeader(`runtime log capture enabled logDir=${LOG_DIR}`); } catch (error) { // Logging setup should never block app startup. console.error("[desktop] failed to initialize packaged logging", error); } } function captureBackendOutput(child: ChildProcess.ChildProcess): void { if (!app.isPackaged || backendLogSink === null) return; const writeChunk = (chunk: unknown): void => { if (!backendLogSink) return; const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk), "utf8"); backendLogSink.write(buffer); }; child.stdout?.on("data", writeChunk); child.stderr?.on("data", writeChunk); } initializePackagedLogging(); if (process.platform === "linux") { app.commandLine.appendSwitch("class", LINUX_WM_CLASS); } function getDestructiveMenuIcon(): Electron.NativeImage | undefined { if (process.platform !== "darwin") return undefined; if (destructiveMenuIconCache !== undefined) { return destructiveMenuIconCache ?? undefined; } try { const icon = nativeImage.createFromNamedImage("trash").resize({ width: 14, height: 14, }); if (icon.isEmpty()) { destructiveMenuIconCache = null; return undefined; } icon.setTemplateImage(true); destructiveMenuIconCache = icon; return icon; } catch { destructiveMenuIconCache = null; return undefined; } } let updatePollTimer: ReturnType | null = null; let updateStartupTimer: ReturnType | null = null; let updateCheckInFlight = false; let updateDownloadInFlight = false; let updateInstallInFlight = false; let updaterConfigured = false; let updateState: DesktopUpdateState = initialUpdateState(); function resolveUpdaterErrorContext(): DesktopUpdateErrorContext { if (updateInstallInFlight) return "install"; if (updateDownloadInFlight) return "download"; if (updateCheckInFlight) return "check"; return updateState.errorContext; } protocol.registerSchemesAsPrivileged([ { scheme: DESKTOP_SCHEME, privileges: { standard: true, secure: true, supportFetchAPI: true, corsEnabled: true, }, }, ]); function resolveAppRoot(): string { if (!app.isPackaged) { return ROOT_DIR; } return app.getAppPath(); } /** Read the baked-in app-update.yml config (if applicable). */ function readAppUpdateYml(): Record | null { try { // electron-updater reads from process.resourcesPath in packaged builds, // or dev-app-update.yml via app.getAppPath() in dev. const ymlPath = app.isPackaged ? Path.join(process.resourcesPath, "app-update.yml") : Path.join(app.getAppPath(), "dev-app-update.yml"); const raw = FS.readFileSync(ymlPath, "utf-8"); // The YAML is simple key-value pairs — avoid pulling in a YAML parser by // doing a line-based parse (fields: provider, owner, repo, releaseType, …). const entries: Record = {}; for (const line of raw.split("\n")) { const match = line.match(/^(\w+):\s*(.+)$/); if (match?.[1] && match[2]) entries[match[1]] = match[2].trim(); } return entries.provider ? entries : null; } catch { return null; } } function normalizeCommitHash(value: unknown): string | null { if (typeof value !== "string") { return null; } const trimmed = value.trim(); if (!COMMIT_HASH_PATTERN.test(trimmed)) { return null; } return trimmed.slice(0, COMMIT_HASH_DISPLAY_LENGTH).toLowerCase(); } function resolveEmbeddedCommitHash(): string | null { const packageJsonPath = Path.join(resolveAppRoot(), "package.json"); if (!FS.existsSync(packageJsonPath)) { return null; } try { const raw = FS.readFileSync(packageJsonPath, "utf8"); const parsed = JSON.parse(raw) as { t3codeCommitHash?: unknown }; return normalizeCommitHash(parsed.t3codeCommitHash); } catch { return null; } } function resolveAboutCommitHash(): string | null { if (aboutCommitHashCache !== undefined) { return aboutCommitHashCache; } const envCommitHash = normalizeCommitHash(process.env.T3CODE_COMMIT_HASH); if (envCommitHash) { aboutCommitHashCache = envCommitHash; return aboutCommitHashCache; } // Only packaged builds are required to expose commit metadata. if (!app.isPackaged) { aboutCommitHashCache = null; return aboutCommitHashCache; } aboutCommitHashCache = resolveEmbeddedCommitHash(); return aboutCommitHashCache; } function resolveBackendEntry(): string { return Path.join(resolveAppRoot(), "apps/server/dist/bin.mjs"); } function resolveBackendCwd(): string { if (!app.isPackaged) { return resolveAppRoot(); } return OS.homedir(); } function resolveDesktopStaticDir(): string | null { const appRoot = resolveAppRoot(); const candidates = [ Path.join(appRoot, "apps/server/dist/client"), Path.join(appRoot, "apps/web/dist"), ]; for (const candidate of candidates) { if (FS.existsSync(Path.join(candidate, "index.html"))) { return candidate; } } return null; } function resolveDesktopStaticPath(staticRoot: string, requestUrl: string): string { const url = new URL(requestUrl); const rawPath = decodeURIComponent(url.pathname); const normalizedPath = Path.posix.normalize(rawPath).replace(/^\/+/, ""); if (normalizedPath.includes("..")) { return Path.join(staticRoot, "index.html"); } const requestedPath = normalizedPath.length > 0 ? normalizedPath : "index.html"; const resolvedPath = Path.join(staticRoot, requestedPath); if (Path.extname(resolvedPath)) { return resolvedPath; } const nestedIndex = Path.join(resolvedPath, "index.html"); if (FS.existsSync(nestedIndex)) { return nestedIndex; } return Path.join(staticRoot, "index.html"); } function isStaticAssetRequest(requestUrl: string): boolean { try { const url = new URL(requestUrl); return Path.extname(url.pathname).length > 0; } catch { return false; } } function handleFatalStartupError(stage: string, error: unknown): void { const message = formatErrorMessage(error); const detail = error instanceof Error && typeof error.stack === "string" ? `\n${error.stack}` : ""; writeDesktopLogHeader(`fatal startup error stage=${stage} message=${message}`); console.error(`[desktop] fatal startup error (${stage})`, error); if (!isQuitting) { isQuitting = true; dialog.showErrorBox("T3 Code failed to start", `Stage: ${stage}\n${message}${detail}`); } stopBackend(); restoreStdIoCapture?.(); app.quit(); } function registerDesktopProtocol(): void { if (isDevelopment || desktopProtocolRegistered) return; const staticRoot = resolveDesktopStaticDir(); if (!staticRoot) { throw new Error( "Desktop static bundle missing. Build apps/server (with bundled client) first.", ); } const staticRootResolved = Path.resolve(staticRoot); const staticRootPrefix = `${staticRootResolved}${Path.sep}`; const fallbackIndex = Path.join(staticRootResolved, "index.html"); protocol.registerFileProtocol(DESKTOP_SCHEME, (request, callback) => { try { const candidate = resolveDesktopStaticPath(staticRootResolved, request.url); const resolvedCandidate = Path.resolve(candidate); const isInRoot = resolvedCandidate === fallbackIndex || resolvedCandidate.startsWith(staticRootPrefix); const isAssetRequest = isStaticAssetRequest(request.url); if (!isInRoot || !FS.existsSync(resolvedCandidate)) { if (isAssetRequest) { callback({ error: -6 }); return; } callback({ path: fallbackIndex }); return; } callback({ path: resolvedCandidate }); } catch { callback({ path: fallbackIndex }); } }); desktopProtocolRegistered = true; } function dispatchMenuAction(action: string): void { const existingWindow = BrowserWindow.getFocusedWindow() ?? mainWindow ?? BrowserWindow.getAllWindows()[0]; const targetWindow = existingWindow ?? createWindow(); if (!existingWindow) { mainWindow = targetWindow; } const send = () => { if (targetWindow.isDestroyed()) return; targetWindow.webContents.send(MENU_ACTION_CHANNEL, action); revealWindow(targetWindow); }; if (targetWindow.webContents.isLoadingMainFrame()) { targetWindow.webContents.once("did-finish-load", send); return; } send(); } function handleCheckForUpdatesMenuClick(): void { const hasUpdateFeedConfig = readAppUpdateYml() !== null || Boolean(process.env.T3CODE_DESKTOP_MOCK_UPDATES); const disabledReason = getAutoUpdateDisabledReason({ isDevelopment, isPackaged: app.isPackaged, platform: process.platform, appImage: process.env.APPIMAGE, disabledByEnv: process.env.T3CODE_DISABLE_AUTO_UPDATE === "1", hasUpdateFeedConfig, }); if (disabledReason) { console.info("[desktop-updater] Manual update check requested, but updates are disabled."); void dialog.showMessageBox({ type: "info", title: "Updates unavailable", message: "Automatic updates are not available right now.", detail: disabledReason, buttons: ["OK"], }); return; } if (!BrowserWindow.getAllWindows().length) { mainWindow = createWindow(); } void checkForUpdatesFromMenu(); } async function checkForUpdatesFromMenu(): Promise { await checkForUpdates("menu"); if (updateState.status === "up-to-date") { void dialog.showMessageBox({ type: "info", title: "You're up to date!", message: `T3 Code ${updateState.currentVersion} is currently the newest version available.`, buttons: ["OK"], }); } else if (updateState.status === "error") { void dialog.showMessageBox({ type: "warning", title: "Update check failed", message: "Could not check for updates.", detail: updateState.message ?? "An unknown error occurred. Please try again later.", buttons: ["OK"], }); } } function configureApplicationMenu(): void { const template: MenuItemConstructorOptions[] = []; if (process.platform === "darwin") { template.push({ label: app.name, submenu: [ { role: "about" }, { label: "Check for Updates...", click: () => handleCheckForUpdatesMenuClick(), }, { type: "separator" }, { label: "Settings...", accelerator: "CmdOrCtrl+,", click: () => dispatchMenuAction("open-settings"), }, { type: "separator" }, { role: "services" }, { type: "separator" }, { role: "hide" }, { role: "hideOthers" }, { role: "unhide" }, { type: "separator" }, { role: "quit" }, ], }); } template.push( { label: "File", submenu: [ ...(process.platform === "darwin" ? [] : [ { label: "Settings...", accelerator: "CmdOrCtrl+,", click: () => dispatchMenuAction("open-settings"), }, { type: "separator" as const }, ]), { role: process.platform === "darwin" ? "close" : "quit" }, ], }, { role: "editMenu" }, { label: "View", submenu: [ { role: "reload" }, { role: "forceReload" }, { role: "toggleDevTools" }, { type: "separator" }, { role: "resetZoom" }, { role: "zoomIn", accelerator: "CmdOrCtrl+=" }, { role: "zoomIn", accelerator: "CmdOrCtrl+Plus", visible: false }, { role: "zoomOut" }, { type: "separator" }, { role: "togglefullscreen" }, ], }, { role: "windowMenu" }, { role: "help", submenu: [ { label: "Check for Updates...", click: () => handleCheckForUpdatesMenuClick(), }, ], }, ); Menu.setApplicationMenu(Menu.buildFromTemplate(template)); } function resolveResourcePath(fileName: string): string | null { const candidates = [ Path.join(__dirname, "../resources", fileName), Path.join(__dirname, "../prod-resources", fileName), Path.join(process.resourcesPath, "resources", fileName), Path.join(process.resourcesPath, fileName), ]; for (const candidate of candidates) { if (FS.existsSync(candidate)) { return candidate; } } return null; } function resolveIconPath(ext: "ico" | "icns" | "png"): string | null { return resolveResourcePath(`icon.${ext}`); } /** * Resolve the Electron userData directory path. * * Electron derives the default userData path from `productName` in * package.json, which currently produces directories with spaces and * parentheses (e.g. `~/.config/T3 Code (Alpha)` on Linux). This is * unfriendly for shell usage and violates Linux naming conventions. * * We override it to a clean lowercase name (`t3code`). If the legacy * directory already exists we keep using it so existing users don't * lose their Chromium profile data (localStorage, cookies, sessions). */ function resolveUserDataPath(): string { const appDataBase = process.platform === "win32" ? process.env.APPDATA || Path.join(OS.homedir(), "AppData", "Roaming") : process.platform === "darwin" ? Path.join(OS.homedir(), "Library", "Application Support") : process.env.XDG_CONFIG_HOME || Path.join(OS.homedir(), ".config"); const legacyPath = Path.join(appDataBase, LEGACY_USER_DATA_DIR_NAME); if (FS.existsSync(legacyPath)) { return legacyPath; } return Path.join(appDataBase, USER_DATA_DIR_NAME); } function configureAppIdentity(): void { app.setName(APP_DISPLAY_NAME); const commitHash = resolveAboutCommitHash(); app.setAboutPanelOptions({ applicationName: APP_DISPLAY_NAME, applicationVersion: app.getVersion(), version: commitHash ?? "unknown", }); if (process.platform === "win32") { app.setAppUserModelId(APP_USER_MODEL_ID); } if (process.platform === "linux") { (app as LinuxDesktopNamedApp).setDesktopName?.(LINUX_DESKTOP_ENTRY_NAME); } if (process.platform === "darwin" && app.dock) { const iconPath = resolveIconPath("png"); if (iconPath) { app.dock.setIcon(iconPath); } } } function clearUpdatePollTimer(): void { if (updateStartupTimer) { clearTimeout(updateStartupTimer); updateStartupTimer = null; } if (updatePollTimer) { clearInterval(updatePollTimer); updatePollTimer = null; } } function revealWindow(window: BrowserWindow): void { if (window.isDestroyed()) { return; } if (window.isMinimized()) { window.restore(); } if (!window.isVisible()) { window.show(); } if (process.platform === "darwin") { app.focus({ steal: true }); } window.focus(); } function emitUpdateState(): void { for (const window of BrowserWindow.getAllWindows()) { if (window.isDestroyed()) continue; window.webContents.send(UPDATE_STATE_CHANNEL, updateState); } } function setUpdateState(patch: Partial): void { updateState = { ...updateState, ...patch }; emitUpdateState(); } function shouldEnableAutoUpdates(): boolean { const hasUpdateFeedConfig = readAppUpdateYml() !== null || Boolean(process.env.T3CODE_DESKTOP_MOCK_UPDATES); return ( getAutoUpdateDisabledReason({ isDevelopment, isPackaged: app.isPackaged, platform: process.platform, appImage: process.env.APPIMAGE, disabledByEnv: process.env.T3CODE_DISABLE_AUTO_UPDATE === "1", hasUpdateFeedConfig, }) === null ); } async function checkForUpdates(reason: string): Promise { if (isQuitting || !updaterConfigured || updateCheckInFlight) return false; if (updateState.status === "downloading" || updateState.status === "downloaded") { console.info( `[desktop-updater] Skipping update check (${reason}) while status=${updateState.status}.`, ); return false; } updateCheckInFlight = true; setUpdateState(reduceDesktopUpdateStateOnCheckStart(updateState, new Date().toISOString())); console.info(`[desktop-updater] Checking for updates (${reason})...`); try { await autoUpdater.checkForUpdates(); return true; } catch (error: unknown) { const message = error instanceof Error ? error.message : String(error); setUpdateState( reduceDesktopUpdateStateOnCheckFailure(updateState, message, new Date().toISOString()), ); console.error(`[desktop-updater] Failed to check for updates: ${message}`); return true; } finally { updateCheckInFlight = false; } } async function downloadAvailableUpdate(): Promise<{ accepted: boolean; completed: boolean }> { if (!updaterConfigured || updateDownloadInFlight || updateState.status !== "available") { return { accepted: false, completed: false }; } updateDownloadInFlight = true; setUpdateState(reduceDesktopUpdateStateOnDownloadStart(updateState)); autoUpdater.disableDifferentialDownload = isArm64HostRunningIntelBuild(desktopRuntimeInfo); console.info("[desktop-updater] Downloading update..."); try { await autoUpdater.downloadUpdate(); return { accepted: true, completed: true }; } catch (error: unknown) { const message = error instanceof Error ? error.message : String(error); setUpdateState(reduceDesktopUpdateStateOnDownloadFailure(updateState, message)); console.error(`[desktop-updater] Failed to download update: ${message}`); return { accepted: true, completed: false }; } finally { updateDownloadInFlight = false; } } async function installDownloadedUpdate(): Promise<{ accepted: boolean; completed: boolean }> { if (isQuitting || !updaterConfigured || updateState.status !== "downloaded") { return { accepted: false, completed: false }; } isQuitting = true; updateInstallInFlight = true; clearUpdatePollTimer(); try { await stopBackendAndWaitForExit(); // Destroy all windows before launching the NSIS installer to avoid the installer finding live windows it needs to close. for (const win of BrowserWindow.getAllWindows()) { win.destroy(); } // `quitAndInstall()` only starts the handoff to the updater. The actual // install may still fail asynchronously, so keep the action incomplete // until we either quit or receive an updater error. autoUpdater.quitAndInstall(true, true); return { accepted: true, completed: false }; } catch (error: unknown) { const message = formatErrorMessage(error); updateInstallInFlight = false; isQuitting = false; setUpdateState(reduceDesktopUpdateStateOnInstallFailure(updateState, message)); console.error(`[desktop-updater] Failed to install update: ${message}`); return { accepted: true, completed: false }; } } function configureAutoUpdater(): void { const githubToken = process.env.T3CODE_DESKTOP_UPDATE_GITHUB_TOKEN?.trim() || process.env.GH_TOKEN?.trim() || ""; if (githubToken) { // When a token is provided, re-configure the feed with `private: true` so // electron-updater uses the GitHub API (api.github.com) instead of the // public Atom feed (github.com/…/releases.atom) which rejects Bearer auth. const appUpdateYml = readAppUpdateYml(); if (appUpdateYml?.provider === "github") { autoUpdater.setFeedURL({ ...appUpdateYml, provider: "github" as const, private: true, token: githubToken, }); } } if (process.env.T3CODE_DESKTOP_MOCK_UPDATES) { autoUpdater.setFeedURL({ provider: "generic", url: `http://localhost:${process.env.T3CODE_DESKTOP_MOCK_UPDATE_SERVER_PORT ?? 3000}`, }); } const enabled = shouldEnableAutoUpdates(); setUpdateState({ ...createInitialDesktopUpdateState(app.getVersion(), desktopRuntimeInfo), enabled, status: enabled ? "idle" : "disabled", }); if (!enabled) { return; } updaterConfigured = true; autoUpdater.autoDownload = false; autoUpdater.autoInstallOnAppQuit = false; // Keep alpha branding, but force all installs onto the stable update track. autoUpdater.channel = DESKTOP_UPDATE_CHANNEL; autoUpdater.allowPrerelease = DESKTOP_UPDATE_ALLOW_PRERELEASE; autoUpdater.allowDowngrade = false; autoUpdater.disableDifferentialDownload = isArm64HostRunningIntelBuild(desktopRuntimeInfo); let lastLoggedDownloadMilestone = -1; if (isArm64HostRunningIntelBuild(desktopRuntimeInfo)) { console.info( "[desktop-updater] Apple Silicon host detected while running Intel build; updates will switch to arm64 packages.", ); } autoUpdater.on("checking-for-update", () => { console.info("[desktop-updater] Looking for updates..."); }); autoUpdater.on("update-available", (info) => { setUpdateState( reduceDesktopUpdateStateOnUpdateAvailable( updateState, info.version, new Date().toISOString(), ), ); lastLoggedDownloadMilestone = -1; console.info(`[desktop-updater] Update available: ${info.version}`); }); autoUpdater.on("update-not-available", () => { setUpdateState(reduceDesktopUpdateStateOnNoUpdate(updateState, new Date().toISOString())); lastLoggedDownloadMilestone = -1; console.info("[desktop-updater] No updates available."); }); autoUpdater.on("error", (error) => { const message = formatErrorMessage(error); if (updateInstallInFlight) { updateInstallInFlight = false; isQuitting = false; setUpdateState(reduceDesktopUpdateStateOnInstallFailure(updateState, message)); console.error(`[desktop-updater] Updater error: ${message}`); return; } if (!updateCheckInFlight && !updateDownloadInFlight) { setUpdateState({ status: "error", message, checkedAt: new Date().toISOString(), downloadPercent: null, errorContext: resolveUpdaterErrorContext(), canRetry: updateState.availableVersion !== null || updateState.downloadedVersion !== null, }); } console.error(`[desktop-updater] Updater error: ${message}`); }); autoUpdater.on("download-progress", (progress) => { const percent = Math.floor(progress.percent); if ( shouldBroadcastDownloadProgress(updateState, progress.percent) || updateState.message !== null ) { setUpdateState(reduceDesktopUpdateStateOnDownloadProgress(updateState, progress.percent)); } const milestone = percent - (percent % 10); if (milestone > lastLoggedDownloadMilestone) { lastLoggedDownloadMilestone = milestone; console.info(`[desktop-updater] Download progress: ${percent}%`); } }); autoUpdater.on("update-downloaded", (info) => { setUpdateState(reduceDesktopUpdateStateOnDownloadComplete(updateState, info.version)); console.info(`[desktop-updater] Update downloaded: ${info.version}`); }); clearUpdatePollTimer(); updateStartupTimer = setTimeout(() => { updateStartupTimer = null; void checkForUpdates("startup"); }, AUTO_UPDATE_STARTUP_DELAY_MS); updateStartupTimer.unref(); updatePollTimer = setInterval(() => { void checkForUpdates("poll"); }, AUTO_UPDATE_POLL_INTERVAL_MS); updatePollTimer.unref(); } function scheduleBackendRestart(reason: string): void { if (isQuitting || restartTimer) return; const delayMs = Math.min(500 * 2 ** restartAttempt, 10_000); restartAttempt += 1; console.error(`[desktop] backend exited unexpectedly (${reason}); restarting in ${delayMs}ms`); restartTimer = setTimeout(() => { restartTimer = null; startBackend(); }, delayMs); } function startBackend(): void { if (isQuitting || backendProcess) return; backendObservabilitySettings = readPersistedBackendObservabilitySettings(); const backendEntry = resolveBackendEntry(); if (!FS.existsSync(backendEntry)) { scheduleBackendRestart(`missing server entry at ${backendEntry}`); return; } const captureBackendLogs = app.isPackaged && backendLogSink !== null; const child = ChildProcess.spawn(process.execPath, [backendEntry, "--bootstrap-fd", "3"], { cwd: resolveBackendCwd(), // In Electron main, process.execPath points to the Electron binary. // Run the child in Node mode so this backend process does not become a GUI app instance. env: { ...backendChildEnv(), ELECTRON_RUN_AS_NODE: "1", }, stdio: captureBackendLogs ? ["ignore", "pipe", "pipe", "pipe"] : ["ignore", "inherit", "inherit", "pipe"], }); const bootstrapStream = child.stdio[3]; if (bootstrapStream && "write" in bootstrapStream) { bootstrapStream.write( `${JSON.stringify({ mode: "desktop", noBrowser: true, port: backendPort, t3Home: BASE_DIR, host: backendBindHost, desktopBootstrapToken: backendBootstrapToken, ...(backendObservabilitySettings.otlpTracesUrl ? { otlpTracesUrl: backendObservabilitySettings.otlpTracesUrl } : {}), ...(backendObservabilitySettings.otlpMetricsUrl ? { otlpMetricsUrl: backendObservabilitySettings.otlpMetricsUrl } : {}), })}\n`, ); bootstrapStream.end(); } else { child.kill("SIGTERM"); scheduleBackendRestart("missing desktop bootstrap pipe"); return; } backendProcess = child; let backendSessionClosed = false; const closeBackendSession = (details: string) => { if (backendSessionClosed) return; backendSessionClosed = true; writeBackendSessionBoundary("END", details); }; writeBackendSessionBoundary( "START", `pid=${child.pid ?? "unknown"} port=${backendPort} cwd=${resolveBackendCwd()}`, ); captureBackendOutput(child); child.once("spawn", () => { restartAttempt = 0; }); child.on("error", (error) => { const wasExpected = expectedBackendExitChildren.has(child); if (backendProcess === child) { backendProcess = null; } closeBackendSession(`pid=${child.pid ?? "unknown"} error=${error.message}`); if (wasExpected) { return; } scheduleBackendRestart(error.message); }); child.on("exit", (code, signal) => { const wasExpected = expectedBackendExitChildren.has(child); if (backendProcess === child) { backendProcess = null; } closeBackendSession( `pid=${child.pid ?? "unknown"} code=${code ?? "null"} signal=${signal ?? "null"}`, ); if (isQuitting || wasExpected) return; const reason = `code=${code ?? "null"} signal=${signal ?? "null"}`; scheduleBackendRestart(reason); }); } function stopBackend(): void { cancelBackendReadinessWait(); if (restartTimer) { clearTimeout(restartTimer); restartTimer = null; } const child = backendProcess; backendProcess = null; if (!child) return; if (child.exitCode === null && child.signalCode === null) { expectedBackendExitChildren.add(child); child.kill("SIGTERM"); setTimeout(() => { if (child.exitCode === null && child.signalCode === null) { child.kill("SIGKILL"); } }, 2_000).unref(); } } async function stopBackendAndWaitForExit(timeoutMs = 5_000): Promise { cancelBackendReadinessWait(); if (restartTimer) { clearTimeout(restartTimer); restartTimer = null; } const child = backendProcess; backendProcess = null; if (!child) return; const backendChild = child; if (backendChild.exitCode !== null || backendChild.signalCode !== null) return; expectedBackendExitChildren.add(backendChild); await new Promise((resolve) => { let settled = false; let forceKillTimer: ReturnType | null = null; let exitTimeoutTimer: ReturnType | null = null; function settle(): void { if (settled) return; settled = true; backendChild.off("exit", onExit); if (forceKillTimer) { clearTimeout(forceKillTimer); } if (exitTimeoutTimer) { clearTimeout(exitTimeoutTimer); } resolve(); } function onExit(): void { settle(); } backendChild.once("exit", onExit); backendChild.kill("SIGTERM"); forceKillTimer = setTimeout(() => { if (backendChild.exitCode === null && backendChild.signalCode === null) { backendChild.kill("SIGKILL"); } }, 2_000); forceKillTimer.unref(); exitTimeoutTimer = setTimeout(() => { settle(); }, timeoutMs); exitTimeoutTimer.unref(); }); } function registerIpcHandlers(): void { ipcMain.removeAllListeners(GET_LOCAL_ENVIRONMENT_BOOTSTRAP_CHANNEL); ipcMain.on(GET_LOCAL_ENVIRONMENT_BOOTSTRAP_CHANNEL, (event) => { event.returnValue = { label: "Local environment", httpBaseUrl: backendHttpUrl || null, wsBaseUrl: backendWsUrl || null, bootstrapToken: backendBootstrapToken || undefined, } as const; }); ipcMain.removeHandler(GET_CLIENT_SETTINGS_CHANNEL); ipcMain.handle(GET_CLIENT_SETTINGS_CHANNEL, async () => readClientSettings(CLIENT_SETTINGS_PATH)); ipcMain.removeHandler(SET_CLIENT_SETTINGS_CHANNEL); ipcMain.handle(SET_CLIENT_SETTINGS_CHANNEL, async (_event, rawSettings: unknown) => { if (typeof rawSettings !== "object" || rawSettings === null) { throw new Error("Invalid client settings payload."); } writeClientSettings(CLIENT_SETTINGS_PATH, rawSettings as ClientSettings); }); ipcMain.removeHandler(GET_SAVED_ENVIRONMENT_REGISTRY_CHANNEL); ipcMain.handle(GET_SAVED_ENVIRONMENT_REGISTRY_CHANNEL, async () => readSavedEnvironmentRegistry(SAVED_ENVIRONMENT_REGISTRY_PATH), ); ipcMain.removeHandler(SET_SAVED_ENVIRONMENT_REGISTRY_CHANNEL); ipcMain.handle(SET_SAVED_ENVIRONMENT_REGISTRY_CHANNEL, async (_event, rawRecords: unknown) => { if (!Array.isArray(rawRecords)) { throw new Error("Invalid saved environment registry payload."); } writeSavedEnvironmentRegistry( SAVED_ENVIRONMENT_REGISTRY_PATH, rawRecords as readonly PersistedSavedEnvironmentRecord[], ); }); ipcMain.removeHandler(GET_SAVED_ENVIRONMENT_SECRET_CHANNEL); ipcMain.handle( GET_SAVED_ENVIRONMENT_SECRET_CHANNEL, async (_event, rawEnvironmentId: unknown) => { if (typeof rawEnvironmentId !== "string" || rawEnvironmentId.trim().length === 0) { return null; } return readSavedEnvironmentSecret({ registryPath: SAVED_ENVIRONMENT_REGISTRY_PATH, environmentId: rawEnvironmentId, secretStorage: getDesktopSecretStorage(), }); }, ); ipcMain.removeHandler(SET_SAVED_ENVIRONMENT_SECRET_CHANNEL); ipcMain.handle( SET_SAVED_ENVIRONMENT_SECRET_CHANNEL, async (_event, rawEnvironmentId: unknown, rawSecret: unknown) => { if (typeof rawEnvironmentId !== "string" || rawEnvironmentId.trim().length === 0) { throw new Error("Invalid saved environment id."); } if (typeof rawSecret !== "string" || rawSecret.trim().length === 0) { throw new Error("Invalid saved environment secret."); } return writeSavedEnvironmentSecret({ registryPath: SAVED_ENVIRONMENT_REGISTRY_PATH, environmentId: rawEnvironmentId, secret: rawSecret, secretStorage: getDesktopSecretStorage(), }); }, ); ipcMain.removeHandler(REMOVE_SAVED_ENVIRONMENT_SECRET_CHANNEL); ipcMain.handle( REMOVE_SAVED_ENVIRONMENT_SECRET_CHANNEL, async (_event, rawEnvironmentId: unknown) => { if (typeof rawEnvironmentId !== "string" || rawEnvironmentId.trim().length === 0) { return; } removeSavedEnvironmentSecret({ registryPath: SAVED_ENVIRONMENT_REGISTRY_PATH, environmentId: rawEnvironmentId, }); }, ); ipcMain.removeHandler(GET_SERVER_EXPOSURE_STATE_CHANNEL); ipcMain.handle(GET_SERVER_EXPOSURE_STATE_CHANNEL, async () => getDesktopServerExposureState()); ipcMain.removeHandler(SET_SERVER_EXPOSURE_MODE_CHANNEL); ipcMain.handle(SET_SERVER_EXPOSURE_MODE_CHANNEL, async (_event, rawMode: unknown) => { if (rawMode !== "local-only" && rawMode !== "network-accessible") { throw new Error("Invalid desktop server exposure input."); } const nextMode = rawMode as DesktopServerExposureMode; if (nextMode === desktopServerExposureMode) { return getDesktopServerExposureState(); } const nextState = await applyDesktopServerExposureMode(nextMode, { persist: true, rejectIfUnavailable: true, }); relaunchDesktopApp(`serverExposureMode=${nextMode}`); return nextState; }); ipcMain.removeHandler(PICK_FOLDER_CHANNEL); ipcMain.handle(PICK_FOLDER_CHANNEL, async () => { const owner = BrowserWindow.getFocusedWindow() ?? mainWindow; const result = owner ? await dialog.showOpenDialog(owner, { properties: ["openDirectory", "createDirectory"], }) : await dialog.showOpenDialog({ properties: ["openDirectory", "createDirectory"], }); if (result.canceled) return null; return result.filePaths[0] ?? null; }); ipcMain.removeHandler(CONFIRM_CHANNEL); ipcMain.handle(CONFIRM_CHANNEL, async (_event, message: unknown) => { if (typeof message !== "string") { return false; } const owner = BrowserWindow.getFocusedWindow() ?? mainWindow; return showDesktopConfirmDialog(message, owner); }); ipcMain.removeHandler(SET_THEME_CHANNEL); ipcMain.handle(SET_THEME_CHANNEL, async (_event, rawTheme: unknown) => { const theme = getSafeTheme(rawTheme); if (!theme) { return; } nativeTheme.themeSource = theme; }); ipcMain.removeHandler(CONTEXT_MENU_CHANNEL); ipcMain.handle( CONTEXT_MENU_CHANNEL, async (_event, items: ContextMenuItem[], position?: { x: number; y: number }) => { const normalizedItems = items .filter((item) => typeof item.id === "string" && typeof item.label === "string") .map((item) => ({ id: item.id, label: item.label, destructive: item.destructive === true, disabled: item.disabled === true, })); if (normalizedItems.length === 0) { return null; } const popupPosition = position && Number.isFinite(position.x) && Number.isFinite(position.y) && position.x >= 0 && position.y >= 0 ? { x: Math.floor(position.x), y: Math.floor(position.y), } : null; const window = BrowserWindow.getFocusedWindow() ?? mainWindow; if (!window) return null; return new Promise((resolve) => { const template: MenuItemConstructorOptions[] = []; let hasInsertedDestructiveSeparator = false; for (const item of normalizedItems) { if (item.destructive && !hasInsertedDestructiveSeparator && template.length > 0) { template.push({ type: "separator" }); hasInsertedDestructiveSeparator = true; } const itemOption: MenuItemConstructorOptions = { label: item.label, enabled: !item.disabled, click: () => resolve(item.id), }; if (item.destructive) { const destructiveIcon = getDestructiveMenuIcon(); if (destructiveIcon) { itemOption.icon = destructiveIcon; } } template.push(itemOption); } const menu = Menu.buildFromTemplate(template); menu.popup({ window, ...popupPosition, callback: () => resolve(null), }); }); }, ); ipcMain.removeHandler(OPEN_EXTERNAL_CHANNEL); ipcMain.handle(OPEN_EXTERNAL_CHANNEL, async (_event, rawUrl: unknown) => { const externalUrl = getSafeExternalUrl(rawUrl); if (!externalUrl) { return false; } try { await shell.openExternal(externalUrl); return true; } catch { return false; } }); ipcMain.removeHandler(UPDATE_GET_STATE_CHANNEL); ipcMain.handle(UPDATE_GET_STATE_CHANNEL, async () => updateState); ipcMain.removeHandler(UPDATE_DOWNLOAD_CHANNEL); ipcMain.handle(UPDATE_DOWNLOAD_CHANNEL, async () => { const result = await downloadAvailableUpdate(); return { accepted: result.accepted, completed: result.completed, state: updateState, } satisfies DesktopUpdateActionResult; }); ipcMain.removeHandler(UPDATE_INSTALL_CHANNEL); ipcMain.handle(UPDATE_INSTALL_CHANNEL, async () => { if (isQuitting) { return { accepted: false, completed: false, state: updateState, } satisfies DesktopUpdateActionResult; } const result = await installDownloadedUpdate(); return { accepted: result.accepted, completed: result.completed, state: updateState, } satisfies DesktopUpdateActionResult; }); ipcMain.removeHandler(UPDATE_CHECK_CHANNEL); ipcMain.handle(UPDATE_CHECK_CHANNEL, async () => { if (!updaterConfigured) { return { checked: false, state: updateState, } satisfies DesktopUpdateCheckResult; } const checked = await checkForUpdates("web-ui"); return { checked, state: updateState, } satisfies DesktopUpdateCheckResult; }); } function getIconOption(): { icon: string } | Record { if (process.platform === "darwin") return {}; // macOS uses .icns from app bundle const ext = process.platform === "win32" ? "ico" : "png"; const iconPath = resolveIconPath(ext); return iconPath ? { icon: iconPath } : {}; } function getInitialWindowBackgroundColor(): string { return nativeTheme.shouldUseDarkColors ? "#0a0a0a" : "#ffffff"; } function createWindow(): BrowserWindow { const window = new BrowserWindow({ width: 1100, height: 780, minWidth: 840, minHeight: 620, show: isDevelopment, autoHideMenuBar: true, backgroundColor: getInitialWindowBackgroundColor(), ...getIconOption(), title: APP_DISPLAY_NAME, titleBarStyle: "hiddenInset", trafficLightPosition: { x: 16, y: 18 }, webPreferences: { preload: Path.join(__dirname, "preload.js"), contextIsolation: true, nodeIntegration: false, sandbox: true, }, }); window.webContents.on("context-menu", (event, params) => { event.preventDefault(); const menuTemplate: MenuItemConstructorOptions[] = []; if (params.misspelledWord) { for (const suggestion of params.dictionarySuggestions.slice(0, 5)) { menuTemplate.push({ label: suggestion, click: () => window.webContents.replaceMisspelling(suggestion), }); } if (params.dictionarySuggestions.length === 0) { menuTemplate.push({ label: "No suggestions", enabled: false }); } menuTemplate.push({ type: "separator" }); } const externalUrl = getSafeExternalUrl(params.linkURL); if (externalUrl) { menuTemplate.push( { label: "Copy Link", click: () => clipboard.writeText(params.linkURL) }, { type: "separator" }, ); } menuTemplate.push( { role: "cut", enabled: params.editFlags.canCut }, { role: "copy", enabled: params.editFlags.canCopy }, { role: "paste", enabled: params.editFlags.canPaste }, { role: "selectAll", enabled: params.editFlags.canSelectAll }, ); Menu.buildFromTemplate(menuTemplate).popup({ window }); }); window.webContents.setWindowOpenHandler(({ url }) => { const externalUrl = getSafeExternalUrl(url); if (externalUrl) { void shell.openExternal(externalUrl); } return { action: "deny" }; }); window.on("page-title-updated", (event) => { event.preventDefault(); window.setTitle(APP_DISPLAY_NAME); }); window.webContents.on("did-finish-load", () => { window.setTitle(APP_DISPLAY_NAME); emitUpdateState(); }); if (!isDevelopment) { window.once("ready-to-show", () => { revealWindow(window); }); } if (isDevelopment) { void window.loadURL(resolveDesktopDevServerUrl()); window.webContents.openDevTools({ mode: "detach" }); setImmediate(() => { revealWindow(window); }); } else { void window.loadURL(resolveDesktopWindowUrl()); } window.on("closed", () => { if (mainWindow === window) { mainWindow = null; } }); return window; } function resolveDesktopWindowUrl(): string { if (backendHttpUrl) { return backendHttpUrl; } return `${DESKTOP_SCHEME}://app`; } // Override Electron's userData path before the `ready` event so that // Chromium session data uses a filesystem-friendly directory name. // Must be called synchronously at the top level — before `app.whenReady()`. app.setPath("userData", resolveUserDataPath()); configureAppIdentity(); async function bootstrap(): Promise { writeDesktopLogHeader("bootstrap start"); const configuredBackendPort = resolveConfiguredDesktopBackendPort(process.env.T3CODE_PORT); if (isDevelopment && configuredBackendPort === undefined) { throw new Error("T3CODE_PORT is required in desktop development."); } backendPort = configuredBackendPort ?? (await resolveDesktopBackendPort({ host: DESKTOP_LOOPBACK_HOST, startPort: DEFAULT_DESKTOP_BACKEND_PORT, })); writeDesktopLogHeader( configuredBackendPort === undefined ? `selected backend port via sequential scan startPort=${DEFAULT_DESKTOP_BACKEND_PORT} port=${backendPort}` : `using configured backend port port=${backendPort}`, ); backendBootstrapToken = Crypto.randomBytes(24).toString("hex"); if (desktopSettings.serverExposureMode !== DEFAULT_DESKTOP_SETTINGS.serverExposureMode) { writeDesktopLogHeader( `bootstrap restoring persisted server exposure mode mode=${desktopSettings.serverExposureMode}`, ); } const serverExposureState = await applyDesktopServerExposureMode( desktopSettings.serverExposureMode, { persist: desktopSettings.serverExposureMode !== DEFAULT_DESKTOP_SETTINGS.serverExposureMode, }, ); writeDesktopLogHeader(`bootstrap resolved backend endpoint baseUrl=${backendHttpUrl}`); if (serverExposureState.endpointUrl) { writeDesktopLogHeader( `bootstrap enabled network access endpointUrl=${serverExposureState.endpointUrl}`, ); } else if (desktopSettings.serverExposureMode === "network-accessible") { writeDesktopLogHeader( "bootstrap fell back to local-only because no advertised network host was available", ); } registerIpcHandlers(); writeDesktopLogHeader("bootstrap ipc handlers registered"); startBackend(); writeDesktopLogHeader("bootstrap backend start requested"); if (isDevelopment) { mainWindow = createWindow(); writeDesktopLogHeader("bootstrap main window created"); void waitForBackendHttpReady(backendHttpUrl) .then(() => { writeDesktopLogHeader("bootstrap backend ready"); }) .catch((error) => { if (isBackendReadinessAborted(error)) { return; } writeDesktopLogHeader( `bootstrap backend readiness warning message=${formatErrorMessage(error)}`, ); console.warn("[desktop] backend readiness check timed out during dev bootstrap", error); }); return; } await waitForBackendHttpReady(backendHttpUrl); writeDesktopLogHeader("bootstrap backend ready"); mainWindow = createWindow(); writeDesktopLogHeader("bootstrap main window created"); } app.on("before-quit", () => { isQuitting = true; updateInstallInFlight = false; writeDesktopLogHeader("before-quit received"); clearUpdatePollTimer(); cancelBackendReadinessWait(); stopBackend(); restoreStdIoCapture?.(); }); app .whenReady() .then(() => { writeDesktopLogHeader("app ready"); configureAppIdentity(); configureApplicationMenu(); registerDesktopProtocol(); configureAutoUpdater(); void bootstrap().catch((error) => { if (isBackendReadinessAborted(error) && isQuitting) { return; } handleFatalStartupError("bootstrap", error); }); app.on("activate", () => { const existingWindow = mainWindow ?? BrowserWindow.getAllWindows()[0]; if (existingWindow) { revealWindow(existingWindow); return; } mainWindow = createWindow(); }); }) .catch((error) => { handleFatalStartupError("whenReady", error); }); app.on("window-all-closed", () => { if (process.platform !== "darwin" && !isQuitting) { app.quit(); } }); if (process.platform !== "win32") { process.on("SIGINT", () => { if (isQuitting) return; isQuitting = true; writeDesktopLogHeader("SIGINT received"); clearUpdatePollTimer(); cancelBackendReadinessWait(); stopBackend(); restoreStdIoCapture?.(); app.quit(); }); process.on("SIGTERM", () => { if (isQuitting) return; isQuitting = true; writeDesktopLogHeader("SIGTERM received"); clearUpdatePollTimer(); stopBackend(); restoreStdIoCapture?.(); app.quit(); }); }