diff --git a/apps/desktop/electron/backend-command.ts b/apps/desktop/electron/backend-command.ts index a3e2aaac6dc..76ab03b0892 100644 --- a/apps/desktop/electron/backend-command.ts +++ b/apps/desktop/electron/backend-command.ts @@ -32,7 +32,9 @@ export function serveBackendArgs(profile?: string) { export function dashboardFallbackArgs(args) { const i = args.indexOf('serve') - if (i === -1) {return args.slice()} + if (i === -1) { + return args.slice() + } return [...args.slice(0, i), 'dashboard', '--no-open', ...args.slice(i + 1)] } diff --git a/apps/desktop/electron/backend-env.test.ts b/apps/desktop/electron/backend-env.test.ts index 627e5c604f1..46169775262 100644 --- a/apps/desktop/electron/backend-env.test.ts +++ b/apps/desktop/electron/backend-env.test.ts @@ -2,12 +2,14 @@ import assert from 'node:assert/strict' import path from 'node:path' import test from 'node:test' -import { appendUniquePathEntries, +import { + appendUniquePathEntries, buildDesktopBackendEnv, buildDesktopBackendPath, normalizeHermesHomeRoot, pathEnvKey, - POSIX_SANE_PATH_ENTRIES } from './backend-env' + POSIX_SANE_PATH_ENTRIES +} from './backend-env' test('desktop backend PATH adds Hermes-managed bins and missing POSIX sane entries', () => { const result = buildDesktopBackendPath({ diff --git a/apps/desktop/electron/backend-env.ts b/apps/desktop/electron/backend-env.ts index 391e5f0afd6..506575618b4 100644 --- a/apps/desktop/electron/backend-env.ts +++ b/apps/desktop/electron/backend-env.ts @@ -23,7 +23,9 @@ function pathModuleForPlatform(platform = process.platform) { } function pathEnvKey(env = process.env, platform = process.platform) { - if (platform !== 'win32') {return 'PATH'} + if (platform !== 'win32') { + return 'PATH' + } return Object.keys(env || {}).find(key => key.toUpperCase() === 'PATH') || 'PATH' } @@ -39,11 +41,15 @@ function appendUniquePathEntries(entries, { delimiter = path.delimiter } = {}) { const ordered = [] for (const entry of entries) { - if (!entry) {continue} + if (!entry) { + continue + } const parts = Array.isArray(entry) ? entry : String(entry).split(delimiter) for (const part of parts) { - if (!part || seen.has(part)) {continue} + if (!part || seen.has(part)) { + continue + } seen.add(part) ordered.push(part) } @@ -68,7 +74,9 @@ function buildDesktopBackendPath({ } function normalizeHermesHomeRoot(hermesHome, { pathModule = pathModuleForPlatform(process.platform) }: any = {}) { - if (!hermesHome) {return hermesHome} + if (!hermesHome) { + return hermesHome + } const resolved = pathModule.resolve(String(hermesHome)) const parent = pathModule.dirname(resolved) @@ -103,10 +111,12 @@ function buildDesktopBackendEnv({ } } -export { appendUniquePathEntries, +export { + appendUniquePathEntries, buildDesktopBackendEnv, buildDesktopBackendPath, delimiterForPlatform, normalizeHermesHomeRoot, pathEnvKey, - POSIX_SANE_PATH_ENTRIES } + POSIX_SANE_PATH_ENTRIES +} diff --git a/apps/desktop/electron/backend-probes.ts b/apps/desktop/electron/backend-probes.ts index c30764e35e3..0617bddde70 100644 --- a/apps/desktop/electron/backend-probes.ts +++ b/apps/desktop/electron/backend-probes.ts @@ -65,8 +65,10 @@ function hermesRuntimeImportProbe() { * @param {object} [opts.env] - Additional environment for the probe. * @returns {boolean} */ -function canImportHermesCli(pythonPath: string, opts:{env?: Record} = {}) { - if (!pythonPath) {return false} +function canImportHermesCli(pythonPath: string, opts: { env?: Record } = {}) { + if (!pythonPath) { + return false + } try { execFileSync(pythonPath, ['-c', hermesRuntimeImportProbe()], { @@ -102,8 +104,10 @@ function canImportHermesCli(pythonPath: string, opts:{env?: Record { assert.equal(isWslEnvironment({ WSL_DISTRO_NAME: 'Ubuntu' }, 'linux'), true) diff --git a/apps/desktop/electron/bootstrap-platform.ts b/apps/desktop/electron/bootstrap-platform.ts index 81008174724..066616c496e 100644 --- a/apps/desktop/electron/bootstrap-platform.ts +++ b/apps/desktop/electron/bootstrap-platform.ts @@ -1,9 +1,13 @@ import fs from 'node:fs' function isWslEnvironment(env = process.env, platform = process.platform, kernelRelease = null) { - if (platform !== 'linux') {return false} + if (platform !== 'linux') { + return false + } - if (env.WSL_DISTRO_NAME || env.WSL_INTEROP) {return true} + if (env.WSL_DISTRO_NAME || env.WSL_INTEROP) { + return true + } try { const release = kernelRelease ?? fs.readFileSync('/proc/sys/kernel/osrelease', 'utf8') @@ -14,10 +18,15 @@ function isWslEnvironment(env = process.env, platform = process.platform, kernel } } -function isWindowsBinaryPathInWsl(filePath, options: {isWsl?: boolean, env?: NodeJS.ProcessEnv, platform?: NodeJS.Platform} = {}) { +function isWindowsBinaryPathInWsl( + filePath, + options: { isWsl?: boolean; env?: NodeJS.ProcessEnv; platform?: NodeJS.Platform } = {} +) { const isWsl = options.isWsl ?? isWslEnvironment(options.env, options.platform) - if (!isWsl) {return false} + if (!isWsl) { + return false + } const normalized = String(filePath || '') .replace(/\\/g, '/') @@ -51,7 +60,7 @@ const GPU_OVERRIDE_OFF = new Set(['0', 'false', 'no', 'off']) * * Pure + dependency-free so it can be unit-tested and called before app ready. */ -function detectRemoteDisplay(options: {env?: NodeJS.ProcessEnv, platform?: NodeJS.Platform} = {}) { +function detectRemoteDisplay(options: { env?: NodeJS.ProcessEnv; platform?: NodeJS.Platform } = {}) { const env = options.env ?? process.env const platform = options.platform ?? process.platform @@ -59,13 +68,19 @@ function detectRemoteDisplay(options: {env?: NodeJS.ProcessEnv, platform?: NodeJ .trim() .toLowerCase() - if (GPU_OVERRIDE_ON.has(override)) {return 'override (HERMES_DESKTOP_DISABLE_GPU)'} + if (GPU_OVERRIDE_ON.has(override)) { + return 'override (HERMES_DESKTOP_DISABLE_GPU)' + } - if (GPU_OVERRIDE_OFF.has(override)) {return null} + if (GPU_OVERRIDE_OFF.has(override)) { + return null + } // Launched from an SSH session → the display is X11-forwarded or otherwise // remote. Covers the common `ssh user@box` + GUI-forwarding case. - if (env.SSH_CONNECTION || env.SSH_CLIENT || env.SSH_TTY) {return 'ssh-session'} + if (env.SSH_CONNECTION || env.SSH_CLIENT || env.SSH_TTY) { + return 'ssh-session' + } if (platform === 'linux') { // X11 forwarding sets DISPLAY to ":N" (e.g. "localhost:10.0"); a @@ -84,13 +99,12 @@ function detectRemoteDisplay(options: {env?: NodeJS.ProcessEnv, platform?: NodeJ // "Console". const sessionName = String(env.SESSIONNAME || '') - if (/^rdp-/i.test(sessionName)) {return `rdp (SESSIONNAME=${sessionName})`} + if (/^rdp-/i.test(sessionName)) { + return `rdp (SESSIONNAME=${sessionName})` + } } return null } -export { bundledRuntimeImportCheck, - detectRemoteDisplay, - isWindowsBinaryPathInWsl, - isWslEnvironment } +export { bundledRuntimeImportCheck, detectRemoteDisplay, isWindowsBinaryPathInWsl, isWslEnvironment } diff --git a/apps/desktop/electron/bootstrap-runner.test.ts b/apps/desktop/electron/bootstrap-runner.test.ts index 8c3b3f3d0a6..a61cec77f59 100644 --- a/apps/desktop/electron/bootstrap-runner.test.ts +++ b/apps/desktop/electron/bootstrap-runner.test.ts @@ -4,10 +4,7 @@ import os from 'node:os' import path from 'node:path' import test from 'node:test' -import { cachedScriptPath, - installedAgentInstallScript, - resolveInstallScript, - runBootstrap } from './bootstrap-runner' +import { cachedScriptPath, installedAgentInstallScript, resolveInstallScript, runBootstrap } from './bootstrap-runner' const SCRIPT_NAME = process.platform === 'win32' ? 'install.ps1' : 'install.sh' diff --git a/apps/desktop/electron/bootstrap-runner.ts b/apps/desktop/electron/bootstrap-runner.ts index 091e36fbed4..21a8cb4cd5d 100644 --- a/apps/desktop/electron/bootstrap-runner.ts +++ b/apps/desktop/electron/bootstrap-runner.ts @@ -70,7 +70,9 @@ function installScriptKind() { } function resolveLocalInstallScript(sourceRepoRoot) { - if (!sourceRepoRoot) {return null} + if (!sourceRepoRoot) { + return null + } const candidate = path.join(sourceRepoRoot, 'scripts', installScriptName()) try { @@ -91,7 +93,9 @@ function bootstrapCacheDir(hermesHome) { // the pinned commit can't be fetched from GitHub (e.g. a locally-built desktop // app stamped to an unpushed HEAD). function installedAgentInstallScript(hermesHome) { - if (!hermesHome) {return null} + if (!hermesHome) { + return null + } const candidate = path.join(hermesHome, 'hermes-agent', 'scripts', installScriptName()) try { @@ -300,7 +304,9 @@ function resolveWindowsPowerShell() { const candidate = powershellUnderRoot(root) try { - if (fs.statSync(candidate).isFile()) {return candidate} + if (fs.statSync(candidate).isFile()) { + return candidate + } } catch { void 0 } @@ -314,7 +320,9 @@ function resolveWindowsPowerShell() { const candidate = path.join(dir, exe) try { - if (fs.statSync(candidate).isFile()) {return candidate} + if (fs.statSync(candidate).isFile()) { + return candidate + } } catch { void 0 } @@ -379,7 +387,9 @@ function spawnPowerShell(scriptPath, args, { emit, stageName, abortSignal, herme const line = stdoutBuf.slice(0, nl).replace(/\r$/, '') stdoutBuf = stdoutBuf.slice(nl + 1) - if (line) {emit && emit({ type: 'log', stage: stageName, line, stream: 'stdout' })} + if (line) { + emit && emit({ type: 'log', stage: stageName, line, stream: 'stdout' }) + } } }) @@ -393,22 +403,32 @@ function spawnPowerShell(scriptPath, args, { emit, stageName, abortSignal, herme const line = stderrBuf.slice(0, nl).replace(/\r$/, '') stderrBuf = stderrBuf.slice(nl + 1) - if (line) {emit && emit({ type: 'log', stage: stageName, line, stream: 'stderr' })} + if (line) { + emit && emit({ type: 'log', stage: stageName, line, stream: 'stderr' }) + } } }) child.on('error', err => { - if (abortSignal) {abortSignal.removeEventListener('abort', onAbort)} + if (abortSignal) { + abortSignal.removeEventListener('abort', onAbort) + } reject(err) }) child.on('close', (code, signal) => { - if (abortSignal) {abortSignal.removeEventListener('abort', onAbort)} + if (abortSignal) { + abortSignal.removeEventListener('abort', onAbort) + } // Flush any trailing bytes - if (stdoutBuf) {emit && emit({ type: 'log', stage: stageName, line: stdoutBuf, stream: 'stdout' } as any)} + if (stdoutBuf) { + emit && emit({ type: 'log', stage: stageName, line: stdoutBuf, stream: 'stdout' } as any) + } - if (stderrBuf) {emit && emit({ type: 'log', stage: stageName, line: stderrBuf, stream: 'stderr' } as any)} + if (stderrBuf) { + emit && emit({ type: 'log', stage: stageName, line: stderrBuf, stream: 'stderr' } as any) + } resolve({ stdout, stderr, code, signal, killed } as any) }) }) @@ -459,7 +479,9 @@ function spawnBash(scriptPath, args, { emit, stageName, abortSignal, hermesHome const line = stdoutBuf.slice(0, nl).replace(/\r$/, '') stdoutBuf = stdoutBuf.slice(nl + 1) - if (line) {emit && emit({ type: 'log', stage: stageName, line, stream: 'stdout' })} + if (line) { + emit && emit({ type: 'log', stage: stageName, line, stream: 'stdout' }) + } } }) @@ -473,21 +495,31 @@ function spawnBash(scriptPath, args, { emit, stageName, abortSignal, hermesHome const line = stderrBuf.slice(0, nl).replace(/\r$/, '') stderrBuf = stderrBuf.slice(nl + 1) - if (line) {emit && emit({ type: 'log', stage: stageName, line, stream: 'stderr' })} + if (line) { + emit && emit({ type: 'log', stage: stageName, line, stream: 'stderr' }) + } } }) child.on('error', err => { - if (abortSignal) {abortSignal.removeEventListener('abort', onAbort)} + if (abortSignal) { + abortSignal.removeEventListener('abort', onAbort) + } reject(err) }) child.on('close', (code, signal) => { - if (abortSignal) {abortSignal.removeEventListener('abort', onAbort)} + if (abortSignal) { + abortSignal.removeEventListener('abort', onAbort) + } - if (stdoutBuf) {emit && emit({ type: 'log', stage: stageName, line: stdoutBuf, stream: 'stdout' })} + if (stdoutBuf) { + emit && emit({ type: 'log', stage: stageName, line: stdoutBuf, stream: 'stdout' }) + } - if (stderrBuf) {emit && emit({ type: 'log', stage: stageName, line: stderrBuf, stream: 'stderr' })} + if (stderrBuf) { + emit && emit({ type: 'log', stage: stageName, line: stderrBuf, stream: 'stderr' }) + } resolve({ stdout, stderr, code, signal, killed }) }) }) @@ -723,7 +755,9 @@ async function runBootstrap(opts) { } try { - if (typeof onEvent === 'function') {onEvent(ev)} + if (typeof onEvent === 'function') { + onEvent(ev) + } } catch (err) { // Don't let a subscriber bug crash the bootstrap runLog.stream.write(`emit error: ${err && err.message}\n`) @@ -812,10 +846,12 @@ async function runBootstrap(opts) { } } -export { cachedScriptPath, +export { + cachedScriptPath, installedAgentInstallScript, // Exposed for testability parseStageResult, resolveInstallScript, resolveLocalInstallScript, - runBootstrap } + runBootstrap +} diff --git a/apps/desktop/electron/connection-config.test.ts b/apps/desktop/electron/connection-config.test.ts index 5def068793d..34492bbd53f 100644 --- a/apps/desktop/electron/connection-config.test.ts +++ b/apps/desktop/electron/connection-config.test.ts @@ -13,7 +13,8 @@ import assert from 'node:assert/strict' import test from 'node:test' -import { AT_COOKIE_VARIANTS, +import { + AT_COOKIE_VARIANTS, authModeFromStatus, buildGatewayWsUrl, buildGatewayWsUrlWithTicket, @@ -27,7 +28,8 @@ import { AT_COOKIE_VARIANTS, resolveAuthMode, resolveTestWsUrl, RT_COOKIE_VARIANTS, - tokenPreview } from './connection-config' + tokenPreview +} from './connection-config' // --- connectionScopeKey / normAuthMode --- diff --git a/apps/desktop/electron/connection-config.ts b/apps/desktop/electron/connection-config.ts index 6c7346a9bdf..5d84972dfc1 100644 --- a/apps/desktop/electron/connection-config.ts +++ b/apps/desktop/electron/connection-config.ts @@ -237,11 +237,17 @@ function authModeFromStatus(statusBody) { * Returns 'oauth' | 'token'. */ function resolveAuthMode(inputAuthMode, existingAuthMode) { - if (inputAuthMode === 'oauth') {return 'oauth'} + if (inputAuthMode === 'oauth') { + return 'oauth' + } - if (inputAuthMode === 'token') {return 'token'} + if (inputAuthMode === 'token') { + return 'token' + } - if (existingAuthMode === 'oauth') {return 'oauth'} + if (existingAuthMode === 'oauth') { + return 'oauth' + } return 'token' } @@ -258,7 +264,9 @@ function resolveAuthMode(inputAuthMode, existingAuthMode) { * need to know whether an unexpired access token is present right now. */ function cookiesHaveSession(cookies) { - if (!Array.isArray(cookies)) {return false} + if (!Array.isArray(cookies)) { + return false + } return cookies.some(c => c && AT_COOKIE_VARIANTS.includes(c.name) && c.value) } @@ -277,12 +285,15 @@ function cookiesHaveSession(cookies) { * the RT is also dead/revoked). */ function cookiesHaveLiveSession(cookies) { - if (!Array.isArray(cookies)) {return false} + if (!Array.isArray(cookies)) { + return false + } return cookies.some(c => c && c.value && (AT_COOKIE_VARIANTS.includes(c.name) || RT_COOKIE_VARIANTS.includes(c.name))) } -export { AT_COOKIE_VARIANTS, +export { + AT_COOKIE_VARIANTS, authModeFromStatus, buildGatewayWsUrl, buildGatewayWsUrlWithTicket, @@ -296,4 +307,5 @@ export { AT_COOKIE_VARIANTS, resolveAuthMode, resolveTestWsUrl, RT_COOKIE_VARIANTS, - tokenPreview } + tokenPreview +} diff --git a/apps/desktop/electron/dashboard-token.test.ts b/apps/desktop/electron/dashboard-token.test.ts index 84916dc2d40..d07a7e3b111 100644 --- a/apps/desktop/electron/dashboard-token.test.ts +++ b/apps/desktop/electron/dashboard-token.test.ts @@ -8,12 +8,14 @@ import assert from 'node:assert/strict' import test from 'node:test' -import { adoptServedDashboardToken, +import { + adoptServedDashboardToken, dashboardIndexUrl, extractInjectedDashboardToken, fetchPublicText, isForeignBackendToken, - resolveServedDashboardToken } from './dashboard-token' + resolveServedDashboardToken +} from './dashboard-token' test('extractInjectedDashboardToken reads the JSON-encoded dashboard token', () => { const html = '' diff --git a/apps/desktop/electron/dashboard-token.ts b/apps/desktop/electron/dashboard-token.ts index 4f4d3b298d4..42f21485343 100644 --- a/apps/desktop/electron/dashboard-token.ts +++ b/apps/desktop/electron/dashboard-token.ts @@ -28,7 +28,9 @@ async function fetchPublicText(url, options: any = {}) { const text = await res.text() - if (!res.ok) {throw new Error(`${res.status}: ${text || res.statusText}`)} + if (!res.ok) { + throw new Error(`${res.status}: ${text || res.statusText}`) + } return text } @@ -36,7 +38,9 @@ async function fetchPublicText(url, options: any = {}) { function extractInjectedDashboardToken(html) { const match = /window\.__HERMES_SESSION_TOKEN__\s*=\s*("(?:\\.|[^"\\])*")/.exec(String(html || '')) - if (!match) {return null} + if (!match) { + return null + } try { return JSON.parse(match[1]) @@ -97,10 +101,12 @@ async function adoptServedDashboardToken(baseUrl, spawnToken, { childAlive, labe return servedToken } -export { adoptServedDashboardToken, +export { + adoptServedDashboardToken, dashboardIndexUrl, DEFAULT_TOKEN_FETCH_TIMEOUT_MS, extractInjectedDashboardToken, fetchPublicText, isForeignBackendToken, - resolveServedDashboardToken } + resolveServedDashboardToken +} diff --git a/apps/desktop/electron/desktop-uninstall.test.ts b/apps/desktop/electron/desktop-uninstall.test.ts index 3a4cdbbfa45..258db6c0f3c 100644 --- a/apps/desktop/electron/desktop-uninstall.test.ts +++ b/apps/desktop/electron/desktop-uninstall.test.ts @@ -12,14 +12,16 @@ import assert from 'node:assert/strict' import test from 'node:test' -import { buildPosixCleanupScript, +import { + buildPosixCleanupScript, buildWindowsCleanupScript, modeRemovesAgent, modeRemovesUserData, resolveRemovableAppPath, shouldRemoveAppBundle, UNINSTALL_MODES, - uninstallArgsForMode } from './desktop-uninstall' + uninstallArgsForMode +} from './desktop-uninstall' // --- uninstallArgsForMode --- diff --git a/apps/desktop/electron/desktop-uninstall.ts b/apps/desktop/electron/desktop-uninstall.ts index 1254aed2d4d..feebe659514 100644 --- a/apps/desktop/electron/desktop-uninstall.ts +++ b/apps/desktop/electron/desktop-uninstall.ts @@ -69,7 +69,9 @@ function modeRemovesUserData(mode) { function resolveRemovableAppPath(execPath, platform, env: any = {}) { const exe = String(execPath || '') - if (!exe) {return null} + if (!exe) { + return null + } // Use the path flavor that matches the TARGET platform, not the host running // this code — so the Windows branch parses backslash paths correctly even @@ -82,7 +84,9 @@ function resolveRemovableAppPath(execPath, platform, env: any = {}) { const contents = p.dirname(macOsDir) // …/Contents const appBundle = p.dirname(contents) // …/Hermes.app - if (appBundle.endsWith('.app')) {return appBundle} + if (appBundle.endsWith('.app')) { + return appBundle + } return null } @@ -91,17 +95,23 @@ function resolveRemovableAppPath(execPath, platform, env: any = {}) { // NSIS per-user installs Hermes.exe directly in the install dir. const dir = p.dirname(exe) - if (/[\\/]Hermes$/i.test(dir) || /[\\/]hermes-desktop$/i.test(dir)) {return dir} + if (/[\\/]Hermes$/i.test(dir) || /[\\/]hermes-desktop$/i.test(dir)) { + return dir + } return null } // Linux: an AppImage exposes its own path via the APPIMAGE env var. - if (env.APPIMAGE) {return env.APPIMAGE} + if (env.APPIMAGE) { + return env.APPIMAGE + } // Unpacked electron-builder tree: …/linux-unpacked/hermes const dir = p.dirname(exe) - if (/-unpacked$/.test(dir)) {return dir} + if (/-unpacked$/.test(dir)) { + return dir + } return null } @@ -245,11 +255,13 @@ function buildWindowsCleanupScript({ return lines.join('\r\n') } -export { buildPosixCleanupScript, +export { + buildPosixCleanupScript, buildWindowsCleanupScript, modeRemovesAgent, modeRemovesUserData, resolveRemovableAppPath, shouldRemoveAppBundle, UNINSTALL_MODES, - uninstallArgsForMode } + uninstallArgsForMode +} diff --git a/apps/desktop/electron/fs-read-dir.ts b/apps/desktop/electron/fs-read-dir.ts index 27b842ea3ff..7d09650c81f 100644 --- a/apps/desktop/electron/fs-read-dir.ts +++ b/apps/desktop/electron/fs-read-dir.ts @@ -36,7 +36,9 @@ function direntIsSymbolicLink(dirent) { } function shouldStatDirent(dirent) { - if (direntIsDirectory(dirent)) {return false} + if (direntIsDirectory(dirent)) { + return false + } return direntIsSymbolicLink(dirent) || !direntIsFile(dirent) } diff --git a/apps/desktop/electron/gateway-ws-probe.ts b/apps/desktop/electron/gateway-ws-probe.ts index 7202547e7e8..9ed14671410 100644 --- a/apps/desktop/electron/gateway-ws-probe.ts +++ b/apps/desktop/electron/gateway-ws-probe.ts @@ -38,11 +38,14 @@ const DEFAULT_READY_GRACE_MS = 750 * @param {string} wsUrl - Fully-formed ws(s):// URL including the credential. * @returns {Promise<{ ok: boolean, reason?: string }>} */ -function probeGatewayWebSocket(wsUrl: string, options:{ - WebSocketImpl?: any, - connectTimeoutMs?: number - readyGraceMs?: number -} = {}) { +function probeGatewayWebSocket( + wsUrl: string, + options: { + WebSocketImpl?: any + connectTimeoutMs?: number + readyGraceMs?: number + } = {} +) { const WebSocketImpl = options.WebSocketImpl const connectTimeoutMs = options.connectTimeoutMs ?? DEFAULT_CONNECT_TIMEOUT_MS const readyGraceMs = options.readyGraceMs ?? DEFAULT_READY_GRACE_MS @@ -74,7 +77,9 @@ function probeGatewayWebSocket(wsUrl: string, options:{ } const finish = result => { - if (settled) {return} + if (settled) { + return + } settled = true clearTimers() @@ -99,7 +104,9 @@ function probeGatewayWebSocket(wsUrl: string, options:{ } const onOpen = () => { - if (settled) {return} + if (settled) { + return + } opened = true // Upgrade accepted. Give the server a brief window to reject the // credential post-handshake (early close) before declaring success. @@ -122,7 +129,9 @@ function probeGatewayWebSocket(wsUrl: string, options:{ } const onClose = event => { - if (settled) {return} + if (settled) { + return + } if (opened) { // Opened, then closed inside the grace window: the upgrade was accepted @@ -173,14 +182,22 @@ function addListener(socket, type, handler) { } function extractErrorReason(event) { - if (!event) {return ''} + if (!event) { + return '' + } - if (event instanceof Error) {return event.message} + if (event instanceof Error) { + return event.message + } const err = event.error || event.message - if (err instanceof Error) {return err.message} + if (err instanceof Error) { + return err.message + } - if (typeof err === 'string') {return err} + if (typeof err === 'string') { + return err + } return '' } @@ -189,15 +206,19 @@ function closeReason(event, fallback) { const code = event && typeof event.code === 'number' ? event.code : null const reason = event && typeof event.reason === 'string' ? event.reason.trim() : '' - if (code && reason) {return `${fallback} (code ${code}: ${reason})`} + if (code && reason) { + return `${fallback} (code ${code}: ${reason})` + } - if (code) {return `${fallback} (code ${code})`} + if (code) { + return `${fallback} (code ${code})` + } - if (reason) {return `${fallback} (${reason})`} + if (reason) { + return `${fallback} (${reason})` + } return fallback } -export { DEFAULT_CONNECT_TIMEOUT_MS, - DEFAULT_READY_GRACE_MS, - probeGatewayWebSocket } +export { DEFAULT_CONNECT_TIMEOUT_MS, DEFAULT_READY_GRACE_MS, probeGatewayWebSocket } diff --git a/apps/desktop/electron/git-review-ops.ts b/apps/desktop/electron/git-review-ops.ts index cc4f4f3d3c8..1baf26d2fca 100644 --- a/apps/desktop/electron/git-review-ops.ts +++ b/apps/desktop/electron/git-review-ops.ts @@ -8,7 +8,7 @@ import { execFile } from 'node:child_process' import fs from 'node:fs/promises' import path from 'node:path' -import simpleGit from "simple-git"; +import simpleGit from 'simple-git' import { resolveRequestedPathForIpc } from './hardening' @@ -31,7 +31,7 @@ function ghEnv(ghBin) { // Run the `gh` CLI in a repo. Resolves { ok, stdout } so callers branch on // availability/auth without a throw. gh missing/unauthed → ok:false. -function runGh(args, cwd, ghBin): Promise<{ok: boolean, stdout: string}> { +function runGh(args, cwd, ghBin): Promise<{ ok: boolean; stdout: string }> { return new Promise(resolve => { execFile( ghBin || 'gh', @@ -242,8 +242,8 @@ async function reviewList(repoPath, scope, baseRef, gitBin) { const files = summary.files.map(file => ({ path: resolveRenamePath(file.file), - added: 'insertions' in file ? file.insertions : 0 , - removed: 'deletions' in file ? file.deletions : 0 , + added: 'insertions' in file ? file.insertions : 0, + removed: 'deletions' in file ? file.deletions : 0, status: 'M', staged: false })) @@ -674,7 +674,8 @@ async function repoStatus(repoPath, gitBin) { return result } -export { branchBase, +export { + branchBase, fileDiffVsHead, repoStatus, resolveRenamePath, @@ -688,4 +689,5 @@ export { branchBase, reviewRevParse, reviewShipInfo, reviewStage, - reviewUnstage } + reviewUnstage +} diff --git a/apps/desktop/electron/git-root.ts b/apps/desktop/electron/git-root.ts index 93ea2598802..bd19c25a8fd 100644 --- a/apps/desktop/electron/git-root.ts +++ b/apps/desktop/electron/git-root.ts @@ -27,7 +27,7 @@ function findGitRoot(start, fsImpl = fs) { return null } -async function gitRootForIpc(startPath, options: {fs?: typeof fs} = {}) { +async function gitRootForIpc(startPath, options: { fs?: typeof fs } = {}) { const fsImpl = options.fs || fs let resolved @@ -47,5 +47,4 @@ async function gitRootForIpc(startPath, options: {fs?: typeof fs} = {}) { } } -export { findGitRoot, - gitRootForIpc } +export { findGitRoot, gitRootForIpc } diff --git a/apps/desktop/electron/git-worktree-ops.test.ts b/apps/desktop/electron/git-worktree-ops.test.ts index 65e59c2d29d..2d4617c254c 100644 --- a/apps/desktop/electron/git-worktree-ops.test.ts +++ b/apps/desktop/electron/git-worktree-ops.test.ts @@ -5,12 +5,14 @@ import os from 'node:os' import path from 'node:path' import test from 'node:test' -import { addWorktree, +import { + addWorktree, ensureGitRepo, listBranches, parseWorktrees, sanitizeBranch, - switchBranch } from './git-worktree-ops' + switchBranch +} from './git-worktree-ops' test('sanitizeBranch: spaces → hyphens, forbidden chars dropped, edges trimmed', () => { assert.equal(sanitizeBranch('beach vibes'), 'beach-vibes') diff --git a/apps/desktop/electron/git-worktree-ops.ts b/apps/desktop/electron/git-worktree-ops.ts index 93efaf13c4e..61d37bbe6f6 100644 --- a/apps/desktop/electron/git-worktree-ops.ts +++ b/apps/desktop/electron/git-worktree-ops.ts @@ -337,11 +337,13 @@ async function switchBranch(repoPath, branch, gitBin) { return { branch: target } } -export { addWorktree, +export { + addWorktree, ensureGitRepo, listBranches, listWorktrees, parseWorktrees, removeWorktree, sanitizeBranch, - switchBranch } + switchBranch +} diff --git a/apps/desktop/electron/hardening.test.ts b/apps/desktop/electron/hardening.test.ts index 324e511ac3e..5d0dc5e2123 100644 --- a/apps/desktop/electron/hardening.test.ts +++ b/apps/desktop/electron/hardening.test.ts @@ -5,13 +5,15 @@ import path from 'node:path' import test from 'node:test' import { pathToFileURL } from 'node:url' -import { DEFAULT_FETCH_TIMEOUT_MS, +import { + DEFAULT_FETCH_TIMEOUT_MS, encryptDesktopSecret, resolveDirectoryForIpc, resolveReadableFileForIpc, resolveRequestedPathForIpc, resolveTimeoutMs, - sensitiveFileBlockReason } from './hardening' + sensitiveFileBlockReason +} from './hardening' async function rejectsWithCode(promise, code: string) { await assert.rejects(promise, (error: any) => { diff --git a/apps/desktop/electron/hardening.ts b/apps/desktop/electron/hardening.ts index b344f596040..5fb113229b3 100644 --- a/apps/desktop/electron/hardening.ts +++ b/apps/desktop/electron/hardening.ts @@ -110,9 +110,9 @@ function sensitiveFileBlockReason(filePath) { return null } -function ipcPathError(code: any, message: string): Error & {code: any} { - const error = new Error(message) as Error & {code: any} - (error as any).code = code +function ipcPathError(code: any, message: string): Error & { code: any } { + const error = new Error(message) as Error & { code: any } + ;(error as any).code = code return error } @@ -146,7 +146,7 @@ function rejectUnsafePathSyntax(filePath, purpose = 'File read') { return raw } -function resolveRequestedPathForIpc(filePath, options: {purpose?: string, baseDir?: fs.PathOrFileDescriptor} = {}) { +function resolveRequestedPathForIpc(filePath, options: { purpose?: string; baseDir?: fs.PathOrFileDescriptor } = {}) { const purpose = String(options.purpose || 'File read') let raw = rejectUnsafePathSyntax(filePath, purpose) @@ -187,7 +187,7 @@ function resolveRequestedPathForIpc(filePath, options: {purpose?: string, baseDi return resolvedPath } -async function statForIpc(fsImpl: {promises: {stat: typeof fs.promises.stat}}, resolvedPath, purpose, typeLabel) { +async function statForIpc(fsImpl: { promises: { stat: typeof fs.promises.stat } }, resolvedPath, purpose, typeLabel) { try { return await fsImpl.promises.stat(resolvedPath) } catch (error) { @@ -231,7 +231,14 @@ function rejectSensitiveFilePath(filePath, purpose) { } } -async function resolveDirectoryForIpc(dirPath, options: {purpose?: string , baseDir?: fs.PathOrFileDescriptor, fs?: {promises:{stat: typeof fs.promises.stat}}} = {}) { +async function resolveDirectoryForIpc( + dirPath, + options: { + purpose?: string + baseDir?: fs.PathOrFileDescriptor + fs?: { promises: { stat: typeof fs.promises.stat } } + } = {} +) { const purpose = String(options.purpose || 'Directory read') const fsImpl = options.fs || fs const resolvedPath = resolveRequestedPathForIpc(dirPath, { baseDir: options.baseDir, purpose }) @@ -246,7 +253,16 @@ async function resolveDirectoryForIpc(dirPath, options: {purpose?: string , base return { realPath, resolvedPath, stat } } -async function resolveReadableFileForIpc(filePath, options: {purpose?: string , baseDir?: fs.PathOrFileDescriptor, fs?: typeof fs, blockSensitive?: boolean, maxBytes?: number} = {}) { +async function resolveReadableFileForIpc( + filePath, + options: { + purpose?: string + baseDir?: fs.PathOrFileDescriptor + fs?: typeof fs + blockSensitive?: boolean + maxBytes?: number + } = {} +) { const purpose = String(options.purpose || 'File read') const fsImpl = options.fs || fs const resolvedPath = resolveRequestedPathForIpc(filePath, { baseDir: options.baseDir, purpose }) @@ -286,7 +302,8 @@ async function resolveReadableFileForIpc(filePath, options: {purpose?: string , return { realPath, resolvedPath, stat } } -export { DATA_URL_READ_MAX_BYTES, +export { + DATA_URL_READ_MAX_BYTES, DEFAULT_FETCH_TIMEOUT_MS, encryptDesktopSecret, rejectUnsafePathSyntax, @@ -295,4 +312,5 @@ export { DATA_URL_READ_MAX_BYTES, resolveRequestedPathForIpc, resolveTimeoutMs, sensitiveFileBlockReason, - TEXT_PREVIEW_SOURCE_MAX_BYTES } + TEXT_PREVIEW_SOURCE_MAX_BYTES +} diff --git a/apps/desktop/electron/link-title-window.test.ts b/apps/desktop/electron/link-title-window.test.ts index e1b1cc5794f..d67b3eb94ca 100644 --- a/apps/desktop/electron/link-title-window.test.ts +++ b/apps/desktop/electron/link-title-window.test.ts @@ -1,10 +1,12 @@ import assert from 'node:assert/strict' import test from 'node:test' -import { createLinkTitleWindow, +import { + createLinkTitleWindow, guardLinkTitleSession, linkTitleWindowOptions, - readLinkTitleWindowTitle } from './link-title-window' + readLinkTitleWindowTitle +} from './link-title-window' function makeFakeBrowserWindow() { const calls = { audioMuted: [] } diff --git a/apps/desktop/electron/link-title-window.ts b/apps/desktop/electron/link-title-window.ts index 9e67e4b1415..f636c4b0734 100644 --- a/apps/desktop/electron/link-title-window.ts +++ b/apps/desktop/electron/link-title-window.ts @@ -52,10 +52,14 @@ export function guardLinkTitleSession(partitionSession) { // guard isDestroyed and swallow Electron's "Object has been destroyed" throws. export function readLinkTitleWindowTitle(window) { try { - if (!window || window.isDestroyed()) {return ''} + if (!window || window.isDestroyed()) { + return '' + } const contents = window.webContents - if (!contents || contents.isDestroyed()) {return ''} + if (!contents || contents.isDestroyed()) { + return '' + } return contents.getTitle() || '' } catch { diff --git a/apps/desktop/electron/main.ts b/apps/desktop/electron/main.ts index 93f2893d676..f569bf2dd45 100644 --- a/apps/desktop/electron/main.ts +++ b/apps/desktop/electron/main.ts @@ -1,15 +1,16 @@ type Method = string -import type { ExecFileSyncOptionsWithStringEncoding} from 'node:child_process'; +import type { ExecFileSyncOptionsWithStringEncoding } from 'node:child_process' import { execFileSync, spawn } from 'node:child_process' import crypto from 'node:crypto' import fs from 'node:fs' import http from 'node:http' import https from 'node:https' -import os from "node:os" +import os from 'node:os' import path from 'node:path' import { pathToFileURL } from 'node:url' -import { app, +import { + app, BrowserWindow, clipboard, dialog, @@ -25,16 +26,18 @@ import { app, screen, session, shell, - systemPreferences } from 'electron' -import nodePty from "node-pty" + systemPreferences +} from 'electron' +import nodePty from 'node-pty' -import { dashboardFallbackArgs, sourceDeclaresServe } from './backend-command'; +import { dashboardFallbackArgs, sourceDeclaresServe } from './backend-command' import { buildDesktopBackendEnv, normalizeHermesHomeRoot } from './backend-env' import { canImportHermesCli, verifyHermesCli } from './backend-probes' import { waitForDashboardPortAnnouncement } from './backend-ready' import { detectRemoteDisplay, isWindowsBinaryPathInWsl, isWslEnvironment } from './bootstrap-platform' import { runBootstrap } from './bootstrap-runner' -import { authModeFromStatus, +import { + authModeFromStatus, buildGatewayWsUrl, buildGatewayWsUrlWithTicket, connectionScopeKey, @@ -46,20 +49,24 @@ import { authModeFromStatus, profileRemoteOverride, resolveAuthMode, resolveTestWsUrl, - tokenPreview } from './connection-config' + tokenPreview +} from './connection-config' import { adoptServedDashboardToken } from './dashboard-token' -import { buildPosixCleanupScript, +import { + buildPosixCleanupScript, buildWindowsCleanupScript, modeRemovesAgent, modeRemovesUserData, resolveRemovableAppPath, shouldRemoveAppBundle, - uninstallArgsForMode } from './desktop-uninstall' + uninstallArgsForMode +} from './desktop-uninstall' import { installEmbedReferer } from './embed-referer' import { readDirForIpc } from './fs-read-dir' import { probeGatewayWebSocket } from './gateway-ws-probe' import { scanGitRepos } from './git-repo-scan' -import { fileDiffVsHead, +import { + fileDiffVsHead, repoStatus, reviewCommit, reviewCommitContext, @@ -71,41 +78,50 @@ import { fileDiffVsHead, reviewRevParse, reviewShipInfo, reviewStage, - reviewUnstage } from './git-review-ops' + reviewUnstage +} from './git-review-ops' import { gitRootForIpc } from './git-root' import { addWorktree, listBranches, listWorktrees, removeWorktree, switchBranch } from './git-worktree-ops' -import { DATA_URL_READ_MAX_BYTES, +import { + DATA_URL_READ_MAX_BYTES, DEFAULT_FETCH_TIMEOUT_MS, encryptDesktopSecret as encryptDesktopSecretStrict, resolveReadableFileForIpc, resolveRequestedPathForIpc, resolveTimeoutMs, - TEXT_PREVIEW_SOURCE_MAX_BYTES } from './hardening' + TEXT_PREVIEW_SOURCE_MAX_BYTES +} from './hardening' import { createLinkTitleWindow, guardLinkTitleSession, readLinkTitleWindowTitle } from './link-title-window' import { serializeJsonBody, setJsonRequestHeaders } from './oauth-net-request' -import { buildSessionWindowUrl, +import { + buildSessionWindowUrl, chatWindowWebPreferences, createSessionWindowRegistry, SESSION_WINDOW_MIN_HEIGHT, - SESSION_WINDOW_MIN_WIDTH } from './session-windows' + SESSION_WINDOW_MIN_WIDTH +} from './session-windows' import { nativeOverlayWidth as computeNativeOverlayWidth, macTitleBarOverlayHeight } from './titlebar-overlay-width' import { resolveBehindCount, shouldCountCommits } from './update-count' import { readLiveUpdateMarker, writeUpdateMarker } from './update-marker' import { runRebuildWithRetry } from './update-rebuild' -import { buildRelaunchScript, +import { + buildRelaunchScript, collectRelaunchArgs, collectRelaunchEnv, decideRelaunchOutcome, resolveUnpackedRelease, sandboxFallbackFromEnv, - sandboxPreflight } from './update-relaunch' + sandboxPreflight +} from './update-relaunch' import { isOfficialSshRemote, OFFICIAL_REPO_HTTPS_URL } from './update-remote' import { fetchMarketplaceThemes, searchMarketplaceThemes } from './vscode-marketplace' -import { computeWindowOptions, +import { + computeWindowOptions, debounce, sanitizeWindowState, MIN_HEIGHT as WINDOW_MIN_HEIGHT, - MIN_WIDTH as WINDOW_MIN_WIDTH } from './window-state' + MIN_WIDTH as WINDOW_MIN_WIDTH +} from './window-state' import { readWindowsUserEnvVar } from './windows-user-env' import { isPackagedInstallPath as isPackagedInstallPathUnderRoots } from './workspace-cwd' import { readWslWindowsClipboardImage } from './wsl-clipboard-image' @@ -134,7 +150,7 @@ const PRELOAD_PATH = IS_PACKAGED ? path.join(APP_ROOT, 'dist', 'electron-preload.js') : path.join(import.meta.dirname, 'preload.ts') -function hiddenWindowsChildOptions(options: any = {}): ExecFileSyncOptionsWithStringEncoding { +function hiddenWindowsChildOptions(options: any = {}): ExecFileSyncOptionsWithStringEncoding { if (!IS_WINDOWS || Object.prototype.hasOwnProperty.call(options, 'windowsHide')) { return options as any } @@ -212,10 +228,9 @@ function loadInstallStamp() { // sees a stamp without needing a packaged build. const candidates = [ process.resourcesPath ? path.join(process.resourcesPath, 'install-stamp.json') : null, - path.join(APP_ROOT, 'build', 'install-stamp.json'), + path.join(APP_ROOT, 'build', 'install-stamp.json') ].filter(Boolean) - for (const p of candidates) { try { const raw = fs.readFileSync(p, 'utf8') @@ -279,9 +294,13 @@ if (INSTALL_STAMP) { // HERMES_HOME beneath the throwaway userData dir so a fresh-install run never // touches the user's real ~/.hermes / %LOCALAPPDATA%\hermes. function resolveHermesHome() { - if (process.env.HERMES_HOME) {return normalizeHermesHomeRoot(process.env.HERMES_HOME)} + if (process.env.HERMES_HOME) { + return normalizeHermesHomeRoot(process.env.HERMES_HOME) + } - if (USER_DATA_OVERRIDE) {return path.join(path.resolve(USER_DATA_OVERRIDE), 'hermes-home')} + if (USER_DATA_OVERRIDE) { + return path.join(path.resolve(USER_DATA_OVERRIDE), 'hermes-home') + } if (IS_WINDOWS) { // A GUI app launched from Explorer inherits the environment block captured @@ -292,7 +311,9 @@ function resolveHermesHome() { // Consult the live User-scoped registry value before the default below. const fromRegistry = readWindowsUserEnvVar('HERMES_HOME') - if (fromRegistry) {return normalizeHermesHomeRoot(fromRegistry)} + if (fromRegistry) { + return normalizeHermesHomeRoot(fromRegistry) + } } if (IS_WINDOWS && process.env.LOCALAPPDATA) { @@ -301,7 +322,9 @@ function resolveHermesHome() { // Migrate transparently to LOCALAPPDATA, but honour an existing legacy // ~/.hermes setup (no LOCALAPPDATA install yet) so users don't lose state. - if (!directoryExists(localappdata) && directoryExists(legacy)) {return legacy} + if (!directoryExists(localappdata) && directoryExists(legacy)) { + return legacy + } return localappdata } @@ -393,7 +416,9 @@ const BOOT_FAKE_MODE = process.env.HERMES_DESKTOP_BOOT_FAKE === '1' const BOOT_FAKE_STEP_MS = (() => { const raw = Number.parseInt(String(process.env.HERMES_DESKTOP_BOOT_FAKE_STEP_MS || ''), 10) - if (!Number.isFinite(raw) || raw <= 0) {return 650} + if (!Number.isFinite(raw) || raw <= 0) { + return 650 + } return Math.max(120, raw) })() @@ -636,15 +661,21 @@ const PREVIEW_LANGUAGE_BY_EXT = { } function looksBinary(buffer) { - if (!buffer.length) {return false} + if (!buffer.length) { + return false + } let suspicious = 0 for (const byte of buffer) { - if (byte === 0) {return true} + if (byte === 0) { + return true + } // Allow common whitespace controls: tab, LF, CR. - if (byte < 32 && byte !== 9 && byte !== 10 && byte !== 13) {suspicious += 1} + if (byte < 32 && byte !== 9 && byte !== 10 && byte !== 13) { + suspicious += 1 + } } return suspicious / buffer.length > 0.12 @@ -831,7 +862,9 @@ let bootProgressState = { // Each step is ['rm', path] or ['mv', src, dst]; executed best-effort so a // missing chain link never aborts the rest. function planDesktopLogRotation(size) { - if (size < DESKTOP_LOG_MAX_BYTES) {return []} + if (size < DESKTOP_LOG_MAX_BYTES) { + return [] + } const backups = n => Array.from({ length: n }, (_, i) => desktopLogBackupPath(i + 1)) // Pathological boot-loop log: reclaim live + every backup outright. @@ -862,8 +895,11 @@ function rotateDesktopLogIfNeededSync() { for (const [op, src, dst] of planDesktopLogRotation(size)) { try { - if (op === 'rm') {fs.rmSync(src, { force: true })} - else {fs.renameSync(src, dst)} + if (op === 'rm') { + fs.rmSync(src, { force: true }) + } else { + fs.renameSync(src, dst) + } } catch { // Best-effort — logging must never block startup/shutdown. } @@ -881,8 +917,11 @@ async function rotateDesktopLogIfNeededAsync() { for (const [op, src, dst] of planDesktopLogRotation(size)) { try { - if (op === 'rm') {await fs.promises.rm(src, { force: true })} - else {await fs.promises.rename(src, dst)} + if (op === 'rm') { + await fs.promises.rm(src, { force: true }) + } else { + await fs.promises.rename(src, dst) + } } catch { // Best-effort — logging must never crash the shell. } @@ -890,7 +929,9 @@ async function rotateDesktopLogIfNeededAsync() { } function flushDesktopLogBufferSync() { - if (!desktopLogBuffer) {return} + if (!desktopLogBuffer) { + return + } const chunk = desktopLogBuffer desktopLogBuffer = '' @@ -904,7 +945,9 @@ function flushDesktopLogBufferSync() { } function flushDesktopLogBufferAsync() { - if (!desktopLogBuffer) {return desktopLogFlushPromise} + if (!desktopLogBuffer) { + return desktopLogFlushPromise + } const chunk = desktopLogBuffer desktopLogBuffer = '' @@ -922,7 +965,9 @@ function flushDesktopLogBufferAsync() { } function scheduleDesktopLogFlush() { - if (desktopLogFlushTimer) {return} + if (desktopLogFlushTimer) { + return + } desktopLogFlushTimer = setTimeout(() => { desktopLogFlushTimer = null void flushDesktopLogBufferAsync() @@ -932,7 +977,9 @@ function scheduleDesktopLogFlush() { function rememberLog(chunk) { const text = String(chunk || '').trim() - if (!text) {return} + if (!text) { + return + } const lines = text.split(/\r?\n/).map(line => `[hermes] ${line}`) hermesLog.push(...lines) @@ -959,7 +1006,9 @@ function rememberLog(chunk) { function openExternalUrl(rawUrl) { const raw = String(rawUrl || '').trim() - if (!raw) {return false} + if (!raw) { + return false + } let parsed @@ -1035,7 +1084,9 @@ function openExternalUrl(rawUrl) { async function openPreviewInBrowser(rawUrl) { const raw = String(rawUrl || '').trim() - if (!raw) {return false} + if (!raw) { + return false + } let parsed @@ -1063,7 +1114,9 @@ async function openPreviewInBrowser(rawUrl) { } function ensureWslWindowsFonts() { - if (!IS_WSL) {return} + if (!IS_WSL) { + return + } const fontsDir = ['/mnt/c/Windows/Fonts', '/mnt/c/windows/fonts'].find(candidate => { try { @@ -1073,7 +1126,9 @@ function ensureWslWindowsFonts() { } }) - if (!fontsDir) {return} + if (!fontsDir) { + return + } try { const confDir = path.join(app.getPath('home'), '.config', 'fontconfig', 'conf.d') @@ -1086,7 +1141,9 @@ function ensureWslWindowsFonts() { existing = '' } - if (existing.includes(fontsDir)) {return} + if (existing.includes(fontsDir)) { + return + } fs.mkdirSync(confDir, { recursive: true }) fs.writeFileSync( @@ -1110,16 +1167,22 @@ function sleep(ms) { function clampBootProgress(value) { const numeric = Number(value) - if (!Number.isFinite(numeric)) {return 0} + if (!Number.isFinite(numeric)) { + return 0 + } return Math.max(0, Math.min(100, Math.round(numeric))) } function broadcastBootProgress() { - if (!mainWindow || mainWindow.isDestroyed()) {return} + if (!mainWindow || mainWindow.isDestroyed()) { + return + } const { webContents } = mainWindow - if (!webContents || webContents.isDestroyed()) {return} + if (!webContents || webContents.isDestroyed()) { + return + } webContents.send('hermes:boot-progress', bootProgressState) } @@ -1196,10 +1259,14 @@ function broadcastBootstrapEvent(ev) { } } - if (!mainWindow || mainWindow.isDestroyed()) {return} + if (!mainWindow || mainWindow.isDestroyed()) { + return + } const { webContents } = mainWindow - if (!webContents || webContents.isDestroyed()) {return} + if (!webContents || webContents.isDestroyed()) { + return + } webContents.send('hermes:bootstrap:event', ev) } @@ -1207,7 +1274,7 @@ function getBootstrapState() { return bootstrapState } -function updateBootProgress(update, options: {allowDecrease?: boolean} = {}) { +function updateBootProgress(update, options: { allowDecrease?: boolean } = {}) { const nextProgressRaw = typeof update.progress === 'number' ? clampBootProgress(update.progress) : bootProgressState.progress @@ -1292,7 +1359,9 @@ const UPDATE_HANDOFF_DWELL_MS = 2500 async function waitForUpdateToFinish() { let marker = readLiveUpdateMarker(HERMES_HOME) - if (!marker) {return false} + if (!marker) { + return false + } rememberLog(`[updates] update in progress (pid=${marker.pid}); deferring backend start until it finishes`) const deadline = Date.now() + UPDATE_WAIT_TIMEOUT_MS @@ -1321,12 +1390,18 @@ function unpackedPathFor(filePath) { } function findOnPath(command) { - if (!command) {return null} + if (!command) { + return null + } if (path.isAbsolute(command) || command.includes(path.sep) || (IS_WINDOWS && command.includes('/'))) { - if (!fileExists(command)) {return null} + if (!fileExists(command)) { + return null + } - if (isWindowsBinaryPathInWsl(command, { isWsl: IS_WSL })) {return null} + if (isWindowsBinaryPathInWsl(command, { isWsl: IS_WSL })) { + return null + } return command } @@ -1349,7 +1424,9 @@ function findOnPath(command) { for (const extension of extensions) { const candidate = path.join(entry, `${command}${extension}`) - if (fileExists(candidate)) {return candidate} + if (fileExists(candidate)) { + return candidate + } } } @@ -1361,20 +1438,28 @@ function isCommandScript(command) { } function unwrapWindowsVenvHermesCommand(command, backendArgs) { - if (!IS_WINDOWS || !command || isCommandScript(command)) {return null} + if (!IS_WINDOWS || !command || isCommandScript(command)) { + return null + } const resolved = path.resolve(String(command)) - if (!/^hermes(?:\.exe)?$/i.test(path.basename(resolved))) {return null} + if (!/^hermes(?:\.exe)?$/i.test(path.basename(resolved))) { + return null + } const scriptsDir = path.dirname(resolved) - if (path.basename(scriptsDir).toLowerCase() !== 'scripts') {return null} + if (path.basename(scriptsDir).toLowerCase() !== 'scripts') { + return null + } const venvRoot = path.dirname(scriptsDir) const python = getVenvPython(venvRoot) - if (!fileExists(python)) {return null} + if (!fileExists(python)) { + return null + } const root = path.dirname(venvRoot) @@ -1389,7 +1474,9 @@ function unwrapWindowsVenvHermesCommand(command, backendArgs) { if ( !canImportHermesCli(python, { env: { - PYTHONPATH: [...(directoryExists(root) ? [root] : []), process.env.PYTHONPATH].filter(Boolean).join(path.delimiter) + PYTHONPATH: [...(directoryExists(root) ? [root] : []), process.env.PYTHONPATH] + .filter(Boolean) + .join(path.delimiter) } }) ) { @@ -1431,10 +1518,14 @@ function unwrapWindowsVenvHermesCommand(command, backendArgs) { const _serveSupportCache = new Map() function backendSupportsServe(backend) { - if (!backend || !backend.command) {return true} + if (!backend || !backend.command) { + return true + } const key = `${backend.command}::${backend.root || ''}` - if (_serveSupportCache.has(key)) {return _serveSupportCache.get(key)} + if (_serveSupportCache.has(key)) { + return _serveSupportCache.get(key) + } let supported = null @@ -1479,7 +1570,9 @@ function getBackendArgsForRuntime(backend) { } function normalizeExecutablePathForCompare(commandPath) { - if (!commandPath) {return null} + if (!commandPath) { + return null + } let resolved = path.resolve(String(commandPath)) @@ -1493,7 +1586,9 @@ function normalizeExecutablePathForCompare(commandPath) { } function looksLikeDesktopAppBinary(commandPath) { - if (!IS_WINDOWS || !commandPath) {return false} + if (!IS_WINDOWS || !commandPath) { + return false + } const normalizedCandidate = normalizeExecutablePathForCompare(commandPath) const normalizedCurrentExec = normalizeExecutablePathForCompare(process.execPath) @@ -1524,7 +1619,9 @@ function isHermesSourceRoot(root) { function findPythonForRoot(root) { const override = process.env.HERMES_DESKTOP_PYTHON - if (override && fileExists(override)) {return override} + if (override && fileExists(override)) { + return override + } const relativePaths = IS_WINDOWS ? [path.join('.venv', 'Scripts', 'python.exe'), path.join('venv', 'Scripts', 'python.exe')] @@ -1533,7 +1630,9 @@ function findPythonForRoot(root) { for (const relativePath of relativePaths) { const candidate = path.join(root, relativePath) - if (fileExists(candidate)) {return candidate} + if (fileExists(candidate)) { + return candidate + } } return findSystemPython() @@ -1545,7 +1644,9 @@ function findSystemPython() { for (const command of ['python3', 'python']) { const candidate = findOnPath(command) - if (candidate) {return candidate} + if (candidate) { + return candidate + } } return null @@ -1611,7 +1712,9 @@ function findSystemPython() { const installPath = match[1].trim() const pythonExe = path.join(installPath, 'python.exe') - if (fileExists(pythonExe)) {return pythonExe} + if (fileExists(pythonExe)) { + return pythonExe + } } } catch { // Key not present — try next. @@ -1626,12 +1729,16 @@ function findSystemPython() { for (const versionDir of SUPPORTED_VERSIONS_NO_DOT) { const systemWide = path.join(programFiles, `Python${versionDir}`, 'python.exe') - if (fileExists(systemWide)) {return systemWide} + if (fileExists(systemWide)) { + return systemWide + } if (localAppData) { const perUser = path.join(localAppData, 'Programs', 'Python', `Python${versionDir}`, 'python.exe') - if (fileExists(perUser)) {return perUser} + if (fileExists(perUser)) { + return perUser + } } } @@ -1656,7 +1763,9 @@ function findSystemPython() { const candidate = out.trim() - if (candidate && fileExists(candidate)) {return candidate} + if (candidate && fileExists(candidate)) { + return candidate + } } catch { // py couldn't find that version — try next. } @@ -1705,7 +1814,9 @@ function findGitBash() { } for (const candidate of candidates) { - if (fileExists(candidate)) {return candidate} + if (fileExists(candidate)) { + return candidate + } } // Last resort — bash on PATH (covers WSL bash, MSYS2, custom installs). @@ -1742,12 +1853,16 @@ function getVenvPython(venvRoot) { function getVenvSitePackagesEntries(venvRoot) { const entries = [] - if (!venvRoot) {return entries} + if (!venvRoot) { + return entries + } if (IS_WINDOWS) { const sitePackages = path.join(venvRoot, 'Lib', 'site-packages') - if (directoryExists(sitePackages)) {entries.push(sitePackages)} + if (directoryExists(sitePackages)) { + entries.push(sitePackages) + } return entries } @@ -1766,7 +1881,9 @@ function getVenvSitePackagesEntries(venvRoot) { if (version) { const sitePackages = path.join(venvRoot, 'lib', `python${version}`, 'site-packages') - if (directoryExists(sitePackages)) {entries.push(sitePackages)} + if (directoryExists(sitePackages)) { + entries.push(sitePackages) + } } return entries @@ -1787,7 +1904,9 @@ function makeDashboardReadyFile() { let _gitBinaryCache = null function resolveGitBinary() { - if (_gitBinaryCache) {return _gitBinaryCache} + if (_gitBinaryCache) { + return _gitBinaryCache + } if (!IS_WINDOWS) { _gitBinaryCache = findOnPath('git') || 'git' @@ -1822,7 +1941,9 @@ function resolveGitBinary() { let _ghBinaryCache = null function resolveGhBinary() { - if (_ghBinaryCache) {return _ghBinaryCache} + if (_ghBinaryCache) { + return _ghBinaryCache + } const candidates = [] @@ -1886,7 +2007,9 @@ function readWindowState() { // getNormalBounds() keeps the pre-maximize size, so un-maximizing next session // lands back where the user actually sized the window. function persistWindowState() { - if (!mainWindow || mainWindow.isDestroyed() || mainWindow.isMinimized()) {return} + if (!mainWindow || mainWindow.isDestroyed() || mainWindow.isMinimized()) { + return + } try { const { x, y, width, height } = mainWindow.getNormalBounds() @@ -1916,14 +2039,14 @@ function resolveUpdateRoot() { return candidates.find(c => directoryExists(path.join(c, '.git'))) || candidates[0] || ACTIVE_HERMES_ROOT } -function runGit(args, options: any = {}): Promise<{code: number, stdout: string, stderr: string}> { +function runGit(args, options: any = {}): Promise<{ code: number; stdout: string; stderr: string }> { return new Promise((resolve, reject) => { const child = spawn( resolveGitBinary(), IS_WINDOWS ? ['-c', 'windows.appendAtomically=false', ...args] : args, hiddenWindowsChildOptions({ cwd: options.cwd, - env: { ...process.env, ...(options.env || {}) as any, GIT_TERMINAL_PROMPT: '0' }, + env: { ...process.env, ...((options.env || {}) as any), GIT_TERMINAL_PROMPT: '0' }, stdio: ['ignore', 'pipe', 'pipe'] }) ) @@ -2154,7 +2277,9 @@ function resolveUpdaterBinary() { } function repairMacUpdaterHelper(updater) { - if (!IS_MAC || !updater) {return} + if (!IS_MAC || !updater) { + return + } try { execFileSync('/usr/bin/xattr', ['-cr', updater], { stdio: 'ignore' }) @@ -2193,7 +2318,9 @@ function venvHermesShimPath(updateRoot) { // this practically always succeeds (no mandatory locking), so it returns false // — correct, since the shim-contention brick is Windows-only. function isShimLocked(shimPath) { - if (!IS_WINDOWS) {return false} + if (!IS_WINDOWS) { + return false + } let fd try { @@ -2224,9 +2351,13 @@ function isShimLocked(shimPath) { // not a process-group leader — a POSIX negative-pgid kill would be meaningless // here anyway). POSIX teardown stays with the existing before-quit SIGTERM. function forceKillProcessTree(pid) { - if (!IS_WINDOWS) {return} + if (!IS_WINDOWS) { + return + } - if (!Number.isInteger(pid) || pid <= 0) {return} + if (!Number.isInteger(pid) || pid <= 0) { + return + } try { execFileSync('taskkill', ['/PID', String(pid), '/T', '/F'], hiddenWindowsChildOptions({ stdio: 'ignore' })) @@ -2267,15 +2398,21 @@ async function releaseBackendLockForUpdate(updateRoot) { // `tag` only flavors the log lines. No-op off Windows (POSIX has no mandatory // locks — the before-quit SIGTERM + the cleanup script's own PID-wait suffice). async function releaseBackendLock(updateRoot, tag) { - if (!IS_WINDOWS) {return { unlocked: true }} + if (!IS_WINDOWS) { + return { unlocked: true } + } // Collect every backend PID the desktop owns: primary window backend + pool. const pids = [] - if (hermesProcess && Number.isInteger(hermesProcess.pid)) {pids.push(hermesProcess.pid)} + if (hermesProcess && Number.isInteger(hermesProcess.pid)) { + pids.push(hermesProcess.pid) + } for (const entry of backendPool.values()) { - if (entry.process && Number.isInteger(entry.process.pid)) {pids.push(entry.process.pid)} + if (entry.process && Number.isInteger(entry.process.pid)) { + pids.push(entry.process.pid) + } } // Graceful first (lets Python flush), then tree-kill to catch grandchildren. @@ -2289,7 +2426,9 @@ async function releaseBackendLock(updateRoot, tag) { stopAllPoolBackends() - for (const pid of pids) {forceKillProcessTree(pid)} + for (const pid of pids) { + forceKillProcessTree(pid) + } const shim = venvHermesShimPath(updateRoot) const deadlineMs = Date.now() + 15000 @@ -2306,13 +2445,19 @@ async function releaseBackendLock(updateRoot, tag) { // instead of trusting the initial sweep. const stragglers = [] - if (hermesProcess && Number.isInteger(hermesProcess.pid)) {stragglers.push(hermesProcess.pid)} - - for (const entry of backendPool.values()) { - if (entry.process && Number.isInteger(entry.process.pid)) {stragglers.push(entry.process.pid)} + if (hermesProcess && Number.isInteger(hermesProcess.pid)) { + stragglers.push(hermesProcess.pid) } - for (const pid of stragglers) {forceKillProcessTree(pid)} + for (const entry of backendPool.values()) { + if (entry.process && Number.isInteger(entry.process.pid)) { + stragglers.push(entry.process.pid) + } + } + + for (const pid of stragglers) { + forceKillProcessTree(pid) + } await new Promise(r => setTimeout(r, 300)) } @@ -2323,7 +2468,9 @@ async function releaseBackendLock(updateRoot, tag) { // imports broken (the July 2026 brotlicffi/_sodium.pyd incidents). Failing // the update loudly and keeping the app running is strictly better than a // bricked install that needs manual venv surgery. - rememberLog(`[${tag}] venv shim still locked after 15s; aborting hand-off (something outside this app holds the venv)`) + rememberLog( + `[${tag}] venv shim still locked after 15s; aborting hand-off (something outside this app holds the venv)` + ) return { unlocked: false } } @@ -2378,7 +2525,9 @@ async function applyUpdates(opts = {}) { if (head.code === 0 && current && current !== 'HEAD') { const branch = await resolveHealedBranch(updateRoot, current) - if (branch !== 'main') {command = `hermes update --branch ${branch}`} + if (branch !== 'main') { + command = `hermes update --branch ${branch}` + } } } catch { // Best-effort: fall back to bare `hermes update` if branch detection fails. @@ -2478,11 +2627,15 @@ async function applyUpdates(opts = {}) { } async function handOffWindowsBootstrapRecovery(reason) { - if (!IS_WINDOWS || !IS_PACKAGED) {return false} + if (!IS_WINDOWS || !IS_PACKAGED) { + return false + } const updater = resolveUpdaterBinary() - if (!updater) {return false} + if (!updater) { + return false + } const updateRoot = resolveUpdateRoot() const { branch: configuredBranch } = readDesktopUpdateConfig() @@ -2548,7 +2701,9 @@ async function handOffWindowsBootstrapRecovery(reason) { function resolveHermesCliBinary(updateRoot) { const venvHermes = path.join(updateRoot, 'venv', 'bin', 'hermes') - if (fileExists(venvHermes)) {return venvHermes} + if (fileExists(venvHermes)) { + return venvHermes + } return findOnPath('hermes') || null } @@ -2578,7 +2733,9 @@ function runStreamedUpdate(command, args, { cwd, env, stage }: any = {}) { for (const line of chunk.toString().split('\n')) { const trimmed = line.trim() - if (trimmed) {emitUpdateProgress({ stage, message: trimmed, percent: null })} + if (trimmed) { + emitUpdateProgress({ stage, message: trimmed, percent: null }) + } } } @@ -2592,10 +2749,14 @@ function runStreamedUpdate(command, args, { cwd, env, stage }: any = {}) { // The running app's .app bundle (packaged macOS): execPath is // .app/Contents/MacOS/; climb three levels to the bundle root. function runningAppBundle() { - if (!IS_MAC) {return null} + if (!IS_MAC) { + return null + } let dir = path.dirname(app.getPath('exe')) // .../Contents/MacOS - for (let i = 0; i < 2; i++) {dir = path.dirname(dir)} // -> .../X.app + for (let i = 0; i < 2; i++) { + dir = path.dirname(dir) + } // -> .../X.app return dir.endsWith('.app') ? dir : null } @@ -2670,11 +2831,11 @@ async function applyUpdatesPosixInApp(opts: any) { emitUpdateProgress({ stage: 'update', message: 'Updating Hermes (git + dependencies)…', percent: 10 }) - const updated = await runStreamedUpdate(hermes, ['update', '--yes', ...branchArgs], { + const updated = (await runStreamedUpdate(hermes, ['update', '--yes', ...branchArgs], { cwd: updateRoot, env, stage: 'update' - }) as any + })) as any if (updated.code !== 0) { emitUpdateProgress({ stage: 'error', message: 'hermes update failed.', error: updated.error || 'update-failed' }) @@ -2940,11 +3101,17 @@ function isActiveRuntimeUsable() { function isBootstrapComplete() { const marker = readBootstrapMarker() - if (!marker || typeof marker !== 'object') {return false} + if (!marker || typeof marker !== 'object') { + return false + } - if (marker.schemaVersion !== BOOTSTRAP_MARKER_SCHEMA_VERSION) {return false} + if (marker.schemaVersion !== BOOTSTRAP_MARKER_SCHEMA_VERSION) { + return false + } - if (typeof marker.pinnedCommit !== 'string' || marker.pinnedCommit.length < 7) {return false} + if (typeof marker.pinnedCommit !== 'string' || marker.pinnedCommit.length < 7) { + return false + } // We DELIBERATELY do NOT verify that the checkout is currently at the // pinned commit -- users update via the in-app update path or `hermes @@ -2975,11 +3142,15 @@ function writeBootstrapMarker(payload) { function resolveWebDist() { const override = process.env.HERMES_DESKTOP_WEB_DIST - if (override && directoryExists(path.resolve(override))) {return path.resolve(override)} + if (override && directoryExists(path.resolve(override))) { + return path.resolve(override) + } const unpackedDist = path.join(unpackedPathFor(APP_ROOT), 'dist') - if (directoryExists(unpackedDist)) {return unpackedDist} + if (directoryExists(unpackedDist)) { + return unpackedDist + } // Final fallback: APP_ROOT/dist. When packaged with asar:true this lives // INSIDE app.asar — not a servable filesystem directory — so the embedded @@ -3004,7 +3175,9 @@ function resolveRendererIndex() { const candidates = [path.join(APP_ROOT, 'dist', 'index.html'), path.join(resolveWebDist(), 'index.html')] const found = candidates.find(fileExists) - if (found) {return found} + if (found) { + return found + } // Nothing on disk. A packaged build with no renderer bundle blank-pages with // a bare ERR_FILE_NOT_FOUND and no clue why (see #39484). Surface the cause // and the fix before Electron loads the missing file. @@ -3050,14 +3223,18 @@ function resolveHermesCwd() { ] for (const candidate of candidates) { - if (!candidate) {continue} + if (!candidate) { + continue + } const resolved = path.resolve(String(candidate)) if (isPackagedInstallPath(resolved)) { continue } - if (directoryExists(resolved)) {return resolved} + if (directoryExists(resolved)) { + return resolved + } } return app.getPath('home') @@ -3128,7 +3305,9 @@ function writeDefaultProjectDir(dir) { function createPythonBackend(root, label, backendArgs, options: any = {}) { const python = findPythonForRoot(root) - if (!python) {return null} + if (!python) { + return null + } const venvRoot = path.join(root, 'venv') const venvPython = getVenvPython(venvRoot) @@ -3182,7 +3361,9 @@ function resolveHermesBackend(backendArgs) { if (overrideRoot && isHermesSourceRoot(overrideRoot)) { const backend = createPythonBackend(overrideRoot, `Hermes source at ${overrideRoot}`, backendArgs) - if (backend) {return backend} + if (backend) { + return backend + } } // 2. Development source -- when running `npm run dev` from a checkout, the @@ -3192,7 +3373,9 @@ function resolveHermesBackend(backendArgs) { if (!IS_PACKAGED && isHermesSourceRoot(SOURCE_REPO_ROOT)) { const backend = createPythonBackend(SOURCE_REPO_ROOT, `Hermes source at ${SOURCE_REPO_ROOT}`, backendArgs) - if (backend) {return backend} + if (backend) { + return backend + } } // 3. Bootstrap-complete ACTIVE_HERMES_ROOT -- the canonical install at @@ -3346,7 +3529,7 @@ async function ensureRuntime(backend) { rememberLog('[bootstrap] no Hermes install found; starting first-launch bootstrap') if (await handOffWindowsBootstrapRecovery('bootstrap-needed')) { - const handoffError: Error & {isBootstrapFailure?: boolean, bootstrapHandedOff?: boolean} = new Error( + const handoffError: Error & { isBootstrapFailure?: boolean; bootstrapHandedOff?: boolean } = new Error( 'Hermes recovery was handed off to Hermes Setup. The desktop will restart when recovery completes.' ) @@ -3564,7 +3747,9 @@ function fetchJson(url, token, options: any = {}) { req.destroy(new Error(`Timed out connecting to Hermes backend after ${timeoutMs}ms`)) }) - if (body) {req.write(body)} + if (body) { + req.write(body) + } req.end() }) } @@ -3651,7 +3836,9 @@ function fetchPublicJson(url, options: any = {}) { req.destroy(new Error(`Timed out connecting to Hermes backend after ${timeoutMs}ms`)) }) - if (body) {req.write(body)} + if (body) { + req.write(body) + } req.end() }) } @@ -3668,17 +3855,29 @@ function extensionForMimeType(mimeType) { .trim() .toLowerCase() - if (type === 'image/png') {return '.png'} + if (type === 'image/png') { + return '.png' + } - if (type === 'image/jpeg') {return '.jpg'} + if (type === 'image/jpeg') { + return '.jpg' + } - if (type === 'image/gif') {return '.gif'} + if (type === 'image/gif') { + return '.gif' + } - if (type === 'image/webp') {return '.webp'} + if (type === 'image/webp') { + return '.webp' + } - if (type === 'image/bmp') {return '.bmp'} + if (type === 'image/bmp') { + return '.bmp' + } - if (type === 'image/svg+xml') {return '.svg'} + if (type === 'image/svg+xml') { + return '.svg' + } return '' } @@ -3738,7 +3937,9 @@ const renderTitleQueue = [] function canonicalTitleCacheKey(rawUrl) { const value = String(rawUrl || '').trim() - if (!value) {return ''} + if (!value) { + return '' + } try { const url = new URL(value) @@ -3752,7 +3953,9 @@ function canonicalTitleCacheKey(rawUrl) { } function cacheTitle(key, title) { - if (titleCache.size >= TITLE_CACHE_LIMIT) {titleCache.delete(titleCache.keys().next().value)} + if (titleCache.size >= TITLE_CACHE_LIMIT) { + titleCache.delete(titleCache.keys().next().value) + } titleCache.set(key, title) } @@ -3773,7 +3976,9 @@ function fetchHtmlTitleWithCurl(rawUrl: string): Promise { return new Promise(resolve => { const url = String(rawUrl || '').trim() - if (!url) {return resolve('')} + if (!url) { + return resolve('') + } const args = [ '--silent', @@ -3802,7 +4007,9 @@ function fetchHtmlTitleWithCurl(rawUrl: string): Promise { let bytes = 0 child.stdout.on('data', chunk => { - if (bytes >= TITLE_BYTE_BUDGET) {return} + if (bytes >= TITLE_BYTE_BUDGET) { + return + } const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk) const remaining = TITLE_BYTE_BUDGET - bytes const next = buffer.length > remaining ? buffer.subarray(0, remaining) : buffer @@ -3812,14 +4019,18 @@ function fetchHtmlTitleWithCurl(rawUrl: string): Promise { child.on('error', () => resolve('')) child.on('close', () => { - if (!chunks.length) {return resolve('')} + if (!chunks.length) { + return resolve('') + } resolve(parseHtmlTitle(Buffer.concat(chunks).toString('utf8'))) }) }) } function getLinkTitleSession() { - if (linkTitleSession || !app.isReady()) {return linkTitleSession} + if (linkTitleSession || !app.isReady()) { + return linkTitleSession + } linkTitleSession = session.fromPartition('hermes:link-titles', { cache: false }) linkTitleSession.webRequest.onBeforeRequest((details, callback) => { callback({ cancel: RENDER_TITLE_BLOCKED_RESOURCES.has(details.resourceType) }) @@ -3843,11 +4054,15 @@ function dequeueRenderTitle() { function runRenderTitleJob(rawUrl) { return new Promise(resolve => { - if (!app.isReady()) {return resolve('')} + if (!app.isReady()) { + return resolve('') + } const partitionSession = getLinkTitleSession() - if (!partitionSession) {return resolve('')} + if (!partitionSession) { + return resolve('') + } let settled = false let window = null @@ -3855,16 +4070,24 @@ function runRenderTitleJob(rawUrl) { let graceTimer = null const finish = title => { - if (settled) {return} + if (settled) { + return + } settled = true - if (hardTimer) {clearTimeout(hardTimer)} + if (hardTimer) { + clearTimeout(hardTimer) + } - if (graceTimer) {clearTimeout(graceTimer)} + if (graceTimer) { + clearTimeout(graceTimer) + } const value = (title || '').replace(/\s+/g, ' ').trim() try { - if (window && !window.isDestroyed()) {window.destroy()} + if (window && !window.isDestroyed()) { + window.destroy() + } } catch { // BrowserWindow may already be torn down; ignore. } @@ -3881,7 +4104,9 @@ function runRenderTitleJob(rawUrl) { const finishWithTitle = () => finish(readLinkTitleWindowTitle(window)) const scheduleGrace = () => { - if (graceTimer) {clearTimeout(graceTimer)} + if (graceTimer) { + clearTimeout(graceTimer) + } graceTimer = setTimeout(finishWithTitle, RENDER_TITLE_GRACE_MS) } @@ -3891,7 +4116,9 @@ function runRenderTitleJob(rawUrl) { window.webContents.on('page-title-updated', scheduleGrace) window.webContents.on('did-finish-load', scheduleGrace) window.webContents.on('did-fail-load', (_event, _code, _desc, _validatedURL, isMainFrame) => { - if (isMainFrame) {finish('')} + if (isMainFrame) { + finish('') + } }) window @@ -3912,19 +4139,25 @@ function fetchHtmlTitleWithRenderer(rawUrl: string): Promise { // Strips known error/captcha titles (e.g. "GetYourGuide – Error", "Just a // moment...") so they don't get cached as the resolved title. -function usableTitle (value: string): string { - return (value && !TITLE_ERROR_RE.test(value) ? value : '') +function usableTitle(value: string): string { + return value && !TITLE_ERROR_RE.test(value) ? value : '' } function fetchLinkTitle(rawUrl) { const url = String(rawUrl || '').trim() const key = canonicalTitleCacheKey(url) - if (!key) {return Promise.resolve('')} + if (!key) { + return Promise.resolve('') + } - if (titleCache.has(key)) {return Promise.resolve(titleCache.get(key))} + if (titleCache.has(key)) { + return Promise.resolve(titleCache.get(key)) + } - if (titleInflight.has(key)) {return titleInflight.get(key)} + if (titleInflight.has(key)) { + return titleInflight.get(key) + } const pending = fetchHtmlTitleWithCurl(url) .catch(() => '') @@ -3945,12 +4178,16 @@ function fetchLinkTitle(rawUrl) { } async function resourceBufferFromUrl(rawUrl) { - if (!rawUrl) {throw new Error('Missing URL')} + if (!rawUrl) { + throw new Error('Missing URL') + } if (rawUrl.startsWith('data:')) { const match = rawUrl.match(/^data:([^;,]+)?(;base64)?,(.*)$/s) - if (!match) {throw new Error('Invalid data URL')} + if (!match) { + throw new Error('Invalid data URL') + } const mimeType = match[1] || 'application/octet-stream' const encoded = match[3] || '' const buffer = match[2] ? Buffer.from(encoded, 'base64') : Buffer.from(decodeURIComponent(encoded), 'utf8') @@ -3993,15 +4230,17 @@ async function resourceBufferFromUrl(rawUrl) { } async function copyImageFromUrl(rawUrl) { - const { buffer } = await resourceBufferFromUrl(rawUrl) as any + const { buffer } = (await resourceBufferFromUrl(rawUrl)) as any const image = nativeImage.createFromBuffer(buffer) - if (image.isEmpty()) {throw new Error('Could not read image')} + if (image.isEmpty()) { + throw new Error('Could not read image') + } clipboard.writeImage(image) } async function saveImageFromUrl(rawUrl) { - const { buffer, mimeType } = await resourceBufferFromUrl(rawUrl) as any + const { buffer, mimeType } = (await resourceBufferFromUrl(rawUrl)) as any const fallbackName = filenameFromUrl(rawUrl, `image${extensionForMimeType(mimeType) || '.png'}`) const result = await dialog.showSaveDialog(mainWindow, { @@ -4009,7 +4248,9 @@ async function saveImageFromUrl(rawUrl) { defaultPath: fallbackName }) - if (result.canceled || !result.filePath) {return false} + if (result.canceled || !result.filePath) { + return false + } await fs.promises.writeFile(result.filePath, buffer) return true @@ -4141,10 +4382,14 @@ async function filePathFromPreviewUrl(rawUrl) { } function sendPreviewFileChanged(payload) { - if (!mainWindow || mainWindow.isDestroyed()) {return} + if (!mainWindow || mainWindow.isDestroyed()) { + return + } const { webContents } = mainWindow - if (!webContents || webContents.isDestroyed()) {return} + if (!webContents || webContents.isDestroyed()) { + return + } webContents.send('hermes:preview-file-changed', payload) } @@ -4162,18 +4407,24 @@ async function watchPreviewFile(rawUrl) { return } - if (timer) {clearTimeout(timer)} + if (timer) { + clearTimeout(timer) + } timer = setTimeout(() => { timer = null - if (!fileExists(filePath)) {return} + if (!fileExists(filePath)) { + return + } sendPreviewFileChanged({ id, path: filePath, url: pathToFileURL(filePath).toString() }) }, PREVIEW_WATCH_DEBOUNCE_MS) }) previewWatchers.set(id, { close: () => { - if (timer) {clearTimeout(timer)} + if (timer) { + clearTimeout(timer) + } watcher.close() } }) @@ -4219,7 +4470,9 @@ async function waitForHermes(baseUrl, token) { } function getWindowButtonPosition() { - if (!IS_MAC) {return null} + if (!IS_MAC) { + return null + } return mainWindow?.getWindowButtonPosition?.() || WINDOW_BUTTON_POSITION } @@ -4237,18 +4490,26 @@ function getWindowState() { } function sendBackendExit(payload) { - if (!mainWindow || mainWindow.isDestroyed()) {return} + if (!mainWindow || mainWindow.isDestroyed()) { + return + } const { webContents } = mainWindow - if (!webContents || webContents.isDestroyed()) {return} + if (!webContents || webContents.isDestroyed()) { + return + } webContents.send('hermes:backend-exit', payload) } function sendClosePreviewRequested() { - if (!mainWindow || mainWindow.isDestroyed()) {return} + if (!mainWindow || mainWindow.isDestroyed()) { + return + } const { webContents } = mainWindow - if (!webContents || webContents.isDestroyed()) {return} + if (!webContents || webContents.isDestroyed()) { + return + } webContents.send('hermes:close-preview-requested') } @@ -4256,17 +4517,23 @@ function sendClosePreviewRequested() { // renderer's WebSocket to the local backend; the renderer reconnects on this // signal so the chat composer doesn't stay stuck on "Starting Hermes...". function sendPowerResume() { - if (!mainWindow || mainWindow.isDestroyed()) {return} + if (!mainWindow || mainWindow.isDestroyed()) { + return + } const { webContents } = mainWindow - if (!webContents || webContents.isDestroyed()) {return} + if (!webContents || webContents.isDestroyed()) { + return + } webContents.send('hermes:power-resume') } let powerResumeRegistered = false function registerPowerResumeListeners() { - if (powerResumeRegistered) {return} + if (powerResumeRegistered) { + return + } powerResumeRegistered = true try { @@ -4285,21 +4552,31 @@ function getAppIconPath() { } function sendOpenUpdatesRequested() { - if (!mainWindow || mainWindow.isDestroyed()) {return} + if (!mainWindow || mainWindow.isDestroyed()) { + return + } const { webContents } = mainWindow - if (!webContents || webContents.isDestroyed()) {return} + if (!webContents || webContents.isDestroyed()) { + return + } webContents.send('hermes:open-updates') - if (!mainWindow.isVisible()) {mainWindow.show()} + if (!mainWindow.isVisible()) { + mainWindow.show() + } mainWindow.focus() } function sendWindowStateChanged(nextIsFullscreen?: boolean) { - if (!mainWindow || mainWindow.isDestroyed()) {return} + if (!mainWindow || mainWindow.isDestroyed()) { + return + } const { webContents } = mainWindow - if (!webContents || webContents.isDestroyed()) {return} + if (!webContents || webContents.isDestroyed()) { + return + } const state = getWindowState() if (typeof nextIsFullscreen === 'boolean') { @@ -4441,7 +4718,9 @@ function installDevToolsShortcut(window) { (IS_MAC && input.meta && input.alt && key === 'i') || (!IS_MAC && input.control && input.shift && key === 'i') - if (!isInspectShortcut) {return} + if (!isInspectShortcut) { + return + } event.preventDefault() toggleDevTools(window) }) @@ -4452,7 +4731,9 @@ function installPreviewShortcut(window) { const key = String(input.key || '').toLowerCase() const isPreviewCloseShortcut = key === 'w' && (IS_MAC ? input.meta : input.control) && !input.alt && !input.shift - if (!isPreviewCloseShortcut || !previewShortcutActive) {return} + if (!isPreviewCloseShortcut || !previewShortcutActive) { + return + } event.preventDefault() sendClosePreviewRequested() @@ -4465,9 +4746,10 @@ function installPreviewShortcut(window) { // read it back on did-finish-load to re-apply after reloads or crash recovery. import { clampZoomLevel, percentToZoomLevel, ZOOM_STORAGE_KEY, zoomLevelToPercent } from './zoom' - function setAndPersistZoomLevel(window, zoomLevel) { - if (!window || window.isDestroyed()) {return} + if (!window || window.isDestroyed()) { + return + } const next = clampZoomLevel(zoomLevel) window.webContents.setZoomLevel(next) // Keep any open settings UI in sync, including changes made via the @@ -4481,13 +4763,17 @@ function setAndPersistZoomLevel(window, zoomLevel) { } function restorePersistedZoomLevel(window) { - if (!window || window.isDestroyed()) {return} + if (!window || window.isDestroyed()) { + return + } window.webContents .executeJavaScript( `(() => { try { return localStorage.getItem(${JSON.stringify(ZOOM_STORAGE_KEY)}) } catch { return null } })()` ) .then(stored => { - if (stored == null || !window || window.isDestroyed()) {return} + if (stored == null || !window || window.isDestroyed()) { + return + } const level = clampZoomLevel(Number(stored)) window.webContents.setZoomLevel(level) }) @@ -4503,7 +4789,9 @@ function installZoomShortcuts(window) { window.webContents.on('before-input-event', (event, input) => { const mod = IS_MAC ? input.meta : input.control - if (!mod || input.alt || input.shift) {return} + if (!mod || input.alt || input.shift) { + return + } const key = input.key @@ -4559,7 +4847,9 @@ function installContextMenu(window) { } if (hasLink) { - if (template.length) {template.push({ type: 'separator' })} + if (template.length) { + template.push({ type: 'separator' }) + } template.push( { label: 'Open Link', @@ -4578,7 +4868,9 @@ function installContextMenu(window) { const suggestions = Array.isArray(params.dictionarySuggestions) ? params.dictionarySuggestions : [] if (isEditable && params.misspelledWord && suggestions.length > 0) { - if (template.length) {template.push({ type: 'separator' })} + if (template.length) { + template.push({ type: 'separator' }) + } for (const suggestion of suggestions.slice(0, 5)) { template.push({ @@ -4595,7 +4887,9 @@ function installContextMenu(window) { } if (hasSelection || isEditable) { - if (template.length) {template.push({ type: 'separator' })} + if (template.length) { + template.push({ type: 'separator' }) + } if (isEditable) { template.push( @@ -4659,7 +4953,7 @@ function installMediaPermissions() { // the check defaults to false and the mic is denied before the request // handler ever runs. session.defaultSession.setPermissionCheckHandler((_webContents, permission, _origin, details) => { - if (permission === 'media' || permission === 'audioCapture' as any /* todo: is this needed? */) { + if (permission === 'media' || permission === ('audioCapture' as any) /* todo: is this needed? */) { // details.mediaType is a single string here (not the mediaTypes array). const mediaType = details?.mediaType @@ -4706,7 +5000,9 @@ function installMediaPermissions() { const OAUTH_SESSION_PARTITION = 'persist:hermes-remote-oauth' function getOauthSession() { - if (oauthSession || !app.isReady()) {return oauthSession} + if (oauthSession || !app.isReady()) { + return oauthSession + } oauthSession = session.fromPartition(OAUTH_SESSION_PARTITION) return oauthSession @@ -4719,7 +5015,9 @@ function getOauthSession() { async function hasOauthSessionCookie(baseUrl) { const sess = getOauthSession() - if (!sess) {return false} + if (!sess) { + return false + } const parsed = new URL(baseUrl) try { @@ -4749,7 +5047,9 @@ async function hasOauthSessionCookie(baseUrl) { async function hasLiveOauthSession(baseUrl) { const sess = getOauthSession() - if (!sess) {return false} + if (!sess) { + return false + } const parsed = new URL(baseUrl) try { @@ -4770,7 +5070,9 @@ async function hasLiveOauthSession(baseUrl) { async function clearOauthSession(baseUrl) { const sess = getOauthSession() - if (!sess) {return} + if (!sess) { + return + } try { const cookies = await sess.cookies.get(baseUrl ? { url: baseUrl } : {}) @@ -4813,25 +5115,38 @@ function openOauthLoginWindow(baseUrl) { let pollTimer = null const finish = err => { - if (settled) {return} + if (settled) { + return + } settled = true - if (pollTimer) {clearInterval(pollTimer)} + if (pollTimer) { + clearInterval(pollTimer) + } try { - if (win && !win.isDestroyed()) {win.destroy()} + if (win && !win.isDestroyed()) { + win.destroy() + } } catch { // window already torn down } - if (err) {reject(err)} - else {resolve({ baseUrl, ok: true })} + if (err) { + reject(err) + } else { + resolve({ baseUrl, ok: true }) + } } const checkCookie = async () => { - if (settled) {return} + if (settled) { + return + } - if (await hasOauthSessionCookie(baseUrl)) {finish(null)} + if (await hasOauthSessionCookie(baseUrl)) { + finish(null) + } } try { @@ -4863,7 +5178,9 @@ function openOauthLoginWindow(baseUrl) { pollTimer = setInterval(() => void checkCookie(), 750) win.on('closed', () => { - if (!settled) {finish(new Error('Login window closed before authentication completed.'))} + if (!settled) { + finish(new Error('Login window closed before authentication completed.')) + } }) // ``next`` is intentionally omitted: the gateway lands on ``/`` after @@ -4936,7 +5253,9 @@ function fetchJsonViaOauthSession(url, options: any = {}) { const chunks = [] res.on('data', chunk => chunks.push(Buffer.from(chunk))) res.on('end', () => { - if (timedOut) {return} + if (timedOut) { + return + } clearTimeout(timer) const text = Buffer.concat(chunks).toString('utf8') const statusCode = res.statusCode || 500 @@ -4972,12 +5291,16 @@ function fetchJsonViaOauthSession(url, options: any = {}) { }) }) request.on('error', error => { - if (timedOut) {return} + if (timedOut) { + return + } clearTimeout(timer) reject(error) }) - if (body) {request.write(body)} + if (body) { + request.write(body) + } request.end() }) } @@ -4986,10 +5309,10 @@ function fetchJsonViaOauthSession(url, options: any = {}) { // Throws (with statusCode 401) if the session cookie is missing/expired — // callers treat that as "needs re-login". async function mintGatewayWsTicket(baseUrl) { - const body = await fetchJsonViaOauthSession(`${baseUrl}/api/auth/ws-ticket`, { + const body = (await fetchJsonViaOauthSession(`${baseUrl}/api/auth/ws-ticket`, { method: 'POST', timeoutMs: 8_000 - }) as any + })) as any const ticket = body?.ticket @@ -5070,7 +5393,9 @@ function sanitizeConnectionProfiles(raw: Record) { continue } - const cleaned: {mode: 'remote' | 'local', url?: string, authMode?: string, token?: object } ={ mode: entry.mode === 'remote' ? 'remote' : 'local', } + const cleaned: { mode: 'remote' | 'local'; url?: string; authMode?: string; token?: object } = { + mode: entry.mode === 'remote' ? 'remote' : 'local' + } const url = String(entry.url || '').trim() if (url) { @@ -5231,7 +5556,7 @@ function buildRemoteBlock(remoteUrl, authMode, token) { return { url: normalizeRemoteBaseUrl(remoteUrl), authMode, token } } -function coerceDesktopConnectionConfig(input :any= {}, existing = readDesktopConnectionConfig(), options: any = {}) { +function coerceDesktopConnectionConfig(input: any = {}, existing = readDesktopConnectionConfig(), options: any = {}) { const persistToken = options.persistToken !== false const key = connectionScopeKey(input.profile) const mode = input.mode === 'remote' ? 'remote' : 'local' @@ -5467,7 +5792,7 @@ async function probeRemoteAuthMode(rawUrl) { // an OAuth-redirect one (``supports_password``). A failure here doesn't // change the auth mode, so swallow it. try { - const body = await fetchPublicJson(`${baseUrl}/api/auth/providers`, { timeoutMs: 8_000 }) as any + const body = (await fetchPublicJson(`${baseUrl}/api/auth/providers`, { timeoutMs: 8_000 })) as any if (Array.isArray(body?.providers)) { providers = body.providers @@ -5526,7 +5851,7 @@ async function testDesktopConnectionConfig(input: any = {}) { authMode = normAuthMode(remote.authMode) } - const status = await fetchJson(`${baseUrl}/api/status`, token, { timeoutMs: 8_000 }) as any + const status = (await fetchJson(`${baseUrl}/api/status`, token, { timeoutMs: 8_000 })) as any // The HTTP status check above proves the backend is reachable, but the chat // surface only works once the renderer's live WebSocket to ``/api/ws`` @@ -5572,7 +5897,9 @@ function resetBootProgressForReconnect() { } function stopBackendChild(child) { - if (!child || child.killed) {return} + if (!child || child.killed) { + return + } try { if (IS_WINDOWS && Number.isInteger(child.pid)) { @@ -5683,10 +6010,14 @@ async function ensureBackend(profile) { function touchPoolBackend(profile) { const key = profile && String(profile).trim() ? String(profile).trim() : null - if (!key) {return} + if (!key) { + return + } const entry = backendPool.get(key) - if (entry) {entry.lastActiveAt = Date.now()} + if (entry) { + entry.lastActiveAt = Date.now() + } } // Evict least-recently-used pool backends until at most `keep` remain — but only @@ -5694,7 +6025,9 @@ function touchPoolBackend(profile) { // window). When every backend is actively kept alive we let the pool exceed the // soft cap rather than kill a running session. function evictLruPoolBackends(keep) { - if (backendPool.size <= keep) {return} + if (backendPool.size <= keep) { + return + } const now = Date.now() const evictable = [...backendPool.entries()] @@ -5704,7 +6037,9 @@ function evictLruPoolBackends(keep) { let removable = backendPool.size - Math.max(0, keep) for (const [profile] of evictable) { - if (removable <= 0) {break} + if (removable <= 0) { + break + } rememberLog(`Evicting idle profile backend "${profile}" (LRU cap ${POOL_MAX_BACKENDS})`) stopPoolBackend(profile) removable -= 1 @@ -5712,7 +6047,9 @@ function evictLruPoolBackends(keep) { } function startPoolIdleReaper() { - if (poolIdleReaper) {return} + if (poolIdleReaper) { + return + } poolIdleReaper = setInterval(() => { const now = Date.now() @@ -5729,7 +6066,9 @@ function startPoolIdleReaper() { } }, 60_000) - if (typeof poolIdleReaper.unref === 'function') {poolIdleReaper.unref()} + if (typeof poolIdleReaper.unref === 'function') { + poolIdleReaper.unref() + } } // Spawn an additional dashboard backend pinned to a named profile. Mirrors the @@ -5860,7 +6199,9 @@ async function spawnPoolBackend(profile, entry) { function stopPoolBackend(profile) { const entry = backendPool.get(profile) - if (!entry) {return} + if (!entry) { + return + } backendPool.delete(profile) stopBackendChild(entry.process) } @@ -5868,7 +6209,9 @@ function stopPoolBackend(profile) { async function teardownPoolBackendAndWait(profile) { const entry = backendPool.get(profile) - if (!entry) {return} + if (!entry) { + return + } backendPool.delete(profile) stopBackendChild(entry.process) @@ -5952,7 +6295,9 @@ async function startHermes() { throw backendStartFailure } - if (connectionPromise) {return connectionPromise} + if (connectionPromise) { + return connectionPromise + } connectionPromise = (async () => { await advanceBootProgress('backend.resolve', 'Resolving Hermes backend', 8) @@ -6191,15 +6536,25 @@ function wireCommonWindowHandlers(win) { const sessionWindows = createSessionWindowRegistry() function focusWindow(win) { - if (!win || win.isDestroyed()) {return} + if (!win || win.isDestroyed()) { + return + } - if (win.isMinimized()) {win.restore()} + if (win.isMinimized()) { + win.restore() + } - if (!win.isVisible()) {win.show()} + if (!win.isVisible()) { + win.show() + } win.focus() } -function spawnSecondaryWindow({ sessionId, watch, newSession }:{ sessionId?: string, watch?: boolean, newSession?: boolean } = {}) { +function spawnSecondaryWindow({ + sessionId, + watch, + newSession +}: { sessionId?: string; watch?: boolean; newSession?: boolean } = {}) { const icon = getAppIconPath() const win = new BrowserWindow({ @@ -6230,7 +6585,9 @@ function spawnSecondaryWindow({ sessionId, watch, newSession }:{ sessionId?: str } win.once('ready-to-show', () => { - if (!win.isDestroyed()) {win.show()} + if (!win.isDestroyed()) { + win.show() + } }) win.on('enter-full-screen', () => sendWindowStateChanged(true)) @@ -6349,7 +6706,9 @@ function spawnPetOverlayWindow(bounds) { wireCommonWindowHandlers(win) win.once('ready-to-show', () => { - if (!win.isDestroyed()) {win.showInactive()} + if (!win.isDestroyed()) { + win.showInactive() + } }) win.on('closed', () => { @@ -6448,10 +6807,14 @@ function createWindow() { } } - if (savedWindowState?.isMaximized) {mainWindow.maximize()} + if (savedWindowState?.isMaximized) { + mainWindow.maximize() + } mainWindow.once('ready-to-show', () => { - if (mainWindow && !mainWindow.isDestroyed()) {mainWindow.show()} + if (mainWindow && !mainWindow.isDestroyed()) { + mainWindow.show() + } }) mainWindow.on('will-enter-full-screen', () => sendWindowStateChanged(true)) @@ -6491,7 +6854,9 @@ function createWindow() { rendererReloadTimes.push(now) setImmediate(() => { - if (!mainWindow || mainWindow.isDestroyed()) {return} + if (!mainWindow || mainWindow.isDestroyed()) { + return + } try { mainWindow.webContents.reload() @@ -6512,7 +6877,9 @@ function createWindow() { const details = detailsOrLevel && typeof detailsOrLevel === 'object' ? detailsOrLevel : null const level = details ? details.level : detailsOrLevel - if (level !== 3) {return} + if (level !== 3) { + return + } const text = details ? details.message : message const src = details ? details.sourceUrl : sourceId @@ -6611,7 +6978,9 @@ ipcMain.handle('hermes:zoom:get', event => { ipcMain.on('hermes:zoom:set-percent', (event, percent) => { const window = BrowserWindow.fromWebContents(event.sender) - if (!window || window.isDestroyed()) {return} + if (!window || window.isDestroyed()) { + return + } setAndPersistZoomLevel(window, percentToZoomLevel(Number(percent))) }) @@ -6936,7 +7305,9 @@ async function interceptSessionRequestForRemote(request) { const body = request.body && typeof request.body === 'object' ? { ...request.body } : request.body - if (body) {delete body.profile} + if (body) { + delete body.profile + } return requestJsonForProfile(profile, pathname, method, body) } @@ -6989,10 +7360,10 @@ async function mergeRemoteProfileSessions(searchParams, remoteProfiles) { const primary = await ensureBackend(null) - const base = await fetchJson(`${primary.baseUrl}/api/profiles/sessions?${searchParams}`, primary.token, { + const base = (await fetchJson(`${primary.baseUrl}/api/profiles/sessions?${searchParams}`, primary.token, { method: 'GET', timeoutMs: DEFAULT_FETCH_TIMEOUT_MS - }).catch(() => ({ sessions: [], total: 0, profile_totals: {} })) as any + }).catch(() => ({ sessions: [], total: 0, profile_totals: {} }))) as any // Over-fetch each remote from offset 0 (limit+offset rows) so the merged window // is correct for this page — mirrors the primary's per-profile over-fetch. @@ -7078,7 +7449,9 @@ ipcMain.handle('hermes:api', async (_event, request) => { }) ipcMain.handle('hermes:notify', (_event, payload) => { - if (!Notification.isSupported()) {return false} + if (!Notification.isSupported()) { + return false + } // Action buttons render only on signed macOS builds; elsewhere they're dropped // and the body click still works. const actions = Array.isArray(payload?.actions) ? payload.actions : [] @@ -7091,7 +7464,9 @@ ipcMain.handle('hermes:notify', (_event, payload) => { }) notification.on('click', () => { - if (!mainWindow || mainWindow.isDestroyed()) {return} + if (!mainWindow || mainWindow.isDestroyed()) { + return + } focusWindow(mainWindow) if (payload?.sessionId) { @@ -7099,7 +7474,9 @@ ipcMain.handle('hermes:notify', (_event, payload) => { } }) notification.on('action', (_actionEvent, index) => { - if (!mainWindow || mainWindow.isDestroyed()) {return} + if (!mainWindow || mainWindow.isDestroyed()) { + return + } const action = actions[index] if (action?.id) { @@ -7153,7 +7530,9 @@ ipcMain.handle('hermes:readFileText', async (_event, filePath) => { ipcMain.handle('hermes:selectPaths', async (_event, options: any = {}) => { const properties = options?.directories ? ['openDirectory'] : ['openFile'] - if (options?.multiple !== false) {properties.push('multiSelections')} + if (options?.multiple !== false) { + properties.push('multiSelections') + } let resolvedDefaultPath @@ -7172,7 +7551,9 @@ ipcMain.handle('hermes:selectPaths', async (_event, options: any = {}) => { filters: Array.isArray(options?.filters) ? options.filters : undefined }) - if (result.canceled) {return []} + if (result.canceled) { + return [] + } return result.filePaths }) @@ -7188,7 +7569,9 @@ ipcMain.handle('hermes:saveImageFromUrl', (_event, url) => saveImageFromUrl(Stri ipcMain.handle('hermes:saveImageBuffer', async (_event, payload) => { const data = payload?.data - if (!data) {throw new Error('saveImageBuffer: missing data')} + if (!data) { + throw new Error('saveImageBuffer: missing data') + } const buffer = Buffer.isBuffer(data) ? data : Buffer.from(data) @@ -7830,7 +8213,9 @@ async function getUninstallSummary() { let settled = false const done = value => { - if (settled) {return} + if (settled) { + return + } settled = true resolve(value) } @@ -7851,7 +8236,9 @@ async function getUninstallSummary() { }) child.on('error', () => done(fallback())) child.on('exit', code => { - if (code !== 0) {return done(fallback())} + if (code !== 0) { + return done(fallback()) + } try { const line = stdout.trim().split('\n').filter(Boolean).pop() || '{}' @@ -8012,13 +8399,17 @@ let _pendingDeepLink = null let _rendererReadyForDeepLink = false function _extractDeepLink(argv) { - if (!Array.isArray(argv)) {return null} + if (!Array.isArray(argv)) { + return null + } return argv.find(a => typeof a === 'string' && a.startsWith(`${HERMES_PROTOCOL}://`)) || null } function handleDeepLink(url) { - if (!url || typeof url !== 'string') {return} + if (!url || typeof url !== 'string') { + return + } let parsed try { @@ -8045,7 +8436,9 @@ function handleDeepLink(url) { } try { - if (mainWindow.isMinimized()) {mainWindow.restore()} + if (mainWindow.isMinimized()) { + mainWindow.restore() + } mainWindow.focus() mainWindow.webContents.send('hermes:deep-link', payload) rememberLog(`[deeplink] delivered ${kind}/${name}`) @@ -8096,9 +8489,12 @@ if (!_gotSingleInstanceLock) { app.on('second-instance', (_event, argv) => { const url = _extractDeepLink(argv) - if (url) {handleDeepLink(url)} - else if (mainWindow) { - if (mainWindow.isMinimized()) {mainWindow.restore()} + if (url) { + handleDeepLink(url) + } else if (mainWindow) { + if (mainWindow.isMinimized()) { + mainWindow.restore() + } mainWindow.focus() } }) @@ -8130,7 +8526,9 @@ app.whenReady().then(() => { // Win/Linux cold start: the launching hermes:// URL is in our own argv. const _coldStartLink = _extractDeepLink(process.argv) - if (_coldStartLink) {handleDeepLink(_coldStartLink)} + if (_coldStartLink) { + handleDeepLink(_coldStartLink) + } app.on('activate', () => { // Recreate the primary window if it's gone. Guard on mainWindow directly @@ -8206,5 +8604,7 @@ app.on('window-all-closed', () => { // the bundle and relaunch — without this the script's PID-wait spins to its // full timeout and the user is left with an invisible app (or an uninstall // that appears to do nothing). - if (process.platform !== 'darwin' || isQuittingForHandoff) {app.quit()} + if (process.platform !== 'darwin' || isQuittingForHandoff) { + app.quit() + } }) diff --git a/apps/desktop/electron/oauth-net-request.ts b/apps/desktop/electron/oauth-net-request.ts index 7c2b44821c6..bab5ef53f69 100644 --- a/apps/desktop/electron/oauth-net-request.ts +++ b/apps/desktop/electron/oauth-net-request.ts @@ -14,5 +14,4 @@ function setJsonRequestHeaders(request) { request.setHeader('Content-Type', 'application/json') } -export { serializeJsonBody, - setJsonRequestHeaders } +export { serializeJsonBody, setJsonRequestHeaders } diff --git a/apps/desktop/electron/session-windows.test.ts b/apps/desktop/electron/session-windows.test.ts index 48f0441d23b..cf65d39ac5d 100644 --- a/apps/desktop/electron/session-windows.test.ts +++ b/apps/desktop/electron/session-windows.test.ts @@ -1,9 +1,7 @@ import assert from 'node:assert/strict' import test from 'node:test' -import { buildSessionWindowUrl, - chatWindowWebPreferences, - createSessionWindowRegistry } from './session-windows' +import { buildSessionWindowUrl, chatWindowWebPreferences, createSessionWindowRegistry } from './session-windows' // A minimal fake BrowserWindow: tracks listeners + destroyed state and lets a // test fire the 'closed' event, mirroring the slice of the Electron API the diff --git a/apps/desktop/electron/session-windows.ts b/apps/desktop/electron/session-windows.ts index 7dbba58ca06..af55608b0f4 100644 --- a/apps/desktop/electron/session-windows.ts +++ b/apps/desktop/electron/session-windows.ts @@ -115,8 +115,10 @@ function createSessionWindowRegistry() { } } -export { buildSessionWindowUrl, +export { + buildSessionWindowUrl, chatWindowWebPreferences, createSessionWindowRegistry, SESSION_WINDOW_MIN_HEIGHT, - SESSION_WINDOW_MIN_WIDTH } + SESSION_WINDOW_MIN_WIDTH +} diff --git a/apps/desktop/electron/titlebar-overlay-width.test.ts b/apps/desktop/electron/titlebar-overlay-width.test.ts index c2c77d918c2..543a14cd56f 100644 --- a/apps/desktop/electron/titlebar-overlay-width.test.ts +++ b/apps/desktop/electron/titlebar-overlay-width.test.ts @@ -1,7 +1,12 @@ import assert from 'node:assert/strict' import test from 'node:test' -import { MACOS_TAHOE_DARWIN_MAJOR, macTitleBarOverlayHeight, nativeOverlayWidth, OVERLAY_FALLBACK_WIDTH } from './titlebar-overlay-width' +import { + MACOS_TAHOE_DARWIN_MAJOR, + macTitleBarOverlayHeight, + nativeOverlayWidth, + OVERLAY_FALLBACK_WIDTH +} from './titlebar-overlay-width' // This static reservation is only the pre-layout FALLBACK. Once laid out the // renderer reads the exact width from navigator.windowControlsOverlay diff --git a/apps/desktop/electron/titlebar-overlay-width.ts b/apps/desktop/electron/titlebar-overlay-width.ts index d31d40fec5a..d6a4c5d1f24 100644 --- a/apps/desktop/electron/titlebar-overlay-width.ts +++ b/apps/desktop/electron/titlebar-overlay-width.ts @@ -15,7 +15,9 @@ export const OVERLAY_FALLBACK_WIDTH = 144 * @param {{ isMac?: boolean }} opts */ export function nativeOverlayWidth({ isWindows = false, isWsl = false, isMac = false } = {}) { - if (isMac) {return 0} + if (isMac) { + return 0 + } return OVERLAY_FALLBACK_WIDTH } @@ -38,4 +40,3 @@ export const MACOS_TAHOE_DARWIN_MAJOR = 25 export function macTitleBarOverlayHeight({ darwinMajor = 0, titlebarHeight = 0 } = {}) { return darwinMajor >= MACOS_TAHOE_DARWIN_MAJOR ? 0 : titlebarHeight } - diff --git a/apps/desktop/electron/update-count.ts b/apps/desktop/electron/update-count.ts index b0bda95a9f0..23fb6cac134 100644 --- a/apps/desktop/electron/update-count.ts +++ b/apps/desktop/electron/update-count.ts @@ -17,7 +17,9 @@ function shouldCountCommits({ isShallow, hasMergeBase }) { // (developers / Docker dev images) keep the exact count path unchanged. function resolveBehindCount({ countStr, currentSha, targetSha, isShallow, hasMergeBase }) { if (!shouldCountCommits({ isShallow, hasMergeBase })) { - if (currentSha && targetSha && currentSha === targetSha) {return 0} + if (currentSha && targetSha && currentSha === targetSha) { + return 0 + } return 1 // behind by an unknown amount — show a generic "update available" } diff --git a/apps/desktop/electron/update-marker.test.ts b/apps/desktop/electron/update-marker.test.ts index 5207d7bf4f3..2910366f3f9 100644 --- a/apps/desktop/electron/update-marker.test.ts +++ b/apps/desktop/electron/update-marker.test.ts @@ -18,7 +18,13 @@ import test from 'node:test' import os from 'os' import path from 'path' -import { isPidAlive, markerPath, readLiveUpdateMarker, UPDATE_MARKER_MAX_AGE_MS, writeUpdateMarker } from './update-marker' +import { + isPidAlive, + markerPath, + readLiveUpdateMarker, + UPDATE_MARKER_MAX_AGE_MS, + writeUpdateMarker +} from './update-marker' function tmpHome(tag) { const dir = fs.mkdtempSync(path.join(os.tmpdir(), `hermes-marker-${tag}-`)) @@ -32,9 +38,9 @@ function writeMarker(home, pid, startedAtSec) { const ALIVE: typeof process.kill = () => true // injected kill that "succeeds" => pid alive -const DEAD : typeof process.kill= () => { - const err = new Error('no such process'); - (err as any).code = 'ESRCH' +const DEAD: typeof process.kill = () => { + const err = new Error('no such process') + ;(err as any).code = 'ESRCH' throw err } @@ -86,8 +92,8 @@ test('isPidAlive: own pid is alive, impossible pid is dead', () => { test('isPidAlive: EPERM counts as alive (process owned by another user)', () => { const eperm = () => { - const err = new Error('operation not permitted'); - (err as any).code = 'EPERM' + const err = new Error('operation not permitted') + ;(err as any).code = 'EPERM' throw err } diff --git a/apps/desktop/electron/update-marker.ts b/apps/desktop/electron/update-marker.ts index 6ecb3eb77d2..543fce0451d 100644 --- a/apps/desktop/electron/update-marker.ts +++ b/apps/desktop/electron/update-marker.ts @@ -38,7 +38,9 @@ export function markerPath(hermesHome) { // EPERM => alive but owned by another user (still "alive" for our purposes). // Injectable `kill` keeps it unit-testable. export function isPidAlive(pid, kill: typeof process.kill = process.kill.bind(process)) { - if (!Number.isInteger(pid) || pid <= 0) {return false} + if (!Number.isInteger(pid) || pid <= 0) { + return false + } try { kill(pid, 0) @@ -61,9 +63,18 @@ export function isPidAlive(pid, kill: typeof process.kill = process.kill.bind(pr * Pure-ish: file I/O against the given path, plus an injectable pid probe and * clock for tests. */ -export function readLiveUpdateMarker(hermesHome, { kill, now = Date.now, maxAgeMs = UPDATE_MARKER_MAX_AGE_MS }: { - now?: () => number, maxAgeMs?: number, kill?: typeof process.kill -} = {}) { +export function readLiveUpdateMarker( + hermesHome, + { + kill, + now = Date.now, + maxAgeMs = UPDATE_MARKER_MAX_AGE_MS + }: { + now?: () => number + maxAgeMs?: number + kill?: typeof process.kill + } = {} +) { const file = markerPath(hermesHome) let raw diff --git a/apps/desktop/electron/update-relaunch.test.ts b/apps/desktop/electron/update-relaunch.test.ts index f46e4f1a996..d42582552d0 100644 --- a/apps/desktop/electron/update-relaunch.test.ts +++ b/apps/desktop/electron/update-relaunch.test.ts @@ -24,7 +24,8 @@ import os from 'node:os' import path from 'node:path' import test from 'node:test' -import { buildRelaunchScript, +import { + buildRelaunchScript, collectRelaunchArgs, collectRelaunchEnv, decideRelaunchOutcome, @@ -32,7 +33,8 @@ import { buildRelaunchScript, sandboxFallbackFromEnv, sandboxPreflight, shellQuote, - unpackedDirName } from './update-relaunch' + unpackedDirName +} from './update-relaunch' const ROOT = '/home/u/.hermes/hermes-agent' const UNPACKED = path.join(ROOT, 'apps', 'desktop', 'release', 'linux-unpacked') diff --git a/apps/desktop/electron/update-relaunch.ts b/apps/desktop/electron/update-relaunch.ts index 0db62c95615..cb1c0955768 100644 --- a/apps/desktop/electron/update-relaunch.ts +++ b/apps/desktop/electron/update-relaunch.ts @@ -39,9 +39,13 @@ import path from 'node:path' // Map process.platform → electron-builder's `release/-unpacked` name. function unpackedDirName(platform) { - if (platform === 'darwin') {return 'mac-unpacked'} // not used (mac swaps bundles) + if (platform === 'darwin') { + return 'mac-unpacked' + } // not used (mac swaps bundles) - if (platform === 'win32') {return 'win-unpacked'} + if (platform === 'win32') { + return 'win-unpacked' + } return 'linux-unpacked' } @@ -56,7 +60,9 @@ function unpackedDirName(platform) { * `.../release/linux-unpacked-evil` can't masquerade as `.../release/linux-unpacked`. */ function resolveUnpackedRelease(execPath, updateRoot, platform) { - if (!execPath || !updateRoot) {return null} + if (!execPath || !updateRoot) { + return null + } const releaseDir = path.join(updateRoot, 'apps', 'desktop', 'release') const unpacked = path.join(releaseDir, unpackedDirName(platform)) const normalizedExec = path.resolve(String(execPath)) @@ -83,9 +89,13 @@ function resolveUnpackedRelease(execPath, updateRoot, platform) { * app. Closeable manual-restart terminal state. */ function decideRelaunchOutcome({ underUnpacked, sandboxOk }) { - if (!underUnpacked) {return 'guiSkew'} + if (!underUnpacked) { + return 'guiSkew' + } - if (!sandboxOk) {return 'manual'} + if (!sandboxOk) { + return 'manual' + } return 'relaunch' } @@ -103,7 +113,9 @@ function decideRelaunchOutcome({ underUnpacked, sandboxOk }) { * `statSync` is injectable so this is testable without a real setuid file. */ function sandboxPreflight(unpackedDir, statSync) { - if (!unpackedDir) {return { ok: false, reason: 'no-unpacked-dir', path: null }} + if (!unpackedDir) { + return { ok: false, reason: 'no-unpacked-dir', path: null } + } const sandboxPath = path.join(unpackedDir, 'chrome-sandbox') let st @@ -126,7 +138,9 @@ function sandboxPreflight(unpackedDir, statSync) { return { ok: false, reason: 'not-root-not-setuid', path: sandboxPath } } - if (!ownedByRoot) {return { ok: false, reason: 'not-root', path: sandboxPath }} + if (!ownedByRoot) { + return { ok: false, reason: 'not-root', path: sandboxPath } + } return { ok: false, reason: 'not-setuid', path: sandboxPath } } @@ -148,9 +162,13 @@ function sandboxPreflight(unpackedDir, statSync) { function sandboxFallbackFromEnv(env, launchArgs) { const disable = String((env && env.ELECTRON_DISABLE_SANDBOX) || '').trim() - if (disable === '1' || disable.toLowerCase() === 'true') {return true} + if (disable === '1' || disable.toLowerCase() === 'true') { + return true + } - if (Array.isArray(launchArgs) && launchArgs.some(a => a === '--no-sandbox')) {return true} + if (Array.isArray(launchArgs) && launchArgs.some(a => a === '--no-sandbox')) { + return true + } return false } @@ -189,10 +207,14 @@ const INTERNAL_ARG_PREFIXES = [ * the exec path itself; there is no entry-script arg as in a dev run). */ function collectRelaunchArgs(argv) { - if (!Array.isArray(argv)) {return []} + if (!Array.isArray(argv)) { + return [] + } return argv.filter(arg => { - if (typeof arg !== 'string' || arg.length === 0) {return false} + if (typeof arg !== 'string' || arg.length === 0) { + return false + } return !INTERNAL_ARG_PREFIXES.some(prefix => prefix.endsWith('=') ? arg.startsWith(prefix) : arg === prefix || arg.startsWith(prefix + '=') @@ -213,10 +235,14 @@ const PRESERVED_ENV_PREFIXES = ['HERMES_DESKTOP_'] function collectRelaunchEnv(env) { const out = {} - if (!env || typeof env !== 'object') {return out} + if (!env || typeof env !== 'object') { + return out + } for (const [key, value] of Object.entries(env)) { - if (value == null) {continue} + if (value == null) { + continue + } if (PRESERVED_ENV_KEYS.includes(key) || PRESERVED_ENV_PREFIXES.some(p => key.startsWith(p))) { out[key] = String(value) @@ -270,7 +296,8 @@ exec ${shellQuote(execPath)}${quotedArgs ? ' ' + quotedArgs : ''} ` } -export { buildRelaunchScript, +export { + buildRelaunchScript, collectRelaunchArgs, collectRelaunchEnv, decideRelaunchOutcome, @@ -281,4 +308,5 @@ export { buildRelaunchScript, sandboxFallbackFromEnv, sandboxPreflight, shellQuote, - unpackedDirName } + unpackedDirName +} diff --git a/apps/desktop/electron/update-remote.test.ts b/apps/desktop/electron/update-remote.test.ts index c4e468a285b..9244b363f29 100644 --- a/apps/desktop/electron/update-remote.test.ts +++ b/apps/desktop/electron/update-remote.test.ts @@ -18,11 +18,13 @@ import assert from 'node:assert/strict' import test from 'node:test' -import { canonicalGitHubRemote, +import { + canonicalGitHubRemote, isOfficialSshRemote, isSshRemote, OFFICIAL_REPO_CANONICAL, - OFFICIAL_REPO_HTTPS_URL } from './update-remote' + OFFICIAL_REPO_HTTPS_URL +} from './update-remote' test('canonicalGitHubRemote normalizes SSH and HTTPS forms to the same value', () => { assert.equal(canonicalGitHubRemote('git@github.com:NousResearch/hermes-agent.git'), OFFICIAL_REPO_CANONICAL) diff --git a/apps/desktop/electron/update-remote.ts b/apps/desktop/electron/update-remote.ts index b6b17ab0a7a..c1f4b827420 100644 --- a/apps/desktop/electron/update-remote.ts +++ b/apps/desktop/electron/update-remote.ts @@ -19,7 +19,9 @@ const OFFICIAL_REPO_CANONICAL = 'github.com/nousresearch/hermes-agent' // no trailing slash, no .git suffix) so SSH and HTTPS forms of the same repo // compare equal. function canonicalGitHubRemote(url) { - if (!url) {return ''} + if (!url) { + return '' + } let value = String(url).trim() if (value.startsWith('git@github.com:')) { @@ -30,7 +32,9 @@ function canonicalGitHubRemote(url) { try { const parsed = new URL(value) - if (parsed.hostname && parsed.pathname) {value = `${parsed.hostname}${parsed.pathname}`} + if (parsed.hostname && parsed.pathname) { + value = `${parsed.hostname}${parsed.pathname}` + } } catch { // Leave non-URL forms unchanged. } @@ -38,7 +42,9 @@ function canonicalGitHubRemote(url) { value = value.trim().replace(/\/+$/, '') - if (value.endsWith('.git')) {value = value.slice(0, -4)} + if (value.endsWith('.git')) { + value = value.slice(0, -4) + } return value.toLowerCase() } @@ -55,8 +61,4 @@ function isOfficialSshRemote(url) { return isSshRemote(url) && canonicalGitHubRemote(url) === OFFICIAL_REPO_CANONICAL } -export { canonicalGitHubRemote, - isOfficialSshRemote, - isSshRemote, - OFFICIAL_REPO_CANONICAL, - OFFICIAL_REPO_HTTPS_URL } +export { canonicalGitHubRemote, isOfficialSshRemote, isSshRemote, OFFICIAL_REPO_CANONICAL, OFFICIAL_REPO_HTTPS_URL } diff --git a/apps/desktop/electron/vscode-marketplace.ts b/apps/desktop/electron/vscode-marketplace.ts index 4ad72d343ad..3fe737dc9cb 100644 --- a/apps/desktop/electron/vscode-marketplace.ts +++ b/apps/desktop/electron/vscode-marketplace.ts @@ -334,10 +334,4 @@ async function fetchMarketplaceThemes(id) { const __testing = { themeEntryName, looksLikeIconTheme } -export { - __testing, - extractThemes, - fetchMarketplaceThemes, - readCentralDirectory, - searchMarketplaceThemes -} +export { __testing, extractThemes, fetchMarketplaceThemes, readCentralDirectory, searchMarketplaceThemes } diff --git a/apps/desktop/electron/window-state.test.ts b/apps/desktop/electron/window-state.test.ts index 83765270675..25a4fcaa119 100644 --- a/apps/desktop/electron/window-state.test.ts +++ b/apps/desktop/electron/window-state.test.ts @@ -7,14 +7,16 @@ import assert from 'node:assert/strict' import test from 'node:test' -import { computeWindowOptions, +import { + computeWindowOptions, debounce, DEFAULT_HEIGHT, DEFAULT_WIDTH, MIN_HEIGHT, MIN_WIDTH, onScreen, - sanitizeWindowState } from './window-state' + sanitizeWindowState +} from './window-state' // A single 1920×1080 monitor (work area trimmed for the taskbar). const PRIMARY = [{ workArea: { x: 0, y: 0, width: 1920, height: 1040 } }] diff --git a/apps/desktop/electron/window-state.ts b/apps/desktop/electron/window-state.ts index 919d3274547..d443c6aaf18 100644 --- a/apps/desktop/electron/window-state.ts +++ b/apps/desktop/electron/window-state.ts @@ -21,26 +21,29 @@ const MIN_VISIBLE = 48 const finite = v => typeof v === 'number' && Number.isFinite(v) const clamp = (v, lo, hi) => Math.max(lo, Math.min(v, hi)) -interface SanitizedWindowState{ - width: number, height: number, isMaximized: boolean, x?: number,y?: number +interface SanitizedWindowState { + width: number + height: number + isMaximized: boolean + x?: number + y?: number } // Parse raw JSON → clean state, or null if garbage. width/height are required // and floored; x/y survive only as a finite pair; isMaximized is strict. -function sanitizeWindowState(raw?: any): SanitizedWindowState | null - - - { - if (!raw || typeof raw !== 'object' || !finite(raw.width) || !finite(raw.height)) {return null} +function sanitizeWindowState(raw?: any): SanitizedWindowState | null { + if (!raw || typeof raw !== 'object' || !finite(raw.width) || !finite(raw.height)) { + return null + } const state: SanitizedWindowState = { width: Math.max(MIN_WIDTH, Math.round(raw.width)), height: Math.max(MIN_HEIGHT, Math.round(raw.height)), - isMaximized: raw.isMaximized === true, + isMaximized: raw.isMaximized === true } if (finite(raw.x) && finite(raw.y)) { - state.x = Math.round(raw.x); + state.x = Math.round(raw.x) state.y = Math.round(raw.y) } @@ -50,10 +53,14 @@ function sanitizeWindowState(raw?: any): SanitizedWindowState | null // True when `bounds` overlaps some display's work area by ≥ MIN_VISIBLE on both // axes. `displays` is Electron's screen.getAllDisplays() shape. function onScreen(bounds, displays) { - if (!Array.isArray(displays)) {return false} + if (!Array.isArray(displays)) { + return false + } return displays.some(({ workArea: a } = {}) => { - if (!a) {return false} + if (!a) { + return false + } const x = Math.min(bounds.x + bounds.width, a.x + a.width) - Math.max(bounds.x, a.x) const y = Math.min(bounds.y + bounds.height, a.y + a.height) - Math.max(bounds.y, a.y) @@ -97,7 +104,7 @@ function computeWindowOptions(state, displays): WindowOptions { finite(state.y) && onScreen({ x: state.x, y: state.y, width: opts.width, height: opts.height }, displays) ) { - opts.x = state.x; + opts.x = state.x opts.y = state.y } @@ -127,7 +134,8 @@ function debounce(fn, delayMs) { return debounced } -export { computeWindowOptions, +export { + computeWindowOptions, debounce, DEFAULT_HEIGHT, DEFAULT_WIDTH, @@ -135,4 +143,5 @@ export { computeWindowOptions, MIN_VISIBLE, MIN_WIDTH, onScreen, - sanitizeWindowState } + sanitizeWindowState +} diff --git a/apps/desktop/electron/windows-child-process.test.ts b/apps/desktop/electron/windows-child-process.test.ts index d1b4242f76c..f1d72dedc97 100644 --- a/apps/desktop/electron/windows-child-process.test.ts +++ b/apps/desktop/electron/windows-child-process.test.ts @@ -9,7 +9,6 @@ const ELECTRON_DIR = path.dirname(fileURLToPath(import.meta.url)) // TODO FIXME these tests all grep source code for specific things. This is an antipattern. // Tests should NEVER read src, only assert behavior. - function readElectronFile(name) { return fs.readFileSync(path.join(ELECTRON_DIR, name), 'utf8').replace(/\r\n/g, '\n') } diff --git a/apps/desktop/electron/windows-user-env.ts b/apps/desktop/electron/windows-user-env.ts index a6bc99b0830..fa91590be41 100644 --- a/apps/desktop/electron/windows-user-env.ts +++ b/apps/desktop/electron/windows-user-env.ts @@ -20,7 +20,9 @@ import { execFileSync } from 'node:child_process' // Returns the raw value string (spaces inside the value preserved), or null when // the requested value line isn't present. function parseRegQueryValue(stdout, name) { - if (!stdout || !name) {return null} + if (!stdout || !name) { + return null + } const typePattern = /^(\S+)\s+(?:REG_SZ|REG_EXPAND_SZ|REG_MULTI_SZ|REG_DWORD|REG_QWORD|REG_BINARY|REG_NONE)\s+(.*)$/ for (const rawLine of String(stdout).split(/\r?\n/)) { @@ -39,7 +41,9 @@ function parseRegQueryValue(stdout, name) { // unexpanded references; plain REG_SZ paths have none, so this is a no-op for // the common F:\... case. Unknown references are left verbatim. function expandWindowsEnvRefs(value, env = process.env) { - if (!value) {return value} + if (!value) { + return value + } return value.replace(/%([^%]+)%/g, (whole, name) => { const key = Object.keys(env).find(k => k.toUpperCase() === String(name).toUpperCase()) @@ -51,10 +55,21 @@ function expandWindowsEnvRefs(value, env = process.env) { // Read a User-scoped env var from HKCU\Environment. Windows-only: returns null // off-Windows (without spawning), on any spawn error, when `reg` exits non-zero // (the value doesn't exist), or when the value is empty. -function readWindowsUserEnvVar(name, { platform = process.platform, env = process.env, exec = execFileSync }: { - platform?: NodeJS.Platform, env?: NodeJS.ProcessEnv, exec?: typeof execFileSync | ((file?: string, args?: any) => string) -} = {}) { - if (platform !== 'win32' || !name) {return null} +function readWindowsUserEnvVar( + name, + { + platform = process.platform, + env = process.env, + exec = execFileSync + }: { + platform?: NodeJS.Platform + env?: NodeJS.ProcessEnv + exec?: typeof execFileSync | ((file?: string, args?: any) => string) + } = {} +) { + if (platform !== 'win32' || !name) { + return null + } let stdout try { @@ -70,12 +85,12 @@ function readWindowsUserEnvVar(name, { platform = process.platform, env = proces const raw = parseRegQueryValue(stdout, name) - if (raw == null) {return null} + if (raw == null) { + return null + } const expanded = expandWindowsEnvRefs(raw, env).trim() return expanded || null } -export { expandWindowsEnvRefs, - parseRegQueryValue, - readWindowsUserEnvVar } +export { expandWindowsEnvRefs, parseRegQueryValue, readWindowsUserEnvVar } diff --git a/apps/desktop/electron/workspace-cwd.ts b/apps/desktop/electron/workspace-cwd.ts index 332ac92dca6..79ea87bee43 100644 --- a/apps/desktop/electron/workspace-cwd.ts +++ b/apps/desktop/electron/workspace-cwd.ts @@ -1,7 +1,7 @@ import path from 'node:path' /** True when `dir` lives inside a packaged app bundle / install tree. */ -function isPackagedInstallPath(dir, { installRoots, isPackaged }: { installRoots: string[], isPackaged:boolean }) { +function isPackagedInstallPath(dir, { installRoots, isPackaged }: { installRoots: string[]; isPackaged: boolean }) { if (!isPackaged || !dir) { return false } diff --git a/apps/desktop/electron/wsl-clipboard-image.test.ts b/apps/desktop/electron/wsl-clipboard-image.test.ts index da3481ffa17..244db40f2a9 100644 --- a/apps/desktop/electron/wsl-clipboard-image.test.ts +++ b/apps/desktop/electron/wsl-clipboard-image.test.ts @@ -1,10 +1,12 @@ import assert from 'node:assert/strict' import test from 'node:test' -import { decodeClipboardImageBase64, +import { + decodeClipboardImageBase64, encodePowerShellCommand, powershellCandidates, - readWslWindowsClipboardImage } from './wsl-clipboard-image' + readWslWindowsClipboardImage +} from './wsl-clipboard-image' const PNG_SIGNATURE = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]) @@ -66,11 +68,11 @@ test('readWslWindowsClipboardImage decodes the first candidate that returns a PN test('readWslWindowsClipboardImage returns null and stops when stdout is empty (no image)', () => { let count = 0 - const exec =(() => { + const exec = (() => { count += 1 - return '' - } )as any + return '' + }) as any const result = readWslWindowsClipboardImage({ exec, diff --git a/apps/desktop/electron/wsl-clipboard-image.ts b/apps/desktop/electron/wsl-clipboard-image.ts index 23fe45e8b15..2859f389c02 100644 --- a/apps/desktop/electron/wsl-clipboard-image.ts +++ b/apps/desktop/electron/wsl-clipboard-image.ts @@ -34,7 +34,9 @@ function powershellCandidates() { function decodeClipboardImageBase64(stdout) { const b64 = String(stdout || '').trim() - if (!b64) {return null} + if (!b64) { + return null + } let buffer @@ -57,7 +59,10 @@ function decodeClipboardImageBase64(stdout) { // Read the Windows clipboard image from inside WSL. Returns a PNG Buffer, or // null when there's no image, PowerShell is unreachable, or output is invalid. // Linux-only by contract (caller gates on IS_WSL); never throws. -function readWslWindowsClipboardImage({ exec = execFileSync, candidates = powershellCandidates() }: {exec?: typeof execFileSync, candidates?: string[]} = {}) { +function readWslWindowsClipboardImage({ + exec = execFileSync, + candidates = powershellCandidates() +}: { exec?: typeof execFileSync; candidates?: string[] } = {}) { const encoded = encodePowerShellCommand(PS_SCRIPT) for (const ps of candidates) { @@ -78,10 +83,14 @@ function readWslWindowsClipboardImage({ exec = execFileSync, candidates = powers const decoded = decodeClipboardImageBase64(stdout) - if (decoded) {return decoded} + if (decoded) { + return decoded + } // Empty stdout = no image on the clipboard; stop, don't try fallbacks. - if (String(stdout || '').trim() === '') {return null} + if (String(stdout || '').trim() === '') { + return null + } } catch { // This powershell.exe candidate is missing/failed — try the next one. } @@ -90,7 +99,4 @@ function readWslWindowsClipboardImage({ exec = execFileSync, candidates = powers return null } -export { decodeClipboardImageBase64, - encodePowerShellCommand, - powershellCandidates, - readWslWindowsClipboardImage } +export { decodeClipboardImageBase64, encodePowerShellCommand, powershellCandidates, readWslWindowsClipboardImage } diff --git a/apps/desktop/electron/zoom.ts b/apps/desktop/electron/zoom.ts index 1f865cfbfa4..f3518f583d6 100644 --- a/apps/desktop/electron/zoom.ts +++ b/apps/desktop/electron/zoom.ts @@ -13,7 +13,9 @@ const MIN_ZOOM_LEVEL = -9 const MAX_ZOOM_LEVEL = 9 export function clampZoomLevel(value) { - if (!Number.isFinite(value)) {return 0} + if (!Number.isFinite(value)) { + return 0 + } return Math.min(Math.max(value, MIN_ZOOM_LEVEL), MAX_ZOOM_LEVEL) } @@ -23,7 +25,9 @@ export function zoomLevelToPercent(level) { } export function percentToZoomLevel(percent) { - if (!Number.isFinite(percent) || percent <= 0) {return 0} + if (!Number.isFinite(percent) || percent <= 0) { + return 0 + } return clampZoomLevel(Math.log(percent / 100) / Math.log(ZOOM_FACTOR_BASE)) -} \ No newline at end of file +} diff --git a/apps/desktop/src/app/artifacts/index.tsx b/apps/desktop/src/app/artifacts/index.tsx index 5ae0cd42474..70a6a0338ca 100644 --- a/apps/desktop/src/app/artifacts/index.tsx +++ b/apps/desktop/src/app/artifacts/index.tsx @@ -216,7 +216,10 @@ export function ArtifactsView({ setStatusbarItemGroup: _setStatusbarItemGroup, . const titles = [...new Set(artifacts.map(artifact => artifact.sessionTitle).filter(Boolean))].slice(0, 2) - const hints = [...extensions.map(ext => t.common.tryHint(`.${ext}`)), ...titles.map(title => t.common.tryHint(title))] + const hints = [ + ...extensions.map(ext => t.common.tryHint(`.${ext}`)), + ...titles.map(title => t.common.tryHint(title)) + ] return hints.length > 0 ? hints : undefined }, [artifacts, t]) diff --git a/apps/desktop/src/app/skills/hub.tsx b/apps/desktop/src/app/skills/hub.tsx index a7ed3ca8fe5..1af1b672d26 100644 --- a/apps/desktop/src/app/skills/hub.tsx +++ b/apps/desktop/src/app/skills/hub.tsx @@ -323,7 +323,9 @@ export function SkillsHub({ query }: SkillsHubProps) {
{term.length > 0 ? h.resultCount(results.length, null) : h.featured} - {anyFetching && results.length > 0 && {h.searching}} + {anyFetching && results.length > 0 && ( + {h.searching} + )} {hasInstalled && ( diff --git a/apps/desktop/src/app/skills/index.tsx b/apps/desktop/src/app/skills/index.tsx index 41620e2c124..ccbcd5c9b41 100644 --- a/apps/desktop/src/app/skills/index.tsx +++ b/apps/desktop/src/app/skills/index.tsx @@ -492,7 +492,11 @@ export function SkillsView({ setStatusbarItemGroup: _setStatusbarItemGroup, ...p try { await editLearningNode(skillEditor.name, skillDraft) - notify({ kind: 'success', title: t.skills.skillUpdated, message: t.skills.appliesToNewSessions(skillEditor.name) }) + notify({ + kind: 'success', + title: t.skills.skillUpdated, + message: t.skills.appliesToNewSessions(skillEditor.name) + }) setSkillEditor(null) void refreshCapabilities() } catch (err) { @@ -577,7 +581,9 @@ export function SkillsView({ setStatusbarItemGroup: _setStatusbarItemGroup, ...p left={sortButton(skillsSortDesc, () => $skillsSortDesc.set(!$skillsSortDesc.get()))} right={ void disableUnused() }]} + items={[ + { disabled: bulkBusy, label: t.skills.disableUnused, onSelect: () => void disableUnused() } + ]} label={t.skills.tabSkills} toggle={bulkSwitch(allSkillsEnabled)} /> diff --git a/apps/desktop/src/components/ui/copy-button.tsx b/apps/desktop/src/components/ui/copy-button.tsx index ff7663ff94a..cee6edcde88 100644 --- a/apps/desktop/src/components/ui/copy-button.tsx +++ b/apps/desktop/src/components/ui/copy-button.tsx @@ -233,5 +233,11 @@ export function CopyButton({ ) // Only icon-only buttons need a tooltip; the text variant already shows its label. - return appearance === 'icon' ? {button} : button + return appearance === 'icon' ? ( + + {button} + + ) : ( + button + ) } diff --git a/apps/desktop/src/i18n/zh-hant.ts b/apps/desktop/src/i18n/zh-hant.ts index 095bf6e286f..5dbec605e90 100644 --- a/apps/desktop/src/i18n/zh-hant.ts +++ b/apps/desktop/src/i18n/zh-hant.ts @@ -281,7 +281,8 @@ export const zhHant = defineLocale({ toolViewTitle: '工具呼叫顯示', toolViewDesc: '產品模式會隱藏原始工具 payload;技術模式會顯示完整輸入/輸出。', uiScaleTitle: '介面縮放', - uiScaleDesc: (percent: number) => `縮放整個應用程式的文字與介面。也可使用 Cmd/Ctrl 加 +、- 或 0 調整。目前:${percent}%`, + uiScaleDesc: (percent: number) => + `縮放整個應用程式的文字與介面。也可使用 Cmd/Ctrl 加 +、- 或 0 調整。目前:${percent}%`, translucencyTitle: '視窗透明', translucencyDesc: '讓整個視窗透出桌面。僅支援 macOS 與 Windows。', embedsTitle: '內嵌預覽', diff --git a/apps/desktop/src/i18n/zh.ts b/apps/desktop/src/i18n/zh.ts index 411bfcf6ac1..84b9f54e281 100644 --- a/apps/desktop/src/i18n/zh.ts +++ b/apps/desktop/src/i18n/zh.ts @@ -372,7 +372,8 @@ export const zh: Translations = { toolViewTitle: '工具调用显示', toolViewDesc: '产品模式隐藏原始工具数据;技术模式显示完整输入/输出。', uiScaleTitle: '界面缩放', - uiScaleDesc: (percent: number) => `缩放整个应用的文字和界面。也可使用 Cmd/Ctrl 加 +、- 或 0 调整。当前:${percent}%`, + uiScaleDesc: (percent: number) => + `缩放整个应用的文字和界面。也可使用 Cmd/Ctrl 加 +、- 或 0 调整。当前:${percent}%`, translucencyTitle: '窗口透明', translucencyDesc: '让整个窗口透出桌面。仅支持 macOS 和 Windows。', embedsTitle: '内嵌预览',