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, type BrowserWindowConstructorOptions, clipboard, dialog, ipcMain, Menu, nativeImage, nativeTheme, protocol, safeStorage, shell, } from "electron"; import type { MenuItemConstructorOptions, OpenDialogOptions } from "electron"; import type { ClientSettings, DesktopTheme, DesktopAppBranding, DesktopServerExposureMode, DesktopServerExposureState, DesktopUpdateChannel, 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 type { RemoteT3RunnerOptions } from "@t3tools/ssh/tunnel"; import { DEFAULT_DESKTOP_BACKEND_PORT, resolveDesktopBackendPort } from "./backendPort.ts"; import { type DesktopSettings, DEFAULT_DESKTOP_SETTINGS, readDesktopSettings, setDesktopServerExposurePreference, setDesktopTailscaleServePreference, setDesktopUpdateChannelPreference, writeDesktopSettings, } from "./desktopSettings.ts"; import { readClientSettings, readSavedEnvironmentRegistry, readSavedEnvironmentSecret, removeSavedEnvironmentSecret, writeClientSettings, writeSavedEnvironmentRegistry, writeSavedEnvironmentSecret, } from "./clientPersistence.ts"; import { isBackendReadinessAborted, waitForHttpReady } from "./backendReadiness.ts"; import { showDesktopConfirmDialog } from "./confirmDialog.ts"; import { resolveDesktopCoreAdvertisedEndpoints, resolveDesktopServerExposure, } from "./serverExposure.ts"; import { DesktopSshEnvironmentBridge, resolveRemoteT3CliPackageSpec } from "./sshEnvironment.ts"; import { syncShellEnvironment } from "./syncShellEnvironment.ts"; import { waitForBackendStartupReady } from "./backendStartupReadiness.ts"; import { getAutoUpdateDisabledReason, shouldBroadcastDownloadProgress } from "./updateState.ts"; import { doesVersionMatchDesktopUpdateChannel } from "./updateChannels.ts"; import { ServerListeningDetector } from "./serverListeningDetector.ts"; import { createInitialDesktopUpdateState, reduceDesktopUpdateStateOnCheckFailure, reduceDesktopUpdateStateOnCheckStart, reduceDesktopUpdateStateOnDownloadComplete, reduceDesktopUpdateStateOnDownloadFailure, reduceDesktopUpdateStateOnDownloadProgress, reduceDesktopUpdateStateOnDownloadStart, reduceDesktopUpdateStateOnInstallFailure, reduceDesktopUpdateStateOnNoUpdate, reduceDesktopUpdateStateOnUpdateAvailable, } from "./updateMachine.ts"; import { isArm64HostRunningIntelBuild, resolveDesktopRuntimeInfo } from "./runtimeArch.ts"; import { resolveDesktopAppBranding } from "./appBranding.ts"; import { bindFirstRevealTrigger, type RevealSubscription } from "./windowReveal.ts"; import { resolveTailscaleAdvertisedEndpoints } from "./tailscaleEndpointProvider.ts"; 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_SET_CHANNEL_CHANNEL = "desktop:update-set-channel"; const UPDATE_DOWNLOAD_CHANNEL = "desktop:update-download"; const UPDATE_INSTALL_CHANNEL = "desktop:update-install"; const UPDATE_CHECK_CHANNEL = "desktop:update-check"; const GET_APP_BRANDING_CHANNEL = "desktop:get-app-branding"; 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 SET_TAILSCALE_SERVE_ENABLED_CHANNEL = "desktop:set-tailscale-serve-enabled"; const GET_ADVERTISED_ENDPOINTS_CHANNEL = "desktop:get-advertised-endpoints"; 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); // Dev-only SSH launcher override. Set this to an absolute path on the SSH host // for a built server entry, for example: // "/Users/julius/Development/Work/codething-mvp/apps/server/dist/bin.mjs" const DEV_REMOTE_T3_SERVER_ENTRY_PATH = process.env.T3CODE_DEV_REMOTE_T3_SERVER_ENTRY_PATH?.trim() ?? ""; const desktopAppBranding: DesktopAppBranding = resolveDesktopAppBranding({ isDevelopment, appVersion: app.getVersion(), }); const APP_DISPLAY_NAME = desktopAppBranding.displayName; const APP_USER_MODEL_ID = isDevelopment ? "com.t3tools.t3code.dev" : "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; function resolvePickFolderDefaultPath(rawOptions: unknown): string | undefined { if (typeof rawOptions !== "object" || rawOptions === null) { return undefined; } const { initialPath } = rawOptions as { initialPath?: unknown }; if (typeof initialPath !== "string") { return undefined; } const trimmedPath = initialPath.trim(); if (trimmedPath.length === 0) { return undefined; } if (trimmedPath === "~") { return OS.homedir(); } if (trimmedPath.startsWith("~/") || trimmedPath.startsWith("~\\")) { return Path.join(OS.homedir(), trimmedPath.slice(2)); } return Path.resolve(trimmedPath); } const DESKTOP_LOOPBACK_HOST = "127.0.0.1"; const DESKTOP_REQUIRED_PORT_PROBE_HOSTS = ["0.0.0.0", "::"] as const; const TITLEBAR_HEIGHT = 40; const TITLEBAR_COLOR = "#01000000"; // #00000000 does not work correctly on Linux const TITLEBAR_LIGHT_SYMBOL_COLOR = "#1f2937"; const TITLEBAR_DARK_SYMBOL_COLOR = "#f8fafc"; function normalizeContextMenuItems(source: readonly ContextMenuItem[]): ContextMenuItem[] { const normalizedItems: ContextMenuItem[] = []; for (const sourceItem of source) { if (typeof sourceItem.id !== "string" || typeof sourceItem.label !== "string") { continue; } const normalizedItem: ContextMenuItem = { id: sourceItem.id, label: sourceItem.label, destructive: sourceItem.destructive === true, disabled: sourceItem.disabled === true, }; if (sourceItem.children) { const normalizedChildren = normalizeContextMenuItems(sourceItem.children); if (normalizedChildren.length === 0) { continue; } normalizedItem.children = normalizedChildren; } normalizedItems.push(normalizedItem); } return normalizedItems; } type WindowTitleBarOptions = Pick< BrowserWindowConstructorOptions, "titleBarOverlay" | "titleBarStyle" | "trafficLightPosition" >; 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 backendInitialWindowOpenInFlight: Promise | null = null; let backendListeningDetector: ServerListeningDetector | 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, app.getVersion()); 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, desktopSettings.updateChannel, ); 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; delete env.T3CODE_DESKTOP_HTTPS_ENDPOINTS; delete env.T3CODE_TAILSCALE_SERVE; delete env.T3CODE_TAILSCALE_SERVE_PORT; return env; } function getDesktopServerExposureState(): DesktopServerExposureState { return { mode: desktopServerExposureMode, endpointUrl: backendEndpointUrl, advertisedHost: backendAdvertisedHost, tailscaleServeEnabled: desktopSettings.tailscaleServeEnabled, tailscaleServePort: desktopSettings.tailscaleServePort, }; } async function getDesktopAdvertisedEndpoints() { const exposure = resolveDesktopServerExposure({ mode: desktopServerExposureMode, port: backendPort, networkInterfaces: OS.networkInterfaces(), ...(backendAdvertisedHost ? { advertisedHostOverride: backendAdvertisedHost } : {}), }); const coreEndpoints = resolveDesktopCoreAdvertisedEndpoints({ port: backendPort, exposure, customHttpsEndpointUrls: resolveCustomHttpsEndpointUrls(), }); const tailscaleEndpoints = await resolveTailscaleAdvertisedEndpoints({ port: backendPort, serveEnabled: desktopSettings.tailscaleServeEnabled, servePort: desktopSettings.tailscaleServePort, networkInterfaces: OS.networkInterfaces(), }); return [...coreEndpoints, ...tailscaleEndpoints]; } 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; } function resolveCustomHttpsEndpointUrls(): readonly string[] { return (process.env.T3CODE_DESKTOP_HTTPS_ENDPOINTS ?? "") .split(",") .map((entry) => entry.trim()) .filter((entry) => entry.length > 0); } 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(); } async function applyDesktopTailscaleServeEnabled( nextSettings: DesktopSettings, ): Promise { desktopSettings = nextSettings; writeDesktopSettings(DESKTOP_SETTINGS_PATH, desktopSettings); relaunchDesktopApp( desktopSettings.tailscaleServeEnabled ? "tailscale-serve-enabled" : "tailscale-serve-disabled", ); 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)}`, ); }) .then(() => desktopSshEnvironmentBridge.dispose().catch(() => undefined)) .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, options?: Parameters[1], ): Promise { cancelBackendReadinessWait(); const controller = new AbortController(); backendReadinessAbortController = controller; try { await waitForHttpReady(baseUrl, { ...options, signal: controller.signal, }); } finally { if (backendReadinessAbortController === controller) { backendReadinessAbortController = null; } } } function cancelBackendReadinessWait(): void { backendReadinessAbortController?.abort(); backendReadinessAbortController = null; } async function waitForBackendWindowReady(baseUrl: string): Promise<"listening" | "http"> { return await waitForBackendStartupReady({ listeningPromise: backendListeningDetector?.promise ?? null, waitForHttpReady: () => waitForBackendHttpReady(baseUrl, { timeoutMs: 60_000, }), cancelHttpWait: cancelBackendReadinessWait, }); } function ensureInitialBackendWindowOpen(): void { const existingWindow = mainWindow ?? BrowserWindow.getAllWindows()[0] ?? null; if (isDevelopment || existingWindow !== null || backendInitialWindowOpenInFlight !== null) { return; } const nextOpen = waitForBackendWindowReady(backendHttpUrl) .then((source) => { writeDesktopLogHeader(`bootstrap backend ready source=${source}`); if (mainWindow ?? BrowserWindow.getAllWindows()[0]) { return; } mainWindow = createWindow(); writeDesktopLogHeader("bootstrap main window created"); }) .catch((error) => { if (isBackendReadinessAborted(error)) { return; } writeDesktopLogHeader( `bootstrap backend readiness warning message=${formatErrorMessage(error)}`, ); console.warn("[desktop] backend readiness check timed out during packaged bootstrap", error); }) .finally(() => { if (backendInitialWindowOpenInFlight === nextOpen) { backendInitialWindowOpenInFlight = null; } }); backendInitialWindowOpenInFlight = nextOpen; } 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 { const attachStream = (stream: NodeJS.ReadableStream | null | undefined): void => { stream?.on("data", (chunk: unknown) => { const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk), "utf8"); backendLogSink?.write(buffer); backendListeningDetector?.push(buffer); }); }; attachStream(child.stdout); attachStream(child.stderr); } 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(); const desktopSshEnvironmentBridge = new DesktopSshEnvironmentBridge({ getMainWindow: () => mainWindow, resolveCliRunner: (): RemoteT3RunnerOptions => { if (isDevelopment && DEV_REMOTE_T3_SERVER_ENTRY_PATH.length > 0) { return { nodeScriptPath: DEV_REMOTE_T3_SERVER_ENTRY_PATH }; } return { packageSpec: resolveRemoteT3CliPackageSpec({ appVersion: app.getVersion(), updateChannel: desktopSettings.updateChannel, isDevelopment, }), }; }, }); 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 { if (isDevelopment && process.platform === "darwin" && ext === "png") { const developmentDockIconPath = Path.join( ROOT_DIR, "assets", "dev", "blueprint-macos-1024.png", ); if (FS.existsSync(developmentDockIconPath)) { return developmentDockIconPath; } } 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 createBaseUpdateState( channel: DesktopUpdateChannel, enabled: boolean, ): DesktopUpdateState { return { ...createInitialDesktopUpdateState(app.getVersion(), desktopRuntimeInfo, channel), enabled, status: enabled ? "idle" : "disabled", }; } function applyAutoUpdaterChannel(channel: DesktopUpdateChannel): void { autoUpdater.channel = channel; autoUpdater.allowPrerelease = channel === "nightly"; autoUpdater.allowDowngrade = channel === "nightly"; console.info( `[desktop-updater] Using update channel '${channel}' (allowPrerelease=${channel === "nightly"}, allowDowngrade=${channel === "nightly"}).`, ); } 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(createBaseUpdateState(desktopSettings.updateChannel, enabled)); if (!enabled) { return; } updaterConfigured = true; autoUpdater.autoDownload = false; autoUpdater.autoInstallOnAppQuit = false; applyAutoUpdaterChannel(desktopSettings.updateChannel); 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) => { if (!doesVersionMatchDesktopUpdateChannel(info.version, updateState.channel)) { console.info( `[desktop-updater] Ignoring ${info.version} because it does not match the selected '${updateState.channel}' channel.`, ); setUpdateState(reduceDesktopUpdateStateOnNoUpdate(updateState, new Date().toISOString())); lastLoggedDownloadMilestone = -1; return; } 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 = !isDevelopment; 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, tailscaleServeEnabled: desktopSettings.tailscaleServeEnabled, tailscaleServePort: desktopSettings.tailscaleServePort, ...(backendObservabilitySettings.otlpTracesUrl ? { otlpTracesUrl: backendObservabilitySettings.otlpTracesUrl } : {}), ...(backendObservabilitySettings.otlpMetricsUrl ? { otlpMetricsUrl: backendObservabilitySettings.otlpMetricsUrl } : {}), })}\n`, ); bootstrapStream.end(); } else { child.kill("SIGTERM"); scheduleBackendRestart("missing desktop bootstrap pipe"); return; } const listeningDetector = new ServerListeningDetector(); backendListeningDetector = listeningDetector; 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) => { if (backendListeningDetector === listeningDetector) { listeningDetector.fail(error); backendListeningDetector = null; } 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) => { if (backendListeningDetector === listeningDetector) { listeningDetector.fail( new Error( `backend exited before logging readiness (code=${code ?? "null"} signal=${signal ?? "null"})`, ), ); backendListeningDetector = null; } 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); }); ensureInitialBackendWindowOpen(); } function stopBackend(): void { cancelBackendReadinessWait(); backendListeningDetector = null; 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_APP_BRANDING_CHANNEL); ipcMain.on(GET_APP_BRANDING_CHANNEL, (event) => { event.returnValue = desktopAppBranding; }); 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, }); }, ); desktopSshEnvironmentBridge.registerIpcHandlers(ipcMain); 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(SET_TAILSCALE_SERVE_ENABLED_CHANNEL); ipcMain.handle(SET_TAILSCALE_SERVE_ENABLED_CHANNEL, async (_event, rawInput: unknown) => { if (typeof rawInput !== "object" || rawInput === null) { throw new Error("Invalid Tailscale Serve input."); } const input = rawInput as { readonly enabled?: unknown; readonly port?: unknown; }; if (typeof input.enabled !== "boolean") { throw new Error("Invalid Tailscale Serve input."); } const nextSettings = setDesktopTailscaleServePreference(desktopSettings, { enabled: input.enabled, ...(typeof input.port === "number" ? { port: input.port } : {}), }); if (nextSettings === desktopSettings) { return getDesktopServerExposureState(); } return applyDesktopTailscaleServeEnabled(nextSettings); }); ipcMain.removeHandler(GET_ADVERTISED_ENDPOINTS_CHANNEL); ipcMain.handle(GET_ADVERTISED_ENDPOINTS_CHANNEL, async () => getDesktopAdvertisedEndpoints()); ipcMain.removeHandler(PICK_FOLDER_CHANNEL); ipcMain.handle(PICK_FOLDER_CHANNEL, async (_event, rawOptions: unknown) => { const owner = BrowserWindow.getFocusedWindow() ?? mainWindow; const defaultPath = resolvePickFolderDefaultPath(rawOptions); const openDialogOptions: OpenDialogOptions = { properties: ["openDirectory", "createDirectory"], ...(defaultPath ? { defaultPath } : {}), }; const result = owner ? await dialog.showOpenDialog(owner, openDialogOptions) : await dialog.showOpenDialog(openDialogOptions); 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 = normalizeContextMenuItems(items); 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 buildTemplate = ( entries: readonly ContextMenuItem[], ): MenuItemConstructorOptions[] => { const template: MenuItemConstructorOptions[] = []; let hasInsertedDestructiveSeparator = false; for (const item of entries) { if (item.destructive && !hasInsertedDestructiveSeparator && template.length > 0) { template.push({ type: "separator" }); hasInsertedDestructiveSeparator = true; } const itemOption: MenuItemConstructorOptions = { label: item.label, enabled: !item.disabled, }; if (item.children && item.children.length > 0) { itemOption.submenu = buildTemplate(item.children); } else { itemOption.click = () => resolve(item.id); } if (item.destructive && (!item.children || item.children.length === 0)) { const destructiveIcon = getDestructiveMenuIcon(); if (destructiveIcon) { itemOption.icon = destructiveIcon; } } template.push(itemOption); } return template; }; const menu = Menu.buildFromTemplate(buildTemplate(normalizedItems)); 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_SET_CHANNEL_CHANNEL); ipcMain.handle(UPDATE_SET_CHANNEL_CHANNEL, async (_event, rawChannel: unknown) => { if (rawChannel !== "latest" && rawChannel !== "nightly") { throw new Error("Invalid desktop update channel input."); } if (updateCheckInFlight || updateDownloadInFlight || updateInstallInFlight) { throw new Error("Cannot change update tracks while an update action is in progress."); } const nextChannel = rawChannel as DesktopUpdateChannel; desktopSettings = setDesktopUpdateChannelPreference(desktopSettings, nextChannel); writeDesktopSettings(DESKTOP_SETTINGS_PATH, desktopSettings); if (nextChannel === updateState.channel) { return updateState; } const enabled = shouldEnableAutoUpdates(); setUpdateState(createBaseUpdateState(nextChannel, enabled)); if (!enabled || !updaterConfigured) { return updateState; } applyAutoUpdaterChannel(nextChannel); const allowDowngrade = autoUpdater.allowDowngrade; // An explicit channel switch should allow the immediate nightly->stable rollback path. autoUpdater.allowDowngrade = true; try { await checkForUpdates("channel-change"); } finally { autoUpdater.allowDowngrade = allowDowngrade; } return 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 getWindowTitleBarOptions(): WindowTitleBarOptions { if (process.platform === "darwin") { return { titleBarStyle: "hiddenInset", trafficLightPosition: { x: 16, y: 18 }, }; } return { titleBarStyle: "hidden", titleBarOverlay: { color: TITLEBAR_COLOR, height: TITLEBAR_HEIGHT, symbolColor: nativeTheme.shouldUseDarkColors ? TITLEBAR_DARK_SYMBOL_COLOR : TITLEBAR_LIGHT_SYMBOL_COLOR, }, }; } function syncWindowAppearance(window: BrowserWindow): void { if (window.isDestroyed()) { return; } window.setBackgroundColor(getInitialWindowBackgroundColor()); const { titleBarOverlay } = getWindowTitleBarOptions(); if (typeof titleBarOverlay === "object") { window.setTitleBarOverlay(titleBarOverlay); } } function syncAllWindowAppearance(): void { for (const window of BrowserWindow.getAllWindows()) { syncWindowAppearance(window); } } nativeTheme.on("updated", syncAllWindowAppearance); function createWindow(): BrowserWindow { const window = new BrowserWindow({ width: 1100, height: 780, minWidth: 840, minHeight: 620, show: false, autoHideMenuBar: true, backgroundColor: getInitialWindowBackgroundColor(), ...getIconOption(), title: APP_DISPLAY_NAME, ...getWindowTitleBarOptions(), webPreferences: { preload: Path.join(__dirname, "preload.cjs"), 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" }, ); } if (params.mediaType === "image") { menuTemplate.push({ label: "Copy Image", click: () => window.webContents.copyImageAt(params.x, params.y), }); menuTemplate.push({ 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(); }); // On Linux/Wayland with `show: false`, Electron's `ready-to-show` only // fires after `show()` is called, deadlocking the standard "wait for // ready, then show" pattern. Add `did-finish-load` as a Linux-only // fallback so the window still surfaces once the renderer has loaded // the page. Other platforms keep the no-flash `ready-to-show` path, // since `did-finish-load` typically fires before the first paint there. const revealSubscribers: RevealSubscription[] = [(fire) => window.once("ready-to-show", fire)]; if (process.platform === "linux") { revealSubscribers.push((fire) => window.webContents.once("did-finish-load", fire)); } bindFirstRevealTrigger(revealSubscribers, () => revealWindow(window)); if (isDevelopment) { void window.loadURL(resolveDesktopDevServerUrl()); window.webContents.openDevTools({ mode: "detach" }); } else { void window.loadURL(backendHttpUrl); } window.on("closed", () => { desktopSshEnvironmentBridge.cancelPendingPasswordPrompts( "SSH authentication was cancelled because the app window closed.", ); if (mainWindow === window) { mainWindow = null; } }); return window; } // 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, requiredHosts: DESKTOP_REQUIRED_PORT_PROBE_HOSTS, })); 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 waitForBackendWindowReady(backendHttpUrl) .then((source) => { writeDesktopLogHeader(`bootstrap backend ready source=${source}`); }) .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; } ensureInitialBackendWindowOpen(); } app.on("before-quit", () => { isQuitting = true; updateInstallInFlight = false; writeDesktopLogHeader("before-quit received"); clearUpdatePollTimer(); cancelBackendReadinessWait(); stopBackend(); void desktopSshEnvironmentBridge.dispose().catch(() => undefined); 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; } if (isDevelopment) { mainWindow = createWindow(); return; } ensureInitialBackendWindowOpen(); }); }) .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(); void desktopSshEnvironmentBridge.dispose().catch(() => undefined); restoreStdIoCapture?.(); app.quit(); }); process.on("SIGTERM", () => { if (isQuitting) return; isQuitting = true; writeDesktopLogHeader("SIGTERM received"); clearUpdatePollTimer(); stopBackend(); void desktopSshEnvironmentBridge.dispose().catch(() => undefined); restoreStdIoCapture?.(); app.quit(); }); }