cleanup(desktop): lint&fmt all
This commit is contained in:
parent
0fac4cd8e9
commit
7c98c65163
15 changed files with 122 additions and 15 deletions
|
|
@ -44,12 +44,14 @@ function appendUniquePathEntries(entries, { delimiter = path.delimiter } = {}) {
|
|||
if (!entry) {
|
||||
continue
|
||||
}
|
||||
|
||||
const parts = Array.isArray(entry) ? entry : String(entry).split(delimiter)
|
||||
|
||||
for (const part of parts) {
|
||||
if (!part || seen.has(part)) {
|
||||
continue
|
||||
}
|
||||
|
||||
seen.add(part)
|
||||
ordered.push(part)
|
||||
}
|
||||
|
|
@ -77,6 +79,7 @@ function normalizeHermesHomeRoot(hermesHome, { pathModule = pathModuleForPlatfor
|
|||
if (!hermesHome) {
|
||||
return hermesHome
|
||||
}
|
||||
|
||||
const resolved = pathModule.resolve(String(hermesHome))
|
||||
const parent = pathModule.dirname(resolved)
|
||||
|
||||
|
|
|
|||
|
|
@ -60,6 +60,7 @@ function waitForDashboardPort(child, timeoutMs = resolvePortAnnounceTimeoutMs())
|
|||
if (done) {
|
||||
return
|
||||
}
|
||||
|
||||
done = true
|
||||
clearTimeout(timer)
|
||||
child.stdout.off('data', onData)
|
||||
|
|
@ -130,12 +131,14 @@ function waitForDashboardReadyFile(readyFile, child, timeoutMs = resolvePortAnno
|
|||
if (done) {
|
||||
return
|
||||
}
|
||||
|
||||
done = true
|
||||
clearTimeout(timer)
|
||||
|
||||
if (interval) {
|
||||
clearInterval(interval)
|
||||
}
|
||||
|
||||
child.off('exit', onExit)
|
||||
child.off('error', onError)
|
||||
}
|
||||
|
|
@ -171,6 +174,7 @@ function waitForDashboardReadyFile(readyFile, child, timeoutMs = resolvePortAnno
|
|||
if (typeof interval.unref === 'function') {
|
||||
interval.unref()
|
||||
}
|
||||
|
||||
check()
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -42,7 +42,6 @@ import { hiddenWindowsChildOptions } from './windows-child-options'
|
|||
|
||||
const IS_WINDOWS = process.platform === 'win32'
|
||||
|
||||
|
||||
const STAMP_COMMIT_RE = /^[0-9a-f]{7,40}$/i
|
||||
|
||||
// Stages flagged needs_user_input=true in the manifest are skipped by the
|
||||
|
|
@ -68,6 +67,7 @@ function resolveLocalInstallScript(sourceRepoRoot) {
|
|||
if (!sourceRepoRoot) {
|
||||
return null
|
||||
}
|
||||
|
||||
const candidate = path.join(sourceRepoRoot, 'scripts', installScriptName())
|
||||
|
||||
try {
|
||||
|
|
@ -91,6 +91,7 @@ function installedAgentInstallScript(hermesHome) {
|
|||
if (!hermesHome) {
|
||||
return null
|
||||
}
|
||||
|
||||
const candidate = path.join(hermesHome, 'hermes-agent', 'scripts', installScriptName())
|
||||
|
||||
try {
|
||||
|
|
@ -420,6 +421,7 @@ function spawnPowerShell(scriptPath, args, { emit, stageName, abortSignal, herme
|
|||
if (abortSignal) {
|
||||
abortSignal.removeEventListener('abort', onAbort)
|
||||
}
|
||||
|
||||
reject(err)
|
||||
})
|
||||
|
||||
|
|
@ -436,6 +438,7 @@ function spawnPowerShell(scriptPath, args, { emit, stageName, abortSignal, herme
|
|||
if (stderrBuf) {
|
||||
emit && emit({ type: 'log', stage: stageName, line: stderrBuf, stream: 'stderr' } as any)
|
||||
}
|
||||
|
||||
resolve({ stdout, stderr, code, signal, killed } as any)
|
||||
})
|
||||
})
|
||||
|
|
@ -512,6 +515,7 @@ function spawnBash(scriptPath, args, { emit, stageName, abortSignal, hermesHome
|
|||
if (abortSignal) {
|
||||
abortSignal.removeEventListener('abort', onAbort)
|
||||
}
|
||||
|
||||
reject(err)
|
||||
})
|
||||
|
||||
|
|
@ -527,6 +531,7 @@ function spawnBash(scriptPath, args, { emit, stageName, abortSignal, hermesHome
|
|||
if (stderrBuf) {
|
||||
emit && emit({ type: 'log', stage: stageName, line: stderrBuf, stream: 'stderr' })
|
||||
}
|
||||
|
||||
resolve({ stdout, stderr, code, signal, killed })
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -106,6 +106,7 @@ function resolveRemovableAppPath(execPath, platform, env: any = {}) {
|
|||
if (env.APPIMAGE) {
|
||||
return env.APPIMAGE
|
||||
}
|
||||
|
||||
// Unpacked electron-builder tree: …/linux-unpacked/hermes
|
||||
const dir = p.dirname(exe)
|
||||
|
||||
|
|
|
|||
|
|
@ -80,6 +80,7 @@ function probeGatewayWebSocket<T>(
|
|||
if (settled) {
|
||||
return
|
||||
}
|
||||
|
||||
settled = true
|
||||
clearTimers()
|
||||
|
||||
|
|
@ -107,6 +108,7 @@ function probeGatewayWebSocket<T>(
|
|||
if (settled) {
|
||||
return
|
||||
}
|
||||
|
||||
opened = true
|
||||
// Upgrade accepted. Give the server a brief window to reject the
|
||||
// credential post-handshake (early close) before declaring success.
|
||||
|
|
@ -189,6 +191,7 @@ function extractErrorReason(event) {
|
|||
if (event instanceof Error) {
|
||||
return event.message
|
||||
}
|
||||
|
||||
const err = event.error || event.message
|
||||
|
||||
if (err instanceof Error) {
|
||||
|
|
|
|||
|
|
@ -112,6 +112,7 @@ function sensitiveFileBlockReason(filePath) {
|
|||
|
||||
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
|
||||
|
|
|
|||
|
|
@ -55,6 +55,7 @@ export function readLinkTitleWindowTitle(window) {
|
|||
if (!window || window.isDestroyed()) {
|
||||
return ''
|
||||
}
|
||||
|
||||
const contents = window.webContents
|
||||
|
||||
if (!contents || contents.isDestroyed()) {
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ import {
|
|||
systemPreferences
|
||||
} from 'electron'
|
||||
import nodePty from 'node-pty'
|
||||
import { decideProfileDeleteAction, profileNameFromDeleteRequest, resolveRouteProfile } from './profile-delete-routing'
|
||||
|
||||
import { stopBackendChild as stopBackendChildImpl } from './backend-child'
|
||||
import { dashboardFallbackArgs, sourceDeclaresServe } from './backend-command'
|
||||
import { buildDesktopBackendEnv, normalizeHermesHomeRoot } from './backend-env'
|
||||
|
|
@ -96,6 +96,7 @@ import {
|
|||
} from './hardening'
|
||||
import { createLinkTitleWindow, guardLinkTitleSession, readLinkTitleWindowTitle } from './link-title-window'
|
||||
import { serializeJsonBody, setJsonRequestHeaders } from './oauth-net-request'
|
||||
import { decideProfileDeleteAction, profileNameFromDeleteRequest, resolveRouteProfile } from './profile-delete-routing'
|
||||
import {
|
||||
buildSessionWindowUrl,
|
||||
chatWindowWebPreferences,
|
||||
|
|
@ -126,10 +127,10 @@ import {
|
|||
MIN_WIDTH as WINDOW_MIN_WIDTH
|
||||
} from './window-state'
|
||||
import { hiddenWindowsChildOptions } from './windows-child-options'
|
||||
import { buildPathExtCandidates, chooseUpdaterArgs, resolveVenvHermesCommand } from './windows-hermes-path'
|
||||
import { readWindowsUserEnvVar } from './windows-user-env'
|
||||
import { isPackagedInstallPath as isPackagedInstallPathUnderRoots } from './workspace-cwd'
|
||||
import { readWslWindowsClipboardImage } from './wsl-clipboard-image'
|
||||
import { buildPathExtCandidates, chooseUpdaterArgs, resolveVenvHermesCommand } from './windows-hermes-path'
|
||||
|
||||
const USER_DATA_OVERRIDE = process.env.HERMES_DESKTOP_USER_DATA_DIR
|
||||
|
||||
|
|
@ -154,7 +155,6 @@ const APP_ROOT = app.getAppPath()
|
|||
// Dev (`npm run dev`) and prod both load the esbuild output from dist/.
|
||||
const PRELOAD_PATH = path.join(APP_ROOT, 'dist', 'electron-preload.js')
|
||||
|
||||
|
||||
// Remote displays (SSH X11 forwarding, VNC, RDP) make Chromium's GPU
|
||||
// compositor flicker — accelerated layers can't be presented cleanly over the
|
||||
// wire, so the window flashes during scroll/streaming/animation. Local
|
||||
|
|
@ -865,6 +865,7 @@ function planDesktopLogRotation(size) {
|
|||
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.
|
||||
|
|
@ -932,6 +933,7 @@ function flushDesktopLogBufferSync() {
|
|||
if (!desktopLogBuffer) {
|
||||
return
|
||||
}
|
||||
|
||||
const chunk = desktopLogBuffer
|
||||
desktopLogBuffer = ''
|
||||
|
||||
|
|
@ -948,6 +950,7 @@ function flushDesktopLogBufferAsync() {
|
|||
if (!desktopLogBuffer) {
|
||||
return desktopLogFlushPromise
|
||||
}
|
||||
|
||||
const chunk = desktopLogBuffer
|
||||
desktopLogBuffer = ''
|
||||
|
||||
|
|
@ -968,6 +971,7 @@ function scheduleDesktopLogFlush() {
|
|||
if (desktopLogFlushTimer) {
|
||||
return
|
||||
}
|
||||
|
||||
desktopLogFlushTimer = setTimeout(() => {
|
||||
desktopLogFlushTimer = null
|
||||
void flushDesktopLogBufferAsync()
|
||||
|
|
@ -980,6 +984,7 @@ function rememberLog(chunk) {
|
|||
if (!text) {
|
||||
return
|
||||
}
|
||||
|
||||
const lines = text.split(/\r?\n/).map(line => `[hermes] ${line}`)
|
||||
hermesLog.push(...lines)
|
||||
|
||||
|
|
@ -1178,11 +1183,13 @@ function broadcastBootProgress() {
|
|||
if (!mainWindow || mainWindow.isDestroyed()) {
|
||||
return
|
||||
}
|
||||
|
||||
const { webContents } = mainWindow
|
||||
|
||||
if (!webContents || webContents.isDestroyed()) {
|
||||
return
|
||||
}
|
||||
|
||||
webContents.send('hermes:boot-progress', bootProgressState)
|
||||
}
|
||||
|
||||
|
|
@ -1262,11 +1269,13 @@ function broadcastBootstrapEvent(ev) {
|
|||
if (!mainWindow || mainWindow.isDestroyed()) {
|
||||
return
|
||||
}
|
||||
|
||||
const { webContents } = mainWindow
|
||||
|
||||
if (!webContents || webContents.isDestroyed()) {
|
||||
return
|
||||
}
|
||||
|
||||
webContents.send('hermes:bootstrap:event', ev)
|
||||
}
|
||||
|
||||
|
|
@ -1469,6 +1478,7 @@ function backendSupportsServe(backend) {
|
|||
if (!backend || !backend.command) {
|
||||
return true
|
||||
}
|
||||
|
||||
const key = `${backend.command}::${backend.root || ''}`
|
||||
|
||||
if (_serveSupportCache.has(key)) {
|
||||
|
|
@ -2269,6 +2279,7 @@ function isShimLocked(shimPath) {
|
|||
if (!IS_WINDOWS) {
|
||||
return false
|
||||
}
|
||||
|
||||
let fd
|
||||
|
||||
try {
|
||||
|
|
@ -2406,6 +2417,7 @@ async function releaseBackendLock(updateRoot, tag) {
|
|||
for (const pid of stragglers) {
|
||||
forceKillProcessTree(pid)
|
||||
}
|
||||
|
||||
await new Promise(r => setTimeout(r, 300))
|
||||
}
|
||||
|
||||
|
|
@ -2700,6 +2712,7 @@ function runningAppBundle() {
|
|||
if (!IS_MAC) {
|
||||
return null
|
||||
}
|
||||
|
||||
let dir = path.dirname(app.getPath('exe')) // .../Contents/MacOS
|
||||
|
||||
for (let i = 0; i < 2; i++) {
|
||||
|
|
@ -3126,6 +3139,7 @@ function resolveRendererIndex() {
|
|||
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.
|
||||
|
|
@ -3174,6 +3188,7 @@ function resolveHermesCwd() {
|
|||
if (!candidate) {
|
||||
continue
|
||||
}
|
||||
|
||||
const resolved = path.resolve(String(candidate))
|
||||
|
||||
if (isPackagedInstallPath(resolved)) {
|
||||
|
|
@ -3698,6 +3713,7 @@ function fetchJson(url, token, options: any = {}) {
|
|||
if (body) {
|
||||
req.write(body)
|
||||
}
|
||||
|
||||
req.end()
|
||||
})
|
||||
}
|
||||
|
|
@ -3787,6 +3803,7 @@ function fetchPublicJson(url, options: any = {}) {
|
|||
if (body) {
|
||||
req.write(body)
|
||||
}
|
||||
|
||||
req.end()
|
||||
})
|
||||
}
|
||||
|
|
@ -3904,6 +3921,7 @@ function cacheTitle(key, title) {
|
|||
if (titleCache.size >= TITLE_CACHE_LIMIT) {
|
||||
titleCache.delete(titleCache.keys().next().value)
|
||||
}
|
||||
|
||||
titleCache.set(key, title)
|
||||
}
|
||||
|
||||
|
|
@ -3958,6 +3976,7 @@ function fetchHtmlTitleWithCurl(rawUrl: string): Promise<string> {
|
|||
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
|
||||
|
|
@ -3970,6 +3989,7 @@ function fetchHtmlTitleWithCurl(rawUrl: string): Promise<string> {
|
|||
if (!chunks.length) {
|
||||
return resolve('')
|
||||
}
|
||||
|
||||
resolve(parseHtmlTitle(Buffer.concat(chunks).toString('utf8')))
|
||||
})
|
||||
})
|
||||
|
|
@ -3979,6 +3999,7 @@ function getLinkTitleSession() {
|
|||
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) })
|
||||
|
|
@ -4021,6 +4042,7 @@ function runRenderTitleJob(rawUrl) {
|
|||
if (settled) {
|
||||
return
|
||||
}
|
||||
|
||||
settled = true
|
||||
|
||||
if (hardTimer) {
|
||||
|
|
@ -4030,6 +4052,7 @@ function runRenderTitleJob(rawUrl) {
|
|||
if (graceTimer) {
|
||||
clearTimeout(graceTimer)
|
||||
}
|
||||
|
||||
const value = (title || '').replace(/\s+/g, ' ').trim()
|
||||
|
||||
try {
|
||||
|
|
@ -4055,6 +4078,7 @@ function runRenderTitleJob(rawUrl) {
|
|||
if (graceTimer) {
|
||||
clearTimeout(graceTimer)
|
||||
}
|
||||
|
||||
graceTimer = setTimeout(finishWithTitle, RENDER_TITLE_GRACE_MS)
|
||||
}
|
||||
|
||||
|
|
@ -4136,6 +4160,7 @@ async function resourceBufferFromUrl(rawUrl) {
|
|||
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')
|
||||
|
|
@ -4184,6 +4209,7 @@ async function copyImageFromUrl(rawUrl) {
|
|||
if (image.isEmpty()) {
|
||||
throw new Error('Could not read image')
|
||||
}
|
||||
|
||||
clipboard.writeImage(image)
|
||||
}
|
||||
|
||||
|
|
@ -4199,6 +4225,7 @@ async function saveImageFromUrl(rawUrl) {
|
|||
if (result.canceled || !result.filePath) {
|
||||
return false
|
||||
}
|
||||
|
||||
await fs.promises.writeFile(result.filePath, buffer)
|
||||
|
||||
return true
|
||||
|
|
@ -4333,11 +4360,13 @@ function sendPreviewFileChanged(payload) {
|
|||
if (!mainWindow || mainWindow.isDestroyed()) {
|
||||
return
|
||||
}
|
||||
|
||||
const { webContents } = mainWindow
|
||||
|
||||
if (!webContents || webContents.isDestroyed()) {
|
||||
return
|
||||
}
|
||||
|
||||
webContents.send('hermes:preview-file-changed', payload)
|
||||
}
|
||||
|
||||
|
|
@ -4358,12 +4387,14 @@ async function watchPreviewFile(rawUrl) {
|
|||
if (timer) {
|
||||
clearTimeout(timer)
|
||||
}
|
||||
|
||||
timer = setTimeout(() => {
|
||||
timer = null
|
||||
|
||||
if (!fileExists(filePath)) {
|
||||
return
|
||||
}
|
||||
|
||||
sendPreviewFileChanged({ id, path: filePath, url: pathToFileURL(filePath).toString() })
|
||||
}, PREVIEW_WATCH_DEBOUNCE_MS)
|
||||
})
|
||||
|
|
@ -4373,6 +4404,7 @@ async function watchPreviewFile(rawUrl) {
|
|||
if (timer) {
|
||||
clearTimeout(timer)
|
||||
}
|
||||
|
||||
watcher.close()
|
||||
}
|
||||
})
|
||||
|
|
@ -4447,11 +4479,13 @@ function sendBackendExit(payload) {
|
|||
if (!mainWindow || mainWindow.isDestroyed()) {
|
||||
return
|
||||
}
|
||||
|
||||
const { webContents } = mainWindow
|
||||
|
||||
if (!webContents || webContents.isDestroyed()) {
|
||||
return
|
||||
}
|
||||
|
||||
webContents.send('hermes:backend-exit', payload)
|
||||
}
|
||||
|
||||
|
|
@ -4459,11 +4493,13 @@ function sendClosePreviewRequested() {
|
|||
if (!mainWindow || mainWindow.isDestroyed()) {
|
||||
return
|
||||
}
|
||||
|
||||
const { webContents } = mainWindow
|
||||
|
||||
if (!webContents || webContents.isDestroyed()) {
|
||||
return
|
||||
}
|
||||
|
||||
webContents.send('hermes:close-preview-requested')
|
||||
}
|
||||
|
||||
|
|
@ -4474,11 +4510,13 @@ function sendPowerResume() {
|
|||
if (!mainWindow || mainWindow.isDestroyed()) {
|
||||
return
|
||||
}
|
||||
|
||||
const { webContents } = mainWindow
|
||||
|
||||
if (!webContents || webContents.isDestroyed()) {
|
||||
return
|
||||
}
|
||||
|
||||
webContents.send('hermes:power-resume')
|
||||
}
|
||||
|
||||
|
|
@ -4488,6 +4526,7 @@ function registerPowerResumeListeners() {
|
|||
if (powerResumeRegistered) {
|
||||
return
|
||||
}
|
||||
|
||||
powerResumeRegistered = true
|
||||
|
||||
try {
|
||||
|
|
@ -4509,16 +4548,19 @@ function sendOpenUpdatesRequested() {
|
|||
if (!mainWindow || mainWindow.isDestroyed()) {
|
||||
return
|
||||
}
|
||||
|
||||
const { webContents } = mainWindow
|
||||
|
||||
if (!webContents || webContents.isDestroyed()) {
|
||||
return
|
||||
}
|
||||
|
||||
webContents.send('hermes:open-updates')
|
||||
|
||||
if (!mainWindow.isVisible()) {
|
||||
mainWindow.show()
|
||||
}
|
||||
|
||||
mainWindow.focus()
|
||||
}
|
||||
|
||||
|
|
@ -4526,11 +4568,13 @@ function sendWindowStateChanged(nextIsFullscreen?: boolean) {
|
|||
if (!mainWindow || mainWindow.isDestroyed()) {
|
||||
return
|
||||
}
|
||||
|
||||
const { webContents } = mainWindow
|
||||
|
||||
if (!webContents || webContents.isDestroyed()) {
|
||||
return
|
||||
}
|
||||
|
||||
const state = getWindowState()
|
||||
|
||||
if (typeof nextIsFullscreen === 'boolean') {
|
||||
|
|
@ -4675,6 +4719,7 @@ function installDevToolsShortcut(window) {
|
|||
if (!isInspectShortcut) {
|
||||
return
|
||||
}
|
||||
|
||||
event.preventDefault()
|
||||
toggleDevTools(window)
|
||||
})
|
||||
|
|
@ -4710,6 +4755,7 @@ function setAndPersistZoomLevel(window, zoomLevel) {
|
|||
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
|
||||
|
|
@ -4726,6 +4772,7 @@ function restorePersistedZoomLevel(window) {
|
|||
if (!window || window.isDestroyed()) {
|
||||
return
|
||||
}
|
||||
|
||||
window.webContents
|
||||
.executeJavaScript(
|
||||
`(() => { try { return localStorage.getItem(${JSON.stringify(ZOOM_STORAGE_KEY)}) } catch { return null } })()`
|
||||
|
|
@ -4734,6 +4781,7 @@ function restorePersistedZoomLevel(window) {
|
|||
if (stored == null || !window || window.isDestroyed()) {
|
||||
return
|
||||
}
|
||||
|
||||
const level = clampZoomLevel(Number(stored))
|
||||
window.webContents.setZoomLevel(level)
|
||||
})
|
||||
|
|
@ -4810,6 +4858,7 @@ function installContextMenu(window) {
|
|||
if (template.length) {
|
||||
template.push({ type: 'separator' })
|
||||
}
|
||||
|
||||
template.push(
|
||||
{
|
||||
label: 'Open Link',
|
||||
|
|
@ -4963,6 +5012,7 @@ function getOauthSession() {
|
|||
if (oauthSession || !app.isReady()) {
|
||||
return oauthSession
|
||||
}
|
||||
|
||||
oauthSession = session.fromPartition(OAUTH_SESSION_PARTITION)
|
||||
|
||||
return oauthSession
|
||||
|
|
@ -4978,6 +5028,7 @@ async function hasOauthSessionCookie(baseUrl) {
|
|||
if (!sess) {
|
||||
return false
|
||||
}
|
||||
|
||||
const parsed = new URL(baseUrl)
|
||||
|
||||
try {
|
||||
|
|
@ -5010,6 +5061,7 @@ async function hasLiveOauthSession(baseUrl) {
|
|||
if (!sess) {
|
||||
return false
|
||||
}
|
||||
|
||||
const parsed = new URL(baseUrl)
|
||||
|
||||
try {
|
||||
|
|
@ -5092,6 +5144,7 @@ function openOauthLoginWindow(baseUrl, { silent = false } = {}) {
|
|||
if (settled) {
|
||||
return
|
||||
}
|
||||
|
||||
settled = true
|
||||
|
||||
if (pollTimer) {
|
||||
|
|
@ -5261,6 +5314,7 @@ function fetchJsonViaOauthSession(url, options: any = {}) {
|
|||
if (timedOut) {
|
||||
return
|
||||
}
|
||||
|
||||
clearTimeout(timer)
|
||||
const text = Buffer.concat(chunks).toString('utf8')
|
||||
const statusCode = res.statusCode || 500
|
||||
|
|
@ -5299,6 +5353,7 @@ function fetchJsonViaOauthSession(url, options: any = {}) {
|
|||
if (timedOut) {
|
||||
return
|
||||
}
|
||||
|
||||
clearTimeout(timer)
|
||||
reject(error)
|
||||
})
|
||||
|
|
@ -5306,6 +5361,7 @@ function fetchJsonViaOauthSession(url, options: any = {}) {
|
|||
if (body) {
|
||||
request.write(body)
|
||||
}
|
||||
|
||||
request.end()
|
||||
})
|
||||
}
|
||||
|
|
@ -5711,6 +5767,7 @@ function sanitizeConnectionProfiles(raw: Record<string, any>) {
|
|||
} = {
|
||||
mode: modeIsRemoteLike(entry.mode) ? entry.mode : 'local'
|
||||
}
|
||||
|
||||
const url = String(entry.url || '').trim()
|
||||
|
||||
if (url) {
|
||||
|
|
@ -6400,6 +6457,7 @@ function touchPoolBackend(profile) {
|
|||
if (!key) {
|
||||
return
|
||||
}
|
||||
|
||||
const entry = backendPool.get(key)
|
||||
|
||||
if (entry) {
|
||||
|
|
@ -6415,6 +6473,7 @@ function evictLruPoolBackends(keep) {
|
|||
if (backendPool.size <= keep) {
|
||||
return
|
||||
}
|
||||
|
||||
const now = Date.now()
|
||||
|
||||
const evictable = [...backendPool.entries()]
|
||||
|
|
@ -6427,6 +6486,7 @@ function evictLruPoolBackends(keep) {
|
|||
if (removable <= 0) {
|
||||
break
|
||||
}
|
||||
|
||||
rememberLog(`Evicting idle profile backend "${profile}" (LRU cap ${POOL_MAX_BACKENDS})`)
|
||||
stopPoolBackend(profile)
|
||||
removable -= 1
|
||||
|
|
@ -6437,6 +6497,7 @@ function startPoolIdleReaper() {
|
|||
if (poolIdleReaper) {
|
||||
return
|
||||
}
|
||||
|
||||
poolIdleReaper = setInterval(() => {
|
||||
const now = Date.now()
|
||||
|
||||
|
|
@ -6589,6 +6650,7 @@ function stopPoolBackend(profile) {
|
|||
if (!entry) {
|
||||
return
|
||||
}
|
||||
|
||||
backendPool.delete(profile)
|
||||
stopBackendChild(entry.process)
|
||||
}
|
||||
|
|
@ -6599,6 +6661,7 @@ async function teardownPoolBackendAndWait(profile) {
|
|||
if (!entry) {
|
||||
return
|
||||
}
|
||||
|
||||
backendPool.delete(profile)
|
||||
|
||||
stopBackendChild(entry.process)
|
||||
|
|
@ -6623,6 +6686,7 @@ function stopAllPoolBackends() {
|
|||
// that decision calls for.
|
||||
async function prepareProfileDeleteRequest(request) {
|
||||
const profile = profileNameFromDeleteRequest(request)
|
||||
|
||||
const decision = decideProfileDeleteAction(profile, {
|
||||
isDefaultProfile: p => p === 'default',
|
||||
isValidProfileName: p => PROFILE_NAME_RE.test(p),
|
||||
|
|
@ -6923,6 +6987,7 @@ function focusWindow(win) {
|
|||
if (!win.isVisible()) {
|
||||
win.show()
|
||||
}
|
||||
|
||||
win.focus()
|
||||
}
|
||||
|
||||
|
|
@ -7360,6 +7425,7 @@ ipcMain.on('hermes:zoom:set-percent', (event, percent) => {
|
|||
if (!window || window.isDestroyed()) {
|
||||
return
|
||||
}
|
||||
|
||||
setAndPersistZoomLevel(window, percentToZoomLevel(Number(percent)))
|
||||
})
|
||||
|
||||
|
|
@ -7861,6 +7927,7 @@ ipcMain.handle('hermes:notify', (_event, payload) => {
|
|||
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 : []
|
||||
|
|
@ -7876,6 +7943,7 @@ ipcMain.handle('hermes:notify', (_event, payload) => {
|
|||
if (!mainWindow || mainWindow.isDestroyed()) {
|
||||
return
|
||||
}
|
||||
|
||||
focusWindow(mainWindow)
|
||||
|
||||
if (payload?.sessionId) {
|
||||
|
|
@ -7886,6 +7954,7 @@ ipcMain.handle('hermes:notify', (_event, payload) => {
|
|||
if (!mainWindow || mainWindow.isDestroyed()) {
|
||||
return
|
||||
}
|
||||
|
||||
const action = actions[index]
|
||||
|
||||
if (action?.id) {
|
||||
|
|
@ -8687,6 +8756,7 @@ async function getUninstallSummary() {
|
|||
if (settled) {
|
||||
return
|
||||
}
|
||||
|
||||
settled = true
|
||||
resolve(value)
|
||||
}
|
||||
|
|
@ -8881,6 +8951,7 @@ function handleDeepLink(url) {
|
|||
if (!url || typeof url !== 'string') {
|
||||
return
|
||||
}
|
||||
|
||||
let parsed
|
||||
|
||||
try {
|
||||
|
|
@ -8910,6 +8981,7 @@ function handleDeepLink(url) {
|
|||
if (mainWindow.isMinimized()) {
|
||||
mainWindow.restore()
|
||||
}
|
||||
|
||||
mainWindow.focus()
|
||||
mainWindow.webContents.send('hermes:deep-link', payload)
|
||||
rememberLog(`[deeplink] delivered ${kind}/${name}`)
|
||||
|
|
@ -8966,6 +9038,7 @@ if (!_gotSingleInstanceLock) {
|
|||
if (mainWindow.isMinimized()) {
|
||||
mainWindow.restore()
|
||||
}
|
||||
|
||||
mainWindow.focus()
|
||||
}
|
||||
})
|
||||
|
|
|
|||
|
|
@ -40,6 +40,7 @@ const ALIVE: typeof process.kill = () => true // injected kill that "succeeds" =
|
|||
|
||||
const DEAD: typeof process.kill = () => {
|
||||
const err = new Error('no such process')
|
||||
|
||||
;(err as any).code = 'ESRCH'
|
||||
throw err
|
||||
}
|
||||
|
|
@ -93,6 +94,7 @@ 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'
|
||||
throw err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -63,6 +63,7 @@ function resolveUnpackedRelease(execPath, updateRoot, platform) {
|
|||
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))
|
||||
|
|
@ -116,6 +117,7 @@ function sandboxPreflight(unpackedDir, statSync) {
|
|||
if (!unpackedDir) {
|
||||
return { ok: false, reason: 'no-unpacked-dir', path: null }
|
||||
}
|
||||
|
||||
const sandboxPath = path.join(unpackedDir, 'chrome-sandbox')
|
||||
let st
|
||||
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ function canonicalGitHubRemote(url) {
|
|||
if (!url) {
|
||||
return ''
|
||||
}
|
||||
|
||||
let value = String(url).trim()
|
||||
|
||||
if (value.startsWith('git@github.com:')) {
|
||||
|
|
|
|||
|
|
@ -61,6 +61,7 @@ function onScreen(bounds, displays) {
|
|||
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)
|
||||
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ function parseRegQueryValue(stdout, name) {
|
|||
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/)) {
|
||||
|
|
@ -70,6 +71,7 @@ function readWindowsUserEnvVar(
|
|||
if (platform !== 'win32' || !name) {
|
||||
return null
|
||||
}
|
||||
|
||||
let stdout
|
||||
|
||||
try {
|
||||
|
|
@ -88,6 +90,7 @@ function readWindowsUserEnvVar(
|
|||
if (raw == null) {
|
||||
return null
|
||||
}
|
||||
|
||||
const expanded = expandWindowsEnvRefs(raw, env).trim()
|
||||
|
||||
return expanded || null
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
||||
import { cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react'
|
||||
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
||||
import { cleanup, findByText, fireEvent, render } from '@testing-library/react'
|
||||
import { cleanup, fireEvent, render } from '@testing-library/react'
|
||||
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { DropdownMenu, DropdownMenuContent } from '@/components/ui/dropdown-menu'
|
||||
|
|
@ -39,7 +39,8 @@ afterEach(() => {
|
|||
|
||||
function renderPanel(onSelectModel = vi.fn()) {
|
||||
const client = new QueryClient({ defaultOptions: { queries: { retry: false } } })
|
||||
render(
|
||||
|
||||
const content = render(
|
||||
<QueryClientProvider client={client}>
|
||||
<DropdownMenu open>
|
||||
<DropdownMenuContent>
|
||||
|
|
@ -49,15 +50,15 @@ function renderPanel(onSelectModel = vi.fn()) {
|
|||
</QueryClientProvider>
|
||||
)
|
||||
|
||||
return onSelectModel
|
||||
return { onSelectModel, content }
|
||||
}
|
||||
|
||||
describe('ModelMenuPanel MoA presets', () => {
|
||||
it('selecting a MoA preset switches PERSISTENTLY via onSelectModel (not the one-shot dispatch)', async () => {
|
||||
const onSelectModel = renderPanel()
|
||||
const { content, onSelectModel } = renderPanel()
|
||||
|
||||
// moaOptions is async (useQuery) — wait for the preset row to mount.
|
||||
const row = await findByText(document.body, 'MoA: BeastMode')
|
||||
const row = await content.findByText('MoA: BeastMode')
|
||||
fireEvent.click(row)
|
||||
|
||||
// #54670: must route through the persistent model-switch path
|
||||
|
|
@ -69,30 +70,35 @@ describe('ModelMenuPanel MoA presets', () => {
|
|||
it('shows the check on the preset that matches the current moa selection', async () => {
|
||||
$currentProvider.set('moa')
|
||||
$currentModel.set('BeastMode')
|
||||
renderPanel()
|
||||
const { content } = renderPanel()
|
||||
|
||||
const row = await findByText(document.body, 'MoA: BeastMode')
|
||||
const row = await content.findByText('MoA: BeastMode')
|
||||
// The check codicon renders as a sibling within the same row item.
|
||||
const item = row.closest('[role="menuitem"]') ?? row.parentElement
|
||||
expect(item?.querySelector('.codicon-check')).not.toBeNull()
|
||||
})
|
||||
|
||||
it('keeps the virtual moa provider out of the main model groups (presets section only)', async () => {
|
||||
renderPanel()
|
||||
const { content } = renderPanel()
|
||||
|
||||
await findByText(document.body, 'MoA: BeastMode')
|
||||
await content.findByText('MoA: BeastMode')
|
||||
|
||||
// The provider group header would read "Mixture of Agents"; the presets
|
||||
// section header reads "MoA presets". Only the latter should exist.
|
||||
// Radix DropdownMenu portals its content to document.body, so assert
|
||||
// against the body (not content.container) to see the rendered items.
|
||||
|
||||
// eslint-disable-next-line no-restricted-globals
|
||||
expect(document.body.textContent).toContain('MoA presets')
|
||||
// eslint-disable-next-line no-restricted-globals
|
||||
expect(document.body.textContent).not.toContain('Mixture of Agents')
|
||||
})
|
||||
|
||||
it('renders presets from the catalog even before a session exists', async () => {
|
||||
$activeSessionId.set('')
|
||||
const onSelectModel = renderPanel()
|
||||
const { onSelectModel, content } = renderPanel()
|
||||
|
||||
const row = await findByText(document.body, 'MoA: BeastMode')
|
||||
const row = await content.findByText('MoA: BeastMode')
|
||||
fireEvent.click(row)
|
||||
|
||||
// Pre-session picks are UI state shipped on the next session.create — the
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue