1
import type { ProductEventMap, ProductEvent } from '@deepseek-ai/dsh-client-product-analytics/types'2
import { WINDOWS_TITLEBAR_HEIGHT } from './windows-layout.ts'3
/** Electron shell: desktop project ownership, custom protocol, windows, and lifecycle. */5
import { readFile, writeFile } from 'node:fs/promises'6
import { join } from 'node:path'7
import { fileURLToPath } from 'node:url'8
import {9
app,10
BrowserWindow,11
clipboard,12
dialog,13
ipcMain,14
Menu,15
powerMonitor,16
nativeImage,17
nativeTheme,18
net,19
protocol,20
session,21
shell,22
type IpcMainInvokeEvent,23
type MenuItemConstructorOptions,24
} from 'electron'25
import { resolveDesktopPaths } from './paths.ts'26
import { DesktopProjectManager } from './project-manager.ts'27
import { DesktopHostFatalError, DesktopHostProcess, DesktopHostUncleanExitError } from './host-process.ts'28
import { DesktopPlatformView, PLATFORM_IPC, platformBounds } from './platform-view.ts'29
import { installDesktopDirectoryPicker } from './directory-picker.ts'30
import { installMicrophonePermissions } from './microphone-permissions.ts'31
import { DesktopBackendController } from './backend-controller.ts'32
import { DESKTOP_IPC, SCHEME, assertDesktopSender, type DesktopUpdateState } from './ipc.ts'33
import { readDeviceInfo } from './device-info.ts'34
import { desktopUpdateReadyConfirmation, formatDesktopMessage, resolveDesktopLocale, resolveDesktopStartupLocale } from './locale.ts'35
import { claimDesktopSingleInstance } from './single-instance.ts'36
import { DesktopUpdateCoordinator } from './update-coordinator.ts'37
import { DesktopCommandManager } from './command-management.ts'38
import { serveWebDocument, authenticateWebHost, forwardWebRequest } from './web-document.ts'39
import { DesktopFatalRecovery } from './fatal-recovery.ts'40
import { pruneCrashReports, RendererConsoleTail, writeCrashReport, type CrashReportSource } from './crash-report.ts'41
import { openWelcomeWindow } from './welcome-window.ts'42
import { WELCOME_IPC, needsWelcome, type WelcomeNotice } from './welcome-api.ts'43
import { connectDesktopWelcome, type DesktopWelcomeBackend } from './welcome-backend.ts'44
import { DesktopUpdateJournal } from './update-journal.ts'45
import { DesktopUpdatePreparationError } from './update-error.ts'46
import { DesktopUpdateSchedule, resolveDesktopUpdateScheduleConfig } from './update-schedule.ts'47
import { desktopUpdateErrorSummary, presentDesktopUpdate } from './update-presentation.ts'48
import { desktopErrorState } from './startup-error.ts'49
import { readDesktopLoginShellEnvironment, resolveDesktopLoginShellConfig } from './login-shell-environment.ts'50
import { DesktopMandatoryUpdatePolicy, resolveDesktopPolicyConfig, type DesktopPolicyState } from './mandatory-update-policy.ts'51
import { desktopClientMetadata, desktopClientVersion } from './client-metadata.ts'52
import { DesktopMandatoryUpdateWindow } from './mandatory-update-window.ts'53
import { DesktopPolicyTestAuth } from './policy-test-auth.ts'54
import { DesktopUpdateDialog, type UpdateDialogOptions } from './update-dialog.ts'55
import { readDesktopRuntime } from './runtime-tree.ts'56
import { DesktopBrowserGuests } from './browser-guests.ts'57
import { installDesktopShortcuts } from './keyboard.ts'58
import { DesktopUpdateOverlays } from './update-overlay.ts'59
import { DesktopQuitConfirmation } from './quit-confirmation.ts'60
import { DesktopTray } from './tray.ts'61
import { DesktopBackgroundNotice } from './background-notice.ts'63
let focusPrimaryWindow = (): void => {}64
let stopForRecovery = async (): Promise<void> => {}65
let shuttingDown = false66
/**67
* Set by quit entries that must not ask: crash recovery exit and restart, and the68
* development restart command. The installer handoff has its own before-quit branch.69
*/70
let skipQuitConfirmation = false71
let windowsLanguage: string | undefined72
/**73
* Whether the backend has reached ready: false until the first ready, back to74
* false when a restart returns it to starting, frozen during shutdown so a75
* failure while tearing down a ready backend still reads as `running`.76
*/77
let backendReady = false78
/** Error-level console output of the primary window, attached to crash reports. */79
const rendererConsole = new RendererConsoleTail()81
// Platform-conventional logs directory (macOS ~/Library/Logs/<name>, otherwise under userData);82
// set before ready so the first fatal report already resolves under it.83
app.setAppLogsPath()85
function currentDesktopLocale(): ReturnType<typeof resolveDesktopLocale> {86
return resolveDesktopLocale(windowsLanguage ?? app.getLocale())87
}88
/** Quit without the task confirmation; the caller has already decided the application must stop. */89
function quitWithoutConfirmation(): void {90
skipQuitConfirmation = true91
app.quit()92
}93
const recovery = new DesktopFatalRecovery({94
messages: () => currentDesktopLocale().messages,95
show: options => dialog.showMessageBox(options),96
stop: () => { shuttingDown = true; return stopForRecovery() },97
disablePlugins: async () => {98
const manager = new DesktopProjectManager(resolveDesktopPaths(), runtimeResources())99
const backupPath = await manager.disableAllPlugins()100
console.info('Desktop profile recovery completed:', { profilePatchBackup: backupPath ?? null, homePatch: 'unchanged' })101
},102
exit: () => { quitWithoutConfirmation() },103
restart: () => { app.relaunch(); quitWithoutConfirmation() },104
writeReport: (error, source) => persistCrashReport(error, source),105
})107
function persistCrashReport(error: unknown, source: CrashReportSource): Promise<string | undefined> {108
return writeCrashReport(app.getPath('logs'), {109
source,110
phase: backendReady ? 'running' : 'startup',111
error,112
...(error instanceof DesktopHostFatalError && error.diagnostic !== undefined ? { hostDiagnostic: error.diagnostic } : {}),113
rendererConsole: rendererConsole.snapshot(),114
app: {115
name: app.name, version: app.getVersion(), platform: process.platform, arch: process.arch,116
electron: process.versions.electron, node: process.versions.node, locale: currentDesktopLocale().id,117
},118
time: new Date(),119
})120
}122
function reportFatal(error: unknown, source: CrashReportSource): void {123
console.error(error)124
if (shuttingDown) {125
// No dialog during shutdown, but the report still records what failed on the way out.126
void persistCrashReport(error, source)127
return128
}129
void recovery.report(error, source).catch((failure: unknown) => { console.error(failure); app.exit(1) })130
}132
protocol.registerSchemesAsPrivileged([{133
scheme: SCHEME,134
privileges: {135
standard: true,136
secure: true,137
supportFetchAPI: true,138
corsEnabled: true,139
stream: true,140
codeCache: true,141
},142
}])144
interface RuntimeResources {145
readonly nodeBin: string146
readonly node: string147
readonly pnpm: string148
readonly dsh: string149
}151
function runtimeResources(): RuntimeResources {152
const development = !app.isPackaged153
const node = process.execPath154
const nodeBin = development ? join(app.getAppPath(), 'scripts', 'node-bin') : join(process.resourcesPath, 'runtime', 'bin')155
const pnpm = (development ? process.env.DSH_DESKTOP_PNPM_ENTRY : undefined)156
?? (development ? join(app.getAppPath(), 'node_modules', 'pnpm', 'bin', 'pnpm.mjs')157
: join(process.resourcesPath, 'runtime', 'pnpm', 'bin', 'pnpm.mjs'))158
const dsh = (development ? process.env.DSH_DESKTOP_DSH_DIR : undefined)159
?? (development ? join(app.getAppPath(), '.desktop-build', 'development', 'project') : join(app.getAppPath(), 'dsh'))160
return { node, nodeBin, pnpm, dsh }161
}163
function developmentPrimaryRuntime(): string {164
const directory = process.env.DSH_DESKTOP_PRIMARY_RUNTIME_DIR165
if (directory === undefined || directory === '') {166
throw new Error('dsh desktop: DSH_DESKTOP_PRIMARY_RUNTIME_DIR is required for an unpackaged launch')167
}168
return directory169
}171
function developmentHostInspectPort(enabled: boolean): number | undefined {172
const configured = process.env.DSH_DESKTOP_HOST_INSPECT_PORT173
if (!enabled || configured === undefined || configured === '') return undefined174
const port = Number(configured)175
if (!Number.isSafeInteger(port) || port < 1 || port > 65_535) {176
throw new Error('dsh desktop: DSH_DESKTOP_HOST_INSPECT_PORT must be an integer from 1 through 65535')177
}178
return port179
}181
/**182
* Opaque chrome fallback matching the built-in sidebar palette (the resolved183
* `--dsw-static-neutral-bluish-900` / `-50` tokens). An approximation for184
* custom themes: Windows swaps in the renderer's measured palette over the185
* windowsAppearance IPC, and macOS shows it only while minimized or hidden.186
* @returns the sidebar fill hex for the active system color scheme.187
*/188
function chromeFallbackFill(): string {189
return nativeTheme.shouldUseDarkColors ? '#1b1b1c' : '#f9fafb'190
}192
/**193
* Add the effective Desktop palette to a Platform authorization URL so the194
* login page opens in the application's theme. `system` resolves through195
* `nativeTheme.shouldUseDarkColors`, which follows the theme source the196
* application preload publishes.197
* @param authorizeUrl - validated Platform authorization URL.198
* @returns the authorization URL carrying `theme=light` or `theme=dark`.199
*/200
function platformLoginUrl(authorizeUrl: string): string {201
const url = new URL(authorizeUrl)202
url.searchParams.set('theme', nativeTheme.shouldUseDarkColors ? 'dark' : 'light')203
return url.href204
}206
function createWindow(preload: string, show = false, primary = false): BrowserWindow {207
const window = new BrowserWindow({208
width: 1280,209
height: 820,210
minWidth: 520,211
minHeight: 600,212
show,213
...(process.platform === 'win32' && primary ? {214
titleBarStyle: 'hidden' as const,215
titleBarOverlay: { height: WINDOWS_TITLEBAR_HEIGHT, color: chromeFallbackFill(),216
symbolColor: nativeTheme.shouldUseDarkColors ? '#f9fafb' : '#0f1115' },217
} : {}),218
// hiddenInset places traffic lights inside the sidebar; sidebar vibrancy219
// needs a transparent window background to show through the page.220
...(process.platform === 'darwin' ? {221
titleBarStyle: 'hiddenInset' as const,222
trafficLightPosition: { x: 16, y: 18 },223
vibrancy: 'sidebar' as const,224
// 'active' keeps the vibrancy material stable when the window blurs;225
// 'followWindow' washes the sidebar out behind an unfocused window.226
visualEffectState: 'active' as const,227
backgroundColor: '#00000000',228
} : {}),229
webPreferences: {230
preload,231
nodeIntegration: false,232
contextIsolation: true,233
sandbox: true,234
webSecurity: true,235
webviewTag: primary,236
devTools: true,237
},238
})239
window.webContents.setWindowOpenHandler(({ url }) => {240
if (['http:', 'https:'].includes(new URL(url).protocol)) void shell.openExternal(url)241
return { action: 'deny' }242
})243
if (process.platform === 'darwin' || process.platform === 'win32') {244
// Fullscreen hides native window controls; overlays drop their caption clearance.245
const sendFullscreen = (): void => {246
if (!window.isDestroyed()) window.webContents.send(DESKTOP_IPC.windowFullscreen, window.isFullScreen())247
}248
window.on('enter-full-screen', sendFullscreen)249
window.on('leave-full-screen', sendFullscreen)250
// Reloads and navigations re-register the preload listener; resend the251
// current state so a fullscreen reload does not fall back to windowed CSS.252
window.webContents.on('did-finish-load', sendFullscreen)253
}254
if (process.platform === 'darwin') {255
// Deminiaturize reattaches the NSVisualEffectView material late256
// (electron/electron#25368), so a transparent window shows the desktop257
// through the sidebar until then. Paint an opaque base while minimized or258
// hidden so the deminiaturize animation and the reattachment gap show a259
// solid fill; restoring flips back, and the null -> 'sidebar' transition260
// forces the material to reattach.261
const applyBackdrop = (): void => {262
if (window.isDestroyed()) return263
if (window.isMinimized() || !window.isVisible()) {264
window.setVibrancy(null)265
window.setBackgroundColor(chromeFallbackFill())266
} else {267
window.setVibrancy('sidebar')268
window.setBackgroundColor('#00000000')269
}270
}271
window.on('minimize', applyBackdrop)272
window.on('hide', applyBackdrop)273
window.on('restore', applyBackdrop)274
window.on('show', applyBackdrop)275
}276
window.webContents.on('context-menu', (_event, { isEditable, selectionText, editFlags }) => {277
const items: MenuItemConstructorOptions[] = []278
if (isEditable) {279
items.push(280
{ role: 'undo', enabled: editFlags.canUndo },281
{ role: 'redo', enabled: editFlags.canRedo },282
{ type: 'separator' },283
{ role: 'cut', enabled: editFlags.canCut },284
{ role: 'copy', enabled: editFlags.canCopy },285
{ role: 'paste', enabled: editFlags.canPaste },286
{ type: 'separator' },287
{ role: 'selectAll', enabled: editFlags.canSelectAll },288
)289
} else if (selectionText.length > 0) {290
items.push({ role: 'copy', enabled: editFlags.canCopy })291
}292
// Empty accelerators suppress Electron's default shortcut labels for native roles.293
if (items.length > 0) {294
const messages = currentDesktopLocale().messages295
Menu.buildFromTemplate(items.map(item => ({296
...item,297
...(process.platform === 'win32' && item.role !== undefined && item.role in messages298
? { label: messages[item.role as keyof typeof messages] } : {}),299
accelerator: '',300
}))).popup({ window })301
}302
})303
window.webContents.on('will-navigate', (event, url) => {304
const destination = new URL(url)305
const current = new URL(window.webContents.getURL())306
if (destination.protocol !== `${SCHEME}:`307
&& !(destination.protocol === 'http:' && destination.origin === current.origin)) {308
event.preventDefault()309
if (['http:', 'https:'].includes(destination.protocol)) void shell.openExternal(url)310
}311
})312
return window313
}315
async function main(): Promise<void> {316
void pruneCrashReports(app.getPath('logs'))317
const journalDirectory = process.env.DSH_DESKTOP_UPDATE_JOURNAL_DIR318
const updateJournal = journalDirectory === undefined ? undefined : new DesktopUpdateJournal(journalDirectory, app.getVersion())319
const resources = runtimeResources()320
const paths = resolveDesktopPaths()321
const development = !app.isPackaged322
const primaryRuntime = development323
? developmentPrimaryRuntime()324
: join(process.resourcesPath, 'runtime', 'primary-runtime')325
const activeProject = paths.profile326
const manager = new DesktopProjectManager(paths, resources)327
// Dock and Finder launches inherit only launchd's environment; every Host shares one login-shell read.328
const loginShellRead = new AbortController()329
// The probe runs in its own process group, which outlives Desktop unless the read is aborted.330
app.on('will-quit', () => { loginShellRead.abort() })331
const loginShell = readDesktopLoginShellEnvironment(process.env, resolveDesktopLoginShellConfig(process.env), {332
signal: loginShellRead.signal,333
}).then((result) => {334
for (const failure of result.failures) console.warn(`desktop login shell: ${failure.shell} failed (${failure.reason})`)335
return result.environment336
})337
let hostEnvironment: NodeJS.ProcessEnv = process.env338
const prepareHostEnvironment = async (): Promise<void> => { hostEnvironment = await loginShell }339
let quitting = false340
let startup: Promise<void> | undefined341
let workspaceRecovery: Promise<void> | undefined342
let mainWindow: BrowserWindow | undefined343
let welcomeWindow: BrowserWindow | undefined344
let enteredWorkspace = false345
// NSIS passes --updated when it launches the application after installation.346
let raiseAfterUpdate = process.platform === 'win32' && process.argv.includes('--updated')347
let shellInstallerOwnsQuit = false348
let requireCleanStop = false349
let updateStoppedHost = false350
let updateStopFailure: DesktopHostUncleanExitError | undefined351
let updateState: DesktopUpdateState = { phase: 'idle' }352
const systemLanguages = app.getPreferredSystemLanguages()353
let locale = resolveDesktopStartupLocale(null, systemLanguages)354
windowsLanguage = locale.id355
let mandatoryPolicy: DesktopMandatoryUpdatePolicy | undefined356
let mandatoryUI: DesktopMandatoryUpdateWindow | undefined357
let policyAuth: DesktopPolicyTestAuth | undefined358
let tray: DesktopTray | undefined359
/**360
* The operating system is ending the session: the quit skips its confirmation. Windows sets it361
* on the definitive session-end message. macOS sets it on the power-off notification, which362
* another application can still cancel, so the next focus or show of the main window clears it.363
*/364
let sessionEnding = false365
const isQuitting = (): boolean => quitting366
const currentMainWindow = (): BrowserWindow | undefined => mainWindow367
const ordinaryDialogs = new Set<AbortController>()368
const currentDialogWindow = (): BrowserWindow | undefined => welcomeWindow ?? mainWindow369
const updateOverlays = new DesktopUpdateOverlays()370
const updateDialog = new DesktopUpdateDialog(fileURLToPath(new URL('./preload-update-dialog.cjs', import.meta.url)), () => locale, updateOverlays)371
const isMandatory = (): boolean => mandatoryPolicy?.state.blocking === true372
const ordinaryMessageBox = async (options: UpdateDialogOptions): Promise<Electron.MessageBoxReturnValue> => {373
const controller = new AbortController()374
ordinaryDialogs.add(controller)375
try {376
const parent = currentDialogWindow()377
if (parent === undefined) return { response: options.cancelId ?? 0, checkboxChecked: false }378
return await updateDialog.show(parent, { ...options, signal: controller.signal })379
}380
finally { ordinaryDialogs.delete(controller) }381
}382
// Copy comes from the same locale as the update prompts so the dialog383
// chrome and its content never mix languages.384
const showAbout = async (): Promise<void> => {385
await ordinaryMessageBox({ type: 'info', title: locale.messages.aboutMenu, message: locale.messages.aboutProduct,386
detail: formatDesktopMessage(locale.messages.aboutVersion, { version: app.getVersion() }),387
buttons: [locale.messages.updateAcknowledge], cancelId: 0 })388
}389
const commandManager = new DesktopCommandManager({390
resources: process.resourcesPath,391
isPackaged: app.isPackaged,392
isInstalledLocation: () => process.platform !== 'darwin' || app.isInApplicationsFolder(),393
isInstalling: () => updateState.phase === 'installing',394
isQuitting,395
messages: () => currentDesktopLocale().messages,396
show: ordinaryMessageBox,397
})398
const appPreload = fileURLToPath(new URL('./preload-app.cjs', import.meta.url))399
const applicationUrl = `${SCHEME}://app/`400
let hostUrl: string | undefined401
let hostCookie: string | undefined402
const browserGuests = new DesktopBrowserGuests(() => hostUrl)403
let injections: readonly unknown[] = []404
let welcomeBackend: DesktopWelcomeBackend | undefined405
let reportedLaunch = false406
let analyticsEnabled = false407
const track = async <K extends keyof ProductEventMap>(eventName: K, attributes: ProductEventMap[K]): Promise<void> => {408
const event = { eventName, attributes, timestamp: Date.now() } as ProductEvent409
try {410
if (analyticsEnabled) await welcomeBackend?.report(event)411
} catch (_error) { /* Analytics cannot interrupt native actions. */ }412
}413
let stopAccount: (() => void) | undefined414
let openedAttempt: string | undefined415
let returnedAttempt: string | undefined416
let pendingWelcomeNotice: WelcomeNotice | undefined417
let previousAccountStatus: string | undefined418
const assertProductSender = (event: IpcMainInvokeEvent): void => {419
assertDesktopSender(event, ['app'])420
if (mainWindow === undefined || mainWindow.isDestroyed() || event.sender !== mainWindow.webContents421
|| event.senderFrame === null || event.senderFrame !== mainWindow.webContents.mainFrame) {422
throw new Error('dsh desktop: rejected IPC from an unowned renderer')423
}424
}425
let navigation: { window: BrowserWindow; url: string; promise: Promise<void> } | undefined426
const navigateMain = (url: string): Promise<void> => {427
const window = mainWindow428
if (quitting || window === undefined || window.isDestroyed()) return Promise.resolve()429
if (navigation?.window === window && navigation.url === url) return navigation.promise430
const next = { window, url, promise: Promise.resolve() }431
next.promise = window.loadURL(url).catch((error: unknown) => {432
if (quitting || shuttingDown || window.isDestroyed() || navigation !== next433
|| (error instanceof Error && 'code' in error && error.code === 'ERR_ABORTED')) return434
navigation = undefined435
throw error436
})437
navigation = next438
return next.promise439
}440
const platformView = new DesktopPlatformView(join(app.getAppPath(), 'lib', 'preload-platform-account.cjs'),441
() => locale.id === 'zh-CN' ? 'zh_CN' : 'en_US', process.platform === 'win32' ? 'win32' : 'darwin')442
const backend = new DesktopBackendController((onFailure) => {443
const hostInspectPort = developmentHostInspectPort(development)444
const host = new DesktopHostProcess(resources.node, resources.dsh, activeProject,445
hostInspectPort, { ...hostEnvironment, DSH_CLIENT_VERSION: desktopClientVersion() }, onFailure,446
primaryRuntime,447
resources, (next) => { platformView.setSession(next) })448
return {449
start: async () => {450
const ready = await host.start()451
hostCookie = await authenticateWebHost(ready.url)452
hostUrl = ready.url453
if (ready.injections === undefined) throw new Error('Desktop Host did not provide boot injections')454
injections = ready.injections455
welcomeBackend = await connectDesktopWelcome(ready.url, (input, init) => net.fetch(input, init), async () => (await session.defaultSession.cookies.get({ url: ready.url })).map(cookie => `${cookie.name}=${cookie.value}`).join('; '))456
analyticsEnabled = await welcomeBackend.analyticsEnabled().catch(() => false)457
if (!reportedLaunch) { reportedLaunch = true; void track('desktop_app_launch', {}) }458
stopAccount?.()459
const accountBackend = welcomeBackend.account460
stopAccount = accountBackend.watch((state) => {461
if (quitting) return462
if (welcomeWindow !== undefined && !welcomeWindow.isDestroyed()) welcomeWindow.webContents.send(WELCOME_IPC.state, state)463
const attempt = state.attempt464
if (attempt?.phase === 'waiting-browser' && attempt.authorizeUrl !== undefined && openedAttempt !== attempt.id) {465
openedAttempt = attempt.id466
void shell.openExternal(platformLoginUrl(attempt.authorizeUrl)).catch(() => undefined)467
}468
if ((attempt?.phase === 'failed' || attempt?.phase === 'expired') && returnedAttempt !== attempt.id) {469
returnedAttempt = attempt.id470
focusPrimaryWindow()471
}472
if (state.status === 'credential-stored' && attempt?.phase === 'succeeded' && welcomeWindow !== undefined) void enterWorkspace({ activate: false }).catch(() => undefined)473
if (previousAccountStatus === 'credential-stored' && state.status === 'signed-out') {474
void readWelcomeState().then(async (value) => {475
if (needsWelcome(value) && !quitting) {476
enteredWorkspace = false477
await showWelcome()478
if (welcomeWindow !== undefined && !welcomeWindow.isDestroyed()) welcomeWindow.webContents.send(WELCOME_IPC.state, state)479
}480
return undefined481
}).catch(() => undefined)482
}483
previousAccountStatus = state.status484
}, () => {485
// The stream reconnects; a transport failure does not change account state.486
}, () => {487
void readWelcomeState().then(async (value) => {488
if (!needsWelcome(value) || quitting) return489
pendingWelcomeNotice = 'session-expired'490
enteredWorkspace = false491
await showWelcome()492
const state = await accountBackend.state()493
if (welcomeWindow !== undefined && !welcomeWindow.isDestroyed()) welcomeWindow.webContents.send(WELCOME_IPC.state, state)494
}).catch(() => undefined)495
}, (enabled) => { analyticsEnabled = enabled })496
},497
stop: async () => {498
analyticsEnabled = false499
stopAccount?.()500
try { await host.stop(requireCleanStop) }501
catch (error) {502
if (!requireCleanStop || !(error instanceof DesktopHostUncleanExitError)) throw error503
// Backend cleanup succeeded; installation still rejects the unsuccessful task teardown.504
updateStopFailure = error505
}506
},507
updateTasks: (action: 'inspect' | 'lock' | 'unlock') => host.updateTasks(action),508
inspectQuit: () => host.inspectQuit(),509
}510
}, (state) => {511
if (state.phase === 'error') reportFatal(state.failure, 'host')512
else if (!shuttingDown) backendReady = state.phase === 'ready'513
})515
const updateErrors = new WeakMap<DesktopUpdateState, Promise<void>>()516
const showUpdateFailure = (state: DesktopUpdateState): Promise<void> => {517
if (state.phase !== 'error') return Promise.resolve()518
if (isMandatory()) { mandatoryUI?.sync(); return Promise.resolve() }519
let shown = updateErrors.get(state)520
if (shown === undefined) {521
shown = ordinaryMessageBox({ type: 'error', title: locale.messages.updateFailedTitle,522
message: desktopUpdateErrorSummary(state, locale.messages),523
technicalDetails: state.technicalDetails ?? state.message ?? '' }).then(() => {})524
updateErrors.set(state, shown)525
}526
return shown527
}528
const publishUpdate = (state: DesktopUpdateState): DesktopUpdateState => {529
updateJournal?.state(state)530
updateState = state531
mandatoryUI?.sync()532
for (const window of BrowserWindow.getAllWindows()) {533
window.webContents.send(DESKTOP_IPC.updatesPresentation, presentDesktopUpdate(state))534
}535
if (state.phase === 'error' && state.failedOperation !== 'check') {536
const restoreHost = state.failedOperation === 'install' && updateStoppedHost && !quitting537
shellInstallerOwnsQuit = false538
updateStoppedHost = false539
if (restoreHost) {540
// Only confirmed process exit permits replacement before another installation confirmation.541
const hostReady = backend.start(prepareHostEnvironment)542
startup = hostReady543
const recovery = hostReady.then(async () => {544
if (quitting) return545
// A replacement Host can have a new port, cookie, or boot injections even at the same URL.546
navigation = undefined547
await navigateMain(applicationUrl)548
if (backend.host !== undefined) updateJournal?.action('workspace-ready')549
})550
workspaceRecovery = recovery551
void recovery.catch((error: unknown) => { reportFatal(error, 'main') }).finally(() => {552
if (startup === hostReady) startup = undefined553
if (workspaceRecovery === recovery) workspaceRecovery = undefined554
})555
}556
void showUpdateFailure(state).catch((error: unknown) => { console.error(error) })557
}558
return state559
}561
const readWelcomeState = async () => {562
if (backend.host === undefined || welcomeBackend === undefined) throw new Error('desktop welcome: backend unavailable')563
return welcomeBackend.read()564
}565
stopForRecovery = () => backend.close()567
const reconcileBackend = (): Promise<void> => {568
startup ??= (async () => {569
await navigateMain(applicationUrl)570
await backend.start(async () => {571
await Promise.all([manager.applyRelease(), prepareHostEnvironment()])572
})573
if (backend.host !== undefined) await openInitialWindow()574
if (backend.host !== undefined) updateJournal?.action('workspace-ready')575
// The existing Web document resumes through the boot IPC response.576
})().catch((error: unknown) => {577
updateJournal?.action('workspace-failed')578
reportFatal(error, 'main')579
throw error580
}).finally(() => { startup = undefined })581
return startup582
}584
const updates = new DesktopUpdateCoordinator(585
publishUpdate,586
async () => {587
await commandManager.idle()588
await workspaceRecovery589
await startup?.catch(() => undefined)590
const host = backend.host591
if (host === undefined) throw new DesktopUpdatePreparationError('tasks-unavailable', locale.messages.updateTasksUnavailable)592
const active = await host.updateTasks('inspect')593
const ready = desktopUpdateReadyConfirmation(locale.messages, updates.state.version ?? '', process.platform)594
const confirmation: Electron.MessageBoxOptions = {595
type: active ? 'warning' : 'info', title: locale.messages.updateTitle,596
message: active ? locale.messages.updateActiveTasks : ready.message,597
detail: active ? locale.messages.updateActiveTasksDetail : ready.detail,598
buttons: active ? [locale.messages.updateStopTasks, locale.messages.updateLater] : [locale.messages.installAndRestart],599
defaultId: 1, cancelId: 1,600
}601
if (isMandatory()) {602
if (!await mandatoryUI?.confirm(updates.state.version ?? '', active)) return false603
} else {604
const parent = currentDialogWindow()605
if (parent === undefined) return false606
const result = await updateDialog.show(parent, confirmation)607
if (result.response !== 0 || isMandatory()) return false608
}609
// The update lock rejects new HTTP requests, including analytics intake.610
await track('desktop_upgrade_install_restart_click', {})611
if (backend.host !== host) throw new DesktopUpdatePreparationError('tasks-unavailable', locale.messages.updateTasksUnavailable)612
try {613
const stillActive = await host.updateTasks('lock')614
if (stillActive && !active) throw new DesktopUpdatePreparationError('tasks-changed', locale.messages.updateTasksChanged)615
mandatoryUI?.preparingRestart(stillActive)616
// The embedded Platform document holds credentials issued by the Host that is about to stop.617
await platformView.closeAndWait()618
requireCleanStop = true619
updateStopFailure = undefined620
await backend.stop()621
updateStoppedHost = true622
// The backend's async cleanup callback can assign this after the reset above.623
const stopFailure = updateStopFailure as DesktopHostUncleanExitError | undefined624
if (stopFailure !== undefined) throw new DesktopUpdatePreparationError('stop-failed', locale.messages.updateStopFailed, stopFailure.message)625
updateJournal?.action('install-confirmed')626
shellInstallerOwnsQuit = true627
} catch (error) {628
if (!updateStoppedHost) await host.updateTasks('unlock').catch((unlockError: unknown) => { console.error(unlockError) })629
throw error630
} finally {631
requireCleanStop = false632
}633
return true634
},635
undefined, undefined, undefined,636
(success, reason) => { void track('desktop_upgrade_download_result', { is_success: success, ...reason === undefined ? {} : { error_reason: reason } }) },638
)640
const updateSchedule = new DesktopUpdateSchedule(updates, resolveDesktopUpdateScheduleConfig(process.env))642
const downloadUpdate = async (version: string): Promise<DesktopUpdateState> => {643
void track('desktop_upgrade_click', {})644
updateJournal?.action('download-requested')645
const state = await updates.download(version)646
if (state.phase !== 'ready' || quitting) return state647
// Only a completed user-driven download opens this prompt; cancelling installation does not reopen it.648
// A confirmation on a hidden window would go unseen, so it waits for the next show; the mandatory649
// flow keeps its own taskbar and Dock attention instead.650
if (!isMandatory()) await windowShown()651
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- A quit can begin while the show is awaited.652
if (quitting) return state653
return updates.install(version)654
}655
const windowShown = (): Promise<void> => new Promise((resolve) => {656
const window = currentDialogWindow()657
if (window === undefined || window.isDestroyed() || window.isVisible()) { resolve(); return }658
window.once('show', () => { resolve() })659
window.once('closed', () => { resolve() })660
})662
protocol.handle(SCHEME, (request) => {663
const url = new URL(request.url)664
// Shell-owned documents live in the application bundle and never pass through the Host.665
if (url.hostname === 'shell') return serveWebDocument(request, join(app.getAppPath(), 'renderer'))666
if (url.hostname === 'app') {667
if (url.pathname === '/' || url.pathname === '/index.html' || url.pathname.startsWith('/assets/')668
|| ['/favicon.svg', '/manifest.webmanifest'].includes(url.pathname)) {669
return serveWebDocument(request, join(resources.dsh, 'node_modules', '@deepseek-ai', 'dsh-web-frontend', 'dist'))670
}671
if (backend.host === undefined || hostUrl === undefined || hostCookie === undefined) {672
return Promise.resolve(new Response(null, { status: 503 }))673
}674
return forwardWebRequest(request, hostUrl, hostCookie)675
}676
return Promise.resolve(new Response(null, { status: 404 }))677
})679
installDesktopDirectoryPicker(() => mainWindow)680
installMicrophonePermissions(session.defaultSession, () => mainWindow?.webContents)681
const shortcuts = installDesktopShortcuts(() => mainWindow, app.getPath('userData'),682
process.platform === 'darwin' ? 'macos' : process.platform === 'win32' ? 'windows' : 'linux', () => { refreshApplicationMenu() }, window => updateOverlays.input(window))683
app.on('will-quit', () => { shortcuts.dispose() })685
ipcMain.handle(DESKTOP_IPC.boot, async (event) => {686
assertDesktopSender(event, ['app'])687
await startup688
if (backend.host === undefined || hostUrl === undefined) throw new Error('Desktop Host is unavailable')689
return { injections, streamBaseUrl: new URL(hostUrl).origin }690
})692
ipcMain.handle(DESKTOP_IPC.bootFailed, (event, message: unknown) => {693
assertDesktopSender(event, ['app'])694
if (event.sender !== mainWindow?.webContents || event.senderFrame !== event.sender.mainFrame) {695
throw new Error('dsh desktop: rejected startup failure from a non-primary frame')696
}697
if (typeof message !== 'string') throw new Error('dsh desktop: startup failure must be text')698
reportFatal(new Error(message), 'web-boot')699
})701
ipcMain.handle(DESKTOP_IPC.browserAcquire, (event, workspace: unknown) => {702
assertProductSender(event)703
return browserGuests.acquire(event.sender, workspace)704
})705
ipcMain.handle(DESKTOP_IPC.browserRelease, (event, lease: unknown) => {706
assertProductSender(event)707
return browserGuests.release(event.sender, lease)708
})710
session.defaultSession.webRequest.onBeforeSendHeaders({ urls: ['ws://127.0.0.1/*'] }, (details, callback) => {711
if (hostUrl === undefined || hostCookie === undefined || details.webContentsId !== mainWindow?.webContents.id) {712
callback({})713
return714
}715
const target = new URL(hostUrl)716
const requested = new URL(details.url)717
if (requested.host !== target.host) { callback({}); return }718
const headers = Object.fromEntries(Object.entries(details.requestHeaders).map(([name, value]) => [name.toLowerCase(), value]))719
if (headers.origin !== 'dsh-app://app') { callback({ cancel: true }); return }720
callback({ requestHeaders: { ...headers, origin: target.origin, cookie: hostCookie, 'sec-fetch-site': 'same-origin' } })721
})723
const assertMainApplication = (event: IpcMainInvokeEvent): BrowserWindow => {724
const owner = mainWindow725
if (owner === undefined || event.sender !== owner.webContents || event.senderFrame !== owner.webContents.mainFrame726
|| !event.senderFrame.url.startsWith('dsh-app://app/')) throw new Error('Rejected Platform command')727
return owner728
}729
ipcMain.on(PLATFORM_IPC.bootstrap, (event) => {730
try { event.returnValue = platformView.bootstrap(event) }731
catch { event.returnValue = null }732
})733
ipcMain.handle(PLATFORM_IPC.open, (event, page: unknown, bounds: unknown) => {734
const owner = assertMainApplication(event)735
if (page !== 'usage' && page !== 'top-up') throw new Error('Invalid Platform page')736
return platformView.open(owner, page, platformBounds(bounds))737
})738
ipcMain.handle(PLATFORM_IPC.bounds, (event, bounds: unknown) => {739
assertMainApplication(event)740
platformView.setBounds(platformBounds(bounds))741
})742
ipcMain.handle(PLATFORM_IPC.close, (event) => { assertMainApplication(event); platformView.close() })743
// Only the main window may synchronize its palette with the native material.744
ipcMain.on(DESKTOP_IPC.nativeThemeSet, (event, source: unknown) => {745
if (mainWindow === undefined || event.sender !== mainWindow.webContents) return746
if (source === 'light' || source === 'dark' || source === 'system') nativeTheme.themeSource = source747
})748
ipcMain.handle(DESKTOP_IPC.localeBootstrap, async (event) => {749
if (event.sender !== mainWindow?.webContents || event.senderFrame !== mainWindow.webContents.mainFrame750
|| new URL(event.senderFrame.url).origin !== new URL(applicationUrl).origin) {751
throw new Error('desktop welcome: rejected locale request from an unowned frame')752
}753
if (welcomeBackend === undefined) throw new Error('desktop welcome: backend unavailable')754
return { languages: systemLanguages, preference: await welcomeBackend.readLocalePreference() }755
})756
ipcMain.on(DESKTOP_IPC.localeChanged, (event, next: unknown) => {757
const window = mainWindow758
if (window === undefined || event.sender !== window.webContents || event.senderFrame !== window.webContents.mainFrame759
|| typeof next !== 'string') return760
const current = resolveDesktopStartupLocale(next, systemLanguages)761
if (current.id === locale.id) return762
locale = current763
platformView.notifyLocaleChanged()764
windowsLanguage = locale.id765
refreshApplicationMenu()766
})767
ipcMain.handle(DESKTOP_IPC.updatesStatus, (event) => {768
assertProductSender(event)769
return presentDesktopUpdate(updates.state)770
})771
ipcMain.handle(DESKTOP_IPC.deviceInfo, (event) => {772
assertProductSender(event)773
return readDeviceInfo()774
})775
ipcMain.handle(DESKTOP_IPC.onboardingApiKey, async (event) => {776
assertProductSender(event)777
return (await readWelcomeState()).hasApiKey778
})779
ipcMain.on(DESKTOP_IPC.onboardingActive, (event, active: unknown) => {780
const window = mainWindow781
if (window === undefined || window.isDestroyed() || event.sender !== window.webContents782
|| event.senderFrame !== window.webContents.mainFrame783
|| !event.senderFrame.url.startsWith(`${SCHEME}://app/`) || typeof active !== 'boolean') return784
window.setMinimumSize(active ? 960 : 520, 600)785
if (active) {786
const { width, height } = window.getBounds()787
if (width < 960) window.setSize(960, height)788
}789
})790
ipcMain.handle(DESKTOP_IPC.updatesOpen, async (event) => {791
assertProductSender(event)792
await openUpdatePrompt()793
})795
let promptOperation: Promise<void> | undefined796
let policyAuthenticationQueued = false797
const openUpdatePrompt = (manual = false): Promise<void> => {798
if (authenticationOperation !== undefined) {799
policyAuth?.focus(); updateDialog.focus()800
}801
let failedOperation: 'check' | 'download' | 'install' = 'check'802
promptOperation ??= Promise.resolve().then(async () => {803
if (manual) updateJournal?.action('check-requested')804
const joinedPolicyAuthentication = authenticationOperation !== undefined805
if (joinedPolicyAuthentication) await authenticatePolicy()806
if (isMandatory()) {807
mandatoryUI?.focus()808
if (manual) await Promise.all([checkPolicyManually(), updateSchedule.check(true)])809
return810
}811
let controller: AbortController | undefined812
let progress: Promise<unknown> | undefined813
try {814
let state = updates.state815
if (manual || state.phase === 'idle' || (state.phase === 'error' && state.failedOperation === 'check')) {816
controller = new AbortController()817
ordinaryDialogs.add(controller)818
const parent = currentDialogWindow()819
progress = parent === undefined ? Promise.resolve() : updateDialog.show(parent, { type: 'info', title: locale.messages.updateCheckTitle,820
message: locale.messages.updateChecking, buttons: [locale.messages.later], cancelId: 0, signal: controller.signal })821
if (!joinedPolicyAuthentication) {822
void checkPolicyManually('deferred').catch((error: unknown) => { console.error(error) })823
}824
state = await updateSchedule.check(true)825
}826
if (isMandatory()) { mandatoryUI?.focus(); return }827
if (state.phase === 'error' && state.failedOperation === 'check') { await showUpdateFailure(state); return }828
if (state.phase === 'idle') {829
await ordinaryMessageBox({ type: 'info', title: locale.messages.updateCheckTitle,830
message: locale.messages.updateCurrent,831
detail: formatDesktopMessage(locale.messages.updateCurrentDetail, { version: app.getVersion() }) })832
return833
}834
if (state.phase === 'ready' || (state.phase === 'error' && state.failedOperation === 'install')) {835
if (state.version !== undefined) {836
failedOperation = 'install'837
await showUpdateFailure(await updates.install(state.version))838
}839
return840
}841
if (state.phase !== 'available' && !(state.phase === 'error' && state.failedOperation === 'download')) return842
if (manual) {843
const result = await ordinaryMessageBox({ title: locale.messages.updateCheckTitle,844
message: formatDesktopMessage(locale.messages.updateAvailable, { version: state.version ?? '' }),845
detail: locale.messages.updateDetail,846
buttons: [locale.messages.updateDownload], cancelId: 1 })847
if (result.response !== 0) return848
}849
if (!isMandatory() && state.version !== undefined) {850
controller?.abort()851
failedOperation = 'download'852
await showUpdateFailure(await downloadUpdate(state.version))853
}854
} finally {855
controller?.abort()856
if (controller !== undefined) ordinaryDialogs.delete(controller)857
await progress858
}859
}).catch((error: unknown) => showUpdateFailure({ phase: 'error', failedOperation,860
message: desktopErrorState(error).message }))861
.finally(() => { promptOperation = undefined; flushQueuedPolicyAuthentication() })862
return promptOperation863
}865
let authenticationOperation: Promise<DesktopPolicyState | undefined> | undefined866
const authenticatePolicy = () => {867
if (authenticationOperation !== undefined) { policyAuth?.focus(); updateDialog.focus() }868
authenticationOperation ??= runPolicyAuthentication().finally(() => { authenticationOperation = undefined })869
return authenticationOperation870
}871
const flushQueuedPolicyAuthentication = (): void => {872
if (!policyAuthenticationQueued || promptOperation !== undefined || authenticationOperation !== undefined873
|| isMandatory() || quitting) return874
policyAuthenticationQueued = false875
void authenticatePolicy().catch((error: unknown) => { console.error(error) })876
}877
const queuePolicyAuthentication = (): void => {878
if (authenticationOperation !== undefined) {879
policyAuth?.focus(); updateDialog.focus()880
return881
}882
policyAuthenticationQueued = true883
flushQueuedPolicyAuthentication()884
}885
const runPolicyAuthentication = async () => {886
if (policyAuth === undefined || mandatoryPolicy === undefined || quitting) return undefined887
const parent = mandatoryUI?.confirmationWindow ?? currentDialogWindow()888
if (parent === undefined) return undefined889
const consent = await updateDialog.show(parent, { type: 'info', title: locale.messages.policyLoginTitle,890
message: locale.messages.policyLoginRequired, buttons: [locale.messages.policyLogin, locale.messages.later], cancelId: 1 })891
if (consent.response !== 0 || isQuitting()) return undefined892
const outcome = await policyAuth.login()893
if (isQuitting() || outcome === 'cancelled') return undefined894
if (outcome === 'failed') {895
await updateDialog.show(parent, { type: 'error', title: locale.messages.policyLoginTitle,896
message: locale.messages.policyLoginFailed, buttons: [locale.messages.updateAcknowledge], cancelId: 0 })897
return undefined898
}899
// Drain a pre-login request before asking the server to evaluate the new cookies.900
await mandatoryPolicy.check('login-return')901
if (isQuitting()) return undefined902
return mandatoryPolicy.check('login-return', true)903
}905
const checkPolicyManually = async (authentication: 'immediate' | 'deferred' = 'immediate') => {906
if (authenticationOperation !== undefined) return authenticatePolicy()907
const policy = await mandatoryPolicy?.check('manual', true)908
if (policy?.error !== 'authentication-required') return policy909
if (authentication === 'immediate') return authenticatePolicy()910
queuePolicyAuthentication()911
return policy912
}914
const automaticCheck = (): void => {915
if (!quitting) void mandatoryPolicy?.check('foreground-or-resume').catch((error: unknown) => { console.error(error) })916
if (!quitting) void updateSchedule.check().catch((error: unknown) => { console.error(error) })917
}918
powerMonitor.on('resume', automaticCheck)919
app.on('will-quit', () => {920
updateSchedule.dispose()921
powerMonitor.off('resume', automaticCheck)922
updates.dispose()923
})925
const applicationIconPath = development ? join(app.getAppPath(), 'resources', 'icon-windows.png')926
: join(process.resourcesPath, 'icon.png')927
app.setAboutPanelOptions({928
applicationName: 'DeepSeek Harness',929
applicationVersion: app.getVersion(),930
// The release has no separate build number; omit Electron's bundle version.931
version: '',932
copyright: '',933
iconPath: applicationIconPath,934
})935
// A custom application menu replaces Electron's default menu, so macOS needs936
// its standard menus and application hide commands declared explicitly.937
// Keep app.name stable: Electron derives its default userData directory from it.938
const darwin = process.platform === 'darwin'939
const platformMenus = (): MenuItemConstructorOptions[] => darwin940
? [shortcuts.fileMenu(currentDesktopLocale().messages), { role: 'editMenu' }, { role: 'windowMenu' }]941
: [{ role: 'editMenu' }]942
const hideCommands: MenuItemConstructorOptions[] = darwin943
? [{ role: 'hide', label: currentDesktopLocale().messages.hideApplication },944
{ role: 'hideOthers', label: currentDesktopLocale().messages.hideOtherApplications },945
{ role: 'unhide', label: currentDesktopLocale().messages.showAllApplications }, { type: 'separator' }]946
: []947
const applicationItems = (): MenuItemConstructorOptions[] => [948
// Windows has no system About panel; Electron's fallback is a plain949
// message box, so the shell shows its own dimmed dialog instead.950
process.platform === 'win32'951
? { label: currentDesktopLocale().messages.aboutMenu,952
click: () => { void showAbout().catch((error: unknown) => { console.error(error) }) } }953
: { label: currentDesktopLocale().messages.aboutMenu, role: 'about' },954
{ type: 'separator' },955
{ label: currentDesktopLocale().messages.checkUpdatesMenu, click: () => { void openUpdatePrompt(true) } },956
...process.platform === 'darwin' || process.platform === 'win32'957
? [{ label: currentDesktopLocale().messages.cliCommandMenu, click: () => { void commandManager.show() } }] : [],958
...development ? [959
{ type: 'separator' as const },960
{ label: currentDesktopLocale().messages.reloadPageMenu, role: 'reload' as const },961
{ label: currentDesktopLocale().messages.restartAppHostMenu, click: () => {962
if (quitting) return963
app.relaunch()964
quitWithoutConfirmation()965
} },966
] : [],967
{ type: 'separator' },968
...hideCommands,969
{ role: 'quit', ...(darwin ? { label: currentDesktopLocale().messages.quitApplication }970
: process.platform === 'win32' ? { label: currentDesktopLocale().messages.exitApplication } : {}) },971
]972
const devToolsItems: MenuItemConstructorOptions[] = [973
{ role: 'toggleDevTools', visible: false },974
{ role: 'toggleDevTools', visible: false, accelerator: 'F12' },975
]976
const refreshApplicationMenu = (): void => {977
Menu.setApplicationMenu(Menu.buildFromTemplate(process.platform === 'win32' ? devToolsItems : [{978
label: darwin ? app.name : currentDesktopLocale().messages.application,979
submenu: [...applicationItems(), ...devToolsItems],980
}, ...platformMenus()]))981
tray?.relabel()982
}983
refreshApplicationMenu()984
const trayIconPath = development ? join(app.getAppPath(), 'resources', 'tray-windows.ico') : join(process.resourcesPath, 'tray.ico')985
if (process.platform === 'win32') {986
// The tray is the way back to a hidden window; without it, relaunching the application still focuses it.987
try {988
tray = new DesktopTray({ iconPath: trayIconPath, locale: currentDesktopLocale,989
open: () => { focusPrimaryWindow() }, quit: () => { app.quit() } })990
} catch (error) { console.warn('desktop tray: unavailable', error) }991
}992
const backgroundNotice = process.platform === 'win32'993
? new DesktopBackgroundNotice({ markerPath: join(app.getPath('userData'), 'background-close-confirmed'),994
locale: () => locale, show: ordinaryMessageBox, focus: () => { updateDialog.focus() } })995
: undefined996
const quitConfirmation = new DesktopQuitConfirmation({997
locale: () => locale,998
inspect: () => backend.host?.inspectQuit(),999
// No owner window: a hidden window stays hidden and the native box is its own top-level window.1000
show: options => dialog.showMessageBox(options),1001
// macOS raises the open alert with the application; Electron exposes no handle to the Windows task1002
// dialog, so a repeated request there only joins the open decision.1003
focus: () => { if (process.platform === 'darwin') app.focus({ steal: true }) },1004
// The task dialog draws its main icon at the system icon size; the multi-size ICO yields that1005
// size directly, where the 1024 px PNG would be scaled down by GDI.1006
...(process.platform === 'win32' ? { icon: nativeImage.createFromPath(trayIconPath) } : {}),1007
})1009
if (process.platform === 'win32') {1010
ipcMain.handle(DESKTOP_IPC.windowsMenu, (event, name: unknown, x: unknown, y: unknown) => {1011
assertDesktopSender(event, ['app'])1012
if (mainWindow === undefined || event.sender !== mainWindow.webContents1013
|| event.senderFrame !== mainWindow.webContents.mainFrame) throw new Error('desktop menu: rejected sender')1014
if ((name !== 'application' && name !== 'edit')1015
|| typeof x !== 'number' || typeof y !== 'number' || !Number.isFinite(x) || !Number.isFinite(y)1016
|| x < 0 || y < 0 || x > 100_000 || y > 100_000) throw new Error('desktop menu: invalid popup request')1017
const window = mainWindow1018
// Editor-owned history listens to key events rather than Chromium's native undo stack.1019
const editItem = (label: string, keyCode: string, modifiers: Array<'control'>, accelerator?: string): MenuItemConstructorOptions => ({1020
label,1021
...(accelerator === undefined ? {} : { accelerator }),1022
click: () => {1023
shortcuts.sendEditingKey(keyCode, modifiers)1024
},1025
})1026
const items: MenuItemConstructorOptions[] = name === 'application' ? applicationItems() : [1027
editItem(currentDesktopLocale().messages.undo, 'Z', ['control'], 'Ctrl+Z'),1028
editItem(currentDesktopLocale().messages.redo, 'Y', ['control'], 'Ctrl+Y'),1029
{ type: 'separator' },1030
editItem(currentDesktopLocale().messages.cut, 'X', ['control'], 'Ctrl+X'),1031
editItem(currentDesktopLocale().messages.copy, 'C', ['control'], 'Ctrl+C'),1032
editItem(currentDesktopLocale().messages.paste, 'V', ['control'], 'Ctrl+V'),1033
editItem(currentDesktopLocale().messages.delete, 'Delete', []),1034
{ type: 'separator' },1035
editItem(currentDesktopLocale().messages.selectAll, 'A', ['control'], 'Ctrl+A'),1036
]1037
const zoom = mainWindow.webContents.getZoomFactor()1038
return new Promise<void>((resolve) => {1039
Menu.buildFromTemplate(items).popup({ window, x: Math.round(x * zoom), y: Math.round(y * zoom), callback: resolve })1040
})1041
})1042
ipcMain.on(DESKTOP_IPC.windowsAppearance, (event, language: unknown, color: unknown, symbolColor: unknown) => {1043
if (mainWindow === undefined || event.sender !== mainWindow.webContents1044
|| event.senderFrame !== mainWindow.webContents.mainFrame) return1045
if (!event.senderFrame.url.startsWith(`${SCHEME}://app/`)) return1046
if (typeof language === 'string' && /^[a-zA-Z]+(?:-[a-zA-Z0-9]+)*$/u.test(language)) {1047
windowsLanguage = language1048
}1049
// Empty colors precede client stylesheet installation; only CSS color values cross IPC.1050
const validColor = (value: unknown): value is string => typeof value === 'string'1051
&& /^(?:#[\da-f]{3,8}|rgba?\([\d.,%\s]+\))$/iu.test(value)1052
if (validColor(color) && validColor(symbolColor)) mainWindow.setTitleBarOverlay({ color, symbolColor })1053
})1054
}1056
const hideMainWindow = (window: BrowserWindow): void => {1057
if (process.platform === 'darwin' && window.isFullScreen()) {1058
// Hiding a fullscreen window leaves an empty black space; leave fullscreen first.1059
window.once('leave-full-screen', () => { if (!window.isDestroyed()) window.hide() })1060
window.setFullScreen(false)1061
} else {1062
window.hide()1063
}1064
}1065
const createMainWindow = (): BrowserWindow => {1066
const window = createWindow(appPreload, false, true)1067
mainWindow = window1068
browserGuests.bind(window, (guest, name) => shortcuts.attachGuest(window, guest, name))1069
shortcuts.attach(window)1070
window.on('focus', automaticCheck)1071
// Closing hides: the page and the Host keep running, and the next show resumes the same document.1072
window.on('close', (event) => {1073
if (quitting || shellInstallerOwnsQuit || sessionEnding) return1074
event.preventDefault()1075
if (updateDialog.isOpen) { updateDialog.focus(); return }1076
const hide = (): void => {1077
if (!quitting && !shellInstallerOwnsQuit && !sessionEnding && !window.isDestroyed()) hideMainWindow(window)1078
}1079
if (backgroundNotice === undefined) hide()1080
else backgroundNotice.close(hide)1081
})1082
if (process.platform === 'win32') {1083
// Shutdown, restart, and log-off must not wait on a confirmation. query-session-end is only a1084
// question that another application can veto without any follow-up message, so it does not count.1085
window.on('session-end', () => { sessionEnding = true })1086
} else {1087
// The macOS power-off notification arrives before the terminate request; a cancelled shutdown1088
// leaves the process running, and user attention on the window shows the session continues.1089
window.on('focus', () => { sessionEnding = false })1090
window.on('show', () => { sessionEnding = false })1091
}1092
window.on('closed', () => { if (mainWindow === window) mainWindow = undefined })1093
window.webContents.on('console-message', (details) => {1094
if (details.level !== 'error') return1095
rendererConsole.push(`${details.sourceId}:${String(details.lineNumber)} ${details.message}`)1096
})1097
window.webContents.on('did-fail-load', (_event, code, description, url, isMainFrame) => {1098
if (isMainFrame && code !== -3 && !quitting && !window.isDestroyed()) {1099
reportFatal(new Error(`Desktop page failed to load: ${url} (${String(code)}: ${description})`), 'renderer')1100
}1101
})1102
window.webContents.on('preload-error', (_event, _path, error) => {1103
if (!quitting && !window.isDestroyed()) reportFatal(error, 'renderer')1104
})1105
window.webContents.on('render-process-gone', (_event, details) => {1106
navigation = undefined1107
if (!quitting && !window.isDestroyed() && details.reason !== 'clean-exit') {1108
reportFatal(new Error(`Desktop renderer exited: ${details.reason}`), 'renderer')1109
}1110
})1111
return window1112
}1113
const enterWorkspace = async ({ activate = true }: { activate?: boolean } = {}): Promise<void> => {1114
if (quitting) return1115
const window = mainWindow ?? createMainWindow()1116
await navigateMain(applicationUrl)1117
if (isQuitting() || recovery.active || window.isDestroyed()) return1118
if (activate) window.show()1119
else window.showInactive()1120
enteredWorkspace = true1121
if (welcomeWindow !== undefined) {1122
welcomeWindow.close()1123
window.webContents.send(DESKTOP_IPC.enterWorkspace)1124
}1125
welcomeWindow = undefined1126
if (raiseAfterUpdate) {1127
raiseAfterUpdate = false1128
window.moveTop()1129
window.focus()1130
}1131
if (activate && development && process.env.DSH_DESKTOP_OPEN_DEVTOOLS !== '0') {1132
window.webContents.openDevTools({ mode: 'detach' })1133
}1134
}1135
let openingWelcome: Promise<void> | undefined1136
const showWelcome = (): Promise<void> => {1137
if (quitting) return Promise.resolve()1138
if (welcomeWindow !== undefined && !welcomeWindow.isDestroyed()) {1139
if (!welcomeWindow.isVisible()) void track('auth_page_view', {})1140
welcomeWindow.show()1141
welcomeWindow.focus()1142
return Promise.resolve()1143
}1144
openingWelcome ??= (async () => {1145
welcomeWindow = await openWelcomeWindow(locale, {1146
analytics: track,1147
analyticsEnabled: () => Promise.resolve(analyticsEnabled),1148
takeNotice: () => {1149
const notice = pendingWelcomeNotice1150
pendingWelcomeNotice = undefined1151
return Promise.resolve(notice)1152
},1153
startSignIn: async () => {1154
if (welcomeBackend === undefined) throw new Error('desktop welcome: backend unavailable')1155
return welcomeBackend.account.start(desktopClientMetadata(locale.id))1156
},1157
cancelSignIn: async (id) => {1158
if (welcomeBackend === undefined) throw new Error('desktop welcome: backend unavailable')1159
return welcomeBackend.account.cancel(id)1160
},1161
copySignInLink: async (id) => {1162
const state = await welcomeBackend?.account.state()1163
if (state?.attempt?.id !== id || state.attempt.phase !== 'waiting-browser' || state.attempt.authorizeUrl === undefined) {1164
throw new Error('desktop welcome: login link is unavailable')1165
}1166
await clipboard.writeText(platformLoginUrl(state.attempt.authorizeUrl))1167
},1168
saveApiKey: async (apiKey) => {1169
if (backend.host === undefined || welcomeBackend === undefined) return { ok: false }1170
const saved = await welcomeBackend.save(apiKey)1171
if (!saved.ok) return saved1172
await enterWorkspace()1173
return { ok: true }1174
},1175
skip: enterWorkspace,1176
})1177
const window = welcomeWindow1178
window.once('closed', () => {1179
void welcomeBackend?.account.state().then((state) => {1180
if (state.attempt !== null && !enteredWorkspace) return welcomeBackend?.account.cancel(state.attempt.id)1181
return undefined1182
}).catch(() => undefined)1183
})1184
window.once('closed', () => {1185
if (welcomeWindow === window) welcomeWindow = undefined1186
if (enteredWorkspace || recovery.active) return1187
// Nothing runs before the workspace opens. macOS keeps the Dock convention and drops the1188
// unused main window so the next activation rebuilds the welcome; elsewhere the close quits.1189
if (process.platform === 'darwin') mainWindow?.destroy()1190
else app.quit()1191
})1192
if (isQuitting() || recovery.active || enteredWorkspace) window.close()1193
else mainWindow?.hide()1194
})().finally(() => { openingWelcome = undefined })1195
return openingWelcome1196
}1197
const openInitialWindow = async (): Promise<void> => {1198
if (quitting || recovery.active) return1199
const state = await readWelcomeState()1200
if (isQuitting() || backend.state.phase !== 'ready') return1201
locale = resolveDesktopStartupLocale(state.localePreference, systemLanguages)1202
windowsLanguage = locale.id1203
refreshApplicationMenu()1204
if (!enteredWorkspace && needsWelcome({ loggedIn: state.loggedIn, hasApiKey: state.hasApiKey })) {1205
// A later login must retain its own activation policy instead of replaying startup focus.1206
raiseAfterUpdate = false1207
await showWelcome()1208
} else {1209
await enterWorkspace()1210
}1211
}1212
focusPrimaryWindow = () => {1213
if (quitting) return1214
if (isMandatory()) { mandatoryUI?.focus(); return }1215
const window = welcomeWindow ?? mainWindow1216
if (window === undefined || window.isDestroyed()) {1217
try { createMainWindow() } catch (error) { reportFatal(error, 'main'); return }1218
void (backend.state.phase === 'ready' ? openInitialWindow() : navigateMain(applicationUrl)).catch((error: unknown) => { reportFatal(error, 'main') })1219
return1220
}1221
// Startup and sign-out select the visible window before activation may reveal the workspace.1222
if (window === mainWindow && !enteredWorkspace) return1223
if (window.isMinimized()) window.restore()1224
window.show()1225
window.focus()1226
}1228
if (app.isPackaged || process.env.DSH_DESKTOP_DEV_APP === '1') app.setAsDefaultProtocolClient('dsh')1229
app.on('open-url', (event, url) => {1230
event.preventDefault()1231
if (url === 'dsh://open' || url === 'dsh://open/') focusPrimaryWindow()1232
})1234
app.on('activate', (_event, hasVisibleWindows) => {1235
if (!hasVisibleWindows) focusPrimaryWindow()1236
})1237
app.on('window-all-closed', () => {1238
if (process.platform !== 'darwin') app.quit()1239
})1240
if (process.platform !== 'win32') powerMonitor.on('shutdown', () => { sessionEnding = true })1241
const finishQuit = (): void => {1242
quitting = true1243
shuttingDown = true1244
updateJournal?.action('quit-requested')1245
quitConfirmation.dispose()1246
backgroundNotice?.dispose()1247
tray?.dispose()1248
stopAccount?.()1249
if (welcomeWindow !== undefined && !welcomeWindow.isDestroyed()) welcomeWindow.hide()1250
if (mainWindow !== undefined && !mainWindow.isDestroyed()) mainWindow.hide()1251
updateSchedule.dispose()1252
updateDialog.dispose()1253
mandatoryUI?.dispose()1254
void Promise.all([Promise.resolve(mandatoryPolicy?.dispose()).then(() => policyAuth?.dispose()), backend.close(),1255
// A Platform cleanup failure is logged without cutting the remaining Host shutdown short.1256
platformView.dispose().catch((error: unknown) => { console.error(error) })])1257
.catch((error: unknown) => { console.error(error) }).finally(() => { app.quit() })1258
}1259
app.on('before-quit', (event) => {1260
if (shellInstallerOwnsQuit) {1261
shuttingDown = true1262
quitConfirmation.dispose()1263
backgroundNotice?.dispose()1264
updateJournal?.action('quit-requested')1265
tray?.dispose()1266
updateDialog.dispose()1267
mandatoryUI?.dispose()1268
// Installation preparation already awaited Platform storage cleanup.1269
void platformView.dispose().catch((error: unknown) => { console.error(error) })1270
return1271
}1272
if (quitting) return1273
event.preventDefault()1274
if (skipQuitConfirmation || sessionEnding) { finishQuit(); return }1275
void quitConfirmation.confirm().then((approved) => {1276
if (quitting || shellInstallerOwnsQuit) return1277
if (approved) { finishQuit(); return }1278
// A quit that started from closing the welcome window destroyed it; a cancelled quit needs it back.1279
if (!enteredWorkspace && !recovery.active) void showWelcome().catch((error: unknown) => { reportFatal(error, 'main') })1280
}).catch((error: unknown) => { console.error(error); if (!quitting && !shellInstallerOwnsQuit) finishQuit() })1281
})1283
mainWindow = createMainWindow()1284
const manifest: unknown = JSON.parse(await readFile(join(app.getAppPath(), 'package.json'), 'utf8'))1285
if (typeof manifest !== 'object' || manifest === null) throw new Error('desktop policy: invalid application manifest')1286
const developmentPolicy = app.isPackaged ? undefined : process.env.DSH_DESKTOP_MANDATORY_UPDATE_CONFIG1287
const policyInput: unknown = app.isPackaged1288
? ('dshMandatoryUpdatePolicy' in manifest ? manifest.dshMandatoryUpdatePolicy : undefined)1289
: developmentPolicy === undefined ? undefined : JSON.parse(developmentPolicy) as unknown1290
const policyConfig = resolveDesktopPolicyConfig(policyInput, !app.isPackaged)1291
if (policyConfig !== undefined) {1292
if (policyConfig.authentication === 'feishu-test') {1293
policyAuth = new DesktopPolicyTestAuth(policyConfig.origin, policyConfig.allowedAuthOrigins, locale,1294
() => mandatoryUI?.confirmationWindow ?? currentDialogWindow(),1295
(event) => { console.info(`desktop policy authentication: ${event}`); updateJournal?.action(`policy-login-${event}`) })1296
}1297
if (!['win32', 'darwin'].includes(process.platform) || !['x64', 'arm64'].includes(process.arch)) throw new Error('desktop policy: unsupported platform')1298
let wasBlocking = false1299
mandatoryPolicy = new DesktopMandatoryUpdatePolicy(policyConfig, {1300
platform: process.platform as 'win32' | 'darwin', arch: process.arch as 'x64' | 'arm64',1301
bundledDshVersion: app.isPackaged ? readDesktopRuntime(resources.dsh).release.version : app.getVersion(),1302
}, (state) => {1303
if (state.error !== 'authentication-required') policyAuthenticationQueued = false1304
if (state.blocking) {1305
for (const controller of ordinaryDialogs) controller.abort()1306
if (!wasBlocking) updateDialog.cancel()1307
}1308
mandatoryUI?.sync()1309
if (state.blocking && !wasBlocking) void updateSchedule.check(false, true).catch((error: unknown) => { console.error(error) })1310
wasBlocking = state.blocking1311
}, policyAuth?.request, () => desktopClientMetadata(locale.id))1312
const policy = mandatoryPolicy1313
mandatoryUI = new DesktopMandatoryUpdateWindow({1314
overlays: updateOverlays,1315
preload: fileURLToPath(new URL('./preload-mandatory.cjs', import.meta.url)), locale,1316
allowedPageOrigins: policyConfig.allowedPageOrigins, parent: () => mainWindow,1317
policy: () => policy.state, update: () => updates.state,1318
refresh: async () => { await Promise.all([checkPolicyManually(), updateSchedule.check(true)]) },1319
download: downloadUpdate, install: version => updates.install(version),1320
})1321
void mandatoryPolicy.check('launch').then((state) => {1322
if (app.isPackaged && state.error === 'authentication-required' && !isQuitting()) queuePolicyAuthentication()1323
}).catch((error: unknown) => { console.error(error) })1324
}1325
automaticCheck()1326
await reconcileBackend().catch(() => undefined)1327
// Window lifecycle callbacks run while backend startup is pending.1328
if (isQuitting()) return1329
const window = currentMainWindow()1330
if (window !== undefined && development && process.env.DSH_DESKTOP_OPEN_DEVTOOLS !== '0') {1331
window.webContents.openDevTools({ mode: 'detach' })1332
}1333
publishUpdate(updateState)1334
}1336
const ownsDesktopInstance = claimDesktopSingleInstance(app, () => { focusPrimaryWindow() })1338
if (ownsDesktopInstance) void app.whenReady().then(main).catch(async (error: unknown) => {1339
const message = error instanceof Error ? error.message : String(error)1340
console.error(error)1341
const diagnosticFile = process.env.DSH_DESKTOP_DIAGNOSTIC_FILE1342
if (diagnosticFile !== undefined) {1343
await writeFile(diagnosticFile, `${error instanceof Error ? error.stack ?? message : message}\n`).catch(() => undefined)1344
}1345
reportFatal(error, 'main')1346
}).catch((error: unknown) => {1347
console.error(error)1348
app.exit(1)1349
})